package vefontcache Vertex :: struct { pos : Vec2, u, v : f32, } DrawCall :: struct { pass : FrameBufferPass, start_index : u32, end_index : u32, clear_before_draw : b32, region : AtlasRegionKind, colour : Colour, } DrawCall_Default :: DrawCall { pass = .None, start_index = 0, end_index = 0, clear_before_draw = false, region = .A, colour = { 1.0, 1.0, 1.0, 1.0 } } DrawList :: struct { vertices : [dynamic]Vertex, indices : [dynamic]u32, 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, Atlas = 2, Target = 3, Target_Uncached = 4, } GlyphDrawBuffer :: struct { over_sample : Vec2, batch : i32, width : i32, height : i32, draw_padding : i32, batch_x : i32, clear_draw_list : DrawList, draw_list : DrawList, } blit_quad :: proc( draw_list : ^DrawList, p0 : Vec2 = {0, 0}, p1 : Vec2 = {1, 1}, uv0 : Vec2 = {0, 0}, uv1 : Vec2 = {1, 1} ) { // profile(#procedure) // logf("Blitting: xy0: %0.2f, %0.2f xy1: %0.2f, %0.2f uv0: %0.2f, %0.2f uv1: %0.2f, %0.2f", // p0.x, p0.y, p1.x, p1.y, uv0.x, uv0.y, uv1.x, uv1.y); v_offset := cast(u32) len(draw_list.vertices) quadv : [4]Vertex = { { {p0.x, p0.y}, uv0.x, uv0.y }, { {p0.x, p1.y}, uv0.x, uv1.y }, { {p1.x, p0.y}, uv1.x, uv0.y }, { {p1.x, p1.y}, uv1.x, uv1.y } } append( & draw_list.vertices, ..quadv[:] ) quad_indices : []u32 = { 0 + v_offset, 1 + v_offset, 2 + v_offset, 2 + v_offset, 1 + v_offset, 3 + v_offset } append( & draw_list.indices, ..quad_indices[:] ) return } cache_glyph :: proc(ctx : ^Context, font : FontID, glyph_index : Glyph, entry : ^Entry, bounds_0, bounds_1 : Vec2, scale, translate : Vec2) -> b32 { // profile(#procedure) if glyph_index == Glyph(0) { return false } shape, error := parser_get_glyph_shape(&entry.parser_info, glyph_index) assert(error == .None) if len(shape) == 0 { return false } outside := Vec2{bounds_0.x - 21, bounds_0.y - 33} draw := DrawCall_Default draw.pass = FrameBufferPass.Glyph draw.start_index = u32(len(ctx.draw_list.indices)) path := &ctx.temp_path clear(path) for edge in shape do #partial switch edge.type { case .Move: if len(path) > 0 { draw_filled_path(&ctx.draw_list, outside, path[:], scale, translate, ctx.debug_print_verbose) clear(path) } fallthrough case .Line: append( path, Vertex { pos = Vec2 { f32(edge.x), f32(edge.y)} } ) case .Curve: assert(len(path) > 0) p0 := path[ len(path) - 1].pos p1 := Vec2{ f32(edge.contour_x0), f32(edge.contour_y0) } p2 := Vec2{ f32(edge.x), f32(edge.y) } step := 1.0 / entry.curve_quality for index : f32 = 1; index <= entry.curve_quality; index += 1 { alpha := index * step append( path, Vertex { pos = eval_point_on_bezier3(p0, p1, p2, alpha) } ) } case .Cubic: assert( len(path) > 0) p0 := path[ len(path) - 1].pos p1 := Vec2{ f32(edge.contour_x0), f32(edge.contour_y0) } p2 := Vec2{ f32(edge.contour_x1), f32(edge.contour_y1) } p3 := Vec2{ f32(edge.x), f32(edge.y) } step := 1.0 / entry.curve_quality for index : f32 = 1; index <= entry.curve_quality; index += 1 { alpha := index * step append( path, Vertex { pos = eval_point_on_bezier4(p0, p1, p2, p3, alpha) } ) } } if len(path) > 0 { draw_filled_path(&ctx.draw_list, outside, path[:], scale, translate, ctx.debug_print_verbose) } draw.end_index = u32(len(ctx.draw_list.indices)) if draw.end_index > draw.start_index { append(&ctx.draw_list.calls, draw) } parser_free_shape(&entry.parser_info, shape) return true } /* Called by: * can_batch_glyph : If it determines that the glyph was not detected and we haven't reached capacity in the atlas * draw_text_shape : Glyph */ cache_glyph_to_atlas :: proc( ctx : ^Context, font : FontID, glyph_index : Glyph, lru_code : u64, atlas_index : i32, entry : ^Entry, region_kind : AtlasRegionKind, region : ^AtlasRegion, over_sample : Vec2 ) { // profile(#procedure) // Get hb_font text metrics. These are unscaled! bounds_0, bounds_1 := parser_get_glyph_box( & entry.parser_info, glyph_index ) bounds_size := Vec2 { f32(bounds_1.x - bounds_0.x), f32(bounds_1.y - bounds_0.y) } // E region is special case and not cached to atlas. if region_kind == .None || region_kind == .E do return // Grab an atlas LRU cache slot. atlas_index := atlas_index 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, must_find = true ) 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 ) } atlas := & ctx.atlas glyph_buffer := & ctx.glyph_buffer atlas_size := Vec2 { f32(atlas.width), f32(atlas.height) } glyph_buffer_size := Vec2 { f32(glyph_buffer.width), f32(glyph_buffer.height) } glyph_padding := cast(f32) atlas.glyph_padding 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 := -1 * vec2(bounds_0) * glyph_draw_scale + vec2( 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 := bounds_size.x * glyph_draw_scale.x + 1.0 + over_sample.x * glyph_padding if i32(f32(glyph_buffer.batch_x) + gwidth_scaled_px) >= i32(glyph_buffer.width) { flush_glyph_buffer_to_atlas( ctx ) } // Calculate the src and destination regions slot_position, slot_szie := atlas_bbox( atlas, region_kind, atlas_index ) dst_glyph_position := slot_position dst_glyph_size := bounds_size * entry.size_scale + glyph_padding dst_size := slot_szie screenspace_x_form( & dst_glyph_position, & dst_glyph_size, atlas_size ) screenspace_x_form( & slot_position, & dst_size, atlas_size ) src_position := Vec2 { f32(glyph_buffer.batch_x), 0 } src_size := bounds_size * glyph_draw_scale + over_sample * glyph_padding textspace_x_form( & src_position, & src_size, glyph_buffer_size ) // Advance glyph_update_batch_x and calculate final glyph drawing transform glyph_draw_translate.x += f32(glyph_buffer.batch_x) glyph_buffer.batch_x += i32(gwidth_scaled_px) screenspace_x_form( & glyph_draw_translate, & glyph_draw_scale, glyph_buffer_size ) clear_target_region : DrawCall { using clear_target_region pass = .Atlas region = .Ignore start_index = cast(u32) len(glyph_buffer.clear_draw_list.indices) blit_quad( & glyph_buffer.clear_draw_list, slot_position, slot_position + dst_size, { 1.0, 1.0 }, { 1.0, 1.0 } ) end_index = cast(u32) len(glyph_buffer.clear_draw_list.indices) } blit_to_atlas : DrawCall { using blit_to_atlas pass = .Atlas region = .None start_index = cast(u32) len(glyph_buffer.draw_list.indices) blit_quad( & glyph_buffer.draw_list, dst_glyph_position, slot_position + dst_glyph_size, src_position, src_position + src_size ) end_index = cast(u32) len(glyph_buffer.draw_list.indices) } append( & glyph_buffer.clear_draw_list.calls, clear_target_region ) append( & glyph_buffer.draw_list.calls, blit_to_atlas ) // Render glyph to glyph_update_FBO cache_glyph( ctx, font, glyph_index, entry, vec2(bounds_0), vec2(bounds_1), glyph_draw_scale, glyph_draw_translate ) } // If the glyuph is found in the atlas, nothing occurs, otherwise, the glyph call is setup to catch it to the atlas check_glyph_in_atlas :: #force_inline proc( ctx : ^Context, font : FontID, entry : ^Entry, glyph_index : Glyph, lru_code : u64, atlas_index : i32, region_kind : AtlasRegionKind, region : ^AtlasRegion, over_sample : Vec2 ) -> b32 { // profile(#procedure) assert( glyph_index != -1 ) // E region can't batch if region_kind == .E || region_kind == .None do return false if ctx.temp_codepoint_seen_num > 1024 do return false // TODO(Ed): Why 1024? if atlas_index == - 1 { if region.next_idx > 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( & region.state ) seen, success := ctx.temp_codepoint_seen[next_evict_codepoint] assert(success != false) if (seen) { return false } } cache_glyph_to_atlas( ctx, font, glyph_index, lru_code, atlas_index, entry, region_kind, region, over_sample ) } assert( LRU_get( & region.state, lru_code ) != -1 ) mark_batch_codepoint_seen( ctx, lru_code) return true } // ve_fontcache_clear_drawlist clear_draw_list :: #force_inline proc ( draw_list : ^DrawList ) { clear( & draw_list.calls ) clear( & draw_list.indices ) clear( & draw_list.vertices ) } directly_draw_massive_glyph :: proc( ctx : ^Context, entry : ^Entry, glyph : Glyph, bounds_0, bounds_1 : Vec2, bounds_size : Vec2, over_sample, position, scale : Vec2 ) { // profile(#procedure) flush_glyph_buffer_to_atlas( ctx ) glyph_padding := f32(ctx.atlas.glyph_padding) glyph_buffer_size := Vec2 { f32(ctx.glyph_buffer.width), f32(ctx.glyph_buffer.height) } // Draw un-antialiased glyph to update FBO. glyph_draw_scale := over_sample * entry.size_scale glyph_draw_translate := -1 * bounds_0 * glyph_draw_scale + vec2_from_scalar(glyph_padding) screenspace_x_form( & glyph_draw_translate, & glyph_draw_scale, glyph_buffer_size ) cache_glyph( ctx, entry.id, glyph, entry, bounds_0, bounds_1, glyph_draw_scale, glyph_draw_translate ) glyph_padding_dbl := glyph_padding * 2 bounds_scaled := bounds_size * entry.size_scale // Figure out the source rect. glyph_position := Vec2 {} glyph_size := vec2(glyph_padding_dbl) glyph_dst_size := glyph_size + bounds_scaled glyph_size += bounds_scaled * over_sample // Figure out the destination rect. bounds_0_scaled := Vec2 { cast(f32) i32(bounds_0.x * entry.size_scale - 0.5), cast(f32) i32(bounds_0.y * entry.size_scale - 0.5), } dst := position + scale * bounds_0_scaled - glyph_padding * scale dst_size := glyph_dst_size * scale textspace_x_form( & glyph_position, & glyph_size, glyph_buffer_size ) // Add the glyph drawcall. calls : [2]DrawCall draw_to_target := & calls[0] { using draw_to_target pass = .Target_Uncached colour = ctx.colour start_index = u32(len(ctx.draw_list.indices)) blit_quad( & ctx.draw_list, dst, dst + dst_size, glyph_position, glyph_position + glyph_size ) end_index = u32(len(ctx.draw_list.indices)) } clear_glyph_update := & calls[1] { // Clear glyph_update_FBO. clear_glyph_update.pass = .Glyph clear_glyph_update.start_index = 0 clear_glyph_update.end_index = 0 clear_glyph_update.clear_before_draw = true } append( & ctx.draw_list.calls, ..calls[:] ) } // 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 : []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 { point := point point.pos = point.pos * scale + translate append( & draw_list.vertices, point ) } outside_vertex := cast(u32) len(draw_list.vertices) { vertex := Vertex { pos = outside_point * scale + translate, u = 0, v = 0, } append( & draw_list.vertices, vertex ) } for index : u32 = 1; index < cast(u32) len(path); index += 1 { indices := & draw_list.indices to_add := [3]u32 { outside_vertex, v_offset + index - 1, v_offset + index } append( indices, ..to_add[:] ) } } draw_text_batch :: proc(ctx: ^Context, entry: ^Entry, shaped: ^ShapedText, batch_start_idx, batch_end_idx : i32, position, scale : Vec2, snap_width, snap_height : f32 ) { flush_glyph_buffer_to_atlas(ctx) atlas := & ctx.atlas atlas_size := Vec2{ f32(atlas.width), f32(atlas.height) } glyph_padding := f32(atlas.glyph_padding) for index := batch_start_idx; index < batch_end_idx; index += 1 { glyph_index := shaped.glyphs[index] if glyph_index == 0 || parser_is_glyph_empty( & entry.parser_info, glyph_index) do continue region_kind, region, over_sample := decide_codepoint_region( ctx, entry, glyph_index ) lru_code := font_glyph_lru_code( entry.id, glyph_index ) atlas_index := region_kind != .E ? LRU_get( & region.state, lru_code ) : -1 bounds_0, bounds_1 := parser_get_glyph_box( & entry.parser_info, glyph_index ) vbounds_0 := vec2(bounds_0) vbounds_1 := vec2(bounds_1) 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 if region_kind == .E { directly_draw_massive_glyph(ctx, entry, glyph_index, vbounds_0, vbounds_1, bounds_size, over_sample, glyph_translate, scale ) } else if atlas_index != -1 { // Draw cacxhed glyph slot_position, _ := atlas_bbox( atlas, region_kind, atlas_index ) glyph_scale := bounds_size * entry.size_scale + glyph_padding bounds_0_scaled := ceil(vbounds_0 * entry.size_scale) dst := glyph_translate + bounds_0_scaled * scale dst_scale := glyph_scale * scale textspace_x_form( & slot_position, & glyph_scale, atlas_size ) call := DrawCall_Default call.pass = .Target call.colour = ctx.colour call.start_index = u32(len(ctx.draw_list.indices)) blit_quad(&ctx.draw_list, dst, dst + dst_scale, slot_position, slot_position + glyph_scale ) call.end_index = u32(len(ctx.draw_list.indices)) append(&ctx.draw_list.calls, call) } } } // Helper for draw_text, all raw text content should be confirmed to be either formatting or visible shapes before getting cached. draw_text_shape :: proc( ctx : ^Context, font : FontID, entry : ^Entry, shaped : ^ShapedText, position, scale : Vec2, snap_width, snap_height : f32 ) -> (cursor_pos : Vec2) { // profile(#procedure) batch_start_idx : i32 = 0 for index : i32 = 0; index < cast(i32) len(shaped.glyphs); index += 1 { glyph_index := shaped.glyphs[ index ] if is_empty( ctx, entry, glyph_index ) do continue region_kind, region, over_sample := decide_codepoint_region( ctx, entry, glyph_index ) lru_code := font_glyph_lru_code(entry.id, glyph_index) atlas_index := cast(i32) -1 if region_kind != .E do atlas_index = LRU_get( & region.state, lru_code ) 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 // First batch the other cached glyphs // flush_glyph_buffer_to_atlas(ctx) draw_text_batch( ctx, entry, shaped, batch_start_idx, index, position, scale, snap_width, snap_height ) reset_batch_codepoint_state( ctx ) cache_glyph_to_atlas( ctx, font, glyph_index, lru_code, atlas_index, entry, region_kind, region, over_sample ) mark_batch_codepoint_seen( ctx, lru_code) batch_start_idx = index } draw_text_batch( ctx, entry, shaped, batch_start_idx, cast(i32) len(shaped.glyphs), position, scale, snap_width , snap_height ) reset_batch_codepoint_state( ctx ) cursor_pos = position + shaped.end_cursor_pos * scale return } flush_glyph_buffer_to_atlas :: proc( ctx : ^Context ) { // profile(#procedure) // Flush drawcalls to draw list merge_draw_list( & ctx.draw_list, & ctx.glyph_buffer.clear_draw_list ) merge_draw_list( & ctx.draw_list, & ctx.glyph_buffer.draw_list) clear_draw_list( & ctx.glyph_buffer.draw_list ) clear_draw_list( & ctx.glyph_buffer.clear_draw_list ) // Clear glyph_update_FBO if ctx.glyph_buffer.batch_x != 0 { call := DrawCall_Default call.pass = .Glyph call.start_index = 0 call.end_index = 0 call.clear_before_draw = true append( & ctx.draw_list.calls, call ) ctx.glyph_buffer.batch_x = 0 } } // ve_fontcache_merge_drawlist merge_draw_list :: proc( dst, src : ^DrawList ) { // profile(#procedure) error : AllocatorError v_offset := cast(u32) len( dst.vertices ) num_appended : int num_appended, error = append( & dst.vertices, ..src.vertices[:] ) assert( error == .None ) i_offset := cast(u32) len(dst.indices) for index : int = 0; index < len(src.indices); index += 1 { ignored : int ignored, error = append( & dst.indices, src.indices[index] + v_offset ) assert( error == .None ) } for index : int = 0; index < len(src.calls); index += 1 { src_call := src.calls[ index ] src_call.start_index += i_offset src_call.end_index += i_offset append( & dst.calls, src_call ) assert( error == .None ) } } optimize_draw_list :: proc(draw_list: ^DrawList, call_offset: int) { // profile(#procedure) assert(draw_list != nil) can_merge_draw_calls :: #force_inline proc "contextless" ( a, b : ^DrawCall ) -> bool { result := \ a.pass == b.pass && a.end_index == b.start_index && a.region == b.region && a.colour == b.colour && ! b.clear_before_draw return result } write_index := call_offset for read_index := call_offset + 1; read_index < len(draw_list.calls); read_index += 1 { draw_current := & draw_list.calls[write_index] draw_next := & draw_list.calls[read_index] if can_merge_draw_calls(draw_current, draw_next) { draw_current.end_index = draw_next.end_index } else { // Move to the next write position and copy the draw call write_index += 1 if write_index != read_index { draw_list.calls[write_index] = (draw_next^) } } } resize( & draw_list.calls, write_index + 1) }