VGA Switcheroo

vga_switcheroo是Linux用于笔记本电脑混合显卡的子系统。这些显卡有两种类型

  • 复用型:具有双GPU和一个复用器芯片,用于在GPU之间切换输出。

  • 非复用型:具有双GPU,但只有一个连接到输出。另一个仅用于分担渲染任务,其结果通过PCIe复制到帧缓冲区中。在Linux上,这通过DRI PRIME支持。

混合显卡在2000年代后期开始出现,最初都是复用型的。较新的笔记本电脑出于成本原因转向非复用架构。一个值得注意的例外是MacBook Pro,它继续使用复用器。复用器具有不同的功能:有些仅切换面板,有些还可以切换外部显示器。有些一次切换所有显示引脚,而另一些只能切换DDC线路。(允许探测非活动GPU的EDID。)此外,复用器通常用于在未使用独立GPU时切断其电源。

DRM驱动程序向vga_switcheroo注册GPU,这些GPU此后被称为客户端。复用器被称为处理程序。非复用机器也注册一个处理程序来控制独立GPU的电源状态,其->switchto回调由于显而易见的原因是一个空操作。独立GPU通常配备用于HDMI/DP音频信号的HDA控制器,它也将注册为客户端,以便vga_switcheroo可以在更改独立GPU的电源状态时处理正确的挂起/恢复顺序。总共有最多三个客户端:两个vga客户端(GPU)和一个音频客户端(在独立GPU上)。代码主要准备好支持具有两个以上GPU的机器,如果它们可用的话。

当前输出切换到的GPU在vga_switcheroo术语中称为活动客户端。未使用的GPU是非活动客户端。当非活动客户端的DRM驱动程序加载时,它将无法探测面板的EDID,因此依赖于VBIOS来提供其显示模式。如果VBIOS模式是错误的,或者根本没有VBIOS(这在MacBook Pro上很常见),则客户端可以替代地请求将DDC线路临时切换到它,前提是处理程序支持此操作。仅切换DDC线路而不切换整个输出可以避免不必要的闪烁。

使用模式

手动切换和手动电源控制

在这种使用模式下,可以读取文件/sys/kernel/debug/vgaswitcheroo/switch以检索当前的vga_switcheroo状态,并且可以向其写入命令以更改状态。一旦两个GPU驱动程序和一个处理程序已向vga_switcheroo注册,该文件就会出现。理解以下命令

  • OFF:关闭未使用的设备的电源。

  • ON:打开未使用的设备的电源。

  • IGD:切换到集成显卡设备。如有必要,打开集成GPU的电源,关闭独立GPU的电源。前提是没有用户空间进程(例如Xorg,alsactl)已打开GPU或音频设备的设备文件。如果切换失败,用户可以在/dev/dri/和/dev/snd/controlC1上调用lsof(8)或fuser(1)以识别阻止切换的进程。

  • DIS:切换到独立显卡设备。

  • DIGD:延迟切换到集成显卡设备。一旦最后一个用户空间进程关闭GPU和音频设备的设备文件,这将执行切换。

  • DDIS:延迟切换到独立显卡设备。

  • MIGD:仅复用器切换到集成显卡设备。不重新映射控制台或更改任一gpu的电源状态。如果当前关闭集成GPU,屏幕将变黑。如果已打开,屏幕将显示VRAM中发生的任何事情。无论哪种方式,用户都必须盲目地输入命令才能切换回来。

  • MDIS:仅复用器切换到独立显卡设备。

对于其电源状态由驱动程序的运行时pm控制的GPU,ON和OFF命令是空操作(请参见下一节)。

对于非复用机器,不应使用IGD/DIS,DIGD/DDIS和MIGD/MDIS命令。

驱动程序电源控制

在这种使用模式下,独立GPU会自动打开和关闭电源,由驱动程序的运行时pm决定。在复用机器上,用户仍然可以通过debugfs接口影响复用器的状态,但是ON和OFF命令对于独立GPU变为空操作。

此模式是Nvidia HybridPower/Optimus和ATI PowerXpress上的默认模式。在内核命令行上指定nouveau.runpm=0,radeon.runpm=0或amdgpu.runpm=0会禁用它。

