Progress on VEFontCache port, working on freetype outline to stbtt shape

This commit is contained in:
Edward R. Gonzalez 2024-06-03 17:43:15 -04:00
parent 26ad2d1e49
commit 1741532d64
7 changed files with 679 additions and 14 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -1,2 +1 @@
package sectr

View File

@ -1,3 +1,3 @@
package sectr
import "codebase:font/VEFontCache"
import "codebase:font/VEFontCache"

View File

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