From b100406a6eaaaf6f92b0711141102e03f8f103bc Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 4 Nov 2022 20:27:48 -0700 Subject: [PATCH] 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, +};