DirectX/10.0/Direct3D/FPS、CPU 使用率和计时器
本教程将介绍三个新类,它们将封装每秒帧数计数器、CPU 使用率计数器和高精度计时器的功能。本教程中的代码基于 Font Engine 教程中的代码。
第一个新类是 FpsClass。FpsClass 将负责记录应用程序运行的每秒帧数。了解每秒渲染的帧数为我们提供了衡量应用程序性能的一个很好的指标。这是用于确定可接受图形渲染速度的行业标准指标之一。它在实现新功能时也很有用,可以查看它们对帧速率的影响。如果新功能将帧速率降低了一半,那么您就可以立即意识到存在重大问题,只需使用这个简单的计数器即可。请记住,当前计算机的标准 fps 速度为 60 fps。低于 60 fps 被认为性能较差,低于 30 fps 则会被人眼明显察觉。编码时的一般规则是最大限度地提高 fps,如果正确实现的新功能严重影响了速度,那么需要证明其合理性,至少要记录下来。
第二个新类是 CpuClass。它将负责记录 CPU 使用率,以便我们可以将当前的 CPU 使用率百分比显示在屏幕上。了解 CPU 使用率对于调试代码的新更改很有用,类似于 fps 的用法。它提供了一个简单且直接的指标,用于识别最近实现的糟糕代码或算法。
最后一个新类是 TimerClass。这是一个高精度计时器,我们可以用它来计时事件并确保应用程序及其各个组件都同步到一个公共时间框架。
框架
[edit | edit source]本教程的框架,包括三个新类,如下所示
我们将从检查三个新类开始本教程。
Fpsclass.h
[edit | edit source]FpsClass 只是一个与计时器关联的计数器。它计算在一秒钟内发生的帧数,并不断更新该计数。
//////////////////////////////////////////////////////////////////////////////// // Filename: fpsclass.h //////////////////////////////////////////////////////////////////////////////// #ifndef _FPSCLASS_H_ #define _FPSCLASS_H_ ///////////// // LINKING // ///////////// #pragma comment(lib, "winmm.lib") ////////////// // INCLUDES // ////////////// #include <windows.h> #include <mmsystem.h> //////////////////////////////////////////////////////////////////////////////// // Class name: FpsClass //////////////////////////////////////////////////////////////////////////////// class FpsClass { public: FpsClass(); FpsClass(const FpsClass&); ~FpsClass(); void Initialize(); void Frame(); int GetFps(); private: int m_fps, m_count; unsigned long m_startTime; }; #endif
Fpsclass.cpp
[edit | edit source]/////////////////////////////////////////////////////////////////////////////// // Filename: fpsclass.cpp /////////////////////////////////////////////////////////////////////////////// #include "fpsclass.h" FpsClass::FpsClass() { } FpsClass::FpsClass(const FpsClass& other) { } FpsClass::~FpsClass() { }
Initialize 函数将所有计数器设置为零并启动计时器。
void FpsClass::Initialize() { m_fps = 0; m_count = 0; m_startTime = timeGetTime(); return; }
Frame 函数必须在每一帧调用,以便它可以将帧计数增加 1。如果发现已过去一秒,它将把帧计数存储在 m_fps 变量中。然后,它将重置计数并重新启动计时器。
void FpsClass::Frame() { m_count++; if(timeGetTime() >= (m_startTime + 1000)) { m_fps = m_count; m_count = 0; m_startTime = timeGetTime(); } }
GetFps 返回刚过去的最后一秒的每秒帧速率。此函数应该不断查询,以便可以将最新的 fps 显示在屏幕上。
int FpsClass::GetFps() { return m_fps; }
Cpuclass.h
[edit | edit source]CpuClass 用于确定每秒发生的总 CPU 使用率百分比。
/////////////////////////////////////////////////////////////////////////////// // Filename: cpuclass.h /////////////////////////////////////////////////////////////////////////////// #ifndef _CPUCLASS_H_ #define _CPUCLASS_H_
我们使用 pdh 库来查询 CPU 使用率。
///////////// // LINKING // ///////////// #pragma comment(lib, "pdh.lib") ////////////// // INCLUDES // ////////////// #include <pdh.h> /////////////////////////////////////////////////////////////////////////////// // Class name: CpuClass /////////////////////////////////////////////////////////////////////////////// class CpuClass { public: CpuClass(); CpuClass(const CpuClass&); ~CpuClass(); void Initialize(); void Shutdown(); void Frame(); int GetCpuPercentage(); private: bool m_canReadCpu; HQUERY m_queryHandle; HCOUNTER m_counterHandle; unsigned long m_lastSampleTime; long m_cpuUsage; }; #endif
Cpuclass.cpp
[edit | edit source]/////////////////////////////////////////////////////////////////////////////// // Filename: cpuclass.cpp /////////////////////////////////////////////////////////////////////////////// #include "cpuclass.h" CpuClass::CpuClass() { } CpuClass::CpuClass(const CpuClass& other) { } CpuClass::~CpuClass() { }
Initialize 函数将设置用于查询 CPU 使用率的句柄。此处设置的查询将合并系统中所有 CPU 的使用情况,并返回总计,而不是每个 CPU 的使用情况。如果由于任何原因无法获得查询句柄或轮询 CPU 使用率,它将把 m_canReadCpu 标志设置为 false,并将 CPU 使用率保持为零百分比。某些 CPU 和操作系统特权级别可能会导致此操作失败。我们还会启动计时器,以便我们只在一秒钟内采样一次 CPU 使用率。
void CpuClass::Initialize() { PDH_STATUS status; // Initialize the flag indicating whether this object can read the system cpu usage or not. m_canReadCpu = true; // Create a query object to poll cpu usage. status = PdhOpenQuery(NULL, 0, &m_queryHandle); if(status != ERROR_SUCCESS) { m_canReadCpu = false; } // Set query object to poll all cpus in the system. status = PdhAddCounter(m_queryHandle, TEXT("\\Processor(_Total)\\% processor time"), 0, &m_counterHandle); if(status != ERROR_SUCCESS) { m_canReadCpu = false; } m_lastSampleTime = GetTickCount(); m_cpuUsage = 0; return; }
Shutdown 函数释放我们用于查询 CPU 使用率的句柄。
void CpuClass::Shutdown() { if(m_canReadCpu) { PdhCloseQuery(m_queryHandle); } return; }
与 FpsClass 一样,我们必须在每一帧调用 Frame 函数。但是,为了减少查询次数,我们使用 m_lastSampleTime 变量来确保我们只在一秒钟内采样一次。因此,每一秒我们都会询问 CPU 的使用率并将该值保存在 m_cpuUsage 中。超过这一点就没有必要了。
void CpuClass::Frame() { PDH_FMT_COUNTERVALUE value; if(m_canReadCpu) { if((m_lastSampleTime + 1000)
GetCpuPercentage 函数将当前 CPU 使用率的值返回给任何调用函数。同样,如果由于任何原因无法读取 CPU,我们只需将使用率设置为零。
int CpuClass::GetCpuPercentage() { int usage; if(m_canReadCpu) { usage = (int)m_cpuUsage; } else { usage = 0; } return usage; }
Timerclass.h
[edit | edit source]TimerClass 是一个高精度计时器,它测量执行帧之间的时间间隔。它的主要用途是同步需要标准时间框架进行移动的对象。在本教程中,我们将不会使用它,但我们将它实现到代码中,以便您了解如何将其应用于您的项目。TimerClass 的最常见用法是使用帧时间来计算当前帧中经过了多少百分比的秒数,然后按该百分比移动对象。
//////////////////////////////////////////////////////////////////////////////// // Filename: timerclass.h //////////////////////////////////////////////////////////////////////////////// #ifndef _TIMERCLASS_H_ #define _TIMERCLASS_H_ ////////////// // INCLUDES // ////////////// #include <windows.h> //////////////////////////////////////////////////////////////////////////////// // Class name: TimerClass //////////////////////////////////////////////////////////////////////////////// class TimerClass { public: TimerClass(); TimerClass(const TimerClass&); ~TimerClass(); bool Initialize(); void Frame(); float GetTime(); private: INT64 m_frequency; float m_ticksPerMs; INT64 m_startTime; float m_frameTime; }; #endif
Timerclass.cpp
[edit | edit source]/////////////////////////////////////////////////////////////////////////////// // Filename: timerclass.cpp /////////////////////////////////////////////////////////////////////////////// #include "timerclass.h" TimerClass::TimerClass() { } TimerClass::TimerClass(const TimerClass& other) { } TimerClass::~TimerClass() { }
Initialize 函数首先查询系统以查看它是否支持高频率计时器。如果它返回频率,我们将使用该值来确定每毫秒将发生多少个计数器滴答。然后,我们可以在每一帧中使用该值来计算帧时间。在 Initialize 函数的末尾,我们将查询此帧的开始时间以启动计时。
bool TimerClass::Initialize() { // Check to see if this system supports high performance timers. QueryPerformanceFrequency((LARGE_INTEGER*)&m_frequency); if(m_frequency == 0) { return false; } // Find out how many times the frequency counter ticks every millisecond. m_ticksPerMs = (float)(m_frequency / 1000); QueryPerformanceCounter((LARGE_INTEGER*)&m_startTime); return true; }
Frame 函数由主程序在每次执行循环中调用。这样我们就可以计算循环之间的时间差,并确定执行此帧所花费的时间。我们将查询、计算并将此帧的时间存储到 m_frameTime 中,以便任何调用对象都可以将其用于同步。然后,我们将当前时间存储为下一帧的开始时间。
void TimerClass::Frame() { INT64 currentTime; float timeDifference; QueryPerformanceCounter((LARGE_INTEGER*)& currentTime); timeDifference = (float)(currentTime - m_startTime); m_frameTime = timeDifference / m_ticksPerMs; m_startTime = currentTime; return; }
GetTime 返回计算的最近的帧时间。
float TimerClass::GetTime() { return m_frameTime; }
Systemclass.h
[edit | edit source]现在我们已经查看了三个新类,我们可以检查它们如何融入框架。对于所有三个新类,它们都将位于 SystemClass 下。
//////////////////////////////////////////////////////////////////////////////// // Filename: systemclass.h //////////////////////////////////////////////////////////////////////////////// #ifndef _SYSTEMCLASS_H_ #define _SYSTEMCLASS_H_ /////////////////////////////// // PRE-PROCESSING DIRECTIVES // /////////////////////////////// #define WIN32_LEAN_AND_MEAN ////////////// // INCLUDES // ////////////// #include <windows.h> /////////////////////// // MY CLASS INCLUDES // /////////////////////// #include "inputclass.h" #include "graphicsclass.h"
我们在此包含三个新类。
#include "fpsclass.h" #include "cpuclass.h" #include "timerclass.h" //////////////////////////////////////////////////////////////////////////////// // Class name: SystemClass //////////////////////////////////////////////////////////////////////////////// class SystemClass { public: SystemClass(); SystemClass(const SystemClass&); ~SystemClass(); bool Initialize(); void Shutdown(); void Run(); LRESULT CALLBACK MessageHandler(HWND, UINT, WPARAM, LPARAM); private: bool Frame(); void InitializeWindows(int&, int&); void ShutdownWindows(); private: LPCWSTR m_applicationName; HINSTANCE m_hinstance; HWND m_hwnd; InputClass* m_Input; GraphicsClass* m_Graphics;
我们为三个新类中的每一个创建一个新对象。
FpsClass* m_Fps; CpuClass* m_Cpu; TimerClass* m_Timer; }; ///////////////////////// // FUNCTION PROTOTYPES // ///////////////////////// static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); ///////////// // GLOBALS // ///////////// static SystemClass* ApplicationHandle = 0; #endif
Systemclass.cpp
[edit | edit source]我们将只介绍自字体教程以来的更改。
//////////////////////////////////////////////////////////////////////////////// // Filename: systemclass.cpp //////////////////////////////////////////////////////////////////////////////// #include "systemclass.h" SystemClass::SystemClass() { m_Input = 0; m_Graphics = 0;
在类构造函数中将三个新对象初始化为 null。
m_Fps = 0; m_Cpu = 0; m_Timer = 0; } bool SystemClass::Initialize() { int screenWidth, screenHeight; bool result; // Initialize the width and height of the screen to zero before sending the variables into the function. screenWidth = 0; screenHeight = 0; // Initialize the windows api. InitializeWindows(screenWidth, screenHeight); // Create the input object. This object will be used to handle reading the keyboard input from the user. m_Input = new InputClass; if(!m_Input) { return false; } // Initialize the input object. result = m_Input->Initialize(m_hinstance, m_hwnd, screenWidth, screenHeight); if(!result) { MessageBox(m_hwnd, L"Could not initialize the input object.", L"Error", MB_OK); return false; } // Create the graphics object. This object will handle rendering all the graphics for this application. m_Graphics = new GraphicsClass; if(!m_Graphics) { return false; } // Initialize the graphics object. result = m_Graphics->Initialize(screenWidth, screenHeight, m_hwnd); if(!result) { return false; }
创建并初始化 FpsClass。
// Create the fps object. m_Fps = new FpsClass; if(!m_Fps) { return false; } // Initialize the fps object. m_Fps->Initialize();
创建并初始化 CpuClass。
// Create the cpu object. m_Cpu = new CpuClass; if(!m_Cpu) { return false; } // Initialize the cpu object. m_Cpu->Initialize();
创建并初始化 TimerClass。
// Create the timer object. m_Timer = new TimerClass; if(!m_Timer) { return false; } // Initialize the timer object. result = m_Timer->Initialize(); if(!result) { MessageBox(m_hwnd, L"Could not initialize the Timer object.", L"Error", MB_OK); return false; } return true; } void SystemClass::Shutdown() {
在 Shutdown 函数中释放三个新类对象。
// Release the timer object. if(m_Timer) { delete m_Timer; m_Timer = 0; } // Release the cpu object. if(m_Cpu) { m_Cpu->Shutdown(); delete m_Cpu; m_Cpu = 0; } // Release the fps object. if(m_Fps) { delete m_Fps; m_Fps = 0; } // Release the graphics object. if(m_Graphics) { m_Graphics->Shutdown(); delete m_Graphics; m_Graphics = 0; } // Release the input object. if(m_Input) { m_Input->Shutdown(); delete m_Input; m_Input = 0; } // Shutdown the window. ShutdownWindows(); return; }
最后修改的是 Frame 函数。每个新类都需要为应用程序执行的每一帧调用自己的 Frame 函数。一旦调用了每个类的 Frame 函数,我们就可以查询每个类的更新数据并将其发送到 GraphicsClass 以供使用。
bool SystemClass::Frame() { bool result; // Update the system stats. m_Timer->Frame(); m_Fps->Frame(); m_Cpu->Frame(); // Do the input frame processing. result = m_Input->Frame(); if(!result) { return false; } // Do the frame processing for the graphics object. result = m_Graphics->Frame(m_Fps->GetFps(), m_Cpu->GetCpuPercentage(), m_Timer->GetTime()); if(!result) { return false; } // Finally render the graphics to the screen. result = m_Graphics->Render(); if(!result) { return false; } return true; }
//////////////////////////////////////////////////////////////////////////////// // Filename: graphicsclass.h //////////////////////////////////////////////////////////////////////////////// #ifndef _GRAPHICSCLASS_H_ #define _GRAPHICSCLASS_H_ ///////////// // GLOBALS // ///////////// const bool FULL_SCREEN = true;
在本教程中,我们禁用了 vsync,以便应用程序可以尽可能快地运行。
const bool VSYNC_ENABLED = false; const float SCREEN_DEPTH = 1000.0f; const float SCREEN_NEAR = 0.1f; /////////////////////// // MY CLASS INCLUDES // /////////////////////// #include "d3dclass.h" #include "cameraclass.h" #include "textclass.h" //////////////////////////////////////////////////////////////////////////////// // Class name: GraphicsClass //////////////////////////////////////////////////////////////////////////////// class GraphicsClass { public: GraphicsClass(); GraphicsClass(const GraphicsClass&); ~GraphicsClass(); bool Initialize(int, int, HWND); void Shutdown(); bool Frame(int, int, float); bool Render(); private: D3DClass* m_D3D; CameraClass* m_Camera; TextClass* m_Text; }; #endif
我只介绍自上一个字体教程以来更改过的函数。
//////////////////////////////////////////////////////////////////////////////// // Filename: graphicsclass.cpp //////////////////////////////////////////////////////////////////////////////// #include "graphicsclass.h"
Frame 函数现在接受 fps、cpu 和计时器计数作为参数。fps 和 cpu 计数在 TextClass 中设置,以便可以将其渲染到屏幕上。
bool GraphicsClass::Frame(int fps, int cpu, float frameTime) { bool result; // Set the frames per second. result = m_Text->SetFps(fps, m_D3D->GetDeviceContext()); if(!result) { return false; } // Set the cpu usage. result = m_Text->SetCpu(cpu, m_D3D->GetDeviceContext()); if(!result) { return false; } // Set the position of the camera. m_Camera->SetPosition(0.0f, 0.0f, -10.0f); return true; }
//////////////////////////////////////////////////////////////////////////////// // Filename: textclass.h //////////////////////////////////////////////////////////////////////////////// #ifndef _TEXTCLASS_H_ #define _TEXTCLASS_H_ /////////////////////// // MY CLASS INCLUDES // /////////////////////// #include "fontclass.h" #include "fontshaderclass.h" //////////////////////////////////////////////////////////////////////////////// // Class name: TextClass //////////////////////////////////////////////////////////////////////////////// class TextClass { private: struct SentenceType { ID3D11Buffer *vertexBuffer, *indexBuffer; int vertexCount, indexCount, maxLength; float red, green, blue; }; struct VertexType { D3DXVECTOR3 position; D3DXVECTOR2 texture; }; public: TextClass(); TextClass(const TextClass&); ~TextClass(); bool Initialize(ID3D11Device*, ID3D11DeviceContext*, HWND, int, int, D3DXMATRIX); void Shutdown(); bool Render(ID3D11DeviceContext*, D3DXMATRIX, D3DXMATRIX);
现在我们有两个新函数用于设置 fps 计数和 cpu 使用率。
bool SetFps(int, ID3D11DeviceContext*); bool SetCpu(int, ID3D11DeviceContext*); private: bool InitializeSentence(SentenceType**, int, ID3D11Device*); bool UpdateSentence(SentenceType*, char*, int, int, float, float, float, ID3D11DeviceContext*); void ReleaseSentence(SentenceType**); bool RenderSentence(ID3D11DeviceContext*, SentenceType*, D3DXMATRIX, D3DXMATRIX); private: FontClass* m_Font; FontShaderClass* m_FontShader; int m_screenWidth, m_screenHeight; D3DXMATRIX m_baseViewMatrix; SentenceType* m_sentence1; SentenceType* m_sentence2; }; #endif
我只介绍自上一个字体教程以来更改过的函数。
/////////////////////////////////////////////////////////////////////////////// // Filename: textclass.cpp /////////////////////////////////////////////////////////////////////////////// #include "textclass.h"
SetFps 函数接受给定的 fps 整数值,并将其转换为字符串。一旦 fps 计数以字符串格式,它就会连接到另一个字符串,使其具有表示 fps 速度的前缀。之后,它将存储在句子结构中以供渲染。SetFps 函数还将 fps 字符串的颜色设置为绿色(如果超过 60 fps)、黄色(如果低于 60 fps)和红色(如果低于 30 fps)。
bool TextClass::SetFps(int fps, ID3D11DeviceContext* deviceContext) { char tempString[16]; char fpsString[16]; float red, green, blue; bool result; // Truncate the fps to below 10,000. if(fps > 9999) { fps = 9999; } // Convert the fps integer to string format. _itoa_s(fps, tempString, 10); // Setup the fps string. strcpy_s(fpsString, "Fps: "); strcat_s(fpsString, tempString); // If fps is 60 or above set the fps color to green. if(fps >= 60) { red = 0.0f; green = 1.0f; blue = 0.0f; } // If fps is below 60 set the fps color to yellow. if(fps
SetCpu 函数与 SetFps 函数类似。它获取 cpu 值并将其转换为字符串,然后将其存储在句子结构中并进行渲染。
bool TextClass::SetCpu(int cpu, ID3D11DeviceContext* deviceContext) { char tempString[16]; char cpuString[16]; bool result; // Convert the cpu integer to string format. _itoa_s(cpu, tempString, 10); // Setup the cpu string. strcpy_s(cpuString, "Cpu: "); strcat_s(cpuString, tempString); strcat_s(cpuString, "%"); // Update the sentence vertex buffer with the new string information. result = UpdateSentence(m_sentence2, cpuString, 20, 40, 0.0f, 1.0f, 0.0f, deviceContext); if(!result) { return false; } return true; }
现在,我们可以在渲染场景时看到 FPS 和 CPU 使用率。此外,我们现在拥有精确计时器,用于确保对象的平移和旋转始终保持一致,无论应用程序运行的帧速率如何。
1. 重新编译代码并确保可以看到 fps 和 cpu 值。按 Esc 退出。
2. 在 graphicsclass.h 文件中打开 vsync,查看你的显卡/显示器将应用程序锁定到哪个刷新速度。