在GPU挂起后,需要调用处理程序以切断GPU的电源。同样,在GPU可以恢复之前,需要恢复电源。这是通过vga_switcheroo_init_domain_pm_ops()实现的,它通过对处理程序的必要调用来扩充GPU的挂起/恢复功能。

当音频设备恢复时,需要唤醒GPU。这是通过一个PCI quirk实现的,该quirk调用device_link_add()来声明对GPU的依赖性。这样,只要音频设备正在使用,GPU就会保持唤醒状态。

在复用机器上,如果复用器最初切换到独立GPU,则用户最终会在启动后GPU断电时得到黑屏。作为一种解决方法,在运行时挂起时,复用器被迫切换到集成GPU,请参见https://bugs.freedesktop.org/show_bug.cgi?id=75917

API

公共函数

int vga_switcheroo_register_handler(const struct vga_switcheroo_handler *handler, enum vga_switcheroo_handler_flags_t handler_flags)

注册处理程序

参数

const struct vga_switcheroo_handler *handler

处理程序回调

enum vga_switcheroo_handler_flags_t handler_flags

处理程序标志

描述

注册处理程序。如果两个vga客户端已经注册,则启用vga_switcheroo。

返回值

成功时返回0,如果已注册处理程序,则返回-EINVAL。

void vga_switcheroo_unregister_handler(void)

注销处理程序

参数

void

无参数

描述

注销处理程序。禁用vga_switcheroo。

enum vga_switcheroo_handler_flags_t vga_switcheroo_handler_flags(void)

获取处理程序标志

参数

void

无参数

描述

客户端获取处理程序标志位掩码的助手函数。

返回值

处理程序标志。值为0表示未注册处理程序或处理程序没有特殊功能。

int vga_switcheroo_register_client(struct pci_dev *pdev, const struct vga_switcheroo_client_ops *ops, bool driver_power_control)

注册vga客户端

参数

struct pci_dev *pdev

客户端pci设备

const struct vga_switcheroo_client_ops *ops

客户端回调

bool driver_power_control

电源状态是否由驱动程序的运行时pm控制

描述

注册vga客户端(GPU)。如果另一个GPU和一个处理程序已注册,则启用vga_switcheroo。客户端的电源状态假定为ON。在此之前,应调用vga_switcheroo_client_probe_defer()以确保满足所有前提条件。

返回值

成功时返回0,内存分配错误时返回-ENOMEM。

int vga_switcheroo_register_audio_client(struct pci_dev *pdev, const struct vga_switcheroo_client_ops *ops, struct pci_dev *vga_dev)

注册音频客户端

参数

struct pci_dev *pdev

客户端pci设备

const struct vga_switcheroo_client_ops *ops

客户端回调

struct pci_dev *vga_dev

绑定到当前音频客户端的pci设备

描述

注册音频客户端(GPU上的音频设备)。假定客户端使用运行时PM。在此之前,应调用vga_switcheroo_client_probe_defer()以确保满足所有前提条件。

返回值

成功时返回0,内存分配错误时返回-ENOMEM,获取客户端id错误时返回-EINVAL。

bool vga_switcheroo_client_probe_defer(struct pci_dev *pdev)

是否推迟探测给定客户端

参数

struct pci_dev *pdev

客户端pci设备

描述

确定是否未满足任何前提条件来探测给定客户端。驱动程序应在其->probe回调中尽早调用此方法,如果评估结果为true,则返回-EPROBE_DEFER。在你调用此函数之前,你不应该注册客户端。

返回值

如果应推迟探测,则返回true,否则返回false

enum vga_switcheroo_state vga_switcheroo_get_client_state(struct pci_dev *pdev)

获取给定客户端的电源状态

参数

struct pci_dev *pdev

客户端pci设备

描述

从vga_switcheroo获取给定客户端的电源状态。该函数仅从hda_intel.c调用。

返回值

电源状态。

void vga_switcheroo_unregister_client(struct pci_dev *pdev)

注销客户端

参数

struct pci_dev *pdev

客户端pci设备

描述

注销客户端。如果这是一个vga客户端(GPU),则禁用vga_switcheroo。

void vga_switcheroo_client_fb_set(struct pci_dev *pdev, struct fb_info *info)

设置给定客户端的帧缓冲区

参数

struct pci_dev *pdev

客户端pci设备

struct fb_info *info

帧缓冲区

描述

