-
[DirectX] QPC로 FPS(프레임 레이트) 구현하기C++/미분류 2024. 2. 20. 16:06
QPC란 Query Performance Counter로, Windows에서 사용되는 시간 측정을 위한 기능입니다.
이번 글에서는 QPC를 이용해서 함수의 호출 주기를 구하는 방법을 살펴보고, 프레임마다 호출되는 함수에 이 방법을 적용하여 FPS를 구해보도록 하겠습니다.
이번 글은 이전 글의 코드([WinAPI] 로그 출력하기 (Console, TRACE))의 예제를 기반으로 작성되었습니다.
1. 살펴보기
WinAPI를 이용한 이전 프로젝트는 화면을 그리기 위해 OnRender함수를 매 프레임마다 호출합니다.
따라서, 매 프레임마다 호출되는 OnRender함수의 호출 주기를 알 수 있다면, 프레임 레이트를 구할 수 있습니다.
2. QPC 사용하기
더보기QPC에서 주로 사용되는 함수는 다음과 같습니다.
LARGE_INTEGER myPrevTime; LARGE_INTEGER myFrequency; QueryPerformanceFrequency(&myFrequency); QueryPerformanceCounter(&myPrevTime);
QueryPerformanceFrequency는 카운터의 주기를 받아옵니다.
주기란 1초에 해당하는 틱의 값으로, 시스템이 부팅될 때 고정되어 변하지 않습니다.
QueryPerformanceCounter는 카운터의 현재 틱 값을 받아옵니다.
호출 시점의 틱을 받아올 수 있다는 특징을 이용해서 다음과 같은 로직을 생각할 수 있습니다.
- 프로그램 시작 시점의 Tick을 저장합니다. (A)
- OnRender함수 호출 시의 Tick을 저장합니다. (B)
- B-A를 계산하면, 프로그램 시작 시점과 OnRender 호출 시의 틱 차이를 얻을 수 있습니다.
- A에 B를 대입합니다. (A = B)
- 다음 OnRender함수 호출 시의 Tick을 저장합니다. (B`)
- B`-A값은 이전 OnRender함수와 현재 OnRender함수 호출 사이의 틱 간격입니다.
- (B`-A)/Frequency는 OnRender함수 호출 사이의 시간 간격입니다. (단위는 Second)
위 로직을 구현하기 위해, 기존 코드의 클래스 필드에 다음과 같은 변수를 추가하겠습니다.
class MyApp { ... private: ... LARGE_INTEGER myPrevTime; LARGE_INTEGER myFrequency; ... };
myPrevTime은 이전 틱 값을 저장할 변수, myFrequency는 카운터의 주기를 저장할 변수입니다.
해당 값을 프로그램 실행 시 초기화합니다.
HRESULT MyApp::initialize(HINSTANCE hInstance) { QueryPerformanceFrequency(&myFrequency); QueryPerformanceCounter(&myPrevTime); ... }
Initialize함수의 제일 처음 부분에 초기화 코드를 추가했습니다.
다음은 OnRender 함수입니다.
HRESULT MyApp::OnRender() { HRESULT hr = S_OK; hr = CreateDeviceResources(); if (SUCCEEDED(hr)) { LARGE_INTEGER currentTime; QueryPerformanceCounter(¤tTime); FLOAT deltaTime = (FLOAT)( (DOUBLE)(currentTime.QuadPart - myPrevTime.QuadPart) / (DOUBLE)(myFrequency.QuadPart) ); myPrevTime = currentTime; ... }
OnRender함수가 정상적으로 실행되는 조건문 직후에 함수의 호출 주기를 구하는 코드를 추가했습니다.
위 코드는 현재 틱(currentTime)을 구하고, 이전 호출의 틱(myPrevTime)의 차를 계산하는 코드입니다.
위 코드를 통해 계산된 deltaTime이 OnRender함수 호출 사이의 시간이 됩니다.
3. FPS 구하기
더보기FPS는 1초 동안 지나간 프레임의 수 입니다.
OnRender함수는 매 프레임마다 호출되며, OnRender함수의 호출 간격을 알 수 있으니, 이 시간을 역수로 취하면 1초 동안 지나간 프레임의 수를 구할 수 있습니다.
하지만 매 간격마다 프레임 레이트를 갱신할 경우, 프리징이 걸렸을 때 등 프레임 레이트가 크게 변동하는 상황이 발생할 수 있습니다.
민감한 프로그램이라면 위의 방법을 취하는 것이 맞지만, 일정 프레임 단위로 평균 프레임 레이트를 표시하는 것도 합리적인 방법이 될 수 있습니다.
이번에는 10프레임의 평균에 대하여 프레임 레이트를 구해보도록 하겠습니다.
우선, 프레임 레이트 계산에 필요한 변수를 클래스에 추가합니다.
class MyApp { ... private: ... std::deque<FLOAT> frameTime; const int frameAvgCount = 10; ... };
frameTime은 이전 10프레임의 시간을 저장할 컨테이너 입니다.
10개 이상의 값이 들어올 경우 이전 값을 제외해야 하니 deque를 사용했습니다.
frameAvgCount는 평균을 계산할 프레임의 수 입니다.
OnRender함수의 deltaTime을 계산하는 코드 이후, 프레임의 평균을 계산하는 코드를 추가하겠습니다.
HRESULT MyApp::OnRender() { HRESULT hr = S_OK; hr = CreateDeviceResources(); if (SUCCEEDED(hr)) { ... frameTime.push_back(deltaTime); while (frameTime.size() > frameAvgCount) frameTime.pop_front(); FLOAT totalTime = 0.0f; for (auto& i : frameTime) { totalTime += i; } FLOAT frameRate = 1 / (totalTime / frameAvgCount); std::cout << frameRate << "\n"; ... }
현재 프레임의 시간을 frameTime에 추가하고, 컨테이너의 원소 수가 frameAvgCount를 초과했을 경우 초과하지 않을 때 까지 비워줍니다.
이후 컨테이너의 값의 평균을 역수로 취하면 10프레임 동안의 프레임 레이트가 계산됩니다.
QPC의 사용법과, 이를 이용한 프레임 레이트를 구하는 방법을 알아보았습니다.
이번 글에서 사용된 코드는 Github에서 보실 수 있습니다.
글과 예제에서는 콘솔 로그를 취해서 프레임 레이트를 출력했습니다.
만드시는 프로그램에 따라, 프레임 레이트가 UI에 노출되어야 할 수 있으니, 값을 적절히 이용하는 것이 좋겠습니다.
이번 글이 도움이 되셨기를 바랍니다.
감사합니다.
'C++ > 미분류' 카테고리의 다른 글
[Direct2D] 비트맵 이미지 그리기 (1) 2024.02.26 [WinAPI] 키보드 입력 (WM_KEYDOWN, WM_KEYUP, GetAsyncKeyState) (0) 2024.02.23 [WinAPI] 로그 출력하기 (Console, TRACE) (0) 2024.02.16 [Direct2D] 도형 그려보기 (0) 2024.02.08 [Win32] Windows application 만들기 (1) 2024.01.25