From 36cc5579753bd5ed268cdf400f882ca7d3461849 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Fri, 10 Jan 2025 09:07:26 -0500 Subject: [PATCH] WIP: Updating public repo with latest version --- Readme.md | 52 +- docs/Readme.md | 2 + scripts/build_sokol_demo.ps1 | 8 +- vefontcache/LRU.odin | 208 +-- vefontcache/atlas.odin | 194 +-- vefontcache/draw.odin | 1275 +++++++-------- vefontcache/freetype_wip.odin | 163 ++ vefontcache/misc.odin | 155 +- vefontcache/parser.odin | 405 ++--- .../{mappings.odin => pkg_mapping.odin} | 67 +- vefontcache/shaped_text.odin | 131 -- vefontcache/shaper.odin | 395 ++++- vefontcache/vefontcache.odin | 1378 ++++++++++++----- 13 files changed, 2724 insertions(+), 1709 deletions(-) create mode 100644 vefontcache/freetype_wip.odin rename vefontcache/{mappings.odin => pkg_mapping.odin} (59%) delete mode 100644 vefontcache/shaped_text.odin diff --git a/Readme.md b/Readme.md index 197985d..9951a30 100644 --- a/Readme.md +++ b/Readme.md @@ -1,28 +1,52 @@ -# VE Font Cache : Odin Port +# VE Font Cache https://github.com/user-attachments/assets/b74f1ec1-f980-45df-b604-d6b7d87d40ff -This is a port of the [VEFontCache](https://github.com/hypernewbie/VEFontCache) library. - +This started off as a port of the [VEFontCache](https://github.com/hypernewbie/VEFontCache) library to the Odin programming language. Its original purpose was for use in game engines, however its rendeirng quality and performance is more than adequate for many other applications. +Since then the library has been overhauled to offer higher performance, improved visual fidelity, additional features, and quality of life improvements. + +Features: + +* Simple and well documented. +* Load and unload fonts at anytime +* Almost entirely configurabe and tunable at runtime! +* Full support for hot-reload + * Clear the caches at any-time! +* Robust quality of life features: + * Tracks text layers! + * Push and pop stack for font, font_size, colour, view, position, scale and zoom! + * Enforce even only font-sizing (useful for linear-zoom) [TODO] + * Snap-positining to view for better hinting +* Basic or advanced text shaping via Harfbuzz +* All rendering is real-time, triangulation done on the CPU, vertex rendering and texture blitting on the gpu. + * Can hand thousands of draw text calls with very large or small shapes. +* 4-Level Regioned Texture Atlas for caching rendered glyphs +* Text shape caching +* Glyph texture buffer for rendering the text with super-sampling to downsample to the atlas or direct to target screen. +* Super-sample by a font size scalar for sharper glyphs +* All caching backed by an optimized 32-bit LRU indexing cache +* Provides a draw list that is backend agnostic (see [backend](./backend) for usage example). + +Upcoming: + +* Support for ear-clipping triangulation + * Support for which triangulation method used on a by font basis? +* Multi-threading supported job queue. + * Lift heavy-lifting portion of the library's context into a thread context. + * Synchronize threads by merging their generated layered drawlist into a file draw-list for processing on the user's render thread. + * User defines how context's are distributed for drawing (a basic quandrant basic selector procedure will be provided.) + See: [docs/Readme.md](docs/Readme.md) for the library's interface. ## Building See [scripts/Readme.md](scripts/Readme.md) for building examples or utilizing the provided backends. -Currently the scripts provided & the library itself were developed & tested on Windows. There are bash scripts for building on linux & mac. +Currently the scripts provided & the library itself were developed & tested on Windows. There are bash scripts for building on linux (they build on WSL but need additional testing). -The library depends on freetype, harfbuzz, & stb_truetype to build. -Note: freetype and harfbuzz could technically be gutted if the user removes their definitions, however they have not been made into a conditional compilation option (yet). - -## Changes from orignal - -* Font Parser & Glyph shaper are abstracted to their own warpper interface -* ve_fontcache_loadfile not ported (ust use core:os or os2, then call load_font) -* Macro defines have been coverted (mostly) to runtime parameters -* Support for hot_reloading -* Curve quality step interpolation for glyph rendering can be set on a per font basis. +The library depends on harfbuzz, & stb_truetype to build. +Note: harfbuzz could technically be gutted if the user removes their definitions, however they have not been made into a conditional compilation option (yet). ![image](https://github.com/user-attachments/assets/2f6c0b36-179c-42fe-8903-7640ae3c209e) diff --git a/docs/Readme.md b/docs/Readme.md index 7195858..5ddbbeb 100644 --- a/docs/Readme.md +++ b/docs/Readme.md @@ -1,5 +1,7 @@ # Interface +TODO: OUTDATED + Notes --- diff --git a/scripts/build_sokol_demo.ps1 b/scripts/build_sokol_demo.ps1 index e9d1ef3..5dad272 100644 --- a/scripts/build_sokol_demo.ps1 +++ b/scripts/build_sokol_demo.ps1 @@ -94,11 +94,11 @@ function build-SokolBackendDemo # $build_args += $flag_micro_architecture_native $build_args += $flag_use_separate_modules $build_args += $flag_thread_count + $CoreCount_Physical - # $build_args += $flag_optimize_none + $build_args += $flag_optimize_none # $build_args += $flag_optimize_minimal # $build_args += $flag_optimize_speed - $build_args += $falg_optimize_aggressive - # $build_args += $flag_debug + # $build_args += $falg_optimize_aggressive + $build_args += $flag_debug $build_args += $flag_pdb_name + $pdb $build_args += $flag_subsystem + 'windows' # $build_args += ($flag_extra_linker_flags + $linker_args ) @@ -111,6 +111,8 @@ function build-SokolBackendDemo # $build_args += $flag_sanitize_address # $build_args += $flag_sanitize_memory + Write-Host $build_args + Invoke-WithColorCodedOutput { & $odin_compiler $build_args } } build-SokolBackendDemo diff --git a/vefontcache/LRU.odin b/vefontcache/LRU.odin index 3682ccf..20e18a3 100644 --- a/vefontcache/LRU.odin +++ b/vefontcache/LRU.odin @@ -1,22 +1,36 @@ package vefontcache -/* -The choice was made to keep the LRU cache implementation as close to the original as possible. +/* Note(Ed): + Original implementation has been changed moderately. + Notably the LRU is now type generic for its key value. + This was done to profile between using u64, u32, and u16. + + What ended up happening was using u32 for both the atlas and the shape cache + yielded a several ms save for processing thousands of draw text calls. + + There was an attempt at an optimization pass but the directives done here (other than force_inline) + are marginal changes at best. */ import "base:runtime" -Pool_ListIter :: i32 -Pool_ListValue :: u64 +// 16-bit hashing was attempted, however it seems to get collisions with djb8_hash_16 -Pool_List_Item :: struct { +LRU_Fail_Mask_16 :: 0xFFFF +LRU_Fail_Mask_32 :: 0xFFFFFFFF +LRU_Fail_Mask_64 :: 0xFFFFFFFFFFFFFFFF + +Pool_ListIter :: i32 + +Pool_List_Item :: struct( $V_Type : typeid ) #packed { +// Pool_List_Item :: struct( $V_Type : typeid ) { prev : Pool_ListIter, next : Pool_ListIter, - value : Pool_ListValue, + value : V_Type, } -Pool_List :: struct { - items : [dynamic]Pool_List_Item, +Pool_List :: struct( $V_Type : typeid) { + items : [dynamic]Pool_List_Item(V_Type), free_list : [dynamic]Pool_ListIter, front : Pool_ListIter, back : Pool_ListIter, @@ -25,10 +39,10 @@ Pool_List :: struct { dbg_name : string, } -pool_list_init :: proc( pool : ^Pool_List, capacity : i32, dbg_name : string = "" ) +pool_list_init :: proc( pool : ^Pool_List($V_Type), capacity : i32, dbg_name : string = "" ) { error : Allocator_Error - pool.items, error = make( [dynamic]Pool_List_Item, int(capacity) ) + pool.items, error = make( [dynamic]Pool_List_Item(V_Type), int(capacity) ) assert( error == .None, "VEFontCache.pool_list_init : Failed to allocate items array") resize( & pool.items, capacity ) @@ -39,130 +53,130 @@ pool_list_init :: proc( pool : ^Pool_List, capacity : i32, dbg_name : string = " pool.capacity = capacity pool.dbg_name = dbg_name - using pool - for id in 0 ..< capacity { - free_list[id] = i32(id) - items[id] = { + for id in 0 ..< pool.capacity { + pool.free_list[id] = Pool_ListIter(id) + pool.items[id] = { prev = -1, next = -1, } } - front = -1 - back = -1 + pool.front = -1 + pool.back = -1 } -pool_list_free :: proc( pool : ^Pool_List ) { +pool_list_free :: proc( pool : ^Pool_List($V_Type) ) { delete( pool.items) delete( pool.free_list) } -pool_list_reload :: proc( pool : ^Pool_List, allocator : Allocator ) { +pool_list_reload :: proc( pool : ^Pool_List($V_Type), allocator : Allocator ) { reload_array( & pool.items, allocator ) reload_array( & pool.free_list, allocator ) } -pool_list_clear :: proc( pool: ^Pool_List ) { - using pool - clear(& items) - clear(& free_list) +pool_list_clear :: proc( pool: ^Pool_List($V_Type) ) +{ + clear(& pool.items) + clear(& pool.free_list) resize( & pool.items, cap(pool.items) ) resize( & pool.free_list, cap(pool.free_list) ) - for id in 0 ..< capacity { - free_list[id] = i32(id) - items[id] = { + for id in 0 ..< pool.capacity { + pool.free_list[id] = Pool_ListIter(id) + pool.items[id] = { prev = -1, next = -1, } } - front = -1 - back = -1 - size = 0 + pool.front = -1 + pool.back = -1 + pool.size = 0 } -pool_list_push_front :: proc( pool : ^Pool_List, value : Pool_ListValue ) +@(optimization_mode="favor_size") +pool_list_push_front :: proc( pool : ^Pool_List($V_Type), value : V_Type ) #no_bounds_check { - using pool - if size >= capacity do return + if pool.size >= pool.capacity do return - length := len(free_list) + length := len(pool.free_list) assert( length > 0 ) - assert( length == int(capacity - size) ) + assert( length == int(pool.capacity - pool.size) ) - id := free_list[ len(free_list) - 1 ] + id := pool.free_list[ len(pool.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 + pop( & pool.free_list ) + pool.items[ id ].prev = -1 + pool.items[ id ].next = pool.front + pool.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 + if pool.front != -1 do pool.items[ pool.front ].prev = id + if pool.back == -1 do pool.back = id + pool.front = id + pool.size += 1 } -pool_list_erase :: proc( pool : ^Pool_List, iter : Pool_ListIter ) +@(optimization_mode="favor_size") +pool_list_erase :: proc( pool : ^Pool_List($V_Type), iter : Pool_ListIter ) #no_bounds_check { - using pool - if size <= 0 do return - assert( iter >= 0 && iter < i32(capacity) ) - assert( len(free_list) == int(capacity - size) ) + if pool.size <= 0 do return + assert( iter >= 0 && iter < Pool_ListIter(pool.capacity) ) + assert( len(pool.free_list) == int(pool.capacity - pool.size) ) - iter_node := & items[ iter ] + iter_node := & pool.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 iter_node.prev != -1 do pool.items[ prev ].next = iter_node.next + if iter_node.next != -1 do pool.items[ next ].prev = iter_node.prev - if front == iter do front = iter_node.next - if back == iter do back = iter_node.prev + if pool.front == iter do pool.front = iter_node.next + if pool.back == iter do pool.back = iter_node.prev iter_node.prev = -1 iter_node.next = -1 iter_node.value = 0 - append( & free_list, iter ) + append( & pool.free_list, iter ) - size -= 1 - if size == 0 { - back = -1 - front = -1 + pool.size -= 1 + if pool.size == 0 { + pool.back = -1 + pool.front = -1 } } -pool_list_move_to_front :: #force_inline proc( pool : ^Pool_List, iter : Pool_ListIter ) +@(optimization_mode="favor_size") +pool_list_move_to_front :: proc "contextless" ( pool : ^Pool_List($V_Type), iter : Pool_ListIter ) #no_bounds_check { - using pool + if pool.front == iter do return - if front == iter do return + item := & pool.items[iter] + if item.prev != -1 do pool.items[ item.prev ].next = item.next + if item.next != -1 do pool.items[ item.next ].prev = item.prev + if pool.back == iter do pool.back = item.prev - item := & items[iter] - if item.prev != -1 do items[ item.prev ].next = item.next - if item.next != -1 do items[ item.next ].prev = item.prev - if back == iter do back = item.prev - - item.prev = -1 - item.next = front - items[ front ].prev = iter - front = iter + item.prev = -1 + item.next = pool.front + pool.items[ pool.front ].prev = iter + pool.front = iter } -pool_list_peek_back :: #force_inline proc ( pool : ^Pool_List ) -> Pool_ListValue { +@(optimization_mode="favor_size") +pool_list_peek_back :: #force_inline proc ( pool : Pool_List($V_Type) ) -> V_Type #no_bounds_check { assert( pool.back != - 1 ) value := pool.items[ pool.back ].value return value } -pool_list_pop_back :: #force_inline proc( pool : ^Pool_List ) -> Pool_ListValue { +@(optimization_mode="favor_size") +pool_list_pop_back :: #force_inline proc( pool : ^Pool_List($V_Type) ) -> V_Type #no_bounds_check { if pool.size <= 0 do return 0 assert( pool.back != -1 ) @@ -171,69 +185,69 @@ pool_list_pop_back :: #force_inline proc( pool : ^Pool_List ) -> Pool_ListValue return value } -LRU_Link :: struct { - pad_top : u64, - +LRU_Link :: struct #packed { value : i32, ptr : Pool_ListIter, - - pad_bottom : u64, } -LRU_Cache :: struct { +LRU_Cache :: struct( $Key_Type : typeid ) { capacity : i32, num : i32, - table : map[u64]LRU_Link, - key_queue : Pool_List, + table : map[Key_Type]LRU_Link, + key_queue : Pool_List(Key_Type), } -lru_init :: proc( cache : ^LRU_Cache, capacity : i32, dbg_name : string = "" ) { +lru_init :: proc( cache : ^LRU_Cache($Key_Type), capacity : i32, dbg_name : string = "" ) { error : Allocator_Error cache.capacity = capacity - cache.table, error = make( map[u64]LRU_Link, uint(capacity) ) + cache.table, error = make( map[Key_Type]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 ) { +lru_free :: proc( cache : ^LRU_Cache($Key_Type) ) { pool_list_free( & cache.key_queue ) delete( cache.table ) } -lru_reload :: #force_inline proc( cache : ^LRU_Cache, allocator : Allocator ) { +lru_reload :: #force_inline proc( cache : ^LRU_Cache($Key_Type), allocator : Allocator ) { reload_map( & cache.table, allocator ) pool_list_reload( & cache.key_queue, allocator ) } -lru_clear :: proc ( cache : ^LRU_Cache ) { +lru_clear :: proc ( cache : ^LRU_Cache($Key_Type) ) { pool_list_clear( & cache.key_queue ) clear(& cache.table) cache.num = 0 } -lru_find :: #force_inline proc "contextless" ( cache : ^LRU_Cache, key : u64, must_find := false ) -> (LRU_Link, bool) { +@(optimization_mode="favor_size") +lru_find :: #force_inline proc "contextless" ( cache : LRU_Cache($Key_Type), key : Key_Type, must_find := false ) -> (LRU_Link, bool) #no_bounds_check { link, success := cache.table[key] return link, success } -lru_get :: #force_inline proc( cache: ^LRU_Cache, key : u64 ) -> i32 { +@(optimization_mode="favor_size") +lru_get :: #force_inline proc ( cache: ^LRU_Cache($Key_Type), key : Key_Type ) -> i32 #no_bounds_check { if link, ok := &cache.table[ key ]; ok { - pool_list_move_to_front(&cache.key_queue, link.ptr) - return link.value + pool_list_move_to_front(&cache.key_queue, link.ptr) + return link.value } return -1 } -lru_get_next_evicted :: #force_inline proc ( cache : ^LRU_Cache ) -> u64 { +@(optimization_mode="favor_size") +lru_get_next_evicted :: #force_inline proc ( cache : LRU_Cache($Key_Type) ) -> Key_Type #no_bounds_check { if cache.key_queue.size >= cache.capacity { - evict := pool_list_peek_back( & cache.key_queue ) + evict := pool_list_peek_back( cache.key_queue ) return evict } - return 0xFFFFFFFFFFFFFFFF + return ~Key_Type(0) } -lru_peek :: #force_inline proc ( cache : ^LRU_Cache, key : u64, must_find := false ) -> i32 { +@(optimization_mode="favor_size") +lru_peek :: #force_inline proc "contextless" ( cache : LRU_Cache($Key_Type), key : Key_Type, must_find := false ) -> i32 #no_bounds_check { iter, success := lru_find( cache, key, must_find ) if success == false { return -1 @@ -241,8 +255,10 @@ lru_peek :: #force_inline proc ( cache : ^LRU_Cache, key : u64, must_find := fal return iter.value } -lru_put :: #force_inline proc( cache : ^LRU_Cache, key : u64, value : i32 ) -> u64 +@(optimization_mode="favor_size") +lru_put :: proc( cache : ^LRU_Cache($Key_Type), key : Key_Type, value : i32 ) -> Key_Type #no_bounds_check { + // profile(#procedure) if link, ok := & cache.table[ key ]; ok { pool_list_move_to_front( & cache.key_queue, link.ptr ) link.value = value @@ -265,8 +281,8 @@ lru_put :: #force_inline proc( cache : ^LRU_Cache, key : u64, value : i32 ) -> u return evict } -lru_refresh :: proc( cache : ^LRU_Cache, key : u64 ) { - link, success := lru_find( cache, key ) +lru_refresh :: proc( cache : ^LRU_Cache($Key_Type), key : Key_Type ) { + 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/vefontcache/atlas.odin b/vefontcache/atlas.odin index 33a2ceb..ddb95ac 100644 --- a/vefontcache/atlas.odin +++ b/vefontcache/atlas.odin @@ -1,127 +1,127 @@ package vefontcache +// There are only 4 actual regions of the atlas. E represents the atlas_decide_region detecting an oversized glyph. +// Note(Ed): None should never really occur anymore. So its safe to most likely add an assert when its detected. Atlas_Region_Kind :: enum u8 { None = 0x00, - A = 0x41, - B = 0x42, - C = 0x43, - D = 0x44, - E = 0x45, + A = 0x01, + B = 0x02, + C = 0x03, + D = 0x04, + E = 0x05, Ignore = 0xFF, // ve_fontcache_cache_glyph_to_atlas uses a -1 value in clear draw call } -Atlas_Region :: struct { - state : LRU_Cache, +// Note(Ed): Using 16 bit hash had collision failures and no observable performance improvement (tried several 16-bit hashers) +Atlas_Key :: u32 - width : i32, - height : i32, +// TODO(Ed) It might perform better with a tailored made hashtable implementation for the LRU_Cache or dedicated array struct/procs for the Atlas. +/* Essentially a sub-atlas of the atlas. There is a state cache per region that tracks the glyph inventory (what slot they occupy). + Unlike the shape cache this one's fixed capacity (natrually) and the next avail slot is tracked. +*/ +Atlas_Region :: struct { + state : LRU_Cache(Atlas_Key), size : Vec2i, capacity : Vec2i, offset : Vec2i, + slot_size : Vec2i, + next_idx : i32, } +/* There are four regions each succeeding region holds larger sized slots. + The generator pipeline for draw lists utilizes the regions array for info lookup. + + Note(Ed): + Padding can techncially be larger than 1, however recently I haven't had any artififact issues... + size_multiplier usage isn't fully resolved. Intent was to further setup over_sampling or just having + a more massive cache for content that used more than the usual common glyphs. +*/ Atlas :: struct { - width : i32, - height : i32, - - glyph_padding : i32, // Padding to add to bounds__scaled for choosing which atlas region. - glyph_over_scalar : f32, // Scalar to apply to bounds__scaled for choosing which atlas region. - region_a : Atlas_Region, region_b : Atlas_Region, region_c : Atlas_Region, region_d : Atlas_Region, + + regions : [5] ^Atlas_Region, + + glyph_padding : f32, // Padding to add to bounds__scaled for choosing which atlas region. + size_multiplier : f32, // Grows all text by this multiple. + + size : Vec2i, } -atlas_bbox :: proc( atlas : ^Atlas, region : Atlas_Region_Kind, 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 ) * atlas.region_a.width) - position.y = cast(f32) (( local_idx / atlas.region_a.capacity.x ) * 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 ) * atlas.region_b.width) - position.y = cast(f32) (( local_idx / atlas.region_b.capacity.x ) * 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 ) * atlas.region_c.width) - position.y = cast(f32) (( local_idx / atlas.region_c.capacity.x ) * 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 ) * atlas.region_d.width) - position.y = cast(f32) (( local_idx / atlas.region_d.capacity.x ) * atlas.region_d.height) - - position.x += f32(atlas.region_d.offset.x) - position.y += f32(atlas.region_d.offset.y) - - case .Ignore, .None, .E: - } +// Hahser for the atlas. +@(optimization_mode="favor_size") +atlas_glyph_lru_code :: #force_inline proc "contextless" ( font : Font_ID, px_size : f32, glyph_index : Glyph ) -> (lru_code : Atlas_Key) { + // lru_code = u32(glyph_index) + ( ( 0x10000 * u32(font) ) & 0xFFFF0000 ) + font := font + glyph_index := glyph_index + px_size := px_size + djb8_hash( & lru_code, to_bytes( & font) ) + djb8_hash( & lru_code, to_bytes( & glyph_index ) ) + djb8_hash( & lru_code, to_bytes( & px_size ) ) return } -decide_codepoint_region :: proc(ctx : ^Context, entry : ^Entry, glyph_index : Glyph ) -> (region_kind : Atlas_Region_Kind, region : ^Atlas_Region, over_sample : Vec2) +@(optimization_mode="favor_size") +atlas_region_bbox :: #force_inline proc( region : Atlas_Region, local_idx : i32 ) -> (position, size: Vec2) { - if parser_is_glyph_empty(&entry.parser_info, glyph_index) { - return .None, nil, {} - } + size = vec2(region.slot_size) - 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) + position.x = cast(f32) (( local_idx % region.capacity.x ) * region.slot_size.x) + position.y = cast(f32) (( local_idx / region.capacity.x ) * region.slot_size.y) - atlas := & ctx.atlas - glyph_buffer := & ctx.glyph_buffer - glyph_padding := f32( atlas.glyph_padding ) * 2 - - bounds_width_scaled := i32(bounds_width * entry.size_scale * atlas.glyph_over_scalar + glyph_padding) - bounds_height_scaled := i32(bounds_height * entry.size_scale * atlas.glyph_over_scalar + glyph_padding) - - // Use a lookup table for faster region selection - region_lookup := [4]struct { kind: Atlas_Region_Kind, region: ^Atlas_Region } { - { .A, & atlas.region_a }, - { .B, & atlas.region_b }, - { .C, & atlas.region_c }, - { .D, & atlas.region_d }, - } - - for region in region_lookup do if bounds_width_scaled <= region.region.width && bounds_height_scaled <= region.region.height { - return region.kind, region.region, glyph_buffer.over_sample - } - - if bounds_width_scaled <= glyph_buffer.width \ - && bounds_height_scaled <= glyph_buffer.height { - over_sample = \ - bounds_width_scaled <= glyph_buffer.width / 2 && - bounds_height_scaled <= glyph_buffer.height / 2 ? \ - {2.0, 2.0} \ - : {1.0, 1.0} - return .E, nil, over_sample - } - return .None, nil, {} + position.x += f32(region.offset.x) + position.y += f32(region.offset.y) + return +} + +@(optimization_mode="favor_size") +atlas_decide_region :: #force_inline proc "contextless" (atlas : Atlas, glyph_buffer_size : Vec2, bounds_size_scaled : Vec2 ) -> (region_kind : Atlas_Region_Kind) +{ + // profile(#procedure) + glyph_padding_dbl := atlas.glyph_padding * 2 + padded_bounds := bounds_size_scaled + glyph_padding_dbl + + for kind in 1 ..= 4 do if + padded_bounds.x <= f32(atlas.regions[kind].slot_size.x) && + padded_bounds.y <= f32(atlas.regions[kind].slot_size.y) + { + return cast(Atlas_Region_Kind) kind + } + + if padded_bounds.x <= glyph_buffer_size.x && padded_bounds.y <= glyph_buffer_size.y{ + return .E + } + return .None +} + +// Grab an atlas LRU cache slot. +@(optimization_mode="favor_size") +atlas_reserve_slot :: #force_inline proc ( region : ^Atlas_Region, lru_code : Atlas_Key ) -> (atlas_index : i32) +{ + if region.next_idx < region.state.capacity + { + evicted := lru_put( & region.state, lru_code, region.next_idx ) + atlas_index = 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 != LRU_Fail_Mask_16) + + 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 ) + return } diff --git a/vefontcache/draw.odin b/vefontcache/draw.odin index 0a75744..69a1888 100644 --- a/vefontcache/draw.odin +++ b/vefontcache/draw.odin @@ -1,20 +1,63 @@ package vefontcache -import "thirdparty:freetype" +/* + 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 "base:intrinsics" import "core:slice" +import "thirdparty:freetype" + +Glyph_Trianglation_Method :: enum(i32) { + Ear_Clipping, + Triangle_Fanning, +} Vertex :: struct { pos : Vec2, u, v : f32, } +Glyph_Bounds_Mat :: matrix[2, 2] f32 + +Glyph_Draw_Quad :: struct { + dst_pos : Vec2, + dst_scale : Vec2, + src_pos : Vec2, + src_scale : Vec2, +} + +// This is used by generate_shape_draw_list & batch_generate_glyphs_draw_list +// to track relevant glyph data in soa format for pipelined processing +Glyph_Pack_Entry :: struct #packed { + position : Vec2, + + atlas_index : i32, + in_atlas : b8, + should_cache : b8, + region_pos : Vec2, + region_size : Vec2, + + over_sample : Vec2, // Only used for oversized glyphs + + shape : Parser_Glyph_Shape, + draw_transform : Transform, + + draw_quad : Glyph_Draw_Quad, + draw_atlas_quad : Glyph_Draw_Quad, + draw_quad_clear : Glyph_Draw_Quad, + buffer_x : f32, + flush_glyph_buffer : b8, +} + Draw_Call :: struct { pass : Frame_Buffer_Pass, start_index : u32, end_index : u32, clear_before_draw : b32, region : Atlas_Region_Kind, - colour : Colour, + colour : RGBAN, } Draw_Call_Default :: Draw_Call { @@ -32,32 +75,50 @@ Draw_List :: struct { calls : [dynamic]Draw_Call, } -// TODO(Ed): This was a rough translation of the raw values the orignal was using, need to give better names... Frame_Buffer_Pass :: enum u32 { None = 0, - Glyph = 1, - Atlas = 2, - Target = 3, - Target_Uncached = 4, + Glyph = 1, // Operations on glyph buffer render target + Atlas = 2, // Operations on atlas render target + Target = 3, // Operations on user's end-destination render target using atlas + Target_Uncached = 4, // Operations on user's end-destination render target using glyph buffer } -Glyph_Draw_Buffer :: struct { - over_sample : Vec2, - batch : i32, - width : i32, - height : i32, - draw_padding : i32, +Glyph_Batch_Cache :: struct { + table : map[Atlas_Key]b8, + num : i32, + cap : i32, +} - batch_x : i32, +// The general tracker for a generator pipeline +Glyph_Draw_Buffer :: struct{ + over_sample : Vec2, + size : Vec2i, + draw_padding : f32, + snap_glyph_height : f32, + + allocated_x : i32, // Space used (horizontally) within the glyph buffer clear_draw_list : Draw_List, draw_list : Draw_List, + + batch_cache : Glyph_Batch_Cache, + shape_gen_scratch : [dynamic]Vertex, // Used during triangulating a glyph into a mesh. + + glyph_pack : #soa[dynamic]Glyph_Pack_Entry, + oversized : [dynamic]i32, + to_cache : [dynamic]i32, + cached : [dynamic]i32, } -blit_quad :: proc( draw_list : ^Draw_List, p0 : Vec2 = {0, 0}, p1 : Vec2 = {1, 1}, uv0 : Vec2 = {0, 0}, uv1 : Vec2 = {1, 1} ) +// Contructs a quad mesh for bliting a texture from one render target (src uv0 & 1) to the destination rendertarget (p0, p1) +@(optimization_mode="favor_size") +blit_quad :: #force_inline proc ( draw_list : ^Draw_List, + 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) quadv : [4]Vertex = { @@ -88,501 +149,19 @@ blit_quad :: proc( draw_list : ^Draw_List, p0 : Vec2 = {0, 0}, p1 : Vec2 = {1, 1 return } -// TODO(Ed): glyph caching cannot be handled in a 'font parser' abstraction. Just going to have explicit procedures to grab info neatly... -cache_glyph_freetype :: proc(ctx: ^Context, font: Font_ID, glyph_index: Glyph, entry: ^Entry, bounds_0, bounds_1: Vec2, scale, translate: Vec2) -> b32 -{ - draw_filled_path_freetype :: proc( draw_list : ^Draw_List, outside_point : Vec2, path : []Vertex, - 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.pos * scale + translate - logf(" %0.2f %0.2f", vec.x, vec.y ) - } - } - - v_offset := cast(u32) len(draw_list.vertices) - for point in path - { - transformed_point := Vertex { - pos = point.pos * scale + translate, - u = 0, - v = 0 - } - append( & draw_list.vertices, transformed_point ) - } - - if len(path) > 2 - { - indices := & draw_list.indices - for index : u32 = 1; index < cast(u32) len(path) - 1; index += 1 { - to_add := [3]u32 { - v_offset, - v_offset + index, - v_offset + index + 1 - } - append( indices, ..to_add[:] ) - } - - // Close the path by connecting the last vertex to the first two - to_add := [3]u32 { - v_offset, - v_offset + cast(u32)(len(path) - 1), - v_offset + 1 - } - append( indices, ..to_add[:] ) - } - } - - if glyph_index == Glyph(0) { - return false - } - - face := entry.parser_info.freetype_info - error := freetype.load_glyph(face, u32(glyph_index), {.No_Bitmap, .No_Scale}) - if error != .Ok { - return false - } - - glyph := face.glyph - if glyph.format != .Outline { - return false - } - - outline := &glyph.outline - if outline.n_points == 0 { - return false - } - - draw := Draw_Call_Default - draw.pass = Frame_Buffer_Pass.Glyph - draw.start_index = cast(u32) len(ctx.draw_list.indices) - - contours := slice.from_ptr(cast( [^]i16) outline.contours, int(outline.n_contours)) - points := slice.from_ptr(cast( [^]freetype.Vector) outline.points, int(outline.n_points)) - tags := slice.from_ptr(cast( [^]u8) outline.tags, int(outline.n_points)) - - path := &ctx.temp_path - clear(path) - - outside := Vec2{ bounds_0.x - 21, bounds_0.y - 33 } - - start_index: int = 0 - for contour_index in 0 ..< int(outline.n_contours) - { - end_index := int(contours[contour_index]) + 1 - prev_point : Vec2 - first_point : Vec2 - - for idx := start_index; idx < end_index; idx += 1 - { - current_pos := Vec2 { f32( points[idx].x ), f32( points[idx].y ) } - if ( tags[idx] & 1 ) == 0 - { - // If current point is off-curve - if (idx == start_index || (tags[ idx - 1 ] & 1) != 0) - { - // current is the first or following an on-curve point - prev_point = current_pos - } - else - { - // current and previous are off-curve, calculate midpoint - midpoint := (prev_point + current_pos) * 0.5 - append( path, Vertex { pos = midpoint } ) // Add midpoint as on-curve point - if idx < end_index - 1 - { - // perform interp from prev_point to current_pos via midpoint - step := 1.0 / entry.curve_quality - for alpha : f32 = 0.0; alpha <= 1.0; alpha += step - { - bezier_point := eval_point_on_bezier3( prev_point, midpoint, current_pos, alpha ) - append( path, Vertex{ pos = bezier_point } ) - } - } - - prev_point = current_pos - } - } - else - { - if idx == start_index { - first_point = current_pos - } - if prev_point != (Vec2{}) { - // there was an off-curve point before this - append(path, Vertex{ pos = prev_point}) // Ensure previous off-curve is handled - } - append(path, Vertex{ pos = current_pos}) - prev_point = {} - } - } - - // ensure the contour is closed - if path[0].pos != path[ len(path) - 1 ].pos { - append(path, Vertex{pos = path[0].pos}) - } - draw_filled_path(&ctx.draw_list, bounds_0, path[:], scale, translate, ctx.debug_print_verbose) - clear(path) - start_index = end_index - } - - if len(path) > 0 { - draw_filled_path(&ctx.draw_list, outside, path[:], scale, translate, ctx.debug_print_verbose) - } - - draw.end_index = cast(u32) len(ctx.draw_list.indices) - if draw.end_index > draw.start_index { - append( & ctx.draw_list.calls, draw) - } - - return true -} - -// TODO(Ed): Is it better to cache the glyph vertices for when it must be re-drawn (directly or two atlas)? -cache_glyph :: proc(ctx : ^Context, font : Font_ID, glyph_index : Glyph, entry : ^Entry, bounds_0, bounds_1 : Vec2, scale, translate : Vec2) -> b32 +// Constructs a triangle fan mesh to fill a shape using the provided path outside_point represents the center point of the fan. +@(optimization_mode="favor_size") +fill_path_via_fan_triangulation :: proc( draw_list : ^Draw_List, + outside_point : Vec2, + path : []Vertex, + scale := Vec2 { 1, 1 }, + translate := Vec2 { 0, 0 } +) #no_bounds_check { // profile(#procedure) - if glyph_index == Glyph(0) { - return false - } - - // Glyph shape handling are not abstractable between freetype and stb_truetype - if entry.parser_info.kind == .Freetype { - result := cache_glyph_freetype( ctx, font, glyph_index, entry, bounds_0, bounds_1, scale, translate ) - return result - } - - shape, error := parser_get_glyph_shape(&entry.parser_info, glyph_index) - assert(error == .None) - if len(shape) == 0 { - return false - } - - outside := Vec2{bounds_0.x - 21, bounds_0.y - 33} - - draw := Draw_Call_Default - draw.pass = Frame_Buffer_Pass.Glyph - draw.start_index = u32(len(ctx.draw_list.indices)) - - path := &ctx.temp_path - clear(path) - - step := 1.0 / entry.curve_quality - for edge in shape do #partial 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, Vertex { pos = Vec2 { f32(edge.x), f32(edge.y)} } ) - - case .Curve: - assert(len(path) > 0) - p0 := path[ len(path) - 1].pos - p1 := Vec2{ f32(edge.contour_x0), f32(edge.contour_y0) } - p2 := Vec2{ f32(edge.x), f32(edge.y) } - - for index : f32 = 1; index <= entry.curve_quality; index += 1 { - alpha := index * step - append( path, Vertex { pos = eval_point_on_bezier3(p0, p1, p2, alpha) } ) - } - - case .Cubic: - assert( len(path) > 0) - p0 := path[ len(path) - 1].pos - 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) } - - for index : f32 = 1; index <= entry.curve_quality; index += 1 { - alpha := index * step - append( path, Vertex { pos = eval_point_on_bezier4(p0, p1, p2, p3, alpha) } ) - } - } - - if len(path) > 0 { - draw_filled_path(&ctx.draw_list, outside, path[:], scale, translate, ctx.debug_print_verbose) - } - - draw.end_index = 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 : Font_ID, - glyph_index : Glyph, - lru_code : u64, - atlas_index : i32, - entry : ^Entry, - region_kind : Atlas_Region_Kind, - region : ^Atlas_Region, - 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 ) - vbounds_0 := vec2(bounds_0) - vbounds_1 := vec2(bounds_1) - bounds_size := vbounds_1 - vbounds_0 - - // 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 glyph render target (FBO) - glyph_draw_scale := over_sample * entry.size_scale - glyph_draw_translate := -1 * vbounds_0 * glyph_draw_scale + vec2( glyph_padding ) - - // Allocate a glyph glyph render target region (FBO) - gwidth_scaled_px := bounds_size.x * glyph_draw_scale.x + over_sample.x * glyph_padding + 1.0 - 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_size := 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_size) - 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 = (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 ) - - clear_target_region : Draw_Call - { - using clear_target_region - 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) - } - - blit_to_atlas : Draw_Call - { - using blit_to_atlas - pass = .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.clear_draw_list.calls, clear_target_region ) - append( & glyph_buffer.draw_list.calls, blit_to_atlas ) - - // Render glyph to glyph render target (FBO) - cache_glyph( ctx, font, glyph_index, entry, vbounds_0, vbounds_1, glyph_draw_scale, glyph_draw_translate ) -} - -// If the glyuph is found in the atlas, nothing occurs, otherwise, the glyph call is setup to catch it to the atlas -check_glyph_in_atlas :: #force_inline proc( ctx : ^Context, font : Font_ID, entry : ^Entry, glyph_index : Glyph, - lru_code : u64, - atlas_index : i32, - region_kind : Atlas_Region_Kind, - region : ^Atlas_Region, - 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 > 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_Draw_List -clear_draw_list :: #force_inline proc ( draw_list : ^Draw_List ) { - 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) - glyph_dst_size := glyph_size + bounds_scaled - glyph_size += bounds_scaled * over_sample - - // Figure out the destination rect. - bounds_0_scaled := (bounds_0 * entry.size_scale) - 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 Draw_Call. - calls : [2]Draw_Call - - draw_to_target := & calls[0] - { - using draw_to_target - 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)) - } - - clear_glyph_update := & calls[1] - { - // Clear glyph render target (FBO) - clear_glyph_update.pass = .Glyph - clear_glyph_update.start_index = 0 - clear_glyph_update.end_index = 0 - clear_glyph_update.clear_before_draw = true - } - append( & ctx.draw_list.calls, ..calls[:] ) -} - -// 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 Draw_Call; caller is responsible for actually appending the Draw_Call. -// ve_fontcache_draw_filled_path -draw_filled_path :: proc( draw_list : ^Draw_List, outside_point : Vec2, path : []Vertex, - 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.pos * scale + translate - logf(" %0.2f %0.2f", vec.x, vec.y ) - } - } - v_offset := cast(u32) len(draw_list.vertices) for point in path { - point := point + point := point point.pos = point.pos * scale + translate append( & draw_list.vertices, point ) } @@ -608,134 +187,612 @@ draw_filled_path :: proc( draw_list : ^Draw_List, outside_point : Vec2, path : [ } } -draw_text_batch :: proc(ctx: ^Context, entry: ^Entry, shaped: ^Shaped_Text, - batch_start_idx, batch_end_idx : i32, - position, scale : Vec2, - snap_width, snap_height : f32 ) +// Glyph triangulation generator +@(optimization_mode="favor_size") +generate_glyph_pass_draw_list :: proc(draw_list : ^Draw_List, path : ^[dynamic]Vertex, + glyph_shape : Parser_Glyph_Shape, + curve_quality : f32, + bounds : Range2, + translate, scale : Vec2 +) #no_bounds_check { - flush_glyph_buffer_to_atlas(ctx) + profile(#procedure) + outside := Vec2{bounds.p0.x - 21, bounds.p0.y - 33} - atlas := & ctx.atlas - atlas_size := Vec2{ f32(atlas.width), f32(atlas.height) } - glyph_padding := f32(atlas.glyph_padding) + draw := Draw_Call_Default + draw.pass = Frame_Buffer_Pass.Glyph + draw.start_index = u32(len(draw_list.indices)) - for index := batch_start_idx; index < batch_end_idx; index += 1 + clear(path) + + step := 1.0 / curve_quality + for edge, index in glyph_shape do #partial switch edge.type { - glyph_index := shaped.glyphs[index] + case .Move: + if len(path) > 0 { + fill_path_via_fan_triangulation( draw_list, outside, path[:], scale, translate) + clear(path) + } + fallthrough - if glyph_index == 0 || parser_is_glyph_empty( & entry.parser_info, glyph_index) do continue + case .Line: + append( path, Vertex { pos = Vec2 { f32(edge.x), f32(edge.y)} } ) - region_kind, region, over_sample := decide_codepoint_region( ctx, entry, glyph_index ) - lru_code := font_glyph_lru_code( entry.id, glyph_index ) - atlas_index := region_kind != .E ? lru_get( & region.state, lru_code ) : -1 - bounds_0, bounds_1 := parser_get_glyph_box( & entry.parser_info, glyph_index ) - vbounds_0 := vec2(bounds_0) - vbounds_1 := vec2(bounds_1) - bounds_size := vbounds_1 - vbounds_0 + case .Curve: + assert(len(path) > 0) + p0 := path[ len(path) - 1].pos + p1 := Vec2{ f32(edge.contour_x0), f32(edge.contour_y0) } + p2 := Vec2{ f32(edge.x), f32(edge.y) } - shaped_position := shaped.positions[index] - glyph_translate := position + (shaped_position) * scale + for index : f32 = 1; index <= curve_quality; index += 1 { + alpha := index * step + append( path, Vertex { pos = eval_point_on_bezier3(p0, p1, p2, alpha) } ) + } - if region_kind == .E - { - directly_draw_massive_glyph(ctx, entry, glyph_index, - vbounds_0, vbounds_1, - bounds_size, - over_sample, glyph_translate, scale ) - } - else if atlas_index != -1 - { - // Draw cached glyph - slot_position, _ := atlas_bbox( atlas, region_kind, atlas_index ) - glyph_scale := bounds_size * entry.size_scale + glyph_padding - bounds_0_scaled := ceil(vbounds_0 * entry.size_scale - 0.5 ) - dst := glyph_translate + bounds_0_scaled * scale - dst_scale := glyph_scale * scale - textspace_x_form( & slot_position, & glyph_scale, atlas_size ) + case .Cubic: + assert( len(path) > 0) + p0 := path[ len(path) - 1].pos + 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) } - call := Draw_Call_Default - call.pass = .Target - call.colour = ctx.colour - call.start_index = u32(len(ctx.draw_list.indices)) + for index : f32 = 1; index <= curve_quality; index += 1 { + alpha := index * step + append( path, Vertex { pos = eval_point_on_bezier4(p0, p1, p2, p3, alpha) } ) + } + } - blit_quad(&ctx.draw_list, - dst, dst + dst_scale, - slot_position, slot_position + glyph_scale ) + if len(path) > 0 { + fill_path_via_fan_triangulation(draw_list, outside, path[:], scale, translate) + } - call.end_index = u32(len(ctx.draw_list.indices)) - append(&ctx.draw_list.calls, call) - } + draw.end_index = u32(len(draw_list.indices)) + if draw.end_index > draw.start_index { + append( & draw_list.calls, draw) } } -// 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 : Font_ID, - entry : ^Entry, - shaped : ^Shaped_Text, - position, scale : Vec2, - snap_width, snap_height : f32 -) -> (cursor_pos : Vec2) +// Just a warpper of generate_shape_draw_list for handling an array of shapes +generate_shapes_draw_list :: #force_inline proc ( ctx : ^Context, + font : Font_ID, + colour : RGBAN, + entry : Entry, + px_size : f32, + font_scale : f32, + position : Vec2, + scale : Vec2, + shapes : []Shaped_Text +) { - // 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 + assert(len(shapes) > 0) + for shape in shapes { + ctx.cursor_pos = {} + ctx.cursor_pos = generate_shape_draw_list( & ctx.draw_list, shape, & ctx.atlas, & ctx.glyph_buffer, ctx.px_scalar, + colour, + entry, + px_size, + font_scale, + position, + scale, + ) + } +} - 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 +/* Generator pipeline for shapes - if region_kind != .E do atlas_index = lru_get( & region.state, lru_code ) - if check_glyph_in_atlas( ctx, font, entry, glyph_index, lru_code, atlas_index, region_kind, region, over_sample ) do continue + If you'd like to make a custom draw procedure, this can either be used directly or + modified to create an augmented derivative for a specific code path. - // We can no longer directly append the shape as it has missing glyphs in the atlas + This procedure has no awareness of layers. That should be handled by a higher-order codepath. + For this level of codepaths what matters is maximizing memory locality for: + * Dealing with shaping (essentially minimizing having to ever deal with it in a hot path if possible) + * Dealing with atlas regioning (the expensive region resolution & parser calls are done on the shape pass) - // 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 ) + Pipleine order: + * Resolve the glyph's position offset from the target position + * Segregate the glyphs into three slices: oversized, to_cache, cached. + * If oversized is not necessary for your use case and your hitting a bottleneck, omit it with setting ENABLE_OVERSIZED_GLYPHS to false. + * You have to to be drawing a px font size > ~140 px for it to trigger. + * The atlas can be scaled with the size_multiplier parameter of startup so that it becomes more irrelevant if processing a larger atlas is a non-issue. + * The segregation will not allow slices to exceed the batch_cache capacity of the glyph_buffer (configurable within startup params) + * When The capacity is reached batch_generate_glyphs_draw_list will be called which will do futher compute and then finally draw_list generation. + * This may perform better with smaller shapes vs larger shapes, but having more shapes has a cache lookup penatly so keep that in mind. +*/ +generate_shape_draw_list :: proc( draw_list : ^Draw_List, shape : Shaped_Text, + atlas : ^Atlas, + glyph_buffer : ^Glyph_Draw_Buffer, + px_scalar : f32, - 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 + colour : RGBAN, + entry : Entry, + px_size : f32, + font_scale : f32, + + target_position : Vec2, + target_scale : Vec2, +) -> (cursor_pos : Vec2) #no_bounds_check +{ + profile(#procedure) + + mark_glyph_seen :: #force_inline proc "contextless" ( cache : ^Glyph_Batch_Cache, lru_code : Atlas_Key ) { + cache.table[lru_code] = true + cache.num += 1 + } + reset_batch :: #force_inline proc( cache : ^Glyph_Batch_Cache ) { + clear_map( & cache.table ) + cache.num = 0 } - 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 ) + atlas_glyph_pad := atlas.glyph_padding + atlas_size := vec2(atlas.size) + glyph_buffer_size := vec2(glyph_buffer.size) - cursor_pos = position + shaped.end_cursor_pos * scale + // Make sure the packs are large enough for the shape + glyph_pack := & glyph_buffer.glyph_pack + oversized := & glyph_buffer.oversized + to_cache := & glyph_buffer.to_cache + cached := & glyph_buffer.cached + resize_soa_non_zero(glyph_pack, len(shape.glyph)) + + append_sub_pack :: #force_inline proc ( pack : ^[dynamic]i32, entry : i32 ) + { + raw := cast(^runtime.Raw_Dynamic_Array) pack + raw.len += 1 + pack[len(pack) - 1] = entry + } + sub_slice :: #force_inline proc "contextless" ( pack : ^[dynamic]i32) -> []i32 { return pack[:] } + + profile_begin("translate") + for & glyph, index in glyph_pack { + glyph.position = target_position + (shape.position[index]) * target_scale + } + profile_end() + + profile_begin("batching & segregating glyphs") + clear(oversized) + clear(to_cache) + clear(cached) + reset_batch( & glyph_buffer.batch_cache) + + for & glyph, index in glyph_pack + { + atlas_key := shape.atlas_lru_code[index] + region_kind := shape.region_kind[index] + bounds := shape.bounds[index] + bounds_size_scaled := size(bounds) * font_scale + + if region_kind == .None { + assert(false, "FAILED TO ASSGIN REGION") + continue + } + when ENABLE_OVERSIZED_GLYPHS + { + if region_kind == .E + { + glyph.over_sample = \ + bounds_size_scaled.x <= glyph_buffer_size.x / 2 && + bounds_size_scaled.y <= glyph_buffer_size.y / 2 ? \ + {2.0, 2.0} \ + : {1.0, 1.0} + append_sub_pack(oversized, cast(i32) index) + continue + } + } + + glyph.over_sample = glyph_buffer.over_sample + region := atlas.regions[region_kind] + glyph.atlas_index = lru_get( & region.state, atlas_key ) + + // Glyphs are prepared in batches based on the capacity of the batch cache. + Prepare_For_Batch: + { + pack := cached + + found_take_slow_path : b8 + success : bool + + // Determine if we hit the limit for this batch. + if glyph.atlas_index == - 1 + { + // Check to see if we reached capacity for the atlas + if region.next_idx > 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_glyph := lru_get_next_evicted( region.state ) + found_take_slow_path, success = glyph_buffer.batch_cache.table[next_evict_glyph] + assert(success != false) + // TODO(Ed): This might not be needed with the new pipeline/batching + if (found_take_slow_path) { + break Prepare_For_Batch + } + } + // profile_begin("glyph needs caching") + glyph.atlas_index = atlas_reserve_slot(region, atlas_key) + pack = to_cache + // profile_end() + } + // profile("append cached") + glyph.region_pos, glyph.region_size = atlas_region_bbox(region ^, glyph.atlas_index) + mark_glyph_seen(& glyph_buffer.batch_cache, atlas_key) + append_sub_pack(pack, cast(i32) index) + // TODO(Ed): This might not be needed with the new pipeline/batching + // if (found_take_slow_path) { + // break Prepare_For_Batch + // } + if glyph_buffer.batch_cache.num >= glyph_buffer.batch_cache.cap do break Prepare_For_Batch + continue + } + + // Batch has been prepared for a set of glyphs time to generate glyphs. + batch_generate_glyphs_draw_list( draw_list, shape, glyph_pack, sub_slice(cached), sub_slice(to_cache), sub_slice(oversized), + atlas, + glyph_buffer, + atlas_size, + glyph_buffer_size, + entry, + colour, + font_scale, + target_scale + ) + + reset_batch( & glyph_buffer.batch_cache) + clear(oversized) + clear(to_cache) + clear(cached) + } + profile_end() + + if len(oversized) > 0 || glyph_buffer.batch_cache.num > 0 + { + // Last batch pass + batch_generate_glyphs_draw_list( draw_list, shape, glyph_pack, sub_slice(cached), sub_slice(to_cache), sub_slice(oversized), + atlas, + glyph_buffer, + atlas_size, + glyph_buffer_size, + entry, + colour, + font_scale, + target_scale, + ) + } + + cursor_pos = target_position + shape.end_cursor_pos * target_scale return } -flush_glyph_buffer_to_atlas :: proc( ctx : ^Context ) -{ - // profile(#procedure) - // Flush Draw_Calls 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 ) +/* + The glyphs types have been segregated by this point into a batch slice of indices to the glyph_pack + The transform and draw quads are computed first (getting the math done in one spot as possible...) + Some of the math from to_cache pass for glyph generation was not moved over (it could be but I'm not sure its worth it...) - // Clear glyph render target (FBO) - if ctx.glyph_buffer.batch_x != 0 + Order: Oversized first, then to_cache, then cached. + + Oversized and to_cache will both enqueue operations for rendering glyphs to the glyph buffer render target. + The compute section will have operations reguarding how many glyphs they may alloate before a flush must occur. + A flush will force one of the following: + * Oversized will have a draw call setup to blit directly from the glyph buffer to the target. + * to_cache will blit the glyphs rendered to the buffer to the atlas. +*/ +@(optimization_mode = "favor_size") +batch_generate_glyphs_draw_list :: proc ( draw_list : ^Draw_List, + shape : Shaped_Text, + glyph_pack : ^#soa[dynamic]Glyph_Pack_Entry, + cached : []i32, + to_cache : []i32, + oversized : []i32, + + atlas : ^Atlas, + glyph_buffer : ^Glyph_Draw_Buffer, + atlas_size : Vec2, + glyph_buffer_size : Vec2, + + entry : Entry, + colour : RGBAN, + font_scale : Vec2, + target_scale : Vec2, +) #no_bounds_check +{ + profile(#procedure) + colour := colour + + profile_begin("glyph transform & draw quads compute") + for id, index in cached { - call := Draw_Call_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 + // Quad to for drawing atlas slot to target + glyph := & glyph_pack[id] + bounds := shape.bounds[id] + bounds_scaled := mul(bounds, font_scale) + glyph_scale := size(bounds_scaled) + atlas.glyph_padding + + quad := & glyph.draw_quad + quad.dst_pos = glyph.position + (bounds_scaled.p0) * target_scale + quad.dst_scale = (glyph_scale) * target_scale + quad.src_scale = (glyph_scale) + quad.src_pos = (glyph.region_pos) + to_target_space( & quad.src_pos, & quad.src_scale, atlas_size ) } + for id, index in to_cache + { + glyph := & glyph_pack[id] + bounds := shape.bounds[id] + bounds_scaled := mul(bounds, font_scale) + glyph_scale := size(bounds_scaled) + glyph_buffer.draw_padding + + f32_allocated_x := cast(f32) glyph_buffer.allocated_x + + // Resolve how much space this glyph will allocate in the buffer + buffer_size := glyph_scale * glyph_buffer.over_sample + // Allocate a glyph glyph render target region (FBO) + to_allocate_x := buffer_size.x + 4.0 + + // If allocation would exceed buffer's bounds the buffer must be flush before this glyph can be rendered. + glyph.flush_glyph_buffer = i32(f32_allocated_x + to_allocate_x) >= i32(glyph_buffer_size.x) + glyph.buffer_x = f32_allocated_x * f32( i32( ! glyph.flush_glyph_buffer ) ) + + // The glyph buffer space transform for generate_glyph_pass_draw_list + draw_transform := & glyph.draw_transform + draw_transform.scale = font_scale * glyph_buffer.over_sample + draw_transform.pos = -1 * (bounds.p0) * draw_transform.scale + glyph_buffer.draw_padding + draw_transform.pos.x += glyph.buffer_x + to_glyph_buffer_space( & draw_transform.pos, & draw_transform.scale, glyph_buffer_size ) + + // Allocate the space + glyph_buffer.allocated_x += i32(to_allocate_x) + + // Quad to for drawing atlas slot to target (used in generate_cached_draw_list) + draw_quad := & glyph.draw_quad + + // Destination (draw_list's target image) + draw_quad.dst_pos = glyph.position + (bounds_scaled.p0) * target_scale + draw_quad.dst_scale = (glyph_scale) * target_scale + + // UV Coordinates for sampling the atlas + draw_quad.src_scale = (glyph_scale) + draw_quad.src_pos = (glyph.region_pos) + to_target_space( & draw_quad.src_pos, & draw_quad.src_scale, atlas_size ) + } + when ENABLE_OVERSIZED_GLYPHS do for id, index in oversized + { + glyph_padding := vec2(glyph_buffer.draw_padding) + glyph := & glyph_pack[id] + bounds := shape.bounds[id] + bounds_scaled := mul(bounds, font_scale) + bounds_size_scaled := size(bounds_scaled) + + f32_allocated_x := cast(f32) glyph_buffer.allocated_x + // Resolve how much space this glyph will allocate in the buffer + buffer_size := (bounds_size_scaled + glyph_padding) * glyph.over_sample + + // Allocate a glyph glyph render target region (FBO) + to_allocate_x := buffer_size.x + 2.0 + glyph_buffer.allocated_x += i32(to_allocate_x) + + // If allocation would exceed buffer's bounds the buffer must be flush before this glyph can be rendered. + glyph.flush_glyph_buffer = i32(f32_allocated_x + to_allocate_x) >= i32(glyph_buffer_size.x) + glyph.buffer_x = f32_allocated_x * f32( i32( ! glyph.flush_glyph_buffer ) ) + + // Quad to for drawing atlas slot to target + draw_quad := & glyph.draw_quad + + // Target position (draw_list's target image) + draw_quad.dst_pos = glyph.position + (bounds_scaled.p0 - glyph_padding) * target_scale + draw_quad.dst_scale = (bounds_size_scaled + glyph_padding) * target_scale + + // The glyph buffer space transform for generate_glyph_pass_draw_list + draw_transform := & glyph.draw_transform + draw_transform.scale = font_scale * glyph.over_sample + draw_transform.pos = -1 * bounds.p0 * draw_transform.scale + vec2(atlas.glyph_padding) + draw_transform.pos.x += glyph.buffer_x + to_glyph_buffer_space( & draw_transform.pos, & draw_transform.scale, glyph_buffer_size ) + + + draw_quad.src_pos = Vec2 { glyph.buffer_x, 0 } + draw_quad.src_scale = bounds_size_scaled * glyph.over_sample + glyph_padding + to_target_space( & draw_quad.src_pos, & draw_quad.src_scale, glyph_buffer_size ) + } + profile_end() + + profile_begin("gen oversized glyphs draw_list") + when ENABLE_OVERSIZED_GLYPHS do if len(oversized) > 0 + { + when ENABLE_DRAW_TYPE_VISUALIZATION { + colour.r = 1.0 + colour.g = 1.0 + colour.b = 0.0 + } + for pack_id, index in oversized { + error : Allocator_Error + glyph_pack[pack_id].shape, error = parser_get_glyph_shape(entry.parser_info, shape.glyph[pack_id]) + assert(error == .None) + } + for id, index in oversized + { + glyph := & glyph_pack[id] + bounds := shape.bounds[id] + if glyph.flush_glyph_buffer do flush_glyph_buffer_draw_list(draw_list, + & glyph_buffer.draw_list, + & glyph_buffer.clear_draw_list, + & glyph_buffer.allocated_x + ) + + generate_glyph_pass_draw_list( draw_list, & glyph_buffer.shape_gen_scratch, + glyph_pack[id].shape, + entry.curve_quality, + bounds, + glyph_pack[id].draw_transform.pos, + glyph_pack[id].draw_transform.scale + ) + + target_quad := & glyph_pack[id].draw_quad + + draw_to_target : Draw_Call + { + draw_to_target.pass = .Target_Uncached + draw_to_target.colour = colour + draw_to_target.start_index = u32(len(draw_list.indices)) + + blit_quad( draw_list, + target_quad.dst_pos, target_quad.dst_pos + target_quad.dst_scale, + target_quad.src_pos, target_quad.src_pos + target_quad.src_scale ) + + draw_to_target.end_index = u32(len(draw_list.indices)) + } + append( & draw_list.calls, draw_to_target ) + } + + flush_glyph_buffer_draw_list(draw_list, & glyph_buffer.draw_list, & glyph_buffer.clear_draw_list, & glyph_buffer.allocated_x) + for id, index in oversized do parser_free_shape(entry.parser_info, glyph_pack[id].shape) + } + profile_end() + + @(optimization_mode = "favor_size") + generate_blit_from_atlas_draw_list :: #force_inline proc (draw_list : ^Draw_List, glyph_pack : #soa[]Glyph_Pack_Entry, sub_pack : []i32, colour : RGBAN ) + { + profile(#procedure) + call := Draw_Call_Default + call.pass = .Target + call.colour = colour + for id, index in sub_pack + { + // profile("glyph") + call.start_index = u32(len(draw_list.indices)) + + quad := glyph_pack[id].draw_quad + blit_quad(draw_list, + quad.dst_pos, quad.dst_pos + quad.dst_scale, + quad.src_pos, quad.src_pos + quad.src_scale + ) + call.end_index = u32(len(draw_list.indices)) + append(& draw_list.calls, call) + } + } + + profile_begin("to_cache: caching to atlas") + if len(to_cache) > 0 + { + for pack_id, index in to_cache { + error : Allocator_Error + glyph_pack[pack_id].shape, error = parser_get_glyph_shape(entry.parser_info, shape.glyph[pack_id]) + assert(error == .None) + } + + for id, index in to_cache + { + // profile("glyph") + glyph := & glyph_pack[id] + bounds := shape.bounds[id] + bounds_scaled := mul(bounds, font_scale) + bounds_size_scaled := size(bounds_scaled) + + if glyph.flush_glyph_buffer do flush_glyph_buffer_draw_list( draw_list, + & glyph_buffer.draw_list, + & glyph_buffer.clear_draw_list, + & glyph_buffer.allocated_x + ) + + dst_region_pos := glyph.region_pos + dst_region_size := glyph.region_size + to_glyph_buffer_space( & dst_region_pos, & dst_region_size, atlas_size ) + + clear_target_region : Draw_Call + clear_target_region.pass = .Atlas + clear_target_region.region = .Ignore + clear_target_region.start_index = cast(u32) len(glyph_buffer.clear_draw_list.indices) + blit_quad( & glyph_buffer.clear_draw_list, + dst_region_pos, dst_region_pos + dst_region_size, + { 1.0, 1.0 }, { 1.0, 1.0 } + ) + clear_target_region.end_index = cast(u32) len(glyph_buffer.clear_draw_list.indices) + + dst_glyph_pos := glyph.region_pos + dst_glyph_size := bounds_size_scaled + atlas.glyph_padding + dst_glyph_size.x = ceil(dst_glyph_size.x) + dst_glyph_size.y = max(dst_glyph_size.y, ceil(dst_glyph_size.y) * glyph_buffer.snap_glyph_height) // Note(Ed): Seems to improve hinting + to_glyph_buffer_space( & dst_glyph_pos, & dst_glyph_size, atlas_size ) + + src_position := Vec2 { glyph.buffer_x, 0 } + src_size := (bounds_size_scaled + atlas.glyph_padding) * glyph_buffer.over_sample + src_size.x = ceil(src_size.x) + src_size.y = max(src_size.y, ceil(src_size.y) * glyph_buffer.snap_glyph_height) // Note(Ed): Seems to improve hinting + to_target_space( & src_position, & src_size, glyph_buffer_size ) + + blit_to_atlas : Draw_Call + blit_to_atlas.pass = .Atlas + blit_to_atlas.region = .None + blit_to_atlas.start_index = cast(u32) len(glyph_buffer.draw_list.indices) + blit_quad( & glyph_buffer.draw_list, + dst_glyph_pos, dst_glyph_pos + dst_glyph_size, + src_position, src_position + src_size ) + blit_to_atlas.end_index = cast(u32) len(glyph_buffer.draw_list.indices) + + append( & glyph_buffer.clear_draw_list.calls, clear_target_region ) + append( & glyph_buffer.draw_list.calls, blit_to_atlas ) + + // Render glyph to glyph render target (FBO) + generate_glyph_pass_draw_list( draw_list, & glyph_buffer.shape_gen_scratch, + glyph.shape, + entry.curve_quality, + bounds, + glyph.draw_transform.pos, + glyph.draw_transform.scale + ) + } + + flush_glyph_buffer_draw_list(draw_list, & glyph_buffer.draw_list, & glyph_buffer.clear_draw_list, & glyph_buffer.allocated_x) + for id, index in to_cache do parser_free_shape(entry.parser_info, glyph_pack[id].shape) + + profile_begin("gen_cached_draw_list: to_cache") + when ENABLE_DRAW_TYPE_VISUALIZATION { + colour.r = 1.0 + colour.g = 0.0 + colour.b = 0.0 + } + generate_blit_from_atlas_draw_list( draw_list, glyph_pack[:], to_cache, colour ) + profile_end() + } + profile_end() + + profile_begin("gen_cached_draw_list: cached") + when ENABLE_DRAW_TYPE_VISUALIZATION { + colour.r = 0.5 + colour.g = 0.5 + colour.b = 0.5 + } + generate_blit_from_atlas_draw_list( draw_list, glyph_pack[:], cached, colour ) + profile_end() } -// ve_fontcache_merge_Draw_List -merge_draw_list :: proc( dst, src : ^Draw_List ) +// Flush the content of the glyph_buffers draw lists to the main draw list +flush_glyph_buffer_draw_list :: proc( #no_alias draw_list, glyph_buffer_draw_list, glyph_buffer_clear_draw_list : ^Draw_List, allocated_x : ^i32 ) { - // profile(#procedure) + profile(#procedure) + merge_draw_list( draw_list, glyph_buffer_clear_draw_list ) + merge_draw_list( draw_list, glyph_buffer_draw_list) + clear_draw_list( glyph_buffer_draw_list ) + clear_draw_list( glyph_buffer_clear_draw_list ) + + call := Draw_Call_Default + call.pass = .Glyph + call.start_index = 0 + call.end_index = 0 + call.clear_before_draw = true + append( & draw_list.calls, call ) + (allocated_x ^) = 0 +} + +@(optimization_mode="favor_size") +clear_draw_list :: #force_inline proc ( draw_list : ^Draw_List ) { + clear( & draw_list.calls ) + clear( & draw_list.indices ) + clear( & draw_list.vertices ) +} + +// Helper used by flush_glyph_buffer_draw_list. Used to append all the content from the src draw list o the destination. +@(optimization_mode="favor_size") +merge_draw_list :: proc ( #no_alias dst, src : ^Draw_List ) #no_bounds_check +{ + profile(#procedure) error : Allocator_Error v_offset := cast(u32) len( dst.vertices ) @@ -744,13 +801,13 @@ merge_draw_list :: proc( dst, src : ^Draw_List ) assert( error == .None ) i_offset := cast(u32) len(dst.indices) - for index : int = 0; index < len(src.indices); index += 1 { + for index : i32 = 0; index < cast(i32) 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 { + for index : i32 = 0; index < cast(i32) len(src.calls); index += 1 { src_call := src.calls[ index ] src_call.start_index += i_offset src_call.end_index += i_offset @@ -759,9 +816,11 @@ merge_draw_list :: proc( dst, src : ^Draw_List ) } } -optimize_draw_list :: proc(draw_list: ^Draw_List, call_offset: int) +// Naive implmentation to merge passes that are equivalent and the following to be merged (b for can_merge_draw_calls) doesn't have a clear todo. +// Its intended for optimiztion passes to occur on a per-layer basis. +optimize_draw_list :: proc (draw_list: ^Draw_List, call_offset: int) #no_bounds_check { - // profile(#procedure) + profile(#procedure) assert(draw_list != nil) can_merge_draw_calls :: #force_inline proc "contextless" ( a, b : ^Draw_Call ) -> bool { diff --git a/vefontcache/freetype_wip.odin b/vefontcache/freetype_wip.odin new file mode 100644 index 0000000..8458182 --- /dev/null +++ b/vefontcache/freetype_wip.odin @@ -0,0 +1,163 @@ +package vefontcache + +when false { +// TODO(Ed): Freetype support + +// TODO(Ed): glyph caching cannot be handled in a 'font parser' abstraction. Just going to have explicit procedures to grab info neatly... +cache_glyph_freetype :: proc(ctx: ^Context, font: Font_ID, glyph_index: Glyph, entry: ^Entry, bounds_0, bounds_1: Vec2, scale, translate: Vec2) -> b32 +{ + draw_filled_path_freetype :: proc( draw_list : ^Draw_List, outside_point : Vec2, path : []Vertex, + 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.pos * scale + translate + logf(" %0.2f %0.2f", vec.x, vec.y ) + } + } + + v_offset := cast(u32) len(draw_list.vertices) + for point in path + { + transformed_point := Vertex { + pos = point.pos * scale + translate, + u = 0, + v = 0 + } + append( & draw_list.vertices, transformed_point ) + } + + if len(path) > 2 + { + indices := & draw_list.indices + for index : u32 = 1; index < cast(u32) len(path) - 1; index += 1 { + to_add := [3]u32 { + v_offset, + v_offset + index, + v_offset + index + 1 + } + append( indices, ..to_add[:] ) + } + + // Close the path by connecting the last vertex to the first two + to_add := [3]u32 { + v_offset, + v_offset + cast(u32)(len(path) - 1), + v_offset + 1 + } + append( indices, ..to_add[:] ) + } + } + + if glyph_index == Glyph(0) { + return false + } + + face := entry.parser_info.freetype_info + error := freetype.load_glyph(face, u32(glyph_index), {.No_Bitmap, .No_Scale}) + if error != .Ok { + return false + } + + glyph := face.glyph + if glyph.format != .Outline { + return false + } + + outline := &glyph.outline + if outline.n_points == 0 { + return false + } + + draw := Draw_Call_Default + draw.pass = Frame_Buffer_Pass.Glyph + draw.start_index = cast(u32) len(ctx.draw_list.indices) + + contours := slice.from_ptr(cast( [^]i16) outline.contours, int(outline.n_contours)) + points := slice.from_ptr(cast( [^]freetype.Vector) outline.points, int(outline.n_points)) + tags := slice.from_ptr(cast( [^]u8) outline.tags, int(outline.n_points)) + + path := &ctx.temp_path + clear(path) + + outside := Vec2{ bounds_0.x - 21, bounds_0.y - 33 } + + start_index: int = 0 + for contour_index in 0 ..< int(outline.n_contours) + { + end_index := int(contours[contour_index]) + 1 + prev_point : Vec2 + first_point : Vec2 + + for idx := start_index; idx < end_index; idx += 1 + { + current_pos := Vec2 { f32( points[idx].x ), f32( points[idx].y ) } + if ( tags[idx] & 1 ) == 0 + { + // If current point is off-curve + if (idx == start_index || (tags[ idx - 1 ] & 1) != 0) + { + // current is the first or following an on-curve point + prev_point = current_pos + } + else + { + // current and previous are off-curve, calculate midpoint + midpoint := (prev_point + current_pos) * 0.5 + append( path, Vertex { pos = midpoint } ) // Add midpoint as on-curve point + if idx < end_index - 1 + { + // perform interp from prev_point to current_pos via midpoint + step := 1.0 / entry.curve_quality + for alpha : f32 = 0.0; alpha <= 1.0; alpha += step + { + bezier_point := eval_point_on_bezier3( prev_point, midpoint, current_pos, alpha ) + append( path, Vertex{ pos = bezier_point } ) + } + } + + prev_point = current_pos + } + } + else + { + if idx == start_index { + first_point = current_pos + } + if prev_point != (Vec2{}) { + // there was an off-curve point before this + append(path, Vertex{ pos = prev_point}) // Ensure previous off-curve is handled + } + append(path, Vertex{ pos = current_pos}) + prev_point = {} + } + } + + // ensure the contour is closed + if path[0].pos != path[ len(path) - 1 ].pos { + append(path, Vertex{pos = path[0].pos}) + } + draw_filled_path(&ctx.draw_list, bounds_0, path[:], scale, translate) + // draw_filled_path(&ctx.draw_list, bounds_0, path[:], scale, translate, ctx.debug_print_verbose) + clear(path) + start_index = end_index + } + + if len(path) > 0 { + // draw_filled_path(&ctx.draw_list, outside, path[:], scale, translate, ctx.debug_print_verbose) + draw_filled_path(&ctx.draw_list, outside, path[:], scale, translate) + } + + draw.end_index = cast(u32) len(ctx.draw_list.indices) + if draw.end_index > draw.start_index { + append( & ctx.draw_list.calls, draw) + } + + return true +} + +} diff --git a/vefontcache/misc.odin b/vefontcache/misc.odin index e34fbf0..7826400 100644 --- a/vefontcache/misc.odin +++ b/vefontcache/misc.odin @@ -1,16 +1,58 @@ package vefontcache +/* + Didn't want to splinter this into more files.. + Just a bunch of utilities. +*/ + import "base:runtime" import "core:simd" import "core:math" import core_log "core:log" -Colour :: [4]f32 +peek_array :: #force_inline proc "contextless" ( self : [dynamic]$Type ) -> Type { + return self[ len(self) - 1 ] +} + +reload_array :: #force_inline proc( self : ^[dynamic]$Type, allocator : Allocator ) { + raw := transmute( ^runtime.Raw_Dynamic_Array) self + raw.allocator = allocator +} + +reload_array_soa :: #force_inline proc( self : ^#soa[dynamic]$Type, allocator : Allocator ) { + raw := runtime.raw_soa_footer(self) + raw.allocator = allocator +} + +reload_map :: #force_inline proc( self : ^map [$KeyType] $EntryType, allocator : Allocator ) { + raw := transmute( ^runtime.Raw_Map) self + raw.allocator = allocator +} + +to_bytes :: #force_inline proc "contextless" ( typed_data : ^$Type ) -> []byte { return slice_ptr( transmute(^byte) typed_data, size_of(Type) ) } + +@(optimization_mode="favor_size") +djb8_hash :: #force_inline proc "contextless" ( hash : ^$Type, bytes : []byte ) { for value in bytes do (hash^) = (( (hash^) << 8) + (hash^) ) + Type(value) } + +RGBA8 :: [4]u8 +RGBAN :: [4]f32 Vec2 :: [2]f32 Vec2i :: [2]i32 Vec2_64 :: [2]f64 +Transform :: struct { + pos : Vec2, + scale : Vec2, +} + +Range2 :: struct { + p0, p1 : Vec2, +} + +mul_range2_vec2 :: #force_inline proc "contextless" ( range : Range2, v : Vec2 ) -> Range2 { return { range.p0 * v, range.p1 * v } } +size_range2 :: #force_inline proc "contextless" ( range : Range2 ) -> Vec2 { return range.p1 - range.p0 } + 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) }} @@ -39,91 +81,29 @@ logf :: proc( fmt : string, args : ..any, level := core_log.Level.Info, loc := 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 : Font_ID, glyph_index : Glyph ) -> (lru_code : u64) { - lru_code = u64(glyph_index) + ( ( 0x100000000 * u64(font) ) & 0xFFFFFFFF00000000 ) - return -} - -is_empty :: #force_inline proc ( ctx : ^Context, entry : ^Entry, glyph_index : Glyph ) -> b32 +@(optimization_mode="favor_size") +to_glyph_buffer_space :: #force_inline proc "contextless" ( #no_alias position, scale : ^Vec2, size : Vec2 ) { - if glyph_index == 0 do return true - if parser_is_glyph_empty( & entry.parser_info, glyph_index ) do return true - return false + pos := position^ + scale_32 := scale^ + + quotient : Vec2 = 1.0 / size + pos = pos * quotient * 2.0 - 1.0 + scale_32 = scale_32 * quotient * 2.0 + + (position^) = pos + (scale^) = scale_32 } -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 -} - -USE_F64_PRECISION_ON_X_FORM_OPS :: false - -screenspace_x_form :: #force_inline proc "contextless" ( position, scale : ^Vec2, size : Vec2 ) +@(optimization_mode="favor_size") +to_target_space :: #force_inline proc "contextless" ( #no_alias position, scale : ^Vec2, size : Vec2 ) { - when USE_F64_PRECISION_ON_X_FORM_OPS - { - 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 - { - pos := position^ - scale_32 := scale^ - - quotient : Vec2 = 1.0 / size - pos = pos * quotient * 2.0 - 1.0 - scale_32 = scale_32 * quotient * 2.0 - - (position^) = pos - (scale^) = scale_32 - } + quotient : Vec2 = 1.0 / size + (position^) *= quotient + (scale^) *= quotient } -textspace_x_form :: #force_inline proc "contextless" ( position, scale : ^Vec2, size : Vec2 ) -{ - when USE_F64_PRECISION_ON_X_FORM_OPS - { - 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 - } -} - -USE_MANUAL_SIMD_FOR_BEZIER_OPS :: false +USE_MANUAL_SIMD_FOR_BEZIER_OPS :: true when ! USE_MANUAL_SIMD_FOR_BEZIER_OPS { @@ -132,11 +112,6 @@ when ! USE_MANUAL_SIMD_FOR_BEZIER_OPS // 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 @@ -154,12 +129,6 @@ when ! USE_MANUAL_SIMD_FOR_BEZIER_OPS // 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 @@ -178,14 +147,17 @@ else { Vec2_SIMD :: simd.f32x4 + @(optimization_mode="favor_size") vec2_to_simd :: #force_inline proc "contextless" (v: Vec2) -> Vec2_SIMD { return Vec2_SIMD{v.x, v.y, 0, 0} } + @(optimization_mode="favor_size") simd_to_vec2 :: #force_inline proc "contextless" (v: Vec2_SIMD) -> Vec2 { return Vec2{ simd.extract(v, 0), simd.extract(v, 1) } } + @(optimization_mode="favor_size") eval_point_on_bezier3 :: #force_inline proc "contextless" (p0, p1, p2: Vec2, alpha: f32) -> Vec2 { simd_p0 := vec2_to_simd(p0) @@ -209,6 +181,7 @@ else return simd_to_vec2(result) } + @(optimization_mode="favor_size") eval_point_on_bezier4 :: #force_inline proc "contextless" (p0, p1, p2, p3: Vec2, alpha: f32) -> Vec2 { simd_p0 := vec2_to_simd(p0) diff --git a/vefontcache/parser.odin b/vefontcache/parser.odin index 2bb0b6f..6be355a 100644 --- a/vefontcache/parser.odin +++ b/vefontcache/parser.odin @@ -2,11 +2,17 @@ package vefontcache /* Notes: +This is a minimal wrapper I originally did incase something than stb_truetype is introduced in the future. +Otherwise, its essentially 1:1 with it. -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. +Freetype isn't really supported and its not a high priority (pretty sure its too slow). +~~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 +STB_Truetype: +* Has macros for its allocation unfortuantely. +TODO(Ed): Just keep a local version of stb_truetype and modify it to support a sokol/odin compatible allocator. +Already wanted to do so anyway to evaluate the glyph point shape implementation on its side. */ import "base:runtime" @@ -14,7 +20,7 @@ import "core:c" import "core:math" import "core:slice" import stbtt "vendor:stb/truetype" -import freetype "thirdparty:freetype" +// import freetype "thirdparty:freetype" Parser_Kind :: enum u32 { STB_TrueType, @@ -26,7 +32,7 @@ Parser_Font_Info :: struct { kind : Parser_Kind, using _ : struct #raw_union { stbtt_info : stbtt.fontinfo, - freetype_info : freetype.Face + // freetype_info : freetype.Face }, data : []byte, } @@ -52,20 +58,20 @@ Parser_Glyph_Shape :: [dynamic]Parser_Glyph_Vertex Parser_Context :: struct { kind : Parser_Kind, - ft_library : freetype.Library, + // ft_library : freetype.Library, } parser_init :: proc( ctx : ^Parser_Context, kind : Parser_Kind ) { - switch kind - { - case .Freetype: - result := freetype.init_free_type( & ctx.ft_library ) - assert( result == freetype.Error.Ok, "VEFontCache.parser_init: Failed to initialize freetype" ) + // switch 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: + // case .STB_TrueType: // Do nothing intentional - } + // } ctx.kind = kind } @@ -74,24 +80,23 @@ parser_shutdown :: proc( ctx : ^Parser_Context ) { // TODO(Ed): Implement } -parser_load_font :: proc( ctx : ^Parser_Context, label : string, data : []byte ) -> (font : Parser_Font_Info) +parser_load_font :: proc( ctx : ^Parser_Context, label : string, data : []byte ) -> (font : Parser_Font_Info, error : b32) { - switch ctx.kind - { - case .Freetype: - when ODIN_OS == .Windows { - error := freetype.new_memory_face( ctx.ft_library, raw_data(data), cast(i32) len(data), 0, & font.freetype_info ) - if error != .Ok do return - } - else when ODIN_OS == .Linux { - error := freetype.new_memory_face( ctx.ft_library, raw_data(data), cast(i64) len(data), 0, & font.freetype_info ) - if error != .Ok do return - } + // switch ctx.kind + // { + // case .Freetype: + // when ODIN_OS == .Windows { + // error_status := freetype.new_memory_face( ctx.ft_library, raw_data(data), cast(i32) len(data), 0, & font.freetype_info ) + // if error != .Ok do error = true + // } + // else when ODIN_OS == .Linux { + // error := freetype.new_memory_face( ctx.ft_library, raw_data(data), cast(i64) len(data), 0, & font.freetype_info ) + // if error_status != .Ok do error = true + // } - case .STB_TrueType: - success := stbtt.InitFont( & font.stbtt_info, raw_data(data), 0 ) - if ! success do return - } + // case .STB_TrueType: + error = ! stbtt.InitFont( & font.stbtt_info, raw_data(data), 0 ) + // } font.label = label font.data = data @@ -101,158 +106,163 @@ parser_load_font :: proc( ctx : ^Parser_Context, label : string, data : []byte ) parser_unload_font :: proc( font : ^Parser_Font_Info ) { - switch font.kind { - case .Freetype: - error := freetype.done_face( font.freetype_info ) - assert( error == .Ok, "VEFontCache.parser_unload_font: Failed to unload freetype face" ) + // 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: + // case .STB_TrueType: // Do Nothing - } + // } } -parser_find_glyph_index :: #force_inline proc "contextless" ( font : ^Parser_Font_Info, codepoint : rune ) -> (glyph_index : Glyph) +parser_find_glyph_index :: #force_inline proc "contextless" ( font : Parser_Font_Info, codepoint : rune ) -> (glyph_index : Glyph) { - switch font.kind - { - case .Freetype: - when ODIN_OS == .Windows { - glyph_index = transmute(Glyph) freetype.get_char_index( font.freetype_info, transmute(u32) codepoint ) - } - else when ODIN_OS == .Linux { - glyph_index = transmute(Glyph) freetype.get_char_index( font.freetype_info, cast(u64) codepoint ) - } + // profile(#procedure) + // switch font.kind + // { + // case .Freetype: + // when ODIN_OS == .Windows { + // glyph_index = transmute(Glyph) freetype.get_char_index( font.freetype_info, transmute(u32) codepoint ) + // } + // else when ODIN_OS == .Linux { + // glyph_index = transmute(Glyph) freetype.get_char_index( font.freetype_info, cast(u64) codepoint ) + // } + // return + + // case .STB_TrueType: + glyph_index = transmute(Glyph) stbtt.FindGlyphIndex( font.stbtt_info, codepoint ) return - - case .STB_TrueType: - glyph_index = transmute(Glyph) stbtt.FindGlyphIndex( & font.stbtt_info, codepoint ) - return - } - return Glyph(-1) + // } + // return Glyph(-1) } -parser_free_shape :: proc( font : ^Parser_Font_Info, shape : Parser_Glyph_Shape ) +parser_free_shape :: #force_inline proc( font : Parser_Font_Info, shape : Parser_Glyph_Shape ) { - switch font.kind - { - case .Freetype: - delete(shape) + // switch font.kind + // { + // case .Freetype: + // delete(shape) - case .STB_TrueType: - stbtt.FreeShape( & font.stbtt_info, transmute( [^]stbtt.vertex) raw_data(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 : ^Parser_Font_Info, codepoint : rune ) -> ( advance, to_left_side_glyph : i32 ) +parser_get_codepoint_horizontal_metrics :: #force_inline proc "contextless" ( font : Parser_Font_Info, codepoint : rune ) -> ( advance, to_left_side_glyph : i32 ) { - switch font.kind - { - case .Freetype: - glyph_index : Glyph - when ODIN_OS == .Windows { - glyph_index = transmute(Glyph) freetype.get_char_index( font.freetype_info, transmute(u32) codepoint ) - } - else when ODIN_OS == .Linux { - glyph_index = transmute(Glyph) freetype.get_char_index( font.freetype_info, cast(u64) codepoint ) - } + // switch font.kind + // { + // case .Freetype: + // glyph_index : Glyph + // when ODIN_OS == .Windows { + // glyph_index = transmute(Glyph) freetype.get_char_index( font.freetype_info, transmute(u32) codepoint ) + // } + // else when ODIN_OS == .Linux { + // glyph_index = transmute(Glyph) freetype.get_char_index( font.freetype_info, cast(u64) 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 - } + // 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 ) - } + // 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 : ^Parser_Font_Info, prev_codepoint, codepoint : rune ) -> i32 +parser_get_codepoint_kern_advance :: #force_inline proc "contextless" ( font : Parser_Font_Info, prev_codepoint, codepoint : rune ) -> i32 { - switch font.kind - { - case .Freetype: - prev_glyph_index : Glyph - glyph_index : Glyph - when ODIN_OS == .Windows { - 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 ) - } - else when ODIN_OS == .Linux { - prev_glyph_index = transmute(Glyph) freetype.get_char_index( font.freetype_info, cast(u64) prev_codepoint ) - glyph_index = transmute(Glyph) freetype.get_char_index( font.freetype_info, cast(u64) codepoint ) - } + // switch font.kind + // { + // case .Freetype: + // prev_glyph_index : Glyph + // glyph_index : Glyph + // when ODIN_OS == .Windows { + // 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 ) + // } + // else when ODIN_OS == .Linux { + // prev_glyph_index = transmute(Glyph) freetype.get_char_index( font.freetype_info, cast(u64) prev_codepoint ) + // glyph_index = transmute(Glyph) freetype.get_char_index( font.freetype_info, cast(u64) 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 ) - } + // 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 ) + // case .STB_TrueType: + kern := stbtt.GetCodepointKernAdvance( font.stbtt_info, prev_codepoint, codepoint ) return kern - } - return -1 + // } + // return -1 } -parser_get_font_vertical_metrics :: #force_inline proc "contextless" ( font : ^Parser_Font_Info ) -> (ascent, descent, line_gap : i32 ) +parser_get_font_vertical_metrics :: #force_inline proc "contextless" ( font : Parser_Font_Info ) -> (ascent, descent, line_gap : i32 ) { - switch font.kind - { - case .Freetype: - info := font.freetype_info - ascent = i32(info.ascender) - descent = i32(info.descender) - line_gap = i32(info.height) - (ascent - descent) + // switch font.kind + // { + // case .Freetype: + // info := font.freetype_info + // ascent = i32(info.ascender) + // descent = i32(info.descender) + // line_gap = i32(info.height) - (ascent - descent) - case .STB_TrueType: - stbtt.GetFontVMetrics( & font.stbtt_info, & ascent, & descent, & line_gap ) - } + // case .STB_TrueType: + stbtt.GetFontVMetrics( font.stbtt_info, & ascent, & descent, & line_gap ) + // } return } -parser_get_glyph_box :: #force_inline proc ( font : ^Parser_Font_Info, glyph_index : Glyph ) -> (bounds_0, bounds_1 : Vec2i) +parser_get_bounds :: #force_inline proc "contextless" ( font : Parser_Font_Info, glyph_index : Glyph ) -> (bounds : Range2) { - switch font.kind - { - case .Freetype: - freetype.load_glyph( font.freetype_info, c.uint(glyph_index), { .No_Bitmap, .No_Hinting, .No_Scale } ) + // profile(#procedure) + bounds_0, bounds_1 : Vec2i - metrics := font.freetype_info.glyph.metrics + // switch font.kind + // { + // case .Freetype: + // freetype.load_glyph( font.freetype_info, c.uint(glyph_index), { .No_Bitmap, .No_Hinting, .No_Scale } ) - 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)} + // metrics := font.freetype_info.glyph.metrics - case .STB_TrueType: + // 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 ) + 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) } - } + bounds_0 = { x0, y0 } + bounds_1 = { x1, y1 } + // } + bounds = { vec2(bounds_0), vec2(bounds_1) } return } -parser_get_glyph_shape :: proc( font : ^Parser_Font_Info, glyph_index : Glyph ) -> (shape : Parser_Glyph_Shape, error : Allocator_Error) +parser_get_glyph_shape :: #force_inline proc ( font : Parser_Font_Info, glyph_index : Glyph ) -> (shape : Parser_Glyph_Shape, error : Allocator_Error) { - switch font.kind - { - case .Freetype: - // TODO(Ed): Don't do this, going a completely different route for handling shapes. - // This abstraction fails to be time-saving or performant. + // switch font.kind + // { + // case .Freetype: + // // TODO(Ed): Don't do this, going a completely different route for handling shapes. + // // This abstraction fails to be time-saving or performant. - case .STB_TrueType: + // case .STB_TrueType: stb_shape : [^]stbtt.vertex - nverts := stbtt.GetGlyphShape( & font.stbtt_info, cast(i32) glyph_index, & stb_shape ) + 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 @@ -260,83 +270,82 @@ parser_get_glyph_shape :: proc( font : ^Parser_Font_Info, glyph_index : Glyph ) shape_raw.cap = int(nverts) shape_raw.allocator = runtime.nil_allocator() error = Allocator_Error.None - return - } + // return + // } return } -parser_is_glyph_empty :: #force_inline proc "contextless" ( font : ^Parser_Font_Info, glyph_index : Glyph ) -> b32 +parser_is_glyph_empty :: #force_inline proc "contextless" ( font : Parser_Font_Info, 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 + // 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 + // case .STB_TrueType: + return stbtt.IsGlyphEmpty( font.stbtt_info, cast(c.int) glyph_index ) + // } + // return false } -parser_scale :: #force_inline proc "contextless" ( font : ^Parser_Font_Info, size : f32 ) -> f32 +parser_scale :: #force_inline proc "contextless" ( font : Parser_Font_Info, 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 + // profile(#procedure) + // size_scale := size < 0.0 ? parser_scale_for_pixel_height( font, -size ) : parser_scale_for_mapping_em_to_pixels( font, size ) + size_scale := size > 0.0 ? parser_scale_for_pixel_height( font, size ) : parser_scale_for_mapping_em_to_pixels( font, -size ) return size_scale } -parser_scale_for_pixel_height :: #force_inline proc "contextless" ( font : ^Parser_Font_Info, size : f32 ) -> f32 +parser_scale_for_pixel_height :: #force_inline proc "contextless" ( font : Parser_Font_Info, 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 + // 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 + // case.STB_TrueType: + return stbtt.ScaleForPixelHeight( font.stbtt_info, size ) + // } + // return 0 } -parser_scale_for_mapping_em_to_pixels :: #force_inline proc "contextless" ( font : ^Parser_Font_Info, size : f32 ) -> f32 +parser_scale_for_mapping_em_to_pixels :: #force_inline proc "contextless" ( font : Parser_Font_Info, 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 + // 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 + // // 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 + // 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 + // 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 + // case .STB_TrueType: + return stbtt.ScaleForMappingEmToPixels( font.stbtt_info, size ) + // } + // return 0 } diff --git a/vefontcache/mappings.odin b/vefontcache/pkg_mapping.odin similarity index 59% rename from vefontcache/mappings.odin rename to vefontcache/pkg_mapping.odin index f6afc8d..9e39d0f 100644 --- a/vefontcache/mappings.odin +++ b/vefontcache/pkg_mapping.odin @@ -1,7 +1,10 @@ package vefontcache +import "base:builtin" + resize_soa_non_zero :: non_zero_resize_soa +import "base:runtime" import "core:hash" - fnv64a :: hash.fnv64a + ginger16 :: hash.ginger16 import "core:math" ceil_f16 :: math.ceil_f16 ceil_f16le :: math.ceil_f16le @@ -34,6 +37,7 @@ import "core:mem" arena_allocator :: mem.arena_allocator arena_init :: mem.arena_init import "core:slice" +import "core:unicode" //#region("Proc overload mappings") @@ -43,6 +47,10 @@ append :: proc { append_elem_string, } +append_soa :: proc { + append_soa_elem +} + ceil :: proc { math.ceil_f16, math.ceil_f16le, @@ -58,8 +66,8 @@ ceil :: proc { } clear :: proc { - clear_dynamic_array, - clear_map, + builtin.clear_dynamic_array, + builtin.clear_map, } floor :: proc { @@ -80,16 +88,39 @@ fill :: proc { slice.fill, } +max :: proc { + linalg.max_single, + linalg.max_double, +} + make :: proc { - make_dynamic_array, - make_dynamic_array_len, - make_dynamic_array_len_cap, - make_map, - make_map_cap, + builtin.make_dynamic_array, + builtin.make_dynamic_array_len, + builtin.make_dynamic_array_len_cap, + builtin.make_slice, + builtin.make_map, + builtin.make_map_cap, +} + +make_soa :: proc { + builtin.make_soa_dynamic_array_len_cap, + builtin.make_soa_slice, +} + +mul :: proc { + mul_range2_vec2, +} + +peek :: proc { + peek_array, } resize :: proc { - resize_dynamic_array, + builtin.resize_dynamic_array, +} + +size :: proc { + size_range2, } vec2 :: proc { @@ -105,4 +136,22 @@ vec2_64 :: proc { vec2_64_from_vec2, } +import "../../grime" + +@(deferred_none = profile_end, disabled = DISABLE_PROFILING) +profile :: #force_inline proc "contextless" ( name : string, loc := #caller_location ) { + grime.profile_begin(name, loc) +} + +@(disabled = DISABLE_PROFILING) +profile_begin :: #force_inline proc "contextless" ( name : string, loc := #caller_location ) { + grime.profile_begin(name, loc) +} + +@(disabled = DISABLE_PROFILING) +profile_end :: #force_inline proc "contextless" () { + grime.profile_end() +} + //#endregion("Proc overload mappings") + diff --git a/vefontcache/shaped_text.odin b/vefontcache/shaped_text.odin deleted file mode 100644 index d23436e..0000000 --- a/vefontcache/shaped_text.odin +++ /dev/null @@ -1,131 +0,0 @@ -package vefontcache - -Shaped_Text :: struct { - glyphs : [dynamic]Glyph, - positions : [dynamic]Vec2, - end_cursor_pos : Vec2, - size : Vec2, -} - -Shaped_Text_Cache :: struct { - storage : [dynamic]Shaped_Text, - state : LRU_Cache, - next_cache_id : i32, -} - -shape_lru_hash :: #force_inline proc "contextless" ( hash : ^u64, bytes : []byte ) { - for value in bytes { - (hash^) = (( (hash^) << 8) + (hash^) ) + u64(value) - } -} - -shape_text_cached :: proc( ctx : ^Context, font : Font_ID, text_utf8 : string, entry : ^Entry ) -> ^Shaped_Text -{ - // profile(#procedure) - font := font - font_bytes := slice_ptr( transmute(^byte) & font, size_of(Font_ID) ) - text_bytes := transmute( []byte) text_utf8 - - lru_code : u64 - shape_lru_hash( & lru_code, font_bytes ) - shape_lru_hash( & lru_code, text_bytes ) - - shape_cache := & ctx.shape_cache - state := & ctx.shape_cache.state - - shape_cache_idx := lru_get( state, lru_code ) - 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, lru_code, shape_cache_idx ) - } - 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, lru_code, shape_cache_idx ) - } - - shape_entry := & shape_cache.storage[ shape_cache_idx ] - shape_text_uncached( ctx, font, text_utf8, entry, shape_entry ) - } - - return & shape_cache.storage[ shape_cache_idx ] -} - -shape_text_uncached :: proc( ctx : ^Context, font : Font_ID, text_utf8 : string, entry : ^Entry, output : ^Shaped_Text ) -{ - // profile(#procedure) - assert( ctx != nil ) - assert( font >= 0 && int(font) < len(ctx.entries) ) - - clear( & output.glyphs ) - clear( & output.positions ) - - ascent_i32, descent_i32, line_gap_i32 := parser_get_font_vertical_metrics( & entry.parser_info ) - ascent := f32(ascent_i32) - descent := f32(descent_i32) - line_gap := f32(line_gap_i32) - line_height := (ascent - descent + line_gap) * entry.size_scale - - if ctx.use_advanced_shaper - { - shaper_shape_from_text( & ctx.shaper_ctx, & entry.shaper_info, output, text_utf8, ascent_i32, descent_i32, line_gap_i32, 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. - - line_count : int = 1 - max_line_width : f32 = 0 - position : Vec2 - - 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' - { - line_count += 1 - max_line_width = max(max_line_width, position.x) - position.x = 0.0 - position.y -= line_height - position.y = position.y - prev_codepoint = rune(0) - continue - } - if abs( entry.size ) <= ctx.shaper_ctx.adv_snap_small_font_threshold { - position.x = ceil(position.x) - } - - append( & output.glyphs, parser_find_glyph_index( & entry.parser_info, codepoint )) - advance, _ := parser_get_codepoint_horizontal_metrics( & entry.parser_info, codepoint ) - - append( & output.positions, Vec2 { - ceil(position.x), - ceil(position.y) - }) - - position.x += f32(advance) * entry.size_scale - prev_codepoint = codepoint - } - - output.end_cursor_pos = position - max_line_width = max(max_line_width, position.x) - - output.size.x = max_line_width - output.size.y = f32(line_count) * line_height - } -} diff --git a/vefontcache/shaper.odin b/vefontcache/shaper.odin index 309c4ec..de2ab71 100644 --- a/vefontcache/shaper.odin +++ b/vefontcache/shaper.odin @@ -6,11 +6,61 @@ Note(Ed): The only reason I didn't directly use harfbuzz is because hamza exists import "core:c" import "thirdparty:harfbuzz" +Shape_Key :: u32 + +/* A text whose codepoints have had their relevant glyphs and + associated data resolved for processing in a draw list generation stage. + Traditionally a shape only refers to resolving which glyph and + its position should be used for rendering. + + For this library's case it also involes keeping any content + that does not have to be resolved once again in the later stage of processing: + * Resolve atlas lru codes + * Resolve glyph bounds and scale + * Resolve atlas region the glyph is associated with. + + Ideally the user should resolve this shape once and cache/store it on their side. + They have the best ability to avoid costly lookups to streamline + a hot path to only focusing on draw list generation that must be computed every frame. +*/ +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_id : Font_ID, + // TODO(Ed): We need to track the font here for usage in user interface when directly drawing the shape. +} + +// Ease of use cache, can handle thousands of lookups per frame with ease. +// TODO(Ed) It might perform better with a tailored made hashtable implementation for the LRU_Cache or dedicated array struct/procs for the Shaped_Text. +Shaped_Text_Cache :: struct { + storage : [dynamic]Shaped_Text, + state : LRU_Cache(Shape_Key), + next_cache_id : i32, +} + +// Used by shaper_shape_text_cached, allows user to specify their own proc at compile-time without having to rewrite the caching implementation. +Shaper_Shape_Text_Uncached_Proc :: #type proc( ctx : ^Shaper_Context, + atlas : Atlas, + glyph_buffer_size : Vec2, + entry : Entry, + font_px_Size : f32, + font_scale : f32, + text_utf8 : string, + output : ^Shaped_Text +) + +// Note(Ed): Not used.. Shaper_Kind :: enum { - Naive = 0, + Latin = 0, Harfbuzz = 1, } +// Not much here other than just keep track of a harfbuzz var and deciding to keep runtime config here used by the shapers. Shaper_Context :: struct { hb_buffer : harfbuzz.Buffer, @@ -18,6 +68,7 @@ Shaper_Context :: struct { adv_snap_small_font_threshold : f32, } +// Only used with harbuzz for now. Resolved during load_font for a font Entry. Shaper_Info :: struct { blob : harfbuzz.Blob, face : harfbuzz.Face, @@ -37,46 +88,59 @@ shaper_shutdown :: proc( ctx : ^Shaper_Context ) } } -shaper_load_font :: proc( ctx : ^Shaper_Context, label : string, data : []byte, user_data : rawptr ) -> (info : Shaper_Info) +shaper_load_font :: #force_inline proc( ctx : ^Shaper_Context, label : string, data : []byte, user_data : rawptr = nil ) -> (info : 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 ) + info.blob = harfbuzz.blob_create( raw_data(data), cast(c.uint) len(data), harfbuzz.Memory_Mode.READONLY, user_data, nil ) + info.face = harfbuzz.face_create( info.blob, 0 ) + info.font = harfbuzz.font_create( info.face ) return } -shaper_unload_font :: proc( ctx : ^Shaper_Info ) +shaper_unload_font :: #force_inline proc( info : ^Shaper_Info ) { - 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 ) + if info.blob != nil do harfbuzz.font_destroy( info.font ) + if info.face != nil do harfbuzz.face_destroy( info.face ) + if info.blob != nil do harfbuzz.blob_destroy( info.blob ) } -shaper_shape_from_text :: proc( ctx : ^Shaper_Context, info : ^Shaper_Info, output :^Shaped_Text, text_utf8 : string, - ascent, descent, line_gap : i32, size, size_scale : f32 ) +// Recommended shaper. Very performant. +// TODO(Ed): Would be nice to properly support vertical shaping, right now its strictly just horizontal... +@(optimization_mode="favor_size") +shaper_shape_harfbuzz :: proc( ctx : ^Shaper_Context, text_utf8 : string, entry : Entry, font_px_Size, font_scale : f32, output :^Shaped_Text ) { - // profile(#procedure) + 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) + ascent := entry.ascent + descent := entry.descent + line_gap := entry.line_gap + max_line_width := f32(0) line_count := 1 - line_height := ((ascent - descent + line_gap) * size_scale) + line_height := ((ascent - descent + line_gap) * font_scale) - position, vertical_position : f32 - shape_run :: proc( buffer : harfbuzz.Buffer, script : harfbuzz.Script, font : harfbuzz.Font, output : ^Shaped_Text, - position, vertical_position, max_line_width: ^f32, line_count: ^int, - ascent, descent, line_gap, size, size_scale: f32, - snap_shape_pos : b32, adv_snap_small_font_threshold : f32 ) + position : Vec2 + + @(optimization_mode="favor_size") + shape_run :: proc( output : ^Shaped_Text, + entry : Entry, + buffer : harfbuzz.Buffer, + script : harfbuzz.Script, + + position : ^Vec2, + max_line_width : ^f32, + line_count : ^int, + + font_px_size : f32, + font_scale : f32, + + snap_shape_pos : b32, + adv_snap_small_font_threshold : f32 + ) { + profile(#procedure) // Set script and direction. We use the system's default langauge. // script = HB_SCRIPT_LATIN harfbuzz.buffer_set_script( buffer, script ) @@ -85,57 +149,58 @@ shaper_shape_from_text :: proc( ctx : ^Shaper_Context, info : ^Shaper_Info, outp // Perform the actual shaping of this run using HarfBuzz. harfbuzz.buffer_set_content_type( buffer, harfbuzz.Buffer_Content_Type.UNICODE ) - harfbuzz.shape( font, buffer, nil, 0 ) + harfbuzz.shape( entry.shaper_info.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 ) - line_height := (ascent - descent + line_gap) * size_scale + line_height := (entry.ascent - entry.descent + entry.line_gap) * font_scale 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 + glyph := cast(Glyph) hb_glyph.codepoint if hb_glyph.cluster > 0 { - (max_line_width^) = max( max_line_width^, position^ ) - (position^) = 0.0 - (vertical_position^) -= line_height - (vertical_position^) = floor(vertical_position^ + 0.5) + (max_line_width^) = max( max_line_width^, position.x ) + position.x = 0.0 + position.y -= line_height + position.y = floor(position.y) (line_count^) += 1 continue } - if abs( size ) <= adv_snap_small_font_threshold + if abs( font_px_size ) <= adv_snap_small_font_threshold { - (position^) = ceil( position^ ) + (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 - pos += offset_x - v_pos += offset_y + glyph_pos := position^ + offset := Vec2 { f32(hb_gposition.x_offset), f32(hb_gposition.y_offset) } * font_scale + glyph_pos += offset if snap_shape_pos { - pos = ceil(pos) - v_pos = ceil(v_pos) + glyph_pos = ceil(glyph_pos) } - append( & output.positions, Vec2 {pos, v_pos}) - (position^) += f32(hb_gposition.x_advance) * size_scale - (vertical_position^) += f32(hb_gposition.y_advance) * size_scale - (max_line_width^) = max(max_line_width^, position^) + advance := Vec2 { + f32(hb_gposition.x_advance) * font_scale, + f32(hb_gposition.y_advance) * font_scale + } + (position^) += advance + (max_line_width^) = max(max_line_width^, position.x) + + is_empty := parser_is_glyph_empty(entry.parser_info, glyph) + if ! is_empty { + append( & output.glyph, glyph ) + append( & output.position, glyph_pos) + } } - output.end_cursor_pos.x = position^ - output.end_cursor_pos.y = vertical_position^ + output.end_cursor_pos = position^ harfbuzz.buffer_clear_contents( buffer ) } @@ -160,26 +225,232 @@ shaper_shape_from_text :: proc( ctx : ^Shaper_Context, info : ^Shaper_Info, outp } // End current run since we've encountered a script change. - shape_run( - ctx.hb_buffer, current_script, info.font, output, - & position, & vertical_position, & max_line_width, & line_count, - ascent, descent, line_gap, size, size_scale, - ctx.snap_glyph_position, ctx.adv_snap_small_font_threshold - ) + shape_run( output, + entry, + ctx.hb_buffer, + current_script, + & position, + & max_line_width, + & line_count, + font_px_Size, + font_scale, + ctx.snap_glyph_position, + ctx.adv_snap_small_font_threshold + ) harfbuzz.buffer_add( ctx.hb_buffer, hb_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, & max_line_width, & line_count, - ascent, descent, line_gap, size, size_scale, - ctx.snap_glyph_position, ctx.adv_snap_small_font_threshold - ) + shape_run( output, + entry, + ctx.hb_buffer, + current_script, + & position, + & max_line_width, + & line_count, + font_px_Size, + font_scale, + ctx.snap_glyph_position, + ctx.adv_snap_small_font_threshold + ) // Set the final size output.size.x = max_line_width output.size.y = f32(line_count) * line_height return } + +shaper_shape_text_uncached_advanced :: #force_inline proc( ctx : ^Shaper_Context, + atlas : Atlas, + glyph_buffer_size : Vec2, + entry : Entry, + font_px_size : f32, + font_scale : f32, + text_utf8 : string, + output : ^Shaped_Text +) +{ + profile(#procedure) + assert( ctx != nil ) + + clear( & output.glyph ) + clear( & output.position ) + + shaper_shape_harfbuzz( ctx, text_utf8, entry, font_px_size, font_scale, output ) + + // Resolve each glyphs: bounds, atlas lru, and the atlas region as we have everything we need now. + + resize( & output.atlas_lru_code, len(output.glyph) ) + resize( & output.region_kind, len(output.glyph) ) + resize( & output.bounds, len(output.glyph) ) + + profile_begin("atlas_lru_code") + for id, index in output.glyph + { + output.atlas_lru_code[index] = atlas_glyph_lru_code(entry.id, font_px_size, id) + } + profile_end() + + profile_begin("bounds & region") + for id, index in output.glyph + { + bounds := & output.bounds[index] + (bounds ^) = parser_get_bounds( entry.parser_info, id ) + bounds_size_scaled := (bounds.p1 - bounds.p0) * font_scale + output.region_kind[index] = atlas_decide_region( atlas, glyph_buffer_size, bounds_size_scaled ) + } + profile_end() +} + +// Basic western alphabet based shaping. Not that much faster than harfbuzz if at all. +shaper_shape_text_latin :: proc( ctx : ^Shaper_Context, + atlas : Atlas, + glyph_buffer_size : Vec2, + entry : Entry, + font_px_size : f32, + font_scale : f32, + text_utf8 : string, + output : ^Shaped_Text +) +{ + profile(#procedure) + assert( ctx != nil ) + + clear( & output.glyph ) + clear( & output.position ) + + line_height := (entry.ascent - entry.descent + entry.line_gap) * font_scale + + line_count : int = 1 + max_line_width : f32 = 0 + position : Vec2 + + prev_codepoint : rune + for codepoint, index in text_utf8 + { + if prev_codepoint > 0 { + kern := parser_get_codepoint_kern_advance( entry.parser_info, prev_codepoint, codepoint ) + position.x += f32(kern) * font_scale + } + if codepoint == '\n' + { + line_count += 1 + max_line_width = max(max_line_width, position.x) + position.x = 0.0 + position.y -= line_height + position.y = position.y + prev_codepoint = rune(0) + continue + } + if abs( font_px_size ) <= ctx.adv_snap_small_font_threshold { + position.x = ceil(position.x) + } + + glyph_index := parser_find_glyph_index( entry.parser_info, codepoint ) + is_glyph_empty := parser_is_glyph_empty( entry.parser_info, glyph_index ) + if ! is_glyph_empty + { + append( & output.glyph, glyph_index) + append( & output.position, Vec2 { + ceil(position.x), + ceil(position.y) + }) + } + + advance, _ := parser_get_codepoint_horizontal_metrics( entry.parser_info, codepoint ) + position.x += f32(advance) * font_scale + prev_codepoint = codepoint + } + + output.end_cursor_pos = position + max_line_width = max(max_line_width, position.x) + + output.size.x = max_line_width + output.size.y = f32(line_count) * line_height + + // Resolve each glyphs: bounds, atlas lru, and the atlas region as we have everything we need now. + + resize( & output.atlas_lru_code, len(output.glyph) ) + resize( & output.region_kind, len(output.glyph) ) + resize( & output.bounds, len(output.glyph) ) + + profile_begin("atlas_lru_code") + for id, index in output.glyph + { + output.atlas_lru_code[index] = atlas_glyph_lru_code(entry.id, font_px_size, id) + } + profile_end() + + profile_begin("bounds & region") + for id, index in output.glyph + { + bounds := & output.bounds[index] + (bounds ^) = parser_get_bounds( entry.parser_info, id ) + bounds_size_scaled := (bounds.p1 - bounds.p0) * font_scale + output.region_kind[index] = atlas_decide_region( atlas, glyph_buffer_size, bounds_size_scaled ) + } + profile_end() +} + +// Shapes are tracked by the library's context using the shape cache +// and the key is resolved using the font, the desired pixel size, and the text bytes to be shaped. +// Thus this procedures cost will be proporitonal to how muh text it has to sift through. +// djb8_hash is used as its been pretty good for thousands of hashed lines that around 6-120 charactes long +// (and its very fast). +@(optimization_mode="favor_size") +shaper_shape_text_cached :: proc( text_utf8 : string, + ctx : ^Shaper_Context, + shape_cache : ^Shaped_Text_Cache, + atlas : Atlas, + glyph_buffer_size : Vec2, + font : Font_ID, + entry : Entry, + font_px_size : f32, + font_scale : f32, + shape_text_uncached : $Shaper_Shape_Text_Uncached_Proc +) -> (shaped_text : Shaped_Text) +{ + profile(#procedure) + font := font + font_px_size := font_px_size + font_bytes := to_bytes( & font ) + size_bytes := to_bytes( & font_px_size ) + text_bytes := transmute( []byte) text_utf8 + + lru_code : Shape_Key + djb8_hash( & lru_code, font_bytes ) + djb8_hash( & lru_code, size_bytes ) + djb8_hash( & lru_code, text_bytes ) + + state := & shape_cache.state + + shape_cache_idx := lru_get( state, lru_code ) + 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, lru_code, shape_cache_idx ) + } + else + { + next_evict_idx := lru_get_next_evicted( state ^ ) + assert( next_evict_idx != LRU_Fail_Mask_32 ) + + shape_cache_idx = lru_peek( state ^, next_evict_idx, must_find = true ) + assert( shape_cache_idx != - 1 ) + + lru_put( state, lru_code, 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 ) + + shaped_text = storage_entry ^ + return + } + + shaped_text = shape_cache.storage[ shape_cache_idx ] + return +} diff --git a/vefontcache/vefontcache.odin b/vefontcache/vefontcache.odin index 8ca238c..8770c09 100644 --- a/vefontcache/vefontcache.odin +++ b/vefontcache/vefontcache.odin @@ -1,285 +1,349 @@ /* -A port of (https://github.com/hypernewbie/VEFontCache) to Odin. - See: https://github.com/Ed94/VEFontCache-Odin */ package vefontcache import "base:runtime" -Font_ID :: distinct i64 +// See: mappings.odin for profiling hookup +DISABLE_PROFILING :: true +ENABLE_OVERSIZED_GLYPHS :: true +// White: Cached Hit, Red: Cache Miss, Yellow: Oversized (Will override user's colors enabled) +ENABLE_DRAW_TYPE_VISUALIZATION :: false + +Font_ID :: distinct i16 Glyph :: distinct i32 +Load_Font_Error :: enum(i32) { + None, + Parser_Failed, +} + Entry :: struct { parser_info : Parser_Font_Info, shaper_info : Shaper_Info, id : Font_ID, used : b32, curve_quality : f32, - size : f32, - size_scale : f32, + + ascent : f32, + descent : f32, + line_gap : f32, } Entry_Default :: Entry { - id = 0, - used = false, - size = 24.0, - size_scale = 1.0, + id = 0, + used = false, + curve_quality = 3, +} + +// Ease of use encapsulation of common fields for a canvas space +VPZ_Transform :: struct { + view : Vec2, + position : Vec2, + zoom : f32, +} + +Scope_Stack :: struct { + font : [dynamic]Font_ID, + font_size : [dynamic]f32, + colour : [dynamic]RGBAN, + view : [dynamic]Vec2, + position : [dynamic]Vec2, + scale : [dynamic]Vec2, + zoom : [dynamic]f32, } Context :: struct { backing : Allocator, - parser_ctx : Parser_Context, - shaper_ctx : Shaper_Context, + parser_ctx : Parser_Context, // Glyph parser state + shaper_ctx : Shaper_Context, // Text shaper state + // The managed font instances entries : [dynamic]Entry, - temp_path : [dynamic]Vertex, - temp_codepoint_seen : map[u64]bool, - temp_codepoint_seen_num : int, + // TODO(Ed): Review these when preparing to handle lifting of working context to a thread context. + glyph_buffer : Glyph_Draw_Buffer, + atlas : Atlas, + shape_cache : Shaped_Text_Cache, + draw_list : Draw_List, - snap_width : f32, - snap_height : f32, - - colour : Colour, - cursor_pos : Vec2, + batch_shapes_buffer : [dynamic]Shaped_Text, // Used for the procs that batch a layer of text. + // Tracks the offsets for the current layer in a draw_list draw_layer : struct { vertices_offset : int, indices_offset : int, calls_offset : int, }, - draw_list : Draw_List, - atlas : Atlas, - glyph_buffer : Glyph_Draw_Buffer, - shape_cache : Shaped_Text_Cache, + // Note(Ed): Not really used anymore. + // debug_print : b32, + // debug_print_verbose : b32, + + snap_width : f32, + snap_height : f32, + + // Will enforce even px_size when drawing. + even_size_only : f32, + + // Whether or not to snap positioning to the pixel of the view + // Helps with hinting + snap_to_view_extent : b32, + + stack : Scope_Stack, + + cursor_pos : Vec2, // TODO(Ed): Review this (most likely not being used properly right now) + // Will apply a boost scalar (1.0 + alpha sharpen) to the colour's alpha which provides some sharpening of the edges. + // Has a boldening side-effect. If overblown will look smeared. + alpha_sharpen : f32, + // Used by draw interface to super-scale the text by + // upscaling px_size with px_scalar and then down-scaling + // the draw_list result by the same amount. + px_scalar : f32, // Improves hinting, positioning, etc. Can make zoomed out text too jagged. default_curve_quality : i32, - use_advanced_shaper : b32, - - debug_print : b32, - debug_print_verbose : b32, -} - - -Init_Atlas_Region_Params :: struct { - width : u32, - height : u32, } Init_Atlas_Params :: struct { - width : u32, - height : u32, - glyph_padding : u32, // Padding to add to bounds__scaled for choosing which atlas region. - glyph_over_scalar : f32, // Scalar to apply to bounds__scaled for choosing which atlas region. - - region_a : Init_Atlas_Region_Params, - region_b : Init_Atlas_Region_Params, - region_c : Init_Atlas_Region_Params, - region_d : Init_Atlas_Region_Params, + size_multiplier : u32, // How much to scale the the atlas size to. (Affects everything, the base is 4096 x 2048 and everything follows from there) + glyph_padding : u32, // Padding to add to bounds__scaled for choosing which atlas region. } Init_Atlas_Params_Default :: Init_Atlas_Params { - width = 4096, - height = 2048, - glyph_padding = 1, - glyph_over_scalar = 1, - - region_a = { - width = 32, - height = 32, - }, - region_b = { - width = 32, - height = 64, - }, - region_c = { - width = 64, - height = 64, - }, - region_d = { - width = 128, - height = 128, - } + size_multiplier = 1, + glyph_padding = 1, } Init_Glyph_Draw_Params :: struct { - over_sample : Vec2, - buffer_batch : u32, - draw_padding : u32, + // During the draw list generation stage when blitting to atlas, the quad wil be ceil()'d to the closest pixel. + snap_glyph_height : b32, + // Intended to be x16 (4x4) super-sampling from the glyph buffer to the atlas. + // Oversized glyphs don't use this and instead do 2x or 1x depending on how massive they are. + over_sample : u32, + // Best to just keep this the same as glyph_padding for the atlas.. + draw_padding : u32, + shape_gen_scratch_reserve : u32, + // How many region.D glyphs can be drawn to the glyph render target buffer at once (worst case scenario) + buffer_glyph_limit : u32, + // How many glyphs can at maximimum be proccessed at once by batch_generate_glyphs_draw_list + batch_glyph_limit : u32, } Init_Glyph_Draw_Params_Default :: Init_Glyph_Draw_Params { - over_sample = Vec2 { 4, 4 }, - buffer_batch = 4, - draw_padding = Init_Atlas_Params_Default.glyph_padding, + snap_glyph_height = false, + over_sample = 4, + draw_padding = Init_Atlas_Params_Default.glyph_padding, + shape_gen_scratch_reserve = 512, + buffer_glyph_limit = 16, + batch_glyph_limit = 256, } Init_Shaper_Params :: struct { - use_advanced_text_shaper : b32, + // Forces a glyph position to align to a pixel (make sure to use snap_to_view_extent with this or else it won't be preserveds) snap_glyph_position : b32, + // Will use more signficant advance during shaping for fonts + // Note(Ed): Thinking of removing, doesn't look good often and its an extra condition in the hot-loop. adv_snap_small_font_threshold : u32, } Init_Shaper_Params_Default :: Init_Shaper_Params { - use_advanced_text_shaper = true, - snap_glyph_position = true, + snap_glyph_position = false, adv_snap_small_font_threshold = 0, } Init_Shape_Cache_Params :: struct { - capacity : u32, - reserve_length : u32, + // Note(Ed): This should mostly just be given the worst-case capacity and reserve at the same time. + // If memory is a concern it can easily be 256 - 2k if not much text is going to be rendered often. + // Shapes should really not exceed 1024 glyphs.. + capacity : u32, + reserve : u32, } Init_Shape_Cache_Params_Default :: Init_Shape_Cache_Params { - capacity = 8 * 1024, - reserve_length = 256, + capacity = 10 * 1024, + reserve = 128, } //#region("lifetime") // ve_fontcache_init -startup :: proc( ctx : ^Context, parser_kind : Parser_Kind = .STB_TrueType, +startup :: proc( ctx : ^Context, parser_kind : Parser_Kind = .STB_TrueType, // Note(Ed): Only sbt_truetype supported for now. allocator := context.allocator, atlas_params := Init_Atlas_Params_Default, glyph_draw_params := Init_Glyph_Draw_Params_Default, shape_cache_params := Init_Shape_Cache_Params_Default, shaper_params := Init_Shaper_Params_Default, + alpha_sharpen : f32 = 0.1, + px_scalar : f32 = 1.89, + + // Curve quality to use for a font when unspecified, + // Affects step size for bezier curve passes in generate_glyph_pass_draw_list default_curve_quality : u32 = 3, - entires_reserve : u32 = 512, - temp_path_reserve : u32 = 1024, - temp_codepoint_seen_reserve : u32 = 2048, + entires_reserve : u32 = 256, + scope_stack_reserve : u32 = 128, ) { assert( ctx != nil, "Must provide a valid context" ) - using ctx ctx.backing = allocator context.allocator = ctx.backing - use_advanced_shaper = shaper_params.use_advanced_text_shaper + ctx.colour = { 1, 1, 1, 1 } + ctx.alpha_sharpen = alpha_sharpen + ctx.px_scalar = px_scalar + + shaper_ctx := & ctx.shaper_ctx shaper_ctx.adv_snap_small_font_threshold = f32(shaper_params.adv_snap_small_font_threshold) shaper_ctx.snap_glyph_position = shaper_params.snap_glyph_position - if default_curve_quality == 0 { - default_curve_quality = 3 - } - ctx.default_curve_quality = default_curve_quality + ctx.default_curve_quality = default_curve_quality == 0 ? 3 : i32(default_curve_quality) error : Allocator_Error - entries, error = make( [dynamic]Entry, len = 0, cap = entires_reserve ) + ctx.entries, error = make( [dynamic]Entry, len = 0, cap = entires_reserve ) assert(error == .None, "VEFontCache.init : Failed to allocate entries") - temp_path, error = make( [dynamic]Vertex, len = 0, cap = 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, len = 0, cap = 4 * Kilobyte ) + ctx.draw_list.vertices, error = make( [dynamic]Vertex, len = 0, cap = 8 * Kilobyte ) assert(error == .None, "VEFontCache.init : Failed to allocate draw_list.vertices") - draw_list.indices, error = make( [dynamic]u32, len = 0, cap = 8 * Kilobyte ) + ctx.draw_list.indices, error = make( [dynamic]u32, len = 0, cap = 16 * Kilobyte ) assert(error == .None, "VEFontCache.init : Failed to allocate draw_list.indices") - draw_list.calls, error = make( [dynamic]Draw_Call, len = 0, cap = 512 ) + ctx.draw_list.calls, error = make( [dynamic]Draw_Call, len = 0, cap = Kilobyte ) assert(error == .None, "VEFontCache.init : Failed to allocate draw_list.calls") - - init_atlas_region :: proc( region : ^Atlas_Region, params : Init_Atlas_Params, region_params : Init_Atlas_Region_Params, factor : Vec2i, expected_cap : i32 ) + + atlas := & ctx.atlas + Atlas_Setup: { - using region + atlas.size_multiplier = f32(atlas_params.size_multiplier) - next_idx = 0; - width = i32(region_params.width) - height = i32(region_params.height) - size = { - i32(params.width) / factor.x, - i32(params.height) / factor.y, + atlas_size := Vec2i { 4096, 2048 } * i32(atlas.size_multiplier) + slot_region_a := Vec2i { 32, 32 } * i32(atlas.size_multiplier) + slot_region_b := Vec2i { 32, 64 } * i32(atlas.size_multiplier) + slot_region_c := Vec2i { 64, 64 } * i32(atlas.size_multiplier) + slot_region_d := Vec2i { 128, 128 } * i32(atlas.size_multiplier) + + init_atlas_region :: proc( region : ^Atlas_Region, atlas_size, slot_size : Vec2i, factor : Vec2i ) + { + region.next_idx = 0; + region.slot_size = slot_size + region.size = atlas_size / factor + region.capacity = region.size / region.slot_size + + error : Allocator_Error + lru_init( & region.state, region.capacity.x * region.capacity.y ) } - capacity = { - size.x / i32(width), - size.y / i32(height), + init_atlas_region( & atlas.region_a, atlas_size, slot_region_a, { 4, 2}) + init_atlas_region( & atlas.region_b, atlas_size, slot_region_b, { 4, 2}) + init_atlas_region( & atlas.region_c, atlas_size, slot_region_c, { 4, 1}) + init_atlas_region( & atlas.region_d, atlas_size, slot_region_d, { 2, 1}) + + atlas.size = atlas_size + atlas.glyph_padding = f32(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 = atlas.size.x / 2 + atlas.region_d.offset.y = 0 + + atlas.regions = { + nil, + & atlas.region_a, + & atlas.region_b, + & atlas.region_c, + & atlas.region_d, } - assert( capacity.x * capacity.y == expected_cap ) - - error : Allocator_Error - lru_init( & state, 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 = i32(atlas_params.width) - atlas.height = i32(atlas_params.height) - atlas.glyph_padding = i32(atlas_params.glyph_padding) - atlas.glyph_over_scalar = atlas_params.glyph_over_scalar - - 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 = atlas.width / 2 - atlas.region_d.offset.y = 0 - - lru_init( & shape_cache.state, i32(shape_cache_params.capacity) ) - - shape_cache.storage, error = make( [dynamic]Shaped_Text, 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, len = 0, cap = shape_cache_params.reserve_length ) - assert( error == .None, "VEFontCache.init : Failed to allocate glyphs array for shape cache storage" ) - - positions, error = make( [dynamic]Vec2, len = 0, cap = shape_cache_params.reserve_length ) - assert( error == .None, "VEFontCache.init : Failed to allocate positions array for shape cache storage" ) - - draw_list.calls, error = make( [dynamic]Draw_Call, len = 0, cap = 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, len = 0, cap = 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, len = 0, cap = glyph_draw_params.buffer_batch * 2 * 4 ) - assert( error == .None, "VEFontCache.init : Failed to allocate vertices array for draw_list" ) } - // Note(From original author): We can actually go over VE_FONTCACHE_GLYPHDRAW_BUFFER_BATCH batches due to smart packing! + Shape_Cache_Setup: { - using glyph_buffer - over_sample = glyph_draw_params.over_sample - batch = cast(i32) glyph_draw_params.buffer_batch - width = atlas.region_d.width * i32(over_sample.x) * batch - height = atlas.region_d.height * i32(over_sample.y) - draw_padding = cast(i32) glyph_draw_params.draw_padding + shape_cache := & ctx.shape_cache + lru_init( & shape_cache.state, i32(shape_cache_params.capacity) ) - draw_list.calls, error = make( [dynamic]Draw_Call, len = 0, cap = glyph_draw_params.buffer_batch * 2 ) - assert( error == .None, "VEFontCache.init : Failed to allocate calls for draw_list" ) + shape_cache.storage, error = make( [dynamic]Shaped_Text, shape_cache_params.capacity ) + assert(error == .None, "VEFontCache.init : Failed to allocate shape_cache.storage") - draw_list.indices, error = make( [dynamic]u32, len = 0, cap = glyph_draw_params.buffer_batch * 2 * 6 ) - assert( error == .None, "VEFontCache.init : Failed to allocate indices array for draw_list" ) + for idx : u32 = 0; idx < shape_cache_params.capacity; idx += 1 + { + stroage_entry := & shape_cache.storage[idx] - draw_list.vertices, error = make( [dynamic]Vertex, len = 0, cap = glyph_draw_params.buffer_batch * 2 * 4 ) - assert( error == .None, "VEFontCache.init : Failed to allocate vertices array for draw_list" ) + stroage_entry.glyph, error = make( [dynamic]Glyph, len = 0, cap = shape_cache_params.reserve ) + assert( error == .None, "VEFontCache.init : Failed to allocate glyphs array for shape cache storage" ) - clear_draw_list.calls, error = make( [dynamic]Draw_Call, len = 0, cap = glyph_draw_params.buffer_batch * 2 ) - assert( error == .None, "VEFontCache.init : Failed to allocate calls for calls for clear_draw_list" ) + stroage_entry.position, error = make( [dynamic]Vec2, len = 0, cap = shape_cache_params.reserve ) + assert( error == .None, "VEFontCache.init : Failed to allocate positions array for shape cache storage" ) - clear_draw_list.indices, error = make( [dynamic]u32, len = 0, cap = glyph_draw_params.buffer_batch * 2 * 4 ) + stroage_entry.atlas_lru_code, error = make( [dynamic]Atlas_Key, len = 0, cap = shape_cache_params.reserve ) + assert( error == .None, "VEFontCache.init : Failed to allocate atlas_lru_code array for shape cache storage" ) + + stroage_entry.region_kind, error = make( [dynamic]Atlas_Region_Kind, len = 0, cap = shape_cache_params.reserve ) + assert( error == .None, "VEFontCache.init : Failed to allocate region_kind array for shape cache storage" ) + + stroage_entry.bounds, error = make( [dynamic]Range2, len = 0, cap = shape_cache_params.reserve ) + assert( error == .None, "VEFontCache.init : Failed to allocate bounds array for shape cache storage" ) + + // stroage_entry.bounds_scaled, error = make( [dynamic]Range2, len = 0, cap = shape_cache_params.reserve ) + // assert( error == .None, "VEFontCache.init : Failed to allocate bounds_scaled array for shape cache storage" ) + } + } + + Glyph_Buffer_Setup: + { + glyph_buffer := & ctx.glyph_buffer + glyph_buffer.snap_glyph_height = cast(f32) i32(glyph_draw_params.snap_glyph_height) + glyph_buffer.over_sample = { f32(glyph_draw_params.over_sample), f32(glyph_draw_params.over_sample) } + glyph_buffer.size.x = atlas.region_d.slot_size.x * i32(glyph_buffer.over_sample.x) * i32(glyph_draw_params.buffer_glyph_limit) + glyph_buffer.size.y = atlas.region_d.slot_size.y * i32(glyph_buffer.over_sample.y) + glyph_buffer.draw_padding = cast(f32) glyph_draw_params.draw_padding + + glyph_buffer.draw_list.vertices, error = make( [dynamic]Vertex, len = 0, cap = 8 * Kilobyte ) + assert( error == .None, "VEFontCache.init : Failed to allocate vertices array for glyph_buffer.draw_list" ) + + glyph_buffer.draw_list.indices, error = make( [dynamic]u32, len = 0, cap = 16 * Kilobyte ) + assert( error == .None, "VEFontCache.init : Failed to allocate indices for glyph_buffer.draw_list" ) + + glyph_buffer.draw_list.calls, error = make( [dynamic]Draw_Call, len = 0, cap = Kilobyte ) + assert( error == .None, "VEFontCache.init : Failed to allocate calls for glyph_buffer.draw_list" ) + + glyph_buffer.clear_draw_list.vertices, error = make( [dynamic]Vertex, len = 0, cap = 2 * Kilobyte ) + assert( error == .None, "VEFontCache.init : Failed to allocate vertices array for clear_draw_list" ) + + glyph_buffer.clear_draw_list.indices, error = make( [dynamic]u32, len = 0, cap = 4 * Kilobyte ) assert( error == .None, "VEFontCache.init : Failed to allocate calls for indices array for clear_draw_list" ) - clear_draw_list.vertices, error = make( [dynamic]Vertex, len = 0, cap = glyph_draw_params.buffer_batch * 2 * 4 ) - assert( error == .None, "VEFontCache.init : Failed to allocate vertices array for clear_draw_list" ) + glyph_buffer.clear_draw_list.calls, error = make( [dynamic]Draw_Call, len = 0, cap = Kilobyte ) + assert( error == .None, "VEFontCache.init : Failed to allocate calls for calls for clear_draw_list" ) + + glyph_buffer.shape_gen_scratch, error = make( [dynamic]Vertex, len = 0, cap = glyph_draw_params.shape_gen_scratch_reserve ) + assert(error == .None, "VEFontCache.init : Failed to allocate shape_gen_scratch") + + batch_cache := & glyph_buffer.batch_cache + batch_cache.cap = i32(glyph_draw_params.batch_glyph_limit) + batch_cache.num = 0 + batch_cache.table, error = make( map[Atlas_Key]b8, uint(glyph_draw_params.batch_glyph_limit) ) + assert(error == .None, "VEFontCache.init : Failed to allocate batch_cache") + + glyph_buffer.glyph_pack,error = make_soa( #soa[dynamic]Glyph_Pack_Entry, length = 0, capacity = uint(shape_cache_params.reserve) ) + glyph_buffer.oversized, error = make( [dynamic]i32, len = 0, cap = uint(shape_cache_params.reserve) ) + glyph_buffer.to_cache, error = make( [dynamic]i32, len = 0, cap = uint(shape_cache_params.reserve) ) + glyph_buffer.cached, error = make( [dynamic]i32, len = 0, cap = uint(shape_cache_params.reserve) ) } - parser_init( & parser_ctx, parser_kind ) - shaper_init( & shaper_ctx ) + parser_init( & ctx.parser_ctx, parser_kind ) + shaper_init( & ctx.shaper_ctx ) + + // Set the default stack values + // Will be popped on shutdown + // push_colour(ctx, {1, 1, 1, 1}) + // push_font_size(ctx, 36) + // push_view(ctx, { 0, 0 }) + // push_position(ctx, {0, 0}) + // push_scale(ctx, 1.0) + // push_zoom(ctx, 1.0) } hot_reload :: proc( ctx : ^Context, allocator : Allocator ) @@ -287,15 +351,29 @@ 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 ) + atlas := & ctx.atlas + glyph_buffer := & ctx.glyph_buffer + shape_cache := & ctx.shape_cache + draw_list := & ctx.draw_list - reload_array( & draw_list.vertices, allocator) - reload_array( & draw_list.indices, allocator ) - reload_array( & draw_list.calls, allocator ) + reload_array( & ctx.entries, 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_map( & glyph_buffer.batch_cache.table, allocator ) + reload_array( & glyph_buffer.shape_gen_scratch, allocator ) + + reload_array_soa( & glyph_buffer.glyph_pack, allocator ) + reload_array( & glyph_buffer.oversized, allocator ) + reload_array( & glyph_buffer.to_cache, allocator ) + reload_array( & glyph_buffer.cached, allocator ) lru_reload( & atlas.region_a.state, allocator) lru_reload( & atlas.region_b.state, allocator) @@ -304,57 +382,43 @@ hot_reload :: proc( ctx : ^Context, allocator : Allocator ) lru_reload( & shape_cache.state, allocator ) for idx : i32 = 0; idx < i32(len(shape_cache.storage)); idx += 1 { - stroage_entry := & shape_cache.storage[idx] - using stroage_entry - - reload_array( & glyphs, allocator ) - reload_array( & positions, allocator ) + storage_entry := & shape_cache.storage[idx] + reload_array( & storage_entry.glyph, allocator) + reload_array( & storage_entry.position, allocator) + reload_array( & storage_entry.atlas_lru_code, allocator) + reload_array( & storage_entry.region_kind, allocator) + reload_array( & storage_entry.bounds, allocator) + // reload_array( & storage_entry.bounds_scaled, 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 ) + + reload_array( & draw_list.vertices, allocator) + reload_array( & draw_list.indices, allocator ) + reload_array( & draw_list.calls, allocator ) } -// ve_foncache_shutdown shutdown :: proc( ctx : ^Context ) { assert( ctx != nil ) context.allocator = ctx.backing - using ctx - for & entry in entries { + atlas := & ctx.atlas + glyph_buffer := & ctx.glyph_buffer + shape_cache := & ctx.shape_cache + draw_list := & ctx.draw_list + + // pop_colour(ctx) + // pop_font_size(ctx) + // pop_view(ctx) + // pop_position(ctx) + // pop_scale(ctx) + // pop_zoom(ctx) + + for & entry in ctx.entries { unload_font( ctx, entry.id ) } - - delete( entries ) - delete( temp_path ) - delete( temp_codepoint_seen ) - - delete( draw_list.vertices ) - delete( draw_list.indices ) - delete( draw_list.calls ) - - lru_free( & atlas.region_a.state ) - lru_free( & atlas.region_b.state ) - lru_free( & atlas.region_c.state ) - lru_free( & atlas.region_d.state ) - - for idx : i32 = 0; idx < i32(len(shape_cache.storage)); idx += 1 { - stroage_entry := & shape_cache.storage[idx] - using stroage_entry - - delete( glyphs ) - delete( positions ) - } - lru_free( & shape_cache.state ) - + delete( ctx.entries ) + delete( glyph_buffer.draw_list.vertices ) delete( glyph_buffer.draw_list.indices ) delete( glyph_buffer.draw_list.calls ) @@ -363,180 +427,38 @@ shutdown :: proc( ctx : ^Context ) delete( glyph_buffer.clear_draw_list.indices ) delete( glyph_buffer.clear_draw_list.calls ) - shaper_shutdown( & shaper_ctx ) - parser_shutdown( & parser_ctx ) -} + delete( glyph_buffer.batch_cache.table ) + delete( glyph_buffer.shape_gen_scratch ) -// ve_fontcache_load -load_font :: proc( ctx : ^Context, label : string, data : []byte, size_px : f32, glyph_curve_quality : u32 = 0 ) -> (font_id : Font_ID) -{ - assert( ctx != nil ) - assert( len(data) > 0 ) - using ctx - context.allocator = backing + delete_soa( glyph_buffer.glyph_pack) + delete( glyph_buffer.oversized) + delete( glyph_buffer.to_cache) + delete( glyph_buffer.cached) - id : i32 = -1 + lru_free( & atlas.region_a.state ) + lru_free( & atlas.region_b.state ) + lru_free( & atlas.region_c.state ) + lru_free( & atlas.region_d.state ) - for index : i32 = 0; index < i32(len(entries)); index += 1 { - if entries[index].used do continue - id = index - break + for idx : i32 = 0; idx < i32(len(shape_cache.storage)); idx += 1 { + storage_entry := & shape_cache.storage[idx] + delete( storage_entry.glyph ) + delete( storage_entry.position ) + delete( storage_entry.atlas_lru_code) + delete( storage_entry.region_kind) + delete( storage_entry.bounds) + // delete( storage_entry.bounds_scaled) } - if id == -1 { - append_elem( & entries, Entry {}) - id = cast(i32) len(entries) - 1 - } - assert( id >= 0 && id < i32(len(entries)) ) + lru_free( & shape_cache.state ) + + delete( draw_list.vertices ) + delete( draw_list.indices ) + delete( draw_list.calls ) - entry := & entries[ id ] - { - using entry - used = true - - parser_info = parser_load_font( & parser_ctx, label, data ) - shaper_info = shaper_load_font( & shaper_ctx, label, data, transmute(rawptr) id ) - - size = size_px - size_scale = parser_scale( & parser_info, size ) - - if glyph_curve_quality == 0 { - curve_quality = f32(ctx.default_curve_quality) - } - else { - curve_quality = f32(glyph_curve_quality) - } - } - entry.id = Font_ID(id) - ctx.entries[ id ].id = Font_ID(id) - - font_id = Font_ID(id) - return + shaper_shutdown( & ctx.shaper_ctx ) + parser_shutdown( & ctx.parser_ctx ) } -// ve_fontcache_unload -unload_font :: proc( ctx : ^Context, font : Font_ID ) -{ - 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 = f32(snap_width) - ctx.snap_height = f32(snap_height) -} - -get_cursor_pos :: #force_inline proc( ctx : ^Context ) -> Vec2 { assert(ctx != nil); return ctx.cursor_pos } -set_colour :: #force_inline proc( ctx : ^Context, colour : Colour ) { assert(ctx != nil); ctx.colour = colour } - -draw_text :: proc( ctx : ^Context, font : Font_ID, text_utf8 : string, position, scale : Vec2 ) -> b32 -{ - // profile(#procedure) - assert( ctx != nil ) - assert( font >= 0 && int(font) < len(ctx.entries) ) - - ctx.cursor_pos = {} - - position := position - if ctx.snap_width > 0 do position.x = ceil(position.x * ctx.snap_width ) / ctx.snap_width - if ctx.snap_height > 0 do position.y = ceil(position.y * ctx.snap_height) / ctx.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, ctx.snap_width, ctx.snap_height ) - } - return true -} - -// ve_fontcache_Draw_List -get_draw_list :: proc( ctx : ^Context, optimize_before_returning := true ) -> ^Draw_List { - 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 : []Draw_Call) { - 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_Draw_List -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 : Font_ID, text_utf8 : string ) -> (measured : Vec2) -{ - // profile(#procedure) - assert( ctx != nil ) - assert( font >= 0 && int(font) < len(ctx.entries) ) - - entry := &ctx.entries[font] - shaped := shape_text_cached(ctx, font, text_utf8, entry) - return shaped.size -} - -get_font_vertical_metrics :: #force_inline proc ( ctx : ^Context, font : Font_ID ) -> ( ascent, descent, line_gap : f32 ) -{ - assert( ctx != nil ) - assert( font >= 0 && int(font) < len(ctx.entries) ) - - entry := & ctx.entries[ font ] - ascent_i32, descent_i32, line_gap_i32 := parser_get_font_vertical_metrics( & entry.parser_info ) - - ascent = (f32(ascent_i32) * entry.size_scale) - descent = (f32(descent_i32) * entry.size_scale) - line_gap = (f32(line_gap_i32) * entry.size_scale) - return -} - -//#endregion("metrics") - // Can be used with hot-reload clear_atlas_region_caches :: proc(ctx : ^Context) { @@ -554,19 +476,675 @@ clear_atlas_region_caches :: proc(ctx : ^Context) // Can be used with hot-reload clear_shape_cache :: proc (ctx : ^Context) { - using ctx - lru_clear(& shape_cache.state) - for idx : i32 = 0; idx < cast(i32) cap(shape_cache.storage); idx += 1 - { - stroage_entry := & shape_cache.storage[idx] - using stroage_entry - end_cursor_pos = {} - size = {} - clear(& glyphs) - clear(& positions) - clear(& draw_list.calls) - clear(& draw_list.indices) - clear(& draw_list.vertices) + lru_clear(& ctx.shape_cache.state) + for idx : i32 = 0; idx < cast(i32) cap(ctx.shape_cache.storage); idx += 1 { + stroage_entry := & ctx.shape_cache.storage[idx] + stroage_entry.end_cursor_pos = {} + stroage_entry.size = {} + clear(& stroage_entry.glyph) + clear(& stroage_entry.position) } ctx.shape_cache.next_cache_id = 0 } + +load_font :: proc( ctx : ^Context, label : string, data : []byte, glyph_curve_quality : u32 = 0 ) -> (font_id : Font_ID, error : Load_Font_Error) +{ + profile(#procedure) + assert( ctx != nil ) + assert( len(data) > 0 ) + context.allocator = ctx.backing + + entries := & ctx.entries + + id : Font_ID = -1 + + for index : i32 = 0; index < i32(len(entries)); index += 1 { + if entries[index].used do continue + id = Font_ID(index) + break + } + if id == -1 { + append_elem( entries, Entry {}) + id = cast(Font_ID) len(entries) - 1 + } + assert( id >= 0 && id < Font_ID(len(entries)) ) + + entry := & entries[ id ] + { + entry.used = true + + profile_begin("calling loaders") + parser_error : b32 + entry.parser_info, parser_error = parser_load_font( & ctx.parser_ctx, label, data ) + if parser_error { + error = .Parser_Failed + return + } + entry.shaper_info = shaper_load_font( & ctx.shaper_ctx, label, data ) + profile_end() + + ascent, descent, line_gap := parser_get_font_vertical_metrics(entry.parser_info) + entry.ascent = f32(ascent) + entry.descent = f32(descent) + entry.line_gap = f32(line_gap) + + if glyph_curve_quality == 0 { + entry.curve_quality = f32(ctx.default_curve_quality) + } + else { + entry.curve_quality = f32(glyph_curve_quality) + } + } + entry.id = Font_ID(id) + ctx.entries[ id ].id = Font_ID(id) + + font_id = Font_ID(id) + return +} + +unload_font :: proc( ctx : ^Context, font : Font_ID ) +{ + assert( ctx != nil ) + assert( font >= 0 && int(font) < len(ctx.entries) ) + context.allocator = ctx.backing + + entry := & ctx.entries[ font ] + entry.used = false + + parser_unload_font( & entry.parser_info ) + shaper_unload_font( & entry.shaper_info ) +} + +//#endregion("lifetime") + +//#region("scoping") + +/* Scope stacking ease of use interface. + +View: Extents in 2D for the relative space the the text is being drawn within. +Used with snap_to_view_extent to enforce position snapping. + +Position: Used with a draw procedure that uses relative positioning will offset the incoming position by the given amount. +Scale : Used with a draw procedure that uses relative scaling, will scale the procedures incoming scale by the given amount. +Zoom : Used with a draw procedure that uses scaling via zoom, will scale the procedure's incoming font size & scale based on an 'canvas' camera's notion of it. +*/ + +@(deferred_in = auto_pop_font) +scope_font :: #force_inline proc( ctx : ^Context, font : Font_ID ) { assert(ctx != nil); append(& ctx.stack.font, font ) } +push_font :: #force_inline proc( ctx : ^Context, font : Font_ID ) { assert(ctx != nil); append(& ctx.stack.font, font ) } +pop_font :: #force_inline proc( ctx : ^Context ) { assert(ctx != nil); pop(& ctx.stack.font) } +auto_pop_font :: #force_inline proc( ctx : ^Context, font : Font_ID ) { assert(ctx != nil); pop(& ctx.stack.font) } + +@(deferred_in = auto_pop_font_size) +scope_font_size :: #force_inline proc( ctx : ^Context, px_size : f32 ) { assert(ctx != nil); append(& ctx.stack.font_size, px_size) } +push_font_size :: #force_inline proc( ctx : ^Context, px_size : f32 ) { assert(ctx != nil); append(& ctx.stack.font_size, px_size) } +pop_font_size :: #force_inline proc( ctx : ^Context ) { assert(ctx != nil); pop(& ctx.stack.font_size) } +auto_pop_font_size :: #force_inline proc( ctx : ^Context, px_size : f32 ) { assert(ctx != nil); pop(& ctx.stack.font_size) } + +@(deferred_in = auto_pop_colour ) +scope_colour :: #force_inline proc( ctx : ^Context, colour : RGBAN ) { assert(ctx != nil); append(& ctx.stack.colour, colour) } +push_colour :: #force_inline proc( ctx : ^Context, colour : RGBAN ) { assert(ctx != nil); append(& ctx.stack.colour, colour) } +pop_colour :: #force_inline proc( ctx : ^Context ) { assert(ctx != nil); pop(& ctx.stack.colour) } +auto_pop_colour :: #force_inline proc( ctx : ^Context, colour : RGBAN ) { assert(ctx != nil); pop(& ctx.stack.colour) } + +@(deferred_in = auto_pop_view) +scope_view :: #force_inline proc( ctx : ^Context, view : Vec2 ) { assert(ctx != nil); append(& ctx.stack.view, view) } +push_view :: #force_inline proc( ctx : ^Context, view : Vec2 ) { assert(ctx != nil); append(& ctx.stack.view, view) } +pop_view :: #force_inline proc( ctx : ^Context ) { assert(ctx != nil); pop(& ctx.stack.view) } +auto_pop_view :: #force_inline proc( ctx : ^Context, view : Vec2 ) { assert(ctx != nil); pop(& ctx.stack.view) } + +@(deferred_in = auto_pop_position) +scope_position :: #force_inline proc( ctx : ^Context, position : Vec2 ) { assert(ctx != nil); append(& ctx.stack.position, position ) } +push_position :: #force_inline proc( ctx : ^Context, position : Vec2 ) { assert(ctx != nil); append(& ctx.stack.position, position ) } +pop_position :: #force_inline proc( ctx : ^Context ) { assert(ctx != nil); pop( & ctx.stack.position) } +auto_pop_position :: #force_inline proc( ctx : ^Context, view : Vec2 ) { assert(ctx != nil); pop( & ctx.stack.position) } + +@(deferred_in = auto_pop_scale) +scope_scale :: #force_inline proc( ctx : ^Context, scale : Vec2 ) { assert(ctx != nil); append(& ctx.stack.scale, scale ) } +push_scale :: #force_inline proc( ctx : ^Context, scale : Vec2 ) { assert(ctx != nil); append(& ctx.stack.scale, scale ) } +pop_scale :: #force_inline proc( ctx : ^Context, ) { assert(ctx != nil); pop(& ctx.stack.scale) } +auto_pop_scale :: #force_inline proc( ctx : ^Context, scale : Vec2 ) { assert(ctx != nil); pop(& ctx.stack.scale) } + +@(deferred_in = auto_pop_zoom ) +scope_zoom :: #force_inline proc( ctx : ^Context, zoom : f32 ) { append(& ctx.stack.zoom, zoom ) } +push_zoom :: #force_inline proc( ctx : ^Context, zoom : f32 ) { append(& ctx.stack.zoom, zoom) } +pop_zoom :: #force_inline proc( ctx : ^Context ) { pop(& ctx.stack.zoom) } +auto_pop_zoom :: #force_inline proc( ctx : ^Context, zoom : f32 ) { pop(& ctx.stack.zoom) } + +@(deferred_in = auto_pop_vpz) +scope_vpz :: #force_inline proc( ctx : ^Context, camera : VPZ_Transform ) { + assert(ctx != nil) + append(& ctx.stack.view, camera.view ) + append(& ctx.stack.position, camera.position ) + append(& ctx.stack.zoom, camera.zoom ) +} +push_vpz :: #force_inline proc( ctx : ^Context, camera : VPZ_Transform ) { + assert(ctx != nil) + append(& ctx.stack.view, camera.view ) + append(& ctx.stack.position, camera.position ) + append(& ctx.stack.zoom, camera.zoom ) +} +pop_vpz :: #force_inline proc( ctx : ^Context ) { + assert(ctx != nil) + pop(& ctx.stack.view ) + pop(& ctx.stack.position) + pop(& ctx.stack.zoom ) +} +auto_pop_vpz :: #force_inline proc( ctx : ^Context, camera : VPZ_Transform ) { + assert(ctx != nil) + pop(& ctx.stack.view ) + pop(& ctx.stack.position) + pop(& ctx.stack.zoom ) +} + +//#endregion("scoping") + +//#region("draw_list generation") + +get_cursor_pos :: #force_inline proc "contextless" ( ctx : Context ) -> Vec2 { return ctx.cursor_pos } + +// Does nothing when view is 1 or 0. +get_snapped_position :: #force_inline proc "contextless" ( position : Vec2, view : Vec2 ) -> (snapped_position : Vec2) { + snap_quotient := 1 / Vec2 { max(view.x, 1), max(view.y, 1) } + should_snap := view * snap_quotient + + snapped_position = position + snapped_position.x = ceil(position.x * view.x) * snap_quotient.x + snapped_position.y = ceil(position.y * view.y) * snap_quotient.y + + snapped_position *= should_snap + snapped_position.x = max(snapped_position.x, position.x) + snapped_position.y = max(snapped_position.y, position.y) + return snapped_position +} + +set_alpha_scalar :: #force_inline proc( ctx : ^Context, scalar : f32 ) { assert(ctx != nil); ctx.alpha_sharpen = scalar } +set_colour :: #force_inline proc( ctx : ^Context, colour : RGBAN ) { assert(ctx != nil); ctx.colour = colour } +set_px_scalar :: #force_inline proc( ctx : ^Context, scalar : f32 ) { assert(ctx != nil); ctx.px_scalar = scalar } + +set_snap_glyph_shape_position :: #force_inline proc( ctx : ^Context, should_snap : b32 ) { + assert(ctx != nil) + ctx.shaper_ctx.snap_glyph_position = should_snap +} + +set_snap_glyph_render_height :: #force_inline proc( ctx : ^Context, should_snap : b32 ) { + assert(ctx != nil) + ctx.glyph_buffer.snap_glyph_height = cast(f32) i32(should_snap) +} + +// Non-scoping context. The most fundamental interface-level draw shape procedure. +// (everything else is quality of life warppers) +@(optimization_mode="favor_size") +draw_text_shape_normalized_space :: #force_inline proc( ctx : ^Context, + font : Font_ID, + px_size : f32, + colour : RGBAN, + view : Vec2, + position : Vec2, + scale : Vec2, + zoom : f32, // TODO(Ed): Implement zoom support + shape : Shaped_Text +) +{ + profile(#procedure) + assert( ctx != nil ) + assert( font >= 0 && int(font) < len(ctx.entries) ) + + adjusted_position := get_snapped_position( position, view ) + + entry := ctx.entries[ font ] + + adjusted_colour := colour + adjusted_colour.a = 1.0 + ctx.alpha_sharpen + + target_px_size := px_size * ctx.px_scalar + target_scale := scale * (1 / ctx.px_scalar) + 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.px_scalar, + adjusted_colour, + entry, + target_px_size, + target_font_scale, + position, + target_scale, + ) +} + +// Non-scoping context. The most fundamental interface-level draw text procedure. +// (everything else is quality of life warppers) +@(optimization_mode = "favor_size") +draw_text_normalized_space :: proc( ctx : ^Context, + font : Font_ID, + px_size : f32, + colour : RGBAN, + view : Vec2, + position : Vec2, + scale : Vec2, + zoom : f32, // TODO(Ed): Implement Zoom support + text_utf8 : string +) +{ + profile(#procedure) + assert( ctx != nil ) + assert( font >= 0 && int(font) < len(ctx.entries) ) + assert( len(text_utf8) > 0 ) + + ctx.cursor_pos = {} + entry := ctx.entries[ font ] + + adjusted_position := get_snapped_position( position, view ) + + adjusted_colour := colour + adjusted_colour.a = 1.0 + ctx.alpha_sharpen + + // Does nothing when px_scalar is 1.0 + target_px_size := px_size * ctx.px_scalar + target_scale := scale * (1 / ctx.px_scalar) + target_font_scale := parser_scale( entry.parser_info, target_px_size ) + + shape := shaper_shape_text_cached( text_utf8, & ctx.shaper_ctx, & ctx.shape_cache, ctx.atlas, vec2(ctx.glyph_buffer.size), + font, + entry, + target_px_size, + target_font_scale, + shaper_shape_text_uncached_advanced + ) + ctx.cursor_pos = generate_shape_draw_list( & ctx.draw_list, shape, & ctx.atlas, & ctx.glyph_buffer, + ctx.px_scalar, + adjusted_colour, + entry, + target_px_size, + target_font_scale, + adjusted_position, + target_scale, + ) +} + +// Equivalent to draw_text_shape_normalized_space, however position's units is scaled to view and must be normalized. +@(optimization_mode="favor_size") +draw_text_shape_view_space :: #force_inline proc( ctx : ^Context, + font : Font_ID, + px_size : f32, + colour : RGBAN, + view : Vec2, + position : Vec2, + scale : Vec2, + zoom : f32, // TODO(Ed): Implement zoom support + shape : Shaped_Text +) +{ + profile(#procedure) + assert( ctx != nil ) + assert( font >= 0 && int(font) < len(ctx.entries) ) + + norm_position := position * (1 / view) + + view := view; view.x = max(view.x, 1); view.y = max(view.y, 1) + adjusted_position := get_snapped_position( norm_position, view ) + + entry := ctx.entries[ font ] + + adjusted_colour := colour + adjusted_colour.a = 1.0 + ctx.alpha_sharpen + + target_px_size := px_size * ctx.px_scalar + target_scale := scale * (1 / ctx.px_scalar) + 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.px_scalar, + adjusted_colour, + entry, + target_px_size, + target_font_scale, + position, + target_scale, + ) +} + +// Equivalent to draw_text_normalized_space, however position's units is scaled to view and must be normalized. +@(optimization_mode = "favor_size") +draw_text_view_space :: proc(ctx : ^Context, + font : Font_ID, + px_size : f32, + colour : RGBAN, + view : Vec2, + view_position : Vec2, + scale : Vec2, + zoom : f32, // TODO(Ed): Implement Zoom support + text_utf8 : string +) +{ + profile(#procedure) + assert( ctx != nil ) + assert( font >= 0 && int(font) < len(ctx.entries) ) + assert( len(text_utf8) > 0 ) + + ctx.cursor_pos = {} + entry := ctx.entries[ font ] + + norm_position := view_position * (1 / view) + + adjusted_position := get_snapped_position( norm_position, view ) + + adjusted_colour := colour + adjusted_colour.a = 1.0 + ctx.alpha_sharpen + + // Does nothing when px_scalar is 1.0 + target_px_size := px_size * ctx.px_scalar + target_scale := scale * (1 / ctx.px_scalar) + target_font_scale := parser_scale( entry.parser_info, target_px_size ) + + shape := shaper_shape_text_cached( text_utf8, & ctx.shaper_ctx, & ctx.shape_cache, ctx.atlas, vec2(ctx.glyph_buffer.size), + font, + entry, + target_px_size, + target_font_scale, + shaper_shape_text_uncached_advanced + ) + ctx.cursor_pos = generate_shape_draw_list( & ctx.draw_list, shape, & ctx.atlas, & ctx.glyph_buffer, + ctx.px_scalar, + adjusted_colour, + entry, + target_px_size, + target_font_scale, + adjusted_position, + target_scale, + ) +} + +@(optimization_mode = "favor_size") +draw_shape :: proc( ctx : ^Context, position, scale : Vec2, shape : Shaped_Text ) +{ + profile(#procedure) + assert( ctx != nil ) + + stack := & ctx.stack + assert(len(stack.font) > 0) + assert(len(stack.view) > 0) + assert(len(stack.colour) > 0) + assert(len(stack.position) > 0) + assert(len(stack.scale) > 0) + assert(len(stack.font_size) > 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) + font := peek(stack.font) + assert( font >= 0 &&int(font) < len(ctx.entries) ) + + view := peek(stack.view); + + ctx.cursor_pos = {} + entry := ctx.entries[ font ] + + adjusted_colour := peek(stack.colour) + adjusted_colour.a = 1.0 + ctx.alpha_sharpen + + // TODO(Ed): Implement zoom for draw_text + zoom := peek(stack.zoom) + + absolute_position := peek(stack.position) + position + absolute_scale := peek(stack.scale) * scale + + adjusted_position := get_snapped_position( absolute_position, view ) + + px_size := peek(stack.font_size) + + // Does nothing when px_scalar is 1.0 + target_px_size := px_size * ctx.px_scalar + target_scale := absolute_scale * (1 / ctx.px_scalar) + 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.px_scalar, + adjusted_colour, + entry, + target_px_size, + target_font_scale, + adjusted_position, + target_scale, + ) +} + +@(optimization_mode = "favor_size") +draw_text :: proc( ctx : ^Context, position, scale : Vec2, text_utf8 : string ) +{ + profile(#procedure) + assert( ctx != nil ) + assert( len(text_utf8) > 0 ) + + stack := & ctx.stack + assert(len(stack.font) > 0) + assert(len(stack.view) > 0) + assert(len(stack.colour) > 0) + assert(len(stack.position) > 0) + assert(len(stack.scale) > 0) + assert(len(stack.font_size) > 0) + assert(len(stack.zoom) > 0) + + font := peek(stack.font) + assert( font >= 0 &&int(font) < len(ctx.entries) ) + + view := peek(stack.view); + + ctx.cursor_pos = {} + entry := ctx.entries[ font ] + + adjusted_colour := peek(stack.colour) + adjusted_colour.a = 1.0 + ctx.alpha_sharpen + + // TODO(Ed): Implement zoom for draw_text + zoom := peek(stack.zoom) + + absolute_position := peek(stack.position) + position + absolute_scale := peek(stack.scale) * scale + + adjusted_position := get_snapped_position( absolute_position, view ) + + px_size := peek(stack.font_size) + + // Does nothing when px_scalar is 1.0 + target_px_size := px_size * ctx.px_scalar + target_scale := absolute_scale * (1 / ctx.px_scalar) + target_font_scale := parser_scale( entry.parser_info, target_px_size ) + + shape := shaper_shape_text_cached( text_utf8, & ctx.shaper_ctx, & ctx.shape_cache, ctx.atlas, vec2(ctx.glyph_buffer.size), + font, + entry, + target_px_size, + target_font_scale, + shaper_shape_text_uncached_advanced + ) + ctx.cursor_pos = generate_shape_draw_list( & ctx.draw_list, shape, & ctx.atlas, & ctx.glyph_buffer, + ctx.px_scalar, + adjusted_colour, + entry, + target_px_size, + target_font_scale, + adjusted_position, + target_scale, + ) +} + +get_draw_list :: #force_inline proc( ctx : ^Context, optimize_before_returning := true ) -> ^Draw_List { + assert( ctx != nil ) + if optimize_before_returning do optimize_draw_list( & ctx.draw_list, 0 ) + return & ctx.draw_list +} + +get_draw_list_layer :: #force_inline proc( ctx : ^Context, optimize_before_returning := true ) -> (vertices : []Vertex, indices : []u32, calls : []Draw_Call) { + 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 +} + +flush_draw_list :: #force_inline proc( ctx : ^Context ) { + assert( ctx != nil ) + clear_draw_list( & ctx.draw_list ) + ctx.draw_layer.vertices_offset = 0 + ctx.draw_layer.indices_offset = 0 + ctx.draw_layer.calls_offset = 0 +} + +flush_draw_list_layer :: #force_inline proc( ctx : ^Context ) { + assert( ctx != nil ) + ctx.draw_layer.vertices_offset = len(ctx.draw_list.vertices) + ctx.draw_layer.indices_offset = len(ctx.draw_list.indices) + ctx.draw_layer.calls_offset = len(ctx.draw_list.calls) +} + +//#endregion("draw_list generation") + +//#region("metrics") + +// The metrics follow the convention for providing their values unscaled from ctx.px_scalar +// Where its assumed when utilizing the draw_list generators or shaping procedures that the shape will be affected by it so it must be handled. +// If px_scalar is 1.0 no effect is done and its just redundant ops. + +measure_shape_size :: #force_inline proc( ctx : ^Context, shape : Shaped_Text ) -> (measured : Vec2) { + measured = shape.size * (1 / ctx.px_scalar) + return +} + +// Don't use this if you already have the shape instead use measure_shape_size +measure_text_size :: #force_inline proc( ctx : ^Context, font : Font_ID, px_size : f32, text_utf8 : string ) -> (measured : Vec2) +{ + // profile(#procedure) + assert( ctx != nil ) + assert( font >= 0 && int(font) < len(ctx.entries) ) + + entry := ctx.entries[font] + + downscale := 1 / ctx.px_scalar + target_px_size := px_size * ctx.px_scalar + target_font_scale := parser_scale( entry.parser_info, target_px_size ) + + shaped := shaper_shape_text_cached( text_utf8, + & ctx.shaper_ctx, + & ctx.shape_cache, + ctx.atlas, + vec2(ctx.glyph_buffer.size), + font, + entry, + target_px_size, + target_font_scale, + shaper_shape_text_uncached_advanced + ) + return shaped.size * downscale +} + +get_font_vertical_metrics :: #force_inline proc ( ctx : ^Context, font : Font_ID, px_size : f32 ) -> ( ascent, descent, line_gap : f32 ) +{ + assert( ctx != nil ) + assert( font >= 0 && int(font) < len(ctx.entries) ) + + entry := ctx.entries[ font ] + + font_scale := parser_scale( entry.parser_info, px_size ) + + ascent = font_scale * entry.ascent + descent = font_scale * entry.descent + line_gap = font_scale * entry.line_gap + return +} + +//#endregion("metrics") + +//#region("shaping") + +shape_text_latin :: #force_inline proc( ctx : ^Context, font : Font_ID, px_size : f32, text_utf8 : string ) -> Shaped_Text +{ + profile(#procedure) + assert( len(text_utf8) > 0 ) + entry := ctx.entries[ font ] + + target_px_size := px_size * ctx.px_scalar + target_font_scale := parser_scale( entry.parser_info, target_px_size ) + + return shaper_shape_text_cached( text_utf8, + & ctx.shaper_ctx, + & ctx.shape_cache, + ctx.atlas, + vec2(ctx.glyph_buffer.size), + font, + entry, + target_px_size, + target_font_scale, + shaper_shape_text_latin + ) +} + +shape_text_advanced :: #force_inline proc( ctx : ^Context, font : Font_ID, px_size : f32, text_utf8 : string ) -> Shaped_Text +{ + profile(#procedure) + assert( len(text_utf8) > 0 ) + entry := ctx.entries[ font ] + + target_px_size := px_size * ctx.px_scalar + target_font_scale := parser_scale( entry.parser_info, target_px_size ) + + return shaper_shape_text_cached( text_utf8, + & ctx.shaper_ctx, + & ctx.shape_cache, + ctx.atlas, + vec2(ctx.glyph_buffer.size), + font, + entry, + target_px_size, + target_font_scale, + shaper_shape_text_uncached_advanced + ) +} + +// User handled shaped text. Will not be cached +// @(disabled = true) +shape_text_latin_uncached :: #force_inline proc( ctx : ^Context, font : Font_ID, px_size: f32, text_utf8 : string, shape : ^Shaped_Text ) +{ + profile(#procedure) + assert( len(text_utf8) > 0 ) + entry := ctx.entries[ font ] + + target_px_size := px_size * ctx.px_scalar + target_font_scale := parser_scale( entry.parser_info, target_px_size ) + + shaper_shape_text_latin(& ctx.shaper_ctx, + ctx.atlas, + vec2(ctx.glyph_buffer.size), + entry, + target_px_size, + target_font_scale, + text_utf8, + shape + ) + return +} + +// User handled shaped text. Will not be cached +// @(disabled = true) +shape_text_advanced_uncached :: #force_inline proc( ctx : ^Context, font : Font_ID, px_size: f32, text_utf8 : string, shape : ^Shaped_Text ) +{ + profile(#procedure) + assert( len(text_utf8) > 0 ) + entry := ctx.entries[ font ] + + target_px_size := px_size * ctx.px_scalar + target_font_scale := parser_scale( entry.parser_info, target_px_size ) + + shaper_shape_text_uncached_advanced(& ctx.shaper_ctx, + ctx.atlas, + vec2(ctx.glyph_buffer.size), + entry, + target_px_size, + target_font_scale, + text_utf8, + shape + ) + return +} + +//#endregion("shaping")