mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
Move the render to a timer that slows down under load
This commit is contained in:
23
src/App.zig
23
src/App.zig
@ -85,31 +85,12 @@ pub fn run(self: App) !void {
|
|||||||
}).callback);
|
}).callback);
|
||||||
|
|
||||||
while (!self.window.shouldClose()) {
|
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
|
// 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.
|
// posted by the libuv watcher so that we trigger a libuv loop tick.
|
||||||
try glfw.waitEvents();
|
try glfw.waitEvents();
|
||||||
|
|
||||||
// If the window wants the event loop to wakeup, then we "kick" the
|
// Mark this so we're in a totally different "frame"
|
||||||
// embed thread to wake up. I'm not sure why we have to do this in a
|
tracy.frameMark();
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the libuv loop
|
// Run the libuv loop
|
||||||
const frame = tracy.frame("libuv");
|
const frame = tracy.frame("libuv");
|
||||||
|
@ -17,7 +17,11 @@ const Pty = @import("Pty.zig");
|
|||||||
const Command = @import("Command.zig");
|
const Command = @import("Command.zig");
|
||||||
const Terminal = @import("terminal/Terminal.zig");
|
const Terminal = @import("terminal/Terminal.zig");
|
||||||
const SegmentedPool = @import("segmented_pool.zig").SegmentedPool;
|
const SegmentedPool = @import("segmented_pool.zig").SegmentedPool;
|
||||||
|
const frame = @import("tracy/tracy.zig").frame;
|
||||||
const trace = @import("tracy/tracy.zig").trace;
|
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);
|
const log = std.log.scoped(.window);
|
||||||
|
|
||||||
@ -49,6 +53,9 @@ terminal: Terminal,
|
|||||||
/// Timer that blinks the cursor.
|
/// Timer that blinks the cursor.
|
||||||
cursor_timer: libuv.Timer,
|
cursor_timer: libuv.Timer,
|
||||||
|
|
||||||
|
/// Render at least 60fps.
|
||||||
|
render_timer: RenderTimer,
|
||||||
|
|
||||||
/// The reader/writer stream for the pty.
|
/// The reader/writer stream for the pty.
|
||||||
pty_stream: libuv.Tty,
|
pty_stream: libuv.Tty,
|
||||||
|
|
||||||
@ -180,6 +187,7 @@ pub fn create(alloc: Allocator, loop: libuv.Loop) !*Window {
|
|||||||
.command = cmd,
|
.command = cmd,
|
||||||
.terminal = term,
|
.terminal = term,
|
||||||
.cursor_timer = timer,
|
.cursor_timer = timer,
|
||||||
|
.render_timer = try RenderTimer.init(loop, self, 16, 64),
|
||||||
.pty_stream = stream,
|
.pty_stream = stream,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -211,6 +219,8 @@ pub fn destroy(self: *Window) void {
|
|||||||
}
|
}
|
||||||
}).callback);
|
}).callback);
|
||||||
|
|
||||||
|
self.render_timer.deinit();
|
||||||
|
|
||||||
// We have to dealloc our window in the close callback because
|
// We have to dealloc our window in the close callback because
|
||||||
// we can't free some of the memory associated with the window
|
// we can't free some of the memory associated with the window
|
||||||
// until the stream is closed.
|
// until the stream is closed.
|
||||||
@ -231,21 +241,6 @@ pub fn shouldClose(self: Window) bool {
|
|||||||
return self.window.shouldClose();
|
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 {
|
fn sizeCallback(window: glfw.Window, width: i32, height: i32) void {
|
||||||
const tracy = trace(@src());
|
const tracy = trace(@src());
|
||||||
defer tracy.end();
|
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});
|
log.err("error updating OpenGL viewport err={}", .{err});
|
||||||
|
|
||||||
// Draw
|
// Draw
|
||||||
win.run() catch |err|
|
win.render_timer.schedule() catch unreachable;
|
||||||
log.err("error redrawing window during resize err={}", .{err});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn charCallback(window: glfw.Window, codepoint: u21) void {
|
fn charCallback(window: glfw.Window, codepoint: u21) void {
|
||||||
@ -369,6 +363,7 @@ fn focusCallback(window: glfw.Window, focused: bool) void {
|
|||||||
defer tracy.end();
|
defer tracy.end();
|
||||||
|
|
||||||
const win = window.getUserPointer(Window) orelse return;
|
const win = window.getUserPointer(Window) orelse return;
|
||||||
|
win.render_timer.schedule() catch unreachable;
|
||||||
if (focused) {
|
if (focused) {
|
||||||
win.wakeup = true;
|
win.wakeup = true;
|
||||||
win.cursor_timer.start(cursorTimerCallback, 0, win.cursor_timer.getRepeat()) catch unreachable;
|
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;
|
const win = t.getData(Window) orelse return;
|
||||||
win.grid.cursor_visible = !win.grid.cursor_visible;
|
win.grid.cursor_visible = !win.grid.cursor_visible;
|
||||||
win.grid.updateCells(win.terminal) catch unreachable;
|
win.grid.updateCells(win.terminal) catch unreachable;
|
||||||
|
win.render_timer.schedule() catch unreachable;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ttyReadAlloc(t: *libuv.Tty, size: usize) ?[]u8 {
|
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
|
// Update the cells for drawing
|
||||||
win.grid.updateCells(win.terminal) catch unreachable;
|
win.grid.updateCells(win.terminal) catch unreachable;
|
||||||
|
|
||||||
|
// Schedule a render
|
||||||
|
win.render_timer.schedule() catch unreachable;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ttyWrite(req: *libuv.WriteReq, status: i32) void {
|
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});
|
//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
88
src/max_timer.zig
Normal 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
Reference in New Issue
Block a user