diff --git a/pkg/libuv/main.zig b/pkg/libuv/main.zig index 060dd3e3a..3db629b42 100644 --- a/pkg/libuv/main.zig +++ b/pkg/libuv/main.zig @@ -1,6 +1,7 @@ const std = @import("std"); const stream = @import("stream.zig"); +pub const c = @import("c.zig"); pub const Loop = @import("Loop.zig"); pub const Async = @import("Async.zig"); pub const Idle = @import("Idle.zig"); diff --git a/src/Window.zig b/src/Window.zig index ba5947334..c76800184 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -59,6 +59,12 @@ renderer: renderer.OpenGL, /// The render state renderer_state: renderer.State, +/// The renderer thread manager +renderer_thread: renderer.Thread, + +/// The actual thread +renderer_thr: std.Thread, + /// The underlying pty for this window. pty: Pty, @@ -450,6 +456,15 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo mutex.* = .{}; errdefer alloc.destroy(mutex); + // Create the renderer thread + var render_thread = try renderer.Thread.init( + alloc, + window, + &self.renderer, + &self.renderer_state, + ); + errdefer render_thread.deinit(); + self.* = .{ .alloc = alloc, .alloc_io_arena = io_arena, @@ -459,6 +474,7 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo .cursor = cursor, .focused = false, .renderer = renderer_impl, + .renderer_thread = render_thread, .renderer_state = .{ .mutex = mutex, .cursor = .{ @@ -469,6 +485,7 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo .terminal = &self.terminal, .devmode = if (!DevMode.enabled) null else &DevMode.instance, }, + .renderer_thr = undefined, .pty = pty, .command = cmd, .mouse = .{}, @@ -489,7 +506,7 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo // Setup our callbacks and user data window.setUserPointer(self); - window.setSizeCallback(sizeCallback); + //window.setSizeCallback(sizeCallback); window.setCharCallback(charCallback); window.setKeyCallback(keyCallback); window.setFocusCallback(focusCallback); @@ -537,10 +554,33 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo DevMode.instance.window = self; } + // Unload our context prior to switching over to the renderer thread + // because OpenGL requires it to be unloaded. + gl.glad.unload(); + try glfw.makeContextCurrent(null); + + // Start our renderer thread + self.renderer_thr = try std.Thread.spawn( + .{}, + renderer.Thread.threadMain, + .{&self.renderer_thread}, + ); + return self; } pub fn destroy(self: *Window) void { + { + // Stop rendering thread + self.renderer_thread.stop.send() catch |err| + log.err("error notifying renderer thread to stop, may stall err={}", .{err}); + self.renderer_thr.join(); + + // We need to become the active rendering thread again + self.renderer.threadEnter(self.window) catch unreachable; + self.renderer_thread.deinit(); + } + if (DevMode.enabled) { // Clear the window DevMode.instance.window = null; @@ -1470,6 +1510,10 @@ fn cursorTimerCallback(t: *libuv.Timer) void { 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; @@ -1589,9 +1633,9 @@ fn renderTimerCallback(t: *libuv.Timer) void { const win = t.getData(Window).?; - // Render - win.renderer.render(win.window, win.renderer_state) catch |err| - log.warn("error rendering err={}", .{err}); + // Trigger a render + win.renderer_thread.wakeup.send() catch |err| + log.err("error sending render notification err={}", .{err}); // Record our run win.render_timer.tick(); diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index c79455f34..a6908e1b4 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -325,7 +325,9 @@ pub fn deinit(self: *OpenGL) void { } /// Callback called by renderer.Thread when it begins. -pub fn threadEnter(window: glfw.Window) !void { +pub fn threadEnter(self: *const OpenGL, window: glfw.Window) !void { + _ = self; + // We need to make the OpenGL context current. OpenGL requires // that a single thread own the a single OpenGL context (if any). This // ensures that the context switches over to our thread. Important: @@ -350,7 +352,9 @@ pub fn threadEnter(window: glfw.Window) !void { } /// Callback called by renderer.Thread when it exits. -pub fn threadExit() void { +pub fn threadExit(self: *const OpenGL) void { + _ = self; + gl.glad.unload(); glfw.makeContextCurrent(null) catch {}; } diff --git a/src/renderer/State.zig b/src/renderer/State.zig new file mode 100644 index 000000000..25963ded5 --- /dev/null +++ b/src/renderer/State.zig @@ -0,0 +1,40 @@ +//! This is the render state that is given to a renderer. + +const std = @import("std"); +const DevMode = @import("../DevMode.zig"); +const terminal = @import("../terminal/main.zig"); + +/// The mutex that must be held while reading any of the data in the +/// members of this state. Note that the state itself is NOT protected +/// by the mutex and is NOT thread-safe, only the members values of the +/// state (i.e. the terminal, devmode, etc. values). +mutex: *std.Thread.Mutex, + +/// A new screen size if the screen was resized. +resize: ?Resize = null, + +/// Cursor configuration for rendering +cursor: Cursor, + +/// The terminal data. +terminal: *terminal.Terminal, + +/// The devmode data. +devmode: ?*const DevMode = null, + +pub const Cursor = struct { + /// Current cursor style. This can be set by escape sequences. To get + /// the default style, the config has to be referenced. + style: terminal.CursorStyle = .default, + + /// Whether the cursor is visible at all. This should not be used for + /// "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, +}; + +pub const Resize = struct {}; diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index 729198fcc..4cb12f99f 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -10,7 +10,7 @@ const renderer = @import("../renderer.zig"); const gl = @import("../opengl.zig"); const Allocator = std.mem.Allocator; -const log = std.log.named(.renderer_thread); +const log = std.log.scoped(.renderer_thread); /// 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 @@ -21,10 +21,27 @@ loop: libuv.Loop, /// any thread. wakeup: libuv.Async, +/// This can be used to stop the renderer on the next loop iteration. +stop: libuv.Async, + +/// The windo we're rendering to. +window: glfw.Window, + +/// The underlying renderer implementation. +renderer: *renderer.OpenGL, + +/// Pointer to the shared state that is used to generate the final render. +state: *const renderer.State, + /// 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) !Thread { +pub fn init( + alloc: Allocator, + window: glfw.Window, + renderer_impl: *renderer.OpenGL, + state: *const renderer.State, +) !Thread { // We always store allocator pointer on the loop data so that // handles can use our global allocator. const allocPtr = try alloc.create(Allocator); @@ -37,10 +54,17 @@ pub fn init(alloc: Allocator) !Thread { loop.setData(allocPtr); // This async handle is used to "wake up" the renderer and force a render. - var async_h = try libuv.Async.init(alloc, loop, (struct { - fn callback(_: *libuv.Async) void {} + var wakeup_h = try libuv.Async.init(alloc, loop, renderCallback); + errdefer wakeup_h.close((struct { + fn callback(h: *libuv.Async) void { + const loop_alloc = h.loop().getData(Allocator).?.*; + h.deinit(loop_alloc); + } }).callback); - errdefer async_h.close((struct { + + // 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); @@ -48,9 +72,12 @@ pub fn init(alloc: Allocator) !Thread { }).callback); return Thread{ - .alloc = alloc, .loop = loop, - .notifier = async_h, + .wakeup = wakeup_h, + .stop = stop_h, + .window = window, + .renderer = renderer_impl, + .state = state, }; } @@ -61,6 +88,20 @@ pub fn deinit(self: *Thread) void { 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. @@ -74,40 +115,54 @@ pub fn deinit(self: *Thread) void { } /// The main entrypoint for the thread. -pub fn threadMain( - window: glfw.Window, - renderer_impl: *const renderer.OpenGL, -) void { +pub fn threadMain(self: *Thread) void { // Call child function so we can use errors... - threadMain_( - window, - renderer_impl, - ) catch |err| { + self.threadMain_() catch |err| { // In the future, we should expose this on the thread struct. log.warn("error in renderer err={}", .{err}); }; } -fn threadMain_( - self: *const Thread, - window: glfw.Window, - renderer_impl: *const renderer.OpenGL, -) !void { - const Renderer = switch (@TypeOf(renderer_impl)) { - .Pointer => |p| p.child, - .Struct => |s| s, - }; - +fn threadMain_(self: *Thread) !void { // Run our thread start/end callbacks. This is important because some // renderers have to do per-thread setup. For example, OpenGL has to set // some thread-local state since that is how it works. - if (@hasDecl(Renderer, "threadEnter")) try renderer_impl.threadEnter(window); - defer if (@hasDecl(Renderer, "threadExit")) renderer_impl.threadExit(); + const Renderer = RendererType(); + if (@hasDecl(Renderer, "threadEnter")) try self.renderer.threadEnter(self.window); + defer if (@hasDecl(Renderer, "threadExit")) self.renderer.threadExit(); - // Setup our timer handle which is used to perform the actual render. - // TODO + // Set up our async handler to support rendering + self.wakeup.setData(self); + defer self.wakeup.setData(null); // Run log.debug("starting renderer thread", .{}); - try self.loop.run(.default); + defer log.debug("exiting renderer thread", .{}); + _ = try self.loop.run(.default); +} + +fn renderCallback(h: *libuv.Async) 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.render(t.window, t.state.*) catch |err| + log.warn("error rendering err={}", .{err}); +} + +fn stopCallback(h: *libuv.Async) void { + h.loop().stop(); +} + +// This is unnecessary right now but is logic we'll need for when we +// abstract renderers out. +fn RendererType() type { + const self: Thread = undefined; + return switch (@typeInfo(@TypeOf(self.renderer))) { + .Pointer => |p| p.child, + .Struct => |s| s, + else => unreachable, + }; }