diff --git a/src/os/macos.zig b/src/os/macos.zig index d405cd161..53dfd1719 100644 --- a/src/os/macos.zig +++ b/src/os/macos.zig @@ -62,6 +62,47 @@ pub fn appSupportDir( }); } +pub const SetQosClassError = error{ + // The thread can't have its QoS class changed usually because + // a different pthread API was called that makes it an invalid + // target. + ThreadIncompatible, +}; + +/// Set the QoS class of the running thread. +/// +/// https://developer.apple.com/documentation/apple-silicon/tuning-your-code-s-performance-for-apple-silicon?preferredLanguage=occ +pub fn setQosClass(class: QosClass) !void { + return switch (std.posix.errno(pthread_set_qos_class_self_np( + class, + 0, + ))) { + .SUCCESS => {}, + .PERM => error.ThreadIncompatible, + + // EPERM is the only known error that can happen based on + // the man pages for pthread_set_qos_class_self_np. I haven't + // checked the XNU source code to see if there are other + // possible errors. + else => @panic("unexpected pthread_set_qos_class_self_np error"), + }; +} + +/// https://developer.apple.com/library/archive/documentation/Performance/Conceptual/power_efficiency_guidelines_osx/PrioritizeWorkAtTheTaskLevel.html#//apple_ref/doc/uid/TP40013929-CH35-SW1 +pub const QosClass = enum(c_uint) { + user_interactive = 0x21, + user_initiated = 0x19, + default = 0x15, + utility = 0x11, + background = 0x09, + unspecified = 0x00, +}; + +extern "c" fn pthread_set_qos_class_self_np( + qos_class: QosClass, + relative_priority: c_int, +) c_int; + pub const NSOperatingSystemVersion = extern struct { major: i64, minor: i64, diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index 91e355480..cc63889fa 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -4,8 +4,10 @@ pub const Thread = @This(); const std = @import("std"); const builtin = @import("builtin"); +const assert = std.debug.assert; const xev = @import("xev"); const crash = @import("../crash/main.zig"); +const internal_os = @import("../os/main.zig"); const renderer = @import("../renderer.zig"); const apprt = @import("../apprt.zig"); const configpkg = @import("../config.zig"); @@ -92,6 +94,10 @@ flags: packed struct { /// This is true when the view is visible. This is used to determine /// if we should be rendering or not. visible: bool = true, + + /// This is true when the view is focused. This defaults to true + /// and it is up to the apprt to set the correct value. + focused: bool = true, } = .{}, pub const DerivedConfig = struct { @@ -199,6 +205,9 @@ fn threadMain_(self: *Thread) !void { }; defer crash.sentry.thread_state = null; + // Setup our thread QoS + self.setQosClass(); + // 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); @@ -237,6 +246,36 @@ fn threadMain_(self: *Thread) !void { _ = try self.loop.run(.until_done); } +fn setQosClass(self: *const Thread) void { + // Thread QoS classes are only relevant on macOS. + if (comptime !builtin.target.isDarwin()) return; + + const class: internal_os.macos.QosClass = class: { + // If we aren't visible (our view is fully occluded) then we + // always drop our rendering priority down because it's just + // mostly wasted work. + // + // The renderer itself should be doing this as well (for example + // Metal will stop our DisplayLink) but this also helps with + // general forced updates and CPU usage i.e. a rebuild cells call. + if (!self.flags.visible) break :class .utility; + + // If we're not focused, but we're visible, then we set a higher + // than default priority because framerates still matter but it isn't + // as important as when we're focused. + if (!self.flags.focused) break :class .user_initiated; + + // We are focused and visible, we are the definition of user interactive. + break :class .user_interactive; + }; + + if (internal_os.macos.setQosClass(class)) { + log.debug("thread QoS class set class={}", .{class}); + } else |err| { + log.warn("error setting QoS class err={}", .{err}); + } +} + fn startDrawTimer(self: *Thread) void { // If our renderer doesn't support animations then we never run this. if (!@hasDecl(renderer.Renderer, "hasAnimations")) return; @@ -273,10 +312,16 @@ fn drainMailbox(self: *Thread) !void { switch (message) { .crash => @panic("crash request, crashing intentionally"), - .visible => |v| { + .visible => |v| visible: { + // If our state didn't change we do nothing. + if (self.flags.visible == v) break :visible; + // Set our visible state self.flags.visible = v; + // Visibility affects our QoS class + self.setQosClass(); + // If we became visible then we immediately trigger a draw. // We don't need to update frame data because that should // still be happening. @@ -293,7 +338,16 @@ fn drainMailbox(self: *Thread) !void { // check the visible state themselves to control their behavior. }, - .focus => |v| { + .focus => |v| focus: { + // If our state didn't change we do nothing. + if (self.flags.focused == v) break :focus; + + // Set our state + self.flags.focused = v; + + // Focus affects our QoS class + self.setQosClass(); + // Set it on the renderer try self.renderer.setFocus(v);