zed-industries/zed · error
Canvas font ID space exhausted
Error message
Canvas font ID space exhausted
What it means
This error comes from the web (WASM/Canvas) GPUI text system when registering a new Canvas-backed font. The web text system splits the FontId space: native platform fonts use normal IDs, and Canvas fallback fonts get IDs with the high CANVAS_FONT_BIT set. Before allocating a new Canvas font ID, it ensures the canvas_fonts vector length is still below the CANVAS_FONT_BIT boundary; if it is not, the font ID space is exhausted and the error is thrown instead of returning a colliding ID.
Solutions
- Verify canvas_font_ids caching is hit for repeated native_ids so duplicate fonts are not registered each call
- Check that native_id values passed in are untagged (do not already contain CANVAS_FONT_BIT), which would corrupt the split of the ID space
- Reduce the number of distinct dynamically registered fonts if running on a narrow usize target (e.g. wasm32 with modified constants)
- File/inspect a bug: on 64-bit targets reaching 2^63 canvas fonts indicates an internal accounting defect
Example fix
// before
ensure!(
state.canvas_fonts.len() < CANVAS_FONT_BIT,
"Canvas font ID space exhausted"
);
// after
// cache lookup must run first so repeated fonts reuse an ID
if let Some(font_id) = state.canvas_font_ids.get(&native_id) {
return Ok(*font_id);
}
ensure!(
state.canvas_fonts.len() < CANVAS_FONT_BIT,
"Canvas font ID space exhausted (len={}, limit={})",
state.canvas_fonts.len(),
CANVAS_FONT_BIT
); Defensive patterns
Strategy: validation
Validate before calling
fn can_register_canvas_font(canvas_fonts_len: usize) -> bool {
canvas_fonts_len < CANVAS_FONT_BIT
}
// call before requesting a font id for a non-native font Type guard
fn is_canvas_font_id(id: FontId) -> bool {
id.0 & CANVAS_FONT_BIT != 0
} Prevention
- Always resolve fonts through the text system API so canvas_font_ids caching dedupes registrations
- Never construct FontId values manually or persist them across sessions
- Avoid dynamically loading thousands of distinct font families; reuse a small set
- Report to maintainers if hit on a 64-bit target — it indicates ID-space accounting corruption
When it happens
Trigger: Calling font_id/fallback_glyph for a font that is not available natively (so it must be registered as a Canvas font) when state.canvas_fonts.len() has reached CANVAS_FONT_BIT (1 << (usize::BITS - 1)), i.e. an astronomical number of distinct Canvas-registered fonts in one process. Effectively unreachable in practice on 64-bit targets unless IDs are corrupted or the constant/bit layout changed.
Common situations: A corrupted or already-tagged native_id being re-registered repeatedly (cache lookup via canvas_font_ids missing), a 32-bit/16-bit reduction of the ID space, or a change to CANVAS_FONT_BIT that makes the canvas half of the space small. Essentially an internal invariant, not something ordinary font usage triggers.
Understand the failure class
Background: "value must be between 0 and 1" / "out of range" / "must not be negative" errors: fixing range-validation failures across open-source libraries — this error's family across 42 libraries.
Related errors
- native font ID space exhausted
- Canvas readback option could not be set
- Canvas returned a negative advance
- Canvas returned non-finite text metrics
- Canvas text bounds exceed device coordinates
AI-assisted analysis of zed-industries/zed@916fc2b8cb (2026-09-19).
Data as JSON: /api/errors/00a2b8064977732d.
Report an issue: GitHub.
Appendix: source
Thrown at crates/gpui_web/src/text_system.rs:110
.get(&native_id)
.cloned()
.context("missing source font descriptor for Canvas fallback")?
};
let monospace = match (
self.native.glyph_for_char(native_id, 'i'),
self.native.glyph_for_char(native_id, 'm'),
) {
(Some(narrow), Some(wide)) => {
self.native.advance(native_id, narrow)?.width
== self.native.advance(native_id, wide)?.width
}
_ => false,
};
let mut state = self.state.write();
if let Some(font_id) = state.canvas_font_ids.get(&native_id) {
return Ok(*font_id);
}
ensure!(
state.canvas_fonts.len() < CANVAS_FONT_BIT,
"Canvas font ID space exhausted"
);
let font_id = FontId(CANVAS_FONT_BIT | state.canvas_fonts.len());
state.canvas_fonts.push(Arc::new(CanvasFont {
native_id,
descriptor,
monospace,
}));
state.canvas_font_ids.insert(native_id, font_id);
Ok(font_id)
}
fn register_glyph(&self, font_id: FontId, text: &str, color: bool) -> Result<GlyphId> {
if let Some(glyph_id) = self.state.read().glyphs.id_for(font_id, text, color) {
return Ok(glyph_id);
}
// Another thread may register the same glyph between the read and write locks.View on GitHub (pinned to 916fc2b8cb)