Windows虚拟显示器MttVDD源码分析 (5) 交换链处理器 (SwapChainProcessor)

6 阅读8分钟

在上一章 间接设备上下文 (IndirectDeviceContext) 中,我们建立了一个“总指挥部”,它统一管理着我们虚拟显示器的所有状态和操作。我们现在有了一个被 Windows 承认的显示器,并且搭建好了与系统沟通的框架。

但是,有一个显而易见的问题:这个虚拟显示器目前还是一片空白!我们虽然创造了它,但还没有接收任何来自操作系统的桌面图像。那么,我们如何才能接收到 Windows 发送过来的源源不断的画面,并让我们的虚拟显示器真正“亮”起来呢?

答案就是本章的主角——SwapChainProcessor,我们驱动程序的图形处理引擎。

传送带的比喻

让我们回到本章开头提到的概念描述:

想象它是一个高速运转的传送带,操作系统把一帧帧画面放上去,这个处理器就负责快速接收并处理它们,然后告诉操作系统“我处理完了,可以给我下一帧了”。

这个比喻非常贴切。

  • 操作系统 (Windows):就像一个勤奋的工人,它不知疲倦地渲染出一帧又一帧的桌面图像(比如你移动鼠标、播放视频时的画面)。
  • 交换链 (SwapChain):这就是那条传送带。它是一个由操作系统和驱动共享的特殊内存区域,专门用来传递图像帧。
  • 交换链处理器 (SwapChainProcessor):这是站在传送带末端的另一个工人。他的唯一任务,就是以最快的速度把传送带上的图像帧取下来。

为什么需要一个专门的“工人”(SwapChainProcessor)来做这件事呢?因为如果让我们的“总指挥”(IndirectDeviceContext)亲自去传送带上搬运每一帧图像,他就会忙得不可开交,无法处理其他重要事务(比如响应用户更改分辨率的请求)。这会导致整个驱动程序反应迟钝,甚至卡死。

因此,SwapChainProcessor 在一个独立的后台线程中运行,确保了图像处理任务不会阻塞驱动程序的主逻辑。这是保证显示流畅、不卡顿的关键设计。

“交换链”到底是什么?

在深入了解处理器之前,我们先花一点时间理解“交换链 (SwapChain)”这个核心概念。

想象一下动画师如何制作动画:他们会用两张画纸。当观众在看第一张画纸上的内容时,动画师正在第二张画纸上绘制下一帧。画好后,他们迅速“交换”这两张纸,让观众看到新的画面,然后他再到第一张画纸上绘制更后面的内容。

“交换链”就是这个过程的数字版本。它通常包含两个或更多的“缓冲区”(Buffer),也就是我们的“画纸”。

  1. 后台缓冲区 (Back Buffer):操作系统正在往这里绘制下一帧的桌面图像。
  2. 前台缓冲区 (Front Buffer):这是当前正在显示给用户的图像。

当操作系统画完一帧后,它会执行一个“交换”操作,后台缓冲区变成前台缓冲区,反之亦然。我们的驱动程序需要做的,就是不断地从这个“交换链”中获取最新的、已经绘制完成的帧。

SwapChainProcessor 的诞生

SwapChainProcessor 并不是从驱动一开始就存在的。它是在操作系统决定要开始在我们的虚拟显示器上显示内容时,才被动态创建的。

这个时机,正是我们在 驱动回调与入口点 (WDF/IddCx Callbacks) 章节中提到的 VirtualDisplayDriverMonitorAssignSwapChain 回调函数被调用的时候。

让我们回顾一下这个流程:

  1. 用户在显示设置中启用了我们的虚拟显示器。
  2. 操作系统调用 VirtualDisplayDriverMonitorAssignSwapChain 回调,并递给我们一个“交换链”的句柄。
  3. 这个回调函数立即通知我们的“总指挥”——间接设备上下文 (IndirectDeviceContext)
  4. “总指挥”接到命令后,立刻创建并启动一个新的 SwapChainProcessor 实例,并将交换链句柄交给它。

