mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-07-31 12:49:03 +00:00
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>
236 lines
9.1 KiB
Zig
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);
|
|
}
|