D3D 디바이스 소실 처리

http://www.gpgstudy.com/forum/viewtopic.php?topic=1451

디바이스 소실의 두가지 경우

1. 디바이스 소실되었고 복구 불가능의 경우

: 이 경우에는 대책이 없다. 전부(IDirect3DDevice를 통해 생성된 모든 리소스들과 IDirect3DDeivce객체) 해제한 후 처음부터 다시 생성한다.

2. 디바이스 소실되었고 복구 가능의 경우

: 이 경우에는 디바이스에 올라간 리소스(리소스 생성시 D3DPOOL_DEFAULT 플래그를 이용하여 생성된 객체들)들만 새로 로딩하면 된다.

특이사항

디바이스가 소실되어도 IDirect3DDevice관련 함수들이 성공을 반환 할 경우가 있다.

도움말에 보면 “디바이스가 소실되어도 동작은 보증된다”고 되어있다. 물론 이때의 동작은 헛동작이다. 아무 효과가 없는것이다. 왜 이런식인가 생각해보니, 밑의 글을 읽어보면 알겠지만,, 언제 어떻게 디바이스 손실이 발생할지 모르니 프로그래머로써는 이런 상황을 대처 할 수 없는것이다. 디바이스와 관련된 모든 함수들에 대해서, 리턴값을 체크하지 않는한 불가능하다는 얘기이다. 그리고 손실된 디바이스 자원이 언제 넘어오게 될지도 모르는 상황에서 이런 처리는 너무 고난이도일껏 같다. 차라리 처리되는것 처럼 보이고, 실제로는 처리가 안되게 하는것이 훨씬 이득일껏 같다. 어디까지 제 추측입니다. 틀렸어도 양해를..

왜 이딴게 있는걸까?

예를들면, 2개의 응용프로그램 A와 B가 돌아가는 상황에서, A란 놈과 B란 놈 둘다 비디오 램을 많이 사용하는 프로그램이었다 치자. 이 상황에서 A란 프로그램이 돌아가고 있는 상황이어서 비디오 램의 여유분이 없었다 치자, 자.. 이제 문제의 B가 실행되어 졌을 경우,, 어떻게 해야할까? Win32는 멀티 태스킹 환경이다. 고로 여러 응용프로그램들이 같은 하드웨어 자원을 공유한다. 즉, DOS처럼 하나의 응용프로그램이 하드웨어 자원을 독점하면 안된다는 것이다. 고로 윈도우즈가 택한방식이 활성화 애플리케이션에게 자원을 넘겨주자는 방식이다. A가 활성화되면, A에게 자원을 주고, B가 활성화되면 B에게 자원을 준다. 그러므로 사용자는 두 응용프로그램을 동시에 사용할 수 있는것이다. 덕분에 프로그래머는 귀찮지만은… 어디까지 제 추측입니다. 틀렸어도 양해를.. 아니지 지적을..

디바이스 소실 처리

IDirect3DDevice::TestCooperativeLevel()을 호출하여, 디바이스를 복구 할 수 있는지 검사해본다. 반환값(HRESULT)이

D3DERR_DEVICELOST인 경우 디바이스가 소실되었고 아직 복구할 수 없는 상태이다. 복구할 수 있을때까지 대기한다.
D3DERR_DEVICENOTRESET인 경우 디바이스가 소실되지만 지금 복구할 수 있는 상태이다. 디바이스를 복구한다.
디바이스 복구

. D3DPOOL_DEFAULT로 잡은 리소스들을 전부 릴리즈한다. %%%(안그러면 다음에 호출될 IDirect3DDevice::Reset()이 실패할것이다.)
. IDirect3DDevice::Reset()을 호출한다. %%%(Reset()을 그냥 IDirect3DDevice의 복구명령어라 생각하면 이해가 편함)
. 디바이스를 다시 셋팅한다.(랜더스테이트, 뷰 행렬등..)
. D3DPOOL_DEFAULT로 잡은 리소스들을 다시 로드한다.
Adapter나 D3DDEVTYPE이 변경되어야 하는 경우에는 디바이스를 다시 생성해야 한다.

