diff --git a/code/font/vefontcache/LRU.odin b/code/font/vefontcache/LRU.odin index f04da54..69b2615 100644 --- a/code/font/vefontcache/LRU.odin +++ b/code/font/vefontcache/LRU.odin @@ -1,4 +1,4 @@ -package VEFontCache +package vefontcache /* The choice was made to keep the LRU cache implementation as close to the original as possible. @@ -54,7 +54,8 @@ pool_list_init :: proc( pool : ^PoolList, capacity : i32, dbg_name : string = "" } pool_list_free :: proc( pool : ^PoolList ) { - // TODO(Ed): Implement + delete( pool.items) + delete( pool.free_list) } pool_list_reload :: proc( pool : ^PoolList, allocator : Allocator ) { @@ -174,7 +175,8 @@ LRU_init :: proc( cache : ^LRU_Cache, capacity : i32, dbg_name : string = "" ) { } LRU_free :: proc( cache : ^LRU_Cache ) { - // TODO(Ed): Implement + pool_list_free( & cache.key_queue ) + delete( cache.table ) } 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 d6583fe..b959d44 100644 --- a/code/font/vefontcache/Readme.md +++ b/code/font/vefontcache/Readme.md @@ -6,43 +6,38 @@ Its original purpose was for use in game engines, however its rendeirng quality See: [docs/Readme.md](docs/Readme.md) for the library's interface +## Building + +See [scripts/Readme.md](scripts/Readme.md) for building examples or utilizing the provided backends. + +Currently the scripts provided & the library itself where developed & tested on Windows. The library itself should not be limited to that OS platform however, just don't have the configuration setup for alternative platforms (yet). + +The library depends on freetype, harfbuzz, & stb_truetype currently to build. +Note: freetype and harfbuzz could technically be gutted if the user removes their definitions, however they have not been made into a conditional compilation option (yet). + ## Changes from orignal * Font Parser & Glyph shaper are abstracted to their own interface -* Font face parser info encapsulated in parser_info struct. * ve_fontcache_loadfile not ported (ust use core:os or os2, then call load_font) * Macro defines have been coverted (mostly) to runtime parameters * Support for hot_reloading +* Curve quality step granularity for glyph rendering can be set on a per font basis. ## TODOs -### Thirdparty support: - -* Setup freetype, harfbuzz, depedency management within the library - -### Documentation: - -* Pureref outline of draw_text exectuion -* Markdown general documentation - -### Content: - -* Port over the original demo utilizing sokol libraries instead -* Provide a sokol_gfx backend package - ### Additional Features: -* Support for freetype -* Support for harfbuzz +* Support for freetype (WIP, Currently a mess... and slow) +* Add ability to conditionally compile dependencies (so that the user may not need to resolve those packages). * Ability to set a draw transform, viewport and projection * By default the library's position is in unsigned normalized render space * Could implement a similar design to sokol_gp's interface -* Allow curve_quality to be set on a per-font basis ### Optimization: +* Check if its better to store the generated glyph vertices if they need to be re-cached or directly drawn. * Look into setting up multi-threading by giving each thread a context - * There is a heavy performance bottleneck in iterating the text/shape/glyphs on the cpu (single-thread) vs the actual rendering + * There is a heavy performance bottleneck in iterating the text/shape/glyphs on the cpu (single-thread) vs the actual rendering *(if doing thousands of drawing commands)* * draw_text can provide in the context a job list per thread for the user to thenk hookup to their own threading solution to handle. * Context would need to be segregated into staged data structures for each thread to utilize * Each should have their own? diff --git a/code/font/vefontcache/atlas.odin b/code/font/vefontcache/atlas.odin index 600a826..47876b0 100644 --- a/code/font/vefontcache/atlas.odin +++ b/code/font/vefontcache/atlas.odin @@ -1,4 +1,4 @@ -package VEFontCache +package vefontcache AtlasRegionKind :: enum u8 { None = 0x00, @@ -86,8 +86,7 @@ atlas_bbox :: proc( atlas : ^Atlas, region : AtlasRegionKind, local_idx : i32 ) return } -decide_codepoint_region :: proc(ctx : ^Context, entry : ^Entry, glyph_index : Glyph -) -> (region_kind : AtlasRegionKind, region : ^AtlasRegion, over_sample : Vec2) +decide_codepoint_region :: proc(ctx : ^Context, entry : ^Entry, glyph_index : Glyph ) -> (region_kind : AtlasRegionKind, region : ^AtlasRegion, over_sample : Vec2) { if parser_is_glyph_empty(&entry.parser_info, glyph_index) { return .None, nil, {} diff --git a/code/font/vefontcache/docs/Readme.md b/code/font/vefontcache/docs/Readme.md index 010f9ce..b0c3aef 100644 --- a/code/font/vefontcache/docs/Readme.md +++ b/code/font/vefontcache/docs/Readme.md @@ -39,11 +39,11 @@ You'll find this used immediately in draw_text it acts as a way to snap the posi If snapping is not desired, set the snap_width and height before calling draw_text to 0. -## get_cursor_pos +### get_cursor_pos Will provide the current cursor_pos for the resulting text drawn. -## set_color +### set_color Sets the color to utilize on `DrawCall`s for FrameBuffer.Target or .Target_Uncached passes @@ -72,6 +72,6 @@ Will update the draw list layer with the latest offset based on the current leng Provides a Vec2 the width and height occupied by the provided text string. The y is measured to be the the largest glyph box bounds height of the text. The width is derived from the `end_cursor_pos` field from a `ShapedText` entry. -## get_font_vertical_metrics +### get_font_vertical_metrics A wrapper for `parser_get_font_vertical_metrics`. Will provide the ascent, descent, and line_gap for a font entry. diff --git a/code/font/vefontcache/docs/draw_text_codepaths.pur b/code/font/vefontcache/docs/draw_text_codepaths.pur deleted file mode 100644 index 801029c..0000000 Binary files a/code/font/vefontcache/docs/draw_text_codepaths.pur and /dev/null differ diff --git a/code/font/vefontcache/draw.odin b/code/font/vefontcache/draw.odin index 983871e..5fcd883 100644 --- a/code/font/vefontcache/draw.odin +++ b/code/font/vefontcache/draw.odin @@ -1,4 +1,7 @@ -package VEFontCache +package vefontcache + +import "thirdparty:freetype" +import "core:slice" Vertex :: struct { pos : Vec2, @@ -29,6 +32,7 @@ DrawList :: struct { calls : [dynamic]DrawCall, } +// TODO(Ed): This was a rough translation of the raw values the orignal was using, need to give better names... FrameBufferPass :: enum u32 { None = 0, Glyph = 1, @@ -84,6 +88,162 @@ blit_quad :: proc( draw_list : ^DrawList, p0 : Vec2 = {0, 0}, p1 : Vec2 = {1, 1} return } +// TODO(Ed): glyph caching cannot be handled in a 'font parser' abstraction. Just going to have explicit procedures to grab info neatly... +cache_glyph_freetype :: proc(ctx: ^Context, font: FontID, glyph_index: Glyph, entry: ^Entry, bounds_0, bounds_1: Vec2, scale, translate: Vec2) -> b32 +{ + draw_filled_path_freetype :: proc( draw_list : ^DrawList, outside_point : Vec2, path : []Vertex, + scale := Vec2 { 1, 1 }, + translate := Vec2 { 0, 0 }, + debug_print_verbose : b32 = false + ) + { + if debug_print_verbose { + log("outline_path:") + for point in path { + vec := point.pos * scale + translate + logf(" %0.2f %0.2f", vec.x, vec.y ) + } + } + + v_offset := cast(u32) len(draw_list.vertices) + for point in path + { + transformed_point := Vertex { + pos = point.pos * scale + translate, + u = 0, + v = 0 + } + append( & draw_list.vertices, transformed_point ) + } + + if len(path) > 2 + { + indices := & draw_list.indices + for index : u32 = 1; index < cast(u32) len(path) - 1; index += 1 { + to_add := [3]u32 { + v_offset, + v_offset + index, + v_offset + index + 1 + } + append( indices, ..to_add[:] ) + } + + // Close the path by connecting the last vertex to the first two + to_add := [3]u32 { + v_offset, + v_offset + cast(u32)(len(path) - 1), + v_offset + 1 + } + append( indices, ..to_add[:] ) + } + } + + if glyph_index == Glyph(0) { + return false + } + + face := entry.parser_info.freetype_info + error := freetype.load_glyph(face, u32(glyph_index), {.No_Bitmap, .No_Scale}) + if error != .Ok { + return false + } + + glyph := face.glyph + if glyph.format != .Outline { + return false + } + + outline := &glyph.outline + if outline.n_points == 0 { + return false + } + + draw := DrawCall_Default + draw.pass = FrameBufferPass.Glyph + draw.start_index = cast(u32) len(ctx.draw_list.indices) + + contours := slice.from_ptr(cast( [^]i16) outline.contours, int(outline.n_contours)) + points := slice.from_ptr(cast( [^]freetype.Vector) outline.points, int(outline.n_points)) + tags := slice.from_ptr(cast( [^]u8) outline.tags, int(outline.n_points)) + + path := &ctx.temp_path + clear(path) + + outside := Vec2{ bounds_0.x - 21, bounds_0.y - 33 } + + start_index: int = 0 + for contour_index in 0 ..< int(outline.n_contours) + { + end_index := int(contours[contour_index]) + 1 + prev_point: Vec2 + first_point: Vec2 + + for idx := start_index; idx < end_index; idx += 1 + { + current_pos := Vec2 { f32( points[idx].x ), f32( points[idx].y ) } + if ( tags[idx] & 1 ) == 0 + { + // If current point is off-curve + if (idx == start_index || (tags[ idx - 1 ] & 1) != 0) + { + // current is the first or following an on-curve point + prev_point = current_pos + } + else + { + // current and previous are off-curve, calculate midpoint + midpoint := (prev_point + current_pos) * 0.5 + append( path, Vertex { pos = midpoint } ) // Add midpoint as on-curve point + if idx < end_index - 1 + { + // perform interp from prev_point to current_pos via midpoint + step := 1.0 / entry.curve_quality + for alpha : f32 = 0.0; alpha <= 1.0; alpha += step + { + bezier_point := eval_point_on_bezier3( prev_point, midpoint, current_pos, alpha ) + append( path, Vertex{ pos = bezier_point } ) + } + } + + prev_point = current_pos + } + } + else + { + if idx == start_index { + first_point = current_pos + } + if prev_point != (Vec2{}) { + // there was an off-curve point before this + append(path, Vertex{ pos = prev_point}) // Ensure previous off-curve is handled + } + append(path, Vertex{ pos = current_pos}) + prev_point = {} + } + } + + // ensure the contour is closed + if path[0].pos != path[ len(path) - 1 ].pos { + append(path, Vertex{pos = path[0].pos}) + } + draw_filled_path(&ctx.draw_list, bounds_0, path[:], scale, translate, ctx.debug_print_verbose) + clear(path) + start_index = end_index + } + + if len(path) > 0 { + draw_filled_path(&ctx.draw_list, outside, path[:], scale, translate, ctx.debug_print_verbose) + } + + draw.end_index = cast(u32) len(ctx.draw_list.indices) + if draw.end_index > draw.start_index { + append( & ctx.draw_list.calls, draw) + } + + return true +} + +// TODO(Ed): Is it better to cache the glyph vertices for when it must be re-drawn (directly or two atlas)? cache_glyph :: proc(ctx : ^Context, font : FontID, glyph_index : Glyph, entry : ^Entry, bounds_0, bounds_1 : Vec2, scale, translate : Vec2) -> b32 { // profile(#procedure) @@ -91,6 +251,12 @@ cache_glyph :: proc(ctx : ^Context, font : FontID, glyph_index : Glyph, entry : return false } + // Glyph shape handling are not abstractable between freetype and stb_truetype + if entry.parser_info.kind == .Freetype { + result := cache_glyph_freetype( ctx, font, glyph_index, entry, bounds_0, bounds_1, scale, translate ) + return result + } + shape, error := parser_get_glyph_shape(&entry.parser_info, glyph_index) assert(error == .None) if len(shape) == 0 { @@ -150,7 +316,7 @@ cache_glyph :: proc(ctx : ^Context, font : FontID, glyph_index : Glyph, entry : draw.end_index = u32(len(ctx.draw_list.indices)) if draw.end_index > draw.start_index { - append(&ctx.draw_list.calls, draw) + append( & ctx.draw_list.calls, draw) } parser_free_shape(&entry.parser_info, shape) @@ -475,7 +641,7 @@ draw_text_batch :: proc(ctx: ^Context, entry: ^Entry, shaped: ^ShapedText, bounds_size := Vec2 { vbounds_1.x - vbounds_0.x, vbounds_1.y - vbounds_0.y } shaped_position := shaped.positions[index] - glyph_translate := position + shaped_position * scale + glyph_translate := position + (shaped_position) * scale if region_kind == .E { diff --git a/code/font/vefontcache/mappings.odin b/code/font/vefontcache/mappings.odin index cc24907..46ea639 100644 --- a/code/font/vefontcache/mappings.odin +++ b/code/font/vefontcache/mappings.odin @@ -1,4 +1,4 @@ -package VEFontCache +package vefontcache import "core:hash" fnv64a :: hash.fnv64a diff --git a/code/font/vefontcache/misc.odin b/code/font/vefontcache/misc.odin index 1b04560..fe591ea 100644 --- a/code/font/vefontcache/misc.odin +++ b/code/font/vefontcache/misc.odin @@ -1,4 +1,4 @@ -package VEFontCache +package vefontcache import "base:runtime" import "core:simd" @@ -20,23 +20,23 @@ vec2i_from_vec2 :: #force_inline proc "contextless" ( v2 : Vec2 ) -> Vec2 @(require_results) floor_vec2 :: proc "contextless" ( v : Vec2 ) -> Vec2 { return { floor_f32(v.x), floor_f32(v.y) } } // This buffer is used below excluisvely to prevent any allocator recusion when verbose logging from allocators. -// This means a single line is limited to 32k buffer (increase naturally if this SOMEHOW becomes a bottleneck...) -// Logger_Allocator_Buffer : [32 * Kilobyte]u8 +// This means a single line is limited to 4k buffer +// Logger_Allocator_Buffer : [4 * Kilobyte]u8 // log :: proc( msg : string, level := core_log.Level.Info, loc := #caller_location ) { -// temp_arena : Arena; arena_init(& temp_arena, Logger_Allocator_Buffer[:]) -// context.allocator = arena_allocator(& temp_arena) -// context.temp_allocator = arena_allocator(& temp_arena) + // temp_arena : Arena; arena_init(& temp_arena, Logger_Allocator_Buffer[:]) + // context.allocator = arena_allocator(& temp_arena) + // context.temp_allocator = arena_allocator(& temp_arena) -// core_log.log( level, msg, location = loc ) + // core_log.log( level, msg, location = loc ) // } // logf :: proc( fmt : string, args : ..any, level := core_log.Level.Info, loc := #caller_location ) { -// temp_arena : Arena; arena_init(& temp_arena, Logger_Allocator_Buffer[:]) -// context.allocator = arena_allocator(& temp_arena) -// context.temp_allocator = arena_allocator(& temp_arena) + // temp_arena : Arena; arena_init(& temp_arena, Logger_Allocator_Buffer[:]) + // context.allocator = arena_allocator(& temp_arena) + // context.temp_allocator = arena_allocator(& temp_arena) -// core_log.logf( level, fmt, ..args, location = loc ) + // core_log.logf( level, fmt, ..args, location = loc ) // } reload_array :: proc( self : ^[dynamic]$Type, allocator : Allocator ) { @@ -121,7 +121,7 @@ textspace_x_form :: #force_inline proc "contextless" ( position, scale : ^Vec2, } } -Use_SIMD_For_Bezier_Ops :: true +Use_SIMD_For_Bezier_Ops :: false when ! Use_SIMD_For_Bezier_Ops { @@ -130,10 +130,10 @@ when ! Use_SIMD_For_Bezier_Ops // ve_fontcache_eval_bezier (quadratic) eval_point_on_bezier3 :: #force_inline proc "contextless" ( p0, p1, p2 : Vec2, alpha : f32 ) -> Vec2 { - p0 := vec2_64(p0) - p1 := vec2_64(p1) - p2 := vec2_64(p2) - alpha := f64(alpha) + // p0 := vec2_64(p0) + // p1 := vec2_64(p1) + // p2 := vec2_64(p2) + // alpha := f64(alpha) weight_start := (1 - alpha) * (1 - alpha) weight_control := 2.0 * (1 - alpha) * alpha @@ -152,11 +152,11 @@ when ! Use_SIMD_For_Bezier_Ops // ve_fontcache_eval_bezier (cubic) eval_point_on_bezier4 :: #force_inline proc "contextless" ( p0, p1, p2, p3 : Vec2, alpha : f32 ) -> Vec2 { - p0 := vec2_64(p0) - p1 := vec2_64(p1) - p2 := vec2_64(p2) - p3 := vec2_64(p3) - alpha := f64(alpha) + // p0 := vec2_64(p0) + // p1 := vec2_64(p1) + // p2 := vec2_64(p2) + // p3 := vec2_64(p3) + // alpha := f64(alpha) weight_start := (1 - alpha) * (1 - alpha) * (1 - alpha) weight_c_a := 3 * (1 - alpha) * (1 - alpha) * alpha @@ -184,59 +184,6 @@ else return Vec2{ simd.extract(v, 0), simd.extract(v, 1) } } - vec2_add_simd :: #force_inline proc "contextless" (a, b: Vec2) -> Vec2 { - simd_a := vec2_to_simd(a) - simd_b := vec2_to_simd(b) - result := simd.add(simd_a, simd_b) - return simd_to_vec2(result) - } - - vec2_sub_simd :: #force_inline proc "contextless" (a, b: Vec2) -> Vec2 { - simd_a := vec2_to_simd(a) - simd_b := vec2_to_simd(b) - result := simd.sub(simd_a, simd_b) - return simd_to_vec2(result) - } - - vec2_mul_simd :: #force_inline proc "contextless" (a: Vec2, s: f32) -> Vec2 { - simd_a := vec2_to_simd(a) - simd_s := Vec2_SIMD{s, s, s, s} - result := simd.mul(simd_a, simd_s) - return simd_to_vec2(result) - } - - vec2_div_simd :: #force_inline proc "contextless" (a: Vec2, s: f32) -> Vec2 { - simd_a := vec2_to_simd(a) - simd_s := Vec2_SIMD{s, s, s, s} - result := simd.div(simd_a, simd_s) - return simd_to_vec2(result) - } - - vec2_dot_simd :: #force_inline proc "contextless" (a, b: Vec2) -> f32 { - simd_a := vec2_to_simd(a) - simd_b := vec2_to_simd(b) - result := simd.mul(simd_a, simd_b) - return simd.reduce_add_ordered(result) - } - - vec2_length_sqr_simd :: #force_inline proc "contextless" (a: Vec2) -> f32 { - return vec2_dot_simd(a, a) - } - - vec2_length_simd :: #force_inline proc "contextless" (a: Vec2) -> f32 { - return math.sqrt(vec2_length_sqr_simd(a)) - } - - vec2_normalize_simd :: #force_inline proc "contextless" (a: Vec2) -> Vec2 { - len := vec2_length_simd(a) - if len > 0 { - inv_len := 1.0 / len - return vec2_mul_simd(a, inv_len) - } - return a - } - - // SIMD-optimized version of eval_point_on_bezier3 eval_point_on_bezier3 :: #force_inline proc "contextless" (p0, p1, p2: Vec2, alpha: f32) -> Vec2 { simd_p0 := vec2_to_simd(p0) diff --git a/code/font/vefontcache/parser.odin b/code/font/vefontcache/parser.odin index 5d7cb74..1eef70d 100644 --- a/code/font/vefontcache/parser.odin +++ b/code/font/vefontcache/parser.odin @@ -1,4 +1,4 @@ -package VEFontCache +package vefontcache /* Notes: @@ -12,6 +12,7 @@ STB_Truetype has macros for its allocation unfortuantely import "base:runtime" import "core:c" import "core:math" +import "core:slice" import stbtt "vendor:stb/truetype" import freetype "thirdparty:freetype" @@ -52,13 +53,11 @@ ParserGlyphShape :: [dynamic]ParserGlyphVertex ParserContext :: struct { kind : ParserKind, ft_library : freetype.Library, - - // fonts : HMapChained(ParserFontInfo), } -parser_init :: proc( ctx : ^ParserContext ) +parser_init :: proc( ctx : ^ParserContext, kind : ParserKind ) { - switch ctx.kind + switch kind { case .Freetype: result := freetype.init_free_type( & ctx.ft_library ) @@ -68,9 +67,7 @@ parser_init :: proc( ctx : ^ParserContext ) // Do nothing intentional } - // error : AllocatorError - // ctx.fonts, error = make( HMapChained(ParserFontInfo), 256 ) - // assert( error == .None, "VEFontCache.parser_init: Failed to allocate fonts array" ) + ctx.kind = kind } parser_shutdown :: proc( ctx : ^ParserContext ) { @@ -79,13 +76,6 @@ parser_shutdown :: proc( ctx : ^ParserContext ) { 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: @@ -99,6 +89,7 @@ parser_load_font :: proc( ctx : ^ParserContext, label : string, data : []byte ) font.label = label font.data = data + font.kind = ctx.kind return } @@ -190,6 +181,10 @@ parser_get_font_vertical_metrics :: #force_inline proc "contextless" ( font : ^P switch font.kind { case .Freetype: + info := font.freetype_info + ascent = i32(info.ascender) + descent = i32(info.descender) + line_gap = i32(info.height) - (ascent - descent) case .STB_TrueType: stbtt.GetFontVMetrics( & font.stbtt_info, & ascent, & descent, & line_gap ) @@ -225,137 +220,8 @@ parser_get_glyph_shape :: proc( font : ^ParserFontInfo, glyph_index : Glyph ) -> 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 - - vertices, error := make( [dynamic]ParserGlyphVertex, 1024 ) - assert( error == .None ) - - // 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( [^]u16) outline.contours - points := transmute( [^]freetype.Vector) outline.points - tags := transmute( [^]u8) outline.tags - - // TODO(Ed): Review this, never tested before and its problably bad. - for contour : i32 = 0; contour < i32(outline.n_contours); contour += 1 - { - start := (contour == 0) ? 0 : i32(contours[ contour - 1 ] + 1) - end := i32(contours[ contour ]) - - for index := start; index < i32(outline.n_points); index += 1 - { - point := points[ index ] - tag := tags[ index ] - - if (tag & FT_CURVE_TAG_ON) != 0 - { - if len(vertices) > 0 && !(vertices[len(vertices) - 1].type == .Move ) - { - // Close the previous contour if needed - append(& vertices, ParserGlyphVertex { type = .Line, - x = i16(points[start].x), y = i16(points[start].y), - contour_x0 = i16(0), contour_y0 = i16(0), - contour_x1 = i16(0), contour_y1 = i16(0), - padding = 0, - }) - } - - append(& vertices, ParserGlyphVertex { type = .Move, - x = i16(point.x), y = i16(point.y), - contour_x0 = i16(0), contour_y0 = i16(0), - contour_x1 = i16(0), contour_y1 = i16(0), - padding = 0, - }) - } - else if (tag & FT_CURVE_TAG_CUBIC) != 0 - { - point1 := points[ index + 1 ] - point2 := points[ index + 2 ] - append(& vertices, ParserGlyphVertex { type = .Cubic, - x = i16(point2.x), y = i16(point2.y), - contour_x0 = i16(point.x), contour_y0 = i16(point.y), - contour_x1 = i16(point1.x), contour_y1 = i16(point1.y), - padding = 0, - }) - index += 2 - } - else if (tag & FT_CURVE_TAG_CONIC) != 0 - { - // TODO(Ed): This is using a very dead simple algo to convert the conic to a cubic curve - // not sure if we need something more sophisticaated - point1 := points[ index + 1 ] - - control_conv :: f32(0.5) // Conic to cubic control point distance - to_float := f32(1.0 / 64.0) - - fp := Vec2 { f32(point.x), f32(point.y) } * to_float - fp1 := Vec2 { f32(point1.x), f32(point1.y) } * to_float - - control1 := freetype.Vector { - point.x + freetype.Pos( (fp1.x - fp.x) * control_conv * 64.0 ), - point.y + freetype.Pos( (fp1.y - fp.y) * control_conv * 64.0 ), - } - control2 := freetype.Vector { - point1.x + freetype.Pos( (fp.x - fp1.x) * control_conv * 64.0 ), - point1.y + freetype.Pos( (fp.y - fp1.y) * control_conv * 64.0 ), - } - append(& vertices, ParserGlyphVertex { type = .Cubic, - x = i16(point1.x), y = i16(point1.y), - contour_x0 = i16(control1.x), contour_y0 = i16(control1.y), - contour_x1 = i16(control2.x), contour_y1 = i16(control2.y), - padding = 0, - }) - index += 1 - } - else - { - append(& vertices, ParserGlyphVertex { type = .Line, - x = i16(point.x), y = i16(point.y), - contour_x0 = i16(0), contour_y0 = i16(0), - contour_x1 = i16(0), contour_y1 = i16(0), - padding = 0, - }) - } - } - - // Close contour - append(& vertices, ParserGlyphVertex { type = .Line, - x = i16(points[start].x), y = i16(points[start].y), - contour_x0 = i16(0), contour_y0 = i16(0), - contour_x1 = i16(0), contour_y1 = i16(0), - padding = 0, - }) - } - - shape = vertices - } + // TODO(Ed): Don't do this, going a completely different route for handling shapes. + // This abstraction fails to be time-saving or performant. case .STB_TrueType: stb_shape : [^]stbtt.vertex @@ -447,48 +313,3 @@ parser_scale_for_mapping_em_to_pixels :: #force_inline proc "contextless" ( font } return 0 } - -when false { -parser_convert_conic_to_cubic_freetype :: proc( vertices : Array(ParserGlyphVertex), p0, p1, p2 : freetype.Vector, tolerance : f32 ) -{ - scratch : [Kilobyte * 4]u8 - scratch_arena : Arena; arena_init(& scratch_arena, scratch[:]) - - points, error := make( Array(freetype.Vector), 256, allocator = arena_allocator( &scratch_arena) ) - assert(error == .None) - - append( & points, p0) - append( & points, p1) - append( & points, p2) - - to_float : f32 = 1.0 / 64.0 - control_conv :: f32(2.0 / 3.0) // Conic to cubic control point distance - - for ; points.num > 1; { - p0 := points.data[0] - p1 := points.data[1] - p2 := points.data[2] - - fp0 := Vec2{ f32(p0.x), f32(p0.y) } * to_float - fp1 := Vec2{ f32(p1.x), f32(p1.y) } * to_float - fp2 := Vec2{ f32(p2.x), f32(p2.y) } * to_float - - delta_x := fp0.x - 2 * fp1.x + fp2.x; - delta_y := fp0.y - 2 * fp1.y + fp2.y; - distance := math.sqrt(delta_x * delta_x + delta_y * delta_y); - - if distance <= tolerance - { - control1 := { - - } - } - else - { - control2 := { - - } - } - } -} -} diff --git a/code/font/vefontcache/shaped_text.odin b/code/font/vefontcache/shaped_text.odin index d265f76..5e7e9bd 100644 --- a/code/font/vefontcache/shaped_text.odin +++ b/code/font/vefontcache/shaped_text.odin @@ -1,4 +1,4 @@ -package VEFontCache +package vefontcache ShapedText :: struct { glyphs : [dynamic]Glyph, @@ -65,8 +65,6 @@ shape_text_uncached :: proc( ctx : ^Context, font : FontID, text_utf8 : string, assert( ctx != nil ) assert( font >= 0 && int(font) < len(ctx.entries) ) - use_full_text_shape := ctx.text_shape_adv - clear( & output.glyphs ) clear( & output.positions ) @@ -76,11 +74,9 @@ shape_text_uncached :: proc( ctx : ^Context, font : FontID, text_utf8 : string, line_gap := f32(line_gap_i32) line_height := (ascent - descent + line_gap) * entry.size_scale - if use_full_text_shape + if ctx.text_shape_adv { - // assert( entry.shaper_info != nil ) 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 @@ -106,7 +102,7 @@ shape_text_uncached :: proc( ctx : ^Context, font : FontID, text_utf8 : string, max_line_width = max(max_line_width, position.x) position.x = 0.0 position.y -= line_height - position.y = ceil(position.y) + position.y = position.y prev_codepoint = rune(0) continue } @@ -117,19 +113,22 @@ shape_text_uncached :: proc( ctx : ^Context, font : FontID, text_utf8 : string, append( & output.glyphs, parser_find_glyph_index( & entry.parser_info, codepoint )) advance, _ := parser_get_codepoint_horizontal_metrics( & entry.parser_info, codepoint ) + if ctx.snap_shape_pos do position.x = ceil(position.x) + append( & output.positions, Vec2 { - ctx.snap_shape_pos ? ceil(position.x) : position.x, + position.x, position.y }) - position.x += ctx.snap_shape_pos ? ceil(f32(advance) * entry.size_scale) : f32(advance) * entry.size_scale + position.x += f32(advance) * entry.size_scale + if ctx.snap_shape_pos do position.x = ceil(position.x) prev_codepoint = codepoint } output.end_cursor_pos = position max_line_width = max(max_line_width, position.x) - output.size.x = ceil(max_line_width) + output.size.x = max_line_width output.size.y = f32(line_count) * line_height } } diff --git a/code/font/vefontcache/shaper.odin b/code/font/vefontcache/shaper.odin index 67a3d23..ecc8495 100644 --- a/code/font/vefontcache/shaper.odin +++ b/code/font/vefontcache/shaper.odin @@ -1,4 +1,4 @@ -package VEFontCache +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. */ @@ -13,7 +13,6 @@ ShaperKind :: enum { ShaperContext :: struct { hb_buffer : harfbuzz.Buffer, - // infos : HMapChained(ShaperInfo), } ShaperInfo :: struct { @@ -26,10 +25,6 @@ shaper_init :: proc( ctx : ^ShaperContext ) { ctx.hb_buffer = harfbuzz.buffer_create() assert( ctx.hb_buffer != nil, "VEFontCache.shaper_init: Failed to create harfbuzz buffer") - - // error : AllocatorError - // ctx.infos, error = make( HMapChained(ShaperInfo), 256 ) - // assert( error == .None, "VEFontCache.shaper_init: Failed to create shaper infos map" ) } shaper_shutdown :: proc( ctx : ^ShaperContext ) @@ -37,20 +32,10 @@ shaper_shutdown :: proc( ctx : ^ShaperContext ) if ctx.hb_buffer != nil { harfbuzz.buffer_destroy( ctx.hb_buffer ) } - - // delete(& ctx.infos) } 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 ) @@ -79,10 +64,14 @@ shaper_shape_from_text :: proc( ctx : ^ShaperContext, info : ^ShaperInfo, output descent := f32(descent) line_gap := f32(line_gap) + max_line_width := f32(0) + line_count := 1 + line_height := (ascent - descent + line_gap) * size_scale + 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 ) + position, vertical_position, max_line_width: ^f32, line_count: ^int, + ascent, descent, line_gap, size, size_scale: f32 ) { // Set script and direction. We use the system's default langauge. // script = HB_SCRIPT_LATIN @@ -91,6 +80,7 @@ shaper_shape_from_text :: proc( ctx : ^ShaperContext, info : ^ShaperInfo, output 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( font, buffer, nil, 0 ) // Loop over glyphs and append to output buffer. @@ -98,6 +88,8 @@ shaper_shape_from_text :: proc( ctx : ^ShaperContext, info : ^ShaperInfo, output glyph_infos := harfbuzz.buffer_get_glyph_infos( buffer, & glyph_count ) glyph_positions := harfbuzz.buffer_get_glyph_positions( buffer, & glyph_count ) + line_height := (ascent - descent + line_gap) * size_scale + for index : i32; index < i32(glyph_count); index += 1 { hb_glyph := glyph_infos[ index ] @@ -106,9 +98,11 @@ shaper_shape_from_text :: proc( ctx : ^ShaperContext, info : ^ShaperInfo, output if hb_glyph.cluster > 0 { + (max_line_width^) = max( max_line_width^, position^ ) (position^) = 0.0 - (vertical_position^) -= (ascent - descent + line_gap) * size_scale + (vertical_position^) -= line_height (vertical_position^) = cast(f32) i32(vertical_position^ + 0.5) + (line_count^) += 1 continue } if abs( size ) <= Advance_Snap_Smallfont_Size @@ -128,6 +122,7 @@ shaper_shape_from_text :: proc( ctx : ^ShaperContext, info : ^ShaperInfo, output (position^) += f32(hb_gposition.x_advance) * size_scale (vertical_position^) += f32(hb_gposition.y_advance) * size_scale + (max_line_width^) = max(max_line_width^, position^) } output.end_cursor_pos.x = position^ @@ -141,25 +136,31 @@ shaper_shape_from_text :: proc( ctx : ^ShaperContext, info : ^ShaperInfo, output for codepoint, byte_offset in text_utf8 { - script := harfbuzz.unicode_script( hb_ucfunc, cast(harfbuzz.Codepoint) codepoint ) + 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 { - harfbuzz.buffer_add( ctx.hb_buffer, cast(harfbuzz.Codepoint) codepoint, codepoint == '\n' ? 1 : 0 ) + 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( 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 ) + shape_run( ctx.hb_buffer, current_script, info.font, output, & position, & vertical_position, & max_line_width, & line_count, ascent, descent, line_gap, size, size_scale ) + harfbuzz.buffer_add( ctx.hb_buffer, hb_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 ) + shape_run( ctx.hb_buffer, current_script, info.font, output, & position, & vertical_position, & max_line_width, & line_count, ascent, descent, line_gap, size, size_scale ) + + // Set the final size + output.size.x = max_line_width + output.size.y = f32(line_count) * line_height return } diff --git a/code/font/vefontcache/vefontcache.odin b/code/font/vefontcache/vefontcache.odin index b7b9046..45da3cb 100644 --- a/code/font/vefontcache/vefontcache.odin +++ b/code/font/vefontcache/vefontcache.odin @@ -3,7 +3,7 @@ A port of (https://github.com/hypernewbie/VEFontCache) to Odin. See: https://github.com/Ed94/VEFontCache-Odin */ -package VEFontCache +package vefontcache import "base:runtime" @@ -32,7 +32,6 @@ Entry_Default :: Entry { Context :: struct { backing : Allocator, - parser_kind : ParserKind, parser_ctx : ParserContext, shaper_ctx : ShaperContext, @@ -45,6 +44,7 @@ Context :: struct { snap_width : f32, snap_height : f32, + colour : Colour, cursor_pos : Vec2, @@ -67,7 +67,7 @@ Context :: struct { debug_print_verbose : b32, } -#region("lifetime") +//#region("lifetime") InitAtlasRegionParams :: struct { width : u32, @@ -136,6 +136,7 @@ startup :: proc( ctx : ^Context, parser_kind : ParserKind, atlas_params := InitAtlasParams_Default, glyph_draw_params := InitGlyphDrawParams_Default, shape_cache_params := InitShapeCacheParams_Default, + use_advanced_text_shaper : b32 = true, snap_shape_position : b32 = true, default_curve_quality : u32 = 3, entires_reserve : u32 = 512, @@ -150,6 +151,7 @@ startup :: proc( ctx : ^Context, parser_kind : ParserKind, context.allocator = ctx.backing snap_shape_pos = snap_shape_position + text_shape_adv = use_advanced_text_shaper if default_curve_quality == 0 { default_curve_quality = 3 @@ -264,7 +266,7 @@ startup :: proc( ctx : ^Context, parser_kind : ParserKind, assert( error == .None, "VEFontCache.init : Failed to allocate vertices array for clear_draw_list" ) } - parser_init( & parser_ctx ) + parser_init( & parser_ctx, parser_kind ) shaper_init( & shaper_ctx ) } @@ -320,9 +322,38 @@ shutdown :: proc( ctx : ^Context ) unload_font( ctx, entry.id ) } - shaper_shutdown( & shaper_ctx ) + delete( entries ) + delete( temp_path ) + delete( temp_codepoint_seen ) - // TODO(Ed): Finish implementing, there is quite a few resource not released here. + delete( draw_list.vertices ) + delete( draw_list.indices ) + delete( draw_list.calls ) + + LRU_free( & atlas.region_a.state ) + LRU_free( & atlas.region_b.state ) + LRU_free( & atlas.region_c.state ) + LRU_free( & atlas.region_d.state ) + + for idx : i32 = 0; idx < i32(len(shape_cache.storage)); idx += 1 { + stroage_entry := & shape_cache.storage[idx] + using stroage_entry + + delete( glyphs ) + delete( positions ) + } + LRU_free( & shape_cache.state ) + + delete( glyph_buffer.draw_list.vertices ) + delete( glyph_buffer.draw_list.indices ) + delete( glyph_buffer.draw_list.calls ) + + delete( glyph_buffer.clear_draw_list.vertices ) + delete( glyph_buffer.clear_draw_list.indices ) + delete( glyph_buffer.clear_draw_list.calls ) + + shaper_shutdown( & shaper_ctx ) + parser_shutdown( & parser_ctx ) } // ve_fontcache_load @@ -352,10 +383,7 @@ load_font :: proc( ctx : ^Context, label : string, data : []byte, size_px : f32, used = true parser_info = parser_load_font( & parser_ctx, label, data ) - // assert( parser_info != nil, "VEFontCache.load_font: Failed to load font info from parser" ) - 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") size = size_px size_scale = size_px < 0.0 ? \ @@ -391,9 +419,9 @@ unload_font :: proc( ctx : ^Context, font : FontID ) shaper_unload_font( & entry.shaper_info ) } -#endregion("lifetime") +//#endregion("lifetime") -#region("drawing") +//#region("drawing") // ve_fontcache_configure_snap configure_snap :: #force_inline proc( ctx : ^Context, snap_width, snap_height : u32 ) { @@ -407,7 +435,7 @@ set_colour :: #force_inline proc "contextless" ( ctx : ^Context, colour : Co draw_text :: proc( ctx : ^Context, font : FontID, text_utf8 : string, position, scale : Vec2 ) -> b32 { - // profile(#procedure) + profile(#procedure) assert( ctx != nil ) assert( font >= 0 && int(font) < len(ctx.entries) ) @@ -469,9 +497,9 @@ flush_draw_list_layer :: proc( ctx : ^Context ) { draw_layer.calls_offset = len(draw_list.calls) } -#endregion("drawing") +//#endregion("drawing") -#region("metrics") +//#region("metrics") measure_text_size :: proc( ctx : ^Context, font : FontID, text_utf8 : string ) -> (measured : Vec2) { @@ -498,4 +526,4 @@ get_font_vertical_metrics :: #force_inline proc ( ctx : ^Context, font : FontID return } -#endregion("metrics") +//#endregion("metrics")