Files
VEFontCache-Odin/vefontcache/shaper.odin
2025-01-10 12:44:53 -05:00

458 lines
14 KiB
Odin

package vefontcache
/*
Note(Ed): The only reason I didn't directly use harfbuzz is:
https://github.com/saidwho12/hamza
and seems to be under active development as an alternative.
*/
import "core:c"
import "thirdparty:harfbuzz"
Shape_Key :: u32
/* A text whose codepoints have had their relevant glyphs and
associated data resolved for processing in a draw list generation stage.
Traditionally a shape only refers to resolving which glyph and
its position should be used for rendering.
For this library's case it also resolves any content that does not have to be done
on a per-frame basis for draw list generation.
* Resolve atlas lru codes
* Resolve glyph bounds and scale
* Resolve atlas region the glyph is associated with.
Ideally the user should resolve this shape once and cache/store it on their side.
They have the best ability to avoid costly lookups.
*/
Shaped_Text :: struct #packed {
glyph : [dynamic]Glyph,
position : [dynamic]Vec2,
atlas_lru_code : [dynamic]Atlas_Key,
region_kind : [dynamic]Atlas_Region_Kind,
bounds : [dynamic]Range2,
end_cursor_pos : Vec2,
size : Vec2,
font_id : Font_ID,
// TODO(Ed): We need to track the font here for usage in user interface when directly drawing the shape.
}
// Ease of use cache, can handle thousands of lookups per frame with ease.
// TODO(Ed) It might perform better with a tailored made hashtable implementation for the LRU_Cache or dedicated array struct/procs for the Shaped_Text.
Shaped_Text_Cache :: struct {
storage : [dynamic]Shaped_Text,
state : LRU_Cache(Shape_Key),
next_cache_id : i32,
}
// Used by shaper_shape_text_cached, allows user to specify their own proc at compile-time without having to rewrite the caching implementation.
Shaper_Shape_Text_Uncached_Proc :: #type proc( ctx : ^Shaper_Context,
atlas : Atlas,
glyph_buffer_size : Vec2,
entry : Entry,
font_px_Size : f32,
font_scale : f32,
text_utf8 : string,
output : ^Shaped_Text
)
// Note(Ed): Not used..
Shaper_Kind :: enum {
Latin = 0,
Harfbuzz = 1,
}
// Not much here other than just keep track of a harfbuzz var and deciding to keep runtime config here used by the shapers.
Shaper_Context :: struct {
hb_buffer : harfbuzz.Buffer,
snap_glyph_position : b32,
adv_snap_small_font_threshold : f32,
}
// Only used with harbuzz for now. Resolved during load_font for a font Entry.
Shaper_Info :: struct {
blob : harfbuzz.Blob,
face : harfbuzz.Face,
font : harfbuzz.Font,
}
shaper_init :: proc( ctx : ^Shaper_Context )
{
ctx.hb_buffer = harfbuzz.buffer_create()
assert( ctx.hb_buffer != nil, "VEFontCache.shaper_init: Failed to create harfbuzz buffer")
}
shaper_shutdown :: proc( ctx : ^Shaper_Context )
{
if ctx.hb_buffer != nil {
harfbuzz.buffer_destroy( ctx.hb_buffer )
}
}
shaper_load_font :: #force_inline proc( ctx : ^Shaper_Context, label : string, data : []byte, user_data : rawptr = nil ) -> (info : Shaper_Info)
{
info.blob = harfbuzz.blob_create( raw_data(data), cast(c.uint) len(data), harfbuzz.Memory_Mode.READONLY, user_data, nil )
info.face = harfbuzz.face_create( info.blob, 0 )
info.font = harfbuzz.font_create( info.face )
return
}
shaper_unload_font :: #force_inline proc( info : ^Shaper_Info )
{
if info.blob != nil do harfbuzz.font_destroy( info.font )
if info.face != nil do harfbuzz.face_destroy( info.face )
if info.blob != nil do harfbuzz.blob_destroy( info.blob )
}
// Recommended shaper. Very performant.
// TODO(Ed): Would be nice to properly support vertical shaping, right now its strictly just horizontal...
@(optimization_mode="favor_size")
shaper_shape_harfbuzz :: proc( ctx : ^Shaper_Context, text_utf8 : string, entry : Entry, font_px_Size, font_scale : f32, output :^Shaped_Text )
{
profile(#procedure)
current_script := harfbuzz.Script.UNKNOWN
hb_ucfunc := harfbuzz.unicode_funcs_get_default()
harfbuzz.buffer_clear_contents( ctx.hb_buffer )
ascent := entry.ascent
descent := entry.descent
line_gap := entry.line_gap
max_line_width := f32(0)
line_count := 1
line_height := ((ascent - descent + line_gap) * font_scale)
position : Vec2
@(optimization_mode="favor_size")
shape_run :: proc( output : ^Shaped_Text,
entry : Entry,
buffer : harfbuzz.Buffer,
script : harfbuzz.Script,
position : ^Vec2,
max_line_width : ^f32,
line_count : ^int,
font_px_size : f32,
font_scale : f32,
snap_shape_pos : b32,
adv_snap_small_font_threshold : f32
)
{
profile(#procedure)
// Set script and direction. We use the system's default langauge.
// script = HB_SCRIPT_LATIN
harfbuzz.buffer_set_script( buffer, script )
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.buffer_set_content_type( buffer, harfbuzz.Buffer_Content_Type.UNICODE )
harfbuzz.shape( entry.shaper_info.font, buffer, nil, 0 )
// Loop over glyphs and append to output buffer.
glyph_count : u32
glyph_infos := harfbuzz.buffer_get_glyph_infos( buffer, & glyph_count )
glyph_positions := harfbuzz.buffer_get_glyph_positions( buffer, & glyph_count )
line_height := (entry.ascent - entry.descent + entry.line_gap) * font_scale
for index : i32; index < i32(glyph_count); index += 1
{
hb_glyph := glyph_infos[ index ]
hb_gposition := glyph_positions[ index ]
glyph := cast(Glyph) hb_glyph.codepoint
if hb_glyph.cluster > 0
{
(max_line_width^) = max( max_line_width^, position.x )
position.x = 0.0
position.y -= line_height
position.y = floor(position.y)
(line_count^) += 1
continue
}
if abs( font_px_size ) <= adv_snap_small_font_threshold
{
(position^) = ceil( position^ )
}
glyph_pos := position^
offset := Vec2 { f32(hb_gposition.x_offset), f32(hb_gposition.y_offset) } * font_scale
glyph_pos += offset
if snap_shape_pos {
glyph_pos = ceil(glyph_pos)
}
advance := Vec2 {
f32(hb_gposition.x_advance) * font_scale,
f32(hb_gposition.y_advance) * font_scale
}
(position^) += advance
(max_line_width^) = max(max_line_width^, position.x)
is_empty := parser_is_glyph_empty(entry.parser_info, glyph)
if ! is_empty {
append( & output.glyph, glyph )
append( & output.position, glyph_pos)
}
}
output.end_cursor_pos = 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
{
hb_codepoint := cast(harfbuzz.Codepoint) codepoint
script := harfbuzz.unicode_script( hb_ucfunc, hb_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 || byte_offset == 0 {
harfbuzz.buffer_add( ctx.hb_buffer, hb_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( output,
entry,
ctx.hb_buffer,
current_script,
& position,
& max_line_width,
& line_count,
font_px_Size,
font_scale,
ctx.snap_glyph_position,
ctx.adv_snap_small_font_threshold
)
harfbuzz.buffer_add( ctx.hb_buffer, hb_codepoint, codepoint == '\n' ? 1 : 0 )
current_script = script
}
// End the last run if needed
shape_run( output,
entry,
ctx.hb_buffer,
current_script,
& position,
& max_line_width,
& line_count,
font_px_Size,
font_scale,
ctx.snap_glyph_position,
ctx.adv_snap_small_font_threshold
)
// Set the final size
output.size.x = max_line_width
output.size.y = f32(line_count) * line_height
return
}
shaper_shape_text_uncached_advanced :: #force_inline proc( ctx : ^Shaper_Context,
atlas : Atlas,
glyph_buffer_size : Vec2,
entry : Entry,
font_px_size : f32,
font_scale : f32,
text_utf8 : string,
output : ^Shaped_Text
)
{
profile(#procedure)
assert( ctx != nil )
clear( & output.glyph )
clear( & output.position )
shaper_shape_harfbuzz( ctx, text_utf8, entry, font_px_size, font_scale, output )
// Resolve each glyphs: bounds, atlas lru, and the atlas region as we have everything we need now.
resize( & output.atlas_lru_code, len(output.glyph) )
resize( & output.region_kind, len(output.glyph) )
resize( & output.bounds, len(output.glyph) )
profile_begin("atlas_lru_code")
for id, index in output.glyph
{
output.atlas_lru_code[index] = atlas_glyph_lru_code(entry.id, font_px_size, id)
}
profile_end()
profile_begin("bounds & region")
for id, index in output.glyph
{
bounds := & output.bounds[index]
(bounds ^) = parser_get_bounds( entry.parser_info, id )
bounds_size_scaled := (bounds.p1 - bounds.p0) * font_scale
output.region_kind[index] = atlas_decide_region( atlas, glyph_buffer_size, bounds_size_scaled )
}
profile_end()
}
// Basic western alphabet based shaping. Not that much faster than harfbuzz if at all.
shaper_shape_text_latin :: proc( ctx : ^Shaper_Context,
atlas : Atlas,
glyph_buffer_size : Vec2,
entry : Entry,
font_px_size : f32,
font_scale : f32,
text_utf8 : string,
output : ^Shaped_Text
)
{
profile(#procedure)
assert( ctx != nil )
clear( & output.glyph )
clear( & output.position )
line_height := (entry.ascent - entry.descent + entry.line_gap) * font_scale
line_count : int = 1
max_line_width : f32 = 0
position : Vec2
prev_codepoint : rune
for codepoint, index in text_utf8
{
if prev_codepoint > 0 {
kern := parser_get_codepoint_kern_advance( entry.parser_info, prev_codepoint, codepoint )
position.x += f32(kern) * font_scale
}
if codepoint == '\n'
{
line_count += 1
max_line_width = max(max_line_width, position.x)
position.x = 0.0
position.y -= line_height
position.y = position.y
prev_codepoint = rune(0)
continue
}
if abs( font_px_size ) <= ctx.adv_snap_small_font_threshold {
position.x = ceil(position.x)
}
glyph_index := parser_find_glyph_index( entry.parser_info, codepoint )
is_glyph_empty := parser_is_glyph_empty( entry.parser_info, glyph_index )
if ! is_glyph_empty
{
append( & output.glyph, glyph_index)
append( & output.position, Vec2 {
ceil(position.x),
ceil(position.y)
})
}
advance, _ := parser_get_codepoint_horizontal_metrics( entry.parser_info, codepoint )
position.x += f32(advance) * font_scale
prev_codepoint = codepoint
}
output.end_cursor_pos = position
max_line_width = max(max_line_width, position.x)
output.size.x = max_line_width
output.size.y = f32(line_count) * line_height
// Resolve each glyphs: bounds, atlas lru, and the atlas region as we have everything we need now.
resize( & output.atlas_lru_code, len(output.glyph) )
resize( & output.region_kind, len(output.glyph) )
resize( & output.bounds, len(output.glyph) )
profile_begin("atlas_lru_code")
for id, index in output.glyph
{
output.atlas_lru_code[index] = atlas_glyph_lru_code(entry.id, font_px_size, id)
}
profile_end()
profile_begin("bounds & region")
for id, index in output.glyph
{
bounds := & output.bounds[index]
(bounds ^) = parser_get_bounds( entry.parser_info, id )
bounds_size_scaled := (bounds.p1 - bounds.p0) * font_scale
output.region_kind[index] = atlas_decide_region( atlas, glyph_buffer_size, bounds_size_scaled )
}
profile_end()
}
// Shapes are tracked by the library's context using the shape cache
// and the key is resolved using the font, the desired pixel size, and the text bytes to be shaped.
// Thus this procedures cost will be proporitonal to how much text it has to sift through.
// djb8_hash is used as its been pretty good for thousands of hashed lines that around 6-250 charactes long
// (and its very fast).
@(optimization_mode="favor_size")
shaper_shape_text_cached :: proc( text_utf8 : string,
ctx : ^Shaper_Context,
shape_cache : ^Shaped_Text_Cache,
atlas : Atlas,
glyph_buffer_size : Vec2,
font : Font_ID,
entry : Entry,
font_px_size : f32,
font_scale : f32,
shape_text_uncached : $Shaper_Shape_Text_Uncached_Proc
) -> (shaped_text : Shaped_Text)
{
profile(#procedure)
font := font
font_px_size := font_px_size
font_bytes := to_bytes( & font )
size_bytes := to_bytes( & font_px_size )
text_bytes := transmute( []byte) text_utf8
lru_code : Shape_Key
djb8_hash( & lru_code, font_bytes )
djb8_hash( & lru_code, size_bytes )
djb8_hash( & lru_code, text_bytes )
state := & shape_cache.state
shape_cache_idx := lru_get( state, lru_code )
if shape_cache_idx == -1
{
if shape_cache.next_cache_id < i32(state.capacity) {
shape_cache_idx = shape_cache.next_cache_id
shape_cache.next_cache_id += 1
evicted := lru_put( state, lru_code, shape_cache_idx )
}
else
{
next_evict_idx := lru_get_next_evicted( state ^ )
assert( next_evict_idx != LRU_Fail_Mask_32 )
shape_cache_idx = lru_peek( state ^, next_evict_idx, must_find = true )
assert( shape_cache_idx != - 1 )
lru_put( state, lru_code, shape_cache_idx )
}
storage_entry := & shape_cache.storage[ shape_cache_idx ]
shape_text_uncached( ctx, atlas, glyph_buffer_size, entry, font_px_size, font_scale, text_utf8, storage_entry )
shaped_text = storage_entry ^
return
}
shaped_text = shape_cache.storage[ shape_cache_idx ]
return
}