finish force rename

This commit is contained in:
2024-07-01 16:14:54 -04:00
parent e793392e85
commit 77301c2b28
15 changed files with 0 additions and 0 deletions

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

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

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

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

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

Binary file not shown.

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

View 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.
![Wireframe with GPU XOR blending](images/wireframe.png)
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:
![Wireframe with GPU XOR blending](images/atlas_small.png)
# 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
![Screenshot 1](images/ve_fontcache_demo1.png)
![Screenshot 2](images/ve_fontcache_demo2.png)
![Screenshot 3](images/raincode.png)
![Screenshot 4](images/ve_fontcache_pressure_test.gif)
# 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

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

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

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

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

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

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

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