ChangeDisplaySettings vs SetFullscreenState


#1

Temat pełnego ekranu w DirectX był wałkowany w Internecie na forach wielokrotnie. Ale zauważyłem, że sposobów na rozwiązanie tej kwestii jest tyle samo co osób, które te rozwiązania przedstawiają. Jedne działają średnio, inne dobrze ale z pewnymi ograniczeniami. Chciałem jednak poruszyć ten temat aby dowiedzieć się jak powinno się to robić prawidłowo. Mówię tu o metodach ChangeDisplaySettings oraz SetFullscreenState, które są używane do przełączania się pomiędzy trybem pełnoekranowym a okienkowym ale nie tylko. Są również używane w przypadku zmiany “w locie” rozdzielczości naszej aplikacji czy też zmiany (również w locie) poziomu multisamplingu w naszej aplikacji, gdzie przecież konieczne jest utworzenie na nowo łańcucha wymiany przy użyciu metody CreateSwapChain. Zacznę jednak od początku…

Najważniejsze i podstawowe pytanie tego tematu brzmi: której metody jest lepiej/bezpieczniej używać? ChangeDisplaySettings czy SetFullscreenState?

Pomimo wszechstosowania metody ChangeDisplaySettings np. w samouczku Rastertek, odezwą się głosy, że metody ChangeDisplaySettings w ogóle nie powinno się stosować w przypadku DirectX, ponieważ jest to metoda GDI poza kontrolą DXGI. Ja specjalnie użyłem tu słowa wszechstosowania, ponieważ da się przewidzieć, że producenci w swoich grach tej metody używają. Ale o tym za chwilę.

Zacznę od moich wątpliwości, co do użycia metody ChangeDisplaySettings. Ta metoda do określenia trybów wyświetlania używa struktury DEVMODE, która nie jest kompatybilna ze strukturą DXGI_MODE_DESC używanej do tworzenia łańcucha wymiany w DirectX. Użycie metod EnumDisplaySettings oraz GetDisplayModeList umożliwia uzyskanie listy trybów wyświetlania (karta graficzna+monitor) i już w tym momencie zauważymy, że listy te się nie pokrywają. Na potrzeby tego tematu napisałem krótki program, który uzyskuje takie listy trybów wyświetlania DEVMODE oraz DXGI_MODE_DESC.

Oto fragment tej listy:

DXGI_MODE_DESC LIST                             DEVMODE LISTMODE
MODE 42 1600x1024 60000/1000 60Hz SC:0          MODE 42 1600x1024 60Hz FO:0
MODE 43 1600x1024 60000/1000 60Hz SC:1          MODE 43 1600x1024 60Hz FO:1
MODE 44 1600x1024 60000/1000 60Hz SC:2          MODE 44 1600x1024 60Hz FO:2
MODE 45 1600x1200 60000/1000 60Hz SC:0          MODE 45 1600x1200 60Hz FO:0
MODE 46 1680x1050 59950/1000 59Hz SC:0          MODE 46 1680x1050 59Hz FO:0
MODE 47 1680x1050 59950/1000 59Hz SC:1          MODE 48 1680x1050 59Hz FO:1
MODE 48 1680x1050 59950/1000 59Hz SC:2          MODE 50 1680x1050 59Hz FO:2
                                                MODE 47 1680x1050 60Hz FO:0
                                                MODE 49 1680x1050 60Hz FO:1
                                                MODE 51 1680x1050 60Hz FO:2
MODE 49 1920x1080 59950/1000 59Hz SC:0          MODE 52 1920x1080 59Hz FO:0
MODE 50 1920x1080 59950/1000 59Hz SC:1          MODE 53 1920x1080 59Hz FO:1
MODE 51 1920x1080 59950/1000 59Hz SC:2         MODE 54 1920x1080 59Hz FO:2
                                                MODE 55 1920x1080 60Hz FO:0
                                                MODE 56 1920x1080 60Hz FO:1
                                               MODE 57 1920x1080 60Hz FO:2