下面是 IndirectDeviceContext::AssignSwapChain 方法中的核心代码,它展示了这个创建过程:

// 文件: Driver.cpp - 在 IndirectDeviceContext::AssignSwapChain 方法中

void IndirectDeviceContext::AssignSwapChain(
    IDDCX_MONITOR& Monitor,
    IDDCX_SWAPCHAIN SwapChain, // 操作系统给的“传送带”
    LUID RenderAdapter,
    HANDLE NewFrameEvent       // 一个“门铃”,新图像来了会响
)
{
    // ... 其他代码 ...

    // 获取用于渲染的Direct3D设备
    auto Device = GetOrCreateDevice(RenderAdapter);

    // 关键步骤:创建一个新的SwapChainProcessor实例,并让它开始工作
    m_ProcessingThread.reset(
        new SwapChainProcessor(SwapChain, Device, NewFrameEvent)
    );
}

这段代码非常直观。它用操作系统提供的 SwapChain(传送带)和 NewFrameEvent(新帧门铃)作为参数,实例化了一个 SwapChainProcessor 对象。从这一刻起,这个新的处理器就开始在后台运转了。

内部实现:处理器的生命周期

现在,让我们深入 SwapChainProcessor 的内部,看看它是如何在一个独立的线程中工作的。

1. 构造与启动线程

当我们 new SwapChainProcessor(...) 时,它的构造函数会立即执行。它的主要任务是创建一个新的后台线程,并让这个线程开始运行。

// 文件: Driver.cpp - SwapChainProcessor 构造函数

SwapChainProcessor::SwapChainProcessor(
    IDDCX_SWAPCHAIN hSwapChain,
    shared_ptr<Direct3DDevice> Device,
    HANDLE NewFrameEvent
) : m_hSwapChain(hSwapChain), m_Device(Device), m_hAvailableBufferEvent(NewFrameEvent)
{
    // ... 创建一个用于通知线程终止的事件 ...
    m_hTerminateEvent.Attach(CreateEvent(nullptr, FALSE, FALSE, nullptr));

    // 创建并立即启动后台线程,让它运行 RunThread 函数
    m_hThread.Attach(CreateThread(nullptr, 0, RunThread, this, 0, nullptr));
}

这里的 CreateThread 是一个 Windows API 函数,它创建了一个新的线程。这个新线程会从 RunThread 这个静态函数开始执行。this 参数将当前的 SwapChainProcessor 实例指针传递给了新线程,这样新线程就知道自己属于哪个处理器。

2. 核心处理循环

RunThread 函数只是一个简单的“引导程序”,它会调用 SwapChainProcessor::RunCore 方法,这里才是真正的核心逻辑所在——一个无限循环,不断地等待并处理新帧。

下面的序列图展示了这个循环的工作流程:

sequenceDiagram
    participant Thread as 后台线程
    participant OS as 操作系统 (IddCx)
    
    Thread->>Thread: 进入无限循环 RunCore()
    Thread->>OS: 等待新帧或终止信号
    Note right of OS: 操作系统渲染了新的一帧
    OS-->>Thread: “新帧可用”事件被触发 (门铃响了)
    Thread->>OS: 调用 IddCxSwapChainReleaseAndAcquireBuffer() 获取新帧
    OS-->>Thread: 返回新帧的图像数据
    Thread->>Thread: (TODO: 在这里处理图像, 比如编码、串流)
    Thread->>OS: 调用 IddCxSwapChainFinishedProcessingFrame() 通知处理完毕
    Note over Thread, OS: 循环继续,等待下一帧...

现在,让我们看看 RunCore 方法的简化版代码,它完美地实现了上图的逻辑:

// 文件: Driver.cpp - SwapChainProcessor::RunCore 方法

