The renderer thread owns its event loop and draw triggers

`renderer.Thread.threadMain_` sets crash metadata and QoS, lets the renderer enter its loop and thread, registers async wakeups, sends an initial wakeup, starts cursor blinking, configures draw timing, and runs the event loop.

self.wakeup.wait(&self.loop, &self.wakeup_c, Thread, self, wakeupCallback);
self.stop.wait(&self.loop, &self.stop_c, Thread, self, stopCallback);
self.draw_now.wait(&self.loop, &self.draw_now_c, Thread, self, drawNowCallback);

try self.wakeup.notify();
self.syncDrawTimer();
_ = try self.loop.run(.until_done);
Renderer messages update render-side state

The renderer mailbox carries visibility, focus, resize, inspector, cursor, config, and crash messages. The drain loop applies these messages on the renderer thread before drawing.

while (self.mailbox.pop()) |message| {
    switch (message) {
        .visible => |v| { self.flags.visible = v; self.setQosClass(); },
        .resize => |v| try self.renderer.setScreenSize(v.screen),
        .change_config => |config| try self.changeConfig(config.thread),
        else => {},
    }
}
Draw dispatch respects visibility, vsync, and platform constraints

`drawFrame` drops work if the surface is invisible. If the renderer owns vsync, non-forced draws are skipped. Some platform runtimes require drawing on the app thread; otherwise the renderer draws directly.

if (!self.flags.visible) return;
if (!now and self.renderer.hasVsync()) return;

if (must_draw_from_app_thread) {
    _ = self.app_mailbox.push(.{ .redraw_surface = self.surface }, .{ .instant = {} });
} else {
    self.renderer.drawFrame(false) catch |err|
        log.warn("error drawing err={}", .{err});
}
The generic renderer prepares frame data from terminal cells

The generic renderer is where terminal grid data becomes shaped glyphs and cell draw data. It iterates rows, applies selection/cursor/preedit logic, uses the font shaper and shaping cache, and ultimately submits GPU work through the selected graphics backend.

var run_iter = self.font_shaper.runIterator(run_iter_opts);
var shaper_run: ?font.shape.TextRun = try run_iter.next(self.alloc);

for (0.., cells_raw[0..cells_len], cells_style[0..cells_len]) |x, *cell, *managed_style| {
    const run = shaper_run orelse break;
    shaper_cells = shaper_cells orelse
        self.font_shaper_cache.get(run) orelse cache: {
            const new_cells = try self.font_shaper.shape(run);
            break :cache new_cells;
        };
}
Display links and focus influence draw cadence

The generic renderer wires a display link callback to `draw_now` where supported. Focus changes can start or stop that display link, shifting between platform-driven vsync and change-driven updates.

try display_link.setOutputCallback(
    xev.Async,
    &displayLinkCallback,
    &thr.draw_now,
);
display_link.start() catch {};

Back to Overview for the full end-to-end map.