This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690//! 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 assert = std.debug.assert;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 FPSconst 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 statealloc: 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 renderingrender_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 blinkingcursor_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 threadapp_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, 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 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 startDrawTimer(self: *Thread) void { // If our renderer doesn't support animations then we never run this. if (!@hasDecl(rendererpkg.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 { // 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);
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_grid => |grid| { self.renderer.setFontGrid(grid.grid); grid.set.deref(grid.old_key); },
.foreground_color => |color| { self.renderer.foreground_color = color; self.renderer.markDirty(); },
.background_color => |color| { self.renderer.background_color = color; self.renderer.markDirty(); },
.cursor_color => |color| { self.renderer.cursor_color = color; self.renderer.markDirty(); },
.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.stopDrawTimer(); self.startDrawTimer(); },
.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; };
// 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.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, 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;}