summaryrefslogtreecommitdiff
path: root/src/macos.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/macos.zig')
-rw-r--r--src/macos.zig179
1 files changed, 125 insertions, 54 deletions
diff --git a/src/macos.zig b/src/macos.zig
index 2e628d54..d463b842 100644
--- a/src/macos.zig
+++ b/src/macos.zig
@@ -117,32 +117,29 @@ const Msg = union(enum) {
}
};
-/// 0.16 has no std.Thread.Mutex, and the gui shell's queue already settled
-/// this: spin on the lock-free std.atomic.Mutex, and keep the critical section
-/// to a pointer append. The reader allocates its chunk BEFORE taking the lock
-/// for exactly that reason — a screenful of output must not make a keystroke
-/// spin through a 64 KiB copy.
-///
-/// ponytail: unbounded. `yes` in a pane can enqueue faster than the host
-/// drains, and nothing throttles the reader — the tty shell gets that for free
-/// from vaxis's 512-slot queue, whose post blocks when full, and the SDL shell
-/// has the same hole this does. Bound it on queued bytes the day someone
-/// watches RSS climb; the trap to avoid is a wait that teardown cannot cancel.
+const inbox_capacity = 512;
+
+const MessageBatch = struct {
+ items: [inbox_capacity]Msg = undefined,
+ len: usize = 0,
+
+ fn slice(batch: *MessageBatch) []Msg {
+ return batch.items[0..batch.len];
+ }
+};
+
const Inbox = struct {
mutex: std.atomic.Mutex = .unlocked,
- items: std.ArrayList(Msg) = .empty,
+ items: [inbox_capacity]Msg = undefined,
+ head: usize = 0,
+ len: usize = 0,
+ closed: bool = false,
/// Set when a wakeup has been delivered and not yet answered by a tick.
- /// Without it a busy shell posts one wakeup per 64 KiB chunk, and each one
- /// is a block on the host's main queue — a screenful of output becomes
- /// thousands of scheduled pumps that all find the same drained inbox.
wake_pending: std.atomic.Value(bool) = .init(false),
fn lock(q: *Inbox) void {
- // Bounded, unlike the gui shell's otherwise identical spin. AppKit's
- // main thread runs at a higher QoS than these reader tasks and a raw
- // CAS spin donates no priority, so a reader preempted inside the append
- // (which can realloc) would have the highest-priority thread in the
- // process spinning on it. Yielding hands the core back.
+ // AppKit's main thread runs at a higher QoS than reader tasks, so yield
+ // periodically rather than donating a full core to a preempted reader.
var spins: u8 = 0;
while (!q.mutex.tryLock()) {
spins +%= 1;
@@ -150,10 +147,70 @@ const Inbox = struct {
}
}
+ fn removeAt(q: *Inbox, offset: usize) Msg {
+ const removed = q.items[(q.head + offset) % q.items.len];
+ var i = offset;
+ while (i + 1 < q.len) : (i += 1)
+ q.items[(q.head + i) % q.items.len] = q.items[(q.head + i + 1) % q.items.len];
+ q.len -= 1;
+ return removed;
+ }
+
+ /// Pty output is lossy under sustained backpressure. EOF is structural:
+ /// admit it by evicting queued output so dead readers are always reaped.
fn push(q: *Inbox, gpa: std.mem.Allocator, m: Msg) void {
q.lock();
defer q.mutex.unlock();
- q.items.append(gpa, m) catch m.free(gpa);
+ if (q.closed) {
+ m.free(gpa);
+ return;
+ }
+ if (q.len == q.items.len) {
+ const incoming_eof = switch (m) {
+ .eof => true,
+ .output => false,
+ };
+ if (!incoming_eof) {
+ m.free(gpa);
+ return;
+ }
+ var offset: usize = 0;
+ while (offset < q.len) : (offset += 1)
+ if (switch (q.items[(q.head + offset) % q.items.len]) {
+ .output => true,
+ .eof => false,
+ }) break;
+ if (offset == q.len) return;
+ q.removeAt(offset).free(gpa);
+ }
+ q.items[(q.head + q.len) % q.items.len] = m;
+ q.len += 1;
+ }
+
+ fn take(q: *Inbox) MessageBatch {
+ q.lock();
+ defer q.mutex.unlock();
+ var batch: MessageBatch = .{};
+ while (q.len > 0) {
+ batch.items[batch.len] = q.items[q.head];
+ batch.len += 1;
+ q.head = (q.head + 1) % q.items.len;
+ q.len -= 1;
+ }
+ q.head = 0;
+ return batch;
+ }
+
+ fn close(q: *Inbox, gpa: std.mem.Allocator) void {
+ q.lock();
+ defer q.mutex.unlock();
+ q.closed = true;
+ while (q.len > 0) {
+ q.items[q.head].free(gpa);
+ q.head = (q.head + 1) % q.items.len;
+ q.len -= 1;
+ }
+ q.head = 0;
}
};
@@ -164,7 +221,8 @@ const State = struct {
core: *pardes.Pardes,
arena: std.heap.ArenaAllocator,
runtime: Runtime,
- cells: std.ArrayList(Cell) = .empty,
+ cells: []Cell = &.{},
+ frame_len: usize = 0,
/// The grid `cells` actually holds. Not read back off the core: a render
/// can move screen_w/screen_h and then fail, and a host that sized its
/// loops from those would walk off the buffer.
@@ -180,9 +238,6 @@ const State = struct {
/// moves a whole row at a time, so fractional trackpad travel accumulates
/// here and is spent as wheel presses — see pardes_scroll.
scroll_lag: f32 = 0,
- /// Swapped with the inbox under the lock, so draining it costs one pointer
- /// exchange and neither side reallocates once capacities have settled.
- tick_msgs: std.ArrayList(Msg) = .empty,
/// Owns the bytes of the user config, which Options only borrows.
config_arena: std.heap.ArenaAllocator,
};
@@ -209,6 +264,9 @@ export fn pardes_init(runtime: ?*const Runtime, cols_arg: u16, rows_arg: u16) c_
fn initCore(runtime: ?*const Runtime, cols_arg: u16, rows_arg: u16) !void {
const gpa = std.heap.smp_allocator;
+ const allocs = pardes.allocators.init(gpa);
+ errdefer pardes.allocators.deinit();
+
const threaded = try gpa.create(std.Io.Threaded);
errdefer gpa.destroy(threaded);
threaded.* = .init(gpa, .{});
@@ -218,7 +276,12 @@ fn initCore(runtime: ?*const Runtime, cols_arg: u16, rows_arg: u16) !void {
var config_arena: std.heap.ArenaAllocator = .init(gpa);
errdefer config_arena.deinit();
- var opts: pardes.Options = .{ .tty_only = true };
+ var opts: pardes.Options = .{
+ .tty_only = true,
+ .image_allocator = allocs.image,
+ .pdf_allocator = allocs.pdf,
+ .tree_sitter_allocator = allocs.tree_sitter,
+ };
// Native shells opt into the user config, and every builtin in it must have
// run before the host can render a frame — so it is read here, before
// Pardes.init, exactly as src/main.zig does it. The env map is rebuilt from
@@ -226,11 +289,14 @@ fn initCore(runtime: ?*const Runtime, cols_arg: u16, rows_arg: u16) !void {
if (captureEnv(config_arena.allocator())) |*env|
opts.startup_config = user_config.load(io, config_arena.allocator(), env);
- // stb_image's allocator shim, for image panes.
- pardes.image.start(io, gpa);
+ pardes.image.start(io, allocs.image);
errdefer pardes.image.stop();
+ if (comptime pardes.pdf_enabled) pardes.pdf.start(allocs.pdf);
+ errdefer if (comptime pardes.pdf_enabled) pardes.pdf.stop();
+ pardes.syntax.start(allocs.tree_sitter);
+ errdefer pardes.syntax.stop();
- const core = try pardes.Pardes.init(gpa, opts);
+ const core = try pardes.Pardes.init(allocs.pardes, opts);
errdefer core.deinit();
// Shells emit OSC 133 prompt marks through these, which is what makes
@@ -246,7 +312,7 @@ fn initCore(runtime: ?*const Runtime, cols_arg: u16, rows_arg: u16) !void {
.threaded = threaded,
.io = io,
.core = core,
- .arena = .init(gpa),
+ .arena = .init(allocs.frame),
.config_arena = config_arena,
.runtime = if (runtime) |r| r.* else .{},
};
@@ -279,17 +345,17 @@ export fn pardes_deinit() void {
for (0..pardes.MAX_PANES) |pane| reap(st, @intCast(pane));
// Only now is the inbox quiet. Anything still queued owns gpa bytes and
// would show up as a leak rather than as the shutdown it actually is.
- for (st.inbox.items.items) |msg| msg.free(st.gpa);
- st.inbox.items.deinit(st.gpa);
- for (st.tick_msgs.items) |msg| msg.free(st.gpa);
- st.tick_msgs.deinit(st.gpa);
- st.cells.deinit(st.gpa);
- st.core.deinit();
+ st.inbox.close(st.gpa);
+ if (st.cells.len > 0) st.gpa.free(st.cells);
st.arena.deinit();
- st.config_arena.deinit();
+ st.core.deinit();
pardes.image.stop();
+ if (comptime pardes.pdf_enabled) pardes.pdf.stop();
+ pardes.syntax.stop();
+ st.config_arena.deinit();
st.threaded.deinit();
st.gpa.destroy(st.threaded);
+ pardes.allocators.deinit();
state = null;
}
@@ -308,15 +374,12 @@ export fn pardes_animating() bool {
/// wakeup does not cost the host a repaint.
export fn pardes_tick() bool {
const st = &(state orelse return false);
- // Cleared BEFORE the swap: a reader that pushes while this drain is running
- // must be able to schedule the tick that will collect it.
+ // Cleared before the drain: a reader that pushes during this tick must be
+ // able to schedule the next one.
st.inbox.wake_pending.store(false, .release);
- st.inbox.lock();
- std.mem.swap(std.ArrayList(Msg), &st.inbox.items, &st.tick_msgs);
- st.inbox.mutex.unlock();
-
- var changed = st.tick_msgs.items.len > 0;
- for (st.tick_msgs.items) |msg| {
+ var batch = st.inbox.take();
+ var changed = batch.len > 0;
+ for (batch.slice()) |msg| {
defer msg.free(st.gpa);
switch (msg) {
.output => |o| {
@@ -327,16 +390,12 @@ export fn pardes_tick() bool {
if (st.gens[e.pane] != e.gen) continue;
// The shell is gone: join its reader (a completed future that
// is never awaited leaks its allocation), close the master and
- // free the slot. Without this the slot is only ever reaped by a
- // later spawn INTO it — and the core emits spawn from newPane
- // alone, so a pane that becomes a file pane instead would hold
- // the dead fd for the life of the app.
+ // free the slot.
reap(st, e.pane);
st.core.update(.{ .eof = .{ .pane = e.pane } });
},
}
}
- st.tick_msgs.clearRetainingCapacity();
if (drainEffects(st, true)) changed = true;
// A live theme transition repaints on its own clock; say so, or the host
// stops ticking and the fade freezes half-applied.
@@ -435,7 +494,7 @@ export fn pardes_frame() u32 {
// The three accessors below must never describe a different frame than the
// count this returns, so a failure empties all of them together rather than
// leaving last frame's buffer behind a fresh cols/rows.
- st.cells.clearRetainingCapacity();
+ st.frame_len = 0;
st.frame_cols = 0;
st.frame_rows = 0;
const surface = st.core.render(st.arena.allocator()) catch |err| {
@@ -443,10 +502,22 @@ export fn pardes_frame() u32 {
return 0;
};
const count: usize = @as(usize, surface.cols) * surface.rows;
- st.cells.resize(st.gpa, count) catch return 0;
+ if (count != st.cells.len) {
+ if (count == 0) {
+ if (st.cells.len > 0) st.gpa.free(st.cells);
+ st.cells = &.{};
+ } else {
+ const resized = if (st.cells.len == 0)
+ st.gpa.alloc(Cell, count)
+ else
+ st.gpa.realloc(st.cells, count);
+ st.cells = resized catch return 0;
+ }
+ }
+ st.frame_len = count;
st.frame_cols = surface.cols;
st.frame_rows = surface.rows;
- for (surface.cells, st.cells.items) |cell, *out| {
+ for (surface.cells, st.cells[0..count]) |cell, *out| {
out.* = .{
.text = @splat(0),
.fg = encodeColor(cell.style.fg),
@@ -462,7 +533,7 @@ export fn pardes_frame() u32 {
export fn pardes_frame_cells() ?[*]const Cell {
const st = &(state orelse return null);
- return if (st.cells.items.len == 0) null else st.cells.items.ptr;
+ return if (st.frame_len == 0) null else st.cells.ptr;
}
export fn pardes_frame_cols() u16 {