systems programming

Shitty Terminal: How “Damage Rendering” and GPU Cells Win (and What “Memory-unsafe” Costs)

Shitty Terminal: How “Damage Rendering” and GPU Cells Win (and What “Memory-unsafe” Costs)

Picture this: you run a tool that dumps output faster than you can blink, and your prompt starts to lag. The shell isn’t slow. The CPU graph looks fine. Still, the terminal feels syrupy—like the pixels are arriving late.

That’s the moment when the terminal emulator (the program that emulates a text terminal) becomes the bottleneck. And it’s exactly the niche that Shitty—an aggressively fast terminal emulator by pg83—tries to dominate. In its own words, it’s “blazingly fast,” with the warning that it’s “memory-unsafe,” and it targets low latency, fast startup, and predictable resource use. (github.com)

But speed isn’t magic. It’s design choices: how you parse terminal control sequences, how you represent a text grid, when you repaint, how you cache glyphs (glyphs are the visual shapes of characters), and which parts you offload to the GPU (the graphics processor).

What a terminal emulator actually has to do

A terminal emulator isn’t “just a viewer.” It’s a live state machine:

  1. It receives a stream of bytes from a process running inside a pseudo terminal.
  2. It interprets control sequences (escape sequences) that move the cursor, change colors, switch screens, and more.
  3. It maintains a 2D grid of “cells” (each cell is usually: one character, plus styling like foreground/background color).
  4. It renders that grid to the screen.

The key plumbing concept here is the PTY.

PTY (pseudo-terminal): a Unix feature that makes a program think it’s connected to a real terminal. A terminal emulator typically owns the “other end” of that PTY so it can send and receive data.

The bytes coming from your program aren’t always plain text. They often include escape sequences like “set color,” “move cursor,” or “insert text.” Even worse, input can be adversarial: not just “normal shell output,” but random bytes or invalid UTF-8 (UTF-8 is the encoding used for most Unicode text).

Shitty explicitly measures performance in both a “printable ASCII” case and a “random bytes with invalid UTF-8” case—because real systems often break in the weird corner cases, not the happy path. ()

Keeping terminal state on the CPU, but rendering on fast compute

Traditional terminal emulators often store the grid in memory and repaint the whole thing (or large parts) every update—especially when the implementation is simple.

Shitty takes a different path: it keeps terminal state on the CPU (the “normal” main processor) and renders cells using native compute backends: Vulkan on Linux and Metal on macOS. ()

That sounds like a platform detail, but it points to a big architecture decision:

  • CPU work: parse input, update the cell grid, decide what changed.
  • GPU work: turn the changed cells into pixels quickly.

The CPU still has to understand terminal semantics. The GPU just needs a fast pipeline for “these glyphs and styles appear in these cells.”

The secret sauce: damage-driven rendering

To render quickly, the terminal must avoid doing unnecessary work. The word you want here is damage.

Damage-driven rendering: only redraw the parts of the screen that actually changed.

If a command prints one line at the bottom, it shouldn’t force a full-screen redraw. If the cursor moves but the characters don’t, repainting everything would be wasted effort.

Shitty lists “damage-driven compute rendering” as a core feature, alongside lazy glyph rasterization and a persistent GPU glyph cache. ()

Let’s make that concrete with a toy example.

A tiny “damage tracker” mental model

Suppose your terminal maintains a 2D array grid[row][col], and each cell includes a character and style. Whenever an operation modifies a region, you mark that region as damaged.

Pseudo-code (not Shitty’s code, but the idea):

struct Cell { char32_t ch; uint32_t style; };

// A region on screen that needs repaint.
struct DamageRect { int r0, r1, c0, c1; };

std::vector<DamageRect> damages;

void set_cell(int r, int c, Cell next) {
 if (grid[r][c].ch!= next.ch || grid[r][c].style!= next.style) {
 grid[r][c] = next;
 damages.push_back({r, r+1, c, c+1});
 }
}

void render_frame() {
 // Merge overlapping rects to reduce work.
 auto merged = merge_rects(damages);
 damages.clear();

 // Send only merged damaged regions to the GPU renderer.
 gpu_render(merged, grid);
}

The real engineering is harder (merging rectangles, coalescing events, making sure you don’t miss updates), but the theme stays: speed comes from precision about “what changed.”

Glyphs, rasterization, and why caching matters

A glyph is the drawn shape of a character. Rendering a glyph generally involves:

  • deciding which font face to use,
  • converting the character code into a glyph outline or bitmap,
  • rasterizing (turning outlines into pixels),
  • then compositing those pixels into the final frame.