设置给定客户端的帧缓冲区。切换时,控制台将重新映射到此缓冲区。

int vga_switcheroo_lock_ddc(struct pci_dev *pdev)

暂时将DDC线路切换到给定客户端

参数

struct pci_dev *pdev

客户端pci设备

描述

暂时将DDC线路切换到由pdev标识的客户端(但保持输出切换到它们所在的位置)。这允许非活动客户端探测EDID。之后必须通过调用vga_switcheroo_unlock_ddc()将DDC线路切换回去,即使此函数返回错误也是如此。

返回值

成功时的先前DDC所有者,如果出错,则返回负整数。具体来说,如果没有注册处理程序或者处理程序不支持切换DDC线路,则返回-ENODEV。此外,处理程序返回的负值会传播回调用方。返回值仅对可能对其感兴趣的任何调用方具有信息目的。可以忽略返回值,而仅依赖于后续EDID探测的结果,如果DDC切换失败,则结果将为NULL

int vga_switcheroo_unlock_ddc(struct pci_dev *pdev)

将DDC线路切换回先前的所有者

参数

struct pci_dev *pdev

客户端pci设备

描述

在调用vga_switcheroo_lock_ddc()之后,将DDC线路切换回先前的所有者。即使vga_switcheroo_lock_ddc()返回错误,也必须调用此函数。

返回值

成功时的先前DDC所有者(即pdev的客户端标识符),如果出错,则返回负整数。具体来说,如果没有注册处理程序或者处理程序不支持切换DDC线路,则返回-ENODEV。此外,处理程序返回的负值会传播回调用方。最后,不允许在没有首先调用vga_switcheroo_lock_ddc()的情况下调用此函数,并将导致-EINVAL

int vga_switcheroo_process_delayed_switch(void)

延迟切换的助手函数

参数

void

无参数

描述

如果存在挂起的延迟切换,则进行处理。

返回值

成功时返回0。如果没有挂起的延迟切换,如果客户端在此期间已注销,或者如果存在阻止切换的其他客户端,则返回-EINVAL。如果实际切换失败,则报告错误并返回0。

int vga_switcheroo_init_domain_pm_ops(struct device *dev, struct dev_pm_domain *domain)

驱动程序电源控制的助手函数

参数

struct device *dev

vga客户端设备

struct dev_pm_domain *domain

电源域

描述

用于其电源状态由驱动程序的运行时pm控制的GPU的助手函数。在GPU挂起后,需要调用处理程序以切断GPU的电源。同样,在GPU可以恢复之前,需要恢复电源。为此,此助手函数通过对处理程序的必要调用来扩充挂起/恢复功能。它只需要在电源开关与被断电的设备分开的平台上调用。

公共结构

struct vga_switcheroo_handler

处理程序回调

定义:

struct vga_switcheroo_handler {
    int (*init)(void);
    int (*switchto)(enum vga_switcheroo_client_id id);
    int (*switch_ddc)(enum vga_switcheroo_client_id id);
    int (*power_state)(enum vga_switcheroo_client_id id, enum vga_switcheroo_state state);
    enum vga_switcheroo_client_id (*get_client_id)(struct pci_dev *pdev);
};

成员

init

初始化处理程序。可选。当启用vga_switcheroo时,即当两个vga客户端已注册时,将调用此方法。它允许处理程序执行一些依赖于vga客户端存在的延迟初始化。目前只有radeon和amdgpu驱动程序使用此方法。返回值将被忽略

switchto

将输出切换到给定客户端。强制。对于非复用机器,这应该是一个空操作。返回0表示成功,否则表示失败(在这种情况下,切换将被中止)

switch_ddc

将DDC线路切换到给定客户端。可选。成功时应返回先前的DDC所有者,失败时返回负整数

power_state

切断或恢复给定客户端的电源。可选。返回值将被忽略

get_client_id

确定给定pci设备是集成GPU还是独立GPU。强制

描述

处理程序回调。复用器本身。switchtoget_client_id方法是强制的,所有其他方法都可以设置为NULL。

struct vga_switcheroo_client_ops

客户端回调

定义:

struct vga_switcheroo_client_ops {
    void (*set_gpu_state)(struct pci_dev *dev, enum vga_switcheroo_state);
    void (*reprobe)(struct pci_dev *dev);
    bool (*can_switch)(struct pci_dev *dev);
    void (*gpu_bound)(struct pci_dev *dev, enum vga_switcheroo_client_id);
};

