mirror of
https://github.com/Ed94/VEFontCache-Odin.git
synced 2025-08-06 06:52:44 -07:00
502 lines
15 KiB
Odin
502 lines
15 KiB
Odin
/*
|
|
A port of (https://github.com/hypernewbie/VEFontCache) to Odin.
|
|
|
|
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 (mostly) 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,
|
|
curve_quality : f32,
|
|
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]Vertex,
|
|
temp_codepoint_seen : map[u64]bool,
|
|
temp_codepoint_seen_num : int,
|
|
|
|
snap_width : f32,
|
|
snap_height : f32,
|
|
|
|
colour : Colour,
|
|
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,
|
|
|
|
default_curve_quality : i32,
|
|
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 : Vec2i,
|
|
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 = 8 * 1024,
|
|
reserve_length = 1 * 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,
|
|
default_curve_quality : u32 = 3,
|
|
entires_reserve : u32 = 512,
|
|
temp_path_reserve : u32 = 1024,
|
|
temp_codepoint_seen_reserve : u32 = 2048,
|
|
)
|
|
{
|
|
assert( ctx != nil, "Must provide a valid context" )
|
|
using ctx
|
|
|
|
ctx.backing = allocator
|
|
context.allocator = ctx.backing
|
|
|
|
if default_curve_quality == 0 {
|
|
default_curve_quality = 3
|
|
}
|
|
ctx.default_curve_quality = default_curve_quality
|
|
|
|
error : AllocatorError
|
|
entries, error = make( [dynamic]Entry, len = 0, cap = entires_reserve )
|
|
assert(error == .None, "VEFontCache.init : Failed to allocate entries")
|
|
|
|
temp_path, error = make( [dynamic]Vertex, len = 0, cap = temp_path_reserve )
|
|
assert(error == .None, "VEFontCache.init : Failed to allocate temp_path")
|
|
|
|
temp_codepoint_seen, error = make( map[u64]bool, uint(temp_codepoint_seen_reserve) )
|
|
assert(error == .None, "VEFontCache.init : Failed to allocate temp_path")
|
|
|
|
draw_list.vertices, error = make( [dynamic]Vertex, len = 0, cap = 4 * Kilobyte )
|
|
assert(error == .None, "VEFontCache.init : Failed to allocate draw_list.vertices")
|
|
|
|
draw_list.indices, error = make( [dynamic]u32, len = 0, cap = 8 * Kilobyte )
|
|
assert(error == .None, "VEFontCache.init : Failed to allocate draw_list.indices")
|
|
|
|
draw_list.calls, error = make( [dynamic]DrawCall, len = 0, cap = 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 = i32(region_params.width)
|
|
height = i32(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
|
|
LRU_init( & state, capacity.x * capacity.y )
|
|
}
|
|
init_atlas_region( & atlas.region_a, atlas_params, atlas_params.region_a, { 4, 2}, 1024 )
|
|
init_atlas_region( & atlas.region_b, atlas_params, atlas_params.region_b, { 4, 2}, 512 )
|
|
init_atlas_region( & atlas.region_c, atlas_params, atlas_params.region_c, { 4, 1}, 512 )
|
|
init_atlas_region( & atlas.region_d, atlas_params, atlas_params.region_d, { 2, 1}, 256 )
|
|
|
|
atlas.width = i32(atlas_params.width)
|
|
atlas.height = i32(atlas_params.height)
|
|
atlas.glyph_padding = i32(atlas_params.glyph_padding)
|
|
|
|
atlas.region_a.offset = {0, 0}
|
|
atlas.region_b.offset.x = 0
|
|
atlas.region_b.offset.y = atlas.region_a.size.y
|
|
atlas.region_c.offset.x = atlas.region_a.size.x
|
|
atlas.region_c.offset.y = 0
|
|
atlas.region_d.offset.x = atlas.width / 2
|
|
atlas.region_d.offset.y = 0
|
|
|
|
LRU_init( & shape_cache.state, i32(shape_cache_params.capacity) )
|
|
|
|
shape_cache.storage, error = make( [dynamic]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, len = 0, cap = shape_cache_params.reserve_length )
|
|
assert( error == .None, "VEFontCache.init : Failed to allocate glyphs array for shape cache storage" )
|
|
|
|
positions, error = make( [dynamic]Vec2, len = 0, cap = shape_cache_params.reserve_length )
|
|
assert( error == .None, "VEFontCache.init : Failed to allocate positions array for shape cache storage" )
|
|
|
|
draw_list.calls, error = make( [dynamic]DrawCall, len = 0, cap = glyph_draw_params.buffer_batch * 2 )
|
|
assert( error == .None, "VEFontCache.init : Failed to allocate calls for draw_list" )
|
|
|
|
draw_list.indices, error = make( [dynamic]u32, len = 0, cap = glyph_draw_params.buffer_batch * 2 * 6 )
|
|
assert( error == .None, "VEFontCache.init : Failed to allocate indices array for draw_list" )
|
|
|
|
draw_list.vertices, error = make( [dynamic]Vertex, len = 0, cap = glyph_draw_params.buffer_batch * 2 * 4 )
|
|
assert( error == .None, "VEFontCache.init : Failed to allocate vertices array for draw_list" )
|
|
}
|
|
|
|
// Note(From original author): We can actually go over VE_FONTCACHE_GLYPHDRAW_BUFFER_BATCH batches due to smart packing!
|
|
{
|
|
using glyph_buffer
|
|
over_sample = vec2(glyph_draw_params.over_sample)
|
|
batch = cast(i32) glyph_draw_params.buffer_batch
|
|
width = atlas.region_d.width * i32(over_sample.x) * batch
|
|
height = atlas.region_d.height * i32(over_sample.y)
|
|
draw_padding = cast(i32) glyph_draw_params.draw_padding
|
|
|
|
draw_list.calls, error = make( [dynamic]DrawCall, len = 0, cap = glyph_draw_params.buffer_batch * 2 )
|
|
assert( error == .None, "VEFontCache.init : Failed to allocate calls for draw_list" )
|
|
|
|
draw_list.indices, error = make( [dynamic]u32, len = 0, cap = glyph_draw_params.buffer_batch * 2 * 6 )
|
|
assert( error == .None, "VEFontCache.init : Failed to allocate indices array for draw_list" )
|
|
|
|
draw_list.vertices, error = make( [dynamic]Vertex, len = 0, cap = glyph_draw_params.buffer_batch * 2 * 4 )
|
|
assert( error == .None, "VEFontCache.init : Failed to allocate vertices array for draw_list" )
|
|
|
|
clear_draw_list.calls, error = make( [dynamic]DrawCall, len = 0, cap = 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, len = 0, cap = 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, len = 0, cap = 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 : i32 = 0; idx < i32(len(shape_cache.storage)); idx += 1 {
|
|
stroage_entry := & shape_cache.storage[idx]
|
|
using stroage_entry
|
|
|
|
reload_array( & glyphs, allocator )
|
|
reload_array( & positions, allocator )
|
|
}
|
|
|
|
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, glyph_curve_quality : u32 = 0 ) -> (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
|
|
used = true
|
|
|
|
parser_info = parser_load_font( & parser_ctx, label, data )
|
|
// assert( parser_info != nil, "VEFontCache.load_font: Failed to load font info from parser" )
|
|
|
|
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")
|
|
|
|
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 )
|
|
|
|
if glyph_curve_quality == 0 {
|
|
curve_quality = f32(ctx.default_curve_quality)
|
|
}
|
|
else {
|
|
curve_quality = f32(glyph_curve_quality)
|
|
}
|
|
}
|
|
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 = f32(snap_width)
|
|
ctx.snap_height = f32(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, scale : Vec2 ) -> b32
|
|
{
|
|
// profile(#procedure)
|
|
assert( ctx != nil )
|
|
assert( font >= 0 && int(font) < len(ctx.entries) )
|
|
|
|
ctx.cursor_pos = {}
|
|
|
|
position := position
|
|
if ctx.snap_width > 0 do position.x = cast(f32) cast(u32) (position.x * ctx.snap_width + 0.5) / ctx.snap_width
|
|
if ctx.snap_height > 0 do position.y = cast(f32) cast(u32) (position.y * ctx.snap_height + 0.5) / ctx.snap_height
|
|
|
|
entry := & ctx.entries[ font ]
|
|
|
|
ChunkType :: enum u32 { Visible, Formatting }
|
|
chunk_kind : ChunkType
|
|
chunk_start : int = 0
|
|
chunk_end : int = 0
|
|
|
|
text_utf8_bytes := transmute([]u8) text_utf8
|
|
text_chunk : string
|
|
|
|
text_chunk = transmute(string) text_utf8_bytes[ : ]
|
|
if len(text_chunk) > 0 {
|
|
shaped := shape_text_cached( ctx, font, text_chunk, entry )
|
|
ctx.cursor_pos = draw_text_shape( ctx, font, entry, shaped, position, scale, ctx.snap_width, ctx.snap_height )
|
|
}
|
|
return true
|
|
}
|
|
|
|
// ve_fontcache_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) )
|
|
|
|
entry := &ctx.entries[font]
|
|
shaped := shape_text_cached(ctx, font, text_utf8, entry)
|
|
return shaped.size
|
|
}
|
|
|
|
get_font_vertical_metrics :: #force_inline proc ( ctx : ^Context, font : FontID ) -> ( ascent, descent, line_gap : f32 )
|
|
{
|
|
assert( ctx != nil )
|
|
assert( font >= 0 && int(font) < len(ctx.entries) )
|
|
|
|
entry := & ctx.entries[ font ]
|
|
ascent_i32, descent_i32, line_gap_i32 := parser_get_font_vertical_metrics( & entry.parser_info )
|
|
|
|
ascent = ceil(f32(ascent_i32) * entry.size_scale)
|
|
descent = ceil(f32(descent_i32) * entry.size_scale)
|
|
line_gap = ceil(f32(line_gap_i32) * entry.size_scale)
|
|
return
|
|
}
|
|
|
|
#endregion("metrics")
|