Home Page Home Page Articoli Inizializzazione e schermo

Inizializzazione e schermo

Si parla dell'inizializzazione dello schermo e delle modalità video in DirectX e di come gestire gli eventi "lost device", come scrivere un Game Loop e alcune info sul VSYNC
Autore: Stefano Cristiano Livello:
Inizializzazioni e schermo
DirectX è una API progettata e sviluppata da Microsoft per i sistemi Windows che da la possibilità di sviluppare videogiochi o, più in generale, applicazioni multimediali. Essa si compone di diverse “sotto-API” ognuna delle quali si occupa di uno specifico settore.

DirectXGraphics si occupa per intero della gestione della grafica, DirectMusic e DirectSound dell’audio, DirectInput delle periferiche di input (ad esempio tastiere, mouse, joypad, joystick), DirectPlay dell’interazione con la rete ed infine DirectShow che si occupa della gestione e dello streaming dei file multimediali video, audio o entrambi.

Nel Listato 1 è riportato il più semplice codice di inizializzazione di DirectXGraphics.


#include
#include
#include

#pragma comment(lib,"d3d8.lib")
#pragma comment(lib,"d3dx8.lib")

typedef CComPtr Direct3D8_Ptr;
typedef CComPtr Direct3DDevice8_Ptr;
typedef CComPtr D3DXFont_Ptr;

Direct3D8_Ptr g_d3d;
D3DXFont_Ptr g_font;
Direct3DDevice8_Ptr g_device;

int APIENTRY WinMain(HINSTANCE instance,HINSTANCE,LPSTR,int)
{
//...Omissis, creazione finestra...

g_d3d.p = Direct3DCreate8(D3D_SDK_VERSION);
if(!g_d3d)
return -1;

D3DDISPLAYMODE display;
g_d3d->GetAdapterDisplayMode(D3DADAPTER_DEFAULT,&display);

D3DPRESENT_PARAMETERS params;
ZeroMemory(¶ms,sizeof(params));

params.Windowed = TRUE;
params.hDeviceWindow = g_hwnd;
params.BackBufferFormat = display.Format;
params.SwapEffect = D3DSWAPEFFECT_DISCARD;
params.MultiSampleType = D3DMULTISAMPLE_NONE;
params.EnableAutoDepthStencil = TRUE;
params.AutoDepthStencilFormat = D3DFMT_D16;

HRESULT hr;
hr = g_d3d->CreateDevice( D3DADAPTER_DEFAULT,D3DDEVTYPE_HAL,g_hwnd,
D3DCREATE_SOFTWARE_VERTEXPROCESSING,
¶ms,&g_device.p);

if(FAILED(hr))
return -1;

D3DXCreateFont(g_device,(HFONT)GetStockObject(SYSTEM_FONT),&g_font.p);
//...Omissis, gestione messaggi...
return 0;
}


Come potete ben vedere le righe di codice necessarie sono davvero pochissime. Bene ora sapete cosa rispondere a chi dice che OpenGL è più semplice di DirectX. Il primo passo è la creazione dell’oggetto Direct3D con Direct3DCreate8. Successivamente chiediamo a tale oggetto COM la modalità corrente del display con IDirect3D::GetAdapterDisplayMode(…) e riempiamo la struttura D3DPRESENT_PARAMETERS con delle informazioni che daranno maggiori informazioni a D3D sulla modalità grafica da inizializzare. Fate attenzione all’uso di ZeroMemory per azzerare la struttura in questione. Quando dichiarate una qualsiasi struttura DirectX (in questo caso D3DPRESENT_PARAMETERS) i suoi campi contengono dei valori a casaccio, quindi è bene azzerarla e poi riempirla con i parametri che intendiamo specificare. Questo perché lo ZERO nella maggior parte dei casi indica a D3D che non si è specificato quel parametro e che quindi lo si lascia di “default”.

Windowed Flag che richiede la modalità in finestra se impostato a TRUE
HdeviceWindow Handle della finestra in cui dobbiamo disegnare
BackBufferFormat Indica il numero dei colori della modalità da inizializzare
SwapEffect Indica la tecnica di presentazione contenuti a schermo
MultiSampleType Specifica se è abilitato l’Anti-aliasing a tutto schermo
EnableAutoDepthStencil Abilita lo ZBuffer e lo Stencil Buffer (vedremo dopo di che si tratta)
AutoDepthStencilFormat Indica la precisione dello ZBuffer e dello Stencil

Stare ora ad enumerare tutti i parametri di D3DPRESENT_PARAMETERS sarebbe un’inutile perdita di tempo: la documentazione dell’SDK è chiarissima a riguardo. Imparate a consultarla e avrete la vita semplice.

