Directx12 3D 프로그래밍 입문/4. Directx12 초기화

4.5 예제 프로그램 프레임 워크

ftftgop3 2023. 10. 22. 17:52

클래스 소개

d3dutil.h, cpp : 유용한 편의용 코드

d3dapp.h, cpp : Directx 응용 프로그램을 캡슐화 하는데 쓰이는 핵심 응용 코드

프레임 워크 목적 : 창 생성 코드 와 Directx 초기화 소스를 숨기는 것

D3Dapp 클래스 : 응용 클래스 들의 기반 클래스(부모 클래스) 로 사용

기능 목록 : 주창 생성, 메시지 루프 실행, window 메시지 처리, directx 초기화

 

여섯 가상 함수를 제공

이 구조의 장점 : 초기화 및 메시지 처리는 D3Dapp 클래스에 구현,

파생 클래스는 필요한 구체적인 코드만 작성 할 수 있다.

class D3DApp
{
protected:

  D3DApp(HINSTANCE hInstance);
  D3DApp(const D3DApp& rhs) = delete;
  D3DApp& operator=(const D3DApp& rhs) = delete;
  
	//com 인터페이스 해제
	//GPU 참조 중 메모리 해제 해버리면 충돌 발생, 명령 대기열 비우기
	virtual D3DApp::~D3DApp()
	{
		if(md3dDevice != nullptr)
			FlushCommandQueue();
	}
	
public:

  static D3DApp* GetApp();
    
	HINSTANCE AppInst()const; //응용프로그램 인스턴스 핸들 리턴
	HWND      MainWnd()const; //주 창 핸들 리턴
	
	float D3DApp::AspectRatio()const //종횡비 리턴
	{
		return static_cast<float>(mClientWidth) / mClientHeight;
	}
	//4X msaa 다중 표본화 활성화 및 비활성화 리턴
	bool Get4xMsaaState()const;
	// 활성화 및 비활성화 지정
  void Set4xMsaaState(bool value);
	
	//window 메시지가 없을 경우에도 게임 로직 동작 하는 함수
	//내부에서 PeekMessage 처리
	int Run();
  
	//기반 클래스의 InitMainWindow(), InitDirect3d 호출
	//파생 클래스의 자원 할당, 장면 물체 초기화 역할
  virtual bool Initialize()
	{
		if(!D3DApp::Initialize())
			return false;

		//고유 초기화 코드
	}
	//윈도우 메시지 처리
  virtual LRESULT MsgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);

protected:
	//RTV, DSV 각각의 힙 생성
  virtual void CreateRtvAndDsvDescriptorHeaps();
	//클라이언트 창 크기 변경 시 호출 
	//창크기에 따라 후면 버퍼, 깊이 스텐실 버퍼 수정
	//후면 버퍼는 함수로 수정, 깊이 스텐실 버퍼는 파괴 후 재생성
	//뷰 또한 재생성, 버퍼 외에도 화면 크기에 의존 하는 부분을 모두 수정 
	virtual void OnResize();
 
	//매 프레임 호출 애니메이션 수행, 카메라 이동, 충돌 검사, 사용자 입력
	virtual void Update(const GameTimer& gt)=0;
	//현재 프레임을 후면 버퍼에 렌더링
	//IDXGISwapChain::Present 후면 버퍼를 화면에 제시
  virtual void Draw(const GameTimer& gt)=0;

	// Convenience overrides for handling mouse input.
	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();
	//Direct3D 초기화
	bool InitDirect3D();
	//명령 대기열, 명령 목록, 할당자 생성
	void CreateCommandObjects();
	//교환 사슬 생성
  void CreateSwapChain();
	//GPU 가 명령 대기열 처리 중 CPU 대기
	void FlushCommandQueue();
	//교환 사슬의 현재 후면 버퍼 리턴
	ID3D12Resource* CurrentBackBuffer()const;
	//현재 후면 버퍼의 RTV 렌더 대상 뷰 리턴
	D3D12_CPU_DESCRIPTOR_HANDLE CurrentBackBufferView()const;
	//현재 후면 버퍼의 DSV 깊이 스텐실 뷰 리턴
	D3D12_CPU_DESCRIPTOR_HANDLE DepthStencilView()const;
	//평균 프레임수, 평균 프레임당 밀리초 계산 
	void CalculateFrameStats();
	
  void LogAdapters(); //모든 디스플레이어 어댑터 열거( 그래픽 카드) 
  void LogAdapterOutputs(IDXGIAdapter* adapter);//모든 출력 열거( 모니터)
	//디스플레이 모드 나열
  void LogOutputDisplayModes(IDXGIOutput* output, DXGI_FORMAT format);

