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, +};