finish force rename
This commit is contained in:
21
code/font/vefontcache/LICENSE.md
Normal file
21
code/font/vefontcache/LICENSE.md
Normal file
@ -0,0 +1,21 @@
|
||||
VEFontCache Odin Port
|
||||
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.
|
||||
|
||||
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,
|
||||
including without limitation the rights to use, copy, modify, merge, publish, distribute,
|
||||
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
|
||||
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
|
||||
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
243
code/font/vefontcache/LRU.odin
Normal file
243
code/font/vefontcache/LRU.odin
Normal file
@ -0,0 +1,243 @@
|
||||
package VEFontCache
|
||||
|
||||
/*
|
||||
The choice was made to keep the LRU cache implementation as close to the original as possible.
|
||||
*/
|
||||
|
||||
import "base:runtime"
|
||||
|
||||
PoolListIter :: i32
|
||||
PoolListValue :: u64
|
||||
|
||||
PoolListItem :: struct {
|
||||
prev : PoolListIter,
|
||||
next : PoolListIter,
|
||||
value : PoolListValue,
|
||||
}
|
||||
|
||||
PoolList :: struct {
|
||||
items : [dynamic]PoolListItem,
|
||||
free_list : [dynamic]PoolListIter,
|
||||
front : PoolListIter,
|
||||
back : PoolListIter,
|
||||
size : i32,
|
||||
capacity : i32,
|
||||
dbg_name : string,
|
||||
}
|
||||
|
||||
pool_list_init :: proc( pool : ^PoolList, capacity : i32, dbg_name : string = "" )
|
||||
{
|
||||
error : AllocatorError
|
||||
pool.items, error = make( [dynamic]PoolListItem, int(capacity) )
|
||||
assert( error == .None, "VEFontCache.pool_list_init : Failed to allocate items array")
|
||||
resize( & pool.items, capacity )
|
||||
|
||||
pool.free_list, error = make( [dynamic]PoolListIter, len = 0, cap = int(capacity) )
|
||||
assert( error == .None, "VEFontCache.pool_list_init : Failed to allocate free_list array")
|
||||
resize( & pool.free_list, capacity )
|
||||
|
||||
pool.capacity = capacity
|
||||
|
||||
pool.dbg_name = dbg_name
|
||||
using pool
|
||||
|
||||
for id in 0 ..< capacity {
|
||||
free_list[id] = i32(id)
|
||||
items[id] = {
|
||||
prev = -1,
|
||||
next = -1,
|
||||
}
|
||||
}
|
||||
|
||||
front = -1
|
||||
back = -1
|
||||
}
|
||||
|
||||
pool_list_free :: proc( pool : ^PoolList ) {
|
||||
// TODO(Ed): Implement
|
||||
}
|
||||
|
||||
pool_list_reload :: proc( pool : ^PoolList, allocator : Allocator ) {
|
||||
reload_array( & pool.items, allocator )
|
||||
reload_array( & pool.free_list, allocator )
|
||||
}
|
||||
|
||||
pool_list_push_front :: proc( pool : ^PoolList, value : PoolListValue )
|
||||
{
|
||||
using pool
|
||||
if size >= capacity do return
|
||||
|
||||
length := len(free_list)
|
||||
assert( length > 0 )
|
||||
assert( length == int(capacity - size) )
|
||||
|
||||
id := free_list[ len(free_list) - 1 ]
|
||||
if pool.dbg_name != "" {
|
||||
logf("pool_list: back %v", id)
|
||||
}
|
||||
pop( & free_list )
|
||||
items[ id ].prev = -1
|
||||
items[ id ].next = front
|
||||
items[ id ].value = value
|
||||
if pool.dbg_name != "" {
|
||||
logf("pool_list: pushed %v into id %v", value, id)
|
||||
}
|
||||
|
||||
if front != -1 do items[ front ].prev = id
|
||||
if back == -1 do back = id
|
||||
front = id
|
||||
size += 1
|
||||
}
|
||||
|
||||
pool_list_erase :: proc( pool : ^PoolList, iter : PoolListIter )
|
||||
{
|
||||
using pool
|
||||
if size <= 0 do return
|
||||
assert( iter >= 0 && iter < i32(capacity) )
|
||||
assert( len(free_list) == int(capacity - size) )
|
||||
|
||||
iter_node := & items[ iter ]
|
||||
prev := iter_node.prev
|
||||
next := iter_node.next
|
||||
|
||||
if iter_node.prev != -1 do items[ prev ].next = iter_node.next
|
||||
if iter_node.next != -1 do items[ next ].prev = iter_node.prev
|
||||
|
||||
if front == iter do front = iter_node.next
|
||||
if back == iter do back = iter_node.prev
|
||||
|
||||
iter_node.prev = -1
|
||||
iter_node.next = -1
|
||||
iter_node.value = 0
|
||||
append( & free_list, iter )
|
||||
|
||||
size -= 1
|
||||
if size == 0 {
|
||||
back = -1
|
||||
front = -1
|
||||
}
|
||||
}
|
||||
|
||||
pool_list_move_to_front :: #force_inline proc( pool : ^PoolList, iter : PoolListIter )
|
||||
{
|
||||
using pool
|
||||
|
||||
if front == iter do return
|
||||
|
||||
item := & items[iter]
|
||||
if item.prev != -1 do items[ item.prev ].next = item.next
|
||||
if item.next != -1 do items[ item.next ].prev = item.prev
|
||||
if back == iter do back = item.prev
|
||||
|
||||
item.prev = -1
|
||||
item.next = front
|
||||
items[ front ].prev = iter
|
||||
front = iter
|
||||
}
|
||||
|
||||
pool_list_peek_back :: #force_inline proc ( pool : ^PoolList ) -> PoolListValue {
|
||||
assert( pool.back != - 1 )
|
||||
value := pool.items[ pool.back ].value
|
||||
return value
|
||||
}
|
||||
|
||||
pool_list_pop_back :: #force_inline proc( pool : ^PoolList ) -> PoolListValue {
|
||||
if pool.size <= 0 do return 0
|
||||
assert( pool.back != -1 )
|
||||
|
||||
value := pool.items[ pool.back ].value
|
||||
pool_list_erase( pool, pool.back )
|
||||
return value
|
||||
}
|
||||
|
||||
LRU_Link :: struct {
|
||||
pad_top : u64,
|
||||
value : i32,
|
||||
ptr : PoolListIter,
|
||||
pad_bottom : u64,
|
||||
}
|
||||
|
||||
LRU_Cache :: struct {
|
||||
capacity : i32,
|
||||
num : i32,
|
||||
table : map[u64]LRU_Link,
|
||||
key_queue : PoolList,
|
||||
}
|
||||
|
||||
LRU_init :: proc( cache : ^LRU_Cache, capacity : i32, dbg_name : string = "" ) {
|
||||
error : AllocatorError
|
||||
cache.capacity = capacity
|
||||
cache.table, error = make( map[u64]LRU_Link, uint(capacity) )
|
||||
assert( error == .None, "VEFontCache.LRU_init : Failed to allocate cache's table")
|
||||
|
||||
pool_list_init( & cache.key_queue, capacity, dbg_name = dbg_name )
|
||||
}
|
||||
|
||||
LRU_free :: proc( cache : ^LRU_Cache ) {
|
||||
// TODO(Ed): Implement
|
||||
}
|
||||
|
||||
LRU_reload :: #force_inline proc( cache : ^LRU_Cache, allocator : Allocator ) {
|
||||
reload_map( & cache.table, allocator )
|
||||
pool_list_reload( & cache.key_queue, allocator )
|
||||
}
|
||||
|
||||
LRU_find :: #force_inline proc "contextless" ( cache : ^LRU_Cache, key : u64, must_find := false ) -> (LRU_Link, bool) {
|
||||
link, success := cache.table[key]
|
||||
return link, success
|
||||
}
|
||||
|
||||
LRU_get :: #force_inline proc( cache: ^LRU_Cache, key : u64 ) -> i32 {
|
||||
if link, ok := &cache.table[ key ]; ok {
|
||||
pool_list_move_to_front(&cache.key_queue, link.ptr)
|
||||
return link.value
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
LRU_get_next_evicted :: #force_inline proc ( cache : ^LRU_Cache ) -> u64 {
|
||||
if cache.key_queue.size >= cache.capacity {
|
||||
evict := pool_list_peek_back( & cache.key_queue )
|
||||
return evict
|
||||
}
|
||||
return 0xFFFFFFFFFFFFFFFF
|
||||
}
|
||||
|
||||
LRU_peek :: #force_inline proc ( cache : ^LRU_Cache, key : u64, must_find := false ) -> i32 {
|
||||
iter, success := LRU_find( cache, key, must_find )
|
||||
if success == false {
|
||||
return -1
|
||||
}
|
||||
return iter.value
|
||||
}
|
||||
|
||||
LRU_put :: #force_inline proc( cache : ^LRU_Cache, key : u64, value : i32 ) -> u64
|
||||
{
|
||||
if link, ok := & cache.table[ key ]; ok {
|
||||
pool_list_move_to_front( & cache.key_queue, link.ptr )
|
||||
link.value = value
|
||||
return key
|
||||
}
|
||||
|
||||
evict := key
|
||||
if cache.key_queue.size >= cache.capacity {
|
||||
evict = pool_list_pop_back(&cache.key_queue)
|
||||
delete_key(&cache.table, evict)
|
||||
cache.num -= 1
|
||||
}
|
||||
|
||||
pool_list_push_front(&cache.key_queue, key)
|
||||
cache.table[key] = LRU_Link{
|
||||
value = value,
|
||||
ptr = cache.key_queue.front,
|
||||
}
|
||||
cache.num += 1
|
||||
return evict
|
||||
}
|
||||
|
||||
LRU_refresh :: proc( cache : ^LRU_Cache, key : u64 ) {
|
||||
link, success := LRU_find( cache, key )
|
||||
pool_list_erase( & cache.key_queue, link.ptr )
|
||||
pool_list_push_front( & cache.key_queue, key )
|
||||
link.ptr = cache.key_queue.front
|
||||
}
|
61
code/font/vefontcache/Readme.md
Normal file
61
code/font/vefontcache/Readme.md
Normal file
@ -0,0 +1,61 @@
|
||||
# VE Font Cache : Odin Port
|
||||
|
||||
This is a port of the [VEFontCache](https://github.com/hypernewbie/VEFontCache) library.
|
||||
|
||||
Its original purpose was for use in game engines, however its rendeirng quality and performance is more than adequate for many other applications.
|
||||
|
||||
See: [docs/Readme.md](docs/Readme.md) for the library's interface
|
||||
|
||||
## Changes from orignal
|
||||
|
||||
* Font Parser & Glyph shaper are abstracted to their own interface
|
||||
* Font face parser info encapsulated in parser_info struct.
|
||||
* ve_fontcache_loadfile not ported (ust use core:os or os2, then call load_font)
|
||||
* Macro defines have been coverted (mostly) to runtime parameters
|
||||
* Support for hot_reloading
|
||||
|
||||
## TODOs
|
||||
|
||||
### Thirdparty support:
|
||||
|
||||
* Setup freetype, harfbuzz, depedency management within the library
|
||||
|
||||
### Documentation:
|
||||
|
||||
* Pureref outline of draw_text exectuion
|
||||
* Markdown general documentation
|
||||
|
||||
### Content:
|
||||
|
||||
* Port over the original demo utilizing sokol libraries instead
|
||||
* Provide a sokol_gfx backend package
|
||||
|
||||
### Additional Features:
|
||||
|
||||
* Support for freetype
|
||||
* Support for harfbuzz
|
||||
* Ability to set a draw transform, viewport and projection
|
||||
* By default the library's position is in unsigned normalized render space
|
||||
* Could implement a similar design to sokol_gp's interface
|
||||
* Allow curve_quality to be set on a per-font basis
|
||||
|
||||
### Optimization:
|
||||
|
||||
* 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
|
||||
* draw_text can provide in the context a job list per thread for the user to thenk hookup to their own threading solution to handle.
|
||||
* Context would need to be segregated into staged data structures for each thread to utilize
|
||||
* Each should have their own?
|
||||
* draw_list
|
||||
* draw_layer
|
||||
* atlas.next_idx
|
||||
* glyph_draw_buffer
|
||||
* shape_cache
|
||||
* This would need to converge to the singlar draw_list on a per layer basis (then user reqeusts a draw_list layer there could a yield to wait for the jobs to finish); if the interface expects the user to issue the commands single-threaded unless, we just assume 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)
|
129
code/font/vefontcache/atlas.odin
Normal file
129
code/font/vefontcache/atlas.odin
Normal file
@ -0,0 +1,129 @@
|
||||
package VEFontCache
|
||||
|
||||
AtlasRegionKind :: enum u8 {
|
||||
None = 0x00,
|
||||
A = 0x41,
|
||||
B = 0x42,
|
||||
C = 0x43,
|
||||
D = 0x44,
|
||||
E = 0x45,
|
||||
Ignore = 0xFF, // ve_fontcache_cache_glyph_to_atlas uses a -1 value in clear draw call
|
||||
}
|
||||
|
||||
AtlasRegion :: struct {
|
||||
state : LRU_Cache,
|
||||
|
||||
width : i32,
|
||||
height : i32,
|
||||
|
||||
size : Vec2i,
|
||||
capacity : Vec2i,
|
||||
offset : Vec2i,
|
||||
|
||||
next_idx : i32,
|
||||
}
|
||||
|
||||
Atlas :: struct {
|
||||
width : i32,
|
||||
height : i32,
|
||||
|
||||
glyph_padding : i32,
|
||||
|
||||
region_a : AtlasRegion,
|
||||
region_b : AtlasRegion,
|
||||
region_c : AtlasRegion,
|
||||
region_d : AtlasRegion,
|
||||
}
|
||||
|
||||
atlas_bbox :: proc( atlas : ^Atlas, region : AtlasRegionKind, local_idx : i32 ) -> (position, size: Vec2)
|
||||
{
|
||||
switch region
|
||||
{
|
||||
case .A:
|
||||
size.x = f32(atlas.region_a.width)
|
||||
size.y = f32(atlas.region_a.height)
|
||||
|
||||
position.x = cast(f32) (( local_idx % atlas.region_a.capacity.x ) * atlas.region_a.width)
|
||||
position.y = cast(f32) (( local_idx / atlas.region_a.capacity.x ) * atlas.region_a.height)
|
||||
|
||||
position.x += f32(atlas.region_a.offset.x)
|
||||
position.y += f32(atlas.region_a.offset.y)
|
||||
|
||||
case .B:
|
||||
size.x = f32(atlas.region_b.width)
|
||||
size.y = f32(atlas.region_b.height)
|
||||
|
||||
position.x = cast(f32) (( local_idx % atlas.region_b.capacity.x ) * atlas.region_b.width)
|
||||
position.y = cast(f32) (( local_idx / atlas.region_b.capacity.x ) * atlas.region_b.height)
|
||||
|
||||
position.x += f32(atlas.region_b.offset.x)
|
||||
position.y += f32(atlas.region_b.offset.y)
|
||||
|
||||
case .C:
|
||||
size.x = f32(atlas.region_c.width)
|
||||
size.y = f32(atlas.region_c.height)
|
||||
|
||||
position.x = cast(f32) (( local_idx % atlas.region_c.capacity.x ) * atlas.region_c.width)
|
||||
position.y = cast(f32) (( local_idx / atlas.region_c.capacity.x ) * atlas.region_c.height)
|
||||
|
||||
position.x += f32(atlas.region_c.offset.x)
|
||||
position.y += f32(atlas.region_c.offset.y)
|
||||
|
||||
case .D:
|
||||
size.x = f32(atlas.region_d.width)
|
||||
size.y = f32(atlas.region_d.height)
|
||||
|
||||
position.x = cast(f32) (( local_idx % atlas.region_d.capacity.x ) * atlas.region_d.width)
|
||||
position.y = cast(f32) (( local_idx / atlas.region_d.capacity.x ) * atlas.region_d.height)
|
||||
|
||||
position.x += f32(atlas.region_d.offset.x)
|
||||
position.y += f32(atlas.region_d.offset.y)
|
||||
|
||||
case .Ignore: fallthrough
|
||||
case .None: fallthrough
|
||||
case .E:
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
decide_codepoint_region :: proc(ctx : ^Context, entry : ^Entry, glyph_index : Glyph
|
||||
) -> (region_kind : AtlasRegionKind, region : ^AtlasRegion, over_sample : Vec2)
|
||||
{
|
||||
if parser_is_glyph_empty(&entry.parser_info, glyph_index) {
|
||||
return .None, nil, {}
|
||||
}
|
||||
|
||||
bounds_0, bounds_1 := parser_get_glyph_box(&entry.parser_info, glyph_index)
|
||||
bounds_width := f32(bounds_1.x - bounds_0.x)
|
||||
bounds_height := f32(bounds_1.y - bounds_0.y)
|
||||
|
||||
atlas := & ctx.atlas
|
||||
glyph_buffer := & ctx.glyph_buffer
|
||||
glyph_padding := f32( atlas.glyph_padding ) * 2
|
||||
|
||||
bounds_width_scaled := i32(bounds_width * entry.size_scale + glyph_padding)
|
||||
bounds_height_scaled := i32(bounds_height * entry.size_scale + glyph_padding)
|
||||
|
||||
// Use a lookup table for faster region selection
|
||||
region_lookup := [4]struct { kind: AtlasRegionKind, region: ^AtlasRegion } {
|
||||
{ .A, & atlas.region_a },
|
||||
{ .B, & atlas.region_b },
|
||||
{ .C, & atlas.region_c },
|
||||
{ .D, & atlas.region_d },
|
||||
}
|
||||
|
||||
for region in region_lookup do if bounds_width_scaled <= region.region.width && bounds_height_scaled <= region.region.height {
|
||||
return region.kind, region.region, glyph_buffer.over_sample
|
||||
}
|
||||
|
||||
if bounds_width_scaled <= glyph_buffer.width \
|
||||
&& bounds_height_scaled <= glyph_buffer.height {
|
||||
over_sample = \
|
||||
bounds_width_scaled <= glyph_buffer.width / 2 &&
|
||||
bounds_height_scaled <= glyph_buffer.height / 2 ? \
|
||||
{2.0, 2.0} \
|
||||
: {1.0, 1.0}
|
||||
return .E, nil, over_sample
|
||||
}
|
||||
return .None, nil, {}
|
||||
}
|
77
code/font/vefontcache/docs/Readme.md
Normal file
77
code/font/vefontcache/docs/Readme.md
Normal file
@ -0,0 +1,77 @@
|
||||
# Interface
|
||||
|
||||
Notes
|
||||
---
|
||||
|
||||
Freetype implementation supports specifying a FT_Memory handle which is a pointer to a FT_MemoryRect. This can be used to define an allocator for the parser. Currently this library does not wrap this interface (yet). If using freetype its recommend to update `parser_init` with the necessary changes to wrap the context's backing allocator for freetype to utilize.
|
||||
|
||||
```c
|
||||
struct FT_MemoryRec_
|
||||
{
|
||||
void* user;
|
||||
FT_Alloc_Func alloc;
|
||||
FT_Free_Func free;
|
||||
FT_Realloc_Func realloc;
|
||||
};
|
||||
```
|
||||
|
||||
This library (seems) to perform best if the text commands are fed in 'whitespace aware chunks', where instead of feeding it entire blobs of text, the user identfies the "words" in the text and feeding the visible and whitespce chunks derived from this to draw_text as separate calls. This improves the caching of the text shapes. The downside is there has to be a time where the text is parsed into tokens beforehand so that the this iteration does not have to occur continously.
|
||||
|
||||
### startup
|
||||
|
||||
Initializes a provided context.
|
||||
|
||||
There are a large amount of parameters to tune the library instance to the user's preference. By default, keep in mind the library defaults to utilize stb_truetype as the font parser and harfbuzz (soon...) for the shaper.
|
||||
|
||||
Much of the data structures within the context struct are not fixed-capacity allocations so make sure that the backing allocator utilized can handle it.
|
||||
|
||||
### hot_reload
|
||||
|
||||
The library supports being used in a dynamically loaded module. If this occurs simply make sure to call this procedure with a reference to the backing allocator provided during startup as all dynamic containers tend to lose a proper reference to the allocator's procedure.
|
||||
|
||||
### shutdown
|
||||
|
||||
Release resources from the context.
|
||||
|
||||
### configure_snap
|
||||
|
||||
You'll find this used immediately in draw_text it acts as a way to snap the position of the text to the nearest pixel for the width and height specified.
|
||||
|
||||
If snapping is not desired, set the snap_width and height before calling draw_text to 0.
|
||||
|
||||
## get_cursor_pos
|
||||
|
||||
Will provide the current cursor_pos for the resulting text drawn.
|
||||
|
||||
## set_color
|
||||
|
||||
Sets the color to utilize on `DrawCall`s for FrameBuffer.Target or .Target_Uncached passes
|
||||
|
||||
### get_draw_list
|
||||
|
||||
Get the enqueded draw_list (vertices, indices, and draw call arrays) in its entirety.
|
||||
By default, if get_draw_list is called, it will first call `optimize_draw_list` to optimize the draw list's calls for the user. If this is undesired, make sure to pass `optimize_before_returning = false` in the arguments.
|
||||
|
||||
### get_draw_list_layer
|
||||
|
||||
Get the enqueued draw_list for the current "layer".
|
||||
A layer is considered the slice of the drawlist's content from the last call to `flush_draw_list_layer` onward.
|
||||
By default, if get_draw_list_layer is called, it will first call `optimize_draw_list` for the user to optimize the slice (exlusively) of the draw list's draw calls. If this is undesired, make sure to pass `optimize_before_returning = false` in the arguments.
|
||||
|
||||
The draw layer offsets are cleared with `flush_draw_list`
|
||||
|
||||
### flush_draw_list
|
||||
|
||||
Will clear the draw list and draw layer offsets.
|
||||
|
||||
### flush_draw_list_layer
|
||||
|
||||
Will update the draw list layer with the latest offset based on the current lenght of the draw list vertices, indices, and calls arrays.
|
||||
|
||||
### measure_text_size
|
||||
|
||||
Provides a Vec2 the width and height occupied by the provided text string. The y is measured to be the the largest glyph box bounds height of the text. The width is derived from the `end_cursor_pos` field from a `ShapedText` entry.
|
||||
|
||||
## get_font_vertical_metrics
|
||||
|
||||
A wrapper for `parser_get_font_vertical_metrics`. Will provide the ascent, descent, and line_gap for a font entry.
|
BIN
code/font/vefontcache/docs/draw_text_codepaths.pur
Normal file
BIN
code/font/vefontcache/docs/draw_text_codepaths.pur
Normal file
Binary file not shown.
17
code/font/vefontcache/docs/original/LICENSE.md
Normal file
17
code/font/vefontcache/docs/original/LICENSE.md
Normal file
@ -0,0 +1,17 @@
|
||||
Vertex Engine GPU Font Cache
|
||||
Copyright 2020 Xi Chen
|
||||
|
||||
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,
|
||||
including without limitation the rights to use, copy, modify, merge, publish, distribute,
|
||||
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
|
||||
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
|
||||
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
114
code/font/vefontcache/docs/original/README.md
Normal file
114
code/font/vefontcache/docs/original/README.md
Normal file
@ -0,0 +1,114 @@
|
||||
# Notice
|
||||
|
||||
This is the original readme fo the C++ implementation by Xi Chen.
|
||||
|
||||
# VE Font Cache is a single header-only GPU font rendering library designed for game engines.
|
||||
|
||||
It aims to:
|
||||
* Be fast and simple to integrate.
|
||||
* Take advantage of modern GPU power.
|
||||
* Be backend agnostic and easy to port to any API such as Vulkan, DirectX, OpenGL.
|
||||
* Load TTF & OTF file formats directly.
|
||||
* Use only runtime cache with no offline calculation.
|
||||
* Render glyphs at reasonable quality at a wide range of font sizes.
|
||||
* Support a good amount of internationalisation. そうですね!
|
||||
* Support cached text shaping with HarfBuzz with simple Latin-style fallback.
|
||||
* Load and unload fonts at any time.
|
||||
|
||||
# How it works
|
||||
|
||||
Glyphs are GPU rasterised with 16x supersampling. This method is a simplification of "Easy Scalable Text Rendering on the GPU",
|
||||
by Evan Wallace, making use of XOR blending. Bézier curves are handled via brute force triangle tessellation; even 6 triangles per
|
||||
curve only generates < 300 triangles, which is nothing for modern GPUs! This avoids complex frag shader for reasonable quality.
|
||||
|
||||

|
||||
|
||||
Texture atlas caching uses naïve grid placement; this wastes a lot of space but ensures interchangeable cache slots allowing for
|
||||
straight up LRU ( Least Recently Used ) caching scheme to be employed.
|
||||
|
||||
The font atlas is a single 4k x 2k R8 texture divided into 4 regions:
|
||||
|
||||
```
|
||||
2k
|
||||
--------------------
|
||||
| | |
|
||||
| A | |
|
||||
| | | 2
|
||||
|---------| C | k
|
||||
| | |
|
||||
1k | B | |
|
||||
| | |
|
||||
--------------------
|
||||
| |
|
||||
| |
|
||||
| | 2
|
||||
| D | k
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
--------------------
|
||||
|
||||
Region A = 32x32 caches, 1024 glyphs
|
||||
Region B = 32x64 caches, 512 glyphs
|
||||
Region C = 64x64 caches, 512 glyphs
|
||||
Region D = 128x128 caches, 256 glyphs
|
||||
```
|
||||
|
||||
Region A is designed for small glyphs, Region B is for tall glyphs, Region C is for large glyphs, and Region D for huge glyphs.
|
||||
Glyphs are first rendered to an intermediate 2k x 512px R8 texture. This allows for minimum 4 Region D glyphs supersampled at
|
||||
4 x 4 = 16x supersampling, and 8 Region C glyphs similarly. A simple 16-tap box downsample shader is then used to blit from this
|
||||
intermediate texture to the final atlas location.
|
||||
|
||||
The atlas texture looks something like this:
|
||||

|
||||
|
||||
# Usage
|
||||
|
||||
Pseudo-code demonstrating simple usage:
|
||||
```cpp
|
||||
#define VE_FONTCACHE_IMPL
|
||||
#include "../ve_fontcache.h"
|
||||
|
||||
static std::vector< uint8_t > buffer;
|
||||
ve_fontcache_init( &cache );
|
||||
ve_fontcache_configure_snap( &cache, width, height );
|
||||
print_font = ve_fontcache_loadfile( &cache, "fonts/NotoSansJP-Light.otf", buffer, 19.0f );
|
||||
ve_fontcache_draw_text( &cache, print_font, u8"hello world", 0, 0, 1.0f / width, 1.0f / height );
|
||||
```
|
||||
|
||||
These header files need to be copied to your project:
|
||||
```
|
||||
ve_fontcache.h
|
||||
utf8.h
|
||||
stb_truetype.h
|
||||
```
|
||||
|
||||
Except HarfBuzz, that's all the required dependencies. That said it's strongly recommended
|
||||
to use HarfBuzz ( TODO: HarfBuzz not supported yet, coming soon!! ) over the default utf8.h latin
|
||||
fallback text shaper.
|
||||
|
||||
## Integration with rendering backend
|
||||
|
||||
VEFontCache is largely backend agnostic. Currently the demo project uses OpenGL 3.3 for Windows.
|
||||
That said it's designed to be integrated with VE, a Vulkan engine.
|
||||
Please read the "How to plug into rendering API" section in ve_fontcache.h for more documentation
|
||||
on how to implement your own backend to plumb this directly into your engine!
|
||||
|
||||
# Screenshots
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
# Similar projects and links
|
||||
|
||||
Here are links to some awesome similar and related projects:
|
||||
* fontstash - https://github.com/memononen/fontstash
|
||||
* stb_truetype ( has font rasterisation itself ) - https://github.com/nothings/stb/blob/master/stb_truetype.h
|
||||
* slug - http://sluglibrary.com/
|
||||
* pathfinder - https://github.com/pcwalton/pathfinder
|
||||
* https://medium.com/@evanwallace/easy-scalable-text-rendering-on-the-gpu-c3f4d782c5ac
|
636
code/font/vefontcache/draw.odin
Normal file
636
code/font/vefontcache/draw.odin
Normal file
@ -0,0 +1,636 @@
|
||||
package VEFontCache
|
||||
|
||||
Vertex :: struct {
|
||||
pos : Vec2,
|
||||
u, v : f32,
|
||||
}
|
||||
|
||||
DrawCall :: struct {
|
||||
pass : FrameBufferPass,
|
||||
start_index : u32,
|
||||
end_index : u32,
|
||||
clear_before_draw : b32,
|
||||
region : AtlasRegionKind,
|
||||
colour : Colour,
|
||||
}
|
||||
|
||||
DrawCall_Default :: DrawCall {
|
||||
pass = .None,
|
||||
start_index = 0,
|
||||
end_index = 0,
|
||||
clear_before_draw = false,
|
||||
region = .A,
|
||||
colour = { 1.0, 1.0, 1.0, 1.0 }
|
||||
}
|
||||
|
||||
DrawList :: struct {
|
||||
vertices : [dynamic]Vertex,
|
||||
indices : [dynamic]u32,
|
||||
calls : [dynamic]DrawCall,
|
||||
}
|
||||
|
||||
FrameBufferPass :: enum u32 {
|
||||
None = 0,
|
||||
Glyph = 1,
|
||||
Atlas = 2,
|
||||
Target = 3,
|
||||
Target_Uncached = 4,
|
||||
}
|
||||
|
||||
GlyphDrawBuffer :: struct {
|
||||
over_sample : Vec2,
|
||||
batch : i32,
|
||||
width : i32,
|
||||
height : i32,
|
||||
draw_padding : i32,
|
||||
|
||||
batch_x : i32,
|
||||
clear_draw_list : DrawList,
|
||||
draw_list : DrawList,
|
||||
}
|
||||
|
||||
blit_quad :: proc( draw_list : ^DrawList, p0 : Vec2 = {0, 0}, p1 : Vec2 = {1, 1}, uv0 : Vec2 = {0, 0}, uv1 : Vec2 = {1, 1} )
|
||||
{
|
||||
// profile(#procedure)
|
||||
// logf("Blitting: xy0: %0.2f, %0.2f xy1: %0.2f, %0.2f uv0: %0.2f, %0.2f uv1: %0.2f, %0.2f",
|
||||
// p0.x, p0.y, p1.x, p1.y, uv0.x, uv0.y, uv1.x, uv1.y);
|
||||
v_offset := cast(u32) len(draw_list.vertices)
|
||||
|
||||
quadv : [4]Vertex = {
|
||||
{
|
||||
{p0.x, p0.y},
|
||||
uv0.x, uv0.y
|
||||
},
|
||||
{
|
||||
{p0.x, p1.y},
|
||||
uv0.x, uv1.y
|
||||
},
|
||||
{
|
||||
{p1.x, p0.y},
|
||||
uv1.x, uv0.y
|
||||
},
|
||||
{
|
||||
{p1.x, p1.y},
|
||||
uv1.x, uv1.y
|
||||
}
|
||||
}
|
||||
append( & draw_list.vertices, ..quadv[:] )
|
||||
|
||||
quad_indices : []u32 = {
|
||||
0 + v_offset, 1 + v_offset, 2 + v_offset,
|
||||
2 + v_offset, 1 + v_offset, 3 + v_offset
|
||||
}
|
||||
append( & draw_list.indices, ..quad_indices[:] )
|
||||
return
|
||||
}
|
||||
|
||||
cache_glyph :: proc(ctx : ^Context, font : FontID, glyph_index : Glyph, entry : ^Entry, bounds_0, bounds_1 : Vec2, scale, translate : Vec2) -> b32
|
||||
{
|
||||
// profile(#procedure)
|
||||
if glyph_index == Glyph(0) {
|
||||
return false
|
||||
}
|
||||
|
||||
shape, error := parser_get_glyph_shape(&entry.parser_info, glyph_index)
|
||||
assert(error == .None)
|
||||
if len(shape) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
outside := Vec2{bounds_0.x - 21, bounds_0.y - 33}
|
||||
|
||||
draw := DrawCall_Default
|
||||
draw.pass = FrameBufferPass.Glyph
|
||||
draw.start_index = u32(len(ctx.draw_list.indices))
|
||||
|
||||
path := &ctx.temp_path
|
||||
clear(path)
|
||||
|
||||
for edge in shape do #partial switch edge.type
|
||||
{
|
||||
case .Move:
|
||||
if len(path) > 0 {
|
||||
draw_filled_path(&ctx.draw_list, outside, path[:], scale, translate, ctx.debug_print_verbose)
|
||||
clear(path)
|
||||
}
|
||||
fallthrough
|
||||
|
||||
case .Line:
|
||||
append( path, Vertex { pos = Vec2 { f32(edge.x), f32(edge.y)} } )
|
||||
|
||||
case .Curve:
|
||||
assert(len(path) > 0)
|
||||
p0 := path[ len(path) - 1].pos
|
||||
p1 := Vec2{ f32(edge.contour_x0), f32(edge.contour_y0) }
|
||||
p2 := Vec2{ f32(edge.x), f32(edge.y) }
|
||||
|
||||
step := 1.0 / entry.curve_quality
|
||||
for index : f32 = 1; index <= entry.curve_quality; index += 1 {
|
||||
alpha := index * step
|
||||
append( path, Vertex { pos = eval_point_on_bezier3(p0, p1, p2, alpha) } )
|
||||
}
|
||||
|
||||
case .Cubic:
|
||||
assert( len(path) > 0)
|
||||
p0 := path[ len(path) - 1].pos
|
||||
p1 := Vec2{ f32(edge.contour_x0), f32(edge.contour_y0) }
|
||||
p2 := Vec2{ f32(edge.contour_x1), f32(edge.contour_y1) }
|
||||
p3 := Vec2{ f32(edge.x), f32(edge.y) }
|
||||
|
||||
step := 1.0 / entry.curve_quality
|
||||
for index : f32 = 1; index <= entry.curve_quality; index += 1 {
|
||||
alpha := index * step
|
||||
append( path, Vertex { pos = eval_point_on_bezier4(p0, p1, p2, p3, alpha) } )
|
||||
}
|
||||
}
|
||||
|
||||
if len(path) > 0 {
|
||||
draw_filled_path(&ctx.draw_list, outside, path[:], scale, translate, ctx.debug_print_verbose)
|
||||
}
|
||||
|
||||
draw.end_index = u32(len(ctx.draw_list.indices))
|
||||
if draw.end_index > draw.start_index {
|
||||
append(&ctx.draw_list.calls, draw)
|
||||
}
|
||||
|
||||
parser_free_shape(&entry.parser_info, shape)
|
||||
return true
|
||||
}
|
||||
|
||||
/*
|
||||
Called by:
|
||||
* can_batch_glyph : If it determines that the glyph was not detected and we haven't reached capacity in the atlas
|
||||
* draw_text_shape : Glyph
|
||||
*/
|
||||
cache_glyph_to_atlas :: proc( ctx : ^Context,
|
||||
font : FontID,
|
||||
glyph_index : Glyph,
|
||||
lru_code : u64,
|
||||
atlas_index : i32,
|
||||
entry : ^Entry,
|
||||
region_kind : AtlasRegionKind,
|
||||
region : ^AtlasRegion,
|
||||
over_sample : Vec2 )
|
||||
{
|
||||
// profile(#procedure)
|
||||
|
||||
// Get hb_font text metrics. These are unscaled!
|
||||
bounds_0, bounds_1 := parser_get_glyph_box( & entry.parser_info, glyph_index )
|
||||
bounds_size := Vec2 {
|
||||
f32(bounds_1.x - bounds_0.x),
|
||||
f32(bounds_1.y - bounds_0.y)
|
||||
}
|
||||
|
||||
// E region is special case and not cached to atlas.
|
||||
if region_kind == .None || region_kind == .E do return
|
||||
|
||||
// Grab an atlas LRU cache slot.
|
||||
atlas_index := atlas_index
|
||||
if atlas_index == -1
|
||||
{
|
||||
if region.next_idx < region.state.capacity
|
||||
{
|
||||
evicted := LRU_put( & region.state, lru_code, i32(region.next_idx) )
|
||||
atlas_index = i32(region.next_idx)
|
||||
region.next_idx += 1
|
||||
assert( evicted == lru_code )
|
||||
}
|
||||
else
|
||||
{
|
||||
next_evict_codepoint := LRU_get_next_evicted( & region.state )
|
||||
assert( next_evict_codepoint != 0xFFFFFFFFFFFFFFFF )
|
||||
|
||||
atlas_index = LRU_peek( & region.state, next_evict_codepoint, must_find = true )
|
||||
assert( atlas_index != -1 )
|
||||
|
||||
evicted := LRU_put( & region.state, lru_code, atlas_index )
|
||||
assert( evicted == next_evict_codepoint )
|
||||
}
|
||||
|
||||
assert( LRU_get( & region.state, lru_code ) != - 1 )
|
||||
}
|
||||
|
||||
atlas := & ctx.atlas
|
||||
glyph_buffer := & ctx.glyph_buffer
|
||||
atlas_size := Vec2 { f32(atlas.width), f32(atlas.height) }
|
||||
glyph_buffer_size := Vec2 { f32(glyph_buffer.width), f32(glyph_buffer.height) }
|
||||
glyph_padding := cast(f32) atlas.glyph_padding
|
||||
|
||||
if ctx.debug_print
|
||||
{
|
||||
@static debug_total_cached : i32 = 0
|
||||
logf("glyph %v%v( %v ) caching to atlas region %v at idx %d. %d total glyphs cached.\n",
|
||||
i32(glyph_index), rune(glyph_index), cast(rune) region_kind, atlas_index, debug_total_cached)
|
||||
debug_total_cached += 1
|
||||
}
|
||||
|
||||
// Draw oversized glyph to update FBO
|
||||
glyph_draw_scale := over_sample * entry.size_scale
|
||||
glyph_draw_translate := -1 * vec2(bounds_0) * glyph_draw_scale + vec2( glyph_padding )
|
||||
glyph_draw_translate.x = cast(f32) (i32(glyph_draw_translate.x + 0.9999999))
|
||||
glyph_draw_translate.y = cast(f32) (i32(glyph_draw_translate.y + 0.9999999))
|
||||
|
||||
// Allocate a glyph_update_FBO region
|
||||
gwidth_scaled_px := bounds_size.x * glyph_draw_scale.x + 1.0 + over_sample.x * glyph_padding
|
||||
if i32(f32(glyph_buffer.batch_x) + gwidth_scaled_px) >= i32(glyph_buffer.width) {
|
||||
flush_glyph_buffer_to_atlas( ctx )
|
||||
}
|
||||
|
||||
// Calculate the src and destination regions
|
||||
slot_position, slot_szie := atlas_bbox( atlas, region_kind, atlas_index )
|
||||
|
||||
dst_glyph_position := slot_position
|
||||
dst_glyph_size := bounds_size * entry.size_scale + glyph_padding
|
||||
dst_size := slot_szie
|
||||
screenspace_x_form( & dst_glyph_position, & dst_glyph_size, atlas_size )
|
||||
screenspace_x_form( & slot_position, & dst_size, atlas_size )
|
||||
|
||||
src_position := Vec2 { f32(glyph_buffer.batch_x), 0 }
|
||||
src_size := bounds_size * glyph_draw_scale + over_sample * glyph_padding
|
||||
textspace_x_form( & src_position, & src_size, glyph_buffer_size )
|
||||
|
||||
// Advance glyph_update_batch_x and calculate final glyph drawing transform
|
||||
glyph_draw_translate.x += f32(glyph_buffer.batch_x)
|
||||
glyph_buffer.batch_x += i32(gwidth_scaled_px)
|
||||
screenspace_x_form( & glyph_draw_translate, & glyph_draw_scale, glyph_buffer_size )
|
||||
|
||||
clear_target_region : DrawCall
|
||||
{
|
||||
using clear_target_region
|
||||
pass = .Atlas
|
||||
region = .Ignore
|
||||
start_index = cast(u32) len(glyph_buffer.clear_draw_list.indices)
|
||||
|
||||
blit_quad( & glyph_buffer.clear_draw_list,
|
||||
slot_position, slot_position + dst_size,
|
||||
{ 1.0, 1.0 }, { 1.0, 1.0 } )
|
||||
|
||||
end_index = cast(u32) len(glyph_buffer.clear_draw_list.indices)
|
||||
}
|
||||
|
||||
blit_to_atlas : DrawCall
|
||||
{
|
||||
using blit_to_atlas
|
||||
pass = .Atlas
|
||||
region = .None
|
||||
start_index = cast(u32) len(glyph_buffer.draw_list.indices)
|
||||
|
||||
blit_quad( & glyph_buffer.draw_list,
|
||||
dst_glyph_position, slot_position + dst_glyph_size,
|
||||
src_position, src_position + src_size )
|
||||
|
||||
end_index = cast(u32) len(glyph_buffer.draw_list.indices)
|
||||
}
|
||||
|
||||
append( & glyph_buffer.clear_draw_list.calls, clear_target_region )
|
||||
append( & glyph_buffer.draw_list.calls, blit_to_atlas )
|
||||
|
||||
// Render glyph to glyph_update_FBO
|
||||
cache_glyph( ctx, font, glyph_index, entry, vec2(bounds_0), vec2(bounds_1), glyph_draw_scale, glyph_draw_translate )
|
||||
}
|
||||
|
||||
// If the glyuph is found in the atlas, nothing occurs, otherwise, the glyph call is setup to catch it to the atlas
|
||||
check_glyph_in_atlas :: #force_inline proc( ctx : ^Context, font : FontID, entry : ^Entry, glyph_index : Glyph,
|
||||
lru_code : u64,
|
||||
atlas_index : i32,
|
||||
region_kind : AtlasRegionKind,
|
||||
region : ^AtlasRegion,
|
||||
over_sample : Vec2
|
||||
) -> b32
|
||||
{
|
||||
// profile(#procedure)
|
||||
assert( glyph_index != -1 )
|
||||
|
||||
// E region can't batch
|
||||
if region_kind == .E || region_kind == .None do return false
|
||||
if ctx.temp_codepoint_seen_num > 1024 do return false
|
||||
// TODO(Ed): Why 1024?
|
||||
|
||||
if atlas_index == - 1
|
||||
{
|
||||
if region.next_idx > region.state.capacity {
|
||||
// We will evict LRU. We must predict which LRU will get evicted, and if it's something we've seen then we need to take slowpath and flush batch.
|
||||
next_evict_codepoint := LRU_get_next_evicted( & region.state )
|
||||
seen, success := ctx.temp_codepoint_seen[next_evict_codepoint]
|
||||
assert(success != false)
|
||||
|
||||
if (seen) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
cache_glyph_to_atlas( ctx, font, glyph_index, lru_code, atlas_index, entry, region_kind, region, over_sample )
|
||||
}
|
||||
|
||||
assert( LRU_get( & region.state, lru_code ) != -1 )
|
||||
mark_batch_codepoint_seen( ctx, lru_code)
|
||||
return true
|
||||
}
|
||||
|
||||
// ve_fontcache_clear_drawlist
|
||||
clear_draw_list :: #force_inline proc ( draw_list : ^DrawList ) {
|
||||
clear( & draw_list.calls )
|
||||
clear( & draw_list.indices )
|
||||
clear( & draw_list.vertices )
|
||||
}
|
||||
|
||||
directly_draw_massive_glyph :: proc( ctx : ^Context,
|
||||
entry : ^Entry,
|
||||
glyph : Glyph,
|
||||
bounds_0, bounds_1 : Vec2,
|
||||
bounds_size : Vec2,
|
||||
over_sample, position, scale : Vec2 )
|
||||
{
|
||||
// profile(#procedure)
|
||||
flush_glyph_buffer_to_atlas( ctx )
|
||||
|
||||
glyph_padding := f32(ctx.atlas.glyph_padding)
|
||||
glyph_buffer_size := Vec2 { f32(ctx.glyph_buffer.width), f32(ctx.glyph_buffer.height) }
|
||||
|
||||
// Draw un-antialiased glyph to update FBO.
|
||||
glyph_draw_scale := over_sample * entry.size_scale
|
||||
glyph_draw_translate := -1 * bounds_0 * glyph_draw_scale + vec2_from_scalar(glyph_padding)
|
||||
screenspace_x_form( & glyph_draw_translate, & glyph_draw_scale, glyph_buffer_size )
|
||||
|
||||
cache_glyph( ctx, entry.id, glyph, entry, bounds_0, bounds_1, glyph_draw_scale, glyph_draw_translate )
|
||||
|
||||
glyph_padding_dbl := glyph_padding * 2
|
||||
bounds_scaled := bounds_size * entry.size_scale
|
||||
|
||||
// Figure out the source rect.
|
||||
glyph_position := Vec2 {}
|
||||
glyph_size := vec2(glyph_padding_dbl)
|
||||
glyph_dst_size := glyph_size + bounds_scaled
|
||||
glyph_size += bounds_scaled * over_sample
|
||||
|
||||
// Figure out the destination rect.
|
||||
bounds_0_scaled := Vec2 {
|
||||
cast(f32) i32(bounds_0.x * entry.size_scale - 0.5),
|
||||
cast(f32) i32(bounds_0.y * entry.size_scale - 0.5),
|
||||
}
|
||||
dst := position + scale * bounds_0_scaled - glyph_padding * scale
|
||||
dst_size := glyph_dst_size * scale
|
||||
textspace_x_form( & glyph_position, & glyph_size, glyph_buffer_size )
|
||||
|
||||
// Add the glyph drawcall.
|
||||
calls : [2]DrawCall
|
||||
|
||||
draw_to_target := & calls[0]
|
||||
{
|
||||
using draw_to_target
|
||||
pass = .Target_Uncached
|
||||
colour = ctx.colour
|
||||
start_index = u32(len(ctx.draw_list.indices))
|
||||
|
||||
blit_quad( & ctx.draw_list,
|
||||
dst, dst + dst_size,
|
||||
glyph_position, glyph_position + glyph_size )
|
||||
|
||||
end_index = u32(len(ctx.draw_list.indices))
|
||||
}
|
||||
|
||||
clear_glyph_update := & calls[1]
|
||||
{
|
||||
// Clear glyph_update_FBO.
|
||||
clear_glyph_update.pass = .Glyph
|
||||
clear_glyph_update.start_index = 0
|
||||
clear_glyph_update.end_index = 0
|
||||
clear_glyph_update.clear_before_draw = true
|
||||
}
|
||||
append( & ctx.draw_list.calls, ..calls[:] )
|
||||
}
|
||||
|
||||
// Constructs a triangle fan to fill a shape using the provided path
|
||||
// outside_point represents the center point of the fan.
|
||||
//
|
||||
// Note(Original Author):
|
||||
// WARNING: doesn't actually append drawcall; caller is responsible for actually appending the drawcall.
|
||||
// ve_fontcache_draw_filled_path
|
||||
draw_filled_path :: proc( draw_list : ^DrawList, outside_point : Vec2, path : []Vertex,
|
||||
scale := Vec2 { 1, 1 },
|
||||
translate := Vec2 { 0, 0 },
|
||||
debug_print_verbose : b32 = false
|
||||
)
|
||||
{
|
||||
if debug_print_verbose
|
||||
{
|
||||
log("outline_path:")
|
||||
for point in path {
|
||||
vec := point.pos * scale + translate
|
||||
logf(" %0.2f %0.2f", vec.x, vec.y )
|
||||
}
|
||||
}
|
||||
|
||||
v_offset := cast(u32) len(draw_list.vertices)
|
||||
for point in path {
|
||||
point := point
|
||||
point.pos = point.pos * scale + translate
|
||||
append( & draw_list.vertices, point )
|
||||
}
|
||||
|
||||
outside_vertex := cast(u32) len(draw_list.vertices)
|
||||
{
|
||||
vertex := Vertex {
|
||||
pos = outside_point * scale + translate,
|
||||
u = 0,
|
||||
v = 0,
|
||||
}
|
||||
append( & draw_list.vertices, vertex )
|
||||
}
|
||||
|
||||
for index : u32 = 1; index < cast(u32) len(path); index += 1 {
|
||||
indices := & draw_list.indices
|
||||
to_add := [3]u32 {
|
||||
outside_vertex,
|
||||
v_offset + index - 1,
|
||||
v_offset + index
|
||||
}
|
||||
append( indices, ..to_add[:] )
|
||||
}
|
||||
}
|
||||
|
||||
draw_text_batch :: proc(ctx: ^Context, entry: ^Entry, shaped: ^ShapedText,
|
||||
batch_start_idx, batch_end_idx : i32,
|
||||
position, scale : Vec2,
|
||||
snap_width, snap_height : f32 )
|
||||
{
|
||||
flush_glyph_buffer_to_atlas(ctx)
|
||||
|
||||
atlas := & ctx.atlas
|
||||
atlas_size := Vec2{ f32(atlas.width), f32(atlas.height) }
|
||||
glyph_padding := f32(atlas.glyph_padding)
|
||||
|
||||
for index := batch_start_idx; index < batch_end_idx; index += 1
|
||||
{
|
||||
glyph_index := shaped.glyphs[index]
|
||||
|
||||
if glyph_index == 0 || parser_is_glyph_empty( & entry.parser_info, glyph_index) do continue
|
||||
|
||||
region_kind, region, over_sample := decide_codepoint_region( ctx, entry, glyph_index )
|
||||
lru_code := font_glyph_lru_code( entry.id, glyph_index )
|
||||
atlas_index := region_kind != .E ? LRU_get( & region.state, lru_code ) : -1
|
||||
bounds_0, bounds_1 := parser_get_glyph_box( & entry.parser_info, glyph_index )
|
||||
vbounds_0 := vec2(bounds_0)
|
||||
vbounds_1 := vec2(bounds_1)
|
||||
bounds_size := Vec2 { vbounds_1.x - vbounds_0.x, vbounds_1.y - vbounds_0.y }
|
||||
|
||||
shaped_position := shaped.positions[index]
|
||||
glyph_translate := position + shaped_position * scale
|
||||
|
||||
if region_kind == .E
|
||||
{
|
||||
directly_draw_massive_glyph(ctx, entry, glyph_index,
|
||||
vbounds_0, vbounds_1,
|
||||
bounds_size,
|
||||
over_sample, glyph_translate, scale )
|
||||
}
|
||||
else if atlas_index != -1
|
||||
{
|
||||
// Draw cacxhed glyph
|
||||
slot_position, _ := atlas_bbox( atlas, region_kind, atlas_index )
|
||||
glyph_scale := bounds_size * entry.size_scale + glyph_padding
|
||||
bounds_0_scaled := ceil(vbounds_0 * entry.size_scale)
|
||||
dst := glyph_translate + bounds_0_scaled * scale
|
||||
dst_scale := glyph_scale * scale
|
||||
textspace_x_form( & slot_position, & glyph_scale, atlas_size )
|
||||
|
||||
call := DrawCall_Default
|
||||
call.pass = .Target
|
||||
call.colour = ctx.colour
|
||||
call.start_index = u32(len(ctx.draw_list.indices))
|
||||
|
||||
blit_quad(&ctx.draw_list,
|
||||
dst, dst + dst_scale,
|
||||
slot_position, slot_position + glyph_scale )
|
||||
|
||||
call.end_index = u32(len(ctx.draw_list.indices))
|
||||
append(&ctx.draw_list.calls, call)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper for draw_text, all raw text content should be confirmed to be either formatting or visible shapes before getting cached.
|
||||
draw_text_shape :: proc( ctx : ^Context,
|
||||
font : FontID,
|
||||
entry : ^Entry,
|
||||
shaped : ^ShapedText,
|
||||
position, scale : Vec2,
|
||||
snap_width, snap_height : f32
|
||||
) -> (cursor_pos : Vec2)
|
||||
{
|
||||
// profile(#procedure)
|
||||
batch_start_idx : i32 = 0
|
||||
for index : i32 = 0; index < cast(i32) len(shaped.glyphs); index += 1
|
||||
{
|
||||
glyph_index := shaped.glyphs[ index ]
|
||||
if is_empty( ctx, entry, glyph_index ) do continue
|
||||
|
||||
region_kind, region, over_sample := decide_codepoint_region( ctx, entry, glyph_index )
|
||||
lru_code := font_glyph_lru_code(entry.id, glyph_index)
|
||||
atlas_index := cast(i32) -1
|
||||
|
||||
if region_kind != .E do atlas_index = LRU_get( & region.state, lru_code )
|
||||
if check_glyph_in_atlas( ctx, font, entry, glyph_index, lru_code, atlas_index, region_kind, region, over_sample ) do continue
|
||||
|
||||
// We can no longer directly append the shape as it has missing glyphs in the atlas
|
||||
|
||||
// First batch the other cached glyphs
|
||||
// flush_glyph_buffer_to_atlas(ctx)
|
||||
draw_text_batch( ctx, entry, shaped, batch_start_idx, index, position, scale, snap_width, snap_height )
|
||||
reset_batch_codepoint_state( ctx )
|
||||
|
||||
cache_glyph_to_atlas( ctx, font, glyph_index, lru_code, atlas_index, entry, region_kind, region, over_sample )
|
||||
mark_batch_codepoint_seen( ctx, lru_code)
|
||||
batch_start_idx = index
|
||||
}
|
||||
|
||||
draw_text_batch( ctx, entry, shaped, batch_start_idx, cast(i32) len(shaped.glyphs), position, scale, snap_width , snap_height )
|
||||
reset_batch_codepoint_state( ctx )
|
||||
|
||||
cursor_pos = position + shaped.end_cursor_pos * scale
|
||||
return
|
||||
}
|
||||
|
||||
flush_glyph_buffer_to_atlas :: proc( ctx : ^Context )
|
||||
{
|
||||
// profile(#procedure)
|
||||
// Flush drawcalls to draw list
|
||||
merge_draw_list( & ctx.draw_list, & ctx.glyph_buffer.clear_draw_list )
|
||||
merge_draw_list( & ctx.draw_list, & ctx.glyph_buffer.draw_list)
|
||||
clear_draw_list( & ctx.glyph_buffer.draw_list )
|
||||
clear_draw_list( & ctx.glyph_buffer.clear_draw_list )
|
||||
|
||||
// Clear glyph_update_FBO
|
||||
if ctx.glyph_buffer.batch_x != 0
|
||||
{
|
||||
call := DrawCall_Default
|
||||
call.pass = .Glyph
|
||||
call.start_index = 0
|
||||
call.end_index = 0
|
||||
call.clear_before_draw = true
|
||||
append( & ctx.draw_list.calls, call )
|
||||
ctx.glyph_buffer.batch_x = 0
|
||||
}
|
||||
}
|
||||
|
||||
// ve_fontcache_merge_drawlist
|
||||
merge_draw_list :: proc( dst, src : ^DrawList )
|
||||
{
|
||||
// profile(#procedure)
|
||||
error : AllocatorError
|
||||
|
||||
v_offset := cast(u32) len( dst.vertices )
|
||||
num_appended : int
|
||||
num_appended, error = append( & dst.vertices, ..src.vertices[:] )
|
||||
assert( error == .None )
|
||||
|
||||
i_offset := cast(u32) len(dst.indices)
|
||||
for index : int = 0; index < len(src.indices); index += 1 {
|
||||
ignored : int
|
||||
ignored, error = append( & dst.indices, src.indices[index] + v_offset )
|
||||
assert( error == .None )
|
||||
}
|
||||
|
||||
for index : int = 0; index < len(src.calls); index += 1 {
|
||||
src_call := src.calls[ index ]
|
||||
src_call.start_index += i_offset
|
||||
src_call.end_index += i_offset
|
||||
append( & dst.calls, src_call )
|
||||
assert( error == .None )
|
||||
}
|
||||
}
|
||||
|
||||
optimize_draw_list :: proc(draw_list: ^DrawList, call_offset: int) {
|
||||
// profile(#procedure)
|
||||
assert(draw_list != nil)
|
||||
|
||||
can_merge_draw_calls :: #force_inline proc "contextless" ( a, b : ^DrawCall ) -> bool {
|
||||
result := \
|
||||
a.pass == b.pass &&
|
||||
a.end_index == b.start_index &&
|
||||
a.region == b.region &&
|
||||
a.colour == b.colour &&
|
||||
! b.clear_before_draw
|
||||
return result
|
||||
}
|
||||
|
||||
write_index := call_offset
|
||||
for read_index := call_offset + 1; read_index < len(draw_list.calls); read_index += 1
|
||||
{
|
||||
draw_current := & draw_list.calls[write_index]
|
||||
draw_next := & draw_list.calls[read_index]
|
||||
|
||||
if can_merge_draw_calls(draw_current, draw_next) {
|
||||
draw_current.end_index = draw_next.end_index
|
||||
}
|
||||
else {
|
||||
// Move to the next write position and copy the draw call
|
||||
write_index += 1
|
||||
if write_index != read_index {
|
||||
draw_list.calls[write_index] = (draw_next^)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resize( & draw_list.calls, write_index + 1)
|
||||
}
|
105
code/font/vefontcache/mappings.odin
Normal file
105
code/font/vefontcache/mappings.odin
Normal file
@ -0,0 +1,105 @@
|
||||
package VEFontCache
|
||||
|
||||
import "core:hash"
|
||||
fnv64a :: hash.fnv64a
|
||||
import "core:math"
|
||||
ceil_f16 :: math.ceil_f16
|
||||
ceil_f16le :: math.ceil_f16le
|
||||
ceil_f16be :: math.ceil_f16be
|
||||
ceil_f32 :: math.ceil_f32
|
||||
ceil_f32le :: math.ceil_f32le
|
||||
ceil_f32be :: math.ceil_f32be
|
||||
ceil_f64 :: math.ceil_f64
|
||||
ceil_f64le :: math.ceil_f64le
|
||||
ceil_f64be :: math.ceil_f64be
|
||||
|
||||
floor_f16 :: math.floor_f16
|
||||
floor_f16le :: math.floor_f16le
|
||||
floor_f16be :: math.floor_f16be
|
||||
floor_f32 :: math.floor_f32
|
||||
floor_f32le :: math.floor_f32le
|
||||
floor_f32be :: math.floor_f32be
|
||||
floor_f64 :: math.floor_f64
|
||||
floor_f64le :: math.floor_f64le
|
||||
floor_f64be :: math.floor_f64be
|
||||
import "core:math/linalg"
|
||||
import "core:mem"
|
||||
Kilobyte :: mem.Kilobyte
|
||||
slice_ptr :: mem.slice_ptr
|
||||
|
||||
Allocator :: mem.Allocator
|
||||
AllocatorError :: mem.Allocator_Error
|
||||
|
||||
Arena :: mem.Arena
|
||||
arena_allocator :: mem.arena_allocator
|
||||
arena_init :: mem.arena_init
|
||||
import "codebase:grime"
|
||||
log :: grime.log
|
||||
logf :: grime.logf
|
||||
profile :: grime.profile
|
||||
|
||||
//#region("Proc overload mappings")
|
||||
|
||||
append :: proc {
|
||||
append_elem,
|
||||
append_elems,
|
||||
append_elem_string,
|
||||
}
|
||||
|
||||
ceil :: proc {
|
||||
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,
|
||||
}
|
||||
|
||||
floor :: proc {
|
||||
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,
|
||||
}
|
||||
|
||||
make :: proc {
|
||||
make_dynamic_array,
|
||||
make_dynamic_array_len,
|
||||
make_dynamic_array_len_cap,
|
||||
make_map,
|
||||
}
|
||||
|
||||
resize :: proc {
|
||||
resize_dynamic_array,
|
||||
}
|
||||
|
||||
vec2 :: proc {
|
||||
vec2_from_scalar,
|
||||
vec2_from_vec2i,
|
||||
}
|
||||
|
||||
vec2i :: proc {
|
||||
vec2i_from_vec2,
|
||||
}
|
||||
|
||||
vec2_64 :: proc {
|
||||
vec2_64_from_vec2,
|
||||
}
|
||||
|
||||
//#endregion("Proc overload mappings")
|
289
code/font/vefontcache/misc.odin
Normal file
289
code/font/vefontcache/misc.odin
Normal file
@ -0,0 +1,289 @@
|
||||
package VEFontCache
|
||||
|
||||
import "base:runtime"
|
||||
import "core:simd"
|
||||
import "core:math"
|
||||
|
||||
// import core_log "core:log"
|
||||
|
||||
Colour :: [4]f32
|
||||
Vec2 :: [2]f32
|
||||
Vec2i :: [2]i32
|
||||
Vec2_64 :: [2]f64
|
||||
|
||||
vec2_from_scalar :: #force_inline proc "contextless" ( scalar : f32 ) -> Vec2 { return { scalar, scalar }}
|
||||
vec2_64_from_vec2 :: #force_inline proc "contextless" ( v2 : Vec2 ) -> Vec2_64 { return { f64(v2.x), f64(v2.y) }}
|
||||
vec2_from_vec2i :: #force_inline proc "contextless" ( v2i : Vec2i ) -> Vec2 { return { f32(v2i.x), f32(v2i.y) }}
|
||||
vec2i_from_vec2 :: #force_inline proc "contextless" ( v2 : Vec2 ) -> Vec2i { return { i32(v2.x), i32(v2.y) }}
|
||||
|
||||
@(require_results) ceil_vec2 :: proc "contextless" ( v : Vec2 ) -> Vec2 { return { ceil_f32(v.x), ceil_f32(v.y) } }
|
||||
@(require_results) floor_vec2 :: proc "contextless" ( v : Vec2 ) -> Vec2 { return { floor_f32(v.x), floor_f32(v.y) } }
|
||||
|
||||
// This buffer is used below excluisvely to prevent any allocator recusion when verbose logging from allocators.
|
||||
// This means a single line is limited to 32k buffer (increase naturally if this SOMEHOW becomes a bottleneck...)
|
||||
// Logger_Allocator_Buffer : [32 * Kilobyte]u8
|
||||
|
||||
// log :: proc( msg : string, level := core_log.Level.Info, loc := #caller_location ) {
|
||||
// temp_arena : Arena; arena_init(& temp_arena, Logger_Allocator_Buffer[:])
|
||||
// context.allocator = arena_allocator(& temp_arena)
|
||||
// context.temp_allocator = arena_allocator(& temp_arena)
|
||||
|
||||
// core_log.log( level, msg, location = loc )
|
||||
// }
|
||||
|
||||
// logf :: proc( fmt : string, args : ..any, level := core_log.Level.Info, loc := #caller_location ) {
|
||||
// temp_arena : Arena; arena_init(& temp_arena, Logger_Allocator_Buffer[:])
|
||||
// context.allocator = arena_allocator(& temp_arena)
|
||||
// context.temp_allocator = arena_allocator(& temp_arena)
|
||||
|
||||
// core_log.logf( level, fmt, ..args, location = loc )
|
||||
// }
|
||||
|
||||
reload_array :: proc( self : ^[dynamic]$Type, allocator : Allocator ) {
|
||||
raw := transmute( ^runtime.Raw_Dynamic_Array) self
|
||||
raw.allocator = allocator
|
||||
}
|
||||
|
||||
reload_map :: proc( self : ^map [$KeyType] $EntryType, allocator : Allocator ) {
|
||||
raw := transmute( ^runtime.Raw_Map) self
|
||||
raw.allocator = allocator
|
||||
}
|
||||
|
||||
font_glyph_lru_code :: #force_inline proc "contextless" ( font : FontID, glyph_index : Glyph ) -> (lru_code : u64) {
|
||||
lru_code = u64(glyph_index) + ( ( 0x100000000 * u64(font) ) & 0xFFFFFFFF00000000 )
|
||||
return
|
||||
}
|
||||
|
||||
is_empty :: #force_inline proc ( ctx : ^Context, entry : ^Entry, glyph_index : Glyph ) -> b32
|
||||
{
|
||||
if glyph_index == 0 do return true
|
||||
if parser_is_glyph_empty( & entry.parser_info, glyph_index ) do return true
|
||||
return false
|
||||
}
|
||||
|
||||
mark_batch_codepoint_seen :: #force_inline proc ( ctx : ^Context, lru_code : u64 ) {
|
||||
ctx.temp_codepoint_seen[lru_code] = true
|
||||
ctx.temp_codepoint_seen_num += 1
|
||||
}
|
||||
|
||||
reset_batch_codepoint_state :: #force_inline proc( ctx : ^Context ) {
|
||||
clear_map( & ctx.temp_codepoint_seen )
|
||||
ctx.temp_codepoint_seen_num = 0
|
||||
}
|
||||
|
||||
screenspace_x_form :: #force_inline proc "contextless" ( position, scale : ^Vec2, size : Vec2 )
|
||||
{
|
||||
if true
|
||||
{
|
||||
pos_64 := vec2_64_from_vec2(position^)
|
||||
scale_64 := vec2_64_from_vec2(scale^)
|
||||
|
||||
quotient : Vec2_64 = 1.0 / vec2_64(size)
|
||||
pos_64 = pos_64 * quotient * 2.0 - 1.0
|
||||
scale_64 = scale_64 * quotient * 2.0
|
||||
|
||||
(position^) = { f32(pos_64.x), f32(pos_64.y) }
|
||||
(scale^) = { f32(scale_64.x), f32(scale_64.y) }
|
||||
}
|
||||
else
|
||||
{
|
||||
pos := position^
|
||||
scale_32 := scale^
|
||||
|
||||
quotient : Vec2 = 1.0 / size
|
||||
pos = pos * quotient * 2.0 - 1.0
|
||||
scale_32 = scale_32 * quotient * 2.0
|
||||
|
||||
(position^) = pos
|
||||
(scale^) = scale_32
|
||||
}
|
||||
}
|
||||
|
||||
textspace_x_form :: #force_inline proc "contextless" ( position, scale : ^Vec2, size : Vec2 )
|
||||
{
|
||||
if true
|
||||
{
|
||||
pos_64 := vec2_64_from_vec2(position^)
|
||||
scale_64 := vec2_64_from_vec2(scale^)
|
||||
|
||||
quotient : Vec2_64 = 1.0 / vec2_64(size)
|
||||
pos_64 *= quotient
|
||||
scale_64 *= quotient
|
||||
|
||||
(position^) = { f32(pos_64.x), f32(pos_64.y) }
|
||||
(scale^) = { f32(scale_64.x), f32(scale_64.y) }
|
||||
}
|
||||
else
|
||||
{
|
||||
quotient : Vec2 = 1.0 / size
|
||||
(position^) *= quotient
|
||||
(scale^) *= quotient
|
||||
}
|
||||
}
|
||||
|
||||
Use_SIMD_For_Bezier_Ops :: true
|
||||
|
||||
when ! Use_SIMD_For_Bezier_Ops
|
||||
{
|
||||
// For a provided alpha value,
|
||||
// allows the function to calculate the position of a point along the curve at any given fraction of its total length
|
||||
// ve_fontcache_eval_bezier (quadratic)
|
||||
eval_point_on_bezier3 :: #force_inline proc "contextless" ( p0, p1, p2 : Vec2, alpha : f32 ) -> Vec2
|
||||
{
|
||||
p0 := vec2_64(p0)
|
||||
p1 := vec2_64(p1)
|
||||
p2 := vec2_64(p2)
|
||||
alpha := f64(alpha)
|
||||
|
||||
weight_start := (1 - alpha) * (1 - alpha)
|
||||
weight_control := 2.0 * (1 - alpha) * alpha
|
||||
weight_end := alpha * alpha
|
||||
|
||||
starting_point := p0 * weight_start
|
||||
control_point := p1 * weight_control
|
||||
end_point := p2 * weight_end
|
||||
|
||||
point := starting_point + control_point + end_point
|
||||
return { f32(point.x), f32(point.y) }
|
||||
}
|
||||
|
||||
// For a provided alpha value,
|
||||
// allows the function to calculate the position of a point along the curve at any given fraction of its total length
|
||||
// ve_fontcache_eval_bezier (cubic)
|
||||
eval_point_on_bezier4 :: #force_inline proc "contextless" ( p0, p1, p2, p3 : Vec2, alpha : f32 ) -> Vec2
|
||||
{
|
||||
p0 := vec2_64(p0)
|
||||
p1 := vec2_64(p1)
|
||||
p2 := vec2_64(p2)
|
||||
p3 := vec2_64(p3)
|
||||
alpha := f64(alpha)
|
||||
|
||||
weight_start := (1 - alpha) * (1 - alpha) * (1 - alpha)
|
||||
weight_c_a := 3 * (1 - alpha) * (1 - alpha) * alpha
|
||||
weight_c_b := 3 * (1 - alpha) * alpha * alpha
|
||||
weight_end := alpha * alpha * alpha
|
||||
|
||||
start_point := p0 * weight_start
|
||||
control_a := p1 * weight_c_a
|
||||
control_b := p2 * weight_c_b
|
||||
end_point := p3 * weight_end
|
||||
|
||||
point := start_point + control_a + control_b + end_point
|
||||
return { f32(point.x), f32(point.y) }
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Vec2_SIMD :: simd.f32x4
|
||||
|
||||
vec2_to_simd :: #force_inline proc "contextless" (v: Vec2) -> Vec2_SIMD {
|
||||
return Vec2_SIMD{v.x, v.y, 0, 0}
|
||||
}
|
||||
|
||||
simd_to_vec2 :: #force_inline proc "contextless" (v: Vec2_SIMD) -> Vec2 {
|
||||
return Vec2{ simd.extract(v, 0), simd.extract(v, 1) }
|
||||
}
|
||||
|
||||
vec2_add_simd :: #force_inline proc "contextless" (a, b: Vec2) -> Vec2 {
|
||||
simd_a := vec2_to_simd(a)
|
||||
simd_b := vec2_to_simd(b)
|
||||
result := simd.add(simd_a, simd_b)
|
||||
return simd_to_vec2(result)
|
||||
}
|
||||
|
||||
vec2_sub_simd :: #force_inline proc "contextless" (a, b: Vec2) -> Vec2 {
|
||||
simd_a := vec2_to_simd(a)
|
||||
simd_b := vec2_to_simd(b)
|
||||
result := simd.sub(simd_a, simd_b)
|
||||
return simd_to_vec2(result)
|
||||
}
|
||||
|
||||
vec2_mul_simd :: #force_inline proc "contextless" (a: Vec2, s: f32) -> Vec2 {
|
||||
simd_a := vec2_to_simd(a)
|
||||
simd_s := Vec2_SIMD{s, s, s, s}
|
||||
result := simd.mul(simd_a, simd_s)
|
||||
return simd_to_vec2(result)
|
||||
}
|
||||
|
||||
vec2_div_simd :: #force_inline proc "contextless" (a: Vec2, s: f32) -> Vec2 {
|
||||
simd_a := vec2_to_simd(a)
|
||||
simd_s := Vec2_SIMD{s, s, s, s}
|
||||
result := simd.div(simd_a, simd_s)
|
||||
return simd_to_vec2(result)
|
||||
}
|
||||
|
||||
vec2_dot_simd :: #force_inline proc "contextless" (a, b: Vec2) -> f32 {
|
||||
simd_a := vec2_to_simd(a)
|
||||
simd_b := vec2_to_simd(b)
|
||||
result := simd.mul(simd_a, simd_b)
|
||||
return simd.reduce_add_ordered(result)
|
||||
}
|
||||
|
||||
vec2_length_sqr_simd :: #force_inline proc "contextless" (a: Vec2) -> f32 {
|
||||
return vec2_dot_simd(a, a)
|
||||
}
|
||||
|
||||
vec2_length_simd :: #force_inline proc "contextless" (a: Vec2) -> f32 {
|
||||
return math.sqrt(vec2_length_sqr_simd(a))
|
||||
}
|
||||
|
||||
vec2_normalize_simd :: #force_inline proc "contextless" (a: Vec2) -> Vec2 {
|
||||
len := vec2_length_simd(a)
|
||||
if len > 0 {
|
||||
inv_len := 1.0 / len
|
||||
return vec2_mul_simd(a, inv_len)
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// SIMD-optimized version of eval_point_on_bezier3
|
||||
eval_point_on_bezier3 :: #force_inline proc "contextless" (p0, p1, p2: Vec2, alpha: f32) -> Vec2
|
||||
{
|
||||
simd_p0 := vec2_to_simd(p0)
|
||||
simd_p1 := vec2_to_simd(p1)
|
||||
simd_p2 := vec2_to_simd(p2)
|
||||
|
||||
one_minus_alpha := 1.0 - alpha
|
||||
weight_start := one_minus_alpha * one_minus_alpha
|
||||
weight_control := 2.0 * one_minus_alpha * alpha
|
||||
weight_end := alpha * alpha
|
||||
|
||||
simd_weights := Vec2_SIMD{weight_start, weight_control, weight_end, 0}
|
||||
result := simd.add(
|
||||
simd.add(
|
||||
simd.mul( simd_p0, simd.swizzle( simd_weights, 0, 0, 0, 0) ),
|
||||
simd.mul( simd_p1, simd.swizzle( simd_weights, 1, 1, 1, 1) )
|
||||
),
|
||||
simd.mul( simd_p2, simd.swizzle(simd_weights, 2, 2, 2, 2) )
|
||||
)
|
||||
|
||||
return simd_to_vec2(result)
|
||||
}
|
||||
|
||||
eval_point_on_bezier4 :: #force_inline proc "contextless" (p0, p1, p2, p3: Vec2, alpha: f32) -> Vec2
|
||||
{
|
||||
simd_p0 := vec2_to_simd(p0)
|
||||
simd_p1 := vec2_to_simd(p1)
|
||||
simd_p2 := vec2_to_simd(p2)
|
||||
simd_p3 := vec2_to_simd(p3)
|
||||
|
||||
one_minus_alpha := 1.0 - alpha
|
||||
weight_start := one_minus_alpha * one_minus_alpha * one_minus_alpha
|
||||
weight_c_a := 3 * one_minus_alpha * one_minus_alpha * alpha
|
||||
weight_c_b := 3 * one_minus_alpha * alpha * alpha
|
||||
weight_end := alpha * alpha * alpha
|
||||
|
||||
simd_weights := Vec2_SIMD { weight_start, weight_c_a, weight_c_b, weight_end }
|
||||
result := simd.add(
|
||||
simd.add(
|
||||
simd.mul( simd_p0, simd.swizzle(simd_weights, 0, 0, 0, 0) ),
|
||||
simd.mul( simd_p1, simd.swizzle(simd_weights, 1, 1, 1, 1) )
|
||||
),
|
||||
simd.add(
|
||||
simd.mul( simd_p2, simd.swizzle(simd_weights, 2, 2, 2, 2) ),
|
||||
simd.mul( simd_p3, simd.swizzle(simd_weights, 3, 3, 3, 3) )
|
||||
)
|
||||
)
|
||||
return simd_to_vec2(result)
|
||||
}
|
||||
}
|
494
code/font/vefontcache/parser.odin
Normal file
494
code/font/vefontcache/parser.odin
Normal file
@ -0,0 +1,494 @@
|
||||
package VEFontCache
|
||||
|
||||
/*
|
||||
Notes:
|
||||
|
||||
Freetype will do memory allocations and has an interface the user can implement.
|
||||
That interface is not exposed from this parser but could be added to parser_init.
|
||||
|
||||
STB_Truetype has macros for its allocation unfortuantely
|
||||
*/
|
||||
|
||||
import "base:runtime"
|
||||
import "core:c"
|
||||
import "core:math"
|
||||
import stbtt "vendor:stb/truetype"
|
||||
import freetype "thirdparty:freetype"
|
||||
|
||||
ParserKind :: enum u32 {
|
||||
STB_TrueType,
|
||||
Freetype,
|
||||
}
|
||||
|
||||
ParserFontInfo :: struct {
|
||||
label : string,
|
||||
kind : ParserKind,
|
||||
using _ : struct #raw_union {
|
||||
stbtt_info : stbtt.fontinfo,
|
||||
freetype_info : freetype.Face
|
||||
},
|
||||
data : []byte,
|
||||
}
|
||||
|
||||
GlyphVertType :: enum u8 {
|
||||
None,
|
||||
Move = 1,
|
||||
Line,
|
||||
Curve,
|
||||
Cubic,
|
||||
}
|
||||
|
||||
// Based directly off of stb_truetype's vertex
|
||||
ParserGlyphVertex :: struct {
|
||||
x, y : i16,
|
||||
contour_x0, contour_y0 : i16,
|
||||
contour_x1, contour_y1 : i16,
|
||||
type : GlyphVertType,
|
||||
padding : u8,
|
||||
}
|
||||
// A shape can be a dynamic array free_type or an opaque set of data handled by stb_truetype
|
||||
ParserGlyphShape :: [dynamic]ParserGlyphVertex
|
||||
|
||||
ParserContext :: struct {
|
||||
kind : ParserKind,
|
||||
ft_library : freetype.Library,
|
||||
|
||||
// fonts : HMapChained(ParserFontInfo),
|
||||
}
|
||||
|
||||
parser_init :: proc( ctx : ^ParserContext )
|
||||
{
|
||||
switch ctx.kind
|
||||
{
|
||||
case .Freetype:
|
||||
result := freetype.init_free_type( & ctx.ft_library )
|
||||
assert( result == freetype.Error.Ok, "VEFontCache.parser_init: Failed to initialize freetype" )
|
||||
|
||||
case .STB_TrueType:
|
||||
// Do nothing intentional
|
||||
}
|
||||
|
||||
// error : AllocatorError
|
||||
// ctx.fonts, error = make( HMapChained(ParserFontInfo), 256 )
|
||||
// assert( error == .None, "VEFontCache.parser_init: Failed to allocate fonts array" )
|
||||
}
|
||||
|
||||
parser_shutdown :: proc( ctx : ^ParserContext ) {
|
||||
// TODO(Ed): Implement
|
||||
}
|
||||
|
||||
parser_load_font :: proc( ctx : ^ParserContext, label : string, data : []byte ) -> (font : ParserFontInfo)
|
||||
{
|
||||
// key := font_key_from_label(label)
|
||||
// font = get( ctx.fonts, key )
|
||||
// if font != nil do return
|
||||
|
||||
// error : AllocatorError
|
||||
// font, error = set( ctx.fonts, key, ParserFontInfo {} )
|
||||
// assert( error == .None, "VEFontCache.parser_load_font: Failed to set a new parser font info" )
|
||||
switch ctx.kind
|
||||
{
|
||||
case .Freetype:
|
||||
error := freetype.new_memory_face( ctx.ft_library, raw_data(data), cast(i32) len(data), 0, & font.freetype_info )
|
||||
if error != .Ok do return
|
||||
|
||||
case .STB_TrueType:
|
||||
success := stbtt.InitFont( & font.stbtt_info, raw_data(data), 0 )
|
||||
if ! success do return
|
||||
}
|
||||
|
||||
font.label = label
|
||||
font.data = data
|
||||
return
|
||||
}
|
||||
|
||||
parser_unload_font :: proc( font : ^ParserFontInfo )
|
||||
{
|
||||
switch font.kind {
|
||||
case .Freetype:
|
||||
error := freetype.done_face( font.freetype_info )
|
||||
assert( error == .Ok, "VEFontCache.parser_unload_font: Failed to unload freetype face" )
|
||||
|
||||
case .STB_TrueType:
|
||||
// Do Nothing
|
||||
}
|
||||
}
|
||||
|
||||
parser_find_glyph_index :: #force_inline proc "contextless" ( font : ^ParserFontInfo, codepoint : rune ) -> (glyph_index : Glyph)
|
||||
{
|
||||
switch font.kind
|
||||
{
|
||||
case .Freetype:
|
||||
glyph_index = transmute(Glyph) freetype.get_char_index( font.freetype_info, transmute(u32) codepoint )
|
||||
return
|
||||
|
||||
case .STB_TrueType:
|
||||
glyph_index = transmute(Glyph) stbtt.FindGlyphIndex( & font.stbtt_info, codepoint )
|
||||
return
|
||||
}
|
||||
return Glyph(-1)
|
||||
}
|
||||
|
||||
parser_free_shape :: proc( font : ^ParserFontInfo, shape : ParserGlyphShape )
|
||||
{
|
||||
switch font.kind
|
||||
{
|
||||
case .Freetype:
|
||||
delete(shape)
|
||||
|
||||
case .STB_TrueType:
|
||||
stbtt.FreeShape( & font.stbtt_info, transmute( [^]stbtt.vertex) raw_data(shape) )
|
||||
}
|
||||
}
|
||||
|
||||
parser_get_codepoint_horizontal_metrics :: #force_inline proc "contextless" ( font : ^ParserFontInfo, codepoint : rune ) -> ( advance, to_left_side_glyph : i32 )
|
||||
{
|
||||
switch font.kind
|
||||
{
|
||||
case .Freetype:
|
||||
glyph_index := transmute(Glyph) freetype.get_char_index( font.freetype_info, transmute(u32) codepoint )
|
||||
if glyph_index != 0
|
||||
{
|
||||
freetype.load_glyph( font.freetype_info, c.uint(codepoint), { .No_Bitmap, .No_Hinting, .No_Scale } )
|
||||
advance = i32(font.freetype_info.glyph.advance.x) >> 6
|
||||
to_left_side_glyph = i32(font.freetype_info.glyph.metrics.hori_bearing_x) >> 6
|
||||
}
|
||||
else
|
||||
{
|
||||
advance = 0
|
||||
to_left_side_glyph = 0
|
||||
}
|
||||
|
||||
case .STB_TrueType:
|
||||
stbtt.GetCodepointHMetrics( & font.stbtt_info, codepoint, & advance, & to_left_side_glyph )
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
parser_get_codepoint_kern_advance :: #force_inline proc "contextless" ( font : ^ParserFontInfo, prev_codepoint, codepoint : rune ) -> i32
|
||||
{
|
||||
switch font.kind
|
||||
{
|
||||
case .Freetype:
|
||||
prev_glyph_index := transmute(Glyph) freetype.get_char_index( font.freetype_info, transmute(u32) prev_codepoint )
|
||||
glyph_index := transmute(Glyph) freetype.get_char_index( font.freetype_info, transmute(u32) codepoint )
|
||||
if prev_glyph_index != 0 && glyph_index != 0
|
||||
{
|
||||
kerning : freetype.Vector
|
||||
font.freetype_info.driver.clazz.get_kerning( font.freetype_info, transmute(u32) prev_codepoint, transmute(u32) codepoint, & kerning )
|
||||
}
|
||||
|
||||
case .STB_TrueType:
|
||||
kern := stbtt.GetCodepointKernAdvance( & font.stbtt_info, prev_codepoint, codepoint )
|
||||
return kern
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
parser_get_font_vertical_metrics :: #force_inline proc "contextless" ( font : ^ParserFontInfo ) -> (ascent, descent, line_gap : i32 )
|
||||
{
|
||||
switch font.kind
|
||||
{
|
||||
case .Freetype:
|
||||
|
||||
case .STB_TrueType:
|
||||
stbtt.GetFontVMetrics( & font.stbtt_info, & ascent, & descent, & line_gap )
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
parser_get_glyph_box :: #force_inline proc ( font : ^ParserFontInfo, glyph_index : Glyph ) -> (bounds_0, bounds_1 : Vec2i)
|
||||
{
|
||||
switch font.kind
|
||||
{
|
||||
case .Freetype:
|
||||
freetype.load_glyph( font.freetype_info, c.uint(glyph_index), { .No_Bitmap, .No_Hinting, .No_Scale } )
|
||||
|
||||
metrics := font.freetype_info.glyph.metrics
|
||||
|
||||
bounds_0 = {i32(metrics.hori_bearing_x), i32(metrics.hori_bearing_y - metrics.height)}
|
||||
bounds_1 = {i32(metrics.hori_bearing_x + metrics.width), i32(metrics.hori_bearing_y)}
|
||||
|
||||
case .STB_TrueType:
|
||||
x0, y0, x1, y1 : i32
|
||||
success := cast(bool) stbtt.GetGlyphBox( & font.stbtt_info, i32(glyph_index), & x0, & y0, & x1, & y1 )
|
||||
assert( success )
|
||||
|
||||
bounds_0 = { i32(x0), i32(y0) }
|
||||
bounds_1 = { i32(x1), i32(y1) }
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
parser_get_glyph_shape :: proc( font : ^ParserFontInfo, glyph_index : Glyph ) -> (shape : ParserGlyphShape, error : AllocatorError)
|
||||
{
|
||||
switch font.kind
|
||||
{
|
||||
case .Freetype:
|
||||
error := freetype.load_glyph( font.freetype_info, cast(u32) glyph_index, { .No_Bitmap, .No_Hinting, .No_Scale } )
|
||||
if error != .Ok {
|
||||
return
|
||||
}
|
||||
|
||||
glyph := font.freetype_info.glyph
|
||||
if glyph.format != .Outline {
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
convert freetype outline to stb_truetype shape
|
||||
|
||||
freetype docs: https://freetype.org/freetype2/docs/glyphs/glyphs-6.html
|
||||
|
||||
stb_truetype shape info:
|
||||
The shape is a series of contours. Each one starts with
|
||||
a STBTT_moveto, then consists of a series of mixed
|
||||
STBTT_lineto and STBTT_curveto segments. A lineto
|
||||
draws a line from previous endpoint to its x,y; a curveto
|
||||
draws a quadratic bezier from previous endpoint to
|
||||
its x,y, using cx,cy as the bezier control point.
|
||||
*/
|
||||
{
|
||||
FT_CURVE_TAG_CONIC :: 0x00
|
||||
FT_CURVE_TAG_ON :: 0x01
|
||||
FT_CURVE_TAG_CUBIC :: 0x02
|
||||
|
||||
vertices, error := make( [dynamic]ParserGlyphVertex, 1024 )
|
||||
assert( error == .None )
|
||||
|
||||
// TODO(Ed): This makes freetype second class I guess but VEFontCache doesn't have native support for freetype originally so....
|
||||
outline := & glyph.outline
|
||||
|
||||
contours := transmute( [^]u16) outline.contours
|
||||
points := transmute( [^]freetype.Vector) outline.points
|
||||
tags := transmute( [^]u8) outline.tags
|
||||
|
||||
// TODO(Ed): Review this, never tested before and its problably bad.
|
||||
for contour : i32 = 0; contour < i32(outline.n_contours); contour += 1
|
||||
{
|
||||
start := (contour == 0) ? 0 : i32(contours[ contour - 1 ] + 1)
|
||||
end := i32(contours[ contour ])
|
||||
|
||||
for index := start; index < i32(outline.n_points); index += 1
|
||||
{
|
||||
point := points[ index ]
|
||||
tag := tags[ index ]
|
||||
|
||||
if (tag & FT_CURVE_TAG_ON) != 0
|
||||
{
|
||||
if len(vertices) > 0 && !(vertices[len(vertices) - 1].type == .Move )
|
||||
{
|
||||
// Close the previous contour if needed
|
||||
append(& vertices, ParserGlyphVertex { type = .Line,
|
||||
x = i16(points[start].x), y = i16(points[start].y),
|
||||
contour_x0 = i16(0), contour_y0 = i16(0),
|
||||
contour_x1 = i16(0), contour_y1 = i16(0),
|
||||
padding = 0,
|
||||
})
|
||||
}
|
||||
|
||||
append(& vertices, ParserGlyphVertex { type = .Move,
|
||||
x = i16(point.x), y = i16(point.y),
|
||||
contour_x0 = i16(0), contour_y0 = i16(0),
|
||||
contour_x1 = i16(0), contour_y1 = i16(0),
|
||||
padding = 0,
|
||||
})
|
||||
}
|
||||
else if (tag & FT_CURVE_TAG_CUBIC) != 0
|
||||
{
|
||||
point1 := points[ index + 1 ]
|
||||
point2 := points[ index + 2 ]
|
||||
append(& vertices, ParserGlyphVertex { type = .Cubic,
|
||||
x = i16(point2.x), y = i16(point2.y),
|
||||
contour_x0 = i16(point.x), contour_y0 = i16(point.y),
|
||||
contour_x1 = i16(point1.x), contour_y1 = i16(point1.y),
|
||||
padding = 0,
|
||||
})
|
||||
index += 2
|
||||
}
|
||||
else if (tag & FT_CURVE_TAG_CONIC) != 0
|
||||
{
|
||||
// TODO(Ed): This is using a very dead simple algo to convert the conic to a cubic curve
|
||||
// not sure if we need something more sophisticaated
|
||||
point1 := points[ index + 1 ]
|
||||
|
||||
control_conv :: f32(0.5) // Conic to cubic control point distance
|
||||
to_float := f32(1.0 / 64.0)
|
||||
|
||||
fp := Vec2 { f32(point.x), f32(point.y) } * to_float
|
||||
fp1 := Vec2 { f32(point1.x), f32(point1.y) } * to_float
|
||||
|
||||
control1 := freetype.Vector {
|
||||
point.x + freetype.Pos( (fp1.x - fp.x) * control_conv * 64.0 ),
|
||||
point.y + freetype.Pos( (fp1.y - fp.y) * control_conv * 64.0 ),
|
||||
}
|
||||
control2 := freetype.Vector {
|
||||
point1.x + freetype.Pos( (fp.x - fp1.x) * control_conv * 64.0 ),
|
||||
point1.y + freetype.Pos( (fp.y - fp1.y) * control_conv * 64.0 ),
|
||||
}
|
||||
append(& vertices, ParserGlyphVertex { type = .Cubic,
|
||||
x = i16(point1.x), y = i16(point1.y),
|
||||
contour_x0 = i16(control1.x), contour_y0 = i16(control1.y),
|
||||
contour_x1 = i16(control2.x), contour_y1 = i16(control2.y),
|
||||
padding = 0,
|
||||
})
|
||||
index += 1
|
||||
}
|
||||
else
|
||||
{
|
||||
append(& vertices, ParserGlyphVertex { type = .Line,
|
||||
x = i16(point.x), y = i16(point.y),
|
||||
contour_x0 = i16(0), contour_y0 = i16(0),
|
||||
contour_x1 = i16(0), contour_y1 = i16(0),
|
||||
padding = 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Close contour
|
||||
append(& vertices, ParserGlyphVertex { type = .Line,
|
||||
x = i16(points[start].x), y = i16(points[start].y),
|
||||
contour_x0 = i16(0), contour_y0 = i16(0),
|
||||
contour_x1 = i16(0), contour_y1 = i16(0),
|
||||
padding = 0,
|
||||
})
|
||||
}
|
||||
|
||||
shape = vertices
|
||||
}
|
||||
|
||||
case .STB_TrueType:
|
||||
stb_shape : [^]stbtt.vertex
|
||||
nverts := stbtt.GetGlyphShape( & font.stbtt_info, cast(i32) glyph_index, & stb_shape )
|
||||
|
||||
shape_raw := transmute( ^runtime.Raw_Dynamic_Array) & shape
|
||||
shape_raw.data = stb_shape
|
||||
shape_raw.len = int(nverts)
|
||||
shape_raw.cap = int(nverts)
|
||||
shape_raw.allocator = runtime.nil_allocator()
|
||||
error = AllocatorError.None
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
parser_is_glyph_empty :: #force_inline proc "contextless" ( font : ^ParserFontInfo, glyph_index : Glyph ) -> b32
|
||||
{
|
||||
switch font.kind
|
||||
{
|
||||
case .Freetype:
|
||||
error := freetype.load_glyph( font.freetype_info, cast(u32) glyph_index, { .No_Bitmap, .No_Hinting, .No_Scale } )
|
||||
if error == .Ok
|
||||
{
|
||||
if font.freetype_info.glyph.format == .Outline {
|
||||
return font.freetype_info.glyph.outline.n_points == 0
|
||||
}
|
||||
else if font.freetype_info.glyph.format == .Bitmap {
|
||||
return font.freetype_info.glyph.bitmap.width == 0 && font.freetype_info.glyph.bitmap.rows == 0;
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
||||
case .STB_TrueType:
|
||||
return stbtt.IsGlyphEmpty( & font.stbtt_info, cast(c.int) glyph_index )
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
parser_scale :: #force_inline proc "contextless" ( font : ^ParserFontInfo, size : f32 ) -> f32
|
||||
{
|
||||
size_scale := size < 0.0 ? \
|
||||
parser_scale_for_pixel_height( font, -size ) \
|
||||
: parser_scale_for_mapping_em_to_pixels( font, size )
|
||||
// size_scale = 1.0
|
||||
return size_scale
|
||||
}
|
||||
|
||||
parser_scale_for_pixel_height :: #force_inline proc "contextless" ( font : ^ParserFontInfo, size : f32 ) -> f32
|
||||
{
|
||||
switch font.kind {
|
||||
case .Freetype:
|
||||
freetype.set_pixel_sizes( font.freetype_info, 0, cast(u32) size )
|
||||
size_scale := size / cast(f32)font.freetype_info.units_per_em
|
||||
return size_scale
|
||||
|
||||
case.STB_TrueType:
|
||||
return stbtt.ScaleForPixelHeight( & font.stbtt_info, size )
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
parser_scale_for_mapping_em_to_pixels :: #force_inline proc "contextless" ( font : ^ParserFontInfo, size : f32 ) -> f32
|
||||
{
|
||||
switch font.kind {
|
||||
case .Freetype:
|
||||
Inches_To_CM :: cast(f32) 2.54
|
||||
Points_Per_CM :: cast(f32) 28.3465
|
||||
CM_Per_Point :: cast(f32) 1.0 / DPT_DPCM
|
||||
CM_Per_Pixel :: cast(f32) 1.0 / DPT_PPCM
|
||||
DPT_DPCM :: cast(f32) 72.0 * Inches_To_CM // 182.88 points/dots per cm
|
||||
DPT_PPCM :: cast(f32) 96.0 * Inches_To_CM // 243.84 pixels per cm
|
||||
DPT_DPI :: cast(f32) 72.0
|
||||
|
||||
// TODO(Ed): Don't assume the dots or pixels per inch.
|
||||
system_dpi :: DPT_DPI
|
||||
|
||||
FT_Font_Size_Point_Unit :: 1.0 / 64.0
|
||||
FT_Point_10 :: 64.0
|
||||
|
||||
points_per_em := (size / system_dpi ) * DPT_DPI
|
||||
freetype.set_char_size( font.freetype_info, 0, cast(freetype.F26Dot6) f32(points_per_em * FT_Point_10), cast(u32) DPT_DPI, cast(u32) DPT_DPI )
|
||||
size_scale := f32(f64(size) / cast(f64) font.freetype_info.units_per_em)
|
||||
return size_scale
|
||||
|
||||
case .STB_TrueType:
|
||||
return stbtt.ScaleForMappingEmToPixels( & font.stbtt_info, size )
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
when false {
|
||||
parser_convert_conic_to_cubic_freetype :: proc( vertices : Array(ParserGlyphVertex), p0, p1, p2 : freetype.Vector, tolerance : f32 )
|
||||
{
|
||||
scratch : [Kilobyte * 4]u8
|
||||
scratch_arena : Arena; arena_init(& scratch_arena, scratch[:])
|
||||
|
||||
points, error := make( Array(freetype.Vector), 256, allocator = arena_allocator( &scratch_arena) )
|
||||
assert(error == .None)
|
||||
|
||||
append( & points, p0)
|
||||
append( & points, p1)
|
||||
append( & points, p2)
|
||||
|
||||
to_float : f32 = 1.0 / 64.0
|
||||
control_conv :: f32(2.0 / 3.0) // Conic to cubic control point distance
|
||||
|
||||
for ; points.num > 1; {
|
||||
p0 := points.data[0]
|
||||
p1 := points.data[1]
|
||||
p2 := points.data[2]
|
||||
|
||||
fp0 := Vec2{ f32(p0.x), f32(p0.y) } * to_float
|
||||
fp1 := Vec2{ f32(p1.x), f32(p1.y) } * to_float
|
||||
fp2 := Vec2{ f32(p2.x), f32(p2.y) } * to_float
|
||||
|
||||
delta_x := fp0.x - 2 * fp1.x + fp2.x;
|
||||
delta_y := fp0.y - 2 * fp1.y + fp2.y;
|
||||
distance := math.sqrt(delta_x * delta_x + delta_y * delta_y);
|
||||
|
||||
if distance <= tolerance
|
||||
{
|
||||
control1 := {
|
||||
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
control2 := {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
135
code/font/vefontcache/shaped_text.odin
Normal file
135
code/font/vefontcache/shaped_text.odin
Normal file
@ -0,0 +1,135 @@
|
||||
package VEFontCache
|
||||
|
||||
ShapedText :: struct {
|
||||
glyphs : [dynamic]Glyph,
|
||||
positions : [dynamic]Vec2,
|
||||
end_cursor_pos : Vec2,
|
||||
size : Vec2,
|
||||
}
|
||||
|
||||
ShapedTextCache :: struct {
|
||||
storage : [dynamic]ShapedText,
|
||||
state : LRU_Cache,
|
||||
next_cache_id : i32,
|
||||
}
|
||||
|
||||
shape_lru_hash :: #force_inline proc "contextless" ( hash : ^u64, bytes : []byte ) {
|
||||
for value in bytes {
|
||||
(hash^) = (( (hash^) << 8) + (hash^) ) + u64(value)
|
||||
}
|
||||
}
|
||||
|
||||
shape_text_cached :: proc( ctx : ^Context, font : FontID, text_utf8 : string, entry : ^Entry ) -> ^ShapedText
|
||||
{
|
||||
// profile(#procedure)
|
||||
font := font
|
||||
font_bytes := slice_ptr( transmute(^byte) & font, size_of(FontID) )
|
||||
text_bytes := transmute( []byte) text_utf8
|
||||
|
||||
lru_code : u64
|
||||
shape_lru_hash( & lru_code, font_bytes )
|
||||
shape_lru_hash( & lru_code, text_bytes )
|
||||
|
||||
shape_cache := & ctx.shape_cache
|
||||
state := & ctx.shape_cache.state
|
||||
|
||||
shape_cache_idx := LRU_get( state, lru_code )
|
||||
if shape_cache_idx == -1
|
||||
{
|
||||
if shape_cache.next_cache_id < i32(state.capacity) {
|
||||
shape_cache_idx = shape_cache.next_cache_id
|
||||
shape_cache.next_cache_id += 1
|
||||
evicted := LRU_put( state, lru_code, shape_cache_idx )
|
||||
}
|
||||
else
|
||||
{
|
||||
next_evict_idx := LRU_get_next_evicted( state )
|
||||
assert( next_evict_idx != 0xFFFFFFFFFFFFFFFF )
|
||||
|
||||
shape_cache_idx = LRU_peek( state, next_evict_idx, must_find = true )
|
||||
assert( shape_cache_idx != - 1 )
|
||||
|
||||
LRU_put( state, lru_code, shape_cache_idx )
|
||||
}
|
||||
|
||||
shape_entry := & shape_cache.storage[ shape_cache_idx ]
|
||||
shape_text_uncached( ctx, font, text_utf8, entry, shape_entry )
|
||||
}
|
||||
|
||||
return & shape_cache.storage[ shape_cache_idx ]
|
||||
}
|
||||
|
||||
shape_text_uncached :: proc( ctx : ^Context, font : FontID, text_utf8 : string, entry : ^Entry, output : ^ShapedText )
|
||||
{
|
||||
// profile(#procedure)
|
||||
assert( ctx != nil )
|
||||
assert( font >= 0 && int(font) < len(ctx.entries) )
|
||||
|
||||
use_full_text_shape := ctx.text_shape_adv
|
||||
|
||||
clear( & output.glyphs )
|
||||
clear( & output.positions )
|
||||
|
||||
ascent_i32, descent_i32, line_gap_i32 := parser_get_font_vertical_metrics( & entry.parser_info )
|
||||
ascent := f32(ascent_i32)
|
||||
descent := f32(descent_i32)
|
||||
line_gap := f32(line_gap_i32)
|
||||
line_height := (ascent - descent + line_gap) * entry.size_scale
|
||||
|
||||
if use_full_text_shape
|
||||
{
|
||||
// assert( entry.shaper_info != nil )
|
||||
shaper_shape_from_text( & ctx.shaper_ctx, & entry.shaper_info, output, text_utf8, ascent_i32, descent_i32, line_gap_i32, entry.size, entry.size_scale )
|
||||
// TODO(Ed): Need to be able to provide the text height as well
|
||||
return
|
||||
}
|
||||
else
|
||||
{
|
||||
// Note(Original Author):
|
||||
// We use our own fallback dumbass text shaping.
|
||||
// WARNING: PLEASE USE HARFBUZZ. GOOD TEXT SHAPING IS IMPORTANT FOR INTERNATIONALISATION.
|
||||
|
||||
line_count : int = 1
|
||||
max_line_width : f32 = 0
|
||||
position : Vec2
|
||||
|
||||
prev_codepoint : rune
|
||||
for codepoint in text_utf8
|
||||
{
|
||||
if prev_codepoint > 0 {
|
||||
kern := parser_get_codepoint_kern_advance( & entry.parser_info, prev_codepoint, codepoint )
|
||||
position.x += f32(kern) * entry.size_scale
|
||||
}
|
||||
if codepoint == '\n'
|
||||
{
|
||||
line_count += 1
|
||||
max_line_width = max(max_line_width, position.x)
|
||||
position.x = 0.0
|
||||
position.y -= line_height
|
||||
position.y = ceil(position.y)
|
||||
prev_codepoint = rune(0)
|
||||
continue
|
||||
}
|
||||
if abs( entry.size ) <= Advance_Snap_Smallfont_Size {
|
||||
position.x = position.x
|
||||
}
|
||||
|
||||
append( & output.glyphs, parser_find_glyph_index( & entry.parser_info, codepoint ))
|
||||
advance, _ := parser_get_codepoint_horizontal_metrics( & entry.parser_info, codepoint )
|
||||
|
||||
append( & output.positions, Vec2 {
|
||||
ctx.snap_shape_pos ? ceil(position.x) : position.x,
|
||||
position.y
|
||||
})
|
||||
|
||||
position.x += ctx.snap_shape_pos ? ceil(f32(advance) * entry.size_scale) : f32(advance) * entry.size_scale
|
||||
prev_codepoint = codepoint
|
||||
}
|
||||
|
||||
output.end_cursor_pos = position
|
||||
max_line_width = max(max_line_width, position.x)
|
||||
|
||||
output.size.x = ceil(max_line_width)
|
||||
output.size.y = f32(line_count) * line_height
|
||||
}
|
||||
}
|
165
code/font/vefontcache/shaper.odin
Normal file
165
code/font/vefontcache/shaper.odin
Normal file
@ -0,0 +1,165 @@
|
||||
package VEFontCache
|
||||
/*
|
||||
Note(Ed): The only reason I didn't directly use harfbuzz is because hamza exists and seems to be under active development as an alternative.
|
||||
*/
|
||||
|
||||
import "core:c"
|
||||
import "thirdparty:harfbuzz"
|
||||
|
||||
ShaperKind :: enum {
|
||||
Naive = 0,
|
||||
Harfbuzz = 1,
|
||||
}
|
||||
|
||||
ShaperContext :: struct {
|
||||
hb_buffer : harfbuzz.Buffer,
|
||||
// infos : HMapChained(ShaperInfo),
|
||||
}
|
||||
|
||||
ShaperInfo :: struct {
|
||||
blob : harfbuzz.Blob,
|
||||
face : harfbuzz.Face,
|
||||
font : harfbuzz.Font,
|
||||
}
|
||||
|
||||
shaper_init :: proc( ctx : ^ShaperContext )
|
||||
{
|
||||
ctx.hb_buffer = harfbuzz.buffer_create()
|
||||
assert( ctx.hb_buffer != nil, "VEFontCache.shaper_init: Failed to create harfbuzz buffer")
|
||||
|
||||
// error : AllocatorError
|
||||
// ctx.infos, error = make( HMapChained(ShaperInfo), 256 )
|
||||
// assert( error == .None, "VEFontCache.shaper_init: Failed to create shaper infos map" )
|
||||
}
|
||||
|
||||
shaper_shutdown :: proc( ctx : ^ShaperContext )
|
||||
{
|
||||
if ctx.hb_buffer != nil {
|
||||
harfbuzz.buffer_destroy( ctx.hb_buffer )
|
||||
}
|
||||
|
||||
// delete(& ctx.infos)
|
||||
}
|
||||
|
||||
shaper_load_font :: proc( ctx : ^ShaperContext, label : string, data : []byte, user_data : rawptr ) -> (info : ShaperInfo)
|
||||
{
|
||||
// key := font_key_from_label( label )
|
||||
// info = get( ctx.infos, key )
|
||||
// if info != nil do return
|
||||
|
||||
// error : AllocatorError
|
||||
// info, error = set( ctx.infos, key, ShaperInfo {} )
|
||||
// assert( error == .None, "VEFontCache.parser_load_font: Failed to set a new shaper info" )
|
||||
|
||||
using info
|
||||
blob = harfbuzz.blob_create( raw_data(data), cast(c.uint) len(data), harfbuzz.Memory_Mode.READONLY, user_data, nil )
|
||||
face = harfbuzz.face_create( blob, 0 )
|
||||
font = harfbuzz.font_create( face )
|
||||
return
|
||||
}
|
||||
|
||||
shaper_unload_font :: proc( ctx : ^ShaperInfo )
|
||||
{
|
||||
using ctx
|
||||
if blob != nil do harfbuzz.font_destroy( font )
|
||||
if face != nil do harfbuzz.face_destroy( face )
|
||||
if blob != nil do harfbuzz.blob_destroy( blob )
|
||||
}
|
||||
|
||||
shaper_shape_from_text :: proc( ctx : ^ShaperContext, info : ^ShaperInfo, output :^ShapedText, text_utf8 : string,
|
||||
ascent, descent, line_gap : i32, size, size_scale : f32 )
|
||||
{
|
||||
// profile(#procedure)
|
||||
current_script := harfbuzz.Script.UNKNOWN
|
||||
hb_ucfunc := harfbuzz.unicode_funcs_get_default()
|
||||
harfbuzz.buffer_clear_contents( ctx.hb_buffer )
|
||||
assert( info.font != nil )
|
||||
|
||||
ascent := f32(ascent)
|
||||
descent := f32(descent)
|
||||
line_gap := f32(line_gap)
|
||||
|
||||
position, vertical_position : f32
|
||||
shape_run :: proc( buffer : harfbuzz.Buffer, script : harfbuzz.Script, font : harfbuzz.Font, output : ^ShapedText,
|
||||
position, vertical_position : ^f32,
|
||||
ascent, descent, line_gap, size, size_scale : f32 )
|
||||
{
|
||||
// Set script and direction. We use the system's default langauge.
|
||||
// script = HB_SCRIPT_LATIN
|
||||
harfbuzz.buffer_set_script( buffer, script )
|
||||
harfbuzz.buffer_set_direction( buffer, harfbuzz.script_get_horizontal_direction( script ))
|
||||
harfbuzz.buffer_set_language( buffer, harfbuzz.language_get_default() )
|
||||
|
||||
// Perform the actual shaping of this run using HarfBuzz.
|
||||
harfbuzz.shape( font, buffer, nil, 0 )
|
||||
|
||||
// Loop over glyphs and append to output buffer.
|
||||
glyph_count : u32
|
||||
glyph_infos := harfbuzz.buffer_get_glyph_infos( buffer, & glyph_count )
|
||||
glyph_positions := harfbuzz.buffer_get_glyph_positions( buffer, & glyph_count )
|
||||
|
||||
for index : i32; index < i32(glyph_count); index += 1
|
||||
{
|
||||
hb_glyph := glyph_infos[ index ]
|
||||
hb_gposition := glyph_positions[ index ]
|
||||
glyph_id := cast(Glyph) hb_glyph.codepoint
|
||||
|
||||
if hb_glyph.cluster > 0
|
||||
{
|
||||
(position^) = 0.0
|
||||
(vertical_position^) -= (ascent - descent + line_gap) * size_scale
|
||||
(vertical_position^) = cast(f32) i32(vertical_position^ + 0.5)
|
||||
continue
|
||||
}
|
||||
if abs( size ) <= Advance_Snap_Smallfont_Size
|
||||
{
|
||||
(position^) = ceil( position^ )
|
||||
}
|
||||
|
||||
append( & output.glyphs, glyph_id )
|
||||
|
||||
pos := position^
|
||||
v_pos := vertical_position^
|
||||
offset_x := f32(hb_gposition.x_offset) * size_scale
|
||||
offset_y := f32(hb_gposition.y_offset) * size_scale
|
||||
append( & output.positions, Vec2 { cast(f32) i32( pos + offset_x + 0.5 ),
|
||||
v_pos + offset_y,
|
||||
})
|
||||
|
||||
(position^) += f32(hb_gposition.x_advance) * size_scale
|
||||
(vertical_position^) += f32(hb_gposition.y_advance) * size_scale
|
||||
}
|
||||
|
||||
output.end_cursor_pos.x = position^
|
||||
output.end_cursor_pos.y = vertical_position^
|
||||
harfbuzz.buffer_clear_contents( buffer )
|
||||
}
|
||||
|
||||
// Note(Original Author):
|
||||
// We first start with simple bidi and run logic.
|
||||
// True CTL is pretty hard and we don't fully support that; patches welcome!
|
||||
|
||||
for codepoint, byte_offset in text_utf8
|
||||
{
|
||||
script := harfbuzz.unicode_script( hb_ucfunc, cast(harfbuzz.Codepoint) codepoint )
|
||||
|
||||
// Can we continue the current run?
|
||||
ScriptKind :: harfbuzz.Script
|
||||
|
||||
special_script : b32 = script == ScriptKind.UNKNOWN || script == ScriptKind.INHERITED || script == ScriptKind.COMMON
|
||||
if special_script || script == current_script {
|
||||
harfbuzz.buffer_add( ctx.hb_buffer, cast(harfbuzz.Codepoint) codepoint, codepoint == '\n' ? 1 : 0 )
|
||||
current_script = special_script ? current_script : script
|
||||
continue
|
||||
}
|
||||
|
||||
// End current run since we've encountered a script change.
|
||||
shape_run( ctx.hb_buffer, current_script, info.font, output, & position, & vertical_position, ascent, descent, line_gap, size, size_scale )
|
||||
harfbuzz.buffer_add( ctx.hb_buffer, cast(harfbuzz.Codepoint) codepoint, codepoint == '\n' ? 1 : 0 )
|
||||
current_script = script
|
||||
}
|
||||
|
||||
// End the last run if needed
|
||||
shape_run( ctx.hb_buffer, current_script, info.font, output, & position, & vertical_position, ascent, descent, line_gap, size, size_scale )
|
||||
return
|
||||
}
|
501
code/font/vefontcache/vefontcache.odin
Normal file
501
code/font/vefontcache/vefontcache.odin
Normal file
@ -0,0 +1,501 @@
|
||||
/*
|
||||
A port of (https://github.com/hypernewbie/VEFontCache) to Odin.
|
||||
|
||||
See: https://github.com/Ed94/VEFontCache-Odin
|
||||
*/
|
||||
package VEFontCache
|
||||
|
||||
import "base:runtime"
|
||||
|
||||
Advance_Snap_Smallfont_Size :: 0
|
||||
|
||||
FontID :: distinct i64
|
||||
Glyph :: distinct i32
|
||||
|
||||
Entry :: struct {
|
||||
parser_info : ParserFontInfo,
|
||||
shaper_info : ShaperInfo,
|
||||
id : FontID,
|
||||
used : b32,
|
||||
curve_quality : f32,
|
||||
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 : [dynamic]Entry,
|
||||
|
||||
temp_path : [dynamic]Vertex,
|
||||
temp_codepoint_seen : map[u64]bool,
|
||||
temp_codepoint_seen_num : int,
|
||||
|
||||
snap_width : f32,
|
||||
snap_height : f32,
|
||||
|
||||
colour : Colour,
|
||||
cursor_pos : Vec2,
|
||||
|
||||
draw_layer : struct {
|
||||
vertices_offset : int,
|
||||
indices_offset : int,
|
||||
calls_offset : int,
|
||||
},
|
||||
|
||||
draw_list : DrawList,
|
||||
atlas : Atlas,
|
||||
glyph_buffer : GlyphDrawBuffer,
|
||||
shape_cache : ShapedTextCache,
|
||||
|
||||
default_curve_quality : i32,
|
||||
text_shape_adv : b32,
|
||||
snap_shape_pos : b32,
|
||||
|
||||
debug_print : b32,
|
||||
debug_print_verbose : b32,
|
||||
}
|
||||
|
||||
#region("lifetime")
|
||||
|
||||
InitAtlasRegionParams :: struct {
|
||||
width : u32,
|
||||
height : u32,
|
||||
}
|
||||
|
||||
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 = 4096,
|
||||
height = 2048,
|
||||
glyph_padding = 4,
|
||||
|
||||
region_a = {
|
||||
width = 32,
|
||||
height = 32,
|
||||
},
|
||||
region_b = {
|
||||
width = 32,
|
||||
height = 64,
|
||||
},
|
||||
region_c = {
|
||||
width = 64,
|
||||
height = 64,
|
||||
},
|
||||
region_d = {
|
||||
width = 128,
|
||||
height = 128,
|
||||
}
|
||||
}
|
||||
|
||||
InitGlyphDrawParams :: struct {
|
||||
over_sample : Vec2i,
|
||||
buffer_batch : u32,
|
||||
draw_padding : u32,
|
||||
}
|
||||
|
||||
InitGlyphDrawParams_Default :: InitGlyphDrawParams {
|
||||
over_sample = { 8, 8 },
|
||||
buffer_batch = 4,
|
||||
draw_padding = InitAtlasParams_Default.glyph_padding,
|
||||
}
|
||||
|
||||
InitShapeCacheParams :: struct {
|
||||
capacity : u32,
|
||||
reserve_length : u32,
|
||||
}
|
||||
|
||||
InitShapeCacheParams_Default :: InitShapeCacheParams {
|
||||
capacity = 8 * 1024,
|
||||
reserve_length = 256,
|
||||
}
|
||||
|
||||
// ve_fontcache_init
|
||||
startup :: proc( ctx : ^Context, parser_kind : ParserKind,
|
||||
allocator := context.allocator,
|
||||
atlas_params := InitAtlasParams_Default,
|
||||
glyph_draw_params := InitGlyphDrawParams_Default,
|
||||
shape_cache_params := InitShapeCacheParams_Default,
|
||||
snap_shape_position : b32 = true,
|
||||
default_curve_quality : u32 = 3,
|
||||
entires_reserve : u32 = 512,
|
||||
temp_path_reserve : u32 = 1024,
|
||||
temp_codepoint_seen_reserve : u32 = 2048,
|
||||
)
|
||||
{
|
||||
assert( ctx != nil, "Must provide a valid context" )
|
||||
using ctx
|
||||
|
||||
ctx.backing = allocator
|
||||
context.allocator = ctx.backing
|
||||
|
||||
snap_shape_pos = snap_shape_position
|
||||
|
||||
if default_curve_quality == 0 {
|
||||
default_curve_quality = 3
|
||||
}
|
||||
ctx.default_curve_quality = default_curve_quality
|
||||
|
||||
error : AllocatorError
|
||||
entries, error = make( [dynamic]Entry, len = 0, cap = entires_reserve )
|
||||
assert(error == .None, "VEFontCache.init : Failed to allocate entries")
|
||||
|
||||
temp_path, error = make( [dynamic]Vertex, len = 0, cap = temp_path_reserve )
|
||||
assert(error == .None, "VEFontCache.init : Failed to allocate temp_path")
|
||||
|
||||
temp_codepoint_seen, error = make( map[u64]bool, uint(temp_codepoint_seen_reserve) )
|
||||
assert(error == .None, "VEFontCache.init : Failed to allocate temp_path")
|
||||
|
||||
draw_list.vertices, error = make( [dynamic]Vertex, len = 0, cap = 4 * Kilobyte )
|
||||
assert(error == .None, "VEFontCache.init : Failed to allocate draw_list.vertices")
|
||||
|
||||
draw_list.indices, error = make( [dynamic]u32, len = 0, cap = 8 * Kilobyte )
|
||||
assert(error == .None, "VEFontCache.init : Failed to allocate draw_list.indices")
|
||||
|
||||
draw_list.calls, error = make( [dynamic]DrawCall, len = 0, cap = 512 )
|
||||
assert(error == .None, "VEFontCache.init : Failed to allocate draw_list.calls")
|
||||
|
||||
init_atlas_region :: proc( region : ^AtlasRegion, params : InitAtlasParams, region_params : InitAtlasRegionParams, factor : Vec2i, expected_cap : i32 )
|
||||
{
|
||||
using region
|
||||
|
||||
next_idx = 0;
|
||||
width = i32(region_params.width)
|
||||
height = i32(region_params.height)
|
||||
size = {
|
||||
i32(params.width) / factor.x,
|
||||
i32(params.height) / factor.y,
|
||||
}
|
||||
capacity = {
|
||||
size.x / i32(width),
|
||||
size.y / i32(height),
|
||||
}
|
||||
assert( capacity.x * capacity.y == expected_cap )
|
||||
|
||||
error : AllocatorError
|
||||
LRU_init( & state, capacity.x * capacity.y )
|
||||
}
|
||||
init_atlas_region( & atlas.region_a, atlas_params, atlas_params.region_a, { 4, 2}, 1024 )
|
||||
init_atlas_region( & atlas.region_b, atlas_params, atlas_params.region_b, { 4, 2}, 512 )
|
||||
init_atlas_region( & atlas.region_c, atlas_params, atlas_params.region_c, { 4, 1}, 512 )
|
||||
init_atlas_region( & atlas.region_d, atlas_params, atlas_params.region_d, { 2, 1}, 256 )
|
||||
|
||||
atlas.width = i32(atlas_params.width)
|
||||
atlas.height = i32(atlas_params.height)
|
||||
atlas.glyph_padding = i32(atlas_params.glyph_padding)
|
||||
|
||||
atlas.region_a.offset = {0, 0}
|
||||
atlas.region_b.offset.x = 0
|
||||
atlas.region_b.offset.y = atlas.region_a.size.y
|
||||
atlas.region_c.offset.x = atlas.region_a.size.x
|
||||
atlas.region_c.offset.y = 0
|
||||
atlas.region_d.offset.x = atlas.width / 2
|
||||
atlas.region_d.offset.y = 0
|
||||
|
||||
LRU_init( & shape_cache.state, i32(shape_cache_params.capacity) )
|
||||
|
||||
shape_cache.storage, error = make( [dynamic]ShapedText, shape_cache_params.capacity )
|
||||
assert(error == .None, "VEFontCache.init : Failed to allocate shape_cache.storage")
|
||||
|
||||
for idx : u32 = 0; idx < shape_cache_params.capacity; idx += 1 {
|
||||
stroage_entry := & shape_cache.storage[idx]
|
||||
using stroage_entry
|
||||
glyphs, error = make( [dynamic]Glyph, len = 0, cap = shape_cache_params.reserve_length )
|
||||
assert( error == .None, "VEFontCache.init : Failed to allocate glyphs array for shape cache storage" )
|
||||
|
||||
positions, error = make( [dynamic]Vec2, len = 0, cap = shape_cache_params.reserve_length )
|
||||
assert( error == .None, "VEFontCache.init : Failed to allocate positions array for shape cache storage" )
|
||||
|
||||
draw_list.calls, error = make( [dynamic]DrawCall, len = 0, cap = glyph_draw_params.buffer_batch * 2 )
|
||||
assert( error == .None, "VEFontCache.init : Failed to allocate calls for draw_list" )
|
||||
|
||||
draw_list.indices, error = make( [dynamic]u32, len = 0, cap = glyph_draw_params.buffer_batch * 2 * 6 )
|
||||
assert( error == .None, "VEFontCache.init : Failed to allocate indices array for draw_list" )
|
||||
|
||||
draw_list.vertices, error = make( [dynamic]Vertex, len = 0, cap = glyph_draw_params.buffer_batch * 2 * 4 )
|
||||
assert( error == .None, "VEFontCache.init : Failed to allocate vertices array for draw_list" )
|
||||
}
|
||||
|
||||
// Note(From original author): We can actually go over VE_FONTCACHE_GLYPHDRAW_BUFFER_BATCH batches due to smart packing!
|
||||
{
|
||||
using glyph_buffer
|
||||
over_sample = vec2(glyph_draw_params.over_sample)
|
||||
batch = cast(i32) glyph_draw_params.buffer_batch
|
||||
width = atlas.region_d.width * i32(over_sample.x) * batch
|
||||
height = atlas.region_d.height * i32(over_sample.y)
|
||||
draw_padding = cast(i32) glyph_draw_params.draw_padding
|
||||
|
||||
draw_list.calls, error = make( [dynamic]DrawCall, len = 0, cap = glyph_draw_params.buffer_batch * 2 )
|
||||
assert( error == .None, "VEFontCache.init : Failed to allocate calls for draw_list" )
|
||||
|
||||
draw_list.indices, error = make( [dynamic]u32, len = 0, cap = glyph_draw_params.buffer_batch * 2 * 6 )
|
||||
assert( error == .None, "VEFontCache.init : Failed to allocate indices array for draw_list" )
|
||||
|
||||
draw_list.vertices, error = make( [dynamic]Vertex, len = 0, cap = glyph_draw_params.buffer_batch * 2 * 4 )
|
||||
assert( error == .None, "VEFontCache.init : Failed to allocate vertices array for draw_list" )
|
||||
|
||||
clear_draw_list.calls, error = make( [dynamic]DrawCall, len = 0, cap = glyph_draw_params.buffer_batch * 2 )
|
||||
assert( error == .None, "VEFontCache.init : Failed to allocate calls for calls for clear_draw_list" )
|
||||
|
||||
clear_draw_list.indices, error = make( [dynamic]u32, len = 0, cap = glyph_draw_params.buffer_batch * 2 * 4 )
|
||||
assert( error == .None, "VEFontCache.init : Failed to allocate calls for indices array for clear_draw_list" )
|
||||
|
||||
clear_draw_list.vertices, error = make( [dynamic]Vertex, len = 0, cap = glyph_draw_params.buffer_batch * 2 * 4 )
|
||||
assert( error == .None, "VEFontCache.init : Failed to allocate vertices array for clear_draw_list" )
|
||||
}
|
||||
|
||||
parser_init( & parser_ctx )
|
||||
shaper_init( & shaper_ctx )
|
||||
}
|
||||
|
||||
hot_reload :: proc( ctx : ^Context, allocator : Allocator )
|
||||
{
|
||||
assert( ctx != nil )
|
||||
ctx.backing = allocator
|
||||
context.allocator = ctx.backing
|
||||
using ctx
|
||||
|
||||
reload_array( & entries, allocator )
|
||||
reload_array( & temp_path, allocator )
|
||||
reload_map( & ctx.temp_codepoint_seen, allocator )
|
||||
|
||||
reload_array( & draw_list.vertices, allocator)
|
||||
reload_array( & draw_list.indices, allocator )
|
||||
reload_array( & draw_list.calls, allocator )
|
||||
|
||||
LRU_reload( & atlas.region_a.state, allocator)
|
||||
LRU_reload( & atlas.region_b.state, allocator)
|
||||
LRU_reload( & atlas.region_c.state, allocator)
|
||||
LRU_reload( & atlas.region_d.state, allocator)
|
||||
|
||||
LRU_reload( & shape_cache.state, allocator )
|
||||
for idx : i32 = 0; idx < i32(len(shape_cache.storage)); idx += 1 {
|
||||
stroage_entry := & shape_cache.storage[idx]
|
||||
using stroage_entry
|
||||
|
||||
reload_array( & glyphs, allocator )
|
||||
reload_array( & positions, allocator )
|
||||
}
|
||||
|
||||
reload_array( & glyph_buffer.draw_list.calls, allocator )
|
||||
reload_array( & glyph_buffer.draw_list.indices, allocator )
|
||||
reload_array( & glyph_buffer.draw_list.vertices, allocator )
|
||||
|
||||
reload_array( & glyph_buffer.clear_draw_list.calls, allocator )
|
||||
reload_array( & glyph_buffer.clear_draw_list.indices, allocator )
|
||||
reload_array( & glyph_buffer.clear_draw_list.vertices, allocator )
|
||||
|
||||
reload_array( & shape_cache.storage, allocator )
|
||||
LRU_reload( & shape_cache.state, allocator )
|
||||
}
|
||||
|
||||
// ve_foncache_shutdown
|
||||
shutdown :: proc( ctx : ^Context )
|
||||
{
|
||||
assert( ctx != nil )
|
||||
context.allocator = ctx.backing
|
||||
using ctx
|
||||
|
||||
for & entry in entries {
|
||||
unload_font( ctx, entry.id )
|
||||
}
|
||||
|
||||
shaper_shutdown( & shaper_ctx )
|
||||
|
||||
// TODO(Ed): Finish implementing, there is quite a few resource not released here.
|
||||
}
|
||||
|
||||
// ve_fontcache_load
|
||||
load_font :: proc( ctx : ^Context, label : string, data : []byte, size_px : f32, glyph_curve_quality : u32 = 0 ) -> (font_id : FontID)
|
||||
{
|
||||
assert( ctx != nil )
|
||||
assert( len(data) > 0 )
|
||||
using ctx
|
||||
context.allocator = backing
|
||||
|
||||
id : i32 = -1
|
||||
|
||||
for index : i32 = 0; index < i32(len(entries)); index += 1 {
|
||||
if entries[index].used do continue
|
||||
id = index
|
||||
break
|
||||
}
|
||||
if id == -1 {
|
||||
append_elem( & entries, Entry {})
|
||||
id = cast(i32) len(entries) - 1
|
||||
}
|
||||
assert( id >= 0 && id < i32(len(entries)) )
|
||||
|
||||
entry := & entries[ id ]
|
||||
{
|
||||
using entry
|
||||
used = true
|
||||
|
||||
parser_info = parser_load_font( & parser_ctx, label, data )
|
||||
// assert( parser_info != nil, "VEFontCache.load_font: Failed to load font info from parser" )
|
||||
|
||||
shaper_info = shaper_load_font( & shaper_ctx, label, data, transmute(rawptr) id )
|
||||
// assert( shaper_info != nil, "VEFontCache.load_font: Failed to load font from shaper")
|
||||
|
||||
size = size_px
|
||||
size_scale = size_px < 0.0 ? \
|
||||
parser_scale_for_pixel_height( & parser_info, -size_px ) \
|
||||
: parser_scale_for_mapping_em_to_pixels( & parser_info, size_px )
|
||||
|
||||
if glyph_curve_quality == 0 {
|
||||
curve_quality = f32(ctx.default_curve_quality)
|
||||
}
|
||||
else {
|
||||
curve_quality = f32(glyph_curve_quality)
|
||||
}
|
||||
}
|
||||
entry.id = FontID(id)
|
||||
ctx.entries[ id ].id = FontID(id)
|
||||
|
||||
font_id = FontID(id)
|
||||
return
|
||||
}
|
||||
|
||||
// ve_fontcache_unload
|
||||
unload_font :: proc( ctx : ^Context, font : FontID )
|
||||
{
|
||||
assert( ctx != nil )
|
||||
assert( font >= 0 && int(font) < len(ctx.entries) )
|
||||
context.allocator = ctx.backing
|
||||
|
||||
using ctx
|
||||
entry := & ctx.entries[ font ]
|
||||
entry.used = false
|
||||
|
||||
parser_unload_font( & entry.parser_info )
|
||||
shaper_unload_font( & entry.shaper_info )
|
||||
}
|
||||
|
||||
#endregion("lifetime")
|
||||
|
||||
#region("drawing")
|
||||
|
||||
// ve_fontcache_configure_snap
|
||||
configure_snap :: #force_inline proc( ctx : ^Context, snap_width, snap_height : u32 ) {
|
||||
assert( ctx != nil )
|
||||
ctx.snap_width = f32(snap_width)
|
||||
ctx.snap_height = f32(snap_height)
|
||||
}
|
||||
|
||||
get_cursor_pos :: #force_inline proc "contextless" ( ctx : ^Context ) -> Vec2 { return ctx.cursor_pos }
|
||||
set_colour :: #force_inline proc "contextless" ( ctx : ^Context, colour : Colour ) { ctx.colour = colour }
|
||||
|
||||
draw_text :: proc( ctx : ^Context, font : FontID, text_utf8 : string, position, scale : Vec2 ) -> b32
|
||||
{
|
||||
// profile(#procedure)
|
||||
assert( ctx != nil )
|
||||
assert( font >= 0 && int(font) < len(ctx.entries) )
|
||||
|
||||
ctx.cursor_pos = {}
|
||||
|
||||
position := position
|
||||
if ctx.snap_width > 0 do position.x = cast(f32) cast(u32) (position.x * ctx.snap_width + 0.5) / ctx.snap_width
|
||||
if ctx.snap_height > 0 do position.y = cast(f32) cast(u32) (position.y * ctx.snap_height + 0.5) / ctx.snap_height
|
||||
|
||||
entry := & ctx.entries[ font ]
|
||||
|
||||
ChunkType :: enum u32 { Visible, Formatting }
|
||||
chunk_kind : ChunkType
|
||||
chunk_start : int = 0
|
||||
chunk_end : int = 0
|
||||
|
||||
text_utf8_bytes := transmute([]u8) text_utf8
|
||||
text_chunk : string
|
||||
|
||||
text_chunk = transmute(string) text_utf8_bytes[ : ]
|
||||
if len(text_chunk) > 0 {
|
||||
shaped := shape_text_cached( ctx, font, text_chunk, entry )
|
||||
ctx.cursor_pos = draw_text_shape( ctx, font, entry, shaped, position, scale, ctx.snap_width, ctx.snap_height )
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ve_fontcache_drawlist
|
||||
get_draw_list :: proc( ctx : ^Context, optimize_before_returning := true ) -> ^DrawList {
|
||||
assert( ctx != nil )
|
||||
if optimize_before_returning do optimize_draw_list( & ctx.draw_list, 0 )
|
||||
return & ctx.draw_list
|
||||
}
|
||||
|
||||
get_draw_list_layer :: proc( ctx : ^Context, optimize_before_returning := true ) -> (vertices : []Vertex, indices : []u32, calls : []DrawCall) {
|
||||
assert( ctx != nil )
|
||||
if optimize_before_returning do optimize_draw_list( & ctx.draw_list, ctx.draw_layer.calls_offset )
|
||||
vertices = ctx.draw_list.vertices[ ctx.draw_layer.vertices_offset : ]
|
||||
indices = ctx.draw_list.indices [ ctx.draw_layer.indices_offset : ]
|
||||
calls = ctx.draw_list.calls [ ctx.draw_layer.calls_offset : ]
|
||||
return
|
||||
}
|
||||
|
||||
// ve_fontcache_flush_drawlist
|
||||
flush_draw_list :: proc( ctx : ^Context ) {
|
||||
assert( ctx != nil )
|
||||
using ctx
|
||||
clear_draw_list( & draw_list )
|
||||
draw_layer.vertices_offset = 0
|
||||
draw_layer.indices_offset = 0
|
||||
draw_layer.calls_offset = 0
|
||||
}
|
||||
|
||||
flush_draw_list_layer :: proc( ctx : ^Context ) {
|
||||
assert( ctx != nil )
|
||||
using ctx
|
||||
draw_layer.vertices_offset = len(draw_list.vertices)
|
||||
draw_layer.indices_offset = len(draw_list.indices)
|
||||
draw_layer.calls_offset = len(draw_list.calls)
|
||||
}
|
||||
|
||||
#endregion("drawing")
|
||||
|
||||
#region("metrics")
|
||||
|
||||
measure_text_size :: proc( ctx : ^Context, font : FontID, text_utf8 : string ) -> (measured : Vec2)
|
||||
{
|
||||
// profile(#procedure)
|
||||
assert( ctx != nil )
|
||||
assert( font >= 0 && int(font) < len(ctx.entries) )
|
||||
|
||||
entry := &ctx.entries[font]
|
||||
shaped := shape_text_cached(ctx, font, text_utf8, entry)
|
||||
return shaped.size
|
||||
}
|
||||
|
||||
get_font_vertical_metrics :: #force_inline proc ( ctx : ^Context, font : FontID ) -> ( ascent, descent, line_gap : f32 )
|
||||
{
|
||||
assert( ctx != nil )
|
||||
assert( font >= 0 && int(font) < len(ctx.entries) )
|
||||
|
||||
entry := & ctx.entries[ font ]
|
||||
ascent_i32, descent_i32, line_gap_i32 := parser_get_font_vertical_metrics( & entry.parser_info )
|
||||
|
||||
ascent = ceil(f32(ascent_i32) * entry.size_scale)
|
||||
descent = ceil(f32(descent_i32) * entry.size_scale)
|
||||
line_gap = ceil(f32(line_gap_i32) * entry.size_scale)
|
||||
return
|
||||
}
|
||||
|
||||
#endregion("metrics")
|
Reference in New Issue
Block a user