26 APR 2026

rahulmnavneeth

font

homedocs

Location

include/mop/core/font.h     — Public API: MopFont, metrics, measure
src/core/font_internal.h    — .mfa binary layout (shared with bake tool)
src/core/font.c             — mmap loader, glyph + kerning lookup
tools/mop_font_bake.c       — TTF → .mfa baker (vendor stb_truetype)
third_party/stb/stb_truetype.h

Why pre-bake

MOP fonts are pre-baked at asset-build time into the .mfa binary format (MOP Font Atlas). At runtime the font is just an atlas texture + a metrics table — there is no runtime TTF parsing or glyph rasterization, so a font that loads will never produce a blurry, missing, or partially-rendered glyph at draw time. This is the "battle-built" promise: clarity is a build-time property.

Public API

typedef struct MopFont MopFont;   /* opaque handle */

typedef enum MopFontAtlasType {
    MOP_FONT_ATLAS_SDF    = 0,    /* single-channel SDF       (default) */
    MOP_FONT_ATLAS_MSDF   = 1,    /* three-channel MSDF       (later)   */
    MOP_FONT_ATLAS_BITMAP = 2,    /* raw alpha (no scaling)   (later)   */
} MopFontAtlasType;

typedef struct MopFontMetrics {
    float    ascent, descent, line_gap, line_height;  /* em units    */
    float    em_size;       /* source em size used at bake (info)    */
    float    px_range;      /* SDF range in atlas pixels (shader uniform) */
    uint32_t glyph_count;
    uint32_t kerning_count;
} MopFontMetrics;

MopFont *mop_font_load(const char *path);
MopFont *mop_font_load_memory(const void *data, size_t size);
void     mop_font_free(MopFont *font);

MopFontMetrics    mop_font_metrics(const MopFont *font);
MopFontAtlasType  mop_font_atlas_type(const MopFont *font);

/* String width in pixels at px_size; includes kerning. */
float mop_text_measure(const MopFont *font, const char *utf8, float px_size);

/* The embedded HUD font (JetBrains Mono Regular).
 * NULL only if the build was produced without `make fonts`. */
const MopFont *mop_font_hud(void);

Default font

JetBrains Mono is MOP's de facto default font. Every mop_text_draw_* call that passes font = NULL resolves to mop_font_hud(), which returns a singleton pointing at the embedded SDF atlas. No host setup, no disk I/O.

.mfa binary layout

Single file, mmap-friendly. One file = one weight.

[ Header — 128 bytes, _Static_assert-locked ]
  magic[4]          "MFA\x01"
  version           u32
  atlas_type        u32   // SDF / MSDF / BITMAP
  atlas_channels    u32   // 1 (SDF, BITMAP) or 3 (MSDF)
  atlas_width/height u32 × 2
  px_range          f32   // SDF range in atlas pixels
  em_size           f32
  ascent / descent / line_gap   f32 × 3
  glyph_count       u32
  kerning_count     u32
  glyph_table_offset    u64
  kerning_table_offset  u64
  atlas_offset      u64
  reserved[48]

[ Glyph table — 32 B × glyph_count, sorted by codepoint ]
  codepoint                u32
  atlas_uv_min/max         u16 × 4   (atlas pixels)
  plane_min/max            f32 × 4   (em units, baseline-relative, Y-up)
  advance                  f32       (em units)

[ Kerning table — 8 B × kerning_count, sorted by (left, right) glyph index ]
  left, right              u16 × 2   (glyph indices, NOT codepoints)
  offset                   f32       (em units)

[ Atlas pixels — raw R8 (SDF/BITMAP) or RGB8 (MSDF) ]

Codepoint lookup is binary search on the sorted glyph table. Kerning lookup is binary search on the (left, right) pair.

Baking — tools/mop_font_bake

Pure C; vendor-only deps (stb_truetype).

mop_font_bake INPUT.ttf --out OUT.mfa
              [--glyphs ascii|ascii+latin1]
              [--size 64]      # source glyph height in pixels
              [--padding 4]    # SDF range in pixels
              [--name SYMBOL]  # also emit OUT.mfa.c

--name SYMBOL triggers an extra emission: a generated C source file (OUT.mfa.c) defining

const uint8_t mop_embedded_<SYMBOL>[]    = { … };
const size_t  mop_embedded_<SYMBOL>_size = N;

This is how the HUD font lands inside libmop — the generated .c gets compiled into the static archive with -DMOP_HAS_EMBEDDED_HUD_FONT=1, and mop_font_hud() calls mop_font_load_memory(mop_embedded_hud_font, …) once and caches the result.

Build flow — make fonts

# 1. Drop the OFL TTF (fetch from JetBrains/JetBrainsMono on GitHub).
cp JetBrainsMono-Regular.ttf assets/fonts/

# 2. Bake.  Produces:
#      build/fonts/jbm_regular.mfa     — runtime atlas
#      build/fonts/jbm_regular.mfa.c   — embed-ready source
make fonts

# 3. Re-link libmop with the embedded font.
make MOP_ENABLE_VULKAN=1

The Makefile detects the generated .mfa.c via wildcard and auto-adds it to CORE_OBJS with -DMOP_HAS_EMBEDDED_HUD_FONT=1. After step 3, mop_font_hud() returns non-NULL with no further host work.

If the TTF isn't dropped in, make fonts errors out (no source rule); make still produces a libmop, and mop_font_hud() returns NULL — host code falls back to mop_font_load("path/to/file.mfa").

Loading at runtime

/* Disk: mmap'd, zero-copy on POSIX. */
MopFont *f = mop_font_load("assets/fonts/jbm_regular.mfa");
if (!f) abort();

/* Memory blob: caller-owned, must outlive the MopFont*. */
extern const uint8_t my_blob[];
extern const size_t  my_blob_size;
MopFont *g = mop_font_load_memory(my_blob, my_blob_size);

/* Same atlas the engine uses — fastest path, no I/O. */
const MopFont *hud = mop_font_hud();   /* do NOT free this */

mop_font_free(f);

Measuring text

MopFontMetrics m  = mop_font_metrics(hud);
float line_h_px   = m.line_height * 12.0f;       /* 12-px line     */
float row_w_px    = mop_text_measure(hud, "frame 16.4ms", 12.0f);

Kerning is included. Newlines reset horizontal advance and skip a line in line_height units. Missing glyphs consume 0.5 em so the layout doesn't collapse.

Cap-height vs line-height centering

Uppercase glyphs occupy roughly the upper 0.72 em of the cell. Centering a single-line label using line_height * 0.5 pushes the glyph visually low. Use cap-height centering instead:

const float CAP_EM = 0.72f;
float vcenter_em  = m.ascent - CAP_EM * 0.5f;
float pixel_y     = anchor_y - vcenter_em * px_size;

The corner navigator and transform gizmo letters use this formula.

Defensive contracts

See Also