L’unica cosa che dovete sapere è che passando D3DSWAPEFFECT_DISCARD non potete accedere ai contenuti del backbuffer, poiché lasciate al driver la scelta della modalità di presentazione della scena a schermo. In genere è bene far decidere al driver dove allocare risorse poiché può effettuare una serie di ottimizzazioni particolari.

Con CreateDevice viene creato l’oggetto IDirect3DDevice8 che la diretta rappresentazione della scheda video. Dei parametri passati ci interessa solo sapere che D3DDEVTYPE_HAL indica di usare l’accelerazione hardware. Se avessimo passato il flag D3DDEVTYPE_REF invece sarebbe stato usato il reference rasterizer di cui si è parlato nel precedente capitolo (sezione debugging).

Per andare a tutto schermo le modifiche da fare al programma CiaoDirectX sono a dire poco banali:

//CODICE MODIFICATO rispetto a CiaoDirectX

D3DPRESENT_PARAMETERS params;
ZeroMemory(¶ms,sizeof(params));
params.Windowed = FALSE; //Prima era TRUE
//Omissis...Come prima...
params.BackBufferWidth = 640; //Dimensione orizzontale...
params.BackBufferHeight = 480; //Dimensione verticale...
//Omissis...Come prima...


Il disegno vero e proprio della scena (beh, forse è un po’ esagerato usare questa parola…in fondo stiamo stampando solo una scritta a schermo…) avviene nel metodo Draw() chiamato in risposta ad un messaggio di ridisegno della finestra WM_PAINT.


LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{

LRESULT res = 0;
if(message==WM_PAINT||message==WM_SIZE)
Draw();
else
res = DefWindowProc(hWnd,message,wParam,lParam);
return res;
}

void Draw()
{
g_device->BeginScene();
g_device->Clear(0,0,D3DCLEAR_TARGET,0,0,0);
g_font->DrawText(_T("Ciao DirectX fullscreen!!!\n(per uscire premere Alt + F4)"),
-1,&g_font_rect,DT_CENTER|DT_VCENTER,0xffffffff);

g_device->EndScene();
g_device->Present(0,0,0,0);
}


Ogni volta che dobbiamo disegnare una nuova scena in DirectX (quindi ad ogni frame) dobbiamo chiamare il metodo IDirect3DDevice8::BeginScene() e ogni volta che abbiamo finito il disegno chiamare il metodo gemello IDirect3DDevice8::EndScene(). Tra queste due chiamate di metodo dobbiamo disegnare tutto quello che ci interessa. Con IDirect3DDevice8::Clear() invece ripuliamo il BackBuffer riempiendolo di nero. Ma cos’è il backbuffer? Urge una breve ma concisa spiegazione di quello che sta succedendo.

Ricapitolando noi abbiamo creato un oggetto IDirect3D8 al quale abbiamo chiesto un IDirect3DDevice8, che rappresenta la scheda video alla quale mandare i comandi di disegno.

Al suo interno IDirect3DDevice provvederà a creare il BackBuffer che è una sorta di schermo virtuale dove vengono effettuate tutte le operazioni di disegno. Una volta terminate tali operazioni (dopo IDirect3DDevice::EndScene()) con il metodo IDirect3DDevice::Present() D3D prende questo backbuffer e lo riversa sullo schermo. Qualcuno potrebbe chiedersi, ma non era più semplice disegnare direttamente sullo schermo? Beh il problema è che scrivendo direttamente sullo schermo l’utente vedrebbe la scena nella sua fase di disegno causando un effetto di “sfarfallio”. Questo è il motivo dell’esistenza del backbuffer. Si possono creare anche più di un backbuffer (campo params.BackBufferCount) e organizzarli in una “catena” di rendering.

Scrivere un GAME LOOP
La sensazione di “fluidità” di un videogioco non dipende esclusivamente dal numero di Frames Per Secondo (FPS) che siamo in grado di gestire. Ci sono giochi che pur girando a 30 FPS non hanno nulla da invidiare ad altri che ne fanno 100 o più. Il segreto sta nel scrivere un “game loop” che sia un minimo intelligente. Il programma CiaoDirectX usa probabilmente il peggior “game loop” che sia possibile inventarsi, dato che aspetta un WM_PAINT per poter ridisegnare la scena. Nessuno ha protestato poiché essendo privo di animazione non è possibile accorgersi che il ridisegno avviene “una volta ogni tanto”. Questa situazione va quindi gestita diversamente.

