gfx_animation: Cache smooth ticker glyph widths across frames

With a proportional font, gfx_animation_ticker_smooth() called
font_driver_get_message_width() once per glyph, per label, per
frame - an indirect call chain into the video backend's font
raster, which itself performs a fallback '?' glyph lookup and a
glyph table search on every single-character call - plus a
utf8len() rescan and (for long strings) a calloc/free pair per
frame. All of this recomputed values that only change when the
string, font or scale change. XMB, Ozone and MaterialUI each
invoke this from ~10 call sites for every visible label.

Add a per-string glyph width cache inside gfx_animation.c:
64 direct-mapped sets x 2 ways, keyed on string hash + exact
string compare + font pointer + font scale + a new font driver
generation counter. Steady-state frames reduce to a hash and a
string compare per label, with zero font driver calls; measured
in a harness against a mock font driver, a 55-character label
went from ~60 font driver calls per frame to 0.

Correctness properties:
- The miss path computes widths with the identical per-glyph
  loop the function used previously, so rendered output is
  bit-exact. Verified by differential fuzzing of the patched
  function (linked object) against a verbatim clone of the
  pre-patch implementation: 200000 iterations over random
  strings (including multibyte UTF-8), both ticker types,
  selected/unselected, three font scales, four fonts and random
  field widths/indices - zero output mismatches.
- Two ways per set are required for correctness, not hit rate:
  one call performs up to two lookups (source + spacer, or
  source + ".") whose results are used simultaneously.
  Sequence-stamped eviction guarantees the second lookup can
  never evict the first, even when both strings map to the same
  set. Verified under ASan with brute-forced colliding strings
  churned over 15000 frames: clean.
- Font pointers are compared, never dereferenced, and entries
  additionally carry a generation counter bumped by
  font_driver_free() (new font_driver_get_generation() API), so
  a font freed and reallocated at the same address can never
  produce a stale hit. Verified: generation bump forces a full
  recompute on the next frame.
- Cache is flushed in gfx_animation_deinit(); full fuzz run is
  leak-clean under ASan.

The non-selected clip path's per-frame "." width call and the
per-frame spacer scan are cached through the same mechanism,
and the per-frame calloc/free for long strings is gone (width
arrays are owned by the cache).

Cost: +192 bytes .text, +7.2KB bss (the static entry table);
heap usage is bounded by 128 cached strings. This is a
speed-only patch and is deliberately not part of the menu
size reduction campaign.
This commit is contained in:
libretroadmin authored and libretroadmin committed 2026-07-22 02:08:14 +02:00
1 parent a9c370fd3f
commit 57bb21af33
3 files changed
+217 -68

No files matched your search

