aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/renderer/generic.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-10 21:51:57 +0300
committerGitHub <noreply@github.com>2026-09-10 21:51:57 +0300
commit425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch)
treee52c4383a6d69216cbe2a32638bf90841d7e2189 /vendor/libghostty-vt/src/renderer/generic.zig
parent90e947a6e26e02f10ec7f1694c4eb5b78a86406b (diff)
chore: upgrade libghostty and preserve terminal behavior (#3906)
Diffstat (limited to 'vendor/libghostty-vt/src/renderer/generic.zig')
-rw-r--r--vendor/libghostty-vt/src/renderer/generic.zig540
1 files changed, 370 insertions, 170 deletions
diff --git a/vendor/libghostty-vt/src/renderer/generic.zig b/vendor/libghostty-vt/src/renderer/generic.zig
index 14b79036..e376e1bf 100644
--- a/vendor/libghostty-vt/src/renderer/generic.zig
+++ b/vendor/libghostty-vt/src/renderer/generic.zig
@@ -1,6 +1,7 @@
const std = @import("std");
const builtin = @import("builtin");
-const xev = @import("xev");
+const global = @import("../global.zig");
+const xev = global.xev;
const wuffs = @import("wuffs");
const apprt = @import("../apprt.zig");
const configpkg = @import("../config.zig");
@@ -26,6 +27,7 @@ const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const Terminal = terminal.Terminal;
const Health = renderer.Health;
+const compat_file = @import("../lib/compat/file.zig");
const getConstraint = @import("../font/nerd_font_attributes.zig").getConstraint;
@@ -98,7 +100,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
/// This mutex must be held whenever any state used in `drawFrame` is
/// being modified, and also when it's being accessed in `drawFrame`.
- draw_mutex: std.Thread.Mutex = .{},
+ draw_mutex: std.Io.Mutex = .init,
/// The configuration we need derived from the main config.
config: DerivedConfig,
@@ -115,6 +117,9 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
/// True if the window is focused
focused: bool,
+ /// True if the window is visible.
+ visible: bool,
+
/// Flag to indicate that our focus state changed for custom
/// shaders to update their state.
custom_shader_focused_changed: bool = false,
@@ -161,12 +166,12 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
/// Timestamp we rendered out first frame.
///
/// This is used when updating custom shader uniforms.
- first_frame_time: ?std.time.Instant = null,
+ first_frame_time: ?std.Io.Timestamp = null,
/// Timestamp when we rendered out more recent frame.
///
/// This is used when updating custom shader uniforms.
- last_frame_time: ?std.time.Instant = null,
+ last_frame_time: ?std.Io.Timestamp = null,
/// The font structures.
font_grid: *font.SharedGrid,
@@ -202,8 +207,15 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
/// Health of the most recently completed frame.
health: std.atomic.Value(Health) = .{ .raw = .healthy },
- /// Our swap chain (multiple buffering)
- swap_chain: SwapChain,
+ /// True when we have a graphics context that can create GPU
+ /// resources. Creating any GPU resource while this is false is invalid.
+ display_realized: bool = true,
+
+ /// Our swap chain (multiple buffering). Null when it has
+ /// been released, either because the surface is hidden
+ /// (`releaseGpuResources`) or because the display is
+ /// unrealized. Rebuilt on the next `drawFrame`.
+ swap_chain: ?SwapChain,
/// This value is used to force-update swap chain targets in the
/// event of a config change that requires it (such as blending mode).
@@ -231,6 +243,18 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
/// Our overlay state, if any.
overlay: ?Overlay = null,
+ /// The base timestamp for the Kitty graphics animation clock.
+ /// Animation frame timing is expressed as milliseconds since
+ /// this instant. Set on the first frame update that observes
+ /// Kitty images.
+ kitty_animation_clock: ?std.Io.Timestamp = null,
+
+ /// When the next Kitty animation frame is due, in
+ /// milliseconds on the animation clock, from the most recent
+ /// frame update. Null when no running animation needs a
+ /// wakeup.
+ kitty_animation_next_ms: ?u64 = null,
+
const HighlightTag = enum(u8) {
search_match,
search_match_selected,
@@ -252,15 +276,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
frame_index: std.math.IntFittingRange(0, buf_count) = 0,
/// Semaphore that we wait on to make sure we have an available
/// frame state struct so we can start working on a new frame.
- frame_sema: std.Thread.Semaphore = .{ .permits = buf_count },
-
- /// Set to true when deinited, if you try to deinit a defunct
- /// swap chain it will just be ignored, to prevent double-free.
- ///
- /// This is required because of `displayUnrealized`, since it
- /// `deinits` the swapchain, which leads to a double-free if
- /// the renderer is deinited after that.
- defunct: bool = false,
+ frame_sema: std.Io.Semaphore = .{ .permits = buf_count },
pub fn init(api: GraphicsAPI, custom_shaders: bool) !SwapChain {
var result: SwapChain = .{ .frames = undefined };
@@ -274,30 +290,26 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
}
pub fn deinit(self: *SwapChain) void {
- if (self.defunct) return;
- self.defunct = true;
-
// Wait for all of our inflight draws to complete
// so that we can cleanly deinit our GPU state.
- for (0..buf_count) |_| self.frame_sema.wait();
+ for (0..buf_count) |_| self.frame_sema.waitUncancelable(
+ global.io(),
+ );
for (&self.frames) |*frame| frame.deinit();
}
/// Get the next frame state to draw to. This will wait on the
/// semaphore to ensure that the frame is available. This must
/// always be paired with a call to releaseFrame.
- pub fn nextFrame(self: *SwapChain) error{Defunct}!*FrameState {
- if (self.defunct) return error.Defunct;
-
- self.frame_sema.wait();
- errdefer self.frame_sema.post();
+ pub fn nextFrame(self: *SwapChain) *FrameState {
+ self.frame_sema.waitUncancelable(global.io());
self.frame_index = (self.frame_index + 1) % buf_count;
return &self.frames[self.frame_index];
}
/// This should be called when the frame has completed drawing.
pub fn releaseFrame(self: *SwapChain) void {
- self.frame_sema.post();
+ self.frame_sema.post(global.io());
}
};
@@ -570,6 +582,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
blending: configpkg.Config.AlphaBlending,
background_blur: configpkg.Config.BackgroundBlur,
scroll_to_bottom_on_output: bool,
+ custom_shader_animation: configpkg.CustomShaderAnimation,
pub fn init(
alloc_gpa: Allocator,
@@ -644,6 +657,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
.blending = config.@"alpha-blending",
.background_blur = config.@"background-blur",
.scroll_to_bottom_on_output = config.@"scroll-to-bottom".output,
+ .custom_shader_animation = config.@"custom-shader-animation",
.arena = arena,
};
}
@@ -682,22 +696,13 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
metrics: font.Metrics,
} = font_critical: {
const grid: *font.SharedGrid = options.font_grid;
- grid.lock.lockShared();
- defer grid.lock.unlockShared();
+ grid.lock.lockSharedUncancelable(global.io());
+ defer grid.lock.unlockShared(global.io());
break :font_critical .{
.metrics = grid.metrics,
};
};
- const display_link: ?DisplayLink = switch (builtin.os.tag) {
- .macos => if (options.config.vsync)
- try macos.video.DisplayLink.createWithActiveCGDisplays()
- else
- null,
- else => null,
- };
- errdefer if (display_link) |v| v.release();
-
var result: Self = .{
.alloc = alloc,
.config = options.config,
@@ -705,6 +710,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
.grid_metrics = font_critical.metrics,
.size = options.size,
.focused = true,
+ .visible = true,
.scrollbar = .zero,
.scrollbar_dirty = false,
.last_bottom_node = null,
@@ -783,7 +789,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// Graphics API stuff
.api = api,
.swap_chain = swap_chain,
- .display_link = display_link,
};
try result.initShaders();
@@ -802,7 +807,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
self.terminal_state.deinit(self.alloc);
if (self.search_selected_match) |*m| m.arena.deinit();
if (self.search_matches) |*m| m.arena.deinit();
- self.swap_chain.deinit();
+ if (self.swap_chain) |*sc| sc.deinit();
if (DisplayLink != void) {
if (self.display_link) |display_link| {
@@ -903,16 +908,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// If we don't support a display link we have no work to do.
if (comptime DisplayLink == void) return;
- // This is when we know our "self" pointer is stable so we can
- // setup the display link. To setup the display link we set our
- // callback and we can start it immediately.
- const display_link = self.display_link orelse return;
- try display_link.setOutputCallback(
- xev.Async,
- &displayLinkCallback,
- &thr.draw_now,
- );
- display_link.start() catch {};
+ self.syncDisplayLink(null, &thr.draw_now);
}
/// Called by renderer.Thread when it exits the main loop.
@@ -943,13 +939,13 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// Lock the draw mutex so that we can
// safely reinitialize our GPU resources.
- self.draw_mutex.lock();
- defer self.draw_mutex.unlock();
+ self.draw_mutex.lockUncancelable(global.io());
+ defer self.draw_mutex.unlock(global.io());
// We assume that the swap chain was deinited in
- // `displayUnrealized`, in which case it should be
- // marked defunct. If not, we have a problem.
- assert(self.swap_chain.defunct);
+ // `displayUnrealized`. If not, we have a problem.
+ assert(self.swap_chain == null);
+ assert(!self.display_realized);
// We reinitialize our shaders and our swap chain.
try self.initShaders();
@@ -957,6 +953,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
self.api,
self.has_custom_shaders,
);
+ self.display_realized = true;
self.reinitialize_shaders = false;
self.target_config_modified = 1;
}
@@ -972,14 +969,16 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// Lock the draw mutex so that we can
// safely deinitialize our GPU resources.
- self.draw_mutex.lock();
- defer self.draw_mutex.unlock();
-
- // We deinit our swap chain and shaders.
- //
- // This will mark them as defunct so that they
- // can't be double-freed or used in draw calls.
- self.swap_chain.deinit();
+ self.draw_mutex.lockUncancelable(global.io());
+ defer self.draw_mutex.unlock(global.io());
+
+ // We deinit our swap chain and shaders. Clearing
+ // `display_realized` ensures drawFrame doesn't attempt
+ // to rebuild the swap chain (we have no GPU context);
+ // displayRealized will.
+ if (self.swap_chain) |*sc| sc.deinit();
+ self.swap_chain = null;
+ self.display_realized = false;
self.shaders.deinit(self.alloc);
}
@@ -999,19 +998,83 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
}
/// Called when we get an updated display ID for our display link.
- pub fn setMacOSDisplayID(self: *Self, id: u32) !void {
+ pub fn setMacOSDisplayID(
+ self: *Self,
+ id: u32,
+ draw_now: *xev.Async,
+ ) !void {
if (comptime DisplayLink == void) return;
- const display_link = self.display_link orelse return;
- log.info("updating display link display id={}", .{id});
- display_link.setCurrentCGDisplay(id) catch |err| {
- log.warn("error setting display link display id err={}", .{err});
- };
+ self.syncDisplayLink(id, draw_now);
}
- /// True if our renderer has animations so that a higher frequency
- /// timer is used.
- pub fn hasAnimations(self: *const Self) bool {
- return self.has_custom_shaders;
+ /// The cadence of continuous (draw-only) animation wakes,
+ /// i.e. 120fps, and the floor for any animation wake delay.
+ pub const draw_interval_ms: u64 = 8;
+
+ /// A point in the future when the renderer needs to be driven
+ /// again to keep animating, and what kind of drive it needs.
+ pub const AnimationWake = struct {
+ /// Delay in milliseconds until the wake is due.
+ delay_ms: u64,
+ kind: Kind,
+
+ pub const Kind = enum {
+ /// A redraw alone suffices, no updateFrame. Much cheaper
+ /// than `update`.
+ draw,
+
+ /// Frame data must be updated first: updateFrame, then draw.
+ update,
+ };
+ };
+
+ /// The soonest animation wake this renderer needs, if any:
+ /// custom shader animation wants continuous draw-only wakes
+ /// at draw_interval_ms while active, and a running Kitty
+ /// graphics animation wants an update wake when its next
+ /// frame is due. The renderer thread drives its animation
+ /// timer off this, re-querying after every wake.
+ ///
+ /// Must be called on the render thread.
+ pub fn animationWake(self: *const Self) ?AnimationWake {
+ // Custom shaders animate by redrawing on a fixed cadence,
+ // gated by configuration and focus.
+ const shader_delay: ?u64 = shader: {
+ if (!self.has_custom_shaders) break :shader null;
+ break :shader switch (self.config.custom_shader_animation) {
+ .false => null,
+ .always => draw_interval_ms,
+ .true => if (self.focused) draw_interval_ms else null,
+ };
+ };
+
+ // Kitty animations tick during updateFrame; between
+ // updates the deadline is absolute on the animation
+ // clock, so a stream of draw wakes recomputing this
+ // cannot starve it into the future.
+ const kitty_delay: ?u64 = kitty: {
+ const next = self.kitty_animation_next_ms orelse break :kitty null;
+ const base = self.kitty_animation_clock orelse break :kitty null;
+ const now: std.Io.Timestamp = .now(global.io(), .awake);
+ const now_ms: u64 = @intCast(@divTrunc(
+ base.durationTo(now).nanoseconds,
+ std.time.ns_per_ms,
+ ));
+ // Never wake faster than the draw interval; an
+ // overdue frame is picked up on the next wake.
+ break :kitty @max(next -| now_ms, draw_interval_ms);
+ };
+
+ // An update wake includes a draw, so it wins ties.
+ if (kitty_delay) |k| {
+ if (shader_delay == null or k <= shader_delay.?) {
+ return .{ .delay_ms = k, .kind = .update };
+ }
+ }
+
+ if (shader_delay) |s| return .{ .delay_ms = s, .kind = .draw };
+
+ return null;
}
/// True if our renderer is using vsync. If true, the renderer or apprt
@@ -1034,33 +1097,118 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// Flag that we need to update our custom shaders
self.custom_shader_focused_changed = true;
- // If we're not focused, then we want to stop the display link
- // because it is a waste of resources and we can move to pure
- // change-driven updates.
- if (comptime DisplayLink != void) link: {
- const display_link = self.display_link orelse break :link;
- if (focus) {
- display_link.start() catch {};
- } else {
- display_link.stop() catch {};
- }
- }
+ self.syncDisplayLink(null, null);
}
/// Callback when the window is visible or occluded.
///
/// Must be called on the render thread.
pub fn setVisible(self: *Self, visible: bool) void {
- // If we're not visible, then we want to stop the display link
- // because it is a waste of resources and we can move to pure
- // change-driven updates.
- if (comptime DisplayLink != void) link: {
- const display_link = self.display_link orelse break :link;
- if (visible and self.focused) {
+ self.visible = visible;
+ self.syncDisplayLink(null, null);
+
+ // When we're hidden, release our GPU resources if GPU
+ // operations are allowed from this thread. Apprts where
+ // they aren't (GTK owns the OpenGL context on the app
+ // thread) call `releaseGpuResources` at the appropriate
+ // time instead.
+ if (comptime !apprt.must_draw_from_app_thread) {
+ if (!visible) self.releaseGpuResources();
+ }
+ }
+
+ /// Release the GPU resources we hold while the surface is not
+ /// visible. Today this is the swap chain (render targets, font
+ /// atlas texture copies, cell buffers, custom shader textures),
+ /// which makes up nearly all of a surface's GPU memory usage;
+ /// a hidden surface doesn't draw, so it doesn't need it. The
+ /// swap chain is rebuilt on the next `drawFrame`.
+ ///
+ /// This is safe to call in any state; resources that are
+ /// already released are skipped.
+ ///
+ /// For OpenGL this must be called on the app thread with the
+ /// GL context current (the same requirement as
+ /// `displayUnrealized`). Other APIs may call this from the
+ /// render thread; see `apprt.must_draw_from_app_thread`.
+ pub fn releaseGpuResources(self: *Self) void {
+ self.draw_mutex.lockUncancelable(global.io());
+ defer self.draw_mutex.unlock(global.io());
+
+ if (self.swap_chain) |*sc| {
+ // Waits for any in-flight frames to complete, then
+ // frees all GPU resources.
+ sc.deinit();
+ self.swap_chain = null;
+
+ // Let the API drop any references it holds to swap
+ // chain resources (e.g. OpenGL's last presented
+ // target).
+ if (comptime @hasDecl(GraphicsAPI, "gpuResourcesReleased")) {
+ self.api.gpuResourcesReleased();
+ }
+ }
+ }
+
+ /// Create or update the display link and match it to the current
+ /// surface state.
+ ///
+ /// Must be called on the render thread and must NOT be called
+ /// while holding `draw_mutex`. Stopping a CVDisplayLink is a
+ /// blocking join on CoreVideo's IO thread, and the apprt calls
+ /// `drawFrame` (which takes `draw_mutex`) from the CoreAnimation
+ /// layer display path on the main thread.
+ fn syncDisplayLink(
+ self: *Self,
+ display_id: ?u32,
+ draw_now: ?*xev.Async,
+ ) void {
+ if (comptime DisplayLink == void) return;
+
+ const display_link = self.display_link orelse display_link: {
+ if (!self.config.vsync) return;
+ const callback = draw_now orelse return;
+ const result = macos.video.DisplayLink.createWithActiveCGDisplays() catch |err| {
+ // A locked macOS session can temporarily have no active
+ // displays. Rendering can continue without vsync and a
+ // later display update will retry this method.
+ log.warn("error creating display link; using fallback rendering err={}", .{err});
+ return;
+ };
+ result.setOutputCallback(
+ xev.Async,
+ &displayLinkCallback,
+ callback,
+ ) catch |err| {
+ log.warn("error configuring display link err={}", .{err});
+ result.release();
+ return;
+ };
+
+ self.display_link = result;
+ log.info("created display link", .{});
+ break :display_link result;
+ };
+
+ if (display_id) |id| {
+ log.info("updating display link display id={}", .{id});
+ display_link.setCurrentCGDisplay(id) catch |err| {
+ log.warn("error setting display link display id err={}", .{err});
+ };
+ }
+
+ const should_run =
+ // Non-visible windows never vsync
+ self.visible and
+ // Only vsync if we have cell changes or animation
+ (self.cells_rebuilt or self.animationWake() != null);
+
+ if (should_run) {
+ if (!display_link.isRunning()) {
display_link.start() catch {};
- } else {
- display_link.stop() catch {};
}
+ } else {
+ display_link.stop() catch {};
}
}
@@ -1068,19 +1216,20 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
///
/// Must be called on the render thread.
pub fn setFontGrid(self: *Self, grid: *font.SharedGrid) void {
- self.draw_mutex.lock();
- defer self.draw_mutex.unlock();
+ self.draw_mutex.lockUncancelable(global.io());
+ defer self.draw_mutex.unlock(global.io());
// Update our grid
self.font_grid = grid;
// Update all our textures so that they sync on the next frame.
// We can modify this without a lock because the GPU does not
- // touch this data.
- for (&self.swap_chain.frames) |*frame| {
+ // touch this data. A released swap chain is rebuilt with
+ // fresh frames that sync all textures on first use.
+ if (self.swap_chain) |*sc| for (&sc.frames) |*frame| {
frame.grayscale_modified = 0;
frame.color_modified = 0;
- }
+ };
// Get our metrics from the grid. This doesn't require a lock because
// the metrics are never recalculated.
@@ -1125,15 +1274,10 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
state: *renderer.State,
cursor_blink_visible: bool,
) Allocator.Error!void {
- // const start = std.time.Instant.now() catch unreachable;
- // const start_micro = std.time.microTimestamp();
- // defer {
- // const end = std.time.Instant.now() catch unreachable;
- // log.warn(
- // "[updateFrame time] start_micro={} duration={}ns",
- // .{ start_micro, end.since(start) / std.time.ns_per_us },
- // );
- // }
+ // CoreText shaping accumulates objects for deferred release over
+ // the course of a frame. Always flush those objects, including
+ // when rebuilding the frame fails due to memory pressure.
+ defer self.font_shaper.endFrame();
// We fully deinit and reset the terminal state every so often
// so that a particularly large terminal state doesn't cause
@@ -1144,6 +1288,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
if (self.terminal_state_frame_count >= max_terminal_state_frame_count) {
self.terminal_state.deinit(self.alloc);
self.terminal_state = .empty;
+ self.terminal_state_frame_count = 0;
}
self.terminal_state_frame_count += 1;
@@ -1163,6 +1308,9 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// Update all our data as tightly as possible within the mutex.
var critical: Critical = critical: {
+ // NOTE: This code needs be updated to 0.16.0 before you
+ // un-comment it ;)
+ //
// const start = try std.time.Instant.now();
// const start_micro = std.time.microTimestamp();
// defer {
@@ -1170,10 +1318,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// std.log.err("[updateFrame critical time] start={}\tduration={} us", .{ start_micro, end.since(start) / std.time.ns_per_us });
// }
- // Lock while signaling demand so the IO parse thread
- // can't starve us. See renderer.State.lockDemand.
- state.lockDemand();
- defer state.unlockDemand();
+ state.lockDemand(global.io());
+ defer state.unlockDemand(global.io());
// If we're in a synchronized output state, we pause all rendering.
if (state.terminal.modes.get(.synchronized_output)) {
@@ -1230,6 +1376,33 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
break :preedit try p.clone(arena_alloc);
};
+ // Advance any running Kitty graphics animations to the
+ // frame due now, and remember when the next frame is
+ // due (as an absolute deadline, see animationWake) so
+ // the renderer thread can schedule a wakeup for it.
+ // This must happen before the dirty check below:
+ // advancing a frame marks the image state dirty.
+ self.kitty_animation_next_ms = next: {
+ // Likely case: we have no kitty images, so do nothing.
+ const storage = &state.terminal.screens.active.kitty_images;
+ if (storage.images.count() == 0) break :next null;
+
+ const now: std.Io.Timestamp = .now(global.io(), .awake);
+ const base = self.kitty_animation_clock orelse base: {
+ self.kitty_animation_clock = now;
+ break :base now;
+ };
+ const now_ms: u64 = @intCast(@divTrunc(
+ base.durationTo(now).nanoseconds,
+ std.time.ns_per_ms,
+ ));
+ const delay = storage.animationTick(
+ global.io(),
+ now_ms,
+ ) orelse break :next null;
+ break :next now_ms + delay;
+ };
+
// If we have Kitty graphics data, we enter a SLOW SLOW SLOW path.
// We only do this if the Kitty image state is dirty meaning only if
// it changes.
@@ -1240,8 +1413,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
if (self.images.kittyRequiresUpdate(state.terminal)) {
// We need to grab the draw mutex since this updates
// our image state that drawFrame uses.
- self.draw_mutex.lock();
- defer self.draw_mutex.unlock();
+ self.draw_mutex.lockUncancelable(global.io());
+ defer self.draw_mutex.unlock(global.io());
self.images.kittyUpdate(
self.alloc,
state.terminal,
@@ -1371,8 +1544,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// Acquire the draw mutex for all remaining state updates.
{
- self.draw_mutex.lock();
- defer self.draw_mutex.unlock();
+ self.draw_mutex.lockUncancelable(global.io());
+ defer self.draw_mutex.unlock(global.io());
// Build our GPU cells
self.rebuildCells(
@@ -1432,9 +1605,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
self.updateCustomShaderUniformsFromState();
}
- // Notify our shaper we're done for the frame. For some shapers,
- // such as CoreText, this triggers off-thread cleanup logic.
- self.font_shaper.endFrame();
+ // Start the display link now that the rebuilt frame is ready.
+ self.syncDisplayLink(null, null);
}
/// Draw the frame to the screen.
@@ -1445,21 +1617,30 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
self: *Self,
sync: bool,
) !void {
- // const start = std.time.Instant.now() catch unreachable;
- // const start_micro = std.time.microTimestamp();
- // defer {
- // const end = std.time.Instant.now() catch unreachable;
- // log.warn(
- // "[drawFrame time] start_micro={} duration={}ns",
- // .{ start_micro, end.since(start) / std.time.ns_per_us },
- // );
- // }
-
- // We hold a the draw mutex to prevent changes to any
- // data we access while we're in the middle of drawing.
- self.draw_mutex.lock();
- defer self.draw_mutex.unlock();
+ // Everything that touches draw state happens under the draw
+ // mutex. The display link is synced only after the mutex is
+ // released; see `syncDisplayLink` for why it must never be
+ // called with the draw mutex held.
+ const sync_display_link = locked: {
+ self.draw_mutex.lockUncancelable(global.io());
+ defer self.draw_mutex.unlock(global.io());
+ break :locked try self.drawFrameLocked(sync);
+ };
+
+ if (sync_display_link) self.syncDisplayLink(null, null);
+ }
+ /// The body of `drawFrame`. Must be called with `draw_mutex` held.
+ ///
+ /// Returns true if the display link should be resynced once the
+ /// draw mutex is released. This is only ever true on the no-redraw
+ /// path, which a sync draw never takes, so the main thread's sync
+ /// draws never touch the display link and `syncDisplayLink` stays
+ /// on the render thread.
+ fn drawFrameLocked(
+ self: *Self,
+ sync: bool,
+ ) !bool {
// After the graphics API is complete (so we defer) we want to
// update our scrollbar state.
defer if (self.scrollbar_dirty) {
@@ -1480,7 +1661,26 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// If either of our surface dimensions is zero
// then drawing is absurd, so we just return.
- if (surface_size.width == 0 or surface_size.height == 0) return;
+ if (surface_size.width == 0 or surface_size.height == 0) return false;
+
+ // If we have no graphics context we can't draw. This is
+ // only the case while unrealized (GTK); displayRealized
+ // rebuilds the swap chain.
+ if (!self.display_realized) return false;
+
+ // Get our swap chain, rebuilding it if it was released
+ // while we were hidden. Rebuilding is deferred to draw
+ // time because resource creation must happen somewhere
+ // our graphics API allows it (OpenGL requires a current
+ // context, which drawFrame guarantees).
+ const swap_chain: *SwapChain, const swap_chain_rebuilt: bool =
+ if (self.swap_chain) |*sc| .{ sc, false } else rebuild: {
+ self.swap_chain = try SwapChain.init(
+ self.api,
+ self.has_custom_shaders,
+ );
+ break :rebuild .{ &self.swap_chain.?, true };
+ };
const size_changed =
self.size.screen.width != surface_size.width or
@@ -1488,10 +1688,14 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// Conditions under which we need to draw the frame, otherwise we
// don't need to since the previous frame should be identical.
+ //
+ // While any animation is in progress (a pending animation wake)
+ // every draw must actually render.
const needs_redraw =
size_changed or
+ swap_chain_rebuilt or
self.cells_rebuilt or
- self.hasAnimations() or
+ self.animationWake() != null or
sync;
if (!needs_redraw) {
@@ -1499,14 +1703,18 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// apprt may be swapping buffers and display an outdated frame
// if we don't draw something new.
try self.api.presentLastTarget();
- return;
+
+ // Ask our caller to resync the display link once the draw
+ // mutex is released, because we can probably pause the
+ // display link at this point.
+ return true;
}
self.cells_rebuilt = false;
// Wait for a frame to be available.
- const frame = try self.swap_chain.nextFrame();
- errdefer self.swap_chain.releaseFrame();
- // log.debug("drawing frame index={}", .{self.swap_chain.frame_index});
+ const frame = swap_chain.nextFrame();
+ errdefer swap_chain.releaseFrame();
+ // log.debug("drawing frame index={}", .{swap_chain.frame_index});
// If we need to reinitialize our shaders, do so.
if (self.reinitialize_shaders) {
@@ -1572,7 +1780,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// Setup our frame data
try frame.uniforms.sync(&.{self.uniforms});
try frame.cells_bg.sync(self.cells.bg_cells);
- const fg_count = try frame.cells.syncFromArrayLists(self.cells.fg_rows.lists);
+ const fg_count = try frame.cells.syncFromArrayLists(self.cells.fg_rows);
// If our background image buffer has changed, sync it.
if (frame.bg_image_buffer_modified != self.bg_image_buffer_modified) {
@@ -1585,16 +1793,16 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
texture: {
const modified = self.font_grid.atlas_grayscale.modified.load(.monotonic);
if (modified <= frame.grayscale_modified) break :texture;
- self.font_grid.lock.lockShared();
- defer self.font_grid.lock.unlockShared();
+ self.font_grid.lock.lockSharedUncancelable(global.io());
+ defer self.font_grid.lock.unlockShared(global.io());
frame.grayscale_modified = self.font_grid.atlas_grayscale.modified.load(.monotonic);
try self.syncAtlasTexture(&self.font_grid.atlas_grayscale, &frame.grayscale);
}
texture: {
const modified = self.font_grid.atlas_color.modified.load(.monotonic);
if (modified <= frame.color_modified) break :texture;
- self.font_grid.lock.lockShared();
- defer self.font_grid.lock.unlockShared();
+ self.font_grid.lock.lockSharedUncancelable(global.io());
+ defer self.font_grid.lock.unlockShared(global.io());
frame.color_modified = self.font_grid.atlas_color.modified.load(.monotonic);
try self.syncAtlasTexture(&self.font_grid.atlas_color, &frame.color);
}
@@ -1732,6 +1940,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
});
}
}
+
+ return false;
}
// Callback from the graphics API when a frame is completed.
@@ -1751,8 +1961,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
}, .{ .forever = {} });
}
- // Always release our semaphore
- self.swap_chain.releaseFrame();
+ // Always release our semaphore. The swap chain is
+ // guaranteed to exist here: it is only torn down after
+ // waiting for all in-flight frames to complete, and this
+ // callback is what signals that completion.
+ self.swap_chain.?.releaseFrame();
}
/// Call this any time the background image path changes.
@@ -1766,17 +1979,22 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
};
// Open the file
- var file = std.fs.openFileAbsolute(path, .{}) catch |err| {
+ var file = std.Io.Dir.openFileAbsolute(
+ global.io(),
+ path,
+ .{},
+ ) catch |err| {
log.warn(
"error opening background image file \"{s}\": {}",
.{ path, err },
);
break :load_background;
};
- defer file.close();
+ defer file.close(global.io());
// Read it
- const contents = file.readToEndAlloc(
+ const contents = compat_file.readToEndAlloc(
+ file,
self.alloc,
std.math.maxInt(u32), // Max size of 4 GiB, for now.
) catch |err| {
@@ -1853,8 +2071,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
/// Update the configuration.
pub fn changeConfig(self: *Self, config: *DerivedConfig) !void {
- self.draw_mutex.lock();
- defer self.draw_mutex.unlock();
+ self.draw_mutex.lockUncancelable(global.io());
+ defer self.draw_mutex.unlock(global.io());
// We always redo the font shaper in case font features changed. We
// could check to see if there was an actual config change but this is
@@ -1934,8 +2152,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
self: *Self,
size: renderer.Size,
) void {
- self.draw_mutex.lock();
- defer self.draw_mutex.unlock();
+ self.draw_mutex.lockUncancelable(global.io());
+ defer self.draw_mutex.unlock(global.io());
// We only actually need the padding from this,
// everything else is derived elsewhere.
@@ -2119,7 +2337,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
const uniforms: *shadertoy.Uniforms = &self.custom_shader_uniforms;
- const now = try std.time.Instant.now();
+ const now: std.Io.Timestamp = .now(global.io(), .awake);
defer self.last_frame_time = now;
const first_frame_time = self.first_frame_time orelse t: {
self.first_frame_time = now;
@@ -2127,10 +2345,10 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
};
const last_frame_time = self.last_frame_time orelse now;
- const since_ns: f32 = @floatFromInt(now.since(first_frame_time));
+ const since_ns: f32 = @floatFromInt(first_frame_time.durationTo(now).nanoseconds);
uniforms.time = since_ns / std.time.ns_per_s;
- const delta_ns: f32 = @floatFromInt(now.since(last_frame_time));
+ const delta_ns: f32 = @floatFromInt(last_frame_time.durationTo(now).nanoseconds);
uniforms.time_delta = delta_ns / std.time.ns_per_s;
uniforms.frame += 1;
@@ -2241,16 +2459,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
self: *Self,
features: []const Overlay.Feature,
) Overlay.InitError!void {
- // const start = std.time.Instant.now() catch unreachable;
- // const start_micro = std.time.microTimestamp();
- // defer {
- // const end = std.time.Instant.now() catch unreachable;
- // log.warn(
- // "[rebuildOverlay time] start_micro={} duration={}ns",
- // .{ start_micro, end.since(start) / std.time.ns_per_us },
- // );
- // }
-
const alloc = self.alloc;
// If we have no features enabled, don't build an overlay.
@@ -2326,14 +2534,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
) Allocator.Error!void {
const state: *terminal.RenderState = &self.terminal_state;
- // const start = try std.time.Instant.now();
- // const start_micro = std.time.microTimestamp();
- // defer {
- // const end = std.time.Instant.now() catch unreachable;
- // // "[rebuildCells time] <START us>\t<TIME_TAKEN us>"
- // std.log.warn("[rebuildCells time] {}\t{}", .{start_micro, end.since(start) / std.time.ns_per_us});
- // }
-
const grid_size_diff =
self.cells.size.rows != state.rows or
self.cells.size.columns != state.cols;