Skip to content

Add optional Metal GPU-accelerated terminal renderer#480

Open
trsdn wants to merge 1 commit intomigueldeicaza:mainfrom
trsdn:metal-renderer
Open

Add optional Metal GPU-accelerated terminal renderer#480
trsdn wants to merge 1 commit intomigueldeicaza:mainfrom
trsdn:metal-renderer

Conversation

@trsdn
Copy link

@trsdn trsdn commented Feb 26, 2026

Summary

Adds an optional Metal-based rendering backend as an alternative to CoreGraphics. The renderer is opt-in — existing behavior is completely unchanged unless the user explicitly sets tv.renderer = MetalTerminalRenderer().

Changes

New files (all in Sources/SwiftTerm/Rendering/):

File Lines Purpose
TerminalRenderer.swift 59 Protocol abstracting rendering backends
CoreGraphicsRenderer.swift 44 Thin wrapper around existing drawTerminalContents()
MetalTerminalRenderer.swift ~1050 GPU-accelerated renderer with instanced drawing
GlyphAtlas.swift ~350 CoreText → MTLTexture shelf-packed glyph cache
ShaderTypes.h 35 Shared Metal/Swift struct definitions
TerminalShaders.metal ~150 Reference shaders (compiled at runtime for SPM)

Modified files:

  • MacTerminalView.swift (+25 lines): renderer property, default CoreGraphicsRenderer in setup(), draw() dispatch

Architecture

TerminalRenderer (protocol)
├── CoreGraphicsRenderer   — wraps existing drawTerminalContents()
└── MetalTerminalRenderer  — GPU-accelerated path
    ├── GlyphAtlas         — CoreText → MTLTexture cache (2048×2048 .r8Unorm)
    └── Metal Shaders      — 4 instanced draw passes

Rendering pipeline: Background → Text → Decorations → Images (all instanced, single draw call per pass).

Key Design Decisions

  1. Retina-native glyph atlas — glyphs rasterized at backingScaleFactor (2x on Retina) for 1:1 texel-to-pixel mapping. No bilinear blur.
  2. Step thresholdstep(0.35, alpha) in fragment shader for crisp binary edges. CoreText anti-aliasing captures full glyph shape, threshold converts to sharp output.
  3. Pixel-snapped gridfloor() on all cell origins prevents sub-pixel seams.
  4. No font smoothing in atlassetShouldSmoothFonts(false) + setShouldAntialias(true). Smoothing in a grayscale context creates wide halos that look washed out.
  5. Self-contained blink timer — MetalTerminalRenderer manages its own blink state, no changes needed to existing text attribute logic.
  6. Graceful fallbackMetalTerminalRenderer.isAvailable returns false on VMs/CI without GPU.
  7. SPM compatible — shaders compiled from source string at runtime since SPM cannot compile .metal files.

Usage

let terminal = LocalProcessTerminalView(frame: frame)
if MetalTerminalRenderer.isAvailable {
    terminal.renderer = MetalTerminalRenderer()
}

What's NOT changed

  • Default rendering path (CoreGraphics) is untouched
  • All 373 existing tests pass
  • No API changes to existing public types
  • No new dependencies

Tested with

  • ✅ Text rendering (bold, italic, underline, strikethrough, blink, dim, inverse)
  • ✅ Full 256-color + TrueColor
  • ✅ Wide characters (CJK)
  • ✅ Box drawing and block elements
  • ✅ Selection overlay + all cursor styles
  • ✅ Dynamic resize
  • ✅ Retina displays
  • ✅ Sixel/Kitty image rendering
  • ✅ Scrollback buffer

Closes #479

Introduce a TerminalRenderer protocol that abstracts the rendering backend,
allowing CoreGraphics (default, backward-compatible) and Metal to be swapped.

New files in Sources/SwiftTerm/Rendering/:
- TerminalRenderer.swift: Protocol with setup/draw/resize/fontChanged/colorsChanged
- CoreGraphicsRenderer.swift: Thin wrapper around existing drawTerminalContents()
- MetalTerminalRenderer.swift: GPU-accelerated renderer with instanced drawing
- GlyphAtlas.swift: CoreText → MTLTexture shelf-packed glyph cache
- ShaderTypes.h: Shared Metal/Swift struct definitions
- TerminalShaders.metal: Reference shaders (compiled at runtime for SPM compat)

Rendering pipeline: 4 instanced draw passes (background → text → decorations → images).
Glyph atlas rasterized at backing scale for 1:1 texel-to-pixel on Retina displays.
Step threshold in fragment shader for crisp text without font smoothing artifacts.

Usage:
  let tv = LocalProcessTerminalView(frame: frame)
  if MetalTerminalRenderer.isAvailable {
      tv.renderer = MetalTerminalRenderer()
  }

Existing CoreGraphics behavior is completely unchanged — Metal is opt-in only.

Closes migueldeicaza#479

Co-authored-by: Copilot <[email protected]>
@migueldeicaza
Copy link
Owner

I have attempted this a few times, the most recent one is in my gpu branch, there are a lot of small and subtle issues lurking behind it. Check it and let me know if there are improvements we should bring from yours to mine, but I am a bit hesitant to bring yours given the wide spectrum of small things I had to address along the way.

@migueldeicaza
Copy link
Owner

My last update was about 3 weeks ago, I will try to merge main into that branch, so we can compare apples to apples.

@migueldeicaza
Copy link
Owner

Ok, I merged my changes into the gpu branch, would love if you could review if my branch is missing any features from the work you did.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Optional Metal GPU-accelerated terminal renderer

2 participants