Merge pull request #1727 from mitchellh/metal-link2

macOS: Unleash the Framerate
This commit is contained in:
Mitchell Hashimoto
2024-05-04 20:20:11 -07:00
committed by GitHub
10 changed files with 304 additions and 38 deletions

View File

@ -504,6 +504,7 @@ ghostty_app_t ghostty_surface_app(ghostty_surface_t);
bool ghostty_surface_transparent(ghostty_surface_t); bool ghostty_surface_transparent(ghostty_surface_t);
bool ghostty_surface_needs_confirm_quit(ghostty_surface_t); bool ghostty_surface_needs_confirm_quit(ghostty_surface_t);
void ghostty_surface_refresh(ghostty_surface_t); void ghostty_surface_refresh(ghostty_surface_t);
void ghostty_surface_draw(ghostty_surface_t);
void ghostty_surface_set_content_scale(ghostty_surface_t, double, double); void ghostty_surface_set_content_scale(ghostty_surface_t, double, double);
void ghostty_surface_set_focus(ghostty_surface_t, bool); void ghostty_surface_set_focus(ghostty_surface_t, bool);
void ghostty_surface_set_occlusion(ghostty_surface_t, bool); void ghostty_surface_set_occlusion(ghostty_surface_t, bool);
@ -541,6 +542,10 @@ uintptr_t ghostty_surface_pwd(ghostty_surface_t, char*, uintptr_t);
bool ghostty_surface_has_selection(ghostty_surface_t); bool ghostty_surface_has_selection(ghostty_surface_t);
uintptr_t ghostty_surface_selection(ghostty_surface_t, char*, uintptr_t); uintptr_t ghostty_surface_selection(ghostty_surface_t, char*, uintptr_t);
#ifdef __APPLE__
void ghostty_surface_set_display_id(ghostty_surface_t, uint32_t);
#endif
ghostty_inspector_t ghostty_surface_inspector(ghostty_surface_t); ghostty_inspector_t ghostty_surface_inspector(ghostty_surface_t);
void ghostty_inspector_free(ghostty_surface_t); void ghostty_inspector_free(ghostty_surface_t);
void ghostty_inspector_set_focus(ghostty_inspector_t, bool); void ghostty_inspector_set_focus(ghostty_inspector_t, bool);

View File