MODE 52 1920x1200 59950/1000 59Hz SC:0          MODE 58 1920x1200 59Hz FO:0
                                                MODE 59 1920x1200 60Hz FO:0

W przypadku wywołania funkcji ChangeDisplaySettings dla rozdzielczości 1920x1080@60Hz a następnie uzyskania listy trybów DXGI_MODE_DESC i utworzenia łańcucha wymiany z opcją Windowed = TRUE zainicjujemy tryb 1920x1080@59Hz. Ta minimalna różnica spowoduje złe wyliczanie synchronizacji pionowej?

Niezgodność listy trybów to jedno. I choć utworzenie łańcucha wymiany w trybie Windowed ma swoje plusy takie jak to, że przełączanie się między trybami polega wyłącznie na zmianie rozmiaru okna. A z pomocą ChangeDisplaySettings zmieniamy rzeczywisty rozmiar ekranu.

U mnie wygląda to mniej więcej tak:

m_swapChain->ResizeTarget(&displayModeList[modeIndex]);
m_swapChain->ResizeBuffers(1, displayModeList[modeIndex].Width, displayModeList[modeIndex].Height, DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH);
m_swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (LPVOID*)&backBuffer);
m_device->CreateRenderTargetView(backBuffer, NULL, &m_renderTargetView);
backBuffer->Release();
backBuffer = 0;
m_device->CreateTexture2D(&depthBufferDesc, NULL, &m_depthStencilBuffer);
m_device->CreateDepthStencilView(m_depthStencilBuffer, &depthStencilViewDesc, &m_depthStencilView);
m_deviceContext->OMSetRenderTargets(1, &m_renderTargetView, m_depthStencilView);
m_deviceContext->RSSetViewports(1, &viewport);
ChangeDisplaySettings(&dmScreenSettings, CDS_FULLSCREEN);

Zmiana rozmiaru targetu, rozmiaru bufora, tworzenie nowego targetu, dalej bufor głębi i na koniec przystosowuje rozdzielczość ekranu do tak zmienionego rozmiaru obrazu.

Jeszcze fajniej przedstawia się zmiana trybu multisamplingu:

m_deviceContext->OMSetRenderTargets(0, 0, 0);
m_renderTargetView->Release();
m_renderTargetView = 0;

DXGI_SWAP_CHAIN_DESC swapChainDesc;
…

if (m_swapChain) {
	m_swapChain->Release();
	m_swapChain = 0;
}

m_factory->CreateSwapChain(m_device, &swapChainDesc, &m_swapChain);
m_swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (LPVOID*)&backBuffer);
m_device->CreateRenderTargetView(backBuffer, NULL, &m_renderTargetView);
backBuffer->Release();
backBuffer = 0;
m_device->CreateTexture2D(&depthBufferDesc, NULL, &m_depthStencilBuffer);
m_device->CreateDepthStencilView(m_depthStencilBuffer, &depthStencilViewDesc, &m_depthStencilView);
m_deviceContext->OMSetRenderTargets(1, &m_renderTargetView, m_depthStencilView);
m_deviceContext->RSSetViewports(1, &viewport);

I tyle. Efekt jest taki, że zmiana multisamplingu nie powoduje chwilowego wygaśnięcia obrazu w przypadku utworzenia nowego łańcucha wymiany. To wszystko dzieje się dzięki użyciu Windowed = TRUE i funkcji ChangeDisplaySettings.

To wygaśnięcie obrazu w momencie zmiany trybu multisamplingu to cecha charakterystyczna dla gier. Bowiem jeśli gra uruchamiana jest rzeczywiście w trybie pełnoekranowym Windowed = FALSE to przed każdą zmianą trybu multisamplingu należy utworzyć nowy łańcuch wymiany, a metoda CreateSwapChain wymaga przełączenia się do trybu okienkowego przy użyciu metody SetFullscreenState. W tym przypadku, nie jest mi znany sposób, aby przełączać się pomiędzy trybami multisamplingu w trybie pełnoekranowym bez chwilowego wyłączenia obrazu.

