diff --git a/src/Window.zig b/src/Window.zig index 4067e3822..20c775c6b 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -25,8 +25,6 @@ const Config = @import("config.zig").Config; const input = @import("input.zig"); const DevMode = @import("DevMode.zig"); -const RenderTimer = max_timer.MaxTimer(renderTimerCallback); - const log = std.log.scoped(.window); // The preallocation size for the write request pool. This should be big @@ -83,9 +81,6 @@ terminal_stream: terminal.Stream(*Window), /// Cursor state. terminal_cursor: Cursor, -/// Render at least 60fps. -render_timer: RenderTimer, - /// The dimensions of the grid in rows and columns. grid_size: renderer.GridSize, @@ -500,7 +495,6 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo .terminal_stream = .{ .handler = self }, .terminal_cursor = .{ .timer = timer }, .grid_size = grid_size, - .render_timer = try RenderTimer.init(loop, self, 6, 12), .pty_stream = stream, .config = config, .bg_r = @intToFloat(f32, config.background.r) / 255.0, @@ -616,8 +610,6 @@ pub fn destroy(self: *Window) void { } }).callback); - self.render_timer.deinit(); - // 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. @@ -668,6 +660,13 @@ fn queueWrite(self: *Window, data: []const u8) !void { } } +/// 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. +fn queueRender(self: *const Window) !void { + try self.renderer_thread.wakeup.send(); +} + /// The cursor position from glfw directly is in screen coordinates but /// all our internal state works in pixels. fn cursorPosToPixels(self: Window, pos: glfw.Window.CursorPos) glfw.Window.CursorPos { @@ -719,7 +718,7 @@ fn sizeCallback(window: glfw.Window, width: i32, height: i32) void { const win = window.getUserPointer(Window) orelse return; // Resize usually forces a redraw - win.render_timer.schedule() catch |err| + win.queueRender() catch |err| log.err("error scheduling render timer in sizeCallback err={}", .{err}); // Recalculate our grid size @@ -758,7 +757,7 @@ fn charCallback(window: glfw.Window, codepoint: u21) void { // If the event was handled by imgui, ignore it. if (imgui.IO.get()) |io| { if (io.cval().WantCaptureKeyboard) { - win.render_timer.schedule() catch |err| + win.queueRender() catch |err| log.err("error scheduling render timer err={}", .{err}); } } else |_| {} @@ -773,7 +772,7 @@ fn charCallback(window: glfw.Window, codepoint: u21) void { // Anytime is character is created, we have to clear the selection if (win.terminal.selection != null) { win.terminal.selection = null; - win.render_timer.schedule() catch |err| + win.queueRender() catch |err| log.err("error scheduling render in charCallback err={}", .{err}); } @@ -781,7 +780,7 @@ fn charCallback(window: glfw.Window, codepoint: u21) void { // 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.render_timer.schedule() catch |err| + win.queueRender() catch |err| log.err("error scheduling render in charCallback err={}", .{err}); // Write the character to the pty @@ -806,7 +805,7 @@ fn keyCallback( // If the event was handled by imgui, ignore it. if (imgui.IO.get()) |io| { if (io.cval().WantCaptureKeyboard) { - win.render_timer.schedule() catch |err| + win.queueRender() catch |err| log.err("error scheduling render timer err={}", .{err}); } } else |_| {} @@ -927,7 +926,7 @@ fn keyCallback( .toggle_dev_mode => if (DevMode.enabled) { DevMode.instance.visible = !DevMode.instance.visible; - win.render_timer.schedule() catch unreachable; + win.queueRender() catch unreachable; } else log.warn("dev mode was not compiled into this binary", .{}), } @@ -1004,7 +1003,7 @@ fn focusCallback(window: glfw.Window, focused: bool) void { // 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. - win.render_timer.schedule() catch unreachable; + win.queueRender() catch unreachable; if (focused) win.terminal_cursor.startTimer() catch unreachable @@ -1024,7 +1023,7 @@ fn refreshCallback(window: glfw.Window) void { const win = window.getUserPointer(Window) orelse return; // The point of this callback is to schedule a render, so do that. - win.render_timer.schedule() catch unreachable; + win.queueRender() catch unreachable; } fn scrollCallback(window: glfw.Window, xoff: f64, yoff: f64) void { @@ -1036,7 +1035,7 @@ fn scrollCallback(window: glfw.Window, xoff: f64, yoff: f64) void { // If our dev mode window is visible then we always schedule a render on // cursor move because the cursor might touch our windows. if (DevMode.enabled and DevMode.instance.visible) { - win.render_timer.schedule() catch |err| + win.queueRender() catch |err| log.err("error scheduling render timer err={}", .{err}); // If the mouse event was handled by imgui, ignore it. @@ -1071,7 +1070,7 @@ fn scrollCallback(window: glfw.Window, xoff: f64, yoff: f64) void { // Schedule render since scrolling usually does something. // TODO(perf): we can only schedule render if we know scrolling // did something - win.render_timer.schedule() catch unreachable; + win.queueRender() catch unreachable; } /// The type of action to report for a mouse event. @@ -1256,7 +1255,7 @@ fn mouseButtonCallback( // If our dev mode window is visible then we always schedule a render on // cursor move because the cursor might touch our windows. if (DevMode.enabled and DevMode.instance.visible) { - win.render_timer.schedule() catch |err| + win.queueRender() catch |err| log.err("error scheduling render timer in cursorPosCallback err={}", .{err}); // If the mouse event was handled by imgui, ignore it. @@ -1326,7 +1325,7 @@ fn mouseButtonCallback( // Selection is always cleared if (win.terminal.selection != null) { win.terminal.selection = null; - win.render_timer.schedule() catch |err| + win.queueRender() catch |err| log.err("error scheduling render in mouseButtinCallback err={}", .{err}); } } @@ -1345,7 +1344,7 @@ fn cursorPosCallback( // If our dev mode window is visible then we always schedule a render on // cursor move because the cursor might touch our windows. if (DevMode.enabled and DevMode.instance.visible) { - win.render_timer.schedule() catch |err| + win.queueRender() catch |err| log.err("error scheduling render timer in cursorPosCallback err={}", .{err}); // If the mouse event was handled by imgui, ignore it. @@ -1380,7 +1379,7 @@ fn cursorPosCallback( if (win.mouse.click_state[@enumToInt(input.MouseButton.left)] != .press) return; // All roads lead to requiring a re-render at this pont. - win.render_timer.schedule() catch |err| + win.queueRender() catch |err| log.err("error scheduling render timer in cursorPosCallback err={}", .{err}); // Convert to pixels from screen coords @@ -1533,7 +1532,7 @@ fn cursorTimerCallback(t: *libuv.Timer) void { // Swap blink state and schedule a render win.renderer_state.cursor.blink = !win.renderer_state.cursor.blink; - win.render_timer.schedule() catch unreachable; + win.queueRender() catch unreachable; } fn ttyReadAlloc(t: *libuv.Tty, size: usize) ?[]u8 { @@ -1582,7 +1581,7 @@ fn ttyRead(t: *libuv.Tty, n: isize, buf: []const u8) void { } // Schedule a render - win.render_timer.schedule() catch unreachable; + 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 @@ -1639,21 +1638,6 @@ fn ttyWrite(req: *libuv.WriteReq, status: i32) void { //log.info("WROTE: {d}", .{status}); } -fn renderTimerCallback(t: *libuv.Timer) void { - const tracy = trace(@src()); - tracy.color(0x006E7F); // blue-ish - defer tracy.end(); - - const win = t.getData(Window).?; - - // 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(); -} - //------------------------------------------------------------------- // Stream Callbacks @@ -1722,7 +1706,7 @@ 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.render_timer.schedule(); + try self.queueRender(); } self.terminal.eraseDisplay(mode); @@ -1775,7 +1759,7 @@ pub fn setMode(self: *Window, mode: terminal.Mode, enabled: bool) !void { self.terminal.modes.reverse_colors = enabled; // Schedule a render since we changed colors - try self.render_timer.schedule(); + try self.queueRender(); }, .origin => { @@ -1803,7 +1787,7 @@ pub fn setMode(self: *Window, mode: terminal.Mode, enabled: bool) !void { self.terminal.primaryScreen(opts); // Schedule a render since we changed screens - try self.render_timer.schedule(); + try self.queueRender(); }, .bracketed_paste => self.bracketed_paste = true, diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index ed598ec4e..9a6b71605 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -24,6 +24,9 @@ wakeup: libuv.Async, /// This can be used to stop the renderer on the next loop iteration. stop: libuv.Async, +/// The timer used for rendering +render_h: libuv.Timer, + /// The windo we're rendering to. window: glfw.Window, @@ -54,7 +57,7 @@ pub fn init( 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, renderCallback); + 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).?.*; @@ -71,10 +74,20 @@ pub fn init( } }).callback); + // The primary timer for rendering. + var render_h = try libuv.Timer.init(alloc, loop); + errdefer render_h.close((struct { + fn callback(h: *libuv.Timer) void { + const loop_alloc = h.loop().getData(Allocator).?.*; + h.deinit(loop_alloc); + } + }).callback); + return Thread{ .loop = loop, .wakeup = wakeup_h, .stop = stop_h, + .render_h = render_h, .window = window, .renderer = renderer_impl, .state = state, @@ -101,6 +114,12 @@ pub fn deinit(self: *Thread) void { h.deinit(handle_alloc); } }).callback); + self.render_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 @@ -124,6 +143,10 @@ pub fn threadMain(self: *Thread) void { } fn threadMain_(self: *Thread) !void { + // Get a copy to our allocator + // const alloc_ptr = self.loop.getData(Allocator).?; + // const alloc = alloc_ptr.*; + // 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. @@ -135,13 +158,34 @@ fn threadMain_(self: *Thread) !void { self.wakeup.setData(self); defer self.wakeup.setData(null); + // Set up our timer and start it for rendering + self.render_h.setData(self); + defer self.render_h.setData(null); + try self.wakeup.send(); + // Run log.debug("starting renderer thread", .{}); defer log.debug("exiting renderer thread", .{}); _ = try self.loop.run(.default); } -fn renderCallback(h: *libuv.Async) void { +fn wakeupCallback(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; + }; + + // 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; + + // Timer is not active, let's start it + t.render_h.start(renderCallback, 10, 0) catch |err| + log.warn("render timer failed to start err={}", .{err}); +} + +fn renderCallback(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", .{});