zed-industries/zed · error

native font ID space exhausted

Error message

native font ID space exhausted

What it means

When resolving a Font descriptor, the web text system delegates to the native font provider and then tags the resulting FontId with CANVAS_FONT_BIT only for Canvas fallback fonts. To keep the two ID spaces disjoint, it asserts the native provider never returns an ID with the CANVAS_FONT_BIT already set. If it does, the native font ID space is considered exhausted/corrupted and this error is thrown.

Solutions

  1. Confirm the FontId passed to font_id originated from the native provider, not from a Canvas fallback font (which has CANVAS_FONT_BIT set)
  2. Check font caching so Canvas-tagged IDs are never routed back through native.font_id()
  3. Inspect the native font provider for ID reuse/wraparound bugs; log the offending font_id.0 value to confirm bit corruption
  4. If seen on a 32-bit or custom target, review the CANVAS_FONT_BIT split which leaves only half the space for native IDs

Example fix

// before
let font_id = self.native.font_id(font)?;
ensure!(
    font_id.0 & CANVAS_FONT_BIT == 0,
    "native font ID space exhausted"
);
// after
let font_id = self.native.font_id(font)?;
ensure!(
    font_id.0 & CANVAS_FONT_BIT == 0,
    "native font ID space exhausted (id={:x})",
    font_id.0
); // offending id indicates a stale Canvas-tagged FontId was passed in
Defensive patterns

Strategy: type-guard

Validate before calling

fn safe_font_id(id: FontId) -> Option<FontId> {
    (id.0 & CANVAS_FONT_BIT == 0).then_some(id)
}
// only pass safe_font_id results back into native font resolution

Type guard

fn is_native_font_id(id: FontId) -> bool {
    id.0 & CANVAS_FONT_BIT == 0
}

Try / catch

match text_system.font_id(&font) {
    Ok(id) => id,
    Err(err) if err.to_string().contains("native font ID space") => {
        // the FontId routed here was Canvas-tagged or corrupted; re-resolve from the descriptor
        re_resolve_font(&font)?
    }
    Err(err) => return Err(err),
}

Prevention

When it happens

Trigger: Calling font_id(&Font) when self.native.font_id(font) returns a FontId whose high bit (CANVAS_FONT_BIT, 1 << (usize::BITS - 1)) is set — i.e. the native provider has handed out an ID colliding with the Canvas half of the space, or a stale/corrupted FontId was fed back through the resolution path.

Common situations: A FontId from a Canvas fallback font being passed back into font_id() as if it were a native font (mixing up ID origins after caching), or a native backend bug/wraparound after an enormous number of font loads. Ordinary font-family resolution will not hit this.

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


AI-assisted analysis of zed-industries/zed@916fc2b8cb (2026-09-19). Data as JSON: /api/errors/8504b5d667ce1987. Report an issue: GitHub.

Appendix: source

Thrown at crates/gpui_web/src/text_system.rs:267

impl PlatformTextSystem for WebTextSystem {
    fn set_missing_glyph_sink(&self, sink: Option<Arc<dyn MissingGlyphSink>>) {
        // Cosmic must not report missing glyphs before Canvas has a chance to replace them.
        self.state.write().missing_glyph_sink = sink;
    }

    fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
        self.native.add_fonts(fonts)?;
        self.state.write().measurements.clear();
        Ok(())
    }

    fn all_font_names(&self) -> Vec<String> {
        self.native.all_font_names()
    }

    fn font_id(&self, font: &Font) -> Result<FontId> {
        let font_id = self.native.font_id(font)?;
        ensure!(
            font_id.0 & CANVAS_FONT_BIT == 0,
            "native font ID space exhausted"
        );
        let mut descriptor = font.clone();
        (descriptor.weight, descriptor.style) = self.native.font_weight_and_style(font_id)?;
        self.state
            .write()
            .descriptors
            .entry(font_id)
            .or_insert(descriptor);
        Ok(font_id)
    }

    fn prewarm_fonts(&self, font_ids: &[FontId]) {
        if font_ids
            .iter()
            .all(|font_id| font_id.0 & CANVAS_FONT_BIT == 0)
        {

View on GitHub (pinned to 916fc2b8cb)