(Font) Change the menu language without restarting

font_driver_reload_fonts() rebuilt each font from the path it was
created with, which re-reads the same face. Right for a file that
changed underneath, useless for a language change, which wants a
different file: Chinese needs pkg/chinese-fallback-font.ttf where the
font was built from ozone/bold.ttf.

So a font that follows the language now says so, and says what it
needs to be worked out again: the assets pkg directory the language
fonts live in, and the path to use when the language wants no special
one. font_driver_set_language_font() records both, and the reload
re-resolves through font_driver_language_font_file() rather than
reusing the old path.

The language setting calls the reload. A context reset would have
done it too, at the cost of tearing down video, audio and input to
change a typeface; this rebuilds the fonts where they stand. Each
font_data_t keeps its address, so nothing holding one has to be told,
and the generation bump makes ozone, materialui, widgets and xmb
recompute their derived metrics on the next frame - which is why
those four went first.

ozone marks its six fonts, and skips marking when an explicit menu
font is set: there is nothing to re-resolve then, and overriding the
user's choice on a language change would be wrong. materialui,
widgets and file_path_special still want the same one-line call.

The tests grow the case this was for: that English keeps the default
face, that Korean re-resolves to the Korean one, and that switching
back restores the default - the point being the path changes rather
than merely being re-read. They need the path helpers linked in now
that font_driver.c joins one. Clean under ASan, UBSan, LeakSanitizer
and TSan.

Thai is still not covered, and not because of this: it has no case
because no shipped font has the glyphs. fallback-font.ttf covers
Arabic but not U+0E01. That needs an asset before it needs code.
This commit is contained in:
R authored and libretroadmin committed 2026-08-08 20:51:58 +02:00
1 parent 4845f3b13f
commit 42a711fab5
7 files changed
+154 -17

No files matched your search