protected:

  static D3DApp* mApp;

  HINSTANCE mhAppInst = nullptr; // application instance handle
  HWND      mhMainWnd = nullptr; // main window handle
	bool      mAppPaused = false;  // 일시 정지 상태
	bool      mMinimized = false;  // 최소화
	bool      mMaximized = false;  // 최대화
	bool      mResizing = false;   // 사용자가 크기 조정용 테두리를 끌고 있는 상태
  bool      mFullscreenState = false;// 전체 화면

	// Set true to use 4X MSAA (?.1.8).  The default is false.
  bool      m4xMsaaState = false;    // 4X MSAA enabled
  UINT      m4xMsaaQuality = 0;      // quality level of 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;
};

중요 메서드

FPS : 초당 프레임 수

FPS 평균 프레임 수 = n /t (t = 1 두면 나누기 없이 구할 수 있다.) t : 초 단위 n : t 동안 처리한 프레임 수

하나의 프레임당 걸리는 시간 = (1s, 1000ms) /FPS 평균 시간

프레임 당 걸리는 시간은 FPS보다 직관적으로 파악 하기 쉽다.

void D3DApp::CalculateFrameStats()
{
	//평균 프레임 수 계산
	//하나의 프레임 당 렌더링 걸리는 평균 시간 
	//창의 제목에 작성
    
	static int frameCnt = 0; //프레임 개수
	static float timeElapsed = 0.0f; // 초 단위

	frameCnt++; //프레임 개수 증가

	// 전체 시간 - 초 단위( ex : 1,2,3)
	if( (mTimer.TotalTime() - timeElapsed) >= 1.0f )
	{
		float fps = (float)frameCnt; // fps = frameCnt / 1
		float mspf = 1000.0f / fps; //1s = 1000ms

        wstring fpsStr = to_wstring(fps);
        wstring mspfStr = to_wstring(mspf);

        wstring windowText = mMainWndCaption +
            L"    fps: " + fpsStr +
            L"   mspf: " + mspfStr;
				
        SetWindowText(mhMainWnd, windowText.c_str());
		
		//프레임 개수 초기화 및 초 단위 증가
		frameCnt = 0;
		timeElapsed += 1.0f;
	}
}

메시지 처리부

//응용 프로그램 활성화 비활성화 전달
//mappPaused 활성화 시 Run()에서 Sleep 호출(cpu 주기를 os 돌려준다.)
case WM_ACTIVATE:
		if( LOWORD(wParam) == WA_INACTIVE )
		{
			mAppPaused = true;
			mTimer.Stop();//시간 정지
		}
		else
		{
			mAppPaused = false;
			mTimer.Start();//시간 재개
		}
		return 0;

//크기 변경 테두리 잡으면 전달
	case WM_ENTERSIZEMOVE:
		mAppPaused = true;
		mResizing  = true;
		mTimer.Stop();
		return 0;
//사용자가 크기 변경 테두리 놓으면 전달
//창 크기에 따라 변화되는 함수 호출
	case WM_EXITSIZEMOVE:
		mAppPaused = false;
		mResizing  = false;
		mTimer.Start();
		OnResize(); // 창크기에 따라 영향 받는 directx 자원 및 뷰 수정
		return 0;

// 창 파괴 시 메시지 프로시저 파괴
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
//메뉴가 활성화 되어서 사용자 키가 눌렀지만 그키가 그 어떤 이벤트를 해당하지 않을때 발생
	case WM_MENUCHAR:
        // alt - enter 삐소리가 나지 않게 한다.
        return MAKELRESULT(0, MNC_CLOSE);

	//창이 너무 작아지지 않게 메시지 처리
	case WM_GETMINMAXINFO:
		((MINMAXINFO*)lParam)->ptMinTrackSize.x = 200;
		((MINMAXINFO*)lParam)->ptMinTrackSize.y = 200; 
		return 0;

