前言

GAMES104的王希说过:

游戏引擎的世界里,它的核心是靠Tick()函数把这个世界驱动起来。

本来单是一个CPU的计时器是不至于为其写一篇博客的,但把GPU计时器功能加上后就不一样了。在这一篇中,我们将讲述如何使用CPU计时器获取帧间隔,以及使用GPU计时器获取GPU中执行一系列指令的间隔。

DirectX11 With Windows SDK完整目录

欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。

CPU计时器

在游戏中,我们需要用到高精度的计时器。在这里我们直接使用龙书的GameTimer,但为了区分后续的GPU计时器,现在将其改名为CpuTimer

class CpuTimer
{
public:
CpuTimer(); float TotalTime()const; // 返回从Reset()调用之后经过的时间,但不包括暂停期间的
float DeltaTime()const; // 返回帧间隔时间 void Reset(); // 计时开始前或者需要重置时调用
void Start(); // 在开始计时或取消暂停的时候调用
void Stop(); // 在需要暂停的时候调用
void Tick(); // 在每一帧开始的时候调用
bool IsStopped() const; // 计时器是否暂停/结束 private:
double m_SecondsPerCount = 0.0;
double m_DeltaTime = -1.0; __int64 m_BaseTime = 0;
__int64 m_PausedTime = 0;
__int64 m_StopTime = 0;
__int64 m_PrevTime = 0;
__int64 m_CurrTime = 0; bool m_Stopped = false;
};

在构造函数中,我们将查询计算机performance counter的频率,因为该频率对于当前CPU是固定的,我们只需要在初始化阶段获取即可。然后我们可以求出单个count经过的时间:

CpuTimer::CpuTimer()
{
__int64 countsPerSec{};
QueryPerformanceFrequency((LARGE_INTEGER*)&countsPerSec);
m_SecondsPerCount = 1.0 / (double)countsPerSec;
}

在开始使用计数器之前,或者想要重置计时器时,我们需要调用一次Reset(),以当前时间作为基准时间。这些__int64的类型存储的单位为count:

void CpuTimer::Reset()
{
__int64 currTime{};
QueryPerformanceCounter((LARGE_INTEGER*)&currTime); m_BaseTime = currTime;
m_PrevTime = currTime;
m_StopTime = 0;
m_PausedTime = 0; // 涉及到多次Reset的话需要将其归0
m_Stopped = false;
}

然后这里我们先看Stop()的实现,就是记录当前Stop的时间和标记为暂停中:

void CpuTimer::Stop()
{
if( !m_Stopped )
{
__int64 currTime{};
QueryPerformanceCounter((LARGE_INTEGER*)&currTime); m_StopTime = currTime;
m_Stopped = true;
}
}

在调用Reset()完成初始化后,我们就可以调用Start()启动计时了。当然如果之前调用过Stop()的话,将当前Stop()Start()经过的暂停时间累加到总的暂停时间:

void CpuTimer::Start()
{
__int64 startTime{};
QueryPerformanceCounter((LARGE_INTEGER*)&startTime); // 累积暂停开始到暂停结束的这段时间
//
// |<-------d------->|
// ----*---------------*-----------------*------------> time
// m_BaseTime m_StopTime startTime if( m_Stopped )
{
m_PausedTime += (startTime - m_StopTime); m_PrevTime = startTime;
m_StopTime = 0;
m_Stopped = false;
}
}

然后在每一帧开始之前调用Tick()函数,更新当前帧与上一帧之间的间隔时间,该用时通过DeltaTime()获取,可以用于物理世界的更新:

void CpuTimer::Tick()
{
if( m_Stopped )
{
m_DeltaTime = 0.0;
return;
} __int64 currTime{};
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
m_CurrTime = currTime; // 当前Tick与上一Tick的帧间隔
m_DeltaTime = (m_CurrTime - m_PrevTime)*m_SecondsPerCount; m_PrevTime = m_CurrTime; if(m_DeltaTime < 0.0)
{
m_DeltaTime = 0.0;
}
} float CpuTimer::DeltaTime() const
{
return (float)m_DeltaTime;
}

如果要获取游戏开始到现在经过的时间(不包括暂停期间),可以使用TotalTime()

