Crates.io | beamterm-renderer |
lib.rs | beamterm-renderer |
version | 0.6.0 |
created_at | 2025-06-06 15:31:30.553219+00 |
updated_at | 2025-08-14 06:49:16.67812+00 |
description | High-performance WebGL2 terminal renderer for beamterm, targeting sub-millisecond render times in web browsers |
homepage | https://github.com/junkdog/beamterm |
repository | https://github.com/junkdog/beamterm |
max_upload_size | |
id | 1703193 |
size | 170,466 |
A high-performance terminal rendering system for web browsers, targeting sub-millisecond render times. beamterm is a terminal renderer, not a full terminal emulator - it handles the display layer while you provide the terminal logic.
Check out interactive examples showcasing both pure Rust applications and JavaScript/TypeScript integrations.
Single Draw Call - Renders entire terminal (e.g., 200Γ80 cells) in one instanced draw
Zero-Copy Updates - Direct memory mapping for dynamic cell updates
Unicode and Emoji Support - Complete Unicode support with grapheme clustering
ASCII Fast Path - Direct bit operations for ASCII characters (no lookups)
Selection Support - Mouse-driven text selection with clipboard integration (Block/Linear modes)
Optional JS/TS Bindings - Provides a JavaScript/TypeScript API for easy integration
beamterm targets sub-millisecond render times across a wide range of hardware:
Metric | Target (Low-end) | Achieved (2019 hardware) |
---|---|---|
Render Timeβ | <1ms @ 16k cells | <1ms @ 45k cells |
Draw Calls | 1 per frame | 1 per frame |
Memory Usage | ~2.8MB | ~2.8MB |
Update Bandwidth (full refresh) | ~8 MB/s @ 60 FPS | ~22 MB/s @ 60 FPS |
The screenshot shows Ratzilla's "canvas waves" demo running in a 426Γ106 terminal (45,156 cells), maintaining sub-millisecond render times on 2019-era hardware (i9-9900K / RTX 2070).
β Includes Ratatui buffer translation, GPU buffer uploads, and draw call execution.
The renderer consists of three crates:
beamterm-atlas
- Generates GPU-optimized font atlases from TTF/OTF files. Automatically
calculates cell dimensions, supports font styles (normal/bold/italic), and outputs packed
texture data.
beamterm-data
- Provides shared data structures and efficient binary serialization. Features
versioned format with header validation and cross-platform encoding.
beamterm-renderer
- The WebGL2 rendering engine. Implements instanced rendering with optimized
buffer management and state tracking for consistent sub-millisecond performance.
The architecture leverages GPU instancing to reuse a single quad geometry across all terminal cells, with per-instance data providing position, character, and color information. All rendering state is encapsulated in a Vertex Array Object (VAO), enabling single-draw-call rendering with minimal CPU overhead. The 2D texture array maximizes cache efficiency by packing related glyphs into horizontal strips within each layer.
The renderer employs several optimization strategies:
texStorage3D
for driver optimization hintsThese strategies combined enable the renderer to achieve consistent sub-millisecond frame times even for large terminals (200Γ80 cells = 16,000 instances).
For a 200Γ80 terminal with 2560 glyphs:
Component | Size | Type |
---|---|---|
Font Atlas | 2.73 MB | Texture memory |
Static Buffers | ~63 KB | Vertex + Instance positions |
Dynamic Buffer | ~125 KB | Cell content |
Overhead | ~10 KB | VAO, shaders, uniforms |
Total | ~2.9 MB | GPU memory |
The renderer provides a high-level Terminal
struct that encapsulates the complete rendering system:
use beamterm_renderer::{Terminal, CellData, FontStyle, GlyphEffect};
// Create terminal with default font atlas
let mut terminal = Terminal::builder("#canvas").build()?;
// Update cells and render
let cells: Vec<CellData> = ...;
terminal.update_cells(cells.into_iter())?;
terminal.render_frame()?;
// Handle resize
terminal.resize(new_width, new_height)?;
The renderer supports mouse-driven text selection with automatic clipboard integration:
// Enable default selection handler
let terminal = Terminal::builder("#canvas")
.default_mouse_input_handler(SelectionMode::Linear, true)
.build()?;
// Or implement custom mouse handling
let terminal = Terminal::builder("#canvas")
.mouse_input_handler(|event, grid| {
// Custom handler logic
})
.build()?;
Main rendering component managing the terminal display. Handles shader programs, cell data, GPU buffers, and rendering state.
Manages the 2D texture array containing all font glyphs. Provides character-to-glyph ID mapping with fast ASCII optimization. Supports loading default or custom font atlases.
Each terminal cell requires:
&str
)FontStyle
enum (Normal, Bold, Italic, BoldItalic)GlyphEffect
enum (None, Underline, Strikethrough)0xAARRGGBB
)The font atlas uses a WebGL 2D texture array where each layer contains a 16Γ1 grid of glyphs (16 per layer). This provides optimal memory utilization and cache efficiency while maintaining O(1) coordinate lookups through simple bit operations. The system supports 512 base glyphs Γ 4 styles + emoji.
The font atlas uses a 2D texture array organized as multiple layers, each containing a 16Γ1 grid of glyphs:
Dimension | Size | Formula | Description |
---|---|---|---|
Width | Cell Γ 16 | 12 Γ 16 = 192px | 16 glyphs horizontally |
Height | Cell Γ 1 | 18 Γ 1 = 18px | 1 glyph vertically |
Layers | βGlyphs/16β | max(glyph.id) / 16 | One layer per 16 glyphs |
The layers are densely packed, but there might be gaps beween font variants and before the first emoji layer, unless all 512 glyphs are used.
The glyph ID is a 16-bit value that efficiently packs both the base glyph identifier and style information (such as weight, style flags, etc.) into a single value. This packed representation is passed directly to the GPU.
Bit(s) | Flag Name | Hex Mask | Binary Mask | Description |
---|---|---|---|---|
0-8 | GLYPH_ID | 0x01FF |
0000_0001_1111_1111 |
Base glyph identifier |
9 | BOLD | 0x0200 |
0000_0010_0000_0000 |
Bold font style |
10 | ITALIC | 0x0400 |
0000_0100_0000_0000 |
Italic font style |
11 | EMOJI | 0x0800 |
0000_1000_0000_0000 |
Emoji character flag |
12 | UNDERLINE | 0x1000 |
0001_0000_0000_0000 |
Underline effect |
13 | STRIKETHROUGH | 0x2000 |
0010_0000_0000_0000 |
Strikethrough effect |
14-15 | RESERVED | 0xC000 |
1100_0000_0000_0000 |
Reserved for future use |
Character | Style | Glyph ID | Calculation | Result |
---|---|---|---|---|
' ' (32) | Normal | 0x0020 | 32Γ·16=2, 32%16=0 | Layer 2, Position 0 |
'A' (65) | Normal | 0x0041 | 65Γ·16=4, 65%16=1 | Layer 4, Position 1 |
'A' (65) | Bold+Italic | 0x0641 | 1601Γ·16=100, 1601%16=1 | Layer 100, Position 1 |
'β¬' | Normal | 0x0080 | Mapped to ID 128 | Layer 8, Position 0 |
'π' | Emoji | 0x0881 | With emoji bit set | Layer 136, Position 1 |
The consistent modular arithmetic ensures that style variants maintain the same horizontal position within their respective layers, improving texture cache coherence.
ASCII characters (0-127) bypass the HashMap lookup entirely through direct bit manipulation.
For ASCII input, the glyph ID is computed as char_code | style_bits
, providing zero-overhead
character mapping. Non-ASCII characters use a HashMap for flexible Unicode support. This approach
optimizes for the common case while maintaining full Unicode capability.
The renderer uses six buffers managed through a Vertex Array Object (VAO) to achieve single-draw-call rendering. Each buffer serves a specific purpose in the instanced rendering pipeline, with careful attention to memory alignment and update patterns.
Buffer | Type | Size | Usage | Update Freq | Purpose |
---|---|---|---|---|---|
Vertex | VBO | 64 bytes | STATIC_DRAW |
Never | Quad geometry |
Index | IBO | 6 bytes | STATIC_DRAW |
Never | Triangle indices |
Instance Position | VBO | 4 bytes/cell | STATIC_DRAW |
On resize | Grid coordinates |
Instance Cell | VBO | 8 bytes/cell | DYNAMIC_DRAW |
Per frame | Glyph ID + colors |
Vertex UBO | UBO | 80 bytes | STATIC_DRAW |
On resize | Projection matrix |
Fragment UBO | UBO | 32 bytes | STATIC_DRAW |
On resize | Cell metadata |
All vertex buffers are encapsulated within a single Vertex Array Object (VAO), enabling state-free rendering with a single draw call.
The Instance Position and Instance Cell buffers are recreated when the terminal size changes,
Location | Attribute | Type | Components | Divisor | Source Buffer |
---|---|---|---|---|---|
0 | Position | vec2 |
x, y | 0 | Vertex |
1 | TexCoord | vec2 |
u, v | 0 | Vertex |
2 | InstancePos | uvec2 |
grid_x, grid_y | 1 | Instance Position |
3 | PackedData | uvec2 |
glyph_id, colors | 1 | Instance Cell |
The 8-byte CellDynamic
structure is tightly packed to minimize bandwidth:
Byte Layout: [0][1][2][3][4][5][6][7]
ββ¬ββ ββββ¬βββ ββββ¬βββ
Glyph ID FG RGB BG RGB
(16-bit) (24-bit) (24-bit)
This layout enables the GPU to fetch all cell data in a single 64-bit read, with the glyph ID encoding both the texture coordinate and style information as described in the Glyph ID Bit Layout section.
For a typical 12Γ18 pixel font with 2560 glyphs:
Component | Size | Details |
---|---|---|
2D Texture Array | ~2.73 MB | 16(12+2)Γ(18+2)Γ128 RGBA (16 glyphs/layer) |
Vertex Buffers | ~200 KB | For 200Γ80 terminal |
Cache Efficiency | Good | Sequential glyphs in same layer |
Memory Access | Coalesced | 64-bit aligned instance data |
The 16Γ1 grid layout ensures that adjacent terminal cells often access the same texture layer, maximizing GPU cache hits. ASCII characters (the most common) are packed into the first 8 layers, providing optimal memory locality for typical terminal content.
The renderer uses a branchless shader pipeline optimized for instanced rendering:
cell.vert
)Transforms cell geometry from grid space to screen space using per-instance attributes. The shader:
cell.frag
)Performs the core rendering logic with efficient 2D array texture lookups:
Extracts 16-bit glyph ID from packed instance data
Masks with 0x0FFF to exclude effect flags before computing layer index (glyph_id β layer/position)
Computes layer index and horizontal position using bit operations
Samples from 2D texture array using direct layer indexing
Detects emoji glyphs via bit 11 for special color handling
Applies underline/strikethrough effects via bits 12-13
Blends foreground/background colors with glyph alpha for anti-aliasing
The renderer requires WebGL2 for:
TEXTURE_2D_ARRAY
, texStorage3D
, texSubImage3D
)drawElementsInstanced
, vertexAttribDivisor
)UNIFORM_BUFFER
, vertexAttribIPointer
)createVertexArray
)# Install Rust toolchain
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup target add wasm32-unknown-unknown
# Install tools
cargo install wasm-pack trunk
# Development server
trunk serve
# Production build
trunk build --release
glyph_id & 0x0F
Maximum 512 base glyphs (9-bit addressing)
Fixed 4 style variants per glyph
Monospace fonts only
Single font family and font size per atlas