线性移动卡顿问题

8
我使用 ID3DXSpriteDirect3D9 中创建了简单、独立于帧率、可变时间步长、线性移动。大多数用户可能无法注意到它,但在某些电脑上(包括我的电脑)经常发生卡顿。
  • 开启或关闭 VSync 均会出现卡顿。

  • 我发现在 OpenGL 渲染器中也有同样的问题。

  • 这不是浮点数问题。

  • 似乎问题只存在于 AERO Transparent Glass 窗口模式中(全屏、无边框全屏或禁用 Aero 时问题可以得到缓解),当窗口失去焦点时问题更加严重。

编辑:

即使发生卡顿,帧间隔时间也不会超出 16 到 17 毫秒。

看起来我的帧间隔时间测量日志代码有问题。我现在已经修复了它。

  • 通常情况下,开启 VSync 后一帧渲染需要 17 毫秒,但有时候(可能是在发生卡顿时)会跳到 25-30 毫秒。

(我只在应用程序退出时记录日志,不会影响性能)

    device->Clear(0, 0, D3DCLEAR_TARGET, D3DCOLOR_ARGB(255, 255, 255, 255), 0, 0);

    device->BeginScene();

    sprite->Begin(D3DXSPRITE_ALPHABLEND);

    QueryPerformanceCounter(&counter);

    float time = counter.QuadPart / (float) frequency.QuadPart;

    float deltaTime = time - currentTime;

    currentTime = time;

    position.x += velocity * deltaTime;

    if (position.x > 640)
        velocity = -250;
    else if (position.x < 0)
        velocity = 250;

    position.x = (int) position.x;

    sprite->Draw(texture, 0, 0, &position, D3DCOLOR_ARGB(255, 255, 255, 255));

    sprite->End();

    device->EndScene();

    device->Present(0, 0, 0, 0);

感谢 Eduard Wirch 和 Ben Voigt 的帮助,我们修复了计时器的问题(尽管这并未解决初始问题)。
float time()
{
    static LARGE_INTEGER start = {0};
    static LARGE_INTEGER frequency;

    if (start.QuadPart == 0)
    {
        QueryPerformanceFrequency(&frequency);
        QueryPerformanceCounter(&start);
    }

    LARGE_INTEGER counter;

    QueryPerformanceCounter(&counter);

    return (float) ((counter.QuadPart - start.QuadPart) / (double) frequency.QuadPart);
}
编辑 #2:

到目前为止,我尝试了三种更新方法:

1) 变量时间步长

    x += velocity * deltaTime;

2) 固定时间步长

    x += 4;

3) 固定时间步长+插值

    accumulator += deltaTime;

    float updateTime = 0.001f;

    while (accumulator > updateTime)
    {
        previousX = x;

        x += velocity * updateTime;

        accumulator -= updateTime;
    }

    float alpha = accumulator / updateTime;

    float interpolatedX = x * alpha + previousX * (1 - alpha);

所有方法的工作方式基本相同,固定时间步长看起来更好,但不能完全依赖帧率,也无法完全解决问题(仍然偶尔会出现跳跃(卡顿)现象)。
到目前为止,禁用AERO透明玻璃或切换到全屏模式是唯一显著的积极变化。
我正在使用NVIDIA最新驱动程序GeForce 332.21 Driver和Windows 7 x64 Ultimate。

尝试比较不同的GPU驱动程序版本。如果在OpenGL中也发生了这种情况,那么它就不是某个DX的怪癖了。 - user180326
我也听说过这种行为,还有OpenGL的一些其他资料也提到了。 - Jonas Schäfer
似乎OpenGL也有完全相同的问题 - https://dev59.com/S3XYa4cB1Zd3GeqP7pHa - Demion
2个回答

8
解决方案的一部分是一个简单的精度数据类型问题。将速度计算替换为一个常数,你会看到非常平滑的运动。分析计算表明,您正在将QueryPerformanceCounter()的结果存储在一个浮点数中。QueryPerformanceCounter()返回一个数字,在我的电脑上看起来像这样:724032629776。这个数字至少需要5个字节才能存储。然而,float使用4个字节(实际数字只有24位)来存储值。因此,当将QueryPerformanceCounter()的结果转换为float时,精度会丢失。有时这会导致deltaTime为零,导致卡顿。
这就部分解释了为什么一些用户没有遇到这个问题。这完全取决于QueryPerformanceCounter()的结果是否适合于float
这个问题的解决方案是:使用double(或者如Ben Voigt建议的那样:在将新值转换为float之前,存储初始性能计数器并从中减去。这将为您提供更多的余地,但当应用程序长时间运行时(取决于性能计数器的增长速度),可能会再次达到float分辨率限制。)
修复了这个问题后,卡顿情况有所改善,但并没有完全消失。分析运行时行为表明,有时会跳过一帧。应用程序GPU命令缓冲区由Present刷新,但是present命令仍然留在应用程序上下文队列中,直到下一个垂直同步(即使Present在垂直同步之前很久就被调用了(14ms))。进一步的分析表明,一个后台进程(f.lux)偶尔会告诉系统设置伽马坡。这个命令需要完整的GPU队列在执行之前运行干净。可能是为了避免副作用。这个GPU刷新是在“present”命令移动到GPU队列之前启动的。系统阻塞了视频调度,直到GPU运行干净。这需要等到下一个垂直同步。因此,present数据包直到下一帧才被移动到GPU队列。这种情况的可见效果是卡顿。
你的电脑上可能不会运行f.lux,但你可能会遇到类似的背景干预。你需要自己在系统上寻找问题的源头。我写了一篇关于如何诊断帧跳过的博客文章:Diagnose frame skips and stutter in DirectX applications。你也可以在那里找到关于诊断f.lux是罪魁祸首的整个故事。

