一、从“能跑”到“跑得稳”

上篇联调结束的时候,我们团队在 GTA 5 上跑通了第一个完整链路——注入层成功拦截了 Present,Manager 正确返回了 back buffer,适配层录制了缩放命令,画面正常显示在屏幕上。

validation layer 没报警,我一度觉得这事差不多了。

结果换了一个场景——GTA 5 里从室内走到室外、或者切换视角触发动态分辨率调整——崩了。

之后几天我陆续测了各种边界场景:窗口 resize、alt-tab 切出、动态分辨率切换、快速连续 Present。碰上各种各样没预料到的情况。现在回头看,一个场景跑通只代表“系统在一种特定渲染状态下是通的”,跟“兼容真实游戏的复杂管线”差了十万八千里。

这篇把测试过程、碰到的问题、怎么修的,如实记下来。

二、测试策略

GTA 5 不是 Vulkan 官方示例那种“一条直线”的渲染管线。真实游戏的渲染结构复杂得多:有多个交换链、有动态分辨率切换、有复杂的后处理链、有多轮降采样和合成。

我按几个维度梳理了 GTA 5 中需要覆盖的场景:

场景 特点 测试目标
正常游戏(室外) 复杂场景、多后处理、动态分辨率 验证注入链路在正常 gameplay 下的稳定性
正常游戏(室内) 较简单场景、较低分辨率 验证资源状态转换是否适配不同分辨率
窗口 resize swapchain 重建 验证中间纹理和 RTV 是否正确重建
alt-tab 切出/切回 焦点切换、设备丢失恢复 验证资源清理和重建路径
快速连续 Present 高帧率、高频注入 验证锁竞争和性能
动态分辨率切换 分辨率变化、swapchain 重建 验证 format/尺寸变化时的适配

目标不是“测很多场景”,而是“覆盖几种不同的渲染状态变化”。测之前先列了一张表,记每个场景的预期表现——swapchain usage 有没有 transfer 位、会不会走回退、swapchain 重建几率大不大。这样测完有对比基准。

三、测试记录

3.1 正常游戏(室内场景)

GTA 5 的室内场景相对简单——较小的 draw call 数量、较少的后处理、较低的分辨率压力。

swapchain usageCOLOR_ATTACHMENT,没有 transfer 位

触发回退:是,CanUseTransfer 返回 false,走清屏路径

表现:清屏正常,画面被覆盖为纯色。帧率无明显波动。室内场景下验证了回退路径没问题。但 transfer 位一直没开,阶段 2 的缩放链路在这个场景上没法验。

3.2 正常游戏(室外场景)

GTA 5 的室外场景是真正的考验——复杂的植被、大量的 draw call、动态分辨率、多轮后处理。

swapchain usageCOLOR_ATTACHMENT | TRANSFER_SRC | TRANSFER_DST

触发回退:否,transfer 位满足

阶段 2 生效:是,能看到缩放重采样的画面

问题:跑了大概三十秒,validation layer 报了一个 resource barrier 警告。

警告内容:在缩放处理完成后,我把 swapchain image 从 TRANSFER_DST 转回 PRESENT。但 GTA 5 紧接着还要做一次后处理——它在我处理过的 swapchain image 上继续操作,但我的 barrier 已经把它转成了 PRESENT 状态,而游戏期望的是 RENDER_TARGET 或 COPY_SOURCE,状态对不上。

这个问题的本质是:我不知道应用在 present 前还会不会对 swapchain image 做额外操作。GTA 5 的渲染管线在不同场景下行为不同——某些场景在 present 前有额外的后处理 pass,某些没有。如果我一刀切地在 present 前把状态定死成 PRESENT,那些在 present 前还要用 swapchain image 做渲染的场景就会受影响。

修法:在注入处理结束后,不把状态转成 PRESENT,而是转回处理前的原始状态。也就是在处理开始时先读一下当前的资源状态,记下来,处理完再还原回去。

这对 Manager 的要求高了一点——需要多记录一个 lastKnownState。在 SwapchainData 里加了一个字段,每次 barrier 时更新:

// 处理前记录原始状态
D3D12_RESOURCE_STATES originalState = sc.lastKnownState;
// ... barrier 转到 COPY_DEST,blit 处理 ...
// 处理后还原
commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(
    sc.buffers[bufferIndex],
    D3D12_RESOURCE_STATE_COPY_DEST,
    originalState
));
sc.lastKnownState = originalState;

改完之后室外场景的警告消失。

