progress on VEFontCache port

This commit is contained in:
Edward R. Gonzalez 2024-06-05 13:27:34 -04:00
parent 991e7a81c0
commit d469fd53e8
6 changed files with 289 additions and 94 deletions

View File

@ -15,6 +15,8 @@ Changes:
*/
package VEFontCache
Advance_Snap_Smallfont_Size :: 12
FontID :: distinct i64
Glyph :: distinct i32
@ -53,10 +55,6 @@ Entry :: struct {
shaper_info : ^ShaperInfo,
id : FontID,
used : b32,
// Note(Ed) : Not sure how I feel about the size specification here
// I rather have different size glyphs for a font on demand (necessary for the canvas UI)
// Might be mis-understaning how this cache works...
size : f32,
size_scale : f32,
}
@ -94,7 +92,8 @@ Context :: struct {
curve_quality : u32,
text_shape_adv : b32,
debug_print_verbose : b32
debug_print : b32,
debug_print_verbose : b32,
}
get_cursor_pos :: proc( ctx : ^Context ) -> Vec2 { return ctx.cursor_pos }
@ -114,6 +113,54 @@ font_key_from_label :: #force_inline proc( label : string ) -> u64 {
return hash
}
// ve_fontcache_configure_snap
configure_snap :: proc( ctx : ^Context, snap_width, snap_height : u32 ) {
assert( ctx != nil )
ctx.snap_width = snap_width
ctx.snap_height = snap_height
}
// 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 :: proc( p0, p1, p2 : Vec2, alpha : f32 ) -> Vec2
{
starting_point := p0 * (1 - alpha) * (1 - alpha)
control_point := p1 * 2.0 * (1 - alpha)
end_point := p2 * alpha * alpha
point := starting_point + control_point + end_point
return point
}
// 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 :: proc( p0, p1, p2, p3 : Vec2, alpha : f32 ) -> Vec2
{
start_point := p0 * (1 - alpha) * (1 - alpha) * (1 - alpha)
control_a := p1 * 3 * (1 - alpha) * (1 - alpha) * alpha
control_b := p2 * 3 * (1 - alpha) * alpha * alpha
end_point := p3 * alpha * alpha * alpha
point := start_point + control_a + control_b + end_point
return point
}
screenspace_x_form :: proc( position, scale : ^Vec2, width, height : f32 ) {
scale.x = (scale.x / width ) * 2.0
scale.y = (scale.y / height) * 2.0
position.x = position.x * (2.0 / width) - 1.0
position.y = position.y * (2.0 / width) - 1.0
}
textspace_x_form :: proc( position, scale : ^Vec2, width, height : f32 ) {
position.x /= width
position.y /= height
scale.x /= width
scale.y /= height
}
InitAtlasRegionParams :: struct {
width : u32,
height : u32,
@ -182,7 +229,6 @@ init :: proc( ctx : ^Context,
glyph_draw_params := InitGlyphDrawParams_Default,
shape_cache_params := InitShapeCacheParams_Default,
curve_quality : u32 = 6,
advance_snap_smallfont_size : u32 = 12,
entires_reserve : u32 = Kilobyte,
temp_path_reserve : u32 = Kilobyte,
temp_codepoint_seen_reserve : u32 = 512,
@ -358,40 +404,6 @@ unload_font :: proc( ctx : ^Context, font : FontID )
shaper_unload_font( entry.shaper_info )
}
// ve_fontcache_configure_snap
configure_snap :: proc( ctx : ^Context, snap_width, snap_height : u32 ) {
assert( ctx != nil )
ctx.snap_width = snap_width
ctx.snap_height = snap_height
}
// 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 :: proc( p0, p1, p2 : Vec2, alpha : f32 ) -> Vec2
{
starting_point := p0 * (1 - alpha) * (1 - alpha)
control_point := p1 * 2.0 * (1 - alpha)
end_point := p2 * alpha * alpha
point := starting_point + control_point + end_point
return point
}
// 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 :: proc( p0, p1, p2, p3 : Vec2, alpha : f32 ) -> Vec2
{
start_point := p0 * (1 - alpha) * (1 - alpha) * (1 - alpha)
control_a := p1 * 3 * (1 - alpha) * (1 - alpha) * alpha
control_b := p2 * 3 * (1 - alpha) * alpha * alpha
end_point := p3 * alpha * alpha * alpha
point := start_point + control_a + control_b + end_point
return point
}
cache_glyph :: proc( ctx : ^Context, font : FontID, glyph_index : Glyph, scale, translate : Vec2 ) -> b32
{
assert( ctx != nil )
@ -513,20 +525,6 @@ cache_glyph :: proc( ctx : ^Context, font : FontID, glyph_index : Glyph, scale,
return false
}
screenspace_x_form :: proc( position, scale : ^Vec2, width, height : f32 ) {
scale.x = (scale.x / width ) * 2.0
scale.y = (scale.y / height) * 2.0
position.x = position.x * (2.0 / width) - 1.0
position.y = position.y * (2.0 / width) - 1.0
}
textspace_x_form :: proc( position, scale : ^Vec2, width, height : f32 ) {
position.x /= width
position.y /= height
scale.x /= width
scale.y /= height
}
cache_glyph_to_atlas :: proc( ctx : ^Context, font : FontID, glyph_index : Glyph )
{
assert( ctx != nil )
@ -541,12 +539,83 @@ cache_glyph_to_atlas :: proc( ctx : ^Context, font : FontID, glyph_index : Glyph
bounds_width := bounds_1.x - bounds_0.x
bounds_height := bounds_1.y - bounds_0.y
region, state, next_idx, over_sample := decide_codepoint_region( ctx, entry, glyph_index )
region_kind, region, over_sample := decide_codepoint_region( ctx, entry, glyph_index )
// E region is special case and not cached to atlas.
if region == .None || region == .E do return
if region_kind == .None || region_kind == .E do return
// Grab an atlas LRU cache slot.
lru_code := font_glyph_lru_code( font, glyph_index )
atlas_index := LRU_get( & region.state, lru_code )
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 )
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 )
}
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 := Vec2 { f32(bounds_0.x), f32(bounds_0.y) } * glyph_draw_scale + Vec2{ f32(ctx.atlas.glyph_padding), f32(ctx.atlas.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 =
// Calculate the src and destination regions
// Advance glyph_update_batch_x and calculate final glyph drawing transform
// Queue up clear on target region on atlas
// Queue up a blit from glyph_update_FBO to the atlas
// Render glyph to glyph_update_FBO
// cache_glyph( )
}
directly_draw_massive_glyph :: proc( ctx : ^Context, entry : ^Entry, glyph : Glyph, bounds_0 : Vec2i, bounds_width, bounds_height : u32, over_sample, position, scale : Vec2 )
{
flush_glyph_buffer_to_atlas( ctx )
glyph_draw_scale := over_sample * entry.size_scale
glyph_draw_translate := - Vec2{ f32(bounds_0.x), f32(bounds_0.y)} * glyph_draw_scale + Vec2{ f32(ctx.atlas.glyph_padding), f32(ctx.atlas.glyph_padding) }
screenspace_x_form( & glyph_draw_translate, & glyph_draw_scale, f32(ctx.atlas.buffer_width), f32(ctx.atlas.buffer_height) )
cache_glyph( ctx, entry.id, glyph, glyph_draw_scale, glyph_draw_translate )
// Figure out the source rect.
// Figure out the destination rect.
// Add the glyph drawcall.
// Clear glyph_update_FBO.
}
is_empty :: proc( ctx : ^Context, entry : ^Entry, glyph_index : Glyph ) -> b32
@ -597,12 +666,36 @@ shape_text_cached :: proc( ctx : ^Context, font : FontID, text_utf8 : string ) -
LRU_put( state, hash, shape_cache_idx )
}
shape_text_uncached( ctx, font, & shape_cache.storage.data[ shape_cache_idx ], text_utf8 )
}
return & shape_cache.storage.data[ shape_cache_idx ]
}
shape_text_uncached :: proc()
shape_text_uncached :: proc( ctx : ^Context, font : FontID, output : ^ShapedText, text_utf8 : string )
{
assert( ctx != nil )
assert( font >= 0 && font < FontID(ctx.entries.num) )
use_full_text_shape := ctx.text_shape_adv
entry := & ctx.entries.data[ font ]
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
}
// We use our own fallback dumbass text shaping.
// WARNING: PLEASE USE HARFBUZZ. GOOD TEXT SHAPING IS IMPORTANT FOR INTERNATIONALISATION.
}

