动态切换多个 GPU 以高效地渲染到显示器。
概述
macOS 支持配备多个 GPU 和显示器的系统。例如,一台 MacBook Pro 可能包含一个低功耗的集成 GPU、一个高性能的独立 GPU、一个强大的外置 GPU 以及额外的显示器。Metal 应用程序必须仔细选择能够为特定显示器最大化效率和性能的 GPU。它们还应优雅地响应任何 GPU 或显示器的变化,例如当用户断开外置 GPU 连接或将窗口在显示器之间移动时。
入门
并非所有 Mac 电脑都同时配备了集成 GPU 和独立 GPU。要检查您的 Mac 中的 GPU,请选择苹果菜单 > 关于本机,点击系统报告按钮,并在左侧选择图形/显示器。GPU 列在视频卡下。配备双 GPU 的 MacBook Pro 电脑有一个默认开启的自动图形切换选项,允许系统在两个 GPU 之间自动切换。要切换自动图形切换状态,请选择苹果菜单 > 系统偏好设置,然后点击节能。自动图形切换复选框显示在顶部。
可选地,您可以通过 Thunderbolt 3 将外置 GPU 连接到您的 Mac,并且还可以将外接显示器连接到外置 GPU。对于这种系统设置,您的 Mac 必须运行 macOS 10.13.4 或更高版本。连接外置 GPU 后,示例可以运行处理外置 GPU 通知中描述的代码。
示例提供了以下交互式 UI 控件:
- 设备选择模式:允许示例自动为显示器选择最佳设备,或者表明您希望手动选择设备。
- 手动设备选择:从可用设备列表中手动选择设备。
此外,驱动显示器的设备标签指示当前正在驱动显示器的设备。
可绘制对象、显示器和 GPU
应用中的每个视图都显示在一个单独的显示器上,而每个显示器由一个单独的 GPU 驱动。要在视图中显示图形内容,视图的显示器会呈现来自驱动该显示器的 GPU 渲染的可绘制对象。
如果您的应用使用未驱动视图所在显示器的 GPU 进行渲染,系统必须在呈现之前将可绘制对象从渲染 GPU 复制到显示 GPU。此传输可能代价高昂,因为 GPU 之间的带宽受到连接它们的总线的限制。对于外置 GPU 来说,这种情况更为严重,因为其 Thunderbolt 3 总线的带宽远低于内部 PCI Express 总线。
呈现可绘制对象的最快路径是使用驱动视图所在显示器的 GPU 进行渲染。例如,在配备独立 GPU 和集成 GPU 的 MacBook Pro 上,在某些条件下(例如热状态、电池寿命或应用需求),集成 GPU 可以驱动 MacBook Pro 的显示器。

另一个例子是连接到外置 GPU 的 Mac,其中外置 GPU 驱动外部显示器。