成员

set_gpu_state

为卡执行相当于挂起/恢复的操作。强制。这不应切断独立GPU的电源,这是处理程序的工作

reprobe

轮询输出。可选。这会在唤醒GPU并将输出切换到GPU后调用

can_switch

检查设备现在是否处于可以切换的位置。强制。如果用户空间进程已打开其设备文件之一,则客户端应返回false

gpu_bound

在GPU绑定时通知音频客户端客户端ID。

描述

客户端回调。客户端可以是GPU或GPU上的音频设备。set_gpu_statecan_switch方法是强制的,reprobe可以设置为NULL。对于音频客户端,reprobe成员是虚假的。OTOH,gpu_bound仅适用于音频客户端,而不适用于GPU客户端。

公共常量

enum vga_switcheroo_handler_flags_t

处理程序标志位掩码

常量

VGA_SWITCHEROO_CAN_SWITCH_DDC

处理程序是否能够单独切换DDC线路。这向客户端发出信号,表明它们应该调用drm_get_edid_switcheroo()来探测EDID

VGA_SWITCHEROO_NEEDS_EDP_CONFIG

处理程序是否无法单独切换AUX通道。这向客户端发出信号,表明活动GPU需要训练链路并将链路参数传达给非活动GPU(通过vga_switcheroo调解)。然后,非活动GPU可以跳过AUX握手并使用这些预校准的值设置其输出(DisplayPort规范v1.1a,第2.5.3.3节)

描述

处理程序标志位掩码。处理程序在向vga_switcheroo注册时使用它来声明其功能。

enum vga_switcheroo_client_id

客户端标识符

常量

VGA_SWITCHEROO_UNKNOWN_ID

分配给vga客户端的初始标识符。确定id需要处理程序,因此GPU在vga_switcheroo_enable()中以延迟方式获得其真实id

VGA_SWITCHEROO_IGD

集成显卡设备

VGA_SWITCHEROO_DIS

独立显卡设备

VGA_SWITCHEROO_MAX_CLIENTS

目前最多支持两个GPU

描述

客户端标识符。音频客户端使用相同的标识符&0x100。

enum vga_switcheroo_state

客户端电源状态

常量

VGA_SWITCHEROO_OFF

VGA_SWITCHEROO_ON

VGA_SWITCHEROO_NOT_FOUND

客户端未向vga_switcheroo注册。仅在vga_switcheroo_get_client_state()中使用,而vga_switcheroo_get_client_state()又仅从hda_intel.c调用

描述

客户端电源状态。

私有结构

struct vgasr_priv

vga_switcheroo私有数据

定义:

struct vgasr_priv {
    bool active;
    bool delayed_switch_active;
    enum vga_switcheroo_client_id delayed_client_id;
    struct dentry *debugfs_root;
    int registered_clients;
    struct list_head clients;
    const struct vga_switcheroo_handler *handler;
    enum vga_switcheroo_handler_flags_t handler_flags;
    struct mutex mux_hw_lock;
    int old_ddc_owner;
};

成员

active

是否启用vga_switcheroo。前提是注册两个GPU和一个处理程序

delayed_switch_active

是否存在挂起的延迟切换

delayed_client_id

挂起延迟切换的客户端

debugfs_root

vga_switcheroo debugfs接口的目录

registered_clients

已注册GPU的数量(仅计算vga客户端,不计算音频客户端)

clients

已注册客户端的列表

handler

已注册处理程序

handler_flags

已注册处理程序的标志

mux_hw_lock

保护复用器状态(尤其是在临时切换DDC线路时)

old_ddc_owner

将在解锁时切换回DDC线路的客户端

描述

vga_switcheroo私有数据。目前每个系统仅支持一个vga_switcheroo实例。

struct vga_switcheroo_client

已注册客户端

定义:

struct vga_switcheroo_client {
    struct pci_dev *pdev;
    struct fb_info *fb_info;
    enum vga_switcheroo_state pwr_state;
    const struct vga_switcheroo_client_ops *ops;
    enum vga_switcheroo_client_id id;
    bool active;
    bool driver_power_control;
    struct list_head list;
    struct pci_dev *vga_dev;
};

