A note on how this report was made: I ran into this bug myself while configuring rio. The analysis, probe code and evidence below were put together with the help of AI-assisted tools, so please treat it as a best-effort diagnosis, offered for reference — every log line and probe output comes from my machine and is reproducible with the snippets provided, but do verify the conclusions against the source.
Environment
|
|
| rio |
0.5.26 (distro package), Wayland/KDE, Arch Linux (CachyOS) |
| fontconfig |
2.18.3 |
Locked deps (rio v0.5.26 Cargo.lock) |
swash 0.2.10, font-kit 0.14.3, ttf-parser 0.25.1 — the exact same versions are used in every probe below |
| Font triggering the bug |
Xiaomi MiSans VF (MiSansVF.ttf, official zip): single-face variable TTF, fvar with 10 named instances (wght 150–700) |
Analysis
With symbol-map entries pointing at a family backed by a VF that has named instances, rio pops a GUI warning "font family not found" on startup (nothing on stdout/stderr). The chain, link by link:
- fontconfig lists each VF named instance as a separate face whose
FC_INDEX follows the FreeType convention named_instance << 16 (e.g. 393216 = 6<<16). Intentional, spec-abiding, meant for FreeType-compatible consumers.
- font-kit (
loader.rs via SystemSource::select_family_by_name) passes the encoded index through unchanged, and reports the OS/2 weight (400) for every face of the VF — the candidates are indistinguishable. find_best_match then takes whichever face fontconfig happened to list first; ordering is not guaranteed and the index=0 default face gets no priority. For MiSans VF the default face was listed 6th; the winner carried index=393216.
FontData::from_data (sugarloaf/src/font/mod.rs ~L1362) calls swash::FontRef::from_index(data, 393216) → None (non-TTC file + nonzero index). The runtime error string Failed to load font from path comes exactly from the ok_or_else wrapping this call.
- The
Err becomes FindResult::NotFound, lands in fonts_not_found, and surfaces as the misleading "font not found" dialog.
Why it looks random: the outcome hinges on fontconfig's face ordering plus a weight tie, both outside rio's control. My Google Sans Code (also a VF, 6 named instances) loads fine only because its default face happens to be listed first on this machine. Reordering/caching differences flip the result — matching intermittent reports like #1818 (Google Sans Code VF failing elsewhere) and issues closed as "works with static fonts".
Reproduction
- Install any single-face variable font with named instances (e.g. MiSans VF).
config.toml:
[fonts]
symbol-map = [
{ start = "2E80", end = "2FFF", font-family = "MiSans VF" },
{ start = "4E00", end = "9FFF", font-family = "MiSans VF" },
]
RIO_LOG_LEVEL=debug rio → warning dialog; log shows the evidence chain below.
- Point
symbol-map at a static-weight family instead → loads fine.
Verification code (probes, rio-identical dependency versions)
Probe 1 — fontconfig returns encoded indexes:
$ fc-match "MiSans VF" --format "%{index}\n" # → 0
$ fc-match "MiSans VF:style=Semibold" --format "%{index}\n" # → 524288 (8<<16)
$ fc-match "MiSans VF:weight=200" --format "%{index}\n" # → 589824 (9<<16)
$ fc-match "Google Sans Code:weight=400" --format "%{index}\n" # → 393216 (6<<16)
Probe 2 — font-kit candidates (order / index / weight):
let family = SystemSource::new().select_family_by_name(name)?;
for h in family.fonts() {
if let Handle::Path { path, font_index } = h {
let f = h.load()?;
println!("index={font_index} weight={} {}", f.properties().weight.0, path.display());
}
}
"Google Sans Code": [0] index=0 w=400 ← default face listed first → survives
[1..5] index=65536..393216, all w=400 (6 named instances)
"MiSans VF": [0] index=393216 w=400 ← an instance listed first → breaks
[5] index=0 w=400 ← default face buried at position 6
(11 faces, all w=400)
"Noto Sans SC": 7 faces, most with encoded indexes → equally at risk
Probe 3 — swash 0.2.10 rejects every nonzero encoded index:
let data = std::fs::read("MiSansVF.ttf")?;
for idx in [0u32, 65536, 262144, 393216, 524288, 589824] {
println!("{idx}: {}", swash::FontRef::from_index(&data, idx as usize).is_some());
}
// 0: true 65536: false 262144: false 393216: false 524288: false 589824: false
Probe 4 — ttf-parser 0.25.1 behaves identically (Err(FaceIndexOutOfBounds) for any nonzero index, Ok only for 0) — i.e. this is the Rust parsing ecosystem's norm, not a swash-only quirk: FreeType's named-instance index convention is simply not implemented there, so the integration layer must normalize it.
Evidence (rio's own log, real run on my machine)
Failing family — candidates all tied at weight=400, match lands on encoded index 393216, load rejected:
INFO sugarloaf::font: Font search: 'Query { families: [Name("MiSans VF")], weight: Weight(400), stretch: Normal, style: Normal }'
DEBUG sugarloaf::font::loader: found candidate: weight=400, stretch=Stretch(1.0), style= Normal (×10, identical)
DEBUG sugarloaf::font::loader: Best match selected at index 0
DEBUG sugarloaf::font::loader: face_source: Path source - /usr/share/fonts/misans-vf/MiSansVF.ttf, index 393216
INFO sugarloaf::font: Failed to load font 'Query { families: [Name("MiSans VF")], ... }', Failed to load font from path: "/usr/share/fonts/misans-vf/MiSansVF.ttf"
Same machine, working family (Google Sans Code): identical query shape, winner has index 0 → Font 'Google Sans Code' found in ....
Suggested fixes (any one, all local to sugarloaf)
- Normalize before parsing:
face_index & 0xFFFF — or retry with 0 on load failure (renders the default instance, same face fontconfig's default entry refers to);
- In
find_best_match, break weight ties in favor of the index == 0 default face;
- At minimum, report "failed to load face (index N)" instead of "font family not found" — the current wording sends users hunting for missing fonts.
Workaround: point symbol-map/families at a static-weight family (verified working here); the VF family remains unusable in rio.
Related issues
A note on how this report was made: I ran into this bug myself while configuring rio. The analysis, probe code and evidence below were put together with the help of AI-assisted tools, so please treat it as a best-effort diagnosis, offered for reference — every log line and probe output comes from my machine and is reproducible with the snippets provided, but do verify the conclusions against the source.
Environment
Cargo.lock)MiSansVF.ttf, official zip): single-face variable TTF,fvarwith 10 named instances (wght 150–700)Analysis
With
symbol-mapentries pointing at a family backed by a VF that has named instances, rio pops a GUI warning "font family not found" on startup (nothing on stdout/stderr). The chain, link by link:FC_INDEXfollows the FreeType conventionnamed_instance << 16(e.g.393216 = 6<<16). Intentional, spec-abiding, meant for FreeType-compatible consumers.loader.rsviaSystemSource::select_family_by_name) passes the encoded index through unchanged, and reports the OS/2 weight (400) for every face of the VF — the candidates are indistinguishable.find_best_matchthen takes whichever face fontconfig happened to list first; ordering is not guaranteed and theindex=0default face gets no priority. For MiSans VF the default face was listed 6th; the winner carriedindex=393216.FontData::from_data(sugarloaf/src/font/mod.rs~L1362) callsswash::FontRef::from_index(data, 393216)→None(non-TTC file + nonzero index). The runtime error stringFailed to load font from pathcomes exactly from theok_or_elsewrapping this call.ErrbecomesFindResult::NotFound, lands infonts_not_found, and surfaces as the misleading "font not found" dialog.Why it looks random: the outcome hinges on fontconfig's face ordering plus a weight tie, both outside rio's control. My
Google Sans Code(also a VF, 6 named instances) loads fine only because its default face happens to be listed first on this machine. Reordering/caching differences flip the result — matching intermittent reports like #1818 (Google Sans Code VF failing elsewhere) and issues closed as "works with static fonts".Reproduction
config.toml:RIO_LOG_LEVEL=debug rio→ warning dialog; log shows the evidence chain below.symbol-mapat a static-weight family instead → loads fine.Verification code (probes, rio-identical dependency versions)
Probe 1 — fontconfig returns encoded indexes:
Probe 2 — font-kit candidates (order / index / weight):
Probe 3 — swash 0.2.10 rejects every nonzero encoded index:
Probe 4 — ttf-parser 0.25.1 behaves identically (
Err(FaceIndexOutOfBounds)for any nonzero index,Okonly for 0) — i.e. this is the Rust parsing ecosystem's norm, not a swash-only quirk: FreeType's named-instance index convention is simply not implemented there, so the integration layer must normalize it.Evidence (rio's own log, real run on my machine)
Failing family — candidates all tied at
weight=400, match lands on encoded index393216, load rejected:Same machine, working family (Google Sans Code): identical query shape, winner has
index 0→Font 'Google Sans Code' found in ....Suggested fixes (any one, all local to sugarloaf)
face_index & 0xFFFF— or retry with0on load failure (renders the default instance, same face fontconfig's default entry refers to);find_best_match, break weight ties in favor of theindex == 0default face;Workaround: point
symbol-map/families at a static-weight family (verified working here); the VF family remains unusable in rio.Related issues