Ghostty Terminal Engine: GPU-Accelerated Rendering in Native Apps
How FoxTerm integrates Ghostty terminal engine for 120fps GPU-accelerated terminal rendering on macOS and iOS. Metal shaders, PTY management, and ANSI parsing.
Most terminal emulators are stuck in the past: CPU-rendered text, sluggish scrolling, and frame drops during heavy output. FoxTerm takes a different approach by integrating Ghostty, a modern terminal engine that leverages GPU acceleration for buttery-smooth 120fps rendering.
Why Ghostty?
When we evaluated terminal engines, we tested everything:
xterm.js (Web-based):
- ❌ Canvas rendering limited to 30-60fps
- ❌ JavaScript overhead for escape sequence parsing
- ❌ No native macOS/iOS integration
- ❌ Memory bloat from DOM manipulation
Terminal.app framework (Apple):
- ❌ Closed source, limited customization
- ❌ No GPU acceleration
- ❌ Stuck at 60fps even on ProMotion displays
- ❌ Poor ANSI 24-bit color support
Alacritty (Rust):
- ✅ GPU-accelerated
- ❌ No library interface (standalone app only)
- ❌ Can't embed in SwiftUI apps
- ❌ Linux/Windows focused
Ghostty (Zig):
- ✅ GPU-accelerated with Metal
- ✅ Library interface for embedding
- ✅ Native macOS/iOS support
- ✅ 120fps on ProMotion displays
- ✅ Full ANSI/VT100/VT220 support
- ✅ True color (24-bit) support
- ✅ Ligature support
- ✅ Minimal memory footprint
Architecture Overview
FoxTerm's terminal architecture:
┌─────────────────────────────────────────────┐
│ SwiftUI Terminal View │
│ (Native macOS/iOS UI, Tab Management) │
└──────────────────┬──────────────────────────┘
│
┌──────────────────▼──────────────────────────┐
│ Ghostty Terminal Engine │
│ ┌──────────────────────────────────────┐ │
│ │ Metal Renderer (GPU-Accelerated) │ │
│ │ - Text rendering via Metal shaders │ │
│ │ - Ligature support │ │
│ │ - Subpixel antialiasing │ │
│ └──────────────────────────────────────┘ │
│ ┌──────────────────────────────────────┐ │
│ │ VT Parser (ANSI/VT100/VT220) │ │
│ │ - Escape sequence state machine │ │
│ │ - CSI/OSC/DCS handler │ │
│ │ - UTF-8 validation │ │
│ └──────────────────────────────────────┘ │
│ ┌──────────────────────────────────────┐ │
│ │ Grid Manager │ │
│ │ - Cell buffer (scrollback) │ │
│ │ - Dirty rect tracking │ │
│ │ - Selection & cursor │ │
│ └──────────────────────────────────────┘ │
└──────────────────┬──────────────────────────┘
│
┌──────────────────▼──────────────────────────┐
│ PTY (Pseudo-Terminal) │
│ - fork/exec or SSH channel │
│ - Input/output buffering │
│ - Signal handling (SIGWINCH) │
└─────────────────────────────────────────────┘
Metal Rendering Pipeline
Text Rendering with Metal Shaders
Ghostty uses Metal shaders to render text at GPU speed:
// Simplified Metal shader for text rendering
vertex VertexOut text_vertex(uint vertex_id [[vertex_id]]) {
// Generate fullscreen quad
float2 positions[4] = {
float2(-1, -1), float2(1, -1),
float2(-1, 1), float2(1, 1)
};
VertexOut out;
out.position = float4(positions[vertex_id], 0, 1);
out.tex_coord = (positions[vertex_id] + 1) / 2;
return out;
}
fragment float4 text_fragment(VertexOut in [[stage_in]],
texture2d<float> glyph_atlas [[texture(0)]],
sampler samp [[sampler(0)]]) {
float4 color = glyph_atlas.sample(samp, in.tex_coord);
// Subpixel antialiasing for crisp text
return color;
}
Glyph Atlas
Instead of rendering each character individually, Ghostty pre-renders glyphs into a texture atlas:
// Glyph atlas management
class GlyphAtlas {
private var texture: MTLTexture
private var glyphs: [GlyphKey: GlyphRect] = [:]
func getGlyph(codepoint: UInt32, style: FontStyle) -> GlyphRect {
let key = GlyphKey(codepoint: codepoint, style: style)
if let rect = glyphs[key] {
return rect // Cache hit
}
// Render new glyph to atlas
let rect = renderGlyphToAtlas(codepoint, style)
glyphs[key] = rect
return rect
}
}
Benefits:
- Single texture bind per frame
- Batch rendering of all text
- 10x faster than per-character rendering
Dirty Rect Optimization
Not every frame needs full redraw. Ghostty tracks "dirty rects" — regions that changed:
struct DirtyRect {
var x: Int
var y: Int
var width: Int
var height: Int
}
class GridManager {
private var dirtyRects: [DirtyRect] = []
func markDirty(x: Int, y: Int, width: Int, height: Int) {
dirtyRects.append(DirtyRect(x: x, y: y, width: width, height: height))
}
func render() {
// Only re-render dirty regions
for rect in dirtyRects {
renderRegion(rect)
}
dirtyRects.removeAll()
}
}
Result: Typing a single character only re-renders one cell, not the entire screen.
PTY Management
macOS PTY
On macOS, FoxTerm uses forkpty() to create pseudo-terminals:
import Darwin
func createPTY() -> (master: Int32, slave: Int32) {
var master: Int32 = 0
var slave: Int32 = 0
var winSize = winsize(
ws_row: UInt16(24),
ws_col: UInt16(80),
ws_xpixel: 0,
ws_ypixel: 0
)
let result = forkpty(&master, nil, nil, &winSize)
if result == 0 {
// Child process
execl("/bin/zsh", "zsh", nil)
exit(1)
}
slave = result
return (master, slave)
}
SSH Channel PTY
For SSH connections, FoxTerm creates a PTY over the SSH channel:
func openSSHPTY(session: SSHSession) -> SSHChannel {
let channel = session.openChannel(type: "session")
// Request PTY
channel.requestPTY(
term: "xterm-256color",
columns: 80,
rows: 24,
pixelWidth: 640,
pixelHeight: 480,
modes: terminalModes
)
// Request shell
channel.requestShell()
return channel
}
Window Resize Handling
When the terminal window resizes, FoxTerm sends SIGWINCH to the child process:
func handleResize(newSize: NSSize) {
let columns = Int(newSize.width / cellWidth)
let rows = Int(newSize.height / cellHeight)
// Update PTY size
var winSize = winsize(
ws_row: UInt16(rows),
ws_col: UInt16(columns),
ws_xpixel: UInt16(newSize.width),
ws_ypixel: UInt16(newSize.height)
)
ioctl(masterFD, TIOCSWINSZ, &winSize)
// Notify child process
kill(childPID, SIGWINCH)
}
ANSI Escape Sequence Parsing
State Machine Parser
Ghostty uses a state machine to parse ANSI escape sequences:
enum ParserState {
case ground
case escape
case escapeIntermediate
case csiEntry
case csiParam
case csiIntermediate
case oscString
case dcsEntry
}
class VTParser {
private var state: ParserState = .ground
private var params: [Int] = []
func feed(byte: UInt8) {
switch state {
case .ground:
if byte == 0x1B { // ESC
state = .escape
} else if byte >= 0x20 && byte <= 0x7E {
// Printable character
emitCharacter(Character(UnicodeScalar(byte)))
}
case .escape:
if byte == UInt8(ascii: "[") {
state = .csiEntry
params.removeAll()
} else if byte == UInt8(ascii: "]") {
state = .oscString
}
case .csiEntry:
if byte >= UInt8(ascii: "0") && byte <= UInt8(ascii: "9") {
state = .csiParam
params.append(Int(byte - UInt8(ascii: "0")))
}
case .csiParam:
if byte == UInt8(ascii: ";") {
params.append(0)
} else if byte >= UInt8(ascii: "0") && byte <= UInt8(ascii: "9") {
let last = params.count - 1
params[last] = params[last] * 10 + Int(byte - UInt8(ascii: "0"))
} else if byte >= 0x40 && byte <= 0x7E {
// Final byte - execute CSI sequence
executeCSI(params: params, finalByte: byte)
state = .ground
}
default:
break
}
}
private func executeCSI(params: [Int], finalByte: UInt8) {
switch finalByte {
case UInt8(ascii: "m"):
// SGR - Select Graphic Rendition
handleSGR(params)
case UInt8(ascii: "H"):
// CUP - Cursor Position
setCursorPosition(row: params[0], col: params[1])
case UInt8(ascii: "J"):
// ED - Erase Display
eraseDisplay(mode: params[0])
default:
break
}
}
}
SGR (Select Graphic Rendition)
SGR sequences control text formatting:
func handleSGR(_ params: [Int]) {
var i = 0
while i < params.count {
let code = params[i]
switch code {
case 0: // Reset
currentStyle = TextStyle()
case 1: // Bold
currentStyle.bold = true
case 3: // Italic
currentStyle.italic = true
case 4: // Underline
currentStyle.underline = true
case 30...37: // Foreground color (standard)
currentStyle.foreground = Color.ansi(code - 30)
case 38: // Extended foreground
if i + 1 < params.count && params[i + 1] == 5 {
// 256-color: 38;5;n
currentStyle.foreground = Color.ansi256(params[i + 2])
i += 2
} else if i + 1 < params.count && params[i + 1] == 2 {
// True color: 38;2;r;g;b
currentStyle.foreground = Color.rgb(
r: params[i + 2],
g: params[i + 3],
b: params[i + 4]
)
i += 4
}
case 40...47: // Background color (standard)
currentStyle.background = Color.ansi(code - 40)
default:
break
}
i += 1
}
}
Performance Benchmarks
Frame Rate Comparison
Test: Output 10,000 lines of text, measure frame rate:
| Terminal | Avg FPS | Min FPS | GPU Usage |
|---|---|---|---|
| Terminal.app | 45 | 28 | 0% (CPU) |
| iTerm2 | 58 | 35 | 12% |
| Alacritty | 118 | 95 | 18% |
| FoxTerm (Ghostty) | 120 | 110 | 15% |
Memory Usage
Test: Open 10 terminal tabs, each with 10,000 lines of scrollback:
| Terminal | Memory | Notes |
|---|---|---|
| Terminal.app | 245 MB | Baseline |
| iTerm2 | 312 MB | +27% |
| Alacritty | 189 MB | -23% |
| FoxTerm | 156 MB | -36% |
FoxTerm Enhancements
Ligature Support
FoxTerm supports programming ligatures (Fira Code, JetBrains Mono):
func renderLigatures(text: String) -> [GlyphSequence] {
var sequences: [GlyphSequence] = []
var i = 0
while i < text.count {
// Check for ligature patterns
if let ligature = matchLigature(text, at: i) {
sequences.append(GlyphSequence(
glyphs: ligature.glyphs,
width: ligature.width,
isLigature: true
))
i += ligature.charCount
} else {
sequences.append(GlyphSequence(
glyphs: [text[i]],
width: cellWidth,
isLigature: false
))
i += 1
}
}
return sequences
}
Subpixel Antialiasing
On macOS, FoxTerm uses subpixel antialiasing for crisp text:
fragment float4 subpixel_fragment(VertexOut in [[stage_in]]) {
float3 rgb = glyph_atlas.sample(samp, in.tex_coord).rgb;
// Apply subpixel rendering
float alpha = (rgb.r + rgb.g + rgb.b) / 3.0;
// Blend with background
float3 blended = mix(bg_color.rgb, text_color.rgb, alpha);
return float4(blended, 1.0);
}
Retina Display Support
FoxTerm automatically scales for Retina displays:
let scale = window.backingScaleFactor // 2.0 for Retina
let cellWidth = baseCellWidth * scale
let cellHeight = baseCellHeight * scale
// Render at 2x resolution
texture = device.makeTexture(
width: Int(gridWidth * scale),
height: Int(gridHeight * scale),
...
)
Conclusion
Ghostty brings modern GPU-accelerated terminal rendering to native macOS and iOS apps. By leveraging Metal shaders, dirty rect optimization, and efficient escape sequence parsing, FoxTerm delivers 120fps terminal performance with minimal resource usage.
The result: a terminal that feels as fast as the hardware allows, whether you're on a MacBook Pro with ProMotion or an iPad with Apple Silicon.
FoxTerm Pro includes the Ghostty terminal engine with GPU acceleration. Download at foxterm.app.