. 모든 리소스들을 릴리즈한다.
. 디바이스를 릴리즈한다.
. 디바이스를 새로 생성한다.
. 디바이스를 다시 셋팅한다.(랜더스테이트, 뷰 행렬등..)
. 모든 리소스들을 다시 로드한다.
디바이스가 소실되는경우

풀스크린 모드일때..

다른 애플리케이션이 디바이스를 점유하는 경우.
애플리케이션이 작업표시줄로 최소화 되는 경우.
다른 애플리케이션이 풀스크린 모드로 생성될경우.
전력관리 이벤트(WM_POWERBROADCAST)가 발생했을 경우.
윈도우 모드일때..

전력관리 이벤트가 발생했을 경우.
다른 애플리케이션이 풀스크린 모드로 생성될경우.. (이경우 제 카드에서는 문제가 없었음)
디스플레이 등록정보에서 색상모드를 변경하는 경우.
테스트 사항

카드 : Radeon 9000 128M, OS : Win 2000

A,B : D3D를 사용하는 애플리케이션

. A:win, B:win ==> A:full, B:win // (A를 풀스크린으로 전환) %%%-A:ok, B:ok
. A:full, B:win ===> A:win B:win // (A를 윈도우모드로 전환) %%%- A:ok, B:lost(고칠수 있음)
. A:full, B:win ===> A:full(minimize), B:win // (마우스로 바탕화면을 찍거나 Alt+Tab) %%%- A:lost(고칠수 없음), B:lost(고칠수 없음)
. A:full(lost), B:win ===> A:full(lost), B:full // (B를 풀스크린으로 전환) %%%- A:lost상태 유지, B:ok (A는 시작하기 전에 이미 lost상태였음)
. A:full(lost), B:full ===> A:full(lost), B:full(lost) // (바탕화면 찍음) %%%- A:(lost상태유지), B:lost(고칠수 없음) (A는 시작전에 이미 lost상태였음)
. A:full, B:win ===> A:종료, B:win(lost) // (A를 종료) %%%- A:종료됨, B:lost(고칠수 있음)
X. 기타 등등, 여러 상황에 따라 디바이스 로스트가 발생 된다. 이런 상황이 더 있으나 해결책을 어느정도 찾은것 같으므로 더 이상은 생략.

X. 그리고 다른 그래픽 카드의경우는 어떻게 처리될지 잘모르겠음.

결론

1. Present()함수의 리턴값은 항상 체크해야 한다. 위와 같이 언제 디바이스 손실이 발생 할지 모른다.

2. 임의로 체크해 보고 싶은 경우. TestCooperativeLevel()함수를 이용하자.

방법론

필수사항 : Present()리턴값은 항상 반드시 꼭 체크하자. 언제 어떻게 어떤 이유로 Present()가 실패할지 모른다. 이건 필수이다.

그리고 Present()가 실패하면 위와같은 방법으로 복구하면 될꺼 같다.

0. Present()함수 실패시만 복원한다.

단점으로는 에러를 알아내는 시점이 Present()가 호출될때(뷰포트를 하나 생성한 경우라면 한프레임에 한번씩)뿐 이라는 것이다. 이 단점은 랜더링 결과물에 접근하여, 그것이 애플리케이션 로직에 적용되는 경우라면 치명적일 수 있다고 예상된다. 그점을 제외하면 가장 무난할 껏 같다.

1. CD3DApplication(Direct3D Wizzard에서 제공)의 방식

