无畏契约自动切换分辨率工具

一个 Windows 后台小工具:检测到无畏契约启动时,自动把桌面分辨率切换到你在 NVIDIA 控制面板里自定义的那个分辨率(含刷新率),游戏退出后自动恢复原分辨率。零依赖、极低占用、开机无感运行。


一、背景与需求

本人玩无畏契约(Valorant)时,和我一样的很多玩家会自定义一个4:3低分辨率(拉伸全屏让人物模型变大,便于瞄准),但是我显示器是2K的,平时桌面又想用高分辨率。每次手动在 NVIDIA 控制面板切换很繁琐。

需求拆解:

  • 检测无畏契约启动 / 退出
  • 自动切换到 NVIDIA 控制面板里已自定义的分辨率(含刷新率、色深)
  • 游戏退出后自动恢复切换前的分辨率
  • 极低占用,不能影响游戏性能
  • 开机自启,无感运行

二、项目流程

  1. 需求确认:明确"自定义分辨率"指的是已在 N 卡控制面板添加好的那个(不是新建),工具只需"选中并应用"它。
  2. 环境检查
    • 确认 NVIDIA 显卡与驱动
    • 检测当前桌面分辨率
    • 从注册表 CustomDisplay 解析出用户自定义的分辨率(宽、高、刷新率)
    • EnumDisplaySettings 枚举系统模式,确认该自定义分辨率可用、可切换
  3. 方案设计:C# 编译成原生 exe(用 Windows 自带 csc.exe)+ WMI 事件监听 + ChangeDisplaySettingsEx 切换 + 托盘图标 + 计划任务开机自启。
  4. 实现:编写源码、编译脚本、配置文件、安装/卸载脚本。
  5. 测试:开游戏自动切换正常;发现关游戏不自动恢复的 bug。
  6. 修复:WMI 的进程退出事件不可靠,加轮询兜底,lock + 状态标志 去重。
  7. 优化:智能轮询——游戏时每 5 秒检测退出,平时每 30 秒兜底,平时 CPU≈0。

三、实现原理

1. 检测自定义分辨率

NVIDIA 控制面板的自定义分辨率存储在注册表:

HKLM\SYSTEM\CurrentControlSet\Control\Video\<显卡GUID>\<序号>\CustomDisplay
  • <显卡GUID> 每台机器不同,可用 PowerShell 枚举找到 NVIDIA 适配器:
Get-ChildItem 'HKLM:\SYSTEM\CurrentControlSet\Control\Video' | ForEach-Object {
    $p = Get-ItemProperty (Join-Path $_.PSPath '0000') -ErrorAction SilentlyContinue
    if ($p.DriverDesc -match 'NVIDIA') { $_.PSChildName }
}
  • CustomDisplay 是二进制值,里面含自定义分辨率的宽、高、刷新率、时序等。部分分辨率有 ASCII 标签(如 CUST:2560x1440x275.000Hz),部分是结构化存储(无标签,需按字节解析)。

更可靠的方式:直接用 EnumDisplaySettings 枚举系统支持的所有显示模式,自定义分辨率会出现在列表里(含刷新率、色深),比逆向二进制更稳。

2. 切换分辨率

核心是两个 Win32 API:

// 枚举所有显示模式,找到目标分辨率的那条 DEVMODE
// 关键:用系统已有的 DEVMODE(含 NVIDIA 自定义时序),不自己造参数
DEVMODE dm = new DEVMODE();
dm.dmSize = (ushort)Marshal.SizeOf(typeof(DEVMODE));
int i = 0;
while (EnumDisplaySettings(null, i, ref dm)) {
    if ((int)dm.dmPelsWidth == targetW &&
        (int)dm.dmPelsHeight == targetH &&
        (int)dm.dmDisplayFrequency == targetHz &&
        (int)dm.dmBitsPerPel == targetBpp) {
        // 找到了,直接应用
        ChangeDisplaySettingsEx(null, ref dm, IntPtr.Zero,
            CDS_UPDATEREGISTRY, IntPtr.Zero);
        break;
    }
    i++;
}

为什么不自己构造 DEVMODE:自定义分辨率的时序参数(CVT 减少消隐等)由 NVIDIA 驱动根据注册表生成,自己造参数容易黑屏或不被接受。用 EnumDisplaySettings 拿到的 DEVMODE 是驱动算好的完整时序,直接应用即可,等价于手动在控制面板里选。

3. 检测游戏进程(双保险)

WMI 事件(即时响应)

// 进程启动事件
var startWatcher = new ManagementEventWatcher(
    "SELECT * FROM Win32_ProcessStartTrace WHERE ProcessName = 'VALORANT-Win64-Shipping.exe'");
startWatcher.EventArrived += (s, e) => OnGameStart();
startWatcher.Start();

// 进程退出事件
var stopWatcher = new ManagementEventWatcher(
    "SELECT * FROM Win32_ProcessStopTrace WHERE ProcessName = 'VALORANT-Win64-Shipping.exe'");
stopWatcher.EventArrived += (s, e) => OnGameStop();
stopWatcher.Start();

轮询兜底(防 WMI 漏事件)

