前言
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 = https://www.cnblogs.com/X-Jun/p/0; // 起始時間戳
uint64_t stopData = 0; // 結束時間戳
Microsoft::WRL::ComPtr disjointQuery; // 連續性查詢
Microsoft::WRL::ComPtr startQuery; // 起始時間戳查詢
Microsoft::WRL::ComPtr 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::Begin和ID3D11DeviceContext::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();
下面是分塊延遲渲染統計各個pass用時的例子:

注意:如果游戲開啟了垂直同步,那么當前幀中的某一個查詢很可能會受到垂直同步的影響被拖長,從而導致原本當前幀GPU計時器的平均用時總和會接近兩個垂直同步信號的間隔,以下圖為例

DirectX11 With Windows SDK完整目錄
歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報,
作者:X_Jun 出處:http://www.cnblogs.com/X-Jun/ 本文著作權歸X_Jun(博客園)所有(CSDN為x_jun96),歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利,轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/498642.html
標籤:其他
下一篇:相機控制, 相機跟隨
