diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-09-10 21:51:57 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-10 21:51:57 +0300 |
| commit | 425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch) | |
| tree | e52c4383a6d69216cbe2a32638bf90841d7e2189 /vendor/libghostty-vt/src/renderer/generic.zig | |
| parent | 90e947a6e26e02f10ec7f1694c4eb5b78a86406b (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.zig | 540 |
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; |
