Update vefontcache to latest

This commit is contained in:
Edward R. Gonzalez 2024-07-02 16:21:07 -04:00
parent 5c3bc243a4
commit bb6e1f78d3
12 changed files with 305 additions and 347 deletions

View File

@ -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 ) {

View File

@ -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?

View File

@ -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, {}

View File

@ -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.

View File

@ -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
{

View File

@ -1,4 +1,4 @@
package VEFontCache
package vefontcache
import "core:hash"
fnv64a :: hash.fnv64a

View File

@ -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)

View File

@ -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 := {
}
}
}
}
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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")