(Font) No file paths left in the font renderers

76f8821 moved the reads out and 266285f dropped the headers, but the
renderers still dealt in paths: freetype kept streams/file_stream.h for
the fontconfig branch, coretext still opened its font itself through a
CFURL, and every init() took a font_path it mostly no longer used.

They take bytes now, and nothing else.

  void *(*init)(uint8_t *font_data, size_t font_data_len,
        unsigned face_index, float font_size,
        enum font_atlas_format fmt);

  const char * const *(*get_default_fonts)(const char *requested,
        unsigned *face_index);

The two things that kept fontconfig in init() are what the new
arguments are for. It resolves against the request and the user's
locale, so the resolver is given the requested path; and it answers
with a face index as well as a path, which matters for a TrueType
collection - a CJK system font selects the wrong face at index 0 - so
the resolver hands one back. What is left in init() is
FT_New_Memory_Face on bytes somebody else read.

coretext went from CFURLCreateWithFileSystemPath and
CGDataProviderCreateWithURL to CGDataProviderCreateWithData over the
buffer it was given, with a release callback so CoreGraphics discharges
the ownership when it is finished. It was the last renderer opening a
file, and the only one that still needed the path.

stb needed nothing but the signature: it had stopped reading the path
already.

What is left in the directory that matches a grep for "path" is the
static candidate list freetype returns to
font_renderer_create_default(), and the fontconfig result string being
copied out of the pattern. Neither opens anything.

The fontconfig branch compiles here for the first time -
libfontconfig1-dev, -DHAVE_FONTCONFIG_SUPPORT - rather than being
carried untested. stb, freetype with and without fontconfig, and
font_driver are all clean; both sample suites pass under ASan, UBSan,
LeakSanitizer and TSan.

coretext is Apple-only and still unbuildable here, and it is the
biggest change of the three: CI is the check on it.
This commit is contained in:
I authored and libretroadmin committed 2026-08-08 23:20:32 +02:00
1 parent 6fceb0fc25
commit 92509d31e5
5 files changed
+181 -203

No files matched your search