即使你找到了帧跳的源头,我也怀疑在启用dwm窗口合成的情况下你是否能够实现稳定的60fps。原因是,你没有直接绘制到屏幕上,而是绘制到dwm的共享表面上。由于它是一个共享资源,它可以被其他人锁定任意长的时间,这使得你无法保持应用程序的帧率稳定。如果你真的需要稳定的帧率,请全屏显示,或者禁用窗口合成(在Windows 7上,Windows 8不允许禁用窗口合成):

#include <dwmapi.h>
...
HRESULT hr = DwmEnableComposition(DWM_EC_DISABLECOMPOSITION);
if (!SUCCEEDED(hr)) {
   // log message or react in a different way
}

2
不,解决方案是首先使用整数算术从QueryPerformanceCounter中减去两个结果,然后将差异转换。 - Ben Voigt
我正在运行带有Aero的Windows 7。当您用这行代码替换位置计算时会发生什么:position.x += 1; - Eduard Wirch
没有解决启用Aero时的卡顿问题。但是非常值得修复,非常感谢。希望我正确实现了它。已在第一篇帖子中添加代码。另外,我检查过deltaTime从未为零(除了一开始)。 - Demion
@Eduard Wirch 我使用了 position.x += 4; 因为在慢速下很难注意到卡顿,而且它似乎工作得更好一些(可能是主观的),但即使如此,我仍然可以偶尔注意到一些峰值,在窗口模式下和当窗口失去焦点时,它像往常一样卡顿。所以问题没有解决。 - Demion
1
@shell:如果你在窗口模式下运行,则你的应用程序与其他进程共享GPU。你无法完全控制垂直同步循环。OP的主要问题是计算精度损失。窗口模式下的卡顿必须逐个案例分析,以查看哪些应用程序会干扰垂直同步循环。如果你遇到这个问题,请阅读我链接的博客文章,了解如何使用GPUView来分析问题。 - Eduard Wirch
显示剩余3条评论

2

我查看了您的源代码,发现您每帧只处理一个窗口消息。这在过去给我造成了卡顿。

我建议循环使用PeekMessage,直到它返回零表示消息队列已耗尽。之后再渲染一帧。

因此,请更改:

if (PeekMessageW(&message, 0, 0, 0, PM_REMOVE))

为了

while (PeekMessageW(&message, 0, 0, 0, PM_REMOVE))

编辑:

我编译并运行了您的代码(使用另一个纹理),它对我来说顺利地显示了移动。我没有启用Aero(Windows 8)。

我注意到一件事:您设置了 D3DCREATE_SOFTWARE_VERTEXPROCESSING。您是否尝试将其设置为 D3DCREATE_HARDWARE_VERTEXPROCESSING


这并没有太大帮助。即使在消息队列循环和渲染循环分别在两个线程中时,仍然会出现口吃的情况。尽管在全屏和无边框窗口全屏模式下效果要好得多(没有明显的口吃)。 - Demion
D3DCREATE_HARDWARE_VERTEXPROCESSING 没有任何区别。不幸的是,许多用户无法重现它,但其他人却遇到了同样的问题,我看到了类似的问题和论坛主题,而且没有解决方案。 - Demion
关于 if/while 的困境,如果代码设置如下:while (gamerunning){if PeekMessage(...){/*handle message*/}else{/*update game*/}} - 事实上你的方法并没有什么区别。实际上,优先使用 if 方法是因为它会在每次接收到新消息后检查游戏是否应该退出,并且当窗口关闭时游戏将不会尝试更新。关于硬件/顶点处理,我建议有一个 if-else 树,像这样:“尝试创建硬件设备,如果失败则尝试混合,如果失败则尝试软件,如果失败则说明你的电脑不行”。 - Proxy
@Proxy 感谢您的反馈。他的代码没有使用if/else,只是使用了if,然后无论如何都会渲染。因此,我建议采用最简解决方案来确定潜在问题。硬件/软件顶点处理也是同样的情况。 - typ1232

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接