float CpuTimer::TotalTime()const
{
// 如果调用了Stop(),暂停中的这段时间我们不需要计入。此外
// m_StopTime - m_BaseTime可能会包含之前的暂停时间,为
// 此我们可以从m_StopTime减去之前累积的暂停的时间
//
// |<-- 暂停的时间 -->|
// ----*---------------*-----------------*------------*------------*------> time
// m_BaseTime m_StopTime startTime m_StopTime m_CurrTime if( m_Stopped )
{
return (float)(((m_StopTime - m_PausedTime)-m_BaseTime)*m_SecondsPerCount);
} // m_CurrTime - m_BaseTime包含暂停时间,但我们不想将它计入。
// 为此我们可以从m_CurrTime减去之前累积的暂停的时间
//
// (m_CurrTime - m_PausedTime) - m_BaseTime
//
// |<-- 暂停的时间 -->|
// ----*---------------*-----------------*------------*------> time
// m_BaseTime m_StopTime startTime m_CurrTime else
{
return (float)(((m_CurrTime-m_PausedTime)-m_BaseTime)*m_SecondsPerCount);
}
}

总的来说,正常的调用顺序是Reset()Start(),然后每一帧调用Tick(),并获取DeltaTime()。在需要暂停的时候就Stop(),恢复用Start()

GPU计时器

假如我们需要统计某一个渲染过程的用时,如后处理、场景渲染、阴影绘制等,可能有人的想法是这样的:

timer.Start();
DrawSomething();
timer.Tick();
float deltaTime = timer.DeltaTime();

实际上这样并不能测量,因为CPU跟GPU是异步执行的。设备上下文所调用的大部分方法实际上是向显卡塞入命令然后立刻返回,这些命令被缓存到一个命令队列中等待被消化。

因此,如果要测量GPU中一段执行过程的用时,我们需要向GPU插入两个时间戳,然后将这两个时间戳的Tick Count回读到CPU,最后通过GPU获取这期间的频率来求出间隔。

目前GpuTimer放在Common文件夹中,供36章以后的项目使用,后续会考虑放到之前的项目中。

GpuTimer类的声明如下:

class GpuTimer
{
public:
GpuTimer() = default; // recentCount为0时统计所有间隔的平均值
// 否则统计最近N帧间隔的平均值
void Init(ID3D11Device* device, ID3D11DeviceContext* deviceContext, size_t recentCount = 0); // 重置平均用时
// recentCount为0时统计所有间隔的平均值
// 否则统计最近N帧间隔的平均值
void Reset(ID3D11DeviceContext* deviceContext, size_t recentCount = 0);
// 给命令队列插入起始时间戳
HRESULT Start();
// 给命令队列插入结束时间戳
void Stop();
// 尝试获取间隔
bool TryGetTime(double* pOut);
// 强制获取间隔(可能会造成阻塞)
double GetTime();
// 计算平均用时
double AverageTime()
{
if (m_RecentCount)
return m_AccumTime / m_DeltaTimes.size();
else
return m_AccumTime / m_AccumCount;
} private: static bool GetQueryDataHelper(ID3D11DeviceContext* pContext, bool loopUntilDone, ID3D11Query* query, void* data, uint32_t dataSize); std::deque<double> m_DeltaTimes; // 最近N帧的查询间隔
double m_AccumTime = 0.0; // 查询间隔的累计总和
size_t m_AccumCount = 0; // 完成回读的查询次数
size_t m_RecentCount = 0; // 保留最近N帧,0则包含所有 std::deque<GpuTimerInfo> m_Queries; // 缓存未完成的查询
Microsoft::WRL::ComPtr<ID3D11Device> m_pDevice;
Microsoft::WRL::ComPtr<ID3D11DeviceContext> m_pImmediateContext;
};

其中,Init()用于获取D3D设备和设备上下文,并根据recentCount确定要统计最近N帧间隔的平均值,还是所有间隔的平均值:

void GpuTimer::Init(ID3D11Device* device, ID3D11DeviceContext* deviceContext, size_t recentCount)
{
m_pDevice = device;
m_pImmediateContext = deviceContext;
m_RecentCount = recentCount;
m_AccumTime = 0.0;
m_AccumCount = 0;
}

在调用Init()后,我们就可以开始调用Start()来给命令队列插入起始时间戳了。但在此之前,我们需要先介绍我们需要给命令队列插入的具体是什么。

ID3D11Device::CreateQueue--创建GPU查询

为了创建GPU查询,我们需要先填充D3D11_QUERY_DESC结构体:

typedef struct D3D11_QUERY_DESC {
D3D11_QUERY Query;
UINT MiscFlags; // 目前填0
} D3D11_QUERY_DESC;

