WebGPU notes / 07
WebGPU Text Overlay in Rust and wgpu
Render an FPS counter and Persian text over a rotating cube with Rust and wgpu. This example uses a bundled font, Unicode shaping, and a glyph atlas, then composites the text in a separate WebGPU render pass.
- Text items
- 2
- Render passes
- 2
- Font
- Vazirmatn
A 3D scene with a 2D text overlay
The mipmap generation example used an overlay to report its GPU and frame rate. Here I focus on how that text is created and drawn, with two text items over a simple animated scene.
The cube uses eight vertices and thirty-six indices, forming twelve triangles. Its WGSL shader transforms positions with a model-view-projection matrix, interpolates vertex colors, and applies a small RGB tint. The cube rotates around Y with a fixed 28° tilt around X; the camera stays in place.
The text remains in screen coordinates while the cube moves behind it. GPU information and FPS appear at the upper left, and a Persian sample appears near the lower right. These labels are rendered into the canvas through sib::render. The demo has no mouse or keyboard controls.
Load the font and build a glyph atlas
The Rust example embeds Vazirmatn-Regular.ttf with include_bytes!. During initialization, it passes those bytes to the text helper:
self.overlay = Some(text::TextOverlay::with_font_data(
context,
[FONT_BYTES.to_vec()],
)?);
The font travels with the native executable or WebAssembly module, so the demo does not require a separate font download or an installed copy of Vazirmatn. Both labels select it with TextFamily::Name("Vazirmatn"). The repository includes the font’s license file.
The text helper used by this checkout wraps glyphon. Text shaping and layout determine which glyphs to draw and where to place them. Glyph images are rasterized on the CPU and cached in GPU atlas textures; the renderer then draws textured glyph quads.
An atlas packs many glyph images into shared texture storage. Repeated letters and digits can reuse cached images instead of uploading a new texture for each character. This example uses glyphon’s atlas and rendering pipeline rather than implementing a font rasterizer in its scene shader.
Style and position the text
Each call to overlay.add_text supplies a string, a TextStyle, and a TextPlacement. Style controls typography and color; placement controls the rectangle in which the text is laid out and clipped.
Scroll sideways to see all table columns.
| Text item | Font size | Line height | Alignment |
|---|---|---|---|
| GPU information and FPS | 22 px | 30 px | Left |
| Persian sample | 28 px | 38 px | Right |
The stats item begins 26 pixels from the left and 24 pixels from the top. Its color is [246, 249, 255, 255]. The Persian item uses [148, 213, 255, 255], giving it a light blue color, with its position recalculated from the surface width and height.
These measurements use render target pixels at the default placement scale of 1. The helper applies word wrapping and clips text to its placement bounds. The stats rectangle has a fixed height of 72 pixels, so a long GPU name can wrap and clip the FPS line on a narrow surface. When adapting the demo, budget space for the text after wrapping, not just its explicit newline count.
Shape Unicode and Persian RTL text
The lower label includes the Latin prefix RTL: and these two Persian phrases:
سلام ایران
متن راست به چپ
They mean “Hello Iran” and “right to left text.” They are kept in their normal Unicode order in the Rust string; the application does not reverse the characters manually.
The default TextStyle uses Shaping::Advanced. Through glyphon, cosmic-text handles shaping and bidirectional layout, including contextual glyph forms and mixed text directions. The helper creates a text buffer with the requested font and metrics, sets its text, and calls shape_until_scroll before preparing it for rendering.
Align::Right places each line against the right edge of its box after layout. Alignment alone cannot join Persian letters or resolve the order of mixed Latin and Persian text. Those are separate responsibilities of shaping and bidirectional layout.
Vazirmatn supplies the glyphs needed for this sample. If you replace the text with another script, load a font that covers it; one bundled font is not a guarantee of support for every language or emoji.
Prepare, render, and update the overlay
Each frame begins with overlay.prepare(context). The helper updates its viewport resolution, prepares the text areas, and makes the glyph and instance data ready for the GPU.
The first render pass clears the color and depth buffers, then draws the cube with depth testing. The second pass loads the existing color and renders the text without a depth attachment. With initialization checks omitted, that final pass looks like this:
{
let mut pass = render_pass::begin_color_load(
encoder, Some("text overlay render pass"), view,
);
overlay.render(&mut pass)?;
}
Loading preserves the cube’s pixels; clearing this pass would erase them. The text pipeline uses alpha blending to composite the glyphs, and the lack of depth testing lets the labels appear above scene geometry.
After recording the text draw, overlay.trim() resets atlas usage tracking so unused glyphs can be evicted later. It does not clear the text items or erase every cached glyph image each frame.
The rotation and matrix uniform update every frame, but the FPS label updates only when FrameStats finishes its default 500 ms sample window. add_text returns a TextItemId, which is saved for that update:
overlay.update_text(id, &value, style, placement)
This replaces and reshapes that item’s text buffer while keeping the overlay’s font system, atlas, and renderer. On resize, the example recreates the depth texture and rebuilds both text items with new placements. The displayed FPS is an application frame-rate sample, not an isolated measurement of text rendering cost.
Run and modify the example
From a local checkout with Rust installed, run the native version:
cargo run --example textoverlay
For WebAssembly, install the wasm32-unknown-unknown target and the wasm-bindgen CLI version matching Cargo.lock, then build and serve:
scripts/build-wasm.sh --release textoverlay
cargo run --bin serve
Open http://127.0.0.1:8080/textoverlay/. Live rendering needs WebGPU support in the browser; this article, the Persian sample, and the screenshot remain readable without it.
Try these changes in the source:
- Edit the string in
rebuild_overlayto test another mixture of Latin and Persian text. - Change font size, line height, color, and alignment, then adjust the placement bounds to fit the result.
- Add another item with
add_text, save its returned ID, and useupdate_textwhen its value changes.
Canvas labels are not selectable HTML text. If an application relies on them for essential status information, expose an accessible text equivalent outside the canvas as well.