diff --git a/code/font/VEFontCache/VEFontCache.odin b/code/font/VEFontCache/VEFontCache.odin index 0ff0ce4..8f4951b 100644 --- a/code/font/VEFontCache/VEFontCache.odin +++ b/code/font/VEFontCache/VEFontCache.odin @@ -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. + + } diff --git a/code/font/VEFontCache/atlas.odin b/code/font/VEFontCache/atlas.odin index 5b32152..66aeb7d 100644 --- a/code/font/VEFontCache/atlas.odin +++ b/code/font/VEFontCache/atlas.odin @@ -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 } diff --git a/code/font/VEFontCache/draw.odin b/code/font/VEFontCache/draw.odin index f2fb8b7..31cb53e 100644 --- a/code/font/VEFontCache/draw.odin +++ b/code/font/VEFontCache/draw.odin @@ -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 diff --git a/code/font/VEFontCache/parser.odin b/code/font/VEFontCache/parser.odin index 7ff22c9..750eeb1 100644 --- a/code/font/VEFontCache/parser.odin +++ b/code/font/VEFontCache/parser.odin @@ -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 { diff --git a/code/font/VEFontCache/shaper.odin b/code/font/VEFontCache/shaper.odin index 278d332..9ec8720 100644 --- a/code/font/VEFontCache/shaper.odin +++ b/code/font/VEFontCache/shaper.odin @@ -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 +} diff --git a/thirdparty/harfbuzz b/thirdparty/harfbuzz index 5112039..d3a08d9 160000 --- a/thirdparty/harfbuzz +++ b/thirdparty/harfbuzz @@ -1 +1 @@ -Subproject commit 5112039d627bf5f6a683b7aca5bb360e53570d97 +Subproject commit d3a08d9fa487dfbc0a1801e86a57f28614d7a308