OS/Windows API

#9 Windows API 더블 버퍼링

VallistA2014. 8. 26. 15:37

첨부 파일: 이미지 더블버퍼링



Windows API에서 이미지, 선, 사각형 등 여러가지들을 뿌려줄 때 깜빡깜빡이는 현상을 전부 경험해 보셨을 것 입니다.

지금까지 처리 방식은 Main HDC에 직접 뿌려주는 방식을 취했습니다.

하지만 이 방식은 굉장히 좋지 못하고 효율적이지 못합니다.

그렇기 때문에 이 방식은 도형이 커지거나 이미지가 커질수록 더 느려지고 안좋아집니다.


이것을 해결하기위해 나온 방법론이 더블 버퍼링(Double Buffer)입니다.

먼저 더블 버퍼링을 알기위해서 알아야 할 것은 전위 버퍼 (Primary Buffer)와 후위 버퍼 (Back Buffer) 입니다.


- 전위 버퍼 (Primary Buffer)

  전위 버퍼는 화면과 대응되는 비디오 메모리를 뜻합니다.

  전위 버퍼에 색상값을 복사해라! 라고 명령을주며 그대로 화면상에 그 색깔대로 그려주게 됩니다.

  만약 실행 도중 해상도를 늘리고나 변경을 할 시 전위버퍼의 크기도 늘어나게 됩니다.

  전위버퍼라고 말하기보다 Primary Buffer라고 말을 많이 합니다.


- 후위 버퍼 (Back Buffer)

  후위 버퍼는  전위 버퍼와 동일한 특성을 가지는 메모리를 뜻합니다.

  이 두 메모리는 특성이 같아서 버퍼의 내용을 빠르게 전위 버퍼로 버퍼 단위로 전송을 할 수 있습니다.

  그래서 게임에서는 이 두개를 사용하여 더블 버퍼링을 만들어 냅니다.

  물론 이 후위 버퍼도 후위버퍼라고 말하기 보다는 Back Buffer라고 말을 합니다.


 우리는 이제 이 두놈을 가지고 더블 버퍼를 만들것 입니다.


 먼저 소스코드 입니다.


//!< CMain.cpp

bool CMain::Init(HWND hWnd)
{
	m_hWnd = hWnd;

	if (m_hWnd == NULL)
		return false;

	m_hDC = GetDC(m_hWnd);
	m_hBackBuffer = CreateCompatibleDC(m_hDC);
	m_hBitmap = LoadBitmap(m_hIns, MAKEINTRESOURCE(m_hBackBuffer));//CreateCompatibleBitmap(m_hDC, D_SCREEN_WIDTH, D_SCREEN_HEIGHT);
	SelectObject(m_hBackBuffer, (HBITMAP)m_hBitmap);
	ReleaseDC(m_hWnd, m_hDC);

	m_cPos.x = 10;
	m_cPos.y = 10;

	SetTimer(m_hWnd, 0, 10, NULL);

	return true;
}

void CMain::Update(float dt)
{
	if (GetAsyncKeyState(VK_LEFT) & 0x8000) m_cPos.x -= 1.0f;
	else if (GetAsyncKeyState(VK_RIGHT) & 0x8000) m_cPos.x += 1.0f;
}

void CMain::Render()
{
	HDC hdc = GetDC(m_hWnd);
	PatBlt(m_hBackBuffer, 0, 0, D_SCREEN_WIDTH, D_SCREEN_HEIGHT, BLACKNESS);

	Graphics graphics(hdc);

	Image		image(L"0.png");
	graphics.DrawImage(&image, m_cPos.x, m_cPos.y);

	BitBlt(m_hDC, 0, 0, D_SCREEN_WIDTH, D_SCREEN_HEIGHT, m_hBackBuffer, 0, 0, SRCCOPY);
	ReleaseDC(m_hWnd, m_hDC);
}

void CMain::Destroy()
{
	KillTimer(m_hWnd, 0);

	SelectObject(m_hBackBuffer, m_hBitmap2);
	DeleteObject(m_hBitmap);
	DeleteDC(m_hBackBuffer);
}

void CMain::GDIStart()
{
	GdiplusStartup(&m_GdiplusToken, &m_GdiplusStartupInput, NULL);
}

void CMain::setHDC(HDC hdc)
{
	m_hDC = hdc;
}

void CMain::setHINSTANCE(HINSTANCE hIns)
{
	m_hIns = hIns;
}


먼저 CMain 부분인데요, 필자는 여기를 분할시켜서 사용을 하였습니다.