Le cause del cosiddetto “Frame Jerkyness”, ovverosia della sensazione di “scatteggio” da parte del giocatore, possono essere molteplici e altrettanti sono gli accorgimenti bisogna prendere. Eccone alcuni.

CPU LIMIT
Questo è uno dei casi più frequenti: durante il rendering il vostro Engine 3D effettua una serie di operazioni che bloccano il flusso del programma (ad esempio un calcolo matematico molto complesso). Per accorgervi se il vostro programma è “CPU limited” potete usare un qualsiasi profiler (ad esempio il VTune di Intel) e cercare di ottimizzare le porzioni di codice incriminate.

GPU LIMIT
Il vostro acceleratore non riesce a disegnare abbastanza velocemente i poligoni che gli mandate perchè:

1) I poligoni sono mandati barbaramente senza fare attenzione agli accorgimenti d’obbligo che i programmatori d’oggi devono osservare e che saranno descritte più avanti nel libro

2) I poligoni sono TROPPI e bisogna creare un qualche sistema che scali il livello di dettaglio (LOD).

MESSAGING WINDOWS
Come ogni altra applicazione anche un videogioco deve gestire la cosiddetta “pompa dei messaggi” di Windows attraverso Get/PeekMessage. Molte volte è proprio windows che vi ruba del prezioso tempo della CPU quando chiamate queste funzioni magari perché deve darlo a qualche applicazione in background. Una soluzione possibile è gestire i messaggi windows in un thread separato, inserendo qualche Critical Section per la sincronizzazione.

VSync
Se siete tra coloro che si divertono a smanettare le opzioni avanzate di configurazione dei giochi 3D, avrete sicuramente avuto a che fare con il VSync. In più vi dico che questa causa è una delle più complicate da gestire di tutte.

Il VSync ha a che fare con il Refresh dello schermo. Quando avete acquistato il monitor del vostro computer tra i vari parametri, oltre al dot pitch e alla risoluzione massima avrete sicuramente tenuto in considerazione il Refresh Rate o anche Frequenza di Aggiornamento. Questa frequenza (espressa in Hertz com’e’ naturale che sia) indica il numero di volte che lo schermo viene ridisegnato in un secondo. La maggior parte dei monitor di fascia media ha un refresh rate compreso tra 60 e 85 Hertz, vuol dire che il gioco che segna 140 FPS (frame per secondo) sta barando? Come fa a disegnare 140 frames al secondo se lo schermo può fisicamente visualizzarne solo 60? La risposta è semplice: il VSync è disabilitato. Cosa vuol dire ciò? Vuol dire che il gioco in questione una volta dato ordine all’acceleratore di disegnare una determinata scena non attende che effettivamente tale operazione sia terminata e gli invia già il prossimo frame. Questo può essere causa dei cosiddetti “tearing effects” cioè dell’effetto di “instabilità” dell’immagine dovuto proprio al fatto che il gioco tenta di scrivere la nuova scena PRIMA che sia effettivamente finito il disegno di quella precedente.

E’ possibile aggirare questo problema? Una soluzione è quella di abilitare il VSync, cioè quando diamo l’ordine all’acceleratore di disegnare la scena a schermo (tramite il comando Present() come vedremo tra un po’) ci mettiamo ad “aspettare” che tale disegno sia finito prima di

disegnare la prossima scena. Ehi, aspettate un momento! Ma “aspettare” non significa mica perdere dei preziosissimi cicli della CPU? Ebbene si, quando aspettate che il Vertical Retrace sia ultimato state effettivamente dicendo al vostro processore di rigirarsi i pollici.

E’ difficile trovare una soluzione a questo problema dato che dipende dalla combinazione di Scheda Video/Monitor/Tipologia Gioco e la soluzione migliore è lasciare decidere all’utente SE abilitare il VSync ed eventualmente la frequenza di Refresh del gioco.

Voglio che sia chiaro che possiamo “legalmente” cambiare la frequenza di aggiornamento del monitor solo quando il nostro gioco è a tutto schermo e abbiamo il pieno controllo del sistema.

Il seguente spezzone di codice mostra come impostare un refesh rate di 70 Hz con VSync:


params.FullScreen_RefreshRateInHz = 70;
params.FullScreen_PresentationInterval = D3DPRESENT_INTERVAL_ONE;


Ovviamente se il refresh 70 Hz non è supportato dal nostro hardware CreateDevice fallirà miseramente. Per sapere quali sono le modalità video supportate dal nostro acceleratore e i rispettivi refresh rate dobbiamo usare i metodi IDirect3DDevice::EnumAdapterModes() in questo modo:


