返回

Xbox 120Hz Present1同步怪事? 深入分析与解决

windows

掰扯明白 Xbox 上 120Hz 刷新率的 Present1 同步怪事

哥们儿,你在 Xbox 上用 IDXGISwapChain1::Present1 控制视频帧,遇到点怪事是吧?特别是在 120Hz 刷新率下,本来期望的流畅感没来,反而出了些幺蛾子。咱们来捋捋这事儿。

你的代码大概是这样调用的:

RECT rect = {static_cast<LONG>(x), static_cast<LONG>(m_height - y - height),
             static_cast<LONG>(x + width), static_cast<LONG>(m_height - y)};
DXGI_PRESENT_PARAMETERS params = {1, &rect, nullptr, nullptr};
HRESULT result = m_swap_chain->Present1(swap_interval, 0, &params); // swap_interval 就是 SyncInterval

问题是这样的:

  • 60Hz 显示器, 30fps 视频, SyncInterval = 1 : 一切正常。每帧间隔 16/17ms,Present1 调用耗时 14/15ms。符合预期,画面看起来稳定。
  • 120Hz 显示器, 60fps 视频, SyncInterval = 1 : 问题来了。Present1 耗时还是 14/15ms,跟 60Hz 下差不多!这导致实际帧间隔也是 16ms 左右,根本没利用上 120Hz 的优势,帧率上不去。
  • 120Hz 显示器, 60fps 视频, SyncInterval = 0 : 耗时变得很奇怪,一会儿 12/13ms,一会儿 1ms,反复横跳。平均下来确实接近 8.3ms(1000ms / 120Hz ≈ 8.3ms per VSync,对应 60fps 的两倍 VSync 速率),但这跳跃式的耗时不是啥好事。
  • 60Hz 显示器, 30fps 视频, SyncInterval = 0 : 同样的跳跃耗时(12/13ms, 1ms, ...),导致每帧画面被显示了大概 4 次(60Hz / 30fps = 2 VSyncs per frame,但这种跳跃模式可能导致奇怪的重复显示)。

这到底是怎么回事?

问题在哪儿?

这表现确实反直觉。按理说,120Hz 刷新率意味着 VSync(垂直同步)间隔是 8.33ms。

  1. SyncInterval = 1 的谜团 :

    • SyncInterval = 1 的意思是“等到下一个垂直空白期(VSync)再显示”。在 120Hz 下,这应该意味着 Present1 在最多 8.33ms 后返回(如果提交时刚好错过 VSync,就等下一个)。
    • 它实际耗时 14/15ms,非常接近 60Hz 的 VSync 间隔(16.67ms)。这暗示 Present1 可能出于某种原因,仍然在按照类似 60Hz 的节奏同步,或者说它等待了 两个 120Hz 的 VSync 周期。
    • 为啥会这样?
      • 驱动或系统层面的行为 : Xbox 的图形驱动或者操作系统底层可能有特殊的调度或电源管理策略,在某些条件下不完全按照纯粹的 120Hz VSync 进行精确同步,特别是对于非游戏的全屏视频场景,可能会有不同的优化。
      • Present1 的内部机制 : Present1 的实现细节咱们不清楚,可能它内部的等待逻辑在特定条件下(比如和 DXGI_PRESENT_PARAMETERS 的组合)没能精确匹配 120Hz。
      • 资源争用或瓶颈 : 虽然不太像,但万一 CPU 或 GPU 在准备下一帧时存在隐藏瓶颈,导致虽然 Present1 意图 在 8.3ms 内完成,但实际被其他工作阻塞了。不过,SyncInterval = 0 的快速返回似乎排除了这个可能。
  2. SyncInterval = 0 的跳跃 :

    • SyncInterval = 0 意思是“别等 VSync,尽快显示”。这通常会造成画面撕裂(Tearing),因为画面可能在屏幕刷新到一半时就被替换了。
    • 12/13ms 和 1ms 交替出现,这很典范!通常发生在:
      1. 你调用 Present1,系统尝试立即显示。假设这需要一些时间(比如 12ms),可能涉及一些 GPU 操作和 OS 调度。
      2. 你马上又准备好下一帧,再次调用 Present1。由于前一帧的显示还没完全利索(可能 GPU 还在忙,或者显示管道里还有东西),这次 Present1 调用可能很快返回(1ms),因为它只是把新帧塞进队列,或者系统判断当前不允许立即显示,就直接返回了。
      3. 下一次调用又重复步骤 1 的耗时。
    • 这种模式造成帧率波动极大,虽然平均耗时看起来“对”,但实际的画面显示节奏是混乱的,观感可能比稳定但较低的帧率更差。这就是为什么在 60Hz/30fps 下看到奇怪的重复显示——帧送达的时机完全乱了套。
  3. DXGI_PRESENT_PARAMETERS 的影响 :

    • 你用了 DirtyRectsCount = 1pDirtyRects,这意味着你只更新屏幕的一部分(脏矩形)。这本身是个优化手段。但在某些驱动或硬件上,处理部分更新可能会引入额外的同步开销或奇怪的行为,特别是在高刷新率下。它和 VSync 的交互可能没那么简单直接。

