From 1741532d64dda16e38f151fa57e309de3a076409 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Mon, 3 Jun 2024 17:43:15 -0400 Subject: [PATCH] Progress on VEFontCache port, working on freetype outline to stbtt shape --- code/font/VEFontCache/VEFontCache.odin | 400 +++++++++++++++++++++- code/font/VEFontCache/mappings.odin | 9 + code/font/VEFontCache/parser.odin | 239 ++++++++++++- code/font/VEFontCache/shaper.odin | 40 +++ code/sectr/font/cache.odin | 1 - code/sectr/font/provider_VEFontCache.odin | 2 +- code/sectr/ui/widgets.odin | 2 +- 7 files changed, 679 insertions(+), 14 deletions(-) diff --git a/code/font/VEFontCache/VEFontCache.odin b/code/font/VEFontCache/VEFontCache.odin index 30c27ae..8dd65d9 100644 --- a/code/font/VEFontCache/VEFontCache.odin +++ b/code/font/VEFontCache/VEFontCache.odin @@ -5,11 +5,17 @@ Status: This port is heavily tied to the grime package in SectrPrototype. TODO(Ed): Make an idiomatic port of this for Odin (or just dupe the data structures...) + +Changes: +- Support for freetype(WIP) +- 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 */ package VEFontCache -Font_ID :: i64 -Glyph :: i32 +FontID :: distinct i64 +Glyph :: distinct i32 Colour :: [4]f32 Vec2 :: [2]f32 @@ -19,7 +25,8 @@ AtlasRegionKind :: enum { A = 0, B = 1, C = 2, - D = 3 + D = 3, + E = 4, } Vertex :: struct { @@ -49,10 +56,14 @@ ShapedTextCache :: struct { } Entry :: struct { - parser_info : ParserInfo, - shaper_info : ShaperInfo, - id : Font_ID, + parser_info : ^ParserFontInfo, + 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, } @@ -87,9 +98,17 @@ Context :: struct { shape_cache : ShapedTextCache, text_shape_adv : b32, + + debug_print_verbose : b32 } -Module_Ctx :: Context +font_key_from_label :: proc( label : string ) -> u64 { + hash : u64 + for str_byte in transmute([]byte) label { + hash = ((hash << 5) + hash) + u64(str_byte) + } + return hash +} InitAtlasRegionParams :: struct { width : u32, @@ -152,6 +171,7 @@ InitShapeCacheParams_Default :: InitShapeCacheParams { reserve_length = 64, } +// ve_fontcache_init init :: proc( ctx : ^Context, allocator := context.allocator, atlas_params := InitAtlasParams_Default, @@ -250,5 +270,371 @@ init :: proc( ctx : ^Context, assert( error != .None, "VEFontCache.init : Failed to allocate vertices array for clear_draw_list" ) } + parser_init( & parser_ctx ) shaper_init( & shaper_ctx ) } + +// ve_foncache_shutdown +shutdown :: proc( ctx : ^Context ) +{ + assert( ctx != nil ) + context.allocator = ctx.backing + using ctx + + for & entry in array_to_slice(entries) { + unload_font( ctx, entry.id ) + } + + shaper_shutdown( & shaper_ctx ) +} + +// ve_fontcache_load +load_font :: proc( ctx : ^Context, label : string, data : []byte, size_px : f32 ) -> FontID +{ + assert( ctx != nil ) + assert( len(data) > 0 ) + using ctx + + id : i32 = -1 + for index : i32 = 0; index < i32(entries.num); index += 1 { + if entries.data[index].used do continue + id = index + break + } + if id == -1 { + append( & entries, Entry {}) + id = cast(i32) entries.num - 1 + } + assert( id >= 0 && id < i32(entries.num) ) + + entry := & entries.data[ id ] + { + using entry + parser_info = parser_load_font( parser_ctx, label, data ) + assert( parser_info != nil, "VEFontCache.load_font: Failed to load font info from parser" ) + + 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 ) + + used = true + + 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") + + return id + } +} + +// ve_fontcache_unload +unload_font :: proc( ctx : ^Context, font : FontID ) +{ + assert( ctx != nil ) + assert( font >= 0 && u64(font) < ctx.entries.num ) + + using ctx + entry := & entries.data[ font ] + entry.used = false + + parser_unload_font( entry.parser_info ) + 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 +} + +// ve_fontcache_drawlist +get_draw_list :: proc( ctx : ^Context ) -> ^DrawList { + assert( ctx != nil ) + return & ctx.draw_list +} + +// ve_fontcache_clear_drawlist +clear_draw_list :: proc( draw_list : ^DrawList ) { + clear( draw_list.calls ) + clear( draw_list.indices ) + clear( draw_list.vertices ) +} + +// ve_fontcache_merge_drawlist +merge_draw_list :: proc( dst, src : ^DrawList ) +{ + error : AllocatorError + + v_offset := cast(u32) dst.vertices.num + // for index : u32 = 0; index < cast(u32) src.vertices.num; index += 1 { + // error = append( & dst.vertices, src.vertices.data[index] ) + // assert( error == .None ) + // } + error = append( & dst.vertices, src.vertices ) + assert( error == .None ) + + i_offset := cast(u32) dst.indices.num + for index : u32 = 0; index < cast(u32) src.indices.num; index += 1 { + error = append( & dst.indices, src.indices.data[index] + v_offset ) + assert( error == .None ) + } + + for index : u32 = 0; index < cast(u32) src.calls.num; index += 1 { + src_call := src.calls.data[ index ] + src_call.start_index += i_offset + src_call.end_index += i_offset + append( & dst.calls, src_call ) + assert( error == .None ) + } +} + +// ve_fontcache_flush_drawlist +flush_draw_list :: proc( ctx : ^Context ) { + assert( ctx != nil ) + clear_draw_list( & ctx.draw_list ) +} + +// 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 +} + +// Constructs a triangle fan to fill a shape using the provided path +// outside_point represents the center point of the fan. +// +// Note(Original Author): +// WARNING: doesn't actually append drawcall; caller is responsible for actually appending the drawcall. +// ve_fontcache_draw_filled_path +draw_filled_path :: proc( draw_list : ^DrawList, outside_point : Vec2, path : []Vec2, + scale := Vec2 { 1, 1 }, + translate := Vec2 { 0, 0 }, + debug_print_verbose : b32 = false +) +{ + if debug_print_verbose + { + log("outline_path: \n") + for point in path { + logf(" %.2f %.2f\n", point.x * scale ) + } + } + + v_offset := cast(u32) draw_list.vertices.num + for point in path { + vertex := Vertex { + pos = point * scale + translate, + u = 0, + v = 0, + } + append( & draw_list.vertices, vertex ) + } + + outside_vertex := cast(u32) draw_list.vertices.num + { + vertex := Vertex { + pos = outside_point * scale + translate, + u = 0, + v = 0, + } + append( & draw_list.vertices, vertex ) + } + + for index : u32 = 1; index < u32(len(path)); index += 1 { + indices := & draw_list.indices + append( indices, outside_vertex ) + append( indices, v_offset + index - 1 ) + append( indices, v_offset + index ) + } +} + +blit_quad :: proc( draw_list : ^DrawList, p0, p1 : Vec2, uv0, uv1 : Vec2 ) +{ + v_offset := cast(u32) draw_list.vertices.num + + vertex := Vertex { + {p0.x, p0.y}, + uv0.x, + uv0.y + } + append( & draw_list.vertices, vertex ) + vertex = Vertex { + {p0.x, p1.y}, + uv0.x, + uv1.y + } + append( & draw_list.vertices, vertex ) + vertex = Vertex { + {p1.x, p0.y}, + uv1.x, + uv0.y + } + append( & draw_list.vertices, vertex ) + vertex = Vertex { + {p1.x, p1.y}, + uv1.x, + uv1.y + } + append( & draw_list.vertices, vertex ) + + quad_indices : []u32 = { + 0, 1, 2, + 2, 1, 3 + } + for index : i32 = 0; index < 6; index += 1 { + append( & draw_list.indices, v_offset + quad_indices[ index ] ) + } +} + +cache_glyph :: proc( ctx : ^Context, font : FontID, glyph_index : Glyph, scale, translate : Vec2 ) -> b32 +{ + assert( ctx != nil ) + assert( font >= 0 && u64(font) < ctx.entries.num ) + entry := & ctx.entries.data[ font ] + if glyph_index == Glyph(0) { + // Note(Original Author): Glyph not in current hb_font + return false + } + + // No shpae to retrieve + if parser_is_glyph_empty( entry.parser_info, glyph_index ) do return true + + // Retrieve the shape definition from the parser. + shape, error := parser_get_glyph_shape( entry.parser_info, glyph_index ) + assert( error == .None ) + if len(shape) == 0 { + return false + } + + if ctx.debug_print_verbose + { + + } + + /* + Note(Original Author): + We need a random point that is outside our shape. We simply pick something diagonally across from top-left bound corner. + Note that this outside point is scaled alongside the glyph in ve_fontcache_draw_filled_path, so we don't need to handle that here. + */ + bounds_0, bounds_1 : Vec2i + // success := parser_get_glyph_box() + + return false +} + +decide_codepoint_region :: proc() -> AtlasRegionKind +{ + return {} +} + +flush_glyph_buffer_to_atlas :: proc() +{ + +} + +screenspace_x_form :: proc() +{ + +} + +textspace_x_form :: proc() +{ + +} + +atlas_bbox :: proc() +{ + +} + +cache_glyph_to_atlas :: proc() +{ + +} + +shape_text_uncached :: proc() +{ + +} + +ELFhash64 :: proc() +{ + +} + +shape_text_cached :: proc() +{ + +} + +directly_draw_massive_glyph :: proc() +{ + +} + +empty :: proc() +{ + +} + +draw_cached_glyph :: proc() +{ + +} + +reset_batch_codepoint_state :: proc() +{ + +} + +can_batch_glyph :: proc() +{ + +} + +draw_text_batch :: proc() +{ + +} + +draw_text :: proc() +{ + +} + +get_cursor_pos :: proc() +{ + +} + +optimize_draw_list :: proc() +{ + +} + +set_colour :: proc() +{ + +} diff --git a/code/font/VEFontCache/mappings.odin b/code/font/VEFontCache/mappings.odin index 36eeeb6..33df591 100644 --- a/code/font/VEFontCache/mappings.odin +++ b/code/font/VEFontCache/mappings.odin @@ -38,6 +38,7 @@ HMapChained :: grime.HMapChained hmap_chained_init :: grime.hmap_chained_init hmap_chained_get :: grime.hmap_chained_get hmap_chained_remove :: grime.hmap_chained_remove +hmap_chained_set :: grime.hmap_chained_set hmap_closest_prime :: grime.hmap_closest_prime // Pool :: grime.Pool @@ -51,6 +52,10 @@ stack_peek_ref :: grime.stack_peek_ref stack_peek :: grime.stack_peek stack_push_contextless :: grime.stack_push_contextless +// logging +log :: grime.log +logf :: grime.logf + //#region("Proc overload mappings") append :: proc { @@ -85,6 +90,10 @@ remove_at :: proc { array_remove_at, } +set :: proc { + hmap_chained_set, +} + to_slice :: proc { array_to_slice, } diff --git a/code/font/VEFontCache/parser.odin b/code/font/VEFontCache/parser.odin index b56f4b7..ea10895 100644 --- a/code/font/VEFontCache/parser.odin +++ b/code/font/VEFontCache/parser.odin @@ -1,19 +1,250 @@ package VEFontCache +/* +Notes: + +Freetype will do memory allocations and has an interface the user can implement. +That interface is not exposed from this parser but could be added to parser_init. +*/ + +import "core:c" import stbtt "vendor:stb/truetype" import freetype "thirdparty:freetype" ParserKind :: enum u32 { - stb_true_type, - freetype, + STB_TrueType, + Freetype, } -ParserInfo :: struct #raw_union { +ParserFontInfo :: struct { + label : string, + kind : ParserKind, + using _ : struct #raw_union { stbtt_info : stbtt.fontinfo, freetype_info : freetype.Face + } } +// Based directly off of stb_truetype's vertex +ParserGlyphVertex :: struct { + x, y : u16, + contour_x0, contour_y0 : u16, + contour_x1, contour_y1 : u16, + type, padding : u8, +} +ParserGlyphShape :: []ParserGlyphVertex + ParserContext :: struct { - ft_library : freetype.Library + kind : ParserKind, + ft_library : freetype.Library, + + fonts : HMapChained(ParserFontInfo), } +parser_init :: proc( ctx : ^ParserContext ) +{ + switch ctx.kind + { + case .Freetype: + result := freetype.init_free_type( & ctx.ft_library ) + assert( result == freetype.Error.Ok, "VEFontCache.parser_init: Failed to initialize freetype" ) + + case .STB_TrueType: + // Do nothing intentional + } + + error : AllocatorError + ctx.fonts, error = make( HMapChained(ParserFontInfo), 256 ) + assert( error == .None, "VEFontCache.parser_init: Failed to allocate fonts array" ) +} + +parser_load_font :: proc( ctx : ParserContext, label : string, data : []byte ) -> (font : ^ParserFontInfo) +{ + key := font_key_from_label(label) + font = get( ctx.fonts, key ) + if font != nil do return + + error : AllocatorError + font, error = set( ctx.fonts, key, ParserFontInfo {} ) + assert( error != .None, "VEFontCache.parser_load_font: Failed to set a new parser font info" ) + switch ctx.kind + { + case .Freetype: + error := freetype.new_memory_face( ctx.ft_library, raw_data(data), cast(i32) len(data), 0, & font.freetype_info ) + if error != .Ok do return + + case .STB_TrueType: + success := stbtt.InitFont( & font.stbtt_info, raw_data(data), 0 ) + if ! success do return + } + + font.label = label + return +} + +parser_unload_font :: proc( font : ^ParserFontInfo ) +{ + switch font.kind { + case .Freetype: + error := freetype.done_face( font.freetype_info ) + assert( error == .Ok, "VEFontCache.parser_unload_font: Failed to unload freetype face" ) + + case .STB_TrueType: + // Do Nothing + } +} + +parser_scale_for_pixel_height :: #force_inline proc( font : ^ParserFontInfo, size : f32 ) -> f32 +{ + switch font.kind { + case .Freetype: + freetype.set_pixel_sizes( font.freetype_info, 0, cast(u32) size ) + size_scale := size / cast(f32)font.freetype_info.units_per_em + return size_scale + + case.STB_TrueType: + return stbtt.ScaleForPixelHeight( & font.stbtt_info, size ) + } + return 0 +} + +parser_scale_for_mapping_em_to_pixels :: proc( font : ^ParserFontInfo, size : f32 ) -> f32 +{ + switch font.kind { + case .Freetype: + Inches_To_CM :: cast(f32) 2.54 + Points_Per_CM :: cast(f32) 28.3465 + CM_Per_Point :: cast(f32) 1.0 / DPT_DPCM + CM_Per_Pixel :: cast(f32) 1.0 / DPT_PPCM + DPT_DPCM :: cast(f32) 72.0 * Inches_To_CM // 182.88 points/dots per cm + DPT_PPCM :: cast(f32) 96.0 * Inches_To_CM // 243.84 pixels per cm + DPT_DPI :: cast(f32) 72.0 + + // TODO(Ed): Don't assume the dots or pixels per inch. + system_dpi :: DPT_DPI + + FT_Font_Size_Point_Unit :: 1.0 / 64.0 + FT_Point_10 :: 64.0 + + points_per_em := (size / system_dpi ) * DPT_DPI + freetype.set_char_size( font.freetype_info, 0, cast(freetype.F26Dot6) (f32(points_per_em) * FT_Point_10), cast(u32) DPT_DPI, cast(u32) DPT_DPI ) + size_scale := size / cast(f32) font.freetype_info.units_per_em; + return size_scale + + case .STB_TrueType: + return stbtt.ScaleForMappingEmToPixels( & font.stbtt_info, size ) + } + return 0 +} + +parser_is_glyph_empty :: proc( font : ^ParserFontInfo, glyph_index : Glyph ) -> b32 +{ + switch font.kind + { + case .Freetype: + error := freetype.load_glyph( font.freetype_info, cast(u32) glyph_index, { .No_Bitmap, .No_Hinting, .No_Scale } ) + if error == .Ok + { + if font.freetype_info.glyph.format == .Outline { + return font.freetype_info.glyph.outline.n_points == 0 + } + else if font.freetype_info.glyph.format == .Bitmap { + return font.freetype_info.glyph.bitmap.width == 0 && font.freetype_info.glyph.bitmap.rows == 0; + } + } + return false + + case .STB_TrueType: + return stbtt.IsGlyphEmpty( & font.stbtt_info, cast(c.int) glyph_index ) + } + return false +} + +// TODO(Ed): This makes freetype second class I guess but VEFontCache doesn't have native support for freetype originally so.... +// parser_convert_freetype_outline_to_stb_truetype_shape :: proc( outline : freetype.Outline ) -> (shape : ParserGlyphShape, error : AllocatorError) +// { + +// } + +parser_get_glyph_shape :: proc( font : ^ParserFontInfo, glyph_index : Glyph ) -> (shape : ParserGlyphShape, error : AllocatorError) +{ + switch font.kind + { + case .Freetype: + error := freetype.load_glyph( font.freetype_info, cast(u32) glyph_index, { .No_Bitmap, .No_Hinting, .No_Scale } ) + if error != .Ok { + return + } + + glyph := font.freetype_info.glyph + if glyph.format != .Outline { + return + } + + /* + convert freetype outline to stb_truetype shape + + freetype docs: https://freetype.org/freetype2/docs/glyphs/glyphs-6.html + + stb_truetype shape info: + The shape is a series of contours. Each one starts with + a STBTT_moveto, then consists of a series of mixed + STBTT_lineto and STBTT_curveto segments. A lineto + draws a line from previous endpoint to its x,y; a curveto + draws a quadratic bezier from previous endpoint to + its x,y, using cx,cy as the bezier control point. + */ + { + FT_CURVE_TAG_CONIC :: 0x00 + FT_CURVE_TAG_ON :: 0x01 + FT_CURVE_TAG_CUBIC :: 0x02 + + // TODO(Ed): This makes freetype second class I guess but VEFontCache doesn't have native support for freetype originally so.... + outline := & glyph.outline + + contours := transmute([^]i16) outline.contours + for contour : i32 = 0; contour < i32(outline.n_contours); contour += 1 + { + start_point := (contour == 0) ? 0 : i32( contours[contour - 1] + 1) + end_point := i32(contours[contour]) + + for index := start_point; index < end_point; index += 1 + { + points := transmute( [^]freetype.Vector) outline.points + tags := transmute( [^]u8) outline.tags + + point := points[index] + tag := tags[index] + + next_index := (index == end_point) ? start_point : index + 1 + next_point := points[next_index] + next_tag := tags[index] + + if (tag & FT_CURVE_TAG_CONIC) > 0 { + + } + } + } + } + + case .STB_TrueType: + stb_shape : [^]stbtt.vertex + nverts := stbtt.GetGlyphShape( & font.stbtt_info, cast(i32) glyph_index, & stb_shape ) + if nverts == 0 || shape == nil { + shape = transmute(ParserGlyphShape) stb_shape[0:0] + } + shape = transmute(ParserGlyphShape) stb_shape[:nverts] + error = AllocatorError.None + return + } + + return +} + +parser_free_shape :: proc( font : ^ParserFontInfo, shape : ParserGlyphShape ) +{ + // switch font.kind + // { + // case .Freetype + // } +} diff --git a/code/font/VEFontCache/shaper.odin b/code/font/VEFontCache/shaper.odin index f334ec0..278d332 100644 --- a/code/font/VEFontCache/shaper.odin +++ b/code/font/VEFontCache/shaper.odin @@ -1,9 +1,17 @@ package VEFontCache +import "core:c" import "thirdparty:harfbuzz" +ShaperKind :: enum { + Naive = 0, + Harfbuzz = 1, +} + ShaperContext :: struct { hb_buffer : harfbuzz.Buffer, + + infos : HMapChained(ShaperInfo), } ShaperInfo :: struct { @@ -16,3 +24,35 @@ shaper_init :: proc( ctx : ^ShaperContext ) { ctx.hb_buffer = harfbuzz.buffer_create() } + +shaper_shutdown :: proc( ctx : ^ShaperContext ) +{ + if ctx.hb_buffer != nil { + harfbuzz.buffer_destory( ctx.hb_buffer ) + } +} + +shaper_load_font :: proc( ctx : ^ShaperContext, label : string, data : []byte, user_data : rawptr ) -> (info : ^ShaperInfo) +{ + key := font_key_from_label( label ) + info = get( ctx.infos, key ) + if info != nil do return + + error : AllocatorError + info, error = set( ctx.infos, key, ShaperInfo {} ) + assert( error != .None, "VEFontCache.parser_load_font: Failed to set a new shaper info" ) + + using info + blob = harfbuzz.blob_create( raw_data(data), cast(c.uint) len(data), harfbuzz.Memory_Mode.READONLY, user_data, nil ) + face = harfbuzz.face_create( blob, 0 ) + font = harfbuzz.font_create( face ) + return +} + +shaper_unload_font :: proc( ctx : ^ShaperInfo ) +{ + using ctx + if blob != nil do harfbuzz.font_destroy( font ) + if face != nil do harfbuzz.face_destroy( face ) + if blob != nil do harfbuzz.blob_destroy( blob ) +} diff --git a/code/sectr/font/cache.odin b/code/sectr/font/cache.odin index 4da2929..2432dbc 100644 --- a/code/sectr/font/cache.odin +++ b/code/sectr/font/cache.odin @@ -1,2 +1 @@ - package sectr diff --git a/code/sectr/font/provider_VEFontCache.odin b/code/sectr/font/provider_VEFontCache.odin index a23da7d..64fc184 100644 --- a/code/sectr/font/provider_VEFontCache.odin +++ b/code/sectr/font/provider_VEFontCache.odin @@ -1,3 +1,3 @@ package sectr -import "codebase:font/VEFontCache" \ No newline at end of file +import "codebase:font/VEFontCache" diff --git a/code/sectr/ui/widgets.odin b/code/sectr/ui/widgets.odin index 599c2ab..ba23fe7 100644 --- a/code/sectr/ui/widgets.odin +++ b/code/sectr/ui/widgets.odin @@ -359,7 +359,7 @@ ui_resizable_handles :: proc( parent : ^UI_Widget, pos : ^Vec2, size : ^Vec2, } } - process_handle_drag :: #force_inline proc ( handle : ^UI_Widget, + process_handle_drag :: proc ( handle : ^UI_Widget, direction : Vec2, target_alignment : Vec2, target_center_aligned : Vec2,