void SwapChainProcessor::RunCore()
{
    // ... 其他初始化 ...

    // 这是一个无限循环,直到被外部终止
    for (;;)
    {
        // 1. 等待“门铃”响起(新帧可用)或“终止”信号
        HANDLE WaitHandles[] = { m_hAvailableBufferEvent, m_hTerminateEvent.Get() };
        DWORD WaitResult = WaitForMultipleObjects(2, WaitHandles, FALSE, 100);

        if (WaitResult == WAIT_OBJECT_0) // “门铃”响了
        {
            // 2. 从交换链中获取新帧
            IDARG_OUT_RELEASEANDACQUIREBUFFER Buffer = {};
            hr = IddCxSwapChainReleaseAndAcquireBuffer(m_hSwapChain, &Buffer);
            
            if (SUCCEEDED(hr))
            {
                // =========================================================
                // TODO: 在这里处理获取到的图像帧 (Buffer.MetaData.pSurface)
                // 这可以是:
                //  - 将图像数据编码成视频流 (H.264, AV1 等)
                //  - 通过网络发送出去
                //  - 进行AI分析
                //  - 或者什么都不做,直接丢弃
                // =========================================================
                
                // 3. 通知操作系统:“我处理完了,你可以给我下一帧了”
                IddCxSwapChainFinishedProcessingFrame(m_hSwapChain);
            }
        }
        else // 如果是终止信号或其他情况
        {
            // 退出循环
            break;
        }
    }
}

这个循环是驱动程序图形性能的心脏。它高效地执行着“等待 -> 获取 -> 处理 -> 释放”的流程,确保了每一帧图像都能被及时地消费掉,从而避免了画面卡顿或延迟。

3. 终结与清理

当用户断开虚拟显示器或卸载驱动时,我们需要优雅地停止这个后台线程。这个过程由 IndirectDeviceContext::UnassignSwapChain 方法触发。

// 文件: Driver.cpp - IndirectDeviceContext::UnassignSwapChain 方法

void IndirectDeviceContext::UnassignSwapChain()
{
    // 简单地重置智能指针,这将自动调用 SwapChainProcessor 的析构函数
    m_ProcessingThread.reset();
}

m_ProcessingThread 被重置时,SwapChainProcessor析构函数~SwapChainProcessor())会被调用。这个析构函数会做一件非常重要的事情:

// 文件: Driver.cpp - SwapChainProcessor 析构函数

SwapChainProcessor::~SwapChainProcessor()
{
    // 触发 m_hTerminateEvent 事件,向后台线程发送“终止”信号
    SetEvent(m_hTerminateEvent.Get());

    if (m_hThread.Get())
    {
        // 等待后台线程安全退出
        WaitForSingleObject(m_hThread.Get(), INFINITE);
    }
}

SetEvent 调用会使 RunCore 循环中的 WaitForMultipleObjects 函数立即返回,并且 WaitResult 不再是 WAIT_OBJECT_0。这将导致循环中断,线程函数执行完毕,最终线程被安全销毁。

总结

在本章中,我们深入了解了 MttVDD 的图形处理引擎——SwapChainProcessor。我们学到了:

  • 它的作用:它是一个专门负责从操作系统接收桌面图像帧的组件,就像一个处理传送带上物品的专职工人。
  • 为什么需要它:它在独立的后台线程中运行,以确保高性能的图像处理不会阻塞驱动程序的主逻辑,从而避免卡顿。
  • 它的生命周期:当系统准备好显示内容时,它在 AssignSwapChain 回调中被创建;当显示停止时,它在 UnassignSwapChain 回调中被销毁。
  • 核心工作流程:它的核心是一个无限循环,不断地等待新帧事件,获取图像缓冲区,处理(目前为空),然后通知操作系统处理完毕。

到目前为止,我们已经构建了一个功能完整的虚拟显示驱动的骨架。我们能够创建显示器,让系统识别它,并能实时接收到桌面图像流。

然而,现代显示技术远不止显示基本的RGB图像那么简单。用户可能希望我们的虚拟显示器支持更广的色域、更高的动态范围(HDR),或者进行精确的颜色校准。我们如何处理这些高级的颜色信息呢?

在下一章中,我们将探索如何让我们的虚拟显示器支持这些高级功能。