Uważam jednak, że naprzemienne używanie ChangeDisplaySettings z metodami DXGI jest niebezpieczne. Funkcja ChangeDisplaySettings jest funkcją odrębną poza DirectX. To co DXGI robi po zmianie rozdzielczości przy użyciu ChangeDisplaySettings to zwykła zaprogramowana reakcja, którą równie dobrze możemy wstawić do obsługi komunikatu SW_SIZE. Pytanie tylko czy to będzie działać zawsze? I czy to jest bezpieczne i zgodne z zasadami programowania?

Efekt działania wygląda na fajny ponieważ tryb Windowed = TRUE uruchamia się bardzo szybko, przełączanie się pomiędzy trybami rozdzielczości również następuje szybko i przełączanie się pomiędzy trybami multisamplingu następuje w ciągłości (bez zanikania obrazu).

W MSDN zaleca się aby utworzyć łańcuch wymiany w trybie okienkowym Windowed = TRUE a następnie użyć metody SetFullscreenState(TRUE, NULL). Takie zalecenie jest na wypadek gdyby tryb wpisany do łańcucha wymiany okazał się niezgodny z trybami DXGI_MODE_DESC. Jeżeli jednak mamy taką listę to nie musimy się o to obawiać. Wystarczy utworzyć łańcuch wymiany z trybem zgodnym z listy i flagę Windowed = FALSE. To działa zawsze.

Ważne jest natomiast aby w konfiguracji łańcucha wymiany DXGI_SWAP_CHAIN_DESC opierać się na strukturze DXGI_MODE_DESC która jest jednym z elementów konfiguracji łańcucha wymiany jako BufferDesc.

Myślę, że prawidłowo należy używać struktur i metod zgodnych z DXGI, mamy wtedy zapewnioną spójność działania i bezpieczeństwo. Tworzymy więc łańcuch wymiany DXGI_SWAP_CHAIN_DESC z jednym ze zgodnych trybów DXGI_MODE_DESC z utworzonej wcześniej listy. I to wydaje się prawidłowe.

Oczywiście coś za coś.
W zmianie trybu rozdzielczości nic nam się nie zmienia. Usuwamy z niej oczywiście

ChangeDisplaySettings(&dmScreenSettings, CDS_FULLSCREEN);

Utworzony łańcuch wymiany z opcją Windowed = FALSE zapewnia dalsze działanie aplikacji w trybie pełnoekranowym po zmianie rozdzielczości.

Natomiast gorzej jest ze zmianą trybu multisamplingu.

Początek wygląda tak samo:

m_deviceContext->OMSetRenderTargets(0, 0, 0);
m_renderTargetView->Release();
m_renderTargetView = 0;

Konfigurację łańcucha wymiany musimy ustawić na taką w jakiej był nasz pulpit systemu:

DXGI_SWAP_CHAIN_DESC swapChainDesc;

swapChainDesc.BufferDesc.Width = DESKTOP_WIDTH;
swapChainDesc.BufferDesc.Height = DESKTOP_HEIGHT;

To jest istotne. Nie wiem dlaczego ale gdy ustawię tutaj od razu rozdzielczość jaką chcę uzyskać dajmy na to 800x600 to po kilku takich zmianach trybu multisamplingu i wyjściu z aplikacji okazuje się, że wszystkie programy które były wcześniej uruchomione na pulpicie i zmaksymalizowane, one będą zmniejszone do rozmiaru 800x600 w górnym-lewym rogu. Co ciekawe nie dzieje się to zawsze, dopiero po kilku zmianach. Winna tu na 100% jest metoda SetFullscreenState którą mamy poniżej. Jakiś bug?

Przed zniszczeniem starego łańcucha wymiany, należy przełączyć aplikację w tryb okienkowy

m_swapChain->SetFullscreenState(FALSE, NULL);

Dopiero zniszczyć:

if (m_swapChain) {
	m_swapChain->Release();
	m_swapChain = 0;
}

Tworzymy nowy łańcuch wymiany:

m_factory->CreateSwapChain(m_device, &swapChainDesc, &m_swapChain);