for(int i =0; i < g_device->GetAdapterModeCount(D3DADAPTER_DEFAULT); i++)
{
D3DDISPLAYMODE mode;
g_d3d->EnumAdapterModes(D3DADAPTER_DEFAULT, i, &mode);
}


La struttura D3DDISPLAYMODE contiene tutti i campi che ci interessano


typedef struct _D3DDISPLAYMODE {
UINT Width;
UINT Height;
UINT RefreshRate;
D3DFORMAT Format;
} D3DDISPLAYMODE;


Dove Width e Height sono le dimensioni della modalità video (ad es. 800x600 oppure 640x480), RefreshRate un intero indicate appunto il Refresh Rate supportato da questa modalità e Format una costante dell’enumerazione D3DFORMAT che ci indica il numero di colori della modalità (ad es. 16 bit oppure 32 bit). Ad esempio se Format è == D3DFMT_R5G6B5 allora vuol dire che quella è una modalità a 16 bit, se invece e == D3DFMT_X8R8G8B8 allora è 32 bit. Possiamo usare i campi Width ed Height per riempire i BackBufferWidth/Height di D3DPRESENT_PARAMETERS.

Stando attenti almeno questi fattori si può ottenere una fluidità accettabile per il nostro gioco. Un’altra cosa alla quale bisogna prestare attenzione è lo smoothing dell’input (che vedremo più avanti,magari in un'altra puntata)

Conoscere l’acceleratore
Non tutte le schede sono uguali, ognuna ha il suo set di effetti che è capace di accelerare in hardware. Proprio per questo DirectX ci da la possibilità di interrogare le “capabilities” di un device per poter sapere tutto su di esso. Tramite il metodo IDirect3DDevice8::GetCaps() e la struttura D3DCAPS8 possiamo avere le informazioni di cui abbiamo bisogno.


if(!(caps.Caps2&D3DCAPS2_FULLSCREENGAMMA))
//Non supporta la correzione gamma a tutto schermo...
if(!(caps.Caps2&D3DCAPS2_DYNAMICTEXTURES))
//Non supporta le texture dinamiche...



Il codice è banale, si chiama GetDeviceCaps e si confrontano i campi della struttura D3DCAPS8 con dei flag specifici il cui significato è spiegato meticolosamente nella documentazione dell’SDK.

L’esempio CapsDetection mostra alcune capabilities della vostra scheda e in $DXSDK$\bin\DXUtils\DXCapsViewer.exe trovate il programma dell’SDK che mostra tutte le CAPS del vostro acceleratore

Gestione Risorse e eventi (Lost Device)
Programmando in Direct3D uno dei problemi più ricorrenti è lo scambio dei dati tra acceleratore video e sistema. Per dati s’intende tutto ciò che serve alla scheda video per generare i pixel del frame corrente e quindi: vertici e indici dei poligoni, texture e superfici bidimensionali e tridimensionali. Nella creazione di una risorsa bisogna sempre specificare il cosiddetto “POOL” di memoria, nel quale essa sarà allocata.

I moderni acceleratori sono dotati di una quantità più o meno variabile di memoria dove possono contenere i dati che gli servono per velocizzare il rendering. Quest’architettura libera la CPU dall’onere di mandare avanti ed indietro i dati nel BUS. Ovviamente la memoria video non è infinita, quindi bisogna adottare degli accorgimenti per sfruttarla al meglio.

I POOL di creazione delle risorse in Direct3D sono tre:

D3DPOOL_DEFAULT
Crea la risorsa nella memoria più congeniale all’acceleratore

D3DPOOL_SYSTEMMEM
Crea la risorsa in memoria di sistema

D3DPOOL_MANAGED
Tiene una copia in memoria di sistema e una su quella dell’acceleratore. Sarà Direct3D manderà avanti ed indietro la risorsa nel modo più efficiente.

Il vantaggio di D3DPOOL_DEFAULT è che in generale Direct3D creerà una sola copia dei dati. Essi sono creati nella memoria video dell’acceleratore oppure, se quest’ultima è già piena, nella memoria AGP o quella di sistema (la RAM). Lo svantaggio di questo POOL è che non potete né leggere né scrivere da queste risorse nei vostri programmi (come vedremo più avanti attraverso Lock/Unlock).

Il vantaggio di D3DPOOL_SYSTEMMEM è la gran velocità di lettura/scrivere dalle risorse dal codice del vostro programma. Lo svantaggio ovviamente sta nel fatto che la risorsa deve essere mandata attraverso il BUS all’acceleratore (impegnando la CPU).

Il vantaggio di D3DPOOL_MANAGED consiste nella gran velocità in lettura/scrittura e nella rapidità del rendering da parte dell’acceleratore. Lo svantaggio di questo flag consiste nel fatto che la risorsa è presente in doppia copia, in memoria di sistema e in memoria Video o eventualmente AGP (a discrezione di D3D).

L’ultima cosa da tenere in considerazione è ciò che riguarda i cosiddetti eventi di “lost device”. Quando la vostra applicazione a tutto schermo perde il focus dell’input (ad esempio quando l’utente effettua un ALT + Tab) e poi lo riacquista bisogna re-inizializzare tutti gli oggetti “Device Dependant”. Questo significa chiamare Release() su tutti gli oggetti e effettuare una IDirect3DDevice8::Reset() sul device passando la struttura D3DPRESENT_PARAMETERS. Il modo più corretto di gestire questi eventi è subito dopo IDirect3DDevice8::Present() in questo modo:


while (D3DERR_DEVICELOST == hr)
{
do
{
Sleep(1000);
hr = g_device->TestCooperativeLevel();
}while (hr != D3DERR_DEVICENOTRESET);

if (FAILED(g_device->Reset(&g_params)))
hr = D3DERR_DEVICELOST;
}



Per semplicità gli esempi precedentemente illustrati non utilizzavano questa tecnica.

Gli oggetti creati con D3DPOOL_SYSTEMMEM e D3DPOOL_MANAGED tuttavia NON hanno bisogno di essere rilasciati e re-inizalizzati dopo un evento di lost device.

Per aggirare il problema si potrebbero usare sempre questi due flag, altrimenti bisogna creare un framework che gestisca il problema.

In ogni caso, se si decide per un motivo qualsiasi di usare il flag D3DPOOL_DEFAULT, è bene allocare PRIMA le risorse che usano tale flag, POI quelle con D3DPOOL_MANAGED o D3DPOOL_SYSTEMMEM.
Voto medio articolo: 5.0 Numero Voti: 1
Stefano Cristiano

Stefano Cristiano

3D Coder. Sto lavorando ad un engine chiamato "Muse Engine" di cui potete leggere al mio sito http://pagghiu.gameprog.it Profilo completo

Articoli collegati

Integrare le Direct3D 8.1 con MFC Usando Visual Studio 6.0
In questo articolo mostreremo come creare una semplice applicazione SDI (Single Document Interface), in cui mostrare un oggetto tridimensionale in formato *.X e di farlo ruotare intorno all'asse Y.
Autore: Biagio Iannuzzi | Difficoltà:
Particle System
Brevi cenni su come implementare un particle system in DirectX
Autore: Stefano Cristiano | Difficoltà: | Voto:
Texture Blending (Parte II) Quake 3 Arena Shader System
Questo articolo è simile alla Parte I e ripete molti aspetti però tratta anche un pò degli shaders di Quake 3 Arena.
Autore: Stefano Cristiano | Difficoltà: | Voto:
Texture Blending (Parte I)
Come gestire gli effetti di blending tra texture. Multitexturing e Multipassing. Effect Files
Autore: Stefano Cristiano | Difficoltà: | Voto:
Sprite
Come creare uno "sprite" e animarlo. Special thanks to Elevator2 per gli sprites di megaman
Autore: Stefano Cristiano | Difficoltà: | Voto:
Texture
Introduzione al concetto di texture, di coordinate texture. Come animare le coordinate texture
Autore: Stefano Cristiano | Difficoltà:
Grafica 2D in DirectX. Vertex Buffers
Introduzione alla grafica 2D. Utilizzo dei vertex buffers
Autore: Stefano Cristiano | Difficoltà: | Voto:
Debbugging di applicazioni DirectX
Consigli e strategie per il Debugging delle applicazioni DirectX.
Autore: Stefano Cristiano | Difficoltà: | Voto:
Framework DXGWrapper
Spiega la filosofia di fondo del framework ideato dal sottoscritto ed usato da tutti i samples degli articoli relativi a DirectX.
Autore: Stefano Cristiano | Difficoltà: | Voto:
Gamma Correction e Colori
Una trattazione breve ma con una base matematica che spiega in parole povere il problema del fattore gamma e i colori in generale
Autore: Stefano Cristiano | Difficoltà: | Voto:
Introduzione a DirectX
Introduzione al mondo DirectX , requisiti, tools necessari, configurazione dell’ambiente di sviluppo.
Autore: Stefano Cristiano | Difficoltà: | Commenti: 2 | Voto:
Copyright © dotNetHell.it 2002-2017
Running on Windows Server 2008 R2 Standard, SQL Server 2012 & ASP.NET 3.5