bool wasRunning = false;
while (true) {
    bool running = Process.GetProcessesByName("VALORANT-Win64-Shipping").Length > 0;
    if (running && !wasRunning) OnGameStart();        // 切换到游戏分辨率
    else if (!running && wasRunning) OnGameStop();     // 恢复桌面
    wasRunning = running;
    // 智能间隔:游戏时 5 秒检测退出,平时 30 秒兜底
    Thread.Sleep(wasRunning ? 5000 : 30000);
}

为什么要双保险:实测 WMI 的 ProcessStartTrace(开游戏)可靠,但 ProcessStopTrace(关游戏)偶尔不触发,导致游戏退出后分辨率不恢复。加轮询兜底后,即使 WMI 漏了,轮询也会在 5 秒内发现游戏退出并恢复。lock + inGame 状态标志 做去重,WMI 和轮询同时触发也不会重复操作。

4. 恢复逻辑

切换前,用 EnumDisplaySettings(null, ENUM_CURRENT_SETTINGS, ref dm) 记录当前桌面分辨率(含刷新率、色深),游戏退出后用记录的值调 ChangeDisplaySettingsEx 恢复。这样不管平时用什么分辨率都能正确恢复,无需配置"恢复目标"。

5. 触发进程的选择

无畏契约的进程链:启动器壳 -> VALORANT.exe -> VALORANT-Win64-Shipping.exe(游戏渲染主进程,占内存最大)。

触发点选 VALORANT-Win64-Shipping.exe:它启动 = 真正进游戏,它退出 = 游戏关了。如果选启动器壳,登录后挂机看商店也会触发,不准。


四、文件结构(路径)

项目目录/
├── ValorantResSwitcher.cs    # C# 源码(监听 + 切换 + 托盘 + 配置 + 日志)
├── ValorantResSwitcher.exe   # 编译产物(无窗口,后台运行)
├── build.bat                 # 编译脚本(调 Windows 自带 csc.exe)
├── config.ini                # 配置:目标分辨率/刷新率/进程名/轮询间隔
├── install.bat               # 安装开机自启(计划任务,需管理员)
├── uninstall.bat             # 卸载
└── switcher.log              # 运行日志(排查用)

关键注册表路径:

HKLM\SYSTEM\CurrentControlSet\Control\Video\<显卡GUID>\0000\CustomDisplay

五、使用方法

1. 配置 config.ini

# 触发的游戏进程名(VALORANT 游戏本体主进程)
process=VALORANT-Win64-Shipping.exe

# 目标分辨率(替换成你在 N 卡控制面板里自定义的那个)
width=1290
height=886
refresh=275
bpp=32

# 轮询间隔(毫秒)。游戏运行时按此间隔检测退出;非游戏时固定 30 秒兜底
poll=5000

2. 编译

双击 build.bat。用 Windows 自带的 .NET Framework C# 编译器,无需安装任何环境。

3. 运行

双击 ValorantResSwitcher.exe,后台运行,托盘出现图标。右键托盘可"立即切换"/"立即恢复"/"退出"。

4. 开机自启

右键 install.bat -> 以管理员身份运行。会创建一个计划任务:登录时以最高权限后台启动,不弹 UAC。

5. 卸载

运行 uninstall.bat


六、踩坑记录

  1. csc.exe 不支持 C# 6+ 语法:Windows 自带的 .NET Framework csc.exeC:\Windows\Microsoft.NET\Framework64\v4.0.30319\)是 C# 5.0 编译器,不支持字符串插值 $"..." 和 null 条件 ?.。需改用字符串拼接 + 显式 null 检查。

  2. WMI 进程退出事件不可靠Win32_ProcessStopTrace 偶尔不触发,导致游戏退出后不恢复。加轮询兜底解决。

  3. PowerShell 脚本编码问题:中文系统 PowerShell 按 GBK 读取 UTF-8 脚本会乱码、甚至语法错误。脚本输出用英文,或 Add-Type 用 here-string,或保存为带 BOM 的 UTF-8。

  4. EnumDisplaySettings 的 null 参数:PowerShell 里 $null 传给 string 参数会变成空字符串 "",导致枚举返回 0 条。需用 [NullString]::Value 传真正的 null。

  5. DEVMODE 结构体大小dmSize 必须设为 Marshal.SizeOf(typeof(DEVMODE)),结构体字段要完整定义到 dmPanningHeight(156 字节),否则 EnumDisplaySettings 可能返回 false。

  6. exe 被占用导致编译失败:重新编译前需先结束正在运行的 exe(任务管理器或 taskkill /im ValorantResSwitcher.exe /f),否则 csc 写不进 exe 文件。


七、资源占用

指标 数值
内存 约 34 MB(.NET Framework 运行时 + 托盘图标)
CPU(平时) ≈ 0(WMI 事件驱动 + 30 秒轮询兜底)
CPU(游戏时) 每 5 秒 1-3 ms(GetProcessesByName
对游戏帧率影响

34 MB 内存是 .NET Framework 的固定开销,想更低需换 native 实现,没必要。CPU 才是影响游戏的关键,事件驱动 + 智能轮询已压到几乎为零。


八、参考资料


九、扩展思路

  • 支持多游戏:config 里配置多个进程名,任一启动即切换。
  • 支持多显示器:切换指定显示器而非主屏。
  • 支持切换 NVIDIA 缩放模式(全屏拉伸 / 纵横比 / 无缩放),需调用 NVAPI 或改注册表。
  • 换 .NET 8 + Native AOT 编译,内存可降到 10 MB 以内。