Files
ghostty/src/renderer/State.zig
Chris Marchesi e8525c0fd9 Update to Zig 0.16.0
This commit represents the majority of the work necessary to upgrade
Ghostty to use Zig 0.16.0.

Key parts:

* In addition to its previous responsibilities, the global state now
  houses state for global I/O implementations and the process
  environment. It is now also utilized in the main application along
  with the C library. Where necessary, global state is isolated from key
  parts of the implementation (e.g., in libghostty subsystems), and it's
  expected that this list will grow.

* We currently manage our own C translation layer where necessary. In
  these cases, cImport has been removed in favor of the new external
  translate-c package. Due to fixes that have needed be made to properly
  translate the dependencies that were swapped out, as mentioned, we
  have had to backport fixes from the current translate-c package (and
  the upstream Arocc dependency). We will host this ourselves until Zig
  0.17.0 is released with these fixes.

* Where necessary (only a small number of cases), some stdlib code from
  0.15.2 (and even from 0.17.0) has been taken, adopted, and vendored in
  lib/compat.

Co-authored-by: Leah Amelia Chen <hi@pluie.me>
2026-07-21 12:35:05 -07:00

236 lines
9.1 KiB
Zig

//! This is the render state that is given to a renderer.
const State = @This();
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const Inspector = @import("../inspector/main.zig").Inspector;
const terminalpkg = @import("../terminal/main.zig");
const inputpkg = @import("../input.zig");
const renderer = @import("../renderer.zig");
/// The mutex that must be held while reading any of the data in the
/// members of this state. Note that the state itself is NOT protected
/// by the mutex and is NOT thread-safe, only the members values of the
/// state (i.e. the terminal, devmode, etc. values).
mutex: *std.Io.Mutex,
/// The terminal data.
terminal: *terminalpkg.Terminal,
/// The terminal inspector, if any. This will be null while the inspector
/// is not active and will be set when it is active.
inspector: ?*Inspector = null,
/// Dead key state. This will render the current dead key preedit text
/// over the cursor. This currently only ever renders a single codepoint.
/// Preedit can in theory be multiple codepoints long but that is left as
/// a future exercise.
preedit: ?Preedit = null,
/// Mouse state. This only contains state relevant to what renderers
/// need about the mouse.
mouse: Mouse = .{},
/// The number of threads currently waiting to acquire `mutex` via
/// `lockDemand`. This is not protected by the mutex; it is read by
/// hot lock/unlock loops (the IO parse thread) in `yieldToDemand` to
/// decide whether to hand the mutex off before relocking it.
demand: std.atomic.Value(u32) = .init(0),
/// Handoff generation counter. Incremented (with a futex wake) by
/// `unlockDemand` after a demanding waiter releases the mutex, so that
/// `yieldToDemand` knows the waiter had its turn.
handoff_gen: std.atomic.Value(u32) = .init(0),
/// How long `yieldToDemand` sleeps waiting for a demanding waiter to
/// take its turn before giving up. This bounds how long the IO parse
/// thread can stall if a wake is lost or the waiter is descheduled; a
/// demanding critical section (the renderer's frame snapshot) is
/// microseconds, so one millisecond is generous.
const handoff_timeout_ns = 1 * std.time.ns_per_ms;
/// Acquire `mutex` while signaling demand for it. Use this instead of
/// locking the mutex directly on threads that must not be starved by
/// a hot lock/unlock loop (the renderer's frame snapshot). Must be
/// released with `unlockDemand`; releasing with `mutex.unlock` keeps
/// the data safe but makes parked `yieldToDemand` callers wait out
/// their full timeout.
///
/// Both `std.Thread.Mutex` and os_unfair_lock are unfair: a running
/// thread that unlocks and immediately relocks beats a sleeping
/// waiter every time, because the waiter must first be woken and
/// scheduled. Under sustained pty output the IO parse thread is
/// exactly such a loop, so without this signal the renderer can
/// starve for as long as the output lasts.
pub fn lockDemand(self: *State, io: std.Io) void {
_ = self.demand.fetchAdd(1, .monotonic);
self.mutex.lockUncancelable(io);
const prev = self.demand.fetchSub(1, .monotonic);
assert(prev > 0);
}
/// Release `mutex` acquired via `lockDemand` and notify hot loops
/// parked in `yieldToDemand` that the demanding waiter had its turn.
pub fn unlockDemand(self: *State, io: std.Io) void {
self.mutex.unlock(io);
_ = self.handoff_gen.fetchAdd(1, .monotonic);
io.futexWake(@TypeOf(self.handoff_gen), &self.handoff_gen, 1);
}
/// Called by hot lock/unlock loops between critical sections, with
/// `mutex` NOT held: if a `lockDemand` waiter exists, sleep until it
/// has acquired and released the mutex (or the timeout passes). This
/// is the handoff that unfair mutexes never do on their own.
///
/// The orderings here are all monotonic because these atomics are a
/// scheduling heuristic, not a synchronization boundary: the mutex
/// itself orders the protected data, and the timeout bounds any
/// staleness.
pub fn yieldToDemand(self: *State, io: std.Io) void {
if (self.demand.load(.monotonic) == 0) return;
// Snapshot the generation before rechecking demand: if the waiter
// acquires and releases between our check and the wait below, the
// generation no longer matches and timedWait returns immediately.
const gen = self.handoff_gen.load(.monotonic);
if (self.demand.load(.monotonic) == 0) return;
io.futexWaitTimeout(
@TypeOf(self.handoff_gen),
&self.handoff_gen,
.init(gen),
.{ .duration = .{ .raw = .fromNanoseconds(handoff_timeout_ns), .clock = .awake } },
) catch {};
}
pub const Mouse = struct {
/// The point on the viewport where the mouse currently is. We use
/// viewport points to avoid the complexity of mapping the mouse to
/// the renderer state.
point: ?terminalpkg.point.Coordinate = null,
/// The mods that are currently active for the last mouse event.
/// This could really just be mods in general and we probably will
/// move it out of mouse state at some point.
mods: inputpkg.Mods = .{},
};
/// The pre-edit state. See Surface.preeditCallback for more information.
pub const Preedit = struct {
/// The codepoints to render as preedit text.
codepoints: []const Codepoint = &.{},
/// A single codepoint to render as preedit text.
pub const Codepoint = struct {
codepoint: u21,
wide: bool = false,
};
/// Deinit this preedit that was cre
pub fn deinit(self: *const Preedit, alloc: Allocator) void {
alloc.free(self.codepoints);
}
/// Allocate a copy of this preedit in the given allocator..
pub fn clone(self: *const Preedit, alloc: Allocator) !Preedit {
return .{
.codepoints = try alloc.dupe(Codepoint, self.codepoints),
};
}
/// The width in cells of all codepoints in the preedit.
pub fn width(self: *const Preedit) usize {
var result: usize = 0;
for (self.codepoints) |cp| {
result += if (cp.wide) 2 else 1;
}
return result;
}
/// Range returns the start and end x position of the preedit text
/// along with any codepoint offset necessary to fit the preedit
/// into the available space.
pub fn range(
self: *const Preedit,
start: terminalpkg.size.CellCountInt,
max: terminalpkg.size.CellCountInt,
) struct {
start: terminalpkg.size.CellCountInt,
end: terminalpkg.size.CellCountInt,
cp_offset: usize,
} {
// If our width is greater than the number of cells we have
// then we need to adjust our codepoint start to a point where
// our width would be less than the number of cells we have.
const w, const cp_offset = width: {
// max is inclusive, so we need to add 1 to it.
const max_width = max - start + 1;
// Rebuild our width in reverse order. This is because we want
// to offset by the end cells, not the start cells (if we have to).
var w: terminalpkg.size.CellCountInt = 0;
for (0..self.codepoints.len) |i| {
const reverse_i = self.codepoints.len - i - 1;
const cp = self.codepoints[reverse_i];
w += if (cp.wide) 2 else 1;
if (w > max_width) {
break :width .{ w, reverse_i };
}
}
// Width fit in the max width so no offset necessary.
break :width .{ w, 0 };
};
// If our preedit goes off the end of the screen, we adjust it so
// that it shifts left.
const end = if (w > 0) start + (w - 1) else start;
const start_offset = if (end > max) end - max else 0;
return .{
.start = start -| start_offset,
.end = end -| start_offset,
.cp_offset = cp_offset,
};
}
};
const test_hangul_ga: u21 = 0xAC00; // U+AC00 HANGUL SYLLABLE GA
test "preedit range covers exact cell width" {
const testing = std.testing;
{
const p: Preedit = .{
.codepoints = &.{.{ .codepoint = 'a' }},
};
const range = p.range(2, 9);
try testing.expectEqual(@as(terminalpkg.size.CellCountInt, 2), range.start);
try testing.expectEqual(@as(terminalpkg.size.CellCountInt, 2), range.end);
try testing.expectEqual(@as(usize, 0), range.cp_offset);
}
{
const p: Preedit = .{
.codepoints = &.{.{ .codepoint = test_hangul_ga, .wide = true }},
};
const range = p.range(2, 9);
try testing.expectEqual(@as(terminalpkg.size.CellCountInt, 2), range.start);
try testing.expectEqual(@as(terminalpkg.size.CellCountInt, 3), range.end);
try testing.expectEqual(@as(usize, 0), range.cp_offset);
}
}
test "preedit range shifts left at right edge" {
const testing = std.testing;
const p: Preedit = .{
.codepoints = &.{.{ .codepoint = test_hangul_ga, .wide = true }},
};
const range = p.range(9, 9);
try testing.expectEqual(@as(terminalpkg.size.CellCountInt, 8), range.start);
try testing.expectEqual(@as(terminalpkg.size.CellCountInt, 9), range.end);
try testing.expectEqual(@as(usize, 0), range.cp_offset);
}