关于枚举类型D3D11_QUERY,我们现在只关注其中两个枚举值:

  • D3D11_QUERY_TIMESTAMP:通过ID3D11DeviceContext::GetData返回的UINT64表示的是一个时间戳的值。该查询还需要D3D11_QUERY_TIMESTAMP_DISJOINT的配合来判断当前查询是否有效。
  • D3D11_QUERY_TIMESTAMP_DISJOINT:用来确定当前的D3D11_QUERY_TIMESTAMP是否返回可信的结果,并可以获取当前流处理器的频率,来允许你将这两个tick变换成经过的时间来求出间隔。该查询只应该在每帧或多帧中执行一次,然后通过ID3D11DeviceContext::GetData返回D3D11_QUERY_DATA_TIMESTAMP_DISJOINT

D3D11_QUERY_DATA_TIMESTAMP_DISJOINT的结构体如下:

typedef struct D3D11_QUERY_DATA_TIMESTAMP_DISJOINT {
UINT64 Frequency; // 当前GPU每秒增加的counter数目
BOOL Disjoint; // 仅当其为false时,两个时间戳的询问才是有效的,表明这期间的频率是固定的
// 若为true,说明可能出现了拔开笔记本电源、过热、由于节点模式导致的功耗降低等
} D3D11_QUERY_DATA_TIMESTAMP_DISJOINT;

由于从GPU回读数据是一件很慢的事情,可能会拖慢1帧到几帧,为此我们需要把创建好的时间戳和频率/连续性查询先缓存起来。这里使用的是GpuTimerInfo

struct GpuTimerInfo
{
D3D11_QUERY_DATA_TIMESTAMP_DISJOINT disjointData {}; // 频率/连续性信息
uint64_t startData = 0; // 起始时间戳
uint64_t stopData = 0; // 结束时间戳
Microsoft::WRL::ComPtr<ID3D11Query> disjointQuery; // 连续性查询
Microsoft::WRL::ComPtr<ID3D11Query> startQuery; // 起始时间戳查询
Microsoft::WRL::ComPtr<ID3D11Query> stopQuery; // 结束时间戳查询
bool isStopped = false; // 是否插入了结束时间戳
};

Start()中我们需要同时创建查询、插入时间戳、开始连续性/频率查询。

HRESULT GpuTimer::Start()
{
if (!m_Queries.empty() && !m_Queries.back().isStopped)
return E_FAIL; GpuTimerInfo& info = m_Queries.emplace_back();
CD3D11_QUERY_DESC queryDesc(D3D11_QUERY_TIMESTAMP);
m_pDevice->CreateQuery(&queryDesc, info.startQuery.GetAddressOf());
m_pDevice->CreateQuery(&queryDesc, info.stopQuery.GetAddressOf());
queryDesc.Query = D3D11_QUERY_TIMESTAMP_DISJOINT;
m_pDevice->CreateQuery(&queryDesc, info.disjointQuery.GetAddressOf()); m_pImmediateContext->Begin(info.disjointQuery.Get());
m_pImmediateContext->End(info.startQuery.Get());
return S_OK;
}