+30 -42
View File
@@ -518,51 +518,48 @@ static bool coretext_font_renderer_render_glyph(CTFontRef face, ct_font_renderer
return true;
}
static void *font_renderer_ct_init(const char *font_path,
/* CoreGraphics calls this when it is done with the buffer, which is
* how ownership of the bytes handed to init() is discharged. */
static void ct_font_data_release(void *info, const void *data, size_t size)
{
(void)info;
(void)size;
free((void*)data);
}
static void *font_renderer_ct_init(
uint8_t *font_data, size_t font_data_len,
unsigned face_index,
float font_size, enum font_atlas_format fmt)
{
char err = 0;
CFStringRef cf_font_path = NULL;
CTFontRef face = NULL;
CFURLRef url = NULL;
CGDataProviderRef dataProvider = NULL;
CGFontRef theCGFont = NULL;
ct_font_renderer_t *handle = (ct_font_renderer_t*)calloc(1, sizeof(*handle));
/* font_data being non-NULL is proof the path exists - it was read
* by font_renderer_create_default() - so no stat is needed here.
* CoreText loads by URL, so the bytes themselves are not used;
* release them rather than hold them for the font's lifetime. */
/* CoreText has no collection index in this path. */
(void)face_index;
if (!handle || !font_data || !font_data_len)
{
free(font_data);
err = 1;
goto error;
}
free(font_data);
/* The bytes were read by font_renderer_create_default(); this
* renderer opens nothing. The provider takes them, and releases
* them through ct_font_data_release() when CoreGraphics is
* finished - so on success they must not be freed here. */
if (!(dataProvider = CGDataProviderCreateWithData(
NULL, font_data, font_data_len, ct_font_data_release)))
{
free(font_data);
err = 1;
goto error;
}
font_data = NULL;
if (!(cf_font_path = CFStringCreateWithCString(
NULL, font_path, kCFStringEncodingUTF8)))
{
err = 1;
goto error;
}
/* Each step is checked before use: several of these APIs do not
* accept NULL arguments. */
if (!(url = CFURLCreateWithFileSystemPath(
kCFAllocatorDefault, cf_font_path, kCFURLPOSIXPathStyle, false)))
{
err = 1;
goto error;
}
if (!(dataProvider = CGDataProviderCreateWithURL(url)))
{
err = 1;
goto error;
}
if (!(theCGFont = CGFontCreateWithDataProvider(dataProvider)))
{
err = 1;
@@ -614,24 +611,12 @@ error:
handle = NULL;
}
if (cf_font_path)
{
CFRelease(cf_font_path);
cf_font_path = NULL;
}
if (face)
{
CFRelease(face);
face = NULL;
}
if (url)
{
CFRelease(url);
url = NULL;
}
if (dataProvider)
{
CFRelease(dataProvider);
@@ -647,13 +632,16 @@ error:
return handle;
}
static const char * const *font_renderer_ct_get_default_fonts(void)
static const char * const *font_renderer_ct_get_default_fonts(
const char *requested, unsigned *face_index)
{
/* A name rather than a path: CoreText looks fonts up by name, and
* there is no way to know one is present without initialising it.
* font_renderer_create_default() will not find this on disk, which
* matches the previous behaviour - init() rejected it too. */
static const char * const names[] = { "Verdana", NULL };
(void)requested;
(void)face_index;
return names;
}
+99 -131
View File
@@ -22,7 +22,6 @@
#include <ft2build.h>
#include <streams/file_stream.h>
#include <retro_miscellaneous.h>
#include <string/stdstring.h>
@@ -386,8 +385,9 @@ static bool font_renderer_create_atlas(ft_font_renderer_t *handle,
return true;
}
static void *font_renderer_ft_init(const char *font_path,
static void *font_renderer_ft_init(
uint8_t *font_data_in, size_t font_data_in_len,
unsigned face_index,
float font_size, enum font_atlas_format fmt)
{
FT_Error err;
@@ -405,147 +405,39 @@ static void *font_renderer_ft_init(const char *font_path,
goto error;
#ifdef WIIU
/* No bytes arrived, so use the OS shared font. */
/* No bytes arrived, so use the OS shared font. Borrowed from the
* OS: not ours to free, so file_data stays NULL. */
if (!font_data_in)
{
void* font_data = NULL;
uint32_t font_data_size = 0;
void* shared_data = NULL;
uint32_t shared_data_size = 0;
if (!OSGetSharedData(SHARED_FONT_DEFAULT, 0,
&font_data, &font_data_size))
&shared_data, &shared_data_size))
goto error;
if ((err = FT_New_Memory_Face(handle->lib, (const FT_Byte*)font_data,
(FT_Long)font_data_size, (FT_Long)0, &handle->face)))
if ((err = FT_New_Memory_Face(handle->lib,
(const FT_Byte*)shared_data, (FT_Long)shared_data_size,
(FT_Long)0, &handle->face)))
goto error;
}
else
#elif defined(HAVE_FONTCONFIG_SUPPORT)
/* If no bytes arrived, or a fallback font was asked for, look the
* real font up in the system instead. The path is consulted here
* because "fallback" is a property of what was requested, not of
* whether a file was found. */
if (!font_data_in || (font_path && strstr(font_path, "fallback")))
{
/* fontconfig supplies its own bytes, so release anything the
* caller read for us rather than holding it for the lifetime of
* the font. Ownership passed to us either way. */
free(font_data_in);
font_data_in = NULL;
FcValue locale_boxed;
uint8_t* font_data = NULL;
int64_t font_data_size = 0;
FcPattern *found = NULL;
FcConfig* config = NULL;
FcResult result = FcResultNoMatch;
FcChar8 *_font_path = NULL;
int face_index = 0;
FcPattern* pattern = NULL;
FcChar8* locale = NULL;
if (!fc_config)
fc_config = FcInitLoadConfigAndFonts();
config = fc_config;
if (!config)
goto error;
/* select Sans fonts */
pattern = FcNameParse((const FcChar8*)"Sans");
if (!pattern)
goto error;
/* since fontconfig uses LL-TT style, we need to normalize
* locale names */
locale = FcLangNormalize((const FcChar8*)get_user_language_iso639_1(false));
/* configure fontconfig substitute policies, this
* will increase the search scope */
FcConfigSubstitute(config, pattern, FcMatchPattern);
/* pull in system-wide defaults, so the
* font selection respects system (or user) configurations */
FcDefaultSubstitute(pattern);
/* Override locale settings, since we are not using the
* system locale; FcLangNormalize can fail, in which case the
* pattern is simply left without a language preference */
if (locale)
{
/* Box the locale data in a FcValue container */
locale_boxed.type = FcTypeString;
locale_boxed.u.s = locale;
FcPatternAdd(pattern, FC_LANG, locale_boxed, false);
}
/* Let's find the best matching font given our search criteria */
found = FcFontMatch(config, pattern, &result);
if (result != FcResultMatch)
goto fc_cleanup;
if (FcPatternGetString(found, FC_FILE, 0,
&_font_path) != FcResultMatch)
goto fc_cleanup;
if (FcPatternGetInteger(found, FC_INDEX, 0,
&face_index) != FcResultMatch)
goto fc_cleanup;
/* The one read left in this directory. fontconfig resolves a
* path here, from the locale and the system configuration, so
* font_renderer_create_default() cannot have pre-read it from a
* static candidate list. */
if (!filestream_read_file((const char*)_font_path,
(void**)&font_data,
&font_data_size))
goto fc_cleanup;
/* Initialize font renderer */
err = FT_New_Memory_Face(handle->lib, (const FT_Byte*)font_data,
(FT_Long)font_data_size, (FT_Long)face_index, &handle->face);
/* free up per-lookup fontconfig structures; the config
* itself is kept alive for the process lifetime (see the
* comment at the fc_config definition) */
FcPatternDestroy(pattern);
if (found)
FcPatternDestroy(found);
if (locale)
FcStrFree(locale);
if (err)
{
free(font_data);
goto error;
}
handle->file_data = font_data;
goto fc_done;
fc_cleanup:
FcPatternDestroy(pattern);
if (found)
FcPatternDestroy(found);
if (locale)
FcStrFree(locale);
if (font_data)
free(font_data);
goto error;
fc_done:
; /* fall through to charmap selection */
}
else
#endif
{
/* Bytes come from font_renderer_create_default(); no file I/O
* here. Take ownership before the face is built, not after, so
* the error path releases them when FT rejects the font. */
/* Bytes and face index come from
* font_renderer_create_default(); this renderer opens nothing.
* Ownership is taken before the face is built, not after, so the
* error path releases them when FT rejects the font. */
if (!font_data_in || !font_data_in_len)
goto error;
handle->file_data = font_data_in;
if ((err = FT_New_Memory_Face(handle->lib,
(const FT_Byte*)font_data_in,
(FT_Long)font_data_in_len, (FT_Long)0, &handle->face)))
(const FT_Byte*)font_data_in, (FT_Long)font_data_in_len,
(FT_Long)face_index, &handle->face)))
goto error;
}
if ((err = FT_Select_Charmap(handle->face, FT_ENCODING_UNICODE)))
goto error;
@@ -601,15 +493,91 @@ static const char * const font_paths[] = {
};
/* Highly OS/platform dependent. */
static const char * const *font_renderer_ft_get_default_fonts(void)
static const char * const *font_renderer_ft_get_default_fonts(
const char *requested, unsigned *face_index)
{
/* Since fontconfig will return parameters more than a simple path
we will process these in the init function */
#if defined(WIIU) || defined(HAVE_FONTCONFIG_SUPPORT)
/* Resolved in init() - fontconfig returns more than a path, and
* WiiU uses the shared system font. */
#if defined(WIIU)
/* The shared system font, fetched in init(); no file to open. */
static const char * const none[] = { "", NULL };
return none;
#elif defined(HAVE_FONTCONFIG_SUPPORT)
/* fontconfig resolves against the request and the user's locale,
* and answers with a face index as well as a path, which is why it
* happens here rather than being picked from the static list
* below. Only the resolving: the read belongs to
* font_renderer_create_default() like every other renderer's. */
static char resolved[PATH_MAX_LENGTH];
static const char * const fc_result[] = { resolved, NULL };
FcValue locale_boxed;
FcPattern *found = NULL;
FcConfig *config = NULL;
FcResult result = FcResultNoMatch;
FcChar8 *_font_path = NULL;
FcPattern *pattern = NULL;
FcChar8 *locale = NULL;
int index = 0;
/* An explicit font that is not one of the bundled fallbacks is
* taken as asked for; "fallback" means the caller wants the real
* system font for this language instead. */
if (requested && *requested && !strstr(requested, "fallback"))
return NULL;
if (!fc_config)
fc_config = FcInitLoadConfigAndFonts();
if (!(config = fc_config))
return NULL;
if (!(pattern = FcNameParse((const FcChar8*)"Sans")))
return NULL;
/* fontconfig uses LL-TT style, so normalize the locale name */
locale = FcLangNormalize((const FcChar8*)get_user_language_iso639_1(false));
/* Widen the search scope, then pull in system-wide defaults so the
* selection respects system or user configuration */
FcConfigSubstitute(config, pattern, FcMatchPattern);
FcDefaultSubstitute(pattern);
/* Override locale settings, since we are not using the system
* locale; FcLangNormalize can fail, in which case the pattern is
* simply left without a language preference */
if (locale)
{
locale_boxed.type = FcTypeString;
locale_boxed.u.s = locale;
FcPatternAdd(pattern, FC_LANG, locale_boxed, false);
}
found = FcFontMatch(config, pattern, &result);
resolved[0] = '\0';
if ( result == FcResultMatch
&& FcPatternGetString(found, FC_FILE, 0, &_font_path)
== FcResultMatch
&& FcPatternGetInteger(found, FC_INDEX, 0, &index)
== FcResultMatch)
{
/* Copied out: fontconfig owns the string until the pattern is
* destroyed, which happens below. */
strlcpy(resolved, (const char*)_font_path, sizeof(resolved));
if (face_index)
*face_index = (unsigned)index;
}
/* free up per-lookup fontconfig structures; the config itself is
* kept alive for the process lifetime (see the comment at the
* fc_config definition) */
FcPatternDestroy(pattern);
if (found)
FcPatternDestroy(found);
if (locale)
FcStrFree(locale);
if (!resolved[0])
return NULL;
return fc_result;
#else
/* Selection happens in font_renderer_create_default(), which is
* what keeps path lookups out of this file. */
+12 -4
View File
@@ -1634,16 +1634,18 @@ static bool font_renderer_stb_create_atlas_fmt(
return true;
}
static void *font_renderer_stb_init(const char *font_path,
static void *font_renderer_stb_init(
uint8_t *font_data, size_t font_data_len,
unsigned face_index,
float font_size, enum font_atlas_format fmt)
{
int ascent, descent, line_gap;
stb_font_renderer_t *self =
(stb_font_renderer_t*)calloc(1, sizeof(*self));
/* font_path is unused: what matters is whether bytes arrived. */
(void)font_path;
/* stb_truetype has no collection support, so the face index is
* not applicable here. */
(void)face_index;
if (!self || font_size < 1.0f)
goto error;
@@ -1722,11 +1724,14 @@ error:
return NULL;
}
static const char * const *font_renderer_stb_get_default_fonts(void)
static const char * const *font_renderer_stb_get_default_fonts(
const char *requested, unsigned *face_index)
{
#ifdef WIIU
/* The shared system font, fetched in init(); no file to open. */
static const char * const wiiu_paths[] = { "", NULL };
(void)requested;
(void)face_index;
return wiiu_paths;
#else
static const char * const paths[] = {
@@ -1772,6 +1777,9 @@ static const char * const *font_renderer_stb_get_default_fonts(void)
NULL
};
(void)requested;
(void)face_index;
/* The empty entry is the fallback: no file, built-in glyphs. */
return paths;
#endif
+23 -23
View File
@@ -202,40 +202,40 @@ int font_renderer_create_default(
const char *path = font_path;
uint8_t *data = NULL;
int64_t len = 0;
unsigned face = 0;
if (!path)
/* Ask the renderer where to look. It gets the requested path so
* it can resolve against it - freetype hands it to fontconfig -
* and answers with candidates and the face to use. Doing the
* lookup and the read here is what keeps file I/O out of the
* renderers entirely. */
{
/* Pick the first candidate that exists. Doing it here rather
* than in the renderer is what keeps file I/O out of them. */
const char * const *cand = font_backends[i]->get_default_fonts
? font_backends[i]->get_default_fonts()
? font_backends[i]->get_default_fonts(font_path, &face)
: NULL;
if (!cand)
continue;
for (; *cand; cand++)
if (cand)
{
/* An empty entry means the renderer has an internal or
* system source and wants no file. */
if (!**cand)
for (path = NULL; *cand; cand++)
{
path = *cand;
break;
}
if (path_is_valid(*cand))
{
path = *cand;
break;
/* An empty entry means the renderer has an internal or
* system source and wants no file. */
if (!**cand || path_is_valid(*cand))
{
path = *cand;
break;
}
}
}
if (!path)
/* Nothing offered and nothing asked for: this backend has
* nothing to work with. */
if (!path && !font_path)
continue;
if (!path)
path = font_path;
}
/* Read it once, here. A failed read is not fatal: the renderer
* may still have an internal source to fall back on. */
if (path && *path)
if (!filestream_read_file(path, (void**)&data, &len) || len <= 0)
{
@@ -243,13 +243,13 @@ int font_renderer_create_default(
len = 0;
}
*handle = font_backends[i]->init(path, data, (size_t)len,
font_size, fmt);
/* Ownership passes to the renderer the moment init() is called,
* not when it succeeds: a renderer can take the buffer and then
* fail - stb stores it, then rejects a malformed font and frees
* it on the way out of init(). Freeing here as well was a double
* free on any unreadable or truncated font file. */
*handle = font_backends[i]->init(data, (size_t)len, face,
font_size, fmt);
if (*handle)
{
*drv = font_backends[i];
+17 -3
View File
@@ -58,8 +58,14 @@ typedef struct font_renderer_driver
* source it has (stb's built-in glyphs, the WiiU shared font,
* fontconfig). On success the renderer takes ownership of
* font_data and frees it in free(). */
void *(*init)(const char *font_path,
uint8_t *font_data, size_t font_data_len,
/* font_data/font_data_len are the bytes of the chosen font, read by
* font_renderer_create_default(); face_index selects the face
* within a collection. Renderers do no file I/O and are not given a
* path: a NULL font_data means nothing usable was found and the
* renderer should fall back to whatever internal or system source
* it has. On success the renderer takes ownership of font_data. */
void *(*init)(uint8_t *font_data, size_t font_data_len,
unsigned face_index,
float font_size, enum font_atlas_format fmt);
struct font_atlas *(*get_atlas)(void *data);
@@ -73,7 +79,15 @@ typedef struct font_renderer_driver
* the list rather than a chosen path keeps path_is_valid() - and so
* all file I/O - out of the renderers. An empty first entry means
* "no file needed", for renderers with an internal source. */
const char * const *(*get_default_fonts)(void);
/* Candidate paths for the requested font, best first,
* NULL-terminated. requested is what the caller asked for, or NULL;
* a renderer may ignore it or resolve against it - freetype asks
* fontconfig, which needs the request and the user's locale.
* face_index is written with the face to use within whichever
* candidate is taken. An empty entry means "no file needed", for a
* renderer with an internal source. */
const char * const *(*get_default_fonts)(const char *requested,
unsigned *face_index);
const char *ident;