mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 08:46:08 +03:00
347 lines
11 KiB
Zig
347 lines
11 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 glfw = @import("glfw");
|
|
const libuv = @import("libuv");
|
|
const renderer = @import("../renderer.zig");
|
|
const BlockingQueue = @import("../blocking_queue.zig").BlockingQueue;
|
|
const tracy = @import("tracy");
|
|
const trace = tracy.trace;
|
|
|
|
const Allocator = std.mem.Allocator;
|
|
const log = std.log.scoped(.renderer_thread);
|
|
|
|
/// 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);
|
|
|
|
/// 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: libuv.Loop,
|
|
|
|
/// This can be used to wake up the renderer and force a render safely from
|
|
/// any thread.
|
|
wakeup: libuv.Async,
|
|
|
|
/// This can be used to stop the renderer on the next loop iteration.
|
|
stop: libuv.Async,
|
|
|
|
/// The timer used for rendering
|
|
render_h: libuv.Timer,
|
|
|
|
/// The timer used for cursor blinking
|
|
cursor_h: libuv.Timer,
|
|
|
|
/// The windo we're rendering to.
|
|
window: glfw.Window,
|
|
|
|
/// 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,
|
|
|
|
/// 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,
|
|
window: glfw.Window,
|
|
renderer_impl: *renderer.Renderer,
|
|
state: *renderer.State,
|
|
) !Thread {
|
|
// We always store allocator pointer on the loop data so that
|
|
// handles can use our global allocator.
|
|
const allocPtr = try alloc.create(Allocator);
|
|
errdefer alloc.destroy(allocPtr);
|
|
allocPtr.* = alloc;
|
|
|
|
// Create our event loop.
|
|
var loop = try libuv.Loop.init(alloc);
|
|
errdefer {
|
|
// Run the loop once to close any of our handles
|
|
_ = loop.run(.nowait) catch 0;
|
|
loop.deinit(alloc);
|
|
}
|
|
loop.setData(allocPtr);
|
|
|
|
// This async handle is used to "wake up" the renderer and force a render.
|
|
var wakeup_h = try libuv.Async.init(alloc, loop, wakeupCallback);
|
|
errdefer wakeup_h.close((struct {
|
|
fn callback(h: *libuv.Async) void {
|
|
const loop_alloc = h.loop().getData(Allocator).?.*;
|
|
h.deinit(loop_alloc);
|
|
}
|
|
}).callback);
|
|
|
|
// This async handle is used to stop the loop and force the thread to end.
|
|
var stop_h = try libuv.Async.init(alloc, loop, stopCallback);
|
|
errdefer stop_h.close((struct {
|
|
fn callback(h: *libuv.Async) void {
|
|
const loop_alloc = h.loop().getData(Allocator).?.*;
|
|
h.deinit(loop_alloc);
|
|
}
|
|
}).callback);
|
|
|
|
// The primary timer for rendering.
|
|
var render_h = try libuv.Timer.init(alloc, loop);
|
|
errdefer render_h.close((struct {
|
|
fn callback(h: *libuv.Timer) void {
|
|
const loop_alloc = h.loop().getData(Allocator).?.*;
|
|
h.deinit(loop_alloc);
|
|
}
|
|
}).callback);
|
|
|
|
// Setup a timer for blinking the cursor
|
|
var cursor_timer = try libuv.Timer.init(alloc, loop);
|
|
errdefer cursor_timer.close((struct {
|
|
fn callback(t: *libuv.Timer) void {
|
|
const alloc_h = t.loop().getData(Allocator).?.*;
|
|
t.deinit(alloc_h);
|
|
}
|
|
}).callback);
|
|
|
|
// The mailbox for messaging this thread
|
|
var mailbox = try Mailbox.create(alloc);
|
|
errdefer mailbox.destroy(alloc);
|
|
|
|
return Thread{
|
|
.loop = loop,
|
|
.wakeup = wakeup_h,
|
|
.stop = stop_h,
|
|
.render_h = render_h,
|
|
.cursor_h = cursor_timer,
|
|
.window = window,
|
|
.renderer = renderer_impl,
|
|
.state = state,
|
|
.mailbox = 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 {
|
|
// Get a copy to our allocator
|
|
const alloc_ptr = self.loop.getData(Allocator).?;
|
|
const alloc = alloc_ptr.*;
|
|
|
|
// Schedule our handles to close
|
|
self.stop.close((struct {
|
|
fn callback(h: *libuv.Async) void {
|
|
const handle_alloc = h.loop().getData(Allocator).?.*;
|
|
h.deinit(handle_alloc);
|
|
}
|
|
}).callback);
|
|
self.wakeup.close((struct {
|
|
fn callback(h: *libuv.Async) void {
|
|
const handle_alloc = h.loop().getData(Allocator).?.*;
|
|
h.deinit(handle_alloc);
|
|
}
|
|
}).callback);
|
|
self.render_h.close((struct {
|
|
fn callback(h: *libuv.Timer) void {
|
|
const handle_alloc = h.loop().getData(Allocator).?.*;
|
|
h.deinit(handle_alloc);
|
|
}
|
|
}).callback);
|
|
self.cursor_h.close((struct {
|
|
fn callback(h: *libuv.Timer) void {
|
|
const handle_alloc = h.loop().getData(Allocator).?.*;
|
|
h.deinit(handle_alloc);
|
|
}
|
|
}).callback);
|
|
|
|
// Run the loop one more time, because destroying our other things
|
|
// like windows usually cancel all our event loop stuff and we need
|
|
// one more run through to finalize all the closes.
|
|
_ = self.loop.run(.default) catch |err|
|
|
log.err("error finalizing event loop: {}", .{err});
|
|
|
|
// Nothing can possibly access the mailbox anymore, destroy it.
|
|
self.mailbox.destroy(alloc);
|
|
|
|
// Dealloc our allocator copy
|
|
alloc.destroy(alloc_ptr);
|
|
|
|
self.loop.deinit(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 {
|
|
tracy.setThreadName("renderer");
|
|
|
|
// 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.window);
|
|
defer self.renderer.threadExit();
|
|
|
|
// Set up our async handler to support rendering
|
|
self.wakeup.setData(self);
|
|
defer self.wakeup.setData(null);
|
|
|
|
// Set up our timer and start it for rendering
|
|
self.render_h.setData(self);
|
|
defer self.render_h.setData(null);
|
|
try self.wakeup.send();
|
|
|
|
// Setup a timer for blinking the cursor
|
|
self.cursor_h.setData(self);
|
|
try self.cursor_h.start(cursorTimerCallback, 600, 600);
|
|
|
|
// If we are using tracy, then we setup a prepare handle so that
|
|
// we can mark the frame.
|
|
var frame_h: libuv.Prepare = if (!tracy.enabled) undefined else frame_h: {
|
|
const alloc_ptr = self.loop.getData(Allocator).?;
|
|
const alloc = alloc_ptr.*;
|
|
const h = try libuv.Prepare.init(alloc, self.loop);
|
|
h.setData(self);
|
|
try h.start(prepFrameCallback);
|
|
|
|
break :frame_h h;
|
|
};
|
|
defer if (tracy.enabled) {
|
|
frame_h.close((struct {
|
|
fn callback(h: *libuv.Prepare) void {
|
|
const alloc_h = h.loop().getData(Allocator).?.*;
|
|
h.deinit(alloc_h);
|
|
}
|
|
}).callback);
|
|
_ = self.loop.run(.nowait) catch {};
|
|
};
|
|
|
|
// Run
|
|
log.debug("starting renderer thread", .{});
|
|
defer log.debug("exiting renderer thread", .{});
|
|
_ = try self.loop.run(.default);
|
|
}
|
|
|
|
/// Drain the mailbox.
|
|
fn drainMailbox(self: *Thread) !void {
|
|
const zone = trace(@src());
|
|
defer zone.end();
|
|
|
|
// This holds the mailbox lock for the duration of the drain. The
|
|
// expectation is that all our message handlers will be non-blocking
|
|
// ENOUGH to not mess up throughput on producers.
|
|
|
|
var drain = self.mailbox.drain();
|
|
defer drain.deinit();
|
|
|
|
while (drain.next()) |message| {
|
|
log.debug("mailbox message={}", .{message});
|
|
switch (message) {
|
|
.focus => |v| {
|
|
// Set it on the renderer
|
|
try self.renderer.setFocus(v);
|
|
|
|
if (!v) {
|
|
// If we're not focused, then we stop the cursor blink
|
|
try self.cursor_h.stop();
|
|
} else {
|
|
// If we're focused, we immediately show the cursor again
|
|
// and then restart the timer.
|
|
if (!try self.cursor_h.isActive()) {
|
|
self.renderer.blinkCursor(true);
|
|
try self.cursor_h.start(
|
|
cursorTimerCallback,
|
|
self.cursor_h.getRepeat(),
|
|
self.cursor_h.getRepeat(),
|
|
);
|
|
}
|
|
}
|
|
},
|
|
|
|
.reset_cursor_blink => {
|
|
self.renderer.blinkCursor(true);
|
|
if (try self.cursor_h.isActive()) {
|
|
_ = try self.cursor_h.again();
|
|
}
|
|
},
|
|
|
|
.font_size => |size| {
|
|
try self.renderer.setFontSize(size);
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
fn wakeupCallback(h: *libuv.Async) void {
|
|
const zone = trace(@src());
|
|
defer zone.end();
|
|
|
|
const t = h.getData(Thread) orelse {
|
|
// This shouldn't happen so we log it.
|
|
log.warn("render callback fired without data set", .{});
|
|
return;
|
|
};
|
|
|
|
// 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.
|
|
const active = t.render_h.isActive() catch true;
|
|
if (active) return;
|
|
|
|
// Timer is not active, let's start it
|
|
t.render_h.start(renderCallback, 10, 0) catch |err|
|
|
log.warn("render timer failed to start err={}", .{err});
|
|
}
|
|
|
|
fn renderCallback(h: *libuv.Timer) void {
|
|
const zone = trace(@src());
|
|
defer zone.end();
|
|
|
|
const t = h.getData(Thread) orelse {
|
|
// This shouldn't happen so we log it.
|
|
log.warn("render callback fired without data set", .{});
|
|
return;
|
|
};
|
|
|
|
t.renderer.render(t.window, t.state) catch |err|
|
|
log.warn("error rendering err={}", .{err});
|
|
}
|
|
|
|
fn cursorTimerCallback(h: *libuv.Timer) void {
|
|
const zone = trace(@src());
|
|
defer zone.end();
|
|
|
|
const t = h.getData(Thread) orelse {
|
|
// This shouldn't happen so we log it.
|
|
log.warn("render callback fired without data set", .{});
|
|
return;
|
|
};
|
|
|
|
t.renderer.blinkCursor(false);
|
|
t.wakeup.send() catch {};
|
|
}
|
|
|
|
fn prepFrameCallback(h: *libuv.Prepare) void {
|
|
_ = h;
|
|
|
|
tracy.frameMark();
|
|
}
|
|
|
|
fn stopCallback(h: *libuv.Async) void {
|
|
h.loop().stop();
|
|
}
|