咋解决?试试这几招

得对症下药,或者说,多试试几种方法,看哪个管用。

方案一:重新审视 SyncInterval

SyncInterval 控制的是当前帧要等待多少个 VSync 周期再显示。

  • 理论值 :

    • 要在 120Hz 显示器上达到 60fps,理论上应该让每一帧显示两个 VSync 周期。所以,SyncInterval = 2 似乎是逻辑上的正确选择 (1000ms / 120Hz * 2 = 16.67ms)。
    • 要在 120Hz 显示器上达到 120fps(如果你的视频源或渲染能跟上),SyncInterval = 1 (8.33ms) 才是目标。
  • 动手试试 :

    • 明确目标帧率 : 你是想让 60fps 的视频在 120Hz 屏幕上播放,并且每帧显示两次 (模拟 60Hz 的效果) 还是尽可能利用刷新率 (虽然视频源只有 60fps)?
    • 尝试 SyncInterval = 2 :
      HRESULT result = m_swap_chain->Present1(2, 0, &params); // SyncInterval = 2
      
      看看此时 Present1 的耗时是否稳定在 16ms 左右,并且帧输出是平滑的 60fps。这可能是最符合你期望“60fps video on 120Hz display”的行为。
    • 验证 SyncInterval = 1 : 再次确认 SyncInterval = 1 在 120Hz 下的行为。如果它总是耗时 15ms 左右,那可能就是 Xbox 平台或驱动对于此参数在这种模式下的特定实现。接受它,或者尝试下面的其他方案。
  • 原理 : 通过调整 SyncInterval,直接告诉系统你期望的同步节奏。如果 SyncInterval = 2 能稳定在 16ms 左右,说明系统能理解并执行“等待两个 VSync 周期”的指令,这比 SyncInterval = 1 的怪异行为要好预测。

方案二:拥抱可变刷新率 (VRR / FreeSync)

