mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-09 03:29:43 -10:00
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:
1 parent
a9c370fd3f
commit
57bb21af33
3 files changed
+217
-68
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user