diff --git a/src/config/Config.zig b/src/config/Config.zig index ae209651b..f9163674d 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -615,6 +615,19 @@ keybind: Keybinds = .{}, /// given a certain viewport size and grid cell size. @"window-padding-balance": bool = false, +/// Synchronize rendering with the screen refresh rate. If true, this will +/// minimize tearing and align redraws with the screen but may cause input +/// latency. If false, this will maximize redraw frequency but may cause tearing, +/// and under heavy load may use more CPU and power. +/// +/// This defaults to false because out of the box a lot of users prefer to +/// feel like the terminal is as responsive as possible. +/// +/// Changing this value at runtime will only affect new terminals. +/// +/// This setting is only supported currently on macOS. +@"window-vsync": bool = false, + /// If true, new windows and tabs will inherit the working directory of the /// previously focused window. If no window was previously focused, the default /// working directory will be used (the `working-directory` option). diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 36bd334d7..ef6f329d9 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -320,6 +320,7 @@ pub const DerivedConfig = struct { min_contrast: f32, custom_shaders: std.ArrayListUnmanaged([:0]const u8), links: link.Set, + vsync: bool, pub fn init( alloc_gpa: Allocator, @@ -382,6 +383,7 @@ pub const DerivedConfig = struct { .custom_shaders = custom_shaders, .links = links, + .vsync = config.@"window-vsync", .arena = arena, }; @@ -473,7 +475,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { }; layer.setProperty("device", gpu_state.device.value); layer.setProperty("opaque", options.config.background_opacity >= 1); - layer.setProperty("displaySyncEnabled", false); // disable v-sync + layer.setProperty("displaySyncEnabled", options.config.vsync); // Make our view layer-backed with our Metal layer. On iOS views are // always layer backed so we don't need to do this. But on iOS the @@ -561,14 +563,13 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { var cells = try mtl_cell.Contents.init(alloc); errdefer cells.deinit(alloc); - const display_link: ?DisplayLink = null; - // Note(mitchellh): if/when we ever want to add vsync, we can use this - // display link to trigger rendering. We don't need this if vsync is off - // because any change will trigger a redraw immediately. - // const display_link: ?DisplayLink = switch (builtin.os.tag) { - // .macos => try macos.video.DisplayLink.createWithActiveCGDisplays(), - // else => null, - // }; + 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(); return Metal{ @@ -721,6 +722,15 @@ pub fn hasAnimations(self: *const Metal) bool { return self.custom_shader_state != null; } +/// True if our renderer is using vsync. If true, the renderer or apprt +/// is responsible for triggering draw_now calls to the render thread. That +/// is the only way to trigger a drawFrame. +pub fn hasVsync(self: *const Metal) bool { + if (comptime DisplayLink == void) return false; + const display_link = self.display_link orelse return false; + return display_link.isRunning(); +} + /// Returns the grid size for a given screen size. This is safe to call /// on any thread. fn gridSize(self: *Metal) ?renderer.GridSize { diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 8fbf4654c..ba1f8837a 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -572,6 +572,14 @@ pub fn hasAnimations(self: *const OpenGL) bool { return state.custom != null; } +/// See Metal +pub fn hasVsync(self: *const OpenGL) bool { + _ = self; + + // OpenGL currently never has vsync + return false; +} + /// Callback when the focus changes for the terminal this is rendering. /// /// Must be called on the render thread. diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index 9345d8652..3ee8ab1b9 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -270,7 +270,7 @@ fn drainMailbox(self: *Thread) !void { // 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(); + if (v) self.drawFrame(false); // Notify the renderer so it can update any state. self.renderer.setVisible(v); @@ -391,10 +391,14 @@ fn changeConfig(self: *Thread, config: *const DerivedConfig) !void { /// Trigger a draw. This will not update frame data or anything, it will /// just trigger a draw/paint. -fn drawFrame(self: *Thread) void { +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 we're doing single-threaded GPU calls then we just wake up the // app thread to redraw at this point. if (renderer.Renderer == renderer.OpenGL and @@ -464,7 +468,7 @@ fn drawNowCallback( // Draw immediately const t = self_.?; - t.drawFrame(); + t.drawFrame(true); return .rearm; } @@ -483,7 +487,7 @@ fn drawCallback( }; // Draw - t.drawFrame(); + t.drawFrame(false); // Only continue if we're still active if (t.draw_active) { @@ -518,9 +522,7 @@ fn renderCallback( t.flags.cursor_blink_visible, ) catch |err| log.warn("error rendering err={}", .{err}); - - // Draw immediately - t.drawFrame(); + t.drawFrame(false); return .disarm; }