이 클래스를 보면,, 꽤나 복잡한 방식(주로 윈도우 메세지를 핸들링한다.)으로 처리 하는데(즉, 디바이스 손실이 일어나기전에 미치 처리함)으로 해결하고 있다. 꼭 이렇게 까지 해야 하는지는 잘모르겠지만… 요는 여러가지 상황에 미리 대처한다는 방식이다. 위의 <테스트 사항>같은 짓을 많이 시도해보고, 어느때 디바이스가 손실되는지 정확하게 알아낸 후, 미리알 수 있는경우(풀스크린에서 Alt+tab에 의해 갑자기 비활성화 시킨다든지 등등) 할수 있는데까지 까지 하는것이다. 그런 눈물겨운 노력이 CD3DApplication에 보면 잘 나온다. 이런건 보고 베끼자…^^ 이경우 100% 문제를 해결했다고 볼 수는 없다고 생각되지만 0번의 경우보단 낫다. 이런 문제를 봐선 Direct3D의 설계의 결함같기도 하다. 결국 이런 문제들을 100% 해결 할 수는 없다고 생각된다.

결과 : 실제로 1번과 같이 대부분 예측되는 치명 적인 몇몇 가지들에 대해 미리 알아내서 처리해주면,, 거의 문제는 안생긴다. 대신 코딩이 지저분해지는건 어쩔수 없다.

stub code

void RenderAll()
{
// …
// 모든 그래픽들에 대한 랜더링을 한다.
// …

// 백버퍼 플리핑.
hr = m_pDevice->Present( NULL, NULL, NULL, NULL );
if ( FAILED(hr) )
{
if ( D3DERR_DEVICELOST == hr )
{
// 디바이스를 소실했고, 복구가 불가능 상태
}
else ( D3DERR_DEVICENOTRESET == hr )
{
// 디바이스를 소실했지만, 복구가 가능한경우.
}
}
}
설계

한가지 방법은 랜더러 클래스(또는 이런 이벤트를 처리할 객체)에 두가지상황(디바이스 손실시 복구가능/복구불가능)이 발생했을때, 로딩할 목록을 등록해 놓은 후, 이벤트가 발생하면 각각 해당 이벤트에 맞게끔 다시 로드하는 식으로, 구성하면 될 껏 같다.

//**** 디바이스 리소스 인터페이스..
// 디바이스 의존적인 리소스들의 추상클래스.
struct IDeviceRes
{
virtual void Invalidate() = 0;
virtual bool SetValidate() = 0;
};

//**** 텍스쳐 클래스
class CTextureRes : public IDeviceRes
{
public:
virtual void Invalidate() { 텍스쳐 해제; }
virtual bool SetValidate() { 텍스쳐 다시 로드; }

bool Load();
void Unload();

protected:
char m_szFileName[256];
LPDIRECT3DTEXTURE9 m_pTexture;
};

//**** 텍스쳐를 관리할 클래스(클라이언트에서 접근하여 사용)
class CDataContainer
{
public:
LoadTexture( const char* szFileName )
{
// 1. 텍스쳐 로딩
// 2. 만약 텍스쳐가 VRam에 잡혔다면..(D3DPOOL_DEFAULT)
// 2.1 랜더러의 의 VRam리스트에 등록(m_pRenderer->m_VRamRes)
// 3. 텍스쳐가 다른모드로 생성되었다면..(D3DPOOL_MANAGED or etc..)
// 3.1 랜더러의 의 Managed리스트에 등록(m_pRenderer->m_ManagedRes)
}

UnloadTexture( const char* szFileName )
{
// 랜더러의 디바이스 목록에서 지운다.
m_pRenderer->Unregist( szFileName );
}

protected:
vector m_TextureContainer;
CRenderer* m_pRenderer;
}