@ -112,6 +112,11 @@ extension Ghostty {
selector: #selector(onUpdateRendererHealth), selector: #selector(onUpdateRendererHealth),
name: Ghostty.Notification.didUpdateRendererHealth, name: Ghostty.Notification.didUpdateRendererHealth,
object: self) object: self)
center.addObserver(
self,
selector: #selector(windowDidChangeScreen),
name: NSWindow.didChangeScreenNotification,
object: nil)
// Setup our surface. This will also initialize all the terminal IO. // Setup our surface. This will also initialize all the terminal IO.
let surface_cfg = baseConfig ?? SurfaceConfiguration() let surface_cfg = baseConfig ?? SurfaceConfiguration()
@ -322,6 +327,19 @@ extension Ghostty {
healthy = health == GHOSTTY_RENDERER_HEALTH_OK healthy = health == GHOSTTY_RENDERER_HEALTH_OK
} }
@objc private func windowDidChangeScreen(notification: SwiftUI.Notification) {
guard let window = self.window else { return }
guard let object = notification.object as? NSWindow, window == object else { return }
guard let screen = window.screen else { return }
guard let surface = self.surface else { return }
// When the window changes screens, we need to update libghostty with the screen
// ID. If vsync is enabled, this will be used with the CVDisplayLink to ensure
// the proper refresh rate is going.
let id = (screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as! NSNumber).uint32Value
ghostty_surface_set_display_id(surface, id)
}
// MARK: - NSView // MARK: - NSView
override func viewDidMoveToWindow() { override func viewDidMoveToWindow() {
@ -439,7 +457,7 @@ extension Ghostty {
override func updateLayer() { override func updateLayer() {
guard let surface = self.surface else { return } guard let surface = self.surface else { return }
ghostty_surface_refresh(surface); ghostty_surface_draw(surface);
} }
override func acceptsFirstMouse(for event: NSEvent?) -> Bool { override func acceptsFirstMouse(for event: NSEvent?) -> Bool {

View File

@ -36,13 +36,25 @@ pub const DisplayLink = opaque {
return c.CVDisplayLinkIsRunning(@ptrCast(self)) != 0; return c.CVDisplayLinkIsRunning(@ptrCast(self)) != 0;
} }
pub fn setCurrentCGDisplay(
self: *DisplayLink,
display_id: c.CGDirectDisplayID,
) Error!void {
if (c.CVDisplayLinkSetCurrentCGDisplay(
@ptrCast(self),
display_id,
) != c.kCVReturnSuccess)
return error.InvalidOperation;
}
// Note: this purposely throws away a ton of arguments I didn't need. // Note: this purposely throws away a ton of arguments I didn't need.
// It would be trivial to refactor this into Zig types and properly // It would be trivial to refactor this into Zig types and properly
// pass this through. // pass this through.
pub fn setOutputCallback( pub fn setOutputCallback(
self: *DisplayLink, self: *DisplayLink,
comptime callbackFn: *const fn (*DisplayLink, ?*anyopaque) void, comptime Userdata: type,
userinfo: ?*anyopaque, comptime callbackFn: *const fn (*DisplayLink, ?*Userdata) void,
userinfo: ?*Userdata,
) Error!void { ) Error!void {
if (c.CVDisplayLinkSetOutputCallback( if (c.CVDisplayLinkSetOutputCallback(
@ptrCast(self), @ptrCast(self),
@ -60,7 +72,10 @@ pub const DisplayLink = opaque {
_ = flagsIn; _ = flagsIn;
_ = flagsOut; _ = flagsOut;
callbackFn(displayLink, inner_userinfo); callbackFn(
displayLink,
@alignCast(@ptrCast(inner_userinfo)),
);
return c.kCVReturnSuccess; return c.kCVReturnSuccess;
} }
}).callback), }).callback),

View File

@ -553,6 +553,13 @@ pub fn close(self: *Surface) void {
self.rt_surface.close(self.needsConfirmQuit()); self.rt_surface.close(self.needsConfirmQuit());
} }
/// Forces the surface to render. This is useful for when the surface
/// is in the middle of animation (such as a resize, etc.) or when
/// the render timer is managed manually by the apprt.
pub fn draw(self: *Surface) !void {
try self.renderer_thread.draw_now.notify();
}
/// Activate the inspector. This will begin collecting inspection data. /// Activate the inspector. This will begin collecting inspection data.
/// This will not affect the GUI. The GUI must use performAction to /// This will not affect the GUI. The GUI must use performAction to
/// show/hide the inspector UI. /// show/hide the inspector UI.

View File

@ -653,6 +653,13 @@ pub const Surface = struct {
}; };
} }
pub fn draw(self: *Surface) void {
self.core_surface.draw() catch |err| {
log.err("error in draw err={}", .{err});
return;
};
}
pub fn updateContentScale(self: *Surface, x: f64, y: f64) void { pub fn updateContentScale(self: *Surface, x: f64, y: f64) void {
// We are an embedded API so the caller can send us all sorts of // We are an embedded API so the caller can send us all sorts of
// garbage. We want to make sure that the float values are valid // garbage. We want to make sure that the float values are valid
@ -1525,6 +1532,12 @@ pub const CAPI = struct {
surface.refresh(); surface.refresh();
} }
/// Tell the surface that it needs to schedule a render
/// call as soon as possible (NOW if possible).
export fn ghostty_surface_draw(surface: *Surface) void {
surface.draw();
}
/// Update the size of a surface. This will trigger resize notifications /// Update the size of a surface. This will trigger resize notifications
/// to the pty and the renderer. /// to the pty and the renderer.
export fn ghostty_surface_set_size(surface: *Surface, w: u32, h: u32) void { export fn ghostty_surface_set_size(surface: *Surface, w: u32, h: u32) void {
@ -1724,6 +1737,15 @@ pub const CAPI = struct {
// Inspector Metal APIs are only available on Apple systems // Inspector Metal APIs are only available on Apple systems
usingnamespace if (builtin.target.isDarwin()) struct { usingnamespace if (builtin.target.isDarwin()) struct {
export fn ghostty_surface_set_display_id(ptr: *Surface, display_id: u32) void {
const surface = &ptr.core_surface;
_ = surface.renderer_thread.mailbox.push(
.{ .macos_display_id = display_id },
.{ .forever = {} },
);
surface.renderer_thread.wakeup.notify() catch {};
}
export fn ghostty_inspector_metal_init(ptr: *Inspector, device: objc.c.id) bool { export fn ghostty_inspector_metal_init(ptr: *Inspector, device: objc.c.id) bool {
return ptr.initMetal(objc.Object.fromId(device)); return ptr.initMetal(objc.Object.fromId(device));
} }

View File

@ -11,6 +11,7 @@ const objc = @import("objc");
const macos = @import("macos"); const macos = @import("macos");
const imgui = @import("imgui"); const imgui = @import("imgui");
const glslang = @import("glslang"); const glslang = @import("glslang");
const xev = @import("xev");
const apprt = @import("../apprt.zig"); const apprt = @import("../apprt.zig");
const configpkg = @import("../config.zig"); const configpkg = @import("../config.zig");
const font = @import("../font/main.zig"); const font = @import("../font/main.zig");
@ -42,6 +43,11 @@ const InstanceBuffer = mtl_buffer.Buffer(u16);
const ImagePlacementList = std.ArrayListUnmanaged(mtl_image.Placement); const ImagePlacementList = std.ArrayListUnmanaged(mtl_image.Placement);
const DisplayLink = switch (builtin.os.tag) {
.macos => *macos.video.DisplayLink,
else => void,
};
// Get native API access on certain platforms so we can do more customization. // Get native API access on certain platforms so we can do more customization.
const glfwNative = glfw.Native(.{ const glfwNative = glfw.Native(.{
.cocoa = builtin.os.tag == .macos, .cocoa = builtin.os.tag == .macos,
@ -92,8 +98,10 @@ current_background_color: terminal.color.RGB,
/// cells goes into a separate shader. /// cells goes into a separate shader.
cells: mtl_cell.Contents, cells: mtl_cell.Contents,
/// If this is true, we do a full cell rebuild on the next frame. /// Set to true after rebuildCells is called. This can be used
cells_rebuild: bool = true, /// to determine if any possible changes have been made to the
/// cells for the draw call.
cells_rebuilt: bool = false,
/// The current GPU uniform values. /// The current GPU uniform values.
uniforms: mtl_shaders.Uniforms, uniforms: mtl_shaders.Uniforms,
@ -115,6 +123,11 @@ shaders: Shaders, // Compiled shaders
/// Metal objects /// Metal objects
layer: objc.Object, // CAMetalLayer layer: objc.Object, // CAMetalLayer
/// The CVDisplayLink used to drive the rendering loop in sync
/// with the display. This is void on platforms that don't support
/// a display link.
display_link: ?DisplayLink = null,
/// Custom shader state. This is only set if we have custom shaders. /// Custom shader state. This is only set if we have custom shaders.
custom_shader_state: ?CustomShaderState = null, custom_shader_state: ?CustomShaderState = null,
@ -545,9 +558,19 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
}; };
}; };
const cells = try mtl_cell.Contents.init(alloc); var cells = try mtl_cell.Contents.init(alloc);
errdefer cells.deinit(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,
// };
errdefer if (display_link) |v| v.release();
return Metal{ return Metal{
.alloc = alloc, .alloc = alloc,
.config = options.config, .config = options.config,
@ -581,6 +604,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
// Metal stuff // Metal stuff
.layer = layer, .layer = layer,
.display_link = display_link,
.custom_shader_state = custom_shader_state, .custom_shader_state = custom_shader_state,
.gpu_state = gpu_state, .gpu_state = gpu_state,
}; };
@ -589,6 +613,13 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
pub fn deinit(self: *Metal) void { pub fn deinit(self: *Metal) void {
self.gpu_state.deinit(); self.gpu_state.deinit();
if (DisplayLink != void) {
if (self.display_link) |display_link| {
display_link.stop() catch {};
display_link.release();
}
}
self.cells.deinit(self.alloc); self.cells.deinit(self.alloc);
self.font_shaper.deinit(); self.font_shaper.deinit();
@ -635,6 +666,55 @@ pub fn threadExit(self: *const Metal) void {
// Metal requires no per-thread state. // Metal requires no per-thread state.
} }
/// Called by renderer.Thread when it starts the main loop.
pub fn loopEnter(self: *Metal, thr: *renderer.Thread) !void {
// 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 {};
}
/// Called by renderer.Thread when it exits the main loop.
pub fn loopExit(self: *const Metal) void {
// If we don't support a display link we have no work to do.
if (comptime DisplayLink == void) return;
// Stop our display link. If this fails its okay it just means
// that we either never started it or the view its attached to
// is gone which is fine.
const display_link = self.display_link orelse return;
display_link.stop() catch {};
}
fn displayLinkCallback(
_: *macos.video.DisplayLink,
ud: ?*xev.Async,
) void {
const draw_now = ud orelse return;
draw_now.notify() catch |err| {
log.err("error notifying draw_now err={}", .{err});
};
}
/// Called when we get an updated display ID for our display link.
pub fn setMacOSDisplayID(self: *Metal, id: u32) !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});
};
}
/// True if our renderer has animations so that a higher frequency /// True if our renderer has animations so that a higher frequency
/// timer is used. /// timer is used.
pub fn hasAnimations(self: *const Metal) bool { pub fn hasAnimations(self: *const Metal) bool {
@ -659,6 +739,35 @@ fn gridSize(self: *Metal) ?renderer.GridSize {
/// Must be called on the render thread. /// Must be called on the render thread.
pub fn setFocus(self: *Metal, focus: bool) !void { pub fn setFocus(self: *Metal, focus: bool) !void {
self.focused = focus; self.focused = focus;
// 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 {};
}
}
}
/// Callback when the window is visible or occluded.
///
/// Must be called on the render thread.
pub fn setVisible(self: *Metal, 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) {
display_link.start() catch {};
} else {
display_link.stop() catch {};
}
}
} }
/// Set the new font size. /// Set the new font size.
@ -720,6 +829,9 @@ pub fn updateFrame(
preedit: ?renderer.State.Preedit, preedit: ?renderer.State.Preedit,
cursor_style: ?renderer.CursorStyle, cursor_style: ?renderer.CursorStyle,
color_palette: terminal.color.Palette, color_palette: terminal.color.Palette,
/// If true, rebuild the full screen.
full_rebuild: bool,
}; };
// Update all our data as tightly as possible within the mutex. // Update all our data as tightly as possible within the mutex.
@ -790,16 +902,20 @@ pub fn updateFrame(
// If we have any terminal dirty flags set then we need to rebuild // If we have any terminal dirty flags set then we need to rebuild
// the entire screen. This can be optimized in the future. // the entire screen. This can be optimized in the future.
{ const full_rebuild: bool = rebuild: {
const Int = @typeInfo(terminal.Terminal.Dirty).Struct.backing_integer.?; {
const v: Int = @bitCast(state.terminal.flags.dirty); const Int = @typeInfo(terminal.Terminal.Dirty).Struct.backing_integer.?;
if (v > 0) self.cells_rebuild = true; const v: Int = @bitCast(state.terminal.flags.dirty);
} if (v > 0) break :rebuild true;
{ }
const Int = @typeInfo(terminal.Screen.Dirty).Struct.backing_integer.?; {
const v: Int = @bitCast(state.terminal.screen.dirty); const Int = @typeInfo(terminal.Screen.Dirty).Struct.backing_integer.?;
if (v > 0) self.cells_rebuild = true; const v: Int = @bitCast(state.terminal.screen.dirty);
} if (v > 0) break :rebuild true;
}
break :rebuild false;
};
// Reset the dirty flags in the terminal and screen. We assume // Reset the dirty flags in the terminal and screen. We assume
// that our rebuild will be successful since so we optimize for // that our rebuild will be successful since so we optimize for
@ -825,6 +941,7 @@ pub fn updateFrame(
.preedit = preedit, .preedit = preedit,
.cursor_style = cursor_style, .cursor_style = cursor_style,
.color_palette = state.terminal.color_palette.colors, .color_palette = state.terminal.color_palette.colors,
.full_rebuild = full_rebuild,
}; };
}; };
defer { defer {
@ -834,6 +951,7 @@ pub fn updateFrame(
// Build our GPU cells // Build our GPU cells
try self.rebuildCells( try self.rebuildCells(
critical.full_rebuild,
&critical.screen, &critical.screen,
critical.mouse, critical.mouse,
critical.preedit, critical.preedit,
@ -875,6 +993,12 @@ pub fn updateFrame(
pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void {
_ = surface; _ = surface;
// If our cells are not rebuilt, do a no-op draw. This means
// that no possible new data can exist that would warrant a full
// GPU update, our existing drawable is valid.
if (!self.cells_rebuilt) return;
self.cells_rebuilt = false;
// Wait for a frame to be available. // Wait for a frame to be available.
const frame = self.gpu_state.nextFrame(); const frame = self.gpu_state.nextFrame();
errdefer self.gpu_state.releaseFrame(); errdefer self.gpu_state.releaseFrame();
@ -1695,6 +1819,7 @@ pub fn setScreenSize(
/// memory and doesn't touch the GPU. /// memory and doesn't touch the GPU.
fn rebuildCells( fn rebuildCells(
self: *Metal, self: *Metal,
rebuild: bool,
screen: *terminal.Screen, screen: *terminal.Screen,
mouse: renderer.State.Mouse, mouse: renderer.State.Mouse,
preedit: ?renderer.State.Preedit, preedit: ?renderer.State.Preedit,
@ -1735,6 +1860,9 @@ fn rebuildCells(
}; };
} else null; } else null;
// If we are doing a full rebuild, then we clear the entire cell buffer.
if (rebuild) self.cells.reset();
// Go row-by-row to build the cells. We go row by row because we do // Go row-by-row to build the cells. We go row by row because we do
// font shaping by row. In the future, we will also do dirty tracking // font shaping by row. In the future, we will also do dirty tracking
// by row. // by row.
@ -1743,12 +1871,14 @@ fn rebuildCells(
while (row_it.next()) |row| { while (row_it.next()) |row| {
y = y - 1; y = y - 1;
// Only rebuild if we are doing a full rebuild or this row is dirty. if (!rebuild) {
// if (row.isDirty()) std.log.warn("dirty y={}", .{y}); // Only rebuild if we are doing a full rebuild or this row is dirty.
if (!self.cells_rebuild and !row.isDirty()) continue; // if (row.isDirty()) std.log.warn("dirty y={}", .{y});
if (!row.isDirty()) continue;
// If we're rebuilding a row, then we always clear the cells // Clear the cells if the row is dirty
self.cells.clear(y); self.cells.clear(y);
}
// True if we want to do font shaping around the cursor. We want to // True if we want to do font shaping around the cursor. We want to
// do font shaping as long as the cursor is enabled. // do font shaping as long as the cursor is enabled.
@ -1892,8 +2022,8 @@ fn rebuildCells(
} }
} }
// We always mark our rebuild flag as false since we're done. // Update that our cells rebuilt
self.cells_rebuild = false; self.cells_rebuilt = true;
// Log some things // Log some things
// log.debug("rebuildCells complete cached_runs={}", .{ // log.debug("rebuildCells complete cached_runs={}", .{

View File

@ -579,6 +579,14 @@ pub fn setFocus(self: *OpenGL, focus: bool) !void {
self.focused = focus; self.focused = focus;
} }
/// Callback when the window is visible or occluded.
///
/// Must be called on the render thread.
pub fn setVisible(self: *OpenGL, visible: bool) void {
_ = self;
_ = visible;
}
/// Set the new font size. /// Set the new font size.
/// ///
/// Must be called on the render thread. /// Must be called on the render thread.

View File

@ -14,7 +14,7 @@ const App = @import("../App.zig");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const log = std.log.scoped(.renderer_thread); const log = std.log.scoped(.renderer_thread);
const DRAW_INTERVAL = 33; // 30 FPS const DRAW_INTERVAL = 8; // 120 FPS
const CURSOR_BLINK_INTERVAL = 600; const CURSOR_BLINK_INTERVAL = 600;
/// The type used for sending messages to the IO thread. For now this is /// The type used for sending messages to the IO thread. For now this is
@ -50,6 +50,11 @@ draw_h: xev.Timer,
draw_c: xev.Completion = .{}, draw_c: xev.Completion = .{},
draw_active: bool = false, draw_active: bool = false,
/// This async is used to force a draw immediately. This does not
/// coalesce like the wakeup does.
draw_now: xev.Async,
draw_now_c: xev.Completion = .{},
/// The timer used for cursor blinking /// The timer used for cursor blinking
cursor_h: xev.Timer, cursor_h: xev.Timer,
cursor_c: xev.Completion = .{}, cursor_c: xev.Completion = .{},
@ -129,6 +134,10 @@ pub fn init(
var draw_h = try xev.Timer.init(); var draw_h = try xev.Timer.init();
errdefer draw_h.deinit(); 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 // Setup a timer for blinking the cursor
var cursor_timer = try xev.Timer.init(); var cursor_timer = try xev.Timer.init();
errdefer cursor_timer.deinit(); errdefer cursor_timer.deinit();
@ -137,7 +146,7 @@ pub fn init(
var mailbox = try Mailbox.create(alloc); var mailbox = try Mailbox.create(alloc);
errdefer mailbox.destroy(alloc); errdefer mailbox.destroy(alloc);
return Thread{ return .{
.alloc = alloc, .alloc = alloc,
.config = DerivedConfig.init(config), .config = DerivedConfig.init(config),
.loop = loop, .loop = loop,
@ -145,6 +154,7 @@ pub fn init(
.stop = stop_h, .stop = stop_h,
.render_h = render_h, .render_h = render_h,
.draw_h = draw_h, .draw_h = draw_h,
.draw_now = draw_now,
.cursor_h = cursor_timer, .cursor_h = cursor_timer,
.surface = surface, .surface = surface,
.renderer = renderer_impl, .renderer = renderer_impl,
@ -161,6 +171,7 @@ pub fn deinit(self: *Thread) void {
self.wakeup.deinit(); self.wakeup.deinit();
self.render_h.deinit(); self.render_h.deinit();
self.draw_h.deinit(); self.draw_h.deinit();
self.draw_now.deinit();
self.cursor_h.deinit(); self.cursor_h.deinit();
self.loop.deinit(); self.loop.deinit();
@ -180,6 +191,11 @@ pub fn threadMain(self: *Thread) void {
fn threadMain_(self: *Thread) !void { fn threadMain_(self: *Thread) !void {
defer log.debug("renderer thread exited", .{}); defer log.debug("renderer thread exited", .{});
// Run our loop start/end callbacks if the renderer cares.
const has_loop = @hasDecl(renderer.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 // Run our thread start/end callbacks. This is important because some
// renderers have to do per-thread setup. For example, OpenGL has to set // renderers have to do per-thread setup. For example, OpenGL has to set
// some thread-local state since that is how it works. // some thread-local state since that is how it works.
@ -189,6 +205,7 @@ fn threadMain_(self: *Thread) !void {
// Start the async handlers // Start the async handlers
self.wakeup.wait(&self.loop, &self.wakeup_c, Thread, self, wakeupCallback); self.wakeup.wait(&self.loop, &self.wakeup_c, Thread, self, wakeupCallback);
self.stop.wait(&self.loop, &self.stop_c, Thread, self, stopCallback); 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. // Send an initial wakeup message so that we render right away.
try self.wakeup.notify(); try self.wakeup.notify();
@ -255,6 +272,9 @@ fn drainMailbox(self: *Thread) !void {
// still be happening. // still be happening.
if (v) self.drawFrame(); if (v) self.drawFrame();
// Notify the renderer so it can update any state.
self.renderer.setVisible(v);
// Note that we're explicitly today not stopping any // Note that we're explicitly today not stopping any
// cursor timers, draw timers, etc. These things have very // cursor timers, draw timers, etc. These things have very
// little resource cost and properly maintaining their active // little resource cost and properly maintaining their active
@ -355,6 +375,12 @@ fn drainMailbox(self: *Thread) !void {
}, },
.inspector => |v| self.flags.has_inspector = v, .inspector => |v| self.flags.has_inspector = v,
.macos_display_id => |v| {
if (@hasDecl(renderer.Renderer, "setMacOSDisplayID")) {
try self.renderer.setMacOSDisplayID(v);
}
},
} }
} }
} }
@ -402,18 +428,43 @@ fn wakeupCallback(
t.drainMailbox() catch |err| t.drainMailbox() catch |err|
log.err("error draining mailbox err={}", .{err}); log.err("error draining mailbox err={}", .{err});
// If the timer is already active then we don't have to do anything. // Render immediately
if (t.render_c.state() == .active) return .rearm; _ = renderCallback(t, undefined, undefined, {});
// Timer is not active, let's start it // The below is not used anymore but if we ever want to introduce
t.render_h.run( // a configuration to introduce a delay to coalesce renders, we can
&t.loop, // use this.
&t.render_c, //
10, // // If the timer is already active then we don't have to do anything.
Thread, // if (t.render_c.state() == .active) return .rearm;
t, //
renderCallback, // // 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();
return .rearm; return .rearm;
} }
@ -468,7 +519,7 @@ fn renderCallback(
) catch |err| ) catch |err|
log.warn("error rendering err={}", .{err}); log.warn("error rendering err={}", .{err});
// Draw // Draw immediately
t.drawFrame(); t.drawFrame();
return .disarm; return .disarm;

View File

@ -69,6 +69,9 @@ pub const Message = union(enum) {
/// Activate or deactivate the inspector. /// Activate or deactivate the inspector.
inspector: bool, inspector: bool,
/// The macOS display ID has changed for the window.
macos_display_id: u32,
/// Initialize a change_config message. /// Initialize a change_config message.
pub fn initChangeConfig(alloc: Allocator, config: *const configpkg.Config) !Message { pub fn initChangeConfig(alloc: Allocator, config: *const configpkg.Config) !Message {
const thread_ptr = try alloc.create(renderer.Thread.DerivedConfig); const thread_ptr = try alloc.create(renderer.Thread.DerivedConfig);

View File

@ -124,6 +124,13 @@ pub const Contents = struct {
self.text.shrinkAndFree(alloc, text_reserved_len); self.text.shrinkAndFree(alloc, text_reserved_len);
} }
/// Reset the cell contents to an empty state without resizing.
pub fn reset(self: *Contents) void {
@memset(self.map, .{});
self.bgs.clearRetainingCapacity();
self.text.shrinkRetainingCapacity(text_reserved_len);
}
/// Returns the slice of fg cell contents to sync with the GPU. /// Returns the slice of fg cell contents to sync with the GPU.
pub fn fgCells(self: *const Contents) []const mtl_shaders.CellText { pub fn fgCells(self: *const Contents) []const mtl_shaders.CellText {
const start: usize = if (self.cursor) 0 else 1; const start: usize = if (self.cursor) 0 else 1;