如果你的 Xbox 和连接的显示器都支持 VRR(Variable Refresh Rate,AMD FreeSync 或 HDMI 2.1 VRR),这通常是最佳方案。VRR 允许显示器的刷新率动态匹配应用程序的帧输出速率,消除撕裂,同时减少卡顿和延迟。

  • 检查支持 :

    • Xbox 设置 : 确保在 Xbox 的显示设置里启用了 VRR。
    • 代码检查 (可选但推荐) : 可以通过 IDXGIFactory5 (或更高版本) 的 CheckFeatureSupport 方法检查系统是否支持 DXGI_FEATURE_PRESENT_ALLOW_TEARING
      // 需要获取 IDXGIFactory5 接口
      IDXGIFactory5* pFactory5 = nullptr;
      // ... (获取 Factory 的代码,可能需要 QueryInterface) ...
      
      BOOL allowTearing = FALSE;
      if (SUCCEEDED(pFactory5->CheckFeatureSupport(DXGI_FEATURE_PRESENT_ALLOW_TEARING, &allowTearing, sizeof(allowTearing)))) {
          // 如果 allowTearing 是 TRUE,并且 SwapChain 是用 DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING 创建的...
      }
      // ... 释放 pFactory5 ...
      
    • 创建 Swap Chain : 创建 IDXGISwapChain1 时,必须包含 DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING 标志。如果你用的是 UWP 模板或者现有代码,可能需要修改 Swap Chain 的创建逻辑。
  • 如何使用 :

    • 如果支持并且开启了 Tearing,你应该使用 SyncInterval = 0,并在 Present1 调用时传递 DXGI_PRESENT_ALLOW_TEARING 标志。
      // 确保 Swap Chain 创建时有 DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING 标志
      // ...
      
      UINT presentFlags = 0;
      // if (allowTearing && m_isWindowed == FALSE /* 通常 Tearing 只在全屏下需要或有效 */) {
      // 实际上,现代系统和 DXGI Flip Model 下,窗口模式也可能支持 Tearing
      // 最佳实践是检查 CheckFeatureSupport 并基于它来决定
      if (allowTearingIsTrueBasedOnCheck) { // 假设你检查过了
           presentFlags = DXGI_PRESENT_ALLOW_TEARING;
      }
      
      // 注意:这里的 SyncInterval 必须是 0
      HRESULT result = m_swap_chain->Present1(0, presentFlags, &params);
      
  • 原理 : 当使用 DXGI_PRESENT_ALLOW_TEARINGSyncInterval = 0 时,你告诉系统:“如果可以,请立即显示,即使会撕裂;但如果 VRR 开启,就按照我提交帧的节奏来调整显示器刷新率。” 这样 Present1 通常会很快返回,因为它不阻塞等待 VSync。实际的显示同步交给 VRR 硬件处理。这应该能解决 SyncInterval = 0 时那种 13ms/1ms 的跳动问题,让帧平滑显示。

  • 安全建议 : 务必检查 CheckFeatureSupport。在不支持 Tearing 的系统上使用 DXGI_PRESENT_ALLOW_TEARING 标志会导致 Present1 调用失败(返回 DXGI_ERROR_INVALID_CALL)。

方案三:检查你的 Swap Chain 配置

你的问题可能也和 Swap Chain 的创建方式有关。现代 DirectX 应用推荐使用 Flip Model Swap Effects (DXGI_SWAP_EFFECT_FLIP_SEQUENTIALDXGI_SWAP_EFFECT_FLIP_DISCARD)。

  • 确认 Swap Effect : 检查创建 IDXGISwapChain1 (或 IDXGISwapChain3 等) 时使用的 DXGI_SWAP_CHAIN_DESC1 结构中的 SwapEffect 成员。

    • 如果是 DXGI_SWAP_EFFECT_FLIP_SEQUENTIALDXGI_SWAP_EFFECT_FLIP_DISCARD,这是推荐的方式。它们通常性能更好,并且是使用 DXGI_PRESENT_ALLOW_TEARING 的前提。
    • 如果是旧的 DXGI_SWAP_EFFECT_DISCARDDXGI_SWAP_EFFECT_SEQUENTIAL (BitBlt 模型),性能可能较差,并且在高刷新率下的行为可能不如 Flip Model 可靠。考虑迁移到 Flip Model。
  • 缓冲区数量 (Buffer Count) : Swap Chain 的缓冲区数量(BufferCount)通常推荐设置为 2 或 3。太少可能导致性能瓶颈,太多则增加延迟。对于视频播放,2 或 3 通常足够。

  • 代码示例 (创建 Flip Model SwapChain 的关键部分) :

    DXGI_SWAP_CHAIN_DESC1 swapChainDesc = {};
    swapChainDesc.Width = static_cast<UINT>(m_width); // 你的渲染目标宽度
    swapChainDesc.Height = static_cast<UINT>(m_height); // 高度
    swapChainDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; // 或其他你用的格式
    swapChainDesc.Stereo = false;
    swapChainDesc.SampleDesc.Count = 1; // No multisampling
    swapChainDesc.SampleDesc.Quality = 0;
    swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
    swapChainDesc.BufferCount = 2; // 试试 2 或 3
    swapChainDesc.Scaling = DXGI_SCALING_STRETCH; // Or None, AspectRatioStretch
    swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; // 或者 FLIP_SEQUENTIAL
    swapChainDesc.AlphaMode = DXGI_ALPHA_MODE_IGNORE;
    swapChainDesc.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING; // 如果你要用方案二的话,加上这个
    
    // ... 使用 DXGIFactory 创建 SwapChain ...
    // ComPtr<IDXGISwapChain1> swapChain;
    // hr = dxgiFactory->CreateSwapChainForCoreWindow(
    //     commandQueue.Get(), // D3D12 Command Queue
    //     reinterpret_cast<IUnknown*>(window), // CoreWindow
    //     &swapChainDesc,
    //     nullptr, // fullscreen desc
    //     &swapChain
    // );
    
  • 原理 : Flip Model 直接将应用渲染的后台缓冲区“翻转”到前台显示,减少了内存拷贝,效率更高,延迟更低。它与现代显示驱动和 VSync/VRR 机制配合得更好。

