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

  1. Verify canvas_font_ids caching is hit for repeated native_ids so duplicate fonts are not registered each call
  2. 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
  3. Reduce the number of distinct dynamically registered fonts if running on a narrow usize target (e.g. wasm32 with modified constants)
  4. 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

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


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)