mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
Merge pull request #1727 from mitchellh/metal-link2
macOS: Unleash the Framerate
This commit is contained in:
@ -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);
|
||||||
|
@ -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 {
|
||||||
|
@ -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),
|
||||||
|
@ -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.
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
|
@ -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={}", .{
|
||||||
|
@ -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.
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
Reference in New Issue
Block a user