//마우스 다운
	case WM_LBUTTONDOWN:
	case WM_MBUTTONDOWN:
	case WM_RBUTTONDOWN:
		OnMouseDown(wParam, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
		return 0;
	case WM_LBUTTONUP:
	case WM_MBUTTONUP:
	case WM_RBUTTONUP:
		OnMouseUp(wParam, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
		return 0;
	case WM_MOUSEMOVE:
		OnMouseMove(wParam, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
		return 0;

파생 클래스 생성 하는 규칙

//D3Dapp 상속 받은 파생 클래스 형태 및 구조 
class InitDirect3DApp : public D3DApp
{
public:
	InitDirect3DApp(HINSTANCE hInstance);
	~InitDirect3DApp();

	virtual bool Initialize()override; //초기화

private:
    virtual void OnResize()override; //창 사이즈 변경 시 수정 사항
    virtual void Update(const GameTimer& gt)override; // 매 프레임 동작 관련
    virtual void Draw(const GameTimer& gt)override;//매 프레임 렌더링

};
	//window 창 생성 시 호출, Directx 초기화, run 실행
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE prevInstance,
				   PSTR cmdLine, int showCmd)
{
	// Enable run-time memory check for debug builds.
#if defined(DEBUG) | defined(_DEBUG)
	_CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );
#endif

    try
    {
        InitDirect3DApp theApp(hInstance);
        if(!theApp.Initialize())
            return 0;

        return theApp.Run();
    }
    catch(DxException& e)
    {
        MessageBox(nullptr, e.ToString().c_str(), L"HR Failed", MB_OK);
        return 0;
    }
}
//매 프레임 렌더링
void InitDirect3DApp::Draw(const GameTimer& gt)
{
	//명령 할당자 초기화, gpu 관련 명령 목록 모두 처리 후 호출 된다.
    ThrowIfFailed(mDirectCmdListAlloc->Reset());
	//명령 목록 재설정 
    ThrowIfFailed(mCommandList->Reset(mDirectCmdListAlloc.Get(), nullptr));

	//current backbuffer 의 자원 상태 전이를 directx 통지
	mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(CurrentBackBuffer(),
		D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET));

	//뷰포트 및 가위 직 사각형 설정( 명령 목록 을 재설정 시 )
    mCommandList->RSSetViewports(1, &mScreenViewport);
    mCommandList->RSSetScissorRects(1, &mScissorRect);

    // 후면 버퍼와 깊이 버퍼 초기화
	//렌더 대상, 지우는 색상, 
	// prects 배열의 원소(사용 안함 0), 렌더 대상에서 지울 직사각형 영역
	mCommandList->ClearRenderTargetView(CurrentBackBufferView(),
 Colors::LightSteelBlue, 0, nullptr);
	//깊이 스텐실 버퍼 dsv, 깊이 또는 스텐실 중 지우는 요소(깊이, 스텐실, 둘다)
	//깊이 버퍼 지우는 값, 스텐실 버퍼 지우는값, 
	//prects 배열 원소, 렌더 대상에서 지울 직사각혀 영역
	mCommandList->ClearDepthStencilView(DepthStencilView(), D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, 1.0f, 0, 0, nullptr);
	
  //렌더링 결과가 기뢱될 렌더 대상 버퍼을 지정
	//파이프 라인에 렌더 대상, 깊이 스텐실 버퍼 묶음 
	//RTV 개수, rtv, rtv 서술자 힙에 연속적 저장시 true, dsv
	mCommandList->OMSetRenderTargets(1, &CurrentBackBufferView(), true, &DepthStencilView());
	
    //currentbackbuffer 자원 상태 전이
	mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(CurrentBackBuffer(),
		D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT));

    // 명령 기록을 마친다
	ThrowIfFailed(mCommandList->Close());
 
    // 명록 기록을 명령 대기열 추가
	ID3D12CommandList* cmdsLists[] = { mCommandList.Get() };
	mCommandQueue->ExecuteCommandLists(_countof(cmdsLists), cmdsLists);
	
	// 화면에 렌더링, 후면 전면 버퍼 교환
	ThrowIfFailed(mSwapChain->Present(0, 0));
