Move the render to a timer that slows down under load

This commit is contained in:
Mitchell Hashimoto
2022-04-29 19:21:06 -07:00
parent b0aa222e58
commit bb01357c42
3 changed files with 132 additions and 38 deletions

View File

@ -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");

View File

@ -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();
}

88
src/max_timer.zig Normal file
View File

@ -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);
}
};
}