자 우리가 봐야할 부분은 여기서 Init, Render, Destroy 부분입니다.

Init 부분을 보면 

m_hDC = GetDC(m_hWnd);
m_hBackBuffer = CreateCompatibleDC(m_hDC);
m_hBitmap = LoadBitmap(m_hIns, MAKEINTRESOURCE(m_hBackBuffer));//CreateCompatibleBitmap(m_hDC, D_SCREEN_WIDTH, D_SCREEN_HEIGHT);
SelectObject(m_hBackBuffer, (HBITMAP)m_hBitmap);
ReleaseDC(m_hWnd, m_hDC);

이런 부분이 있는 것을 확인할 수 있으실 것 입니다.


이 부분이 무엇이냐 하면, GetDC로 DC를 가져와서 BackBuffer에 현재 DC를 저장합니다.

물론 현재 DC는 Primary Buffer입니다.

그러면 즉 지금 CreateCompatibleDC로 인해서 m_hBackBuffer에는 현재의 Primary Buffer가 들어가게 됩니다.

그리고 밑에서 m_hBitmap에 현재의 화면에 있는 모든 색들을 검출합니다. MAKEINTRESOURCE로 백 버퍼에 있는것을 다 담게되죠.

그리고 현재의 PrimaryBuffer를 Release DC로 해제 합니다.


Render 부분을 보시면, 

HDC hdc = GetDC(m_hWnd);
PatBlt(m_hBackBuffer, 0, 0, D_SCREEN_WIDTH, D_SCREEN_HEIGHT, BLACKNESS);

Graphics graphics(hdc);

Image		image(L"0.png");
graphics.DrawImage(&image, m_cPos.x, m_cPos.y);

BitBlt(m_hDC, 0, 0, D_SCREEN_WIDTH, D_SCREEN_HEIGHT, m_hBackBuffer, 0, 0, SRCCOPY);
ReleaseDC(m_hWnd, m_hDC);

요로코놈 하는 부분이 있습니다. 

여기서 중심으로 봐야할 부분음 PatBlt와 BitBlt 입니다.


PatBlt는 패턴 방식 초기화 함수 입니다.

이 함수는 해당 DC의 비트맵 영역을 패턴 형태로 초기화 하는데 사용합니다. 여기서 말하는 패턴은 브러시인데 지정한 사각영역을 현재 DC에 선택된 브러시로 채웁니다. 


BitBlt는 고속복사 함수입니다.

메모리 DC의 비트맵을 현재 DC에 고속복사하는 함수입니다.

하지만 여기서 두 DC는 반드시 호환되는 DC거나 한쪽이 흑백이어야 합니다.

완전히 다른 Color Format을 가진경우에는 복사를 할 수 없습니다.


즉 PatBlt로 초기화를 하고 이미지를 뿌립니다.

그 다음 BitBlt로 백버퍼에 저장합니다.

이러한 형식으로 이미지를 뿌려주게 됩니다.


그 다음 Destroy 함수입니다.


Destroy 함수에서는 메모리를 삭제해줍니다.