I tutaj dopiero zmieniamy rozmiar łańcucha wymiany na odpowiedni

m_swapChain->ResizeTarget(&displayModeList[displayCurrentMode]);
m_swapChain->ResizeBuffers(1, displayModeList[displayCurrentMode].Width, displayModeList[displayCurrentMode].Height, DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH);

Dalej już po staremu:

m_swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (LPVOID*)&backBuffer);
m_device->CreateRenderTargetView(backBuffer, NULL, &m_renderTargetView);
backBuffer->Release();
backBuffer = 0;
m_device->CreateTexture2D(&depthBufferDesc, NULL, &m_depthStencilBuffer);
m_device->CreateDepthStencilView(m_depthStencilBuffer, &depthStencilViewDesc, &m_depthStencilView);
m_deviceContext->OMSetRenderTargets(1, &m_renderTargetView, m_depthStencilView);
m_deviceContext->RSSetViewports(1, &viewport);

Przełączanie trybu multisamplingu jest więc nieco bardziej kłopotliwe i zawsze powoduje chwilowy zanik obrazu związany z przejściem w tryb okienkowy.

Ogólnie nastawiłem się aby mój silnik gry był uruchamiany wyłącznie w trybie pełnoekranowym (nie planuję trybu okienkowego, uważam go za zbędny). Jednak w jakiś sposób musiałem zaimplementować obsługę kombinacji klawiszy ALT+TAB.

W obsłudze komunikatów rozwiązałem to tak:

case WM_ACTIVATE:	{
	if (LOWORD(wparam) != WA_INACTIVE) {
		GLOBAL.WINDOW_ACTIVE = TRUE;
		ShowWindow(hwnd, SW_RESTORE);
	} else {
		GLOBAL.WINDOW_ACTIVE = FALSE;
		ShowWindow(hwnd, SW_SHOWMINNOACTIVE);
	}
	return 0;
}

Natomiast w pętli tak:

while (1) {

	// Przechwytuje komunikaty z okna
	if (PeekMessage(&msg, GLOBAL.HWND, 0, 0, PM_REMOVE)) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	// Jeśli program dostaje globalny komunikat wyjścia, zakończy swoje działanie
	if (GLOBAL.EXIT) {
		break;
	} else {
		// W przeciwnym przypadku wykonuje przetwarzanie kolejnej klatki, chyba że okno jest nieaktywne
		if (GLOBAL.WINDOW_ACTIVE) {
			if (fullscreen_state) {
				if (!Frame()) return false;
			} else {
				m_Graphics->m_D3D->GetSwapChain()->SetFullscreenState(TRUE, NULL);
			}
		} else {
			if (fullscreen_state) {
				m_Graphics->m_D3D->GetSwapChain()->SetFullscreenState(FALSE, NULL);
				fullscreen_state = false;
			} else {
				// nie robi nic (okno jest zminimalizowane i nieaktywne)					
			}
		}
	}
}

Oczywiście w przypadku użycia sposobu z ChangeDisplaySettings zamiast SetFullScreenState wstawiam ChangeDisplaySettings z odpowiednimi argumentami. To działa ale czy jest prawidłowe?

Wracając do pytania. Jakiej według Was metody powinno się używać prawidłowo? Napiszcie swoje argumenty za i przeciw.


#2

Aktualizacja:

W metodzie zmieniającej tryb multisamplingu ustawienie łańcucha wymiany na rozdzielczość pulpitu a następnie wywołanie metod ResizeTarget i ResizeBuffers po utworzeniu nowego łańcucha wymiany jednak nic nie daje.

Zauważyłem jednak, że pewne opóźnienie po metodzie SetFullscreenState ale jeszcze przed CreateSwapChain rozwiązuje problem. Po czymś takim działa i po wyjściu z aplikacji inne zmaksymalizowane okna nie pomniejszają się:

m_swapChain->SetFullscreenState(FALSE, NULL);
Sleep(100);
m_factory->CreateSwapChain(m_device, &swapChainDesc, &m_swapChain);

Trochę to bez sensu. Ktoś pomoże?