From f99157aae51fd0d3f0361a9778e931fd820e24c2 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Sun, 2 Jun 2024 17:29:44 -0400 Subject: [PATCH] Starting the process of porting VEFontCache --- Readme.md | 26 +-- code/font/VEFontCache/LRU.odin | 69 ++++++++ code/font/VEFontCache/VEFontCache.odin | 201 ++++++++++++++++++++++ code/font/VEFontCache/atlas.odin | 34 ++++ code/font/VEFontCache/draw.odin | 32 ++++ code/font/VEFontCache/mappings.odin | 84 +++++++++ code/font/VEFontCache/parser.odin | 19 ++ code/font/VEFontCache/shaper.odin | 14 ++ code/sectr/font/provider_VEFontCache.odin | 3 + scripts/build.ps1 | 18 +- scripts/update_deps.ps1 | 9 +- 11 files changed, 491 insertions(+), 18 deletions(-) create mode 100644 code/font/VEFontCache/LRU.odin create mode 100644 code/font/VEFontCache/atlas.odin create mode 100644 code/font/VEFontCache/draw.odin create mode 100644 code/font/VEFontCache/mappings.odin create mode 100644 code/font/VEFontCache/parser.odin create mode 100644 code/font/VEFontCache/shaper.odin create mode 100644 code/sectr/font/provider_VEFontCache.odin diff --git a/Readme.md b/Readme.md index 39216f1..d576c4b 100644 --- a/Readme.md +++ b/Readme.md @@ -28,26 +28,32 @@ The dependencies are: Major 'codebase modules': -* Engine : Main loop, logging, client interface for host, etc +* App : General app config & contextual state +* Engine : client interface for host, tick, update, rendering. * Has the following definitions: startup, shutdown, reload, tick, clean_frame (which host hooks up to when managing the client dll) -* Env : Core Memory & State definition + orchestration + * Will handle async ops. * Font Provider : Manages fonts. - * When loading fonts, the provider currently uses raylib to generate bitmap glyth sheets for a range of font sizes at once. - * Goal is to eventually render using SDF shaders. + * Bulk of visualization must be able to render text effectively + * Going to use some form of caching. + * Needs to be able to scale text in-realtime to linear values. * Grime : Name speaks for itself, stuff not directly related to the target features to iterate upon for the prototype. * Defining dependency aliases or procedure overload tables, rolling own allocator, data structures, etc. * Input : All human input related features - * Base input features (polling & related) are platform abstracted from raylib - * Input Events -* Parser : AST generation, editing, and serialization. A 1/3 of this prototype will most likely be this alone. -* Project : Encpasulation of user config/state separate from persistent app config/state as a 'project' - * Manages the codebase (program model database) + * Base input features (polling & related) are platform abstracted from sokol_app + * Entirely user rebindable +* Parsers + * AST generation, editing, and serialization. + * Parsers for different levels of "synatitic & semantic awareness", Formatting -> Domain Specific AST + * Figure out pragmatic transformations between ASTs. +* Project : Encpasulation of user config/context/state separate from persistent app's + * Manages the codebase (database & model view controller) * Manages workspaces : View compositions of the codebase * UI : Core graphic user interface framework, AST visualzation & editing, backend visualization - * Will most likely be the bulk of this prototype. * PIMGUI (Persistent Immediate Mode User Interface) * Auto-layout * Supports heavy procedural generation of box widgets + * Viewports + * Docking/Tiling, Floating, Canvas Due to the nature of the prototype there are 'sub-groups' such as the codebase being its own ordeal as well as the workspace. They'll be elaborated in their own documentation diff --git a/code/font/VEFontCache/LRU.odin b/code/font/VEFontCache/LRU.odin new file mode 100644 index 0000000..293f177 --- /dev/null +++ b/code/font/VEFontCache/LRU.odin @@ -0,0 +1,69 @@ +package VEFontCache + +/* +The choice was made to keep the LUR cache implementation as close to the original as possible. +*/ + +PoolListIter :: u32 +PoolListValue :: u64 + +PoolListItem :: struct { + prev : PoolListIter, + next : PoolListIter, + value : PoolListValue, +} + +PoolList :: struct { + items : Array( PoolListItem ), + free_list : Array( PoolListIter ), + front : PoolListIter, + back : PoolListIter, + size : i32, + capacity : i32, +} + +pool_list_init :: proc( pool : ^PoolList, capacity : u32 ) +{ + error : AllocatorError + pool.items, error = make( Array( PoolListItem ), u64(capacity) ) + assert( error == .None, "VEFontCache.pool_list_init : Failed to allocate items array") + + pool.free_list, error = make( Array( PoolListIter ), u64(capacity) ) + assert( error == .None, "VEFontCache.pool_list_init : Failed to allocate free_list array") + + pool.capacity = i32(capacity) + + for id in 0 ..< capacity do pool.free_list.data[id] = id +} + +pool_list_push_front :: proc( pool : ^PoolList, value : PoolListValue ) +{ + using pool + if size >= capacity do return + assert( free_list.num > 0 ) + assert( free_list.num == u64(capacity - size) ) + + id := array_back( free_list ) +} + +LRU_Link :: struct { + value : i32, + ptr : PoolListIter, +} + +LRU_Cache :: struct { + capacity : i32, + table : HMapChained(LRU_Link), + key_queue : PoolList, +} + +LRU_init :: proc( cache : ^LRU_Cache, capacity : u32 ) +{ + error : AllocatorError + cache.capacity = i32(capacity) + cache.table, error = make( HMapChained(LRU_Link), uint(capacity) ) + assert( error != .None, "VEFontCache.LRU_init : Failed to allocate cache's table") + +} + + diff --git a/code/font/VEFontCache/VEFontCache.odin b/code/font/VEFontCache/VEFontCache.odin index e534dde..c45853b 100644 --- a/code/font/VEFontCache/VEFontCache.odin +++ b/code/font/VEFontCache/VEFontCache.odin @@ -1,3 +1,204 @@ +/* +A port of (https://github.com/hypernewbie/VEFontCache) to Odin. + +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...) +*/ package VEFontCache +Font_ID :: i64 +Glyph :: i32 +Colour :: [4]f32 +Vec2 :: [2]f32 +Vec2i :: [2]u32 + +AtlasRegionKind :: enum { + A = 0, + B = 1, + C = 2, + D = 3 +} + +Vertex :: struct { + pos : Vec2, + u, v : f32, +} + +// GlyphDrawBuffer :: struct { +// over_sample : Vec2, + +// batch : i32, +// width : i32, +// height : i32, +// padding : i32, +// } + +ShapedText :: struct { + Glyphs : Array(Glyph), + Positions : Array(Vec2), + end_cursor_pos : Vec2, +} + +ShapedTextCache :: struct { + storage : Array(ShapedText), + state : LRU_Cache, + next_cache_id : i32, +} + +Entry :: struct { + parser_info : ParserInfo, + shaper_info : ShaperInfo, + id : Font_ID, + used : b32, + size : f32, + size_scale : f32, +} + +Entry_Default :: Entry { + id = 0, + used = false, + size = 24.0, + size_scale = 1.0, +} + +Context :: struct { + backing : Allocator, + + parser_kind : ParserKind, + parser_ctx : ParserContext, + shaper_ctx : ShaperContext, + + entries : Array(Entry), + + temp_path : Array(Vec2), + temp_codepoint_seen : HMapChained(bool), + + snap_width : u32, + snap_height : u32, + + colour : Colour, + cursor_pos : Vec2, + + draw_list : DrawList, + atlas : Atlas, + shape_cache : ShapedTextCache, + + text_shape_adv : b32, +} + +Module_Ctx :: Context + +InitAtlasRegionParams :: struct { + width : u32, + height : u32, + offset : Vec2i, +} + +InitAtlasParams :: struct { + width : u32, + height : u32, + glyph_padding : u32, + + region_a : InitAtlasRegionParams, + region_b : InitAtlasRegionParams, + region_c : InitAtlasRegionParams, + region_d : InitAtlasRegionParams, +} + +InitAtlasParams_Default :: InitAtlasParams { + width = 4 * Kilobyte, + height = 2 * Kilobyte, + glyph_padding = 1, + + +} + +InitGlyphDrawParams :: struct { + over_sample : Vec2i, + buffer_batch : u32, + padding : u32, +} + +InitGlyphDrawParams_Default :: InitGlyphDrawParams { + over_sample = { 4, 4 }, + buffer_batch = 4, + padding = InitAtlasParams_Default.glyph_padding, +} + +InitShapeCacheParams :: struct { + capacity : u32, + reserve_length : u32, +} + +InitShapeCacheParams_Default :: InitShapeCacheParams { + capacity = 256, + reserve_length = 64, +} + +init :: proc( ctx : ^Context, + allocator := context.allocator, + atlas_params := InitAtlasParams_Default, + glyph_draw_params := InitGlyphDrawParams_Default, + shape_cache_params := InitShapeCacheParams_Default, + advance_snap_smallfont_size : u32 = 12, + entires_reserve : u32 = Kilobyte, + temp_path_reserve : u32 = Kilobyte, + temp_codepoint_seen_reserve : u32 = 4 * Kilobyte, +) +{ + assert( ctx != nil, "Must provide a valid context" ) + using ctx + + ctx.backing = allocator + context.allocator = ctx.backing + + error : AllocatorError + entries, error = make( Array(Entry), u64(entires_reserve) ) + assert(error == .None, "VEFontCache.init : Failed to allocate entries") + + temp_path, error = make( Array(Vec2), u64(temp_path_reserve) ) + assert(error == .None, "VEFontCache.init : Failed to allocate temp_path") + + temp_codepoint_seen, error = make( HMapChained(bool), uint(temp_codepoint_seen_reserve) ) + assert(error == .None, "VEFontCache.init : Failed to allocate temp_path") + + draw_list.vertices, error = make( Array(Vertex), 4 * Kilobyte ) + assert(error == .None, "VEFontCache.init : Failed to allocate draw_list.vertices") + + draw_list.indices, error = make( Array(u32), 8 * Kilobyte ) + assert(error == .None, "VEFontCache.init : Failed to allocate draw_list.indices") + + draw_list.calls, error = make( Array(DrawCall), 512 ) + assert(error == .None, "VEFontCache.init : Failed to allocate draw_list.calls") + + init_atlas_region :: proc( region : ^AtlasRegion, params : InitAtlasParams, region_params : InitAtlasRegionParams ) { + using region + + next_idx = 0; + width = region_params.width + height = region_params.height + size = { + params.width / 4, + params.height / 2, + } + capacity = { + size.x / width, + size.y / height, + } + offset = region_params.offset + + error : AllocatorError + // state.cache, error = make( HMapChained(LRU_Link), uint(capacity.x * capacity.y) ) + // assert( error == .None, "VEFontCache.init_atlas_region : Failed to allocate state.cache") + LRU_init( & state, capacity.x * capacity.y ) + } + init_atlas_region( & atlas.region_a, atlas_params, atlas_params.region_a ) + init_atlas_region( & atlas.region_b, atlas_params, atlas_params.region_b ) + init_atlas_region( & atlas.region_c, atlas_params, atlas_params.region_c ) + init_atlas_region( & atlas.region_d, atlas_params, atlas_params.region_d ) + + +} diff --git a/code/font/VEFontCache/atlas.odin b/code/font/VEFontCache/atlas.odin new file mode 100644 index 0000000..135ce5d --- /dev/null +++ b/code/font/VEFontCache/atlas.odin @@ -0,0 +1,34 @@ +package VEFontCache + +GlyphUpdateBatch :: struct { + update_batch_x : i32, + clear_draw_list : DrawList, + draw_list : DrawList, +} + +AtlasRegion :: struct { + state : LRU_Cache, + + width : u32, + height : u32, + + size : Vec2i, + capacity : Vec2i, + offset : Vec2i, + + next_idx : u32, +} + +Atlas :: struct { + width : u32, + height : u32, + + glyph_pad : u16, + + region_a : AtlasRegion, + region_b : AtlasRegion, + region_c : AtlasRegion, + region_d : AtlasRegion, + + using glyph_update_batch : GlyphUpdateBatch, +} diff --git a/code/font/VEFontCache/draw.odin b/code/font/VEFontCache/draw.odin new file mode 100644 index 0000000..d03f6a7 --- /dev/null +++ b/code/font/VEFontCache/draw.odin @@ -0,0 +1,32 @@ +package VEFontCache + +FrameBufferPass :: enum { + Glyph = 1, + Atlas = 2, + Target = 3, + Target_Unchanged = 4, +} + +DrawCall :: struct { + pass : u32, + start_index : u32, + end_index : u32, + clear_before_draw : b32, + region : AtlasRegionKind, + colour : [4]f32, +} + +DrawCall_Default :: DrawCall { + pass = 0, + start_index = 0, + end_index = 0, + clear_before_draw = false, + region = .A, + colour = { 1.0, 1.0, 1.0, 1.0 } +} + +DrawList :: struct { + vertices : Array(Vertex), + indices : Array(u32), + calls : Array(DrawCall), +} diff --git a/code/font/VEFontCache/mappings.odin b/code/font/VEFontCache/mappings.odin new file mode 100644 index 0000000..0de1a43 --- /dev/null +++ b/code/font/VEFontCache/mappings.odin @@ -0,0 +1,84 @@ +package VEFontCache + +import "core:mem" + +Kilobyte :: mem.Kilobyte + +Allocator :: mem.Allocator +AllocatorError :: mem.Allocator_Error + +import "codebase:grime" + +// asserts +ensure :: grime.ensure +verify :: grime.verify + +// container + +Array :: grime.Array + +array_init :: grime.array_init +array_append :: grime.array_append +array_append_at :: grime.array_append_at +array_back :: grime.array_back +array_clear :: grime.array_clear +array_free :: grime.array_free +array_remove_at :: grime.array_remove_at +array_to_slice :: grime.array_to_slice +array_to_slice_cpacity :: grime.array_to_slice_capacity +array_underlying_slice :: grime.array_underlying_slice + +HMapChained :: grime.HMapChained + +hmap_chained_init :: grime.hmap_chained_init + +// Pool :: grime.Pool + +StackFixed :: grime.StackFixed + +stack_clear :: grime.stack_clear +stack_push :: grime.stack_push +stack_pop :: grime.stack_pop +stack_peek_ref :: grime.stack_peek_ref +stack_peek :: grime.stack_peek +stack_push_contextless :: grime.stack_push_contextless + +//#region("Proc overload mappings") + +append :: proc { + grime.array_append_array, + grime.array_append_slice, + grime.array_append_value, +} + +append_at :: proc { + grime.array_append_at_slice, + grime.array_append_at_value, +} + +clear :: proc { + array_clear, +} + +delete :: proc { + array_free, +} + +make :: proc { + array_init, + hmap_chained_init, +} + +remove_at :: proc { + array_remove_at, +} + +to_slice :: proc { + array_to_slice, +} + +underlying_slice :: proc { + array_underlying_slice, +} + +//#endregion("Proc overload mappings") diff --git a/code/font/VEFontCache/parser.odin b/code/font/VEFontCache/parser.odin new file mode 100644 index 0000000..b56f4b7 --- /dev/null +++ b/code/font/VEFontCache/parser.odin @@ -0,0 +1,19 @@ +package VEFontCache + +import stbtt "vendor:stb/truetype" +import freetype "thirdparty:freetype" + +ParserKind :: enum u32 { + stb_true_type, + freetype, +} + +ParserInfo :: struct #raw_union { + stbtt_info : stbtt.fontinfo, + freetype_info : freetype.Face +} + +ParserContext :: struct { + ft_library : freetype.Library +} + diff --git a/code/font/VEFontCache/shaper.odin b/code/font/VEFontCache/shaper.odin new file mode 100644 index 0000000..a1e3f12 --- /dev/null +++ b/code/font/VEFontCache/shaper.odin @@ -0,0 +1,14 @@ +package VEFontCache + +import "thirdparty:harfbuzz" + +ShaperContext :: struct { + hb_buffer : harfbuzz.Buffer, +} + +ShaperInfo :: struct { + blob : harfbuzz.Blob, + face : harfbuzz.Face, + font : harfbuzz.Font, +} + diff --git a/code/sectr/font/provider_VEFontCache.odin b/code/sectr/font/provider_VEFontCache.odin new file mode 100644 index 0000000..a23da7d --- /dev/null +++ b/code/sectr/font/provider_VEFontCache.odin @@ -0,0 +1,3 @@ +package sectr + +import "codebase:font/VEFontCache" \ No newline at end of file diff --git a/scripts/build.ps1 b/scripts/build.ps1 index 5f3221f..4cf688c 100644 --- a/scripts/build.ps1 +++ b/scripts/build.ps1 @@ -138,16 +138,24 @@ push-location $path_root write-host "`nBuilding Sectr Prototype`n" - $package_grime = join-path $path_code 'grime' - $module_host = join-path $path_code 'host' - $module_sectr = join-path $path_code 'sectr' + $path_font = join-path $path_code 'font' + + $package_grime = join-path $path_code 'grime' + $package_fstash = join-path $path_font 'fontstash' + $package_VEFontCache = join-path $path_font 'VEFontCache' + $module_host = join-path $path_code 'host' + $module_sectr = join-path $path_code 'sectr' if ($force){ + mark-ModuleDirty $package_fstash + mark-ModuleDirty $package_VEFontCache mark-ModuleDirty $package_grime mark-ModuleDirty $module_sectr mark-ModuleDirty $module_host } - $pkg_grime_dirty = check-ModuleForChanges $package_grime + $pkg_fstash_dirty = check-ModuleForChanges $package_fstash + $pkg_VEFontCache_dirty = check-ModuleForChanges $package_VEFontCache + $pkg_grime_dirty = check-ModuleForChanges $package_grime $pkg_collection_codebase = 'codebase=' + $path_code $pkg_collection_thirdparty = 'thirdparty=' + $path_thirdparty @@ -165,7 +173,7 @@ push-location $path_root function build-sectr { - $should_build = (check-ModuleForChanges $module_sectr) -or $pkg_grime_dirty + $should_build = (check-ModuleForChanges $module_sectr) -or $pkg_grime_dirty -or $pkg_fstash_dirty -or $pkg_VEFontCache_dirty if ( -not( $should_build)) { write-host 'Skipping sectr build, module up to date' return $module_unchanged diff --git a/scripts/update_deps.ps1 b/scripts/update_deps.ps1 index 3db16c0..1d2795e 100644 --- a/scripts/update_deps.ps1 +++ b/scripts/update_deps.ps1 @@ -8,13 +8,15 @@ $path_toolchain = join-path $path_root 'toolchain' $url_backtrace_repo = 'https://github.com/Ed94/back.git' $url_freetype = 'https://github.com/Ed94/odin-freetype.git' +$url_harfbuzz = 'https://github.com/Ed94/odin_harfbuzz.git' $url_ini_parser = 'https://github.com/laytan/odin-ini-parser.git' $url_odin_repo = 'https://github.com/Ed94/Odin.git' $url_sokol = 'https://github.com/Ed94/sokol-odin.git' $url_sokol_tools = 'https://github.com/floooh/sokol-tools-bin.git' $path_backtrace = join-path $path_thirdparty 'backtrace' -$path_freetype = join-path $path_thirdparty 'freetype' +$path_freetype = join-path $path_thirdparty 'freetype' +$path_harfbuzz = join-path $path_thirdparty 'harfbuzz' $path_ini_parser = join-path $path_thirdparty 'ini' $path_odin = join-path $path_toolchain 'Odin' $path_sokol = join-path $path_thirdparty 'sokol' @@ -85,8 +87,9 @@ function Update-GitRepo push-location $path_thirdparty -Update-GitRepo -path $path_odin -url $url_odin_repo -build_command '.\scripts\build.ps1' -Update-GitRepo -path $path_sokol -url $url_sokol -build_command '.\build_windows.ps1' +Update-GitRepo -path $path_odin -url $url_odin_repo -build_command '.\scripts\build.ps1' +Update-GitRepo -path $path_sokol -url $url_sokol -build_command '.\build_windows.ps1' +Update-GitRepo -path $path_harfbuzz -url $url_harfbuzz -build_command '.\scripts\build.ps1' function clone-gitrepo { param( [string] $path, [string] $url ) if (test-path $path) {