需要注意的是,D3D11_QUERY_TIMESTAMP只通过ID3D11DeviceContext::End来插入起始时间戳;D3D11_QUERY_TIMESTAMP_DISJOINT则需要区分``ID3D11DeviceContext::BeginID3D11DeviceContext::End`。

在完成某个特效渲染后,我们可以调用Stop()来插入结束时间戳,并完成连续性/频率的查询:

void GpuTimer::Stop()
{
GpuTimerInfo& info = m_Queries.back();
m_pImmediateContext->End(info.disjointQuery.Get());
m_pImmediateContext->End(info.stopQuery.Get());
info.isStopped = true;
}

调用Stop()后,这时我们还不一定能够拿到间隔。考虑到运行时的性能分析考虑的是多间隔求平均,我们可以接受延迟几帧的回读。为此,我们可以使用TryGetTime(),尝试对时间最久远、仍未完成的查询尝试GPU回读:

bool GpuTimer::GetQueryDataHelper(ID3D11DeviceContext* pContext, bool loopUntilDone, ID3D11Query* query, void* data, uint32_t dataSize)
{
if (query == nullptr)
return false; HRESULT hr = S_OK;
int attempts = 0;
do
{
// 尝试GPU回读
hr = pContext->GetData(query, data, dataSize, 0);
if (hr == S_OK)
return true;
attempts++;
if (attempts > 100)
Sleep(1);
if (attempts > 1000)
{
assert(false);
return false;
}
} while (loopUntilDone && (hr == S_FALSE));
return false; bool GpuTimer::TryGetTime(double* pOut)
{
if (m_Queries.empty())
return false; GpuTimerInfo& info = m_Queries.front();
if (!info.isStopped) return false;
if (info.disjointQuery && !GetQueryDataHelper(m_pImmediateContext.Get(), false, info.disjointQuery.Get(), &info.disjointData, sizeof(info.disjointData)))
return false;
info.disjointQuery.Reset(); if (info.startQuery && !GetQueryDataHelper(m_pImmediateContext.Get(), false, info.startQuery.Get(), &info.startData, sizeof(info.startData)))
return false;
info.startQuery.Reset(); if (info.stopQuery && !GetQueryDataHelper(m_pImmediateContext.Get(), false, info.stopQuery.Get(), &info.stopData, sizeof(info.stopData)))
return false;
info.stopQuery.Reset(); if (!info.disjointData.Disjoint)
{
double deltaTime = static_cast<double>(info.stopData - info.startData) / info.disjointData.Frequency;
if (m_RecentCount > 0)
m_DeltaTimes.push_back(deltaTime);
m_AccumTime += deltaTime;
m_AccumCount++;
if (m_DeltaTimes.size() > m_RecentCount)
{
m_AccumTime -= m_DeltaTimes.front();
m_DeltaTimes.pop_front();
}
if (pOut) *pOut = deltaTime;
}
else
{
double deltaTime = -1.0;
} m_Queries.pop_front();
return true;
}

如果你就是在当前帧获取间隔,可以使用GetTime()

double GpuTimer::GetTime()
{
if (m_Queries.empty())
return -1.0; GpuTimerInfo& info = m_Queries.front();
if (!info.isStopped) return -1.0; if (info.disjointQuery)
{
GetQueryDataHelper(m_pImmediateContext.Get(), true, info.disjointQuery.Get(), &info.disjointData, sizeof(info.disjointData));
info.disjointQuery.Reset();
}
if (info.startQuery)
{
GetQueryDataHelper(m_pImmediateContext.Get(), true, info.startQuery.Get(), &info.startData, sizeof(info.startData));
info.startQuery.Reset();
}
if (info.stopQuery)
{
GetQueryDataHelper(m_pImmediateContext.Get(), true, info.stopQuery.Get(), &info.stopData, sizeof(info.stopData));
info.stopQuery.Reset();
} double deltaTime = -1.0;
if (!info.disjointData.Disjoint)
{
deltaTime = static_cast<double>(info.stopData - info.startData) / info.disjointData.Frequency;
if (m_RecentCount > 0)
m_DeltaTimes.push_back(deltaTime);
m_AccumTime += deltaTime;
m_AccumCount++;
if (m_DeltaTimes.size() > m_RecentCount)
{
m_AccumTime -= m_DeltaTimes.front();
m_DeltaTimes.pop_front();
}
} m_Queries.pop_front();
return deltaTime;
}

重置GPU计时器的话使用Reset()方法:

void GpuTimer::Reset(ID3D11DeviceContext* deviceContext, size_t recentCount)
{
m_Queries.clear();
m_DeltaTimes.clear();
m_pImmediateContext = deviceContext;
m_AccumTime = 0.0;
m_AccumCount = 0;
if (recentCount)
m_RecentCount = recentCount;
}

下面的代码展示如何使用GPU计时器:

m_GpuTimer.Init(m_pd3dDevice.Get(), m_pd3dImmediateContext.Get());

// ...
m_GpuTimer.Start();
{
// 一些绘制过程...
}
m_GpuTimer.Stop(); // ...
m_GpuTimer.TryGetTime(nullptr); // 只是为了更新下面的平均值
float avgTime = m_GpuTimer.AverageTime();

注意:如果游戏开启了垂直同步,那么当前帧中的某一个查询很可能会受到垂直同步的影响被拖长,从而导致原本当前帧GPU计时器的平均用时总和会接近两个垂直同步信号的间隔。

DirectX11 With Windows SDK完整目录

欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。

DirectX11--CPU与GPU计时器的更多相关文章

  1. OpenCL与CUDA,CPU与GPU

    OpenCL OpenCL(全称Open Computing Language,开放运算语言)是第一个面向异构系统通用目的并行编程的开放式.免费标准,也是一个统一的编程环境,便于软件开发人员为高性能计 ...

  2. 浅谈CPU和GPU的区别

    导读: CPU和GPU之所以大不相同,是由于其设计目标的不同,它们分别针对了两种不同的应用场景.CPU需要很强的通用性来处理各种不同的数据类型,而GPU面对的则是类型高度统一的.相互无依赖的大规模数据 ...

  3. CPU和GPU性能对比

    计算20000次10000点的fft,分别使用CPU和GPU,得 the running time of cpu is : 2.3696s the running time of gpu is : 0 ...

  4. CPU和GPU实现julia

    CPU和GPU实现julia           主要目的是通过对比,学习研究如何编写CUDA程序.julia的算法还是有一定难度的,但不是重点.由于GPU实现了也是做图像识别程序,所以缺省的就是和O ...

  5. 图像重采样(CPU和GPU)

    1 前言 之前在写影像融合算法的时候,免不了要实现将多光谱影像重采样到全色大小.当时为了不影响融合算法整体开发进度,其中重采样功能用的是GDAL开源库中的Warp接口实现的. 后来发现GDAL War ...

  6. CPU和GPU的区别

    个人认为CPU和GPU各有自己的适应领域.CPU(Central Processing Unit)计算核心较少,通常是双核.四核.八核,但是拥有大量的共享缓存.预测.乱序执行等优化,可以做逻辑非常复杂 ...

  7. CPU和GPU的差别

    详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt317 首先需要解释CPU和GPU这两个缩写分别代表什么.CPU即中央处理器, ...

  8. Shader 入门笔记(二) CPU和GPU之间的通信

    渲染流水线的起点是CPU,即应用阶段. 1)把数据加载到显存中 2)设置渲染状态,通俗说这些状态定义了场景中的网格是怎样被渲染的. 3)调用DrawCall,一个命令,CPU通知GPU.(这个命令仅仅 ...

  9. Caffe源码理解2:SyncedMemory CPU和GPU间的数据同步

    目录 写在前面 成员变量的含义及作用 构造与析构 内存同步管理 参考 博客:blog.shinelee.me | 博客园 | CSDN 写在前面 在Caffe源码理解1中介绍了Blob类,其中的数据成 ...

随机推荐

  1. 单例模式应用 | Shared_ptr引用计数管理器

    在我们模拟设计 shared_ptr 智能指针时发现,不同类型的 Shared_ptr 不能使用同一个引用计数管理器,这显然会造成内存上的浪费.因此我们考虑将其设计为单例模式使其所有的 Shared_ ...

  2. Architecture Principles

    Architecture Principles - Completed Components Name Statement Rationale Implications TOGAF Principle ...

  3. 原生js造轮子之模仿JQ的slideDown()与slideUp()

    代码如下: const slider = (function() { var Slider = {}; // the constructed function,timeManager,as such ...

  4. html5网页录音和语音识别

    背景 在输入方式上,人们总是在追寻一种更高效,门槛更低的方式,来降低用户使用产品的学习成本.语音输入也是一种尝试较多的方式,有些直接使用语音(如微信语音聊天),有些需要将语音转化为文字(语音识别).接 ...

  5. Web 开发中 Blob 与 FileAPI 使用简述

    本文节选自 Awesome CheatSheet/DOM CheatSheet,主要是对 DOM 操作中常见的 Blob.File API 相关概念进行简要描述. Web 开发中 Blob 与 Fil ...

  6. 关于recyclerview其item数据重复问题

    查找方法(query)的list只定义对象,不实例化,等到要添加的时候,再new一个新的对象出来. 千万不要如下图这样,否则item显示出来的永远是最新数据. (这个bug找了两天,还是基本功不扎实, ...

  7. leetcode921. 使括号有效的最少添加

    题目描述: 给定一个由 '(' 和 ')' 括号组成的字符串 S,我们需要添加最少的括号( '(' 或是 ')',可以在任何位置),以使得到的括号字符串有效. 从形式上讲,只有满足下面几点之一,括号字 ...

  8. 布局框架frameset

    <!DOCTYPE html>//demo.html <html> <head> <meta charset="UTF-8"> &l ...

  9. Java JDK 动态代理实现和代码分析

    JDK 动态代理 内容 一.动态代理解析 1. 代理模式 2. 为什么要使用动态代理 3. JDK 动态代理简单结构图 4. JDK 动态代理实现步骤 5. JDK 动态代理 API 5.1 java ...

  10. C. Sum of Cubes

    原题链接 https://codeforces.com/problemset/problem/1490/C 题目 题意 如果一个数 n = x3 + y3 (x, y可以相等, 且> 0) 输出 ...