成员

pdev

客户端pci设备

fb_info

在切换时控制台重新映射到的帧缓冲区

pwr_state

如果使用手动电源控制,则为当前电源状态。对于驱动程序电源控制,请调用vga_switcheroo_pwr_state()。

ops

客户端回调

id

客户端标识符。确定id需要处理程序,因此gpu最初分配了VGA_SWITCHEROO_UNKNOWN_ID,然后在vga_switcheroo_enable()中给出了它们的真实id

active

当前输出是否切换到此客户端

driver_power_control

电源状态是否由驱动程序的运行时pm控制。如果为true,则将ON和OFF写入vga_switcheroo debugfs接口是一个空操作,因此不会干扰运行时pm

list

客户端列表

vga_dev

pci设备,指示哪个GPU绑定到当前音频客户端

描述

已注册客户端。客户端可以是GPU或GPU上的音频设备。对于音频客户端,fb_infoactive成员是虚假的。对于GPU客户端,vga_dev是虚假的。

处理程序

apple-gmux处理程序

gmux是内置于MacBook Pro中的微控制器,用于支持双GPU:pre-retinas上的Lattice XP2,pre-T2 retinas上的Renesas R4F2113

在T2 Macbooks上,gmux是T2协处理器的SMC的一部分。SMC具有与NXP PCAL6524 GPIO扩展器的I2C连接,该扩展器启用/禁用独立GPU的稳压器,驱动显示面板电源,并具有一个GPIO来切换eDP复用器。Intel CPU可以通过MMIO与gmux交互,类似于控制主SMC接口的方式。

(MacPro6,1 2013也有一个gmux,但是尚不清楚原因,因为它具有双GPU,但没有内置显示器。)

gmux连接到南桥的LPC总线。根据微控制器的不同,其I/O端口的访问方式也不同:访问pre-retina gmux的驱动程序函数以_pio_作为后缀,pre-T2 retina gmux的驱动程序函数以_index_作为后缀,T2 Macs上的驱动程序函数以_mmio_作为后缀。

gmux还连接到南桥的GPIO引脚,因此能够触发ACPI GPE。ACPI名称GMGP保存此GPIO引脚的编号。在MBP5 2008/09上,它是Nvidia MCP79的GPIO引脚22,在以下几代产品中,它是Intel PCH的GPIO引脚6,在MMIO gmux上,它是引脚21。

GPE仅表示发生了中断,实际事件类型是通过读取gmux寄存器来识别的。

除了GMGP名称外,gmux的ACPI设备还有两种方法GMSP和GMLV。GMLV可能表示“GMUX级别”,并读取GPIO的值,而GMSP可能表示“GMUX设置极性”,并且似乎写入GPIO的值。在较新的Macbooks上(这是在MacBookPro14,3之前或之后的某个时间引入的),ACPI GPE方法区分OS类型:在Darwin上,仅发出通知信号,而在其他OS上,读取GPIO的值然后反转。

由于Linux伪装成Darwin,因此最终进入仅通知代码路径。在MMIO gmux上,这似乎导致我们无法清除中断,除非我们调用GMSP(0)。没有这个,会涌现无法清除的状态=0中断。此问题似乎是MMIO gmux独有的。

图形复用器

在 pre-retina 型号上,两个 GPU 的 LVDS 输出都连接到 gmux,gmux 会将其中一个输出复用到面板。gmux 的一个技巧是在切换时延长其输出的消隐间隔,以使其与切换到的 GPU 同步。这允许用户察觉不到的无闪烁切换(US 8,687,007 B2)。

在 retina 型号上,复用不再由 gmux 本身完成,而是由一个单独的芯片完成,该芯片由 gmux 控制。该芯片有三个来源,分别是 NXP CBTL06142TI HD3SS212Pericom PI3VDP12412。面板使用 eDP 而不是 LVDS 驱动,因为 retina 分辨率所需的像素时钟超过了 LVDS 的限制。

Pre-retina 型号能够单独切换面板的 DDC 引脚。这由 TI SN74LV4066A 处理,它由 gmux 控制。因此,非活动 GPU 可以在不切换整个面板的情况下探测面板的 EDID。Retina 型号缺少此功能,因为用于 eDP 复用的芯片无法单独切换 AUX 通道(请参阅链接的数据表,Pericom 能够做到,但未被使用)。但是,retina 面板在其 DPCD 中设置了 NO_AUX_HANDSHAKE_LINK_TRAINING 位,允许非活动 GPU 跳过 AUX 握手,并使用活动 GPU 预先校准的链路参数设置输出。