int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam, int nCmdShow)
{
	//!< 지역변수 선언
	HINSTANCE					hInst;
	HWND						hWnd;
	WNDCLASS					WndClass;
	MSG							Message;
	RECT						Rect;

	//!< GDI+ 엔진 시작
	CMain::GetInstance()->GDIStart();

	//!< 윈도우 설정
	Rect						= {0, 0, D_SCREEN_WIDTH, D_SCREEN_HEIGHT};
	hInst						= hInstance;
	WndClass.cbClsExtra			= 0;
	WndClass.cbWndExtra			= 0;
	WndClass.hbrBackground		= (HBRUSH)GetStockObject(BLACK_BRUSH);
	WndClass.hCursor			= LoadCursor(NULL, IDC_ARROW);
	WndClass.hIcon				= LoadIcon(NULL, IDI_APPLICATION);
	WndClass.hInstance			= hInst;
	WndClass.lpfnWndProc		= (WNDPROC)WndProc;
	WndClass.lpszClassName		= D_GAME_NAME;
	WndClass.lpszMenuName		= NULL;
	WndClass.style				= CS_HREDRAW | CS_VREDRAW;
	
	//!< 윈도우 설정을 넘겨주는 곳
	RegisterClass(&WndClass);

	//!< 윈도우의 크기를 똑같도록 설정 (이거 안하면 윈도우 크기가 박스 포함한 값으로 됨
	AdjustWindowRect(&Rect, WS_OVERLAPPED | WS_SYSMENU | WS_CAPTION, false);

	//!< 윈도우를 메모리에 올림
	hWnd = CreateWindow(D_GAME_NAME, D_GAME_NAME, WS_OVERLAPPED | WS_SYSMENU | WS_CAPTION, D_SCREEN_XPOS, D_SCREEN_YPOS, D_SCREEN_WIDTH, D_SCREEN_HEIGHT, NULL, NULL, hInst, 0);
	
	//!< 인스턴스 정보를 가져옴
	CMain::GetInstance()->setHINSTANCE(hInst);

	//!< 초기화 실행 (만약 NULL 값이 들어오면 false)
	if (!CMain::GetInstance()->Init(hWnd))
		return false;
			
	//!< 윈도우를 화면상에 그림
	ShowWindow(hWnd, nCmdShow);

	//!< 메시지 루프
	while (1)
	{
		if (PeekMessage(&Message, 0, 0, 0, PM_REMOVE))		//!< 메시지가 들어왔을 경우
		{
			if (Message.message == WM_QUIT)
				break;

			TranslateMessage(&Message);
			DispatchMessage(&Message);
		}
		else                                                //!< 메시지가 들어오지 않았을 경우
		{
			CMain::GetInstance()->Update(0);
			CMain::GetInstance()->Render();
		}
	}

	//!< 루프가 끝나면 메모리 해제
	CMain::GetInstance()->Destroy();						

	return true;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
	switch (iMessage)
	{
		case WM_TIMER:
			CMain::GetInstance()->Update(0);
			break;
		case WM_KEYDOWN:
			switch (wParam)
			{
				case VK_ESCAPE:
					CMain::GetInstance()->Destroy();
					PostQuitMessage(0);
				break;
			}
		break;
		case WM_DESTROY:
			CMain::GetInstance()->Destroy();
			PostQuitMessage(0);
			break;
	}

	return (DefWindowProc(hWnd, iMessage, wParam, lParam));
}


WinMain쪽과 WndProc이 있습니다.


WinMain쪽에서 우리가 Main을 만들었던 것들이, CMain::GetInstance()-> 의 형태로 불러와지고 있습니다.


우리가 여기서 주의깊게 봐야할 것은


//!< 메시지 루프
while (1)
{
	if (PeekMessage(&Message, 0, 0, 0, PM_REMOVE))		//!< 메시지가 들어왔을 경우
	{
		if (Message.message == WM_QUIT)
			break;
		TranslateMessage(&Message);
		DispatchMessage(&Message);
	}
	else                                                //!< 메시지가 들어오지 않았을 경우
	{
		CMain::GetInstance()->Update(0);
		CMain::GetInstance()->Render();
	}
}


이 부분 입니다.

이 부분에서 알 수 있는 사실은 우리는 더이상 GetMessage를 쓰지 않는다는 것 입니다.

GetMessage가 아닌 PeekMessage를 씀으로써 우리는 메시지가 돌지 않는 동안에도 안정적이게 Update와 Render를 호출하게 됩니다.


이러한 형식으로 이제부터 작업을 할때 해주시면 되겠습니다.


다음글 부터는 본격적으로 라이브러리를 작업하고 MP3 Player을 만들어 보는 것으로 시작하도록 하겠습니다.



P.S)


 GDI+ 의 경우에는 더블버퍼링이 자동으로 걸려있어서 따로 안하셔도 됩니다. 

 이제 저 위의 소스에서 Bitmap으로 변경하여 더블버퍼링을 써보도록합시다.

'OS > Windows API' 카테고리의 다른 글

__stdcall과 __cdecl의 차이점  (1) 2014.09.27
APIENTRY와 CALLBACK의 차이  (0) 2014.09.27
#8 Windows API (GDI+) 이미지 뿌리기  (0) 2014.08.26
#7 Windows API 선, 사각형, 원 뿌리기  (0) 2014.08.26
#6 Windows API Timer  (1) 2014.08.26

댓글

VallistA

병특이 끝나서 게임에서 웹으로 스위칭한 프로그래머.
프로그래밍 정보등을 공유합니다.
현재는 이 블로그를 운영하지 않습니다.
vallista.kr 로 와주시면 감사하겠습니다!

자고 싶습니다. ㅠㅠ

Github      :: 링크

궁금한점 문의 주시면 답변드리도록 하겠습니다

VISITED

Today :

Total :

SNS

  • 페이스북아이콘
  • 카카오톡아이콘
  • 트위터아이콘

Lately Post

Lately Comment