mirror of
https://github.com/Ed94/VEFontCache-Odin.git
synced 2025-08-04 22:22:43 -07:00
initial commit
Code lifted from sectr prototype Still quite a bit todo before its considered "done"
This commit is contained in:
21
LICENSE.md
Normal file
21
LICENSE.md
Normal file
@@ -0,0 +1,21 @@
|
||||
VEFontCache Odin Port
|
||||
Copyright 2024 Edward R. Gonzalez
|
||||
|
||||
This project is based on Vertex Engine GPU Font Cache
|
||||
by Xi Chen (https://github.com/hypernewbie/VEFontCache). It has been substantially
|
||||
rewritten and redesigned for the Odin programming language.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||
including without limitation the rights to use, copy, modify, merge, publish, distribute,
|
||||
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
|
||||
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
|
||||
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
239
LRU.odin
Normal file
239
LRU.odin
Normal file
@@ -0,0 +1,239 @@
|
||||
package VEFontCache
|
||||
|
||||
/*
|
||||
The choice was made to keep the LRU cache implementation as close to the original as possible.
|
||||
*/
|
||||
|
||||
import "base:runtime"
|
||||
|
||||
PoolListIter :: i32
|
||||
PoolListValue :: u64
|
||||
|
||||
PoolListItem :: struct {
|
||||
prev : PoolListIter,
|
||||
next : PoolListIter,
|
||||
value : PoolListValue,
|
||||
}
|
||||
|
||||
PoolList :: struct {
|
||||
items : [dynamic]PoolListItem,
|
||||
free_list : [dynamic]PoolListIter,
|
||||
front : PoolListIter,
|
||||
back : PoolListIter,
|
||||
size : u32,
|
||||
capacity : u32,
|
||||
dbg_name : string,
|
||||
}
|
||||
|
||||
pool_list_init :: proc( pool : ^PoolList, capacity : u32, dbg_name : string = "" )
|
||||
{
|
||||
error : AllocatorError
|
||||
pool.items, error = make( [dynamic]PoolListItem, u64(capacity) )
|
||||
assert( error == .None, "VEFontCache.pool_list_init : Failed to allocate items array")
|
||||
resize( & pool.items, capacity )
|
||||
|
||||
pool.free_list, error = make( [dynamic]PoolListIter, u64(capacity) )
|
||||
assert( error == .None, "VEFontCache.pool_list_init : Failed to allocate free_list array")
|
||||
resize( & pool.free_list, capacity )
|
||||
|
||||
pool.capacity = capacity
|
||||
|
||||
pool.dbg_name = dbg_name
|
||||
using pool
|
||||
|
||||
for id in 0 ..< capacity {
|
||||
free_list[id] = i32(id)
|
||||
items[id] = {
|
||||
prev = -1,
|
||||
next = -1,
|
||||
}
|
||||
}
|
||||
|
||||
front = -1
|
||||
back = -1
|
||||
}
|
||||
|
||||
pool_list_free :: proc( pool : ^PoolList )
|
||||
{
|
||||
// TODO(Ed): Implement
|
||||
}
|
||||
|
||||
pool_list_reload :: proc( pool : ^PoolList, allocator : Allocator )
|
||||
{
|
||||
reload_array( & pool.items, allocator )
|
||||
reload_array( & pool.free_list, allocator )
|
||||
}
|
||||
|
||||
pool_list_push_front :: proc( pool : ^PoolList, value : PoolListValue )
|
||||
{
|
||||
using pool
|
||||
if size >= capacity do return
|
||||
|
||||
length := len(free_list)
|
||||
assert( length > 0 )
|
||||
assert( length == int(capacity - size) )
|
||||
|
||||
id := free_list[ len(free_list) - 1 ]
|
||||
if pool.dbg_name != "" {
|
||||
logf("pool_list: back %v", id)
|
||||
}
|
||||
pop( & free_list )
|
||||
items[ id ].prev = -1
|
||||
items[ id ].next = front
|
||||
items[ id ].value = value
|
||||
if pool.dbg_name != "" {
|
||||
logf("pool_list: pushed %v into id %v", value, id)
|
||||
}
|
||||
|
||||
if front != -1 do items[ front ].prev = id
|
||||
if back == -1 do back = id
|
||||
front = id
|
||||
size += 1
|
||||
}
|
||||
|
||||
pool_list_erase :: proc( pool : ^PoolList, iter : PoolListIter )
|
||||
{
|
||||
using pool
|
||||
if size <= 0 do return
|
||||
assert( iter >= 0 && iter < i32(capacity) )
|
||||
assert( len(free_list) == int(capacity - size) )
|
||||
|
||||
iter_node := & items[ iter ]
|
||||
prev := iter_node.prev
|
||||
next := iter_node.next
|
||||
|
||||
if iter_node.prev != -1 do items[ prev ].next = iter_node.next
|
||||
if iter_node.next != -1 do items[ next ].prev = iter_node.prev
|
||||
|
||||
if front == iter do front = iter_node.next
|
||||
if back == iter do back = iter_node.prev
|
||||
|
||||
iter_node.prev = -1
|
||||
iter_node.next = -1
|
||||
iter_node.value = 0
|
||||
append( & free_list, iter )
|
||||
|
||||
size -= 1
|
||||
if size == 0 {
|
||||
back = -1
|
||||
front = -1
|
||||
}
|
||||
}
|
||||
|
||||
pool_list_peek_back :: #force_inline proc ( pool : ^PoolList ) -> PoolListValue {
|
||||
assert( pool.back != - 1 )
|
||||
value := pool.items[ pool.back ].value
|
||||
return value
|
||||
}
|
||||
|
||||
pool_list_pop_back :: #force_inline proc( pool : ^PoolList ) -> PoolListValue {
|
||||
if pool.size <= 0 do return 0
|
||||
assert( pool.back != -1 )
|
||||
|
||||
value := pool.items[ pool.back ].value
|
||||
pool_list_erase( pool, pool.back )
|
||||
return value
|
||||
}
|
||||
|
||||
LRU_Link :: struct {
|
||||
pad_top : u64,
|
||||
value : i32,
|
||||
ptr : PoolListIter,
|
||||
pad_bottom : u64,
|
||||
}
|
||||
|
||||
LRU_Cache :: struct {
|
||||
capacity : u32,
|
||||
num : u32,
|
||||
table : map[u64]LRU_Link,
|
||||
key_queue : PoolList,
|
||||
}
|
||||
|
||||
LRU_init :: proc( cache : ^LRU_Cache, capacity : u32, dbg_name : string = "" ) {
|
||||
error : AllocatorError
|
||||
cache.capacity = capacity
|
||||
cache.table, error = make( map[u64]LRU_Link, uint(capacity) )
|
||||
assert( error == .None, "VEFontCache.LRU_init : Failed to allocate cache's table")
|
||||
|
||||
pool_list_init( & cache.key_queue, capacity, dbg_name = dbg_name )
|
||||
}
|
||||
|
||||
LRU_free :: proc( cache : ^LRU_Cache )
|
||||
{
|
||||
// TODO(Ed): Implement
|
||||
}
|
||||
|
||||
LRU_reload :: #force_inline proc( cache : ^LRU_Cache, allocator : Allocator )
|
||||
{
|
||||
reload_map( & cache.table, allocator )
|
||||
pool_list_reload( & cache.key_queue, allocator )
|
||||
}
|
||||
|
||||
LRU_hash_key :: #force_inline proc( key : u64 ) -> ( hash : u64 ) {
|
||||
bytes := transmute( [8]byte ) key
|
||||
hash = fnv64a( bytes[:] )
|
||||
return
|
||||
}
|
||||
|
||||
LRU_find :: #force_inline proc "contextless" ( cache : ^LRU_Cache, key : u64, must_find := false ) -> (LRU_Link, bool) {
|
||||
link, success := cache.table[key]
|
||||
return link, success
|
||||
}
|
||||
|
||||
LRU_get :: #force_inline proc( cache : ^LRU_Cache, key : u64 ) -> i32 {
|
||||
iter, success := LRU_find( cache, key )
|
||||
if success == false {
|
||||
return -1
|
||||
}
|
||||
LRU_refresh( cache, key )
|
||||
return iter.value
|
||||
}
|
||||
|
||||
LRU_get_next_evicted :: #force_inline proc ( cache : ^LRU_Cache ) -> u64
|
||||
{
|
||||
if cache.key_queue.size >= cache.capacity {
|
||||
evict := pool_list_peek_back( & cache.key_queue )
|
||||
return evict
|
||||
}
|
||||
return 0xFFFFFFFFFFFFFFFF
|
||||
}
|
||||
|
||||
LRU_peek :: #force_inline proc ( cache : ^LRU_Cache, key : u64, must_find := false ) -> i32 {
|
||||
iter, success := LRU_find( cache, key, must_find )
|
||||
if success == false {
|
||||
return -1
|
||||
}
|
||||
return iter.value
|
||||
}
|
||||
|
||||
LRU_put :: #force_inline proc ( cache : ^LRU_Cache, key : u64, value : i32 ) -> u64
|
||||
{
|
||||
iter, success := cache.table[key]
|
||||
if success {
|
||||
LRU_refresh( cache, key )
|
||||
iter.value = value
|
||||
return key
|
||||
}
|
||||
|
||||
evict := key
|
||||
if cache.key_queue.size >= cache.capacity {
|
||||
evict = pool_list_pop_back( & cache.key_queue )
|
||||
delete_key( & cache.table, evict )
|
||||
cache.num -= 1
|
||||
}
|
||||
|
||||
pool_list_push_front( & cache.key_queue, key )
|
||||
cache.table[key] = LRU_Link {
|
||||
value = value,
|
||||
ptr = cache.key_queue.front
|
||||
}
|
||||
cache.num += 1
|
||||
return evict
|
||||
}
|
||||
|
||||
LRU_refresh :: proc( cache : ^LRU_Cache, key : u64 ) {
|
||||
link, success := LRU_find( cache, key )
|
||||
pool_list_erase( & cache.key_queue, link.ptr )
|
||||
pool_list_push_front( & cache.key_queue, key )
|
||||
link.ptr = cache.key_queue.front
|
||||
}
|
29
Readme.md
Normal file
29
Readme.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# VE Font Cache : Odin Port
|
||||
|
||||
This is a port of the library base on [fork](https://github.com/hypernewbie/VEFontCache)
|
||||
|
||||
Its original purpose was for use in game engines, however its rendeirng quality and performance is more than adequate for many other applications.
|
||||
|
||||
See: [docs/Readme.md](docs/Readme.md) for the library's interface
|
||||
|
||||
TODO (Making it a more idiomatic library):
|
||||
|
||||
* Setup freetype, harfbuzz, depedency management within the library
|
||||
|
||||
TODO Documentation:
|
||||
|
||||
* Pureref outline of draw_text exectuion
|
||||
* Markdown general documentation
|
||||
|
||||
TODO Content:
|
||||
|
||||
* Port over the original demo utilizing sokol libraries instead
|
||||
* Provide a sokol_gfx backend package
|
||||
|
||||
TODO Additional Features:
|
||||
|
||||
* Support for freetype
|
||||
* Support for harfbuzz
|
||||
* Ability to set a draw transform, viewport and projection
|
||||
* By default the library's position is in unsigned normalized render space
|
||||
* Allow curve_quality to be set on a per-font basis
|
504
VEFontCache.odin
Normal file
504
VEFontCache.odin
Normal file
@@ -0,0 +1,504 @@
|
||||
/*
|
||||
A port of (https://github.com/hypernewbie/VEFontCache) to Odin.
|
||||
|
||||
Status:
|
||||
This port is heavily tied to the grime package in SectrPrototype.
|
||||
|
||||
Changes:
|
||||
- Font Parser & Glyph Shaper are abstracted to their own interface
|
||||
- Font Face parser info stored separately from entries
|
||||
- ve_fontcache_loadfile not ported (just use odin's core:os or os2), then call load_font
|
||||
- Macro defines have been made into runtime parameters
|
||||
*/
|
||||
package VEFontCache
|
||||
|
||||
import "base:runtime"
|
||||
|
||||
Advance_Snap_Smallfont_Size :: 0
|
||||
|
||||
FontID :: distinct i64
|
||||
Glyph :: distinct i32
|
||||
|
||||
Entry :: struct {
|
||||
parser_info : ParserFontInfo,
|
||||
shaper_info : ShaperInfo,
|
||||
id : FontID,
|
||||
used : b32,
|
||||
size : f32,
|
||||
size_scale : f32,
|
||||
}
|
||||
|
||||
Entry_Default :: Entry {
|
||||
id = 0,
|
||||
used = false,
|
||||
size = 24.0,
|
||||
size_scale = 1.0,
|
||||
}
|
||||
|
||||
Context :: struct {
|
||||
backing : Allocator,
|
||||
|
||||
parser_kind : ParserKind,
|
||||
parser_ctx : ParserContext,
|
||||
shaper_ctx : ShaperContext,
|
||||
|
||||
entries : [dynamic]Entry,
|
||||
|
||||
temp_path : [dynamic]Vec2,
|
||||
temp_codepoint_seen : map[u64]bool,
|
||||
temp_codepoint_seen_num : u32,
|
||||
|
||||
snap_width : u32,
|
||||
snap_height : u32,
|
||||
|
||||
colour : Colour,
|
||||
cursor_pos : Vec2,
|
||||
|
||||
// draw_cursor_pos : Vec2,
|
||||
|
||||
draw_layer : struct {
|
||||
vertices_offset : int,
|
||||
indices_offset : int,
|
||||
calls_offset : int,
|
||||
},
|
||||
|
||||
draw_list : DrawList,
|
||||
atlas : Atlas,
|
||||
glyph_buffer : GlyphDrawBuffer,
|
||||
shape_cache : ShapedTextCache,
|
||||
|
||||
curve_quality : u32,
|
||||
text_shape_adv : b32,
|
||||
|
||||
debug_print : b32,
|
||||
debug_print_verbose : b32,
|
||||
}
|
||||
|
||||
#region("lifetime")
|
||||
|
||||
InitAtlasRegionParams :: struct {
|
||||
width : u32,
|
||||
height : u32,
|
||||
}
|
||||
|
||||
InitAtlasParams :: struct {
|
||||
width : u32,
|
||||
height : u32,
|
||||
glyph_padding : u32,
|
||||
|
||||
region_a : InitAtlasRegionParams,
|
||||
region_b : InitAtlasRegionParams,
|
||||
region_c : InitAtlasRegionParams,
|
||||
region_d : InitAtlasRegionParams,
|
||||
}
|
||||
|
||||
InitAtlasParams_Default :: InitAtlasParams {
|
||||
width = 4096,
|
||||
height = 2048,
|
||||
glyph_padding = 4,
|
||||
|
||||
region_a = {
|
||||
width = 32,
|
||||
height = 32,
|
||||
},
|
||||
region_b = {
|
||||
width = 32,
|
||||
height = 64,
|
||||
},
|
||||
region_c = {
|
||||
width = 64,
|
||||
height = 64,
|
||||
},
|
||||
region_d = {
|
||||
width = 128,
|
||||
height = 128,
|
||||
}
|
||||
}
|
||||
|
||||
InitGlyphDrawParams :: struct {
|
||||
over_sample : Vec2,
|
||||
buffer_batch : u32,
|
||||
draw_padding : u32,
|
||||
}
|
||||
|
||||
InitGlyphDrawParams_Default :: InitGlyphDrawParams {
|
||||
over_sample = { 8, 8 },
|
||||
buffer_batch = 4,
|
||||
draw_padding = InitAtlasParams_Default.glyph_padding,
|
||||
}
|
||||
|
||||
InitShapeCacheParams :: struct {
|
||||
capacity : u32,
|
||||
reserve_length : u32,
|
||||
}
|
||||
|
||||
InitShapeCacheParams_Default :: InitShapeCacheParams {
|
||||
capacity = 1024,
|
||||
reserve_length = 1024,
|
||||
}
|
||||
|
||||
// ve_fontcache_init
|
||||
startup :: proc( ctx : ^Context, parser_kind : ParserKind,
|
||||
allocator := context.allocator,
|
||||
atlas_params := InitAtlasParams_Default,
|
||||
glyph_draw_params := InitGlyphDrawParams_Default,
|
||||
shape_cache_params := InitShapeCacheParams_Default,
|
||||
curve_quality : u32 = 3,
|
||||
entires_reserve : u32 = 512,
|
||||
temp_path_reserve : u32 = 512,
|
||||
temp_codepoint_seen_reserve : u32 = 512,
|
||||
)
|
||||
{
|
||||
assert( ctx != nil, "Must provide a valid context" )
|
||||
using ctx
|
||||
|
||||
ctx.backing = allocator
|
||||
context.allocator = ctx.backing
|
||||
|
||||
if curve_quality == 0 {
|
||||
curve_quality = 3
|
||||
}
|
||||
ctx.curve_quality = curve_quality
|
||||
|
||||
error : AllocatorError
|
||||
entries, error = make( [dynamic]Entry, entires_reserve )
|
||||
assert(error == .None, "VEFontCache.init : Failed to allocate entries")
|
||||
|
||||
temp_path, error = make( [dynamic]Vec2, temp_path_reserve )
|
||||
assert(error == .None, "VEFontCache.init : Failed to allocate temp_path")
|
||||
|
||||
temp_codepoint_seen, error = make( map[u64]bool, uint(temp_codepoint_seen_reserve) )
|
||||
assert(error == .None, "VEFontCache.init : Failed to allocate temp_path")
|
||||
|
||||
draw_list.vertices, error = make( [dynamic]Vertex, 4 * Kilobyte )
|
||||
assert(error == .None, "VEFontCache.init : Failed to allocate draw_list.vertices")
|
||||
|
||||
draw_list.indices, error = make( [dynamic]u32, 8 * Kilobyte )
|
||||
assert(error == .None, "VEFontCache.init : Failed to allocate draw_list.indices")
|
||||
|
||||
draw_list.calls, error = make( [dynamic]DrawCall, 512 )
|
||||
assert(error == .None, "VEFontCache.init : Failed to allocate draw_list.calls")
|
||||
|
||||
init_atlas_region :: proc( region : ^AtlasRegion, params : InitAtlasParams, region_params : InitAtlasRegionParams, factor : Vec2i, expected_cap : i32 ) {
|
||||
using region
|
||||
|
||||
next_idx = 0;
|
||||
width = region_params.width
|
||||
height = region_params.height
|
||||
size = {
|
||||
i32(params.width) / factor.x,
|
||||
i32(params.height) / factor.y,
|
||||
}
|
||||
capacity = {
|
||||
size.x / i32(width),
|
||||
size.y / i32(height),
|
||||
}
|
||||
assert( capacity.x * capacity.y == expected_cap )
|
||||
|
||||
error : AllocatorError
|
||||
// state.cache, error = make( HMapChained(LRU_Link), uint(capacity.x * capacity.y) )
|
||||
// assert( error == .None, "VEFontCache.init_atlas_region : Failed to allocate state.cache")
|
||||
LRU_init( & state, u32(capacity.x * capacity.y) )
|
||||
}
|
||||
init_atlas_region( & atlas.region_a, atlas_params, atlas_params.region_a, { 4, 2}, 1024 )
|
||||
init_atlas_region( & atlas.region_b, atlas_params, atlas_params.region_b, { 4, 2}, 512 )
|
||||
init_atlas_region( & atlas.region_c, atlas_params, atlas_params.region_c, { 4, 1}, 512 )
|
||||
init_atlas_region( & atlas.region_d, atlas_params, atlas_params.region_d, { 2, 1}, 256 )
|
||||
|
||||
atlas.width = atlas_params.width
|
||||
atlas.height = atlas_params.height
|
||||
atlas.glyph_padding = atlas_params.glyph_padding
|
||||
|
||||
atlas.region_a.offset = {0, 0}
|
||||
atlas.region_b.offset.x = 0
|
||||
atlas.region_b.offset.y = atlas.region_a.size.y
|
||||
atlas.region_c.offset.x = atlas.region_a.size.x
|
||||
atlas.region_c.offset.y = 0
|
||||
atlas.region_d.offset.x = i32(atlas.width) / 2
|
||||
atlas.region_d.offset.y = 0
|
||||
|
||||
LRU_init( & shape_cache.state, shape_cache_params.capacity )
|
||||
|
||||
shape_cache.storage, error = make( [dynamic]ShapedText, shape_cache_params.capacity )
|
||||
assert(error == .None, "VEFontCache.init : Failed to allocate shape_cache.storage")
|
||||
|
||||
for idx : u32 = 0; idx < shape_cache_params.capacity; idx += 1 {
|
||||
stroage_entry := & shape_cache.storage[idx]
|
||||
using stroage_entry
|
||||
glyphs, error = make( [dynamic]Glyph, shape_cache_params.reserve_length )
|
||||
assert( error == .None, "VEFontCache.init : Failed to allocate glyphs array for shape cache storage" )
|
||||
|
||||
positions, error = make( [dynamic]Vec2, shape_cache_params.reserve_length )
|
||||
assert( error == .None, "VEFontCache.init : Failed to allocate positions array for shape cache storage" )
|
||||
}
|
||||
|
||||
// Note(From original author): We can actually go over VE_FONTCACHE_GLYPHDRAW_BUFFER_BATCH batches due to smart packing!
|
||||
{
|
||||
using glyph_buffer
|
||||
over_sample = glyph_draw_params.over_sample
|
||||
batch = glyph_draw_params.buffer_batch
|
||||
width = atlas.region_d.width * u32(over_sample.x) * batch
|
||||
height = atlas.region_d.height * u32(over_sample.y)
|
||||
draw_padding = glyph_draw_params.draw_padding
|
||||
|
||||
draw_list.calls, error = make( [dynamic]DrawCall, cast(u64) glyph_draw_params.buffer_batch * 2 )
|
||||
assert( error == .None, "VEFontCache.init : Failed to allocate calls for draw_list" )
|
||||
|
||||
draw_list.indices, error = make( [dynamic]u32, cast(u64) glyph_draw_params.buffer_batch * 2 * 6 )
|
||||
assert( error == .None, "VEFontCache.init : Failed to allocate indices array for draw_list" )
|
||||
|
||||
draw_list.vertices, error = make( [dynamic]Vertex, glyph_draw_params.buffer_batch * 2 * 4 )
|
||||
assert( error == .None, "VEFontCache.init : Failed to allocate vertices array for draw_list" )
|
||||
|
||||
clear_draw_list.calls, error = make( [dynamic]DrawCall, cast(u64) glyph_draw_params.buffer_batch * 2 )
|
||||
assert( error == .None, "VEFontCache.init : Failed to allocate calls for calls for clear_draw_list" )
|
||||
|
||||
clear_draw_list.indices, error = make( [dynamic]u32, cast(u64) glyph_draw_params.buffer_batch * 2 * 4 )
|
||||
assert( error == .None, "VEFontCache.init : Failed to allocate calls for indices array for clear_draw_list" )
|
||||
|
||||
clear_draw_list.vertices, error = make( [dynamic]Vertex, glyph_draw_params.buffer_batch * 2 * 4 )
|
||||
assert( error == .None, "VEFontCache.init : Failed to allocate vertices array for clear_draw_list" )
|
||||
}
|
||||
|
||||
parser_init( & parser_ctx )
|
||||
shaper_init( & shaper_ctx )
|
||||
}
|
||||
|
||||
hot_reload :: proc( ctx : ^Context, allocator : Allocator )
|
||||
{
|
||||
assert( ctx != nil )
|
||||
ctx.backing = allocator
|
||||
context.allocator = ctx.backing
|
||||
using ctx
|
||||
|
||||
reload_array( & entries, allocator )
|
||||
reload_array( & temp_path, allocator )
|
||||
reload_map( & ctx.temp_codepoint_seen, allocator )
|
||||
|
||||
reload_array( & draw_list.vertices, allocator)
|
||||
reload_array( & draw_list.indices, allocator )
|
||||
reload_array( & draw_list.calls, allocator )
|
||||
|
||||
LRU_reload( & atlas.region_a.state, allocator)
|
||||
LRU_reload( & atlas.region_b.state, allocator)
|
||||
LRU_reload( & atlas.region_c.state, allocator)
|
||||
LRU_reload( & atlas.region_d.state, allocator)
|
||||
|
||||
LRU_reload( & shape_cache.state, allocator )
|
||||
for idx : u32 = 0; idx < u32(len(shape_cache.storage)); idx += 1 {
|
||||
stroage_entry := & shape_cache.storage[idx]
|
||||
using stroage_entry
|
||||
|
||||
reload_array( & glyphs, allocator )
|
||||
reload_array( & positions, allocator )
|
||||
}
|
||||
|
||||
reload_array( & glyph_buffer.draw_list.calls, allocator )
|
||||
reload_array( & glyph_buffer.draw_list.indices, allocator )
|
||||
reload_array( & glyph_buffer.draw_list.vertices, allocator )
|
||||
|
||||
reload_array( & glyph_buffer.clear_draw_list.calls, allocator )
|
||||
reload_array( & glyph_buffer.clear_draw_list.indices, allocator )
|
||||
reload_array( & glyph_buffer.clear_draw_list.vertices, allocator )
|
||||
|
||||
reload_array( & shape_cache.storage, allocator )
|
||||
LRU_reload( & shape_cache.state, allocator )
|
||||
}
|
||||
|
||||
// ve_foncache_shutdown
|
||||
shutdown :: proc( ctx : ^Context )
|
||||
{
|
||||
assert( ctx != nil )
|
||||
context.allocator = ctx.backing
|
||||
using ctx
|
||||
|
||||
for & entry in entries {
|
||||
unload_font( ctx, entry.id )
|
||||
}
|
||||
|
||||
shaper_shutdown( & shaper_ctx )
|
||||
|
||||
// TODO(Ed): Finish implementing, there is quite a few resource not released here.
|
||||
}
|
||||
|
||||
// ve_fontcache_load
|
||||
load_font :: proc( ctx : ^Context, label : string, data : []byte, size_px : f32 ) -> (font_id : FontID)
|
||||
{
|
||||
assert( ctx != nil )
|
||||
assert( len(data) > 0 )
|
||||
using ctx
|
||||
context.allocator = backing
|
||||
|
||||
id : i32 = -1
|
||||
|
||||
for index : i32 = 0; index < i32(len(entries)); index += 1 {
|
||||
if entries[index].used do continue
|
||||
id = index
|
||||
break
|
||||
}
|
||||
if id == -1 {
|
||||
append_elem( & entries, Entry {})
|
||||
id = cast(i32) len(entries) - 1
|
||||
}
|
||||
assert( id >= 0 && id < i32(len(entries)) )
|
||||
|
||||
entry := & entries[ id ]
|
||||
{
|
||||
using entry
|
||||
|
||||
parser_info = parser_load_font( & parser_ctx, label, data )
|
||||
// assert( parser_info != nil, "VEFontCache.load_font: Failed to load font info from parser" )
|
||||
|
||||
size = size_px
|
||||
size_scale = size_px < 0.0 ? \
|
||||
parser_scale_for_pixel_height( & parser_info, -size_px ) \
|
||||
: parser_scale_for_mapping_em_to_pixels( & parser_info, size_px )
|
||||
|
||||
used = true
|
||||
|
||||
shaper_info = shaper_load_font( & shaper_ctx, label, data, transmute(rawptr) id )
|
||||
// assert( shaper_info != nil, "VEFontCache.load_font: Failed to load font from shaper")
|
||||
}
|
||||
entry.id = FontID(id)
|
||||
ctx.entries[ id ].id = FontID(id)
|
||||
|
||||
font_id = FontID(id)
|
||||
return
|
||||
}
|
||||
|
||||
// ve_fontcache_unload
|
||||
unload_font :: proc( ctx : ^Context, font : FontID )
|
||||
{
|
||||
assert( ctx != nil )
|
||||
assert( font >= 0 && int(font) < len(ctx.entries) )
|
||||
context.allocator = ctx.backing
|
||||
|
||||
using ctx
|
||||
entry := & ctx.entries[ font ]
|
||||
entry.used = false
|
||||
|
||||
parser_unload_font( & entry.parser_info )
|
||||
shaper_unload_font( & entry.shaper_info )
|
||||
}
|
||||
|
||||
#endregion("lifetime")
|
||||
|
||||
#region("drawing")
|
||||
|
||||
// ve_fontcache_configure_snap
|
||||
configure_snap :: #force_inline proc( ctx : ^Context, snap_width, snap_height : u32 ) {
|
||||
assert( ctx != nil )
|
||||
ctx.snap_width = snap_width
|
||||
ctx.snap_height = snap_height
|
||||
}
|
||||
|
||||
get_cursor_pos :: #force_inline proc "contextless" ( ctx : ^Context ) -> Vec2 { return ctx.cursor_pos }
|
||||
set_colour :: #force_inline proc "contextless" ( ctx : ^Context, colour : Colour ) { ctx.colour = colour }
|
||||
|
||||
draw_text :: proc( ctx : ^Context, font : FontID, text_utf8 : string, position : Vec2, scale : Vec2 ) -> b32
|
||||
{
|
||||
// profile(#procedure)
|
||||
assert( ctx != nil )
|
||||
assert( font >= 0 && int(font) < len(ctx.entries) )
|
||||
|
||||
ctx.cursor_pos = {}
|
||||
|
||||
position := position
|
||||
snap_width := f32(ctx.snap_width)
|
||||
snap_height := f32(ctx.snap_height)
|
||||
if ctx.snap_width > 0 do position.x = cast(f32) cast(u32) (position.x * snap_width + 0.5) / snap_width
|
||||
if ctx.snap_height > 0 do position.y = cast(f32) cast(u32) (position.y * snap_height + 0.5) / snap_height
|
||||
|
||||
entry := & ctx.entries[ font ]
|
||||
|
||||
ChunkType :: enum u32 { Visible, Formatting }
|
||||
chunk_kind : ChunkType
|
||||
chunk_start : int = 0
|
||||
chunk_end : int = 0
|
||||
|
||||
text_utf8_bytes := transmute([]u8) text_utf8
|
||||
text_chunk : string
|
||||
|
||||
text_chunk = transmute(string) text_utf8_bytes[ : ]
|
||||
if len(text_chunk) > 0 {
|
||||
shaped := shape_text_cached( ctx, font, text_chunk, entry )
|
||||
ctx.cursor_pos = draw_text_shape( ctx, font, entry, shaped, position, scale, snap_width, snap_height )
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ve_fontcache_drawlist
|
||||
get_draw_list :: proc( ctx : ^Context, optimize_before_returning := true ) -> ^DrawList {
|
||||
assert( ctx != nil )
|
||||
if optimize_before_returning do optimize_draw_list( & ctx.draw_list, 0 )
|
||||
return & ctx.draw_list
|
||||
}
|
||||
|
||||
get_draw_list_layer :: proc( ctx : ^Context, optimize_before_returning := true ) -> (vertices : []Vertex, indices : []u32, calls : []DrawCall) {
|
||||
assert( ctx != nil )
|
||||
if optimize_before_returning do optimize_draw_list( & ctx.draw_list, ctx.draw_layer.calls_offset )
|
||||
vertices = ctx.draw_list.vertices[ ctx.draw_layer.vertices_offset : ]
|
||||
indices = ctx.draw_list.indices [ ctx.draw_layer.indices_offset : ]
|
||||
calls = ctx.draw_list.calls [ ctx.draw_layer.calls_offset : ]
|
||||
return
|
||||
}
|
||||
|
||||
// ve_fontcache_flush_drawlist
|
||||
flush_draw_list :: proc( ctx : ^Context ) {
|
||||
assert( ctx != nil )
|
||||
using ctx
|
||||
clear_draw_list( & draw_list )
|
||||
draw_layer.vertices_offset = 0
|
||||
draw_layer.indices_offset = 0
|
||||
draw_layer.calls_offset = 0
|
||||
}
|
||||
|
||||
flush_draw_list_layer :: proc( ctx : ^Context ) {
|
||||
assert( ctx != nil )
|
||||
using ctx
|
||||
draw_layer.vertices_offset = len(draw_list.vertices)
|
||||
draw_layer.indices_offset = len(draw_list.indices)
|
||||
draw_layer.calls_offset = len(draw_list.calls)
|
||||
}
|
||||
|
||||
#endregion("drawing")
|
||||
|
||||
#region("metrics")
|
||||
|
||||
measure_text_size :: proc( ctx : ^Context, font : FontID, text_utf8 : string ) -> (measured : Vec2)
|
||||
{
|
||||
// profile(#procedure)
|
||||
assert( ctx != nil )
|
||||
assert( font >= 0 && int(font) < len(ctx.entries) )
|
||||
|
||||
atlas := ctx.atlas
|
||||
entry := & ctx.entries[ font ]
|
||||
shaped := shape_text_cached( ctx, font, text_utf8, entry )
|
||||
padding := cast(f32) atlas.glyph_padding
|
||||
|
||||
for index : i32 = 0; index < i32(len(shaped.glyphs)); index += 1
|
||||
{
|
||||
glyph_index := shaped.glyphs[ index ]
|
||||
if is_empty( ctx, entry, glyph_index ) do continue
|
||||
|
||||
bounds_0, bounds_1 := parser_get_glyph_box( & entry.parser_info, glyph_index )
|
||||
bounds_size := bounds_1 - bounds_0
|
||||
|
||||
glyph_size := Vec2 { f32(bounds_size.x), f32(bounds_size.y) } * entry.size_scale
|
||||
measured.y = max(measured.y, glyph_size.y)
|
||||
}
|
||||
measured.x = shaped.end_cursor_pos.x
|
||||
return measured
|
||||
}
|
||||
|
||||
get_font_vertical_metrics :: #force_inline proc ( ctx : ^Context, font : FontID ) -> ( ascent, descent, line_gap : i32 )
|
||||
{
|
||||
assert( ctx != nil )
|
||||
assert( font >= 0 && int(font) < len(ctx.entries) )
|
||||
|
||||
entry := & ctx.entries[ font ]
|
||||
ascent, descent, line_gap = parser_get_font_vertical_metrics( & entry.parser_info )
|
||||
return
|
||||
}
|
||||
|
||||
#endregion("metrics")
|
153
atlas.odin
Normal file
153
atlas.odin
Normal file
@@ -0,0 +1,153 @@
|
||||
package VEFontCache
|
||||
|
||||
AtlasRegionKind :: enum u8 {
|
||||
None = 0x00,
|
||||
A = 0x41,
|
||||
B = 0x42,
|
||||
C = 0x43,
|
||||
D = 0x44,
|
||||
E = 0x45,
|
||||
Ignore = 0xFF, // ve_fontcache_cache_glyph_to_atlas uses a -1 value in clear draw call
|
||||
}
|
||||
|
||||
AtlasRegion :: struct {
|
||||
state : LRU_Cache,
|
||||
|
||||
width : u32,
|
||||
height : u32,
|
||||
|
||||
size : Vec2i,
|
||||
capacity : Vec2i,
|
||||
offset : Vec2i,
|
||||
|
||||
next_idx : u32,
|
||||
}
|
||||
|
||||
Atlas :: struct {
|
||||
width : u32,
|
||||
height : u32,
|
||||
|
||||
glyph_padding : u32,
|
||||
|
||||
region_a : AtlasRegion,
|
||||
region_b : AtlasRegion,
|
||||
region_c : AtlasRegion,
|
||||
region_d : AtlasRegion,
|
||||
}
|
||||
|
||||
atlas_bbox :: proc( atlas : ^Atlas, region : AtlasRegionKind, local_idx : i32 ) -> (position, size: Vec2)
|
||||
{
|
||||
switch region
|
||||
{
|
||||
case .A:
|
||||
size.x = f32(atlas.region_a.width)
|
||||
size.y = f32(atlas.region_a.height)
|
||||
|
||||
position.x = cast(f32) (( local_idx % atlas.region_a.capacity.x ) * i32(atlas.region_a.width))
|
||||
position.y = cast(f32) (( local_idx / atlas.region_a.capacity.x ) * i32(atlas.region_a.height))
|
||||
|
||||
position.x += f32(atlas.region_a.offset.x)
|
||||
position.y += f32(atlas.region_a.offset.y)
|
||||
|
||||
case .B:
|
||||
size.x = f32(atlas.region_b.width)
|
||||
size.y = f32(atlas.region_b.height)
|
||||
|
||||
position.x = cast(f32) (( local_idx % atlas.region_b.capacity.x ) * i32(atlas.region_b.width))
|
||||
position.y = cast(f32) (( local_idx / atlas.region_b.capacity.x ) * i32(atlas.region_b.height))
|
||||
|
||||
position.x += f32(atlas.region_b.offset.x)
|
||||
position.y += f32(atlas.region_b.offset.y)
|
||||
|
||||
case .C:
|
||||
size.x = f32(atlas.region_c.width)
|
||||
size.y = f32(atlas.region_c.height)
|
||||
|
||||
position.x = cast(f32) (( local_idx % atlas.region_c.capacity.x ) * i32(atlas.region_c.width))
|
||||
position.y = cast(f32) (( local_idx / atlas.region_c.capacity.x ) * i32(atlas.region_c.height))
|
||||
|
||||
position.x += f32(atlas.region_c.offset.x)
|
||||
position.y += f32(atlas.region_c.offset.y)
|
||||
|
||||
case .D:
|
||||
size.x = f32(atlas.region_d.width)
|
||||
size.y = f32(atlas.region_d.height)
|
||||
|
||||
position.x = cast(f32) (( local_idx % atlas.region_d.capacity.x ) * i32(atlas.region_d.width))
|
||||
position.y = cast(f32) (( local_idx / atlas.region_d.capacity.x ) * i32(atlas.region_d.height))
|
||||
|
||||
position.x += f32(atlas.region_d.offset.x)
|
||||
position.y += f32(atlas.region_d.offset.y)
|
||||
|
||||
case .Ignore: fallthrough
|
||||
case .None: fallthrough
|
||||
case .E:
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
decide_codepoint_region :: proc( ctx : ^Context, entry : ^Entry, glyph_index : Glyph
|
||||
) -> (region_kind : AtlasRegionKind, region : ^AtlasRegion, over_sample : Vec2)
|
||||
{
|
||||
if parser_is_glyph_empty( & entry.parser_info, glyph_index ) {
|
||||
region_kind = .None
|
||||
}
|
||||
|
||||
bounds_0, bounds_1 := parser_get_glyph_box( & entry.parser_info, glyph_index )
|
||||
bounds_width := f32(bounds_1.x - bounds_0.x)
|
||||
bounds_height := f32(bounds_1.y - bounds_0.y)
|
||||
|
||||
atlas := & ctx.atlas
|
||||
glyph_buffer := & ctx.glyph_buffer
|
||||
|
||||
glyph_padding := f32(atlas.glyph_padding) * 2
|
||||
|
||||
bounds_width_scaled := cast(u32) (bounds_width * entry.size_scale + glyph_padding)
|
||||
bounds_height_scaled := cast(u32) (bounds_height * entry.size_scale + glyph_padding)
|
||||
|
||||
if bounds_width_scaled <= atlas.region_a.width && bounds_height_scaled <= atlas.region_a.height
|
||||
{
|
||||
// Region A for small glyphs. These are good for things such as punctuation.
|
||||
region_kind = .A
|
||||
region = & atlas.region_a
|
||||
}
|
||||
else if bounds_width_scaled <= atlas.region_b.width && bounds_height_scaled <= atlas.region_b.height
|
||||
{
|
||||
// Region B for tall glyphs. These are good for things such as european alphabets.
|
||||
region_kind = .B
|
||||
region = & atlas.region_b
|
||||
}
|
||||
else if bounds_width_scaled <= atlas.region_c.width && bounds_height_scaled <= atlas.region_c.height
|
||||
{
|
||||
// Region C for big glyphs. These are good for things such as asian typography.
|
||||
region_kind = .C
|
||||
region = & atlas.region_c
|
||||
}
|
||||
else if bounds_width_scaled <= atlas.region_d.width && bounds_height_scaled <= atlas.region_d.height
|
||||
{
|
||||
// Region D for huge glyphs. These are good for things such as titles and 4k.
|
||||
region_kind = .D
|
||||
region = & atlas.region_d
|
||||
}
|
||||
else if bounds_width_scaled <= glyph_buffer.width && bounds_height_scaled <= glyph_buffer.height
|
||||
{
|
||||
// Region 'E' for massive glyphs. These are rendered uncached and un-oversampled.
|
||||
region_kind = .E
|
||||
region = nil
|
||||
if bounds_width_scaled <= glyph_buffer.width / 2 && bounds_height_scaled <= glyph_buffer.height / 2 {
|
||||
over_sample = { 2.0, 2.0 }
|
||||
}
|
||||
else {
|
||||
over_sample = { 1.0, 1.0 }
|
||||
}
|
||||
return
|
||||
}
|
||||
else {
|
||||
region_kind = .None
|
||||
return
|
||||
}
|
||||
|
||||
over_sample = glyph_buffer.over_sample
|
||||
assert(region != nil)
|
||||
return
|
||||
}
|
75
docs/Readme.md
Normal file
75
docs/Readme.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Interface
|
||||
|
||||
Notes
|
||||
---
|
||||
|
||||
Freetype implementation supports specifying a FT_Memory handle which is a pointer to a FT_MemoryRect. This can be used to define an allocator for the parser. Currently this library does not wrap this interface (yet). If using freetype its recommend to update `parser_init` with the necessary changes to wrap the context's backing allocator for freetype to utilize.
|
||||
|
||||
```c
|
||||
struct FT_MemoryRec_
|
||||
{
|
||||
void* user;
|
||||
FT_Alloc_Func alloc;
|
||||
FT_Free_Func free;
|
||||
FT_Realloc_Func realloc;
|
||||
};
|
||||
```
|
||||
|
||||
### startup
|
||||
|
||||
Initializes a provided context.
|
||||
|
||||
There are a large amount of parameters to tune the library instance to the user's preference. By default, keep in mind the library defaults to utilize stb_truetype as the font parser and harfbuzz (soon...) for the shaper.
|
||||
|
||||
Much of the data structures within the context struct are not fixed-capacity allocations so make sure that the backing allocator utilized can handle it.
|
||||
|
||||
### hot_reload
|
||||
|
||||
The library supports being used in a dynamically loaded module. If this occurs simply make sure to call this procedure with a reference to the backing allocator provided during startup as all dynamic containers tend to lose a proper reference to the allocator's procedure.
|
||||
|
||||
### shutdown
|
||||
|
||||
Release resources from the context.
|
||||
|
||||
### configure_snap
|
||||
|
||||
You'll find this used immediately in draw_text it acts as a way to snap the position of the text to the nearest pixel for the width and height specified.
|
||||
|
||||
If snapping is not desired, set the snap_width and height before calling draw_text to 0.
|
||||
|
||||
## get_cursor_pos
|
||||
|
||||
Will provide the current cursor_pos for the resulting text drawn.
|
||||
|
||||
## set_color
|
||||
|
||||
Sets the color to utilize on `DrawCall`s for FrameBuffer.Target or .Target_Uncached passes
|
||||
|
||||
### get_draw_list
|
||||
|
||||
Get the enqueded draw_list (vertices, indices, and draw call arrays) in its entirety.
|
||||
By default, if get_draw_list is called, it will first call `optimize_draw_list` to optimize the draw list's calls for the user. If this is undesired, make sure to pass `optimize_before_returning = false` in the arguments.
|
||||
|
||||
### get_draw_list_layer
|
||||
|
||||
Get the enqueued draw_list for the current "layer".
|
||||
A layer is considered the slice of the drawlist's content from the last call to `flush_draw_list_layer` onward.
|
||||
By default, if get_draw_list_layer is called, it will first call `optimize_draw_list` for the user to optimize the slice (exlusively) of the draw list's draw calls. If this is undesired, make sure to pass `optimize_before_returning = false` in the arguments.
|
||||
|
||||
The draw layer offsets are cleared with `flush_draw_list`
|
||||
|
||||
### flush_draw_list
|
||||
|
||||
Will clear the draw list and draw layer offsets.
|
||||
|
||||
### flush_draw_list_layer
|
||||
|
||||
Will update the draw list layer with the latest offset based on the current lenght of the draw list vertices, indices, and calls arrays.
|
||||
|
||||
### measure_text_size
|
||||
|
||||
Provides a Vec2 the width and height occupied by the provided text string. The y is measured to be the the largest glyph box bounds height of the text. The width is derived from the `end_cursor_pos` field from a `ShapedText` entry.
|
||||
|
||||
## get_font_vertical_metrics
|
||||
|
||||
A wrapper for `parser_get_font_vertical_metrics`. Will provide the ascent, descent, and line_gap for a font entry.
|
BIN
docs/draw_text_codepaths.pur
Normal file
BIN
docs/draw_text_codepaths.pur
Normal file
Binary file not shown.
17
docs/original/LICENSE.md
Normal file
17
docs/original/LICENSE.md
Normal file
@@ -0,0 +1,17 @@
|
||||
Vertex Engine GPU Font Cache
|
||||
Copyright 2020 Xi Chen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||
including without limitation the rights to use, copy, modify, merge, publish, distribute,
|
||||
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
|
||||
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
|
||||
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
114
docs/original/README.md
Normal file
114
docs/original/README.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# Notice
|
||||
|
||||
This is the original readme fo the C++ implementation by Xi Chen.
|
||||
|
||||
# VE Font Cache is a single header-only GPU font rendering library designed for game engines.
|
||||
|
||||
It aims to:
|
||||
* Be fast and simple to integrate.
|
||||
* Take advantage of modern GPU power.
|
||||
* Be backend agnostic and easy to port to any API such as Vulkan, DirectX, OpenGL.
|
||||
* Load TTF & OTF file formats directly.
|
||||
* Use only runtime cache with no offline calculation.
|
||||
* Render glyphs at reasonable quality at a wide range of font sizes.
|
||||
* Support a good amount of internationalisation. そうですね!
|
||||
* Support cached text shaping with HarfBuzz with simple Latin-style fallback.
|
||||
* Load and unload fonts at any time.
|
||||
|
||||
# How it works
|
||||
|
||||
Glyphs are GPU rasterised with 16x supersampling. This method is a simplification of "Easy Scalable Text Rendering on the GPU",
|
||||
by Evan Wallace, making use of XOR blending. Bézier curves are handled via brute force triangle tessellation; even 6 triangles per
|
||||
curve only generates < 300 triangles, which is nothing for modern GPUs! This avoids complex frag shader for reasonable quality.
|
||||
|
||||

|
||||
|
||||
Texture atlas caching uses naïve grid placement; this wastes a lot of space but ensures interchangeable cache slots allowing for
|
||||
straight up LRU ( Least Recently Used ) caching scheme to be employed.
|
||||
|
||||
The font atlas is a single 4k x 2k R8 texture divided into 4 regions:
|
||||
|
||||
```
|
||||
2k
|
||||
--------------------
|
||||
| | |
|
||||
| A | |
|
||||
| | | 2
|
||||
|---------| C | k
|
||||
| | |
|
||||
1k | B | |
|
||||
| | |
|
||||
--------------------
|
||||
| |
|
||||
| |
|
||||
| | 2
|
||||
| D | k
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
--------------------
|
||||
|
||||
Region A = 32x32 caches, 1024 glyphs
|
||||
Region B = 32x64 caches, 512 glyphs
|
||||
Region C = 64x64 caches, 512 glyphs
|
||||
Region D = 128x128 caches, 256 glyphs
|
||||
```
|
||||
|
||||
Region A is designed for small glyphs, Region B is for tall glyphs, Region C is for large glyphs, and Region D for huge glyphs.
|
||||
Glyphs are first rendered to an intermediate 2k x 512px R8 texture. This allows for minimum 4 Region D glyphs supersampled at
|
||||
4 x 4 = 16x supersampling, and 8 Region C glyphs similarly. A simple 16-tap box downsample shader is then used to blit from this
|
||||
intermediate texture to the final atlas location.
|
||||
|
||||
The atlas texture looks something like this:
|
||||

|
||||
|
||||
# Usage
|
||||
|
||||
Pseudo-code demonstrating simple usage:
|
||||
```cpp
|
||||
#define VE_FONTCACHE_IMPL
|
||||
#include "../ve_fontcache.h"
|
||||
|
||||
static std::vector< uint8_t > buffer;
|
||||
ve_fontcache_init( &cache );
|
||||
ve_fontcache_configure_snap( &cache, width, height );
|
||||
print_font = ve_fontcache_loadfile( &cache, "fonts/NotoSansJP-Light.otf", buffer, 19.0f );
|
||||
ve_fontcache_draw_text( &cache, print_font, u8"hello world", 0, 0, 1.0f / width, 1.0f / height );
|
||||
```
|
||||
|
||||
These header files need to be copied to your project:
|
||||
```
|
||||
ve_fontcache.h
|
||||
utf8.h
|
||||
stb_truetype.h
|
||||
```
|
||||
|
||||
Except HarfBuzz, that's all the required dependencies. That said it's strongly recommended
|
||||
to use HarfBuzz ( TODO: HarfBuzz not supported yet, coming soon!! ) over the default utf8.h latin
|
||||
fallback text shaper.
|
||||
|
||||
## Integration with rendering backend
|
||||
|
||||
VEFontCache is largely backend agnostic. Currently the demo project uses OpenGL 3.3 for Windows.
|
||||
That said it's designed to be integrated with VE, a Vulkan engine.
|
||||
Please read the "How to plug into rendering API" section in ve_fontcache.h for more documentation
|
||||
on how to implement your own backend to plumb this directly into your engine!
|
||||
|
||||
# Screenshots
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
# Similar projects and links
|
||||
|
||||
Here are links to some awesome similar and related projects:
|
||||
* fontstash - https://github.com/memononen/fontstash
|
||||
* stb_truetype ( has font rasterisation itself ) - https://github.com/nothings/stb/blob/master/stb_truetype.h
|
||||
* slug - http://sluglibrary.com/
|
||||
* pathfinder - https://github.com/pcwalton/pathfinder
|
||||
* https://medium.com/@evanwallace/easy-scalable-text-rendering-on-the-gpu-c3f4d782c5ac
|
718
draw.odin
Normal file
718
draw.odin
Normal file
@@ -0,0 +1,718 @@
|
||||
package VEFontCache
|
||||
|
||||
Vertex :: struct {
|
||||
pos : Vec2,
|
||||
u, v : f32,
|
||||
}
|
||||
|
||||
DrawCall :: struct {
|
||||
pass : FrameBufferPass,
|
||||
start_index : u32,
|
||||
end_index : u32,
|
||||
clear_before_draw : b32,
|
||||
region : AtlasRegionKind,
|
||||
colour : Colour,
|
||||
}
|
||||
|
||||
DrawCall_Default :: DrawCall {
|
||||
pass = .None,
|
||||
start_index = 0,
|
||||
end_index = 0,
|
||||
clear_before_draw = false,
|
||||
region = .A,
|
||||
colour = { 1.0, 1.0, 1.0, 1.0 }
|
||||
}
|
||||
|
||||
DrawList :: struct {
|
||||
vertices : [dynamic]Vertex,
|
||||
indices : [dynamic]u32,
|
||||
calls : [dynamic]DrawCall,
|
||||
}
|
||||
|
||||
FrameBufferPass :: enum u32 {
|
||||
None = 0,
|
||||
Glyph = 1,
|
||||
Atlas = 2,
|
||||
Target = 3,
|
||||
Target_Uncached = 4,
|
||||
}
|
||||
|
||||
GlyphDrawBuffer :: struct {
|
||||
over_sample : Vec2,
|
||||
batch : u32,
|
||||
width : u32,
|
||||
height : u32,
|
||||
draw_padding : u32,
|
||||
|
||||
batch_x : i32,
|
||||
clear_draw_list : DrawList,
|
||||
draw_list : DrawList,
|
||||
}
|
||||
|
||||
blit_quad :: proc( draw_list : ^DrawList, p0 : Vec2 = {0, 0}, p1 : Vec2 = {1, 1}, uv0 : Vec2 = {0, 0}, uv1 : Vec2 = {1, 1} )
|
||||
{
|
||||
// profile(#procedure)
|
||||
// logf("Blitting: xy0: %0.2f, %0.2f xy1: %0.2f, %0.2f uv0: %0.2f, %0.2f uv1: %0.2f, %0.2f",
|
||||
// p0.x, p0.y, p1.x, p1.y, uv0.x, uv0.y, uv1.x, uv1.y);
|
||||
v_offset := cast(u32) len(draw_list.vertices)
|
||||
|
||||
vertex := Vertex {
|
||||
{p0.x, p0.y},
|
||||
uv0.x, uv0.y
|
||||
}
|
||||
append_elem( & draw_list.vertices, vertex )
|
||||
|
||||
vertex = Vertex {
|
||||
{p0.x, p1.y},
|
||||
uv0.x, uv1.y
|
||||
}
|
||||
append_elem( & draw_list.vertices, vertex )
|
||||
|
||||
vertex = Vertex {
|
||||
{p1.x, p0.y},
|
||||
uv1.x, uv0.y
|
||||
}
|
||||
append_elem( & draw_list.vertices, vertex )
|
||||
|
||||
vertex = Vertex {
|
||||
{p1.x, p1.y},
|
||||
uv1.x, uv1.y
|
||||
}
|
||||
append_elem( & draw_list.vertices, vertex )
|
||||
|
||||
quad_indices : []u32 = {
|
||||
0, 1, 2,
|
||||
2, 1, 3
|
||||
}
|
||||
for index : i32 = 0; index < 6; index += 1 {
|
||||
append( & draw_list.indices, v_offset + quad_indices[ index ] )
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
cache_glyph :: proc( ctx : ^Context, font : FontID, glyph_index : Glyph, entry : ^Entry, bounds_0, bounds_1 : Vec2, scale, translate : Vec2 ) -> b32
|
||||
{
|
||||
// profile(#procedure)
|
||||
if glyph_index == Glyph(0) {
|
||||
// Note(Original Author): Glyph not in current hb_font
|
||||
return false
|
||||
}
|
||||
|
||||
// Retrieve the shape definition from the parser.
|
||||
shape, error := parser_get_glyph_shape( & entry.parser_info, glyph_index )
|
||||
assert( error == .None )
|
||||
if len(shape) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
if ctx.debug_print_verbose
|
||||
{
|
||||
log( "shape:")
|
||||
for vertex in shape
|
||||
{
|
||||
if vertex.type == .Move {
|
||||
logf("move_to %d %d", vertex.x, vertex.y )
|
||||
}
|
||||
else if vertex.type == .Line {
|
||||
logf("line_to %d %d", vertex.x, vertex.y )
|
||||
}
|
||||
else if vertex.type == .Curve {
|
||||
logf("curve_to %d %d through %d %d", vertex.x, vertex.y, vertex.contour_x0, vertex.contour_y0 )
|
||||
}
|
||||
else if vertex.type == .Cubic {
|
||||
logf("cubic_to %d %d through %d %d and %d %d",
|
||||
vertex.x, vertex.y,
|
||||
vertex.contour_x0, vertex.contour_y0,
|
||||
vertex.contour_x1, vertex.contour_y1 )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Note(Original Author):
|
||||
We need a random point that is outside our shape. We simply pick something diagonally across from top-left bound corner.
|
||||
Note that this outside point is scaled alongside the glyph in ve_fontcache_draw_filled_path, so we don't need to handle that here.
|
||||
*/
|
||||
outside := Vec2 {
|
||||
bounds_0.x - 21,
|
||||
bounds_0.y - 33,
|
||||
}
|
||||
|
||||
// Note(Original Author): Figure out scaling so it fits within our box.
|
||||
draw := DrawCall_Default
|
||||
draw.pass = FrameBufferPass.Glyph
|
||||
draw.start_index = u32(len(ctx.draw_list.indices))
|
||||
|
||||
// Note(Original Author);
|
||||
// Draw the path using simplified version of https://medium.com/@evanwallace/easy-scalable-text-rendering-on-the-gpu-c3f4d782c5ac.
|
||||
// Instead of involving fragment shader code we simply make use of modern GPU ability to crunch triangles and brute force curve definitions.
|
||||
path := ctx.temp_path
|
||||
clear( & path)
|
||||
for edge in shape do switch edge.type
|
||||
{
|
||||
case .Move:
|
||||
if len(path) > 0 {
|
||||
draw_filled_path( & ctx.draw_list, outside, path[:], scale, translate, ctx.debug_print_verbose )
|
||||
}
|
||||
clear( & path)
|
||||
fallthrough
|
||||
|
||||
case .Line:
|
||||
append( & path, Vec2{ f32(edge.x), f32(edge.y) })
|
||||
|
||||
case .Curve:
|
||||
assert( len(path) > 0 )
|
||||
p0 := path[ len(path) - 1 ]
|
||||
p1 := Vec2{ f32(edge.contour_x0), f32(edge.contour_y0) }
|
||||
p2 := Vec2{ f32(edge.x), f32(edge.y) }
|
||||
|
||||
step := 1.0 / f32(ctx.curve_quality)
|
||||
alpha := step
|
||||
for index := i32(0); index < i32(ctx.curve_quality); index += 1 {
|
||||
append( & path, eval_point_on_bezier3( p0, p1, p2, alpha ))
|
||||
alpha += step
|
||||
}
|
||||
|
||||
case .Cubic:
|
||||
assert( len(path) > 0 )
|
||||
p0 := path[ len(path) - 1]
|
||||
p1 := Vec2{ f32(edge.contour_x0), f32(edge.contour_y0) }
|
||||
p2 := Vec2{ f32(edge.contour_x1), f32(edge.contour_y1) }
|
||||
p3 := Vec2{ f32(edge.x), f32(edge.y) }
|
||||
|
||||
step := 1.0 / f32(ctx.curve_quality)
|
||||
alpha := step
|
||||
for index := i32(0); index < i32(ctx.curve_quality); index += 1 {
|
||||
append( & path, eval_point_on_bezier4( p0, p1, p2, p3, alpha ))
|
||||
alpha += step
|
||||
}
|
||||
|
||||
case .None:
|
||||
assert(false, "Unknown edge type or invalid")
|
||||
}
|
||||
if len(path) > 0 {
|
||||
draw_filled_path( & ctx.draw_list, outside, path[:], scale, translate, ctx.debug_print_verbose )
|
||||
}
|
||||
|
||||
// Note(Original Author): Apend the draw call
|
||||
draw.end_index = cast(u32) len(ctx.draw_list.indices)
|
||||
if draw.end_index > draw.start_index {
|
||||
append(& ctx.draw_list.calls, draw)
|
||||
}
|
||||
|
||||
parser_free_shape( & entry.parser_info, shape )
|
||||
return true
|
||||
}
|
||||
|
||||
/*
|
||||
Called by:
|
||||
* can_batch_glyph : If it determines that the glyph was not detected and we haven't reached capacity in the atlas
|
||||
* draw_text_shape : Glyph
|
||||
*/
|
||||
cache_glyph_to_atlas :: proc( ctx : ^Context,
|
||||
font : FontID,
|
||||
glyph_index : Glyph,
|
||||
lru_code : u64,
|
||||
atlas_index : i32,
|
||||
entry : ^Entry,
|
||||
region_kind : AtlasRegionKind,
|
||||
region : ^AtlasRegion,
|
||||
over_sample : Vec2 )
|
||||
{
|
||||
// profile(#procedure)
|
||||
|
||||
// Get hb_font text metrics. These are unscaled!
|
||||
bounds_0, bounds_1 := parser_get_glyph_box( & entry.parser_info, glyph_index )
|
||||
bounds_size := Vec2 {
|
||||
f32(bounds_1.x - bounds_0.x),
|
||||
f32(bounds_1.y - bounds_0.y)
|
||||
}
|
||||
|
||||
// E region is special case and not cached to atlas.
|
||||
if region_kind == .None || region_kind == .E do return
|
||||
|
||||
// Grab an atlas LRU cache slot.
|
||||
atlas_index := atlas_index
|
||||
if atlas_index == -1
|
||||
{
|
||||
if region.next_idx < region.state.capacity
|
||||
{
|
||||
evicted := LRU_put( & region.state, lru_code, i32(region.next_idx) )
|
||||
atlas_index = i32(region.next_idx)
|
||||
region.next_idx += 1
|
||||
assert( evicted == lru_code )
|
||||
}
|
||||
else
|
||||
{
|
||||
next_evict_codepoint := LRU_get_next_evicted( & region.state )
|
||||
assert( next_evict_codepoint != 0xFFFFFFFFFFFFFFFF )
|
||||
|
||||
atlas_index = LRU_peek( & region.state, next_evict_codepoint, must_find = true )
|
||||
assert( atlas_index != -1 )
|
||||
|
||||
evicted := LRU_put( & region.state, lru_code, atlas_index )
|
||||
assert( evicted == next_evict_codepoint )
|
||||
}
|
||||
|
||||
assert( LRU_get( & region.state, lru_code ) != - 1 )
|
||||
}
|
||||
|
||||
atlas := & ctx.atlas
|
||||
glyph_buffer := & ctx.glyph_buffer
|
||||
atlas_size := Vec2 { f32(atlas.width), f32(atlas.height) }
|
||||
glyph_buffer_size := Vec2 { f32(glyph_buffer.width), f32(glyph_buffer.height) }
|
||||
glyph_padding := cast(f32) atlas.glyph_padding
|
||||
|
||||
if ctx.debug_print
|
||||
{
|
||||
@static debug_total_cached : i32 = 0
|
||||
logf("glyph %v%v( %v ) caching to atlas region %v at idx %d. %d total glyphs cached.\n",
|
||||
i32(glyph_index), rune(glyph_index), cast(rune) region_kind, atlas_index, debug_total_cached)
|
||||
debug_total_cached += 1
|
||||
}
|
||||
|
||||
// Draw oversized glyph to update FBO
|
||||
glyph_draw_scale := over_sample * entry.size_scale
|
||||
glyph_draw_translate := -1 * vec2(bounds_0) * glyph_draw_scale + vec2( glyph_padding )
|
||||
glyph_draw_translate.x = cast(f32) (i32(glyph_draw_translate.x + 0.9999999))
|
||||
glyph_draw_translate.y = cast(f32) (i32(glyph_draw_translate.y + 0.9999999))
|
||||
|
||||
// Allocate a glyph_update_FBO region
|
||||
gwidth_scaled_px := bounds_size.x * glyph_draw_scale.x + 1.0 + over_sample.x * glyph_padding
|
||||
if i32(f32(glyph_buffer.batch_x) + gwidth_scaled_px) >= i32(glyph_buffer.width) {
|
||||
flush_glyph_buffer_to_atlas( ctx )
|
||||
}
|
||||
|
||||
// Calculate the src and destination regions
|
||||
slot_position, slot_szie := atlas_bbox( atlas, region_kind, atlas_index )
|
||||
|
||||
dst_glyph_position := slot_position
|
||||
dst_glyph_size := bounds_size * entry.size_scale + glyph_padding
|
||||
dst_size := slot_szie
|
||||
screenspace_x_form( & dst_glyph_position, & dst_glyph_size, atlas_size )
|
||||
screenspace_x_form( & slot_position, & dst_size, atlas_size )
|
||||
|
||||
src_position := Vec2 { f32(glyph_buffer.batch_x), 0 }
|
||||
src_size := bounds_size * glyph_draw_scale + over_sample * glyph_padding
|
||||
textspace_x_form( & src_position, & src_size, glyph_buffer_size )
|
||||
|
||||
// Advance glyph_update_batch_x and calculate final glyph drawing transform
|
||||
glyph_draw_translate.x += f32(glyph_buffer.batch_x)
|
||||
glyph_buffer.batch_x += i32(gwidth_scaled_px)
|
||||
screenspace_x_form( & glyph_draw_translate, & glyph_draw_scale, glyph_buffer_size )
|
||||
|
||||
call : DrawCall
|
||||
{
|
||||
// Queue up clear on target region on atlas
|
||||
using call
|
||||
pass = .Atlas
|
||||
region = .Ignore
|
||||
start_index = cast(u32) len(glyph_buffer.clear_draw_list.indices)
|
||||
|
||||
blit_quad( & glyph_buffer.clear_draw_list,
|
||||
slot_position, slot_position + dst_size,
|
||||
{ 1.0, 1.0 }, { 1.0, 1.0 } )
|
||||
|
||||
end_index = cast(u32) len(glyph_buffer.clear_draw_list.indices)
|
||||
append( & glyph_buffer.clear_draw_list.calls, call )
|
||||
|
||||
// Queue up a blit from glyph_update_FBO to the atlas
|
||||
region = .None
|
||||
start_index = cast(u32) len(glyph_buffer.draw_list.indices)
|
||||
|
||||
blit_quad( & glyph_buffer.draw_list,
|
||||
dst_glyph_position, slot_position + dst_glyph_size,
|
||||
src_position, src_position + src_size )
|
||||
|
||||
end_index = cast(u32) len(glyph_buffer.draw_list.indices)
|
||||
append( & glyph_buffer.draw_list.calls, call )
|
||||
}
|
||||
|
||||
// Render glyph to glyph_update_FBO
|
||||
cache_glyph( ctx, font, glyph_index, entry, vec2(bounds_0), vec2(bounds_1), glyph_draw_scale, glyph_draw_translate )
|
||||
}
|
||||
|
||||
can_batch_glyph :: #force_inline proc( ctx : ^Context, font : FontID, entry : ^Entry, glyph_index : Glyph,
|
||||
lru_code : u64,
|
||||
atlas_index : i32,
|
||||
region_kind : AtlasRegionKind,
|
||||
region : ^AtlasRegion,
|
||||
over_sample : Vec2
|
||||
) -> b32
|
||||
{
|
||||
// profile(#procedure)
|
||||
assert( glyph_index != -1 )
|
||||
|
||||
// E region can't batch
|
||||
if region_kind == .E || region_kind == .None do return false
|
||||
if ctx.temp_codepoint_seen_num > 1024 do return false
|
||||
// TODO(Ed): Why 1024?
|
||||
|
||||
if atlas_index == - 1
|
||||
{
|
||||
if region.next_idx > u32( region.state.capacity) {
|
||||
// We will evict LRU. We must predict which LRU will get evicted, and if it's something we've seen then we need to take slowpath and flush batch.
|
||||
next_evict_codepoint := LRU_get_next_evicted( & region.state )
|
||||
seen, success := ctx.temp_codepoint_seen[next_evict_codepoint]
|
||||
assert(success != false)
|
||||
|
||||
if (seen) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
cache_glyph_to_atlas( ctx, font, glyph_index, lru_code, atlas_index, entry, region_kind, region, over_sample )
|
||||
}
|
||||
|
||||
assert( LRU_get( & region.state, lru_code ) != -1 )
|
||||
mark_batch_codepoint_seen( ctx, lru_code)
|
||||
return true
|
||||
}
|
||||
|
||||
// ve_fontcache_clear_drawlist
|
||||
clear_draw_list :: #force_inline proc ( draw_list : ^DrawList ) {
|
||||
clear( & draw_list.calls )
|
||||
clear( & draw_list.indices )
|
||||
clear( & draw_list.vertices )
|
||||
}
|
||||
|
||||
directly_draw_massive_glyph :: proc( ctx : ^Context,
|
||||
entry : ^Entry,
|
||||
glyph : Glyph,
|
||||
bounds_0, bounds_1 : Vec2,
|
||||
bounds_size : Vec2,
|
||||
over_sample, position, scale : Vec2 )
|
||||
{
|
||||
// profile(#procedure)
|
||||
flush_glyph_buffer_to_atlas( ctx )
|
||||
|
||||
glyph_padding := f32(ctx.atlas.glyph_padding)
|
||||
glyph_buffer_size := Vec2 { f32(ctx.glyph_buffer.width), f32(ctx.glyph_buffer.height) }
|
||||
|
||||
// Draw un-antialiased glyph to update FBO.
|
||||
glyph_draw_scale := over_sample * entry.size_scale
|
||||
glyph_draw_translate := -1 * bounds_0 * glyph_draw_scale + vec2_from_scalar(glyph_padding)
|
||||
screenspace_x_form( & glyph_draw_translate, & glyph_draw_scale, glyph_buffer_size )
|
||||
|
||||
cache_glyph( ctx, entry.id, glyph, entry, bounds_0, bounds_1, glyph_draw_scale, glyph_draw_translate )
|
||||
|
||||
glyph_padding_dbl := glyph_padding * 2
|
||||
bounds_scaled := bounds_size * entry.size_scale
|
||||
|
||||
// Figure out the source rect.
|
||||
glyph_position := Vec2 {}
|
||||
glyph_size := vec2(glyph_padding_dbl)
|
||||
glyph_dst_size := glyph_size + bounds_scaled
|
||||
glyph_size += bounds_scaled * over_sample
|
||||
|
||||
// Figure out the destination rect.
|
||||
bounds_0_scaled := Vec2 {
|
||||
cast(f32) i32(bounds_0.x * entry.size_scale - 0.5),
|
||||
cast(f32) i32(bounds_0.y * entry.size_scale - 0.5),
|
||||
}
|
||||
dst := position + scale * bounds_0_scaled - glyph_padding * scale
|
||||
dst_size := glyph_dst_size * scale
|
||||
textspace_x_form( & glyph_position, & glyph_size, glyph_buffer_size )
|
||||
|
||||
// Add the glyph drawcall.
|
||||
call : DrawCall
|
||||
{
|
||||
using call
|
||||
pass = .Target_Uncached
|
||||
colour = ctx.colour
|
||||
start_index = u32(len(ctx.draw_list.indices))
|
||||
|
||||
blit_quad( & ctx.draw_list,
|
||||
dst, dst + dst_size,
|
||||
glyph_position, glyph_position + glyph_size )
|
||||
|
||||
end_index = u32(len(ctx.draw_list.indices))
|
||||
append( & ctx.draw_list.calls, call )
|
||||
}
|
||||
|
||||
// Clear glyph_update_FBO.
|
||||
call.pass = .Glyph
|
||||
call.start_index = 0
|
||||
call.end_index = 0
|
||||
call.clear_before_draw = true
|
||||
append( & ctx.draw_list.calls, call )
|
||||
}
|
||||
|
||||
draw_cached_glyph :: proc( ctx : ^Context,
|
||||
entry : ^Entry,
|
||||
glyph_index : Glyph,
|
||||
lru_code : u64,
|
||||
atlas_index : i32,
|
||||
bounds_0, bounds_1 : Vec2,
|
||||
region_kind : AtlasRegionKind,
|
||||
region : ^AtlasRegion,
|
||||
over_sample : Vec2,
|
||||
position, scale : Vec2
|
||||
) -> b32
|
||||
{
|
||||
// profile(#procedure)
|
||||
bounds_size := Vec2 {
|
||||
f32(bounds_1.x - bounds_0.x),
|
||||
f32(bounds_1.y - bounds_0.y),
|
||||
}
|
||||
|
||||
// E region is special case and not cached to atlas
|
||||
if region_kind == .E
|
||||
{
|
||||
directly_draw_massive_glyph( ctx, entry, glyph_index, bounds_0, bounds_1, bounds_size, over_sample, position, scale )
|
||||
return true
|
||||
}
|
||||
|
||||
// Is this codepoint cached?
|
||||
if atlas_index == - 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
atlas := & ctx.atlas
|
||||
atlas_size := Vec2 { f32(atlas.width), f32(atlas.height) }
|
||||
glyph_padding := f32(atlas.glyph_padding)
|
||||
|
||||
// Figure out the source bounding box in the atlas texture
|
||||
slot_position, _ := atlas_bbox( atlas, region_kind, atlas_index )
|
||||
|
||||
glyph_scale := bounds_size * entry.size_scale + glyph_padding
|
||||
|
||||
bounds_0_scaled := bounds_0 * entry.size_scale //- { 0.5, 0.5 }
|
||||
bounds_0_scaled = ceil(bounds_0_scaled)
|
||||
|
||||
dst := position + bounds_0_scaled * scale
|
||||
dst -= glyph_padding * scale
|
||||
dst_scale := glyph_scale * scale
|
||||
|
||||
textspace_x_form( & slot_position, & glyph_scale, atlas_size )
|
||||
|
||||
// Add the glyph drawcall
|
||||
call := DrawCall_Default
|
||||
{
|
||||
using call
|
||||
pass = .Target
|
||||
colour = ctx.colour
|
||||
start_index = cast(u32) len(ctx.draw_list.indices)
|
||||
|
||||
blit_quad( & ctx.draw_list,
|
||||
dst, dst + dst_scale,
|
||||
slot_position, slot_position + glyph_scale )
|
||||
end_index = cast(u32) len(ctx.draw_list.indices)
|
||||
}
|
||||
append( & ctx.draw_list.calls, call )
|
||||
return true
|
||||
}
|
||||
|
||||
// Constructs a triangle fan to fill a shape using the provided path
|
||||
// outside_point represents the center point of the fan.
|
||||
//
|
||||
// Note(Original Author):
|
||||
// WARNING: doesn't actually append drawcall; caller is responsible for actually appending the drawcall.
|
||||
// ve_fontcache_draw_filled_path
|
||||
draw_filled_path :: proc( draw_list : ^DrawList, outside_point : Vec2, path : []Vec2,
|
||||
scale := Vec2 { 1, 1 },
|
||||
translate := Vec2 { 0, 0 },
|
||||
debug_print_verbose : b32 = false
|
||||
)
|
||||
{
|
||||
if debug_print_verbose
|
||||
{
|
||||
log("outline_path:")
|
||||
for point in path {
|
||||
vec := point * scale + translate
|
||||
logf(" %0.2f %0.2f", vec.x, vec.y )
|
||||
}
|
||||
}
|
||||
|
||||
v_offset := cast(u32) len(draw_list.vertices)
|
||||
for point in path {
|
||||
vertex := Vertex {
|
||||
pos = point * scale + translate,
|
||||
u = 0,
|
||||
v = 0,
|
||||
}
|
||||
append( & draw_list.vertices, vertex )
|
||||
}
|
||||
|
||||
outside_vertex := cast(u32) len(draw_list.vertices)
|
||||
{
|
||||
vertex := Vertex {
|
||||
pos = outside_point * scale + translate,
|
||||
u = 0,
|
||||
v = 0,
|
||||
}
|
||||
append( & draw_list.vertices, vertex )
|
||||
}
|
||||
|
||||
for index : u32 = 1; index < cast(u32) len(path); index += 1 {
|
||||
indices := & draw_list.indices
|
||||
append( indices, outside_vertex )
|
||||
append( indices, v_offset + index - 1 )
|
||||
append( indices, v_offset + index )
|
||||
}
|
||||
}
|
||||
|
||||
draw_text_batch :: proc( ctx : ^Context, entry : ^Entry, shaped : ^ShapedText,
|
||||
batch_start_idx, batch_end_idx : i32,
|
||||
position, scale : Vec2,
|
||||
snap_width, snap_height : f32 )
|
||||
{
|
||||
flush_glyph_buffer_to_atlas( ctx )
|
||||
for index := batch_start_idx; index < batch_end_idx; index += 1
|
||||
{
|
||||
glyph_index := shaped.glyphs[ index ]
|
||||
|
||||
if glyph_index == 0 do continue
|
||||
if parser_is_glyph_empty( & entry.parser_info, glyph_index ) do continue
|
||||
|
||||
region_kind, region, over_sample := decide_codepoint_region( ctx, entry, glyph_index )
|
||||
lru_code := font_glyph_lru_code(entry.id, glyph_index)
|
||||
atlas_index := cast(i32) -1
|
||||
|
||||
if region_kind != .E do atlas_index = LRU_get( & region.state, lru_code )
|
||||
bounds_0, bounds_1 := parser_get_glyph_box( & entry.parser_info, glyph_index )
|
||||
|
||||
shaped_position := shaped.positions[index]
|
||||
glyph_translate := position + shaped_position * scale
|
||||
|
||||
glyph_cached := draw_cached_glyph( ctx,
|
||||
entry, glyph_index,
|
||||
lru_code, atlas_index,
|
||||
vec2(bounds_0), vec2(bounds_1),
|
||||
region_kind, region, over_sample,
|
||||
glyph_translate, scale)
|
||||
assert( glyph_cached == true )
|
||||
}
|
||||
}
|
||||
|
||||
// Helper for draw_text, all raw text content should be confirmed to be either formatting or visible shapes before getting cached.
|
||||
draw_text_shape :: proc( ctx : ^Context,
|
||||
font : FontID,
|
||||
entry : ^Entry,
|
||||
shaped : ^ShapedText,
|
||||
position, scale : Vec2,
|
||||
snap_width, snap_height : f32
|
||||
) -> (cursor_pos : Vec2)
|
||||
{
|
||||
// position := position //+ ctx.cursor_pos * scale
|
||||
// profile(#procedure)
|
||||
batch_start_idx : i32 = 0
|
||||
for index : i32 = 0; index < cast(i32) len(shaped.glyphs); index += 1
|
||||
{
|
||||
glyph_index := shaped.glyphs[ index ]
|
||||
if is_empty( ctx, entry, glyph_index ) do continue
|
||||
|
||||
region_kind, region, over_sample := decide_codepoint_region( ctx, entry, glyph_index )
|
||||
lru_code := font_glyph_lru_code(entry.id, glyph_index)
|
||||
atlas_index := cast(i32) -1
|
||||
|
||||
if region_kind != .E do atlas_index = LRU_get( & region.state, lru_code )
|
||||
if can_batch_glyph( ctx, font, entry, glyph_index, lru_code, atlas_index, region_kind, region, over_sample ) do continue
|
||||
|
||||
// Glyph has not been catched, needs to be directly drawn.
|
||||
|
||||
// First batch the other cached glyphs
|
||||
// flush_glyph_buffer_to_atlas(ctx)
|
||||
draw_text_batch( ctx, entry, shaped, batch_start_idx, index, position, scale, snap_width, snap_height )
|
||||
reset_batch_codepoint_state( ctx )
|
||||
|
||||
cache_glyph_to_atlas( ctx, font, glyph_index, lru_code, atlas_index, entry, region_kind, region, over_sample )
|
||||
mark_batch_codepoint_seen( ctx, lru_code)
|
||||
batch_start_idx = index
|
||||
}
|
||||
|
||||
// flush_glyph_buffer_to_atlas(ctx)
|
||||
draw_text_batch( ctx, entry, shaped, batch_start_idx, cast(i32) len(shaped.glyphs), position, scale, snap_width , snap_height )
|
||||
reset_batch_codepoint_state( ctx )
|
||||
cursor_pos = shaped.end_cursor_pos
|
||||
return
|
||||
}
|
||||
|
||||
flush_glyph_buffer_to_atlas :: proc( ctx : ^Context )
|
||||
{
|
||||
// profile(#procedure)
|
||||
// Flush drawcalls to draw list
|
||||
merge_draw_list( & ctx.draw_list, & ctx.glyph_buffer.clear_draw_list )
|
||||
merge_draw_list( & ctx.draw_list, & ctx.glyph_buffer.draw_list)
|
||||
clear_draw_list( & ctx.glyph_buffer.draw_list )
|
||||
clear_draw_list( & ctx.glyph_buffer.clear_draw_list )
|
||||
|
||||
// Clear glyph_update_FBO
|
||||
if ctx.glyph_buffer.batch_x != 0
|
||||
{
|
||||
call := DrawCall_Default
|
||||
call.pass = .Glyph
|
||||
call.start_index = 0
|
||||
call.end_index = 0
|
||||
call.clear_before_draw = true
|
||||
append( & ctx.draw_list.calls, call )
|
||||
ctx.glyph_buffer.batch_x = 0
|
||||
}
|
||||
}
|
||||
|
||||
// ve_fontcache_merge_drawlist
|
||||
merge_draw_list :: proc( dst, src : ^DrawList )
|
||||
{
|
||||
// profile(#procedure)
|
||||
error : AllocatorError
|
||||
|
||||
v_offset := cast(u32) len( dst.vertices )
|
||||
num_appended : int
|
||||
num_appended, error = append( & dst.vertices, ..src.vertices[:] )
|
||||
assert( error == .None )
|
||||
|
||||
i_offset := cast(u32) len(dst.indices)
|
||||
for index : int = 0; index < len(src.indices); index += 1 {
|
||||
ignored : int
|
||||
ignored, error = append( & dst.indices, src.indices[index] + v_offset )
|
||||
assert( error == .None )
|
||||
}
|
||||
|
||||
for index : int = 0; index < len(src.calls); index += 1 {
|
||||
src_call := src.calls[ index ]
|
||||
src_call.start_index += i_offset
|
||||
src_call.end_index += i_offset
|
||||
append( & dst.calls, src_call )
|
||||
assert( error == .None )
|
||||
}
|
||||
}
|
||||
|
||||
optimize_draw_list :: proc( draw_list : ^DrawList, call_offset : int )
|
||||
{
|
||||
// profile(#procedure)
|
||||
assert( draw_list != nil )
|
||||
|
||||
write_index : int = call_offset
|
||||
for index : int = 1 + call_offset; index < len(draw_list.calls); index += 1
|
||||
{
|
||||
assert( write_index <= index )
|
||||
draw_0 := & draw_list.calls[ write_index ]
|
||||
draw_1 := & draw_list.calls[ index ]
|
||||
|
||||
merge : b32 = true
|
||||
if draw_0.pass != draw_1.pass do merge = false
|
||||
if draw_0.end_index != draw_1.start_index do merge = false
|
||||
if draw_0.region != draw_1.region do merge = false
|
||||
if draw_1.clear_before_draw do merge = false
|
||||
if draw_0.colour != draw_1.colour do merge = false
|
||||
|
||||
if merge
|
||||
{
|
||||
// logf("merging %v : %v %v", draw_0.pass, write_index, index )
|
||||
draw_0.end_index = draw_1.end_index
|
||||
draw_1.start_index = 0
|
||||
draw_1.end_index = 0
|
||||
}
|
||||
else
|
||||
{
|
||||
// logf("can't merge %v : %v %v", draw_0.pass, write_index, index )
|
||||
write_index += 1
|
||||
if write_index != index {
|
||||
draw_2 := & draw_list.calls[ write_index ]
|
||||
draw_2^ = draw_1^
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resize( & draw_list.calls, write_index + 1 )
|
||||
}
|
81
mappings.odin
Normal file
81
mappings.odin
Normal file
@@ -0,0 +1,81 @@
|
||||
package VEFontCache
|
||||
|
||||
import "core:hash"
|
||||
fnv64a :: hash.fnv64a
|
||||
import "core:math"
|
||||
ceil_f16 :: math.ceil_f16
|
||||
ceil_f16le :: math.ceil_f16le
|
||||
ceil_f16be :: math.ceil_f16be
|
||||
ceil_f32 :: math.ceil_f32
|
||||
ceil_f32le :: math.ceil_f32le
|
||||
ceil_f32be :: math.ceil_f32be
|
||||
ceil_f64 :: math.ceil_f64
|
||||
ceil_f64le :: math.ceil_f64le
|
||||
ceil_f64be :: math.ceil_f64be
|
||||
import "core:math/linalg"
|
||||
import "core:mem"
|
||||
Kilobyte :: mem.Kilobyte
|
||||
slice_ptr :: mem.slice_ptr
|
||||
|
||||
Allocator :: mem.Allocator
|
||||
AllocatorError :: mem.Allocator_Error
|
||||
|
||||
Arena :: mem.Arena
|
||||
arena_allocator :: mem.arena_allocator
|
||||
arena_init :: mem.arena_init
|
||||
// import "codebase:grime"
|
||||
// log :: grime.log
|
||||
// logf :: grime.logf
|
||||
// profile :: grime.profile
|
||||
|
||||
//#region("Proc overload mappings")
|
||||
|
||||
append :: proc {
|
||||
append_elem,
|
||||
append_elems,
|
||||
append_elem_string,
|
||||
}
|
||||
|
||||
ceil :: proc {
|
||||
math.ceil_f16,
|
||||
math.ceil_f16le,
|
||||
math.ceil_f16be,
|
||||
math.ceil_f32,
|
||||
math.ceil_f32le,
|
||||
math.ceil_f32be,
|
||||
math.ceil_f64,
|
||||
math.ceil_f64le,
|
||||
math.ceil_f64be,
|
||||
|
||||
ceil_vec2,
|
||||
}
|
||||
|
||||
clear :: proc {
|
||||
clear_dynamic_array,
|
||||
}
|
||||
|
||||
make :: proc {
|
||||
make_dynamic_array,
|
||||
make_dynamic_array_len,
|
||||
make_dynamic_array_len_cap,
|
||||
make_map,
|
||||
}
|
||||
|
||||
resize :: proc {
|
||||
resize_dynamic_array,
|
||||
}
|
||||
|
||||
vec2 :: proc {
|
||||
vec2_from_scalar,
|
||||
vec2_from_vec2i,
|
||||
}
|
||||
|
||||
vec2i :: proc {
|
||||
vec2i_from_vec2,
|
||||
}
|
||||
|
||||
vec2_64 :: proc {
|
||||
vec2_64_from_vec2,
|
||||
}
|
||||
|
||||
//#endregion("Proc overload mappings")
|
165
misc.odin
Normal file
165
misc.odin
Normal file
@@ -0,0 +1,165 @@
|
||||
package VEFontCache
|
||||
|
||||
import "base:runtime"
|
||||
import core_log "core:log"
|
||||
|
||||
Colour :: [4]f32
|
||||
Vec2 :: [2]f32
|
||||
Vec2i :: [2]i32
|
||||
Vec2_64 :: [2]f64
|
||||
|
||||
vec2_from_scalar :: #force_inline proc "contextless" ( scalar : f32 ) -> Vec2 { return { scalar, scalar }}
|
||||
vec2_64_from_vec2 :: #force_inline proc "contextless" ( v2 : Vec2 ) -> Vec2_64 { return { f64(v2.x), f64(v2.y) }}
|
||||
vec2_from_vec2i :: #force_inline proc "contextless" ( v2i : Vec2i ) -> Vec2 { return { f32(v2i.x), f32(v2i.y) }}
|
||||
vec2i_from_vec2 :: #force_inline proc "contextless" ( v2 : Vec2 ) -> Vec2i { return { i32(v2.x), i32(v2.y) }}
|
||||
|
||||
@(require_results) ceil_vec2 :: proc "contextless" ( v : Vec2 ) -> Vec2 { return { ceil_f32(v.x), ceil_f32(v.y) } }
|
||||
|
||||
// This buffer is used below excluisvely to prevent any allocator recusion when verbose logging from allocators.
|
||||
// This means a single line is limited to 32k buffer (increase naturally if this SOMEHOW becomes a bottleneck...)
|
||||
Logger_Allocator_Buffer : [32 * Kilobyte]u8
|
||||
|
||||
log :: proc( msg : string, level := core_log.Level.Info, loc := #caller_location ) {
|
||||
temp_arena : Arena; arena_init(& temp_arena, Logger_Allocator_Buffer[:])
|
||||
context.allocator = arena_allocator(& temp_arena)
|
||||
context.temp_allocator = arena_allocator(& temp_arena)
|
||||
|
||||
core_log.log( level, msg, location = loc )
|
||||
}
|
||||
|
||||
logf :: proc( fmt : string, args : ..any, level := core_log.Level.Info, loc := #caller_location ) {
|
||||
temp_arena : Arena; arena_init(& temp_arena, Logger_Allocator_Buffer[:])
|
||||
context.allocator = arena_allocator(& temp_arena)
|
||||
context.temp_allocator = arena_allocator(& temp_arena)
|
||||
|
||||
core_log.logf( level, fmt, ..args, location = loc )
|
||||
}
|
||||
|
||||
reload_array :: proc( self : ^[dynamic]$Type, allocator : Allocator ) {
|
||||
raw := transmute( ^runtime.Raw_Dynamic_Array) self
|
||||
raw.allocator = allocator
|
||||
}
|
||||
|
||||
reload_map :: proc( self : ^map [$KeyType] $EntryType, allocator : Allocator ) {
|
||||
raw := transmute( ^runtime.Raw_Map) self
|
||||
raw.allocator = allocator
|
||||
}
|
||||
|
||||
font_glyph_lru_code :: #force_inline proc "contextless" ( font : FontID, glyph_index : Glyph ) -> (lru_code : u64) {
|
||||
lru_code = u64(glyph_index) + ( ( 0x100000000 * u64(font) ) & 0xFFFFFFFF00000000 )
|
||||
return
|
||||
}
|
||||
|
||||
shape_lru_hash :: #force_inline proc "contextless" ( label : string ) -> u64 {
|
||||
hash : u64
|
||||
for str_byte in transmute([]byte) label {
|
||||
hash = ((hash << 8) + hash) + u64(str_byte)
|
||||
}
|
||||
return hash
|
||||
}
|
||||
|
||||
// For a provided alpha value,
|
||||
// allows the function to calculate the position of a point along the curve at any given fraction of its total length
|
||||
// ve_fontcache_eval_bezier (quadratic)
|
||||
eval_point_on_bezier3 :: #force_inline proc "contextless" ( p0, p1, p2 : Vec2, alpha : f32 ) -> Vec2
|
||||
{
|
||||
p0 := vec2_64(p0)
|
||||
p1 := vec2_64(p1)
|
||||
p2 := vec2_64(p2)
|
||||
alpha := f64(alpha)
|
||||
|
||||
weight_start := (1 - alpha) * (1 - alpha)
|
||||
weight_control := 2.0 * (1 - alpha) * alpha
|
||||
weight_end := alpha * alpha
|
||||
|
||||
starting_point := p0 * weight_start
|
||||
control_point := p1 * weight_control
|
||||
end_point := p2 * weight_end
|
||||
|
||||
point := starting_point + control_point + end_point
|
||||
return { f32(point.x), f32(point.y) }
|
||||
}
|
||||
|
||||
// For a provided alpha value,
|
||||
// allows the function to calculate the position of a point along the curve at any given fraction of its total length
|
||||
// ve_fontcache_eval_bezier (cubic)
|
||||
eval_point_on_bezier4 :: #force_inline proc "contextless" ( p0, p1, p2, p3 : Vec2, alpha : f32 ) -> Vec2
|
||||
{
|
||||
p0 := vec2_64(p0)
|
||||
p1 := vec2_64(p1)
|
||||
p2 := vec2_64(p2)
|
||||
p3 := vec2_64(p3)
|
||||
alpha := f64(alpha)
|
||||
|
||||
weight_start := (1 - alpha) * (1 - alpha) * (1 - alpha)
|
||||
weight_c_a := 3 * (1 - alpha) * (1 - alpha) * alpha
|
||||
weight_c_b := 3 * (1 - alpha) * alpha * alpha
|
||||
weight_end := alpha * alpha * alpha
|
||||
|
||||
start_point := p0 * weight_start
|
||||
control_a := p1 * weight_c_a
|
||||
control_b := p2 * weight_c_b
|
||||
end_point := p3 * weight_end
|
||||
|
||||
point := start_point + control_a + control_b + end_point
|
||||
return { f32(point.x), f32(point.y) }
|
||||
}
|
||||
|
||||
is_empty :: #force_inline proc ( ctx : ^Context, entry : ^Entry, glyph_index : Glyph ) -> b32
|
||||
{
|
||||
if glyph_index == 0 do return true
|
||||
if parser_is_glyph_empty( & entry.parser_info, glyph_index ) do return true
|
||||
return false
|
||||
}
|
||||
|
||||
mark_batch_codepoint_seen :: #force_inline proc ( ctx : ^Context, lru_code : u64 ) {
|
||||
ctx.temp_codepoint_seen[lru_code] = true
|
||||
ctx.temp_codepoint_seen_num += 1
|
||||
}
|
||||
|
||||
reset_batch_codepoint_state :: #force_inline proc( ctx : ^Context ) {
|
||||
clear_map( & ctx.temp_codepoint_seen )
|
||||
ctx.temp_codepoint_seen_num = 0
|
||||
}
|
||||
|
||||
screenspace_x_form :: #force_inline proc "contextless" ( position, scale : ^Vec2, size : Vec2 ) {
|
||||
when true
|
||||
{
|
||||
pos_64 := vec2_64_from_vec2(position^)
|
||||
scale_64 := vec2_64_from_vec2(scale^)
|
||||
|
||||
quotient : Vec2_64 = 1.0 / vec2_64(size)
|
||||
pos_64 = pos_64 * quotient * 2.0 - 1.0
|
||||
scale_64 = scale_64 * quotient * 2.0
|
||||
|
||||
(position^) = { f32(pos_64.x), f32(pos_64.y) }
|
||||
(scale^) = { f32(scale_64.x), f32(scale_64.y) }
|
||||
}
|
||||
else
|
||||
{
|
||||
quotient : Vec2 = 1.0 / size
|
||||
(position^) *= quotient * 2.0 - 1.0
|
||||
(scale^) *= quotient * 2.0
|
||||
}
|
||||
}
|
||||
|
||||
textspace_x_form :: #force_inline proc "contextless" ( position, scale : ^Vec2, size : Vec2 ) {
|
||||
when true
|
||||
{
|
||||
pos_64 := vec2_64_from_vec2(position^)
|
||||
scale_64 := vec2_64_from_vec2(scale^)
|
||||
|
||||
quotient : Vec2_64 = 1.0 / vec2_64(size)
|
||||
pos_64 *= quotient
|
||||
scale_64 *= quotient
|
||||
|
||||
(position^) = { f32(pos_64.x), f32(pos_64.y) }
|
||||
(scale^) = { f32(scale_64.x), f32(scale_64.y) }
|
||||
}
|
||||
else
|
||||
{
|
||||
quotient : Vec2 = 1.0 / size
|
||||
(position^) *= quotient
|
||||
(scale^) *= quotient
|
||||
}
|
||||
}
|
495
parser.odin
Normal file
495
parser.odin
Normal file
@@ -0,0 +1,495 @@
|
||||
package VEFontCache
|
||||
|
||||
/*
|
||||
Notes:
|
||||
|
||||
Freetype will do memory allocations and has an interface the user can implement.
|
||||
That interface is not exposed from this parser but could be added to parser_init.
|
||||
|
||||
STB_Truetype has macros for its allocation unfortuantely
|
||||
*/
|
||||
|
||||
import "base:runtime"
|
||||
import "core:c"
|
||||
import "core:math"
|
||||
import stbtt "vendor:stb/truetype"
|
||||
import freetype "thirdparty:freetype"
|
||||
|
||||
ParserKind :: enum u32 {
|
||||
STB_TrueType,
|
||||
Freetype,
|
||||
}
|
||||
|
||||
ParserFontInfo :: struct {
|
||||
label : string,
|
||||
kind : ParserKind,
|
||||
using _ : struct #raw_union {
|
||||
stbtt_info : stbtt.fontinfo,
|
||||
freetype_info : freetype.Face
|
||||
},
|
||||
data : []byte,
|
||||
}
|
||||
|
||||
GlyphVertType :: enum u8 {
|
||||
None,
|
||||
Move = 1,
|
||||
Line,
|
||||
Curve,
|
||||
Cubic,
|
||||
}
|
||||
|
||||
// Based directly off of stb_truetype's vertex
|
||||
ParserGlyphVertex :: struct {
|
||||
x, y : i16,
|
||||
contour_x0, contour_y0 : i16,
|
||||
contour_x1, contour_y1 : i16,
|
||||
type : GlyphVertType,
|
||||
padding : u8,
|
||||
}
|
||||
// A shape can be a dynamic array free_type or an opaque set of data handled by stb_truetype
|
||||
ParserGlyphShape :: [dynamic]ParserGlyphVertex
|
||||
|
||||
ParserContext :: struct {
|
||||
kind : ParserKind,
|
||||
ft_library : freetype.Library,
|
||||
|
||||
// fonts : HMapChained(ParserFontInfo),
|
||||
}
|
||||
|
||||
parser_init :: proc( ctx : ^ParserContext )
|
||||
{
|
||||
switch ctx.kind
|
||||
{
|
||||
case .Freetype:
|
||||
result := freetype.init_free_type( & ctx.ft_library )
|
||||
assert( result == freetype.Error.Ok, "VEFontCache.parser_init: Failed to initialize freetype" )
|
||||
|
||||
case .STB_TrueType:
|
||||
// Do nothing intentional
|
||||
}
|
||||
|
||||
// error : AllocatorError
|
||||
// ctx.fonts, error = make( HMapChained(ParserFontInfo), 256 )
|
||||
// assert( error == .None, "VEFontCache.parser_init: Failed to allocate fonts array" )
|
||||
}
|
||||
|
||||
parser_shutdown :: proc( ctx : ^ParserContext )
|
||||
{
|
||||
// TODO(Ed): Implement
|
||||
}
|
||||
|
||||
parser_load_font :: proc( ctx : ^ParserContext, label : string, data : []byte ) -> (font : ParserFontInfo)
|
||||
{
|
||||
// key := font_key_from_label(label)
|
||||
// font = get( ctx.fonts, key )
|
||||
// if font != nil do return
|
||||
|
||||
// error : AllocatorError
|
||||
// font, error = set( ctx.fonts, key, ParserFontInfo {} )
|
||||
// assert( error == .None, "VEFontCache.parser_load_font: Failed to set a new parser font info" )
|
||||
switch ctx.kind
|
||||
{
|
||||
case .Freetype:
|
||||
error := freetype.new_memory_face( ctx.ft_library, raw_data(data), cast(i32) len(data), 0, & font.freetype_info )
|
||||
if error != .Ok do return
|
||||
|
||||
case .STB_TrueType:
|
||||
success := stbtt.InitFont( & font.stbtt_info, raw_data(data), 0 )
|
||||
if ! success do return
|
||||
}
|
||||
|
||||
font.label = label
|
||||
font.data = data
|
||||
return
|
||||
}
|
||||
|
||||
parser_unload_font :: proc( font : ^ParserFontInfo )
|
||||
{
|
||||
switch font.kind {
|
||||
case .Freetype:
|
||||
error := freetype.done_face( font.freetype_info )
|
||||
assert( error == .Ok, "VEFontCache.parser_unload_font: Failed to unload freetype face" )
|
||||
|
||||
case .STB_TrueType:
|
||||
// Do Nothing
|
||||
}
|
||||
}
|
||||
|
||||
parser_find_glyph_index :: #force_inline proc "contextless" ( font : ^ParserFontInfo, codepoint : rune ) -> (glyph_index : Glyph)
|
||||
{
|
||||
switch font.kind
|
||||
{
|
||||
case .Freetype:
|
||||
glyph_index = transmute(Glyph) freetype.get_char_index( font.freetype_info, transmute(u32) codepoint )
|
||||
return
|
||||
|
||||
case .STB_TrueType:
|
||||
glyph_index = transmute(Glyph) stbtt.FindGlyphIndex( & font.stbtt_info, codepoint )
|
||||
return
|
||||
}
|
||||
return Glyph(-1)
|
||||
}
|
||||
|
||||
parser_free_shape :: proc( font : ^ParserFontInfo, shape : ParserGlyphShape )
|
||||
{
|
||||
switch font.kind
|
||||
{
|
||||
case .Freetype:
|
||||
delete(shape)
|
||||
|
||||
case .STB_TrueType:
|
||||
stbtt.FreeShape( & font.stbtt_info, transmute( [^]stbtt.vertex) raw_data(shape) )
|
||||
}
|
||||
}
|
||||
|
||||
parser_get_codepoint_horizontal_metrics :: #force_inline proc "contextless" ( font : ^ParserFontInfo, codepoint : rune ) -> ( advance, to_left_side_glyph : i32 )
|
||||
{
|
||||
switch font.kind
|
||||
{
|
||||
case .Freetype:
|
||||
glyph_index := transmute(Glyph) freetype.get_char_index( font.freetype_info, transmute(u32) codepoint )
|
||||
if glyph_index != 0
|
||||
{
|
||||
freetype.load_glyph( font.freetype_info, c.uint(codepoint), { .No_Bitmap, .No_Hinting, .No_Scale } )
|
||||
advance = i32(font.freetype_info.glyph.advance.x) >> 6
|
||||
to_left_side_glyph = i32(font.freetype_info.glyph.metrics.hori_bearing_x) >> 6
|
||||
}
|
||||
else
|
||||
{
|
||||
advance = 0
|
||||
to_left_side_glyph = 0
|
||||
}
|
||||
|
||||
case .STB_TrueType:
|
||||
stbtt.GetCodepointHMetrics( & font.stbtt_info, codepoint, & advance, & to_left_side_glyph )
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
parser_get_codepoint_kern_advance :: #force_inline proc "contextless" ( font : ^ParserFontInfo, prev_codepoint, codepoint : rune ) -> i32
|
||||
{
|
||||
switch font.kind
|
||||
{
|
||||
case .Freetype:
|
||||
prev_glyph_index := transmute(Glyph) freetype.get_char_index( font.freetype_info, transmute(u32) prev_codepoint )
|
||||
glyph_index := transmute(Glyph) freetype.get_char_index( font.freetype_info, transmute(u32) codepoint )
|
||||
if prev_glyph_index != 0 && glyph_index != 0
|
||||
{
|
||||
kerning : freetype.Vector
|
||||
font.freetype_info.driver.clazz.get_kerning( font.freetype_info, transmute(u32) prev_codepoint, transmute(u32) codepoint, & kerning )
|
||||
}
|
||||
|
||||
case .STB_TrueType:
|
||||
kern := stbtt.GetCodepointKernAdvance( & font.stbtt_info, prev_codepoint, codepoint )
|
||||
return kern
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
parser_get_font_vertical_metrics :: #force_inline proc "contextless" ( font : ^ParserFontInfo ) -> (ascent, descent, line_gap : i32 )
|
||||
{
|
||||
switch font.kind
|
||||
{
|
||||
case .Freetype:
|
||||
|
||||
case .STB_TrueType:
|
||||
stbtt.GetFontVMetrics( & font.stbtt_info, & ascent, & descent, & line_gap )
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
parser_get_glyph_box :: #force_inline proc ( font : ^ParserFontInfo, glyph_index : Glyph ) -> (bounds_0, bounds_1 : Vec2i)
|
||||
{
|
||||
switch font.kind
|
||||
{
|
||||
case .Freetype:
|
||||
freetype.load_glyph( font.freetype_info, c.uint(glyph_index), { .No_Bitmap, .No_Hinting, .No_Scale } )
|
||||
|
||||
metrics := font.freetype_info.glyph.metrics
|
||||
|
||||
bounds_0 = {i32(metrics.hori_bearing_x), i32(metrics.hori_bearing_y - metrics.height)}
|
||||
bounds_1 = {i32(metrics.hori_bearing_x + metrics.width), i32(metrics.hori_bearing_y)}
|
||||
|
||||
case .STB_TrueType:
|
||||
x0, y0, x1, y1 : i32
|
||||
success := cast(bool) stbtt.GetGlyphBox( & font.stbtt_info, i32(glyph_index), & x0, & y0, & x1, & y1 )
|
||||
assert( success )
|
||||
|
||||
bounds_0 = { i32(x0), i32(y0) }
|
||||
bounds_1 = { i32(x1), i32(y1) }
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
parser_get_glyph_shape :: proc( font : ^ParserFontInfo, glyph_index : Glyph ) -> (shape : ParserGlyphShape, error : AllocatorError)
|
||||
{
|
||||
switch font.kind
|
||||
{
|
||||
case .Freetype:
|
||||
error := freetype.load_glyph( font.freetype_info, cast(u32) glyph_index, { .No_Bitmap, .No_Hinting, .No_Scale } )
|
||||
if error != .Ok {
|
||||
return
|
||||
}
|
||||
|
||||
glyph := font.freetype_info.glyph
|
||||
if glyph.format != .Outline {
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
convert freetype outline to stb_truetype shape
|
||||
|
||||
freetype docs: https://freetype.org/freetype2/docs/glyphs/glyphs-6.html
|
||||
|
||||
stb_truetype shape info:
|
||||
The shape is a series of contours. Each one starts with
|
||||
a STBTT_moveto, then consists of a series of mixed
|
||||
STBTT_lineto and STBTT_curveto segments. A lineto
|
||||
draws a line from previous endpoint to its x,y; a curveto
|
||||
draws a quadratic bezier from previous endpoint to
|
||||
its x,y, using cx,cy as the bezier control point.
|
||||
*/
|
||||
{
|
||||
FT_CURVE_TAG_CONIC :: 0x00
|
||||
FT_CURVE_TAG_ON :: 0x01
|
||||
FT_CURVE_TAG_CUBIC :: 0x02
|
||||
|
||||
vertices, error := make( [dynamic]ParserGlyphVertex, 1024 )
|
||||
assert( error == .None )
|
||||
|
||||
// TODO(Ed): This makes freetype second class I guess but VEFontCache doesn't have native support for freetype originally so....
|
||||
outline := & glyph.outline
|
||||
|
||||
contours := transmute( [^]u16) outline.contours
|
||||
points := transmute( [^]freetype.Vector) outline.points
|
||||
tags := transmute( [^]u8) outline.tags
|
||||
|
||||
// TODO(Ed): Review this, never tested before and its problably bad.
|
||||
for contour : i32 = 0; contour < i32(outline.n_contours); contour += 1
|
||||
{
|
||||
start := (contour == 0) ? 0 : i32(contours[ contour - 1 ] + 1)
|
||||
end := i32(contours[ contour ])
|
||||
|
||||
for index := start; index < i32(outline.n_points); index += 1
|
||||
{
|
||||
point := points[ index ]
|
||||
tag := tags[ index ]
|
||||
|
||||
if (tag & FT_CURVE_TAG_ON) != 0
|
||||
{
|
||||
if len(vertices) > 0 && !(vertices[len(vertices) - 1].type == .Move )
|
||||
{
|
||||
// Close the previous contour if needed
|
||||
append(& vertices, ParserGlyphVertex { type = .Line,
|
||||
x = i16(points[start].x), y = i16(points[start].y),
|
||||
contour_x0 = i16(0), contour_y0 = i16(0),
|
||||
contour_x1 = i16(0), contour_y1 = i16(0),
|
||||
padding = 0,
|
||||
})
|
||||
}
|
||||
|
||||
append(& vertices, ParserGlyphVertex { type = .Move,
|
||||
x = i16(point.x), y = i16(point.y),
|
||||
contour_x0 = i16(0), contour_y0 = i16(0),
|
||||
contour_x1 = i16(0), contour_y1 = i16(0),
|
||||
padding = 0,
|
||||
})
|
||||
}
|
||||
else if (tag & FT_CURVE_TAG_CUBIC) != 0
|
||||
{
|
||||
point1 := points[ index + 1 ]
|
||||
point2 := points[ index + 2 ]
|
||||
append(& vertices, ParserGlyphVertex { type = .Cubic,
|
||||
x = i16(point2.x), y = i16(point2.y),
|
||||
contour_x0 = i16(point.x), contour_y0 = i16(point.y),
|
||||
contour_x1 = i16(point1.x), contour_y1 = i16(point1.y),
|
||||
padding = 0,
|
||||
})
|
||||
index += 2
|
||||
}
|
||||
else if (tag & FT_CURVE_TAG_CONIC) != 0
|
||||
{
|
||||
// TODO(Ed): This is using a very dead simple algo to convert the conic to a cubic curve
|
||||
// not sure if we need something more sophisticaated
|
||||
point1 := points[ index + 1 ]
|
||||
|
||||
control_conv :: f32(0.5) // Conic to cubic control point distance
|
||||
to_float := f32(1.0 / 64.0)
|
||||
|
||||
fp := Vec2 { f32(point.x), f32(point.y) } * to_float
|
||||
fp1 := Vec2 { f32(point1.x), f32(point1.y) } * to_float
|
||||
|
||||
control1 := freetype.Vector {
|
||||
point.x + freetype.Pos( (fp1.x - fp.x) * control_conv * 64.0 ),
|
||||
point.y + freetype.Pos( (fp1.y - fp.y) * control_conv * 64.0 ),
|
||||
}
|
||||
control2 := freetype.Vector {
|
||||
point1.x + freetype.Pos( (fp.x - fp1.x) * control_conv * 64.0 ),
|
||||
point1.y + freetype.Pos( (fp.y - fp1.y) * control_conv * 64.0 ),
|
||||
}
|
||||
append(& vertices, ParserGlyphVertex { type = .Cubic,
|
||||
x = i16(point1.x), y = i16(point1.y),
|
||||
contour_x0 = i16(control1.x), contour_y0 = i16(control1.y),
|
||||
contour_x1 = i16(control2.x), contour_y1 = i16(control2.y),
|
||||
padding = 0,
|
||||
})
|
||||
index += 1
|
||||
}
|
||||
else
|
||||
{
|
||||
append(& vertices, ParserGlyphVertex { type = .Line,
|
||||
x = i16(point.x), y = i16(point.y),
|
||||
contour_x0 = i16(0), contour_y0 = i16(0),
|
||||
contour_x1 = i16(0), contour_y1 = i16(0),
|
||||
padding = 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Close contour
|
||||
append(& vertices, ParserGlyphVertex { type = .Line,
|
||||
x = i16(points[start].x), y = i16(points[start].y),
|
||||
contour_x0 = i16(0), contour_y0 = i16(0),
|
||||
contour_x1 = i16(0), contour_y1 = i16(0),
|
||||
padding = 0,
|
||||
})
|
||||
}
|
||||
|
||||
shape = vertices
|
||||
}
|
||||
|
||||
case .STB_TrueType:
|
||||
stb_shape : [^]stbtt.vertex
|
||||
nverts := stbtt.GetGlyphShape( & font.stbtt_info, cast(i32) glyph_index, & stb_shape )
|
||||
|
||||
shape_raw := transmute( ^runtime.Raw_Dynamic_Array) & shape
|
||||
shape_raw.data = stb_shape
|
||||
shape_raw.len = int(nverts)
|
||||
shape_raw.cap = int(nverts)
|
||||
shape_raw.allocator = runtime.nil_allocator()
|
||||
error = AllocatorError.None
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
parser_is_glyph_empty :: #force_inline proc "contextless" ( font : ^ParserFontInfo, glyph_index : Glyph ) -> b32
|
||||
{
|
||||
switch font.kind
|
||||
{
|
||||
case .Freetype:
|
||||
error := freetype.load_glyph( font.freetype_info, cast(u32) glyph_index, { .No_Bitmap, .No_Hinting, .No_Scale } )
|
||||
if error == .Ok
|
||||
{
|
||||
if font.freetype_info.glyph.format == .Outline {
|
||||
return font.freetype_info.glyph.outline.n_points == 0
|
||||
}
|
||||
else if font.freetype_info.glyph.format == .Bitmap {
|
||||
return font.freetype_info.glyph.bitmap.width == 0 && font.freetype_info.glyph.bitmap.rows == 0;
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
||||
case .STB_TrueType:
|
||||
return stbtt.IsGlyphEmpty( & font.stbtt_info, cast(c.int) glyph_index )
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
parser_scale :: #force_inline proc "contextless" ( font : ^ParserFontInfo, size : f32 ) -> f32
|
||||
{
|
||||
size_scale := size < 0.0 ? \
|
||||
parser_scale_for_pixel_height( font, -size ) \
|
||||
: parser_scale_for_mapping_em_to_pixels( font, size )
|
||||
// size_scale = 1.0
|
||||
return size_scale
|
||||
}
|
||||
|
||||
parser_scale_for_pixel_height :: #force_inline proc "contextless" ( font : ^ParserFontInfo, size : f32 ) -> f32
|
||||
{
|
||||
switch font.kind {
|
||||
case .Freetype:
|
||||
freetype.set_pixel_sizes( font.freetype_info, 0, cast(u32) size )
|
||||
size_scale := size / cast(f32)font.freetype_info.units_per_em
|
||||
return size_scale
|
||||
|
||||
case.STB_TrueType:
|
||||
return stbtt.ScaleForPixelHeight( & font.stbtt_info, size )
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
parser_scale_for_mapping_em_to_pixels :: #force_inline proc "contextless" ( font : ^ParserFontInfo, size : f32 ) -> f32
|
||||
{
|
||||
switch font.kind {
|
||||
case .Freetype:
|
||||
Inches_To_CM :: cast(f32) 2.54
|
||||
Points_Per_CM :: cast(f32) 28.3465
|
||||
CM_Per_Point :: cast(f32) 1.0 / DPT_DPCM
|
||||
CM_Per_Pixel :: cast(f32) 1.0 / DPT_PPCM
|
||||
DPT_DPCM :: cast(f32) 72.0 * Inches_To_CM // 182.88 points/dots per cm
|
||||
DPT_PPCM :: cast(f32) 96.0 * Inches_To_CM // 243.84 pixels per cm
|
||||
DPT_DPI :: cast(f32) 72.0
|
||||
|
||||
// TODO(Ed): Don't assume the dots or pixels per inch.
|
||||
system_dpi :: DPT_DPI
|
||||
|
||||
FT_Font_Size_Point_Unit :: 1.0 / 64.0
|
||||
FT_Point_10 :: 64.0
|
||||
|
||||
points_per_em := (size / system_dpi ) * DPT_DPI
|
||||
freetype.set_char_size( font.freetype_info, 0, cast(freetype.F26Dot6) f32(points_per_em * FT_Point_10), cast(u32) DPT_DPI, cast(u32) DPT_DPI )
|
||||
size_scale := f32(f64(size) / cast(f64) font.freetype_info.units_per_em)
|
||||
return size_scale
|
||||
|
||||
case .STB_TrueType:
|
||||
return stbtt.ScaleForMappingEmToPixels( & font.stbtt_info, size )
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
when false {
|
||||
parser_convert_conic_to_cubic_freetype :: proc( vertices : Array(ParserGlyphVertex), p0, p1, p2 : freetype.Vector, tolerance : f32 )
|
||||
{
|
||||
scratch : [Kilobyte * 4]u8
|
||||
scratch_arena : Arena; arena_init(& scratch_arena, scratch[:])
|
||||
|
||||
points, error := make( Array(freetype.Vector), 256, allocator = arena_allocator( &scratch_arena) )
|
||||
assert(error == .None)
|
||||
|
||||
append( & points, p0)
|
||||
append( & points, p1)
|
||||
append( & points, p2)
|
||||
|
||||
to_float : f32 = 1.0 / 64.0
|
||||
control_conv :: f32(2.0 / 3.0) // Conic to cubic control point distance
|
||||
|
||||
for ; points.num > 1; {
|
||||
p0 := points.data[0]
|
||||
p1 := points.data[1]
|
||||
p2 := points.data[2]
|
||||
|
||||
fp0 := Vec2{ f32(p0.x), f32(p0.y) } * to_float
|
||||
fp1 := Vec2{ f32(p1.x), f32(p1.y) } * to_float
|
||||
fp2 := Vec2{ f32(p2.x), f32(p2.y) } * to_float
|
||||
|
||||
delta_x := fp0.x - 2 * fp1.x + fp2.x;
|
||||
delta_y := fp0.y - 2 * fp1.y + fp2.y;
|
||||
distance := math.sqrt(delta_x * delta_x + delta_y * delta_y);
|
||||
|
||||
if distance <= tolerance
|
||||
{
|
||||
control1 := {
|
||||
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
control2 := {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
132
shaped_text.odin
Normal file
132
shaped_text.odin
Normal file
@@ -0,0 +1,132 @@
|
||||
package VEFontCache
|
||||
|
||||
import "core:math"
|
||||
|
||||
ShapedText :: struct {
|
||||
glyphs : [dynamic]Glyph,
|
||||
positions : [dynamic]Vec2,
|
||||
end_cursor_pos : Vec2,
|
||||
}
|
||||
|
||||
ShapedTextCache :: struct {
|
||||
storage : [dynamic]ShapedText,
|
||||
state : LRU_Cache,
|
||||
next_cache_id : i32,
|
||||
}
|
||||
|
||||
shape_text_cached :: proc( ctx : ^Context, font : FontID, text_utf8 : string, entry : ^Entry ) -> ^ShapedText
|
||||
{
|
||||
// profile(#procedure)
|
||||
@static buffer : [64 * Kilobyte]byte
|
||||
|
||||
font := font
|
||||
text_size := len(text_utf8)
|
||||
sice_end_offset := size_of(FontID) + len(text_utf8)
|
||||
|
||||
buffer_slice := buffer[:]
|
||||
font_bytes := slice_ptr( transmute(^byte) & font, size_of(FontID) )
|
||||
copy( buffer_slice, font_bytes )
|
||||
|
||||
text_bytes := transmute( []byte) text_utf8
|
||||
buffer_slice_post_font := buffer[ size_of(FontID) : sice_end_offset ]
|
||||
copy( buffer_slice_post_font, text_bytes )
|
||||
|
||||
hash := shape_lru_hash( transmute(string) buffer[: sice_end_offset ] )
|
||||
|
||||
shape_cache := & ctx.shape_cache
|
||||
state := & ctx.shape_cache.state
|
||||
|
||||
shape_cache_idx := LRU_get( state, hash )
|
||||
if shape_cache_idx == -1
|
||||
{
|
||||
if shape_cache.next_cache_id < i32(state.capacity) {
|
||||
shape_cache_idx = shape_cache.next_cache_id
|
||||
shape_cache.next_cache_id += 1
|
||||
evicted := LRU_put( state, hash, shape_cache_idx )
|
||||
assert( evicted == hash )
|
||||
}
|
||||
else
|
||||
{
|
||||
next_evict_idx := LRU_get_next_evicted( state )
|
||||
assert( next_evict_idx != 0xFFFFFFFFFFFFFFFF )
|
||||
|
||||
shape_cache_idx = LRU_peek( state, next_evict_idx, must_find = true )
|
||||
assert( shape_cache_idx != - 1 )
|
||||
|
||||
LRU_put( state, hash, shape_cache_idx )
|
||||
}
|
||||
|
||||
shape_text_uncached( ctx, font, text_utf8, entry, & shape_cache.storage[ shape_cache_idx ] )
|
||||
}
|
||||
|
||||
return & shape_cache.storage[ shape_cache_idx ]
|
||||
}
|
||||
|
||||
// TODO(Ed): Make position rounding an option
|
||||
shape_text_uncached :: proc( ctx : ^Context, font : FontID, text_utf8 : string, entry : ^Entry, output : ^ShapedText )
|
||||
{
|
||||
// profile(#procedure)
|
||||
assert( ctx != nil )
|
||||
assert( font >= 0 && int(font) < len(ctx.entries) )
|
||||
|
||||
use_full_text_shape := ctx.text_shape_adv
|
||||
|
||||
clear( & output.glyphs )
|
||||
clear( & output.positions )
|
||||
|
||||
ascent, descent, line_gap := parser_get_font_vertical_metrics( & entry.parser_info )
|
||||
|
||||
if use_full_text_shape
|
||||
{
|
||||
// assert( entry.shaper_info != nil )
|
||||
shaper_shape_from_text( & ctx.shaper_ctx, & entry.shaper_info, output, text_utf8, ascent, descent, line_gap, entry.size, entry.size_scale )
|
||||
return
|
||||
}
|
||||
else
|
||||
{
|
||||
// Note(Original Author):
|
||||
// We use our own fallback dumbass text shaping.
|
||||
// WARNING: PLEASE USE HARFBUZZ. GOOD TEXT SHAPING IS IMPORTANT FOR INTERNATIONALISATION.
|
||||
ascent := f32(ascent)
|
||||
descent := f32(descent)
|
||||
line_gap := f32(line_gap)
|
||||
|
||||
position : Vec2
|
||||
advance : i32 = 0
|
||||
to_left_side_glyph : i32 = 0
|
||||
|
||||
prev_codepoint : rune
|
||||
for codepoint in text_utf8
|
||||
{
|
||||
if prev_codepoint > 0 {
|
||||
kern := parser_get_codepoint_kern_advance( & entry.parser_info, prev_codepoint, codepoint )
|
||||
position.x += f32(kern) * entry.size_scale
|
||||
}
|
||||
if codepoint == '\n'
|
||||
{
|
||||
position.x = 0.0
|
||||
position.y -= (ascent - descent + line_gap) * entry.size_scale
|
||||
position.y = ceil(position.y)
|
||||
prev_codepoint = rune(0)
|
||||
continue
|
||||
}
|
||||
if abs( entry.size ) <= Advance_Snap_Smallfont_Size {
|
||||
position.x = math.ceil( position.x )
|
||||
}
|
||||
|
||||
append( & output.glyphs, parser_find_glyph_index( & entry.parser_info, codepoint ))
|
||||
advance, to_left_side_glyph = parser_get_codepoint_horizontal_metrics( & entry.parser_info, codepoint )
|
||||
|
||||
append( & output.positions, Vec2 {
|
||||
ceil(position.x),
|
||||
position.y
|
||||
})
|
||||
// append( & output.positions, position )
|
||||
|
||||
position.x += f32(advance) * entry.size_scale
|
||||
prev_codepoint = codepoint
|
||||
}
|
||||
|
||||
output.end_cursor_pos = position
|
||||
}
|
||||
}
|
165
shaper.odin
Normal file
165
shaper.odin
Normal file
@@ -0,0 +1,165 @@
|
||||
package VEFontCache
|
||||
/*
|
||||
Note(Ed): The only reason I didn't directly use harfbuzz is because hamza exists and seems to be under active development as an alternative.
|
||||
*/
|
||||
|
||||
import "core:c"
|
||||
import "thirdparty:harfbuzz"
|
||||
|
||||
ShaperKind :: enum {
|
||||
Naive = 0,
|
||||
Harfbuzz = 1,
|
||||
}
|
||||
|
||||
ShaperContext :: struct {
|
||||
hb_buffer : harfbuzz.Buffer,
|
||||
// infos : HMapChained(ShaperInfo),
|
||||
}
|
||||
|
||||
ShaperInfo :: struct {
|
||||
blob : harfbuzz.Blob,
|
||||
face : harfbuzz.Face,
|
||||
font : harfbuzz.Font,
|
||||
}
|
||||
|
||||
shaper_init :: proc( ctx : ^ShaperContext )
|
||||
{
|
||||
ctx.hb_buffer = harfbuzz.buffer_create()
|
||||
assert( ctx.hb_buffer != nil, "VEFontCache.shaper_init: Failed to create harfbuzz buffer")
|
||||
|
||||
// error : AllocatorError
|
||||
// ctx.infos, error = make( HMapChained(ShaperInfo), 256 )
|
||||
// assert( error == .None, "VEFontCache.shaper_init: Failed to create shaper infos map" )
|
||||
}
|
||||
|
||||
shaper_shutdown :: proc( ctx : ^ShaperContext )
|
||||
{
|
||||
if ctx.hb_buffer != nil {
|
||||
harfbuzz.buffer_destory( ctx.hb_buffer )
|
||||
}
|
||||
|
||||
// delete(& ctx.infos)
|
||||
}
|
||||
|
||||
shaper_load_font :: proc( ctx : ^ShaperContext, label : string, data : []byte, user_data : rawptr ) -> (info : ShaperInfo)
|
||||
{
|
||||
// key := font_key_from_label( label )
|
||||
// info = get( ctx.infos, key )
|
||||
// if info != nil do return
|
||||
|
||||
// error : AllocatorError
|
||||
// info, error = set( ctx.infos, key, ShaperInfo {} )
|
||||
// assert( error == .None, "VEFontCache.parser_load_font: Failed to set a new shaper info" )
|
||||
|
||||
using info
|
||||
blob = harfbuzz.blob_create( raw_data(data), cast(c.uint) len(data), harfbuzz.Memory_Mode.READONLY, user_data, nil )
|
||||
face = harfbuzz.face_create( blob, 0 )
|
||||
font = harfbuzz.font_create( face )
|
||||
return
|
||||
}
|
||||
|
||||
shaper_unload_font :: proc( ctx : ^ShaperInfo )
|
||||
{
|
||||
using ctx
|
||||
if blob != nil do harfbuzz.font_destroy( font )
|
||||
if face != nil do harfbuzz.face_destroy( face )
|
||||
if blob != nil do harfbuzz.blob_destroy( blob )
|
||||
}
|
||||
|
||||
shaper_shape_from_text :: proc( ctx : ^ShaperContext, info : ^ShaperInfo, output :^ShapedText, text_utf8 : string,
|
||||
ascent, descent, line_gap : i32, size, size_scale : f32 )
|
||||
{
|
||||
// profile(#procedure)
|
||||
current_script := harfbuzz.Script.UNKNOWN
|
||||
hb_ucfunc := harfbuzz.unicode_funcs_get_default()
|
||||
harfbuzz.buffer_clear_contents( ctx.hb_buffer )
|
||||
assert( info.font != nil )
|
||||
|
||||
ascent := f32(ascent)
|
||||
descent := f32(descent)
|
||||
line_gap := f32(line_gap)
|
||||
|
||||
position, vertical_position : f32
|
||||
shape_run :: proc( buffer : harfbuzz.Buffer, script : harfbuzz.Script, font : harfbuzz.Font, output : ^ShapedText,
|
||||
position, vertical_position : ^f32,
|
||||
ascent, descent, line_gap, size, size_scale : f32 )
|
||||
{
|
||||
// Set script and direction. We use the system's default langauge.
|
||||
// script = HB_SCRIPT_LATIN
|
||||
harfbuzz.buffer_set_script( buffer, script )
|
||||
harfbuzz.buffer_set_direction( buffer, harfbuzz.script_get_horizontal_direction( script ))
|
||||
harfbuzz.buffer_set_language( buffer, harfbuzz.language_get_default() )
|
||||
|
||||
// Perform the actual shaping of this run using HarfBuzz.
|
||||
harfbuzz.shape( font, buffer, nil, 0 )
|
||||
|
||||
// Loop over glyphs and append to output buffer.
|
||||
glyph_count : u32
|
||||
glyph_infos := harfbuzz.buffer_get_glyph_infos( buffer, & glyph_count )
|
||||
glyph_positions := harfbuzz.buffer_get_glyph_positions( buffer, & glyph_count )
|
||||
|
||||
for index : i32; index < i32(glyph_count); index += 1
|
||||
{
|
||||
hb_glyph := glyph_infos[ index ]
|
||||
hb_gposition := glyph_positions[ index ]
|
||||
glyph_id := cast(Glyph) hb_glyph.codepoint
|
||||
|
||||
if hb_glyph.cluster > 0
|
||||
{
|
||||
(position^) = 0.0
|
||||
(vertical_position^) -= (ascent - descent + line_gap) * size_scale
|
||||
(vertical_position^) = cast(f32) i32(vertical_position^ + 0.5)
|
||||
continue
|
||||
}
|
||||
if abs( size ) <= Advance_Snap_Smallfont_Size
|
||||
{
|
||||
(position^) = ceil( position^ )
|
||||
}
|
||||
|
||||
append( & output.glyphs, glyph_id )
|
||||
|
||||
pos := position^
|
||||
v_pos := vertical_position^
|
||||
offset_x := f32(hb_gposition.x_offset) * size_scale
|
||||
offset_y := f32(hb_gposition.y_offset) * size_scale
|
||||
append( & output.positions, Vec2 { cast(f32) i32( pos + offset_x + 0.5 ),
|
||||
v_pos + offset_y,
|
||||
})
|
||||
|
||||
(position^) += f32(hb_gposition.x_advance) * size_scale
|
||||
(vertical_position^) += f32(hb_gposition.y_advance) * size_scale
|
||||
}
|
||||
|
||||
output.end_cursor_pos.x = position^
|
||||
output.end_cursor_pos.y = vertical_position^
|
||||
harfbuzz.buffer_clear_contents( buffer )
|
||||
}
|
||||
|
||||
// Note(Original Author):
|
||||
// We first start with simple bidi and run logic.
|
||||
// True CTL is pretty hard and we don't fully support that; patches welcome!
|
||||
|
||||
for codepoint, byte_offset in text_utf8
|
||||
{
|
||||
script := harfbuzz.unicode_script( hb_ucfunc, cast(harfbuzz.Codepoint) codepoint )
|
||||
|
||||
// Can we continue the current run?
|
||||
ScriptKind :: harfbuzz.Script
|
||||
|
||||
special_script : b32 = script == ScriptKind.UNKNOWN || script == ScriptKind.INHERITED || script == ScriptKind.COMMON
|
||||
if special_script || script == current_script {
|
||||
harfbuzz.buffer_add( ctx.hb_buffer, cast(harfbuzz.Codepoint) codepoint, codepoint == '\n' ? 1 : 0 )
|
||||
current_script = special_script ? current_script : script
|
||||
continue
|
||||
}
|
||||
|
||||
// End current run since we've encountered a script change.
|
||||
shape_run( ctx.hb_buffer, current_script, info.font, output, & position, & vertical_position, ascent, descent, line_gap, size, size_scale )
|
||||
harfbuzz.buffer_add( ctx.hb_buffer, cast(harfbuzz.Codepoint) codepoint, codepoint == '\n' ? 1 : 0 )
|
||||
current_script = script
|
||||
}
|
||||
|
||||
// End the last run if needed
|
||||
shape_run( ctx.hb_buffer, current_script, info.font, output, & position, & vertical_position, ascent, descent, line_gap, size, size_scale )
|
||||
return
|
||||
}
|
Reference in New Issue
Block a user