From 43415446b05984ae382cdee61495a5a5c5f80400 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Fri, 28 Jun 2024 03:26:55 -0400 Subject: [PATCH] VEFontCache: look into to trying to cache the draw_list for a shape plus other misc changes --- code/font/VEFontCache/LRU.odin | 8 +- code/font/VEFontCache/Readme.md | 7 ++ code/font/VEFontCache/VEFontCache.odin | 61 +++++----- .../VEFontCache/docs/draw_text_codepaths.pur | Bin 48520 -> 58941 bytes code/font/VEFontCache/draw.odin | 56 +++++++-- code/font/VEFontCache/misc.odin | 7 -- code/font/VEFontCache/shaped_text.odin | 109 ++++++++++++------ 7 files changed, 160 insertions(+), 88 deletions(-) diff --git a/code/font/VEFontCache/LRU.odin b/code/font/VEFontCache/LRU.odin index db5d440..441bc97 100644 --- a/code/font/VEFontCache/LRU.odin +++ b/code/font/VEFontCache/LRU.odin @@ -28,11 +28,11 @@ PoolList :: struct { pool_list_init :: proc( pool : ^PoolList, capacity : u32, dbg_name : string = "" ) { error : AllocatorError - pool.items, error = make( [dynamic]PoolListItem, u64(capacity) ) + pool.items, error = make( [dynamic]PoolListItem, int(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) ) + pool.free_list, error = make( [dynamic]PoolListIter, len = 0, cap = int(capacity) ) assert( error == .None, "VEFontCache.pool_list_init : Failed to allocate free_list array") resize( & pool.free_list, capacity ) @@ -55,7 +55,7 @@ pool_list_init :: proc( pool : ^PoolList, capacity : u32, dbg_name : string = "" pool_list_free :: proc( pool : ^PoolList ) { - // TODO(Ed): Implement + // TODO(Ed): Implement } pool_list_reload :: proc( pool : ^PoolList, allocator : Allocator ) @@ -160,7 +160,7 @@ LRU_init :: proc( cache : ^LRU_Cache, capacity : u32, dbg_name : string = "" ) { LRU_free :: proc( cache : ^LRU_Cache ) { - // TODO(Ed): Implement + // TODO(Ed): Implement } LRU_reload :: #force_inline proc( cache : ^LRU_Cache, allocator : Allocator ) diff --git a/code/font/VEFontCache/Readme.md b/code/font/VEFontCache/Readme.md index 05f7611..a9dd600 100644 --- a/code/font/VEFontCache/Readme.md +++ b/code/font/VEFontCache/Readme.md @@ -27,3 +27,10 @@ TODO Additional Features: * 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 + +TODO Optimization: + +* Look into caching the draw_list for each shape instead of the glyphs/positions + * Each shape is already constrained to a Entry which is restricted to already a size-class for the glyphs + * Caching a glyph to atlas or generating the draw command for a glyph quad to screen is expensive for large batches. +* Attempt to look into chunking shapes again if caching the draw_list for a shape is found to be optimal diff --git a/code/font/VEFontCache/VEFontCache.odin b/code/font/VEFontCache/VEFontCache.odin index 2a8c710..87d5cae 100644 --- a/code/font/VEFontCache/VEFontCache.odin +++ b/code/font/VEFontCache/VEFontCache.odin @@ -161,25 +161,26 @@ startup :: proc( ctx : ^Context, parser_kind : ParserKind, ctx.curve_quality = curve_quality error : AllocatorError - entries, error = make( [dynamic]Entry, entires_reserve ) + entries, error = make( [dynamic]Entry, len = 0, cap = entires_reserve ) assert(error == .None, "VEFontCache.init : Failed to allocate entries") - temp_path, error = make( [dynamic]Vec2, temp_path_reserve ) + temp_path, error = make( [dynamic]Vec2, 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, 4 * Kilobyte ) + 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, 8 * Kilobyte ) + 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, 512 ) + 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 ) { + init_atlas_region :: proc( region : ^AtlasRegion, params : InitAtlasParams, region_params : InitAtlasRegionParams, factor : Vec2i, expected_cap : i32 ) + { using region next_idx = 0; @@ -225,11 +226,20 @@ startup :: proc( ctx : ^Context, parser_kind : ParserKind, 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 ) + 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, shape_cache_params.reserve_length ) + 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! @@ -241,22 +251,22 @@ startup :: proc( ctx : ^Context, parser_kind : ParserKind, 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 ) + 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, cast(u64) glyph_draw_params.buffer_batch * 2 * 6 ) + 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, glyph_draw_params.buffer_batch * 2 * 4 ) + 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, cast(u64) glyph_draw_params.buffer_batch * 2 ) + 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, cast(u64) glyph_draw_params.buffer_batch * 2 * 4 ) + 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, glyph_draw_params.buffer_batch * 2 * 4 ) + 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" ) } @@ -395,7 +405,7 @@ configure_snap :: #force_inline proc( ctx : ^Context, snap_width, 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 +draw_text :: proc( ctx : ^Context, font : FontID, text_utf8 : string, position, scale : Vec2 ) -> b32 { // profile(#procedure) assert( ctx != nil ) @@ -471,24 +481,9 @@ measure_text_size :: proc( ctx : ^Context, font : FontID, text_utf8 : string ) - 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 + 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 : i32 ) diff --git a/code/font/VEFontCache/docs/draw_text_codepaths.pur b/code/font/VEFontCache/docs/draw_text_codepaths.pur index 061c9b92e9c12bfdb4d990396beaf6d88cf7b206..801029cb8c7067835a2ac67f482eb4a0bf6a1761 100644 GIT binary patch literal 58941 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 literal 48520 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 (cursor_pos : Vec2) { + draw_hash := shape_draw_hash( shaped, position, scale ) + dirty_shape := ! (len(shaped.draw_list.calls) > 0) || draw_hash != shaped.draw_hash + // position := position //+ ctx.cursor_pos * scale // profile(#procedure) batch_start_idx : i32 = 0 @@ -607,7 +630,10 @@ draw_text_shape :: proc( ctx : ^Context, 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 + if check_glyph_in_atlas( ctx, font, entry, glyph_index, lru_code, atlas_index, region_kind, region, over_sample ) do continue + + // We can no longer directly append the shape as it has missing glyphs in the atlas + dirty_shape = true // Glyph has not been catched, needs to be directly drawn. @@ -621,10 +647,18 @@ draw_text_shape :: proc( ctx : ^Context, 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 ) + // if dirty_shape { + 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 ) + // shaped.draw_hash = draw_hash + // } + // else { + // flush_glyph_buffer_to_atlas( ctx ) + // merge_draw_list( & ctx.draw_list, & shaped.draw_list ) + // } reset_batch_codepoint_state( ctx ) - cursor_pos = position + shaped.end_cursor_pos * scae + + cursor_pos = position + shaped.end_cursor_pos * scale return } diff --git a/code/font/VEFontCache/misc.odin b/code/font/VEFontCache/misc.odin index 7cc52a0..733ebdc 100644 --- a/code/font/VEFontCache/misc.odin +++ b/code/font/VEFontCache/misc.odin @@ -50,13 +50,6 @@ font_glyph_lru_code :: #force_inline proc "contextless" ( font : FontID, glyph_i 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 diff --git a/code/font/VEFontCache/shaped_text.odin b/code/font/VEFontCache/shaped_text.odin index 510ed30..50768ef 100644 --- a/code/font/VEFontCache/shaped_text.odin +++ b/code/font/VEFontCache/shaped_text.odin @@ -1,11 +1,13 @@ package VEFontCache -import "core:math" - ShapedText :: struct { + draw_list : DrawList, glyphs : [dynamic]Glyph, positions : [dynamic]Vec2, end_cursor_pos : Vec2, + size : Vec2, + storage_hash : u64, + draw_hash : u64, } ShapedTextCache :: struct { @@ -14,36 +16,68 @@ ShapedTextCache :: struct { next_cache_id : i32, } + +shape_draw_hash :: #force_inline proc "contextless" ( shaped : ^ShapedText, pos, scale : Vec2 ) -> (draw_hash : u64) +{ + pos := pos + scale := scale + pos_bytes := slice_ptr( transmute(^byte) & pos, size_of(Vec2)) + scale_bytes := slice_ptr( transmute(^byte) & scale, size_of(Vec2)) + + draw_hash = shaped.storage_hash + shape_lru_hash( & shaped.draw_hash, pos_bytes ) + shape_lru_hash( & shaped.draw_hash, scale_bytes ) + return +} + +// shape_lru_hash_og :: #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 +// } + +shape_lru_hash :: #force_inline proc "contextless" ( hash : ^u64, bytes : []byte ) { + for value in bytes { + (hash^) = (( (hash^) << 8) + (hash^) ) + u64(value) + } +} + shape_text_cached :: proc( ctx : ^Context, font : FontID, text_utf8 : string, entry : ^Entry ) -> ^ShapedText { // profile(#procedure) - @static buffer : [64 * Kilobyte]byte + font := font + font_bytes := slice_ptr( transmute(^byte) & font, size_of(FontID) ) + text_bytes := transmute( []byte) text_utf8 - font := font - text_size := len(text_utf8) - sice_end_offset := size_of(FontID) + len(text_utf8) + lru_code : u64 + shape_lru_hash( & lru_code, font_bytes ) + shape_lru_hash( & lru_code, text_bytes ) - buffer_slice := buffer[:] - font_bytes := slice_ptr( transmute(^byte) & font, size_of(FontID) ) - copy( buffer_slice, font_bytes ) + // @static buffer : [64 * Kilobyte]byte + // text_size := len(text_utf8) + // sice_end_offset := size_of(FontID) + len(text_utf8) - text_bytes := transmute( []byte) text_utf8 - buffer_slice_post_font := buffer[ size_of(FontID) : sice_end_offset ] - copy( buffer_slice_post_font, text_bytes ) + // buffer_slice := buffer[:] + // copy( buffer_slice, font_bytes ) - hash := shape_lru_hash( transmute(string) buffer[: sice_end_offset ] ) + // buffer_slice_post_font := buffer[ size_of(FontID) : sice_end_offset ] + // copy( buffer_slice_post_font, text_bytes ) + + // lru_code := shape_lru_hash_og( transmute(string) buffer[: sice_end_offset ] ) shape_cache := & ctx.shape_cache state := & ctx.shape_cache.state - shape_cache_idx := LRU_get( state, hash ) + 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, hash, shape_cache_idx ) - assert( evicted == hash ) + evicted := LRU_put( state, lru_code, shape_cache_idx ) + assert( evicted == lru_code ) } else { @@ -53,16 +87,17 @@ shape_text_cached :: proc( ctx : ^Context, font : FontID, text_utf8 : string, en shape_cache_idx = LRU_peek( state, next_evict_idx, must_find = true ) assert( shape_cache_idx != - 1 ) - LRU_put( state, hash, shape_cache_idx ) + LRU_put( state, lru_code, shape_cache_idx ) } - shape_text_uncached( ctx, font, text_utf8, entry, & shape_cache.storage[ shape_cache_idx ] ) + shape_entry := & shape_cache.storage[ shape_cache_idx ] + shape_entry.storage_hash = lru_code + shape_text_uncached( ctx, font, text_utf8, entry, shape_entry ) } 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) @@ -71,15 +106,21 @@ shape_text_uncached :: proc( ctx : ^Context, font : FontID, text_utf8 : string, use_full_text_shape := ctx.text_shape_adv + clear_draw_list( & output.draw_list ) clear( & output.glyphs ) clear( & output.positions ) - ascent, descent, line_gap := parser_get_font_vertical_metrics( & entry.parser_info ) + ascent_i32, descent_i32, line_gap_i32 := parser_get_font_vertical_metrics( & entry.parser_info ) + ascent := f32(ascent_i32) + descent := f32(descent_i32) + line_gap := f32(line_gap_i32) + line_height := (ascent - descent + line_gap) * entry.size_scale 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 ) + shaper_shape_from_text( & ctx.shaper_ctx, & entry.shaper_info, output, text_utf8, ascent_i32, descent_i32, line_gap_i32, entry.size, entry.size_scale ) + // TODO(Ed): Need to be able to provide the text height as well return } else @@ -87,13 +128,10 @@ shape_text_uncached :: proc( ctx : ^Context, font : FontID, text_utf8 : string, // 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 + line_count : int = 1 + max_line_width : f32 = 0 + position : Vec2 prev_codepoint : rune for codepoint in text_utf8 @@ -104,29 +142,34 @@ shape_text_uncached :: proc( ctx : ^Context, font : FontID, text_utf8 : string, } if codepoint == '\n' { - position.x = 0.0 - position.y -= (ascent - descent + line_gap) * entry.size_scale - position.y = ceil(position.y) + line_count += 1 + max_line_width = max(max_line_width, position.x) + position.x = 0.0 + position.y -= line_height + position.y = ceil(position.y) prev_codepoint = rune(0) continue } if abs( entry.size ) <= Advance_Snap_Smallfont_Size { - position.x = math.ceil( position.x ) + position.x = 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 ) + advance, _ := 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 + max_line_width = max(max_line_width, position.x) + + output.size.x = max_line_width + output.size.y = f32(line_count) * line_height } }