WIP: Updating public repo with latest version

This commit is contained in:
2025-01-10 09:07:26 -05:00
parent d329327555
commit 36cc557975
13 changed files with 2724 additions and 1709 deletions

View File

@@ -1,28 +1,52 @@
# VE Font Cache : Odin Port # VE Font Cache
https://github.com/user-attachments/assets/b74f1ec1-f980-45df-b604-d6b7d87d40ff 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. 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. See: [docs/Readme.md](docs/Readme.md) for the library's interface.
## Building ## Building
See [scripts/Readme.md](scripts/Readme.md) for building examples or utilizing the provided backends. 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. The library depends on 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). Note: 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.
![image](https://github.com/user-attachments/assets/2f6c0b36-179c-42fe-8903-7640ae3c209e) ![image](https://github.com/user-attachments/assets/2f6c0b36-179c-42fe-8903-7640ae3c209e)

View File

@@ -1,5 +1,7 @@
# Interface # Interface
TODO: OUTDATED
Notes Notes
--- ---

View File

@@ -94,11 +94,11 @@ function build-SokolBackendDemo
# $build_args += $flag_micro_architecture_native # $build_args += $flag_micro_architecture_native
$build_args += $flag_use_separate_modules $build_args += $flag_use_separate_modules
$build_args += $flag_thread_count + $CoreCount_Physical $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_minimal
# $build_args += $flag_optimize_speed # $build_args += $flag_optimize_speed
$build_args += $falg_optimize_aggressive # $build_args += $falg_optimize_aggressive
# $build_args += $flag_debug $build_args += $flag_debug
$build_args += $flag_pdb_name + $pdb $build_args += $flag_pdb_name + $pdb
$build_args += $flag_subsystem + 'windows' $build_args += $flag_subsystem + 'windows'
# $build_args += ($flag_extra_linker_flags + $linker_args ) # $build_args += ($flag_extra_linker_flags + $linker_args )
@@ -111,6 +111,8 @@ function build-SokolBackendDemo
# $build_args += $flag_sanitize_address # $build_args += $flag_sanitize_address
# $build_args += $flag_sanitize_memory # $build_args += $flag_sanitize_memory
Write-Host $build_args
Invoke-WithColorCodedOutput { & $odin_compiler $build_args } Invoke-WithColorCodedOutput { & $odin_compiler $build_args }
} }
build-SokolBackendDemo build-SokolBackendDemo

View File

