diff --git a/code/font/vefontcache/LICENSE.md b/code/font/vefontcache/LICENSE.md index a602421..6b1915b 100644 --- a/code/font/vefontcache/LICENSE.md +++ b/code/font/vefontcache/LICENSE.md @@ -1,9 +1,8 @@ -VEFontCache Odin Port +VE Text Rendering Library Copyright 2024 Edward R. Gonzalez This project is based on Vertex Engine GPU Font Cache -by Xi Chen (https://github.com/hypernewbie/VEFontCache). It has been substantially -rewritten and redesigned for the Odin programming language. +by Xi Chen (https://github.com/hypernewbie/VEFontCache). It has been substantially overhauled from its original implementation. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, diff --git a/code/font/vefontcache/LRU.odin b/code/font/vefontcache/LRU.odin index e3c5ded..0f8f1b3 100644 --- a/code/font/vefontcache/LRU.odin +++ b/code/font/vefontcache/LRU.odin @@ -1,7 +1,15 @@ -package vefontcache +package vetext -/* -The choice was made to keep the LRU cache implementation as close to the original as possible. +/* Note(Ed): + Original implementation has been changed moderately. + Notably the LRU is now type generic for its key value. + This was done to profile between using u64, u32, and u16. + + What ended up happening was using u32 for both the atlas and the shape cache + yielded a several ms save for processing thousands of draw text calls. + + There was an attempt at an optimization pass but the directives done here (other than force_inline) + are marginal changes at best. */ import "base:runtime" @@ -177,7 +185,7 @@ pool_list_pop_back :: #force_inline proc( pool : ^Pool_List($V_Type) ) -> V_Type return value } -LRU_Link :: struct { +LRU_Link :: struct #packed { value : i32, ptr : Pool_ListIter, } diff --git a/code/font/vefontcache/Readme.md b/code/font/vefontcache/Readme.md index d83c753..b765e2e 100644 --- a/code/font/vefontcache/Readme.md +++ b/code/font/vefontcache/Readme.md @@ -1,9 +1,45 @@ -# VE Font Cache : Odin Port +# VE Text Rendering Library -This is a port of the [VEFontCache](https://github.com/hypernewbie/VEFontCache) library. +> Vertex Engine GPU Text Rendering Library +https://github.com/user-attachments/assets/b74f1ec1-f980-45df-b604-d6b7d87d40ff + +This started off as a port of the [VEFontCache](https://github.com/hypernewbie/VEFontCache) library to the Odin programming language. Its original purpose was for use in game engines, however its rendeirng quality and performance is more than adequate for many other applications. +Since then the library has been overhauled to offer higher performance, improved visual fidelity, additional features, and quality of life improvements. + +Features: + +* Simple and well documented. +* Load and unload fonts at anytime +* Almost entirely configurabe and tunable at runtime! +* Full support for hot-reload + * Clear the caches at any-time! +* Robust quality of life features: + * Tracks text layers! + * Push and pop stack for font, font_size, view, position, scale and zoom! + * Enforce even only font-sizing + * Snap-positining to view for better hinting +* Basic or advanced text shaping via Harfbuzz +* All rendering is real-time, triangulation done on the CPU, vertex rendering and texture blitting on the gpu. + * Can hand thousands of draw text calls with very large or small shapes. +* 4-Level Regioned Texture Atlas for caching rendered glyphs +* Text shape caching +* Glyph texture buffer for rendering the text with super-sampling to downsample to the atlas or direct to target screen. +* Super-sample by a font size scalar for sharper glyphs +* All caching backed by an optimized 32-bit LRU indexing cache +* Provides a draw list that is backend agnostic (see [backend](./backend) for usage example). + +Upcoming: + +* Support for ear-clipping triangulation + * Support for which triangulation method used on a by font basis? +* Multi-threading supported job queue. + * Lift heavy-lifting portion of the library's context into a thread context. + * Synchronize threads by merging their generated layered drawlist into a file draw-list for processing on the user's render thread. + * User defines how context's are distributed for drawing (a basic quandrant basic selector procedure will be provided.) + See: [docs/Readme.md](docs/Readme.md) for the library's interface. ## Building @@ -12,40 +48,7 @@ See [scripts/Readme.md](scripts/Readme.md) for building examples or utilizing th Currently the scripts provided & the library itself were developed & tested on Windows. There are bash scripts for building on linux (they build on WSL but need additional testing). -The library depends on freetype, harfbuzz, & stb_truetype 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). +The library depends on harfbuzz, & stb_truetype to build. +Note: 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 warpper interface -* 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 interpolation for glyph rendering can be set on a per font basis. -* All codepaths heavily changed (its faster) - -## TODOs - -### Additional Features: - -* 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 - -### Optimization: - -* Check if its better to store the glyph vertices if they need to be re-cached to atlas 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 *(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 - * This would need to converge to the singlar draw_list on a per layer basis. The interface expects the user to issue commands single-threaded unless, its assumed the user is going to feed the gpu the commands & data through separate threads as well (not ideal ux). - * How the contexts are given jobs should be left up to the user (can recommend a screen quadrant based approach in demo examples) - -Failed Attempts: - -* Attempted to chunk the text to more granular 'shapes' from `draw_list` before doing the actual call to `draw_text_shape`. This lead to a larger performance cost due to the additional iteration across the text string. -* Attempted to cache the shape draw_list for future calls. Led to larger performance cost due to additional iteration in the `merge_draw_list`. - * The shapes glyphs must still be traversed to identify if the glyph is cached. This arguably could be handled in `shape_text_uncached`, however that would require a significan't amount of refactoring to identify... (and would be more unergonomic when shapers libs are processing the text) +![image](https://github.com/user-attachments/assets/2f6c0b36-179c-42fe-8903-7640ae3c209e) diff --git a/code/font/vefontcache/atlas.odin b/code/font/vefontcache/atlas.odin index 36a7113..27eb9bc 100644 --- a/code/font/vefontcache/atlas.odin +++ b/code/font/vefontcache/atlas.odin @@ -1,5 +1,7 @@ -package vefontcache +package vetext +// There are only 4 actual regions of the atlas. E represents the atlas_decide_region detecting an oversized glyph. +// Note(Ed): None should never really occur anymore. So its safe to most likely add an assert when its detected. Atlas_Region_Kind :: enum u8 { None = 0x00, A = 0x01, @@ -10,8 +12,13 @@ Atlas_Region_Kind :: enum u8 { Ignore = 0xFF, // ve_fontcache_cache_glyph_to_atlas uses a -1 value in clear draw call } +// Note(Ed): Using 16 bit hash had collision failures and no observable performance improvement (tried several 16-bit hashers) Atlas_Region_Key :: u32 +// TODO(Ed) It might perform better with a tailored made hashtable implementation for the LRU_Cache or dedicated array struct/procs for the Atlas. +/* Essentially a sub-atlas of the atlas. There is a state cache per region that tracks the glyph inventory (what slot they occupy). + Unlike the shape cache this one's fixed capacity (natrually) and the next avail slot is tracked. +*/ Atlas_Region :: struct { state : LRU_Cache(Atlas_Region_Key), @@ -24,6 +31,14 @@ Atlas_Region :: struct { next_idx : i32, } +/* There are four regions each succeeding region holds larger sized slots. + The generator pipeline for draw lists utilizes the regions array for info lookup. + + Note(Ed): + Padding can techncially be larger than 1, however recently I haven't had any artififact issues... + size_multiplier usage isn't fully resolved. Intent was to further setup over_sampling or just having + a more massive cache for content that used more than the usual common glyphs. +*/ Atlas :: struct { region_a : Atlas_Region, region_b : Atlas_Region, @@ -38,7 +53,7 @@ Atlas :: struct { size : Vec2i, } - +// Hahser for the atlas. atlas_glyph_lru_code :: #force_inline proc "contextless" ( font : Font_ID, px_size : f32, glyph_index : Glyph ) -> (lru_code : Atlas_Region_Key) { // lru_code = u32(glyph_index) + ( ( 0x10000 * u32(font) ) & 0xFFFF0000 ) font := font diff --git a/code/font/vefontcache/draw.odin b/code/font/vefontcache/draw.odin index 9ba98bf..e637d69 100644 --- a/code/font/vefontcache/draw.odin +++ b/code/font/vefontcache/draw.odin @@ -1,10 +1,21 @@ -package vefontcache +package vetext + +/* + Note(Ed): This may be seperated in the future into another file dedending on how much is involved with supportin ear-clipping triangulation. + + +*/ import "base:runtime" import "base:intrinsics" import "core:slice" import "thirdparty:freetype" +Glyph_Trianglation_Method :: enum(i32) { + Ear_Clipping, + Triangle_Fanning, +} + Vertex :: struct { pos : Vec2, u, v : f32, @@ -65,7 +76,7 @@ Draw_Call :: struct { end_index : u32, clear_before_draw : b32, region : Atlas_Region_Kind, - colour : Colour, + colour : RGBAN, } Draw_Call_Default :: Draw_Call { @@ -97,10 +108,12 @@ Glyph_Batch_Cache :: struct { cap : i32, } +// The general tracker for a generator pipeline Glyph_Draw_Buffer :: struct{ - over_sample : Vec2, - size : Vec2i, - draw_padding : f32, + over_sample : Vec2, + size : Vec2i, + draw_padding : f32, + snap_glyph_height : f32, allocated_x : i32, // Space used (horizontally) within the glyph buffer clear_draw_list : Draw_List, @@ -115,6 +128,7 @@ Glyph_Draw_Buffer :: struct{ cached : [dynamic]i32, } +// Contructs a @(optimization_mode="favor_size") blit_quad :: #force_inline proc ( draw_list : ^Draw_List, p0 : Vec2 = {0, 0}, p1 : Vec2 = {1, 1}, uv0 : Vec2 = {0, 0}, uv1 : Vec2 = {1, 1} ) { @@ -149,9 +163,9 @@ blit_quad :: #force_inline proc ( draw_list : ^Draw_List, p0 : Vec2 = {0, 0}, p1 return } -// Constructs a triangle fan to fill a shape using the provided path outside_point represents the center point of the fan. +// Constructs a triangle fan mesh to fill a shape using the provided path outside_point represents the center point of the fan. @(optimization_mode="favor_size") -construct_filled_path :: proc( draw_list : ^Draw_List, +fill_path_via_fan_triangulation :: proc( draw_list : ^Draw_List, outside_point : Vec2, path : []Vertex, scale := Vec2 { 1, 1 }, @@ -187,6 +201,7 @@ construct_filled_path :: proc( draw_list : ^Draw_List, } } +// Glyph triangulation generator @(optimization_mode="favor_size") generate_glyph_pass_draw_list :: proc(draw_list : ^Draw_List, path : ^[dynamic]Vertex, glyph_shape : Parser_Glyph_Shape, @@ -209,7 +224,7 @@ generate_glyph_pass_draw_list :: proc(draw_list : ^Draw_List, path : ^[dynamic]V { case .Move: if len(path) > 0 { - construct_filled_path( draw_list, outside, path[:], scale, translate) + fill_path_via_fan_triangulation( draw_list, outside, path[:], scale, translate) clear(path) } fallthrough @@ -242,7 +257,7 @@ generate_glyph_pass_draw_list :: proc(draw_list : ^Draw_List, path : ^[dynamic]V } if len(path) > 0 { - construct_filled_path(draw_list, outside, path[:], scale, translate) + fill_path_via_fan_triangulation(draw_list, outside, path[:], scale, translate) } draw.end_index = u32(len(draw_list.indices)) @@ -251,39 +266,63 @@ generate_glyph_pass_draw_list :: proc(draw_list : ^Draw_List, path : ^[dynamic]V } } -generate_shapes_draw_list :: proc ( ctx : ^Context, font : Font_ID, colour : Colour, entry : Entry, px_size, font_scale : f32, position, scale : Vec2, shapes : []Shaped_Text ) +// Just a warpper of generate_shape_draw_list for handling an array of shapes +generate_shapes_draw_list :: #force_inline proc ( ctx : ^Context, font : Font_ID, colour : RGBAN, entry : Entry, px_size, font_scale : f32, position, scale : Vec2, shapes : []Shaped_Text ) { assert(len(shapes) > 0) for shape in shapes { ctx.cursor_pos = {} ctx.cursor_pos = generate_shape_draw_list( & ctx.draw_list, shape, & ctx.atlas, & ctx.glyph_buffer, ctx.px_scalar, + ctx.enable_draw_type_visualization, colour, entry, px_size, font_scale, position, scale, - ctx.snap_width, - ctx.snap_height ) } } -@(optimization_mode="favor_size") +/* Core generator pipeline for shapes + + If you'd like to make a custom draw procedure, this may either be directly used, or + should be straight forward to make an augmented derivative for a specific codepath. + + This procedure has no awareness of layers. That should be handled by a higher-order codepath. + For this level of codepaths what matters is maximizing memory locality for: + * Dealing with shaping (essentially minimizing having to ever deal with it in a hot path if possible) + * Metric resolution of glyphs within a shape + * Note: It may be better to have the shape to track/store the glyph bounds and lru codes + * The shaper has access to the atlas's dependencies and to parser's metrics (including the unscaled bounds). + + Pipleine order: + * Resolve atlas lru codes and track shape indexes + * Resolve the glyph's position offset from the target position + * Resolve glyph bounds and scale + * Resolve atlas region the glyph is associated with + * Segregate the glyphs into three slices: oversized, to_cache, cached. + * If oversized is not necessary for your use case and your hitting a bottle neck, remove it in a derivative procedure. + * You have to to be drawing a px font size > ~140 px for it to trigger. + * The atlas can be scaled with the size_multiplier parameter of startup so that it becomes more irrelevant if processing a larger atlas is a non-issue. + * The segregation will not allow slices to exceed the batch_cache capacity of the glyph_buffer (configurable within startup params) + * When The capacity is reached batch_generate_glyphs_draw_list will be called which will do futher compute and then finally draw_list generation. + * This may perform better with smaller shapes vs larger shapes, but having more shapes has a cache lookup penatly so keep that in mind. +*/ generate_shape_draw_list :: proc( draw_list : ^Draw_List, shape : Shaped_Text, atlas : ^Atlas, glyph_buffer : ^Glyph_Draw_Buffer, px_scalar : f32, - colour : Colour, - entry : Entry, - px_size : f32, - font_scale : f32, + enable_debug_vis_type : f32, + + colour : RGBAN, + entry : Entry, + px_size : f32, + font_scale : f32, target_position : Vec2, target_scale : Vec2, - snap_width : f32, - snap_height : f32 ) -> (cursor_pos : Vec2) #no_bounds_check { profile(#procedure) @@ -425,7 +464,8 @@ generate_shape_draw_list :: proc( draw_list : ^Draw_List, shape : Shaped_Text, entry, colour, font_scale, - target_scale + target_scale, + enable_debug_vis_type ) reset_batch( & glyph_buffer.batch_cache) @@ -446,7 +486,8 @@ generate_shape_draw_list :: proc( draw_list : ^Draw_List, shape : Shaped_Text, entry, colour, font_scale, - target_scale + target_scale, + enable_debug_vis_type ) } @@ -454,6 +495,19 @@ generate_shape_draw_list :: proc( draw_list : ^Draw_List, shape : Shaped_Text, return } +/* + The glyphs types have been segregated by this point into a batch slice of indices to the glyph_pack + The transform and draw quads are computed first (getting the math done in one spot as possible...) + Some of the math from to_cache pass for glyph generation was not moved over (it could be but I'm not sure its worth it...) + + Order: Oversized first, then to_cache, then cached. + + Oversized and to_cache will both enqueue operations for rendering glyphs to the glyph buffer render target. + The compute section will have operations reguarding how many glyphs they made alloate before a flush must occur. + A flush will force one of the following: + * Oversized will have a draw call setup to blit directly from the glyph buffer to the target. + * to_cache will blit the glyphs rendered to the buffer to the atlas. +*/ batch_generate_glyphs_draw_list :: proc ( draw_list : ^Draw_List, glyph_pack : ^#soa[dynamic]Glyph_Pack_Entry, cached : []i32, @@ -465,29 +519,53 @@ batch_generate_glyphs_draw_list :: proc ( draw_list : ^Draw_List, atlas_size : Vec2, glyph_buffer_size : Vec2, - entry : Entry, - colour : Colour, - font_scale : Vec2, - target_scale : Vec2, + entry : Entry, + colour : RGBAN, + font_scale : Vec2, + target_scale : Vec2, + enable_debug_vis_type : f32, ) #no_bounds_check { profile(#procedure) - - when ENABLE_DRAW_TYPE_VIS { - colour := colour - } + colour := colour profile_begin("glyph buffer transform & draw quads compute") - for id, index in cached + for id, index in oversized { - // Quad to for drawing atlas slot to target glyph := & glyph_pack[id] - quad := & glyph.draw_quad - quad.dst_pos = glyph.position + (glyph.bounds_scaled.p0) * target_scale - quad.dst_scale = (glyph.scale) * target_scale - quad.src_scale = (glyph.scale) - quad.src_pos = (glyph.region_pos) - to_target_space( & quad.src_pos, & quad.src_scale, atlas_size ) + + f32_allocated_x := cast(f32) glyph_buffer.allocated_x + // Resolve how much space this glyph will allocate in the buffer + buffer_size := (glyph.bounds_size_scaled + glyph_buffer.draw_padding) * glyph.over_sample + + // Allocate a glyph glyph render target region (FBO) + to_allocate_x := buffer_size.x + 2.0 + glyph_buffer.allocated_x += i32(to_allocate_x) + + // If allocation would exceed buffer's bounds the buffer must be flush before this glyph can be rendered. + glyph.flush_glyph_buffer = i32(f32_allocated_x + to_allocate_x) >= i32(glyph_buffer_size.x) + glyph.buffer_x = f32_allocated_x * f32( i32( ! glyph.flush_glyph_buffer ) ) + + // Quad to for drawing atlas slot to target + draw_quad := & glyph.draw_quad + + glyph_padding := vec2(glyph_buffer.draw_padding) + + // Target position (draw_list's target image) + draw_quad.dst_pos = glyph.position + (glyph.bounds_scaled.p0 - glyph_padding) * target_scale + draw_quad.dst_scale = (glyph.bounds_size_scaled + glyph_padding) * target_scale + + // The glyph buffer space transform for generate_glyph_pass_draw_list + draw_transform := & glyph.draw_transform + draw_transform.scale = font_scale * glyph.over_sample + draw_transform.pos = -1 * glyph.bounds.p0 * draw_transform.scale + vec2(atlas.glyph_padding) + draw_transform.pos.x += glyph.buffer_x + to_glyph_buffer_space( & draw_transform.pos, & draw_transform.scale, glyph_buffer_size ) + + + draw_quad.src_pos = Vec2 { glyph.buffer_x, 0 } + draw_quad.src_scale = glyph.bounds_size_scaled * glyph.over_sample + glyph_padding + to_target_space( & draw_quad.src_pos, & draw_quad.src_scale, glyph_buffer_size ) } for id, index in to_cache { @@ -526,52 +604,25 @@ batch_generate_glyphs_draw_list :: proc ( draw_list : ^Draw_List, draw_quad.src_pos = (glyph.region_pos) to_target_space( & draw_quad.src_pos, & draw_quad.src_scale, atlas_size ) } - for id, index in oversized + for id, index in cached { - glyph := & glyph_pack[id] - - f32_allocated_x := cast(f32) glyph_buffer.allocated_x - // Resolve how much space this glyph will allocate in the buffer - buffer_size := (glyph.bounds_size_scaled + glyph_buffer.draw_padding) * glyph.over_sample - - // Allocate a glyph glyph render target region (FBO) - to_allocate_x := buffer_size.x + 2.0 - glyph_buffer.allocated_x += i32(to_allocate_x) - - // If allocation would exceed buffer's bounds the buffer must be flush before this glyph can be rendered. - glyph.flush_glyph_buffer = i32(f32_allocated_x + to_allocate_x) >= i32(glyph_buffer_size.x) - glyph.buffer_x = f32_allocated_x * f32( i32( ! glyph.flush_glyph_buffer ) ) - // Quad to for drawing atlas slot to target - draw_quad := & glyph.draw_quad - - glyph_padding := vec2(glyph_buffer.draw_padding) - - // Target position (draw_list's target image) - draw_quad.dst_pos = glyph.position + (glyph.bounds_scaled.p0 - glyph_padding) * target_scale - draw_quad.dst_scale = (glyph.bounds_size_scaled + glyph_padding) * target_scale - - // The glyph buffer space transform for generate_glyph_pass_draw_list - draw_transform := & glyph.draw_transform - draw_transform.scale = font_scale * glyph.over_sample - draw_transform.pos = -1 * glyph.bounds.p0 * draw_transform.scale + vec2(atlas.glyph_padding) - draw_transform.pos.x += glyph.buffer_x - to_glyph_buffer_space( & draw_transform.pos, & draw_transform.scale, glyph_buffer_size ) - - - draw_quad.src_pos = Vec2 { glyph.buffer_x, 0 } - draw_quad.src_scale = glyph.bounds_size_scaled * glyph.over_sample + glyph_padding - to_target_space( & draw_quad.src_pos, & draw_quad.src_scale, glyph_buffer_size ) + glyph := & glyph_pack[id] + quad := & glyph.draw_quad + quad.dst_pos = glyph.position + (glyph.bounds_scaled.p0) * target_scale + quad.dst_scale = (glyph.scale) * target_scale + quad.src_scale = (glyph.scale) + quad.src_pos = (glyph.region_pos) + to_target_space( & quad.src_pos, & quad.src_scale, atlas_size ) } profile_end() profile_begin("generate oversized glyphs draw_list") + if len(oversized) > 0 { - when ENABLE_DRAW_TYPE_VIS { - colour.r = 1.0 - colour.g = 1.0 - colour.b = 0.0 - } + colour.r = max(colour.a, enable_debug_vis_type) + colour.g = max(colour.g, enable_debug_vis_type) + colour.b = colour.b * f32(cast(i32) ! b32(cast(i32) enable_debug_vis_type)) for id, index in oversized { error : Allocator_Error glyph_pack[id].shape, error = parser_get_glyph_shape(entry.parser_info, glyph_pack[id].index) @@ -611,12 +662,34 @@ batch_generate_glyphs_draw_list :: proc ( draw_list : ^Draw_List, append( & draw_list.calls, draw_to_target ) } - if len(oversized) > 0 do flush_glyph_buffer_draw_list(draw_list, & glyph_buffer.draw_list, & glyph_buffer.clear_draw_list, & glyph_buffer.allocated_x) + flush_glyph_buffer_draw_list(draw_list, & glyph_buffer.draw_list, & glyph_buffer.clear_draw_list, & glyph_buffer.allocated_x) for id, index in oversized do parser_free_shape(entry.parser_info, glyph_pack[id].shape) } profile_end() + generate_cached_draw_list :: #force_inline proc (draw_list : ^Draw_List, glyph_pack : #soa[]Glyph_Pack_Entry, sub_pack : []i32, colour : RGBAN ) + { + profile(#procedure) + call := Draw_Call_Default + call.pass = .Target + call.colour = colour + for id, index in sub_pack + { + profile("glyph") + call.start_index = u32(len(draw_list.indices)) + + quad := glyph_pack[id].draw_quad + blit_quad(draw_list, + quad.dst_pos, quad.dst_pos + quad.dst_scale, + quad.src_pos, quad.src_pos + quad.src_scale + ) + call.end_index = u32(len(draw_list.indices)) + append(& draw_list.calls, call) + } + } + profile_begin("to_cache: caching to atlas") + if len(to_cache) > 0 { for id, index in to_cache { error : Allocator_Error @@ -651,12 +724,12 @@ batch_generate_glyphs_draw_list :: proc ( draw_list : ^Draw_List, dst_glyph_pos := glyph.region_pos dst_glyph_size := glyph.bounds_size_scaled + atlas.glyph_padding - // dst_glyph_size.y = ceil(dst_glyph_size.y) // Note(Ed): Seems to improve hinting + dst_glyph_size.y = ceil(dst_glyph_size.y) * glyph_buffer.snap_glyph_height // Note(Ed): Seems to improve hinting to_glyph_buffer_space( & dst_glyph_pos, & dst_glyph_size, atlas_size ) src_position := Vec2 { glyph.buffer_x, 0 } src_size := (glyph.bounds_size_scaled + atlas.glyph_padding) * glyph_buffer.over_sample - // src_size.y = ceil(src_size.y) // Note(Ed): Seems to improve hinting + src_size.y = ceil(src_size.y) * glyph_buffer.snap_glyph_height // Note(Ed): Seems to improve hinting to_target_space( & src_position, & src_size, glyph_buffer_size ) blit_to_atlas : Draw_Call @@ -681,49 +754,28 @@ batch_generate_glyphs_draw_list :: proc ( draw_list : ^Draw_List, ) } - if len(to_cache) > 0 do flush_glyph_buffer_draw_list(draw_list, & glyph_buffer.draw_list, & glyph_buffer.clear_draw_list, & glyph_buffer.allocated_x) + profile_begin("generate_cached_draw_list: to_cache") + if enable_debug_vis_tyeps { + colour.r = 1.0 + colour.g = 1.0 + colour.b = 1.0 + } + generate_cached_draw_list( draw_list, glyph_pack[:], cached, colour ) + profile_end() + + flush_glyph_buffer_draw_list(draw_list, & glyph_buffer.draw_list, & glyph_buffer.clear_draw_list, & glyph_buffer.allocated_x) for id, index in to_cache do parser_free_shape(entry.parser_info, glyph_pack[id].shape) } profile_end() - generate_cached_draw_list :: #force_inline proc (draw_list : ^Draw_List, glyph_pack : #soa[]Glyph_Pack_Entry, sub_pack : []i32, colour : Colour ) - { - profile(#procedure) - call := Draw_Call_Default - call.pass = .Target - call.colour = colour - for id, index in sub_pack - { - profile("glyph") - call.start_index = u32(len(draw_list.indices)) - - quad := glyph_pack[id].draw_quad - blit_quad(draw_list, - quad.dst_pos, quad.dst_pos + quad.dst_scale, - quad.src_pos, quad.src_pos + quad.src_scale - ) - call.end_index = u32(len(draw_list.indices)) - append(& draw_list.calls, call) - } - } - profile_begin("generate_cached_draw_list: to_cache") - when ENABLE_DRAW_TYPE_VIS { + if enable_debug_vis_tyeps { colour.r = 0.80 colour.g = 0.25 colour.b = 0.25 } generate_cached_draw_list( draw_list, glyph_pack[:], to_cache, colour ) profile_end() - - profile_begin("generate_cached_draw_list: to_cache") - when ENABLE_DRAW_TYPE_VIS { - colour.r = 1.0 - colour.g = 1.0 - colour.b = 1.0 - } - generate_cached_draw_list( draw_list, glyph_pack[:], cached, colour ) - profile_end() } // Flush the content of the glyph_buffers draw lists to the main draw list @@ -747,7 +799,6 @@ flush_glyph_buffer_draw_list :: proc( #no_alias draw_list, glyph_buffer_draw_lis (allocated_x ^) = 0 } -// ve_fontcache_clear_Draw_List @(optimization_mode="favor_size") clear_draw_list :: #force_inline proc ( draw_list : ^Draw_List ) { clear( & draw_list.calls ) @@ -755,7 +806,7 @@ clear_draw_list :: #force_inline proc ( draw_list : ^Draw_List ) { clear( & draw_list.vertices ) } -// ve_fontcache_merge_Draw_List +// Helper used by flush_glyph_buffer_draw_list. Used to append all the content from the src draw list o the destination. @(optimization_mode="favor_size") merge_draw_list :: proc ( #no_alias dst, src : ^Draw_List ) #no_bounds_check { @@ -783,6 +834,8 @@ merge_draw_list :: proc ( #no_alias dst, src : ^Draw_List ) #no_bounds_check } } +// Naive implmentation to merge passes that are equivalent and the following to be merged (b for can_merge_draw_calls) doesn't have a clear todo. +// Its intended for optimiztion passes to occur on a per-layer basis. optimize_draw_list :: proc (draw_list: ^Draw_List, call_offset: int) #no_bounds_check { profile(#procedure) diff --git a/code/font/vefontcache/freetype_wip.odin b/code/font/vefontcache/freetype_wip.odin index 8458182..fa6abf0 100644 --- a/code/font/vefontcache/freetype_wip.odin +++ b/code/font/vefontcache/freetype_wip.odin @@ -1,4 +1,4 @@ -package vefontcache +package vetext when false { // TODO(Ed): Freetype support diff --git a/code/font/vefontcache/misc.odin b/code/font/vefontcache/misc.odin index f2bf1f7..d14d1ee 100644 --- a/code/font/vefontcache/misc.odin +++ b/code/font/vefontcache/misc.odin @@ -1,4 +1,9 @@ -package vefontcache +package vetext + +/* + Didn't want to splinter this into more files.. + Just a bunch of utilities. +*/ import "base:runtime" import "core:simd" @@ -6,6 +11,10 @@ import "core:math" import core_log "core:log" +peek_array :: #force_inline proc "contextless" ( self : [dynamic]$Type ) -> Type { + return self[ len(self) - 1 ] +} + reload_array :: #force_inline proc( self : ^[dynamic]$Type, allocator : Allocator ) { raw := transmute( ^runtime.Raw_Dynamic_Array) self raw.allocator = allocator @@ -43,7 +52,8 @@ to_bytes :: #force_inline proc "contextless" ( typed_data : ^$Type ) -> []byte { @(optimization_mode="favor_size") djb8_hash :: #force_inline proc "contextless" ( hash : ^$Type, bytes : []byte ) { for value in bytes do (hash^) = (( (hash^) << 8) + (hash^) ) + Type(value) } -Colour :: [4]f32 +RGBA8 :: [4]f32 +RGBAN :: [4]f32 Vec2 :: [2]f32 Vec2i :: [2]i32 Vec2_64 :: [2]f64 diff --git a/code/font/vefontcache/parser.odin b/code/font/vefontcache/parser.odin index 17ec818..9f27477 100644 --- a/code/font/vefontcache/parser.odin +++ b/code/font/vefontcache/parser.odin @@ -1,12 +1,18 @@ -package vefontcache +package vetext /* Notes: +This is a minimal wrapper I originally did incase something than stb_truetype is introduced in the future. +Otherwise, its essentially 1:1 with it. -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. +Freetype isn't really supported and its not a high priority (pretty sure its too slow). +~~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.~~ -STB_Truetype has macros for its allocation unfortuantely +STB_Truetype: +* Has macros for its allocation unfortuantely. +TODO(Ed): Just keep a local version of stb_truetype and modify it to support a sokol/odin compatible allocator. +Already wanted to do so anyway to evaluate the shape generation implementation. */ import "base:runtime" diff --git a/code/font/vefontcache/mappings.odin b/code/font/vefontcache/pkg_mapping.odin similarity index 72% rename from code/font/vefontcache/mappings.odin rename to code/font/vefontcache/pkg_mapping.odin index 3a0eee2..111b211 100644 --- a/code/font/vefontcache/mappings.odin +++ b/code/font/vefontcache/pkg_mapping.odin @@ -1,5 +1,6 @@ -package vefontcache +package vetext +import "base:builtin" import "base:runtime" import "core:hash" ginger16 :: hash.ginger16 @@ -50,34 +51,34 @@ append_soa :: proc { } ceil :: proc { - ceil_f16, - ceil_f16le, - ceil_f16be, - ceil_f32, - ceil_f32le, - ceil_f32be, - ceil_f64, - ceil_f64le, - ceil_f64be, + math.ceil_f16, + math.ceil_f16le, + math.ceil_f16be, + math.ceil_f32, + math.ceil_f32le, + math.ceil_f32be, + math.ceil_f64, + math.ceil_f64le, + math.ceil_f64be, ceil_vec2, } clear :: proc { - clear_dynamic_array, - clear_map, + builtin.clear_dynamic_array, + builtin.clear_map, } floor :: proc { - floor_f16, - floor_f16le, - floor_f16be, - floor_f32, - floor_f32le, - floor_f32be, - floor_f64, - floor_f64le, - floor_f64be, + math.floor_f16, + math.floor_f16le, + math.floor_f16be, + math.floor_f32, + math.floor_f32le, + math.floor_f32be, + math.floor_f64, + math.floor_f64le, + math.floor_f64be, floor_vec2, } @@ -87,21 +88,30 @@ fill :: proc { } make :: proc { - make_dynamic_array, - make_dynamic_array_len, - make_dynamic_array_len_cap, - make_slice, - make_map, - make_map_cap, + builtin.make_dynamic_array, + builtin.make_dynamic_array_len, + builtin.make_dynamic_array_len_cap, + builtin.make_slice, + builtin.make_map, + builtin.make_map_cap, } make_soa :: proc { - make_soa_dynamic_array_len_cap, - make_soa_slice, + builtin.make_soa_dynamic_array_len_cap, + builtin.make_soa_slice, +} + +peek :: proc { + peek_array, +} + +pop_array :: proc { + builtin.pop, + // pop_safe, } resize :: proc { - resize_dynamic_array, + builtin.resize_dynamic_array, } vec2 :: proc { diff --git a/code/font/vefontcache/shaper.odin b/code/font/vefontcache/shaper.odin index 653b693..b069b53 100644 --- a/code/font/vefontcache/shaper.odin +++ b/code/font/vefontcache/shaper.odin @@ -1,4 +1,4 @@ -package vefontcache +package vetext /* 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. */ @@ -8,28 +8,51 @@ import "thirdparty:harfbuzz" Shape_Key :: u32 +/* A text whose codepoints have had their relevant glyphs and + associated data resolved for processing in a draw list generation stage. + Traditionally a shape only refers to resolving which glyph and + its position should be used for rendering. + + For this library's case it also involes keeping any content + that does not have to be resolved up once again in a later stage of + preparing it for rendering. + + Ideally the user should resolve this shape once and cache/store it on their side. + They have the best ability to avoid costly lookups to streamline + a hot path to only focusing on draw list generation that must be computed every frame. + + For ease of use the cache does a relatively good job and only adds a + few hundred nano-seconds to resolve a shape's lookup from its source specification. + If your doing something heavy though (where there is thousands, or tens-of thousands) + your not going to be satisfied with keeping that in the iteration). +*/ Shaped_Text :: struct { glyphs : [dynamic]Glyph, positions : [dynamic]Vec2, + // bounds : [dynamic]Range2, // TODO(Ed): Test tracking/resolving the bounds here, its expensive to resolve at the draw list generation stage. + // region_kinds : [dynamic]Atlas_Region_Kind, // TODO(ed): test tracking/resolving the assigne atlas region here, for some reason as ^^. end_cursor_pos : Vec2, size : Vec2, - entry : ^Entry, - font : Font_ID, } +// Ease of use cache, can handle thousands of lookups per frame with ease. +// TODO(Ed) It might perform better with a tailored made hashtable implementation for the LRU_Cache or dedicated array struct/procs for the Shaped_Text. Shaped_Text_Cache :: struct { storage : [dynamic]Shaped_Text, state : LRU_Cache(Shape_Key), next_cache_id : i32, } +// Used by shaper_shape_text_cached, allows user to specify their own proc at compile-time without having to rewrite the caching implementation. Shaper_Shape_Text_Uncached_Proc :: #type proc( ctx : ^Shaper_Context, entry : Entry, font_px_Size, font_scale : f32, text_utf8 : string, output : ^Shaped_Text ) +// Note(Ed): Not used.. Shaper_Kind :: enum { - Naive = 0, + Latin = 0, Harfbuzz = 1, } +// Not much here other than just keep track of a harfbuzz var and deciding to keep runtime config here used by the shapers. Shaper_Context :: struct { hb_buffer : harfbuzz.Buffer, @@ -37,6 +60,7 @@ Shaper_Context :: struct { adv_snap_small_font_threshold : f32, } +// Only used with harbuzz for now. Resolved during load_font for a font Entry. Shaper_Info :: struct { blob : harfbuzz.Blob, face : harfbuzz.Face, @@ -71,6 +95,8 @@ shaper_unload_font :: #force_inline proc( info : ^Shaper_Info ) if info.blob != nil do harfbuzz.blob_destroy( info.blob ) } +// Recommended shaper. Very performant. +// TODO(Ed): Would be nice to properly support vertical shaping, right now its strictly just horizontal... @(optimization_mode="favor_size") shaper_shape_harfbuzz :: proc( ctx : ^Shaper_Context, text_utf8 : string, entry : Entry, font_px_Size, font_scale : f32, output :^Shaped_Text ) { @@ -141,7 +167,7 @@ shaper_shape_harfbuzz :: proc( ctx : ^Shaper_Context, text_utf8 : string, entry } if abs( font_px_size ) <= adv_snap_small_font_threshold { - (position^) = ceil( position^ ) + (position^) = ceil( position^ ) } glyph_pos := position^ @@ -244,6 +270,7 @@ shaper_shape_text_uncached_advanced :: #force_inline proc( ctx : ^Shaper_Context shaper_shape_harfbuzz( ctx, text_utf8, entry, font_px_size, font_scale, output ) } +// Basic western alphabet based shaping. Not that much faster than harfbuzz if at all. shaper_shape_text_latin :: proc( ctx : ^Shaper_Context, entry : Entry, font_px_Size : f32, @@ -308,6 +335,12 @@ shaper_shape_text_latin :: proc( ctx : ^Shaper_Context, output.size.y = f32(line_count) * line_height } +// Shapes are tracked by the library's context using the shape cache +// and the key is resolved using the font, the desired pixel size, and the text bytes to be shaped. +// Thus this procedures cost will be proporitonal to how muh text it has to sift through. +// djb8_hash is used as its been pretty good for thousands of hashed lines that around 6-120 charactes long +// (and its very fast). +@(optimization_mode="favor_size") shaper_shape_text_cached :: proc( text_utf8 : string, ctx : ^Shaper_Context, shape_cache : ^Shaped_Text_Cache, diff --git a/code/font/vefontcache/vefontcache.odin b/code/font/vefontcache/vefontcache.odin index 914a00e..04a7f1a 100644 --- a/code/font/vefontcache/vefontcache.odin +++ b/code/font/vefontcache/vefontcache.odin @@ -3,12 +3,10 @@ A port of (https://github.com/hypernewbie/VEFontCache) to Odin. See: https://github.com/Ed94/VEFontCache-Odin */ -package vefontcache +package vetext import "base:runtime" -// White: Cached Hit, Red: Cache Miss, Yellow: Oversized -ENABLE_DRAW_TYPE_VIS :: false // See: mappings.odin for profiling hookup DISABLE_PROFILING :: true @@ -21,12 +19,11 @@ Load_Font_Error :: enum(i32) { } Entry :: struct { - parser_info : Parser_Font_Info, - shaper_info : Shaper_Info, - id : Font_ID, - used : b32, - curve_quality : f32, - // snap_glyph_pos : + parser_info : Parser_Font_Info, + shaper_info : Shaper_Info, + id : Font_ID, + used : b32, + curve_quality : f32, ascent : f32, descent : f32, @@ -39,6 +36,23 @@ Entry_Default :: Entry { curve_quality = 3, } +// Ease of use encapsulation of common fields for a canvas space +Camera :: struct { + view : [dynamic]Vec2, + position : [dynamic]Vec2, + zoom : [dynamic]f32, +} + +Scope_Stack :: struct { + font : [dynamic]Font_ID, + font_size : [dynamic]f32, + colour : [dynamic]RGBAN, + view : [dynamic]Vec2, + position : [dynamic]Vec2, + scale : [dynamic]Vec2, + zoom : [dynamic]f32, +} + Context :: struct { backing : Allocator, @@ -48,11 +62,14 @@ Context :: struct { // The managed font instances entries : [dynamic]Entry, + // TODO(Ed): Review these when preparing to handle lifting of working context to a thread context. glyph_buffer : Glyph_Draw_Buffer, atlas : Atlas, shape_cache : Shaped_Text_Cache, draw_list : Draw_List, + batch_shapes_buffer : [dynamic]Shaped_Text, // Used for the procs that batch a layer of text. + // Tracks the offsets for the current layer in a draw_list draw_layer : struct { vertices_offset : int, @@ -60,25 +77,35 @@ Context :: struct { calls_offset : int, }, - debug_print : b32, - debug_print_verbose : b32, + // Note(Ed): Not really used anymore. + // debug_print : b32, + // debug_print_verbose : b32, - //TODO(Ed): Add a push/pop stack for the below - - // Helps with hinting snap_width : f32, snap_height : f32, - // camera : Camera, // TODO(Ed): Add camera support - colour : Colour, // Color used in draw interface - cursor_pos : Vec2, - alpha_sharpen : f32, // Will apply a boost scalar (1.0 + alpha sharpen) to the colour's alpha which provides some sharpening of the edges. + // Will enforce even px_size when drawing. + even_size_only : f32, + + // Whether or not to snap positioning to the pixel of the view + // Helps with hinting + snap_to_view_extent : b32, + + stack : Scope_Stack, + + colour : RGBAN, // Color used in draw interface TODO(Ed): use the stack + cursor_pos : Vec2, // TODO(Ed): Review this, no longer used much at all... (still useful I guess) + // Will apply a boost scalar (1.0 + alpha sharpen) to the colour's alpha which provides some sharpening of the edges. + // Has a boldening side-effect. If overblown will look smeared. + alpha_sharpen : f32, // Used by draw interface to super-scale the text by // upscaling px_size with px_scalar and then down-scaling // the draw_list result by the same amount. - px_scalar : f32, // Used to upscale which font size is used to render (improves hinting) + px_scalar : f32, // Improves hinting, positioning, etc. Can make zoomed out text too jagged. - default_curve_quality : i32, + // White: Cached Hit, Red: Cache Miss, Yellow: Oversized (Will override user's colors when active) + enable_draw_type_visualization : f32, + default_curve_quality : i32, } Init_Atlas_Params :: struct { @@ -92,14 +119,22 @@ Init_Atlas_Params_Default :: Init_Atlas_Params { } Init_Glyph_Draw_Params :: struct { + // During the draw list generation stage when blitting to atlas, the quad wil be ceil()'d to the closest pixel. + snap_glyph_height : b32, + // Intended to be x16 (4x4) super-sampling from the glyph buffer to the atlas. + // Oversized glyphs don't use this and instead do 2x or 1x depending on how massive they are. over_sample : u32, + // Best to just keep this the same as glyph_padding for the atlas.. draw_padding : u32, shape_gen_scratch_reserve : u32, - buffer_glyph_limit : u32, // How many region.D glyphs can be drawn to the glyph render target buffer at once (worst case scenario) - batch_glyph_limit : u32, // How many glyphs can at maximimum be proccessed at once by batch_generate_glyphs_draw_list + // How many region.D glyphs can be drawn to the glyph render target buffer at once (worst case scenario) + buffer_glyph_limit : u32, + // How many glyphs can at maximimum be proccessed at once by batch_generate_glyphs_draw_list + batch_glyph_limit : u32, } Init_Glyph_Draw_Params_Default :: Init_Glyph_Draw_Params { + snap_glyph_height = true, over_sample = 4, draw_padding = Init_Atlas_Params_Default.glyph_padding, shape_gen_scratch_reserve = 10 * 1024, @@ -108,23 +143,28 @@ Init_Glyph_Draw_Params_Default :: Init_Glyph_Draw_Params { } Init_Shaper_Params :: struct { + // Forces a glyph position to align to a pixel (make sure to use snap_to_view_extent with this or else it won't be preserveds) snap_glyph_position : b32, + // Will use more signficant advance during shaping for fonts + // Note(Ed): Thinking of removing, doesn't look good often and its an extra condition in the hot-loop. adv_snap_small_font_threshold : u32, } Init_Shaper_Params_Default :: Init_Shaper_Params { - snap_glyph_position = false, + snap_glyph_position = true, adv_snap_small_font_threshold = 0, } Init_Shape_Cache_Params :: struct { - capacity : u32, - reserve_length : u32, + // Note(Ed): This should mostly just be given the worst-case capacity and reserve at the same time. + // If memory is a concern it can easily be 256 - 2k if not much text is going to be rendered often. + capacity : u32, + reserve : u32, } Init_Shape_Cache_Params_Default :: Init_Shape_Cache_Params { - capacity = 10 * 1024, - reserve_length = 10 * 1024, + capacity = 10 * 1024, + reserve = 10 * 1024, } //#region("lifetime") @@ -143,6 +183,7 @@ startup :: proc( ctx : ^Context, parser_kind : Parser_Kind = .STB_TrueType, // Affects step size for bezier curve passes in generate_glyph_pass_draw_list default_curve_quality : u32 = 3, entires_reserve : u32 = 256, + scope_stack_reserve : u32 = 128, ) { assert( ctx != nil, "Must provide a valid context" ) @@ -230,10 +271,10 @@ startup :: proc( ctx : ^Context, parser_kind : Parser_Kind = .STB_TrueType, for idx : u32 = 0; idx < shape_cache_params.capacity; idx += 1 { stroage_entry := & shape_cache.storage[idx] - stroage_entry.glyphs, error = make( [dynamic]Glyph, len = 0, cap = shape_cache_params.reserve_length ) + stroage_entry.glyphs, error = make( [dynamic]Glyph, len = 0, cap = shape_cache_params.reserve ) assert( error == .None, "VEFontCache.init : Failed to allocate glyphs array for shape cache storage" ) - stroage_entry.positions, error = make( [dynamic]Vec2, len = 0, cap = shape_cache_params.reserve_length ) + stroage_entry.positions, error = make( [dynamic]Vec2, len = 0, cap = shape_cache_params.reserve ) assert( error == .None, "VEFontCache.init : Failed to allocate positions array for shape cache storage" ) } } @@ -384,6 +425,34 @@ shutdown :: proc( ctx : ^Context ) parser_shutdown( & ctx.parser_ctx ) } +// Can be used with hot-reload +clear_atlas_region_caches :: proc(ctx : ^Context) +{ + lru_clear(& ctx.atlas.region_a.state) + lru_clear(& ctx.atlas.region_b.state) + lru_clear(& ctx.atlas.region_c.state) + lru_clear(& ctx.atlas.region_d.state) + + ctx.atlas.region_a.next_idx = 0 + ctx.atlas.region_b.next_idx = 0 + ctx.atlas.region_c.next_idx = 0 + ctx.atlas.region_d.next_idx = 0 +} + +// Can be used with hot-reload +clear_shape_cache :: proc (ctx : ^Context) +{ + lru_clear(& ctx.shape_cache.state) + for idx : i32 = 0; idx < cast(i32) cap(ctx.shape_cache.storage); idx += 1 { + stroage_entry := & ctx.shape_cache.storage[idx] + stroage_entry.end_cursor_pos = {} + stroage_entry.size = {} + clear(& stroage_entry.glyphs) + clear(& stroage_entry.positions) + } + ctx.shape_cache.next_cache_id = 0 +} + load_font :: proc( ctx : ^Context, label : string, data : []byte, glyph_curve_quality : u32 = 0 ) -> (font_id : Font_ID, error : Load_Font_Error) { profile(#procedure) @@ -454,7 +523,7 @@ unload_font :: proc( ctx : ^Context, font : Font_ID ) //#endregion("lifetime") -//#region("drawing") +//#region("scoping") configure_snap :: #force_inline proc( ctx : ^Context, snap_width, snap_height : u32 ) { assert( ctx != nil ) @@ -462,34 +531,125 @@ configure_snap :: #force_inline proc( ctx : ^Context, snap_width, snap_height : ctx.snap_height = f32(snap_height) } -get_cursor_pos :: #force_inline proc( ctx : ^Context ) -> Vec2 { assert(ctx != nil); return ctx.cursor_pos } -set_alpha_scalar :: #force_inline proc( ctx : ^Context, scalar : f32 ) { assert(ctx != nil); ctx.alpha_sharpen = scalar } -set_px_scalar :: #force_inline proc( ctx : ^Context, scalar : f32 ) { assert(ctx != nil); ctx.px_scalar = scalar } -set_colour :: #force_inline proc( ctx : ^Context, colour : Colour ) { assert(ctx != nil); ctx.colour = colour } +/* Scope stacking ease of use interface. + +View: Extents in 2D for the relative space the the text is being drawn within. +Used with snap_to_view_extent to enforce position snapping. + +Position: Used with a draw procedure that uses relative positioning will offset the incoming position by the given amount. +Scale : Used with a draw procedure that uses relative scaling, will scale the procedures incoming scale by the given amount. +Zoom : Used with a draw procedure that uses scaling via zoom, will scale the procedure's incoming font size & scale based on an 'canvas' camera's notion of it. + +The pkg_mapping.odin provides an explicit set of overloads for these: +scope() +push() +pop() +*/ + +@(deferred_none = pop_font) +scope_font :: #force_inline proc( ctx : ^Context, font : Font_ID ) { assert(ctx != nil); append(& ctx.stack.font, font ) } +push_font :: #force_inline proc( ctx : ^Context, font : Font_ID ) { assert(ctx != nil); append(& ctx.stack.font, font ) } +pop_font :: #force_inline proc( ctx : ^Context, font : Font_ID ) { assert(ctx != nil); pop_array(& ctx.stack.font) } + +@(deferred_none = pop_font_size) +scope_font_size :: #force_inline proc( ctx : ^Context, px_size : f32 ) { assert(ctx != nil); append(& ctx.stack.font_size, px_size) } +push_font_size :: #force_inline proc( ctx : ^Context, px_size : f32 ) { assert(ctx != nil); append(& ctx.stack.font_size, px_size) } +pop_font_size :: #force_inline proc( ctx : ^Context, px_size : f32 ) { assert(ctx != nil); pop_array(& ctx.stack.font_size) } + +@(deferred_none = pop_colour ) +scope_colour :: #force_inline proc( ctx : ^Context, colour : RGBAN ) { assert(ctx != nil); append(& ctx.stack.colour, colour) } +push_colour :: #force_inline proc( ctx : ^Context, colour : RGBAN ) { assert(ctx != nil); append(& ctx.stack.colour, colour) } +pop_colour :: #force_inline proc( ctx : ^Context ) { assert(ctx != nil); pop_array(& ctx.stack.colour) } + +@(deferred_none = pop_view) +scope_view :: #force_inline proc( ctx : ^Context, view : Vec2 ) { assert(ctx != nil); append(& ctx.stack.view, view) } +push_view :: #force_inline proc( ctx : ^Context, view : Vec2 ) { assert(ctx != nil); append(& ctx.stack.view, view) } +pop_view :: #force_inline proc( ctx : ^Context ) { assert(ctx != nil); pop_array(& ctx.stack.view) } + +@(deferred_none = pop_position) +scope_position :: #force_inline proc( ctx : ^Context, position : Vec2 ) { assert(ctx != nil); append(& ctx.stack.position, position ) } +push_position :: #force_inline proc( ctx : ^Context, position : Vec2 ) { assert(ctx != nil); append(& ctx.stack.position, position ) } +pop_position :: #force_inline proc( ctx : ^Context ) { assert(ctx != nil); pop_array( & ctx.stack.position) } + +@(deferred_none = pop_scale) +scope_scale :: #force_inline proc( ctx : ^Context, scale : Vec2 ) { append(& ctx.stack.scale, scale ) } +push_scale :: #force_inline proc( ctx : ^Context, scale : Vec2 ) { append(& ctx.stack.scale, scale ) } +pop_scale :: #force_inline proc( ctx : ^Context, scale : Vec2 ) { pop_array(& ctx.stack.scale) } + +@(deferred_none = pop_zoom ) +scope_zoom :: #force_inline proc( ctx : ^Context, zoom : f32 ) { append(& ctx.stack.zoom, zoom ) } +push_zoom :: #force_inline proc( ctx : ^Context, zoom : f32 ) { append(& ctx.stack.zoom, zoom) } +pop_zoom :: #force_inline proc( ctx : ^Context ) { pop_array(& ctx.stack.zoom) } + +@(deferred_none = pop_camera) +scope_camera :: #force_inline proc( ctx : ^Context, camera : Camera ) { + append(& ctx.stack.view, camera.view ) + append(& ctx.stack.position, camera.position ) + append(& ctx.stack.zoom, camera.zoom ) +} +push_camera :: #force_inline proc( ctx : ^Context, camera : Camera ) { + append(& ctx.stack.view, camera.view ) + append(& ctx.stack.position, camera.position ) + append(& ctx.stack.zoom, camera.zoom ) +} +pop_camera :: #force_inline proc( ctx : ^Context ) { + pop_array(& ctx.stack.view ) + pop_array(& ctx.stack.position) + pop_array(& ctx.stack.zoom ) +} + +//#endregion("scoping") + +//#region("draw_list generation") + +get_cursor_pos :: #force_inline proc "contextless" ( ctx : Context ) -> Vec2 { return ctx.cursor_pos } + +// Does nothing when view is 1 or 0. +get_snapped_position :: #force_inline proc "contextless" ( ctx : Context, position : Vec2 ) -> (snapped_position : Vec2) { + snap_width := max(ctx.snap_width, 1) + snap_height := max(ctx.snap_height, 1) + snap_quotient := 1 / Vec2 { snap_width, snap_height } + snapped_position = position + snapped_position.x = ceil(position.x * snap_width ) * snap_quotient.x + snapped_position.y = ceil(position.y * snap_height) * snap_quotient.y + return +} + +set_alpha_scalar :: #force_inline proc( ctx : ^Context, scalar : f32 ) { assert(ctx != nil); ctx.alpha_sharpen = scalar } +set_colour :: #force_inline proc( ctx : ^Context, colour : RGBAN ) { assert(ctx != nil); ctx.colour = colour } +set_draw_type_visualization :: #force_inline proc( ctx : ^Context, should_enable : b32 ) { assert(ctx != nil); ctx.enable_draw_type_visualization = cast(f32) i32(should_enable); } +set_px_scalar :: #force_inline proc( ctx : ^Context, scalar : f32 ) { assert(ctx != nil); ctx.px_scalar = scalar } set_snap_glyph_pos :: #force_inline proc( ctx : ^Context, should_snap : b32 ) { assert(ctx != nil) ctx.shaper_ctx.snap_glyph_position = should_snap } +// Non-scoping context. The most fundamental interface-level draw shape procedure. +// (everything else is either batching/pipelining or quality of life warppers) +// Note: Prefer over draw_text_normalized_space if possible to resolve shape once persistently or at least once per frame or non-dirty state. @(optimization_mode="favor_size") -draw_text :: #force_inline proc( ctx : ^Context, font : Font_ID, px_size : f32, position, scale : Vec2, text_utf8 : string ) +draw_text_shape_normalized_space :: #force_inline proc( ctx : ^Context, + font : Font_ID, + px_size : f32, + colour : RGBAN, + view : Vec2, + position : Vec2, + scale : Vec2, + zoom : f32, + shape : Shaped_Text +) { profile(#procedure) assert( ctx != nil ) assert( font >= 0 && int(font) < len(ctx.entries) ) - assert( len(text_utf8) > 0 ) + + adjusted_position := get_snapped_position( ctx, position ) entry := ctx.entries[ font ] - ctx.cursor_pos = {} - - position := position - position.x = ceil(position.x * ctx.snap_width ) / ctx.snap_width - position.y = ceil(position.y * ctx.snap_height) / ctx.snap_height - - colour := ctx.colour - colour.a = 1.0 + ctx.alpha_sharpen + adjusted_colour := colour + adjusted_colour.a = 1.0 + ctx.alpha_sharpen font_scale := parser_scale( entry.parser_info, px_size ) @@ -497,141 +657,129 @@ draw_text :: #force_inline proc( ctx : ^Context, font : Font_ID, px_size : f32, downscale := scale * (1 / ctx.px_scalar) font_scale_upscale := parser_scale( entry.parser_info, px_upscale ) - shape := shaper_shape_text_cached( text_utf8, & ctx.shaper_ctx, & ctx.shape_cache, - font, + ctx.cursor_pos = generate_shape_draw_list( & ctx.draw_list, shape, & ctx.atlas, & ctx.glyph_buffer, ctx.px_scalar, + adjusted_colour, entry, px_upscale, font_scale_upscale, + position, + downscale, + ) +} + +// Non-scoping context. The most fundamental interface-level draw text procedure. +// (everything else is either batching/pipelining or quality of life warppers) +// Note: shape lookup can be expensive on high call usage. +draw_text_normalized_space :: #force_inline proc( ctx : ^Context, + font : Font_ID, + px_size : f32, + colour : RGBAN, + view : Vec2, + position : Vec2, + scale : Vec2, + zoom : f32, + text_utf8 : string +) +{ + profile(#procedure) + assert( ctx != nil ) + assert( font >= 0 && int(font) < len(ctx.entries) ) + assert( len(text_utf8) > 0 ) + assert( ctx.snap_width > 0 && ctx.snap_height > 0 ) + + ctx.cursor_pos = {} + entry := ctx.entries[ font ] + + adjusted_position := get_snapped_position( ctx, position ) + + colour := ctx.colour + colour.a = 1.0 + ctx.alpha_sharpen + + // Does nothing when px_scalar is 1.0 + target_px_size := px_size * ctx.px_scalar + target_scale := scale * (1 / ctx.px_scalar) + target_font_scale := parser_scale( entry.parser_info, px_upscale ) + + shape := shaper_shape_text_cached( text_utf8, & ctx.shaper_ctx, & ctx.shape_cache, + font, + entry, + target_px_size, + target_font_scale, shaper_shape_text_uncached_advanced ) ctx.cursor_pos = generate_shape_draw_list( & ctx.draw_list, shape, & ctx.atlas, & ctx.glyph_buffer, ctx.px_scalar, colour, entry, - px_upscale, - font_scale_upscale, - position, - downscale, - ctx.snap_width, - ctx.snap_height + target_px_size, + target_font_scale, + adjusted_position, + target_scale, ) } -draw_text_slice :: #force_inline proc( ctx : ^Context, font : Font_ID, px_size : f32, position, scale : Vec2, texts : []string ) + +draw_shape :: #force_inline proc( ctx : ^Context, position, scale : Vec2, shape : Shaped_Text ) { + // peek_position +} + +draw_text :: #force_inline proc( ctx : ^Context, text_utf8 : string, position, scale : Vec2 ) { + +} + +Text_Layer_Elem :: struct { + text : string, + view : Vec2, + position : Vec2, + scale : Vec2, + font : Font_ID, + px_size : f32, + zoom : f32, + colour : Colour, +} + +// Batch a layer of text. Use get_draw_list_layer to process the layer immediately after. +draw_text_layer :: #force_inline proc( ctx : ^Context, layer : []Text_Layer_Elem ) { profile(#procedure) assert( ctx != nil ) assert( font >= 0 && int(font) < len(ctx.entries) ) assert( len(texts) > 0 ) - entry := ctx.entries[ font ] + for elem in layer + { + entry := ctx.entries[ elem.font ] - ctx.cursor_pos = {} + ctx.cursor_pos = {} - position := position - position.x = ceil(position.x * ctx.snap_width ) / ctx.snap_width - position.y = ceil(position.y * ctx.snap_height) / ctx.snap_height + adjusted_position := get_snapped_position( ctx, position ) - colour := ctx.colour - colour.a = 1.0 + ctx.alpha_sharpen + colour := ctx.colour + colour.a = 1.0 + ctx.alpha_sharpen - font_scale := parser_scale( entry.parser_info, px_size ) + font_scale := parser_scale( entry.parser_info, px_size ) - px_upscale := px_size * ctx.px_scalar - downscale := scale * (1 / ctx.px_scalar) - font_scale_upscale := parser_scale( entry.parser_info, px_upscale ) + px_upscale := px_size * ctx.px_scalar + downscale := scale * (1 / ctx.px_scalar) + font_scale_upscale := parser_scale( entry.parser_info, px_upscale ) - shapes := make( []Shaped_Text, len(texts) ) - for str, id in texts { - assert( len(str) > 0 ) - shape := shaper_shape_text_cached( str, & ctx.shaper_ctx, & ctx.shape_cache, - font, - entry, - px_upscale, - font_scale_upscale, - shaper_shape_text_uncached_advanced - ) - shapes[id] = shape + shapes := make( []Shaped_Text, len(texts) ) + for str, id in texts + { + assert( len(str) > 0 ) + shape := shaper_shape_text_cached( str, & ctx.shaper_ctx, & ctx.shape_cache, + font, + entry, + px_upscale, + font_scale_upscale, + shaper_shape_text_uncached_advanced + ) + shapes[id] = shape + } + + generate_shapes_draw_list(ctx, font, colour, entry, px_upscale, font_scale_upscale, position, downscale, shapes ) } - - generate_shapes_draw_list(ctx, font, colour, entry, px_upscale, font_scale_upscale, position, downscale, shapes ) } - -// draw_text_no_snap :: #force_inline proc( ctx : ^Context, font : Font_ID, px_size : f32, position, scale : Vec2, text_utf8 : string ) -// { -// profile(#procedure) -// assert( ctx != nil ) -// assert( font >= 0 && int(font) < len(ctx.entries) ) -// assert( len(text_utf8) > 0 ) - -// ctx.cursor_pos = {} - -// entry := ctx.entries[ font ] - -// colour := ctx.colour -// colour.a = 1.0 + ctx.alpha_sharpen - -// px_size_upscaled := px_size * ctx.px_scalar -// scale_downsample := scale * (1 / ctx.px_scalar) -// font_scale_upscale := parser_scale( entry.parser_info, px_size_upscaled ) - -// font_scale := parser_scale( entry.parser_info, -px_size ) -// shape := shaper_shape_text_cached( text_utf8, & ctx.shaper_ctx, & ctx.shape_cache, font, entry, px_size, font_scale, shaper_shape_text_latin ) -// ctx.cursor_pos = generate_shape_draw_list( & ctx.draw_list, shape, & ctx.atlas, & ctx.glyph_buffer, colour, entry, font_scale, position, scale, ctx.snap_width, ctx.snap_height ) -// } - -// Resolve the shape and track it to reduce iteration overhead -@(optimization_mode="favor_size") -draw_text_shape :: #force_inline proc( ctx : ^Context, font : Font_ID, px_size : f32, position, scale : Vec2, shape : Shaped_Text ) -{ - profile(#procedure) - assert( ctx != nil ) - assert( font >= 0 && int(font) < len(ctx.entries) ) - position := position - position.x = ceil(position.x * ctx.snap_width ) / ctx.snap_width - position.y = ceil(position.y * ctx.snap_height) / ctx.snap_height - - entry := ctx.entries[ font ] - - colour := ctx.colour - colour.a = 1.0 + ctx.alpha_sharpen - - font_scale := parser_scale( entry.parser_info, px_size ) - - px_upscale := px_size * ctx.px_scalar - downscale := scale * (1 / ctx.px_scalar) - font_scale_upscale := parser_scale( entry.parser_info, px_upscale ) - - ctx.cursor_pos = generate_shape_draw_list( & ctx.draw_list, shape, & ctx.atlas, & ctx.glyph_buffer, ctx.px_scalar, - colour, - entry, - px_upscale, - font_scale_upscale, - position, - downscale, - ctx.snap_width, - ctx.snap_height - ) -} - -// Resolve the shape and track it to reduce iteration overhead -// draw_text_shape_no_snap :: #force_inline proc( ctx : ^Context, font : Font_ID, px_size : f32, position, scale : Vec2, shape : Shaped_Text ) -// { -// profile(#procedure) -// assert( ctx != nil ) -// assert( font >= 0 && int(font) < len(ctx.entries) ) - -// colour := ctx.colour -// colour.a = 1.0 + ctx.alpha_sharpen - -// px_size_upscaled := px_size * ctx.px_scalar -// scale := scale * (1 / ctx.px_scalar) - -// entry := ctx.entries[ font ] -// font_scale := parser_scale( entry.parser_info, px_size ) -// ctx.cursor_pos = generate_shape_draw_list( & ctx.draw_list, shape, & ctx.atlas, & ctx.glyph_buffer, colour, entry, font_scale, position, scale, ctx.snap_width, ctx.snap_height ) -// } - get_draw_list :: #force_inline proc( ctx : ^Context, optimize_before_returning := true ) -> ^Draw_List { assert( ctx != nil ) if optimize_before_returning do optimize_draw_list( & ctx.draw_list, 0 ) @@ -662,10 +810,20 @@ flush_draw_list_layer :: #force_inline proc( ctx : ^Context ) { ctx.draw_layer.calls_offset = len(ctx.draw_list.calls) } -//#endregion("drawing") +//#endregion("draw_list generation") //#region("metrics") +// The metrics follow the convention for providing their values unscaled from ctx.px_scalar +// Where its assumed when utilizing the draw_list generators or shaping procedures that the shape will be affected by it so it must be handled. +// If px_scalar is 1.0 no effect is done and its just redundant ops. + +measure_shape_size :: #force_inline proc( ctx : ^Context, shape : Shaped_Text ) -> (measured : Vec2) { + measured = shape.size * (1 / ctx.px_scalar) + return +} + +// Don't use this if you already have the shape instead use measure_shape_size measure_text_size :: #force_inline proc( ctx : ^Context, font : Font_ID, px_size : f32, text_utf8 : string ) -> (measured : Vec2) { // profile(#procedure) @@ -676,11 +834,12 @@ measure_text_size :: #force_inline proc( ctx : ^Context, font : Font_ID, px_size font_scale := parser_scale( entry.parser_info, px_size ) + downscale := 1 / ctx.px_scalar px_size_upscaled := px_size * ctx.px_scalar font_scale_upscaled := parser_scale( entry.parser_info, px_size_upscaled ) - shaped := shaper_shape_text_cached( text_utf8, & ctx.shaper_ctx, & ctx.shape_cache, font, entry, px_size_upscaled, font_scale_upscaled, shaper_shape_text_uncached_advanced ) - return shaped.size + shaped := shaper_shape_text_cached( text_utf8, & ctx.shaper_ctx, & ctx.shape_cache, font, entry, px_size_upscaled, font_scale_upscaled, shaper_shape_text_uncached_advanced ) + return shaped.size * downscale } get_font_vertical_metrics :: #force_inline proc ( ctx : ^Context, font : Font_ID, px_size : f32 ) -> ( ascent, descent, line_gap : f32 ) @@ -689,12 +848,8 @@ get_font_vertical_metrics :: #force_inline proc ( ctx : ^Context, font : Font_ID assert( font >= 0 && int(font) < len(ctx.entries) ) entry := ctx.entries[ font ] - - font_scale := parser_scale( entry.parser_info, px_size ) - px_size_upscaled := px_size * ctx.px_scalar - downscale := 1 / px_size_upscaled - font_scale_upscaled := parser_scale( entry.parser_info, px_size_upscaled ) + font_scale := parser_scale( entry.parser_info, px_size ) ascent = font_scale * entry.ascent descent = font_scale * entry.descent @@ -720,7 +875,7 @@ shape_text_latin :: #force_inline proc( ctx : ^Context, font : Font_ID, px_size return shaper_shape_text_cached( text_utf8, & ctx.shaper_ctx, & ctx.shape_cache, font, entry, - px_size, + px_size_upscaled, font_scale_upscaled, shaper_shape_text_latin ) @@ -740,13 +895,14 @@ shape_text_advanced :: #force_inline proc( ctx : ^Context, font : Font_ID, px_si return shaper_shape_text_cached( text_utf8, & ctx.shaper_ctx, & ctx.shape_cache, font, entry, - px_size, + px_size_upscaled, font_scale_upscaled, shaper_shape_text_uncached_advanced ) } // User handled shaped text. Will not be cached +// @(disabled = true) shape_text_latin_uncached :: #force_inline proc( ctx : ^Context, font : Font_ID, text_utf8 : string, entry : ^Entry, shape : ^Shaped_Text ) { assert(false) @@ -754,6 +910,7 @@ shape_text_latin_uncached :: #force_inline proc( ctx : ^Context, font : Font_ID, } // User handled shaped text. Will not be cached +// @(disabled = true) shape_text_advanced_uncahed :: #force_inline proc( ctx : ^Context, font : Font_ID, text_utf8 : string, entry : ^Entry, shape : ^Shaped_Text ) { assert(false) @@ -761,31 +918,3 @@ shape_text_advanced_uncahed :: #force_inline proc( ctx : ^Context, font : Font_I } //#endregion("shaping") - -// Can be used with hot-reload -clear_atlas_region_caches :: proc(ctx : ^Context) -{ - lru_clear(& ctx.atlas.region_a.state) - lru_clear(& ctx.atlas.region_b.state) - lru_clear(& ctx.atlas.region_c.state) - lru_clear(& ctx.atlas.region_d.state) - - ctx.atlas.region_a.next_idx = 0 - ctx.atlas.region_b.next_idx = 0 - ctx.atlas.region_c.next_idx = 0 - ctx.atlas.region_d.next_idx = 0 -} - -// Can be used with hot-reload -clear_shape_cache :: proc (ctx : ^Context) -{ - lru_clear(& ctx.shape_cache.state) - for idx : i32 = 0; idx < cast(i32) cap(ctx.shape_cache.storage); idx += 1 { - stroage_entry := & ctx.shape_cache.storage[idx] - stroage_entry.end_cursor_pos = {} - stroage_entry.size = {} - clear(& stroage_entry.glyphs) - clear(& stroage_entry.positions) - } - ctx.shape_cache.next_cache_id = 0 -} diff --git a/code/sectr/app/screen.odin b/code/sectr/app/screen.odin index be8e211..0a76584 100644 --- a/code/sectr/app/screen.odin +++ b/code/sectr/app/screen.odin @@ -26,6 +26,9 @@ ui_screen_reload :: proc( screen_ui : ^UI_ScreenState ) { ui_screen_tick :: proc( screen_ui : ^UI_ScreenState ) { profile("Screenspace Imgui") + font_provider_set_px_scalar( app_config().text_size_screen_scalar ) + // screen_ui.zoom_scale = 1.0 + ui_graph_build( screen_ui ) ui_floating_manager( & screen_ui.floating ) ui_floating("Menu Bar", & screen_ui.menu_bar, ui_screen_menu_bar_builder) diff --git a/code/sectr/app/settings_menu.odin b/code/sectr/app/settings_menu.odin index ccd8d8b..4c3e9fa 100644 --- a/code/sectr/app/settings_menu.odin +++ b/code/sectr/app/settings_menu.odin @@ -496,7 +496,7 @@ ui_settings_menu_builder :: proc( captures : rawptr = nil ) -> ( should_raise : } } - Font_Size_Screen_Scalar: + Text_Size_Screen_Scalar: { ui_settings_entry_inputbox( & font_size_screen_scalar_input, false, "settings_menu.font_size_screen_scalar", str_intern("Font: Size Screen Scalar"), UI_TextInput_Policy { @@ -515,17 +515,46 @@ ui_settings_menu_builder :: proc( captures : rawptr = nil ) -> ( should_raise : value, success := parse_f32(to_string(array_to_slice(input_str))) if success { value = clamp(value, 0.001, 9999.0) - config.font_size_screen_scalar = value + config.text_size_screen_scalar = value } } else { clear( input_str ) - append( & input_str, to_runes(str_fmt("%v", config.font_size_screen_scalar))) + append( & input_str, to_runes(str_fmt("%v", config.text_size_screen_scalar))) } } - Font_Size_Canvas_Scalar: + Text_Snap_Glyph_Positions: + { + ui_settings_entry_inputbox( & font_size_canvas_scalar_input, false, "settings_menu.font_size_canvas_scalar", str_intern("Font: Size Canvas Scalar"), + UI_TextInput_Policy { + digits_only = true, + disallow_leading_zeros = false, + disallow_decimal = false, + digit_min = 0, + digit_max = 1, + max_length = 1, + } + ) + using font_size_canvas_scalar_input + + if was_active + { + value, success := parse_f32(to_string(array_to_slice(input_str))) + if success { + value = clamp(value, 0, 1) + config.text_snap_glyph_positions = cast(b32) i32(value) + } + } + else + { + clear( input_str ) + append( & input_str, to_runes(str_fmt("%v", config.text_size_canvas_scalar))) + } + } + + Text_Size_Canvas_Scalar: { ui_settings_entry_inputbox( & font_size_canvas_scalar_input, false, "settings_menu.font_size_canvas_scalar", str_intern("Font: Size Canvas Scalar"), UI_TextInput_Policy { @@ -544,19 +573,19 @@ ui_settings_menu_builder :: proc( captures : rawptr = nil ) -> ( should_raise : value, success := parse_f32(to_string(array_to_slice(input_str))) if success { value = clamp(value, 0.001, 9999.0) - config.font_size_canvas_scalar = value + config.text_size_canvas_scalar = value } } else { clear( input_str ) - append( & input_str, to_runes(str_fmt("%v", config.font_size_canvas_scalar))) + append( & input_str, to_runes(str_fmt("%v", config.text_size_canvas_scalar))) } } Text_Alpha_Sharpen: { - ui_settings_entry_inputbox( & font_size_canvas_scalar_input, false, "settings_menu.font_size_canvas_scalar", str_intern("Font: Size Canvas Scalar"), + ui_settings_entry_inputbox( & font_size_canvas_scalar_input, false, "settings_menu.text_alpha_sharpen", str_intern("Text: Alpha Sharpen"), UI_TextInput_Policy { digits_only = true, disallow_leading_zeros = false, @@ -572,14 +601,14 @@ ui_settings_menu_builder :: proc( captures : rawptr = nil ) -> ( should_raise : { value, success := parse_f32(to_string(array_to_slice(input_str))) if success { - value = clamp(value, 0.001, 9999.0) - config.font_size_canvas_scalar = value + value = clamp(value, 0.001, 10.0) + config.text_alpha_sharpen = value } } else { clear( input_str ) - append( & input_str, to_runes(str_fmt("%v", config.font_size_canvas_scalar))) + append( & input_str, to_runes(str_fmt("%v", config.text_size_canvas_scalar))) } } } diff --git a/code/sectr/app/state.odin b/code/sectr/app/state.odin index 514d005..4e5fddd 100644 --- a/code/sectr/app/state.odin +++ b/code/sectr/app/state.odin @@ -161,8 +161,10 @@ AppConfig :: struct { color_theme : AppColorTheme, - font_size_screen_scalar : f32, - font_size_canvas_scalar : f32, + text_snap_glyph_positions : b32, + text_size_screen_scalar : f32, + text_size_canvas_scalar : f32, + text_alpha_sharpen : f32, } AppWindow :: struct { diff --git a/code/sectr/engine/client_api.odin b/code/sectr/engine/client_api.odin index f114e49..ea4c00d 100644 --- a/code/sectr/engine/client_api.odin +++ b/code/sectr/engine/client_api.odin @@ -152,8 +152,10 @@ startup :: proc( prof : ^SpallProfiler, persistent_mem, frame_mem, transient_mem color_theme = App_Thm_Dusk - font_size_screen_scalar = 1.0 - font_size_canvas_scalar = 1.0 + text_snap_glyph_positions = true + text_size_screen_scalar = 2.0 + text_size_canvas_scalar = 2.0 + text_alpha_sharpen = 0.25 } Desired_OS_Scheduler_MS :: 1 @@ -354,7 +356,7 @@ startup :: proc( prof : ^SpallProfiler, persistent_mem, frame_mem, transient_mem // debug.path_lorem = str_fmt("C:/projects/SectrPrototype/examples/Lorem Ipsum (197).txt", allocator = persistent_slab_allocator()) // debug.path_lorem = str_fmt("C:/projects/SectrPrototype/examples/Lorem Ipsum (1022).txt", allocator = persistent_slab_allocator()) - debug.path_lorem = str_fmt("C:/projects/SectrPrototype/examples/sokol_gp.h", allocator = persistent_slab_allocator()) + // debug.path_lorem = str_fmt("C:/projects/SectrPrototype/examples/sokol_gp.h", allocator = persistent_slab_allocator()) // debug.path_lorem = str_fmt("C:/projects/SectrPrototype/examples/ve_fontcache.h", allocator = persistent_slab_allocator()) alloc_error : AllocatorError; success : bool diff --git a/code/sectr/engine/render.odin b/code/sectr/engine/render.odin index 708905c..4d42095 100644 --- a/code/sectr/engine/render.odin +++ b/code/sectr/engine/render.odin @@ -57,7 +57,7 @@ render_mode_2d_workspace :: proc( screen_extent : Vec2, cam : Camera, input : In screen_size := screen_extent * 2 // TODO(Ed): Eventually will be the viewport extents - ve.set_px_scalar( ve_ctx, app_config().font_size_canvas_scalar ) + font_provider_set_px_scalar( app_config().text_size_canvas_scalar ) ve.configure_snap( ve_ctx, u32(screen_size.x), u32(screen_size.y) ) // ve.configure_snap( ve_ctx, 0, 0 ) @@ -123,7 +123,7 @@ render_mode_screenspace :: proc( screen_extent : Extents2, screen_ui : ^UI_State screen_size := screen_extent * 2 screen_ratio := screen_size.x * ( 1.0 / screen_size.y ) - ve.set_px_scalar( ve_ctx, app_config().font_size_screen_scalar ) + font_provider_set_px_scalar( app_config().text_size_canvas_scalar ) ve.configure_snap( ve_ctx, u32(screen_size.x), u32(screen_size.y) ) render_screen_ui( screen_extent, screen_ui, ve_ctx, ve_render ) @@ -265,14 +265,6 @@ render_mode_screenspace :: proc( screen_extent : Extents2, screen_ui : ^UI_State } } - if true { - zoom_adjust_size := 16 * state.project.workspace.cam.zoom - over_sample := f32(state.config.font_size_canvas_scalar) - debug_text("font_size_canvas_scalar: %v", config.font_size_canvas_scalar) - ve_id, resolved_size := font_provider_resolve_draw_id( default_font, zoom_adjust_size * over_sample ) - debug_text("font_size resolved: %v px", resolved_size) - } - render_text_layer( screen_extent, ve_ctx, ve_render ) } @@ -663,6 +655,7 @@ render_ui_via_box_list :: proc( box_list : []UI_RenderBoxInfo, text_list : []UI_ if cam != nil { draw_text_string_pos_extent_zoomed( entry.text, font, entry.font_size, entry.position, cam_offset, screen_size, screen_size_norm, cam.zoom, entry.color ) + // draw_text_shape_pos_extent_zoomed( entry.shape, font, entry.font_size, entry.position, cam_offset, screen_size, screen_size_norm, cam.zoom, entry.color ) } else { draw_text_string_pos_extent( entry.text, font, entry.font_size, entry.position, entry.color ) @@ -987,10 +980,6 @@ draw_text_string_pos_extent_zoomed :: #force_inline proc( text : string, id : Fo zoom_adjust_size := size * zoom - // Over-sample font-size for any render under a camera - // over_sample : f32 = f32(config.font_size_canvas_scalar) - // zoom_adjust_size *= over_sample - pos_offset := (pos + cam_offset) render_pos := ws_view_to_render_pos(pos) normalized_pos := render_pos * screen_size_norm @@ -1007,11 +996,7 @@ draw_text_string_pos_extent_zoomed :: #force_inline proc( text : string, id : Fo text_scale.y = clamp( text_scale.y, 0, screen_size.y ) } - // Down-sample back - // text_scale /= over_sample - color_norm := normalize_rgba8(color) - // ve.set_px_scalar( & get_state().font_provider_ctx.ve_ctx, config.font_size_canvas_scalar ) ve.set_colour( & get_state().font_provider_ctx.ve_ctx, color_norm ) ve.draw_text( & get_state().font_provider_ctx.ve_ctx, ve_id, f32(resolved_size), normalized_pos, text_scale, text ) } diff --git a/code/sectr/engine/update.odin b/code/sectr/engine/update.odin index d2792d8..a512be7 100644 --- a/code/sectr/engine/update.odin +++ b/code/sectr/engine/update.odin @@ -302,19 +302,20 @@ update :: proc( delta_time : f64 ) -> b32 // TODO(Ed): We need input buffer so that we can consume input actions based on the UI with priority - font_provider_set_px_scalar( app_config().font_size_screen_scalar ) ui_screen_tick( & get_state().screen_ui ) //region WorkspaceImgui Tick if true { - font_provider_set_px_scalar( app_config().font_size_canvas_scalar ) + font_provider_set_px_scalar( app_config().text_size_canvas_scalar ) profile("Workspace Imgui") // Creates the root box node, set its as the first parent. ui_graph_build( & state.project.workspace.ui ) ui := ui_context + ui.zoom_scale = state.project.workspace.cam.zoom + frame_style_flags : UI_LayoutFlags = { .Fixed_Position_X, .Fixed_Position_Y, .Fixed_Width, .Fixed_Height, diff --git a/code/sectr/font/provider.odin b/code/sectr/font/provider.odin index 3f6fa0f..8364391 100644 --- a/code/sectr/font/provider.odin +++ b/code/sectr/font/provider.odin @@ -140,6 +140,12 @@ font_provider_resolve_draw_id :: #force_inline proc( id : FontID, size := Font_U return } +measure_text_shape :: #force_inline proc( shape : ShapedText ) -> Vec2 +{ + measured := ve.measure_shape_size( & get_state().font_provider_ctx.ve_ctx, shape ) + return measured +} + measure_text_size :: #force_inline proc( text : string, font : FontID, font_size := Font_Use_Default_Size, spacing : f32 ) -> Vec2 { ve_id, size := font_provider_resolve_draw_id( font, font_size ) diff --git a/code/sectr/ui/core/base.odin b/code/sectr/ui/core/base.odin index 43fb73a..9101bdc 100644 --- a/code/sectr/ui/core/base.odin +++ b/code/sectr/ui/core/base.odin @@ -123,6 +123,8 @@ UI_State :: struct { // build_arenas : [2]Arena, // build_arena : ^ Arena, + zoom_scale : f32, + built_box_count : i32, caches : [2] HMapChained( UI_Box ), @@ -285,15 +287,6 @@ ui_graph_build_end :: proc( ui : ^UI_State ) { if len(current.text) > 0 { profile("text shape") - // app_window := get_state().app_window - // screen_extent := app_window.extent - // screen_size := screen_extent * 2 - // screen_size_norm := 1 / screen_size - - font_size_screen_scalar := app_config().font_size_screen_scalar - - // over_sample : f32 = f32(get_state().config.font_size_canvas_scalar) - current.computed.text_shape = shape_text_cached( current.text, current.style.font, current.layout.font_size, 1.0 ) } ui_box_compute_layout( current ) diff --git a/code/sectr/ui/core/layout_compute.odin b/code/sectr/ui/core/layout_compute.odin index 2ccd1a6..c754400 100644 --- a/code/sectr/ui/core/layout_compute.odin +++ b/code/sectr/ui/core/layout_compute.odin @@ -73,7 +73,7 @@ ui_box_compute_layout :: proc( box : ^UI_Box, text_size : Vec2 if len(box.text) > 0 { - text_size = computed.text_shape.size + text_size = measure_text_shape( computed.text_shape ) // if layout.font_size == computed.text_size.y { // text_size = computed.text_size // }