AI 帮我分析了这个问题:出这个 bug 的时候,我不确定该不该把状态转回 PRESENT。AI 指出,如果应用在 present 前还会继续使用 swapchain image,强制转成 PRESENT 会导致后续操作失败。正确的做法是“记录原始状态,用完还原”,而不是假设 PRESENT 是最终状态。这个分析让我明白了问题本质。

3.3 窗口 resize(拖拽边缘)

GTA 5 支持窗口模式下拖拽边缘调整大小。每次 resize 都会触发 swapchain 重建——Release 旧的 swapchain,创建新的。

swapchain usageCOLOR_ATTACHMENT | TRANSFER_SRC | TRANSFER_DST(新 swapchain 同样带 transfer 位)

触发回退:否

阶段 2 生效:是

问题:拖拽几次之后,程序崩溃。

定位了半天,问题出在我 Manager 里的 EnsureDownscaleImage。swapchain 重建时,旧的 downscale image 没有被正确释放,新的 downscale image 创建时用了旧的尺寸信息(还是 resize 之前的尺寸),导致后续 blit 操作越界。

修法:在 swapchain 销毁时,不仅要释放 back buffers,还要释放 downscale image 和相关 RTV。在 UnregisterSwapchain 里补上清理逻辑:

void ResourceStateManager::UnregisterSwapchain(IDXGISwapChain* swapchain)
{
    auto it = swapchains.find(swapchain);
    if (it == swapchains.end()) return;
    
    auto& sc = it->second;
    // 释放 downscale image
    if (sc.downscaleImage) {
        sc.downscaleImage->Release();
        sc.downscaleImage = nullptr;
    }
    // 释放 back buffers
    for (auto& buffer : sc.buffers) {
        if (buffer) buffer->Release();
    }
    sc.buffers.clear();
    // 释放 RTV heap 等
    // ...
    swapchains.erase(it);
}

改完之后,持续拖拽窗口边缘 resize,不再崩溃。中间纹理能正确跟随 swapchain 尺寸变化重建。

AI 帮我检查了资源释放顺序:写 UnregisterSwapchain 时,AI 提醒我释放顺序很重要——应该先释放依赖于 back buffer 的资源(RTV、downscale image),再释放 back buffer 本身。如果顺序反了,可能会出现“资源已被释放但还有引用”的问题。

3.4 alt-tab 切出/切回

GTA 5 在 alt-tab 切出时会触发焦点切换,切回时可能触发设备恢复或 swapchain 重建。

问题:切回游戏后,画面黑屏。

排查发现,alt-tab 切出时 D3D12 设备可能进入“丢失”状态(DXGI_ERROR_DEVICE_REMOVED),切回时游戏会重建设备和 swapchain。但我的 Manager 里还存着旧的 device 指针和 swapchain 数据,没有感知到重建。

修法:在 CreateSwapChain 的 Hook 中检测到新的 swapchain 创建时,如果发现有旧的 swapchain 数据残留(且旧 swapchain 已失效),先清理旧数据再注册新的。

同时在设备丢失时(Present 返回 DXGI_ERROR_DEVICE_REMOVED),清空所有缓存的 device 相关指针,强制进入 IDLE 状态,等待游戏重建。

这个修法让 alt-tab 切回后能恢复正常渲染。

3.5 动态分辨率切换

GTA 5 在场景切换(室内↔室外)时会动态调整渲染分辨率。这会导致 swapchain 的 back buffer 尺寸变化——但 swapchain 本身可能不重建(取决于具体实现)。

问题:从室外切回室内后,中间纹理的尺寸没有更新,还是室外的高分辨率尺寸,导致 blit 操作写入越界。

修法:在每次 Present 时检查 swapchain 的 back buffer 尺寸是否发生了变化。如果变了,标记 downscaleImage 为“需要重建”,下次 EnsureDownscaleImage 时用新尺寸重新创建。

这个检测逻辑加在 GetPresentBuffer 里:

ID3D12Resource* GetPresentBuffer(IDXGISwapChain* swapchain, UINT bufferIndex)
{
    auto& sc = swapchains[swapchain];
    auto desc = sc.buffers[0]->GetDesc();
    if (desc.Width != sc.width || desc.Height != sc.height) {
        // 尺寸变了,标记中间纹理需要重建
        sc.width = desc.Width;
        sc.height = desc.Height;
        if (sc.downscaleImage) {
            sc.downscaleImage->Release();
            sc.downscaleImage = nullptr;
        }
    }
    return sc.buffers[bufferIndex];
}

四、测试小结

