diff options
Diffstat (limited to 'vendor/libghostty-vt/src/renderer/Thread.zig')
| -rw-r--r-- | vendor/libghostty-vt/src/renderer/Thread.zig | 715 |
1 files changed, 715 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/renderer/Thread.zig b/vendor/libghostty-vt/src/renderer/Thread.zig new file mode 100644 index 00000000..50872137 --- /dev/null +++ b/vendor/libghostty-vt/src/renderer/Thread.zig @@ -0,0 +1,715 @@ +//! 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("../global.zig").xev; +const crash = @import("../crash/main.zig"); +const internal_os = @import("../os/main.zig"); +const rendererpkg = @import("../renderer.zig"); +const apprt = @import("../apprt.zig"); +const configpkg = @import("../config.zig"); +const BlockingQueue = @import("../datastruct/main.zig").BlockingQueue; +const App = @import("../App.zig"); + +const Allocator = std.mem.Allocator; +const log = std.log.scoped(.renderer_thread); + +const DRAW_INTERVAL = 8; // 120 FPS +const CURSOR_BLINK_INTERVAL = 600; + +/// Whether calls to `drawFrame` must be done from the app thread. +/// +/// If this is `true` then we send a `redraw_surface` message to the apprt +/// whenever we need to draw instead of calling `drawFrame` directly. +const must_draw_from_app_thread = + if (@hasDecl(apprt.App, "must_draw_from_app_thread")) + apprt.App.must_draw_from_app_thread + else + false; + +/// 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(rendererpkg.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, + +/// This async is used to force a draw immediately. This does not +/// coalesce like the wakeup does. +draw_now: xev.Async, +draw_now_c: xev.Completion = .{}, + +/// 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: *rendererpkg.Renderer, + +/// Pointer to the shared state that is used to generate the final render. +state: *rendererpkg.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, + + /// This is true when the view is visible. This is used to determine + /// if we should be rendering or not. + visible: bool = true, + + /// This is true when the view is focused. This defaults to true + /// and it is up to the apprt to set the correct value. + focused: bool = true, +} = .{}, + +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: *rendererpkg.Renderer, + state: *rendererpkg.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(); + + // Draw now async, see comments. + var draw_now = try xev.Async.init(); + errdefer draw_now.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 .{ + .alloc = alloc, + .config = .init(config), + .loop = loop, + .wakeup = wakeup_h, + .stop = stop_h, + .render_h = render_h, + .draw_h = draw_h, + .draw_now = draw_now, + .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.draw_now.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", .{}); + + // Right now, on Darwin, `std.Thread.setName` can only name the current + // thread, and we have no way to get the current thread from within it, + // so instead we use this code to name the thread instead. + if (builtin.os.tag.isDarwin()) { + internal_os.macos.pthread_setname_np(&"renderer".*); + } + + // Setup our crash metadata + crash.sentry.thread_state = .{ + .type = .renderer, + .surface = self.renderer.surface_mailbox.surface, + }; + defer crash.sentry.thread_state = null; + + // Setup our thread QoS + self.setQosClass(); + + // Run our loop start/end callbacks if the renderer cares. + const has_loop = @hasDecl(rendererpkg.Renderer, "loopEnter"); + if (has_loop) try self.renderer.loopEnter(self); + defer if (has_loop) self.renderer.loopExit(); + + // 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); + self.draw_now.wait(&self.loop, &self.draw_now_c, Thread, self, drawNowCallback); + + // 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, + cursorBlinkInterval(), + Thread, + self, + cursorTimerCallback, + ); + + // Start the draw timer + self.syncDrawTimer(); + + // Run + log.debug("starting renderer thread", .{}); + defer log.debug("starting renderer thread shutdown", .{}); + _ = try self.loop.run(.until_done); +} + +fn setQosClass(self: *const Thread) void { + // Thread QoS classes are only relevant on macOS. + if (comptime !builtin.target.os.tag.isDarwin()) return; + + const class: internal_os.macos.QosClass = class: { + // If we aren't visible (our view is fully occluded) then we + // always drop our rendering priority down because it's just + // mostly wasted work. + // + // The renderer itself should be doing this as well (for example + // Metal will stop our DisplayLink) but this also helps with + // general forced updates and CPU usage i.e. a rebuild cells call. + if (!self.flags.visible) break :class .utility; + + // If we're not focused, but we're visible, then we set a higher + // than default priority because framerates still matter but it isn't + // as important as when we're focused. + if (!self.flags.focused) break :class .user_initiated; + + // We are focused and visible, we are the definition of user interactive. + break :class .user_interactive; + }; + + if (internal_os.macos.setQosClass(class)) { + log.debug("thread QoS class set class={}", .{class}); + } else |err| { + log.warn("error setting QoS class err={}", .{err}); + } +} + +fn syncDrawTimer(self: *Thread) void { + skip: { + // If our renderer supports animations and has them, then we + // can apply draw timer based on custom shader animation configuration. + if (@hasDecl(rendererpkg.Renderer, "hasAnimations") and + self.renderer.hasAnimations()) + { + // If our config says to always animate, we do so. + switch (self.config.custom_shader_animation) { + // Always animate + .always => break :skip, + // Only when focused + .true => if (self.flags.focused) break :skip, + // Never animate + .false => {}, + } + } + + // We're skipping the draw timer. Stop it on the next iteration. + self.draw_active = 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, + ); +} + +/// Drain the mailbox. +fn drainMailbox(self: *Thread) !void { + // There's probably a more elegant way to do this... + // + // This is effectively an @autoreleasepool{} block, which we need in + // order to ensure that autoreleased objects are properly released. + const pool = if (builtin.os.tag.isDarwin()) + @import("objc").AutoreleasePool.init() + else + void; + defer if (builtin.os.tag.isDarwin()) pool.deinit(); + + while (self.mailbox.pop()) |message| { + log.debug("mailbox message={}", .{message}); + switch (message) { + .crash => @panic("crash request, crashing intentionally"), + + .visible => |v| visible: { + // If our state didn't change we do nothing. + if (self.flags.visible == v) break :visible; + + // Set our visible state + self.flags.visible = v; + + // Visibility affects our QoS class + self.setQosClass(); + + // If we became visible then we immediately trigger a draw. + // We don't need to update frame data because that should + // still be happening. + if (v) self.drawFrame(false); + + // Notify the renderer so it can update any state. + self.renderer.setVisible(v); + + // Note that we're explicitly today not stopping any + // cursor timers, draw timers, etc. These things have very + // little resource cost and properly maintaining their active + // state across different transitions is going to be bug-prone, + // so its easier to just let them keep firing and have them + // check the visible state themselves to control their behavior. + }, + + .focus => |v| focus: { + // If our state didn't change we do nothing. + if (self.flags.focused == v) break :focus; + + // Set our state + self.flags.focused = v; + + // Focus affects our QoS class + self.setQosClass(); + + // Set it on the renderer + try self.renderer.setFocus(v); + + // We always resync our draw timer (may disable it) + self.syncDrawTimer(); + + if (!v) { + // 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 { + // 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, + cursorBlinkInterval(), + 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, + cursorBlinkInterval(), + Thread, + self, + cursorTimerCallback, + ); + } + }, + + .font_grid => |grid| { + self.renderer.setFontGrid(grid.grid); + grid.set.deref(grid.old_key); + }, + + .resize => |v| self.renderer.setScreenSize(v), + + .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.syncDrawTimer(); + }, + + .search_viewport_matches => |v| { + // Note we don't free the new value because we expect our + // allocators to match. + if (self.renderer.search_matches) |*m| m.arena.deinit(); + self.renderer.search_matches = v; + self.renderer.search_matches_dirty = true; + }, + + .search_selected_match => |v| { + // Note we don't free the new value because we expect our + // allocators to match. + if (self.renderer.search_selected_match) |*m| m.arena.deinit(); + self.renderer.search_selected_match = v; + self.renderer.search_matches_dirty = true; + }, + + .inspector => |v| { + self.flags.has_inspector = v; + }, + + .macos_display_id => |v| { + if (@hasDecl(rendererpkg.Renderer, "setMacOSDisplayID")) { + try self.renderer.setMacOSDisplayID(v); + } + }, + } + } +} + +fn changeConfig(self: *Thread, config: *const DerivedConfig) !void { + self.config = config.*; +} + +/// Trigger a draw. This will not update frame data or anything, it will +/// just trigger a draw/paint. +fn drawFrame(self: *Thread, now: bool) void { + // If we're invisible, we do not draw. + if (!self.flags.visible) return; + + // If the renderer is managing a vsync on its own, we only draw + // when we're forced to via `now`. + if (!now and self.renderer.hasVsync()) return; + + if (must_draw_from_app_thread) { + _ = self.app_mailbox.push( + .{ .redraw_surface = self.surface }, + .{ .instant = {} }, + ); + } else { + self.renderer.drawFrame(false) catch |err| + log.warn("error drawing err={}", .{err}); + } +} + +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}); + + // Render immediately + _ = renderCallback(t, undefined, undefined, {}); + + // The below is not used anymore but if we ever want to introduce + // a configuration to introduce a delay to coalesce renders, we can + // use this. + // + // // 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 drawNowCallback( + self_: ?*Thread, + _: *xev.Loop, + _: *xev.Completion, + r: xev.Async.WaitError!void, +) xev.CallbackAction { + _ = r catch |err| { + log.err("error in draw now err={}", .{err}); + return .rearm; + }; + + // Draw immediately + const t = self_.?; + t.drawFrame(true); + + return .rearm; +} + +fn drawCallback( + self_: ?*Thread, + _: *xev.Loop, + _: *xev.Completion, + r: xev.Timer.RunError!void, +) xev.CallbackAction { + _ = r catch unreachable; + const t: *Thread = self_ orelse { + // This shouldn't happen so we log it. + log.warn("render callback fired without data set", .{}); + return .disarm; + }; + + // Draw + t.drawFrame(false); + + // 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: *Thread = self_ orelse { + // This shouldn't happen so we log it. + log.warn("render callback fired without data set", .{}); + return .disarm; + }; + + // Update our frame data + t.renderer.updateFrame( + t.state, + t.flags.cursor_blink_visible, + ) catch |err| + log.warn("error rendering err={}", .{err}); + + // Draw + t.drawFrame(false); + + 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: *Thread = 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, + cursorBlinkInterval(), + 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; +} + +/// Returns the interval for the blinking cursor in milliseconds. +fn cursorBlinkInterval() u64 { + if (std.valgrind.runningOnValgrind() > 0) { + // If we're running under Valgrind, the cursor blink adds enough + // churn that it makes some stalls annoying unless you're on a + // super powerful computer, so we delay it. + // + // This is a hack, we should change some of our cursor timer + // logic to be more efficient: + // https://github.com/ghostty-org/ghostty/issues/8003 + return CURSOR_BLINK_INTERVAL * 5; + } + + return CURSOR_BLINK_INTERVAL; +} |