@@ -1,22 +1,36 @@
package vefontcache package vefontcache
/* /* Note(Ed):
The choice was made to keep the LRU cache implementation as close to the original as possible. 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" import "base:runtime"
Pool_ListIter :: i32 // 16-bit hashing was attempted, however it seems to get collisions with djb8_hash_16
Pool_ListValue :: u64
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, prev : Pool_ListIter,
next : Pool_ListIter, next : Pool_ListIter,
value : Pool_ListValue, value : V_Type,
} }
Pool_List :: struct { Pool_List :: struct( $V_Type : typeid) {
items : [dynamic]Pool_List_Item, items : [dynamic]Pool_List_Item(V_Type),
free_list : [dynamic]Pool_ListIter, free_list : [dynamic]Pool_ListIter,
front : Pool_ListIter, front : Pool_ListIter,
back : Pool_ListIter, back : Pool_ListIter,
@@ -25,10 +39,10 @@ Pool_List :: struct {
dbg_name : string, 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 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") assert( error == .None, "VEFontCache.pool_list_init : Failed to allocate items array")
resize( & pool.items, capacity ) resize( & pool.items, capacity )
@@ -39,130 +53,130 @@ pool_list_init :: proc( pool : ^Pool_List, capacity : i32, dbg_name : string = "
pool.capacity = capacity pool.capacity = capacity
pool.dbg_name = dbg_name pool.dbg_name = dbg_name
using pool
for id in 0 ..< capacity { for id in 0 ..< pool.capacity {
free_list[id] = i32(id) pool.free_list[id] = Pool_ListIter(id)
items[id] = { pool.items[id] = {
prev = -1, prev = -1,
next = -1, next = -1,
} }
} }
front = -1 pool.front = -1
back = -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.items)
delete( pool.free_list) 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.items, allocator )
reload_array( & pool.free_list, allocator ) reload_array( & pool.free_list, allocator )
} }
pool_list_clear :: proc( pool: ^Pool_List ) { pool_list_clear :: proc( pool: ^Pool_List($V_Type) )
using pool {
clear(& items) clear(& pool.items)
clear(& free_list) clear(& pool.free_list)
resize( & pool.items, cap(pool.items) ) resize( & pool.items, cap(pool.items) )
resize( & pool.free_list, cap(pool.free_list) ) resize( & pool.free_list, cap(pool.free_list) )
for id in 0 ..< capacity { for id in 0 ..< pool.capacity {
free_list[id] = i32(id) pool.free_list[id] = Pool_ListIter(id)
items[id] = { pool.items[id] = {
prev = -1, prev = -1,
next = -1, next = -1,
} }
} }
front = -1 pool.front = -1
back = -1 pool.back = -1
size = 0 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 pool.size >= pool.capacity do return
if size >= capacity do return
length := len(free_list) length := len(pool.free_list)
assert( length > 0 ) 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 != "" { if pool.dbg_name != "" {
logf("pool_list: back %v", id) logf("pool_list: back %v", id)
} }
pop( & free_list ) pop( & pool.free_list )
items[ id ].prev = -1 pool.items[ id ].prev = -1
items[ id ].next = front pool.items[ id ].next = pool.front
items[ id ].value = value pool.items[ id ].value = value
if pool.dbg_name != "" { if pool.dbg_name != "" {
logf("pool_list: pushed %v into id %v", value, id) logf("pool_list: pushed %v into id %v", value, id)
} }
if front != -1 do items[ front ].prev = id if pool.front != -1 do pool.items[ pool.front ].prev = id
if back == -1 do back = id if pool.back == -1 do pool.back = id
front = id pool.front = id
size += 1 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 pool.size <= 0 do return
if size <= 0 do return assert( iter >= 0 && iter < Pool_ListIter(pool.capacity) )
assert( iter >= 0 && iter < i32(capacity) ) assert( len(pool.free_list) == int(pool.capacity - pool.size) )
assert( len(free_list) == int(capacity - size) )
iter_node := & items[ iter ] iter_node := & pool.items[ iter ]
prev := iter_node.prev prev := iter_node.prev
next := iter_node.next next := iter_node.next
if iter_node.prev != -1 do items[ prev ].next = iter_node.next if iter_node.prev != -1 do pool.items[ prev ].next = iter_node.next
if iter_node.next != -1 do items[ next ].prev = iter_node.prev if iter_node.next != -1 do pool.items[ next ].prev = iter_node.prev
if front == iter do front = iter_node.next if pool.front == iter do pool.front = iter_node.next
if back == iter do back = iter_node.prev if pool.back == iter do pool.back = iter_node.prev
iter_node.prev = -1 iter_node.prev = -1
iter_node.next = -1 iter_node.next = -1
iter_node.value = 0 iter_node.value = 0
append( & free_list, iter ) append( & pool.free_list, iter )
size -= 1 pool.size -= 1
if size == 0 { if pool.size == 0 {
back = -1 pool.back = -1
front = -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] item.prev = -1
if item.prev != -1 do items[ item.prev ].next = item.next item.next = pool.front
if item.next != -1 do items[ item.next ].prev = item.prev pool.items[ pool.front ].prev = iter
if back == iter do back = item.prev pool.front = iter
item.prev = -1
item.next = front
items[ front ].prev = iter
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 ) assert( pool.back != - 1 )
value := pool.items[ pool.back ].value value := pool.items[ pool.back ].value
return 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 if pool.size <= 0 do return 0
assert( pool.back != -1 ) assert( pool.back != -1 )
@@ -171,69 +185,69 @@ pool_list_pop_back :: #force_inline proc( pool : ^Pool_List ) -> Pool_ListValue
return value return value
} }
LRU_Link :: struct { LRU_Link :: struct #packed {
pad_top : u64,
value : i32, value : i32,
ptr : Pool_ListIter, ptr : Pool_ListIter,
pad_bottom : u64,
} }
LRU_Cache :: struct { LRU_Cache :: struct( $Key_Type : typeid ) {
capacity : i32, capacity : i32,
num : i32, num : i32,
table : map[u64]LRU_Link, table : map[Key_Type]LRU_Link,
key_queue : Pool_List, 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 error : Allocator_Error
cache.capacity = capacity 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") assert( error == .None, "VEFontCache.lru_init : Failed to allocate cache's table")
pool_list_init( & cache.key_queue, capacity, dbg_name = dbg_name ) 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 ) pool_list_free( & cache.key_queue )
delete( cache.table ) 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 ) reload_map( & cache.table, allocator )
pool_list_reload( & cache.key_queue, 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 ) pool_list_clear( & cache.key_queue )
clear(& cache.table) clear(& cache.table)
cache.num = 0 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] link, success := cache.table[key]
return link, success 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 { if link, ok := &cache.table[ key ]; ok {
pool_list_move_to_front(&cache.key_queue, link.ptr) pool_list_move_to_front(&cache.key_queue, link.ptr)
return link.value return link.value
} }
return -1 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 { 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 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 ) iter, success := lru_find( cache, key, must_find )
if success == false { if success == false {
return -1 return -1
@@ -241,8 +255,10 @@ lru_peek :: #force_inline proc ( cache : ^LRU_Cache, key : u64, must_find := fal
return iter.value 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 { if link, ok := & cache.table[ key ]; ok {
pool_list_move_to_front( & cache.key_queue, link.ptr ) pool_list_move_to_front( & cache.key_queue, link.ptr )
link.value = value link.value = value
@@ -265,8 +281,8 @@ lru_put :: #force_inline proc( cache : ^LRU_Cache, key : u64, value : i32 ) -> u
return evict return evict
} }
lru_refresh :: proc( cache : ^LRU_Cache, key : u64 ) { lru_refresh :: proc( cache : ^LRU_Cache($Key_Type), key : Key_Type ) {
link, success := lru_find( cache, key ) link, success := lru_find( cache ^, key )
pool_list_erase( & cache.key_queue, link.ptr ) pool_list_erase( & cache.key_queue, link.ptr )
pool_list_push_front( & cache.key_queue, key ) pool_list_push_front( & cache.key_queue, key )
link.ptr = cache.key_queue.front link.ptr = cache.key_queue.front

View File

@@ -1,127 +1,127 @@
package vefontcache 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 { Atlas_Region_Kind :: enum u8 {
None = 0x00, None = 0x00,
A = 0x41, A = 0x01,
B = 0x42, B = 0x02,
C = 0x43, C = 0x03,
D = 0x44, D = 0x04,
E = 0x45, E = 0x05,
Ignore = 0xFF, // ve_fontcache_cache_glyph_to_atlas uses a -1 value in clear draw call Ignore = 0xFF, // ve_fontcache_cache_glyph_to_atlas uses a -1 value in clear draw call
} }
Atlas_Region :: struct { // Note(Ed): Using 16 bit hash had collision failures and no observable performance improvement (tried several 16-bit hashers)
state : LRU_Cache, Atlas_Key :: u32
width : 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.
height : i32, /* 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, size : Vec2i,
capacity : Vec2i, capacity : Vec2i,
offset : Vec2i, offset : Vec2i,
slot_size : Vec2i,
next_idx : i32, 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 { Atlas :: struct {
width : i32,
height : i32,
glyph_padding : i32, // Padding to add to bounds_<width/height>_scaled for choosing which atlas region.
glyph_over_scalar : f32, // Scalar to apply to bounds_<width/height>_scaled for choosing which atlas region.
region_a : Atlas_Region, region_a : Atlas_Region,
region_b : Atlas_Region, region_b : Atlas_Region,
region_c : Atlas_Region, region_c : Atlas_Region,
region_d : Atlas_Region, region_d : Atlas_Region,
regions : [5] ^Atlas_Region,
glyph_padding : f32, // Padding to add to bounds_<width/height>_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) // Hahser for the atlas.
{ @(optimization_mode="favor_size")
switch region 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 )
case .A: font := font
size.x = f32(atlas.region_a.width) glyph_index := glyph_index
size.y = f32(atlas.region_a.height) px_size := px_size
djb8_hash( & lru_code, to_bytes( & font) )
position.x = cast(f32) (( local_idx % atlas.region_a.capacity.x ) * atlas.region_a.width) djb8_hash( & lru_code, to_bytes( & glyph_index ) )
position.y = cast(f32) (( local_idx / atlas.region_a.capacity.x ) * atlas.region_a.height) djb8_hash( & lru_code, to_bytes( & px_size ) )
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:
}
return 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) { size = vec2(region.slot_size)
return .None, nil, {}
}
bounds_0, bounds_1 := parser_get_glyph_box(&entry.parser_info, glyph_index) position.x = cast(f32) (( local_idx % region.capacity.x ) * region.slot_size.x)
bounds_width := f32(bounds_1.x - bounds_0.x) position.y = cast(f32) (( local_idx / region.capacity.x ) * region.slot_size.y)
bounds_height := f32(bounds_1.y - bounds_0.y)
atlas := & ctx.atlas position.x += f32(region.offset.x)
glyph_buffer := & ctx.glyph_buffer position.y += f32(region.offset.y)
glyph_padding := f32( atlas.glyph_padding ) * 2 return
}
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) @(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)
// Use a lookup table for faster region selection {
region_lookup := [4]struct { kind: Atlas_Region_Kind, region: ^Atlas_Region } { // profile(#procedure)
{ .A, & atlas.region_a }, glyph_padding_dbl := atlas.glyph_padding * 2
{ .B, & atlas.region_b }, padded_bounds := bounds_size_scaled + glyph_padding_dbl
{ .C, & atlas.region_c },
{ .D, & atlas.region_d }, 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)
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 return cast(Atlas_Region_Kind) kind
} }
if bounds_width_scaled <= glyph_buffer.width \ if padded_bounds.x <= glyph_buffer_size.x && padded_bounds.y <= glyph_buffer_size.y{
&& bounds_height_scaled <= glyph_buffer.height { return .E
over_sample = \ }
bounds_width_scaled <= glyph_buffer.width / 2 && return .None
bounds_height_scaled <= glyph_buffer.height / 2 ? \ }
{2.0, 2.0} \
: {1.0, 1.0} // Grab an atlas LRU cache slot.
return .E, nil, over_sample @(optimization_mode="favor_size")
} atlas_reserve_slot :: #force_inline proc ( region : ^Atlas_Region, lru_code : Atlas_Key ) -> (atlas_index : i32)
return .None, nil, {} {
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
} }

File diff suppressed because it is too large Load Diff

View File

@@ -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
}
}

View File

@@ -1,16 +1,58 @@
package vefontcache package vefontcache
/*
Didn't want to splinter this into more files..
Just a bunch of utilities.
*/
import "base:runtime" import "base:runtime"
import "core:simd" import "core:simd"
import "core:math" import "core:math"
import core_log "core:log" 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 Vec2 :: [2]f32
Vec2i :: [2]i32 Vec2i :: [2]i32
Vec2_64 :: [2]f64 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_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_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) }} 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 ) core_log.logf( level, fmt, ..args, location = loc )
} }
reload_array :: proc( self : ^[dynamic]$Type, allocator : Allocator ) { @(optimization_mode="favor_size")
raw := transmute( ^runtime.Raw_Dynamic_Array) self to_glyph_buffer_space :: #force_inline proc "contextless" ( #no_alias position, scale : ^Vec2, size : Vec2 )
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
{ {
if glyph_index == 0 do return true pos := position^
if parser_is_glyph_empty( & entry.parser_info, glyph_index ) do return true scale_32 := scale^
return false
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 ) { @(optimization_mode="favor_size")
ctx.temp_codepoint_seen[lru_code] = true to_target_space :: #force_inline proc "contextless" ( #no_alias position, scale : ^Vec2, size : Vec2 )
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 )
{ {
when USE_F64_PRECISION_ON_X_FORM_OPS quotient : Vec2 = 1.0 / size
{ (position^) *= quotient
pos_64 := vec2_64_from_vec2(position^) (scale^) *= quotient
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
}
} }
textspace_x_form :: #force_inline proc "contextless" ( position, scale : ^Vec2, size : Vec2 ) USE_MANUAL_SIMD_FOR_BEZIER_OPS :: true
{
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
when ! USE_MANUAL_SIMD_FOR_BEZIER_OPS when ! USE_MANUAL_SIMD_FOR_BEZIER_OPS
{ {
@@ -132,11 +112,6 @@ when ! USE_MANUAL_SIMD_FOR_BEZIER_OPS
// ve_fontcache_eval_bezier (quadratic) // ve_fontcache_eval_bezier (quadratic)
eval_point_on_bezier3 :: #force_inline proc "contextless" ( p0, p1, p2 : Vec2, alpha : f32 ) -> Vec2 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_start := (1 - alpha) * (1 - alpha)
weight_control := 2.0 * (1 - alpha) * alpha weight_control := 2.0 * (1 - alpha) * alpha
weight_end := alpha * alpha weight_end := alpha * alpha
@@ -154,12 +129,6 @@ when ! USE_MANUAL_SIMD_FOR_BEZIER_OPS
// ve_fontcache_eval_bezier (cubic) // ve_fontcache_eval_bezier (cubic)
eval_point_on_bezier4 :: #force_inline proc "contextless" ( p0, p1, p2, p3 : Vec2, alpha : f32 ) -> Vec2 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_start := (1 - alpha) * (1 - alpha) * (1 - alpha)
weight_c_a := 3 * (1 - alpha) * (1 - alpha) * alpha weight_c_a := 3 * (1 - alpha) * (1 - alpha) * alpha
weight_c_b := 3 * (1 - alpha) * alpha * alpha weight_c_b := 3 * (1 - alpha) * alpha * alpha
@@ -178,14 +147,17 @@ else
{ {
Vec2_SIMD :: simd.f32x4 Vec2_SIMD :: simd.f32x4
@(optimization_mode="favor_size")
vec2_to_simd :: #force_inline proc "contextless" (v: Vec2) -> Vec2_SIMD { vec2_to_simd :: #force_inline proc "contextless" (v: Vec2) -> Vec2_SIMD {
return Vec2_SIMD{v.x, v.y, 0, 0} return Vec2_SIMD{v.x, v.y, 0, 0}
} }
@(optimization_mode="favor_size")
simd_to_vec2 :: #force_inline proc "contextless" (v: Vec2_SIMD) -> Vec2 { simd_to_vec2 :: #force_inline proc "contextless" (v: Vec2_SIMD) -> Vec2 {
return Vec2{ simd.extract(v, 0), simd.extract(v, 1) } 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 eval_point_on_bezier3 :: #force_inline proc "contextless" (p0, p1, p2: Vec2, alpha: f32) -> Vec2
{ {
simd_p0 := vec2_to_simd(p0) simd_p0 := vec2_to_simd(p0)
@@ -209,6 +181,7 @@ else
return simd_to_vec2(result) 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 eval_point_on_bezier4 :: #force_inline proc "contextless" (p0, p1, p2, p3: Vec2, alpha: f32) -> Vec2
{ {
simd_p0 := vec2_to_simd(p0) simd_p0 := vec2_to_simd(p0)

View File

@@ -2,11 +2,17 @@ package vefontcache
/* /*
Notes: 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. Freetype isn't really supported and its not a high priority (pretty sure its too slow).
That interface is not exposed from this parser but could be added to parser_init. ~~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" import "base:runtime"
@@ -14,7 +20,7 @@ import "core:c"
import "core:math" import "core:math"
import "core:slice" import "core:slice"
import stbtt "vendor:stb/truetype" import stbtt "vendor:stb/truetype"
import freetype "thirdparty:freetype" // import freetype "thirdparty:freetype"
Parser_Kind :: enum u32 { Parser_Kind :: enum u32 {
STB_TrueType, STB_TrueType,
@@ -26,7 +32,7 @@ Parser_Font_Info :: struct {
kind : Parser_Kind, kind : Parser_Kind,
using _ : struct #raw_union { using _ : struct #raw_union {
stbtt_info : stbtt.fontinfo, stbtt_info : stbtt.fontinfo,
freetype_info : freetype.Face // freetype_info : freetype.Face
}, },
data : []byte, data : []byte,
} }
@@ -52,20 +58,20 @@ Parser_Glyph_Shape :: [dynamic]Parser_Glyph_Vertex
Parser_Context :: struct { Parser_Context :: struct {
kind : Parser_Kind, kind : Parser_Kind,
ft_library : freetype.Library, // ft_library : freetype.Library,
} }
parser_init :: proc( ctx : ^Parser_Context, kind : Parser_Kind ) parser_init :: proc( ctx : ^Parser_Context, kind : Parser_Kind )
{ {
switch kind // switch kind
{ // {
case .Freetype: // case .Freetype:
result := freetype.init_free_type( & ctx.ft_library ) // result := freetype.init_free_type( & ctx.ft_library )
assert( result == freetype.Error.Ok, "VEFontCache.parser_init: Failed to initialize freetype" ) // assert( result == freetype.Error.Ok, "VEFontCache.parser_init: Failed to initialize freetype" )
case .STB_TrueType: // case .STB_TrueType:
// Do nothing intentional // Do nothing intentional
} // }
ctx.kind = kind ctx.kind = kind
} }
@@ -74,24 +80,23 @@ parser_shutdown :: proc( ctx : ^Parser_Context ) {
// TODO(Ed): Implement // 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 // switch ctx.kind
{ // {
case .Freetype: // case .Freetype:
when ODIN_OS == .Windows { // when ODIN_OS == .Windows {
error := freetype.new_memory_face( ctx.ft_library, raw_data(data), cast(i32) len(data), 0, & font.freetype_info ) // error_status := freetype.new_memory_face( ctx.ft_library, raw_data(data), cast(i32) len(data), 0, & font.freetype_info )
if error != .Ok do return // if error != .Ok do error = true
} // }
else when ODIN_OS == .Linux { // else when ODIN_OS == .Linux {
error := freetype.new_memory_face( ctx.ft_library, raw_data(data), cast(i64) len(data), 0, & font.freetype_info ) // error := freetype.new_memory_face( ctx.ft_library, raw_data(data), cast(i64) len(data), 0, & font.freetype_info )
if error != .Ok do return // if error_status != .Ok do error = true
} // }
case .STB_TrueType: // case .STB_TrueType:
success := stbtt.InitFont( & font.stbtt_info, raw_data(data), 0 ) error = ! stbtt.InitFont( & font.stbtt_info, raw_data(data), 0 )
if ! success do return // }
}
font.label = label font.label = label
font.data = data 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 ) parser_unload_font :: proc( font : ^Parser_Font_Info )
{ {
switch font.kind { // switch font.kind {
case .Freetype: // case .Freetype:
error := freetype.done_face( font.freetype_info ) // error := freetype.done_face( font.freetype_info )
assert( error == .Ok, "VEFontCache.parser_unload_font: Failed to unload freetype face" ) // assert( error == .Ok, "VEFontCache.parser_unload_font: Failed to unload freetype face" )
case .STB_TrueType: // case .STB_TrueType:
// Do Nothing // 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 // profile(#procedure)
{ // switch font.kind
case .Freetype: // {
when ODIN_OS == .Windows { // case .Freetype:
glyph_index = transmute(Glyph) freetype.get_char_index( font.freetype_info, transmute(u32) codepoint ) // 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 ) // 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 return
// }
case .STB_TrueType: // return Glyph(-1)
glyph_index = transmute(Glyph) stbtt.FindGlyphIndex( & font.stbtt_info, codepoint )
return
}
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 // switch font.kind
{ // {
case .Freetype: // case .Freetype:
delete(shape) // delete(shape)
case .STB_TrueType: // case .STB_TrueType:
stbtt.FreeShape( & font.stbtt_info, transmute( [^]stbtt.vertex) raw_data(shape) ) 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 // switch font.kind
{ // {
case .Freetype: // case .Freetype:
glyph_index : Glyph // glyph_index : Glyph
when ODIN_OS == .Windows { // when ODIN_OS == .Windows {
glyph_index = transmute(Glyph) freetype.get_char_index( font.freetype_info, transmute(u32) codepoint ) // glyph_index = transmute(Glyph) freetype.get_char_index( font.freetype_info, transmute(u32) codepoint )
} // }
else when ODIN_OS == .Linux { // else when ODIN_OS == .Linux {
glyph_index = transmute(Glyph) freetype.get_char_index( font.freetype_info, cast(u64) codepoint ) // glyph_index = transmute(Glyph) freetype.get_char_index( font.freetype_info, cast(u64) codepoint )
} // }
if glyph_index != 0 // if glyph_index != 0
{ // {
freetype.load_glyph( font.freetype_info, c.uint(codepoint), { .No_Bitmap, .No_Hinting, .No_Scale } ) // freetype.load_glyph( font.freetype_info, c.uint(codepoint), { .No_Bitmap, .No_Hinting, .No_Scale } )
advance = i32(font.freetype_info.glyph.advance.x) >> 6 // advance = i32(font.freetype_info.glyph.advance.x) >> 6
to_left_side_glyph = i32(font.freetype_info.glyph.metrics.hori_bearing_x) >> 6 // to_left_side_glyph = i32(font.freetype_info.glyph.metrics.hori_bearing_x) >> 6
} // }
else // else
{ // {
advance = 0 // advance = 0
to_left_side_glyph = 0 // to_left_side_glyph = 0
} // }
case .STB_TrueType: // case .STB_TrueType:
stbtt.GetCodepointHMetrics( & font.stbtt_info, codepoint, & advance, & to_left_side_glyph ) stbtt.GetCodepointHMetrics( font.stbtt_info, codepoint, & advance, & to_left_side_glyph )
} // }
return 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 // switch font.kind
{ // {
case .Freetype: // case .Freetype:
prev_glyph_index : Glyph // prev_glyph_index : Glyph
glyph_index : Glyph // glyph_index : Glyph
when ODIN_OS == .Windows { // when ODIN_OS == .Windows {
prev_glyph_index = transmute(Glyph) freetype.get_char_index( font.freetype_info, transmute(u32) prev_codepoint ) // 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 ) // glyph_index = transmute(Glyph) freetype.get_char_index( font.freetype_info, transmute(u32) codepoint )
} // }
else when ODIN_OS == .Linux { // else when ODIN_OS == .Linux {
prev_glyph_index = transmute(Glyph) freetype.get_char_index( font.freetype_info, cast(u64) prev_codepoint ) // 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 ) // glyph_index = transmute(Glyph) freetype.get_char_index( font.freetype_info, cast(u64) codepoint )
} // }
if prev_glyph_index != 0 && glyph_index != 0 // if prev_glyph_index != 0 && glyph_index != 0
{ // {
kerning : freetype.Vector // kerning : freetype.Vector
font.freetype_info.driver.clazz.get_kerning( font.freetype_info, transmute(u32) prev_codepoint, transmute(u32) codepoint, & kerning ) // font.freetype_info.driver.clazz.get_kerning( font.freetype_info, transmute(u32) prev_codepoint, transmute(u32) codepoint, & kerning )
} // }
case .STB_TrueType: // case .STB_TrueType:
kern := stbtt.GetCodepointKernAdvance( & font.stbtt_info, prev_codepoint, codepoint ) kern := stbtt.GetCodepointKernAdvance( font.stbtt_info, prev_codepoint, codepoint )
return kern 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 // switch font.kind
{ // {
case .Freetype: // case .Freetype:
info := font.freetype_info // info := font.freetype_info
ascent = i32(info.ascender) // ascent = i32(info.ascender)
descent = i32(info.descender) // descent = i32(info.descender)
line_gap = i32(info.height) - (ascent - descent) // line_gap = i32(info.height) - (ascent - descent)
case .STB_TrueType: // case .STB_TrueType:
stbtt.GetFontVMetrics( & font.stbtt_info, & ascent, & descent, & line_gap ) stbtt.GetFontVMetrics( font.stbtt_info, & ascent, & descent, & line_gap )
} // }
return 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 // profile(#procedure)
{ bounds_0, bounds_1 : Vec2i
case .Freetype:
freetype.load_glyph( font.freetype_info, c.uint(glyph_index), { .No_Bitmap, .No_Hinting, .No_Scale } )
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)} // metrics := font.freetype_info.glyph.metrics
bounds_1 = {i32(metrics.hori_bearing_x + metrics.width), i32(metrics.hori_bearing_y)}
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 x0, y0, x1, y1 : i32
success := cast(bool) stbtt.GetGlyphBox( & font.stbtt_info, i32(glyph_index), & x0, & y0, & x1, & y1 ) success := cast(bool) stbtt.GetGlyphBox( font.stbtt_info, i32(glyph_index), & x0, & y0, & x1, & y1 )
assert( success ) // assert( success )
bounds_0 = { i32(x0), i32(y0) } bounds_0 = { x0, y0 }
bounds_1 = { i32(x1), i32(y1) } bounds_1 = { x1, y1 }
} // }
bounds = { vec2(bounds_0), vec2(bounds_1) }
return 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 // switch font.kind
{ // {
case .Freetype: // case .Freetype:
// TODO(Ed): Don't do this, going a completely different route for handling shapes. // // TODO(Ed): Don't do this, going a completely different route for handling shapes.
// This abstraction fails to be time-saving or performant. // // This abstraction fails to be time-saving or performant.
case .STB_TrueType: // case .STB_TrueType:
stb_shape : [^]stbtt.vertex 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 := transmute( ^runtime.Raw_Dynamic_Array) & shape
shape_raw.data = stb_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.cap = int(nverts)
shape_raw.allocator = runtime.nil_allocator() shape_raw.allocator = runtime.nil_allocator()
error = Allocator_Error.None error = Allocator_Error.None
return // 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 // switch font.kind
{ // {
case .Freetype: // case .Freetype:
error := freetype.load_glyph( font.freetype_info, cast(u32) glyph_index, { .No_Bitmap, .No_Hinting, .No_Scale } ) // error := freetype.load_glyph( font.freetype_info, cast(u32) glyph_index, { .No_Bitmap, .No_Hinting, .No_Scale } )
if error == .Ok // if error == .Ok
{ // {
if font.freetype_info.glyph.format == .Outline { // if font.freetype_info.glyph.format == .Outline {
return font.freetype_info.glyph.outline.n_points == 0 // return font.freetype_info.glyph.outline.n_points == 0
} // }
else if font.freetype_info.glyph.format == .Bitmap { // else if font.freetype_info.glyph.format == .Bitmap {
return font.freetype_info.glyph.bitmap.width == 0 && font.freetype_info.glyph.bitmap.rows == 0; // return font.freetype_info.glyph.bitmap.width == 0 && font.freetype_info.glyph.bitmap.rows == 0;
} // }
} // }
return false // return false
case .STB_TrueType: // case .STB_TrueType:
return stbtt.IsGlyphEmpty( & font.stbtt_info, cast(c.int) glyph_index ) return stbtt.IsGlyphEmpty( font.stbtt_info, cast(c.int) glyph_index )
} // }
return false // 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 ? \ // profile(#procedure)
parser_scale_for_pixel_height( font, -size ) \ // size_scale := size < 0.0 ? parser_scale_for_pixel_height( font, -size ) : parser_scale_for_mapping_em_to_pixels( 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 )
// size_scale = 1.0
return size_scale 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 { // switch font.kind {
case .Freetype: // case .Freetype:
freetype.set_pixel_sizes( font.freetype_info, 0, cast(u32) size ) // freetype.set_pixel_sizes( font.freetype_info, 0, cast(u32) size )
size_scale := size / cast(f32)font.freetype_info.units_per_em // size_scale := size / cast(f32)font.freetype_info.units_per_em
return size_scale // return size_scale
case.STB_TrueType: // case.STB_TrueType:
return stbtt.ScaleForPixelHeight( & font.stbtt_info, size ) return stbtt.ScaleForPixelHeight( font.stbtt_info, size )
} // }
return 0 // 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 { // switch font.kind {
case .Freetype: // case .Freetype:
Inches_To_CM :: cast(f32) 2.54 // Inches_To_CM :: cast(f32) 2.54
Points_Per_CM :: cast(f32) 28.3465 // Points_Per_CM :: cast(f32) 28.3465
CM_Per_Point :: cast(f32) 1.0 / DPT_DPCM // CM_Per_Point :: cast(f32) 1.0 / DPT_DPCM
CM_Per_Pixel :: cast(f32) 1.0 / DPT_PPCM // 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_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_PPCM :: cast(f32) 96.0 * Inches_To_CM // 243.84 pixels per cm
DPT_DPI :: cast(f32) 72.0 // DPT_DPI :: cast(f32) 72.0
// TODO(Ed): Don't assume the dots or pixels per inch. // // TODO(Ed): Don't assume the dots or pixels per inch.
system_dpi :: DPT_DPI // system_dpi :: DPT_DPI
FT_Font_Size_Point_Unit :: 1.0 / 64.0 // FT_Font_Size_Point_Unit :: 1.0 / 64.0
FT_Point_10 :: 64.0 // FT_Point_10 :: 64.0
points_per_em := (size / system_dpi ) * DPT_DPI // 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 ) // 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) // size_scale := f32(f64(size) / cast(f64) font.freetype_info.units_per_em)
return size_scale // return size_scale
case .STB_TrueType: // case .STB_TrueType:
return stbtt.ScaleForMappingEmToPixels( & font.stbtt_info, size ) return stbtt.ScaleForMappingEmToPixels( font.stbtt_info, size )
} // }
return 0 // return 0
} }

View File

@@ -1,7 +1,10 @@
package vefontcache package vefontcache
import "base:builtin"
resize_soa_non_zero :: non_zero_resize_soa
import "base:runtime"
import "core:hash" import "core:hash"
fnv64a :: hash.fnv64a ginger16 :: hash.ginger16
import "core:math" import "core:math"
ceil_f16 :: math.ceil_f16 ceil_f16 :: math.ceil_f16
ceil_f16le :: math.ceil_f16le ceil_f16le :: math.ceil_f16le
@@ -34,6 +37,7 @@ import "core:mem"
arena_allocator :: mem.arena_allocator arena_allocator :: mem.arena_allocator
arena_init :: mem.arena_init arena_init :: mem.arena_init
import "core:slice" import "core:slice"
import "core:unicode"
//#region("Proc overload mappings") //#region("Proc overload mappings")
@@ -43,6 +47,10 @@ append :: proc {
append_elem_string, append_elem_string,
} }
append_soa :: proc {
append_soa_elem
}
ceil :: proc { ceil :: proc {
math.ceil_f16, math.ceil_f16,
math.ceil_f16le, math.ceil_f16le,
@@ -58,8 +66,8 @@ ceil :: proc {
} }
clear :: proc { clear :: proc {
clear_dynamic_array, builtin.clear_dynamic_array,
clear_map, builtin.clear_map,
} }
floor :: proc { floor :: proc {
@@ -80,16 +88,39 @@ fill :: proc {
slice.fill, slice.fill,
} }
max :: proc {
linalg.max_single,
linalg.max_double,
}
make :: proc { make :: proc {
make_dynamic_array, builtin.make_dynamic_array,
make_dynamic_array_len, builtin.make_dynamic_array_len,
make_dynamic_array_len_cap, builtin.make_dynamic_array_len_cap,
make_map, builtin.make_slice,
make_map_cap, 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 :: proc {
resize_dynamic_array, builtin.resize_dynamic_array,
}
size :: proc {
size_range2,
} }
vec2 :: proc { vec2 :: proc {
@@ -105,4 +136,22 @@ vec2_64 :: proc {
vec2_64_from_vec2, 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") //#endregion("Proc overload mappings")

View File

@@ -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
}
}

View File

@@ -6,11 +6,61 @@ Note(Ed): The only reason I didn't directly use harfbuzz is because hamza exists
import "core:c" import "core:c"
import "thirdparty:harfbuzz" 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 { Shaper_Kind :: enum {
Naive = 0, Latin = 0,
Harfbuzz = 1, 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 { Shaper_Context :: struct {
hb_buffer : harfbuzz.Buffer, hb_buffer : harfbuzz.Buffer,
@@ -18,6 +68,7 @@ Shaper_Context :: struct {
adv_snap_small_font_threshold : f32, adv_snap_small_font_threshold : f32,
} }
// Only used with harbuzz for now. Resolved during load_font for a font Entry.
Shaper_Info :: struct { Shaper_Info :: struct {
blob : harfbuzz.Blob, blob : harfbuzz.Blob,
face : harfbuzz.Face, 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 info.blob = harfbuzz.blob_create( raw_data(data), cast(c.uint) len(data), harfbuzz.Memory_Mode.READONLY, user_data, nil )
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 )
face = harfbuzz.face_create( blob, 0 ) info.font = harfbuzz.font_create( info.face )
font = harfbuzz.font_create( face )
return return
} }
shaper_unload_font :: proc( ctx : ^Shaper_Info ) shaper_unload_font :: #force_inline proc( info : ^Shaper_Info )
{ {
using ctx if info.blob != nil do harfbuzz.font_destroy( info.font )
if blob != nil do harfbuzz.font_destroy( font ) if info.face != nil do harfbuzz.face_destroy( info.face )
if face != nil do harfbuzz.face_destroy( face ) if info.blob != nil do harfbuzz.blob_destroy( info.blob )
if blob != nil do harfbuzz.blob_destroy( blob )
} }
shaper_shape_from_text :: proc( ctx : ^Shaper_Context, info : ^Shaper_Info, output :^Shaped_Text, text_utf8 : string, // Recommended shaper. Very performant.
ascent, descent, line_gap : i32, size, size_scale : f32 ) // 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 current_script := harfbuzz.Script.UNKNOWN
hb_ucfunc := harfbuzz.unicode_funcs_get_default() hb_ucfunc := harfbuzz.unicode_funcs_get_default()
harfbuzz.buffer_clear_contents( ctx.hb_buffer ) 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) max_line_width := f32(0)
line_count := 1 line_count := 1
line_height := ((ascent - descent + line_gap) * size_scale) line_height := ((ascent - descent + line_gap) * font_scale)
position, vertical_position : f32 position : Vec2
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, @(optimization_mode="favor_size")
ascent, descent, line_gap, size, size_scale: f32, shape_run :: proc( output : ^Shaped_Text,
snap_shape_pos : b32, adv_snap_small_font_threshold : f32 ) 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. // Set script and direction. We use the system's default langauge.
// script = HB_SCRIPT_LATIN // script = HB_SCRIPT_LATIN
harfbuzz.buffer_set_script( buffer, script ) 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. // Perform the actual shaping of this run using HarfBuzz.
harfbuzz.buffer_set_content_type( buffer, harfbuzz.Buffer_Content_Type.UNICODE ) 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. // Loop over glyphs and append to output buffer.
glyph_count : u32 glyph_count : u32
glyph_infos := harfbuzz.buffer_get_glyph_infos( buffer, & glyph_count ) glyph_infos := harfbuzz.buffer_get_glyph_infos( buffer, & glyph_count )
glyph_positions := harfbuzz.buffer_get_glyph_positions( 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 for index : i32; index < i32(glyph_count); index += 1
{ {
hb_glyph := glyph_infos[ index ] hb_glyph := glyph_infos[ index ]
hb_gposition := glyph_positions[ index ] hb_gposition := glyph_positions[ index ]
glyph_id := cast(Glyph) hb_glyph.codepoint glyph := cast(Glyph) hb_glyph.codepoint
if hb_glyph.cluster > 0 if hb_glyph.cluster > 0
{ {
(max_line_width^) = max( max_line_width^, position^ ) (max_line_width^) = max( max_line_width^, position.x )
(position^) = 0.0 position.x = 0.0
(vertical_position^) -= line_height position.y -= line_height
(vertical_position^) = floor(vertical_position^ + 0.5) position.y = floor(position.y)
(line_count^) += 1 (line_count^) += 1
continue 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 ) glyph_pos := position^
offset := Vec2 { f32(hb_gposition.x_offset), f32(hb_gposition.y_offset) } * font_scale
pos := position^ glyph_pos += offset
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
if snap_shape_pos { if snap_shape_pos {
pos = ceil(pos) glyph_pos = ceil(glyph_pos)
v_pos = ceil(v_pos)
} }
append( & output.positions, Vec2 {pos, v_pos})
(position^) += f32(hb_gposition.x_advance) * size_scale advance := Vec2 {
(vertical_position^) += f32(hb_gposition.y_advance) * size_scale f32(hb_gposition.x_advance) * font_scale,
(max_line_width^) = max(max_line_width^, position^) 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 = position^
output.end_cursor_pos.y = vertical_position^
harfbuzz.buffer_clear_contents( buffer ) 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. // End current run since we've encountered a script change.
shape_run( shape_run( output,
ctx.hb_buffer, current_script, info.font, output, entry,
& position, & vertical_position, & max_line_width, & line_count, ctx.hb_buffer,
ascent, descent, line_gap, size, size_scale, current_script,
ctx.snap_glyph_position, ctx.adv_snap_small_font_threshold & 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 ) harfbuzz.buffer_add( ctx.hb_buffer, hb_codepoint, codepoint == '\n' ? 1 : 0 )
current_script = script current_script = script
} }
// End the last run if needed // End the last run if needed
shape_run( shape_run( output,
ctx.hb_buffer, current_script, info.font, output, entry,
& position, & vertical_position, & max_line_width, & line_count, ctx.hb_buffer,
ascent, descent, line_gap, size, size_scale, current_script,
ctx.snap_glyph_position, ctx.adv_snap_small_font_threshold & 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 // Set the final size
output.size.x = max_line_width output.size.x = max_line_width
output.size.y = f32(line_count) * line_height output.size.y = f32(line_count) * line_height
return 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
}

File diff suppressed because it is too large Load Diff