mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
526 lines
16 KiB
Zig
526 lines
16 KiB
Zig
//! Represents the renderer thread logic. The renderer thread is able to
|
|
//! be woken up to render.
|
|
pub const Thread = @This();
|
|
|
|
const std = @import("std");
|
|
const builtin = @import("builtin");
|
|
const xev = @import("xev");
|
|
const renderer = @import("../renderer.zig");
|
|
const apprt = @import("../apprt.zig");
|
|
const configpkg = @import("../config.zig");
|
|
const BlockingQueue = @import("../blocking_queue.zig").BlockingQueue;
|
|
const App = @import("../App.zig");
|
|
|
|
const Allocator = std.mem.Allocator;
|
|
const log = std.log.scoped(.renderer_thread);
|
|
|
|
const DRAW_INTERVAL = 33; // 30 FPS
|
|
const CURSOR_BLINK_INTERVAL = 600;
|
|
|
|
/// The type used for sending messages to the IO thread. For now this is
|
|
/// hardcoded with a capacity. We can make this a comptime parameter in
|
|
/// the future if we want it configurable.
|
|
pub const Mailbox = BlockingQueue(renderer.Message, 64);
|
|
|
|
/// Allocator used for some state
|
|
alloc: std.mem.Allocator,
|
|
|
|
/// The main event loop for the application. The user data of this loop
|
|
/// is always the allocator used to create the loop. This is a convenience
|
|
/// so that users of the loop always have an allocator.
|
|
loop: xev.Loop,
|
|
|
|
/// This can be used to wake up the renderer and force a render safely from
|
|
/// any thread.
|
|
wakeup: xev.Async,
|
|
wakeup_c: xev.Completion = .{},
|
|
|
|
/// This can be used to stop the renderer on the next loop iteration.
|
|
stop: xev.Async,
|
|
stop_c: xev.Completion = .{},
|
|
|
|
/// The timer used for rendering
|
|
render_h: xev.Timer,
|
|
render_c: xev.Completion = .{},
|
|
|
|
/// The timer used for draw calls. Draw calls don't update from the
|
|
/// terminal state so they're much cheaper. They're used for animation
|
|
/// and are paused when the terminal is not focused.
|
|
draw_h: xev.Timer,
|
|
draw_c: xev.Completion = .{},
|
|
draw_active: bool = false,
|
|
|
|
/// The timer used for cursor blinking
|
|
cursor_h: xev.Timer,
|
|
cursor_c: xev.Completion = .{},
|
|
cursor_c_cancel: xev.Completion = .{},
|
|
|
|
/// The surface we're rendering to.
|
|
surface: *apprt.Surface,
|
|
|
|
/// The underlying renderer implementation.
|
|
renderer: *renderer.Renderer,
|
|
|
|
/// Pointer to the shared state that is used to generate the final render.
|
|
state: *renderer.State,
|
|
|
|
/// The mailbox that can be used to send this thread messages. Note
|
|
/// this is a blocking queue so if it is full you will get errors (or block).
|
|
mailbox: *Mailbox,
|
|
|
|
/// Mailbox to send messages to the app thread
|
|
app_mailbox: App.Mailbox,
|
|
|
|
/// Configuration we need derived from the main config.
|
|
config: DerivedConfig,
|
|
|
|
flags: packed struct {
|
|
/// This is true when a blinking cursor should be visible and false
|
|
/// when it should not be visible. This is toggled on a timer by the
|
|
/// thread automatically.
|
|
cursor_blink_visible: bool = false,
|
|
|
|
/// This is true when the inspector is active.
|
|
has_inspector: bool = false,
|
|
} = .{},
|
|
|
|
pub const DerivedConfig = struct {
|
|
custom_shader_animation: configpkg.CustomShaderAnimation,
|
|
|
|
pub fn init(config: *const configpkg.Config) DerivedConfig {
|
|
return .{
|
|
.custom_shader_animation = config.@"custom-shader-animation",
|
|
};
|
|
}
|
|
};
|
|
|
|
/// Initialize the thread. This does not START the thread. This only sets
|
|
/// up all the internal state necessary prior to starting the thread. It
|
|
/// is up to the caller to start the thread with the threadMain entrypoint.
|
|
pub fn init(
|
|
alloc: Allocator,
|
|
config: *const configpkg.Config,
|
|
surface: *apprt.Surface,
|
|
renderer_impl: *renderer.Renderer,
|
|
state: *renderer.State,
|
|
app_mailbox: App.Mailbox,
|
|
) !Thread {
|
|
// Create our event loop.
|
|
var loop = try xev.Loop.init(.{});
|
|
errdefer loop.deinit();
|
|
|
|
// This async handle is used to "wake up" the renderer and force a render.
|
|
var wakeup_h = try xev.Async.init();
|
|
errdefer wakeup_h.deinit();
|
|
|
|
// This async handle is used to stop the loop and force the thread to end.
|
|
var stop_h = try xev.Async.init();
|
|
errdefer stop_h.deinit();
|
|
|
|
// The primary timer for rendering.
|
|
var render_h = try xev.Timer.init();
|
|
errdefer render_h.deinit();
|
|
|
|
// Draw timer, see comments.
|
|
var draw_h = try xev.Timer.init();
|
|
errdefer draw_h.deinit();
|
|
|
|
// Setup a timer for blinking the cursor
|
|
var cursor_timer = try xev.Timer.init();
|
|
errdefer cursor_timer.deinit();
|
|
|
|
// The mailbox for messaging this thread
|
|
var mailbox = try Mailbox.create(alloc);
|
|
errdefer mailbox.destroy(alloc);
|
|
|
|
return Thread{
|
|
.alloc = alloc,
|
|
.config = DerivedConfig.init(config),
|
|
.loop = loop,
|
|
.wakeup = wakeup_h,
|
|
.stop = stop_h,
|
|
.render_h = render_h,
|
|
.draw_h = draw_h,
|
|
.cursor_h = cursor_timer,
|
|
.surface = surface,
|
|
.renderer = renderer_impl,
|
|
.state = state,
|
|
.mailbox = mailbox,
|
|
.app_mailbox = app_mailbox,
|
|
};
|
|
}
|
|
|
|
/// Clean up the thread. This is only safe to call once the thread
|
|
/// completes executing; the caller must join prior to this.
|
|
pub fn deinit(self: *Thread) void {
|
|
self.stop.deinit();
|
|
self.wakeup.deinit();
|
|
self.render_h.deinit();
|
|
self.draw_h.deinit();
|
|
self.cursor_h.deinit();
|
|
self.loop.deinit();
|
|
|
|
// Nothing can possibly access the mailbox anymore, destroy it.
|
|
self.mailbox.destroy(self.alloc);
|
|
}
|
|
|
|
/// The main entrypoint for the thread.
|
|
pub fn threadMain(self: *Thread) void {
|
|
// Call child function so we can use errors...
|
|
self.threadMain_() catch |err| {
|
|
// In the future, we should expose this on the thread struct.
|
|
log.warn("error in renderer err={}", .{err});
|
|
};
|
|
}
|
|
|
|
fn threadMain_(self: *Thread) !void {
|
|
defer log.debug("renderer thread exited", .{});
|
|
|
|
// 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.
|
|
try self.renderer.threadEnter(self.surface);
|
|
defer self.renderer.threadExit();
|
|
|
|
// Start the async handlers
|
|
self.wakeup.wait(&self.loop, &self.wakeup_c, Thread, self, wakeupCallback);
|
|
self.stop.wait(&self.loop, &self.stop_c, Thread, self, stopCallback);
|
|
|
|
// Send an initial wakeup message so that we render right away.
|
|
try self.wakeup.notify();
|
|
|
|
// Start blinking the cursor.
|
|
self.cursor_h.run(
|
|
&self.loop,
|
|
&self.cursor_c,
|
|
CURSOR_BLINK_INTERVAL,
|
|
Thread,
|
|
self,
|
|
cursorTimerCallback,
|
|
);
|
|
|
|
// Start the draw timer
|
|
self.startDrawTimer();
|
|
|
|
// Run
|
|
log.debug("starting renderer thread", .{});
|
|
defer log.debug("starting renderer thread shutdown", .{});
|
|
_ = try self.loop.run(.until_done);
|
|
}
|
|
|
|
fn startDrawTimer(self: *Thread) void {
|
|
// If our renderer doesn't suppoort animations then we never run this.
|
|
if (!@hasDecl(renderer.Renderer, "hasAnimations")) return;
|
|
if (!self.renderer.hasAnimations()) return;
|
|
if (self.config.custom_shader_animation == .false) return;
|
|
|
|
// Set our active state so it knows we're running. We set this before
|
|
// even checking the active state in case we have a pending shutdown.
|
|
self.draw_active = true;
|
|
|
|
// If our draw timer is already active, then we don't have to do anything.
|
|
if (self.draw_c.state() == .active) return;
|
|
|
|
// Start the timer which loops
|
|
self.draw_h.run(
|
|
&self.loop,
|
|
&self.draw_c,
|
|
DRAW_INTERVAL,
|
|
Thread,
|
|
self,
|
|
drawCallback,
|
|
);
|
|
}
|
|
|
|
fn stopDrawTimer(self: *Thread) void {
|
|
// This will stop the draw on the next iteration.
|
|
self.draw_active = false;
|
|
}
|
|
|
|
/// Drain the mailbox.
|
|
fn drainMailbox(self: *Thread) !void {
|
|
while (self.mailbox.pop()) |message| {
|
|
log.debug("mailbox message={}", .{message});
|
|
switch (message) {
|
|
.focus => |v| {
|
|
// Set it on the renderer
|
|
try self.renderer.setFocus(v);
|
|
|
|
if (!v) {
|
|
if (self.config.custom_shader_animation != .always) {
|
|
// Stop the draw timer
|
|
self.stopDrawTimer();
|
|
}
|
|
|
|
// If we're not focused, then we stop the cursor blink
|
|
if (self.cursor_c.state() == .active and
|
|
self.cursor_c_cancel.state() == .dead)
|
|
{
|
|
self.cursor_h.cancel(
|
|
&self.loop,
|
|
&self.cursor_c,
|
|
&self.cursor_c_cancel,
|
|
void,
|
|
null,
|
|
cursorCancelCallback,
|
|
);
|
|
}
|
|
} else {
|
|
// Start the draw timer
|
|
self.startDrawTimer();
|
|
|
|
// If we're focused, we immediately show the cursor again
|
|
// and then restart the timer.
|
|
if (self.cursor_c.state() != .active) {
|
|
self.flags.cursor_blink_visible = true;
|
|
self.cursor_h.run(
|
|
&self.loop,
|
|
&self.cursor_c,
|
|
CURSOR_BLINK_INTERVAL,
|
|
Thread,
|
|
self,
|
|
cursorTimerCallback,
|
|
);
|
|
}
|
|
}
|
|
},
|
|
|
|
.reset_cursor_blink => {
|
|
self.flags.cursor_blink_visible = true;
|
|
if (self.cursor_c.state() == .active) {
|
|
self.cursor_h.reset(
|
|
&self.loop,
|
|
&self.cursor_c,
|
|
&self.cursor_c_cancel,
|
|
CURSOR_BLINK_INTERVAL,
|
|
Thread,
|
|
self,
|
|
cursorTimerCallback,
|
|
);
|
|
}
|
|
},
|
|
|
|
.font_size => |size| {
|
|
try self.renderer.setFontSize(size);
|
|
},
|
|
|
|
.foreground_color => |color| {
|
|
self.renderer.foreground_color = color;
|
|
},
|
|
|
|
.background_color => |color| {
|
|
self.renderer.background_color = color;
|
|
},
|
|
|
|
.cursor_color => |color| {
|
|
self.renderer.cursor_color = color;
|
|
},
|
|
|
|
.resize => |v| {
|
|
try self.renderer.setScreenSize(v.screen_size, v.padding);
|
|
},
|
|
|
|
.change_config => |config| {
|
|
defer config.alloc.destroy(config.thread);
|
|
defer config.alloc.destroy(config.impl);
|
|
try self.changeConfig(config.thread);
|
|
try self.renderer.changeConfig(config.impl);
|
|
|
|
// Stop and start the draw timer to capture the new
|
|
// hasAnimations value.
|
|
self.stopDrawTimer();
|
|
self.startDrawTimer();
|
|
},
|
|
|
|
.inspector => |v| self.flags.has_inspector = v,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn changeConfig(self: *Thread, config: *const DerivedConfig) !void {
|
|
self.config = config.*;
|
|
}
|
|
|
|
fn wakeupCallback(
|
|
self_: ?*Thread,
|
|
_: *xev.Loop,
|
|
_: *xev.Completion,
|
|
r: xev.Async.WaitError!void,
|
|
) xev.CallbackAction {
|
|
_ = r catch |err| {
|
|
log.err("error in wakeup err={}", .{err});
|
|
return .rearm;
|
|
};
|
|
|
|
const t = self_.?;
|
|
|
|
// When we wake up, we check the mailbox. Mailbox producers should
|
|
// wake up our thread after publishing.
|
|
t.drainMailbox() catch |err|
|
|
log.err("error draining mailbox err={}", .{err});
|
|
|
|
// If the timer is already active then we don't have to do anything.
|
|
if (t.render_c.state() == .active) return .rearm;
|
|
|
|
// Timer is not active, let's start it
|
|
t.render_h.run(
|
|
&t.loop,
|
|
&t.render_c,
|
|
10,
|
|
Thread,
|
|
t,
|
|
renderCallback,
|
|
);
|
|
|
|
return .rearm;
|
|
}
|
|
|
|
fn drawCallback(
|
|
self_: ?*Thread,
|
|
_: *xev.Loop,
|
|
_: *xev.Completion,
|
|
r: xev.Timer.RunError!void,
|
|
) xev.CallbackAction {
|
|
_ = r catch unreachable;
|
|
const t = self_ orelse {
|
|
// This shouldn't happen so we log it.
|
|
log.warn("render callback fired without data set", .{});
|
|
return .disarm;
|
|
};
|
|
|
|
// If we're doing single-threaded GPU calls then we just wake up the
|
|
// app thread to redraw at this point.
|
|
if (renderer.Renderer == renderer.OpenGL and
|
|
renderer.OpenGL.single_threaded_draw)
|
|
{
|
|
_ = t.app_mailbox.push(
|
|
.{ .redraw_surface = t.surface },
|
|
.{ .instant = {} },
|
|
);
|
|
} else {
|
|
t.renderer.drawFrame(t.surface) catch |err|
|
|
log.warn("error drawing err={}", .{err});
|
|
}
|
|
|
|
// Only continue if we're still active
|
|
if (t.draw_active) {
|
|
t.draw_h.run(&t.loop, &t.draw_c, DRAW_INTERVAL, Thread, t, drawCallback);
|
|
}
|
|
|
|
return .disarm;
|
|
}
|
|
|
|
fn renderCallback(
|
|
self_: ?*Thread,
|
|
_: *xev.Loop,
|
|
_: *xev.Completion,
|
|
r: xev.Timer.RunError!void,
|
|
) xev.CallbackAction {
|
|
_ = r catch unreachable;
|
|
const t = self_ orelse {
|
|
// This shouldn't happen so we log it.
|
|
log.warn("render callback fired without data set", .{});
|
|
return .disarm;
|
|
};
|
|
|
|
// If we have an inspector, let the app know we want to rerender that.
|
|
if (t.flags.has_inspector) {
|
|
_ = t.app_mailbox.push(.{ .redraw_inspector = t.surface }, .{ .instant = {} });
|
|
}
|
|
|
|
// Update our frame data
|
|
t.renderer.updateFrame(
|
|
t.surface,
|
|
t.state,
|
|
t.flags.cursor_blink_visible,
|
|
) catch |err|
|
|
log.warn("error rendering err={}", .{err});
|
|
|
|
// If we're doing single-threaded GPU calls then we also wake up the
|
|
// app thread to redraw at this point.
|
|
if (renderer.Renderer == renderer.OpenGL and
|
|
renderer.OpenGL.single_threaded_draw)
|
|
{
|
|
_ = t.app_mailbox.push(.{ .redraw_surface = t.surface }, .{ .instant = {} });
|
|
return .disarm;
|
|
}
|
|
|
|
// Draw
|
|
t.renderer.drawFrame(t.surface) catch |err|
|
|
log.warn("error drawing err={}", .{err});
|
|
|
|
return .disarm;
|
|
}
|
|
|
|
fn cursorTimerCallback(
|
|
self_: ?*Thread,
|
|
_: *xev.Loop,
|
|
_: *xev.Completion,
|
|
r: xev.Timer.RunError!void,
|
|
) xev.CallbackAction {
|
|
_ = r catch |err| switch (err) {
|
|
// This is sent when our timer is canceled. That's fine.
|
|
error.Canceled => return .disarm,
|
|
|
|
else => {
|
|
log.warn("error in cursor timer callback err={}", .{err});
|
|
unreachable;
|
|
},
|
|
};
|
|
|
|
const t = self_ orelse {
|
|
// This shouldn't happen so we log it.
|
|
log.warn("render callback fired without data set", .{});
|
|
return .disarm;
|
|
};
|
|
|
|
t.flags.cursor_blink_visible = !t.flags.cursor_blink_visible;
|
|
t.wakeup.notify() catch {};
|
|
|
|
t.cursor_h.run(&t.loop, &t.cursor_c, CURSOR_BLINK_INTERVAL, Thread, t, cursorTimerCallback);
|
|
return .disarm;
|
|
}
|
|
|
|
fn cursorCancelCallback(
|
|
_: ?*void,
|
|
_: *xev.Loop,
|
|
_: *xev.Completion,
|
|
r: xev.Timer.CancelError!void,
|
|
) xev.CallbackAction {
|
|
// This makes it easier to work across platforms where different platforms
|
|
// support different sets of errors, so we just unify it.
|
|
const CancelError = xev.Timer.CancelError || error{
|
|
Canceled,
|
|
NotFound,
|
|
Unexpected,
|
|
};
|
|
|
|
_ = r catch |err| switch (@as(CancelError, @errorCast(err))) {
|
|
error.Canceled => {}, // success
|
|
error.NotFound => {}, // completed before it could cancel
|
|
else => {
|
|
log.warn("error in cursor cancel callback err={}", .{err});
|
|
unreachable;
|
|
},
|
|
};
|
|
|
|
return .disarm;
|
|
}
|
|
|
|
// fn prepFrameCallback(h: *libuv.Prepare) void {
|
|
// _ = h;
|
|
//
|
|
// tracy.frameMark();
|
|
// }
|
|
|
|
fn stopCallback(
|
|
self_: ?*Thread,
|
|
_: *xev.Loop,
|
|
_: *xev.Completion,
|
|
r: xev.Async.WaitError!void,
|
|
) xev.CallbackAction {
|
|
_ = r catch unreachable;
|
|
self_.?.loop.stop();
|
|
return .disarm;
|
|
}
|