平滑切换设备
示例中的视图控制器管理所有 Metal 设备,每个设备代表不同的 GPU。当示例运行 viewDidLoad
方法时,视图控制器为系统中可用的每个设备初始化一个新的 AAPLRenderer
。示例一次只使用一个设备,但它为每个设备初始化渲染器以预加载和镜像应用的 Metal 资源到所有设备上。因此,当应用程序在运行时在 GPU 之间切换时,示例可以平滑地在设备之间过渡,因为等效资源已经在每个设备上可用并加载。这种预加载和镜像策略避免了如果需要在切换时加载资源会产生的显著延迟。
注意:
预加载和镜像资源允许您平滑地在设备之间过渡,但这也会增加应用的总内存使用量。必须仔细确定哪些资源应该预加载和镜像,哪些资源应在应用在设备之间切换时才加载。
设置视图显示的最佳设备
视图出现后,示例获取视图所在显示器的 CGDirectDisplayID
值。示例使用此标识符来获取驱动该显示器的 Metal 设备。
objc
// 获取视图所在显示器的显示器 ID
CGDirectDisplayID viewDisplayID = (CGDirectDisplayID) [_view.window.screen.deviceDescription[@"NSScreenNumber"] unsignedIntegerValue];
// 获取驱动显示器的 Metal 设备
id<MTLDevice> newPreferredDevice = CGDirectDisplayCopyCurrentMetalDevice(viewDisplayID);
示例为视图控制器的 MTKView
设置此设备,并选择与该设备关联的 AAPLRenderer
来执行应用的渲染。此设置确保系统使用驱动显示器的设备进行渲染,并避免将任何可绘制对象从一个 GPU 复制到另一个 GPU。
处理显示更改通知
为了保持视图显示的最佳设备更新,示例注册两个系统通知:
NSApplicationDidChangeScreenParametersNotification
:当 Mac 的显示配置发生变化时,系统发布此通知。例如,用户连接或断开外部显示器时,或者启用自动图形切换并且系统在独立 GPU 和集成 GPU 之间切换以驱动显示器时。NSWindowDidChangeScreenNotification
:当任何窗口(包括包含应用视图的窗口)移动到不同显示器时,系统发布此通知。
objc
// 注册 NSApplicationDidChangeScreenParametersNotification,当系统的显示配置发生变化时触发
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleScreenChanges:)
name:NSApplicationDidChangeScreenParametersNotification
object:nil];
// 注册 NSWindowDidChangeScreenNotification,当窗口改变屏幕时触发
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleScreenChanges:)
name:NSWindowDidChangeScreenNotification
object:nil];
在这两种情况下,系统调用示例的 handleScreenChanges:
方法处理通知。然后,示例通过选择驱动显示器的设备对应的 AAPLRenderer
对象来选择视图显示的最佳设备。
设置 GPU 弹出策略
默认情况下,当应用程序使用的外置 GPU 从系统移除时,macOS 会完全重启应用程序。通常,应用程序通过以下方式处理重新启动:
- 在 macOS 关闭应用程序之前,系统调用应用的
application:willEncodeRestorableState:
方法时尽可能多地保存状态。 - 在 macOS 重新启动应用程序之后,系统调用应用的
application:didDecodeRestorableState:
方法时恢复任何已保存的状态。
示例通过选择自己处理外置 GPU 移除而不需要 macOS 退出并重新启动应用程序来避免这种应用重启过程。示例的 Info.plist
文件中有一个带有 wait
值的 GPUEjectPolicy
键,指示应用通过响应由 Metal 发布的相关通知明确处理外置 GPU 的移除。
注册外部 GPU 通知
示例调用 MTLCopyAllDevicesWithObserver
函数获取系统中所有可用的 Metal 设备。此方法允许示例提供一个 MTLDeviceNotificationHandler
块,在添加或移除外置 GPU 时执行。此处理器提供两个参数:device
(添加或移除的设备)和 notifyName
(描述触发通知的事件的值)。
objc
MTLDeviceNotificationHandler notificationHandler;
AAPLViewController * __weak controller = self;
notificationHandler = ^(id<MTLDevice> device, MTLDeviceNotificationName name)
{
[controller markHotPlugNotificationForDevice:device name:name];
};
// 查询所有支持的 Metal 设备并设置观察者,以便应用可以在系统添加或移除外置 GPU 时接收通知
id<NSObject> metalDeviceObserver = nil;
NSArray<id<MTLDevice>> * availableDevices =
MTLCopyAllDevicesWithObserver(&metalDeviceObserver,
notificationHandler);
响应外部 GPU 通知
通知处理器可以在任何线程上执行。然而,所有 UI 更新必须在主线程上发生,且应用的状态变化必须显式地实现线程安全。为了符合这些线程要求,视图控制器使用 @synchronized
指令保护对 _hotPlugEvent
和 _hotPlugDevice
实例变量的访问。(@synchronized
指令是在 Objective-C 代码中创建互斥锁的一种便捷方式。)
当发生通知时,示例在 markHotPlugNotificationForDevice:name:
方法中设置这些实例变量。
objc
- (void)markHotPlugNotificationForDevice:(nonnull id<MTLDevice>)device
name:(nonnull MTLDeviceNotificationName)name
{
@synchronized(self)
{
if ([name isEqualToString:MTLDeviceWasAddedNotification])
{
_hotPlugEvent = AAPLHotPlugEventDeviceAdded;
}
else if ([name isEqualToString:MTLDeviceRemovalRequestedNotification])
{
_hotPlugEvent = AAPLHotPlugEventDeviceEjected;
}
else if ([name isEqualToString:MTLDeviceWasRemovedNotification])
{
_hotPlugEvent = AAPLHotPlugEventDevicePulled;
}
_hotPlugDevice = device;
}
}
示例在主线程上读取这些实例变量并在 handlePossibleHotPlugEvent
方法中处理通知。
objc
- (void)handlePossibleHotPlugEvent
{
AAPLHotPlugEvent hotPlugEvent;
id<MTLDevice> hotPlugDevice;
@synchronized(self)
{
hotPlugEvent = _hotPlugEvent;
hotPlugDevice = _hotPlugDevice;
_hotPlugDevice = nil;
}
if(hotPlugDevice)
{
switch (hotPlugEvent)
{
case AAPLHotPlugEventDeviceAdded:
[self handleMTLDeviceAddedNotification:hotPlugDevice];
break;
case AAPLHotPlugEventDeviceEjected:
case AAPLHotPlugEventDevicePulled:
[self handleMTLDeviceRemovalNotification:hotPlugDevice];
break;
}
}
}
当表示外置 GPU 的设备被添加到系统时,handlePossibleHotPlugEvent
方法将设备添加到 _supportedDevices
数组中并为该设备初始化一个新的 AAPLRenderer
。当此类设备从系统移除时,同一方法会从 _supportedDevices
数组中删除设备并销毁其关联的 AAPLRenderer
。如果移除的设备正在用于渲染,示例会切换到另一个设备和渲染器。
注销通知
视图消失后,示例显式注销自身从前的显示或设备通知。否则,系统的通知中心和 Metal 无法释放示例的视图控制器。
objc
- (void)viewDidDisappear
{
[[NSNotificationCenter defaultCenter] removeObserver:self
name:NSApplicationDidChangeScreenParametersNotification
object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self
name:NSWindowDidChangeScreenNotification
object:nil];
MTLRemoveDeviceObserver(_metalDeviceObserver);
}