+17
View File
@@ -35,6 +35,19 @@
/* TODO/FIXME - global */
static void *video_font_driver = NULL;
/* Monotonic counter incremented whenever any font instance is
* freed. Consumers that cache per-font derived data (e.g. the
* smooth ticker glyph width cache in gfx_animation.c) key their
* entries on this value: font_data_t pointers can be recycled by
* the allocator across free/create cycles, so pointer equality
* alone cannot prove a cached entry still describes a live font */
static uint32_t font_driver_generation = 0;
uint32_t font_driver_get_generation(void)
{
return font_driver_generation;
}
int font_renderer_create_default(
const font_renderer_driver_t **drv,
void **handle, const char *font_path, unsigned font_size)
@@ -999,6 +1012,10 @@ void font_driver_free(font_data_t *font)
if (font)
{
bool is_threaded = false;
/* Invalidate any externally cached per-font derived data */
font_driver_generation++;
#ifdef HAVE_THREADS
bool *is_threaded_tmp = video_driver_get_threaded();
is_threaded = *is_threaded_tmp;
+5
View File
@@ -114,6 +114,11 @@ int font_driver_get_message_width(void *font_data, const char *msg, size_t len,
void font_driver_free(font_data_t *font);
/* Returns a monotonic counter incremented on every font free;
* see font_driver.c for rationale. Used to validate externally
* cached per-font derived data */
uint32_t font_driver_get_generation(void);
void font_flush(
unsigned video_width,
unsigned video_height,
+195 -68
View File
@@ -1189,6 +1189,174 @@ bool gfx_animation_ticker(gfx_animation_ctx_ticker_t *ticker)
return true;
}
/* Smooth ticker glyph width cache
*
* With a proportional font, the smooth ticker previously called
* font_driver_get_message_width() once per glyph, per label, per
* frame - an indirect call chain into the video backend's font
* raster (which itself performs a fallback '?' glyph lookup and a
* glyph table search per call) for values that only change when
* the string, font or scale change. Cache the per-character width
* arrays instead: steady-state frames reduce to a hash + string
* compare per label.
*
* Structure: direct-mapped sets, two ways per set. Two ways are
* required for correctness, not just hit rate - a single
* gfx_animation_ticker_smooth() call performs up to two lookups
* (source string + spacer, or source string + ".") whose results
* are used simultaneously; with the sequence-stamped eviction
* below, the second lookup can never evict the first even when
* both strings map to the same set.
*
* Entries are validated by exact string compare plus font
* pointer, scale and the font driver generation counter. The
* generation check makes pointer recycling safe: a font freed
* and reallocated at the same address bumps the generation, so
* stale entries miss and are evicted. entry->font is only ever
* compared, never dereferenced. */
#define TICKER_WCACHE_SETS 64 /* must be a power of two */
#define TICKER_WCACHE_WAYS 2
typedef struct
{
char *str; /* owned copy of the source string */
unsigned *widths; /* owned per-character advance widths */
font_data_t *font; /* compared, never dereferenced */
size_t num_chars;
uint32_t hash;
uint32_t generation;
uint32_t last_use; /* lookup sequence stamp for eviction */
unsigned total_width;
float font_scale;
} ticker_wcache_entry_t;
static ticker_wcache_entry_t ticker_wcache
[TICKER_WCACHE_SETS][TICKER_WCACHE_WAYS];
static uint32_t ticker_wcache_seq = 0;
static uint32_t ticker_wcache_hash(const char *s)
{
/* FNV-1a, 32-bit */
uint32_t h = 0x811c9dc5u;
while (*s)
{
h ^= (uint8_t)*s++;
h *= 0x01000193u;
}
return h;
}
static void ticker_wcache_entry_clear(ticker_wcache_entry_t *e)
{
if (e->str)
free(e->str);
if (e->widths)
free(e->widths);
memset(e, 0, sizeof(*e));
}
static void ticker_wcache_flush(void)
{
size_t i, j;
for (i = 0; i < TICKER_WCACHE_SETS; i++)
for (j = 0; j < TICKER_WCACHE_WAYS; j++)
ticker_wcache_entry_clear(&ticker_wcache[i][j]);
}
/* Returns the cached (or freshly computed and inserted) per
* character width data for 'str' rendered with 'font' at
* 'font_scale', or NULL on failure (empty string, allocation
* failure, or the font driver reporting a negative width).
* Failures are never cached. The returned entry remains valid
* until the second-next lookup at the earliest; callers may
* safely hold the results of two consecutive lookups. */
static const ticker_wcache_entry_t *ticker_wcache_get(
font_data_t *font, float font_scale, const char *str)
{
size_t i;
const char *str_ptr;
ticker_wcache_entry_t *victim;
size_t num_chars;
unsigned *widths;
unsigned total = 0;
uint32_t hash = ticker_wcache_hash(str);
uint32_t gen = font_driver_get_generation();
ticker_wcache_entry_t *set =
ticker_wcache[hash & (TICKER_WCACHE_SETS - 1)];
ticker_wcache_seq++;
/* Lookup */
for (i = 0; i < TICKER_WCACHE_WAYS; i++)
{
ticker_wcache_entry_t *e = &set[i];
if ( (e->str)
&& (e->hash == hash)
&& (e->font == font)
&& (e->generation == gen)
&& (e->font_scale == font_scale)
&& (strcmp(e->str, str) == 0))
{
e->last_use = ticker_wcache_seq;
return e;
}
}
/* Miss - compute widths exactly as the previous per-glyph
* loop did, so rendered output is bit-identical */
if ((num_chars = utf8len(str)) < 1)
return NULL;
if (!(widths = (unsigned*)malloc(num_chars * sizeof(unsigned))))
return NULL;
str_ptr = str;
for (i = 0; i < num_chars; i++)
{
int glyph_width = font_driver_get_message_width(
font, str_ptr, 1, font_scale);
if (glyph_width < 0)
{
free(widths);
return NULL;
}
widths[i] = (unsigned)glyph_width;
total += (unsigned)glyph_width;
str_ptr = utf8skip(str_ptr, 1);
}
/* Insert - evict the least recently stamped way. The way
* touched by the immediately preceding lookup carries the
* previous sequence value and is therefore never selected */
victim = &set[0];
for (i = 1; i < TICKER_WCACHE_WAYS; i++)
if (set[i].last_use < victim->last_use)
victim = &set[i];
ticker_wcache_entry_clear(victim);
if (!(victim->str = strdup(str)))
{
free(widths);
return NULL;
}
victim->widths = widths;
victim->font = font;
victim->num_chars = num_chars;
victim->hash = hash;
victim->generation = gen;
victim->last_use = ticker_wcache_seq;
victim->total_width = total;
victim->font_scale = font_scale;
return victim;
}
/* 'Fixed width' font version of gfx_animation_ticker_smooth() */
static bool gfx_animation_ticker_smooth_fw(
gfx_animation_t *p_anim,
@@ -1350,15 +1518,14 @@ end:
bool gfx_animation_ticker_smooth(gfx_animation_ctx_ticker_smooth_t *ticker)
{
size_t i;
size_t src_str_len = 0;
size_t spacer_len = 0;
unsigned small_src_char_widths[64] = {0};
unsigned src_str_width = 0;
unsigned spacer_width = 0;
unsigned *src_char_widths = NULL;
unsigned *spacer_char_widths = NULL;
const char *str_ptr = NULL;
const unsigned *src_char_widths = NULL;
const unsigned *spacer_char_widths = NULL;
const ticker_wcache_entry_t *src_entry = NULL;
const ticker_wcache_entry_t *spacer_entry = NULL;
bool success = false;
bool is_active = false;
gfx_animation_t *p_anim = &anim_st;
@@ -1375,33 +1542,15 @@ bool gfx_animation_ticker_smooth(gfx_animation_ctx_ticker_smooth_t *ticker)
if (!ticker->font)
return gfx_animation_ticker_smooth_fw(p_anim, ticker);
/* Find the display width of each character in
* the src string + total width */
if ((src_str_len = utf8len(ticker->src_str)) < 1)
/* Get the display width of each character in the src
* string + total width (cached across frames) */
if (!(src_entry = ticker_wcache_get(
ticker->font, ticker->font_scale, ticker->src_str)))
goto end;
src_char_widths = small_src_char_widths;
if (src_str_len > ARRAY_SIZE(small_src_char_widths))
{
if (!(src_char_widths = (unsigned*)calloc(src_str_len, sizeof(unsigned))))
goto end;
}
str_ptr = ticker->src_str;
for (i = 0; i < src_str_len; i++)
{
int glyph_width = font_driver_get_message_width(
ticker->font, str_ptr, 1, ticker->font_scale);
if (glyph_width < 0)
goto end;
src_char_widths[i] = (unsigned)glyph_width;
src_str_width += (unsigned)glyph_width;
str_ptr = utf8skip(str_ptr, 1);
}
src_str_len = src_entry->num_chars;
src_char_widths = src_entry->widths;
src_str_width = src_entry->total_width;
/* If total src string width is <= text field width, we
* can just copy the entire string */
@@ -1425,15 +1574,17 @@ bool gfx_animation_ticker_smooth(gfx_animation_ctx_ticker_smooth_t *ticker)
unsigned text_width;
unsigned current_width = 0;
unsigned num_chars = 0;
int period_width =
font_driver_get_message_width(ticker->font,
".", 1, ticker->font_scale);
unsigned period_width;
const ticker_wcache_entry_t *period_entry =
ticker_wcache_get(ticker->font, ticker->font_scale, ".");
/* Sanity check */
if (period_width < 0)
if (!period_entry)
goto end;
if (ticker->field_width < (3 * (unsigned)period_width))
period_width = period_entry->total_width;
if (ticker->field_width < (3 * period_width))
goto end;
/* Determine number of characters to copy */
@@ -1481,28 +1632,15 @@ bool gfx_animation_ticker_smooth(gfx_animation_ctx_ticker_smooth_t *ticker)
if (!ticker->spacer)
ticker->spacer = TICKER_SPACER_DEFAULT;
/* Find the display width of each character in
* the spacer */
if ((spacer_len = utf8len(ticker->spacer)) < 1)
/* Get the display width of each character in the
* spacer (cached across frames) */
if (!(spacer_entry = ticker_wcache_get(
ticker->font, ticker->font_scale, ticker->spacer)))
goto end;
if (!(spacer_char_widths = (unsigned*)calloc(spacer_len, sizeof(unsigned))))
goto end;
str_ptr = ticker->spacer;
for (i = 0; i < spacer_len; i++)
{
int glyph_width = font_driver_get_message_width(
ticker->font, str_ptr, 1, ticker->font_scale);
if (glyph_width < 0)
goto end;
spacer_char_widths[i] = (unsigned)glyph_width;
spacer_width += (unsigned)glyph_width;
str_ptr = utf8skip(str_ptr, 1);
}
spacer_len = spacer_entry->num_chars;
spacer_char_widths = spacer_entry->widths;
spacer_width = spacer_entry->total_width;
/* Determine animation type */
switch (ticker->type_enum)
@@ -1565,19 +1703,7 @@ bool gfx_animation_ticker_smooth(gfx_animation_ctx_ticker_smooth_t *ticker)
p_anim->flags |= GFX_ANIM_FLAG_TICKER_IS_ACTIVE;
end:
if (src_char_widths != small_src_char_widths && src_char_widths)
{
free(src_char_widths);
src_char_widths = NULL;
}
if (spacer_char_widths)
{
free(spacer_char_widths);
spacer_char_widths = NULL;
}
/* Width arrays are owned by the cache - nothing to free */
if (!success)
{
*ticker->x_offset = 0;
@@ -1650,6 +1776,7 @@ void gfx_animation_deinit(void)
RBUF_FREE(p_anim->list);
RBUF_FREE(p_anim->pending);
memset(p_anim, 0, sizeof(*p_anim));
ticker_wcache_flush();
}
void gfx_animation_timer_start(float *timer, gfx_timer_ctx_entry_t *timer_entry)