测试结果汇总:

场景 渲染结构 Transfer 阶段 稳定性 备注
室内场景 直渲 swapchain 阶段1清屏 回退路径验证
室外场景 复杂后处理 阶段2缩放 ⚠️→✅ 状态还原修了之后稳定
窗口 resize swapchain 重建 阶段2缩放 ⚠️→✅ 资源清理补上后稳定
alt-tab 切出/切回 设备丢失恢复 阶段2缩放 ⚠️→✅ 设备状态检测补上后稳定
动态分辨率切换 分辨率变化 阶段2缩放 ⚠️→✅ 尺寸检测补上后稳定

五个场景,全部修通了。坦白说比我想的差——原以为联调通了就差不多了,结果一测真实游戏场景翻出这么多问题。但比联调刚结束时“以为一个场景就代表全部”的盲目乐观要真实得多。

五、从测试里抽象出来的规律

几个问题背后的共性,试着总结一下:

规律一:swapchain usage 是不能假设的。

GTA 5 的室内场景没有开 transfer 位,室外场景开了。同一个游戏、不同场景下行为都不一样。这不是 bug,是 D3D12 规范允许的。对中间件来说,只能检测不能假设,检测不到就走回退,没有第二条路。

规律二:资源状态的处理不能是单向的。

最初“转成 COPY_DEST,用完转回 PRESENT”的思路,只在“应用在 present 前不再碰 swapchain image”的场景下成立。碰上 GTA 5 室外场景这种自己还要用 swapchain 做后处理的,就被打脸了。还原原始状态是一个更安全的选择。

规律三:中间件引入的每一条额外 API 调用,在高频路径上都会积累。

窗口 resize 和动态分辨率切换场景里,频繁的 GetDesc 和资源创建销毁拖慢了帧率。在高频路径(每帧 Present)上,非必要的操作该砍就砍,结果该缓存就缓存。

规律四:同步问题的根因往往不在崩溃点。

deferred 示例的闪烁问题,虽然这篇博客主要记录 GTA 5 测试,但这个规律在 GTA 5 里同样成立——某些帧的异常暗画面,表面看是后处理没执行,实际是 resource barrier 的阶段不对。这种问题在单 pass 的简单管线里永远暴露不出来。以后每加一种新的管线类型测试,都可能翻出新的同步隐患。

六、当前限制与遗留问题

测了一圈,系统的几个明确限制也清楚了:

swapchain usage 不含 transfer 时只能回退到清屏。 效果上完全不是“缩放”,只是一个可用性兜底。

多 queue 场景没有覆盖。 目前假设 present 和注入在同一个 command queue 上。如果有应用用独立的 copy queue 做 present,现在的同步链可能出错。

GTA 5 的某些极端场景还没跑完。 快速连续 alt-tab 来回切、长时间运行后的内存泄漏检查,这些还没做。

只测了 GTA 5。 其他游戏引擎的管线复杂度可能完全不同。Unreal Engine 的渲染管线跟 GTA 5 的 RAGE 引擎差别很大。

七、AI 帮我做了什么

测试和排查过程中,AI 帮了我几次:

1. 分析资源状态还原的问题。

出室外场景的 barrier 警告时,我不确定该不该把状态转回 PRESENT。AI 指出,如果应用在 present 前还会继续使用 swapchain image,强制转成 PRESENT 会导致后续操作失败。正确的做法是“记录原始状态,用完还原”,而不是假设 PRESENT 是最终状态。

2. 帮我检查资源释放顺序。

写 UnregisterSwapchain 时,AI 提醒我释放顺序很重要——应该先释放依赖于 back buffer 的资源(RTV、downscale image),再释放 back buffer 本身。如果顺序反了,可能会出现“资源已被释放但还有引用”的问题。

3. 帮我梳理测试策略。

测试之前,我把 GTA 5 中要覆盖的场景列给 AI 看。它指出我漏掉了“快速连续 Present”的场景——如果游戏帧率很高(比如 120fps+),每帧都要执行注入操作,锁竞争和性能开销会成为瓶颈。这个建议让我在测试计划里补上了高帧率场景。

八、下一步

测试暴露出来的问题里,最紧迫的是把 GTA 5 的极端场景跑完——快速连续 alt-tab 来回切、长时间运行后的稳定性。这些在实际使用中太常见了。

性能方面也该打点了。现在知道哪些问题修了,但整体注入开销到底是多少、锁等待占了多少、blit 本身占了多少,还没有量化数据。下一篇打算集中做性能打点和初步优化。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