From 156826ceefe19e9508fdc320394c55bfbc521139 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Wed, 26 Jun 2024 23:44:51 -0400 Subject: [PATCH] initial commit Code lifted from sectr prototype Still quite a bit todo before its considered "done" --- LICENSE.md | 21 + LRU.odin | 239 ++++++++++++ Readme.md | 29 ++ VEFontCache.odin | 504 ++++++++++++++++++++++++ atlas.odin | 153 ++++++++ docs/Readme.md | 75 ++++ docs/draw_text_codepaths.pur | Bin 0 -> 48520 bytes docs/original/LICENSE.md | 17 + docs/original/README.md | 114 ++++++ draw.odin | 718 +++++++++++++++++++++++++++++++++++ mappings.odin | 81 ++++ misc.odin | 165 ++++++++ parser.odin | 495 ++++++++++++++++++++++++ shaped_text.odin | 132 +++++++ shaper.odin | 165 ++++++++ 15 files changed, 2908 insertions(+) create mode 100644 LICENSE.md create mode 100644 LRU.odin create mode 100644 Readme.md create mode 100644 VEFontCache.odin create mode 100644 atlas.odin create mode 100644 docs/Readme.md create mode 100644 docs/draw_text_codepaths.pur create mode 100644 docs/original/LICENSE.md create mode 100644 docs/original/README.md create mode 100644 draw.odin create mode 100644 mappings.odin create mode 100644 misc.odin create mode 100644 parser.odin create mode 100644 shaped_text.odin create mode 100644 shaper.odin diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..a602421 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +VEFontCache Odin Port +Copyright 2024 Edward R. Gonzalez + +This project is based on Vertex Engine GPU Font Cache +by Xi Chen (https://github.com/hypernewbie/VEFontCache). It has been substantially +rewritten and redesigned for the Odin programming language. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES +OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/LRU.odin b/LRU.odin new file mode 100644 index 0000000..db5d440 --- /dev/null +++ b/LRU.odin @@ -0,0 +1,239 @@ +package VEFontCache + +/* +The choice was made to keep the LRU cache implementation as close to the original as possible. +*/ + +import "base:runtime" + +PoolListIter :: i32 +PoolListValue :: u64 + +PoolListItem :: struct { + prev : PoolListIter, + next : PoolListIter, + value : PoolListValue, +} + +PoolList :: struct { + items : [dynamic]PoolListItem, + free_list : [dynamic]PoolListIter, + front : PoolListIter, + back : PoolListIter, + size : u32, + capacity : u32, + dbg_name : string, +} + +pool_list_init :: proc( pool : ^PoolList, capacity : u32, dbg_name : string = "" ) +{ + error : AllocatorError + pool.items, error = make( [dynamic]PoolListItem, u64(capacity) ) + assert( error == .None, "VEFontCache.pool_list_init : Failed to allocate items array") + resize( & pool.items, capacity ) + + pool.free_list, error = make( [dynamic]PoolListIter, u64(capacity) ) + assert( error == .None, "VEFontCache.pool_list_init : Failed to allocate free_list array") + resize( & pool.free_list, capacity ) + + pool.capacity = capacity + + pool.dbg_name = dbg_name + using pool + + for id in 0 ..< capacity { + free_list[id] = i32(id) + items[id] = { + prev = -1, + next = -1, + } + } + + front = -1 + back = -1 +} + +pool_list_free :: proc( pool : ^PoolList ) +{ + // TODO(Ed): Implement +} + +pool_list_reload :: proc( pool : ^PoolList, allocator : Allocator ) +{ + reload_array( & pool.items, allocator ) + reload_array( & pool.free_list, allocator ) +} + +pool_list_push_front :: proc( pool : ^PoolList, value : PoolListValue ) +{ + using pool + if size >= capacity do return + + length := len(free_list) + assert( length > 0 ) + assert( length == int(capacity - size) ) + + id := free_list[ len(free_list) - 1 ] + if pool.dbg_name != "" { + logf("pool_list: back %v", id) + } + pop( & free_list ) + items[ id ].prev = -1 + items[ id ].next = front + items[ id ].value = value + if pool.dbg_name != "" { + logf("pool_list: pushed %v into id %v", value, id) + } + + if front != -1 do items[ front ].prev = id + if back == -1 do back = id + front = id + size += 1 +} + +pool_list_erase :: proc( pool : ^PoolList, iter : PoolListIter ) +{ + using pool + if size <= 0 do return + assert( iter >= 0 && iter < i32(capacity) ) + assert( len(free_list) == int(capacity - size) ) + + iter_node := & items[ iter ] + prev := iter_node.prev + next := iter_node.next + + if iter_node.prev != -1 do items[ prev ].next = iter_node.next + if iter_node.next != -1 do items[ next ].prev = iter_node.prev + + if front == iter do front = iter_node.next + if back == iter do back = iter_node.prev + + iter_node.prev = -1 + iter_node.next = -1 + iter_node.value = 0 + append( & free_list, iter ) + + size -= 1 + if size == 0 { + back = -1 + front = -1 + } +} + +pool_list_peek_back :: #force_inline proc ( pool : ^PoolList ) -> PoolListValue { + assert( pool.back != - 1 ) + value := pool.items[ pool.back ].value + return value +} + +pool_list_pop_back :: #force_inline proc( pool : ^PoolList ) -> PoolListValue { + if pool.size <= 0 do return 0 + assert( pool.back != -1 ) + + value := pool.items[ pool.back ].value + pool_list_erase( pool, pool.back ) + return value +} + +LRU_Link :: struct { + pad_top : u64, + value : i32, + ptr : PoolListIter, + pad_bottom : u64, +} + +LRU_Cache :: struct { + capacity : u32, + num : u32, + table : map[u64]LRU_Link, + key_queue : PoolList, +} + +LRU_init :: proc( cache : ^LRU_Cache, capacity : u32, dbg_name : string = "" ) { + error : AllocatorError + cache.capacity = capacity + cache.table, error = make( map[u64]LRU_Link, uint(capacity) ) + assert( error == .None, "VEFontCache.LRU_init : Failed to allocate cache's table") + + pool_list_init( & cache.key_queue, capacity, dbg_name = dbg_name ) +} + +LRU_free :: proc( cache : ^LRU_Cache ) +{ + // TODO(Ed): Implement +} + +LRU_reload :: #force_inline proc( cache : ^LRU_Cache, allocator : Allocator ) +{ + reload_map( & cache.table, allocator ) + pool_list_reload( & cache.key_queue, allocator ) +} + +LRU_hash_key :: #force_inline proc( key : u64 ) -> ( hash : u64 ) { + bytes := transmute( [8]byte ) key + hash = fnv64a( bytes[:] ) + return +} + +LRU_find :: #force_inline proc "contextless" ( cache : ^LRU_Cache, key : u64, must_find := false ) -> (LRU_Link, bool) { + link, success := cache.table[key] + return link, success +} + +LRU_get :: #force_inline proc( cache : ^LRU_Cache, key : u64 ) -> i32 { + iter, success := LRU_find( cache, key ) + if success == false { + return -1 + } + LRU_refresh( cache, key ) + return iter.value +} + +LRU_get_next_evicted :: #force_inline proc ( cache : ^LRU_Cache ) -> u64 +{ + if cache.key_queue.size >= cache.capacity { + evict := pool_list_peek_back( & cache.key_queue ) + return evict + } + return 0xFFFFFFFFFFFFFFFF +} + +LRU_peek :: #force_inline proc ( cache : ^LRU_Cache, key : u64, must_find := false ) -> i32 { + iter, success := LRU_find( cache, key, must_find ) + if success == false { + return -1 + } + return iter.value +} + +LRU_put :: #force_inline proc ( cache : ^LRU_Cache, key : u64, value : i32 ) -> u64 +{ + iter, success := cache.table[key] + if success { + LRU_refresh( cache, key ) + iter.value = value + return key + } + + evict := key + if cache.key_queue.size >= cache.capacity { + evict = pool_list_pop_back( & cache.key_queue ) + delete_key( & cache.table, evict ) + cache.num -= 1 + } + + pool_list_push_front( & cache.key_queue, key ) + cache.table[key] = LRU_Link { + value = value, + ptr = cache.key_queue.front + } + cache.num += 1 + return evict +} + +LRU_refresh :: proc( cache : ^LRU_Cache, key : u64 ) { + link, success := LRU_find( cache, key ) + pool_list_erase( & cache.key_queue, link.ptr ) + pool_list_push_front( & cache.key_queue, key ) + link.ptr = cache.key_queue.front +} diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..05f7611 --- /dev/null +++ b/Readme.md @@ -0,0 +1,29 @@ +# VE Font Cache : Odin Port + +This is a port of the library base on [fork](https://github.com/hypernewbie/VEFontCache) + +Its original purpose was for use in game engines, however its rendeirng quality and performance is more than adequate for many other applications. + +See: [docs/Readme.md](docs/Readme.md) for the library's interface + +TODO (Making it a more idiomatic library): + +* Setup freetype, harfbuzz, depedency management within the library + +TODO Documentation: + +* Pureref outline of draw_text exectuion +* Markdown general documentation + +TODO Content: + +* Port over the original demo utilizing sokol libraries instead +* Provide a sokol_gfx backend package + +TODO Additional Features: + +* Support for freetype +* Support for harfbuzz +* Ability to set a draw transform, viewport and projection + * By default the library's position is in unsigned normalized render space +* Allow curve_quality to be set on a per-font basis diff --git a/VEFontCache.odin b/VEFontCache.odin new file mode 100644 index 0000000..2a8c710 --- /dev/null +++ b/VEFontCache.odin @@ -0,0 +1,504 @@ +/* +A port of (https://github.com/hypernewbie/VEFontCache) to Odin. + +Status: +This port is heavily tied to the grime package in SectrPrototype. + +Changes: +- Font Parser & Glyph Shaper are abstracted to their own interface +- Font Face parser info stored separately from entries +- ve_fontcache_loadfile not ported (just use odin's core:os or os2), then call load_font +- Macro defines have been made into runtime parameters +*/ +package VEFontCache + +import "base:runtime" + +Advance_Snap_Smallfont_Size :: 0 + +FontID :: distinct i64 +Glyph :: distinct i32 + +Entry :: struct { + parser_info : ParserFontInfo, + shaper_info : ShaperInfo, + id : FontID, + used : b32, + size : f32, + size_scale : f32, +} + +Entry_Default :: Entry { + id = 0, + used = false, + size = 24.0, + size_scale = 1.0, +} + +Context :: struct { + backing : Allocator, + + parser_kind : ParserKind, + parser_ctx : ParserContext, + shaper_ctx : ShaperContext, + + entries : [dynamic]Entry, + + temp_path : [dynamic]Vec2, + temp_codepoint_seen : map[u64]bool, + temp_codepoint_seen_num : u32, + + snap_width : u32, + snap_height : u32, + + colour : Colour, + cursor_pos : Vec2, + + // draw_cursor_pos : Vec2, + + draw_layer : struct { + vertices_offset : int, + indices_offset : int, + calls_offset : int, + }, + + draw_list : DrawList, + atlas : Atlas, + glyph_buffer : GlyphDrawBuffer, + shape_cache : ShapedTextCache, + + curve_quality : u32, + text_shape_adv : b32, + + debug_print : b32, + debug_print_verbose : b32, +} + +#region("lifetime") + +InitAtlasRegionParams :: struct { + width : u32, + height : u32, +} + +InitAtlasParams :: struct { + width : u32, + height : u32, + glyph_padding : u32, + + region_a : InitAtlasRegionParams, + region_b : InitAtlasRegionParams, + region_c : InitAtlasRegionParams, + region_d : InitAtlasRegionParams, +} + +InitAtlasParams_Default :: InitAtlasParams { + width = 4096, + height = 2048, + glyph_padding = 4, + + region_a = { + width = 32, + height = 32, + }, + region_b = { + width = 32, + height = 64, + }, + region_c = { + width = 64, + height = 64, + }, + region_d = { + width = 128, + height = 128, + } +} + +InitGlyphDrawParams :: struct { + over_sample : Vec2, + buffer_batch : u32, + draw_padding : u32, +} + +InitGlyphDrawParams_Default :: InitGlyphDrawParams { + over_sample = { 8, 8 }, + buffer_batch = 4, + draw_padding = InitAtlasParams_Default.glyph_padding, +} + +InitShapeCacheParams :: struct { + capacity : u32, + reserve_length : u32, +} + +InitShapeCacheParams_Default :: InitShapeCacheParams { + capacity = 1024, + reserve_length = 1024, +} + +// ve_fontcache_init +startup :: proc( ctx : ^Context, parser_kind : ParserKind, + allocator := context.allocator, + atlas_params := InitAtlasParams_Default, + glyph_draw_params := InitGlyphDrawParams_Default, + shape_cache_params := InitShapeCacheParams_Default, + curve_quality : u32 = 3, + entires_reserve : u32 = 512, + temp_path_reserve : u32 = 512, + temp_codepoint_seen_reserve : u32 = 512, +) +{ + assert( ctx != nil, "Must provide a valid context" ) + using ctx + + ctx.backing = allocator + context.allocator = ctx.backing + + if curve_quality == 0 { + curve_quality = 3 + } + ctx.curve_quality = curve_quality + + error : AllocatorError + entries, error = make( [dynamic]Entry, entires_reserve ) + assert(error == .None, "VEFontCache.init : Failed to allocate entries") + + temp_path, error = make( [dynamic]Vec2, temp_path_reserve ) + assert(error == .None, "VEFontCache.init : Failed to allocate temp_path") + + temp_codepoint_seen, error = make( map[u64]bool, uint(temp_codepoint_seen_reserve) ) + assert(error == .None, "VEFontCache.init : Failed to allocate temp_path") + + draw_list.vertices, error = make( [dynamic]Vertex, 4 * Kilobyte ) + assert(error == .None, "VEFontCache.init : Failed to allocate draw_list.vertices") + + draw_list.indices, error = make( [dynamic]u32, 8 * Kilobyte ) + assert(error == .None, "VEFontCache.init : Failed to allocate draw_list.indices") + + draw_list.calls, error = make( [dynamic]DrawCall, 512 ) + assert(error == .None, "VEFontCache.init : Failed to allocate draw_list.calls") + + init_atlas_region :: proc( region : ^AtlasRegion, params : InitAtlasParams, region_params : InitAtlasRegionParams, factor : Vec2i, expected_cap : i32 ) { + using region + + next_idx = 0; + width = region_params.width + height = region_params.height + size = { + i32(params.width) / factor.x, + i32(params.height) / factor.y, + } + capacity = { + size.x / i32(width), + size.y / i32(height), + } + assert( capacity.x * capacity.y == expected_cap ) + + error : AllocatorError + // state.cache, error = make( HMapChained(LRU_Link), uint(capacity.x * capacity.y) ) + // assert( error == .None, "VEFontCache.init_atlas_region : Failed to allocate state.cache") + LRU_init( & state, u32(capacity.x * capacity.y) ) + } + init_atlas_region( & atlas.region_a, atlas_params, atlas_params.region_a, { 4, 2}, 1024 ) + init_atlas_region( & atlas.region_b, atlas_params, atlas_params.region_b, { 4, 2}, 512 ) + init_atlas_region( & atlas.region_c, atlas_params, atlas_params.region_c, { 4, 1}, 512 ) + init_atlas_region( & atlas.region_d, atlas_params, atlas_params.region_d, { 2, 1}, 256 ) + + atlas.width = atlas_params.width + atlas.height = atlas_params.height + atlas.glyph_padding = atlas_params.glyph_padding + + atlas.region_a.offset = {0, 0} + atlas.region_b.offset.x = 0 + atlas.region_b.offset.y = atlas.region_a.size.y + atlas.region_c.offset.x = atlas.region_a.size.x + atlas.region_c.offset.y = 0 + atlas.region_d.offset.x = i32(atlas.width) / 2 + atlas.region_d.offset.y = 0 + + LRU_init( & shape_cache.state, shape_cache_params.capacity ) + + shape_cache.storage, error = make( [dynamic]ShapedText, shape_cache_params.capacity ) + assert(error == .None, "VEFontCache.init : Failed to allocate shape_cache.storage") + + for idx : u32 = 0; idx < shape_cache_params.capacity; idx += 1 { + stroage_entry := & shape_cache.storage[idx] + using stroage_entry + glyphs, error = make( [dynamic]Glyph, shape_cache_params.reserve_length ) + assert( error == .None, "VEFontCache.init : Failed to allocate glyphs array for shape cache storage" ) + + positions, error = make( [dynamic]Vec2, shape_cache_params.reserve_length ) + assert( error == .None, "VEFontCache.init : Failed to allocate positions array for shape cache storage" ) + } + + // Note(From original author): We can actually go over VE_FONTCACHE_GLYPHDRAW_BUFFER_BATCH batches due to smart packing! + { + using glyph_buffer + over_sample = glyph_draw_params.over_sample + batch = glyph_draw_params.buffer_batch + width = atlas.region_d.width * u32(over_sample.x) * batch + height = atlas.region_d.height * u32(over_sample.y) + draw_padding = glyph_draw_params.draw_padding + + draw_list.calls, error = make( [dynamic]DrawCall, cast(u64) glyph_draw_params.buffer_batch * 2 ) + assert( error == .None, "VEFontCache.init : Failed to allocate calls for draw_list" ) + + draw_list.indices, error = make( [dynamic]u32, cast(u64) glyph_draw_params.buffer_batch * 2 * 6 ) + assert( error == .None, "VEFontCache.init : Failed to allocate indices array for draw_list" ) + + draw_list.vertices, error = make( [dynamic]Vertex, glyph_draw_params.buffer_batch * 2 * 4 ) + assert( error == .None, "VEFontCache.init : Failed to allocate vertices array for draw_list" ) + + clear_draw_list.calls, error = make( [dynamic]DrawCall, cast(u64) glyph_draw_params.buffer_batch * 2 ) + assert( error == .None, "VEFontCache.init : Failed to allocate calls for calls for clear_draw_list" ) + + clear_draw_list.indices, error = make( [dynamic]u32, cast(u64) glyph_draw_params.buffer_batch * 2 * 4 ) + assert( error == .None, "VEFontCache.init : Failed to allocate calls for indices array for clear_draw_list" ) + + clear_draw_list.vertices, error = make( [dynamic]Vertex, glyph_draw_params.buffer_batch * 2 * 4 ) + assert( error == .None, "VEFontCache.init : Failed to allocate vertices array for clear_draw_list" ) + } + + parser_init( & parser_ctx ) + shaper_init( & shaper_ctx ) +} + +hot_reload :: proc( ctx : ^Context, allocator : Allocator ) +{ + assert( ctx != nil ) + ctx.backing = allocator + context.allocator = ctx.backing + using ctx + + reload_array( & entries, allocator ) + reload_array( & temp_path, allocator ) + reload_map( & ctx.temp_codepoint_seen, allocator ) + + reload_array( & draw_list.vertices, allocator) + reload_array( & draw_list.indices, allocator ) + reload_array( & draw_list.calls, allocator ) + + LRU_reload( & atlas.region_a.state, allocator) + LRU_reload( & atlas.region_b.state, allocator) + LRU_reload( & atlas.region_c.state, allocator) + LRU_reload( & atlas.region_d.state, allocator) + + LRU_reload( & shape_cache.state, allocator ) + for idx : u32 = 0; idx < u32(len(shape_cache.storage)); idx += 1 { + stroage_entry := & shape_cache.storage[idx] + using stroage_entry + + reload_array( & glyphs, allocator ) + reload_array( & positions, allocator ) + } + + reload_array( & glyph_buffer.draw_list.calls, allocator ) + reload_array( & glyph_buffer.draw_list.indices, allocator ) + reload_array( & glyph_buffer.draw_list.vertices, allocator ) + + reload_array( & glyph_buffer.clear_draw_list.calls, allocator ) + reload_array( & glyph_buffer.clear_draw_list.indices, allocator ) + reload_array( & glyph_buffer.clear_draw_list.vertices, allocator ) + + reload_array( & shape_cache.storage, allocator ) + LRU_reload( & shape_cache.state, allocator ) +} + +// ve_foncache_shutdown +shutdown :: proc( ctx : ^Context ) +{ + assert( ctx != nil ) + context.allocator = ctx.backing + using ctx + + for & entry in entries { + unload_font( ctx, entry.id ) + } + + shaper_shutdown( & shaper_ctx ) + + // TODO(Ed): Finish implementing, there is quite a few resource not released here. +} + +// ve_fontcache_load +load_font :: proc( ctx : ^Context, label : string, data : []byte, size_px : f32 ) -> (font_id : FontID) +{ + assert( ctx != nil ) + assert( len(data) > 0 ) + using ctx + context.allocator = backing + + id : i32 = -1 + + for index : i32 = 0; index < i32(len(entries)); index += 1 { + if entries[index].used do continue + id = index + break + } + if id == -1 { + append_elem( & entries, Entry {}) + id = cast(i32) len(entries) - 1 + } + assert( id >= 0 && id < i32(len(entries)) ) + + entry := & entries[ id ] + { + using entry + + parser_info = parser_load_font( & parser_ctx, label, data ) + // assert( parser_info != nil, "VEFontCache.load_font: Failed to load font info from parser" ) + + size = size_px + size_scale = size_px < 0.0 ? \ + parser_scale_for_pixel_height( & parser_info, -size_px ) \ + : parser_scale_for_mapping_em_to_pixels( & parser_info, size_px ) + + used = true + + shaper_info = shaper_load_font( & shaper_ctx, label, data, transmute(rawptr) id ) + // assert( shaper_info != nil, "VEFontCache.load_font: Failed to load font from shaper") + } + entry.id = FontID(id) + ctx.entries[ id ].id = FontID(id) + + font_id = FontID(id) + return +} + +// ve_fontcache_unload +unload_font :: proc( ctx : ^Context, font : FontID ) +{ + assert( ctx != nil ) + assert( font >= 0 && int(font) < len(ctx.entries) ) + context.allocator = ctx.backing + + using ctx + entry := & ctx.entries[ font ] + entry.used = false + + parser_unload_font( & entry.parser_info ) + shaper_unload_font( & entry.shaper_info ) +} + +#endregion("lifetime") + +#region("drawing") + +// ve_fontcache_configure_snap +configure_snap :: #force_inline proc( ctx : ^Context, snap_width, snap_height : u32 ) { + assert( ctx != nil ) + ctx.snap_width = snap_width + ctx.snap_height = snap_height +} + +get_cursor_pos :: #force_inline proc "contextless" ( ctx : ^Context ) -> Vec2 { return ctx.cursor_pos } +set_colour :: #force_inline proc "contextless" ( ctx : ^Context, colour : Colour ) { ctx.colour = colour } + +draw_text :: proc( ctx : ^Context, font : FontID, text_utf8 : string, position : Vec2, scale : Vec2 ) -> b32 +{ + // profile(#procedure) + assert( ctx != nil ) + assert( font >= 0 && int(font) < len(ctx.entries) ) + + ctx.cursor_pos = {} + + position := position + snap_width := f32(ctx.snap_width) + snap_height := f32(ctx.snap_height) + if ctx.snap_width > 0 do position.x = cast(f32) cast(u32) (position.x * snap_width + 0.5) / snap_width + if ctx.snap_height > 0 do position.y = cast(f32) cast(u32) (position.y * snap_height + 0.5) / snap_height + + entry := & ctx.entries[ font ] + + ChunkType :: enum u32 { Visible, Formatting } + chunk_kind : ChunkType + chunk_start : int = 0 + chunk_end : int = 0 + + text_utf8_bytes := transmute([]u8) text_utf8 + text_chunk : string + + text_chunk = transmute(string) text_utf8_bytes[ : ] + if len(text_chunk) > 0 { + shaped := shape_text_cached( ctx, font, text_chunk, entry ) + ctx.cursor_pos = draw_text_shape( ctx, font, entry, shaped, position, scale, snap_width, snap_height ) + } + return true +} + +// ve_fontcache_drawlist +get_draw_list :: proc( ctx : ^Context, optimize_before_returning := true ) -> ^DrawList { + assert( ctx != nil ) + if optimize_before_returning do optimize_draw_list( & ctx.draw_list, 0 ) + return & ctx.draw_list +} + +get_draw_list_layer :: proc( ctx : ^Context, optimize_before_returning := true ) -> (vertices : []Vertex, indices : []u32, calls : []DrawCall) { + assert( ctx != nil ) + if optimize_before_returning do optimize_draw_list( & ctx.draw_list, ctx.draw_layer.calls_offset ) + vertices = ctx.draw_list.vertices[ ctx.draw_layer.vertices_offset : ] + indices = ctx.draw_list.indices [ ctx.draw_layer.indices_offset : ] + calls = ctx.draw_list.calls [ ctx.draw_layer.calls_offset : ] + return +} + +// ve_fontcache_flush_drawlist +flush_draw_list :: proc( ctx : ^Context ) { + assert( ctx != nil ) + using ctx + clear_draw_list( & draw_list ) + draw_layer.vertices_offset = 0 + draw_layer.indices_offset = 0 + draw_layer.calls_offset = 0 +} + +flush_draw_list_layer :: proc( ctx : ^Context ) { + assert( ctx != nil ) + using ctx + draw_layer.vertices_offset = len(draw_list.vertices) + draw_layer.indices_offset = len(draw_list.indices) + draw_layer.calls_offset = len(draw_list.calls) +} + +#endregion("drawing") + +#region("metrics") + +measure_text_size :: proc( ctx : ^Context, font : FontID, text_utf8 : string ) -> (measured : Vec2) +{ + // profile(#procedure) + assert( ctx != nil ) + assert( font >= 0 && int(font) < len(ctx.entries) ) + + atlas := ctx.atlas + entry := & ctx.entries[ font ] + shaped := shape_text_cached( ctx, font, text_utf8, entry ) + padding := cast(f32) atlas.glyph_padding + + for index : i32 = 0; index < i32(len(shaped.glyphs)); index += 1 + { + glyph_index := shaped.glyphs[ index ] + if is_empty( ctx, entry, glyph_index ) do continue + + bounds_0, bounds_1 := parser_get_glyph_box( & entry.parser_info, glyph_index ) + bounds_size := bounds_1 - bounds_0 + + glyph_size := Vec2 { f32(bounds_size.x), f32(bounds_size.y) } * entry.size_scale + measured.y = max(measured.y, glyph_size.y) + } + measured.x = shaped.end_cursor_pos.x + return measured +} + +get_font_vertical_metrics :: #force_inline proc ( ctx : ^Context, font : FontID ) -> ( ascent, descent, line_gap : i32 ) +{ + assert( ctx != nil ) + assert( font >= 0 && int(font) < len(ctx.entries) ) + + entry := & ctx.entries[ font ] + ascent, descent, line_gap = parser_get_font_vertical_metrics( & entry.parser_info ) + return +} + +#endregion("metrics") diff --git a/atlas.odin b/atlas.odin new file mode 100644 index 0000000..180d5f2 --- /dev/null +++ b/atlas.odin @@ -0,0 +1,153 @@ +package VEFontCache + +AtlasRegionKind :: enum u8 { + None = 0x00, + A = 0x41, + B = 0x42, + C = 0x43, + D = 0x44, + E = 0x45, + Ignore = 0xFF, // ve_fontcache_cache_glyph_to_atlas uses a -1 value in clear draw call +} + +AtlasRegion :: struct { + state : LRU_Cache, + + width : u32, + height : u32, + + size : Vec2i, + capacity : Vec2i, + offset : Vec2i, + + next_idx : u32, +} + +Atlas :: struct { + width : u32, + height : u32, + + glyph_padding : u32, + + region_a : AtlasRegion, + region_b : AtlasRegion, + region_c : AtlasRegion, + region_d : AtlasRegion, +} + +atlas_bbox :: proc( atlas : ^Atlas, region : AtlasRegionKind, local_idx : i32 ) -> (position, size: Vec2) +{ + switch region + { + case .A: + size.x = f32(atlas.region_a.width) + size.y = f32(atlas.region_a.height) + + position.x = cast(f32) (( local_idx % atlas.region_a.capacity.x ) * i32(atlas.region_a.width)) + position.y = cast(f32) (( local_idx / atlas.region_a.capacity.x ) * i32(atlas.region_a.height)) + + position.x += f32(atlas.region_a.offset.x) + position.y += f32(atlas.region_a.offset.y) + + case .B: + size.x = f32(atlas.region_b.width) + size.y = f32(atlas.region_b.height) + + position.x = cast(f32) (( local_idx % atlas.region_b.capacity.x ) * i32(atlas.region_b.width)) + position.y = cast(f32) (( local_idx / atlas.region_b.capacity.x ) * i32(atlas.region_b.height)) + + position.x += f32(atlas.region_b.offset.x) + position.y += f32(atlas.region_b.offset.y) + + case .C: + size.x = f32(atlas.region_c.width) + size.y = f32(atlas.region_c.height) + + position.x = cast(f32) (( local_idx % atlas.region_c.capacity.x ) * i32(atlas.region_c.width)) + position.y = cast(f32) (( local_idx / atlas.region_c.capacity.x ) * i32(atlas.region_c.height)) + + position.x += f32(atlas.region_c.offset.x) + position.y += f32(atlas.region_c.offset.y) + + case .D: + size.x = f32(atlas.region_d.width) + size.y = f32(atlas.region_d.height) + + position.x = cast(f32) (( local_idx % atlas.region_d.capacity.x ) * i32(atlas.region_d.width)) + position.y = cast(f32) (( local_idx / atlas.region_d.capacity.x ) * i32(atlas.region_d.height)) + + position.x += f32(atlas.region_d.offset.x) + position.y += f32(atlas.region_d.offset.y) + + case .Ignore: fallthrough + case .None: fallthrough + case .E: + } + return +} + +decide_codepoint_region :: proc( ctx : ^Context, entry : ^Entry, glyph_index : Glyph +) -> (region_kind : AtlasRegionKind, region : ^AtlasRegion, over_sample : Vec2) +{ + if parser_is_glyph_empty( & entry.parser_info, glyph_index ) { + region_kind = .None + } + + bounds_0, bounds_1 := parser_get_glyph_box( & entry.parser_info, glyph_index ) + bounds_width := f32(bounds_1.x - bounds_0.x) + bounds_height := f32(bounds_1.y - bounds_0.y) + + atlas := & ctx.atlas + glyph_buffer := & ctx.glyph_buffer + + glyph_padding := f32(atlas.glyph_padding) * 2 + + bounds_width_scaled := cast(u32) (bounds_width * entry.size_scale + glyph_padding) + bounds_height_scaled := cast(u32) (bounds_height * entry.size_scale + glyph_padding) + + if bounds_width_scaled <= atlas.region_a.width && bounds_height_scaled <= atlas.region_a.height + { + // Region A for small glyphs. These are good for things such as punctuation. + region_kind = .A + region = & atlas.region_a + } + else if bounds_width_scaled <= atlas.region_b.width && bounds_height_scaled <= atlas.region_b.height + { + // Region B for tall glyphs. These are good for things such as european alphabets. + region_kind = .B + region = & atlas.region_b + } + else if bounds_width_scaled <= atlas.region_c.width && bounds_height_scaled <= atlas.region_c.height + { + // Region C for big glyphs. These are good for things such as asian typography. + region_kind = .C + region = & atlas.region_c + } + else if bounds_width_scaled <= atlas.region_d.width && bounds_height_scaled <= atlas.region_d.height + { + // Region D for huge glyphs. These are good for things such as titles and 4k. + region_kind = .D + region = & atlas.region_d + } + else if bounds_width_scaled <= glyph_buffer.width && bounds_height_scaled <= glyph_buffer.height + { + // Region 'E' for massive glyphs. These are rendered uncached and un-oversampled. + region_kind = .E + region = nil + if bounds_width_scaled <= glyph_buffer.width / 2 && bounds_height_scaled <= glyph_buffer.height / 2 { + over_sample = { 2.0, 2.0 } + } + else { + over_sample = { 1.0, 1.0 } + } + return + } + else { + region_kind = .None + return + } + + over_sample = glyph_buffer.over_sample + assert(region != nil) + return +} diff --git a/docs/Readme.md b/docs/Readme.md new file mode 100644 index 0000000..7a3c91a --- /dev/null +++ b/docs/Readme.md @@ -0,0 +1,75 @@ +# Interface + +Notes +--- + +Freetype implementation supports specifying a FT_Memory handle which is a pointer to a FT_MemoryRect. This can be used to define an allocator for the parser. Currently this library does not wrap this interface (yet). If using freetype its recommend to update `parser_init` with the necessary changes to wrap the context's backing allocator for freetype to utilize. + +```c + struct FT_MemoryRec_ + { + void* user; + FT_Alloc_Func alloc; + FT_Free_Func free; + FT_Realloc_Func realloc; + }; + ``` + +### startup + +Initializes a provided context. + +There are a large amount of parameters to tune the library instance to the user's preference. By default, keep in mind the library defaults to utilize stb_truetype as the font parser and harfbuzz (soon...) for the shaper. + +Much of the data structures within the context struct are not fixed-capacity allocations so make sure that the backing allocator utilized can handle it. + +### hot_reload + +The library supports being used in a dynamically loaded module. If this occurs simply make sure to call this procedure with a reference to the backing allocator provided during startup as all dynamic containers tend to lose a proper reference to the allocator's procedure. + +### shutdown + +Release resources from the context. + +### configure_snap + +You'll find this used immediately in draw_text it acts as a way to snap the position of the text to the nearest pixel for the width and height specified. + +If snapping is not desired, set the snap_width and height before calling draw_text to 0. + +## get_cursor_pos + +Will provide the current cursor_pos for the resulting text drawn. + +## set_color + +Sets the color to utilize on `DrawCall`s for FrameBuffer.Target or .Target_Uncached passes + +### get_draw_list + +Get the enqueded draw_list (vertices, indices, and draw call arrays) in its entirety. +By default, if get_draw_list is called, it will first call `optimize_draw_list` to optimize the draw list's calls for the user. If this is undesired, make sure to pass `optimize_before_returning = false` in the arguments. + +### get_draw_list_layer + +Get the enqueued draw_list for the current "layer". +A layer is considered the slice of the drawlist's content from the last call to `flush_draw_list_layer` onward. +By default, if get_draw_list_layer is called, it will first call `optimize_draw_list` for the user to optimize the slice (exlusively) of the draw list's draw calls. If this is undesired, make sure to pass `optimize_before_returning = false` in the arguments. + +The draw layer offsets are cleared with `flush_draw_list` + +### flush_draw_list + +Will clear the draw list and draw layer offsets. + +### flush_draw_list_layer + +Will update the draw list layer with the latest offset based on the current lenght of the draw list vertices, indices, and calls arrays. + +### measure_text_size + +Provides a Vec2 the width and height occupied by the provided text string. The y is measured to be the the largest glyph box bounds height of the text. The width is derived from the `end_cursor_pos` field from a `ShapedText` entry. + +## get_font_vertical_metrics + +A wrapper for `parser_get_font_vertical_metrics`. Will provide the ascent, descent, and line_gap for a font entry. diff --git a/docs/draw_text_codepaths.pur b/docs/draw_text_codepaths.pur new file mode 100644 index 0000000000000000000000000000000000000000..061c9b92e9c12bfdb4d990396beaf6d88cf7b206 GIT binary patch literal 48520 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 buffer; +ve_fontcache_init( &cache ); +ve_fontcache_configure_snap( &cache, width, height ); +print_font = ve_fontcache_loadfile( &cache, "fonts/NotoSansJP-Light.otf", buffer, 19.0f ); +ve_fontcache_draw_text( &cache, print_font, u8"hello world", 0, 0, 1.0f / width, 1.0f / height ); +``` + +These header files need to be copied to your project: +``` +ve_fontcache.h +utf8.h +stb_truetype.h +``` + +Except HarfBuzz, that's all the required dependencies. That said it's strongly recommended +to use HarfBuzz ( TODO: HarfBuzz not supported yet, coming soon!! ) over the default utf8.h latin +fallback text shaper. + +## Integration with rendering backend + +VEFontCache is largely backend agnostic. Currently the demo project uses OpenGL 3.3 for Windows. +That said it's designed to be integrated with VE, a Vulkan engine. +Please read the "How to plug into rendering API" section in ve_fontcache.h for more documentation +on how to implement your own backend to plumb this directly into your engine! + +# Screenshots + +![Screenshot 1](images/ve_fontcache_demo1.png) + +![Screenshot 2](images/ve_fontcache_demo2.png) + +![Screenshot 3](images/raincode.png) + +![Screenshot 4](images/ve_fontcache_pressure_test.gif) + +# Similar projects and links + +Here are links to some awesome similar and related projects: +* fontstash - https://github.com/memononen/fontstash +* stb_truetype ( has font rasterisation itself ) - https://github.com/nothings/stb/blob/master/stb_truetype.h +* slug - http://sluglibrary.com/ +* pathfinder - https://github.com/pcwalton/pathfinder +* https://medium.com/@evanwallace/easy-scalable-text-rendering-on-the-gpu-c3f4d782c5ac diff --git a/draw.odin b/draw.odin new file mode 100644 index 0000000..04f96a2 --- /dev/null +++ b/draw.odin @@ -0,0 +1,718 @@ +package VEFontCache + +Vertex :: struct { + pos : Vec2, + u, v : f32, +} + +DrawCall :: struct { + pass : FrameBufferPass, + start_index : u32, + end_index : u32, + clear_before_draw : b32, + region : AtlasRegionKind, + colour : Colour, +} + +DrawCall_Default :: DrawCall { + pass = .None, + start_index = 0, + end_index = 0, + clear_before_draw = false, + region = .A, + colour = { 1.0, 1.0, 1.0, 1.0 } +} + +DrawList :: struct { + vertices : [dynamic]Vertex, + indices : [dynamic]u32, + calls : [dynamic]DrawCall, +} + +FrameBufferPass :: enum u32 { + None = 0, + Glyph = 1, + Atlas = 2, + Target = 3, + Target_Uncached = 4, +} + +GlyphDrawBuffer :: struct { + over_sample : Vec2, + batch : u32, + width : u32, + height : u32, + draw_padding : u32, + + batch_x : i32, + clear_draw_list : DrawList, + draw_list : DrawList, +} + +blit_quad :: proc( draw_list : ^DrawList, p0 : Vec2 = {0, 0}, p1 : Vec2 = {1, 1}, uv0 : Vec2 = {0, 0}, uv1 : Vec2 = {1, 1} ) +{ + // profile(#procedure) + // logf("Blitting: xy0: %0.2f, %0.2f xy1: %0.2f, %0.2f uv0: %0.2f, %0.2f uv1: %0.2f, %0.2f", + // p0.x, p0.y, p1.x, p1.y, uv0.x, uv0.y, uv1.x, uv1.y); + v_offset := cast(u32) len(draw_list.vertices) + + vertex := Vertex { + {p0.x, p0.y}, + uv0.x, uv0.y + } + append_elem( & draw_list.vertices, vertex ) + + vertex = Vertex { + {p0.x, p1.y}, + uv0.x, uv1.y + } + append_elem( & draw_list.vertices, vertex ) + + vertex = Vertex { + {p1.x, p0.y}, + uv1.x, uv0.y + } + append_elem( & draw_list.vertices, vertex ) + + vertex = Vertex { + {p1.x, p1.y}, + uv1.x, uv1.y + } + append_elem( & draw_list.vertices, vertex ) + + quad_indices : []u32 = { + 0, 1, 2, + 2, 1, 3 + } + for index : i32 = 0; index < 6; index += 1 { + append( & draw_list.indices, v_offset + quad_indices[ index ] ) + } + return +} + +cache_glyph :: proc( ctx : ^Context, font : FontID, glyph_index : Glyph, entry : ^Entry, bounds_0, bounds_1 : Vec2, scale, translate : Vec2 ) -> b32 +{ + // profile(#procedure) + if glyph_index == Glyph(0) { + // Note(Original Author): Glyph not in current hb_font + return false + } + + // Retrieve the shape definition from the parser. + shape, error := parser_get_glyph_shape( & entry.parser_info, glyph_index ) + assert( error == .None ) + if len(shape) == 0 { + return false + } + + if ctx.debug_print_verbose + { + log( "shape:") + for vertex in shape + { + if vertex.type == .Move { + logf("move_to %d %d", vertex.x, vertex.y ) + } + else if vertex.type == .Line { + logf("line_to %d %d", vertex.x, vertex.y ) + } + else if vertex.type == .Curve { + logf("curve_to %d %d through %d %d", vertex.x, vertex.y, vertex.contour_x0, vertex.contour_y0 ) + } + else if vertex.type == .Cubic { + logf("cubic_to %d %d through %d %d and %d %d", + vertex.x, vertex.y, + vertex.contour_x0, vertex.contour_y0, + vertex.contour_x1, vertex.contour_y1 ) + } + } + } + + /* + Note(Original Author): + We need a random point that is outside our shape. We simply pick something diagonally across from top-left bound corner. + Note that this outside point is scaled alongside the glyph in ve_fontcache_draw_filled_path, so we don't need to handle that here. + */ + outside := Vec2 { + bounds_0.x - 21, + bounds_0.y - 33, + } + + // Note(Original Author): Figure out scaling so it fits within our box. + draw := DrawCall_Default + draw.pass = FrameBufferPass.Glyph + draw.start_index = u32(len(ctx.draw_list.indices)) + + // Note(Original Author); + // Draw the path using simplified version of https://medium.com/@evanwallace/easy-scalable-text-rendering-on-the-gpu-c3f4d782c5ac. + // Instead of involving fragment shader code we simply make use of modern GPU ability to crunch triangles and brute force curve definitions. + path := ctx.temp_path + clear( & path) + for edge in shape do switch edge.type + { + case .Move: + if len(path) > 0 { + draw_filled_path( & ctx.draw_list, outside, path[:], scale, translate, ctx.debug_print_verbose ) + } + clear( & path) + fallthrough + + case .Line: + append( & path, Vec2{ f32(edge.x), f32(edge.y) }) + + case .Curve: + assert( len(path) > 0 ) + p0 := path[ len(path) - 1 ] + p1 := Vec2{ f32(edge.contour_x0), f32(edge.contour_y0) } + p2 := Vec2{ f32(edge.x), f32(edge.y) } + + step := 1.0 / f32(ctx.curve_quality) + alpha := step + for index := i32(0); index < i32(ctx.curve_quality); index += 1 { + append( & path, eval_point_on_bezier3( p0, p1, p2, alpha )) + alpha += step + } + + case .Cubic: + assert( len(path) > 0 ) + p0 := path[ len(path) - 1] + p1 := Vec2{ f32(edge.contour_x0), f32(edge.contour_y0) } + p2 := Vec2{ f32(edge.contour_x1), f32(edge.contour_y1) } + p3 := Vec2{ f32(edge.x), f32(edge.y) } + + step := 1.0 / f32(ctx.curve_quality) + alpha := step + for index := i32(0); index < i32(ctx.curve_quality); index += 1 { + append( & path, eval_point_on_bezier4( p0, p1, p2, p3, alpha )) + alpha += step + } + + case .None: + assert(false, "Unknown edge type or invalid") + } + if len(path) > 0 { + draw_filled_path( & ctx.draw_list, outside, path[:], scale, translate, ctx.debug_print_verbose ) + } + + // Note(Original Author): Apend the draw call + draw.end_index = cast(u32) len(ctx.draw_list.indices) + if draw.end_index > draw.start_index { + append(& ctx.draw_list.calls, draw) + } + + parser_free_shape( & entry.parser_info, shape ) + return true +} + +/* + Called by: + * can_batch_glyph : If it determines that the glyph was not detected and we haven't reached capacity in the atlas + * draw_text_shape : Glyph +*/ +cache_glyph_to_atlas :: proc( ctx : ^Context, + font : FontID, + glyph_index : Glyph, + lru_code : u64, + atlas_index : i32, + entry : ^Entry, + region_kind : AtlasRegionKind, + region : ^AtlasRegion, + over_sample : Vec2 ) +{ + // profile(#procedure) + + // Get hb_font text metrics. These are unscaled! + bounds_0, bounds_1 := parser_get_glyph_box( & entry.parser_info, glyph_index ) + bounds_size := Vec2 { + f32(bounds_1.x - bounds_0.x), + f32(bounds_1.y - bounds_0.y) + } + + // E region is special case and not cached to atlas. + if region_kind == .None || region_kind == .E do return + + // Grab an atlas LRU cache slot. + atlas_index := atlas_index + if atlas_index == -1 + { + if region.next_idx < region.state.capacity + { + evicted := LRU_put( & region.state, lru_code, i32(region.next_idx) ) + atlas_index = i32(region.next_idx) + region.next_idx += 1 + assert( evicted == lru_code ) + } + else + { + next_evict_codepoint := LRU_get_next_evicted( & region.state ) + assert( next_evict_codepoint != 0xFFFFFFFFFFFFFFFF ) + + atlas_index = LRU_peek( & region.state, next_evict_codepoint, must_find = true ) + assert( atlas_index != -1 ) + + evicted := LRU_put( & region.state, lru_code, atlas_index ) + assert( evicted == next_evict_codepoint ) + } + + assert( LRU_get( & region.state, lru_code ) != - 1 ) + } + + atlas := & ctx.atlas + glyph_buffer := & ctx.glyph_buffer + atlas_size := Vec2 { f32(atlas.width), f32(atlas.height) } + glyph_buffer_size := Vec2 { f32(glyph_buffer.width), f32(glyph_buffer.height) } + glyph_padding := cast(f32) atlas.glyph_padding + + if ctx.debug_print + { + @static debug_total_cached : i32 = 0 + logf("glyph %v%v( %v ) caching to atlas region %v at idx %d. %d total glyphs cached.\n", + i32(glyph_index), rune(glyph_index), cast(rune) region_kind, atlas_index, debug_total_cached) + debug_total_cached += 1 + } + + // Draw oversized glyph to update FBO + glyph_draw_scale := over_sample * entry.size_scale + glyph_draw_translate := -1 * vec2(bounds_0) * glyph_draw_scale + vec2( glyph_padding ) + glyph_draw_translate.x = cast(f32) (i32(glyph_draw_translate.x + 0.9999999)) + glyph_draw_translate.y = cast(f32) (i32(glyph_draw_translate.y + 0.9999999)) + + // Allocate a glyph_update_FBO region + gwidth_scaled_px := bounds_size.x * glyph_draw_scale.x + 1.0 + over_sample.x * glyph_padding + if i32(f32(glyph_buffer.batch_x) + gwidth_scaled_px) >= i32(glyph_buffer.width) { + flush_glyph_buffer_to_atlas( ctx ) + } + + // Calculate the src and destination regions + slot_position, slot_szie := atlas_bbox( atlas, region_kind, atlas_index ) + + dst_glyph_position := slot_position + dst_glyph_size := bounds_size * entry.size_scale + glyph_padding + dst_size := slot_szie + screenspace_x_form( & dst_glyph_position, & dst_glyph_size, atlas_size ) + screenspace_x_form( & slot_position, & dst_size, atlas_size ) + + src_position := Vec2 { f32(glyph_buffer.batch_x), 0 } + src_size := bounds_size * glyph_draw_scale + over_sample * glyph_padding + textspace_x_form( & src_position, & src_size, glyph_buffer_size ) + + // Advance glyph_update_batch_x and calculate final glyph drawing transform + glyph_draw_translate.x += f32(glyph_buffer.batch_x) + glyph_buffer.batch_x += i32(gwidth_scaled_px) + screenspace_x_form( & glyph_draw_translate, & glyph_draw_scale, glyph_buffer_size ) + + call : DrawCall + { + // Queue up clear on target region on atlas + using call + pass = .Atlas + region = .Ignore + start_index = cast(u32) len(glyph_buffer.clear_draw_list.indices) + + blit_quad( & glyph_buffer.clear_draw_list, + slot_position, slot_position + dst_size, + { 1.0, 1.0 }, { 1.0, 1.0 } ) + + end_index = cast(u32) len(glyph_buffer.clear_draw_list.indices) + append( & glyph_buffer.clear_draw_list.calls, call ) + + // Queue up a blit from glyph_update_FBO to the atlas + region = .None + start_index = cast(u32) len(glyph_buffer.draw_list.indices) + + blit_quad( & glyph_buffer.draw_list, + dst_glyph_position, slot_position + dst_glyph_size, + src_position, src_position + src_size ) + + end_index = cast(u32) len(glyph_buffer.draw_list.indices) + append( & glyph_buffer.draw_list.calls, call ) + } + + // Render glyph to glyph_update_FBO + cache_glyph( ctx, font, glyph_index, entry, vec2(bounds_0), vec2(bounds_1), glyph_draw_scale, glyph_draw_translate ) +} + +can_batch_glyph :: #force_inline proc( ctx : ^Context, font : FontID, entry : ^Entry, glyph_index : Glyph, + lru_code : u64, + atlas_index : i32, + region_kind : AtlasRegionKind, + region : ^AtlasRegion, + over_sample : Vec2 +) -> b32 +{ + // profile(#procedure) + assert( glyph_index != -1 ) + + // E region can't batch + if region_kind == .E || region_kind == .None do return false + if ctx.temp_codepoint_seen_num > 1024 do return false + // TODO(Ed): Why 1024? + + if atlas_index == - 1 + { + if region.next_idx > u32( region.state.capacity) { + // We will evict LRU. We must predict which LRU will get evicted, and if it's something we've seen then we need to take slowpath and flush batch. + next_evict_codepoint := LRU_get_next_evicted( & region.state ) + seen, success := ctx.temp_codepoint_seen[next_evict_codepoint] + assert(success != false) + + if (seen) { + return false + } + } + + cache_glyph_to_atlas( ctx, font, glyph_index, lru_code, atlas_index, entry, region_kind, region, over_sample ) + } + + assert( LRU_get( & region.state, lru_code ) != -1 ) + mark_batch_codepoint_seen( ctx, lru_code) + return true +} + +// ve_fontcache_clear_drawlist +clear_draw_list :: #force_inline proc ( draw_list : ^DrawList ) { + clear( & draw_list.calls ) + clear( & draw_list.indices ) + clear( & draw_list.vertices ) +} + +directly_draw_massive_glyph :: proc( ctx : ^Context, + entry : ^Entry, + glyph : Glyph, + bounds_0, bounds_1 : Vec2, + bounds_size : Vec2, + over_sample, position, scale : Vec2 ) +{ + // profile(#procedure) + flush_glyph_buffer_to_atlas( ctx ) + + glyph_padding := f32(ctx.atlas.glyph_padding) + glyph_buffer_size := Vec2 { f32(ctx.glyph_buffer.width), f32(ctx.glyph_buffer.height) } + + // Draw un-antialiased glyph to update FBO. + glyph_draw_scale := over_sample * entry.size_scale + glyph_draw_translate := -1 * bounds_0 * glyph_draw_scale + vec2_from_scalar(glyph_padding) + screenspace_x_form( & glyph_draw_translate, & glyph_draw_scale, glyph_buffer_size ) + + cache_glyph( ctx, entry.id, glyph, entry, bounds_0, bounds_1, glyph_draw_scale, glyph_draw_translate ) + + glyph_padding_dbl := glyph_padding * 2 + bounds_scaled := bounds_size * entry.size_scale + + // Figure out the source rect. + glyph_position := Vec2 {} + glyph_size := vec2(glyph_padding_dbl) + glyph_dst_size := glyph_size + bounds_scaled + glyph_size += bounds_scaled * over_sample + + // Figure out the destination rect. + bounds_0_scaled := Vec2 { + cast(f32) i32(bounds_0.x * entry.size_scale - 0.5), + cast(f32) i32(bounds_0.y * entry.size_scale - 0.5), + } + dst := position + scale * bounds_0_scaled - glyph_padding * scale + dst_size := glyph_dst_size * scale + textspace_x_form( & glyph_position, & glyph_size, glyph_buffer_size ) + + // Add the glyph drawcall. + call : DrawCall + { + using call + pass = .Target_Uncached + colour = ctx.colour + start_index = u32(len(ctx.draw_list.indices)) + + blit_quad( & ctx.draw_list, + dst, dst + dst_size, + glyph_position, glyph_position + glyph_size ) + + end_index = u32(len(ctx.draw_list.indices)) + append( & ctx.draw_list.calls, call ) + } + + // Clear glyph_update_FBO. + call.pass = .Glyph + call.start_index = 0 + call.end_index = 0 + call.clear_before_draw = true + append( & ctx.draw_list.calls, call ) +} + +draw_cached_glyph :: proc( ctx : ^Context, + entry : ^Entry, + glyph_index : Glyph, + lru_code : u64, + atlas_index : i32, + bounds_0, bounds_1 : Vec2, + region_kind : AtlasRegionKind, + region : ^AtlasRegion, + over_sample : Vec2, + position, scale : Vec2 +) -> b32 +{ + // profile(#procedure) + bounds_size := Vec2 { + f32(bounds_1.x - bounds_0.x), + f32(bounds_1.y - bounds_0.y), + } + + // E region is special case and not cached to atlas + if region_kind == .E + { + directly_draw_massive_glyph( ctx, entry, glyph_index, bounds_0, bounds_1, bounds_size, over_sample, position, scale ) + return true + } + + // Is this codepoint cached? + if atlas_index == - 1 { + return false + } + + atlas := & ctx.atlas + atlas_size := Vec2 { f32(atlas.width), f32(atlas.height) } + glyph_padding := f32(atlas.glyph_padding) + + // Figure out the source bounding box in the atlas texture + slot_position, _ := atlas_bbox( atlas, region_kind, atlas_index ) + + glyph_scale := bounds_size * entry.size_scale + glyph_padding + + bounds_0_scaled := bounds_0 * entry.size_scale //- { 0.5, 0.5 } + bounds_0_scaled = ceil(bounds_0_scaled) + + dst := position + bounds_0_scaled * scale + dst -= glyph_padding * scale + dst_scale := glyph_scale * scale + + textspace_x_form( & slot_position, & glyph_scale, atlas_size ) + + // Add the glyph drawcall + call := DrawCall_Default + { + using call + pass = .Target + colour = ctx.colour + start_index = cast(u32) len(ctx.draw_list.indices) + + blit_quad( & ctx.draw_list, + dst, dst + dst_scale, + slot_position, slot_position + glyph_scale ) + end_index = cast(u32) len(ctx.draw_list.indices) + } + append( & ctx.draw_list.calls, call ) + return true +} + +// Constructs a triangle fan to fill a shape using the provided path +// outside_point represents the center point of the fan. +// +// Note(Original Author): +// WARNING: doesn't actually append drawcall; caller is responsible for actually appending the drawcall. +// ve_fontcache_draw_filled_path +draw_filled_path :: proc( draw_list : ^DrawList, outside_point : Vec2, path : []Vec2, + scale := Vec2 { 1, 1 }, + translate := Vec2 { 0, 0 }, + debug_print_verbose : b32 = false +) +{ + if debug_print_verbose + { + log("outline_path:") + for point in path { + vec := point * scale + translate + logf(" %0.2f %0.2f", vec.x, vec.y ) + } + } + + v_offset := cast(u32) len(draw_list.vertices) + for point in path { + vertex := Vertex { + pos = point * scale + translate, + u = 0, + v = 0, + } + append( & draw_list.vertices, vertex ) + } + + outside_vertex := cast(u32) len(draw_list.vertices) + { + vertex := Vertex { + pos = outside_point * scale + translate, + u = 0, + v = 0, + } + append( & draw_list.vertices, vertex ) + } + + for index : u32 = 1; index < cast(u32) len(path); index += 1 { + indices := & draw_list.indices + append( indices, outside_vertex ) + append( indices, v_offset + index - 1 ) + append( indices, v_offset + index ) + } +} + +draw_text_batch :: proc( ctx : ^Context, entry : ^Entry, shaped : ^ShapedText, + batch_start_idx, batch_end_idx : i32, + position, scale : Vec2, + snap_width, snap_height : f32 ) +{ + flush_glyph_buffer_to_atlas( ctx ) + for index := batch_start_idx; index < batch_end_idx; index += 1 + { + glyph_index := shaped.glyphs[ index ] + + if glyph_index == 0 do continue + if parser_is_glyph_empty( & entry.parser_info, glyph_index ) do continue + + region_kind, region, over_sample := decide_codepoint_region( ctx, entry, glyph_index ) + lru_code := font_glyph_lru_code(entry.id, glyph_index) + atlas_index := cast(i32) -1 + + if region_kind != .E do atlas_index = LRU_get( & region.state, lru_code ) + bounds_0, bounds_1 := parser_get_glyph_box( & entry.parser_info, glyph_index ) + + shaped_position := shaped.positions[index] + glyph_translate := position + shaped_position * scale + + glyph_cached := draw_cached_glyph( ctx, + entry, glyph_index, + lru_code, atlas_index, + vec2(bounds_0), vec2(bounds_1), + region_kind, region, over_sample, + glyph_translate, scale) + assert( glyph_cached == true ) + } +} + +// Helper for draw_text, all raw text content should be confirmed to be either formatting or visible shapes before getting cached. +draw_text_shape :: proc( ctx : ^Context, + font : FontID, + entry : ^Entry, + shaped : ^ShapedText, + position, scale : Vec2, + snap_width, snap_height : f32 +) -> (cursor_pos : Vec2) +{ + // position := position //+ ctx.cursor_pos * scale + // profile(#procedure) + batch_start_idx : i32 = 0 + for index : i32 = 0; index < cast(i32) len(shaped.glyphs); index += 1 + { + glyph_index := shaped.glyphs[ index ] + if is_empty( ctx, entry, glyph_index ) do continue + + region_kind, region, over_sample := decide_codepoint_region( ctx, entry, glyph_index ) + lru_code := font_glyph_lru_code(entry.id, glyph_index) + atlas_index := cast(i32) -1 + + if region_kind != .E do atlas_index = LRU_get( & region.state, lru_code ) + if can_batch_glyph( ctx, font, entry, glyph_index, lru_code, atlas_index, region_kind, region, over_sample ) do continue + + // Glyph has not been catched, needs to be directly drawn. + + // First batch the other cached glyphs + // flush_glyph_buffer_to_atlas(ctx) + draw_text_batch( ctx, entry, shaped, batch_start_idx, index, position, scale, snap_width, snap_height ) + reset_batch_codepoint_state( ctx ) + + cache_glyph_to_atlas( ctx, font, glyph_index, lru_code, atlas_index, entry, region_kind, region, over_sample ) + mark_batch_codepoint_seen( ctx, lru_code) + batch_start_idx = index + } + + // flush_glyph_buffer_to_atlas(ctx) + draw_text_batch( ctx, entry, shaped, batch_start_idx, cast(i32) len(shaped.glyphs), position, scale, snap_width , snap_height ) + reset_batch_codepoint_state( ctx ) + cursor_pos = shaped.end_cursor_pos + return +} + +flush_glyph_buffer_to_atlas :: proc( ctx : ^Context ) +{ + // profile(#procedure) + // Flush drawcalls to draw list + merge_draw_list( & ctx.draw_list, & ctx.glyph_buffer.clear_draw_list ) + merge_draw_list( & ctx.draw_list, & ctx.glyph_buffer.draw_list) + clear_draw_list( & ctx.glyph_buffer.draw_list ) + clear_draw_list( & ctx.glyph_buffer.clear_draw_list ) + + // Clear glyph_update_FBO + if ctx.glyph_buffer.batch_x != 0 + { + call := DrawCall_Default + call.pass = .Glyph + call.start_index = 0 + call.end_index = 0 + call.clear_before_draw = true + append( & ctx.draw_list.calls, call ) + ctx.glyph_buffer.batch_x = 0 + } +} + +// ve_fontcache_merge_drawlist +merge_draw_list :: proc( dst, src : ^DrawList ) +{ + // profile(#procedure) + error : AllocatorError + + v_offset := cast(u32) len( dst.vertices ) + num_appended : int + num_appended, error = append( & dst.vertices, ..src.vertices[:] ) + assert( error == .None ) + + i_offset := cast(u32) len(dst.indices) + for index : int = 0; index < len(src.indices); index += 1 { + ignored : int + ignored, error = append( & dst.indices, src.indices[index] + v_offset ) + assert( error == .None ) + } + + for index : int = 0; index < len(src.calls); index += 1 { + src_call := src.calls[ index ] + src_call.start_index += i_offset + src_call.end_index += i_offset + append( & dst.calls, src_call ) + assert( error == .None ) + } +} + +optimize_draw_list :: proc( draw_list : ^DrawList, call_offset : int ) +{ + // profile(#procedure) + assert( draw_list != nil ) + + write_index : int = call_offset + for index : int = 1 + call_offset; index < len(draw_list.calls); index += 1 + { + assert( write_index <= index ) + draw_0 := & draw_list.calls[ write_index ] + draw_1 := & draw_list.calls[ index ] + + merge : b32 = true + if draw_0.pass != draw_1.pass do merge = false + if draw_0.end_index != draw_1.start_index do merge = false + if draw_0.region != draw_1.region do merge = false + if draw_1.clear_before_draw do merge = false + if draw_0.colour != draw_1.colour do merge = false + + if merge + { + // logf("merging %v : %v %v", draw_0.pass, write_index, index ) + draw_0.end_index = draw_1.end_index + draw_1.start_index = 0 + draw_1.end_index = 0 + } + else + { + // logf("can't merge %v : %v %v", draw_0.pass, write_index, index ) + write_index += 1 + if write_index != index { + draw_2 := & draw_list.calls[ write_index ] + draw_2^ = draw_1^ + } + } + } + + resize( & draw_list.calls, write_index + 1 ) +} diff --git a/mappings.odin b/mappings.odin new file mode 100644 index 0000000..db538fa --- /dev/null +++ b/mappings.odin @@ -0,0 +1,81 @@ +package VEFontCache + +import "core:hash" + fnv64a :: hash.fnv64a +import "core:math" + ceil_f16 :: math.ceil_f16 + ceil_f16le :: math.ceil_f16le + ceil_f16be :: math.ceil_f16be + ceil_f32 :: math.ceil_f32 + ceil_f32le :: math.ceil_f32le + ceil_f32be :: math.ceil_f32be + ceil_f64 :: math.ceil_f64 + ceil_f64le :: math.ceil_f64le + ceil_f64be :: math.ceil_f64be +import "core:math/linalg" +import "core:mem" + Kilobyte :: mem.Kilobyte + slice_ptr :: mem.slice_ptr + + Allocator :: mem.Allocator + AllocatorError :: mem.Allocator_Error + + Arena :: mem.Arena + arena_allocator :: mem.arena_allocator + arena_init :: mem.arena_init +// import "codebase:grime" + // log :: grime.log + // logf :: grime.logf + // profile :: grime.profile + +//#region("Proc overload mappings") + +append :: proc { + append_elem, + append_elems, + append_elem_string, +} + +ceil :: proc { + math.ceil_f16, + math.ceil_f16le, + math.ceil_f16be, + math.ceil_f32, + math.ceil_f32le, + math.ceil_f32be, + math.ceil_f64, + math.ceil_f64le, + math.ceil_f64be, + + ceil_vec2, +} + +clear :: proc { + clear_dynamic_array, +} + +make :: proc { + make_dynamic_array, + make_dynamic_array_len, + make_dynamic_array_len_cap, + make_map, +} + +resize :: proc { + resize_dynamic_array, +} + +vec2 :: proc { + vec2_from_scalar, + vec2_from_vec2i, +} + +vec2i :: proc { + vec2i_from_vec2, +} + +vec2_64 :: proc { + vec2_64_from_vec2, +} + +//#endregion("Proc overload mappings") diff --git a/misc.odin b/misc.odin new file mode 100644 index 0000000..a27b04c --- /dev/null +++ b/misc.odin @@ -0,0 +1,165 @@ +package VEFontCache + +import "base:runtime" +import core_log "core:log" + +Colour :: [4]f32 +Vec2 :: [2]f32 +Vec2i :: [2]i32 +Vec2_64 :: [2]f64 + +vec2_from_scalar :: #force_inline proc "contextless" ( scalar : f32 ) -> Vec2 { return { scalar, scalar }} +vec2_64_from_vec2 :: #force_inline proc "contextless" ( v2 : Vec2 ) -> Vec2_64 { return { f64(v2.x), f64(v2.y) }} +vec2_from_vec2i :: #force_inline proc "contextless" ( v2i : Vec2i ) -> Vec2 { return { f32(v2i.x), f32(v2i.y) }} +vec2i_from_vec2 :: #force_inline proc "contextless" ( v2 : Vec2 ) -> Vec2i { return { i32(v2.x), i32(v2.y) }} + +@(require_results) ceil_vec2 :: proc "contextless" ( v : Vec2 ) -> Vec2 { return { ceil_f32(v.x), ceil_f32(v.y) } } + +// This buffer is used below excluisvely to prevent any allocator recusion when verbose logging from allocators. +// This means a single line is limited to 32k buffer (increase naturally if this SOMEHOW becomes a bottleneck...) +Logger_Allocator_Buffer : [32 * Kilobyte]u8 + +log :: proc( msg : string, level := core_log.Level.Info, loc := #caller_location ) { + temp_arena : Arena; arena_init(& temp_arena, Logger_Allocator_Buffer[:]) + context.allocator = arena_allocator(& temp_arena) + context.temp_allocator = arena_allocator(& temp_arena) + + core_log.log( level, msg, location = loc ) +} + +logf :: proc( fmt : string, args : ..any, level := core_log.Level.Info, loc := #caller_location ) { + temp_arena : Arena; arena_init(& temp_arena, Logger_Allocator_Buffer[:]) + context.allocator = arena_allocator(& temp_arena) + context.temp_allocator = arena_allocator(& temp_arena) + + core_log.logf( level, fmt, ..args, location = loc ) +} + +reload_array :: proc( self : ^[dynamic]$Type, allocator : Allocator ) { + raw := transmute( ^runtime.Raw_Dynamic_Array) self + raw.allocator = allocator +} + +reload_map :: proc( self : ^map [$KeyType] $EntryType, allocator : Allocator ) { + raw := transmute( ^runtime.Raw_Map) self + raw.allocator = allocator +} + +font_glyph_lru_code :: #force_inline proc "contextless" ( font : FontID, glyph_index : Glyph ) -> (lru_code : u64) { + lru_code = u64(glyph_index) + ( ( 0x100000000 * u64(font) ) & 0xFFFFFFFF00000000 ) + return +} + +shape_lru_hash :: #force_inline proc "contextless" ( label : string ) -> u64 { + hash : u64 + for str_byte in transmute([]byte) label { + hash = ((hash << 8) + hash) + u64(str_byte) + } + return hash +} + +// For a provided alpha value, +// allows the function to calculate the position of a point along the curve at any given fraction of its total length +// ve_fontcache_eval_bezier (quadratic) +eval_point_on_bezier3 :: #force_inline proc "contextless" ( p0, p1, p2 : Vec2, alpha : f32 ) -> Vec2 +{ + p0 := vec2_64(p0) + p1 := vec2_64(p1) + p2 := vec2_64(p2) + alpha := f64(alpha) + + weight_start := (1 - alpha) * (1 - alpha) + weight_control := 2.0 * (1 - alpha) * alpha + weight_end := alpha * alpha + + starting_point := p0 * weight_start + control_point := p1 * weight_control + end_point := p2 * weight_end + + point := starting_point + control_point + end_point + return { f32(point.x), f32(point.y) } +} + +// For a provided alpha value, +// allows the function to calculate the position of a point along the curve at any given fraction of its total length +// ve_fontcache_eval_bezier (cubic) +eval_point_on_bezier4 :: #force_inline proc "contextless" ( p0, p1, p2, p3 : Vec2, alpha : f32 ) -> Vec2 +{ + p0 := vec2_64(p0) + p1 := vec2_64(p1) + p2 := vec2_64(p2) + p3 := vec2_64(p3) + alpha := f64(alpha) + + weight_start := (1 - alpha) * (1 - alpha) * (1 - alpha) + weight_c_a := 3 * (1 - alpha) * (1 - alpha) * alpha + weight_c_b := 3 * (1 - alpha) * alpha * alpha + weight_end := alpha * alpha * alpha + + start_point := p0 * weight_start + control_a := p1 * weight_c_a + control_b := p2 * weight_c_b + end_point := p3 * weight_end + + point := start_point + control_a + control_b + end_point + return { f32(point.x), f32(point.y) } +} + +is_empty :: #force_inline proc ( ctx : ^Context, entry : ^Entry, glyph_index : Glyph ) -> b32 +{ + if glyph_index == 0 do return true + if parser_is_glyph_empty( & entry.parser_info, glyph_index ) do return true + return false +} + +mark_batch_codepoint_seen :: #force_inline proc ( ctx : ^Context, lru_code : u64 ) { + ctx.temp_codepoint_seen[lru_code] = true + ctx.temp_codepoint_seen_num += 1 +} + +reset_batch_codepoint_state :: #force_inline proc( ctx : ^Context ) { + clear_map( & ctx.temp_codepoint_seen ) + ctx.temp_codepoint_seen_num = 0 +} + +screenspace_x_form :: #force_inline proc "contextless" ( position, scale : ^Vec2, size : Vec2 ) { + when true + { + pos_64 := vec2_64_from_vec2(position^) + scale_64 := vec2_64_from_vec2(scale^) + + quotient : Vec2_64 = 1.0 / vec2_64(size) + pos_64 = pos_64 * quotient * 2.0 - 1.0 + scale_64 = scale_64 * quotient * 2.0 + + (position^) = { f32(pos_64.x), f32(pos_64.y) } + (scale^) = { f32(scale_64.x), f32(scale_64.y) } + } + else + { + quotient : Vec2 = 1.0 / size + (position^) *= quotient * 2.0 - 1.0 + (scale^) *= quotient * 2.0 + } +} + +textspace_x_form :: #force_inline proc "contextless" ( position, scale : ^Vec2, size : Vec2 ) { + when true + { + pos_64 := vec2_64_from_vec2(position^) + scale_64 := vec2_64_from_vec2(scale^) + + quotient : Vec2_64 = 1.0 / vec2_64(size) + pos_64 *= quotient + scale_64 *= quotient + + (position^) = { f32(pos_64.x), f32(pos_64.y) } + (scale^) = { f32(scale_64.x), f32(scale_64.y) } + } + else + { + quotient : Vec2 = 1.0 / size + (position^) *= quotient + (scale^) *= quotient + } +} diff --git a/parser.odin b/parser.odin new file mode 100644 index 0000000..52796ec --- /dev/null +++ b/parser.odin @@ -0,0 +1,495 @@ +package VEFontCache + +/* +Notes: + +Freetype will do memory allocations and has an interface the user can implement. +That interface is not exposed from this parser but could be added to parser_init. + +STB_Truetype has macros for its allocation unfortuantely +*/ + +import "base:runtime" +import "core:c" +import "core:math" +import stbtt "vendor:stb/truetype" +import freetype "thirdparty:freetype" + +ParserKind :: enum u32 { + STB_TrueType, + Freetype, +} + +ParserFontInfo :: struct { + label : string, + kind : ParserKind, + using _ : struct #raw_union { + stbtt_info : stbtt.fontinfo, + freetype_info : freetype.Face + }, + data : []byte, +} + +GlyphVertType :: enum u8 { + None, + Move = 1, + Line, + Curve, + Cubic, +} + +// Based directly off of stb_truetype's vertex +ParserGlyphVertex :: struct { + x, y : i16, + contour_x0, contour_y0 : i16, + contour_x1, contour_y1 : i16, + type : GlyphVertType, + padding : u8, +} +// A shape can be a dynamic array free_type or an opaque set of data handled by stb_truetype +ParserGlyphShape :: [dynamic]ParserGlyphVertex + +ParserContext :: struct { + kind : ParserKind, + ft_library : freetype.Library, + + // fonts : HMapChained(ParserFontInfo), +} + +parser_init :: proc( ctx : ^ParserContext ) +{ + switch ctx.kind + { + case .Freetype: + result := freetype.init_free_type( & ctx.ft_library ) + assert( result == freetype.Error.Ok, "VEFontCache.parser_init: Failed to initialize freetype" ) + + case .STB_TrueType: + // Do nothing intentional + } + + // error : AllocatorError + // ctx.fonts, error = make( HMapChained(ParserFontInfo), 256 ) + // assert( error == .None, "VEFontCache.parser_init: Failed to allocate fonts array" ) +} + +parser_shutdown :: proc( ctx : ^ParserContext ) +{ + // TODO(Ed): Implement +} + +parser_load_font :: proc( ctx : ^ParserContext, label : string, data : []byte ) -> (font : ParserFontInfo) +{ + // key := font_key_from_label(label) + // font = get( ctx.fonts, key ) + // if font != nil do return + + // error : AllocatorError + // font, error = set( ctx.fonts, key, ParserFontInfo {} ) + // assert( error == .None, "VEFontCache.parser_load_font: Failed to set a new parser font info" ) + switch ctx.kind + { + case .Freetype: + error := freetype.new_memory_face( ctx.ft_library, raw_data(data), cast(i32) len(data), 0, & font.freetype_info ) + if error != .Ok do return + + case .STB_TrueType: + success := stbtt.InitFont( & font.stbtt_info, raw_data(data), 0 ) + if ! success do return + } + + font.label = label + font.data = data + return +} + +parser_unload_font :: proc( font : ^ParserFontInfo ) +{ + switch font.kind { + case .Freetype: + error := freetype.done_face( font.freetype_info ) + assert( error == .Ok, "VEFontCache.parser_unload_font: Failed to unload freetype face" ) + + case .STB_TrueType: + // Do Nothing + } +} + +parser_find_glyph_index :: #force_inline proc "contextless" ( font : ^ParserFontInfo, codepoint : rune ) -> (glyph_index : Glyph) +{ + switch font.kind + { + case .Freetype: + glyph_index = transmute(Glyph) freetype.get_char_index( font.freetype_info, transmute(u32) codepoint ) + return + + case .STB_TrueType: + glyph_index = transmute(Glyph) stbtt.FindGlyphIndex( & font.stbtt_info, codepoint ) + return + } + return Glyph(-1) +} + +parser_free_shape :: proc( font : ^ParserFontInfo, shape : ParserGlyphShape ) +{ + switch font.kind + { + case .Freetype: + delete(shape) + + case .STB_TrueType: + stbtt.FreeShape( & font.stbtt_info, transmute( [^]stbtt.vertex) raw_data(shape) ) + } +} + +parser_get_codepoint_horizontal_metrics :: #force_inline proc "contextless" ( font : ^ParserFontInfo, codepoint : rune ) -> ( advance, to_left_side_glyph : i32 ) +{ + switch font.kind + { + case .Freetype: + glyph_index := transmute(Glyph) freetype.get_char_index( font.freetype_info, transmute(u32) codepoint ) + if glyph_index != 0 + { + freetype.load_glyph( font.freetype_info, c.uint(codepoint), { .No_Bitmap, .No_Hinting, .No_Scale } ) + advance = i32(font.freetype_info.glyph.advance.x) >> 6 + to_left_side_glyph = i32(font.freetype_info.glyph.metrics.hori_bearing_x) >> 6 + } + else + { + advance = 0 + to_left_side_glyph = 0 + } + + case .STB_TrueType: + stbtt.GetCodepointHMetrics( & font.stbtt_info, codepoint, & advance, & to_left_side_glyph ) + } + return +} + +parser_get_codepoint_kern_advance :: #force_inline proc "contextless" ( font : ^ParserFontInfo, prev_codepoint, codepoint : rune ) -> i32 +{ + switch font.kind + { + case .Freetype: + prev_glyph_index := transmute(Glyph) freetype.get_char_index( font.freetype_info, transmute(u32) prev_codepoint ) + glyph_index := transmute(Glyph) freetype.get_char_index( font.freetype_info, transmute(u32) codepoint ) + if prev_glyph_index != 0 && glyph_index != 0 + { + kerning : freetype.Vector + font.freetype_info.driver.clazz.get_kerning( font.freetype_info, transmute(u32) prev_codepoint, transmute(u32) codepoint, & kerning ) + } + + case .STB_TrueType: + kern := stbtt.GetCodepointKernAdvance( & font.stbtt_info, prev_codepoint, codepoint ) + return kern + } + return -1 +} + +parser_get_font_vertical_metrics :: #force_inline proc "contextless" ( font : ^ParserFontInfo ) -> (ascent, descent, line_gap : i32 ) +{ + switch font.kind + { + case .Freetype: + + case .STB_TrueType: + stbtt.GetFontVMetrics( & font.stbtt_info, & ascent, & descent, & line_gap ) + } + return +} + +parser_get_glyph_box :: #force_inline proc ( font : ^ParserFontInfo, glyph_index : Glyph ) -> (bounds_0, bounds_1 : Vec2i) +{ + switch font.kind + { + case .Freetype: + freetype.load_glyph( font.freetype_info, c.uint(glyph_index), { .No_Bitmap, .No_Hinting, .No_Scale } ) + + metrics := font.freetype_info.glyph.metrics + + bounds_0 = {i32(metrics.hori_bearing_x), i32(metrics.hori_bearing_y - metrics.height)} + bounds_1 = {i32(metrics.hori_bearing_x + metrics.width), i32(metrics.hori_bearing_y)} + + case .STB_TrueType: + x0, y0, x1, y1 : i32 + success := cast(bool) stbtt.GetGlyphBox( & font.stbtt_info, i32(glyph_index), & x0, & y0, & x1, & y1 ) + assert( success ) + + bounds_0 = { i32(x0), i32(y0) } + bounds_1 = { i32(x1), i32(y1) } + } + return +} + +parser_get_glyph_shape :: proc( font : ^ParserFontInfo, glyph_index : Glyph ) -> (shape : ParserGlyphShape, error : AllocatorError) +{ + switch font.kind + { + case .Freetype: + error := freetype.load_glyph( font.freetype_info, cast(u32) glyph_index, { .No_Bitmap, .No_Hinting, .No_Scale } ) + if error != .Ok { + return + } + + glyph := font.freetype_info.glyph + if glyph.format != .Outline { + return + } + + /* + convert freetype outline to stb_truetype shape + + freetype docs: https://freetype.org/freetype2/docs/glyphs/glyphs-6.html + + stb_truetype shape info: + The shape is a series of contours. Each one starts with + a STBTT_moveto, then consists of a series of mixed + STBTT_lineto and STBTT_curveto segments. A lineto + draws a line from previous endpoint to its x,y; a curveto + draws a quadratic bezier from previous endpoint to + its x,y, using cx,cy as the bezier control point. + */ + { + FT_CURVE_TAG_CONIC :: 0x00 + FT_CURVE_TAG_ON :: 0x01 + FT_CURVE_TAG_CUBIC :: 0x02 + + vertices, error := make( [dynamic]ParserGlyphVertex, 1024 ) + assert( error == .None ) + + // TODO(Ed): This makes freetype second class I guess but VEFontCache doesn't have native support for freetype originally so.... + outline := & glyph.outline + + contours := transmute( [^]u16) outline.contours + points := transmute( [^]freetype.Vector) outline.points + tags := transmute( [^]u8) outline.tags + + // TODO(Ed): Review this, never tested before and its problably bad. + for contour : i32 = 0; contour < i32(outline.n_contours); contour += 1 + { + start := (contour == 0) ? 0 : i32(contours[ contour - 1 ] + 1) + end := i32(contours[ contour ]) + + for index := start; index < i32(outline.n_points); index += 1 + { + point := points[ index ] + tag := tags[ index ] + + if (tag & FT_CURVE_TAG_ON) != 0 + { + if len(vertices) > 0 && !(vertices[len(vertices) - 1].type == .Move ) + { + // Close the previous contour if needed + append(& vertices, ParserGlyphVertex { type = .Line, + x = i16(points[start].x), y = i16(points[start].y), + contour_x0 = i16(0), contour_y0 = i16(0), + contour_x1 = i16(0), contour_y1 = i16(0), + padding = 0, + }) + } + + append(& vertices, ParserGlyphVertex { type = .Move, + x = i16(point.x), y = i16(point.y), + contour_x0 = i16(0), contour_y0 = i16(0), + contour_x1 = i16(0), contour_y1 = i16(0), + padding = 0, + }) + } + else if (tag & FT_CURVE_TAG_CUBIC) != 0 + { + point1 := points[ index + 1 ] + point2 := points[ index + 2 ] + append(& vertices, ParserGlyphVertex { type = .Cubic, + x = i16(point2.x), y = i16(point2.y), + contour_x0 = i16(point.x), contour_y0 = i16(point.y), + contour_x1 = i16(point1.x), contour_y1 = i16(point1.y), + padding = 0, + }) + index += 2 + } + else if (tag & FT_CURVE_TAG_CONIC) != 0 + { + // TODO(Ed): This is using a very dead simple algo to convert the conic to a cubic curve + // not sure if we need something more sophisticaated + point1 := points[ index + 1 ] + + control_conv :: f32(0.5) // Conic to cubic control point distance + to_float := f32(1.0 / 64.0) + + fp := Vec2 { f32(point.x), f32(point.y) } * to_float + fp1 := Vec2 { f32(point1.x), f32(point1.y) } * to_float + + control1 := freetype.Vector { + point.x + freetype.Pos( (fp1.x - fp.x) * control_conv * 64.0 ), + point.y + freetype.Pos( (fp1.y - fp.y) * control_conv * 64.0 ), + } + control2 := freetype.Vector { + point1.x + freetype.Pos( (fp.x - fp1.x) * control_conv * 64.0 ), + point1.y + freetype.Pos( (fp.y - fp1.y) * control_conv * 64.0 ), + } + append(& vertices, ParserGlyphVertex { type = .Cubic, + x = i16(point1.x), y = i16(point1.y), + contour_x0 = i16(control1.x), contour_y0 = i16(control1.y), + contour_x1 = i16(control2.x), contour_y1 = i16(control2.y), + padding = 0, + }) + index += 1 + } + else + { + append(& vertices, ParserGlyphVertex { type = .Line, + x = i16(point.x), y = i16(point.y), + contour_x0 = i16(0), contour_y0 = i16(0), + contour_x1 = i16(0), contour_y1 = i16(0), + padding = 0, + }) + } + } + + // Close contour + append(& vertices, ParserGlyphVertex { type = .Line, + x = i16(points[start].x), y = i16(points[start].y), + contour_x0 = i16(0), contour_y0 = i16(0), + contour_x1 = i16(0), contour_y1 = i16(0), + padding = 0, + }) + } + + shape = vertices + } + + case .STB_TrueType: + stb_shape : [^]stbtt.vertex + nverts := stbtt.GetGlyphShape( & font.stbtt_info, cast(i32) glyph_index, & stb_shape ) + + shape_raw := transmute( ^runtime.Raw_Dynamic_Array) & shape + shape_raw.data = stb_shape + shape_raw.len = int(nverts) + shape_raw.cap = int(nverts) + shape_raw.allocator = runtime.nil_allocator() + error = AllocatorError.None + return + } + + return +} + +parser_is_glyph_empty :: #force_inline proc "contextless" ( font : ^ParserFontInfo, glyph_index : Glyph ) -> b32 +{ + switch font.kind + { + case .Freetype: + error := freetype.load_glyph( font.freetype_info, cast(u32) glyph_index, { .No_Bitmap, .No_Hinting, .No_Scale } ) + if error == .Ok + { + if font.freetype_info.glyph.format == .Outline { + return font.freetype_info.glyph.outline.n_points == 0 + } + else if font.freetype_info.glyph.format == .Bitmap { + return font.freetype_info.glyph.bitmap.width == 0 && font.freetype_info.glyph.bitmap.rows == 0; + } + } + return false + + case .STB_TrueType: + return stbtt.IsGlyphEmpty( & font.stbtt_info, cast(c.int) glyph_index ) + } + return false +} + +parser_scale :: #force_inline proc "contextless" ( font : ^ParserFontInfo, size : f32 ) -> f32 +{ + size_scale := size < 0.0 ? \ + parser_scale_for_pixel_height( font, -size ) \ + : parser_scale_for_mapping_em_to_pixels( font, size ) + // size_scale = 1.0 + return size_scale +} + +parser_scale_for_pixel_height :: #force_inline proc "contextless" ( font : ^ParserFontInfo, size : f32 ) -> f32 +{ + switch font.kind { + case .Freetype: + freetype.set_pixel_sizes( font.freetype_info, 0, cast(u32) size ) + size_scale := size / cast(f32)font.freetype_info.units_per_em + return size_scale + + case.STB_TrueType: + return stbtt.ScaleForPixelHeight( & font.stbtt_info, size ) + } + return 0 +} + +parser_scale_for_mapping_em_to_pixels :: #force_inline proc "contextless" ( font : ^ParserFontInfo, size : f32 ) -> f32 +{ + switch font.kind { + case .Freetype: + Inches_To_CM :: cast(f32) 2.54 + Points_Per_CM :: cast(f32) 28.3465 + CM_Per_Point :: cast(f32) 1.0 / DPT_DPCM + CM_Per_Pixel :: cast(f32) 1.0 / DPT_PPCM + DPT_DPCM :: cast(f32) 72.0 * Inches_To_CM // 182.88 points/dots per cm + DPT_PPCM :: cast(f32) 96.0 * Inches_To_CM // 243.84 pixels per cm + DPT_DPI :: cast(f32) 72.0 + + // TODO(Ed): Don't assume the dots or pixels per inch. + system_dpi :: DPT_DPI + + FT_Font_Size_Point_Unit :: 1.0 / 64.0 + FT_Point_10 :: 64.0 + + points_per_em := (size / system_dpi ) * DPT_DPI + freetype.set_char_size( font.freetype_info, 0, cast(freetype.F26Dot6) f32(points_per_em * FT_Point_10), cast(u32) DPT_DPI, cast(u32) DPT_DPI ) + size_scale := f32(f64(size) / cast(f64) font.freetype_info.units_per_em) + return size_scale + + case .STB_TrueType: + return stbtt.ScaleForMappingEmToPixels( & font.stbtt_info, size ) + } + return 0 +} + +when false { +parser_convert_conic_to_cubic_freetype :: proc( vertices : Array(ParserGlyphVertex), p0, p1, p2 : freetype.Vector, tolerance : f32 ) +{ + scratch : [Kilobyte * 4]u8 + scratch_arena : Arena; arena_init(& scratch_arena, scratch[:]) + + points, error := make( Array(freetype.Vector), 256, allocator = arena_allocator( &scratch_arena) ) + assert(error == .None) + + append( & points, p0) + append( & points, p1) + append( & points, p2) + + to_float : f32 = 1.0 / 64.0 + control_conv :: f32(2.0 / 3.0) // Conic to cubic control point distance + + for ; points.num > 1; { + p0 := points.data[0] + p1 := points.data[1] + p2 := points.data[2] + + fp0 := Vec2{ f32(p0.x), f32(p0.y) } * to_float + fp1 := Vec2{ f32(p1.x), f32(p1.y) } * to_float + fp2 := Vec2{ f32(p2.x), f32(p2.y) } * to_float + + delta_x := fp0.x - 2 * fp1.x + fp2.x; + delta_y := fp0.y - 2 * fp1.y + fp2.y; + distance := math.sqrt(delta_x * delta_x + delta_y * delta_y); + + if distance <= tolerance + { + control1 := { + + } + } + else + { + control2 := { + + } + } + } +} +} diff --git a/shaped_text.odin b/shaped_text.odin new file mode 100644 index 0000000..510ed30 --- /dev/null +++ b/shaped_text.odin @@ -0,0 +1,132 @@ +package VEFontCache + +import "core:math" + +ShapedText :: struct { + glyphs : [dynamic]Glyph, + positions : [dynamic]Vec2, + end_cursor_pos : Vec2, +} + +ShapedTextCache :: struct { + storage : [dynamic]ShapedText, + state : LRU_Cache, + next_cache_id : i32, +} + +shape_text_cached :: proc( ctx : ^Context, font : FontID, text_utf8 : string, entry : ^Entry ) -> ^ShapedText +{ + // profile(#procedure) + @static buffer : [64 * Kilobyte]byte + + font := font + text_size := len(text_utf8) + sice_end_offset := size_of(FontID) + len(text_utf8) + + buffer_slice := buffer[:] + font_bytes := slice_ptr( transmute(^byte) & font, size_of(FontID) ) + copy( buffer_slice, font_bytes ) + + text_bytes := transmute( []byte) text_utf8 + buffer_slice_post_font := buffer[ size_of(FontID) : sice_end_offset ] + copy( buffer_slice_post_font, text_bytes ) + + hash := shape_lru_hash( transmute(string) buffer[: sice_end_offset ] ) + + shape_cache := & ctx.shape_cache + state := & ctx.shape_cache.state + + shape_cache_idx := LRU_get( state, hash ) + if shape_cache_idx == -1 + { + if shape_cache.next_cache_id < i32(state.capacity) { + shape_cache_idx = shape_cache.next_cache_id + shape_cache.next_cache_id += 1 + evicted := LRU_put( state, hash, shape_cache_idx ) + assert( evicted == hash ) + } + else + { + next_evict_idx := LRU_get_next_evicted( state ) + assert( next_evict_idx != 0xFFFFFFFFFFFFFFFF ) + + shape_cache_idx = LRU_peek( state, next_evict_idx, must_find = true ) + assert( shape_cache_idx != - 1 ) + + LRU_put( state, hash, shape_cache_idx ) + } + + shape_text_uncached( ctx, font, text_utf8, entry, & shape_cache.storage[ shape_cache_idx ] ) + } + + return & shape_cache.storage[ shape_cache_idx ] +} + +// TODO(Ed): Make position rounding an option +shape_text_uncached :: proc( ctx : ^Context, font : FontID, text_utf8 : string, entry : ^Entry, output : ^ShapedText ) +{ + // profile(#procedure) + assert( ctx != nil ) + assert( font >= 0 && int(font) < len(ctx.entries) ) + + use_full_text_shape := ctx.text_shape_adv + + clear( & output.glyphs ) + clear( & output.positions ) + + ascent, descent, line_gap := parser_get_font_vertical_metrics( & entry.parser_info ) + + if use_full_text_shape + { + // assert( entry.shaper_info != nil ) + shaper_shape_from_text( & ctx.shaper_ctx, & entry.shaper_info, output, text_utf8, ascent, descent, line_gap, entry.size, entry.size_scale ) + return + } + else + { + // Note(Original Author): + // We use our own fallback dumbass text shaping. + // WARNING: PLEASE USE HARFBUZZ. GOOD TEXT SHAPING IS IMPORTANT FOR INTERNATIONALISATION. + ascent := f32(ascent) + descent := f32(descent) + line_gap := f32(line_gap) + + position : Vec2 + advance : i32 = 0 + to_left_side_glyph : i32 = 0 + + prev_codepoint : rune + for codepoint in text_utf8 + { + if prev_codepoint > 0 { + kern := parser_get_codepoint_kern_advance( & entry.parser_info, prev_codepoint, codepoint ) + position.x += f32(kern) * entry.size_scale + } + if codepoint == '\n' + { + position.x = 0.0 + position.y -= (ascent - descent + line_gap) * entry.size_scale + position.y = ceil(position.y) + prev_codepoint = rune(0) + continue + } + if abs( entry.size ) <= Advance_Snap_Smallfont_Size { + position.x = math.ceil( position.x ) + } + + append( & output.glyphs, parser_find_glyph_index( & entry.parser_info, codepoint )) + advance, to_left_side_glyph = parser_get_codepoint_horizontal_metrics( & entry.parser_info, codepoint ) + + append( & output.positions, Vec2 { + ceil(position.x), + position.y + }) + // append( & output.positions, position ) + + position.x += f32(advance) * entry.size_scale + prev_codepoint = codepoint + } + + output.end_cursor_pos = position + } +} diff --git a/shaper.odin b/shaper.odin new file mode 100644 index 0000000..18a5014 --- /dev/null +++ b/shaper.odin @@ -0,0 +1,165 @@ +package VEFontCache +/* +Note(Ed): The only reason I didn't directly use harfbuzz is because hamza exists and seems to be under active development as an alternative. +*/ + +import "core:c" +import "thirdparty:harfbuzz" + +ShaperKind :: enum { + Naive = 0, + Harfbuzz = 1, +} + +ShaperContext :: struct { + hb_buffer : harfbuzz.Buffer, + // infos : HMapChained(ShaperInfo), +} + +ShaperInfo :: struct { + blob : harfbuzz.Blob, + face : harfbuzz.Face, + font : harfbuzz.Font, +} + +shaper_init :: proc( ctx : ^ShaperContext ) +{ + ctx.hb_buffer = harfbuzz.buffer_create() + assert( ctx.hb_buffer != nil, "VEFontCache.shaper_init: Failed to create harfbuzz buffer") + + // error : AllocatorError + // ctx.infos, error = make( HMapChained(ShaperInfo), 256 ) + // assert( error == .None, "VEFontCache.shaper_init: Failed to create shaper infos map" ) +} + +shaper_shutdown :: proc( ctx : ^ShaperContext ) +{ + if ctx.hb_buffer != nil { + harfbuzz.buffer_destory( ctx.hb_buffer ) + } + + // delete(& ctx.infos) +} + +shaper_load_font :: proc( ctx : ^ShaperContext, label : string, data : []byte, user_data : rawptr ) -> (info : ShaperInfo) +{ + // key := font_key_from_label( label ) + // info = get( ctx.infos, key ) + // if info != nil do return + + // error : AllocatorError + // info, error = set( ctx.infos, key, ShaperInfo {} ) + // assert( error == .None, "VEFontCache.parser_load_font: Failed to set a new shaper info" ) + + using info + blob = harfbuzz.blob_create( raw_data(data), cast(c.uint) len(data), harfbuzz.Memory_Mode.READONLY, user_data, nil ) + face = harfbuzz.face_create( blob, 0 ) + font = harfbuzz.font_create( face ) + return +} + +shaper_unload_font :: proc( ctx : ^ShaperInfo ) +{ + using ctx + if blob != nil do harfbuzz.font_destroy( font ) + if face != nil do harfbuzz.face_destroy( face ) + if blob != nil do harfbuzz.blob_destroy( blob ) +} + +shaper_shape_from_text :: proc( ctx : ^ShaperContext, info : ^ShaperInfo, output :^ShapedText, text_utf8 : string, + ascent, descent, line_gap : i32, size, size_scale : f32 ) +{ + // profile(#procedure) + current_script := harfbuzz.Script.UNKNOWN + hb_ucfunc := harfbuzz.unicode_funcs_get_default() + harfbuzz.buffer_clear_contents( ctx.hb_buffer ) + assert( info.font != nil ) + + ascent := f32(ascent) + descent := f32(descent) + line_gap := f32(line_gap) + + position, vertical_position : f32 + shape_run :: proc( buffer : harfbuzz.Buffer, script : harfbuzz.Script, font : harfbuzz.Font, output : ^ShapedText, + position, vertical_position : ^f32, + ascent, descent, line_gap, size, size_scale : f32 ) + { + // Set script and direction. We use the system's default langauge. + // script = HB_SCRIPT_LATIN + harfbuzz.buffer_set_script( buffer, script ) + harfbuzz.buffer_set_direction( buffer, harfbuzz.script_get_horizontal_direction( script )) + harfbuzz.buffer_set_language( buffer, harfbuzz.language_get_default() ) + + // Perform the actual shaping of this run using HarfBuzz. + harfbuzz.shape( font, buffer, nil, 0 ) + + // Loop over glyphs and append to output buffer. + glyph_count : u32 + glyph_infos := harfbuzz.buffer_get_glyph_infos( buffer, & glyph_count ) + glyph_positions := harfbuzz.buffer_get_glyph_positions( buffer, & glyph_count ) + + for index : i32; index < i32(glyph_count); index += 1 + { + hb_glyph := glyph_infos[ index ] + hb_gposition := glyph_positions[ index ] + glyph_id := cast(Glyph) hb_glyph.codepoint + + if hb_glyph.cluster > 0 + { + (position^) = 0.0 + (vertical_position^) -= (ascent - descent + line_gap) * size_scale + (vertical_position^) = cast(f32) i32(vertical_position^ + 0.5) + continue + } + if abs( size ) <= Advance_Snap_Smallfont_Size + { + (position^) = ceil( position^ ) + } + + append( & output.glyphs, glyph_id ) + + pos := position^ + v_pos := vertical_position^ + offset_x := f32(hb_gposition.x_offset) * size_scale + offset_y := f32(hb_gposition.y_offset) * size_scale + append( & output.positions, Vec2 { cast(f32) i32( pos + offset_x + 0.5 ), + v_pos + offset_y, + }) + + (position^) += f32(hb_gposition.x_advance) * size_scale + (vertical_position^) += f32(hb_gposition.y_advance) * size_scale + } + + output.end_cursor_pos.x = position^ + output.end_cursor_pos.y = vertical_position^ + harfbuzz.buffer_clear_contents( buffer ) + } + + // Note(Original Author): + // We first start with simple bidi and run logic. + // True CTL is pretty hard and we don't fully support that; patches welcome! + + for codepoint, byte_offset in text_utf8 + { + script := harfbuzz.unicode_script( hb_ucfunc, cast(harfbuzz.Codepoint) codepoint ) + + // Can we continue the current run? + ScriptKind :: harfbuzz.Script + + special_script : b32 = script == ScriptKind.UNKNOWN || script == ScriptKind.INHERITED || script == ScriptKind.COMMON + if special_script || script == current_script { + harfbuzz.buffer_add( ctx.hb_buffer, cast(harfbuzz.Codepoint) codepoint, codepoint == '\n' ? 1 : 0 ) + current_script = special_script ? current_script : script + continue + } + + // End current run since we've encountered a script change. + shape_run( ctx.hb_buffer, current_script, info.font, output, & position, & vertical_position, ascent, descent, line_gap, size, size_scale ) + harfbuzz.buffer_add( ctx.hb_buffer, cast(harfbuzz.Codepoint) codepoint, codepoint == '\n' ? 1 : 0 ) + current_script = script + } + + // End the last run if needed + shape_run( ctx.hb_buffer, current_script, info.font, output, & position, & vertical_position, ascent, descent, line_gap, size, size_scale ) + return +}