View File

@ -85,22 +85,22 @@ can_batch_glyph :: proc( ctx : ^Context, font : FontID, entry : ^Entry, glyph_in
// Decide which atlas to target
assert( glyph_index != -1 )
region, state, next_index, over_sample := decide_codepoint_region( ctx, entry, glyph_index )
region_kind, region, over_sample := decide_codepoint_region( ctx, entry, glyph_index )
// E region can't batch
if region == .E || region == .None do return false
if ctx.temp_codepoint_seen_num > 1024 do return false
if region_kind == .E || region_kind == .None do return false
if ctx.temp_codepoint_seen_num > 1024 do return false
// Note(Ed): Why 1024?
// Is this glyph cached?
// lru_code := u64(glyph_index) + ( ( 0x100000000 * u64(font) ) & 0xFFFFFFFF00000000 )
lru_code := font_glyph_lru_code(font, glyph_index)
atlas_index := LRU_get( state, lru_code )
atlas_index := LRU_get( & region.state, lru_code )
if atlas_index == - 1
{
if (next_index^) >= u32(state.capacity) {
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( state )
next_evict_codepoint := LRU_get_next_evicted( & region.state )
seen := get( ctx.temp_codepoint_seen, next_evict_codepoint )
assert(seen != nil)
@ -112,17 +112,17 @@ can_batch_glyph :: proc( ctx : ^Context, font : FontID, entry : ^Entry, glyph_in
cache_glyph_to_atlas( ctx, font, glyph_index )
}
assert( LRU_get( state, lru_code ) != 1 )
assert( LRU_get( & region.state, lru_code ) != 1 )
set( ctx.temp_codepoint_seen, lru_code, true )
ctx.temp_codepoint_seen_num += 1
return true
}
decide_codepoint_region :: proc( ctx : ^Context, entry : ^Entry, glyph_index : Glyph
) -> (region : AtlasRegionKind, state : ^LRU_Cache, next_idx : ^u32, over_sample : Vec2)
) -> (region_kind : AtlasRegionKind, region : ^AtlasRegion, over_sample : Vec2)
{
if parser_is_glyph_empty( entry.parser_info, glyph_index ) {
region = .None
region_kind = .None
}
bounds_0, bounds_1 := parser_get_glyph_box( entry.parser_info, glyph_index )
@ -137,37 +137,32 @@ decide_codepoint_region :: proc( ctx : ^Context, entry : ^Entry, glyph_index : G
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 = .A
state = & atlas.region_a.state
next_idx = & atlas.region_a.next_idx
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 = .B
state = & atlas.region_b.state
next_idx = & atlas.region_b.next_idx
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 = .C
state = & atlas.region_c.state
next_idx = & atlas.region_c.next_idx
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 = .D
state = & atlas.region_d.state
next_idx = & atlas.region_d.next_idx
region_kind = .D
region = & atlas.region_d
}
else if bounds_width_scaled <= atlas.buffer_width && bounds_height_scaled <= atlas.buffer_height
{
// Region 'E' for massive glyphs. These are rendered uncached and un-oversampled.
region = .E
state = nil
next_idx = nil
region_kind = .E
region = nil
if bounds_width_scaled <= atlas.buffer_width / 2 && bounds_height_scaled <= atlas.buffer_height / 2 {
over_sample = { 2.0, 2.0 }
}
@ -177,11 +172,10 @@ decide_codepoint_region :: proc( ctx : ^Context, entry : ^Entry, glyph_index : G
return
}
else {
region = .None
region_kind = .None
return
}
assert(state != nil)
assert(next_idx != nil)
assert(region != nil)
return
}

View File

@ -89,11 +89,6 @@ clear_draw_list :: proc( draw_list : ^DrawList ) {
clear( draw_list.vertices )
}
directly_draw_massive_glyph :: proc( ctx : ^Context, entry : ^Entry, glyph : Glyph, bounds_0 : Vec2i, bounds_width, bounds_height : u32, over_sample, position, scale : Vec2 )
{
}
draw_cached_glyph :: proc( ctx : ^Context, entry : ^Entry, glyph_index : Glyph, position, scale : Vec2 ) -> b32
{
// Glyph not in current font
@ -106,10 +101,10 @@ draw_cached_glyph :: proc( ctx : ^Context, entry : ^Entry, glyph_index : Glyph,
bounds_height := bounds_1.y - bounds_0.y
// Decide which atlas to target
region, state, next_idx, over_sample := decide_codepoint_region( ctx, entry, glyph_index )
region_kind, region, over_sample := decide_codepoint_region( ctx, entry, glyph_index )
// E region is special case and not cached to atlas
if region == .E
if region_kind == .E
{
directly_draw_massive_glyph( ctx, entry, glyph_index, bounds_0, bounds_width, bounds_height, over_sample, position, scale )
return true
@ -118,7 +113,7 @@ draw_cached_glyph :: proc( ctx : ^Context, entry : ^Entry, glyph_index : Glyph,
// Is this codepoint cached?
// lru_code := u64(glyph_index) + ( ( 0x100000000 * u64(entry.id) ) & 0xFFFFFFFF00000000 )
lru_code := font_glyph_lru_code(entry.id, glyph_index)
atlas_index := LRU_get( state, lru_code )
atlas_index := LRU_get( & region.state, lru_code )
if atlas_index == - 1 {
return false
}
@ -126,7 +121,7 @@ draw_cached_glyph :: proc( ctx : ^Context, entry : ^Entry, glyph_index : Glyph,
atlas := & ctx.atlas
// Figure out the source bounding box in the atlas texture
position, width, height := atlas_bbox( atlas, region, u32(atlas_index) )
position, width, height := atlas_bbox( atlas, region_kind, u32(atlas_index) )
glyph_position := position
glyph_width := f32(bounds_width) * entry.size_scale

View File

@ -106,6 +106,18 @@ parser_unload_font :: proc( font : ^ParserFontInfo )
}
}
parser_get_font_vertical_metrics :: proc( 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_scale_for_pixel_height :: #force_inline proc( font : ^ParserFontInfo, size : f32 ) -> f32
{
switch font.kind {

View File

@ -1,6 +1,10 @@
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 "core:math"
import "thirdparty:harfbuzz"
ShaperKind :: enum {
@ -56,3 +60,100 @@ shaper_unload_font :: proc( ctx : ^ShaperInfo )
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 )
{
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.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 math.abs( size ) <= Advance_Snap_Smallfont_Size
{
(position^) = math.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
}

2
thirdparty/harfbuzz vendored

@ -1 +1 @@
Subproject commit 5112039d627bf5f6a683b7aca5bb360e53570d97
Subproject commit d3a08d9fa487dfbc0a1801e86a57f28614d7a308