From 20a77123d43841fc873dc3b57d337ae6cad51868 Mon Sep 17 00:00:00 2001 From: Emily Date: Sat, 2 Nov 2024 17:48:56 +0000 Subject: [PATCH] =?UTF-8?q?renderer/metal:=20prefer=20low=E2=80=90power=20?= =?UTF-8?q?GPUs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some Intel MacBook Pro laptops have both an integrated and discrete GPU and support automatically switching between them. The system uses the integrated GPU by default, but the default Metal device on those systems is the discrete GPU. This means that Metal‐using applications activate it by default, presumably as the intended audience is high‐performance graphics applications. This is unfortunate for productivity applications like terminals, however, as the discrete GPU decreases battery life and worsens the thermal throttling problems these machines have always had. Prefer to use an integrated GPU when present and not using an external GPU. The behaviour should be unchanged on Apple Silicon, as the platform only supports one GPU. I have confirmed that the resulting app runs, works, and doesn’t activate the AMD GPU on my MacBook Pro, but have not done any measurements of the resulting performance impact. If it is considered sufficiently noticeable, a GPU preference setting could be added. See , , , and for discussion, measurements, and changes relating to this issue in the Zed project. The logic implemented here reflects what Zed ended up settling on. The [Metal documentation] recommends using `MTLCopyAllDevicesWithObserver` to receive notifications of when the list of available GPUs changes, such as when [external GPUs are connected or disconnected]. I didn’t bother implementing that because it seemed like a lot of fussy work to deal with migrating everything to a new GPU on the fly just for a niche use case on a legacy platform. Zed doesn’t implement it and I haven’t heard about anyone complaining that their computer caught fire when they unplugged an external GPU, so hopefully it’s fine. [Metal documentation]: https://developer.apple.com/documentation/metal/gpu_devices_and_work_submission/multi-gpu_systems/finding_multiple_gpus_on_an_intel-based_mac [external GPUs are connected or disconnected]: https://developer.apple.com/documentation/metal/gpu_devices_and_work_submission/multi-gpu_systems/handling_external_gpu_additions_and_removals Closes: #2572 --- src/renderer/Metal.zig | 21 ++++++++++++++++++++- src/renderer/metal/api.zig | 2 +- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 6aac83419..cb0f5a3de 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -175,7 +175,7 @@ pub const GPUState = struct { instance: InstanceBuffer, // MTLBuffer pub fn init() !GPUState { - const device = objc.Object.fromId(mtl.MTLCreateSystemDefaultDevice()); + const device = try chooseDevice(); const queue = device.msgSend(objc.Object, objc.sel("newCommandQueue"), .{}); errdefer queue.release(); @@ -200,6 +200,25 @@ pub const GPUState = struct { return result; } + fn chooseDevice() error{NoMetalDevice}!objc.Object { + const devices = objc.Object.fromId(mtl.MTLCopyAllDevices()); + defer devices.release(); + var chosen_device: ?objc.Object = null; + var iter = devices.iterate(); + while (iter.next()) |device| { + // We want a GPU that’s connected to a display. + if (device.getProperty(bool, "isHeadless")) continue; + chosen_device = device; + // If the user has an eGPU plugged in, they probably want + // to use it. Otherwise, integrated GPUs are better for + // battery life and thermals. + if (device.getProperty(bool, "isRemovable") or + device.getProperty(bool, "isLowPower")) break; + } + const device = chosen_device orelse return error.NoMetalDevice; + return device.retain(); + } + pub fn deinit(self: *GPUState) void { // Wait for all of our inflight draws to complete so that // we can cleanly deinit our GPU state. diff --git a/src/renderer/metal/api.zig b/src/renderer/metal/api.zig index 0781812ac..bd4f407cd 100644 --- a/src/renderer/metal/api.zig +++ b/src/renderer/metal/api.zig @@ -175,4 +175,4 @@ pub const MTLSize = extern struct { depth: c_ulong, }; -pub extern "c" fn MTLCreateSystemDefaultDevice() ?*anyopaque; +pub extern "c" fn MTLCopyAllDevices() ?*anyopaque;