From 2adad0b05b8c6efbebb226bd919ba61ae3384dd0 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 9 Apr 2026 15:08:55 +0200 Subject: [PATCH] feat(gui): introduce deadline rendering --- src/renderer/gui/renderer.zig | 13 ++++++----- src/renderer/vaxis/renderer.zig | 6 +++--- src/renderer/win32/renderer.zig | 6 +++--- src/tui/tui.zig | 38 ++++++++++++++++++++++++++++----- 4 files changed, 47 insertions(+), 16 deletions(-) diff --git a/src/renderer/gui/renderer.zig b/src/renderer/gui/renderer.zig index 2cc582f2..5a645703 100644 --- a/src/renderer/gui/renderer.zig +++ b/src/renderer/gui/renderer.zig @@ -137,8 +137,8 @@ fn fmtmsg(self: *Self, value: anytype) std.Io.Writer.Error![]const u8 { return self.event_buffer.written(); } -pub fn render(self: *Self) error{}!bool { - if (!self.window_ready) return false; +pub fn render(self: *Self) error{}!?i64 { + if (!self.window_ready) return null; var cursor = self.cursor_info; @@ -175,9 +175,12 @@ pub fn render(self: *Self) error{}!bool { app.updateScreen(&self.vx.screen, cursor, self.secondary_cursors.items); - if (!self.cursor_info.vis or !self.cursor_blink) return false; - const idle = std.time.microTimestamp() - self.blink_last_change; - return idle < self.blink_idle_us; + if (!self.cursor_info.vis or !self.cursor_blink) return null; + const now_check = std.time.microTimestamp(); + if (now_check - self.blink_last_change >= self.blink_idle_us) return null; + const elapsed = @mod(now_check - self.blink_epoch, self.blink_period_us * 2); + const deadline = now_check + (self.blink_period_us - @mod(elapsed, self.blink_period_us)); + return deadline; } pub fn sigwinch(self: *Self) !void { diff --git a/src/renderer/vaxis/renderer.zig b/src/renderer/vaxis/renderer.zig index 30710f43..ccfb7383 100644 --- a/src/renderer/vaxis/renderer.zig +++ b/src/renderer/vaxis/renderer.zig @@ -219,11 +219,11 @@ pub fn run(self: *Self) Error!void { try self.loop.start(); } -pub fn render(self: *Self) !bool { - if (in_panic.load(.acquire)) return false; +pub fn render(self: *Self) !?i64 { + if (in_panic.load(.acquire)) return null; try self.vx.render(self.tty.writer()); try self.tty.writer().flush(); - return false; + return null; } pub fn sigwinch(self: *Self) !void { diff --git a/src/renderer/win32/renderer.zig b/src/renderer/win32/renderer.zig index 40e4d6a1..8506b86a 100644 --- a/src/renderer/win32/renderer.zig +++ b/src/renderer/win32/renderer.zig @@ -165,10 +165,10 @@ fn fmtmsg(self: *Self, value: anytype) std.Io.Writer.Error![]const u8 { return self.event_buffer.written(); } -pub fn render(self: *Self) error{}!bool { - const hwnd = self.hwnd orelse return false; +pub fn render(self: *Self) error{}!?i64 { + const hwnd = self.hwnd orelse return null; _ = gui.updateScreen(hwnd, &self.vx.screen); - return false; + return null; } pub fn stop(self: *Self) void { // this is guaranteed because stop won't be called until after diff --git a/src/tui/tui.zig b/src/tui/tui.zig index c0050d97..40a99bbc 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -75,6 +75,7 @@ render_pending: bool = false, keepalive_timer: ?tp.Cancellable = null, input_idle_timer: ?tp.Cancellable = null, mouse_idle_timer: ?tp.Cancellable = null, +render_deadline_timer: ?tp.Cancellable = null, fontface_: []const u8 = "", fontfaces_: std.ArrayListUnmanaged([]const u8) = .{}, input_is_idle: bool = false, @@ -293,6 +294,11 @@ fn deinit(self: *Self) void { t.deinit(); self.keepalive_timer = null; } + if (self.render_deadline_timer) |*t| { + t.cancel() catch {}; + t.deinit(); + self.render_deadline_timer = null; + } if (self.input_mode_) |*m| { m.deinit(); self.input_mode_ = null; @@ -681,17 +687,24 @@ fn render(self: *Self) void { top_layer_.draw(self.rdr_.stdplane()); } - const renderer_more = ret: { + if (self.render_deadline_timer) |*t| { + t.cancel() catch {}; + t.deinit(); + self.render_deadline_timer = null; + } + + const render_deadline: ?i64 = ret: { const frame = tracy.initZone(@src(), .{ .name = renderer.log_name ++ " render" }); defer frame.deinit(); - const m = self.rdr_.render() catch |e| blk: { + const dl = self.rdr_.render() catch |e| blk: { self.logger.err("render", e); - break :blk false; + break :blk @as(?i64, null); }; tracy.frameMark(); self.unrendered_input_events_count = 0; - break :ret m; + break :ret dl; }; + self.top_layer_reset(); self.idle_frame_count = if (self.unrendered_input_events_count > 0) @@ -699,13 +712,28 @@ fn render(self: *Self) void { else self.idle_frame_count + 1; - if (more or renderer_more or self.idle_frame_count < idle_frames or self.no_sleep) { + const deadline_within_frame = if (render_deadline) |dl| + dl - std.time.microTimestamp() < @as(i64, @intCast(self.frame_time)) + else + false; + + if (more or deadline_within_frame or self.idle_frame_count < idle_frames or self.no_sleep) { if (!self.frame_clock_running) { self.frame_clock.start() catch {}; self.frame_clock_running = true; tp.trace(tp.channel.widget, .{ "frame_clock_running", "started", more }); } } else { + if (render_deadline) |deadline| { + const delay_us: u64 = @intCast(@max(0, deadline - std.time.microTimestamp())); + self.render_deadline_timer = tp.self_pid().delay_send_cancellable( + self.allocator, + "tui.render_deadline", + delay_us, + .{"render"}, + ) catch null; + } + if (self.frame_clock_running) { self.frame_clock.stop() catch {}; self.frame_clock_running = false;