//**** 문제의 랜더러 클래스
class CRenderer
{
public:
// 디바이스가 해제 됬거나 해제 하라.
// – 이 함수는 외부에서 임의로 호출이 가능하다.
void InvalidateDevice( HRESULT hrDeviceState )
{
if ( FAILED( hrDeviceState )
{
if ( D3DERR_DEVICENOTRESET == hrDeviceState )
{
// 1. m_VRamRes 리소스만 해제한다.
// for_each(m_VRamRes의 시작에서 끝까지) { m_VRamRes[i]->Invalidate(); }
}
else if ( D3DERR_DEVICELOST == hrDeviceState )
{
// 1. m_VRamRes과 m_ManagedRes들에 대해서 해제한다.(전 리소스들에 대해 수행)
// for_each(m_VRamRes의 시작에서 끝까지) { m_VRamRes[i]->Invalidate(); }
// for_each(m_ManagedRes의 시작에서 끝가지) { m_ManagedRes[i]->Invalidate(); }

// 2. 디바이스를 릴리즈한다.
}
}
m_hrDeviceState = hrDeviceState; // 디바이스 상태값 업데이트
}

// 디바이스가 활성화하라.
// – 이 함수도 외부에서 임의로 호출이 가능하다.
bool SetValidateDevice()
{
if ( FAILED(m_hrDeviceState) )
{
if ( D3DERR_DEVICENOTRESET == m_hrDeviceState )
{
//**** 디바이스를 복구할수 있다.
// 1. IDrect3DDevice::Reset()한다.
// 2. 랜더 스테이트 / 뷰행렬 셋팅
// 3. m_VRamRes 리소스들을 다시 로딩한다.
// for_each(m_VRamRes의 시작에서 끝까지) { m_VRamRes[i]->SetValidate(); }
}
else if ( D3DERR_DEVICELOST == m_hrDeviceState )
{
//**** m_VRamRes 복구할수 없다.
// 1. 디바이스를 재생성한다.
// 2. 랜더 스테이트 / 뷰행렬 셋팅
// 3. m_VRamRes / m_ManagedRes 리소스를 로딩한다.
// for_each(m_VRamRes의 시작에서 끝까지) { m_VRamRes[i]->SetValidate(); }
// for_each(m_ManagedRes의 시작에서 끝가지) { m_ManagedRes[i]->SetValidate(); }
}

// 검증해본다.
m_hrDeviceState = m_pDevice->TestCooperativeLevel();
return SUCCEEDED(m_hrDeviceState);
}
return true;
}

// 랜더링한다.
void RenderAll()
{
//**** 디바이스가 유효한 상태가 아니라면 그냥 리턴.
if ( FAILED(m_hrDeviceState) ) return;

// …
//**** 모든 그래픽들에 대한 랜더링을 한다.
// …

//**** 백버퍼 플리핑.
m_hrDeviceState = m_pDevice->Present( NULL, NULL, NULL, NULL );
if ( FAILED(m_hrDeviceState) )
{
// 디바이스에 뭔가 이상이 있다면,,
InvalidateDevice( m_hrDeviceState );

// 외부에 알려준다. 아마 누군가에게 알려줄 필요가 있을껏이다.
// CApp나 혹은 다른 누군가에게..
}
}

protected:
//**** properties
// 디바이스 의존적인 메모리 관리
vector m_VRamRes; // POOL_DEFAULT
vector m_ManagedRes; // MANAGED or etc
HRESULT m_hrDeviceState; // 현재의 디바이스 상태값

LPDIRECT3DDEVICE9 m_pDevice;
};
추신

괜찮은 방법이나, 사용되어지는 방법이 있으면 올려주세요. 저도 구현은 안해본 사람입니다. 아마 틀린 부분도 있으리라 생각됩니다. 많은 지적을.. 써놓고 보니 장문이네요.

< 덧글 >

그리고 업데이트 내용입니다. gpgstudy.com의 손님께서 쓰신글의 요약입니다.

TestCooperativeLevel()로 D3DERR_DEVICENOTRESET임을 확인한후 Reset()을 했을때 실패할 수도 있다는군요.

나중에 TestCooperativeLevel()로 확인해 보니 D3DERR_DEVICELOST였다고 합니다. 결국 Reset()이 실패했을경우에도 다시 체크를 한후 소실상태라면 디바이스를 다시 생성해야 할껏같습니다.

댓글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다