方案四:试试全帧提交

你目前使用了脏矩形 (pDirtyRects)。虽然是优化,但有时也可能带来麻烦。

  • 做个试验 : 临时修改 Present1 调用,进行全帧提交,看看问题是否消失。

    DXGI_PRESENT_PARAMETERS params = {}; // 清零所有成员
    // 或者显式设置: params.DirtyRectsCount = 0; params.pDirtyRects = nullptr;
    // params.ScrollRectOffset = nullptr; params.pScrollRect = nullptr;
    
    HRESULT result = m_swap_chain->Present1(swap_interval, 0, &params); // 使用修改后的 params
    
  • 原理 : 全帧提交避免了驱动处理部分更新可能引入的复杂逻辑和潜在 bug。如果这样能解决 120Hz/SyncInterval=1 的同步问题,那说明问题可能出在脏矩形和高刷新率同步的交互上。之后你可以再决定是放弃脏矩形优化,还是去查找更具体的驱动/平台问题。

方案五:利用 Xbox 开发工具 Profiling

如果你有权限使用 Xbox 开发套件(XDK)或 GDK 中的性能分析工具,比如 PIX:

  • 捕捉数据 : 在出现问题的场景下,使用 PIX 捕捉一小段时间的 GPU/CPU 活动。

  • 分析 Present 事件 : 重点关注 Present1 调用相关的 CPU 等待时间和 GPU 工作负载。查看 GPU 时间线,确认帧提交和实际显示(VSync)之间的关系。

  • 寻找瓶颈 : PIX 可以显示详细的系统事件和资源状态,可能会揭示出隐藏的等待、驱动内部的耗时或者资源冲突。

  • 原理 : “眼见为实”。工具能提供比代码计时更深层次的信息,直接看到底层发生了什么,是定位疑难杂症的终极武器。

进阶使用技巧和思考

  1. 延迟考量 :

    • SyncInterval = 0 (无 VRR 时) 虽然平均延迟可能低,但不稳定。
    • SyncInterval = 1 在 120Hz 下目标是 8.3ms 延迟(从 Present 调用到显示),你遇到的 15ms 表明延迟增加了。
    • SyncInterval = 2 在 120Hz 下目标是 16.6ms 延迟。
    • VRR + SyncInterval = 0 + Tearing Flag: 延迟通常是最低且最稳定的,因为它尽可能快地将帧交给显示器。
      选择哪个取决于你对延迟和平滑度的权衡。对于视频播放,平滑度通常比极致低延迟更重要,所以 SyncInterval = 2 或 VRR 是不错的选择。
  2. CPU 与 GPU 同步 : 确保你的渲染/解码线程和调用 Present1 的线程之间没有不必要的同步开销。比如,不要让 CPU 过早地等待 GPU 完成渲染(可以使用 Fences 来精确同步),也不要让 GPU 因等待 CPU 提交数据而空闲。

  3. Xbox 特定优化 : 查阅 Xbox 开发文档(NDA 内容,如果你有权限),看看是否有关于高刷新率显示、视频播放或 Present1 使用的最佳实践或已知限制。平台上可能有特定的 API 或设置能更好地控制同步行为。

  4. 电源模式 : 检查 Xbox 的电源设置。某些节能模式可能会限制性能或显示刷新率,间接影响 Present1 的行为。

没有银弹,你可能需要组合尝试上面的方法。先从简单的(修改 SyncInterval,尝试全帧提交)开始,再到复杂的(VRR 配置,Swap Chain 修改,Profiling)。祝你好运,搞定这个 120Hz 的小妖精!

相关资源链接 (可能需要微软开发者账号)