From bb01357c421d64f072944f3d140d07997340c6a2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 29 Apr 2022 19:21:06 -0700 Subject: [PATCH] Move the render to a timer that slows down under load --- src/App.zig | 23 ++----------- src/Window.zig | 59 ++++++++++++++++++++++--------- src/max_timer.zig | 88 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+), 38 deletions(-) create mode 100644 src/max_timer.zig diff --git a/src/App.zig b/src/App.zig index 2dcbe920d..70451e09d 100644 --- a/src/App.zig +++ b/src/App.zig @@ -85,31 +85,12 @@ pub fn run(self: App) !void { }).callback); while (!self.window.shouldClose()) { - // Mark this so we're in a totally different "frame" - tracy.frameMark(); - - // Track the render part of the frame separately. - { - const frame = tracy.frame("render"); - defer frame.end(); - try self.window.run(); - } - // Block for any glfw events. This may also be an "empty" event // posted by the libuv watcher so that we trigger a libuv loop tick. try glfw.waitEvents(); - // If the window wants the event loop to wakeup, then we "kick" the - // embed thread to wake up. I'm not sure why we have to do this in a - // loop, this is surely a lacking in my understanding of libuv. But - // this works. - if (self.window.wakeup) { - self.window.wakeup = false; - while (embed.sleeping.load(.SeqCst) and embed.terminate.load(.SeqCst) == false) { - try async_h.send(); - try embed.loopRun(); - } - } + // Mark this so we're in a totally different "frame" + tracy.frameMark(); // Run the libuv loop const frame = tracy.frame("libuv"); diff --git a/src/Window.zig b/src/Window.zig index 50564fea2..bb1932e00 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -17,7 +17,11 @@ const Pty = @import("Pty.zig"); const Command = @import("Command.zig"); const Terminal = @import("terminal/Terminal.zig"); const SegmentedPool = @import("segmented_pool.zig").SegmentedPool; +const frame = @import("tracy/tracy.zig").frame; const trace = @import("tracy/tracy.zig").trace; +const max_timer = @import("max_timer.zig"); + +const RenderTimer = max_timer.MaxTimer(renderTimerCallback); const log = std.log.scoped(.window); @@ -49,6 +53,9 @@ terminal: Terminal, /// Timer that blinks the cursor. cursor_timer: libuv.Timer, +/// Render at least 60fps. +render_timer: RenderTimer, + /// The reader/writer stream for the pty. pty_stream: libuv.Tty, @@ -180,6 +187,7 @@ pub fn create(alloc: Allocator, loop: libuv.Loop) !*Window { .command = cmd, .terminal = term, .cursor_timer = timer, + .render_timer = try RenderTimer.init(loop, self, 16, 64), .pty_stream = stream, }; @@ -211,6 +219,8 @@ 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. @@ -231,21 +241,6 @@ pub fn shouldClose(self: Window) bool { return self.window.shouldClose(); } -pub fn run(self: Window) !void { - const tracy = trace(@src()); - defer tracy.end(); - - // Set our background - gl.clearColor(0.2, 0.3, 0.3, 1.0); - gl.clear(gl.c.GL_COLOR_BUFFER_BIT); - - // Render the grid - try self.grid.render(); - - // Swap - try self.window.swapBuffers(); -} - fn sizeCallback(window: glfw.Window, width: i32, height: i32) void { const tracy = trace(@src()); defer tracy.end(); @@ -281,8 +276,7 @@ fn sizeCallback(window: glfw.Window, width: i32, height: i32) void { log.err("error updating OpenGL viewport err={}", .{err}); // Draw - win.run() catch |err| - log.err("error redrawing window during resize err={}", .{err}); + win.render_timer.schedule() catch unreachable; } fn charCallback(window: glfw.Window, codepoint: u21) void { @@ -369,6 +363,7 @@ fn focusCallback(window: glfw.Window, focused: bool) void { defer tracy.end(); const win = window.getUserPointer(Window) orelse return; + win.render_timer.schedule() catch unreachable; if (focused) { win.wakeup = true; win.cursor_timer.start(cursorTimerCallback, 0, win.cursor_timer.getRepeat()) catch unreachable; @@ -389,6 +384,7 @@ fn cursorTimerCallback(t: *libuv.Timer) void { const win = t.getData(Window) orelse return; win.grid.cursor_visible = !win.grid.cursor_visible; win.grid.updateCells(win.terminal) catch unreachable; + win.render_timer.schedule() catch unreachable; } fn ttyReadAlloc(t: *libuv.Tty, size: usize) ?[]u8 { @@ -434,6 +430,9 @@ fn ttyRead(t: *libuv.Tty, n: isize, buf: []const u8) void { // Update the cells for drawing win.grid.updateCells(win.terminal) catch unreachable; + + // Schedule a render + win.render_timer.schedule() catch unreachable; } fn ttyWrite(req: *libuv.WriteReq, status: i32) void { @@ -450,3 +449,29 @@ fn ttyWrite(req: *libuv.WriteReq, status: i32) void { //log.info("WROTE: {d}", .{status}); } + +fn renderTimerCallback(t: *libuv.Timer) void { + const tracy = trace(@src()); + defer tracy.end(); + + const win = t.getData(Window).?; + + // Set our background + gl.clearColor(0.2, 0.3, 0.3, 1.0); + gl.clear(gl.c.GL_COLOR_BUFFER_BIT); + + // Render the grid + win.grid.render() catch |err| { + log.err("error rendering grid: {}", .{err}); + return; + }; + + // Swap + win.window.swapBuffers() catch |err| { + log.err("error swapping buffers: {}", .{err}); + return; + }; + + // Record our run + win.render_timer.tick(); +} diff --git a/src/max_timer.zig b/src/max_timer.zig new file mode 100644 index 000000000..addaf389f --- /dev/null +++ b/src/max_timer.zig @@ -0,0 +1,88 @@ +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const libuv = @import("libuv/main.zig"); + +/// A coalescing timer that forces a run after a certain maximum time +/// since the last run. This is used for example by the renderer to try +/// to render at a high FPS but gracefully fall back under high IO load so +/// that we can process more data and increase throughput. +pub fn MaxTimer(comptime cb: fn (*libuv.Timer) void) type { + return struct { + const Self = @This(); + + /// The underlying libuv timer. + timer: libuv.Timer, + + /// The maximum time between timer calls. This is best effort based on + /// event loop load. If the event loop is busy, the timer will be run on + /// the next available tick. + max: u64, + + /// The fastest the timer will ever run. + min: u64, + + /// The last time this timer ran. + last: u64 = 0, + + pub fn init( + loop: libuv.Loop, + data: ?*anyopaque, + min: u64, + max: u64, + ) !Self { + const alloc = loop.getData(Allocator).?.*; + var timer = try libuv.Timer.init(alloc, loop); + timer.setData(data); + + // The maximum time can't be less than the interval otherwise this + // will just constantly fire. + if (max < min) return error.MaxShorterThanTimer; + return Self{ + .timer = timer, + .min = min, + .max = max, + }; + } + + pub fn deinit(self: *Self) void { + self.timer.close((struct { + fn callback(t: *libuv.Timer) void { + const alloc = t.loop().getData(Allocator).?.*; + t.deinit(alloc); + } + }).callback); + self.* = undefined; + } + + /// This should be called from the callback to update the last called time. + pub fn tick(self: *Self) void { + self.timer.loop().updateTime(); + self.last = self.timer.loop().now(); + self.timer.stop() catch unreachable; + } + + /// Schedule the timer to run. If the timer is not started, it'll + /// run on the next min tick. If the timer is started, this will + /// delay the timer up to max time since the last run. + pub fn schedule(self: *Self) !void { + // If the timer hasn't been started, start it now and schedule + // a tick as soon as possible. + if (!try self.timer.isActive()) { + try self.timer.start(cb, self.min, self.min); + return; + } + + // If we are past the max time, we run the timer now. + try self.timer.stop(); + self.timer.loop().updateTime(); + if (self.timer.loop().now() - self.last > self.max) { + @call(.{ .modifier = .always_inline }, cb, .{&self.timer}); + return; + } + + // We still have time, restart the timer so that it is min time away. + try self.timer.start(cb, self.min, 0); + } + }; +}