Shitty emphasizes lazy glyph rasterization and a persistent GPU glyph cache. (github.com)

Lazy rasterization means: don’t rasterize every possible glyph upfront. Rasterize only when it’s needed, and keep results around.

The persistent GPU glyph cache means: don’t re-upload or re-rasterize the same glyph every time it reappears. Terminal UIs tend to reuse the same character set over and over—ASCII first, then more Unicode, then box drawing, then emojis (depending on your workload).

Unicode: grapheme clusters and “double-width” reality

Fast rendering is still only half the story. Text correctness is the other half.

Shitty calls out Unicode grapheme clusters, combining characters, emoji sequences, and double-width characters. ()

Two beginner-friendly definitions:

  • Grapheme cluster: what a human thinks of as “one character,” even when it’s encoded as multiple Unicode code points (for example, a base letter plus combining accent).
  • Double-width character: a character that should occupy two columns in a monospace terminal grid.

If your cell grid assumes “one code point = one cell,” you’ll get misalignment. The terminal will drift: text that should line up won’t.

So correctness requires a Unicode-aware text pipeline before rendering. That pipeline is CPU work, and it has to stay fast.

“Memory-unsafe”: what it implies in a terminal

Shitty is written in C++23 and built with Clang, and it bluntly advertises itself as memory-unsafe. ()

In C and C++, “memory-unsafe” usually means the code can do things that languages like Rust try to prevent by default: out-of-bounds reads/writes, use-after-free, or other undefined behavior.

That’s not just a philosophical issue. In a terminal emulator, unsafe memory bugs can become serious fast because:

  • the input stream is untrusted (remote shells, arbitrary programs),
  • the emulator processes attacker-influenced bytes continuously,
  • the code often sits close to low-level OS primitives like PTYs and rendering buffers.

And yet, Shitty doesn’t seem to be ignoring testing. It describes sanitizer builds (including ASan and UBSan) and multiple testing modes. ()

ASan (Address Sanitizer) and UBSan (Undefined Behavior Sanitizer) are compiler-based runtime tools that try to detect memory errors and undefined behavior during testing.

The interesting tension here is: can a project choose unsafe performance-critical techniques, while still investing heavily in detection and regression tests? Shitty’s README reads like an attempt to answer “yes, but aggressively.” ()

Why the benchmarks are set up the way they are

Shitty reports performance numbers on a MacBook with a defined cell size (14×28px cells, 80×24 grid, 500 lines of scrollback) and then compares wall time and throughput to several other terminals. ()

Those details matter because terminal speed is extremely workload-dependent:

  • Same amount of output, different escape sequence mix.
  • Different font, which changes glyph rasterization cost.
  • Different scrollback, which changes how much history must be managed.

The “equalized setup” approach (ensuring each terminal uses the same font and grid assumptions before measurement) is a reminder that performance claims are often measuring “implementation choices,” not just CPU speed.

Practical takeaways: how to build “fast enough” for real users

Even if you never touch terminal emulator internals, the design patterns are transferable to any high-frequency UI renderer:

  • Track damage: avoid redrawing the whole world when a small region changed.
  • Cache expensive work: rasterize glyphs lazily and reuse results (GPU caches help).
  • Correctness before speed: grapheme clusters and double-width characters aren’t optional if users expect alignment.
  • Measure worst cases: random bytes and invalid UTF-8 represent real “adversarial” inputs that break naïve parsers.
  • Treat safety as part of performance: catching memory bugs early (sanitizers, fuzzing, conformance suites) prevents the slow death of regressions.

Shitty’s loud branding—fast, GPU-backed, CPU-stateful, and explicitly memory-unsafe—basically forces a debate that most terminal projects dance around. The renderer can be optimized until it feels like magic, but the foundations (parsing, state updates, glyph handling, and correctness under weird inputs) are the part that decides whether the magic lasts.

Closing thought

A terminal emulator sits at the crossroads of systems programming and UI rendering. When it’s fast, you stop noticing it. When it’s slow, you blame everything else.

Shitty’s main lesson is that “faster terminal” isn’t a single tweak. It’s a stack of choices—damage-driven rendering, glyph caching, GPU compute backends, and Unicode-aware layout—plus an honest stance about how much safety tradeoff is being made to squeeze latency down. ()

ahsan

ahsan

Hello! I am Mr Ahsan, the writer of the Website. I am from Netherland. I like to write about technology and the news around it.

Comments (0)

No comments yet. Be the first to respond!

Leave a Comment

Your comment will be visible after review.