From 8dd67662b32464671352cf9fe46f178109455168 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 3 Nov 2022 10:51:55 -0700 Subject: [PATCH 01/22] Blocking queue implementation for thread message passing --- src/blocking_queue.zig | 251 +++++++++++++++++++++++++++++++++++++++++ src/main.zig | 1 + 2 files changed, 252 insertions(+) create mode 100644 src/blocking_queue.zig diff --git a/src/blocking_queue.zig b/src/blocking_queue.zig new file mode 100644 index 000000000..8014c37a1 --- /dev/null +++ b/src/blocking_queue.zig @@ -0,0 +1,251 @@ +//! Blocking queue implementation aimed primarily for message passing +//! between threads. + +const std = @import("std"); +const builtin = @import("builtin"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + +/// Returns a blocking queue implementation for type T. +/// +/// This is tailor made for ghostty usage so it isn't meant to be maximally +/// generic, but I'm happy to make it more generic over time. Traits of this +/// queue that are specific to our usage: +/// +/// - Fixed size. We expect our queue to quickly drain and also not be +/// too large so we prefer a fixed size queue for now. +/// - No blocking pop. We use an external event loop mechanism such as +/// eventfd to notify our waiter that there is no data available so +/// we don't need to implement a blocking pop. +/// - Drain function. Most queues usually pop one at a time. We have +/// a mechanism for draining since on every IO loop our TTY drains +/// the full queue so we can get rid of the overhead of a ton of +/// locks and bounds checking and do a one-time drain. +/// +/// One key usage pattern is that our blocking queues are single producer +/// single consumer (SPSC). This should let us do some interesting optimizations +/// in the future. At the time of writing this, the blocking queue implementation +/// is purposely naive to build something quickly, but we should benchmark +/// and make this more optimized as necessary. +pub fn BlockingQueue( + comptime T: type, + comptime capacity: usize, +) type { + return struct { + const Self = @This(); + + // The type we use for queue size types. We can optimize this + // in the future to be the correct bit-size for our preallocated + // size for this queue. + const Size = u32; + + // The bounds of this queue. We recast this to Size so we can do math. + const bounds = @intCast(Size, capacity); + + /// Specifies the timeout for an operation. + pub const Timeout = union(enum) { + /// Fail instantly (non-blocking). + instant: void, + + /// Run forever or until interrupted + forever: void, + + /// Nanoseconds + ns: u64, + }; + + /// Our data. The values are undefined until they are written. + data: [bounds]T, + + /// The next location to write (next empty loc) and next location + /// to read (next non-empty loc). The number of written elements. + write: Size, + read: Size, + len: Size, + + /// The big mutex that must be held to read/write. + mutex: std.Thread.Mutex, + + /// A CV for being notified when the queue is no longer full. This is + /// used for writing. Note we DON'T have a CV for waiting on the + /// queue not being EMPTY because we use external notifiers for that. + cond_not_full: std.Thread.Condition, + not_full_waiters: usize, + + /// Allocate the blocking queue. Allocation must always happen on + /// the heap due to shared concurrency state. + pub fn create(alloc: Allocator) !*Self { + const ptr = try alloc.create(Self); + errdefer alloc.destroy(ptr); + + ptr.* = .{ + .data = undefined, + .len = 0, + .write = 0, + .read = 0, + .mutex = .{}, + .cond_not_full = .{}, + .not_full_waiters = 0, + }; + + return ptr; + } + + /// Free all the resources for this queue. This should only be + /// called once all producers and consumers have quit. + pub fn destroy(self: *Self, alloc: Allocator) void { + self.* = undefined; + alloc.destroy(self); + } + + /// Push a value to the queue. This returns the total size of the + /// queue (unread items) after the push. A return value of zero + /// means that the push failed. + pub fn push(self: *Self, value: T, timeout: Timeout) Size { + self.mutex.lock(); + defer self.mutex.unlock(); + + // The + if (self.full()) { + switch (timeout) { + // If we're not waiting, then we failed to write. + .instant => return 0, + + .forever => { + self.not_full_waiters += 1; + defer self.not_full_waiters -= 1; + self.cond_not_full.wait(&self.mutex); + }, + + .ns => |ns| { + self.not_full_waiters += 1; + defer self.not_full_waiters -= 1; + self.cond_not_full.timedWait(&self.mutex, ns) catch return 0; + }, + } + + // If we're still full, then we failed to write. This can + // happen in situations where we are interrupted. + if (self.full()) return 0; + } + + // Add our data and update our accounting + self.data[self.write] = value; + self.write += 1; + if (self.write >= bounds) self.write -= bounds; + self.len += 1; + + return self.len; + } + + /// Pop a value from the queue without blocking. + pub fn pop(self: *Self) ?T { + self.mutex.lock(); + defer self.mutex.unlock(); + + // If we're empty we have nothing + if (self.len == 0) return null; + + // Get the index we're going to read data from and do some + // accounting. We don't copy the value here to avoid copying twice. + const n = self.read; + self.read += 1; + if (self.read >= bounds) self.read -= bounds; + self.len -= 1; + + // If we have consumers waiting on a full queue, notify. + if (self.not_full_waiters > 0) self.cond_not_full.signal(); + + return self.data[n]; + } + + /// Pop all values from the queue. This will hold the big mutex + /// until `deinit` is called on the return value. This is used if + /// you know you're going to "pop" and utilize all the values + /// quickly to avoid many locks, bounds checks, and cv signals. + pub fn drain(self: *Self) DrainIterator { + self.mutex.lock(); + return .{ .queue = self }; + } + + pub const DrainIterator = struct { + queue: *Self, + + pub fn next(self: *DrainIterator) ?T { + if (self.queue.len == 0) return null; + + // Read and account + const n = self.queue.read; + self.queue.read += 1; + if (self.queue.read >= bounds) self.queue.read -= bounds; + self.queue.len -= 1; + + return self.queue.data[n]; + } + + pub fn deinit(self: *DrainIterator) void { + // If we have consumers waiting on a full queue, notify. + if (self.queue.not_full_waiters > 0) self.queue.cond_not_full.signal(); + + // Unlock + self.queue.mutex.unlock(); + } + }; + + /// Returns true if the queue is full. This is not public because + /// it requires the lock to be held. + inline fn full(self: *Self) bool { + return self.len == bounds; + } + }; +} + +test "basic push and pop" { + const testing = std.testing; + const alloc = testing.allocator; + + const Q = BlockingQueue(u64, 4); + const q = try Q.create(alloc); + defer q.destroy(alloc); + + // Should have no values + try testing.expect(q.pop() == null); + + // Push until we're full + try testing.expectEqual(@as(Q.Size, 1), q.push(1, .{ .instant = {} })); + try testing.expectEqual(@as(Q.Size, 2), q.push(2, .{ .instant = {} })); + try testing.expectEqual(@as(Q.Size, 3), q.push(3, .{ .instant = {} })); + try testing.expectEqual(@as(Q.Size, 4), q.push(4, .{ .instant = {} })); + try testing.expectEqual(@as(Q.Size, 0), q.push(5, .{ .instant = {} })); + + // Pop! + try testing.expect(q.pop().? == 1); + try testing.expect(q.pop().? == 2); + try testing.expect(q.pop().? == 3); + try testing.expect(q.pop().? == 4); + try testing.expect(q.pop() == null); + + // Drain does nothing + var it = q.drain(); + try testing.expect(it.next() == null); + it.deinit(); + + // Verify we can still push + try testing.expectEqual(@as(Q.Size, 1), q.push(1, .{ .instant = {} })); +} + +test "timed push" { + const testing = std.testing; + const alloc = testing.allocator; + + const Q = BlockingQueue(u64, 1); + const q = try Q.create(alloc); + defer q.destroy(alloc); + + // Push + try testing.expectEqual(@as(Q.Size, 1), q.push(1, .{ .instant = {} })); + try testing.expectEqual(@as(Q.Size, 0), q.push(2, .{ .instant = {} })); + + // Timed push should fail + try testing.expectEqual(@as(Q.Size, 0), q.push(2, .{ .ns = 1000 })); +} diff --git a/src/main.zig b/src/main.zig index cd92fd761..1f92efbe1 100644 --- a/src/main.zig +++ b/src/main.zig @@ -203,6 +203,7 @@ test { _ = @import("terminal/main.zig"); // TODO + _ = @import("blocking_queue.zig"); _ = @import("config.zig"); _ = @import("homedir.zig"); _ = @import("passwd.zig"); From 35c1decd58af93efcd693d195ce5010706a6bf5a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 3 Nov 2022 13:30:30 -0700 Subject: [PATCH 02/22] Start pulling out IO thread and IO implementation --- src/Window.zig | 45 ++++++++++++- src/termio.zig | 17 +++++ src/termio/Exec.zig | 100 +++++++++++++++++++++++++++++ src/termio/Options.zig | 13 ++++ src/termio/Thread.zig | 140 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 312 insertions(+), 3 deletions(-) create mode 100644 src/termio.zig create mode 100644 src/termio/Exec.zig create mode 100644 src/termio/Options.zig create mode 100644 src/termio/Thread.zig diff --git a/src/Window.zig b/src/Window.zig index f95965f89..cf18862f0 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -10,6 +10,7 @@ const builtin = @import("builtin"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; const renderer = @import("renderer.zig"); +const termio = @import("termio.zig"); const objc = @import("objc"); const glfw = @import("glfw"); const imgui = @import("imgui"); @@ -76,6 +77,11 @@ command: Command, /// Mouse state. mouse: Mouse, +/// The terminal IO handler. +io: termio.Impl, +io_thread: termio.Thread, +io_thr: std.Thread, + /// The terminal emulator internal state. This is the abstract "terminal" /// that manages input, grid updating, etc. and is renderer-agnostic. It /// just stores internal state about a grid. This is connected back to @@ -445,6 +451,18 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo var io_arena = std.heap.ArenaAllocator.init(alloc); errdefer io_arena.deinit(); + // Start our IO implementation + var io = try termio.Impl.init(alloc, .{ + .grid_size = grid_size, + .screen_size = screen_size, + .config = config, + }); + errdefer io.deinit(alloc); + + // Create the IO thread + var io_thread = try termio.Thread.init(alloc, &self.io); + errdefer io_thread.deinit(); + // The mutex used to protect our renderer state. var mutex = try alloc.create(std.Thread.Mutex); mutex.* = .{}; @@ -484,6 +502,9 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo .pty = pty, .command = cmd, .mouse = .{}, + .io = io, + .io_thread = io_thread, + .io_thr = undefined, .terminal = term, .terminal_stream = .{ .handler = self }, .terminal_cursor = .{ .timer = timer }, @@ -524,11 +545,11 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo // Load imgui. This must be done LAST because it has to be done after // all our GLFW setup is complete. if (DevMode.enabled) { - const io = try imgui.IO.get(); - io.cval().IniFilename = "ghostty_dev_mode.ini"; + const dev_io = try imgui.IO.get(); + dev_io.cval().IniFilename = "ghostty_dev_mode.ini"; // Add our built-in fonts so it looks slightly better - const dev_atlas = @ptrCast(*imgui.FontAtlas, io.cval().Fonts); + const dev_atlas = @ptrCast(*imgui.FontAtlas, dev_io.cval().Fonts); dev_atlas.addFontFromMemoryTTF( face_ttf, @intToFloat(f32, font_size.pixels()), @@ -553,6 +574,13 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo .{&self.renderer_thread}, ); + // Start our IO thread + self.io_thr = try std.Thread.spawn( + .{}, + termio.Thread.threadMain, + .{&self.io_thread}, + ); + return self; } @@ -579,6 +607,17 @@ pub fn destroy(self: *Window) void { self.imgui_ctx.destroy(); } + { + // Stop our IO thread + self.io_thread.stop.send() catch |err| + log.err("error notifying io thread to stop, may stall err={}", .{err}); + self.io_thr.join(); + self.io_thread.deinit(); + + // Deinitialize our terminal IO + self.io.deinit(self.alloc); + } + // Deinitialize the pty. This closes the pty handles. This should // cause a close in the our subprocess so just wait for that. self.pty.deinit(); diff --git a/src/termio.zig b/src/termio.zig new file mode 100644 index 000000000..9d0de3ffb --- /dev/null +++ b/src/termio.zig @@ -0,0 +1,17 @@ +//! IO implementation and utilities. The IO implementation is responsible +//! for taking the config, spinning up a child process, and handling IO +//! with the termianl. + +pub const Exec = @import("termio/Exec.zig"); +pub const Options = @import("termio/Options.zig"); +pub const Thread = @import("termio/Thread.zig"); + +/// The implementation to use for the IO. This is just "exec" for now but +/// this is somewhat pluggable so that in the future we can introduce other +/// options for other platforms (i.e. wasm) or even potentially a vtable +/// implementation for runtime polymorphism. +pub const Impl = Exec; + +test { + @import("std").testing.refAllDecls(@This()); +} diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig new file mode 100644 index 000000000..b5c7c4817 --- /dev/null +++ b/src/termio/Exec.zig @@ -0,0 +1,100 @@ +//! Implementation of IO that uses child exec to talk to the child process. +pub const Exec = @This(); + +const std = @import("std"); +const builtin = @import("builtin"); +const Allocator = std.mem.Allocator; +const termio = @import("../termio.zig"); +const Command = @import("../Command.zig"); +const Pty = @import("../Pty.zig"); +const terminal = @import("../terminal/main.zig"); +const libuv = @import("libuv"); + +const log = std.log.scoped(.io_exec); + +/// This is the pty fd created for the subcommand. +pty: Pty, + +/// This is the container for the subcommand. +command: Command, + +/// The terminal emulator internal state. This is the abstract "terminal" +/// that manages input, grid updating, etc. and is renderer-agnostic. It +/// just stores internal state about a grid. +terminal: terminal.Terminal, + +/// Initialize the exec implementation. This will also start the child +/// process. +pub fn init(alloc: Allocator, opts: termio.Options) !Exec { + // Create our pty + var pty = try Pty.open(.{ + .ws_row = @intCast(u16, opts.grid_size.rows), + .ws_col = @intCast(u16, opts.grid_size.columns), + .ws_xpixel = @intCast(u16, opts.screen_size.width), + .ws_ypixel = @intCast(u16, opts.screen_size.height), + }); + errdefer pty.deinit(); + + // Determine the path to the binary we're executing + const path = (try Command.expandPath(alloc, opts.config.command orelse "sh")) orelse + return error.CommandNotFound; + defer alloc.free(path); + + // Set our env vars + var env = try std.process.getEnvMap(alloc); + defer env.deinit(); + try env.put("TERM", "xterm-256color"); + + // Build our subcommand + var cmd: Command = .{ + .path = path, + .args = &[_][]const u8{path}, + .env = &env, + .cwd = opts.config.@"working-directory", + .pre_exec = (struct { + fn callback(c: *Command) void { + const p = c.getData(Pty) orelse unreachable; + p.childPreExec() catch |err| + log.err("error initializing child: {}", .{err}); + } + }).callback, + .data = &pty, + }; + // note: can't set these in the struct initializer because it + // sets the handle to "0". Probably a stage1 zig bug. + cmd.stdin = std.fs.File{ .handle = pty.slave }; + cmd.stdout = cmd.stdin; + cmd.stderr = cmd.stdin; + try cmd.start(alloc); + log.info("started subcommand path={s} pid={?}", .{ path, cmd.pid }); + + // Create our terminal + var term = try terminal.Terminal.init(alloc, opts.grid_size.columns, opts.grid_size.rows); + errdefer term.deinit(alloc); + + return Exec{ + .pty = pty, + .command = cmd, + .terminal = term, + }; +} + +pub fn deinit(self: *Exec, alloc: Allocator) void { + // Deinitialize the pty. This closes the pty handles. This should + // cause a close in the our subprocess so just wait for that. + self.pty.deinit(); + _ = self.command.wait() catch |err| + log.err("error waiting for command to exit: {}", .{err}); + + // Clean up the terminal state + self.terminal.deinit(alloc); +} + +pub fn threadEnter(self: *Exec, loop: libuv.Loop) !void { + _ = self; + _ = loop; +} + +pub fn threadExit(self: *Exec) void { + _ = self; +} diff --git a/src/termio/Options.zig b/src/termio/Options.zig new file mode 100644 index 000000000..750a07771 --- /dev/null +++ b/src/termio/Options.zig @@ -0,0 +1,13 @@ +//! The options that are used to configure a terminal IO implementation. + +const renderer = @import("../renderer.zig"); +const Config = @import("../config.zig").Config; + +/// The size of the terminal grid. +grid_size: renderer.GridSize, + +/// The size of the viewport in pixels. +screen_size: renderer.ScreenSize, + +/// The app configuration. +config: *const Config, diff --git a/src/termio/Thread.zig b/src/termio/Thread.zig new file mode 100644 index 000000000..ac30fb070 --- /dev/null +++ b/src/termio/Thread.zig @@ -0,0 +1,140 @@ +//! Represents the IO thread logic. The IO thread is responsible for +//! the child process and pty management. +pub const Thread = @This(); + +const std = @import("std"); +const builtin = @import("builtin"); +const libuv = @import("libuv"); +const termio = @import("../termio.zig"); + +const Allocator = std.mem.Allocator; +const log = std.log.scoped(.io_thread); + +/// The main event loop for the thread. The user data of this loop +/// is always the allocator used to create the loop. This is a convenience +/// so that users of the loop always have an allocator. +loop: libuv.Loop, + +/// This can be used to wake up the thread. +wakeup: libuv.Async, + +/// This can be used to stop the thread on the next loop iteration. +stop: libuv.Async, + +/// The underlying IO implementation. +impl: *termio.Impl, + +/// Initialize the thread. This does not START the thread. This only sets +/// up all the internal state necessary prior to starting the thread. It +/// is up to the caller to start the thread with the threadMain entrypoint. +pub fn init( + alloc: Allocator, + impl: *termio.Impl, +) !Thread { + // We always store allocator pointer on the loop data so that + // handles can use our global allocator. + const allocPtr = try alloc.create(Allocator); + errdefer alloc.destroy(allocPtr); + allocPtr.* = alloc; + + // Create our event loop. + var loop = try libuv.Loop.init(alloc); + errdefer loop.deinit(alloc); + loop.setData(allocPtr); + + // This async handle is used to "wake up" the renderer and force a render. + var wakeup_h = try libuv.Async.init(alloc, loop, wakeupCallback); + errdefer wakeup_h.close((struct { + fn callback(h: *libuv.Async) void { + const loop_alloc = h.loop().getData(Allocator).?.*; + h.deinit(loop_alloc); + } + }).callback); + + // This async handle is used to stop the loop and force the thread to end. + var stop_h = try libuv.Async.init(alloc, loop, stopCallback); + errdefer stop_h.close((struct { + fn callback(h: *libuv.Async) void { + const loop_alloc = h.loop().getData(Allocator).?.*; + h.deinit(loop_alloc); + } + }).callback); + + return Thread{ + .loop = loop, + .wakeup = wakeup_h, + .stop = stop_h, + .impl = impl, + }; +} + +/// Clean up the thread. This is only safe to call once the thread +/// completes executing; the caller must join prior to this. +pub fn deinit(self: *Thread) void { + // Get a copy to our allocator + const alloc_ptr = self.loop.getData(Allocator).?; + const alloc = alloc_ptr.*; + + // Schedule our handles to close + self.stop.close((struct { + fn callback(h: *libuv.Async) void { + const handle_alloc = h.loop().getData(Allocator).?.*; + h.deinit(handle_alloc); + } + }).callback); + self.wakeup.close((struct { + fn callback(h: *libuv.Async) void { + const handle_alloc = h.loop().getData(Allocator).?.*; + h.deinit(handle_alloc); + } + }).callback); + + // Run the loop one more time, because destroying our other things + // like windows usually cancel all our event loop stuff and we need + // one more run through to finalize all the closes. + _ = self.loop.run(.default) catch |err| + log.err("error finalizing event loop: {}", .{err}); + + // Dealloc our allocator copy + alloc.destroy(alloc_ptr); + + self.loop.deinit(alloc); +} + +/// The main entrypoint for the thread. +pub fn threadMain(self: *Thread) void { + // Call child function so we can use errors... + self.threadMain_() catch |err| { + // In the future, we should expose this on the thread struct. + log.warn("error in io thread err={}", .{err}); + }; +} + +fn threadMain_(self: *Thread) !void { + // Run our thread start/end callbacks. This allows the implementation + // to hook into the event loop as needed. + try self.impl.threadEnter(self.loop); + defer self.impl.threadExit(); + + // Set up our async handler to support rendering + self.wakeup.setData(self); + defer self.wakeup.setData(null); + + // Run + log.debug("starting IO thread", .{}); + defer log.debug("exiting IO thread", .{}); + _ = try self.loop.run(.default); +} + +fn wakeupCallback(h: *libuv.Async) void { + _ = h; + // const t = h.getData(Thread) orelse { + // // This shouldn't happen so we log it. + // log.warn("render callback fired without data set", .{}); + // return; + // }; +} + +fn stopCallback(h: *libuv.Async) void { + h.loop().stop(); +} From 9b3d22e55e2fef2697a75f06b59d31c87ae5bf0f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 3 Nov 2022 15:07:51 -0700 Subject: [PATCH 03/22] IO thread has more state setup --- src/Window.zig | 1 + src/renderer/State.zig | 1 + src/termio/Exec.zig | 183 +++++++++++++++++++++++++++++++++++++++-- src/termio/Options.zig | 6 ++ src/termio/Thread.zig | 5 +- 5 files changed, 189 insertions(+), 7 deletions(-) diff --git a/src/Window.zig b/src/Window.zig index cf18862f0..b39a6f005 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -456,6 +456,7 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo .grid_size = grid_size, .screen_size = screen_size, .config = config, + .renderer_state = &self.renderer_state, }); errdefer io.deinit(alloc); diff --git a/src/renderer/State.zig b/src/renderer/State.zig index f03182f27..bbf066da4 100644 --- a/src/renderer/State.zig +++ b/src/renderer/State.zig @@ -1,6 +1,7 @@ //! This is the render state that is given to a renderer. const std = @import("std"); +const Allocator = std.mem.Allocator; const DevMode = @import("../DevMode.zig"); const terminal = @import("../terminal/main.zig"); const renderer = @import("../renderer.zig"); diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index b5c7c4817..0fee6b341 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -9,6 +9,7 @@ const Command = @import("../Command.zig"); const Pty = @import("../Pty.zig"); const terminal = @import("../terminal/main.zig"); const libuv = @import("libuv"); +const renderer = @import("../renderer.zig"); const log = std.log.scoped(.io_exec); @@ -23,6 +24,13 @@ command: Command, /// just stores internal state about a grid. terminal: terminal.Terminal, +/// The stream parser. This parses the stream of escape codes and so on +/// from the child process and calls callbacks in the stream handler. +terminal_stream: terminal.Stream(StreamHandler), + +/// The shared render state +renderer_state: *renderer.State, + /// Initialize the exec implementation. This will also start the child /// process. pub fn init(alloc: Allocator, opts: termio.Options) !Exec { @@ -76,6 +84,8 @@ pub fn init(alloc: Allocator, opts: termio.Options) !Exec { .pty = pty, .command = cmd, .terminal = term, + .terminal_stream = undefined, + .renderer_state = opts.renderer_state, }; } @@ -86,15 +96,178 @@ pub fn deinit(self: *Exec, alloc: Allocator) void { _ = self.command.wait() catch |err| log.err("error waiting for command to exit: {}", .{err}); - // Clean up the terminal state + // Clean up our other members self.terminal.deinit(alloc); } -pub fn threadEnter(self: *Exec, loop: libuv.Loop) !void { - _ = self; - _ = loop; +pub fn threadEnter(self: *Exec, loop: libuv.Loop) !ThreadData { + // Get a copy to our allocator + const alloc_ptr = loop.getData(Allocator).?; + const alloc = alloc_ptr.*; + + // Setup our data that is used for callbacks + var ev_data_ptr = try alloc.create(EventData); + errdefer alloc.destroy(ev_data_ptr); + + // Read data + var stream = try libuv.Tty.init(alloc, loop, self.pty.master); + errdefer stream.deinit(alloc); + stream.setData(ev_data_ptr); + try stream.readStart(ttyReadAlloc, ttyRead); + + // Setup our event data before we start + ev_data_ptr.* = .{ + .read_arena = std.heap.ArenaAllocator.init(alloc), + .renderer_state = self.renderer_state, + .data_stream = stream, + .terminal_stream = .{ + .handler = .{ + .terminal = &self.terminal, + }, + }, + }; + errdefer ev_data_ptr.deinit(); + + // Return our data + return ThreadData{ + .alloc = alloc, + .ev = ev_data_ptr, + }; } -pub fn threadExit(self: *Exec) void { +pub fn threadExit(self: *Exec, data: ThreadData) void { _ = self; + _ = data; } + +const ThreadData = struct { + /// Allocator used for the event data + alloc: Allocator, + + /// The data that is attached to the callbacks. + ev: *EventData, + + pub fn deinit(self: *ThreadData) void { + self.ev.deinit(); + self.alloc.destroy(self.ev); + self.* = undefined; + } +}; + +const EventData = struct { + /// This is the arena allocator used for IO read buffers. Since we use + /// libuv under the covers, this lets us rarely heap allocate since we're + /// usually just reusing buffers from this. + read_arena: std.heap.ArenaAllocator, + + /// The stream parser. This parses the stream of escape codes and so on + /// from the child process and calls callbacks in the stream handler. + terminal_stream: terminal.Stream(StreamHandler), + + /// The shared render state + renderer_state: *renderer.State, + + /// The data stream is the main IO for the pty. + data_stream: libuv.Tty, + + pub fn deinit(self: *EventData) void { + self.read_arena.deinit(); + + // Stop our data stream + self.data_stream.readStop(); + self.data_stream.close((struct { + fn callback(h: *libuv.Tty) void { + const handle_alloc = h.loop().getData(Allocator).?.*; + h.deinit(handle_alloc); + } + }).callback); + } +}; + +fn ttyReadAlloc(t: *libuv.Tty, size: usize) ?[]u8 { + const ev = t.getData(EventData) orelse return null; + const alloc = ev.read_arena.allocator(); + return alloc.alloc(u8, size) catch null; +} + +fn ttyRead(t: *libuv.Tty, n: isize, buf: []const u8) void { + const ev = t.getData(EventData).?; + defer { + const alloc = ev.read_arena.allocator(); + alloc.free(buf); + } + + // log.info("DATA: {d}", .{n}); + // log.info("DATA: {any}", .{buf[0..@intCast(usize, n)]}); + + // First check for errors in the case n is less than 0. + libuv.convertError(@intCast(i32, n)) catch |err| { + switch (err) { + // ignore EOF because it should end the process. + libuv.Error.EOF => {}, + else => log.err("read error: {}", .{err}), + } + + return; + }; + + // We are modifying terminal state from here on out + ev.renderer_state.mutex.lock(); + defer ev.renderer_state.mutex.unlock(); + + // Whenever a character is typed, we ensure the cursor is in the + // non-blink state so it is rendered if visible. + ev.renderer_state.cursor.blink = false; + // TODO + // if (win.terminal_cursor.timer.isActive() catch false) { + // _ = win.terminal_cursor.timer.again() catch null; + // } + + // Schedule a render + // TODO + //win.queueRender() catch unreachable; + + // Process the terminal data. This is an extremely hot part of the + // terminal emulator, so we do some abstraction leakage to avoid + // function calls and unnecessary logic. + // + // The ground state is the only state that we can see and print/execute + // ASCII, so we only execute this hot path if we're already in the ground + // state. + // + // Empirically, this alone improved throughput of large text output by ~20%. + var i: usize = 0; + const end = @intCast(usize, n); + // TODO: re-enable this + if (ev.terminal_stream.parser.state == .ground and false) { + for (buf[i..end]) |c| { + switch (terminal.parse_table.table[c][@enumToInt(terminal.Parser.State.ground)].action) { + // Print, call directly. + .print => ev.print(@intCast(u21, c)) catch |err| + log.err("error processing terminal data: {}", .{err}), + + // C0 execute, let our stream handle this one but otherwise + // continue since we're guaranteed to be back in ground. + .execute => ev.terminal_stream.execute(c) catch |err| + log.err("error processing terminal data: {}", .{err}), + + // Otherwise, break out and go the slow path until we're + // back in ground. There is a slight optimization here where + // could try to find the next transition to ground but when + // I implemented that it didn't materially change performance. + else => break, + } + + i += 1; + } + } + + if (i < end) { + ev.terminal_stream.nextSlice(buf[i..end]) catch |err| + log.err("error processing terminal data: {}", .{err}); + } +} + +const StreamHandler = struct { + terminal: *terminal.Terminal, +}; diff --git a/src/termio/Options.zig b/src/termio/Options.zig index 750a07771..a08721fde 100644 --- a/src/termio/Options.zig +++ b/src/termio/Options.zig @@ -11,3 +11,9 @@ screen_size: renderer.ScreenSize, /// The app configuration. config: *const Config, + +/// The render state. The IO implementation can modify anything here. The +/// window thread will setup the initial "terminal" pointer but the IO impl +/// is free to change that if that is useful (i.e. doing some sort of dual +/// terminal implementation.) +renderer_state: *renderer.State, diff --git a/src/termio/Thread.zig b/src/termio/Thread.zig index ac30fb070..9e81aa482 100644 --- a/src/termio/Thread.zig +++ b/src/termio/Thread.zig @@ -113,8 +113,9 @@ pub fn threadMain(self: *Thread) void { fn threadMain_(self: *Thread) !void { // Run our thread start/end callbacks. This allows the implementation // to hook into the event loop as needed. - try self.impl.threadEnter(self.loop); - defer self.impl.threadExit(); + var data = try self.impl.threadEnter(self.loop); + defer data.deinit(); + defer self.impl.threadExit(data); // Set up our async handler to support rendering self.wakeup.setData(self); From d916d56bff76e10b11d542513547d3e4780eb41c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 3 Nov 2022 15:32:29 -0700 Subject: [PATCH 04/22] IO thread stream handler is in, lots of commented TODOs --- src/terminal/stream.zig | 2 +- src/termio/Exec.zig | 345 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 344 insertions(+), 3 deletions(-) diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 343310af4..085d78964 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -440,7 +440,7 @@ pub fn Stream(comptime Handler: type) type { } fn configureCharset( - self: Self, + self: *Self, intermediates: []const u8, set: charsets.Charset, ) !void { diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 0fee6b341..dbb508ece 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -7,6 +7,7 @@ const Allocator = std.mem.Allocator; const termio = @import("../termio.zig"); const Command = @import("../Command.zig"); const Pty = @import("../Pty.zig"); +const SegmentedPool = @import("../segmented_pool.zig").SegmentedPool; const terminal = @import("../terminal/main.zig"); const libuv = @import("libuv"); const renderer = @import("../renderer.zig"); @@ -123,6 +124,7 @@ pub fn threadEnter(self: *Exec, loop: libuv.Loop) !ThreadData { .terminal_stream = .{ .handler = .{ .terminal = &self.terminal, + .renderer_state = self.renderer_state, }, }, }; @@ -148,13 +150,17 @@ const ThreadData = struct { ev: *EventData, pub fn deinit(self: *ThreadData) void { - self.ev.deinit(); + self.ev.deinit(self.alloc); self.alloc.destroy(self.ev); self.* = undefined; } }; const EventData = struct { + // The preallocation size for the write request pool. This should be big + // enough to satisfy most write requests. It must be a power of 2. + const WRITE_REQ_PREALLOC = std.math.pow(usize, 2, 5); + /// This is the arena allocator used for IO read buffers. Since we use /// libuv under the covers, this lets us rarely heap allocate since we're /// usually just reusing buffers from this. @@ -170,9 +176,22 @@ const EventData = struct { /// The data stream is the main IO for the pty. data_stream: libuv.Tty, - pub fn deinit(self: *EventData) void { + /// This is the pool of available (unused) write requests. If you grab + /// one from the pool, you must put it back when you're done! + write_req_pool: SegmentedPool(libuv.WriteReq.T, WRITE_REQ_PREALLOC) = .{}, + + /// The pool of available buffers for writing to the pty. + write_buf_pool: SegmentedPool([64]u8, WRITE_REQ_PREALLOC) = .{}, + + pub fn deinit(self: *EventData, alloc: Allocator) void { self.read_arena.deinit(); + // Clear our write pools. We know we aren't ever going to do + // any more IO since we stop our data stream below so we can just + // drop this. + self.write_req_pool.deinit(alloc); + self.write_buf_pool.deinit(alloc); + // Stop our data stream self.data_stream.readStop(); self.data_stream.close((struct { @@ -268,6 +287,328 @@ fn ttyRead(t: *libuv.Tty, n: isize, buf: []const u8) void { } } +/// This is used as the handler for the terminal.Stream type. This is +/// stateful and is expected to live for the entire lifetime of the terminal. +/// It is NOT VALID to stop a stream handler, create a new one, and use that +/// unless all of the member fields are copied. const StreamHandler = struct { terminal: *terminal.Terminal, + renderer_state: *renderer.State, + + /// Bracketed paste mode + bracketed_paste: bool = false, + + // TODO + fn queueRender(self: *StreamHandler) !void { + _ = self; + } + + // TODO + fn queueWrite(self: *StreamHandler, data: []const u8) !void { + _ = self; + _ = data; + } + + pub fn print(self: *StreamHandler, c: u21) !void { + try self.terminal.print(c); + } + + pub fn bell(self: StreamHandler) !void { + _ = self; + log.info("BELL", .{}); + } + + pub fn backspace(self: *StreamHandler) !void { + self.terminal.backspace(); + } + + pub fn horizontalTab(self: *StreamHandler) !void { + try self.terminal.horizontalTab(); + } + + pub fn linefeed(self: *StreamHandler) !void { + // Small optimization: call index instead of linefeed because they're + // identical and this avoids one layer of function call overhead. + try self.terminal.index(); + } + + pub fn carriageReturn(self: *StreamHandler) !void { + self.terminal.carriageReturn(); + } + + pub fn setCursorLeft(self: *StreamHandler, amount: u16) !void { + self.terminal.cursorLeft(amount); + } + + pub fn setCursorRight(self: *StreamHandler, amount: u16) !void { + self.terminal.cursorRight(amount); + } + + pub fn setCursorDown(self: *StreamHandler, amount: u16) !void { + self.terminal.cursorDown(amount); + } + + pub fn setCursorUp(self: *StreamHandler, amount: u16) !void { + self.terminal.cursorUp(amount); + } + + pub fn setCursorCol(self: *StreamHandler, col: u16) !void { + self.terminal.setCursorColAbsolute(col); + } + + pub fn setCursorRow(self: *StreamHandler, row: u16) !void { + if (self.terminal.modes.origin) { + // TODO + log.err("setCursorRow: implement origin mode", .{}); + unreachable; + } + + self.terminal.setCursorPos(row, self.terminal.screen.cursor.x + 1); + } + + pub fn setCursorPos(self: *StreamHandler, row: u16, col: u16) !void { + self.terminal.setCursorPos(row, col); + } + + pub fn eraseDisplay(self: *StreamHandler, mode: terminal.EraseDisplay) !void { + if (mode == .complete) { + // Whenever we erase the full display, scroll to bottom. + try self.terminal.scrollViewport(.{ .bottom = {} }); + try self.queueRender(); + } + + self.terminal.eraseDisplay(mode); + } + + pub fn eraseLine(self: *StreamHandler, mode: terminal.EraseLine) !void { + self.terminal.eraseLine(mode); + } + + pub fn deleteChars(self: *StreamHandler, count: usize) !void { + try self.terminal.deleteChars(count); + } + + pub fn eraseChars(self: *StreamHandler, count: usize) !void { + self.terminal.eraseChars(count); + } + + pub fn insertLines(self: *StreamHandler, count: usize) !void { + try self.terminal.insertLines(count); + } + + pub fn insertBlanks(self: *StreamHandler, count: usize) !void { + self.terminal.insertBlanks(count); + } + + pub fn deleteLines(self: *StreamHandler, count: usize) !void { + try self.terminal.deleteLines(count); + } + + pub fn reverseIndex(self: *StreamHandler) !void { + try self.terminal.reverseIndex(); + } + + pub fn index(self: *StreamHandler) !void { + try self.terminal.index(); + } + + pub fn nextLine(self: *StreamHandler) !void { + self.terminal.carriageReturn(); + try self.terminal.index(); + } + + pub fn setTopAndBottomMargin(self: *StreamHandler, top: u16, bot: u16) !void { + self.terminal.setScrollingRegion(top, bot); + } + + // pub fn setMode(self: *StreamHandler, mode: terminal.Mode, enabled: bool) !void { + // switch (mode) { + // .reverse_colors => { + // self.terminal.modes.reverse_colors = enabled; + // + // // Schedule a render since we changed colors + // try self.queueRender(); + // }, + // + // .origin => { + // self.terminal.modes.origin = enabled; + // self.terminal.setCursorPos(1, 1); + // }, + // + // .autowrap => { + // self.terminal.modes.autowrap = enabled; + // }, + // + // .cursor_visible => { + // self.renderer_state.cursor.visible = enabled; + // }, + // + // .alt_screen_save_cursor_clear_enter => { + // const opts: terminal.Terminal.AlternateScreenOptions = .{ + // .cursor_save = true, + // .clear_on_enter = true, + // }; + // + // if (enabled) + // self.terminal.alternateScreen(opts) + // else + // self.terminal.primaryScreen(opts); + // + // // Schedule a render since we changed screens + // try self.queueRender(); + // }, + // + // .bracketed_paste => self.bracketed_paste = true, + // + // .enable_mode_3 => { + // // Disable deccolm + // self.terminal.setDeccolmSupported(enabled); + // + // // Force resize back to the window size + // self.terminal.resize(self.alloc, self.grid_size.columns, self.grid_size.rows) catch |err| + // log.err("error updating terminal size: {}", .{err}); + // }, + // + // .@"132_column" => try self.terminal.deccolm( + // self.alloc, + // if (enabled) .@"132_cols" else .@"80_cols", + // ), + // + // .mouse_event_x10 => self.terminal.modes.mouse_event = if (enabled) .x10 else .none, + // .mouse_event_normal => self.terminal.modes.mouse_event = if (enabled) .normal else .none, + // .mouse_event_button => self.terminal.modes.mouse_event = if (enabled) .button else .none, + // .mouse_event_any => self.terminal.modes.mouse_event = if (enabled) .any else .none, + // + // .mouse_format_utf8 => self.terminal.modes.mouse_format = if (enabled) .utf8 else .x10, + // .mouse_format_sgr => self.terminal.modes.mouse_format = if (enabled) .sgr else .x10, + // .mouse_format_urxvt => self.terminal.modes.mouse_format = if (enabled) .urxvt else .x10, + // .mouse_format_sgr_pixels => self.terminal.modes.mouse_format = if (enabled) .sgr_pixels else .x10, + // + // else => if (enabled) log.warn("unimplemented mode: {}", .{mode}), + // } + // } + + pub fn setAttribute(self: *StreamHandler, attr: terminal.Attribute) !void { + switch (attr) { + .unknown => |unk| log.warn("unimplemented or unknown attribute: {any}", .{unk}), + + else => self.terminal.setAttribute(attr) catch |err| + log.warn("error setting attribute {}: {}", .{ attr, err }), + } + } + + pub fn deviceAttributes( + self: *StreamHandler, + req: terminal.DeviceAttributeReq, + params: []const u16, + ) !void { + _ = params; + + switch (req) { + // VT220 + .primary => self.queueWrite("\x1B[?62;c") catch |err| + log.warn("error queueing device attr response: {}", .{err}), + else => log.warn("unimplemented device attributes req: {}", .{req}), + } + } + + pub fn deviceStatusReport( + self: *StreamHandler, + req: terminal.DeviceStatusReq, + ) !void { + switch (req) { + .operating_status => self.queueWrite("\x1B[0n") catch |err| + log.warn("error queueing device attr response: {}", .{err}), + + .cursor_position => { + const pos: struct { + x: usize, + y: usize, + } = if (self.terminal.modes.origin) .{ + // TODO: what do we do if cursor is outside scrolling region? + .x = self.terminal.screen.cursor.x, + .y = self.terminal.screen.cursor.y -| self.terminal.scrolling_region.top, + } else .{ + .x = self.terminal.screen.cursor.x, + .y = self.terminal.screen.cursor.y, + }; + + // Response always is at least 4 chars, so this leaves the + // remainder for the row/column as base-10 numbers. This + // will support a very large terminal. + var buf: [32]u8 = undefined; + const resp = try std.fmt.bufPrint(&buf, "\x1B[{};{}R", .{ + pos.y + 1, + pos.x + 1, + }); + + try self.queueWrite(resp); + }, + + else => log.warn("unimplemented device status req: {}", .{req}), + } + } + + pub fn setCursorStyle( + self: *StreamHandler, + style: terminal.CursorStyle, + ) !void { + self.renderer_state.cursor.style = style; + } + + pub fn decaln(self: *StreamHandler) !void { + try self.terminal.decaln(); + } + + pub fn tabClear(self: *StreamHandler, cmd: terminal.TabClear) !void { + self.terminal.tabClear(cmd); + } + + pub fn tabSet(self: *StreamHandler) !void { + self.terminal.tabSet(); + } + + pub fn saveCursor(self: *StreamHandler) !void { + self.terminal.saveCursor(); + } + + pub fn restoreCursor(self: *StreamHandler) !void { + self.terminal.restoreCursor(); + } + + pub fn enquiry(self: *StreamHandler) !void { + try self.queueWrite(""); + } + + pub fn scrollDown(self: *StreamHandler, count: usize) !void { + try self.terminal.scrollDown(count); + } + + pub fn scrollUp(self: *StreamHandler, count: usize) !void { + try self.terminal.scrollUp(count); + } + + pub fn setActiveStatusDisplay( + self: *StreamHandler, + req: terminal.StatusDisplay, + ) !void { + self.terminal.status_display = req; + } + + pub fn configureCharset( + self: *StreamHandler, + slot: terminal.CharsetSlot, + set: terminal.Charset, + ) !void { + self.terminal.configureCharset(slot, set); + } + + pub fn invokeCharset( + self: *StreamHandler, + active: terminal.CharsetActiveSlot, + slot: terminal.CharsetSlot, + single: bool, + ) !void { + self.terminal.invokeCharset(active, slot, single); + } }; From a8e7c520414be26a1ee424c3fd77561cd5ec8dc7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 3 Nov 2022 15:49:04 -0700 Subject: [PATCH 05/22] IO thread can trigger render and write data --- src/Window.zig | 27 +++++++-------- src/termio/Exec.zig | 76 +++++++++++++++++++++++++++++++++--------- src/termio/Options.zig | 5 +++ 3 files changed, 79 insertions(+), 29 deletions(-) diff --git a/src/Window.zig b/src/Window.zig index b39a6f005..45cb46d57 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -451,19 +451,6 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo var io_arena = std.heap.ArenaAllocator.init(alloc); errdefer io_arena.deinit(); - // Start our IO implementation - var io = try termio.Impl.init(alloc, .{ - .grid_size = grid_size, - .screen_size = screen_size, - .config = config, - .renderer_state = &self.renderer_state, - }); - errdefer io.deinit(alloc); - - // Create the IO thread - var io_thread = try termio.Thread.init(alloc, &self.io); - errdefer io_thread.deinit(); - // The mutex used to protect our renderer state. var mutex = try alloc.create(std.Thread.Mutex); mutex.* = .{}; @@ -478,6 +465,20 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo ); errdefer render_thread.deinit(); + // Start our IO implementation + var io = try termio.Impl.init(alloc, .{ + .grid_size = grid_size, + .screen_size = screen_size, + .config = config, + .renderer_state = &self.renderer_state, + .renderer_wakeup = render_thread.wakeup, + }); + errdefer io.deinit(alloc); + + // Create the IO thread + var io_thread = try termio.Thread.init(alloc, &self.io); + errdefer io_thread.deinit(); + self.* = .{ .alloc = alloc, .alloc_io_arena = io_arena, diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index dbb508ece..1a1a0fd1e 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -32,6 +32,10 @@ terminal_stream: terminal.Stream(StreamHandler), /// The shared render state renderer_state: *renderer.State, +/// A handle to wake up the renderer. This hints to the renderer that that +/// a repaint should happen. +renderer_wakeup: libuv.Async, + /// Initialize the exec implementation. This will also start the child /// process. pub fn init(alloc: Allocator, opts: termio.Options) !Exec { @@ -87,6 +91,7 @@ pub fn init(alloc: Allocator, opts: termio.Options) !Exec { .terminal = term, .terminal_stream = undefined, .renderer_state = opts.renderer_state, + .renderer_wakeup = opts.renderer_wakeup, }; } @@ -120,11 +125,12 @@ pub fn threadEnter(self: *Exec, loop: libuv.Loop) !ThreadData { ev_data_ptr.* = .{ .read_arena = std.heap.ArenaAllocator.init(alloc), .renderer_state = self.renderer_state, + .renderer_wakeup = self.renderer_wakeup, .data_stream = stream, .terminal_stream = .{ .handler = .{ + .ev = ev_data_ptr, .terminal = &self.terminal, - .renderer_state = self.renderer_state, }, }, }; @@ -173,6 +179,10 @@ const EventData = struct { /// The shared render state renderer_state: *renderer.State, + /// A handle to wake up the renderer. This hints to the renderer that that + /// a repaint should happen. + renderer_wakeup: libuv.Async, + /// The data stream is the main IO for the pty. data_stream: libuv.Tty, @@ -201,8 +211,47 @@ const EventData = struct { } }).callback); } + + /// This queues a render operation with the renderer thread. The render + /// isn't guaranteed to happen immediately but it will happen as soon as + /// practical. + inline fn queueRender(self: *EventData) !void { + try self.renderer_wakeup.send(); + } + + /// Queue a write to the pty. + fn queueWrite(self: *EventData, data: []const u8) !void { + // We go through and chunk the data if necessary to fit into + // our cached buffers that we can queue to the stream. + var i: usize = 0; + while (i < data.len) { + const req = try self.write_req_pool.get(); + const buf = try self.write_buf_pool.get(); + const end = @min(data.len, i + buf.len); + std.mem.copy(u8, buf, data[i..end]); + try self.data_stream.write( + .{ .req = req }, + &[1][]u8{buf[0..(end - i)]}, + ttyWrite, + ); + + i = end; + } + } }; +fn ttyWrite(req: *libuv.WriteReq, status: i32) void { + const tty = req.handle(libuv.Tty).?; + const ev = tty.getData(EventData).?; + ev.write_req_pool.put(); + ev.write_buf_pool.put(); + + libuv.convertError(status) catch |err| + log.err("write error: {}", .{err}); + + //log.info("WROTE: {d}", .{status}); +} + fn ttyReadAlloc(t: *libuv.Tty, size: usize) ?[]u8 { const ev = t.getData(EventData) orelse return null; const alloc = ev.read_arena.allocator(); @@ -243,8 +292,7 @@ fn ttyRead(t: *libuv.Tty, n: isize, buf: []const u8) void { // } // Schedule a render - // TODO - //win.queueRender() catch unreachable; + ev.queueRender() catch unreachable; // Process the terminal data. This is an extremely hot part of the // terminal emulator, so we do some abstraction leakage to avoid @@ -257,12 +305,11 @@ fn ttyRead(t: *libuv.Tty, n: isize, buf: []const u8) void { // Empirically, this alone improved throughput of large text output by ~20%. var i: usize = 0; const end = @intCast(usize, n); - // TODO: re-enable this - if (ev.terminal_stream.parser.state == .ground and false) { + if (ev.terminal_stream.parser.state == .ground) { for (buf[i..end]) |c| { switch (terminal.parse_table.table[c][@enumToInt(terminal.Parser.State.ground)].action) { // Print, call directly. - .print => ev.print(@intCast(u21, c)) catch |err| + .print => ev.terminal_stream.handler.print(@intCast(u21, c)) catch |err| log.err("error processing terminal data: {}", .{err}), // C0 execute, let our stream handle this one but otherwise @@ -292,21 +339,18 @@ fn ttyRead(t: *libuv.Tty, n: isize, buf: []const u8) void { /// It is NOT VALID to stop a stream handler, create a new one, and use that /// unless all of the member fields are copied. const StreamHandler = struct { + ev: *EventData, terminal: *terminal.Terminal, - renderer_state: *renderer.State, /// Bracketed paste mode bracketed_paste: bool = false, - // TODO - fn queueRender(self: *StreamHandler) !void { - _ = self; + inline fn queueRender(self: *StreamHandler) !void { + try self.ev.queueRender(); } - // TODO - fn queueWrite(self: *StreamHandler, data: []const u8) !void { - _ = self; - _ = data; + inline fn queueWrite(self: *StreamHandler, data: []const u8) !void { + try self.ev.queueWrite(data); } pub fn print(self: *StreamHandler, c: u21) !void { @@ -440,7 +484,7 @@ const StreamHandler = struct { // }, // // .cursor_visible => { - // self.renderer_state.cursor.visible = enabled; + // self.ev.renderer_state.cursor.visible = enabled; // }, // // .alt_screen_save_cursor_clear_enter => { @@ -553,7 +597,7 @@ const StreamHandler = struct { self: *StreamHandler, style: terminal.CursorStyle, ) !void { - self.renderer_state.cursor.style = style; + self.ev.renderer_state.cursor.style = style; } pub fn decaln(self: *StreamHandler) !void { diff --git a/src/termio/Options.zig b/src/termio/Options.zig index a08721fde..7577181f5 100644 --- a/src/termio/Options.zig +++ b/src/termio/Options.zig @@ -1,5 +1,6 @@ //! The options that are used to configure a terminal IO implementation. +const libuv = @import("libuv"); const renderer = @import("../renderer.zig"); const Config = @import("../config.zig").Config; @@ -17,3 +18,7 @@ config: *const Config, /// is free to change that if that is useful (i.e. doing some sort of dual /// terminal implementation.) renderer_state: *renderer.State, + +/// A handle to wake up the renderer. This hints to the renderer that that +/// a repaint should happen. +renderer_wakeup: libuv.Async, From b100406a6eaaaf6f92b0711141102e03f8f103bc Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 4 Nov 2022 20:27:48 -0700 Subject: [PATCH 06/22] termio: start the thread mailbox, hook up resize --- src/Window.zig | 24 +++++++++++++++----- src/termio.zig | 1 + src/termio/Exec.zig | 35 +++++++++++++++++++++++++++-- src/termio/Thread.zig | 50 +++++++++++++++++++++++++++++++++++++----- src/termio/message.zig | 17 ++++++++++++++ 5 files changed, 114 insertions(+), 13 deletions(-) create mode 100644 src/termio/message.zig diff --git a/src/Window.zig b/src/Window.zig index 45cb46d57..ded771243 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -473,7 +473,7 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo .renderer_state = &self.renderer_state, .renderer_wakeup = render_thread.wakeup, }); - errdefer io.deinit(alloc); + errdefer io.deinit(); // Create the IO thread var io_thread = try termio.Thread.init(alloc, &self.io); @@ -617,7 +617,7 @@ pub fn destroy(self: *Window) void { self.io_thread.deinit(); // Deinitialize our terminal IO - self.io.deinit(self.alloc); + self.io.deinit(); } // Deinitialize the pty. This closes the pty handles. This should @@ -744,13 +744,27 @@ fn sizeCallback(window: glfw.Window, width: i32, height: i32) void { const win = window.getUserPointer(Window) orelse return; - // Resize usually forces a redraw - win.queueRender() catch |err| - log.err("error scheduling render timer in sizeCallback err={}", .{err}); + // TODO: if our screen size didn't change, then we should avoid the + // overhead of inter-thread communication // Recalculate our grid size win.grid_size.update(screen_size, win.renderer.cell_size); + // Mail the IO thread + _ = win.io_thread.mailbox.push(.{ + .resize = .{ + .grid_size = win.grid_size, + .screen_size = screen_size, + }, + }, .{ .forever = {} }); + win.io_thread.wakeup.send() catch {}; + + // TODO: everything below here goes away with the IO thread + + // Resize usually forces a redraw + win.queueRender() catch |err| + log.err("error scheduling render timer in sizeCallback err={}", .{err}); + // Update the size of our pty win.pty.setSize(.{ .ws_row = @intCast(u16, win.grid_size.rows), diff --git a/src/termio.zig b/src/termio.zig index 9d0de3ffb..5464ba1e2 100644 --- a/src/termio.zig +++ b/src/termio.zig @@ -2,6 +2,7 @@ //! for taking the config, spinning up a child process, and handling IO //! with the termianl. +pub const message = @import("termio/message.zig"); pub const Exec = @import("termio/Exec.zig"); pub const Options = @import("termio/Options.zig"); pub const Thread = @import("termio/Thread.zig"); diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 1a1a0fd1e..6a214565b 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -14,6 +14,9 @@ const renderer = @import("../renderer.zig"); const log = std.log.scoped(.io_exec); +/// Allocator +alloc: Allocator, + /// This is the pty fd created for the subcommand. pty: Pty, @@ -86,6 +89,7 @@ pub fn init(alloc: Allocator, opts: termio.Options) !Exec { errdefer term.deinit(alloc); return Exec{ + .alloc = alloc, .pty = pty, .command = cmd, .terminal = term, @@ -95,7 +99,7 @@ pub fn init(alloc: Allocator, opts: termio.Options) !Exec { }; } -pub fn deinit(self: *Exec, alloc: Allocator) void { +pub fn deinit(self: *Exec) void { // Deinitialize the pty. This closes the pty handles. This should // cause a close in the our subprocess so just wait for that. self.pty.deinit(); @@ -103,7 +107,7 @@ pub fn deinit(self: *Exec, alloc: Allocator) void { log.err("error waiting for command to exit: {}", .{err}); // Clean up our other members - self.terminal.deinit(alloc); + self.terminal.deinit(self.alloc); } pub fn threadEnter(self: *Exec, loop: libuv.Loop) !ThreadData { @@ -148,6 +152,33 @@ pub fn threadExit(self: *Exec, data: ThreadData) void { _ = data; } +/// Resize the terminal. +pub fn resize( + self: *Exec, + grid_size: renderer.GridSize, + screen_size: renderer.ScreenSize, +) !void { + // Update the size of our pty + try self.pty.setSize(.{ + .ws_row = @intCast(u16, grid_size.rows), + .ws_col = @intCast(u16, grid_size.columns), + .ws_xpixel = @intCast(u16, screen_size.width), + .ws_ypixel = @intCast(u16, screen_size.height), + }); + + // Enter the critical area that we want to keep small + { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + + // We need to setup our render state to store our new pending size + self.renderer_state.resize_screen = screen_size; + + // Update the size of our terminal state + try self.terminal.resize(self.alloc, grid_size.columns, grid_size.rows); + } +} + const ThreadData = struct { /// Allocator used for the event data alloc: Allocator, diff --git a/src/termio/Thread.zig b/src/termio/Thread.zig index 9e81aa482..12949edcb 100644 --- a/src/termio/Thread.zig +++ b/src/termio/Thread.zig @@ -6,10 +6,16 @@ const std = @import("std"); const builtin = @import("builtin"); const libuv = @import("libuv"); const termio = @import("../termio.zig"); +const BlockingQueue = @import("../blocking_queue.zig").BlockingQueue; const Allocator = std.mem.Allocator; const log = std.log.scoped(.io_thread); +/// The type used for sending messages to the IO thread. For now this is +/// hardcoded with a capacity. We can make this a comptime parameter in +/// the future if we want it configurable. +const Mailbox = BlockingQueue(termio.message.IO, 64); + /// The main event loop for the thread. The user data of this loop /// is always the allocator used to create the loop. This is a convenience /// so that users of the loop always have an allocator. @@ -24,6 +30,10 @@ stop: libuv.Async, /// The underlying IO implementation. impl: *termio.Impl, +/// The mailbox that can be used to send this thread messages. Note +/// this is a blocking queue so if it is full you will get errors (or block). +mailbox: *Mailbox, + /// Initialize the thread. This does not START the thread. This only sets /// up all the internal state necessary prior to starting the thread. It /// is up to the caller to start the thread with the threadMain entrypoint. @@ -60,11 +70,16 @@ pub fn init( } }).callback); + // The mailbox for messaging this thread + var mailbox = try Mailbox.create(alloc); + errdefer mailbox.destroy(alloc); + return Thread{ .loop = loop, .wakeup = wakeup_h, .stop = stop_h, .impl = impl, + .mailbox = mailbox, }; } @@ -95,6 +110,9 @@ pub fn deinit(self: *Thread) void { _ = self.loop.run(.default) catch |err| log.err("error finalizing event loop: {}", .{err}); + // Nothing can possibly access the mailbox anymore, destroy it. + self.mailbox.destroy(alloc); + // Dealloc our allocator copy alloc.destroy(alloc_ptr); @@ -127,13 +145,33 @@ fn threadMain_(self: *Thread) !void { _ = try self.loop.run(.default); } +/// Drain the mailbox, handling all the messages in our terminal implementation. +fn drainMailbox(self: *Thread) !void { + // This holds the mailbox lock for the duration of the drain. The + // expectation is that all our message handlers will be non-blocking + // ENOUGH to not mess up throughput on producers. + var drain = self.mailbox.drain(); + defer drain.deinit(); + + while (drain.next()) |message| { + log.debug("mailbox message={}", .{message}); + switch (message) { + .resize => |v| try self.impl.resize(v.grid_size, v.screen_size), + } + } +} + fn wakeupCallback(h: *libuv.Async) void { - _ = h; - // const t = h.getData(Thread) orelse { - // // This shouldn't happen so we log it. - // log.warn("render callback fired without data set", .{}); - // return; - // }; + const t = h.getData(Thread) orelse { + // This shouldn't happen so we log it. + log.warn("wakeup callback fired without data set", .{}); + return; + }; + + // When we wake up, we check the mailbox. Mailbox producers should + // wake up our thread after publishing. + t.drainMailbox() catch |err| + log.err("error draining mailbox err={}", .{err}); } fn stopCallback(h: *libuv.Async) void { diff --git a/src/termio/message.zig b/src/termio/message.zig new file mode 100644 index 000000000..b7f1b6cdc --- /dev/null +++ b/src/termio/message.zig @@ -0,0 +1,17 @@ +const renderer = @import("../renderer.zig"); +const terminal = @import("../terminal/main.zig"); + +/// The messages that can be sent to an IO thread. +pub const IO = union(enum) { + /// Resize the window. + resize: struct { + grid_size: renderer.GridSize, + screen_size: renderer.ScreenSize, + }, + + // /// Clear the selection + // clear_selection: void, + // + // /// Scroll the viewport + // scroll_viewport: terminal.Terminal.ScrollViewport, +}; From f1d2df1a54892b17d37484880346c1dd68e0f2fe Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 4 Nov 2022 20:33:44 -0700 Subject: [PATCH 07/22] fully hook up resize --- src/termio/Exec.zig | 143 ++++++++++++++++++++++++-------------------- 1 file changed, 77 insertions(+), 66 deletions(-) diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 6a214565b..6abace770 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -39,6 +39,9 @@ renderer_state: *renderer.State, /// a repaint should happen. renderer_wakeup: libuv.Async, +/// The cached grid size whenever a resize is called. +grid_size: renderer.GridSize, + /// Initialize the exec implementation. This will also start the child /// process. pub fn init(alloc: Allocator, opts: termio.Options) !Exec { @@ -96,6 +99,7 @@ pub fn init(alloc: Allocator, opts: termio.Options) !Exec { .terminal_stream = undefined, .renderer_state = opts.renderer_state, .renderer_wakeup = opts.renderer_wakeup, + .grid_size = opts.grid_size, }; } @@ -133,8 +137,10 @@ pub fn threadEnter(self: *Exec, loop: libuv.Loop) !ThreadData { .data_stream = stream, .terminal_stream = .{ .handler = .{ + .alloc = self.alloc, .ev = ev_data_ptr, .terminal = &self.terminal, + .grid_size = &self.grid_size, }, }, }; @@ -166,6 +172,9 @@ pub fn resize( .ws_ypixel = @intCast(u16, screen_size.height), }); + // Update our cached grid size + self.grid_size = grid_size; + // Enter the critical area that we want to keep small { self.renderer_state.mutex.lock(); @@ -371,6 +380,8 @@ fn ttyRead(t: *libuv.Tty, n: isize, buf: []const u8) void { /// unless all of the member fields are copied. const StreamHandler = struct { ev: *EventData, + alloc: Allocator, + grid_size: *renderer.GridSize, terminal: *terminal.Terminal, /// Bracketed paste mode @@ -496,72 +507,72 @@ const StreamHandler = struct { self.terminal.setScrollingRegion(top, bot); } - // pub fn setMode(self: *StreamHandler, mode: terminal.Mode, enabled: bool) !void { - // switch (mode) { - // .reverse_colors => { - // self.terminal.modes.reverse_colors = enabled; - // - // // Schedule a render since we changed colors - // try self.queueRender(); - // }, - // - // .origin => { - // self.terminal.modes.origin = enabled; - // self.terminal.setCursorPos(1, 1); - // }, - // - // .autowrap => { - // self.terminal.modes.autowrap = enabled; - // }, - // - // .cursor_visible => { - // self.ev.renderer_state.cursor.visible = enabled; - // }, - // - // .alt_screen_save_cursor_clear_enter => { - // const opts: terminal.Terminal.AlternateScreenOptions = .{ - // .cursor_save = true, - // .clear_on_enter = true, - // }; - // - // if (enabled) - // self.terminal.alternateScreen(opts) - // else - // self.terminal.primaryScreen(opts); - // - // // Schedule a render since we changed screens - // try self.queueRender(); - // }, - // - // .bracketed_paste => self.bracketed_paste = true, - // - // .enable_mode_3 => { - // // Disable deccolm - // self.terminal.setDeccolmSupported(enabled); - // - // // Force resize back to the window size - // self.terminal.resize(self.alloc, self.grid_size.columns, self.grid_size.rows) catch |err| - // log.err("error updating terminal size: {}", .{err}); - // }, - // - // .@"132_column" => try self.terminal.deccolm( - // self.alloc, - // if (enabled) .@"132_cols" else .@"80_cols", - // ), - // - // .mouse_event_x10 => self.terminal.modes.mouse_event = if (enabled) .x10 else .none, - // .mouse_event_normal => self.terminal.modes.mouse_event = if (enabled) .normal else .none, - // .mouse_event_button => self.terminal.modes.mouse_event = if (enabled) .button else .none, - // .mouse_event_any => self.terminal.modes.mouse_event = if (enabled) .any else .none, - // - // .mouse_format_utf8 => self.terminal.modes.mouse_format = if (enabled) .utf8 else .x10, - // .mouse_format_sgr => self.terminal.modes.mouse_format = if (enabled) .sgr else .x10, - // .mouse_format_urxvt => self.terminal.modes.mouse_format = if (enabled) .urxvt else .x10, - // .mouse_format_sgr_pixels => self.terminal.modes.mouse_format = if (enabled) .sgr_pixels else .x10, - // - // else => if (enabled) log.warn("unimplemented mode: {}", .{mode}), - // } - // } + pub fn setMode(self: *StreamHandler, mode: terminal.Mode, enabled: bool) !void { + switch (mode) { + .reverse_colors => { + self.terminal.modes.reverse_colors = enabled; + + // Schedule a render since we changed colors + try self.queueRender(); + }, + + .origin => { + self.terminal.modes.origin = enabled; + self.terminal.setCursorPos(1, 1); + }, + + .autowrap => { + self.terminal.modes.autowrap = enabled; + }, + + .cursor_visible => { + self.ev.renderer_state.cursor.visible = enabled; + }, + + .alt_screen_save_cursor_clear_enter => { + const opts: terminal.Terminal.AlternateScreenOptions = .{ + .cursor_save = true, + .clear_on_enter = true, + }; + + if (enabled) + self.terminal.alternateScreen(opts) + else + self.terminal.primaryScreen(opts); + + // Schedule a render since we changed screens + try self.queueRender(); + }, + + .bracketed_paste => self.bracketed_paste = true, + + .enable_mode_3 => { + // Disable deccolm + self.terminal.setDeccolmSupported(enabled); + + // Force resize back to the window size + self.terminal.resize(self.alloc, self.grid_size.columns, self.grid_size.rows) catch |err| + log.err("error updating terminal size: {}", .{err}); + }, + + .@"132_column" => try self.terminal.deccolm( + self.alloc, + if (enabled) .@"132_cols" else .@"80_cols", + ), + + .mouse_event_x10 => self.terminal.modes.mouse_event = if (enabled) .x10 else .none, + .mouse_event_normal => self.terminal.modes.mouse_event = if (enabled) .normal else .none, + .mouse_event_button => self.terminal.modes.mouse_event = if (enabled) .button else .none, + .mouse_event_any => self.terminal.modes.mouse_event = if (enabled) .any else .none, + + .mouse_format_utf8 => self.terminal.modes.mouse_format = if (enabled) .utf8 else .x10, + .mouse_format_sgr => self.terminal.modes.mouse_format = if (enabled) .sgr else .x10, + .mouse_format_urxvt => self.terminal.modes.mouse_format = if (enabled) .urxvt else .x10, + .mouse_format_sgr_pixels => self.terminal.modes.mouse_format = if (enabled) .sgr_pixels else .x10, + + else => if (enabled) log.warn("unimplemented mode: {}", .{mode}), + } + } pub fn setAttribute(self: *StreamHandler, attr: terminal.Attribute) !void { switch (attr) { From 1a7b9f7465302ed55511b79535208960807cc6f1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 4 Nov 2022 20:47:01 -0700 Subject: [PATCH 08/22] termio: clear selection --- src/Window.zig | 8 +++++++- src/termio/Exec.zig | 11 +++++++++++ src/termio/Thread.zig | 25 +++++++++++++++++++------ src/termio/message.zig | 4 ++-- 4 files changed, 39 insertions(+), 9 deletions(-) diff --git a/src/Window.zig b/src/Window.zig index ded771243..3930f21ba 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -810,7 +810,13 @@ fn charCallback(window: glfw.Window, codepoint: u21) void { return; } - // Anytime is character is created, we have to clear the selection + // Anytime a char is created, we have to clear the selection if there is one. + _ = win.io_thread.mailbox.push(.{ + .clear_selection = {}, + }, .{ .forever = {} }); + + // TODO: the stuff below goes away with IO thread + if (win.terminal.selection != null) { win.terminal.selection = null; win.queueRender() catch |err| diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 6abace770..8762cc56f 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -188,6 +188,17 @@ pub fn resize( } } +pub fn clearSelection(self: *Exec) !void { + // We don't need a lock to read because nothing else can possibly write + // as we're looking at this. + if (self.terminal.selection != null) { + // We need to lock so we can write because other things might be reading. + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + self.terminal.selection = null; + } +} + const ThreadData = struct { /// Allocator used for the event data alloc: Allocator, diff --git a/src/termio/Thread.zig b/src/termio/Thread.zig index 12949edcb..d0dd76cfc 100644 --- a/src/termio/Thread.zig +++ b/src/termio/Thread.zig @@ -150,15 +150,28 @@ fn drainMailbox(self: *Thread) !void { // This holds the mailbox lock for the duration of the drain. The // expectation is that all our message handlers will be non-blocking // ENOUGH to not mess up throughput on producers. - var drain = self.mailbox.drain(); - defer drain.deinit(); + var redraw: bool = false; + { + var drain = self.mailbox.drain(); + defer drain.deinit(); - while (drain.next()) |message| { - log.debug("mailbox message={}", .{message}); - switch (message) { - .resize => |v| try self.impl.resize(v.grid_size, v.screen_size), + while (drain.next()) |message| { + // If we have a message we always redraw + redraw = true; + + log.debug("mailbox message={}", .{message}); + switch (message) { + .resize => |v| try self.impl.resize(v.grid_size, v.screen_size), + .clear_selection => try self.impl.clearSelection(), + } } } + + // Trigger a redraw after we've drained so we don't waste cyces + // messaging a redraw. + if (redraw) { + try self.impl.renderer_wakeup.send(); + } } fn wakeupCallback(h: *libuv.Async) void { diff --git a/src/termio/message.zig b/src/termio/message.zig index b7f1b6cdc..966e5ccb6 100644 --- a/src/termio/message.zig +++ b/src/termio/message.zig @@ -9,8 +9,8 @@ pub const IO = union(enum) { screen_size: renderer.ScreenSize, }, - // /// Clear the selection - // clear_selection: void, + /// Clear the selection + clear_selection: void, // // /// Scroll the viewport // scroll_viewport: terminal.Terminal.ScrollViewport, From 989046a06cda2b915e487b4bc63f8bb062eafcd3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 4 Nov 2022 22:13:37 -0700 Subject: [PATCH 09/22] More IO events --- src/Window.zig | 18 ++++++++++++++++++ src/main.zig | 1 + src/termio/Exec.zig | 26 ++++++++++++++++++++++++-- src/termio/Thread.zig | 2 ++ src/termio/message.zig | 21 ++++++++++++++++++--- 5 files changed, 63 insertions(+), 5 deletions(-) diff --git a/src/Window.zig b/src/Window.zig index 3930f21ba..a9aafa5ca 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -815,6 +815,24 @@ fn charCallback(window: glfw.Window, codepoint: u21) void { .clear_selection = {}, }, .{ .forever = {} }); + // Scroll to the bottom + _ = win.io_thread.mailbox.push(.{ + .scroll_viewport = .{ .bottom = {} }, + }, .{ .forever = {} }); + + // Write the char to the pty + var data: termio.message.IO.SmallWriteArray = undefined; + data[0] = @intCast(u8, codepoint); + _ = win.io_thread.mailbox.push(.{ + .small_write = .{ + .data = data, + .len = 1, + }, + }, .{ .forever = {} }); + + // After sending all our messages we have to notify our IO thread + win.io_thread.wakeup.send() catch {}; + // TODO: the stuff below goes away with IO thread if (win.terminal.selection != null) { diff --git a/src/main.zig b/src/main.zig index 1f92efbe1..a985416cb 100644 --- a/src/main.zig +++ b/src/main.zig @@ -196,6 +196,7 @@ test { _ = @import("font/main.zig"); _ = @import("renderer.zig"); _ = @import("terminal/Terminal.zig"); + _ = @import("termio.zig"); _ = @import("input.zig"); // Libraries diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 8762cc56f..47675b16b 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -3,6 +3,7 @@ pub const Exec = @This(); const std = @import("std"); const builtin = @import("builtin"); +const assert = std.debug.assert; const Allocator = std.mem.Allocator; const termio = @import("../termio.zig"); const Command = @import("../Command.zig"); @@ -42,6 +43,9 @@ renderer_wakeup: libuv.Async, /// The cached grid size whenever a resize is called. grid_size: renderer.GridSize, +/// The data associated with the currently running thread. +data: ?*EventData, + /// Initialize the exec implementation. This will also start the child /// process. pub fn init(alloc: Allocator, opts: termio.Options) !Exec { @@ -100,6 +104,7 @@ pub fn init(alloc: Allocator, opts: termio.Options) !Exec { .renderer_state = opts.renderer_state, .renderer_wakeup = opts.renderer_wakeup, .grid_size = opts.grid_size, + .data = null, }; } @@ -115,6 +120,8 @@ pub fn deinit(self: *Exec) void { } pub fn threadEnter(self: *Exec, loop: libuv.Loop) !ThreadData { + assert(self.data == null); + // Get a copy to our allocator const alloc_ptr = loop.getData(Allocator).?; const alloc = alloc_ptr.*; @@ -146,7 +153,10 @@ pub fn threadEnter(self: *Exec, loop: libuv.Loop) !ThreadData { }; errdefer ev_data_ptr.deinit(); - // Return our data + // Store our data so our callbacks can access it + self.data = ev_data_ptr; + + // Return our thread data return ThreadData{ .alloc = alloc, .ev = ev_data_ptr, @@ -154,8 +164,9 @@ pub fn threadEnter(self: *Exec, loop: libuv.Loop) !ThreadData { } pub fn threadExit(self: *Exec, data: ThreadData) void { - _ = self; _ = data; + + self.data = null; } /// Resize the terminal. @@ -199,6 +210,17 @@ pub fn clearSelection(self: *Exec) !void { } } +pub fn scrollViewport(self: *Exec, scroll: terminal.Terminal.ScrollViewport) !void { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + + try self.terminal.scrollViewport(scroll); +} + +pub inline fn queueWrite(self: *Exec, data: []const u8) !void { + try self.data.?.queueWrite(data); +} + const ThreadData = struct { /// Allocator used for the event data alloc: Allocator, diff --git a/src/termio/Thread.zig b/src/termio/Thread.zig index d0dd76cfc..c05f3dbb8 100644 --- a/src/termio/Thread.zig +++ b/src/termio/Thread.zig @@ -163,6 +163,8 @@ fn drainMailbox(self: *Thread) !void { switch (message) { .resize => |v| try self.impl.resize(v.grid_size, v.screen_size), .clear_selection => try self.impl.clearSelection(), + .scroll_viewport => |v| try self.impl.scrollViewport(v), + .small_write => |v| try self.impl.queueWrite(v.data[0..v.len]), } } } diff --git a/src/termio/message.zig b/src/termio/message.zig index 966e5ccb6..b80ae2e4f 100644 --- a/src/termio/message.zig +++ b/src/termio/message.zig @@ -1,8 +1,11 @@ +const std = @import("std"); const renderer = @import("../renderer.zig"); const terminal = @import("../terminal/main.zig"); /// The messages that can be sent to an IO thread. pub const IO = union(enum) { + pub const SmallWriteArray = [22]u8; + /// Resize the window. resize: struct { grid_size: renderer.GridSize, @@ -11,7 +14,19 @@ pub const IO = union(enum) { /// Clear the selection clear_selection: void, - // - // /// Scroll the viewport - // scroll_viewport: terminal.Terminal.ScrollViewport, + + /// Scroll the viewport + scroll_viewport: terminal.Terminal.ScrollViewport, + + /// Write where the data fits in the union. + small_write: struct { + data: [22]u8, + len: u8, + }, }; + +test { + // Ensure we don't grow our IO message size without explicitly wanting to. + const testing = std.testing; + try testing.expectEqual(@as(usize, 24), @sizeOf(IO)); +} From 5cb6ebe34d4df575836e92ca2447359062497f57 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 4 Nov 2022 22:32:06 -0700 Subject: [PATCH 10/22] Actually, we'll manage selection and viewports on the windowing thread --- src/Window.zig | 67 +++++++++++++++++++++++++++--------------- src/termio/Exec.zig | 18 ------------ src/termio/Thread.zig | 2 -- src/termio/message.zig | 6 ---- 4 files changed, 43 insertions(+), 50 deletions(-) diff --git a/src/Window.zig b/src/Window.zig index a9aafa5ca..aad2f9116 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -810,17 +810,25 @@ fn charCallback(window: glfw.Window, codepoint: u21) void { return; } - // Anytime a char is created, we have to clear the selection if there is one. - _ = win.io_thread.mailbox.push(.{ - .clear_selection = {}, - }, .{ .forever = {} }); + // Critical area + { + win.renderer_state.mutex.lock(); + defer win.renderer_state.mutex.unlock(); - // Scroll to the bottom - _ = win.io_thread.mailbox.push(.{ - .scroll_viewport = .{ .bottom = {} }, - }, .{ .forever = {} }); + // Clear the selction if we have one. + if (win.terminal.selection != null) { + win.terminal.selection = null; + win.queueRender() catch |err| + log.err("error scheduling render in charCallback err={}", .{err}); + } - // Write the char to the pty + // We want to scroll to the bottom + // TODO: detect if we're at the bottom to avoid the render call here. + win.terminal.scrollViewport(.{ .bottom = {} }) catch |err| + log.err("error scrolling viewport err={}", .{err}); + } + + // Ask our IO thread to write the data var data: termio.message.IO.SmallWriteArray = undefined; data[0] = @intCast(u8, codepoint); _ = win.io_thread.mailbox.push(.{ @@ -835,16 +843,8 @@ fn charCallback(window: glfw.Window, codepoint: u21) void { // TODO: the stuff below goes away with IO thread - if (win.terminal.selection != null) { - win.terminal.selection = null; - win.queueRender() catch |err| - log.err("error scheduling render in charCallback err={}", .{err}); - } - // We want to scroll to the bottom // TODO: detect if we're at the bottom to avoid the render call here. - win.terminal.scrollViewport(.{ .bottom = {} }) catch |err| - log.err("error scrolling viewport err={}", .{err}); win.queueRender() catch |err| log.err("error scheduling render in charCallback err={}", .{err}); @@ -959,8 +959,13 @@ fn keyCallback( }, .copy_to_clipboard => { - if (win.terminal.selection) |sel| { - var buf = win.terminal.screen.selectionString(win.alloc, sel) catch |err| { + // We can read from the renderer state without holding + // the lock because only we will write to this field. + if (win.renderer_state.terminal.selection) |sel| { + var buf = win.renderer_state.terminal.screen.selectionString( + win.alloc, + sel, + ) catch |err| { log.err("error reading selection string err={}", .{err}); return; }; @@ -1129,12 +1134,17 @@ fn scrollCallback(window: glfw.Window, xoff: f64, yoff: f64) void { const sign: isize = if (yoff > 0) -1 else 1; const delta: isize = sign * @max(@divFloor(win.grid_size.rows, 15), 1); log.info("scroll: delta={}", .{delta}); - win.terminal.scrollViewport(.{ .delta = delta }) catch |err| - log.err("error scrolling viewport err={}", .{err}); - // Schedule render since scrolling usually does something. - // TODO(perf): we can only schedule render if we know scrolling - // did something + // Modify our viewport, this requires a lock since it affects rendering + { + win.renderer_state.mutex.lock(); + defer win.renderer_state.mutex.unlock(); + + win.terminal.scrollViewport(.{ .delta = delta }) catch |err| + log.err("error scrolling viewport err={}", .{err}); + } + + // TODO: drop after IO thread win.queueRender() catch unreachable; } @@ -1389,6 +1399,9 @@ fn mouseButtonCallback( // Selection is always cleared if (win.terminal.selection != null) { + // We only need the lock to write since only we'll ever write. + win.renderer_state.mutex.lock(); + defer win.renderer_state.mutex.unlock(); win.terminal.selection = null; win.queueRender() catch |err| log.err("error scheduling render in mouseButtinCallback err={}", .{err}); @@ -1462,6 +1475,12 @@ fn cursorPosCallback( // often. // TODO: unit test this, this logic sucks + // At some point below we likely write selection state so we just grab + // a lock. This is not optimal efficiency but its not a common operation + // so its okay. + win.renderer_state.mutex.lock(); + defer win.renderer_state.mutex.unlock(); + // If we were selecting, and we switched directions, then we restart // calculations because it forces us to reconsider if the first cell is // selected. diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 47675b16b..dfbedfb63 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -199,24 +199,6 @@ pub fn resize( } } -pub fn clearSelection(self: *Exec) !void { - // We don't need a lock to read because nothing else can possibly write - // as we're looking at this. - if (self.terminal.selection != null) { - // We need to lock so we can write because other things might be reading. - self.renderer_state.mutex.lock(); - defer self.renderer_state.mutex.unlock(); - self.terminal.selection = null; - } -} - -pub fn scrollViewport(self: *Exec, scroll: terminal.Terminal.ScrollViewport) !void { - self.renderer_state.mutex.lock(); - defer self.renderer_state.mutex.unlock(); - - try self.terminal.scrollViewport(scroll); -} - pub inline fn queueWrite(self: *Exec, data: []const u8) !void { try self.data.?.queueWrite(data); } diff --git a/src/termio/Thread.zig b/src/termio/Thread.zig index c05f3dbb8..0338617f6 100644 --- a/src/termio/Thread.zig +++ b/src/termio/Thread.zig @@ -162,8 +162,6 @@ fn drainMailbox(self: *Thread) !void { log.debug("mailbox message={}", .{message}); switch (message) { .resize => |v| try self.impl.resize(v.grid_size, v.screen_size), - .clear_selection => try self.impl.clearSelection(), - .scroll_viewport => |v| try self.impl.scrollViewport(v), .small_write => |v| try self.impl.queueWrite(v.data[0..v.len]), } } diff --git a/src/termio/message.zig b/src/termio/message.zig index b80ae2e4f..ba3d7be3b 100644 --- a/src/termio/message.zig +++ b/src/termio/message.zig @@ -12,12 +12,6 @@ pub const IO = union(enum) { screen_size: renderer.ScreenSize, }, - /// Clear the selection - clear_selection: void, - - /// Scroll the viewport - scroll_viewport: terminal.Terminal.ScrollViewport, - /// Write where the data fits in the union. small_write: struct { data: [22]u8, From f2d9475d5d9ebc21f5a95f1ba1ac32ecaa357116 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 5 Nov 2022 09:39:56 -0700 Subject: [PATCH 11/22] Switch over to the IO thread. A messy commit! --- src/Window.zig | 781 ++++++++--------------------------------- src/termio/Thread.zig | 3 +- src/termio/message.zig | 34 +- 3 files changed, 176 insertions(+), 642 deletions(-) diff --git a/src/Window.zig b/src/Window.zig index aad2f9116..eb76d6ec3 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -68,12 +68,6 @@ renderer_thread: renderer.Thread, /// The actual thread renderer_thr: std.Thread, -/// The underlying pty for this window. -pty: Pty, - -/// The command we're running for our tty. -command: Command, - /// Mouse state. mouse: Mouse, @@ -82,24 +76,12 @@ io: termio.Impl, io_thread: termio.Thread, io_thr: std.Thread, -/// The terminal emulator internal state. This is the abstract "terminal" -/// that manages input, grid updating, etc. and is renderer-agnostic. It -/// just stores internal state about a grid. This is connected back to -/// a renderer. -terminal: terminal.Terminal, - -/// The stream parser. -terminal_stream: terminal.Stream(*Window), - /// Cursor state. terminal_cursor: Cursor, /// The dimensions of the grid in rows and columns. grid_size: renderer.GridSize, -/// The reader/writer stream for the pty. -pty_stream: libuv.Tty, - /// This is the pool of available (unused) write requests. If you grab /// one from the pool, you must put it back when you're done! write_req_pool: SegmentedPool(libuv.WriteReq.T, WRITE_REQ_PREALLOC) = .{}, @@ -383,56 +365,6 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo .height = @floatToInt(u32, renderer_impl.cell_size.height * 4), }, .{ .width = null, .height = null }); - // Create our pty - var pty = try Pty.open(.{ - .ws_row = @intCast(u16, grid_size.rows), - .ws_col = @intCast(u16, grid_size.columns), - .ws_xpixel = @intCast(u16, window_size.width), - .ws_ypixel = @intCast(u16, window_size.height), - }); - errdefer pty.deinit(); - - // Create our child process - const path = (try Command.expandPath(alloc, config.command orelse "sh")) orelse - return error.CommandNotFound; - defer alloc.free(path); - - var env = try std.process.getEnvMap(alloc); - defer env.deinit(); - try env.put("TERM", "xterm-256color"); - - var cmd: Command = .{ - .path = path, - .args = &[_][]const u8{path}, - .env = &env, - .cwd = config.@"working-directory", - .pre_exec = (struct { - fn callback(c: *Command) void { - const p = c.getData(Pty) orelse unreachable; - p.childPreExec() catch |err| - log.err("error initializing child: {}", .{err}); - } - }).callback, - .data = &pty, - }; - // note: can't set these in the struct initializer because it - // sets the handle to "0". Probably a stage1 zig bug. - cmd.stdin = std.fs.File{ .handle = pty.slave }; - cmd.stdout = cmd.stdin; - cmd.stderr = cmd.stdin; - try cmd.start(alloc); - log.debug("started subcommand path={s} pid={?}", .{ path, cmd.pid }); - - // Read data - var stream = try libuv.Tty.init(alloc, loop, pty.master); - errdefer stream.deinit(alloc); - stream.setData(self); - try stream.readStart(ttyReadAlloc, ttyRead); - - // Create our terminal - var term = try terminal.Terminal.init(alloc, grid_size.columns, grid_size.rows); - errdefer term.deinit(alloc); - // Setup a timer for blinking the cursor var timer = try libuv.Timer.init(alloc, loop); errdefer timer.deinit(alloc); @@ -497,21 +429,16 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo .visible = true, .blink = false, }, - .terminal = &self.terminal, + .terminal = &self.io.terminal, .devmode = if (!DevMode.enabled) null else &DevMode.instance, }, .renderer_thr = undefined, - .pty = pty, - .command = cmd, .mouse = .{}, .io = io, .io_thread = io_thread, .io_thr = undefined, - .terminal = term, - .terminal_stream = .{ .handler = self }, .terminal_cursor = .{ .timer = timer }, .grid_size = grid_size, - .pty_stream = stream, .config = config, .bg_r = @intToFloat(f32, config.background.r) / 255.0, .bg_g = @intToFloat(f32, config.background.g) / 255.0, @@ -620,13 +547,6 @@ pub fn destroy(self: *Window) void { self.io.deinit(); } - // Deinitialize the pty. This closes the pty handles. This should - // cause a close in the our subprocess so just wait for that. - self.pty.deinit(); - _ = self.command.wait() catch |err| - log.err("error waiting for command to exit: {}", .{err}); - - self.terminal.deinit(self.alloc); self.window.destroy(); self.terminal_cursor.timer.close((struct { @@ -636,21 +556,6 @@ pub fn destroy(self: *Window) void { } }).callback); - // We have to dealloc our window in the close callback because - // we can't free some of the memory associated with the window - // until the stream is closed. - self.pty_stream.readStop(); - self.pty_stream.close((struct { - fn callback(t: *libuv.Tty) void { - const win = t.getData(Window).?; - const alloc = win.alloc; - t.deinit(alloc); - win.write_req_pool.deinit(alloc); - win.write_buf_pool.deinit(alloc); - win.alloc.destroy(win); - } - }).callback); - // We can destroy the cursor right away. glfw will just revert any // windows using it to the default. self.cursor.destroy(); @@ -661,32 +566,16 @@ pub fn destroy(self: *Window) void { self.alloc_io_arena.deinit(); self.alloc.destroy(self.renderer_state.mutex); + + self.write_req_pool.deinit(self.alloc); + self.write_buf_pool.deinit(self.alloc); + self.alloc.destroy(self); } pub fn shouldClose(self: Window) bool { return self.window.shouldClose(); } -/// Queue a write to the pty. -fn queueWrite(self: *Window, data: []const u8) !void { - // We go through and chunk the data if necessary to fit into - // our cached buffers that we can queue to the stream. - var i: usize = 0; - while (i < data.len) { - const req = try self.write_req_pool.get(); - const buf = try self.write_buf_pool.get(); - const end = @min(data.len, i + buf.len); - std.mem.copy(u8, buf, data[i..end]); - try self.pty_stream.write( - .{ .req = req }, - &[1][]u8{buf[0..(end - i)]}, - ttyWrite, - ); - - i = end; - } -} - /// This queues a render operation with the renderer thread. The render /// isn't guaranteed to happen immediately but it will happen as soon as /// practical. @@ -758,33 +647,6 @@ fn sizeCallback(window: glfw.Window, width: i32, height: i32) void { }, }, .{ .forever = {} }); win.io_thread.wakeup.send() catch {}; - - // TODO: everything below here goes away with the IO thread - - // Resize usually forces a redraw - win.queueRender() catch |err| - log.err("error scheduling render timer in sizeCallback err={}", .{err}); - - // Update the size of our pty - win.pty.setSize(.{ - .ws_row = @intCast(u16, win.grid_size.rows), - .ws_col = @intCast(u16, win.grid_size.columns), - .ws_xpixel = @intCast(u16, width), - .ws_ypixel = @intCast(u16, height), - }) catch |err| log.err("error updating pty screen size err={}", .{err}); - - // Enter the critical area that we want to keep small - { - win.renderer_state.mutex.lock(); - defer win.renderer_state.mutex.unlock(); - - // We need to setup our render state to store our new pending size - win.renderer_state.resize_screen = screen_size; - - // Update the size of our terminal state - win.terminal.resize(win.alloc, win.grid_size.columns, win.grid_size.rows) catch |err| - log.err("error updating terminal size: {}", .{err}); - } } fn charCallback(window: glfw.Window, codepoint: u21) void { @@ -816,23 +678,23 @@ fn charCallback(window: glfw.Window, codepoint: u21) void { defer win.renderer_state.mutex.unlock(); // Clear the selction if we have one. - if (win.terminal.selection != null) { - win.terminal.selection = null; + if (win.io.terminal.selection != null) { + win.io.terminal.selection = null; win.queueRender() catch |err| log.err("error scheduling render in charCallback err={}", .{err}); } // We want to scroll to the bottom // TODO: detect if we're at the bottom to avoid the render call here. - win.terminal.scrollViewport(.{ .bottom = {} }) catch |err| + win.io.terminal.scrollViewport(.{ .bottom = {} }) catch |err| log.err("error scrolling viewport err={}", .{err}); } // Ask our IO thread to write the data - var data: termio.message.IO.SmallWriteArray = undefined; + var data: termio.message.WriteReq.Small.Array = undefined; data[0] = @intCast(u8, codepoint); _ = win.io_thread.mailbox.push(.{ - .small_write = .{ + .write_small = .{ .data = data, .len = 1, }, @@ -840,17 +702,6 @@ fn charCallback(window: glfw.Window, codepoint: u21) void { // After sending all our messages we have to notify our IO thread win.io_thread.wakeup.send() catch {}; - - // TODO: the stuff below goes away with IO thread - - // We want to scroll to the bottom - // TODO: detect if we're at the bottom to avoid the render call here. - win.queueRender() catch |err| - log.err("error scheduling render in charCallback err={}", .{err}); - - // Write the character to the pty - win.queueWrite(&[1]u8{@intCast(u8, codepoint)}) catch |err| - log.err("error queueing write in charCallback err={}", .{err}); } fn keyCallback( @@ -952,10 +803,13 @@ fn keyCallback( .ignore => {}, .csi => |data| { - win.queueWrite("\x1B[") catch |err| - log.err("error queueing write in keyCallback err={}", .{err}); - win.queueWrite(data) catch |err| - log.warn("error pasting clipboard: {}", .{err}); + _ = win.io_thread.mailbox.push(.{ + .write_stable = "\x1B[", + }, .{ .forever = {} }); + _ = win.io_thread.mailbox.push(.{ + .write_stable = data, + }, .{ .forever = {} }); + win.io_thread.wakeup.send() catch {}; }, .copy_to_clipboard => { @@ -985,12 +839,24 @@ fn keyCallback( }; if (data.len > 0) { - if (win.bracketed_paste) win.queueWrite("\x1B[200~") catch |err| - log.err("error queueing write in keyCallback err={}", .{err}); - win.queueWrite(data) catch |err| - log.warn("error pasting clipboard: {}", .{err}); - if (win.bracketed_paste) win.queueWrite("\x1B[201~") catch |err| - log.err("error queueing write in keyCallback err={}", .{err}); + if (win.bracketed_paste) { + _ = win.io_thread.mailbox.push(.{ + .write_stable = "\x1B[200~", + }, .{ .forever = {} }); + } + + // TODO: NO! ALLOCATE THIS + _ = win.io_thread.mailbox.push(.{ + .write_stable = data, + }, .{ .forever = {} }); + + if (win.bracketed_paste) { + _ = win.io_thread.mailbox.push(.{ + .write_stable = "\x1B[201~", + }, .{ .forever = {} }); + } + + win.io_thread.wakeup.send() catch {}; } }, @@ -1058,8 +924,18 @@ fn keyCallback( }); }; if (char > 0) { - win.queueWrite(&[1]u8{char}) catch |err| - log.err("error queueing write in keyCallback err={}", .{err}); + // Ask our IO thread to write the data + var data: termio.message.WriteReq.Small.Array = undefined; + data[0] = @intCast(u8, char); + _ = win.io_thread.mailbox.push(.{ + .write_small = .{ + .data = data, + .len = 1, + }, + }, .{ .forever = {} }); + + // After sending all our messages we have to notify our IO thread + win.io_thread.wakeup.send() catch {}; } } } @@ -1140,11 +1016,10 @@ fn scrollCallback(window: glfw.Window, xoff: f64, yoff: f64) void { win.renderer_state.mutex.lock(); defer win.renderer_state.mutex.unlock(); - win.terminal.scrollViewport(.{ .delta = delta }) catch |err| + win.io.terminal.scrollViewport(.{ .delta = delta }) catch |err| log.err("error scrolling viewport err={}", .{err}); } - // TODO: drop after IO thread win.queueRender() catch unreachable; } @@ -1161,8 +1036,14 @@ fn mouseReport( // TODO: posToViewport currently clamps to the window boundary, // do we want to not report mouse events at all outside the window? + // Everything in here requires reading/writing mouse state so we + // acquire a big lock. Mouse events are rare so this should be okay + // but we can make this more fine-grained later. + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + // Depending on the event, we may do nothing at all. - switch (self.terminal.modes.mouse_event) { + switch (self.io.terminal.modes.mouse_event) { .none => return, // X10 only reports clicks with mouse button 1, 2, 3. We verify @@ -1188,8 +1069,8 @@ fn mouseReport( const viewport_point = self.posToViewport(pos.xpos, pos.ypos); // For button events, we only report if we moved cells - if (self.terminal.modes.mouse_event == .button or - self.terminal.modes.mouse_event == .any) + if (self.io.terminal.modes.mouse_event == .button or + self.io.terminal.modes.mouse_event == .any) { if (self.mouse.event_point.x == viewport_point.x and self.mouse.event_point.y == viewport_point.y) return; @@ -1206,7 +1087,7 @@ fn mouseReport( if (button == null) { // Null button means motion without a button pressed acc = 3; - } else if (action == .release and self.terminal.modes.mouse_format != .sgr) { + } else if (action == .release and self.io.terminal.modes.mouse_format != .sgr) { // Release is 3. It is NOT 3 in SGR mode because SGR can tell // the application what button was released. acc = 3; @@ -1222,7 +1103,7 @@ fn mouseReport( } // X10 doesn't have modifiers - if (self.terminal.modes.mouse_event != .x10) { + if (self.io.terminal.modes.mouse_event != .x10) { if (mods.shift) acc += 4; if (mods.super) acc += 8; if (mods.ctrl) acc += 16; @@ -1234,7 +1115,7 @@ fn mouseReport( break :code acc; }; - switch (self.terminal.modes.mouse_format) { + switch (self.io.terminal.modes.mouse_format) { .x10 => { if (viewport_point.x > 222 or viewport_point.y > 222) { log.info("X10 mouse format can only encode X/Y up to 223", .{}); @@ -1242,29 +1123,47 @@ fn mouseReport( } // + 1 below is because our x/y is 0-indexed and proto wants 1 - var buf = [_]u8{ '\x1b', '[', 'M', 0, 0, 0 }; - buf[3] = 32 + button_code; - buf[4] = 32 + @intCast(u8, viewport_point.x) + 1; - buf[5] = 32 + @intCast(u8, viewport_point.y) + 1; - try self.queueWrite(&buf); + var data: termio.message.WriteReq.Small.Array = undefined; + assert(data.len >= 5); + data[0] = '\x1b'; + data[1] = '['; + data[2] = 'M'; + data[3] = 32 + button_code; + data[4] = 32 + @intCast(u8, viewport_point.x) + 1; + data[5] = 32 + @intCast(u8, viewport_point.y) + 1; + + // Ask our IO thread to write the data + _ = self.io_thread.mailbox.push(.{ + .write_small = .{ + .data = data, + .len = 5, + }, + }, .{ .forever = {} }); }, .utf8 => { // Maximum of 12 because at most we have 2 fully UTF-8 encoded chars - var buf: [12]u8 = undefined; - buf[0] = '\x1b'; - buf[1] = '['; - buf[2] = 'M'; + var data: termio.message.WriteReq.Small.Array = undefined; + assert(data.len >= 12); + data[0] = '\x1b'; + data[1] = '['; + data[2] = 'M'; // The button code will always fit in a single u8 - buf[3] = 32 + button_code; + data[3] = 32 + button_code; // UTF-8 encode the x/y var i: usize = 4; - i += try std.unicode.utf8Encode(@intCast(u21, 32 + viewport_point.x + 1), buf[i..]); - i += try std.unicode.utf8Encode(@intCast(u21, 32 + viewport_point.y + 1), buf[i..]); + i += try std.unicode.utf8Encode(@intCast(u21, 32 + viewport_point.x + 1), data[i..]); + i += try std.unicode.utf8Encode(@intCast(u21, 32 + viewport_point.y + 1), data[i..]); - try self.queueWrite(buf[0..i]); + // Ask our IO thread to write the data + _ = self.io_thread.mailbox.push(.{ + .write_small = .{ + .data = data, + .len = @intCast(u8, i), + }, + }, .{ .forever = {} }); }, .sgr => { @@ -1273,28 +1172,40 @@ fn mouseReport( // Response always is at least 4 chars, so this leaves the // remainder for numbers which are very large... - var buf: [32]u8 = undefined; - const resp = try std.fmt.bufPrint(&buf, "\x1B[<{d};{d};{d}{c}", .{ + var data: termio.message.WriteReq.Small.Array = undefined; + const resp = try std.fmt.bufPrint(&data, "\x1B[<{d};{d};{d}{c}", .{ button_code, viewport_point.x + 1, viewport_point.y + 1, final, }); - try self.queueWrite(resp); + // Ask our IO thread to write the data + _ = self.io_thread.mailbox.push(.{ + .write_small = .{ + .data = data, + .len = @intCast(u8, resp.len), + }, + }, .{ .forever = {} }); }, .urxvt => { // Response always is at least 4 chars, so this leaves the // remainder for numbers which are very large... - var buf: [32]u8 = undefined; - const resp = try std.fmt.bufPrint(&buf, "\x1B[{d};{d};{d}M", .{ + var data: termio.message.WriteReq.Small.Array = undefined; + const resp = try std.fmt.bufPrint(&data, "\x1B[{d};{d};{d}M", .{ 32 + button_code, viewport_point.x + 1, viewport_point.y + 1, }); - try self.queueWrite(resp); + // Ask our IO thread to write the data + _ = self.io_thread.mailbox.push(.{ + .write_small = .{ + .data = data, + .len = @intCast(u8, resp.len), + }, + }, .{ .forever = {} }); }, .sgr_pixels => { @@ -1303,17 +1214,26 @@ fn mouseReport( // Response always is at least 4 chars, so this leaves the // remainder for numbers which are very large... - var buf: [32]u8 = undefined; - const resp = try std.fmt.bufPrint(&buf, "\x1B[<{d};{d};{d}{c}", .{ + var data: termio.message.WriteReq.Small.Array = undefined; + const resp = try std.fmt.bufPrint(&data, "\x1B[<{d};{d};{d}{c}", .{ button_code, pos.xpos, pos.ypos, final, }); - try self.queueWrite(resp); + // Ask our IO thread to write the data + _ = self.io_thread.mailbox.push(.{ + .write_small = .{ + .data = data, + .len = @intCast(u8, resp.len), + }, + }, .{ .forever = {} }); }, } + + // After sending all our messages we have to notify our IO thread + try self.io_thread.wakeup.send(); } fn mouseButtonCallback( @@ -1360,8 +1280,11 @@ fn mouseButtonCallback( win.mouse.click_state[@enumToInt(button)] = action; win.mouse.mods = @bitCast(input.Mods, mods); + win.renderer_state.mutex.lock(); + defer win.renderer_state.mutex.unlock(); + // Report mouse events if enabled - if (win.terminal.modes.mouse_event != .none) { + if (win.io.terminal.modes.mouse_event != .none) { const pos = window.getCursorPos() catch |err| { log.err("error reading cursor position: {}", .{err}); return; @@ -1393,16 +1316,13 @@ fn mouseButtonCallback( // Store it const point = win.posToViewport(pos.xpos, pos.ypos); - win.mouse.left_click_point = point.toScreen(&win.terminal.screen); + win.mouse.left_click_point = point.toScreen(&win.io.terminal.screen); win.mouse.left_click_xpos = pos.xpos; win.mouse.left_click_ypos = pos.ypos; // Selection is always cleared - if (win.terminal.selection != null) { - // We only need the lock to write since only we'll ever write. - win.renderer_state.mutex.lock(); - defer win.renderer_state.mutex.unlock(); - win.terminal.selection = null; + if (win.io.terminal.selection != null) { + win.io.terminal.selection = null; win.queueRender() catch |err| log.err("error scheduling render in mouseButtinCallback err={}", .{err}); } @@ -1431,8 +1351,12 @@ fn cursorPosCallback( } else |_| {} } + // We are reading/writing state for the remainder + win.renderer_state.mutex.lock(); + defer win.renderer_state.mutex.unlock(); + // Do a mouse report - if (win.terminal.modes.mouse_event != .none) { + if (win.io.terminal.modes.mouse_event != .none) { // We use the first mouse button we find pressed in order to report // since the spec (afaict) does not say... const button: ?input.MouseButton = button: for (win.mouse.click_state) |state, i| { @@ -1467,7 +1391,7 @@ fn cursorPosCallback( // Convert to points const viewport_point = win.posToViewport(xpos, ypos); - const screen_point = viewport_point.toScreen(&win.terminal.screen); + const screen_point = viewport_point.toScreen(&win.io.terminal.screen); // NOTE(mitchellh): This logic super sucks. There has to be an easier way // to calculate this, but this is good for a v1. Selection isn't THAT @@ -1475,22 +1399,16 @@ fn cursorPosCallback( // often. // TODO: unit test this, this logic sucks - // At some point below we likely write selection state so we just grab - // a lock. This is not optimal efficiency but its not a common operation - // so its okay. - win.renderer_state.mutex.lock(); - defer win.renderer_state.mutex.unlock(); - // If we were selecting, and we switched directions, then we restart // calculations because it forces us to reconsider if the first cell is // selected. - if (win.terminal.selection) |sel| { + if (win.io.terminal.selection) |sel| { const reset: bool = if (sel.end.before(sel.start)) sel.start.before(screen_point) else screen_point.before(sel.start); - if (reset) win.terminal.selection = null; + if (reset) win.io.terminal.selection = null; } // Our logic for determing if the starting cell is selected: @@ -1522,7 +1440,7 @@ fn cursorPosCallback( else cell_xpos < cell_xboundary; - win.terminal.selection = if (selected) .{ + win.io.terminal.selection = if (selected) .{ .start = screen_point, .end = screen_point, } else null; @@ -1532,7 +1450,7 @@ fn cursorPosCallback( // If this is a different cell and we haven't started selection, // we determine the starting cell first. - if (win.terminal.selection == null) { + if (win.io.terminal.selection == null) { // - If we're moving to a point before the start, then we select // the starting cell if we started after the boundary, else // we start selection of the prior cell. @@ -1546,7 +1464,7 @@ fn cursorPosCallback( .y = click_point.y, .x = click_point.x - 1, } else terminal.point.ScreenPoint{ - .x = win.terminal.screen.cols - 1, + .x = win.io.terminal.screen.cols - 1, .y = click_point.y -| 1, }; } @@ -1554,7 +1472,7 @@ fn cursorPosCallback( if (win.mouse.left_click_xpos < cell_xboundary) { break :start click_point; } else { - break :start if (click_point.x < win.terminal.screen.cols - 1) terminal.point.ScreenPoint{ + break :start if (click_point.x < win.io.terminal.screen.cols - 1) terminal.point.ScreenPoint{ .y = click_point.y, .x = click_point.x + 1, } else terminal.point.ScreenPoint{ @@ -1564,7 +1482,7 @@ fn cursorPosCallback( } }; - win.terminal.selection = .{ .start = start, .end = screen_point }; + win.io.terminal.selection = .{ .start = start, .end = screen_point }; return; } @@ -1573,8 +1491,8 @@ fn cursorPosCallback( // We moved! Set the selection end point. The start point should be // set earlier. - assert(win.terminal.selection != null); - win.terminal.selection.?.end = screen_point; + assert(win.io.terminal.selection != null); + win.io.terminal.selection.?.end = screen_point; } fn posToViewport(self: Window, xpos: f64, ypos: f64) terminal.point.Viewport { @@ -1589,13 +1507,13 @@ fn posToViewport(self: Window, xpos: f64, ypos: f64) terminal.point.Viewport { // Can be off the screen if the user drags it out, so max // it out on our available columns - break :x @min(x, self.terminal.cols - 1); + break :x @min(x, self.io.terminal.cols - 1); }, .y = if (ypos < 0) 0 else y: { const cell_height = @floatCast(f64, self.renderer.cell_size.height); const y = @floatToInt(usize, ypos / cell_height); - break :y @min(y, self.terminal.rows - 1); + break :y @min(y, self.io.terminal.rows - 1); }, }; } @@ -1619,415 +1537,6 @@ fn cursorTimerCallback(t: *libuv.Timer) void { win.queueRender() catch unreachable; } -fn ttyReadAlloc(t: *libuv.Tty, size: usize) ?[]u8 { - const tracy = trace(@src()); - defer tracy.end(); - - const win = t.getData(Window) orelse return null; - const alloc = win.alloc_io_arena.allocator(); - return alloc.alloc(u8, size) catch null; -} - -fn ttyRead(t: *libuv.Tty, n: isize, buf: []const u8) void { - const tracy = trace(@src()); - tracy.color(0xEAEA7F); // yellow-ish - defer tracy.end(); - - const win = t.getData(Window).?; - defer { - const alloc = win.alloc_io_arena.allocator(); - alloc.free(buf); - } - - // log.info("DATA: {d}", .{n}); - // log.info("DATA: {any}", .{buf[0..@intCast(usize, n)]}); - - // First check for errors in the case n is less than 0. - libuv.convertError(@intCast(i32, n)) catch |err| { - switch (err) { - // ignore EOF because it should end the process. - libuv.Error.EOF => {}, - else => log.err("read error: {}", .{err}), - } - - return; - }; - - // We are modifying terminal state from here on out - win.renderer_state.mutex.lock(); - defer win.renderer_state.mutex.unlock(); - - // Whenever a character is typed, we ensure the cursor is in the - // non-blink state so it is rendered if visible. - win.renderer_state.cursor.blink = false; - if (win.terminal_cursor.timer.isActive() catch false) { - _ = win.terminal_cursor.timer.again() catch null; - } - - // Schedule a render - win.queueRender() catch unreachable; - - // Process the terminal data. This is an extremely hot part of the - // terminal emulator, so we do some abstraction leakage to avoid - // function calls and unnecessary logic. - // - // The ground state is the only state that we can see and print/execute - // ASCII, so we only execute this hot path if we're already in the ground - // state. - // - // Empirically, this alone improved throughput of large text output by ~20%. - var i: usize = 0; - const end = @intCast(usize, n); - if (win.terminal_stream.parser.state == .ground) { - for (buf[i..end]) |c| { - switch (terminal.parse_table.table[c][@enumToInt(terminal.Parser.State.ground)].action) { - // Print, call directly. - .print => win.print(@intCast(u21, c)) catch |err| - log.err("error processing terminal data: {}", .{err}), - - // C0 execute, let our stream handle this one but otherwise - // continue since we're guaranteed to be back in ground. - .execute => win.terminal_stream.execute(c) catch |err| - log.err("error processing terminal data: {}", .{err}), - - // Otherwise, break out and go the slow path until we're - // back in ground. There is a slight optimization here where - // could try to find the next transition to ground but when - // I implemented that it didn't materially change performance. - else => break, - } - - i += 1; - } - } - - if (i < end) { - win.terminal_stream.nextSlice(buf[i..end]) catch |err| - log.err("error processing terminal data: {}", .{err}); - } -} - -fn ttyWrite(req: *libuv.WriteReq, status: i32) void { - const tracy = trace(@src()); - defer tracy.end(); - - const tty = req.handle(libuv.Tty).?; - const win = tty.getData(Window).?; - win.write_req_pool.put(); - win.write_buf_pool.put(); - - libuv.convertError(status) catch |err| - log.err("write error: {}", .{err}); - - //log.info("WROTE: {d}", .{status}); -} - -//------------------------------------------------------------------- -// Stream Callbacks - -pub fn print(self: *Window, c: u21) !void { - try self.terminal.print(c); -} - -pub fn bell(self: Window) !void { - _ = self; - log.info("BELL", .{}); -} - -pub fn backspace(self: *Window) !void { - self.terminal.backspace(); -} - -pub fn horizontalTab(self: *Window) !void { - try self.terminal.horizontalTab(); -} - -pub fn linefeed(self: *Window) !void { - // Small optimization: call index instead of linefeed because they're - // identical and this avoids one layer of function call overhead. - try self.terminal.index(); -} - -pub fn carriageReturn(self: *Window) !void { - self.terminal.carriageReturn(); -} - -pub fn setCursorLeft(self: *Window, amount: u16) !void { - self.terminal.cursorLeft(amount); -} - -pub fn setCursorRight(self: *Window, amount: u16) !void { - self.terminal.cursorRight(amount); -} - -pub fn setCursorDown(self: *Window, amount: u16) !void { - self.terminal.cursorDown(amount); -} - -pub fn setCursorUp(self: *Window, amount: u16) !void { - self.terminal.cursorUp(amount); -} - -pub fn setCursorCol(self: *Window, col: u16) !void { - self.terminal.setCursorColAbsolute(col); -} - -pub fn setCursorRow(self: *Window, row: u16) !void { - if (self.terminal.modes.origin) { - // TODO - log.err("setCursorRow: implement origin mode", .{}); - unreachable; - } - - self.terminal.setCursorPos(row, self.terminal.screen.cursor.x + 1); -} - -pub fn setCursorPos(self: *Window, row: u16, col: u16) !void { - self.terminal.setCursorPos(row, col); -} - -pub fn eraseDisplay(self: *Window, mode: terminal.EraseDisplay) !void { - if (mode == .complete) { - // Whenever we erase the full display, scroll to bottom. - try self.terminal.scrollViewport(.{ .bottom = {} }); - try self.queueRender(); - } - - self.terminal.eraseDisplay(mode); -} - -pub fn eraseLine(self: *Window, mode: terminal.EraseLine) !void { - self.terminal.eraseLine(mode); -} - -pub fn deleteChars(self: *Window, count: usize) !void { - try self.terminal.deleteChars(count); -} - -pub fn eraseChars(self: *Window, count: usize) !void { - self.terminal.eraseChars(count); -} - -pub fn insertLines(self: *Window, count: usize) !void { - try self.terminal.insertLines(count); -} - -pub fn insertBlanks(self: *Window, count: usize) !void { - self.terminal.insertBlanks(count); -} - -pub fn deleteLines(self: *Window, count: usize) !void { - try self.terminal.deleteLines(count); -} - -pub fn reverseIndex(self: *Window) !void { - try self.terminal.reverseIndex(); -} - -pub fn index(self: *Window) !void { - try self.terminal.index(); -} - -pub fn nextLine(self: *Window) !void { - self.terminal.carriageReturn(); - try self.terminal.index(); -} - -pub fn setTopAndBottomMargin(self: *Window, top: u16, bot: u16) !void { - self.terminal.setScrollingRegion(top, bot); -} - -pub fn setMode(self: *Window, mode: terminal.Mode, enabled: bool) !void { - switch (mode) { - .reverse_colors => { - self.terminal.modes.reverse_colors = enabled; - - // Schedule a render since we changed colors - try self.queueRender(); - }, - - .origin => { - self.terminal.modes.origin = enabled; - self.terminal.setCursorPos(1, 1); - }, - - .autowrap => { - self.terminal.modes.autowrap = enabled; - }, - - .cursor_visible => { - self.renderer_state.cursor.visible = enabled; - }, - - .alt_screen_save_cursor_clear_enter => { - const opts: terminal.Terminal.AlternateScreenOptions = .{ - .cursor_save = true, - .clear_on_enter = true, - }; - - if (enabled) - self.terminal.alternateScreen(opts) - else - self.terminal.primaryScreen(opts); - - // Schedule a render since we changed screens - try self.queueRender(); - }, - - .bracketed_paste => self.bracketed_paste = true, - - .enable_mode_3 => { - // Disable deccolm - self.terminal.setDeccolmSupported(enabled); - - // Force resize back to the window size - self.terminal.resize(self.alloc, self.grid_size.columns, self.grid_size.rows) catch |err| - log.err("error updating terminal size: {}", .{err}); - }, - - .@"132_column" => try self.terminal.deccolm( - self.alloc, - if (enabled) .@"132_cols" else .@"80_cols", - ), - - .mouse_event_x10 => self.terminal.modes.mouse_event = if (enabled) .x10 else .none, - .mouse_event_normal => self.terminal.modes.mouse_event = if (enabled) .normal else .none, - .mouse_event_button => self.terminal.modes.mouse_event = if (enabled) .button else .none, - .mouse_event_any => self.terminal.modes.mouse_event = if (enabled) .any else .none, - - .mouse_format_utf8 => self.terminal.modes.mouse_format = if (enabled) .utf8 else .x10, - .mouse_format_sgr => self.terminal.modes.mouse_format = if (enabled) .sgr else .x10, - .mouse_format_urxvt => self.terminal.modes.mouse_format = if (enabled) .urxvt else .x10, - .mouse_format_sgr_pixels => self.terminal.modes.mouse_format = if (enabled) .sgr_pixels else .x10, - - else => if (enabled) log.warn("unimplemented mode: {}", .{mode}), - } -} - -pub fn setAttribute(self: *Window, attr: terminal.Attribute) !void { - switch (attr) { - .unknown => |unk| log.warn("unimplemented or unknown attribute: {any}", .{unk}), - - else => self.terminal.setAttribute(attr) catch |err| - log.warn("error setting attribute {}: {}", .{ attr, err }), - } -} - -pub fn deviceAttributes( - self: *Window, - req: terminal.DeviceAttributeReq, - params: []const u16, -) !void { - _ = params; - - switch (req) { - // VT220 - .primary => self.queueWrite("\x1B[?62;c") catch |err| - log.warn("error queueing device attr response: {}", .{err}), - else => log.warn("unimplemented device attributes req: {}", .{req}), - } -} - -pub fn deviceStatusReport( - self: *Window, - req: terminal.DeviceStatusReq, -) !void { - switch (req) { - .operating_status => self.queueWrite("\x1B[0n") catch |err| - log.warn("error queueing device attr response: {}", .{err}), - - .cursor_position => { - const pos: struct { - x: usize, - y: usize, - } = if (self.terminal.modes.origin) .{ - // TODO: what do we do if cursor is outside scrolling region? - .x = self.terminal.screen.cursor.x, - .y = self.terminal.screen.cursor.y -| self.terminal.scrolling_region.top, - } else .{ - .x = self.terminal.screen.cursor.x, - .y = self.terminal.screen.cursor.y, - }; - - // Response always is at least 4 chars, so this leaves the - // remainder for the row/column as base-10 numbers. This - // will support a very large terminal. - var buf: [32]u8 = undefined; - const resp = try std.fmt.bufPrint(&buf, "\x1B[{};{}R", .{ - pos.y + 1, - pos.x + 1, - }); - - try self.queueWrite(resp); - }, - - else => log.warn("unimplemented device status req: {}", .{req}), - } -} - -pub fn setCursorStyle( - self: *Window, - style: terminal.CursorStyle, -) !void { - self.renderer_state.cursor.style = style; -} - -pub fn decaln(self: *Window) !void { - try self.terminal.decaln(); -} - -pub fn tabClear(self: *Window, cmd: terminal.TabClear) !void { - self.terminal.tabClear(cmd); -} - -pub fn tabSet(self: *Window) !void { - self.terminal.tabSet(); -} - -pub fn saveCursor(self: *Window) !void { - self.terminal.saveCursor(); -} - -pub fn restoreCursor(self: *Window) !void { - self.terminal.restoreCursor(); -} - -pub fn enquiry(self: *Window) !void { - try self.queueWrite(""); -} - -pub fn scrollDown(self: *Window, count: usize) !void { - try self.terminal.scrollDown(count); -} - -pub fn scrollUp(self: *Window, count: usize) !void { - try self.terminal.scrollUp(count); -} - -pub fn setActiveStatusDisplay( - self: *Window, - req: terminal.StatusDisplay, -) !void { - self.terminal.status_display = req; -} - -pub fn configureCharset( - self: *Window, - slot: terminal.CharsetSlot, - set: terminal.Charset, -) !void { - self.terminal.configureCharset(slot, set); -} - -pub fn invokeCharset( - self: *Window, - active: terminal.CharsetActiveSlot, - slot: terminal.CharsetSlot, - single: bool, -) !void { - self.terminal.invokeCharset(active, slot, single); -} - const face_ttf = @embedFile("font/res/FiraCode-Regular.ttf"); const face_bold_ttf = @embedFile("font/res/FiraCode-Bold.ttf"); const face_emoji_ttf = @embedFile("font/res/NotoColorEmoji.ttf"); diff --git a/src/termio/Thread.zig b/src/termio/Thread.zig index 0338617f6..7201101d7 100644 --- a/src/termio/Thread.zig +++ b/src/termio/Thread.zig @@ -162,7 +162,8 @@ fn drainMailbox(self: *Thread) !void { log.debug("mailbox message={}", .{message}); switch (message) { .resize => |v| try self.impl.resize(v.grid_size, v.screen_size), - .small_write => |v| try self.impl.queueWrite(v.data[0..v.len]), + .write_small => |v| try self.impl.queueWrite(v.data[0..v.len]), + .write_stable => |v| try self.impl.queueWrite(v), } } } diff --git a/src/termio/message.zig b/src/termio/message.zig index ba3d7be3b..e6ce2b287 100644 --- a/src/termio/message.zig +++ b/src/termio/message.zig @@ -1,11 +1,10 @@ const std = @import("std"); +const Allocator = std.mem.Allocator; const renderer = @import("../renderer.zig"); const terminal = @import("../terminal/main.zig"); /// The messages that can be sent to an IO thread. pub const IO = union(enum) { - pub const SmallWriteArray = [22]u8; - /// Resize the window. resize: struct { grid_size: renderer.GridSize, @@ -13,10 +12,35 @@ pub const IO = union(enum) { }, /// Write where the data fits in the union. - small_write: struct { - data: [22]u8, + write_small: WriteReq.Small, + + /// Write where the data pointer is stable. + write_stable: []const u8, +}; + +/// Represents a write request. +pub const WriteReq = union(enum) { + pub const Small = struct { + pub const Array = [22]u8; + data: Array, len: u8, - }, + }; + + pub const Alloc = struct { + alloc: Allocator, + data: []u8, + }; + + /// A small write where the data fits into this union size. + small: Small, + + /// A stable pointer so we can just pass the slice directly through. + /// This is useful i.e. for const data. + stable: []const u8, + + /// Allocated and must be freed with the provided allocator. This + /// should be rarely used. + alloc: Alloc, }; test { From 57b4c73bb272457d782143fcf141b51c20fb4d0d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 5 Nov 2022 17:00:45 -0700 Subject: [PATCH 12/22] remove unused fields on Window --- src/Window.zig | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/src/Window.zig b/src/Window.zig index eb76d6ec3..abad724c0 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -32,16 +32,11 @@ const glfwNative = glfw.Native(.{ const log = std.log.scoped(.window); -// The preallocation size for the write request pool. This should be big -// enough to satisfy most write requests. It must be a power of 2. -const WRITE_REQ_PREALLOC = std.math.pow(usize, 2, 5); - // The renderer implementation to use. const Renderer = renderer.Renderer; /// Allocator alloc: Allocator, -alloc_io_arena: std.heap.ArenaAllocator, /// The font structures font_lib: font.Library, @@ -82,22 +77,9 @@ terminal_cursor: Cursor, /// The dimensions of the grid in rows and columns. grid_size: renderer.GridSize, -/// This is the pool of available (unused) write requests. If you grab -/// one from the pool, you must put it back when you're done! -write_req_pool: SegmentedPool(libuv.WriteReq.T, WRITE_REQ_PREALLOC) = .{}, - -/// The pool of available buffers for writing to the pty. -write_buf_pool: SegmentedPool([64]u8, WRITE_REQ_PREALLOC) = .{}, - /// The app configuration config: *const Config, -/// Window background color -bg_r: f32, -bg_g: f32, -bg_b: f32, -bg_a: f32, - /// Bracketed paste mode bracketed_paste: bool = false, @@ -377,12 +359,6 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo errdefer cursor.destroy(); try window.setCursor(cursor); - // Create our IO allocator arena. Libuv appears to guarantee (in code, - // not in docs) that read_alloc is called directly before a read so - // we can use an arena to make allocation faster. - var io_arena = std.heap.ArenaAllocator.init(alloc); - errdefer io_arena.deinit(); - // The mutex used to protect our renderer state. var mutex = try alloc.create(std.Thread.Mutex); mutex.* = .{}; @@ -413,7 +389,6 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo self.* = .{ .alloc = alloc, - .alloc_io_arena = io_arena, .font_lib = font_lib, .font_group = font_group, .window = window, @@ -440,10 +415,6 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo .terminal_cursor = .{ .timer = timer }, .grid_size = grid_size, .config = config, - .bg_r = @intToFloat(f32, config.background.r) / 255.0, - .bg_g = @intToFloat(f32, config.background.g) / 255.0, - .bg_b = @intToFloat(f32, config.background.b) / 255.0, - .bg_a = 1.0, .imgui_ctx = if (!DevMode.enabled) void else try imgui.Context.create(), }; @@ -564,11 +535,8 @@ pub fn destroy(self: *Window) void { self.font_lib.deinit(); self.alloc.destroy(self.font_group); - self.alloc_io_arena.deinit(); self.alloc.destroy(self.renderer_state.mutex); - self.write_req_pool.deinit(self.alloc); - self.write_buf_pool.deinit(self.alloc); self.alloc.destroy(self); } From a05b08fdc79b0c2062c14ad87d3cb9fba4529659 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 5 Nov 2022 17:04:24 -0700 Subject: [PATCH 13/22] move bracketed paste to terminal state --- src/Window.zig | 13 ++++++++----- src/terminal/Terminal.zig | 2 ++ src/termio/Exec.zig | 5 +---- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Window.zig b/src/Window.zig index abad724c0..3ecfc9d8d 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -80,9 +80,6 @@ grid_size: renderer.GridSize, /// The app configuration config: *const Config, -/// Bracketed paste mode -bracketed_paste: bool = false, - /// Set to true for a single GLFW key/char callback cycle to cause the /// char callback to ignore. GLFW seems to always do key followed by char /// callbacks so we abuse that here. This is to solve an issue where commands @@ -807,7 +804,13 @@ fn keyCallback( }; if (data.len > 0) { - if (win.bracketed_paste) { + const bracketed = bracketed: { + win.renderer_state.mutex.lock(); + defer win.renderer_state.mutex.unlock(); + break :bracketed win.renderer_state.terminal.modes.bracketed_paste; + }; + + if (bracketed) { _ = win.io_thread.mailbox.push(.{ .write_stable = "\x1B[200~", }, .{ .forever = {} }); @@ -818,7 +821,7 @@ fn keyCallback( .write_stable = data, }, .{ .forever = {} }); - if (win.bracketed_paste) { + if (bracketed) { _ = win.io_thread.mailbox.push(.{ .write_stable = "\x1B[201~", }, .{ .forever = {} }); diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 5de275707..246acce4b 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -75,6 +75,8 @@ modes: packed struct { mouse_event: MouseEvents = .none, mouse_format: MouseFormat = .x10, + bracketed_paste: bool = false, // 2004 + test { // We have this here so that we explicitly fail when we change the // size of modes. The size of modes is NOT particularly important, diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index dfbedfb63..0ecbd6032 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -399,9 +399,6 @@ const StreamHandler = struct { grid_size: *renderer.GridSize, terminal: *terminal.Terminal, - /// Bracketed paste mode - bracketed_paste: bool = false, - inline fn queueRender(self: *StreamHandler) !void { try self.ev.queueRender(); } @@ -559,7 +556,7 @@ const StreamHandler = struct { try self.queueRender(); }, - .bracketed_paste => self.bracketed_paste = true, + .bracketed_paste => self.terminal.modes.bracketed_paste = enabled, .enable_mode_3 => { // Disable deccolm From 90061016df1bd020584cebc7c4f1919824312836 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 5 Nov 2022 17:05:24 -0700 Subject: [PATCH 14/22] field rename --- src/Window.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Window.zig b/src/Window.zig index 3ecfc9d8d..e10ea35c2 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -780,8 +780,8 @@ fn keyCallback( .copy_to_clipboard => { // We can read from the renderer state without holding // the lock because only we will write to this field. - if (win.renderer_state.terminal.selection) |sel| { - var buf = win.renderer_state.terminal.screen.selectionString( + if (win.io.terminal.selection) |sel| { + var buf = win.io.terminal.screen.selectionString( win.alloc, sel, ) catch |err| { @@ -807,7 +807,7 @@ fn keyCallback( const bracketed = bracketed: { win.renderer_state.mutex.lock(); defer win.renderer_state.mutex.unlock(); - break :bracketed win.renderer_state.terminal.modes.bracketed_paste; + break :bracketed win.io.terminal.modes.bracketed_paste; }; if (bracketed) { From 8652b2170e202dfc9addd3eef86cb850ae4949ba Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 5 Nov 2022 17:12:37 -0700 Subject: [PATCH 15/22] fix deadlock with mouse reports --- src/Window.zig | 35 +++++++++++++++-------------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/src/Window.zig b/src/Window.zig index e10ea35c2..817ee2a10 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -961,19 +961,6 @@ fn scrollCallback(window: glfw.Window, xoff: f64, yoff: f64) void { } else |_| {} } - // If we're scrolling up or down, then send a mouse event - if (yoff != 0) { - const pos = window.getCursorPos() catch |err| { - log.err("error reading cursor position: {}", .{err}); - return; - }; - - win.mouseReport(if (yoff < 0) .five else .four, .press, win.mouse.mods, pos) catch |err| { - log.err("error reporting mouse event: {}", .{err}); - return; - }; - } - //log.info("SCROLL: {} {}", .{ xoff, yoff }); _ = xoff; @@ -982,13 +969,27 @@ fn scrollCallback(window: glfw.Window, xoff: f64, yoff: f64) void { const delta: isize = sign * @max(@divFloor(win.grid_size.rows, 15), 1); log.info("scroll: delta={}", .{delta}); - // Modify our viewport, this requires a lock since it affects rendering { win.renderer_state.mutex.lock(); defer win.renderer_state.mutex.unlock(); + // Modify our viewport, this requires a lock since it affects rendering win.io.terminal.scrollViewport(.{ .delta = delta }) catch |err| log.err("error scrolling viewport err={}", .{err}); + + // If we're scrolling up or down, then send a mouse event. This requires + // a lock since we read terminal state. + if (yoff != 0) { + const pos = window.getCursorPos() catch |err| { + log.err("error reading cursor position: {}", .{err}); + return; + }; + + win.mouseReport(if (yoff < 0) .five else .four, .press, win.mouse.mods, pos) catch |err| { + log.err("error reporting mouse event: {}", .{err}); + return; + }; + } } win.queueRender() catch unreachable; @@ -1007,12 +1008,6 @@ fn mouseReport( // TODO: posToViewport currently clamps to the window boundary, // do we want to not report mouse events at all outside the window? - // Everything in here requires reading/writing mouse state so we - // acquire a big lock. Mouse events are rare so this should be okay - // but we can make this more fine-grained later. - self.renderer_state.mutex.lock(); - defer self.renderer_state.mutex.unlock(); - // Depending on the event, we may do nothing at all. switch (self.io.terminal.modes.mouse_event) { .none => return, From 95d054b185503e649754640c5f27e3d62b0b9b5a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 5 Nov 2022 17:37:21 -0700 Subject: [PATCH 16/22] allocate data for paste data if its too large --- src/Window.zig | 8 ++--- src/termio/Thread.zig | 4 +++ src/termio/message.zig | 72 ++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 78 insertions(+), 6 deletions(-) diff --git a/src/Window.zig b/src/Window.zig index 817ee2a10..d9c864372 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -816,10 +816,10 @@ fn keyCallback( }, .{ .forever = {} }); } - // TODO: NO! ALLOCATE THIS - _ = win.io_thread.mailbox.push(.{ - .write_stable = data, - }, .{ .forever = {} }); + _ = win.io_thread.mailbox.push(termio.message.IO.writeReq( + win.alloc, + data, + ) catch unreachable, .{ .forever = {} }); if (bracketed) { _ = win.io_thread.mailbox.push(.{ diff --git a/src/termio/Thread.zig b/src/termio/Thread.zig index 7201101d7..9859138a1 100644 --- a/src/termio/Thread.zig +++ b/src/termio/Thread.zig @@ -164,6 +164,10 @@ fn drainMailbox(self: *Thread) !void { .resize => |v| try self.impl.resize(v.grid_size, v.screen_size), .write_small => |v| try self.impl.queueWrite(v.data[0..v.len]), .write_stable => |v| try self.impl.queueWrite(v), + .write_alloc => |v| { + defer v.alloc.free(v.data); + try self.impl.queueWrite(v.data); + }, } } } diff --git a/src/termio/message.zig b/src/termio/message.zig index e6ce2b287..45c4596a4 100644 --- a/src/termio/message.zig +++ b/src/termio/message.zig @@ -1,9 +1,14 @@ const std = @import("std"); +const assert = std.debug.assert; const Allocator = std.mem.Allocator; const renderer = @import("../renderer.zig"); const terminal = @import("../terminal/main.zig"); /// The messages that can be sent to an IO thread. +/// +/// This is not a tiny structure (~40 bytes at the time of writing this comment), +/// but the messages are IO thread sends are also very few. At the current size +/// we can queue 26,000 messages before consuming a MB of RAM. pub const IO = union(enum) { /// Resize the window. resize: struct { @@ -16,12 +21,52 @@ pub const IO = union(enum) { /// Write where the data pointer is stable. write_stable: []const u8, + + /// Write where the data is allocated and must be freed. + write_alloc: WriteReq.Alloc, + + /// Return a write request for the given data. This will use + /// write_small if it fits or write_alloc otherwise. This should NOT + /// be used for stable pointers which can be manually set to write_stable. + pub fn writeReq(alloc: Allocator, data: anytype) !IO { + switch (@typeInfo(@TypeOf(data))) { + .Pointer => |info| { + assert(info.size == .Slice); + assert(info.child == u8); + + // If it fits in our small request, do that. + if (data.len <= WriteReq.Small.Max) { + var buf: WriteReq.Small.Array = undefined; + std.mem.copy(u8, &buf, data); + return IO{ + .write_small = .{ + .data = buf, + .len = @intCast(u8, data.len), + }, + }; + } + + // Otherwise, allocate + var buf = try alloc.dupe(u8, data); + errdefer alloc.free(buf); + return IO{ + .write_alloc = .{ + .alloc = alloc, + .data = buf, + }, + }; + }, + + else => unreachable, + } + } }; /// Represents a write request. pub const WriteReq = union(enum) { pub const Small = struct { - pub const Array = [22]u8; + pub const Max = 38; + pub const Array = [Max]u8; data: Array, len: u8, }; @@ -43,8 +88,31 @@ pub const WriteReq = union(enum) { alloc: Alloc, }; +test { + std.testing.refAllDecls(@This()); +} + test { // Ensure we don't grow our IO message size without explicitly wanting to. const testing = std.testing; - try testing.expectEqual(@as(usize, 24), @sizeOf(IO)); + try testing.expectEqual(@as(usize, 40), @sizeOf(IO)); +} + +test "IO.writeReq small" { + const testing = std.testing; + const alloc = testing.allocator; + + const input = "hello!"; + const io = try IO.writeReq(alloc, @as([]const u8, input)); + try testing.expect(io == .write_small); +} + +test "IO.writeReq alloc" { + const testing = std.testing; + const alloc = testing.allocator; + + const input = "hello! " ** 100; + const io = try IO.writeReq(alloc, @as([]const u8, input)); + try testing.expect(io == .write_alloc); + io.write_alloc.alloc.free(io.write_alloc.data); } From 9a44e45785d853c3c52090005d5976daef0f2aed Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 5 Nov 2022 17:45:21 -0700 Subject: [PATCH 17/22] bug: assume focused on launch --- src/Window.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Window.zig b/src/Window.zig index d9c864372..8ac499a35 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -394,7 +394,7 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo .renderer_thread = render_thread, .renderer_state = .{ .mutex = mutex, - .focused = false, + .focused = true, .resize_screen = screen_size, .cursor = .{ .style = .blinking_block, From e2d8ffc3c1a12acaa39e9e751aefc033a610347c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 5 Nov 2022 18:51:39 -0700 Subject: [PATCH 18/22] renderer mailbox, focus message --- src/Window.zig | 11 +++++------ src/renderer.zig | 1 + src/renderer/Metal.zig | 11 ++++++++++- src/renderer/OpenGL.zig | 11 ++++++++++- src/renderer/State.zig | 3 --- src/renderer/Thread.zig | 41 +++++++++++++++++++++++++++++++++++++++- src/renderer/message.zig | 11 +++++++++++ 7 files changed, 77 insertions(+), 12 deletions(-) create mode 100644 src/renderer/message.zig diff --git a/src/Window.zig b/src/Window.zig index 8ac499a35..4d8421584 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -394,7 +394,6 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo .renderer_thread = render_thread, .renderer_state = .{ .mutex = mutex, - .focused = true, .resize_screen = screen_size, .cursor = .{ .style = .blinking_block, @@ -917,6 +916,11 @@ fn focusCallback(window: glfw.Window, focused: bool) void { const win = window.getUserPointer(Window) orelse return; + // Notify our render thread of the new state + _ = win.renderer_thread.mailbox.push(.{ + .focus = focused, + }, .{ .forever = {} }); + // We have to schedule a render because no matter what we're changing // the cursor. If we're focused its reappearing, if we're not then // its changing to hollow and not blinking. @@ -926,11 +930,6 @@ fn focusCallback(window: glfw.Window, focused: bool) void { win.terminal_cursor.startTimer() catch unreachable else win.terminal_cursor.stopTimer() catch unreachable; - - // We are modifying renderer state from here on out - win.renderer_state.mutex.lock(); - defer win.renderer_state.mutex.unlock(); - win.renderer_state.focused = focused; } fn refreshCallback(window: glfw.Window) void { diff --git a/src/renderer.zig b/src/renderer.zig index 2f3018558..8d2a2fe40 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -10,6 +10,7 @@ const builtin = @import("builtin"); pub usingnamespace @import("renderer/cursor.zig"); +pub usingnamespace @import("renderer/message.zig"); pub usingnamespace @import("renderer/size.zig"); pub const Metal = @import("renderer/Metal.zig"); pub const OpenGL = @import("renderer/OpenGL.zig"); diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 168a171b1..f6f23d350 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -33,6 +33,9 @@ alloc: std.mem.Allocator, /// Current cell dimensions for this grid. cell_size: renderer.CellSize, +/// True if the window is focused +focused: bool, + /// Whether the cursor is visible or not. This is used to control cursor /// blinking. cursor_visible: bool, @@ -205,6 +208,7 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Metal { .cell_size = .{ .width = metrics.cell_width, .height = metrics.cell_height }, .background = .{ .r = 0, .g = 0, .b = 0 }, .foreground = .{ .r = 255, .g = 255, .b = 255 }, + .focused = true, .cursor_visible = true, .cursor_style = .box, @@ -290,6 +294,11 @@ pub fn threadExit(self: *const Metal) void { // Metal requires no per-thread state. } +/// Callback when the focus changes for the terminal this is rendering. +pub fn setFocus(self: *Metal, focus: bool) !void { + self.focused = focus; +} + /// The primary render callback that is completely thread-safe. pub fn render( self: *Metal, @@ -315,7 +324,7 @@ pub fn render( defer state.resize_screen = null; // Setup our cursor state - if (state.focused) { + if (self.focused) { self.cursor_visible = state.cursor.visible and !state.cursor.blink; self.cursor_style = renderer.CursorStyle.fromTerminal(state.cursor.style) orelse .box; } else { diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index ee10e17a0..e42bbc60a 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -74,6 +74,9 @@ foreground: terminal.color.RGB, /// Default background color background: terminal.color.RGB, +/// True if the window is focused +focused: bool, + /// The raw structure that maps directly to the buffer sent to the vertex shader. /// This must be "extern" so that the field order is not reordered by the /// Zig compiler. @@ -292,6 +295,7 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !OpenGL { .cursor_style = .box, .background = .{ .r = 0, .g = 0, .b = 0 }, .foreground = .{ .r = 255, .g = 255, .b = 255 }, + .focused = true, }; } @@ -432,6 +436,11 @@ pub fn threadExit(self: *const OpenGL) void { glfw.makeContextCurrent(null) catch {}; } +/// Callback when the focus changes for the terminal this is rendering. +pub fn setFocus(self: *OpenGL, focus: bool) !void { + self.focused = focus; +} + /// The primary render callback that is completely thread-safe. pub fn render( self: *OpenGL, @@ -455,7 +464,7 @@ pub fn render( defer state.resize_screen = null; // Setup our cursor state - if (state.focused) { + if (self.focused) { self.cursor_visible = state.cursor.visible and !state.cursor.blink; self.cursor_style = renderer.CursorStyle.fromTerminal(state.cursor.style) orelse .box; } else { diff --git a/src/renderer/State.zig b/src/renderer/State.zig index bbf066da4..f172a1dd8 100644 --- a/src/renderer/State.zig +++ b/src/renderer/State.zig @@ -12,9 +12,6 @@ const renderer = @import("../renderer.zig"); /// state (i.e. the terminal, devmode, etc. values). mutex: *std.Thread.Mutex, -/// True if the window is focused -focused: bool, - /// A new screen size if the screen was resized. resize_screen: ?renderer.ScreenSize, diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index 5fb10420a..feb31ebdc 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -7,11 +7,16 @@ const builtin = @import("builtin"); const glfw = @import("glfw"); const libuv = @import("libuv"); const renderer = @import("../renderer.zig"); -const gl = @import("../opengl.zig"); +const BlockingQueue = @import("../blocking_queue.zig").BlockingQueue; const Allocator = std.mem.Allocator; const log = std.log.scoped(.renderer_thread); +/// The type used for sending messages to the IO thread. For now this is +/// hardcoded with a capacity. We can make this a comptime parameter in +/// the future if we want it configurable. +const Mailbox = BlockingQueue(renderer.Message, 64); + /// The main event loop for the application. The user data of this loop /// is always the allocator used to create the loop. This is a convenience /// so that users of the loop always have an allocator. @@ -36,6 +41,10 @@ renderer: *renderer.Renderer, /// Pointer to the shared state that is used to generate the final render. state: *renderer.State, +/// The mailbox that can be used to send this thread messages. Note +/// this is a blocking queue so if it is full you will get errors (or block). +mailbox: *Mailbox, + /// Initialize the thread. This does not START the thread. This only sets /// up all the internal state necessary prior to starting the thread. It /// is up to the caller to start the thread with the threadMain entrypoint. @@ -83,6 +92,10 @@ pub fn init( } }).callback); + // The mailbox for messaging this thread + var mailbox = try Mailbox.create(alloc); + errdefer mailbox.destroy(alloc); + return Thread{ .loop = loop, .wakeup = wakeup_h, @@ -91,6 +104,7 @@ pub fn init( .window = window, .renderer = renderer_impl, .state = state, + .mailbox = mailbox, }; } @@ -127,6 +141,9 @@ pub fn deinit(self: *Thread) void { _ = self.loop.run(.default) catch |err| log.err("error finalizing event loop: {}", .{err}); + // Nothing can possibly access the mailbox anymore, destroy it. + self.mailbox.destroy(alloc); + // Dealloc our allocator copy alloc.destroy(alloc_ptr); @@ -164,6 +181,23 @@ fn threadMain_(self: *Thread) !void { _ = try self.loop.run(.default); } +/// Drain the mailbox. +fn drainMailbox(self: *Thread) !void { + // This holds the mailbox lock for the duration of the drain. The + // expectation is that all our message handlers will be non-blocking + // ENOUGH to not mess up throughput on producers. + + var drain = self.mailbox.drain(); + defer drain.deinit(); + + while (drain.next()) |message| { + log.debug("mailbox message={}", .{message}); + switch (message) { + .focus => |v| try self.renderer.setFocus(v), + } + } +} + fn wakeupCallback(h: *libuv.Async) void { const t = h.getData(Thread) orelse { // This shouldn't happen so we log it. @@ -171,6 +205,11 @@ fn wakeupCallback(h: *libuv.Async) void { return; }; + // When we wake up, we check the mailbox. Mailbox producers should + // wake up our thread after publishing. + t.drainMailbox() catch |err| + log.err("error draining mailbox err={}", .{err}); + // If the timer is already active then we don't have to do anything. const active = t.render_h.isActive() catch true; if (active) return; diff --git a/src/renderer/message.zig b/src/renderer/message.zig new file mode 100644 index 000000000..1d60d0343 --- /dev/null +++ b/src/renderer/message.zig @@ -0,0 +1,11 @@ +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + +/// The messages that can be sent to a renderer thread. +pub const Message = union(enum) { + /// A change in state in the window focus that this renderer is + /// rendering within. This is only sent when a change is detected so + /// the renderer is expected to handle all of these. + focus: bool, +}; From aa98e3ca3aaf121b3a8f2933a91ad6453ccc8880 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 5 Nov 2022 19:18:22 -0700 Subject: [PATCH 19/22] Move cursor timer to renderer --- src/Window.zig | 30 ++--------------------- src/renderer/Metal.zig | 7 +++++- src/renderer/OpenGL.zig | 7 +++++- src/renderer/State.zig | 4 --- src/renderer/Thread.zig | 54 ++++++++++++++++++++++++++++++++++++++++- src/termio/Exec.zig | 2 +- 6 files changed, 68 insertions(+), 36 deletions(-) diff --git a/src/Window.zig b/src/Window.zig index 4d8421584..a35338583 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -70,10 +70,6 @@ mouse: Mouse, io: termio.Impl, io_thread: termio.Thread, io_thr: std.Thread, - -/// Cursor state. -terminal_cursor: Cursor, - /// The dimensions of the grid in rows and columns. grid_size: renderer.GridSize, @@ -137,6 +133,7 @@ const Mouse = struct { /// need a stable pointer for user data callbacks. Therefore, a stack-only /// initialization is not currently possible. pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Window { + _ = loop; var self = try alloc.create(Window); errdefer alloc.destroy(self); @@ -344,13 +341,6 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo .height = @floatToInt(u32, renderer_impl.cell_size.height * 4), }, .{ .width = null, .height = null }); - // Setup a timer for blinking the cursor - var timer = try libuv.Timer.init(alloc, loop); - errdefer timer.deinit(alloc); - errdefer timer.close(null); - timer.setData(self); - try timer.start(cursorTimerCallback, 600, 600); - // Create the cursor const cursor = try glfw.Cursor.createStandard(.ibeam); errdefer cursor.destroy(); @@ -398,7 +388,6 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo .cursor = .{ .style = .blinking_block, .visible = true, - .blink = false, }, .terminal = &self.io.terminal, .devmode = if (!DevMode.enabled) null else &DevMode.instance, @@ -408,7 +397,6 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo .io = io, .io_thread = io_thread, .io_thr = undefined, - .terminal_cursor = .{ .timer = timer }, .grid_size = grid_size, .config = config, @@ -516,13 +504,6 @@ pub fn destroy(self: *Window) void { self.window.destroy(); - self.terminal_cursor.timer.close((struct { - fn callback(t: *libuv.Timer) void { - const alloc = t.loop().getData(Allocator).?.*; - t.deinit(alloc); - } - }).callback); - // We can destroy the cursor right away. glfw will just revert any // windows using it to the default. self.cursor.destroy(); @@ -921,15 +902,8 @@ fn focusCallback(window: glfw.Window, focused: bool) void { .focus = focused, }, .{ .forever = {} }); - // We have to schedule a render because no matter what we're changing - // the cursor. If we're focused its reappearing, if we're not then - // its changing to hollow and not blinking. + // Schedule render which also drains our mailbox win.queueRender() catch unreachable; - - if (focused) - win.terminal_cursor.startTimer() catch unreachable - else - win.terminal_cursor.stopTimer() catch unreachable; } fn refreshCallback(window: glfw.Window) void { diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index f6f23d350..dc606f979 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -299,6 +299,11 @@ pub fn setFocus(self: *Metal, focus: bool) !void { self.focused = focus; } +/// Called to toggle the blink state of the cursor +pub fn blinkCursor(self: *Metal) void { + self.cursor_visible = !self.cursor_visible; +} + /// The primary render callback that is completely thread-safe. pub fn render( self: *Metal, @@ -325,7 +330,7 @@ pub fn render( // Setup our cursor state if (self.focused) { - self.cursor_visible = state.cursor.visible and !state.cursor.blink; + self.cursor_visible = self.cursor_visible and state.cursor.visible; self.cursor_style = renderer.CursorStyle.fromTerminal(state.cursor.style) orelse .box; } else { self.cursor_visible = true; diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index e42bbc60a..76c7a3443 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -441,6 +441,11 @@ pub fn setFocus(self: *OpenGL, focus: bool) !void { self.focused = focus; } +/// Called to toggle the blink state of the cursor +pub fn blinkCursor(self: *OpenGL) void { + self.cursor_visible = !self.cursor_visible; +} + /// The primary render callback that is completely thread-safe. pub fn render( self: *OpenGL, @@ -465,7 +470,7 @@ pub fn render( // Setup our cursor state if (self.focused) { - self.cursor_visible = state.cursor.visible and !state.cursor.blink; + self.cursor_visible = self.cursor_visible and state.cursor.visible; self.cursor_style = renderer.CursorStyle.fromTerminal(state.cursor.style) orelse .box; } else { self.cursor_visible = true; diff --git a/src/renderer/State.zig b/src/renderer/State.zig index f172a1dd8..f06d1784c 100644 --- a/src/renderer/State.zig +++ b/src/renderer/State.zig @@ -33,8 +33,4 @@ pub const Cursor = struct { /// "blink" settings, see "blink" for that. This is used to turn the /// cursor ON or OFF. visible: bool = true, - - /// Whether the cursor is currently blinking. If it is blinking, then - /// the cursor will not be rendered. - blink: bool = false, }; diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index feb31ebdc..8aff8c7e1 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -32,6 +32,9 @@ stop: libuv.Async, /// The timer used for rendering render_h: libuv.Timer, +/// The timer used for cursor blinking +cursor_h: libuv.Timer, + /// The windo we're rendering to. window: glfw.Window, @@ -92,6 +95,15 @@ pub fn init( } }).callback); + // Setup a timer for blinking the cursor + var cursor_timer = try libuv.Timer.init(alloc, loop); + errdefer cursor_timer.close((struct { + fn callback(t: *libuv.Timer) void { + const alloc_h = t.loop().getData(Allocator).?.*; + t.deinit(alloc_h); + } + }).callback); + // The mailbox for messaging this thread var mailbox = try Mailbox.create(alloc); errdefer mailbox.destroy(alloc); @@ -101,6 +113,7 @@ pub fn init( .wakeup = wakeup_h, .stop = stop_h, .render_h = render_h, + .cursor_h = cursor_timer, .window = window, .renderer = renderer_impl, .state = state, @@ -134,6 +147,12 @@ pub fn deinit(self: *Thread) void { h.deinit(handle_alloc); } }).callback); + self.cursor_h.close((struct { + fn callback(h: *libuv.Timer) void { + const handle_alloc = h.loop().getData(Allocator).?.*; + h.deinit(handle_alloc); + } + }).callback); // Run the loop one more time, because destroying our other things // like windows usually cancel all our event loop stuff and we need @@ -175,6 +194,10 @@ fn threadMain_(self: *Thread) !void { defer self.render_h.setData(null); try self.wakeup.send(); + // Setup a timer for blinking the cursor + self.cursor_h.setData(self); + try self.cursor_h.start(cursorTimerCallback, 600, 600); + // Run log.debug("starting renderer thread", .{}); defer log.debug("exiting renderer thread", .{}); @@ -193,7 +216,25 @@ fn drainMailbox(self: *Thread) !void { while (drain.next()) |message| { log.debug("mailbox message={}", .{message}); switch (message) { - .focus => |v| try self.renderer.setFocus(v), + .focus => |v| { + // Set it on the renderer + try self.renderer.setFocus(v); + + if (!v) { + // If we're not focused, then we stop the cursor blink + try self.cursor_h.stop(); + } else { + // If we're focused, we immediately show the cursor again + // and then restart the timer. + if (!try self.cursor_h.isActive()) { + try self.cursor_h.start( + cursorTimerCallback, + 0, + self.cursor_h.getRepeat(), + ); + } + } + }, } } } @@ -230,6 +271,17 @@ fn renderCallback(h: *libuv.Timer) void { log.warn("error rendering err={}", .{err}); } +fn cursorTimerCallback(h: *libuv.Timer) void { + const t = h.getData(Thread) orelse { + // This shouldn't happen so we log it. + log.warn("render callback fired without data set", .{}); + return; + }; + + t.renderer.blinkCursor(); + t.wakeup.send() catch {}; +} + fn stopCallback(h: *libuv.Async) void { h.loop().stop(); } diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 0ecbd6032..37c189fed 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -340,7 +340,7 @@ fn ttyRead(t: *libuv.Tty, n: isize, buf: []const u8) void { // Whenever a character is typed, we ensure the cursor is in the // non-blink state so it is rendered if visible. - ev.renderer_state.cursor.blink = false; + //ev.renderer_state.cursor.blink = false; // TODO // if (win.terminal_cursor.timer.isActive() catch false) { // _ = win.terminal_cursor.timer.again() catch null; From 746858cea63ef58ca997d3969678a27cfce06f64 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 5 Nov 2022 19:26:42 -0700 Subject: [PATCH 20/22] implement cursor reset when data comes in pty --- src/Window.zig | 1 + src/renderer/Metal.zig | 4 ++-- src/renderer/OpenGL.zig | 4 ++-- src/renderer/Thread.zig | 14 +++++++++++--- src/renderer/message.zig | 4 ++++ src/termio/Exec.zig | 22 ++++++++++++++-------- src/termio/Options.zig | 3 +++ 7 files changed, 37 insertions(+), 15 deletions(-) diff --git a/src/Window.zig b/src/Window.zig index a35338583..7de185282 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -367,6 +367,7 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo .config = config, .renderer_state = &self.renderer_state, .renderer_wakeup = render_thread.wakeup, + .renderer_mailbox = render_thread.mailbox, }); errdefer io.deinit(); diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index dc606f979..1b4a9112a 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -300,8 +300,8 @@ pub fn setFocus(self: *Metal, focus: bool) !void { } /// Called to toggle the blink state of the cursor -pub fn blinkCursor(self: *Metal) void { - self.cursor_visible = !self.cursor_visible; +pub fn blinkCursor(self: *Metal, reset: bool) void { + self.cursor_visible = reset or !self.cursor_visible; } /// The primary render callback that is completely thread-safe. diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 76c7a3443..b5479ce69 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -442,8 +442,8 @@ pub fn setFocus(self: *OpenGL, focus: bool) !void { } /// Called to toggle the blink state of the cursor -pub fn blinkCursor(self: *OpenGL) void { - self.cursor_visible = !self.cursor_visible; +pub fn blinkCursor(self: *OpenGL, reset: bool) void { + self.cursor_visible = reset or !self.cursor_visible; } /// The primary render callback that is completely thread-safe. diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index 8aff8c7e1..ebc06fd8d 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -15,7 +15,7 @@ const log = std.log.scoped(.renderer_thread); /// The type used for sending messages to the IO thread. For now this is /// hardcoded with a capacity. We can make this a comptime parameter in /// the future if we want it configurable. -const Mailbox = BlockingQueue(renderer.Message, 64); +pub const Mailbox = BlockingQueue(renderer.Message, 64); /// The main event loop for the application. The user data of this loop /// is always the allocator used to create the loop. This is a convenience @@ -227,14 +227,22 @@ fn drainMailbox(self: *Thread) !void { // If we're focused, we immediately show the cursor again // and then restart the timer. if (!try self.cursor_h.isActive()) { + self.renderer.blinkCursor(true); try self.cursor_h.start( cursorTimerCallback, - 0, + self.cursor_h.getRepeat(), self.cursor_h.getRepeat(), ); } } }, + + .reset_cursor_blink => { + self.renderer.blinkCursor(true); + if (try self.cursor_h.isActive()) { + _ = try self.cursor_h.again(); + } + }, } } } @@ -278,7 +286,7 @@ fn cursorTimerCallback(h: *libuv.Timer) void { return; }; - t.renderer.blinkCursor(); + t.renderer.blinkCursor(false); t.wakeup.send() catch {}; } diff --git a/src/renderer/message.zig b/src/renderer/message.zig index 1d60d0343..87cbc9f4e 100644 --- a/src/renderer/message.zig +++ b/src/renderer/message.zig @@ -8,4 +8,8 @@ pub const Message = union(enum) { /// rendering within. This is only sent when a change is detected so /// the renderer is expected to handle all of these. focus: bool, + + /// Reset the cursor blink by immediately showing the cursor then + /// restarting the timer. + reset_cursor_blink: void, }; diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 37c189fed..88e791b9a 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -40,6 +40,9 @@ renderer_state: *renderer.State, /// a repaint should happen. renderer_wakeup: libuv.Async, +/// The mailbox for notifying the renderer of things. +renderer_mailbox: *renderer.Thread.Mailbox, + /// The cached grid size whenever a resize is called. grid_size: renderer.GridSize, @@ -103,6 +106,7 @@ pub fn init(alloc: Allocator, opts: termio.Options) !Exec { .terminal_stream = undefined, .renderer_state = opts.renderer_state, .renderer_wakeup = opts.renderer_wakeup, + .renderer_mailbox = opts.renderer_mailbox, .grid_size = opts.grid_size, .data = null, }; @@ -141,6 +145,7 @@ pub fn threadEnter(self: *Exec, loop: libuv.Loop) !ThreadData { .read_arena = std.heap.ArenaAllocator.init(alloc), .renderer_state = self.renderer_state, .renderer_wakeup = self.renderer_wakeup, + .renderer_mailbox = self.renderer_mailbox, .data_stream = stream, .terminal_stream = .{ .handler = .{ @@ -238,6 +243,9 @@ const EventData = struct { /// a repaint should happen. renderer_wakeup: libuv.Async, + /// The mailbox for notifying the renderer of things. + renderer_mailbox: *renderer.Thread.Mailbox, + /// The data stream is the main IO for the pty. data_stream: libuv.Tty, @@ -334,18 +342,16 @@ fn ttyRead(t: *libuv.Tty, n: isize, buf: []const u8) void { return; }; + // Whenever a character is typed, we ensure the cursor is in the + // non-blink state so it is rendered if visible. + _ = ev.renderer_mailbox.push(.{ + .reset_cursor_blink = {}, + }, .{ .forever = {} }); + // We are modifying terminal state from here on out ev.renderer_state.mutex.lock(); defer ev.renderer_state.mutex.unlock(); - // Whenever a character is typed, we ensure the cursor is in the - // non-blink state so it is rendered if visible. - //ev.renderer_state.cursor.blink = false; - // TODO - // if (win.terminal_cursor.timer.isActive() catch false) { - // _ = win.terminal_cursor.timer.again() catch null; - // } - // Schedule a render ev.queueRender() catch unreachable; diff --git a/src/termio/Options.zig b/src/termio/Options.zig index 7577181f5..752516e77 100644 --- a/src/termio/Options.zig +++ b/src/termio/Options.zig @@ -22,3 +22,6 @@ renderer_state: *renderer.State, /// A handle to wake up the renderer. This hints to the renderer that that /// a repaint should happen. renderer_wakeup: libuv.Async, + +/// The mailbox for renderer messages. +renderer_mailbox: *renderer.Thread.Mailbox, From cd705359e8a6b677704b359fa37a1ae210d40abb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 5 Nov 2022 19:30:15 -0700 Subject: [PATCH 21/22] Window thread is now single event loop! --- src/App.zig | 69 +------------------------------------------------- src/Window.zig | 49 ++--------------------------------- 2 files changed, 3 insertions(+), 115 deletions(-) diff --git a/src/App.zig b/src/App.zig index 7867db4c7..89e96192e 100644 --- a/src/App.zig +++ b/src/App.zig @@ -7,7 +7,6 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const glfw = @import("glfw"); const Window = @import("Window.zig"); -const libuv = @import("libuv"); const tracy = @import("tracy"); const Config = @import("config.zig").Config; @@ -20,11 +19,6 @@ alloc: Allocator, /// single window operations. window: *Window, -// The main event loop for the application. The user data of this loop -// is always the allocator used to create the loop. This is a convenience -// so that users of the loop always have an allocator. -loop: libuv.Loop, - // The configuration for the app. config: *const Config, @@ -32,63 +26,23 @@ config: *const Config, /// up the renderer state, compiles the shaders, etc. This is the primary /// "startup" logic. pub fn init(alloc: Allocator, config: *const Config) !App { - // Create the event loop - var loop = try libuv.Loop.init(alloc); - errdefer loop.deinit(alloc); - - // We always store allocator pointer on the loop data so that - // handles can use our global allocator. - const allocPtr = try alloc.create(Allocator); - errdefer alloc.destroy(allocPtr); - allocPtr.* = alloc; - loop.setData(allocPtr); - // Create the window - var window = try Window.create(alloc, loop, config); + var window = try Window.create(alloc, config); errdefer window.destroy(); return App{ .alloc = alloc, .window = window, - .loop = loop, .config = config, }; } pub fn deinit(self: *App) void { self.window.destroy(); - - // Run the loop one more time, because destroying our other things - // like windows usually cancel all our event loop stuff and we need - // one more run through to finalize all the closes. - _ = self.loop.run(.default) catch |err| - log.err("error finalizing event loop: {}", .{err}); - - // Dealloc our allocator copy - self.alloc.destroy(self.loop.getData(Allocator).?); - - self.loop.deinit(self.alloc); self.* = undefined; } pub fn run(self: App) !void { - // We are embedding two event loops: glfw and libuv. To do this, we - // create a separate thread that watches for libuv events and notifies - // glfw to wake up so we can run the libuv tick. - var embed = try libuv.Embed.init(self.alloc, self.loop, (struct { - fn callback() void { - glfw.postEmptyEvent() catch unreachable; - } - }).callback); - defer embed.deinit(self.alloc); - try embed.start(); - - // This async handle is used to "wake up" the embed thread so we can - // exit immediately once the windows want to close. - var async_h = try libuv.Async.init(self.alloc, self.loop, (struct { - fn callback(_: *libuv.Async) void {} - }).callback); - while (!self.window.shouldClose()) { // Block for any glfw events. This may also be an "empty" event // posted by the libuv watcher so that we trigger a libuv loop tick. @@ -96,26 +50,5 @@ pub fn run(self: App) !void { // Mark this so we're in a totally different "frame" tracy.frameMark(); - - // Run the libuv loop - const frame = tracy.frame("libuv"); - defer frame.end(); - try embed.loopRun(); } - - // Notify the embed thread to stop. We do this before we send on the - // async handle so that when the thread goes around it exits. - embed.stop(); - - // Wake up the event loop and schedule our close. - try async_h.send(); - async_h.close((struct { - fn callback(h: *libuv.Async) void { - const alloc = h.loop().getData(Allocator).?.*; - h.deinit(alloc); - } - }).callback); - - // Wait for the thread to end which should be almost instant. - try embed.join(); } diff --git a/src/Window.zig b/src/Window.zig index 7de185282..3b6f60021 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -14,11 +14,9 @@ const termio = @import("termio.zig"); const objc = @import("objc"); const glfw = @import("glfw"); const imgui = @import("imgui"); -const libuv = @import("libuv"); const Pty = @import("Pty.zig"); const font = @import("font/main.zig"); const Command = @import("Command.zig"); -const SegmentedPool = @import("segmented_pool.zig").SegmentedPool; const trace = @import("tracy").trace; const terminal = @import("terminal/main.zig"); const Config = @import("config.zig").Config; @@ -70,6 +68,7 @@ mouse: Mouse, io: termio.Impl, io_thread: termio.Thread, io_thr: std.Thread, + /// The dimensions of the grid in rows and columns. grid_size: renderer.GridSize, @@ -82,30 +81,6 @@ config: *const Config, /// like such as "control-v" will write a "v" even if they're intercepted. ignore_char: bool = false, -/// Information related to the current cursor for the window. -// -// QUESTION(mitchellh): should this be attached to the Screen instead? -// I'm not sure if the cursor settings stick to the screen, i.e. if you -// change to an alternate screen if those are preserved. Need to check this. -const Cursor = struct { - /// Timer for cursor blinking. - timer: libuv.Timer, - - /// Start (or restart) the timer. This is idempotent. - pub fn startTimer(self: Cursor) !void { - try self.timer.start( - cursorTimerCallback, - 0, - self.timer.getRepeat(), - ); - } - - /// Stop the timer. This is idempotent. - pub fn stopTimer(self: Cursor) !void { - try self.timer.stop(); - } -}; - /// Mouse state for the window. const Mouse = struct { /// The last tracked mouse button state by button. @@ -132,8 +107,7 @@ const Mouse = struct { /// Create a new window. This allocates and returns a pointer because we /// need a stable pointer for user data callbacks. Therefore, a stack-only /// initialization is not currently possible. -pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Window { - _ = loop; +pub fn create(alloc: Allocator, config: *const Config) !*Window { var self = try alloc.create(Window); errdefer alloc.destroy(self); @@ -1458,25 +1432,6 @@ fn posToViewport(self: Window, xpos: f64, ypos: f64) terminal.point.Viewport { }; } -fn cursorTimerCallback(t: *libuv.Timer) void { - const tracy = trace(@src()); - defer tracy.end(); - - const win = t.getData(Window) orelse return; - - // We are modifying renderer state from here on out - win.renderer_state.mutex.lock(); - defer win.renderer_state.mutex.unlock(); - - // If the cursor is currently invisible, then we do nothing. Ideally - // in this state the timer would be cancelled but no big deal. - if (!win.renderer_state.cursor.visible) return; - - // Swap blink state and schedule a render - win.renderer_state.cursor.blink = !win.renderer_state.cursor.blink; - win.queueRender() catch unreachable; -} - const face_ttf = @embedFile("font/res/FiraCode-Regular.ttf"); const face_bold_ttf = @embedFile("font/res/FiraCode-Bold.ttf"); const face_emoji_ttf = @embedFile("font/res/NotoColorEmoji.ttf"); From 8f1fcc64e84490b929afc5ece2593825b5cdd9e2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 5 Nov 2022 19:34:41 -0700 Subject: [PATCH 22/22] rename termio thread message struct --- src/Window.zig | 16 +++++----- src/termio.zig | 2 +- src/termio/Thread.zig | 2 +- src/termio/message.zig | 66 +++++++++++++++++++++--------------------- 4 files changed, 43 insertions(+), 43 deletions(-) diff --git a/src/Window.zig b/src/Window.zig index 3b6f60021..4f77d1baf 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -611,7 +611,7 @@ fn charCallback(window: glfw.Window, codepoint: u21) void { } // Ask our IO thread to write the data - var data: termio.message.WriteReq.Small.Array = undefined; + var data: termio.Message.WriteReq.Small.Array = undefined; data[0] = @intCast(u8, codepoint); _ = win.io_thread.mailbox.push(.{ .write_small = .{ @@ -771,7 +771,7 @@ fn keyCallback( }, .{ .forever = {} }); } - _ = win.io_thread.mailbox.push(termio.message.IO.writeReq( + _ = win.io_thread.mailbox.push(termio.Message.writeReq( win.alloc, data, ) catch unreachable, .{ .forever = {} }); @@ -851,7 +851,7 @@ fn keyCallback( }; if (char > 0) { // Ask our IO thread to write the data - var data: termio.message.WriteReq.Small.Array = undefined; + var data: termio.Message.WriteReq.Small.Array = undefined; data[0] = @intCast(u8, char); _ = win.io_thread.mailbox.push(.{ .write_small = .{ @@ -1037,7 +1037,7 @@ fn mouseReport( } // + 1 below is because our x/y is 0-indexed and proto wants 1 - var data: termio.message.WriteReq.Small.Array = undefined; + var data: termio.Message.WriteReq.Small.Array = undefined; assert(data.len >= 5); data[0] = '\x1b'; data[1] = '['; @@ -1057,7 +1057,7 @@ fn mouseReport( .utf8 => { // Maximum of 12 because at most we have 2 fully UTF-8 encoded chars - var data: termio.message.WriteReq.Small.Array = undefined; + var data: termio.Message.WriteReq.Small.Array = undefined; assert(data.len >= 12); data[0] = '\x1b'; data[1] = '['; @@ -1086,7 +1086,7 @@ fn mouseReport( // Response always is at least 4 chars, so this leaves the // remainder for numbers which are very large... - var data: termio.message.WriteReq.Small.Array = undefined; + var data: termio.Message.WriteReq.Small.Array = undefined; const resp = try std.fmt.bufPrint(&data, "\x1B[<{d};{d};{d}{c}", .{ button_code, viewport_point.x + 1, @@ -1106,7 +1106,7 @@ fn mouseReport( .urxvt => { // Response always is at least 4 chars, so this leaves the // remainder for numbers which are very large... - var data: termio.message.WriteReq.Small.Array = undefined; + var data: termio.Message.WriteReq.Small.Array = undefined; const resp = try std.fmt.bufPrint(&data, "\x1B[{d};{d};{d}M", .{ 32 + button_code, viewport_point.x + 1, @@ -1128,7 +1128,7 @@ fn mouseReport( // Response always is at least 4 chars, so this leaves the // remainder for numbers which are very large... - var data: termio.message.WriteReq.Small.Array = undefined; + var data: termio.Message.WriteReq.Small.Array = undefined; const resp = try std.fmt.bufPrint(&data, "\x1B[<{d};{d};{d}{c}", .{ button_code, pos.xpos, diff --git a/src/termio.zig b/src/termio.zig index 5464ba1e2..f988ddb5f 100644 --- a/src/termio.zig +++ b/src/termio.zig @@ -2,7 +2,7 @@ //! for taking the config, spinning up a child process, and handling IO //! with the termianl. -pub const message = @import("termio/message.zig"); +pub usingnamespace @import("termio/message.zig"); pub const Exec = @import("termio/Exec.zig"); pub const Options = @import("termio/Options.zig"); pub const Thread = @import("termio/Thread.zig"); diff --git a/src/termio/Thread.zig b/src/termio/Thread.zig index 9859138a1..2dcfe9476 100644 --- a/src/termio/Thread.zig +++ b/src/termio/Thread.zig @@ -14,7 +14,7 @@ const log = std.log.scoped(.io_thread); /// The type used for sending messages to the IO thread. For now this is /// hardcoded with a capacity. We can make this a comptime parameter in /// the future if we want it configurable. -const Mailbox = BlockingQueue(termio.message.IO, 64); +const Mailbox = BlockingQueue(termio.Message, 64); /// The main event loop for the thread. The user data of this loop /// is always the allocator used to create the loop. This is a convenience diff --git a/src/termio/message.zig b/src/termio/message.zig index 45c4596a4..623c41b40 100644 --- a/src/termio/message.zig +++ b/src/termio/message.zig @@ -9,7 +9,7 @@ const terminal = @import("../terminal/main.zig"); /// This is not a tiny structure (~40 bytes at the time of writing this comment), /// but the messages are IO thread sends are also very few. At the current size /// we can queue 26,000 messages before consuming a MB of RAM. -pub const IO = union(enum) { +pub const Message = union(enum) { /// Resize the window. resize: struct { grid_size: renderer.GridSize, @@ -28,7 +28,7 @@ pub const IO = union(enum) { /// Return a write request for the given data. This will use /// write_small if it fits or write_alloc otherwise. This should NOT /// be used for stable pointers which can be manually set to write_stable. - pub fn writeReq(alloc: Allocator, data: anytype) !IO { + pub fn writeReq(alloc: Allocator, data: anytype) !Message { switch (@typeInfo(@TypeOf(data))) { .Pointer => |info| { assert(info.size == .Slice); @@ -38,7 +38,7 @@ pub const IO = union(enum) { if (data.len <= WriteReq.Small.Max) { var buf: WriteReq.Small.Array = undefined; std.mem.copy(u8, &buf, data); - return IO{ + return Message{ .write_small = .{ .data = buf, .len = @intCast(u8, data.len), @@ -49,7 +49,7 @@ pub const IO = union(enum) { // Otherwise, allocate var buf = try alloc.dupe(u8, data); errdefer alloc.free(buf); - return IO{ + return Message{ .write_alloc = .{ .alloc = alloc, .data = buf, @@ -60,32 +60,32 @@ pub const IO = union(enum) { else => unreachable, } } -}; -/// Represents a write request. -pub const WriteReq = union(enum) { - pub const Small = struct { - pub const Max = 38; - pub const Array = [Max]u8; - data: Array, - len: u8, + /// Represents a write request. + pub const WriteReq = union(enum) { + pub const Small = struct { + pub const Max = 38; + pub const Array = [Max]u8; + data: Array, + len: u8, + }; + + pub const Alloc = struct { + alloc: Allocator, + data: []u8, + }; + + /// A small write where the data fits into this union size. + small: Small, + + /// A stable pointer so we can just pass the slice directly through. + /// This is useful i.e. for const data. + stable: []const u8, + + /// Allocated and must be freed with the provided allocator. This + /// should be rarely used. + alloc: Alloc, }; - - pub const Alloc = struct { - alloc: Allocator, - data: []u8, - }; - - /// A small write where the data fits into this union size. - small: Small, - - /// A stable pointer so we can just pass the slice directly through. - /// This is useful i.e. for const data. - stable: []const u8, - - /// Allocated and must be freed with the provided allocator. This - /// should be rarely used. - alloc: Alloc, }; test { @@ -95,24 +95,24 @@ test { test { // Ensure we don't grow our IO message size without explicitly wanting to. const testing = std.testing; - try testing.expectEqual(@as(usize, 40), @sizeOf(IO)); + try testing.expectEqual(@as(usize, 40), @sizeOf(Message)); } -test "IO.writeReq small" { +test "Message.writeReq small" { const testing = std.testing; const alloc = testing.allocator; const input = "hello!"; - const io = try IO.writeReq(alloc, @as([]const u8, input)); + const io = try Message.writeReq(alloc, @as([]const u8, input)); try testing.expect(io == .write_small); } -test "IO.writeReq alloc" { +test "Message.writeReq alloc" { const testing = std.testing; const alloc = testing.allocator; const input = "hello! " ** 100; - const io = try IO.writeReq(alloc, @as([]const u8, input)); + const io = try Message.writeReq(alloc, @as([]const u8, input)); try testing.expect(io == .write_alloc); io.write_alloc.alloc.free(io.write_alloc.data); }