SectrPrototype/examples/ve_fontcache.h
Ed_ 597c88c6b7 Misc + made a more controlled digital zoom
Trying to get digital zoom to closer target levels that would match specific even font sizes

Various other changes from iterating on VEFontCache
2024-06-29 22:36:22 -04:00

1799 lines
69 KiB
C++

/*
-- 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.
*/
/*
---------------------------------- How to plug into rendering API ----------------------------------------
1. Create these simple shaders ( GLSL example provided ):
// vs_source_shared
#version 330 core
in vec2 vpos;
in vec2 vtex;
out vec2 uv;
void main( void ) {
uv = vtex;
gl_Position = vec4( vpos.xy, 0.0, 1.0 );
}
// fs_source_render_glyph
#version 330 core
out vec4 fragc;
void main( void ) {
fragc = vec4( 1.0, 1.0, 1.0, 1.0 );
}
// fs_source_blit_atlas
#version 330 core
in vec2 uv;
out vec4 fragc;
uniform uint region;
uniform sampler2D src_texture;
float downsample( vec2 uv, vec2 texsz )
{
float v =
texture( src_texture, uv + vec2( 0.0f, 0.0f ) * texsz ).x * 0.25f +
texture( src_texture, uv + vec2( 0.0f, 1.0f ) * texsz ).x * 0.25f +
texture( src_texture, uv + vec2( 1.0f, 0.0f ) * texsz ).x * 0.25f +
texture( src_texture, uv + vec2( 1.0f, 1.0f ) * texsz ).x * 0.25f;
return v;
}
void main( void ) {
const vec2 texsz = 1.0f / vec2( 2048 , 512 ); // VE_FONTCACHE_GLYPHDRAW_BUFFER_WIDTH/HEIGHT
if ( region == 0u || region == 1u || region == 2u ) {
float v =
downsample( uv + vec2( -1.5f, -1.5f ) * texsz, texsz ) * 0.25f +
downsample( uv + vec2( 0.5f, -1.5f ) * texsz, texsz ) * 0.25f +
downsample( uv + vec2( -1.5f, 0.5f ) * texsz, texsz ) * 0.25f +
downsample( uv + vec2( 0.5f, 0.5f ) * texsz, texsz ) * 0.25f;
fragc = vec4( 1, 1, 1, v );
} else {
fragc = vec4( 0, 0, 0, 1 );
}
}
// vs_source_draw_text
#version 330 core
in vec2 vpos;
in vec2 vtex;
out vec2 uv;
void main( void ) {
uv = vtex;
gl_Position = vec4( vpos.xy * 2.0f - 1.0f, 0.0, 1.0 );
}
// fs_source_draw_text
#version 330 core
in vec2 uv;
out vec4 fragc;
uniform sampler2D src_texture;
uniform uint downsample;
uniform vec4 colour;
void main( void ) {
float v = texture( src_texture, uv ).x;
if ( downsample == 1u ) {
const vec2 texsz = 1.0f / vec2( 2048 , 512 ); // VE_FONTCACHE_GLYPHDRAW_BUFFER_WIDTH/HEIGHT
v =
texture( src_texture, uv + vec2(-0.5f,-0.5f ) * texsz ).x * 0.25f +
texture( src_texture, uv + vec2(-0.5f, 0.5f ) * texsz ).x * 0.25f +
texture( src_texture, uv + vec2( 0.5f,-0.5f ) * texsz ).x * 0.25f +
texture( src_texture, uv + vec2( 0.5f, 0.5f ) * texsz ).x * 0.25f;
}
fragc = vec4( colour.xyz, colour.a * v );
}
fontcache_shader_render_glyph = compile_shader( vs_source_shared, fs_source_render_glyph );
fontcache_shader_blit_atlas = compile_shader( vs_source_shared, fs_source_blit_atlas );
fontcache_shader_draw_text = compile_shader( vs_source_draw_text, fs_source_draw_text );
2. Set up these 2 render target textures ( OpenGL example ):
// First render target is 2k x 512 single-channel 8-byte red.
glBindFramebuffer( GL_FRAMEBUFFER, fontcache_fbo[ 0 ] );
glBindTexture( GL_TEXTURE_2D, fontcache_fbo_texture[0] );
glTexImage2D( GL_TEXTURE_2D, 0, GL_R8, VE_FONTCACHE_GLYPHDRAW_BUFFER_WIDTH, VE_FONTCACHE_GLYPHDRAW_BUFFER_HEIGHT, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST );
glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fontcache_fbo_texture[ 0 ], 0 );
// Second render target is 4k x 2k single-channel 8-byte red.
glBindFramebuffer( GL_FRAMEBUFFER, fontcache_fbo[ 1 ] );
glBindTexture( GL_TEXTURE_2D, fontcache_fbo_texture[ 1 ] );
glTexImage2D( GL_TEXTURE_2D, 0, GL_R8, VE_FONTCACHE_ATLAS_WIDTH, VE_FONTCACHE_ATLAS_HEIGHT, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST );
glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fontcache_fbo_texture[ 1 ], 0 );
3. Implement drawlist execute method ( OpenGL example ):
glDisable( GL_CULL_FACE );
glEnable( GL_BLEND );
glBlendEquation( GL_FUNC_ADD );
for ( auto& dcall : drawlist->dcalls ) {
if ( dcall.pass == VE_FONTCACHE_FRAMEBUFFER_PASS_GLYPH ) {
glUseProgram( fontcache_shader_render_glyph );
glBindFramebuffer( GL_FRAMEBUFFER, fontcache_fbo[ 0 ] );
glBlendFunc( GL_ONE_MINUS_DST_COLOR, GL_ONE_MINUS_SRC_COLOR );
glViewport( 0, 0, VE_FONTCACHE_GLYPHDRAW_BUFFER_WIDTH, VE_FONTCACHE_GLYPHDRAW_BUFFER_HEIGHT );
glScissor( 0, 0, VE_FONTCACHE_GLYPHDRAW_BUFFER_WIDTH, VE_FONTCACHE_GLYPHDRAW_BUFFER_HEIGHT );
glDisable( GL_FRAMEBUFFER_SRGB );
} else if ( dcall.pass == VE_FONTCACHE_FRAMEBUFFER_PASS_ATLAS ) {
glUseProgram( fontcache_shader_blit_atlas );
glBindFramebuffer( GL_FRAMEBUFFER, fontcache_fbo[ 1 ] );
glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
glViewport( 0, 0, VE_FONTCACHE_ATLAS_WIDTH, VE_FONTCACHE_ATLAS_HEIGHT );
glScissor( 0, 0, VE_FONTCACHE_ATLAS_WIDTH, VE_FONTCACHE_ATLAS_HEIGHT );
glUniform1i( glGetUniformLocation( fontcache_shader_blit_atlas, "src_texture" ), 0 );
glUniform1ui( glGetUniformLocation( fontcache_shader_blit_atlas, "region" ), dcall.region );
glActiveTexture( GL_TEXTURE0 );
glBindTexture( GL_TEXTURE_2D, fontcache_fbo_texture[0] );
glDisable( GL_FRAMEBUFFER_SRGB );
} else {
glUseProgram( fontcache_shader_draw_text );
glBindFramebuffer( GL_FRAMEBUFFER, 0 );
glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
glViewport( 0, 0, window_size.width, window_size.height );
glScissor( 0, 0, window_size.width, window_size.height );
glUniform1i( glGetUniformLocation( fontcache_shader_draw_text, "src_texture" ), 0 );
glUniform1ui( glGetUniformLocation( fontcache_shader_draw_text, "downsample" ), dcall.pass == VE_FONTCACHE_FRAMEBUFFER_PASS_TARGET_UNCACHED ? 1 : 0 );
glUniform4fv( glGetUniformLocation( fontcache_shader_draw_text, "colour" ), 1, dcall.colour );
glActiveTexture( GL_TEXTURE0 );
glBindTexture( GL_TEXTURE_2D, dcall.pass == VE_FONTCACHE_FRAMEBUFFER_PASS_TARGET_UNCACHED ? fontcache_fbo_texture[0] : fontcache_fbo_texture[1] );
glEnable( GL_FRAMEBUFFER_SRGB );
}
if ( dcall.clear_before_draw ) {
glClearColor( 0.0f, 0.0f, 0.0f, 0.0f );
glClear( GL_COLOR_BUFFER_BIT );
}
if ( dcall.end_index - dcall.start_index == 0 )
continue;
glDrawElements( GL_TRIANGLES, dcall.end_index - dcall.start_index, GL_UNSIGNED_INT, ( GLvoid* ) ( dcall.start_index * sizeof( uint32_t ) ) );
}
*/
#pragma once
#include <cstdio>
#include <vector>
#include <memory>
#include <unordered_map>
#include <deque>
#ifndef VE_STBTT_NOIMPL
#define STB_TRUETYPE_IMPLEMENTATION
#endif // VE_STBTT_NOIMPL
#ifndef VE_STBTT_INCLUDED
#define VE_STBTT_INCLUDED
#include "stb_truetype.h"
#endif // VE_STBTT_INCLUDED
#ifndef VE_FONTCACHE_CURVE_QUALITY
#define VE_FONTCACHE_CURVE_QUALITY 6
#endif // VE_FONTCACHE_CURVE_QUALITY
#ifndef VE_FONTCACHE_HARFBUZZ
#pragma message( "WARNING: Please include Harfbuzz and define VE_FONTCACHE_HARFBUZZ for production. Using default fallback dumbass non-portable unoptimised text shaper." )
#endif // VE_FONTCACHE_HARFBUZZ
#include "utf8.h"
/*
---------------------------------- Font Atlas Caching Strategy --------------------------------------------
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
*/
#define VE_FONTCACHE_ATLAS_WIDTH 4096
#define VE_FONTCACHE_ATLAS_HEIGHT 2048
#define VE_FONTCACHE_ATLAS_GLYPH_PADDING 1
#define VE_FONTCACHE_ATLAS_REGION_A_WIDTH 32
#define VE_FONTCACHE_ATLAS_REGION_A_HEIGHT 32
#define VE_FONTCACHE_ATLAS_REGION_A_XSIZE ( VE_FONTCACHE_ATLAS_WIDTH / 4 )
#define VE_FONTCACHE_ATLAS_REGION_A_YSIZE ( VE_FONTCACHE_ATLAS_HEIGHT / 2 )
#define VE_FONTCACHE_ATLAS_REGION_A_XCAPACITY ( VE_FONTCACHE_ATLAS_REGION_A_XSIZE / VE_FONTCACHE_ATLAS_REGION_A_WIDTH )
#define VE_FONTCACHE_ATLAS_REGION_A_YCAPACITY ( VE_FONTCACHE_ATLAS_REGION_A_YSIZE / VE_FONTCACHE_ATLAS_REGION_A_HEIGHT )
#define VE_FONTCACHE_ATLAS_REGION_A_CAPACITY ( VE_FONTCACHE_ATLAS_REGION_A_XCAPACITY * VE_FONTCACHE_ATLAS_REGION_A_YCAPACITY )
#define VE_FONTCACHE_ATLAS_REGION_A_XOFFSET 0
#define VE_FONTCACHE_ATLAS_REGION_A_YOFFSET 0
#define VE_FONTCACHE_ATLAS_REGION_B_WIDTH 32
#define VE_FONTCACHE_ATLAS_REGION_B_HEIGHT 64
#define VE_FONTCACHE_ATLAS_REGION_B_XSIZE ( VE_FONTCACHE_ATLAS_WIDTH / 4 )
#define VE_FONTCACHE_ATLAS_REGION_B_YSIZE ( VE_FONTCACHE_ATLAS_HEIGHT / 2 )
#define VE_FONTCACHE_ATLAS_REGION_B_XCAPACITY ( VE_FONTCACHE_ATLAS_REGION_B_XSIZE / VE_FONTCACHE_ATLAS_REGION_B_WIDTH )
#define VE_FONTCACHE_ATLAS_REGION_B_YCAPACITY ( VE_FONTCACHE_ATLAS_REGION_B_YSIZE / VE_FONTCACHE_ATLAS_REGION_B_HEIGHT )
#define VE_FONTCACHE_ATLAS_REGION_B_CAPACITY ( VE_FONTCACHE_ATLAS_REGION_B_XCAPACITY * VE_FONTCACHE_ATLAS_REGION_B_YCAPACITY )
#define VE_FONTCACHE_ATLAS_REGION_B_XOFFSET 0
#define VE_FONTCACHE_ATLAS_REGION_B_YOFFSET VE_FONTCACHE_ATLAS_REGION_A_YSIZE
#define VE_FONTCACHE_ATLAS_REGION_C_WIDTH 64
#define VE_FONTCACHE_ATLAS_REGION_C_HEIGHT 64
#define VE_FONTCACHE_ATLAS_REGION_C_XSIZE ( VE_FONTCACHE_ATLAS_WIDTH / 4 )
#define VE_FONTCACHE_ATLAS_REGION_C_YSIZE ( VE_FONTCACHE_ATLAS_HEIGHT )
#define VE_FONTCACHE_ATLAS_REGION_C_XCAPACITY ( VE_FONTCACHE_ATLAS_REGION_C_XSIZE / VE_FONTCACHE_ATLAS_REGION_C_WIDTH )
#define VE_FONTCACHE_ATLAS_REGION_C_YCAPACITY ( VE_FONTCACHE_ATLAS_REGION_C_YSIZE / VE_FONTCACHE_ATLAS_REGION_C_HEIGHT )
#define VE_FONTCACHE_ATLAS_REGION_C_CAPACITY ( VE_FONTCACHE_ATLAS_REGION_C_XCAPACITY * VE_FONTCACHE_ATLAS_REGION_C_YCAPACITY )
#define VE_FONTCACHE_ATLAS_REGION_C_XOFFSET VE_FONTCACHE_ATLAS_REGION_A_XSIZE
#define VE_FONTCACHE_ATLAS_REGION_C_YOFFSET 0
#define VE_FONTCACHE_ATLAS_REGION_D_WIDTH 128
#define VE_FONTCACHE_ATLAS_REGION_D_HEIGHT 128
#define VE_FONTCACHE_ATLAS_REGION_D_XSIZE ( VE_FONTCACHE_ATLAS_WIDTH / 2 )
#define VE_FONTCACHE_ATLAS_REGION_D_YSIZE ( VE_FONTCACHE_ATLAS_HEIGHT )
#define VE_FONTCACHE_ATLAS_REGION_D_XCAPACITY ( VE_FONTCACHE_ATLAS_REGION_D_XSIZE / VE_FONTCACHE_ATLAS_REGION_D_WIDTH )
#define VE_FONTCACHE_ATLAS_REGION_D_YCAPACITY ( VE_FONTCACHE_ATLAS_REGION_D_YSIZE / VE_FONTCACHE_ATLAS_REGION_D_HEIGHT )
#define VE_FONTCACHE_ATLAS_REGION_D_CAPACITY ( VE_FONTCACHE_ATLAS_REGION_D_XCAPACITY * VE_FONTCACHE_ATLAS_REGION_D_YCAPACITY )
#define VE_FONTCACHE_ATLAS_REGION_D_XOFFSET ( VE_FONTCACHE_ATLAS_WIDTH / 2 )
#define VE_FONTCACHE_ATLAS_REGION_D_YOFFSET 0
static_assert( VE_FONTCACHE_ATLAS_REGION_A_CAPACITY == 1024, "VE FontCache Atlas Sanity check fail. Please update this assert if you changed atlas packing strategy." );
static_assert( VE_FONTCACHE_ATLAS_REGION_B_CAPACITY == 512, "VE FontCache Atlas Sanity check fail. Please update this assert if you changed atlas packing strategy." );
static_assert( VE_FONTCACHE_ATLAS_REGION_C_CAPACITY == 512, "VE FontCache Atlas Sanity check fail. Please update this assert if you changed atlas packing strategy." );
static_assert( VE_FONTCACHE_ATLAS_REGION_D_CAPACITY == 256, "VE FontCache Atlas Sanity check fail. Please update this assert if you changed atlas packing strategy." );
#define VE_FONTCACHE_GLYPHDRAW_OVERSAMPLE_X 4
#define VE_FONTCACHE_GLYPHDRAW_OVERSAMPLE_Y 4
#define VE_FONTCACHE_GLYPHDRAW_BUFFER_BATCH 4
#define VE_FONTCACHE_GLYPHDRAW_BUFFER_WIDTH ( VE_FONTCACHE_ATLAS_REGION_D_WIDTH * VE_FONTCACHE_GLYPHDRAW_OVERSAMPLE_X * VE_FONTCACHE_GLYPHDRAW_BUFFER_BATCH )
#define VE_FONTCACHE_GLYPHDRAW_BUFFER_HEIGHT ( VE_FONTCACHE_ATLAS_REGION_D_HEIGHT * VE_FONTCACHE_GLYPHDRAW_OVERSAMPLE_Y )
// Set to same value as VE_FONTCACHE_ATLAS_GLYPH_PADDING for best results!
#define VE_FONTCACHE_GLYPHDRAW_PADDING VE_FONTCACHE_ATLAS_GLYPH_PADDING
#define VE_FONTCACHE_FRAMEBUFFER_PASS_GLYPH 1
#define VE_FONTCACHE_FRAMEBUFFER_PASS_ATLAS 2
#define VE_FONTCACHE_FRAMEBUFFER_PASS_TARGET 3
#define VE_FONTCACHE_FRAMEBUFFER_PASS_TARGET_UNCACHED 4
// How many to store in text shaping cache. Shaping cache is also stored in LRU format.
#define VE_FONTCACHE_SHAPECACHE_SIZE 256
// How much to reserve for each shape cache. This adds up to a ~0.768mb cache.
#define VE_FONTCACHE_SHAPECACHE_RESERVE_LENGTH 64
// Max. text size for caching. This means the cache has ~3.072mb upper bound.
#define VE_FONTCACHE_SHAPECACHE_MAX_LENGTH 256
// Sizes below this snap their advance to next pixel boundary.
#define VE_FONTCACHE_ADVANCE_SNAP_SMALLFONT_SIZE 12
// --------------------------------------------------------------- Data Types ---------------------------------------------------
typedef int64_t ve_font_id;
typedef int32_t ve_codepoint;
typedef int32_t ve_glyph;
typedef char ve_atlas_region;
struct ve_fontcache_entry
{
ve_font_id font_id = 0;
stbtt_fontinfo info;
bool used = false;
float size = 24.0f;
float size_scale = 1.0f;
#ifdef VE_FONTCACHE_HARFBUZZ
hb_blob_t* hb_blob = nullptr;
hb_face_t* hb_face = nullptr;
hb_font_t* hb_font = nullptr;
#endif // VE_FONTCACHE_HARFBUZZ
};
struct ve_fontcache_vertex
{
float x; float y;
float u; float v;
};
struct ve_fontcache_vec2
{
float x; float y;
};
inline ve_fontcache_vec2 ve_fontcache_make_vec2( float x_, float y_ ) { ve_fontcache_vec2 v; v.x = x_; v.y = y_; return v; }
struct ve_fontcache_draw
{
uint32_t pass = 0; // One of VE_FONTCACHE_FRAMEBUFFER_PASS_* values.
uint32_t start_index = 0;
uint32_t end_index = 0;
bool clear_before_draw = false;
uint32_t region = 0;
float colour[4] = { 1.0f, 1.0f, 1.0f, 1.0f };
};
struct ve_fontcache_drawlist
{
std::vector< ve_fontcache_vertex > vertices;
std::vector< uint32_t > indices;
std::vector< ve_fontcache_draw > dcalls;
};
typedef uint32_t ve_fontcache_poollist_itr;
typedef uint64_t ve_fontcache_poollist_value;
struct ve_fontcache_poollist_item
{
ve_fontcache_poollist_itr prev = -1;
ve_fontcache_poollist_itr next = -1;
ve_fontcache_poollist_value value = 0;
};
struct ve_fontcache_poollist
{
std::vector< ve_fontcache_poollist_item > pool;
std::vector< ve_fontcache_poollist_itr > freelist;
ve_fontcache_poollist_itr front = -1;
ve_fontcache_poollist_itr back = -1;
size_t size = 0;
size_t capacity = 0;
};
struct ve_fontcache_LRU_link
{
int value = 0;
ve_fontcache_poollist_itr ptr;
};
struct ve_fontcache_LRU
{
int capacity = 0;
std::unordered_map< uint64_t, ve_fontcache_LRU_link > cache;
ve_fontcache_poollist key_queue;
};
struct ve_fontcache_atlas
{
uint32_t next_atlas_idx_A = 0;
uint32_t next_atlas_idx_B = 0;
uint32_t next_atlas_idx_C = 0;
uint32_t next_atlas_idx_D = 0;
ve_fontcache_LRU stateA;
ve_fontcache_LRU stateB;
ve_fontcache_LRU stateC;
ve_fontcache_LRU stateD;
uint32_t glyph_update_batch_x = 0;
ve_fontcache_drawlist glyph_update_batch_clear_drawlist;
ve_fontcache_drawlist glyph_update_batch_drawlist;
};
struct ve_fontcache_shaped_text
{
std::vector< ve_glyph > glyphs;
std::vector< ve_fontcache_vec2 > pos;
ve_fontcache_vec2 end_cursor_pos;
};
struct ve_fontcache_shaped_text_cache
{
std::vector< ve_fontcache_shaped_text > storage;
ve_fontcache_LRU state;
uint32_t next_cache_idx = 0;
};
struct ve_fontcache
{
std::vector< ve_fontcache_entry > entry;
std::vector< ve_fontcache_vec2 > temp_path;
std::unordered_map< uint64_t, bool > temp_codepoint_seen;
uint32_t snap_width = 0;
uint32_t snap_height = 0;
float colour[4] = { 1.0f, 1.0f, 1.0f, 1.0f };
ve_fontcache_vec2 cursor_pos;
ve_fontcache_drawlist drawlist;
ve_fontcache_atlas atlas;
ve_fontcache_shaped_text_cache shape_cache;
bool text_shape_advanced = true;
#ifdef VE_FONTCACHE_HARFBUZZ
hb_buffer_t* hb_text_buffer = nullptr;
#endif // VE_FONTCACHE_HARFBUZZ
};
// --------------------------------------------------------------------- VEFontCache Function Declarations -------------------------------------------------------------
// Call this to initialise a fontcache.
void ve_fontcache_init( ve_fontcache* cache );
// Call this to shutdown everything.
void ve_fontcache_shutdown( ve_fontcache* cache );
// Load hb_font from in-memory buffer. Supports otf, ttf, everything STB_truetype supports.
// Caller still owns data and must hold onto it; This buffer cannot be freed while hb_font is still being used.
// SBTTT will keep track of weak pointers into this memory.
// If you're loading the same hb_font at different size_px values, it is OK to share the same data buffer amongst them.
//
ve_font_id ve_fontcache_load( ve_fontcache* cache, const void* data, size_t data_size, float size_px = 24.0f );
// Load hb_font from file. Supports otf, ttf, everything STB_truetype supports.
// Caller still owns given buffer and must hold onto it. This buffer cannot be freed while hb_font is still being used.
// SBTTT will keep track of weak pointers into this memory.
// If you're loading the same hb_font at different size_px values, it is OK to share the same buffer amongst them.
//
ve_font_id ve_fontcache_loadfile( ve_fontcache* cache, const char* filename, std::vector< uint8_t >& buffer, float size_px = 24.0f );
// Unload a font and relase memory. Calling ve_fontcache_shutdown already does this on all loaded fonts.
void ve_fontcache_unload( ve_fontcache* cache, ve_font_id id );
// Configure snapping glyphs to pixel border when hb_font is rendered to 2D screen. May affect kerning. This may be changed at any time.
// Set both to zero to disable pixel snapping.
void ve_fontcache_configure_snap( ve_fontcache* cache, uint32_t snap_width = 0, uint32_t snap_height = 0 );
// Call this per-frame after draw list has been executed. This will clear the drawlist for next frame.
void ve_fontcache_flush_drawlist( ve_fontcache* cache );
// Main draw text function. This batches caches both shape and glyphs, and uses fallback path when not available.
// Note that this function immediately appends everything needed to render the next to the cache->drawlist.
// If you want to draw to multiple unrelated targets, simply draw_text, then loop through execute draw list, draw_text again, and loop through execute draw list again.
// Suggest scalex = 1 / screen_width and scaley = 1 / screen height! scalex and scaley will need to account for aspect ratio.
//
bool ve_fontcache_draw_text( ve_fontcache* cache, ve_font_id font, const std::string& text_utf8, float posx = 0.0f, float posy = 0.0f, float scalex = 1.0f, float scaley = 1.0f );
// Get where the last ve_fontcache_draw_text call left off.
ve_fontcache_vec2 ve_fontcache_get_cursor_pos( ve_fontcache* cache );
// Merges drawcalls. Significantly improves drawcall overhead, highly recommended. Call this before looping through and executing drawlist.
void ve_fontcache_optimise_drawlist( ve_fontcache* cache );
// Retrieves current drawlist from cache.
ve_fontcache_drawlist* ve_fontcache_get_drawlist( ve_fontcache* cache );
// Set text colour of subsequent text draws.
void ve_fontcache_set_colour( ve_fontcache* cache, float c[4] );
inline void ve_fontcache_enable_advanced_text_shaping( ve_fontcache* cache, bool enabled = true )
{
cache->text_shape_advanced = enabled;
}
// --------------------------------------------------------------------- Generic Data Structure Declarations -------------------------------------------------------------
// Generic pool list for alloc-free LRU implementation.
void ve_fontcache_poollist_init( ve_fontcache_poollist& plist, int capacity );
void ve_fontcache_poollist_push_front( ve_fontcache_poollist& plist, ve_fontcache_poollist_value v );
void ve_fontcache_poollist_erase( ve_fontcache_poollist& plist, ve_fontcache_poollist_itr it );
ve_fontcache_poollist_value ve_fontcache_poollist_peek_back( ve_fontcache_poollist& plist );
ve_fontcache_poollist_value ve_fontcache_poollist_pop_back( ve_fontcache_poollist& plist );
// Generic LRU ( Least-Recently-Used ) cache implementation, reused for both atlas and shape cache.
void ve_fontcache_LRU_init( ve_fontcache_LRU& LRU, int capacity );
int ve_fontcache_LRU_get( ve_fontcache_LRU& LRU, uint64_t key );
int ve_fontcache_LRU_peek( ve_fontcache_LRU& LRU, uint64_t key );
uint64_t ve_fontcache_LRU_put( ve_fontcache_LRU& LRU, uint64_t key, int val );
void ve_fontcache_LRU_refresh( ve_fontcache_LRU& LRU, uint64_t key );
uint64_t ve_fontcache_LRU_get_next_evicted( ve_fontcache_LRU& LRU );
// --------------------------------------------------------------------- VEFontCache Function Implementations -------------------------------------------------------------
#ifdef VE_FONTCACHE_IMPL
void ve_fontcache_init( ve_fontcache* cache )
{
STBTT_assert( cache );
// Reserve global context data.
cache->entry.reserve( 8 );
cache->temp_path.reserve( 256 );
cache->temp_codepoint_seen.reserve( 512 );
cache->drawlist.vertices.reserve( 4096 );
cache->drawlist.indices.reserve( 8192 );
cache->drawlist.dcalls.reserve( 512 );
// Reserve data atlas LRU regions.
cache->atlas.next_atlas_idx_A = 0;
cache->atlas.next_atlas_idx_B = 0;
cache->atlas.next_atlas_idx_C = 0;
cache->atlas.next_atlas_idx_D = 0;
ve_fontcache_LRU_init( cache->atlas.stateA, VE_FONTCACHE_ATLAS_REGION_A_CAPACITY );
ve_fontcache_LRU_init( cache->atlas.stateB, VE_FONTCACHE_ATLAS_REGION_B_CAPACITY );
ve_fontcache_LRU_init( cache->atlas.stateC, VE_FONTCACHE_ATLAS_REGION_C_CAPACITY );
ve_fontcache_LRU_init( cache->atlas.stateD, VE_FONTCACHE_ATLAS_REGION_D_CAPACITY );
// Reserve data for shape cache. This is pretty big!
ve_fontcache_LRU_init( cache->shape_cache.state, VE_FONTCACHE_SHAPECACHE_SIZE );
cache->shape_cache.storage.resize( VE_FONTCACHE_SHAPECACHE_SIZE );
for ( int i = 0; i < VE_FONTCACHE_SHAPECACHE_SIZE; i++ ) {
cache->shape_cache.storage[ i ].glyphs.reserve( VE_FONTCACHE_SHAPECACHE_RESERVE_LENGTH );
cache->shape_cache.storage[ i ].pos.reserve( VE_FONTCACHE_SHAPECACHE_RESERVE_LENGTH );
}
// We can actually go over VE_FONTCACHE_GLYPHDRAW_BUFFER_BATCH batches due to smart packing!
cache->atlas.glyph_update_batch_drawlist.dcalls.reserve( VE_FONTCACHE_GLYPHDRAW_BUFFER_BATCH * 2 );
cache->atlas.glyph_update_batch_drawlist.vertices.reserve( VE_FONTCACHE_GLYPHDRAW_BUFFER_BATCH * 2 * 4 );
cache->atlas.glyph_update_batch_drawlist.indices.reserve( VE_FONTCACHE_GLYPHDRAW_BUFFER_BATCH * 2 * 6 );
cache->atlas.glyph_update_batch_clear_drawlist.dcalls.reserve( VE_FONTCACHE_GLYPHDRAW_BUFFER_BATCH * 2 );
cache->atlas.glyph_update_batch_clear_drawlist.vertices.reserve( VE_FONTCACHE_GLYPHDRAW_BUFFER_BATCH * 2 * 4 );
cache->atlas.glyph_update_batch_clear_drawlist.indices.reserve( VE_FONTCACHE_GLYPHDRAW_BUFFER_BATCH * 2 * 6 );
#ifdef VE_FONTCACHE_HARFBUZZ
cache->hb_text_buffer = hb_buffer_create();
#endif // VE_FONTCACHE_HARFBUZZ
}
void ve_fontcache_shutdown( ve_fontcache* cache )
{
STBTT_assert( cache );
for ( ve_fontcache_entry& et : cache->entry ) {
ve_fontcache_unload( cache, et.font_id );
}
#ifdef VE_FONTCACHE_HARFBUZZ
if ( cache->hb_text_buffer )
hb_buffer_destroy( cache->hb_text_buffer );
#endif // VE_FONTCACHE_HARFBUZZ
}
ve_font_id ve_fontcache_load( ve_fontcache* cache, const void* data, size_t data_size, float size_px )
{
STBTT_assert( cache );
if ( !data ) return -1;
// Allocate cache entry.
int id = -1;
for( int i = 0; i < cache->entry.size(); i++ ) {
if ( !cache->entry[i].used ) {
id = i;
break;
}
}
if ( id == -1 ) {
cache->entry.push_back( ve_fontcache_entry() );
id = cache->entry.size() - 1;
}
STBTT_assert( id >= 0 && id < cache->entry.size() );
// Load hb_font from memory.
auto& et = cache->entry[id];
int success = stbtt_InitFont( &et.info, ( const unsigned char* ) data, 0 );
if ( !success ) {
return -1;
}
et.font_id = id;
et.size = size_px;
et.size_scale = size_px < 0.0f ? stbtt_ScaleForPixelHeight( &et.info, -size_px ) : stbtt_ScaleForMappingEmToPixels( &et.info, size_px );
et.used = true;
#ifdef VE_FONTCACHE_HARFBUZZ
et.hb_blob = hb_blob_create( ( const char* ) data, data_size, HB_MEMORY_MODE_READONLY, ( void* )( uintptr_t ) id, nullptr );
et.hb_face = hb_face_create( et.hb_blob, 0 );
et.hb_font = hb_font_create( et.hb_face );
#endif // VE_FONTCACHE_HARFBUZZ
return id;
}
ve_font_id ve_fontcache_loadfile( ve_fontcache* cache, const char* filename, std::vector< uint8_t >& buffer, float size_px )
{
FILE *fp = fopen( filename, "rb" );
if ( !fp ) return -1;
fseek( fp, 0, SEEK_END );
size_t sz = ftell( fp );
fseek( fp, 0, SEEK_SET );
buffer.resize( sz );
if ( fread( buffer.data(), 1, sz, fp ) != sz ) {
buffer.clear();
fclose( fp );
return -1;
}
fclose( fp );
ve_font_id ret = ve_fontcache_load( cache, buffer.data(), buffer.size(), size_px );
return ret;
}
void ve_fontcache_unload( ve_fontcache* cache, ve_font_id font )
{
STBTT_assert( cache );
STBTT_assert( font >= 0 && font < cache->entry.size() );
auto& et = cache->entry[ font ];
et.used = false;
#ifdef VE_FONTCACHE_HARFBUZZ
if ( et.hb_font ) {
hb_font_destroy( et.hb_font );
et.hb_font = nullptr;
}
if ( et.hb_face ) {
hb_face_destroy( et.hb_face );
et.hb_face = nullptr;
}
if ( et.hb_blob ) {
hb_blob_destroy( et.hb_blob );
et.hb_blob = nullptr;
}
#endif // VE_FONTCACHE_HARFBUZZ
}
void ve_fontcache_configure_snap( ve_fontcache* cache, uint32_t snap_width, uint32_t snap_height )
{
STBTT_assert( cache );
cache->snap_width = snap_width;
cache->snap_height = snap_height;
}
ve_fontcache_drawlist* ve_fontcache_get_drawlist( ve_fontcache* cache )
{
STBTT_assert( cache );
return &cache->drawlist;
}
void ve_fontcache_clear_drawlist( ve_fontcache_drawlist& drawlist )
{
drawlist.dcalls.clear();
drawlist.indices.clear();
drawlist.vertices.clear();
}
void ve_fontcache_merge_drawlist( ve_fontcache_drawlist& dest, const ve_fontcache_drawlist& src )
{
int voffset = dest.vertices.size();
for ( int i = 0; i < src.vertices.size(); i++ ) {
dest.vertices.push_back( src.vertices[i] );
}
int ioffset = dest.indices.size();
for ( int i = 0; i < src.indices.size(); i++ ) {
dest.indices.push_back( src.indices[i] + voffset );
}
for ( int i = 0; i < src.dcalls.size(); i++ ) {
ve_fontcache_draw dcall = src.dcalls[i];
dcall.start_index += ioffset;
dcall.end_index += ioffset;
dest.dcalls.push_back( dcall );
}
}
void ve_fontcache_flush_drawlist( ve_fontcache* cache )
{
STBTT_assert( cache );
ve_fontcache_clear_drawlist( cache->drawlist );
}
inline ve_fontcache_vec2 ve_fontcache_eval_bezier( ve_fontcache_vec2 p0, ve_fontcache_vec2 p1, ve_fontcache_vec2 p2, float t )
{
float t2 = t * t, c0 = ( 1.0f - t ) * ( 1.0f - t ), c1 = 2.0f * ( 1.0f - t ) * t, c2 = t2;
return ve_fontcache_make_vec2(
c0 * p0.x + c1 * p1.x + c2 * p2.x,
c0 * p0.y + c1 * p1.y + c2 * p2.y
);
}
inline ve_fontcache_vec2 ve_fontcache_eval_bezier( ve_fontcache_vec2 p0, ve_fontcache_vec2 p1, ve_fontcache_vec2 p2, ve_fontcache_vec2 p3, float t )
{
float t2 = t * t, t3 = t2 * t;
float c0 = ( 1.0f - t ) * ( 1.0f - t ) * ( 1.0f - t ),
c1 = 3.0f * ( 1.0f - t ) * ( 1.0f - t ) * t,
c2 = 3.0f * ( 1.0f - t ) * t2,
c3 = t3;
return ve_fontcache_make_vec2(
c0 * p0.x + c1 * p1.x + c2 * p2.x + c3 * p3.x,
c0 * p0.y + c1 * p1.y + c2 * p2.y + c3 * p3.y
);
}
// WARNING: doesn't actually append drawcall; caller is responsible for actually appending the drawcall.
void ve_fontcache_draw_filled_path( ve_fontcache_drawlist& drawlist, ve_fontcache_vec2 outside, std::vector< ve_fontcache_vec2 >& path,
float scaleX = 1.0f, float scaleY = 1.0f, float translateX = 0.0f, float translateY = 0.0f )
{
#ifdef VE_FONTCACHE_DEBUGPRINT_VERBOSE
printf( "outline_path: \n" );
for ( int i = 0; i < path.size(); i++ ) {
printf( " %.2f %.2f\n", path[i].x * scaleX + translateX, path[i].y * scaleY + + translateY );
}
#endif // VE_FONTCACHE_DEBUGPRINT_VERBOSE
int voffset = drawlist.vertices.size();
for ( int i = 0; i < path.size(); i++ ) {
ve_fontcache_vertex vertex;
vertex.x = path[i].x * scaleX + translateX;
vertex.y = path[i].y * scaleY + translateY;
vertex.u = 0.0f;
vertex.v = 0.0f;
drawlist.vertices.push_back( vertex );
}
int voutside = drawlist.vertices.size();
{
ve_fontcache_vertex vertex;
vertex.x = outside.x * scaleX + translateX;
vertex.y = outside.y * scaleY + translateY;
vertex.u = 0.0f;
vertex.v = 0.0f;
drawlist.vertices.push_back( vertex );
}
for ( int i = 1; i < path.size(); i++ ) {
drawlist.indices.push_back( voutside );
drawlist.indices.push_back( voffset + i - 1 );
drawlist.indices.push_back( voffset + i );
}
}
// WARNING: doesn't actually append drawcall; caller is responsible for actually appending the drawcall.
void ve_fontcache_blit_quad( ve_fontcache_drawlist& drawlist, float x0 = 0.0f, float y0 = 0.0f, float x1 = 1.0f, float y1 = 1.0f, float u0 = 0.0f, float v0 = 0.0f, float u1 = 1.0f, float v1 = 1.0f )
{
//printf("Blitting: xy0: %0.2f, %0.2f xy1: %0.2f, %0.2f uv0: %0.2f, %0.2f uv1: %0.2f, %0.2f\n",
//x0, y0, x1, y1, u0, v0, u1, v1);
int voffset = drawlist.vertices.size();
ve_fontcache_vertex vertex;
vertex.x = x0; vertex.y = y0;
vertex.u = u0; vertex.v = v0;
drawlist.vertices.push_back( vertex );
vertex.x = x0; vertex.y = y1;
vertex.u = u0; vertex.v = v1;
drawlist.vertices.push_back( vertex );
vertex.x = x1; vertex.y = y0;
vertex.u = u1; vertex.v = v0;
drawlist.vertices.push_back( vertex );
vertex.x = x1; vertex.y = y1;
vertex.u = u1; vertex.v = v1;
drawlist.vertices.push_back( vertex );
static uint32_t quad_indices[] = {
0, 1, 2,
2, 1, 3
};
for ( int i = 0; i < 6; i++ ) {
drawlist.indices.push_back( voffset + quad_indices[i] );
}
}
bool ve_fontcache_cache_glyph(
ve_fontcache* cache, ve_font_id font, ve_glyph glyph_index,
float scaleX = 1.0f, float scaleY = 1.0f, float translateX = 0.0f, float translateY = 0.0f )
{
STBTT_assert( cache );
STBTT_assert( font >= 0 && font < cache->entry.size() );
ve_fontcache_entry& entry = cache->entry[ font ];
if ( !glyph_index ) {
// Glyph not in current hb_font.
return false;
}
// Retrieve the shape definition from STB TrueType.
if ( stbtt_IsGlyphEmpty( &entry.info, glyph_index ) )
return true;
stbtt_vertex* shape = nullptr;
int nverts = stbtt_GetGlyphShape( &entry.info, glyph_index, &shape );
if ( !nverts || !shape ) {
return false;
}
#ifdef VE_FONTCACHE_DEBUGPRINT_VERBOSE
printf( "shape: \n" );
for ( int i = 0; i < nverts; i++ ) {
if ( shape[i].type == STBTT_vmove ) {
printf(" move_to %d %d\n", shape[i].x, shape[i].y );
} else if ( shape[i].type == STBTT_vline ) {
printf(" line_to %d %d\n", shape[i].x, shape[i].y );
} else if ( shape[i].type == STBTT_vcurve ) {
printf(" curve_to %d %d through %d %d\n", shape[i].x, shape[i].y, shape[i].cx, shape[i].cy );
} else if ( shape[i].type == STBTT_vcubic ) {
printf(" cubic_to %d %d through %d %d and %d %d\n", shape[i].x, shape[i].y, shape[i].cx, shape[i].cy, shape[i].cx1, shape[i].cy1 );
}
}
#endif // VE_FONTCACHE_DEBUGPRINT_VERBOSE
// We need a random point that is outside our shape. We simply pick something diagonally across from top-left bound corner.
// Note that this outside point is scaled alongside the glyph in ve_fontcache_draw_filled_path, so we don't need to handle that here.
int bounds_x0, bounds_x1, bounds_y0, bounds_y1;
int success = stbtt_GetGlyphBox( &entry.info, glyph_index, &bounds_x0, &bounds_y0, &bounds_x1, &bounds_y1 );
STBTT_assert( success );
ve_fontcache_vec2 outside = ve_fontcache_make_vec2( bounds_x0 - 21, bounds_y0 - 33 );
// Figure out scaling so it fits within our box.
ve_fontcache_draw draw;
draw.pass = VE_FONTCACHE_FRAMEBUFFER_PASS_GLYPH;
draw.start_index = cache->drawlist.indices.size();
// Draw the path using simplified version of https://medium.com/@evanwallace/easy-scalable-text-rendering-on-the-gpu-c3f4d782c5ac.
// Instead of involving fragment shader code we simply make use of modern GPU ability to crunch triangles and brute force curve definitions.
//
std::vector< ve_fontcache_vec2 >& path = cache->temp_path;
path.clear();
for ( int i = 0; i < nverts; i++ ) {
stbtt_vertex& edge = shape[i];
switch ( edge.type ) {
case STBTT_vmove:
if ( path.size() > 0 ) {
ve_fontcache_draw_filled_path( cache->drawlist, outside, path, scaleX, scaleY, translateX, translateY );
}
path.clear();
// Fallthrough.
case STBTT_vline:
path.push_back( ve_fontcache_make_vec2( shape[i].x, shape[i].y ) );
break;
case STBTT_vcurve: {
STBTT_assert( path.size() > 0 );
ve_fontcache_vec2 p0 = path[ path.size() - 1 ];
ve_fontcache_vec2 p1 = ve_fontcache_make_vec2( shape[i].cx, shape[i].cy );
ve_fontcache_vec2 p2 = ve_fontcache_make_vec2( shape[i].x, shape[i].y );
float step = 1.0f / VE_FONTCACHE_CURVE_QUALITY, t = step;
for ( int i = 0; i < VE_FONTCACHE_CURVE_QUALITY; i++ ) {
path.push_back( ve_fontcache_eval_bezier( p0, p1, p2, t ) );
t += step;
}
break;
}
case STBTT_vcubic: {
STBTT_assert( path.size() > 0 );
ve_fontcache_vec2 p0 = path[ path.size() - 1 ];
ve_fontcache_vec2 p1 = ve_fontcache_make_vec2( shape[i].cx, shape[i].cy );
ve_fontcache_vec2 p2 = ve_fontcache_make_vec2( shape[i].cx1, shape[i].cy1 );
ve_fontcache_vec2 p3 = ve_fontcache_make_vec2( shape[i].x, shape[i].y );
float step = 1.0f / VE_FONTCACHE_CURVE_QUALITY, t = step;
for ( int i = 0; i < VE_FONTCACHE_CURVE_QUALITY; i++ ) {
path.push_back( ve_fontcache_eval_bezier( p0, p1, p2, p3, t ) );
t += step;
}
break;
}
default:
STBTT_assert( !"Unknown shape edge type." );
}
}
if ( path.size() > 0 ) {
ve_fontcache_draw_filled_path( cache->drawlist, outside, path, scaleX, scaleY, translateX, translateY );
}
// Append the draw call.
draw.end_index = cache->drawlist.indices.size();
if ( draw.end_index > draw.start_index ) {
cache->drawlist.dcalls.push_back( draw );
}
stbtt_FreeShape( &entry.info, shape );
return true;
}
static ve_atlas_region ve_fontcache_decide_codepoint_region( ve_fontcache* cache, ve_fontcache_entry& entry, int glyph_index,
ve_fontcache_LRU*& state, uint32_t*& next_idx, float& oversample_x, float& oversample_y )
{
if ( stbtt_IsGlyphEmpty( &entry.info, glyph_index ) )
return '\0';
// Get hb_font text metrics. These are unscaled!
int bounds_x0, bounds_x1, bounds_y0, bounds_y1;
int success = stbtt_GetGlyphBox( &entry.info, glyph_index, &bounds_x0, &bounds_y0, &bounds_x1, &bounds_y1 );
int bounds_width = bounds_x1 - bounds_x0;
int bounds_height = bounds_y1 - bounds_y0;
STBTT_assert( success );
// Decide which atlas to target. This logic should work well for reasonable on-screen text sizes of around 24px.
// For 4k+ displays, caching hb_font at a lower pt and drawing it upscaled at a higher pt is recommended.
//
ve_atlas_region region;
float bwidth_scaled = bounds_width * entry.size_scale + 2.0f * VE_FONTCACHE_ATLAS_GLYPH_PADDING;
float bheight_scaled = bounds_height * entry.size_scale + 2.0f * VE_FONTCACHE_ATLAS_GLYPH_PADDING;
if ( bwidth_scaled <= VE_FONTCACHE_ATLAS_REGION_A_WIDTH && bheight_scaled <= VE_FONTCACHE_ATLAS_REGION_A_HEIGHT )
{
// Region A for small glyphs. These are good for things such as punctuation.
region = 'A';
state = &cache->atlas.stateA;
next_idx = &cache->atlas.next_atlas_idx_A;
}
else if ( bwidth_scaled <= VE_FONTCACHE_ATLAS_REGION_B_WIDTH && bheight_scaled > VE_FONTCACHE_ATLAS_REGION_B_HEIGHT )
{
// Region B for tall glyphs. These are good for things such as european alphabets.
region = 'B';
state = &cache->atlas.stateB;
next_idx = &cache->atlas.next_atlas_idx_B;
}
else if ( bwidth_scaled <= VE_FONTCACHE_ATLAS_REGION_C_WIDTH && bheight_scaled <= VE_FONTCACHE_ATLAS_REGION_C_HEIGHT )
{
// Region C for big glyphs. These are good for things such as asian typography.
region = 'C';
state = &cache->atlas.stateC;
next_idx = &cache->atlas.next_atlas_idx_C;
}
else if ( bwidth_scaled <= VE_FONTCACHE_ATLAS_REGION_D_WIDTH && bheight_scaled <= VE_FONTCACHE_ATLAS_REGION_D_HEIGHT )
{
// Region D for huge glyphs. These are good for things such as titles and 4k.
region = 'D';
state = &cache->atlas.stateD;
next_idx = &cache->atlas.next_atlas_idx_D;
}
else if ( bwidth_scaled <= VE_FONTCACHE_GLYPHDRAW_BUFFER_WIDTH && bheight_scaled <= VE_FONTCACHE_GLYPHDRAW_BUFFER_HEIGHT )
{
// Region 'E' for massive glyphs. These are rendered uncached and un-oversampled.
region = 'E';
state = nullptr;
next_idx = nullptr;
if ( bwidth_scaled <= VE_FONTCACHE_GLYPHDRAW_BUFFER_WIDTH / 2 && bheight_scaled <= VE_FONTCACHE_GLYPHDRAW_BUFFER_HEIGHT / 2 ) {
oversample_x = 2.0f; oversample_y = 2.0f;
} else {
oversample_x = 1.0f; oversample_y = 1.0f;
}
return region;
}
else
{
return '\0';
}
STBTT_assert( state );
STBTT_assert( next_idx );
return region;
}
static void ve_fontcache_flush_glyph_buffer_to_atlas( ve_fontcache* cache )
{
// Flush drawcalls to draw list.
ve_fontcache_merge_drawlist( cache->drawlist, cache->atlas.glyph_update_batch_clear_drawlist );
ve_fontcache_merge_drawlist( cache->drawlist, cache->atlas.glyph_update_batch_drawlist );
ve_fontcache_clear_drawlist( cache->atlas.glyph_update_batch_clear_drawlist );
ve_fontcache_clear_drawlist( cache->atlas.glyph_update_batch_drawlist );
// Clear glyph_update_FBO.
if ( cache->atlas.glyph_update_batch_x != 0 ) {
ve_fontcache_draw dcall;
dcall.pass = VE_FONTCACHE_FRAMEBUFFER_PASS_GLYPH;
dcall.start_index = 0;
dcall.end_index = 0;
dcall.clear_before_draw = true;
cache->drawlist.dcalls.push_back( dcall );
cache->atlas.glyph_update_batch_x = 0;
}
}
static void ve_fontcache_screenspace_xform( float& x, float& y, float& scalex, float& scaley, float width, float height )
{
scalex /= width;
scaley /= height;
scalex *= 2.0f;
scaley *= 2.0f;
x *= ( 2.0f / width );
y *= ( 2.0f / height );
x -= 1.0f;
y -= 1.0f;
}
static void ve_fontcache_texspace_xform( float& x, float& y, float& scalex, float& scaley, float width, float height )
{
x /= width;
y /= height;
scalex /= width;
scaley /= height;
}
static void ve_fontcache_atlas_bbox( ve_atlas_region region, int local_idx, float& x, float& y, float& width, float& height )
{
if ( region == 'A' ) {
width = VE_FONTCACHE_ATLAS_REGION_A_WIDTH;
height = VE_FONTCACHE_ATLAS_REGION_A_HEIGHT;
x = ( local_idx % VE_FONTCACHE_ATLAS_REGION_A_XCAPACITY ) * VE_FONTCACHE_ATLAS_REGION_A_WIDTH;
y = ( local_idx / VE_FONTCACHE_ATLAS_REGION_A_XCAPACITY ) * VE_FONTCACHE_ATLAS_REGION_A_HEIGHT;
x += VE_FONTCACHE_ATLAS_REGION_A_XOFFSET;
y += VE_FONTCACHE_ATLAS_REGION_A_YOFFSET;
} else if ( region == 'B' ) {
width = VE_FONTCACHE_ATLAS_REGION_B_WIDTH;
height = VE_FONTCACHE_ATLAS_REGION_B_HEIGHT;
x = ( local_idx % VE_FONTCACHE_ATLAS_REGION_B_XCAPACITY ) * VE_FONTCACHE_ATLAS_REGION_B_WIDTH;
y = ( local_idx / VE_FONTCACHE_ATLAS_REGION_B_XCAPACITY ) * VE_FONTCACHE_ATLAS_REGION_B_HEIGHT;
x += VE_FONTCACHE_ATLAS_REGION_B_XOFFSET;
y += VE_FONTCACHE_ATLAS_REGION_B_YOFFSET;
} else if ( region == 'C' ) {
width = VE_FONTCACHE_ATLAS_REGION_C_WIDTH;
height = VE_FONTCACHE_ATLAS_REGION_C_HEIGHT;
x = ( local_idx % VE_FONTCACHE_ATLAS_REGION_C_XCAPACITY ) * VE_FONTCACHE_ATLAS_REGION_C_WIDTH;
y = ( local_idx / VE_FONTCACHE_ATLAS_REGION_C_XCAPACITY ) * VE_FONTCACHE_ATLAS_REGION_C_HEIGHT;
x += VE_FONTCACHE_ATLAS_REGION_C_XOFFSET;
y += VE_FONTCACHE_ATLAS_REGION_C_YOFFSET;
} else {
STBTT_assert( region == 'D' );
width = VE_FONTCACHE_ATLAS_REGION_D_WIDTH;
height = VE_FONTCACHE_ATLAS_REGION_D_HEIGHT;
x = ( local_idx % VE_FONTCACHE_ATLAS_REGION_D_XCAPACITY ) * VE_FONTCACHE_ATLAS_REGION_D_WIDTH;
y = ( local_idx / VE_FONTCACHE_ATLAS_REGION_D_XCAPACITY ) * VE_FONTCACHE_ATLAS_REGION_D_HEIGHT;
x += VE_FONTCACHE_ATLAS_REGION_D_XOFFSET;
y += VE_FONTCACHE_ATLAS_REGION_D_YOFFSET;
}
}
void ve_fontcache_cache_glyph_to_atlas( ve_fontcache* cache, ve_font_id font, ve_glyph glyph_index )
{
STBTT_assert( cache );
STBTT_assert( font >= 0 && font < cache->entry.size() );
ve_fontcache_entry& entry = cache->entry[ font ];
if ( !glyph_index ) {
// Glyph not in current hb_font.
return;
}
if ( stbtt_IsGlyphEmpty( &entry.info, glyph_index ) )
return;
// Get hb_font text metrics. These are unscaled!
int bounds_x0, bounds_x1, bounds_y0, bounds_y1;
int success = stbtt_GetGlyphBox( &entry.info, glyph_index, &bounds_x0, &bounds_y0, &bounds_x1, &bounds_y1 );
int bounds_width = bounds_x1 - bounds_x0,
bounds_height = bounds_y1 - bounds_y0;
STBTT_assert( success );
// Decide which atlas to target.
ve_fontcache_LRU* state = nullptr; uint32_t* next_idx = nullptr;
float oversample_x = VE_FONTCACHE_GLYPHDRAW_OVERSAMPLE_X,
oversample_y = VE_FONTCACHE_GLYPHDRAW_OVERSAMPLE_Y;
ve_atlas_region region = ve_fontcache_decide_codepoint_region( cache, entry, glyph_index, state, next_idx, oversample_x, oversample_y );
// E region is special case and not cached to atlas.
if ( region == '\0' || region == 'E' ) return;
// Grab an atlas LRU cache slot.
uint64_t lru_code = glyph_index + ( ( 0x100000000ULL * font ) & 0xFFFFFFFF00000000ULL );
int atlas_index = ve_fontcache_LRU_get( *state, lru_code );
if ( atlas_index == -1 )
{
if ( *next_idx < state->capacity )
{
uint64_t evicted = ve_fontcache_LRU_put( *state, lru_code, *next_idx );
atlas_index = *next_idx;
( *next_idx )++;
STBTT_assert( evicted == lru_code );
}
else
{
uint64_t next_evict_codepoint = ve_fontcache_LRU_get_next_evicted( *state );
STBTT_assert( next_evict_codepoint != 0xFFFFFFFFFFFFFFFFULL );
atlas_index = ve_fontcache_LRU_peek( *state, next_evict_codepoint );
STBTT_assert( atlas_index != -1 );
uint64_t evicted = ve_fontcache_LRU_put( *state, lru_code, atlas_index );
STBTT_assert( evicted == next_evict_codepoint );
}
STBTT_assert( ve_fontcache_LRU_get( *state, lru_code ) != -1 );
}
#ifdef VE_FONTCACHE_DEBUGPRINT
static int debug_total_cached = 0;
//printf( "glyph 0x%x( %c ) caching to atlas region %c at idx %d. %d total glyphs cached.\n", unicode, (char) unicode, (char) region, atlas_index, debug_total_cached++ );
#endif // VE_FONTCACHE_DEBUGPRINT
// Draw oversized glyph to update FBO.
float glyph_draw_scale_x = entry.size_scale * oversample_x;
float glyph_draw_scale_y = entry.size_scale * oversample_y;
float glyph_draw_translate_x = -bounds_x0 * glyph_draw_scale_x + VE_FONTCACHE_GLYPHDRAW_PADDING;
float glyph_draw_translate_y = -bounds_y0 * glyph_draw_scale_y + VE_FONTCACHE_GLYPHDRAW_PADDING;
glyph_draw_translate_x = ( int ) ( glyph_draw_translate_x + 0.9999999f );
glyph_draw_translate_y = ( int ) ( glyph_draw_translate_y + 0.9999999f );
// Allocate a glyph_update_FBO region.
int gdwidth_scaled_px = ( int )( bounds_width * glyph_draw_scale_x + 1.0f ) + 2 * oversample_x * VE_FONTCACHE_GLYPHDRAW_PADDING;
if( cache->atlas.glyph_update_batch_x + gdwidth_scaled_px >= VE_FONTCACHE_GLYPHDRAW_BUFFER_WIDTH ) {
ve_fontcache_flush_glyph_buffer_to_atlas( cache );
}
// Calculate the src and destination regions.
float destx, desty, destw, desth, srcx, srcy, srcw, srch;
ve_fontcache_atlas_bbox( region, atlas_index, destx, desty, destw, desth );
float dest_glyph_x = destx + VE_FONTCACHE_ATLAS_GLYPH_PADDING,
dest_glyph_y = desty + VE_FONTCACHE_ATLAS_GLYPH_PADDING,
dest_glyph_w = bounds_width * entry.size_scale,
dest_glyph_h = bounds_height * entry.size_scale;
dest_glyph_x -= VE_FONTCACHE_GLYPHDRAW_PADDING;
dest_glyph_y -= VE_FONTCACHE_GLYPHDRAW_PADDING;
dest_glyph_w += 2 * VE_FONTCACHE_GLYPHDRAW_PADDING;
dest_glyph_h += 2 * VE_FONTCACHE_GLYPHDRAW_PADDING;
ve_fontcache_screenspace_xform( dest_glyph_x, dest_glyph_y, dest_glyph_w, dest_glyph_h, VE_FONTCACHE_ATLAS_WIDTH, VE_FONTCACHE_ATLAS_HEIGHT );
ve_fontcache_screenspace_xform( destx, desty, destw, desth, VE_FONTCACHE_ATLAS_WIDTH, VE_FONTCACHE_ATLAS_HEIGHT );
srcx = cache->atlas.glyph_update_batch_x;
srcy = 0.0;
srcw = bounds_width * glyph_draw_scale_x;
srch = bounds_height * glyph_draw_scale_y;
srcw += 2 * oversample_x * VE_FONTCACHE_GLYPHDRAW_PADDING;
srch += 2 * oversample_y * VE_FONTCACHE_GLYPHDRAW_PADDING;
ve_fontcache_texspace_xform( srcx, srcy, srcw, srch, VE_FONTCACHE_GLYPHDRAW_BUFFER_WIDTH, VE_FONTCACHE_GLYPHDRAW_BUFFER_HEIGHT );
// Advance glyph_update_batch_x and calcuate final glyph drawing transform.
glyph_draw_translate_x += cache->atlas.glyph_update_batch_x;
cache->atlas.glyph_update_batch_x += gdwidth_scaled_px;
ve_fontcache_screenspace_xform( glyph_draw_translate_x, glyph_draw_translate_y, glyph_draw_scale_x, glyph_draw_scale_y, VE_FONTCACHE_GLYPHDRAW_BUFFER_WIDTH, VE_FONTCACHE_GLYPHDRAW_BUFFER_HEIGHT );
// Queue up clear on target region on atlas.
ve_fontcache_draw dcall;
dcall.pass = VE_FONTCACHE_FRAMEBUFFER_PASS_ATLAS;
dcall.region = ( uint32_t )( -1 );
dcall.start_index = cache->atlas.glyph_update_batch_clear_drawlist.indices.size();
ve_fontcache_blit_quad( cache->atlas.glyph_update_batch_clear_drawlist, destx, desty, destx + destw, desty + desth, 1.0f, 1.0f, 1.0f, 1.0f );
dcall.end_index = cache->atlas.glyph_update_batch_clear_drawlist.indices.size();
cache->atlas.glyph_update_batch_clear_drawlist.dcalls.push_back( dcall );
// Queue up a blit from glyph_update_FBO to the atlas.
dcall.region = ( uint32_t )( 0 );
dcall.start_index = cache->atlas.glyph_update_batch_drawlist.indices.size();
ve_fontcache_blit_quad( cache->atlas.glyph_update_batch_drawlist, dest_glyph_x, dest_glyph_y, destx + dest_glyph_w, desty + dest_glyph_h, srcx, srcy, srcx + srcw, srcy + srch );
dcall.end_index = cache->atlas.glyph_update_batch_drawlist.indices.size();
cache->atlas.glyph_update_batch_drawlist.dcalls.push_back( dcall );
// Render glyph to glyph_update_FBO.
ve_fontcache_cache_glyph(
cache, font, glyph_index,
glyph_draw_scale_x, glyph_draw_scale_y,
glyph_draw_translate_x, glyph_draw_translate_y
);
}
void ve_fontcache_shape_text_uncached( ve_fontcache* cache, ve_font_id font, ve_fontcache_shaped_text& output, const std::string& text_utf8 )
{
STBTT_assert( cache );
STBTT_assert( font >= 0 && font < cache->entry.size() );
bool use_full_text_shape = cache->text_shape_advanced;
ve_fontcache_entry& entry = cache->entry[ font ];
output.glyphs.clear();
output.pos.clear();
int ascent = 0, descent = 0, line_gap = 0;
stbtt_GetFontVMetrics( &entry.info, &ascent, &descent, &line_gap );
#ifdef VE_FONTCACHE_HARFBUZZ
if ( use_full_text_shape ) {
// Use HarfBuzz for text shaping. Yay! This is good.
hb_script_t current_script = HB_SCRIPT_UNKNOWN;
hb_unicode_funcs_t* hb_ucfunc = hb_unicode_funcs_get_default();
hb_buffer_clear_contents( cache->hb_text_buffer );
STBTT_assert( entry.hb_font );
// Lambda to shape a run using HarfBuzz library.
float pos = 0.0f, vpos = 0.0f;
auto ve_fontcache_shape_hb_run = [&]( hb_buffer_t * buf, hb_script_t script )
{
// Set script and direction. We use the system's default language.
//script = HB_SCRIPT_LATIN;
hb_buffer_set_script( buf, script );
hb_buffer_set_direction( buf, hb_script_get_horizontal_direction( script ) );
hb_buffer_set_language( buf, hb_language_get_default() );
// Perform the actual shaping of this run using HarfBuzz.
hb_shape( entry.hb_font, buf, nullptr, 0 );
// Loop over glyphs and append to output buffer.
unsigned int glyph_count;
hb_glyph_info_t *glyph_info = hb_buffer_get_glyph_infos( buf, &glyph_count );
hb_glyph_position_t *glyph_pos = hb_buffer_get_glyph_positions( buf, &glyph_count );
for ( int i = 0; i < glyph_count; i++ ) {
ve_glyph glyph_id = ( ve_glyph ) glyph_info[i].codepoint;
if ( glyph_info[i].cluster ) {
pos = 0.0f;
vpos -= ( ascent - descent + line_gap ) * entry.size_scale;
vpos = ( int ) ( vpos + 0.5f );
continue;
}
if ( std::abs( entry.size ) <= VE_FONTCACHE_ADVANCE_SNAP_SMALLFONT_SIZE ) {
// Expand advance to closest pixel for hb_font small sizes.
pos = std::ceilf( pos );
}
output.glyphs.push_back( glyph_id );
float offset_x = glyph_pos[i].x_offset * entry.size_scale, offset_y = glyph_pos[i].y_offset * entry.size_scale;
output.pos.push_back( ve_fontcache_make_vec2( int( pos + offset_x + 0.5 ), vpos + offset_y ) );
pos += glyph_pos[i].x_advance * entry.size_scale;
vpos += glyph_pos[i].y_advance * entry.size_scale;
}
output.end_cursor_pos.x = pos;
output.end_cursor_pos.y = vpos;
hb_buffer_clear_contents( buf );
};
// We first start with simple bidi and run logic.
// True CTL is pretty hard and we don't fully support that; patches welcome!
void* v = nullptr;
utf8_int32_t codepoint;
size_t u32_length = utf8len( text_utf8.data() );
for ( v = utf8codepoint( text_utf8.data(), &codepoint ); codepoint; v = utf8codepoint( v, &codepoint ) ) {
hb_script_t script = hb_unicode_script( hb_ucfunc, ( hb_codepoint_t ) codepoint );
// Can we continue the current run?
bool special_script = ( script == HB_SCRIPT_UNKNOWN || script == HB_SCRIPT_INHERITED || script == HB_SCRIPT_COMMON );
if ( special_script || script == current_script ) {
hb_buffer_add( cache->hb_text_buffer, ( hb_codepoint_t ) codepoint, codepoint == '\n' ? 1 : 0 );
current_script = special_script ? current_script : script;
continue;
}
// End current run since we've encountered a script change.
ve_fontcache_shape_hb_run( cache->hb_text_buffer, current_script );
hb_buffer_add( cache->hb_text_buffer, ( hb_codepoint_t ) codepoint, codepoint == '\n' ? 1 : 0 );
current_script = script;
}
// End the last run if needed.
ve_fontcache_shape_hb_run( cache->hb_text_buffer, current_script );
return;
}
#else // VE_FONTCACHE_HARFBUZZ
cache->text_shape_advanced = false;
#endif // VE_FONTCACHE_HARFBUZZ
// We use our own fallback dumbass text shaping.
// WARNING: PLEASE USE HARFBUZZ. GOOD TEXT SHAPING IS IMPORTANT FOR INTERNATIONALISATION.
utf8_int32_t codepoint, prev_codepoint = 0;
size_t u32_length = utf8len( text_utf8.data() );
output.glyphs.reserve( u32_length );
output.pos.reserve( u32_length );
float pos = 0.0f,
vpos = 0.0f;
int advance = 0,
to_left_side_glyph = 0;
// Loop through text and shape.
for ( void* v = utf8codepoint( text_utf8.data(), &codepoint ); codepoint; v = utf8codepoint( v, &codepoint ) )
{
if ( prev_codepoint )
{
int kern = stbtt_GetCodepointKernAdvance( &entry.info, prev_codepoint, codepoint );
pos += kern * entry.size_scale;
}
if ( codepoint == '\n' )
{
pos = 0.0f;
vpos -= ( ascent - descent + line_gap ) * entry.size_scale;
vpos = ( int ) ( vpos + 0.5f );
prev_codepoint = 0;
continue;
}
if ( std::abs( entry.size ) <= VE_FONTCACHE_ADVANCE_SNAP_SMALLFONT_SIZE )
{
// Expand advance to closest pixel for hb_font small sizes.
pos = std::ceilf( pos );
}
output.glyphs.push_back( stbtt_FindGlyphIndex( &entry.info, codepoint ) );
stbtt_GetCodepointHMetrics( &entry.info, codepoint, &advance, &to_left_side_glyph );
output.pos.push_back( ve_fontcache_make_vec2( int( pos + 0.5 ), vpos ) );
float adv = advance * entry.size_scale;
pos += adv;
prev_codepoint = codepoint;
}
output.end_cursor_pos.x = pos;
output.end_cursor_pos.y = vpos;
}
template < typename T >
void ve_fontcache_ELFhash64( uint64_t& hash, const T* ptr, size_t count = 1 )
{
uint64_t x = 0;
auto bytes = reinterpret_cast< const uint8_t* >( ptr );
for ( int i = 0; i < sizeof( T ) * count; i++ ) {
hash = ( hash << 4 ) + bytes[i];
if( ( x = hash & 0xF000000000000000ULL ) != 0 ) {
hash ^= ( x >> 24 );
}
hash &= ~x;
}
}
static ve_fontcache_shaped_text& ve_fontcache_shape_text_cached( ve_fontcache* cache, ve_font_id font, const std::string& text_utf8 )
{
uint64_t hash = 0x9f8e00d51d263c24ULL;
ve_fontcache_ELFhash64( hash, ( const uint8_t* ) text_utf8.data(), text_utf8.size() );
ve_fontcache_ELFhash64( hash, &font );
ve_fontcache_LRU& state = cache->shape_cache.state;
int shape_cache_idx = ve_fontcache_LRU_get( state, hash );
if ( shape_cache_idx == -1 )
{
if ( cache->shape_cache.next_cache_idx < state.capacity )
{
shape_cache_idx = cache->shape_cache.next_cache_idx++;
ve_fontcache_LRU_put( state, hash, shape_cache_idx );
}
else
{
uint64_t next_evict_idx = ve_fontcache_LRU_get_next_evicted( state );
STBTT_assert( next_evict_idx != 0xFFFFFFFFFFFFFFFFULL );
shape_cache_idx = ve_fontcache_LRU_peek( state, next_evict_idx );
STBTT_assert( shape_cache_idx != -1 );
ve_fontcache_LRU_put( state, hash, shape_cache_idx );
}
ve_fontcache_shape_text_uncached( cache, font, cache->shape_cache.storage[ shape_cache_idx ], text_utf8 );
}
return cache->shape_cache.storage[ shape_cache_idx ];
}
static void ve_fontcache_directly_draw_massive_glyph( ve_fontcache* cache, ve_fontcache_entry& entry, ve_glyph glyph, int bounds_x0, int bounds_y0, int bounds_width, int bounds_height,
float oversample_x = 1.0f, float oversample_y = 1.0f, float posx = 0.0f, float posy = 0.0f, float scalex = 1.0f, float scaley = 1.0f )
{
// Flush out whatever was in the glyph buffer beforehand to atlas.
ve_fontcache_flush_glyph_buffer_to_atlas( cache );
// Draw un-antialiased glyph to update FBO.
float glyph_draw_scale_x = entry.size_scale * oversample_x;
float glyph_draw_scale_y = entry.size_scale * oversample_y;
float glyph_draw_translate_x = -bounds_x0 * glyph_draw_scale_x + VE_FONTCACHE_GLYPHDRAW_PADDING;
float glyph_draw_translate_y = -bounds_y0 * glyph_draw_scale_y + VE_FONTCACHE_GLYPHDRAW_PADDING;
ve_fontcache_screenspace_xform( glyph_draw_translate_x, glyph_draw_translate_y,
glyph_draw_scale_x, glyph_draw_scale_y,
VE_FONTCACHE_GLYPHDRAW_BUFFER_WIDTH, VE_FONTCACHE_GLYPHDRAW_BUFFER_HEIGHT );
// Render glyph to glyph_update_FBO.
ve_fontcache_cache_glyph(
cache, entry.font_id, glyph,
glyph_draw_scale_x, glyph_draw_scale_y,
glyph_draw_translate_x, glyph_draw_translate_y
);
// Figure out the source rect.
float glyph_x = 0.0f,
glyph_y = 0.0f,
glyph_w = bounds_width * entry.size_scale * oversample_x,
glyph_h = bounds_height * entry.size_scale * oversample_y;
float glyph_dest_w = bounds_width * entry.size_scale,
glyph_dest_h = bounds_height * entry.size_scale;
glyph_w += 2 * VE_FONTCACHE_ATLAS_GLYPH_PADDING;
glyph_h += 2 * VE_FONTCACHE_ATLAS_GLYPH_PADDING;
glyph_dest_w += 2 * VE_FONTCACHE_ATLAS_GLYPH_PADDING;
glyph_dest_h += 2 * VE_FONTCACHE_ATLAS_GLYPH_PADDING;
// Figure out the destination rect.
float bounds_x0_scaled = int( bounds_x0 * entry.size_scale - 0.5f );
float bounds_y0_scaled = int( bounds_y0 * entry.size_scale - 0.5f );
float dest_x = posx + scalex * bounds_x0_scaled;
float dest_y = posy + scaley * bounds_y0_scaled;
float dest_w = scalex * glyph_dest_w,
dest_h = scaley * glyph_dest_h;
dest_x -= scalex * VE_FONTCACHE_ATLAS_GLYPH_PADDING;
dest_y -= scaley * VE_FONTCACHE_ATLAS_GLYPH_PADDING;
ve_fontcache_texspace_xform( glyph_x, glyph_y, glyph_w, glyph_h, VE_FONTCACHE_GLYPHDRAW_BUFFER_WIDTH, VE_FONTCACHE_GLYPHDRAW_BUFFER_HEIGHT );
// Add the glyph drawcall.
ve_fontcache_draw dcall;
dcall.pass = VE_FONTCACHE_FRAMEBUFFER_PASS_TARGET_UNCACHED;
dcall.colour[0] = cache->colour[0]; dcall.colour[1] = cache->colour[1]; dcall.colour[2] = cache->colour[2]; dcall.colour[3] = cache->colour[3];
dcall.start_index = cache->drawlist.indices.size();
ve_fontcache_blit_quad( cache->drawlist,
dest_x, dest_y,
dest_x + dest_w, dest_y + dest_h,
glyph_x, glyph_y,
glyph_x + glyph_w, glyph_y + glyph_h );
dcall.end_index = cache->drawlist.indices.size();
cache->drawlist.dcalls.push_back( dcall );
// Clear glyph_update_FBO.
dcall.pass = VE_FONTCACHE_FRAMEBUFFER_PASS_GLYPH;
dcall.start_index = 0;
dcall.end_index = 0;
dcall.clear_before_draw = true;
cache->drawlist.dcalls.push_back( dcall );
}
static bool ve_fontcache_empty( ve_fontcache* cache, ve_fontcache_entry& entry, ve_glyph glyph_index )
{
if ( !glyph_index ) {
// Glyph not in current hb_font.
return true;
}
if ( stbtt_IsGlyphEmpty( &entry.info, glyph_index ) )
return true;
return false;
}
// This function only draws codepoints that have been drawn. Returns false without drawing anything if uncached.
bool ve_fontcache_draw_cached_glyph( ve_fontcache* cache, ve_fontcache_entry& entry, ve_glyph glyph_index, float posx = 0.0f, float posy = 0.0f, float scalex = 1.0f, float scaley = 1.0f )
{
if ( !glyph_index ) {
// Glyph not in current hb_font.
return true;
}
if ( stbtt_IsGlyphEmpty( &entry.info, glyph_index ) )
return true;
// Get hb_font text metrics. These are unscaled!
int bounds_x0, bounds_x1, bounds_y0, bounds_y1;
int success = stbtt_GetGlyphBox( &entry.info, glyph_index, &bounds_x0, &bounds_y0, &bounds_x1, &bounds_y1 );
int bounds_width = bounds_x1 - bounds_x0,
bounds_height = bounds_y1 - bounds_y0;
STBTT_assert( success );
// Decide which atlas to target.
ve_fontcache_LRU* state = nullptr;
uint32_t* next_idx = nullptr;
float oversample_x = VE_FONTCACHE_GLYPHDRAW_OVERSAMPLE_X,
oversample_y = VE_FONTCACHE_GLYPHDRAW_OVERSAMPLE_Y;
ve_atlas_region region = ve_fontcache_decide_codepoint_region( cache, entry, glyph_index, state, next_idx, oversample_x, oversample_y );
// E region is special case and not cached to atlas.
if ( region == 'E' ) {
ve_fontcache_directly_draw_massive_glyph(
cache, entry, glyph_index,
bounds_x0, bounds_y0, bounds_width, bounds_height,
oversample_x, oversample_y,
posx, posy, scalex, scaley
);
return true;
}
// Is this codepoint cached?
uint64_t lru_code = glyph_index + ( ( 0x100000000ULL * entry.font_id ) & 0xFFFFFFFF00000000ULL );
int atlas_index = ve_fontcache_LRU_get( *state, lru_code );
if( atlas_index == -1 ) {
return false;
}
// Figure out the source bounding box in atlas texture.
float atlas_x, atlas_y, atlas_w, atlas_h;
ve_fontcache_atlas_bbox( region, atlas_index, atlas_x, atlas_y, atlas_w, atlas_h );
float glyph_x = atlas_x,
glyph_y = atlas_y,
glyph_w = bounds_width * entry.size_scale,
glyph_h = bounds_height * entry.size_scale;
glyph_w += 2 * VE_FONTCACHE_ATLAS_GLYPH_PADDING;
glyph_h += 2 * VE_FONTCACHE_ATLAS_GLYPH_PADDING;
// Figure out the destination rect.
float bounds_x0_scaled = int( bounds_x0 * entry.size_scale - 0.5f );
float bounds_y0_scaled = int( bounds_y0 * entry.size_scale - 0.5f );
float dest_x = posx + scalex * bounds_x0_scaled;
float dest_y = posy + scaley * bounds_y0_scaled;
float dest_w = scalex * glyph_w,
dest_h = scaley * glyph_h;
dest_x -= scalex * VE_FONTCACHE_ATLAS_GLYPH_PADDING;
dest_y -= scaley * VE_FONTCACHE_ATLAS_GLYPH_PADDING;
ve_fontcache_texspace_xform( glyph_x, glyph_y, glyph_w, glyph_h, VE_FONTCACHE_ATLAS_WIDTH, VE_FONTCACHE_ATLAS_HEIGHT );
// Add the glyph drawcall.
ve_fontcache_draw dcall;
dcall.pass = VE_FONTCACHE_FRAMEBUFFER_PASS_TARGET;
dcall.colour[0] = cache->colour[0]; dcall.colour[1] = cache->colour[1]; dcall.colour[2] = cache->colour[2]; dcall.colour[3] = cache->colour[3];
dcall.start_index = cache->drawlist.indices.size();
ve_fontcache_blit_quad( cache->drawlist, dest_x, dest_y, dest_x + dest_w, dest_y + dest_h, glyph_x, glyph_y, glyph_x + glyph_w, glyph_y + glyph_h );
dcall.end_index = cache->drawlist.indices.size();
cache->drawlist.dcalls.push_back( dcall );
return true;
}
static void ve_fontcache_reset_batch_codepoint_state( ve_fontcache* cache )
{
cache->temp_codepoint_seen.clear();
cache->temp_codepoint_seen.reserve( 256 );
}
static bool ve_fontcache_can_batch_glyph( ve_fontcache* cache, ve_font_id font, ve_fontcache_entry& entry, ve_glyph glyph_index )
{
STBTT_assert( cache );
STBTT_assert( entry.font_id == font );
// Decide which atlas to target.
STBTT_assert( glyph_index != -1 );
ve_fontcache_LRU* state = nullptr; uint32_t* next_idx = nullptr;
float oversample_x = VE_FONTCACHE_GLYPHDRAW_OVERSAMPLE_X, oversample_y = VE_FONTCACHE_GLYPHDRAW_OVERSAMPLE_Y;
ve_atlas_region region = ve_fontcache_decide_codepoint_region( cache, entry, glyph_index, state, next_idx, oversample_x, oversample_y );
// E region can't batch.
if ( region == 'E' || region == '\0' ) return false;
if ( cache->temp_codepoint_seen.size() > 1024 ) return false;
// Is this glyph cached?
uint64_t lru_code = glyph_index + ( ( 0x100000000ULL * entry.font_id ) & 0xFFFFFFFF00000000ULL );
int atlas_index = ve_fontcache_LRU_get( *state, lru_code );
if ( atlas_index == -1 ) {
if ( *next_idx >= 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.
uint64_t next_evict_codepoint = ve_fontcache_LRU_get_next_evicted( *state );
if ( cache->temp_codepoint_seen[ next_evict_codepoint ] ) {
return false;
}
}
ve_fontcache_cache_glyph_to_atlas( cache, font, glyph_index );
}
STBTT_assert( ve_fontcache_LRU_get( *state, lru_code ) != -1 );
cache->temp_codepoint_seen[ lru_code ] = true;
return true;
}
static void ve_fontcache_draw_text_batch( ve_fontcache* cache, ve_fontcache_entry& entry, ve_fontcache_shaped_text& shaped,
int batch_start_idx, int batch_end_idx, float posx, float posy, float scalex, float scaley )
{
ve_fontcache_flush_glyph_buffer_to_atlas( cache );
for( int j = batch_start_idx; j < batch_end_idx; j++ ) {
ve_glyph glyph_index = shaped.glyphs[j];
float glyph_translate_x = posx + shaped.pos[j].x * scalex,
glyph_translate_y = posy + shaped.pos[j].y * scaley;
bool glyph_cached = ve_fontcache_draw_cached_glyph( cache, entry, glyph_index, glyph_translate_x, glyph_translate_y, scalex, scaley );
STBTT_assert( glyph_cached );
}
}
bool ve_fontcache_draw_text( ve_fontcache* cache, ve_font_id font, const std::string& text_utf8, float posx, float posy, float scalex, float scaley )
{
STBTT_assert(cache);
STBTT_assert(font >= 0 && font < cache->entry.size());
ve_fontcache_shaped_text& shaped = ve_fontcache_shape_text_cached( cache, font, text_utf8 );
if ( cache->snap_width ) posx = ( ( int ) ( posx * cache->snap_width + 0.5f ) ) / ( float ) cache->snap_width;
if ( cache->snap_height ) posy = ( ( int ) ( posy * cache->snap_height + 0.5f ) ) / ( float ) cache->snap_height;
ve_fontcache_entry& entry = cache->entry[ font ];
int batch_start_idx = 0;
for( int i = 0; i < shaped.glyphs.size(); i++ )
{
ve_glyph glyph_index = shaped.glyphs[i];
if ( ve_fontcache_empty( cache, entry, glyph_index ) )
continue;
if ( ve_fontcache_can_batch_glyph( cache, font, entry, glyph_index ) ) {
continue;
}
ve_fontcache_draw_text_batch( cache, entry, shaped, batch_start_idx, i, posx, posy, scalex, scaley );
ve_fontcache_reset_batch_codepoint_state( cache );
ve_fontcache_cache_glyph_to_atlas( cache, font, glyph_index );
uint64_t lru_code = glyph_index + ( ( 0x100000000ULL * font ) & 0xFFFFFFFF00000000ULL );
cache->temp_codepoint_seen[ lru_code ] = true;
batch_start_idx = i;
}
ve_fontcache_draw_text_batch( cache, entry, shaped, batch_start_idx, shaped.glyphs.size(), posx, posy, scalex, scaley );
ve_fontcache_reset_batch_codepoint_state( cache );
cache->cursor_pos.x = posx + shaped.end_cursor_pos.x * scalex;
cache->cursor_pos.y = posy + shaped.end_cursor_pos.x * scaley;
return true;
}
ve_fontcache_vec2 ve_fontcache_get_cursor_pos( ve_fontcache* cache )
{
return cache->cursor_pos;
}
void ve_fontcache_optimise_drawlist( ve_fontcache* cache )
{
STBTT_assert( cache );
int write_idx = 0;
for ( int i = 1; i < cache->drawlist.dcalls.size(); i++ ) {
STBTT_assert( write_idx <= i );
ve_fontcache_draw& draw0 = cache->drawlist.dcalls[ write_idx ];
ve_fontcache_draw& draw1 = cache->drawlist.dcalls[ i ];
bool merge = true;
if ( draw0.pass != draw1.pass ) merge = false;
if ( draw0.end_index != draw1.start_index ) merge = false;
if ( draw0.region != draw1.region ) merge = false;
if ( draw1.clear_before_draw ) merge = false;
if ( draw0.colour[0] != draw1.colour[0]
|| draw0.colour[1] != draw1.colour[1]
|| draw0.colour[2] != draw1.colour[2]
|| draw0.colour[3] != draw1.colour[3] ) merge = false;
if ( merge ) {
draw0.end_index = draw1.end_index;
draw1.start_index = draw1.end_index = 0;
} else {
ve_fontcache_draw& draw2 = cache->drawlist.dcalls[ ++write_idx ];
if ( write_idx != i ) draw2 = draw1;
}
}
cache->drawlist.dcalls.resize( write_idx + 1 );
}
void ve_fontcache_set_colour( ve_fontcache* cache, float c[4] )
{
cache->colour[0] = c[0];
cache->colour[1] = c[1];
cache->colour[2] = c[2];
cache->colour[3] = c[3];
}
// ------------------------------------------------------ Generic Data Structure Implementations ------------------------------------------
void ve_fontcache_poollist_init( ve_fontcache_poollist& plist, int capacity )
{
plist.pool.resize( capacity );
plist.freelist.resize( capacity );
plist.capacity = capacity;
for ( int i = 0; i < capacity; i++ ) plist.freelist[ i ] = i;
}
void ve_fontcache_poollist_push_front( ve_fontcache_poollist& plist, ve_fontcache_poollist_value v )
{
if ( plist.size >= plist.capacity ) return;
assert( plist.freelist.size() > 0 );
assert( plist.freelist.size() == plist.capacity - plist.size );
ve_fontcache_poollist_itr idx = plist.freelist.back();
plist.freelist.pop_back();
plist.pool[ idx ].prev = -1;
plist.pool[ idx ].next = plist.front;
plist.pool[ idx ].value = v;
if ( plist.front != -1 ) plist.pool[ plist.front ].prev = idx;
if ( plist.back == -1 ) plist.back = idx;
plist.front = idx;
plist.size++;
}
void ve_fontcache_poollist_erase( ve_fontcache_poollist& plist, ve_fontcache_poollist_itr it )
{
if ( plist.size <= 0 ) return;
assert( it >= 0 && it < plist.capacity );
assert( plist.freelist.size() == plist.capacity - plist.size );
if ( plist.pool[ it ].prev != -1 )
plist.pool[ plist.pool[ it ].prev ].next = plist.pool[ it ].next;
if ( plist.pool[ it ].next != -1 )
plist.pool[ plist.pool[ it ].next ].prev = plist.pool[ it ].prev;
if ( plist.front == it ) plist.front = plist.pool[ it ].next;
if ( plist.back == it ) plist.back = plist.pool[ it ].prev;
plist.pool[ it ].prev = -1;
plist.pool[ it ].next = -1;
plist.pool[ it ].value = 0;
plist.freelist.push_back( it );
if ( --plist.size == 0 ) plist.back = plist.front = -1;
}
ve_fontcache_poollist_value ve_fontcache_poollist_peek_back( ve_fontcache_poollist& plist )
{
assert( plist.back != -1 );
return plist.pool[ plist.back ].value;
}
ve_fontcache_poollist_value ve_fontcache_poollist_pop_back( ve_fontcache_poollist& plist )
{
if ( plist.size <= 0 ) return 0;
assert( plist.back != -1 );
ve_fontcache_poollist_value v = plist.pool[ plist.back ].value;
ve_fontcache_poollist_erase( plist, plist.back );
return v;
}
void ve_fontcache_LRU_init( ve_fontcache_LRU& LRU, int capacity )
{
// Thanks to dfsbfs from leetcode! This code is eavily based on that with simplifications made on top.
// ref: https://leetcode.com/problems/lru-cache/discuss/968703/c%2B%2B
// https://leetcode.com/submissions/detail/436667816/
LRU.capacity = capacity;
LRU.cache.reserve( capacity );
ve_fontcache_poollist_init( LRU.key_queue, capacity );
}
void ve_fontcache_LRU_refresh( ve_fontcache_LRU& LRU, uint64_t key )
{
auto it = LRU.cache.find( key );
ve_fontcache_poollist_erase( LRU.key_queue, it->second.ptr );
ve_fontcache_poollist_push_front( LRU.key_queue, key );
it->second.ptr = LRU.key_queue.front;
}
int ve_fontcache_LRU_get( ve_fontcache_LRU& LRU, uint64_t key )
{
auto it = LRU.cache.find( key );
if ( it == LRU.cache.end() ) {
return -1;
}
ve_fontcache_LRU_refresh( LRU, key );
return it->second.value;
}
int ve_fontcache_LRU_peek( ve_fontcache_LRU& LRU, uint64_t key )
{
auto it = LRU.cache.find( key );
if ( it == LRU.cache.end() ) {
return -1;
}
return it->second.value;
}
uint64_t ve_fontcache_LRU_put( ve_fontcache_LRU& LRU, uint64_t key, int val )
{
auto it = LRU.cache.find( key );
if ( it != LRU.cache.end() ) {
ve_fontcache_LRU_refresh( LRU, key );
it->second.value = val;
return key;
}
uint64_t evict = key;
if ( LRU.key_queue.size >= LRU.capacity ) {
evict = ve_fontcache_poollist_pop_back( LRU.key_queue );
LRU.cache.erase( evict );
}
ve_fontcache_poollist_push_front( LRU.key_queue, key );
LRU.cache[ key ].value = val;
LRU.cache[ key ].ptr = LRU.key_queue.front;
return evict;
}
uint64_t ve_fontcache_LRU_get_next_evicted( ve_fontcache_LRU& LRU )
{
if ( LRU.key_queue.size >= LRU.capacity ) {
uint64_t evict = ve_fontcache_poollist_peek_back( LRU.key_queue );;
return evict;
}
return 0xFFFFFFFFFFFFFFFFULL;
}
#endif // VE_FONTCACHE_IMPL