外部 DP 端口仅在最初的两代一体式 MacBook Pro 上完全可切换,即 MBP5 2008/09 和 MBP6 2010。这由 NXP CBTL06141 完成,它由 gmux 控制。它是 retina 型号上 eDP 复用器的前身,区别在于支持 2.7 与 5.4 Gbit/s。

以下几代 MacBook Pro 用组合的 DP/Thunderbolt 端口取代了外部 DP 端口,并失去了在 GPU 之间切换它的能力,将其连接到独立 GPU 或 Thunderbolt 控制器。奇怪的是,虽然完整的端口不再可切换,但 AUX 和 HPD 仍然可以通过 NXP CBTL03062(在 pre-retina 型号 MBP8 2011 和 MBP9 2012 上)或两个 TI TS3DS10224(在 pre-T2 retina 型号上)在 gmux 的控制下切换。由于集成 GPU 缺少主链路,因此外部显示器对其显示为幻像,无法进行链路训练。

gmux 接收所有显示连接器的 HPD 信号,并在热插拔时发送中断。在无法切换外部端口的几代产品中,可以唤醒独立 GPU 以驱动新连接的显示器。在这些代产品上切换 AUX 的能力可以用于提高热插拔检测的可靠性,方法是让集成 GPU 在独立 GPU 休眠时轮询端口,但目前我们没有利用此功能。

我们对外部端口的切换策略是,在能够完全切换它的几代产品中,当向 vga_switcheroo 发出 IGD / DIS 命令时,端口与面板一起切换。因此,可以使用集成 GPU 在电池供电情况下驱动例如投影仪。如果需要更高的性能,用户可以手动切换到独立 GPU。

在所有较新的代产品中,外部端口只能由独立 GPU 驱动。如果在面板切换到集成 GPU 时插入显示器,两个 GPU 都将被使用以获得最大性能。为了降低功耗,用户可以手动切换到独立 GPU,从而挂起集成 GPU。

gmux 在启动时的初始切换状态可通过 EFI 变量 gpu-power-prefs-fa4ce28d-b62f-4c99-9cc3-6815686e30f9(第 5 个字节,1 = IGD,0 = DIS)配置。根据此设置,EFI 固件告诉 gmux 切换面板和外部 DP 连接器,并为所选 GPU 分配帧缓冲区。

电源控制

gmux 能够切断独立 GPU 的电源。它会自动处理正确的序列,以拆卸和启动核心电压、VRAM 和 PCIe 的电源轨。

背光控制

在单 GPU MacBook 上,背光的 PWM 信号由 GPU 生成。相比之下,在双 GPU MacBook Pro 上,可以挂起任一 GPU 以节省能量。因此,PWM 信号需要由单独的背光驱动程序生成,该驱动程序由 gmux 控制。最早的 MBP5 2008/09 代使用 TI LP8543 背光驱动程序。较新的型号使用 TI LP8545 或 TI LP8548。

公共函数

bool apple_gmux_detect(struct pnp_dev *pnp_dev, enum apple_gmux_type *type_ret)

检测 gmux 是否内置在机器中

参数

struct pnp_dev *pnp_dev

要探测的设备,如果为 NULL,则使用第一个匹配的设备

enum apple_gmux_type *type_ret

(通过引用)返回设备的 apple_gmux_type

描述

通过实际探测来检测是否存在受支持的 gmux 设备。这避免了 apple_gmux_present() 在某些型号上返回的误报。

返回值

如果检测到受支持的 gmux ACPI 设备,并且内核已配置 CONFIG_APPLE_GMUX,则为 true,否则为 false

bool apple_gmux_present(void)

检查是否存在 gmux ACPI 设备

参数

void

无参数

描述

驱动程序可以使用此功能来激活特定于双 GPU MacBook Pro 和 Mac Pro 的怪癖,例如延迟探测、运行时电源管理和背光。

返回值

如果存在 gmux ACPI 设备并且内核已配置 CONFIG_APPLE_GMUX,则为 true,否则为 false