+55 -4
View File
@@ -88,6 +88,38 @@ const char *font_driver_language_font_file(void)
return NULL;
}
void font_driver_set_language_font(font_data_t *font,
const char *pkg_dir, const char *default_path)
{
if (!font)
return;
free(font->lang_pkg_dir);
free(font->lang_default_path);
font->lang_pkg_dir = (pkg_dir && *pkg_dir)
? strdup(pkg_dir) : NULL;
font->lang_default_path = (default_path && *default_path)
? strdup(default_path) : NULL;
}
/* The path this font should be using now. For a language-following
* font that is worked out again from the current language; for any
* other it is the path it already has. */
static const char *font_driver_resolve_path(font_data_t *font,
char *s, size_t len)
{
const char *lang_font;
if (!font->lang_pkg_dir || !font->lang_default_path)
return font->path;
if (!(lang_font = font_driver_language_font_file()))
return font->lang_default_path;
fill_pathname_join_special(s, font->lang_pkg_dir, lang_font, len);
return s;
}
unsigned font_driver_reload_fonts(void)
{
font_data_t *font;
@@ -106,9 +138,22 @@ unsigned font_driver_reload_fonts(void)
/* The backend's own init does the resolving and the reading, so
* this is the same call that created the font in the first
* place - just against whatever the path now points at. */
if (!(fresh = renderer->init(font->video_data, font->path,
font->size, font->is_threaded)))
continue; /* keep the old font rather than lose text */
{
char resolved[PATH_MAX_LENGTH];
const char *want = font_driver_resolve_path(font,
resolved, sizeof(resolved));
if (!(fresh = renderer->init(font->video_data, want,
font->size, font->is_threaded)))
continue; /* keep the old font rather than lose text */
/* Remember what it is actually built from now. */
if (want != font->path)
{
free(font->path);
font->path = strdup(want);
}
}
/* Swap in place: the font_data_t address does not change, so
* every holder of the pointer stays valid. Only the renderer
@@ -864,7 +909,11 @@ void font_driver_free(font_data_t *font)
}
free(font->path);
font->path = NULL;
free(font->lang_pkg_dir);
free(font->lang_default_path);
font->path = NULL;
font->lang_pkg_dir = NULL;
font->lang_default_path = NULL;
#ifdef HAVE_THREADS
/* Ask for the real threaded state, not the video_threaded
@@ -916,6 +965,8 @@ font_data_t *font_driver_init_first(
font->video_data = video_data;
font->path = (font_path && *font_path)
? strdup(font_path) : NULL;
font->lang_pkg_dir = NULL;
font->lang_default_path = NULL;
font->is_threaded = is_threaded;
font_driver_cache_metrics(font);
+13
View File
@@ -92,6 +92,11 @@ typedef struct font_data
struct font_data *next; /* list of live fonts */
void *video_data;
char *path; /* NULL when the renderer chose */
/* Set for a font whose file depends on the menu language. Kept so
* the path can be worked out again rather than merely re-read: a
* language change wants a different face, not the same one. */
char *lang_pkg_dir;
char *lang_default_path;
bool is_threaded;
/* Line metrics, read from the renderer once when the font is
* created. Renderers fill these at init and never change them, so
@@ -159,6 +164,14 @@ unsigned font_driver_reload_fonts(void);
* which is three places to forget when a language is added. */
const char *font_driver_language_font_file(void);
/* Declare that this font's file follows the menu language, and give
* the two things needed to work it out again: the assets pkg
* directory the language fonts live in, and the path to use when the
* language needs no special font. font_driver_reload_fonts() then
* re-resolves instead of re-reading. */
void font_driver_set_language_font(font_data_t *font,
const char *pkg_dir, const char *default_path);
/* Returns a monotonic counter incremented on every font free;
* see font_driver.c for rationale. Used to validate externally
* cached per-font derived data */
+37 -8
View File
@@ -10049,6 +10049,7 @@ static void ozone_set_layout(
const char *path_menu_font = settings->paths.path_menu_ozone_font;
char tmp_dir[DIR_MAX_LENGTH];
char font_path[PATH_MAX_LENGTH];
char default_font_path[PATH_MAX_LENGTH];
bool font_inited = false;
float scale_factor = ozone->last_scale_factor;
float padding_factor = settings->floats.ozone_padding_factor;
@@ -10117,13 +10118,14 @@ static void ozone_set_layout(
{
const char *lang_font = font_driver_language_font_file();
fill_pathname_join_special(tmp_dir, path_directory_assets, "pkg", sizeof(tmp_dir));
fill_pathname_join_special(default_font_path, ozone->assets_path, "bold.ttf",
sizeof(default_font_path));
if (lang_font)
{
fill_pathname_join_special(tmp_dir, path_directory_assets, "pkg", sizeof(tmp_dir));
fill_pathname_join_special(font_path, tmp_dir, lang_font, sizeof(font_path));
}
else
fill_pathname_join_special(font_path, ozone->assets_path, "bold.ttf", sizeof(font_path));
strlcpy(font_path, default_font_path, sizeof(font_path));
}
if (path_menu_font && *path_menu_font)
@@ -10137,13 +10139,14 @@ static void ozone_set_layout(
{
const char *lang_font = font_driver_language_font_file();
fill_pathname_join_special(tmp_dir, path_directory_assets, "pkg", sizeof(tmp_dir));
fill_pathname_join_special(default_font_path, ozone->assets_path, "regular.ttf",
sizeof(default_font_path));
if (lang_font)
{
fill_pathname_join_special(tmp_dir, path_directory_assets, "pkg", sizeof(tmp_dir));
fill_pathname_join_special(font_path, tmp_dir, lang_font, sizeof(font_path));
}
else
fill_pathname_join_special(font_path, ozone->assets_path, "regular.ttf", sizeof(font_path));
strlcpy(font_path, default_font_path, sizeof(font_path));
}
if (path_menu_font && *path_menu_font)
@@ -10181,6 +10184,32 @@ static void ozone_set_layout(
if (!(((ozone->flags & OZONE_FLAG_HAS_ALL_ASSETS) > 0) && font_inited))
ozone->flags &= ~OZONE_FLAG_HAS_ALL_ASSETS;
/* Mark every font as following the menu language, so a language
* change can rebuild them in place instead of asking for a
* restart. The title takes the bold face, the rest the regular
* one; both are overridden by an explicit menu font setting, in
* which case there is nothing to re-resolve. */
if (!(path_menu_font && *path_menu_font))
{
char bold_path[PATH_MAX_LENGTH];
fill_pathname_join_special(bold_path, ozone->assets_path,
"bold.ttf", sizeof(bold_path));
font_driver_set_language_font(ozone->fonts.title.font,
tmp_dir, bold_path);
font_driver_set_language_font(ozone->fonts.sidebar.font,
tmp_dir, default_font_path);
font_driver_set_language_font(ozone->fonts.entries_label.font,
tmp_dir, default_font_path);
font_driver_set_language_font(ozone->fonts.entries_sublabel.font,
tmp_dir, default_font_path);
font_driver_set_language_font(ozone->fonts.time.font,
tmp_dir, default_font_path);
font_driver_set_language_font(ozone->fonts.footer.font,
tmp_dir, default_font_path);
}
/* Cache footer text labels
* > Fonts have been (re)initialised, so need
* to recalculate label widths */
+10 -2
View File
@@ -8934,16 +8934,24 @@ static void general_write_handler(rarch_setting_t *setting)
case MENU_ENUM_LABEL_INPUT_POLL_TYPE_BEHAVIOR:
core_set_poll_type(*setting->value.target.integer);
break;
#if defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__) && defined(HAVE_MENU)
case MENU_ENUM_LABEL_USER_LANGUAGE:
#if defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__) && defined(HAVE_MENU)
/* The native Win32 menubar bakes translated strings at
* menu-creation time (popup headers in particular have no
* resource ID and so are not walked by win32_localize_menu
* afterwards). Rebuild the whole menubar so every string -
* including popup headers - reflects the new language. */
win32_menubar_rebuild();
break;
#endif
/* Arabic and Persian, Chinese and Korean each want a
* different face. Rebuild the fonts that follow the language
* where they stand: each font_data_t keeps its address, so
* nothing holding one has to be told, and the generation bump
* makes the derived metrics recompute on the next frame.
* A context reset would do it too, at the cost of tearing
* down video, audio and input to change a typeface. */
font_driver_reload_fonts();
break;
case MENU_ENUM_LABEL_VIDEO_SCALE_INTEGER:
{
video_driver_state_t *video_st = video_state_get_ptr();
+7 -1
View File
@@ -4,7 +4,13 @@ SAN ?= address,undefined
CFLAGS := -std=gnu99 -g -O1 -fsanitize=$(SAN) -DHAVE_STB_FONT -DRARCH_INTERNAL \
-I$(RA) -I$(RA)/libretro-common/include -I$(RA)/gfx/include \
-I$(RA)/deps -I$(RA)/deps/stb
SRC := $(RA)/gfx/font_driver.c
SRC := $(RA)/gfx/font_driver.c \
$(RA)/libretro-common/file/file_path.c \
$(RA)/libretro-common/compat/compat_strl.c \
$(RA)/libretro-common/string/stdstring.c \
$(RA)/libretro-common/encodings/encoding_utf.c \
$(RA)/libretro-common/lists/string_list.c \
$(RA)/libretro-common/time/rtime.c
all: lifecycle create
lifecycle: font_lifecycle_test.c stubs_retroarch.c $(SRC)
$(CC) $(CFLAGS) -o $@ $^ -lm
@@ -68,6 +68,8 @@ font_renderer_t fake_font = {
/* --- scenarios --- */
int read_should_fail = 0;
extern unsigned test_language;
#define TEST_LANG_KOREAN 10 /* RETRO_LANGUAGE_KOREAN */
static int fails = 0;
#define CHECK(c, msg) do { if (!(c)) { printf(" FAIL: %s\n", msg); fails++; } } while (0)
@@ -170,6 +172,32 @@ int main(void)
}
}
/* 8. a language-following font re-resolves rather than re-reads */
{
font_data_t *L = mk("/assets/ozone/bold.ttf", 16.0f);
CHECK(L != NULL, "language font created");
font_driver_set_language_font(L, "/assets/pkg", "/assets/ozone/bold.ttf");
/* English: no special font, so the default path stands */
test_language = 0;
font_driver_reload_fonts();
CHECK(L->path && !strcmp(L->path, "/assets/ozone/bold.ttf"),
"english keeps the default face");
/* Korean: the path must change, not merely be re-read */
test_language = TEST_LANG_KOREAN;
font_driver_reload_fonts();
CHECK(L->path && strstr(L->path, "korean-fallback-font.ttf") != NULL,
"korean re-resolves to the korean face");
/* and back */
test_language = 0;
font_driver_reload_fonts();
CHECK(L->path && !strcmp(L->path, "/assets/ozone/bold.ttf"),
"switching back restores the default face");
font_driver_free(L);
}
font_driver_free(a);
font_driver_free(c);
CHECK(live_renderer_state == 0, "all renderer state released at teardown");
+4 -2
View File
@@ -28,11 +28,13 @@ bool filestream_read_file(const char *path, void **buf, int64_t *len)
/* font_driver_language_font_file() asks which language is selected.
* English here: the tests are about the lifecycle, not the mapping. */
unsigned test_language = 0; /* RETRO_LANGUAGE_ENGLISH */
unsigned *msg_hash_get_uint(enum msg_hash_action type)
{
static unsigned lang = 0; /* RETRO_LANGUAGE_ENGLISH */
(void)type;
return ⟨
return &test_language;
}
static video_driver_state_t vst;