• DirectX12_Windows_GameDevelop_4:Direct3D应用程序框架


    一、性能计时器

    (1)基础知识

    • 为了制作出精准的动画效果就需要精确地计量时间,特别是要准确地度量动画每帧画面之间的时间间隔如果帧率较高,则帧间隔时间就会比较短,因此我们需要使用高精度的计时器
    • 为了精确地度量时间,我们将采用性能计时器。为此我们需要使用头文件性能计时器返回的时间度量单位叫做计数(count),可通过QueryPerformanceCounter函数来获取性能计时器测量的当前时刻(以计数为单位)。使用QueryPerformanceFrequency函数可以获取性能计时器的频率,其单位为:计数/秒

    (2)代码实现

    • 根据以上内容就可以设计我们的性能计时器了,话不多说,都在码里。
    • 头文件:
    //***************************************************************************************
    // GameTimer.h by Frank Luna (C) 2011 All Rights Reserved.
    //***************************************************************************************
    
    #ifndef GAMETIMER_H
    #define GAMETIMER_H
    
    class GameTimer
    {
    public:
    	GameTimer();
    
    	float TotalTime()const; // 获取总时间(单位:秒)
    	float DeltaTime()const; // 获取帧间隔(单位:秒)
    
    	void Reset(); // 在开始消息循环之前调用
    	void Start(); // 解除计时器暂停状态时调用
    	void Stop();  // 停止计时器时调用
    	void Tick();  // 每帧调用更新计时器
    
    private:
    	double mSecondsPerCount;	// 记录每个计数所包含的秒数
    	double mDeltaTime;			// 记录帧间隔(单位:秒)
    
    	__int64 mBaseTime;			// 计时器开始计时的计数(成为基础计数)
    	__int64 mPausedTime;		// 计时器暂停期间的总计数(仅记录暂停-开启期间的总计数,如果目前还处于暂停而未开启,则不包括目前暂停期间计数)
    	__int64 mStopTime;			// 计时器暂停时的计数(若未暂停则其值为0)
    	__int64 mPrevTime;			// 计时器上一帧的计数
    	__int64 mCurrTime;			// 计时器此帧的计数
    
    	bool mStopped;				// 计时器是否暂停
    };
    
    #endif // GAMETIMER_H
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 源文件:
    //***************************************************************************************
    // GameTimer.cpp by Frank Luna (C) 2011 All Rights Reserved.
    //***************************************************************************************
    
    #include 
    #include "GameTimer.h"
    
    GameTimer::GameTimer() // 计时器构造函数
    : mSecondsPerCount(0.0), mDeltaTime(-1.0), mBaseTime(0), 
      mPausedTime(0), mPrevTime(0), mCurrTime(0), mStopped(false)
    {
    	// 构造列表里把帧间隔mDeltaTime设为-1表示错误的帧间隔,因为其值应该通过调用Tick函数计算得到
    
    	__int64 countsPerSec;
    	QueryPerformanceFrequency((LARGE_INTEGER*)&countsPerSec); // 获取性能计时器的频率
    	mSecondsPerCount = 1.0 / (double)countsPerSec;  // 计算每个计数包含的秒数
    }
    
    // Returns the total time elapsed since Reset() was called, NOT counting any
    // time when the clock is stopped.
    float GameTimer::TotalTime()const // 获取计数器记录的总时间
    {
    	// If we are stopped, do not count the time that has passed since we stopped.
    	// Moreover, if we previously already had a pause, the distance 
    	// mStopTime - mBaseTime includes paused time, which we do not want to count.
    	// To correct this, we can subtract the paused time from mStopTime:  
    	//
    	//                     |<--paused time-->|
    	// ----*---------------*-----------------*------------*------------*------> time
    	//  mBaseTime       mStopTime        startTime     mStopTime    mCurrTime
    	
    	// 如果不考虑计数器停止,则 计数器总时间 = (此帧计数 - 基础计数) * 每计数包含秒数
    	//                                       = (mCurrTime - mBaseTime) * mSecondsPerCount
    	// 如果考虑计时器停止,则   计时器总时间 =  (此帧计数 - 基础计数 - 停止期间计数) * 每计数包含秒数
    	//                                       =  ((mCurrTime-mPausedTime)-mBaseTime)*mSecondsPerCount
    	// 下文else中代码表示计时器此时未停止,其记录的总时间就是上述公式
    	// 如果此时计时器已经停止,则代表不能使用此帧计数 作为计时器最后的时间,而应该使用停止时计数
    	// 则下文if中代码,表示记录总时间的公式就是:((mStopTime - mPausedTime)-mBaseTime)*mSecondsPerCount
    
    	if( mStopped )
    	{
    		return (float)(((mStopTime - mPausedTime)-mBaseTime)*mSecondsPerCount);
    	}
    
    	// The distance mCurrTime - mBaseTime includes paused time,
    	// which we do not want to count.  To correct this, we can subtract 
    	// the paused time from mCurrTime:  
    	//
    	//  (mCurrTime - mPausedTime) - mBaseTime 
    	//
    	//                     |<--paused time-->|
    	// ----*---------------*-----------------*------------*------> time
    	//  mBaseTime       mStopTime        startTime     mCurrTime
    	
    	else // 如果计数器未停止
    	{
    		return (float)(((mCurrTime-mPausedTime)-mBaseTime)*mSecondsPerCount);
    	}
    }
    
    float GameTimer::DeltaTime()const	// 获取帧间隔
    {
    	return (float)mDeltaTime;
    }
    
    void GameTimer::Reset() // 重置计时器
    {
    	__int64 currTime;
    	QueryPerformanceCounter((LARGE_INTEGER*)&currTime); // 获取重置时的计数
    
    	mBaseTime = currTime; // 计数器基础计数 = 计数器重置时的计数值
    	mPrevTime = currTime; // 计数器的上一帧计数 会在下一帧计算帧间隔时用到,因此其值为此时计数
    	mStopTime = 0;		  // 停止计数值为0
    	mStopped  = false;	  // 初始计数器未停止
    }
    
    void GameTimer::Start()   // 开始计时(仅当计时器暂停时,重新启用计时器)
    {
    	__int64 startTime;
    	QueryPerformanceCounter((LARGE_INTEGER*)&startTime);
    
    
    	// Accumulate the time elapsed between stop and start pairs.
    	//
    	//                     |<-------d------->|
    	// ----*---------------*-----------------*------------> time
    	//  mBaseTime       mStopTime        startTime     
    
    	if( mStopped ) // 如果计数器停止了
    	{
    		// 单次停止期间的计数 = 重新开启时计数 - 停止时计数
    		// 由于计数器可能多次停止和开始,mPausedTime记录的是计时器总停止期间的计数,因此此处用"+="
    		mPausedTime += (startTime - mStopTime); 	
    
    		mPrevTime = startTime;	// 记录上一帧计数,会在下一帧计算帧间隔时用到
    		mStopTime = 0;			// 将停止时计数重置为0
    		mStopped  = false;		// 重置计时器状态为未停止
    	}
    }
    
    void GameTimer::Stop() // 停止计时器
    {
    	if( !mStopped )
    	{
    		__int64 currTime;
    		QueryPerformanceCounter((LARGE_INTEGER*)&currTime); // 获取此时计数
    
    		mStopTime = currTime;	// 记录计时器在此时计数停止
    		mStopped  = true;		// 将计时器状态设为已停止
    	}
    }
    
    void GameTimer::Tick()	// 每帧时调用更新计时器
    {
    	if( mStopped )		// 若已经停止计时则不更新任何数据,并且设帧间隔为0秒
    	{
    		mDeltaTime = 0.0;
    		return;
    	}
    
    	__int64 currTime;	
    	QueryPerformanceCounter((LARGE_INTEGER*)&currTime);    // 获取此帧计数
    	mCurrTime = currTime;	// 使用currTime记录此帧计数
    
    	// Time difference between this frame and the previous.
    	// 帧间隔秒数 = (此帧计数 - 上帧计数) * 每个计数所代表秒数
    	mDeltaTime = (mCurrTime - mPrevTime)*mSecondsPerCount; 
    
    	// Prepare for next frame.
    	// 更新上帧计数为此帧计数(因为此帧间隔已计算完毕,将进入下一帧,下一帧的上帧计数就为此帧计数)
    	mPrevTime = mCurrTime;
    
    	// Force nonnegative.  The DXSDK's CDXUTTimer mentions that if the 
    	// processor goes into a power save mode or we get shuffled to another
    	// processor, then mDeltaTime can be negative.
    	// 如果处理器进入省电模式或更换了处理器,则帧间隔mDeltaTime可能为负,因此此处强制其非负
    	if(mDeltaTime < 0.0)
    	{
    		mDeltaTime = 0.0;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141

    (3)问题讨论

    • 注意的是,按道理来说无论在哪一个处理器上调用QieryPerformanceCounter都应该返回当前时刻的计数值。然而由于基本输入/输出系统(BIOS)或硬件抽象层(HAL)上的缺陷,导致在不同的处理器上可能会得到不同的结果
    • 因此如果应用程序主线程切换到其他处理器上执行命令,则可能会导致帧间隔的波动甚至错误计算,如负帧间隔。
    • 我们可以使用SetThreadAffinityMask函数,防止应用程序主线程切换处理器,解决上述问题得到正确的计数差值以及帧间隔。

    二、Direct3D应用程序框架示例

    • 龙书所有的演示程序都使用了下列文件:
    头文件源文件
    d3dApp.hd3dApp.cpp
    d3dUtil.hd3dUtil.cpp
    • d3dUtil.h和d3dUtil.cpp文件中含有程序所需的实用工具代码。
    • d3dApp.h和d3dApp.cpp文件中含有Direct3D应用程序类核心代码。

    (1)D3DApp类

    • D3DApp类是一种基础的Direct3D应用程序类,它提供了创建应用程序主窗口、运行程序消息循环、处理窗口消息以及初始化Direct3D等多种功能的函数。
    • 此外该类还为应用程序例程定义了一组框架函数
    • 我们可以根据需求编写一个继承于D3DApp的类,重写架构的虚函数,以此从D3DApp类中派生出自定义的用户代码。
    • D3DApp类的定义如下:
    #include "../../Common/d3dUtil.h"
    #include "../../Common/GameTimer.h"
    
    // 链接所需的d3d12库
    #pragma comment(lib,"d3dcompiler.lib")
    #pragma comment(lib,"D3D12.lib")
    #pragma comment(lib,"dxgi.lib")
    
    class D3DApp
    {
    protected:
    	
    	D3DApp(HINSTANCE hInstance);	
    	D3DApp(const D3DApp& rhs) = delete;
    	D3DApp& operator=(const D3DApp& rhs) = delete;
    	virtual ~D3DApp();
    
    public:
    	
    	static D3DApp* GetApp();
    	
    	HINSTANCE AppInst()const;
    	HWND MainWnd()const;
    	float AspectRatio()const;
    
    	bool Get4xMsaaState()const;
    	void Set4xMsaaState(bool value);
    
    	int Run();
    
    	virtual bool Initialize();
    	virtual LRESULT MsgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
    
    protected:
    
    	virtual void CreateRtvAndDsvDescriptorHeaps();
    	virtual void OnResize();
    	virtual void Update(const GameTimer& gt) = 0;
    	virtual void Draw(const GameTimer& gt) = 0;
    
    	// 鼠标输入信息的处理流程
    	virtual void OnMouseDown(WPARAM btnState, int x, int y) {}
    	virtual void OnMouseUp(WPARAM btnState, int x, int y) {}
    	virtual void OnMouseMove(WPARAM btnState, int x, int y) {}
    
    protected:
    
    	bool InitMainWindow();
    	bool InitDirect3D();
    	void CreateCommandObjects();
    	void CreateSwapChain();
    
    
    	void FlushCommandQueue();
    
    	ID3D12Resource* CurrentBackBuffer()const
    	{
    		return mSwapChainBuffer[mCurrBackBuffer].Get();
    	}
    
    	D3D12_CPU_DESCRIPTOR_HANDLE CurrentBackBufferView()const
    	{
    		return CD3DX12_CPU_DESCRIPTOR_HANDLE(
    			mRtvHeap->GetCPUDescriptorHandleForHeapStart(),
    			mCurrBackBuffer,
    			mRtvDescriptorSize
    		);
    	}
    
    	D3D12_CPU_DESCRIPTOR_HANDLE DepthStencilView()const
    	{
    		return mDsvHeap->GetCPUDescriptorHandleForHeapStart();
    	}
    
    	void CalculateFrameStats();
    
    	void LogAdapters();
    	void LogAdapterOutputs(IDXGIAdapter* adapter);
    	void LogOutputDisplayModes(IDXGIOutput* output, DXGI_FORMAT format);
    
    protected:
    
    	static D3DApp* mApp;
    
    	HINSTANCE mhAppInst = nullptr;		// 应用程序实例句柄
    	HWND mhMainWnd = nullptr;			// 主窗口句柄
    	bool mAppPaused = false;			// 应用程序是否暂停
    	bool mMinimized = false;			// 应用程序是否最小化
    	bool mMaximized = false;			// 应用程序是否最大化
    	bool mResizing = false;				// 大小调整栏是否受到拖拽
    	bool mFullscreenState = false;		// 是否开启全屏模式
    
    	bool m4xMsaaState = false;			// 是否其启用4X MSAA技术
    	UINT m4xMsasQuality = 0;			// 4X MSAA的质量级别
    
    	GameTimer mTimer;					// 记录帧间隔和游戏总时间
    
    	Microsoft::WRL::ComPtr<IDXGIFactory4> mdxgiFactory;
    	Microsoft::WRL::ComPtr<IDXGISwapChain> mSwapChain;
    	Microsoft::WRL::ComPtr<ID3D12Device> md3dDevice;
    
    	Microsoft::WRL::ComPtr<ID3D12Fence> mFence;
    	UINT64 mCurrentFence = 0;
    
    	Microsoft::WRL::ComPtr<ID3D12CommandQueue> mCommandQueue;
    	Microsoft::WRL::ComPtr<ID3D12CommandAllocator> mDirectCmdListAlloc;
    	Microsoft::WRL::ComPtr<ID3D12GraphicsCommandList> mCommandList;
     
    	static const int SwapChainBufferCount = 2;
    	int mCurrBackBuffer = 0;
    	Microsoft::WRL::ComPtr<ID3D12Resource> mSwapChainBuffer[SwapChainBufferCount];
    	Microsoft::WRL::ComPtr<ID3D12Resource> mDepthStencilBuffer;
    
    	Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> mRtvHeap;
    	Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> mDsvHeap;
    
    	D3D12_VIEWPORT mScreenViewport;
    	D3D12_RECT mScissorRect;
    
    	UINT mRtvDescriptorSize = 0;
    	UINT mDsvDescriptorSize = 0;
    	UINT mCbvSrvUavDescriptorSize = 0;
    
    	// 用户应该在派生类的派生构造函数中自定义这些初始值
    	std::wstring mMainWndCaption = L"d3d App";
    	D3D_DRIVER_TYPE md3dDriverType = D3D_DRIVER_TYPE_HARDWARE;
    	DXGI_FORMAT mBackBufferFormat = DXGI_FORMAT_R8G8B8A8_UNORM;
    	DXGI_FORMAT mDepthStencilFormat = DXGI_FORMAT_D24_UNORM_S8_UINT;
    	int mClientWidth = 800;
    	int mClientHeight = 600;
    
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • D3DApp类的非框架方法见龙书4.5.2章即126页。
    • D3DApp类的六个框架方法见龙书4.5.3章即127页。
    • 除了六个框架方法是虚函数外D3DApp中还有3个虚函数,它们是用来处理鼠标消息的

    (2)帧的统计信息

    • 游戏和图形应用程序往往都会测量每秒渲染的帧数(frames per second,FPS)作为一种画面流畅度的标杆
    • 计算FPS的代码如下:
    void D3DApp::CalculateFrameStats()
    {
    	static int frameCnt = 0;		  // 记录1秒内渲染的帧数
    	static float timeElapsed = 0.0f;  // 记录上次计算帧率时的时刻
    
    	frameCnt++;  // 每一帧都会调用此函数,frameCnt累计帧数
    
    	// 如果距离上一次计算帧率已经达到了1秒,则计算并更新帧率
    	if ((mTimer.TotalTime() - timeElapsed) >= 1.0f)
    	{
    		// 计算帧率,即为这1秒内渲染的帧数frameCnt
    		float fps = (float)frameCnt;
    		// 计算渲染每帧所需的毫秒数
    		float mspf = 1000.0f / fps;
    
    		// 将fps和mspf转换为wstring类型
    		std::wstring fpsStr = std::to_wstring(fps);
    		std::wstring mspfStr = std::to_wstring(mspf);
    
    		// 构造帧率信息字符串
    		std::wstring windowText = mMainWndCaption +
    			L" fps:" + fpsStr +
    			L" mspf:" + mspfStr;
    		SetWindowText(mhMainWnd, windowText.c_str());
    
    		// 重置帧数,累计时刻值,因为timeElapsed是和TotalTime返回的总时间比较
    		frameCnt = 0;
    		timeElapsed += 1.0f;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • fps和mspf是衡量渲染效率的两个标准但是mspf要比fps更具体mspf能直接地表达渲染一帧所需时间据mspf的变化我们可以准确地知道渲染每帧所用时间的变化而fps是非线性的,从1000帧到250帧 和 从100帧到76.9帧,它们渲染每帧的时间都仅变化了3ms,但帧率的变化差距是很大的。

    三、完整的Direct3D应用程序框架

    • 话不多说,都在注解里:
    #pragma once
    
    // 如果在调试模式,则使用VS自带的内存泄露检测工具
    #if defined(DEBUG) || defined(_DEBUG)
    #define _CRTDBG_MAP_ALLOC
    #include 
    #endif
    
    #include "../../Common/d3dUtil.h"       // 包含一些工具
    #include "../../Common/GameTimer.h"     // 计时器类
    #include 
    
    // 链接必要的d3d1库。
    #pragma comment(lib,"d3dcompiler.lib")
    #pragma comment(lib, "D3D12.lib")
    #pragma comment(lib, "dxgi.lib")
    
    using Microsoft::WRL::ComPtr;
    using namespace std;
    using namespace DirectX;
    
    // D3DApp应用程序主窗体的窗口过程函数
    LRESULT CALLBACK
    MainWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
    
    // Direct3D Application类
    class D3DApp
    {
    protected:  
        /*
            以下函数为protected,提供给D3DApp的子类继承的。
        */
    
        D3DApp(HINSTANCE hInstance)                     // 构造函数
        {
            // 设计D3DApp为单例类,只能存在类的唯一实例
            assert(mApp == nullptr);    // 第二次或多次调用构造函数会报错
            mApp = this;                // 第一次调用,则记录其为唯一实例
        }
    
        D3DApp(const D3DApp& rhs) = delete;             // 拷贝构造函数,无法使用
        D3DApp& operator=(const D3DApp& rhs) = delete;  // 赋值运算函数,无法使用
    
        virtual ~D3DApp()                               // 析构函数,支持动态绑定
        {
            if (md3dDevice != nullptr)       // 如果D3D设备不为空,就刷新命令队列
                FlushCommandQueue();
        }
    
    public:
        /*
            以下函数为public,提供给类外调用的。
        */
    
        static D3DApp* GetApp()              // 获取此类的唯一实例
        {
            return mApp;
        }
    
        HINSTANCE AppInst()const             // 获取应用程序实例的句柄。只有运行中的程序实例,才有资格分配到实例句柄。一个应用程序可以运行多个实例,每运行一个实例,系统都会给该实例分配一个句柄值,并且通过hInstance传入WinMain中。
        {
            return mhAppInst;
        }
    
        HWND      MainWnd()const             // 获取程序主窗体的句柄
        {
            return mhMainWnd;
        }
    
        float     AspectRatio()const         // 获取屏幕长宽比
        {
            return static_cast<float>(mClientWidth) / mClientHeight;
        }
    
        bool Get4xMsaaState()const           // 获取4X MSAA的启用状态
        {
            return m4xMsaaState;
        }
    
        void Set4xMsaaState(bool value)      // 设置4X MSAA的状态
        {
            if (m4xMsaaState != value)       // 如果设置状态不同于原有状态
            {
                m4xMsaaState = value;
    
                // 根据新的多样本设置重新创建交换链和缓冲区
                CreateSwapChain();  // 创建交换链
                OnResize();         // 调整缓冲区大小
            }
        }
    
        int Run()               // 应用程序的运行函数
        {
            MSG msg = { 0 };    // MSG结构体表示Windows中的消息类型
    
            mTimer.Reset();     // 开始消息循环前-重置计时器
    
            while (msg.message != WM_QUIT)   // 如果从消息队列中检索到WM_QUIT信号则退出
            {
                // 如果存在Window消息,则对其进行处理
                if (PeekMessage(&msg, 0, 0, 0, PM_REMOVE)) 
                {
                    /*
                        PeekMessage函数第一个参数表示接受信息的变量。第二个表示
                        要检查的窗口的句柄,如果为零表示检索属于当前线程的任何窗口的消息。
                        第三和四个参数表示要检查的范围,都为0表示返回所有消息。
                        第五个参数表示如何处理消息,PM_REMOVE表示处理后将从消息队列删除
                    */
                    TranslateMessage(&msg);  // 将虚拟键消息转换为字符消息
                    DispatchMessage(&msg);   // 把消息分派给对应的窗口过程
                }
                // 否则,做动画/游戏
                else
                {
                    mTimer.Tick();              // 每帧时调用更新计时器
    
                    if (!mAppPaused)            // 如果没有停止应用程序
                    {
                        CalculateFrameStats();  // 计算帧统计信息
                        Update(mTimer);         // 更新数据
                        Draw(mTimer);           // 渲染画面
                    }
                    else                        // 如果程序暂停
                    {
                        Sleep(100);             // 则节能低速地运行消息循环
                    }
                }
            }
    
            return (int)msg.wParam;     // 返回应用程序的退出代码
        }
    
        virtual bool Initialize()       // 初始化函数
        {
            if (!InitMainWindow())      // 初始化应用程序主窗体
                return false;
    
            if (!InitDirect3D())        // 初始化Direct3D
                return false;
    
            OnResize();                 // 执行初始调整大小代码
    
            return true;
        }
    
        // 处理消息函数(参数:窗体句柄、消息类型、消息附加信息)
        virtual LRESULT MsgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
        {
            switch (msg)  // 根据消息类型分类处理
            {
            // 当窗口被激活或停用时,会发送WM_ACTIVATE
            case WM_ACTIVATE:
                if (LOWORD(wParam) == WA_INACTIVE) // 当窗口被停用时,我们暂停程序和计时器
                {
                    mAppPaused = true;
                    mTimer.Stop();
                }
                else                               // 当窗口激活时,启动程序和计时器
                {
                    mAppPaused = false;
                    mTimer.Start();
                }
                return 0;
    
            // 当用户调整窗口大小时,会发送WM_SIZE
            case WM_SIZE:
                // 保存新的客户端区域维度。
                mClientWidth = LOWORD(lParam);
                mClientHeight = HIWORD(lParam);
    
                // 当且仅当设备存在时进行设置
                if (md3dDevice)
                {
                    if (wParam == SIZE_MINIMIZED)       // 若窗口已最小化
                    {
                        mAppPaused = true;   // 暂停程序(因为最小化看不到画面)
                        mMinimized = true;   // 标记窗口最小化
                        mMaximized = false;  
                    }
                    else if (wParam == SIZE_MAXIMIZED)  // 若窗口已最大化
                    {
                        mAppPaused = false;  // 重置程序未暂停(最大化可以看到画面)
                        mMinimized = false;  
                        mMaximized = true;   // 标记窗口最大化
                        OnResize();          // 执行调整大小函数。最小化时看不到画面因此可以不执行修改
                    }
                    else if (wParam == SIZE_RESTORED)   // 若窗口大小改变,但不是最大化或最小化
                    {
                        // 若之前为最小化状态,则取消程序暂停、标记最小化为否、执行调整大小函数
                        if (mMinimized)
                        {
                            mAppPaused = false;
                            mMinimized = false;
                            OnResize();
                        }
    
                        // 若之前为最大化状态,则取消程序暂停、标记最大化为否、执行调整大小函数
                        else if (mMaximized)
                        {
                            mAppPaused = false;
                            mMaximized = false;
                            OnResize();
                        }
    
                        // 若之前为最大化状态,则取消程序暂停、标记最大化为否、执行调整大小函数
                        else if (mResizing)
                        {
                            // 根据拖动接收到的每个WM_SIZE消息调整大小,这毫无意义(而且很慢)
                            // 因此用户在拖动调整大小栏时,我们不会调整缓冲区大小
                            // 当用户调整窗口大小结束发送WM_EXITSIZEMOVE消息时,再设置缓冲区大小
                        }
    
                        else // 只要窗口大小调整完毕,调用调整函数OnResize
                        {
                            OnResize();
                        }
                    }
                }
                return 0;
    
            // 在窗口进入移动或调整大小模式循环后,会发送WM_ENTERSIZEMOVE
            case WM_ENTERSIZEMOVE:
                mAppPaused = true;  // 暂停程序
                mResizing = true;   // 设置程序的状态为:正在调整大小
                mTimer.Stop();      // 停止计时器
                return 0;
    
            // 在窗口退出移动或调整大小模式循环后,会发送WM_EXITSIZEMOVE
            case WM_EXITSIZEMOVE:
                mAppPaused = false; // 启动程序
                mResizing = false;  // 设置程序状态为:未调整大小
                mTimer.Start();     // 启动计时器
                OnResize();         // 调用调整大小函数
                return 0;
    
            // 在窗口被销毁时发送WM_DESTROY
            case WM_DESTROY:
                PostQuitMessage(0); // 向系统指示线程已发出终止请求,参数为应用程序退出代码。
                return 0;
    
            // 当菜单处于活动状态并且用户按下与任何助记键或加速键不对应的键时发送WM_MENUCHAR
            case WM_MENUCHAR:
                //当我们按下alt时不要发出嘟嘟声。
                return MAKELRESULT(0, MNC_CLOSE);
    
            // 当窗口的大小或位置即将更改时,发送WM_GETMINMAXINFO。应用程序可以使用此消息来替代窗口的默认的最小或最大跟踪大小
            // 最大跟踪大小是使用边框调整窗口大小可以生成的最大窗口大小。 最小跟踪大小是通过使用边框调整窗口大小可以生成的最小窗口大小
            case WM_GETMINMAXINFO: 
                ((MINMAXINFO*)lParam)->ptMinTrackSize.x = 200;      // 捕捉此消息以防止窗口变得太小
                ((MINMAXINFO*)lParam)->ptMinTrackSize.y = 200;      // 将窗口通过边框调整能达到的最小大小设为:200x200
                return 0;
    
            case WM_LBUTTONDOWN:    // 当用户在光标位于窗口的工作区中按下鼠标左键时发布
            case WM_MBUTTONDOWN:    // 当用户在光标位于窗口的工作区中按下鼠标中间按钮时发布
            case WM_RBUTTONDOWN:    // 当用户在光标位于窗口的工作区中按下鼠标右键时发布
                OnMouseDown(wParam, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));    // 调用鼠标按下处理函数(参数:虚拟键类型、鼠标光标的坐标xy)
                return 0;
            case WM_LBUTTONUP:    // 当用户在光标位于窗口的工作区中释放鼠标左键时发布
            case WM_MBUTTONUP:    // 当用户在光标位于窗口的工作区中释放鼠标中间按钮时发布
            case WM_RBUTTONUP:    // 当用户在光标位于窗口的工作区中释放鼠标右键时发布
                OnMouseUp(wParam, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));      // 调用鼠标释放处理函数(参数:虚拟键类型、鼠标光标的坐标xy)
                return 0;
            case WM_MOUSEMOVE:    // 当光标移动时发布到窗口
                OnMouseMove(wParam, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));    // 调用鼠标移动处理函数(参数:虚拟键类型、鼠标光标的坐标xy)
                return 0;
            case WM_KEYUP:        // 释放非系统键时,使用键盘焦点发布到窗口。非系统键是在 未 按下 Alt 键时按下的键,或在窗口具有键盘焦点时按下的键盘键
                if (wParam == VK_ESCAPE)            // 用户按下ESC键时结束程序
                {
                    PostQuitMessage(0);
                }
                else if ((int)wParam == VK_F2)      // 当用户按下F2时开启或关闭4X MSAA
                    Set4xMsaaState(!m4xMsaaState);
    
                return 0;
            }
    
            return DefWindowProc(hwnd, msg, wParam, lParam);    // 如果自定义的窗口过程没有处理某消息,则使用默认的窗口过程函数进行处理
        }
    
    protected:
        /*
            以下函数为protected,提供给D3DApp及其子类的类成员使用。
        */
    
        // 创建描述符堆
        virtual void CreateRtvAndDsvDescriptorHeaps()
        {
            // 创建描述结构体
            D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc;
            rtvHeapDesc.NumDescriptors = SwapChainBufferCount;  // 描述符堆中的描述符数量
            rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;  // 描述符的类型为RTV
            rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;// 其他选项设为无
            rtvHeapDesc.NodeMask = 0;
            ThrowIfFailed(md3dDevice->CreateDescriptorHeap(     // 用ID3D12Device接口创建描述符堆
                &rtvHeapDesc, IID_PPV_ARGS(mRtvHeap.GetAddressOf())
            ));
    
            D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc;             // 同上
            dsvHeapDesc.NumDescriptors = 1;                     // 数量为1
            dsvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;  // 类型是DSV
            dsvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
            dsvHeapDesc.NodeMask = 0;
            ThrowIfFailed(md3dDevice->CreateDescriptorHeap(
                &dsvHeapDesc, IID_PPV_ARGS(mDsvHeap.GetAddressOf())));
        }
    
        // 调整窗口大小函数
        virtual void OnResize()
        {
            assert(md3dDevice);             // 要求D3D设备已创建
            assert(mSwapChain);             // 要求交换链已创建
            assert(mDirectCmdListAlloc);    // 要求命令分配器已创建
    
            // 在更改任何资源之前刷新命令队列,保证GPU命令执行完毕
            FlushCommandQueue();
    
            /*
                通过D3D12GraphicsCommandList::Reset方法将命令列表恢复为刚创建时的初态
                Reset方法类似std::vector类的clear方法,清空容器内容以此对其进行复用
            */ 
            ThrowIfFailed(mCommandList->Reset(mDirectCmdListAlloc.Get(), nullptr));
    
            // 释放我们以前创建的缓冲区(后台和深度模板)资源,因为现在需要重写创建
            for (int i = 0; i < SwapChainBufferCount; ++i)
                mSwapChainBuffer[i].Reset();    
            mDepthStencilBuffer.Reset();
    
            // 调整交换链的大小
            ThrowIfFailed(mSwapChain->ResizeBuffers(
                SwapChainBufferCount,           // 交换链中缓冲区数目
                mClientWidth, mClientHeight,    // 新的缓冲区大小
                mBackBufferFormat,
                DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH));
    
            // 由于需要重写创建后台缓冲区,因此将后台缓冲区索引重置为0
            mCurrBackBuffer = 0;
    
            // CPU描述符句柄
            CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHeapHandle(mRtvHeap->GetCPUDescriptorHandleForHeapStart());
            
            // 对交换链中的每个缓冲区创建渲染目标视图
            for (UINT i = 0; i < SwapChainBufferCount; ++i)
            {
                // 先获取交换链中每个缓冲区资源,函数中i表示缓冲区索引,将获取到的资源记录到资源数组中
                ThrowIfFailed(
                    mSwapChain->GetBuffer(i, IID_PPV_ARGS(&mSwapChainBuffer[i])));
    
                // ID3D12Device::CreateRenderTargetView方法: 创建渲染目标视图
                md3dDevice->CreateRenderTargetView(
                    mSwapChainBuffer[i].Get(),  // 指定用作渲染目标的资源 
                    nullptr,                    // 资源中元素的数据类型,如果在资源创建时已指定,则可设为空
                    rtvHeapHandle);             // 引用所创建渲染目标视图的描述符句柄
    
                // 将rtvHeapHandle句柄偏移到描述符对中下一个缓冲区
                rtvHeapHandle.Offset(1, mRtvDescriptorSize);
            }
    
            // 创建深度/模具缓冲区和视图。
            D3D12_RESOURCE_DESC depthStencilDesc;
    
            // 使用结构体描述资源信息
            depthStencilDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; // 资源的维度:2D纹理
            depthStencilDesc.Alignment = 0;
            depthStencilDesc.Width = mClientWidth;                           // 资源的像素宽度
            depthStencilDesc.Height = mClientHeight;                         // 资源的像素高度
            depthStencilDesc.DepthOrArraySize = 1;                           // 资源的像素深度
            depthStencilDesc.MipLevels = 1;                                  // 资源的mipmap层级数量: 深度/模板缓冲资区只能有一个级别
            depthStencilDesc.Format = mDepthStencilFormat;                   // 像素数据的格式
            depthStencilDesc.SampleDesc.Count = m4xMsaaState ? 4 : 1;        // 每个像素采样次数
            depthStencilDesc.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0; // 质量级别
            depthStencilDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;          // 纹理布局
            depthStencilDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL;// 杂项标志
    
            D3D12_CLEAR_VALUE optClear;                             // 描述用于清理资源的优化值
            optClear.Format = mDepthStencilFormat;                  // 格式为像素数据的格式
            optClear.DepthStencil.Depth = 1.0f;                     // 深度清理为1.0
            optClear.DepthStencil.Stencil = 0;                      // 模板清理为0.0
            ThrowIfFailed(md3dDevice->CreateCommittedResource(      // 创建深度/模板缓冲区资源
                &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),// 指定堆为默认堆
                D3D12_HEAP_FLAG_NONE,                             // 杂项无选项
                &depthStencilDesc,                                // 描述资源结构体的指针
                D3D12_RESOURCE_STATE_COMMON,                      // 初始通常设为此
                &optClear,                                        // 清除优化对象的指针,若不希望优化则设为空
                IID_PPV_ARGS(mDepthStencilBuffer.GetAddressOf())
            ));
    
            // 利用资源的格式,为整个资源的第0层 mip层创建描述符
            md3dDevice->CreateDepthStencilView(
                mDepthStencilBuffer.Get(),
                nullptr,
                DepthStencilView()
            );
    
            mCommandList->ResourceBarrier(             // 转换资源屏障,告知GPU资源状态正在进行转换
                1,
                &CD3DX12_RESOURCE_BARRIER::Transition( // 将资源从初始状态转换为深度缓冲区
                    mDepthStencilBuffer.Get(),
                    D3D12_RESOURCE_STATE_COMMON,
                    D3D12_RESOURCE_STATE_DEPTH_WRITE
                )
            );
    
            // 执行调整大小命令
            ThrowIfFailed(mCommandList->Close());   // 关闭命令记录
            ID3D12CommandList* cmdsLists[] = { mCommandList.Get() };
            mCommandQueue->ExecuteCommandLists(_countof(cmdsLists), cmdsLists);  // 提交命令列表
    
            // 等待调整大小完成
            FlushCommandQueue();
    
            // 更新视口变换以覆盖客户端区域
            mScreenViewport.TopLeftX = 0;   // 视口左上角相对于后台缓冲区左上角的坐标
            mScreenViewport.TopLeftY = 0;
            mScreenViewport.Width = static_cast<float>(mClientWidth);   // 视口的宽和高 
            mScreenViewport.Height = static_cast<float>(mClientHeight);
            mScreenViewport.MinDepth = 0.0f;    // 视口的最大最小深度,所有物体的深度会被转换到这个范围内
            mScreenViewport.MaxDepth = 1.0f;
    
            // 更新裁剪矩形的大小
            mScissorRect = { 0, 0, mClientWidth, mClientHeight };
        }
    
        // 每帧更新函数,为纯虚函数,每个D3D应用程序必须独立定义
        virtual void Update(const GameTimer& gt) = 0;
    
        // 每帧渲染函数,为纯虚函数,每个D3D应用程序必须独立定义
        virtual void Draw(const GameTimer& gt) = 0;
    
        // 用于处理鼠标输入的函数 (D3DApp类中这些函数为空,即什么也不处理)
        virtual void OnMouseDown(WPARAM btnState, int x, int y) { }
        virtual void OnMouseUp(WPARAM btnState, int x, int y) { }
        virtual void OnMouseMove(WPARAM btnState, int x, int y) { }
    
    protected:
        /*
            以下函数为protected,提供给D3DApp及其子类的类成员使用。
        */
    
        // 初始化应用程序的主窗口
        bool InitMainWindow()
        {
            // 创建窗口描述结构体WNDCLASS并进行设置
            WNDCLASS wc;
            wc.style = CS_HREDRAW | CS_VREDRAW;  // 这两种位标志当前工作区的宽度或高度改变时就重新绘制窗口
            wc.lpfnWndProc = MainWndProc;        // 指向窗口过程函数的指针
            wc.cbClsExtra = 0;                   // 是否分配额外的内存空间,不需要则设为0
            wc.cbWndExtra = 0;                   
            wc.hInstance = mhAppInst;            // 当前应用实例的句柄
            wc.hIcon = LoadIcon(0, IDI_APPLICATION);    // 将应用程序的图标设为默认图标
            wc.hCursor = LoadCursor(0, IDC_ARROW);      // 将应用程序的光标样式设为默认
            wc.hbrBackground = (HBRUSH)GetStockObject(NULL_BRUSH);  // 设置窗口工作区的背景颜色为白色
            wc.lpszMenuName = 0;                        // 指定窗口菜单,没有菜单设为0
            wc.lpszClassName = L"MainWnd";              // 指定窗口类结构体的名字
    
            // 将窗口注册到Windows系统
            if (!RegisterClass(&wc))
            {
                // 如果注册失败 (返回空指针),则弹出消息盒显示错误信息,并返回窗口创建失败
                MessageBox(0, L"RegisterClass Failed.", 0, 0);
                return false;
            }
    
            /*
                客户端矩形是完全包围工作区的最小矩形。 
                窗口矩形是完全包围窗口的最小矩形,其中包括工作区和非工作区。
            */
            RECT R = { 0, 0, mClientWidth, mClientHeight };   // 定义所需的客户端矩形大小
            AdjustWindowRect(&R, WS_OVERLAPPEDWINDOW, false); // 计算对应窗口矩形所需的大小,第二个参数为窗口样式,此例中表示窗口是重叠的窗口样式
            int width = R.right - R.left;       // 计算窗口矩形的宽度
            int height = R.bottom - R.top;      // 计算窗口矩形的高度
    
            // 创建窗口 (根据刚才计算出的窗口矩形宽高)
            mhMainWnd = CreateWindow(L"MainWnd", mMainWndCaption.c_str(),
                WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, width, height, 0, 0, mhAppInst, 0);
            
            // 如果窗口创建失败则弹出消息框提醒,并返回应用程序主窗口初始化失败
            if (!mhMainWnd)
            {
                MessageBox(0, L"CreateWindow Failed.", 0, 0);
                return false;
            }
    
            // 设置指定窗口的显示状态,SW_SHOW表示:激活窗口并以当前大小和位置显示窗口
            ShowWindow(mhMainWnd, SW_SHOW);
    
            // 如果窗口的更新区域不为空, UpdateWindow 函数通过向窗口发送 WM_PAINT 消息来更新指定窗口的工作区
            UpdateWindow(mhMainWnd);
    
            // 返回应用程序的主窗口初始化成功
            return true;
        }
    
        // 初始化Direct3D函数
        bool InitDirect3D()
        {
    #if defined(DEBUG) || defined(_DEBUG) 
            // 如果在Debug模式,启用D3D12的调试层,
            // 启用调试后,D3D会在错误发生时向VC++的输出窗口发送调试信息
            {
                ComPtr<ID3D12Debug> debugController;
                ThrowIfFailed(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController)));
                debugController->EnableDebugLayer();
            }
    #endif
    
        /*
            第一步:创建Direct3D设备
        */
    
        // 创建可用于生成其他 DXGI 对象的 DXGI 1.0 Factory
        ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(&mdxgiFactory)));
    
        // 创建一个D3D设备
        // D3D12CreateDevice参数为:(适配器指针,应用程序所需最低功能级别,
        // 所建ID3D12Device接口的COM ID,返回所创建的D3D12设备
        HRESULT hardwareResult = D3D12CreateDevice(
            nullptr,
            D3D_FEATURE_LEVEL_12_0,
            IID_PPV_ARGS(&md3dDevice));
        // 适配器指针传入空代表使用主显示适配器,
        // 可以分析出需要COM指针即riid的地方,通过使用IID_PPV_ARGS即可
    
        // 如果创建失败,应用程序回退至WARP软件适配器
        if (FAILED(hardwareResult))
        {
            ComPtr<IDXGIAdapter> pWarpAdapter;
            ThrowIfFailed(
                mdxgiFactory->EnumWarpAdapter(IID_PPV_ARGS(&pWarpAdapter)));
    
            // 不同windows版本的WARP最高支持的功能级别也不同
            // 再次创建D3D设备时,仅适配器指针参数不同,传入WARP适配器指针
            ThrowIfFailed(D3D12CreateDevice(
                pWarpAdapter.Get(),
                D3D_FEATURE_LEVEL_12_0,
                IID_PPV_ARGS(&md3dDevice)
            ));
        }
    
    
        /*
           第二步:创建围栏并计算描述符大小
       */
    
       // 创建围栏
        ThrowIfFailed(md3dDevice->CreateFence(
            0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&mFence)));
    
        // 获取描述符大小
        mRtvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
        mDsvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_DSV);
        mCbvSrvUavDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
    
        
        /*
            第三步:检测系统对4X MSAA的支持
        */
    
        // 检测对4X MSAA质量级别的支持
        D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS msQualityLevels;
        msQualityLevels.Format = mBackBufferFormat;                         // 纹理格式
        msQualityLevels.SampleCount = 4;                                    // 采样次数
        msQualityLevels.Flags = D3D12_MULTISAMPLE_QUALITY_LEVELS_FLAG_NONE; // 默认选项
        msQualityLevels.NumQualityLevels = 0;                               // 质量级别
    
        // 使用ID3D12Device::CheckFeatureSupport函数,查询我们设置的这种图像质量的级别
        ThrowIfFailed(md3dDevice->CheckFeatureSupport(
            D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS,
            &msQualityLevels,
            sizeof(msQualityLevels)
        ));
    
        // 我们查询的采样数为4,因此返回的质量级别,即为达到4X MSAA的质量级别
        m4xMsaaQuality = msQualityLevels.NumQualityLevels;
        // 只要返回的质量级别不为零,则表示支持此种图像质量,在该处即支持4X MSAA
        assert(m4xMsaaQuality > 0 && "Unexpected MSAA quality level.");
    
    #ifdef _DEBUG
        LogAdapters();
    #endif
    
        /*
            第四步:创建命令队列和命令列表
        */
        CreateCommandObjects();
    
        /*
            第五步:描述并创建交换链
        */
        CreateSwapChain();
    
        /*
            第六步:创建描述符堆
        */
        CreateRtvAndDsvDescriptorHeaps();
    
        return true;
        }
    
        // 第四步: 创建命令队列和命令列表
        void CreateCommandObjects()
        {
            // 声明一个命令队列
            ComPtr<ID3D12CommandQueue> mCommandQueue;
    
            // 调用ID3D12Device::CreateCommandQueue方法创建队列前,
            // 需要填写D3D12_COMMAND_QUEUE_DESC结构体来描述队列
            D3D12_COMMAND_QUEUE_DESC queueDesc = {};
    
            // 定义队列中的命令类型
            queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
            // 设置其他选项为空
            queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
    
            // 调用ID3D12Device::CreateCommandQueue方法创建命令队列
            ThrowIfFailed(md3dDevice->CreateCommandQueue(
                &queueDesc, IID_PPV_ARGS(&mCommandQueue)
            ));
    
            // 声明ID3D12CommandAllocator命令内存管理对象
            ComPtr<ID3D12CommandAllocator> mCommandAllocator;
    
            // 记录在命令列表内的命令,实际上是存储在与之关联的命令分配器上
            // 通过ID3D12Device创建命令分配器ID3D12CommandAllocator
            ThrowIfFailed(md3dDevice->CreateCommandAllocator(
                D3D12_COMMAND_LIST_TYPE_DIRECT,                 // 与此命令分配器关联的命令列表类型
                IID_PPV_ARGS(mCommandAllocator.GetAddressOf())
            ));
    
            // 通过ID3D12Device创建命令列表ID3D12GraphicsCommandList
            ComPtr<ID3D12GraphicsCommandList> mCommandList;
            ThrowIfFailed(md3dDevice->CreateCommandList(
                0,                                       // 指定与所建命令列表相关联的物理GPU,对于仅有一个GPU的系统而言设为0
                D3D12_COMMAND_LIST_TYPE_DIRECT,          // 命令列表的类型
                mCommandAllocator.Get(),                 // 关联的命令分配器
                nullptr,                                 // 打包和初始化无绘制命令时传nullptr
                IID_PPV_ARGS(mCommandList.GetAddressOf())// 输出指向所建列表的指针
            ));
    
            // 在关闭状态下启动
            // 这是因为当我们第一次引用命令列表时,我们会重置它,并且在调用Reset之前需要关闭它。
            mCommandList->Close();
        }
    
        // 第五步:描述并创建交换链
        void CreateSwapChain()
        {
            // 释放我们将要重新创建的上一个交换链。
            mSwapChain.Reset();
    
            // 使用DXGI_SWAP_CHAIN_DESC结构体描述交换链的特性
            DXGI_SWAP_CHAIN_DESC sd;
    
            sd.BufferDesc.Width = mClientWidth;             // 缓冲区分辨率宽度
            sd.BufferDesc.Height = mClientHeight;           // 缓冲区分辨率高度
            sd.BufferDesc.RefreshRate.Numerator = 60;       // 设置刷新率分子
            sd.BufferDesc.RefreshRate.Denominator = 1;      // 设置刷新率分母
            sd.BufferDesc.Format = mBackBufferFormat;       // 设置缓冲区格式
            sd.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED; // 设置扫描线顺序为未指定
            sd.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;  // 设置缩放为未指定
            sd.SampleDesc.Count = 1;                         // 设置采样数目为1
            sd.SampleDesc.Quality = 0;                       // 设置质量级别为0
            sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;// 设置后台缓冲区为渲染目标输出
            sd.BufferCount = SwapChainBufferCount;           // 设置交换链中的缓冲区数量
            sd.OutputWindow = mhMainWnd;                     // 设置渲染窗口的句柄
            sd.Windowed = true;                              // 设置为true程序将在窗口模式运行,否则为全屏
            sd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
            sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;
            // 可选标志,设为此项表示程序切换为全屏时,选择最适合于当前应用程序窗口尺寸的显示模式
            // 若无该标志,则采用当前桌面的显示模式。
    
            // 使用IDXGIFactory::CreateSwapChain方法,创建交换链接口
            ThrowIfFailed(mdxgiFactory->CreateSwapChain(
                mCommandQueue.Get(),
                &sd,
                mSwapChain.GetAddressOf()
            ));
        }
    
        // 刷新命令队列
        void FlushCommandQueue()
        {
            // 将围栏值增加1
            mCurrentFence++;
    
            // 向GPU命令队列末尾添加命令:将mFence的值修改为mCurrentFence
            ThrowIfFailed(mCommandQueue->Signal(
                mFence.Get(), mCurrentFence
            ));
    
            // 如果mFence < mCurrentFence,则GPU没有执行完刚才添加的命令,则GPU没有处理完所有命令
            if (mFence->GetCompletedValue() < mCurrentFence)
            {
                HANDLE eventHandle = CreateEventEx(nullptr, false, false, EVENT_ALL_ACCESS);
    
                // 如果mFence等于mCurrentFence,则激发事件eventHandle
                ThrowIfFailed((
                    mFence->SetEventOnCompletion(mCurrentFence, eventHandle)
                    ));
    
                // CPU等待eventHandle激发,当且仅当激发事件即GPU处理完命令时,CPU继续运行
                WaitForSingleObject(eventHandle, INFINITE);
                CloseHandle(eventHandle);
            }
        }
    
        // 获取当前后台缓冲区的RTV描述符
        ID3D12Resource* CurrentBackBuffer()const
        {
            return mSwapChainBuffer[mCurrBackBuffer].Get();
        }
    
        // 获取当前后台缓冲区的视图
        D3D12_CPU_DESCRIPTOR_HANDLE CurrentBackBufferView()const
        {
            return CD3DX12_CPU_DESCRIPTOR_HANDLE(
                mRtvHeap->GetCPUDescriptorHandleForHeapStart(),
                mCurrBackBuffer,
                mRtvDescriptorSize);
        }
    
        // 获取深度模板缓冲区的RTV视图
        D3D12_CPU_DESCRIPTOR_HANDLE DepthStencilView()const
        {
            return mDsvHeap->GetCPUDescriptorHandleForHeapStart();
        }
    
        // 计算帧统计信息
        void CalculateFrameStats()
        {
            // Code computes the average frames per second, and also the 
        // average time it takes to render one frame.  These stats 
        // are appended to the window caption bar.
    
            static int frameCnt = 0;
            static float timeElapsed = 0.0f;
    
            frameCnt++;
    
            // Compute averages over one second period.
            if ((mTimer.TotalTime() - timeElapsed) >= 1.0f)
            {
                float fps = (float)frameCnt; // fps = frameCnt / 1
                float mspf = 1000.0f / fps;
    
                wstring fpsStr = to_wstring(fps);
                wstring mspfStr = to_wstring(mspf);
    
                wstring windowText = mMainWndCaption +
                    L"    fps: " + fpsStr +
                    L"   mspf: " + mspfStr;
    
                SetWindowText(mhMainWnd, windowText.c_str());
    
                // Reset for next average.
                frameCnt = 0;
                timeElapsed += 1.0f;
            }
        }
    
        // 枚举适配器函数
        void LogAdapters()
        {
            UINT i = 0;
            IDXGIAdapter* adapter = nullptr;
            std::vector<IDXGIAdapter*> adapterList;
            while (mdxgiFactory->EnumAdapters(i, &adapter) != DXGI_ERROR_NOT_FOUND)
            {
                DXGI_ADAPTER_DESC desc;
                adapter->GetDesc(&desc);
    
                std::wstring text = L"***Adapter: ";
                text += desc.Description;
                text += L"\n";
    
                OutputDebugString(text.c_str());
    
                adapterList.push_back(adapter);
    
                ++i;
            }
    
            for (size_t i = 0; i < adapterList.size(); ++i)
            {
                LogAdapterOutputs(adapterList[i]);
                ReleaseCom(adapterList[i]);
            }
        }
    
        // 适配器信息输出函数
        void LogAdapterOutputs(IDXGIAdapter* adapter)
        {
            UINT i = 0;
            IDXGIOutput* output = nullptr;
            while (adapter->EnumOutputs(i, &output) != DXGI_ERROR_NOT_FOUND)
            {
                DXGI_OUTPUT_DESC desc;
                output->GetDesc(&desc);
    
                std::wstring text = L"***Output: ";
                text += desc.DeviceName;
                text += L"\n";
                OutputDebugString(text.c_str());
    
                LogOutputDisplayModes(output, mBackBufferFormat);
    
                ReleaseCom(output);
    
                ++i;
            }
        }
    
        // 输出适配器显示模式函数
        void LogOutputDisplayModes(IDXGIOutput* output, DXGI_FORMAT format)
        {
            UINT count = 0;
            UINT flags = 0;
    
            // Call with nullptr to get list count.
            output->GetDisplayModeList(format, flags, &count, nullptr);
    
            std::vector<DXGI_MODE_DESC> modeList(count);
            output->GetDisplayModeList(format, flags, &count, &modeList[0]);
    
            for (auto& x : modeList)
            {
                UINT n = x.RefreshRate.Numerator;
                UINT d = x.RefreshRate.Denominator;
                std::wstring text =
                    L"Width = " + std::to_wstring(x.Width) + L" " +
                    L"Height = " + std::to_wstring(x.Height) + L" " +
                    L"Refresh = " + std::to_wstring(n) + L"/" + std::to_wstring(d) +
                    L"\n";
    
                ::OutputDebugString(text.c_str());
            }
        }
    
    protected:
        /*
            以下成员属性为protected,提供给D3DApp的子类继承的。
        */
    
        static D3DApp* mApp;            // 唯一的应用程序类实例
    
        HINSTANCE mhAppInst = nullptr; // 应用程序实例句柄
        HWND      mhMainWnd = nullptr; // 主窗口句柄
        bool      mAppPaused = false;  // 应用程序是否暂停
        bool      mMinimized = false;  // 应用程序是否最小化
        bool      mMaximized = false;  // 应用程序是否最大化
        bool      mResizing = false;   // 应用程序是否正在被调整大小
        bool      mFullscreenState = false;// 是否启用全屏
    
        bool      m4xMsaaState = false;    // 是否启用4X MSAA功能
        UINT      m4xMsaaQuality = 0;      // 4X MSAA的质量水平
    
        // 应用程序的计时器
        GameTimer mTimer;
    
        Microsoft::WRL::ComPtr<IDXGIFactory4> mdxgiFactory; // DirectX图形基础设施工厂
        Microsoft::WRL::ComPtr<IDXGISwapChain> mSwapChain;  // 交换链
        Microsoft::WRL::ComPtr<ID3D12Device> md3dDevice;    // D3D设备
    
        Microsoft::WRL::ComPtr<ID3D12Fence> mFence;         // 围栏
        UINT64 mCurrentFence = 0;                           // 记录围栏值
    
        Microsoft::WRL::ComPtr<ID3D12CommandQueue> mCommandQueue;           // 命令队列
        Microsoft::WRL::ComPtr<ID3D12CommandAllocator> mDirectCmdListAlloc; // 命令分配器
        Microsoft::WRL::ComPtr<ID3D12GraphicsCommandList> mCommandList;     // 命令列表
    
        static const int SwapChainBufferCount = 2;  // 交换链中后台缓冲区数量
        int mCurrBackBuffer = 0;                    // 当前后台缓冲区的索引
        Microsoft::WRL::ComPtr<ID3D12Resource> mSwapChainBuffer[SwapChainBufferCount];  // 后台缓冲区资源数组
        Microsoft::WRL::ComPtr<ID3D12Resource> mDepthStencilBuffer;                     // 深度模板缓冲区资源
    
        Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> mRtvHeap;      // 渲染目标视图描述符堆
        Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> mDsvHeap;      // 深度/模板描述符堆
    
        D3D12_VIEWPORT mScreenViewport; // 定义视口
        D3D12_RECT mScissorRect;        // 定义一个裁剪矩形
    
        UINT mRtvDescriptorSize = 0;        // RTV描述符大小,RTV描述符: 渲染目标视图资源
        UINT mDsvDescriptorSize = 0;        // DSV描述符大小,DSV描述符: 深度/模板视图资源
        UINT mCbvSrvUavDescriptorSize = 0;  // CbvSrvUav描述符大小,CBV描述符: 常量缓冲区视图资源...
    
        // 派生类应该在派生构造函数中设置这些值,以自定义起始值。
        std::wstring mMainWndCaption = L"d3d App";                          // 主窗口标题
        D3D_DRIVER_TYPE md3dDriverType = D3D_DRIVER_TYPE_HARDWARE;          // 设置Direct3D驱动类型为硬件驱动,即在硬件中实现Direct3D功能
        DXGI_FORMAT mBackBufferFormat = DXGI_FORMAT_R8G8B8A8_UNORM;         // 后台缓冲区的数据类型
        DXGI_FORMAT mDepthStencilFormat = DXGI_FORMAT_D24_UNORM_S8_UINT;    // 深度/模板缓冲区的数据类型
        int mClientWidth = 800;                                             // 客户端即工作区的宽度和高度
        int mClientHeight = 600;
    };
    
    // D3DApp应用程序主窗体的窗口过程函数
    LRESULT CALLBACK
    MainWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
    {
        // 转发消息,调用类唯一实例的窗口过程函数
        return D3DApp::GetApp()->MsgProc(hwnd, msg, wParam, lParam);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259
    • 260
    • 261
    • 262
    • 263
    • 264
    • 265
    • 266
    • 267
    • 268
    • 269
    • 270
    • 271
    • 272
    • 273
    • 274
    • 275
    • 276
    • 277
    • 278
    • 279
    • 280
    • 281
    • 282
    • 283
    • 284
    • 285
    • 286
    • 287
    • 288
    • 289
    • 290
    • 291
    • 292
    • 293
    • 294
    • 295
    • 296
    • 297
    • 298
    • 299
    • 300
    • 301
    • 302
    • 303
    • 304
    • 305
    • 306
    • 307
    • 308
    • 309
    • 310
    • 311
    • 312
    • 313
    • 314
    • 315
    • 316
    • 317
    • 318
    • 319
    • 320
    • 321
    • 322
    • 323
    • 324
    • 325
    • 326
    • 327
    • 328
    • 329
    • 330
    • 331
    • 332
    • 333
    • 334
    • 335
    • 336
    • 337
    • 338
    • 339
    • 340
    • 341
    • 342
    • 343
    • 344
    • 345
    • 346
    • 347
    • 348
    • 349
    • 350
    • 351
    • 352
    • 353
    • 354
    • 355
    • 356
    • 357
    • 358
    • 359
    • 360
    • 361
    • 362
    • 363
    • 364
    • 365
    • 366
    • 367
    • 368
    • 369
    • 370
    • 371
    • 372
    • 373
    • 374
    • 375
    • 376
    • 377
    • 378
    • 379
    • 380
    • 381
    • 382
    • 383
    • 384
    • 385
    • 386
    • 387
    • 388
    • 389
    • 390
    • 391
    • 392
    • 393
    • 394
    • 395
    • 396
    • 397
    • 398
    • 399
    • 400
    • 401
    • 402
    • 403
    • 404
    • 405
    • 406
    • 407
    • 408
    • 409
    • 410
    • 411
    • 412
    • 413
    • 414
    • 415
    • 416
    • 417
    • 418
    • 419
    • 420
    • 421
    • 422
    • 423
    • 424
    • 425
    • 426
    • 427
    • 428
    • 429
    • 430
    • 431
    • 432
    • 433
    • 434
    • 435
    • 436
    • 437
    • 438
    • 439
    • 440
    • 441
    • 442
    • 443
    • 444
    • 445
    • 446
    • 447
    • 448
    • 449
    • 450
    • 451
    • 452
    • 453
    • 454
    • 455
    • 456
    • 457
    • 458
    • 459
    • 460
    • 461
    • 462
    • 463
    • 464
    • 465
    • 466
    • 467
    • 468
    • 469
    • 470
    • 471
    • 472
    • 473
    • 474
    • 475
    • 476
    • 477
    • 478
    • 479
    • 480
    • 481
    • 482
    • 483
    • 484
    • 485
    • 486
    • 487
    • 488
    • 489
    • 490
    • 491
    • 492
    • 493
    • 494
    • 495
    • 496
    • 497
    • 498
    • 499
    • 500
    • 501
    • 502
    • 503
    • 504
    • 505
    • 506
    • 507
    • 508
    • 509
    • 510
    • 511
    • 512
    • 513
    • 514
    • 515
    • 516
    • 517
    • 518
    • 519
    • 520
    • 521
    • 522
    • 523
    • 524
    • 525
    • 526
    • 527
    • 528
    • 529
    • 530
    • 531
    • 532
    • 533
    • 534
    • 535
    • 536
    • 537
    • 538
    • 539
    • 540
    • 541
    • 542
    • 543
    • 544
    • 545
    • 546
    • 547
    • 548
    • 549
    • 550
    • 551
    • 552
    • 553
    • 554
    • 555
    • 556
    • 557
    • 558
    • 559
    • 560
    • 561
    • 562
    • 563
    • 564
    • 565
    • 566
    • 567
    • 568
    • 569
    • 570
    • 571
    • 572
    • 573
    • 574
    • 575
    • 576
    • 577
    • 578
    • 579
    • 580
    • 581
    • 582
    • 583
    • 584
    • 585
    • 586
    • 587
    • 588
    • 589
    • 590
    • 591
    • 592
    • 593
    • 594
    • 595
    • 596
    • 597
    • 598
    • 599
    • 600
    • 601
    • 602
    • 603
    • 604
    • 605
    • 606
    • 607
    • 608
    • 609
    • 610
    • 611
    • 612
    • 613
    • 614
    • 615
    • 616
    • 617
    • 618
    • 619
    • 620
    • 621
    • 622
    • 623
    • 624
    • 625
    • 626
    • 627
    • 628
    • 629
    • 630
    • 631
    • 632
    • 633
    • 634
    • 635
    • 636
    • 637
    • 638
    • 639
    • 640
    • 641
    • 642
    • 643
    • 644
    • 645
    • 646
    • 647
    • 648
    • 649
    • 650
    • 651
    • 652
    • 653
    • 654
    • 655
    • 656
    • 657
    • 658
    • 659
    • 660
    • 661
    • 662
    • 663
    • 664
    • 665
    • 666
    • 667
    • 668
    • 669
    • 670
    • 671
    • 672
    • 673
    • 674
    • 675
    • 676
    • 677
    • 678
    • 679
    • 680
    • 681
    • 682
    • 683
    • 684
    • 685
    • 686
    • 687
    • 688
    • 689
    • 690
    • 691
    • 692
    • 693
    • 694
    • 695
    • 696
    • 697
    • 698
    • 699
    • 700
    • 701
    • 702
    • 703
    • 704
    • 705
    • 706
    • 707
    • 708
    • 709
    • 710
    • 711
    • 712
    • 713
    • 714
    • 715
    • 716
    • 717
    • 718
    • 719
    • 720
    • 721
    • 722
    • 723
    • 724
    • 725
    • 726
    • 727
    • 728
    • 729
    • 730
    • 731
    • 732
    • 733
    • 734
    • 735
    • 736
    • 737
    • 738
    • 739
    • 740
    • 741
    • 742
    • 743
    • 744
    • 745
    • 746
    • 747
    • 748
    • 749
    • 750
    • 751
    • 752
    • 753
    • 754
    • 755
    • 756
    • 757
    • 758
    • 759
    • 760
    • 761
    • 762
    • 763
    • 764
    • 765
    • 766
    • 767
    • 768
    • 769
    • 770
    • 771
    • 772
    • 773
    • 774
    • 775
    • 776
    • 777
    • 778
    • 779
    • 780
    • 781
    • 782
    • 783
    • 784
    • 785
    • 786
    • 787
    • 788
    • 789
    • 790
    • 791
    • 792
    • 793
    • 794
    • 795
    • 796
    • 797
    • 798
    • 799
    • 800
    • 801
    • 802
    • 803
    • 804
    • 805
    • 806
    • 807
    • 808
    • 809
    • 810
    • 811
    • 812
    • 813
    • 814
    • 815
    • 816
    • 817
    • 818
    • 819
    • 820
    • 821
    • 822
    • 823
    • 824
    • 825
    • 826
    • 827
    • 828
    • 829
    • 830
    • 831
    • 832
    • 833
    • 834
    • 835
    • 836
    • 837
    • 838
    • 839
    • 840
    • 841
    • 842
    • 843
    • 844
    • 845
    • 846
    • 847
    • 848
    • 849
    • 850
    • 851
    • 852
    • 853
    • 854
    • 855
    • 856
    • 857
    • 858
    • 859
    • 860
    • 861
    • 862
    • 863
    • 864
    • 865
    • 866
    • 867
    • 868
    • 869
    • 870
    • 871
    • 872
    • 873
    • 874
    • 875
    • 876
    • 877
    • 878
    • 879
    • 880
    • 881
    • 882
    • 883
    • 884
    • 885
    • 886
    • 887
    • 888
    • 889
    • 890
    • 891
    • 892
    • 893
    • 894
    • 895
    • 896
    • 897
    • 898
    • 899
    • 900
  • 相关阅读:
    代码随想录阅读笔记-字符串【翻转字符串中单词】
    UniApp 踩坑日记
    在 Qt 框架中,有许多内置的信号可用于不同的类和对象\triggered
    pycharm中做web应用(14)基于Django和mysql 做用户登录验证4
    开源笔记leanote搭建记录
    3D造型渲染软件DAZ Studio mac中文版介绍
    18年,51cto老师录视频- Vue.js前端开发基础与项目实战的接口,不能用了
    计算机体系结构的概念和学习目的
    Charles 抓包工具教程(三) Charles模拟弱网环境
    浏览器安全级别怎么设置,设置浏览器安全级别的方法
  • 原文地址:https://blog.csdn.net/qq_51563654/article/details/133695940