//후면 버퍼 색인 갱신
	mCurrBackBuffer = (mCurrBackBuffer + 1) % SwapChainBufferCount;
	
	//해당 프레임 명령 처리 동안 대기(비 효율적 방법)
	FlushCommandQueue();
}

~D3dapp() : 소멸자 에서는 GPU 의 명령 대기열을 비운 는 함수 호출

gpu에서 참조 하는 자원들을 안정적으로 파괴

Handle : 핸들 테이블<리소스 주소 , 핸들 값(int) >을 이용 해서 운영체제 의 리소스를 참조 한다.

프로그램의 안정성을 높일 수 있다.

 

Flush command queue() : GPU 처리중 CPU를 대기 하게 만든다.

여섯 가상 함수를 재정의 후 사용

Initialize() : 프로그램의 고유 초기화 코드 작성

msgproc : 윈도위 메시지 처리 및 메시치 처리를 안할 때 렌더링 함수 호출

Creatertvanddsvdescriptorheaps : RTV 서술자 및 DSV 서술자 생성 및 각 서술자에 맞는 힙을 생성

OnResize : 화면 사이즈가 변경 될 떄 호출

후면 버퍼와 깊이 스텐실 버퍼의 사이즈를 변경 한다 후면 버퍼의 경우 함수 호출로 변경 하지만

깊이 스텐실 버퍼는 파괴 후 생성 한다

후면 버퍼와 깊이 스텐실 버퍼를 데이터가 수정 후 각각의 뷰를 새롭게 구성

그 외에도 화면 크기에 연관된 투영 행렬도 수정

update : 시간의 흐름에 따른 응용 프로그램 갱신 (애니메이션, 카메라, 충돌 검출, 사용자 입력)

Draw : 렌더링 명령을 직접적으로 호출

프레임 통계치

1초당 평균 프레임 수 = 프레임 /t : t를 1로 지정 하면 나누기 연산을 패스

평균 프레임 당 처리 속도 = 1000 / 1초당 프레임

평균 프레임 당 처리 속도가 중요한 이유는 프로젝트 개발 시 처리 속도를 명확하게 알 수 있다.

메시지 처리부

응용 프로그램의 핵심부 는 유후 처리 도중에 실행 된다. 윈도우 메시지가 발생하지 않을 경우

WM_ACTIVATE

응용 프로그램이 비활성 화 시 타이머 정지, 응용 프로그램의 상태를 갱신 하지 않는다.

CPU 주기를 낭비 하는 일 없다.

WM_SIZE

응용 프로그램 창 크기가 변경 시 발생

만약 프로그램 창을 드래그 시 계속해서 후면 버퍼, 깊이 스텐실 버퍼를 수정 하는 건 매우 비효율적

그래서 드래그가 종료되는 시점에 후면 버퍼 스텐실 버퍼를 수정 한다.

초기화 예제 중요 함수

Draw 함수 처리 순서

  1. 명령 기록에 관련된 메모리 재활용 으로 명령 할당자 재설정(GPU 명령 목록 처리 후 reset)
  2. 명령 목록 재 설정(GPU 명령 대기열에 추가 된 후 )
  3. 뷰포트, 가위 직사각형 설정
  4. 후면 버퍼와 깊이 버퍼를 초기화(현재 후면 버퍼의 색인을 이용해서 후면 버퍼 초기화)
  5. 렌더링 결과가 기록 될 렌더 대상 버퍼 저장
  6. 렌더링에 사용할 렌더 대상과 깊이 스텐실 버퍼를 파이프라인 에 묶는다.
  7. 명령들의 기록 마친다
  8. 명령 목록을 명령 대기열에 추가
  9. 후면 버퍼와 전면 버퍼 교환 후 후면 버퍼의 색인을 갱신
  10. 명령 들이 모두 처리하길 대기, 비효율적 flushcommandqueue()

디버깅

HRESULT 오류 부호 error code를 돌려 준다.

TrowIfFailed : 매크로 함수, 일반 함수로 구현 되면 일반 함수의 파일과 행 파일로 치환 되기 때문에 매크로를 사용 한다.

 

참고 및 내용 인용
프랭크 D 루나 지음, 류광 옮김 
한빛 미디어
Directx 12를 이용한 3D 게임 프로그래밍 입문(2017)