Progetto 3DCloudViz NICE -DIEE WP 1 Studio di tecnologie per l’esecuzione accelerata di applicazioni DirectX in ambiente virtuale Deliverable D1 R.3.1 Definizione dei requisiti tecnici e funzionali e progettazione di un protocollo di visualizzazione remota di applicazioni 3D basate su DirectX in esecuzione su macchine virtuali Progetto 3DCloudViz Deliverable D3 Indice 1 R.1.1 Definizione dei requisiti tecnici e funzionali e progettazione di un protocollo di visualizzazione remota di applicazioni 3D basate su DirectX in esecuzione su macchine virtuali . 3 1.1 Introduzione ................................................................................................................................. 3 1.2 Analisi di strategie per la Library Interposition per l’API DirectX ............................................ 3 1.3 Analisi di varie strategie per la Function interception per l’API DirectX .................................. 8 1.4 Limitazioni dell’approccio tramite Library Injection ................................................................ 17 1.5 Analisi di fattibilità del supporto per External Rendering......................................................... 18 1.6 Avanzamenti tecnologici della piattaforma di sviluppo ............................................................ 20 1.7 Analisi e implementazione di Screen Grabbing tramite NVDIA GRID ................................... 23 1.8 Conclusioni ed obiettivi raggiunti ............................................................................................. 24 2 Progetto 3DCloudViz Deliverable D3 1 R.1.1 Definizione dei requisiti tecnici e funzionali e progettazione di un protocollo di visualizzazione remota di applicazioni 3D basate su DirectX in esecuzione su macchine virtuali 1.1 Introduzione NICE ha tra i propri prodotti un software client-server denominato DCV (Desktop Cloud Visualization), dedicato alla remotizzazione del desktop. Consente all’utente di accedere via rete ad una workstation remota (fisica o virtuale) capace di grafica ad alte prestazioni da una varietà di device, tra cui PC, tablets e thin clients. Ad oggi il software è focalizzato all’accesso remoto a applicazioni grafic-intensive basate su OpenGL, in particolare applicazioni tecnico-scientifiche, quali visualizzatori di modelli, applicazioni CAE e applicazioni CAD. L’obiettivo di questo WP consiste nell’esplorare i possibili approcci per estendere NICE DCV per il supporto alla remotizzazione di applicazioni basate su DirectX sulla piattaforma Microsoft Windows. Requisiti Funzionali In sintesi le attività di RI e SS punteranno a. consentire l’accesso remoto ad applicazioni DirectX su sistemi Microsoft Windows (Windows 7 sarà la versione di riferimento del sistema operativo) compatibilità con i client NICE DCV esistenti impatto prestazionale limitato per applicazioni tecnico scientifiche (CAE, CAD, ...): le prestazioni devono essere comparabili a quelle della remotizzazione di applicazioni OpenGL supporto per macchine fisiche supporto per Virtual Machines, possibilmente in modo indipendente dal tipo di hypervisor supporto per differenti versioni DirectX (9, 10, 11) 1.2 Analisi di strategie per la Library Interposition per l’API DirectX La nostra indagine di ricerca per un wrapper DirectX ci ha inizialmente spinti a voler riadattare le soluzioni già adottare per il wrapper di OpenGL già adottato e collaudato all’interno del pacchetto DCV. Tuttavia il tentativo per il riadattamento delle strategie già usate per OpenGL ha incontrato nelle DirectX delle difficoltà tecniche così ampie che si è deciso, nel tempo, di optare per soluzioni alternative, fino a trovare quella funzionate per il nostro caso d’uso. Il lavoro di analisi è nato dalla rivisitazione del wrapper di OpenGL già implementato in DCV, passando attraverso lo studio del WDDM (Windows Display Driver Model) per una comprensione delle DirectX, con tutte le difficoltà per il wrapping di una libreria grafica (DirectX) che fonda le sue radici più profonde nel driver usato per la gestione dell’aspetto grafico dell’intero sistema operativo, per cui non dirottabile con le classiche tecniche di library interposition. Il wrapper OpenGL di DCV Le OpenGL sono una libreria grafica strutturata secondo una logica client-server. L’intera interfaccia di funzioni opengl implementate dall’installable client driver è racchiusa all’interno del modulo opengl32.dll, per cui è noto che le applicazioni che utilizzano opengl fanno riferimento ai metodi contenuti all’interno di questa libreria e non ai moduli del driver grafico che ne implementano le funzionalità. Per wrappare le opengl è necessario trovare un sistema per dirottare le chiamate della opengl32.dll a quelle di una libreria proprietaria di interposizione, detta anche proxy-dll. L’approccio più semplice è quello di creare una proxy-dll che abbia gli stessi identici entry points pubblici della libreria che si intende wrappare e farla caricare all’applicazione. Se il nostro scopo è reimplementare il client di opengl per serializzare le chiamate e inviarle ad un rendering server esterno, potremmo creare ad esempio una 3 Progetto 3DCloudViz Deliverable D3 opengl32.dll da sostituire all’originale all’interno della directory di sistema windows/system32/. Tuttavia questo approccio è errato per diversi motivi: ● La opengl32.dll viene installata da tutti i driver grafici dei fornitori di tutto il mondo (i.e. NVIDIA, Amd, Intel), per cui la sua sostituzione coatta rappresenta una corruzione del sistema e dell’installable client driver. ● Essendo installata dai driver grafici, non c’è garanzia che la nostra libreria non venga rimpiazzata di frequente all’update dei driver grafici, qualora fosse necessario effettuarli. ● E’ difficile che le OpenGL dei driver vengano rimpiazzate al 100% dal wrapper. Anche nel caso in cui si tratta di un totale wrapping con rendering esterno, è pur sempre necessario effettuare delle chiamate al client driver per l’enumerazione dei pfd (pixel format descriptor) o per un eventuale fallback a estensioni native. Un rimedio semplice a questi tre problemi si realizza lasciando intatta la opengl32.dll nella directory di sistema, e copiando la proxy-dll all’interno della directory dove viene eseguita l’applicazione. Il funzionamento di questo espediente risiede nel modo in cui il sistema operativo risolve la ricerca del path di una libreria nel momento in cui un’applicazione ha bisogno di aprirla: 1) perchè la possiede nella IAT (Import Address Table); 2) perchè è stata caricata a runtime tramite LoadLibrary. In entrambi i casi e qualora non fosse specificato il path della dll, la libreria viene ricercata dal sistema operativo nel seguente ordine: 1. La directory dove viene eseguita l’applicazione. 2. La directory di sistema. Con GetSystemDirectory è possibile ottenere questo path 3. La directory di sistema a 16-bit. Non esiste alcuna funzione per ottenere il path di questa directory, ma è ugualmente cercata. 4. La directory di windows. Con GetWindowsDirectory è possibile ottenere questo path 5. La directory corrente. 6. Le directory che sono elencate nella variabile d’ambiente PATH. Come è possibile vedere, la copia della proxy-dll all’interno della directory dove viene eseguita l’applicazione permette il caricamento della libreria a primo colpo, salvo delle eccezioni che non possiamo ignorare. Se un’applicazione dovesse caricare le opengl32.dll specificando il path assoluto con una LoadLibrary(“c:\windows\system32\opengl32.dll”), in questo caso la nostra proxy-dll non verrebbe presa in considerazione. Si potrebbe installare un redirect creando un file .local così che il sistema verrebbe forzato a scegliere la libreria all’interno del path dell’applicazione, tuttavia questo espediente potrebbe introdurre delle alterazioni. Inoltre, esiste una lista di librerie conosciute che vengono caricate dal sistema operativo all’avvio e che vengono riutilizzate qualora la dll da caricare fosse già inclusa nella chiave di registro: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs C’è dunque una concreta possibilità che la opengl32.dll si trovi in questa lista (solitamente non è mai così, ma qualche admin o un’installazione potrebbe modificare i registri) e che la nostra proxy-dll non venga caricata anche se si trova dentro la directory dell’applicazione. Ma anche se non fosse così, ci sono degli svantaggi concreti nel copiare la proxy-dll all’interno della cartella dell’applicazione: ● ● ● ● Alcune applicazioni potrebbero possedere una loro versione di opengl32.dll già presente nella cartella dell’eseguibile. La nostra proxy-dll rischierebbe di alterare il comportamento dell’applicazione. La maggior parte dei tool per il debugging di applicazioni opengl copiano una opengl32.dll proprietaria all’interno della cartella dell’eseguibile per permettere il tracciamento delle funzioni. Se copiassimo la nostra, non potremmo usare queste strategie di debugging. Copiare una proxy-dll all’interno delle directory di tutte le applicazioni del sistema non è una scelta valida se si intende fare wrapping a system-wide, come nel nostro caso. 4 Progetto 3DCloudViz ● Deliverable D3 La disinstallazione del wrapper richiederebbe la cancellazione di tutte le proxy-dll installate nel sistema. Un evento proibitivo che richiederebbe un lavorio eccessivo coi files, la chiusura di tutti i processi ed il riavvio del sistema operativo. Come abbiamo potuto vedere, nonostante gli strumenti del sistema operativo per l’interposizione e la redirezione di librerie siano moltepici, non ce n’è uno solo che può essere utile nel nostro caso d’uso, neppure per il wrapping di OpenGL. L’unico modo per poter far caricare la nostra proxy-dll a system-wide, senza sovrascrivere la libreria originale o copiare la proxy-dll nelle directory di tutti i processi del sistema ci richiede di andare oltre le funzionalità messe a disposizione per l’utente di windows e scendere più in basso nel kernel, scrivendo un apposito filter driver che gestisca la redirezione nel caso in cui il sistema operativo stia cercando di caricare una opengl32.dll nella directory di sistema. Abbiamo deciso di chiamare dcvogl.dll l’intera proxy-dll che effettuerà il wrapping totale delle OpenGL. Senza scendere nei dettagli implementativi, la redirezione implementata dal filter driver funziona nel seguente modo: ● ● Quando viene cercata la opengl32.dll nel path di sistema c:\windows\system32, il filter driver fa caricare al sistema operativo la dcvogl.dll Quando viene cercata la libreria dcvogl32.dll, viene caricata la opengl32.dll presente nel path di sistema. Questo ci consente di caricare la libreria opengl installata dal driver grafici dei fornitori. In questo modo, è possibile attivare e disattivare il filter driver senza procurare alcuna alterazione del sistema operativo o dover copiare/cancellare proxy-dll in tutte le cartelle. L’approccio si rivela ottimo per un wrapping di tipo system-wide per le OpenGL ed è utilizzato nella versione attuale di dcv per le macchine virtuali Windows, con rendering server esterno. DirectX e filter driver Come già accennato in precedenza, il metodo di library interposition con filter driver non funziona con le DirectX. Questo perché le DirectX sono ancorate al WDDM (Windows Display Driver Model) che bypassa il filter driver, in quanto il contratto privato con dxgkrnl.sys non passa attraverso lo stack di Input/Output. Se prendiamo in esame le DirectX9, come per OpenGL, l’intera libreria grafica è racchiusa in un’unico modulo d3d9.dll. Tuttavia, in questo caso, se applicassimo le redirezioni del filter driver per caricare una eventuale dcvd3d9.dll come proxy-dll, finiremmo sempre per caricare la d3d9.dll usata dal sistema operativo. Questo ci mette in guai seri, in quanto ci pone di fronte come unica alternativa di copiare la proxy dll all’interno delle cartelle delle processi, con tutti i lati negativi e le impossibilità implementative che abbiamo già visto. E non finisce qui. Le versioni di DirectX successive alla 9 passano attraverso una libreria intermedia per la gestione dei modi grafici che si chiama dxgi.dll e che non dipende più dalla versione delle DirectX installate nel sistema, ma che è ancorata al driver grafico e più cambiare i punti di ingresso pubblici e la sua implementazione interna in funzione dei driver grafici installati dai vari fornitori. Questo rende la tecnica di library interposition con proxy-dll totalmente inapplicabile per DirectX e ci ha costretti a procastinare lo sviluppo di un wrapper serio per DirectX, facendoci volgere lo sguardo verso strade alternative totalmente diverse, dalla programmazione di driver video proprietari o tecniche di api hooking. La riscrittura di driver grafici è una scelta troppo onerosa dal punto di vista delle risorse umane e dei costi aggiuntivi di cui si avrebbe bisogno per supportare l’ampia gamma di piattaforme e versioni del sistema operativo presenti sul mercato, per cui si è deciso di fare uso delle tecniche di api hooking. Le tecniche di api hooking, in generale, consistono nell’iniezione forzata di una libreria all’interno di un processo mentre è in esecuzione, per alterare parti di codice a runtime e modificarne il comportamento. Questo ci permette di dirottare le funzioni di DirectX verso funzioni proprietarie senza far ricorso a tecniche di library interposition. 5 Progetto 3DCloudViz Deliverable D3 Hook server process Di certo una tecnica molto popolare per l’iniezione di dll all’interno di un processo risiede nell’api di hooking fornita da Windows. Come descritto nella guida MSDN, un hook è un “trucco” nel meccanismo per la gestione dei messaggi di sistema. Un’applicazione può installare una funzione filtro proprietaria per monitorare il traffico dei messaggi del sistema e processare certi tipi di messaggi prima che raggiungano la procedura della finestra di interesse. Un hook è normalmente implementato all’interno di una dll al fine di incontrare i requisiti di base per un campo d’azione a system-wide. Il concetto che sta alla base di questi hook è che la procedura di callback dell’hook viene eseguita nello spazio di indirizzamento di ciascun processo che viene agganciato all’interno del sistema. Per installare un hook è necessario chiamare la funzione SetWindowsHookEx() che, con i giusti parametri, riesce a installare un hook per il tracciamento dei messaggi per la gestione degli eventi del sistema. Una volta che il processo installa un system-wide hook, il sistema operativo mappa la dll all’interno dello spazio di indirizzamento di ciascuno dei suoi processi client. Dunque le variabili globali all’interno della dll saranno “perprocesso” e non protranno essere condivise tra i processi che hanno caricato l’hook della dll. Tutte le variabili che contengono dati condivisi devono essere rimpiazzate nella sezione dei dati condivisi. Il diagrama di seguito mostra un esempio di come un hook venga registrato dall’hook server e iniettato all’interno dello spazio di indirizzamento chiamato “Application one” e “Application two”: Un system-wide hook è registrato soltanto una volta quando viene eseguita una chiamata a SetWindowsHookEx(). Se non si verifica alcun errore, viene ritornato un handle all’hook. Il valore di ritorno è richiesto alla fine dell’hook proprietario quando è necessario effettuare una chiamata a CallNextHookEx() per garantire il funzionamento del sistema. Se la chiamata a SetWindowsHookEx() ha avuto buon esito, allora il sistema operativo inietta la dll automaticamente all’interno di tutti i processi che incontrano i requisiti per questo particolare tipo di filtro, per come descritto al momento della chiamata. Ad esempio, un hook installato per il messaggio WH_GETMESSAGE avrebbe il seguente codice: LRESULT CALLBACK GetMsgProc(int code, // hook code WPARAM wParam, // removal option LPARAM lParam) // message { // We must pass the all messages on to CallNextHookEx. return ::CallNextHookEx(sg_hGetMsgHook, code, wParam, lParam); } 6 Progetto 3DCloudViz Deliverable D3 Un system-wide hook è caricato da più processi alla volta che non condividono lo stesso spazio di indirizzamento. Ad sempio, l’handle all’hook sg_hGetMsgHook, che è ottenuto da SetWindowsHookEx() ed è usato come parametro nella CallNextHookEx() dev’essere usato virtualmente in tutti gli spazi di indirizzamento. Questo significa che il suo valore dev’essere condiviso tra i processi agganciati e l’applicazione che fa da server di hook. Al fine di rendere questa variabile visibile a tutti i processi dobbiamo memorizzarla nella sezione dei dati condivisi. Una volta che la dll di hook è caricata dentro lo spazio di indirizzamento del processo di interesse, non c’è alcun modo per scaricarla fino a quando non viene chiamata una UnhookWindowsHookEx() dal server di hook o l’applicazione del server di hook viene terminata. Quando un server di hook chiama UnhookWindowsHookEx() il sistema operativo itera attraverso una lista interna con tutti i processi che sono stati forzati a caricare la dll di hook. Il sistema operativo decrementa il lock count della dll e quando raggiunge lo zero, la dll viene automaticamente rimossa dello spazio di indirizzamento del processo. Alcuni vantaggi nell’usare questo approccio: ● ● ● Questo meccanismo è supportato dalla famiglia NT/2K e 9x Windows e si spera che sarà mantenuto nelle future versioni di Windows. Questo metodo fornisce un sistema per sganciare le dll quando il server di hook decide che non sono più richieste, con una chiamata a UnhookWindowsHookEx() Tuttavia, anche se possiamo considerare questo sistema di hook estremamente utile, ci sono degli svantaggi che ci hanno costretti a non utilizzarlo: ● I windows hook possono degradare in modo significativo le performance dell’intero sistema, visto che incrementano il numero di processamenti che il sistema deve performare per ciascun messaggio. ● La SetWindowsHookEx ha effetto se il processo chiamante giace nello stesso desktop dei processi dentro i quali si vogliono installare gli hook. Questo significa che è necessario chiudere e riaprire il processo ogni volta che si effettua un cambiamento di desktop su diverse sessioni utente, o lanciare un processo in ascolto per ogni sessione utente. Tutto questo non fa che peggiorare le performance. ● La SetWindowsHookEx installa hook solo per monitorare gli eventi legati ai messaggi all’interno dello stesso desktop. Esiste una concreta possibilità che un’applicazione carichi e usi DirectX prima di aver lanciato un solo messaggio, per cui questa tecnica si rivela nel complesso instabile per una interception completa e funzionale delle DirectX su tutti i processi. L’ultimo punto è lo svantaggio più enorme perché non ci permette di avere la totale garanzia che il nostro hook potrà funzionare sui processi che usano DirectX prima ancora di aver aperto una finestra e generato un solo messaggio del desktop, fatto che è ampiamente probabile visto che non è affatto necessario che vi sia una finestra per creare un oggetto directx qualsiasi, ma solo il device per il rendering. Per questo motivo, abbiamo deciso di abbandonare definitivamente questo approccio per tentare altre strade. Registro AppInit_DLLs E’ possibile iniettare i processi che linkano la user32.dll aggiungendo il path della libreria all’interno della chiave di registro: HKEY_LOCAL_MACHINE \ Software \ Microsoft \ Windows NT \ CurrentVersion \ Windows \ AppInit_DLLs 7 Progetto 3DCloudViz Deliverable D3 Il valore di questa chiave può contenere il path di una singola dll da iniettare o più dll separate da spazi o da virgole. Questo metodo non presenta alcun problema e sembra iniettare tutti i processi, a patto che linkino la user32.dll, ma non esiste una sola applicazione DirectX che non lo faccia immediatamente all’avvio. L’unico problema protrebbe essere l’iniezione di alcuni processi di vitale importanza per il sistema che potrebbero degradare le performance o che non vi è alcun interesse nel tracciarli, per questo si può facilmente evitare di installare un hook aggiungendo i processi vietati in una black list. Ma è un problema ben piccolo se consideriamo che l’hook funziona sempre a prescindere dai messaggi di sistema, diversamente dalla SetWindowsHookEx, e non esistendo alcuna alternativa valida per il nostro caso d’uso, abbiamo deciso di utilizzare questo approccio in via definitiva. Una volta iniettata la dll, è necessario intercettare le funzioni da wrappare e installare un hook che esegua delle parti di codice alternative al posto della funzione di interesse. Abbiamo effettuato un’analisi approfondita delle maggiori tecniche per poter raggiungere lo scopo, di cui di seguito ne riportiamo i fatti salienti con le scelte che sono state effettuate per il nostro caso d’uso. Analisi di varie strategie per la Function interception per l’API DirectX 1.3 Le tecniche di function interception devono alterare in qualche modo le istruzioni e le tabelle dell’applicazione per fare in modo che la chiamata della funzione di interesse venga dirottata in funzione alternativa che è stata iniettata all’interno del processo e che possiamo programmare a nostro piacimento per ottenere il comportamento desiderato. L’alterazione generica dell’applicazione per il dirottamento di un metodo è nota anche col termine di hook, che si può installare in due modi: - Modificando la IAT. Ogni applicazione o dll ha un header noto col termine di Portable Executable (PE) dentro il quale viene memorizzata una Import Address Table (IAT) con gli indirizzi di tutte le funzioni importate dai moduli utilizzati dall’applicazione (i.e. d3d9.dll). L’installazione dell’hook consiste nel trovare la posizione nella tabella dov’è contenuto l’indirizzo della funzione di interesse e sostituirlo con l’indirizzo della funzione alternativa. Questa tecnica ovviamente vale solo per i metodi importati da moduli esterni e non per le funzioni contenute all’interno dell’applicazione. - Modificando le istruzioni assembly. E’ necessario sovrascrivere i primi 6 byte della funzione di interesse con il seguente codice assembly: - JMP addressDistance - RET Questa alterazione delle istruzioni farà in modo che alla chiamata della funzione di interesse avvenga un salto diretto alla funzione alternativa. All’interno della funzione alternativa, se ci interesserà eseguire nuovamente la funzione di interesse, dovremo fare in modo di ripristinare i vecchi 6 bytes di codice altrimenti l’alterazione si tradurrebbe nell’esecuzione di un loop infinito, causando il crash dell’intero processo. La manutenzione di un sistema custom per la function interception nasconde molte insidie, tra cui l’attivazione/disattivazione degli hook per evitare l’insorgere di loop infiniti, il supporto dei processi a 64 bit ed il multithreading, problemi che se non gestiti nel migliore dei modi possono tradursi in crash fatali e compromettere l’intera stabilità del sistema se l’installazione degli hook viene effettuata in tutti i processi del sistema operativo. Abbiamo preferito dunque rivolgerci a librerie già esistenti per l’api hooking, che siano robuste, multithreading, license free e supportino tutti i tipi di processi, a 32 e 64 bit. Easyhook vs. Detours Per un hook robusto dei metodi abbiamo preferito rivolgerci a una libreria di Api Hooking già esistente e non implementare in assembly gli hook delle funzioni, andando da caso a caso, per evitare sul nascere problemi di instabilità e di compatibilità con i diversi tipi di architettura. Tuttavia non è stata una scelta semplice perchè, oltre ad essere un problema poco affrontato in letteratura, una libreria in grado di supportare un wrapping importante 8 Progetto 3DCloudViz Deliverable D3 come quello delle directx per tutte le applicazioni grafiche del sistema, oltre ad essere potente e stabile, deve soddisfare i seguenti requisiti: ● ● ● Permettere di iniettare il codice in processi a 32 e 64 bit Installare e rimuovere tutti gli hook dal processo, in modo stabile e su più threads Godere di una licenza gratuita La Microsoft mette a disposizione una libreria di api hooking chiamata Detours che è piuttosto decente in termini di stabilità. Tuttavia questa libreria soffre di serie limitazioni in termini di licenza. Detours Express gode di una licenza gratuita per progetti di ricerca non commerciali, tuttavia è limitata alla versione a 32 bit per processori x86. Se si vogliono supportare anche i processi a 64 bit (come nel nostro caso) è necessario rivolgersi a Detours Professional che include una licenza per scopi commerciali e il supporto per tutti i processori 32 e 64 bit windows compatibili presenti sul mercato alla “modica” cifra di 10000 dollari. Peggio ancora, l’ultimo aggiornamento della libreria Detours risale al lontano 2006, per cui alla luce dello sviluppo tecnologico avvenuto nel frattempo è difficile capire quanto riesca ad essere affidabile con gli ultimi processori a 64 bit usciti sul mercato. Per questo motivo, abbiamo deciso di abbandonare Detours per rivolgerci a una libreria molto simile chiamata Easyhook, che oltre ad essere rilasciata sotto licenza LGPL sembra godere di un’ottima stabilità per entrambi i processi a 32 e 64 bit su tutti i processori che abbiamo avuto modo di testare, può essere usata per progetti commerciali ed è ancora mantenuta in vita dai suoi sviluppatori. Detto questo possiamo dare un’occhiata ai metodi che servono per installare l’hook delle funzioni, di cui due sono i più importanti: ● LhInstallHook: Installa un hook per l’entry point di interesse, reindirizzando tutte le chiamate al metodo dell’hook che si sta per installare. Come valore di ritorno di ha un handle che verrà rilasciato all’unload della libreria o esplicitamente con una chiamataa LhUninstallHook(). EASYHOOK_NT_EXPORT LhInstallHook(void void void TRACED_HOOK_HANDLE ● *InEntryPoint, *InHookProc, *InCallback, OutHandle); LhSetExclusiveACL: Rende attivo un hook ACL locale esclusivo sulla base di una lista di thread IDs fornita in ingresso. Viene sempre effettuata un’intersezione tra gli insiemi di ACL locali e globali. Se ad esempio si passa una lista vuota, nessun thread sarà escluso dall’hooking della funzione. Questo metodo è utile per avere un controllo degli hook su più thread in parallelo. EASYHOOK_NT_EXPORT LhSetExclusiveACL(ULONG* ULONG TRACED_HOOK_HANDLE InHandle); InThreadIdList, InThreadCount, Component Object Model Tutte le api di DirectX sono basate su un set astratto di interfacce che appartiene al Component Object Model, per cui la costruzione del wrapper dovrà catturare e dirottare le chiamate contenute all’interno di questi tipi di oggetti. Tutto questo, ovviamente, rende la costruzione di un wrapper DirectX più complessa e laboriosa rispetto a OpenGL ed una conoscenza approfondita del Component Object Model. Il concetto di fondo è che l’intero software è costruito tramite un’impalcatura modulare di componenti COMcompatibili. I differenti tipi di componenti sono identificati da diversi class IDs (CLSIDs) in modo univoco e globale, con il termine più ricorrente di Globally Unique Identifiers (GUIDs). I GUID sono memorizzati come valori a 128 bit e possono essere rappresentati con 32 cifre esadecimali in gruppi separati da trattini (i.e. 21EC2020 - 3AEA - 1069 - A2DD - 08002B30309D). Ciascun componente espone le sue funzionalità attraverso 9 Progetto 3DCloudViz Deliverable D3 una o più interfacce. Le diverse interfacce supportate da un componente sono distinte l’una dall’altra usando degli ID di Interfaccia (IIDs), che sono anche dei GUIDs. Interfacce Tutti i componenti COM implementano un’interfaccia IUnknown che espone i metodi QueryInterface, AddRef e Release: ● QueryInterface consente al chiamante di ottenere i reference a diversi tipi di interfaccia implementate dal componente. E’ simile al dynamic_cast<> del C++ oppure i cast usati in Java o C#. Nello specifico, è usato per ottenere un puntatore a un’altra interfaccia dato un GUID che identifica univocamente quel tipo di interfaccia. Se il componente non implementa quel tipo di interfaccia, allora verrà restituito in uscita un errore di tipo E_NOINTERFACE ● AddRef è usato per incrementare il reference count quando un nuovo client sta acquisendo l’oggetto. In uscita viene restituito il valore del nuovo reference count. ● Release è usato per decrementare il reference count quando il client ha finito di usare l’oggetto. In uscita viene restituito il valore del nuovo reference count. Il componente verrà cancellato quando il reference count avrà raggiunto lo zero. interface IUnknown { virtual HRESULT QueryInterface (REFIID riid, void **ppvObject) = 0; virtual ULONG AddRef () = 0; virtual ULONG Release () = 0; }; L’ID dell’interfaccia IUnknown interface ID è definita come un GUID col valore di {00000000 - 0000 - 0000 C000 - 000000000046}. Le interfacce del componente sono richieste per esibire proprietà riflessiva, transitiva e simmetrica. La proprietà riflessiva si riferisce all’abilità di un’interfaccia di restituire un’istanza alla stessa interfaccia tramite una chiamata QueryInterface. La proprietà simmetrica richiede che quando un’interfaccia B è ricavata da un’interfaccia A attraverso la QueryInterface, allo stesso modo è possibile ricavare un’interfaccia A da un’interfaccia B. La proprietà transitiva richiede che se è possibile ottenere un’interfaccia B da un interfaccia A e un’interfaccia C da un’interfaccia B, allora è possibile ottenere un’interfaccia C da un’interfaccia A. Classi Una classe COM (coclass) è l’implementazione concreta di una o più interfacce e rispecchia le classi di un qualsiasi lunguaggio di programmazione orientato agli oggetti. Le classi sono create utilizzando i rispettivi class ID oppure le rispettive stringhe di identificatori programmatici (progid). Come molti altri linguaggi orientati agli oggetti, COM fornisce un criterio di separazione tra interfacce e implementazione. Questa distinzione è specialmente forte nel COM, dove non è possibile accedere direttamente agli oggetti ma solo attraverso le loro interfacce. COM ha anche il supporto per implementazioni multiple della stessa interfaccia, così che i clienti possono scegliere a runtime l’implementazione da istanzare. Reference counting 10 Progetto 3DCloudViz Deliverable D3 Tutti gli oggetti COM utilizzano un reference counting per gestire la vita degli oggetti. I reference count sono controllati dai client attraverso l’uso dei metodi AddRef e Release implementati all’interno dell’interfaccia IUnknown dalla quale derivano tutti gli altri oggetti. Gli oggetti COM sono dunque responsabili per il rilascio della loro stessa memoria quando il reference count avrà raggiunto lo zero. In definitiva, possiamo dire che è necessario chiamare AddRef o Release solo nei seguenti casi: ● Funzione e metodi che ritornano riferimenti a interfaccia (via valore di ritorno o via parametri di output passati come doppio puntatore) possono incrementare il reference count dell’oggetto di ritorno. ● La Release dev’essere chiamata sul puntatore a un’interfaccia prima che il puntatore è sovrascritto o viene perso in altro modo. ● Se una copia è fatta su un puntatore che fa riferimento a un’interfaccia, allora una AddRef dev’essere chiamata su quel puntatore. ● AddRef e Release devono essere chiamate su una specifica interfaccia a cui viene fatto riferimento in quanto un oggetto può implementare il numero di riferimenti per interfaccia al fine di allocare le risorse interne solo per le interfacce che vengono referenziate. Tutte le chiamate al reference count non sono inviate a un oggetto remoto attraverso il wire; un proxy tiene solo un riferimento a un oggetto remoto e mantiene il suo reference count locale. Wrapping dei componenti Per la costruzione di un wrapper DirectX, è necessario eseguire lo wrapping delle interfacce dei componenti programmando delle interfacce identiche che si preoccupano di istanziare e chiamare le funzioni delle interfacce originali (Interface Wrapping), oppure dirottare le funzioni delle interface facendo un hook dei puntatori a funzione delle rispettive vtables (Hooking VTable function addresses). Come vedremo, entrambi i metodi rappresentano due tecniche diverse con i rispettivi pro e contro. Interface wrapping Tipicamente nelle DirectX9 la maggior parte delle chiamate vengono eseguite nel contesto di un componente con interfaccia IDirect3D9, tramite il quale è poi possibile enumerare i display, creare i device directx e tutto il resto. Per poter wrappare tutte le interfacce di interesse è necessario dunque wrappare l’interfaccia IDirect3D9 sostituendo l’unica funzione utilizzata per istanziarla, ossia la Direct3DCreate9. Il miglior approccio per farlo consiste nell’installazione di un hook per il metodo Direct3DCreate9 non appena la d3dhook.dll è stata iniettata dal sistema operativo alla partenza del processo e dunque prima che la funzione venga eseguita per la prima volta: BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: installProcessHooks(); break; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: 11 Progetto 3DCloudViz Deliverable D3 break; } return TRUE; } Questo procedimento nasconde un’insidia: anche se dobbiamo fare l’hook della Direct3DCreate9 non appena il processo viene fatto partire, non possiamo fare l’hook della funzione se il processo non ha caricato la d3d9.dll per la prima volta, è dunque importante capire quando ciò avviene. Tipicamente in un processo è possibile linkare le librerie dinamiche in due modi: Link statico a libreria dinamica. Le chiamate ad un modulo esterno vengono incluse a build time nella import address table (IAT) del portable executable (PE) dell’applicazione. Questo significa che quando la nostra d3dhook.dll viene iniettata all’interno del processo, la d3d9.dll è già presente ed è possibile installare l’hook della Direct3DCreate9. Appena la d3dhook.dll è iniettata, è necessario eseguire un check della d3d9.dll tramite una GetModuleHandle ed installare l’hook della Direct3DCreate9, altrimenti bisogna procedere in modo diverso per coprire il secondo caso. static void checkDirectXHooks() { dcv::Mutex &hookDirectXMutex = getHookDirectXMutex(); hookDirectXMutex.lock(); HMODULE hMod = GetModuleHandle("d3d9.dll"); if (hMod && !hModD3d9) { installDirectX9Hooks(hMod); } hookDirectXMutex.unlock(); } Link dinamico a libreria dinamica. Il modulo d3d9.dll può essere caricato a runtime in un momento qualsiasi durante la vita del processo tramite una chiamata alla funzione LoadLibrary. In realtà si potrebbe rimediare caricando la d3d9.dll alla partenza di tutti i processi del sistema e facendo un hook della Direct3DCreate9, tuttavia non è possibile perchè il caricamento delle DirectX9 per tutti i processi è troppo dispendioso e rischierebbe di compromettere la stabilità del sistema in modo serio. E’ necessario dunque introdurre un criterio intelligente per intercettare l’istante in cui viene caricata la d3d9.dll che sia leggero e non comprometta il corretto funzionamento del sistema operativo. Il metodo più stabile da noi testato finora consiste nell’hook immediato della LoadLibrary non appena viene iniettata la libreria, visto che tale funzione è sempre presente al momento dell’iniezione. Con un hook della LoadLibrary possiamo metterci in ascolto di tutti i moduli che vengono caricati in seguito e installare l’hook della Direct3DCreate9 non appena si verifica il caricamento della d3d9.dll, per la prima volta: HMODULE WINAPI dcv_LoadLibraryA(LPCSTR lpFileName) { HMODULE hMod = LoadLibraryA(lpFileName); checkDirectXHooks(); //check if d3d9.dll is loaded and hook methods return hMod; } HMODULE WINAPI dcv_LoadLibraryW(LPWSTR lpFileName) { HMODULE hMod = LoadLibraryW(lpFileName); 12 Progetto 3DCloudViz Deliverable D3 checkDirectXHooks(); //check if d3d9.dll is loaded and hook methods return hMod; } static bool installProcessHooks() { if (!initEasyHook()) { return false; } resetDirectXHooks(); checkDirectXHooks(); //check if d3d9.dll is loaded and hook methods ULONG ACLEntries[1] = {0}, nf; hModKernel32 = GetModuleHandle(TEXT("kernel32.dll")); if (hModKernel32) { nf = 0; sys_LoadLibraryA = (LoadLibraryAPROC)GetProcAddress(hModKernel32, "LoadLibraryA"); if (sys_LoadLibraryA) { hHookSys[nf] = new HOOK_TRACE_INFO(); sys_LhInstallHook(sys_LoadLibraryA, dcv_LoadLibraryA, (PVOID)0x12345678, hHookSys[nf]); sys_LhSetExclusiveACL(ACLEntries, 0, hHookSys[nf]); nf++; } sys_LoadLibraryW = (LoadLibraryWPROC)GetProcAddress(hModKernel32, "LoadLibraryW"); if (sys_LoadLibraryW) { hHookSys[nf] = new HOOK_TRACE_INFO(); sys_LhInstallHook(sys_LoadLibraryW, dcv_LoadLibraryW, (PVOID)0x12345678, hHookSys[nf]); sys_LhSetExclusiveACL(ACLEntries, 0, hHookSys[nf]); nf++; } } return true; } Una volta installato l’hook della Direct3DCreate9, possiamo ritornare un’istanza della nostra implementazione di IDirect3D9 che wrappa l’originale e che contiene come membro un’istanza dell’interfaccia originale per poter eseguire le chiamate native: // dcvIDirect3D9.h class dcvIDirect3D9 : public IDirect3D9 { public: dcvIDirect3D9(IDirect3D9 *pOriginal); 13 Progetto 3DCloudViz Deliverable D3 virtual ~dcvIDirect3D9(void); HRESULT __stdcall QueryInterface(REFIID riid, void **ppvObj); ULONG __stdcall AddRef(void); ULONG __stdcall Release(void); HRESULT __stdcall RegisterSoftwareDevice(void *pInitializeFunction); [...] private: IDirect3D9 *m_pIDirect3D9; }; // dcvIDirect3D9.cpp dcvIDirect3D9::dcvIDirect3D9(IDirect3D9 *pOriginal) { m_pIDirect3D9 = pOriginal; } dcvIDirect3D9::~dcvIDirect3D9(void) { } HRESULT __stdcall dcvIDirect3D9::QueryInterface(REFIID riid, void **ppvObj) { *ppvObj = NULL; HRESULT hRes = m_pIDirect3D9->QueryInterface(riid, ppvObj); if (hRes == NOERROR) { *ppvObj = this; } return hRes; } ULONG __stdcall dcvIDirect3D9::AddRef(void) { return m_pIDirect3D9->AddRef(); } ULONG __stdcall dcvIDirect3D9::Release(void) { ULONG count = m_pIDirect3D9->Release(); if (count == 0) { delete(this); } return count; } HRESULT __stdcall dcvIDirect3D9::RegisterSoftwareDevice(void *pInitializeFunction) { return m_pIDirect3D9->RegisterSoftwareDevice(pInitializeFunction); } Questo ci permette di intercettare la chiamata IDirect3D9::CreateDevice che crea a sua volta un device con interfaccia IDirect3DDevice9 così da poter passare la nostra interfaccia custom come già fatto per IDirect3D9 e 14 Progetto 3DCloudViz Deliverable D3 così via. Questo procedimento, eseguito in maniera attenta e metodica, ci permette di wrappare tutte le interfacce DirectX ed averne il totale controllo: HRESULT __stdcall dcvIDirect3D9::CreateDevice(UINT Adapter, D3DDEVTYPE DeviceType, HWND hFocusWindow, DWORD BehaviorFlags, D3DPRESENT_PARAMETERS *pPresentationParameters, IDirect3DDevice9 **ppReturnedDeviceInterface) { dcvIDirect3DDevice9 *pdcvIDirect3DDevice9; IDirect3DDevice9 *pIDirect3DDevice9_ = NULL; HRESULT hres = m_pIDirect3D9->CreateDevice(Adapter, DeviceType, hFocusWindow, BehaviorFlags, pPresentationParameters, &pIDirect3DDevice9_); pdcvIDirect3DDevice9 = new dcvIDirect3DDevice9(pIDirect3DDevice9_); *ppReturnedDeviceInterface = pdcvIDirect3DDevice9; return hres; } Il vantaggio nell’utilizzo di questa tecnica sta nella facilità di inclusione di risorse aggiuntive per interfaccia, che possono essere incluse come membri al suo interno, inizializzate alla chiamata del costruttore e rilasciate alla chiamata del distruttore. Uno svantaggio enorme, invece, sta proprio nel dover tenere traccia di tutte le funzioni COM che istanziano nuove interfacce o restituiscono interfacce già istanziate, incrementando o decrementando il reference count, nel rispetto della proprietà transitiva, riflessiva e simmetrica delle interfacce. In questo caso, la complessità dell’interface wrapping è direttamente proporzionale alla complessità dell’architettura COM della libreria da wrappare (i.e. DirectX10 e 11 sono di gran lunga più complesse di DirectX9). Bisogna dunque prevedere tutte le combinazioni possibili e immaginabili di interface wrapping previste dalla libreria, così da poter passare le interfacce giuste al sistema quando richiesto, evitando di perdere la traccia dell’hook o addirittura di crashare l’intero processo. Questo metodo, seppur complesso e difficile da mantenere, è l’unica strada che può essere percorsa per il wrapping dell’intera libreria DirectX9/10/11, per la serializzazione e l’invio di tutte le chiamate da un client ad un rendering server esterno. Tuttavia, con l’avvento delle vGPU ed il pieno supporto delle DirectX9/10/11 su macchina virtuale, si è deciso di abbandonare questo approccio in quanto potenzialmente instabile, enorme e complesso da mantenere alla luce dell’esistenza di metodi già esistenti e ben testati, come appunto quelli implementati dai driver vGPU (nei capitoli successivi vedremo un’analisi più approfondita del problema). Si è deciso dunque di utilizzare una tecnica alternativa che permette di arrivare direttamente alla chiamata Present(), per catturare lo schermo renderizzato tramite vGPU e inviarlo al client, senza dover tenere traccia di tutte le interfacce intermedie. Hooking VTable function addresses Questo approccio prevede l’uso della Virtual Method Table (VTable) di un’instanza di IDirect3DDevice9 per determinare l’indirizzo di ciascun metodo virtuale così da poterli dirottare in funzioni alternative installando un hook sui rispettivi puntatori a funzione. Per accedere alla vtable abbiamo bisogno di creare una istanza temporanea di IDirect3DDevice9 poco prima di installare gli hook, e dunque creare un dummy device directx9 che non fa praticamente nulla se non regalarci l’istanza di cui abbiamo bisogno: IDirect3D9 *pIDirect3D9 = Direct3DCreate9_fn(D3D_SDK_VERSION); IDirect3DDevice9 *pIDirect3DDevice9; ZeroMemory(&dxPresentParams, sizeof(dxPresentParams)); dxPresentParams.BackBufferCount = 1; 15 Progetto 3DCloudViz Deliverable D3 dxPresentParams.MultiSampleType = D3DMULTISAMPLE_NONE; dxPresentParams.MultiSampleQuality = 0; dxPresentParams.SwapEffect = D3DSWAPEFFECT_DISCARD; dxPresentParams.hDeviceWindow = hWnd; dxPresentParams.Flags = 0; dxPresentParams.FullScreen_RefreshRateInHz = D3DPRESENT_RATE_DEFAULT; dxPresentParams.PresentationInterval = D3DPRESENT_INTERVAL_DEFAULT; dxPresentParams.BackBufferFormat = D3DFMT_X8R8G8B8; dxPresentParams.EnableAutoDepthStencil = FALSE; dxPresentParams.Windowed = TRUE; pIDirect3D9->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd, D3DCREATE_HARDWARE_VERTEXPROCESSING, &dxPresentParams, &pIDirect3DDevice9); Da qua in poi bisogna semplicemente ricavare il puntatore alla vtable e scorrere la lista per ottenere i puntatori ai metodi. Per ottenere il giusto puntatore alle funzioni per le quali si intende installare l’hook, è necessario conoscere l’ordine esatto delle funzioni all’interno della vtable e gli argomenti da passare, che sono deducibili dalla dichiarazione dell’interfaccia all’interno dell’header della libreria: uintptr_t* pInterfaceVTable = (uintptr_t*)*(uintptr_t*)pIDirect3DDevice9; Present9_fn = (Present9_Type)pInterfaceVTable[17]; TRACED_HOOK_HANDLE hHook = new HOOK_TRACE_INFO(); ULONG ACLEntries[1] = {0}; LhInstallHook_fn(Present9_fn, dcvPresent9, (PVOID)0x12345678, hHook); LhSetExclusiveACL_fn(ACLEntries, 0, hHook); Visto che stiamo parlando delle funzioni virtuali di un oggetto, è necessario aggiungere un primo argomento a tutti gli altri argomenti che è un puntatore all’oggetto (IDirect3DDevice9 *) per il quale si effettua la chiamata: HRESULT STDMETHODCALLTYPE dcvPresent9(IDirect3DDevice9 *pIDirect3DDevice9, CONST RECT* pSourceRect, CONST RECT* pDestRect, HWND hDestWindowOverride, CONST RGNDATA* pDirtyRegion) { D3DDEVICE_CREATION_PARAMETERS cparams; pIDirect3DDevice9->GetCreationParameters(&cparams); dcvDirect3D9Readback(pIDirect3DDevice9, hDestWindowOverride ? hDestWindowOverride : cparams.hFocusWindow, pSourceRect, pDestRect, pDirtyRegion); return Present9_fn(self, 16 Progetto 3DCloudViz Deliverable D3 pSourceRect, pDestRect, hDestWindowOverride, pDirtyRegion); } Questa tecnica è efficace se si intende wrappare solo un set limitato di funzionalità DirectX. Al contrario, per il wrapping dell’intera libreria DirectX9 e la serializzazione delle chiamate da inviare a un rendering server esterno, si rivela totalmente inutile per i seguenti motivi: - L’installazione di un hook per ogni funzione della vtable di tutte le interfacce è artificiosa e compromette la stabilità del sistema. - Non è possibile installare l’hook di funzioni appartenenti a interfacce che non sono supportate sulla macchina virtuale. Se le directx sono assenti o con supporto parziale, l’intero api hooking rischia di non funzionare. - Non è possibile creare e passare interfacce custom, requisito fondamentale se si intende supportare il rendering esterno con features e interfacce non supportate nella macchina virtuale, inclusa la totale assenza delle directx. Tuttavia, questa tecnica si rivela particolarmente robusta, efficace e facile da mantenere nel caso in cui si intenda catturare lo schermo di un’applicazione che sta girando su macchina fisica o macchina virtuale con vGPU, per poi comprimerlo e inviarlo a un client che faccia da endstation in un sistema di remotizzazione. Più avanti vedremo come, assieme alla facilità di manutenzione di un sistema leggero e facile da mantenere, unito allo sviluppo di nuove tecnologie emergenti per la remotizzazione tipo NVIDIA GRID e vGPU, si è preferito usare questo tipo di approccio optando per una macchina virtuale (Guest) che tramite vGPU virtualizza le DirectX9/10/11 in una macchina fisica (Host), che cattura e invia le immagini compresse via gpu ad un client (Endstation), con un comportamento tuttosommato simile a quello di opengl con rendering server esterno e dei costi di manutenzione di gran lunga inferiori, visto che la virtualizzazione di DirectX in questo caso è implementata e mantenuta dai programmatori di NVIDIA. 1.4 Limitazioni dell’approccio tramite Library Injection Per quanto la library injection si sia rivelata la miglior scelta per il wrapping di DirectX fino a questo momento, la tecnica presenta delle limitazioni che farebbero preferire delle strade alternative, se solo fosse possibile averne. Un problema riguarda il modo in cui le funzioni del wrapper possono chiamare le funzioni della libreria originale. Abbiamo già detto che è necessario disattivare e riattivare l’hook della funzione qualora si intenda eseguire la funzione originale, e questo già va a discapito delle performance, ma non è finita. E’ possibile che una funzione debba chiamare altre funzioni senza passare dai rispettivi wrapper, per cui è necessario disattivare l’hook per queste funzioni, senza toccare gli altri thread che verrebbero penalizzati. Questo lavoro di attivazione/disattivazione e sincronizzazione per ogni chiamata si traduce in un lieve calo di performance. In questo caso il calo è solo lieve perché le funzioni di cui fare hook sono poche visto che la maggior parte delle chiamate sono gestite da componenti COM, ma nel caso in cui le chiamate fossero centinaia, o addirittura migliaia, la tecnica non sarebbe affatto da utilizzare se le perfomance sono un punto critico. Una limitazione, invece, risiede proprio nei principi che stanno alla base dell’injection e dell’api hooking. L’alterazione del comportamento delle applicazioni non è una pratica ben vista dal sistema operativo per scopi che esulino dal debugging delle applicazioni, e potrebbe essere intercettata da alcuni antivirus come un tentativo di infezione del sistema. Le librerie iniettate, oltre ad alterare il comportamento dei processi, li appesantisticono con parti di codice che la maggior parte delle volte sono inutili, se i processi iniettati non sono applicazioni da remotizzare. Per questo 17 Progetto 3DCloudViz Deliverable D3 motivo si è deciso di fare uso di una blacklist, per evitare quanto meno l’iniezione indesiderata dei processi da non remotizzare, le cui performance e stabilità rappresentano un punto critico per il funzionamento dell’intero sistema. Tuttavia è impossibile stilare una blacklist che minimizzi l’utilizzo di injection in tutti i casi possibili e immaginabili, per cui la maggior parte delle volte questa tecnica rappresenta uno spreco sensibile di risorse. Un altro problema riguarda la rimozione delle librerie iniettate. Diversamente dal metodo che fa uso di SetWindowsHookEx che prevede un sistema automatico di rimozione, nel nostro caso è un requisito praticamente impossibile da raggiungere, per i seguenti motivi: ● non abbiamo modo di segnalare ai processi in funzione l’istante in cui è necessario espellere la libreria iniettata. Si potrebbero usare gli eventi globali del sistema, ma un loop che si mette in ascolto e rilascia per ogni processo, oltre ad essere letale in termini di stabilità, promette di degradare le performance dell’intero sistema. ● nel caso in cui l’applicazione stia facendo ancora uso di DirectX, non è possibile deiniettare il wrapper, pena il crash istantaneo del processo. Si dovrebbero implementare delle funzioni bloccanti che controllano per ogni chiamata, su ogni thread del processo, se è pervenuto un segnale di deinjection. Tuttavia, oltre al degrado di performance, per deiniettare correttamente il wrapper si dovrebbe avere la certezza che tutte le attività interne siano state portate a termine. Tutto questo si traduce in problemi di stabilità e calo di performance che non possiamo permetterci. Per colpa di questi problemi, i processi sono costretti a restare iniettati per tutta la loro vita. Questo significa che la rimozione del wrapper per un’eventuale disinstallazione o upgrade non può essere eseguito se non programmando degli script di installer che prevedono almeno un reboot del sistema. 1.5 Analisi di fattibilità del supporto per External Rendering Sebbene riportato come ultimo punto tra i requisiti funzionali, abbiamo deciso di esplorare all’inizio del progetto la fattibilità del supporto all’External Rendering, in quanto tale supporto influenza in modo strutturale le scelte implementative. NICE DCV supporta la delega del rendering OpenGL ad una macchina Linux definita Rendering Server. NICE DCV si occupa di serializzare e inviare al rendering server lo stream di comandi OpenGL; il rendering server si occupa di eseguire il rendering dell’immagine facendo uso della GPU e di inviare il risultato al client, che ha poi il compito di comporre l’immagine OpenGL renderizzata con il desktop remoto che viene ricevuto direttamente dall’host Windows. Come visto in precedenza, le DirectX non sono strutturate su un’interfaccia di comandi ma di componenti COM. In questo caso la situazione si complica, perché oltre alla serializzazione dello stream dei comandi per il rendering, è necessario gestire la creazione ed il rilascio dei componenti COM sia lato client, sia lato server. E’ un lavoro molto oneroso se si considera: ● Ogni componente implementa una o più interfacce che devono essere wrappate e devono rispettare la proprietà riflessiva, simmetrica e transitiva. ● Ogni singolo oggetto delle DirectX (device, adapter, rendering target, texture, surface, swap chain, etc...) è stato implementato come un componente COM, con una o più interfacce. ● Esistono funzioni che manipolano il reference count dei componenti COM. Per ottenere un comportamento corretto della libreria, bisognerebbe andare da caso a caso e implementare il corretto reference counting. 18 Progetto 3DCloudViz ● Deliverable D3 Diversamente da OpenGL, le DirectX sono multithreading con più contesti sullo thread o su più thread in parallelo. Questo aggiunge una complicazione in più. Un’applicazione di solito genera migliaia di chiamate per i vari QueryInterface e la gestione del reference counting, visto che i componenti COM sono molteplici e sono svariati i metodi per poterli gestire. Serializzare le chiamate dirette di ciancun componente COM farebbe precipitare verso il basso le performance dell’applicazione e finirebbe per saturare il network. Per ovviare a tutto questo, si dovrebbe escogitare uno strato di astrazione che faccia da intermediario tra il client e il wire sull’application host, e tra il wire e il server sul rendering server, che gestisca i componenti COM e serializzi sul wire solo un set indispensabile di chiamate per il rendering, il mapping delle risorse grafiche, la gestione degli adapters, la presentazione degli schermi, gli shaders, insomma, bisognerebbe reinventare da capo un core leggero e serializzabile di DirectX che somiglierebbe molto all’implementazione delle funzionalità del driver grafico distribuito dai fornitori. Un lavoro troppo oneroso in termini di risorse umane e tempo a disposizione. ● challenge della traduzione da DIrcetX a OpenGL Il rendering server di NICE DCV ad oggi richiede di essere in esecuizione su una macchina GNU/Linux e su tale sistema operativo la libreria per la grafica 3D è OpenGL. DirectX è disponibile nativamente solo su sistemi operativi Windows. Ne consegue che per implementare l’external rendering per DirectX sia necessario tradurre le chiamate DirectX nelle corrispondenti chiamate OpenGL, tuttavia tale traduzione presenta svariati problemi in quanto le due librerie non hanno una corrispondenza diretta ed espongono modelli di programmazione diversi. L’alternativa sarebbe portare il rendering server su Windows, ma tale strada non è stata perseguita in quanto considerata non strategica durante questo progetto, poichè vogliamo che il rendering server possa continuare ad essere eseguito su di un host che funga da hypervisor KVM. In realtà esiste già un progetto che implementa uno strato intermedio simile a quello che andiamo cercando: il WineD3D, che è un sottocomponente del Wine. Il Wine è un software scritto in C nato originariamente per sistemi operativi GNU/Linux, poi esteso ad altri sistemi operativi, con lo scopo di permettere il funzionamento dei programmi sviluppati per il sistema operativo windows. Non esistendo una versione DirectX per questi sistemi operativi, ed essendo al contrario le OpenGL crossplatform, si è deciso di implementare un wrapper dal nome WineD3D che converta le chiamate DirectX in OpenGL, così da poter utilizzare questa libreria grafica, già presente nei sistemi operativi dove si intende far partire le applicazioni windows. Avendo già programmato un wrapper di OpenGL e un interception di DirectX, la nostra idea è stata quello di fondere le nostre componenti a quelle del Wine per poter implementare un wrapper DirectX che comuni con un rendering server OpenGL, mettendo le componenti in serie nel seguente modo: DirectX Interception -> Wine D3D -> OpenGL Wrapper -> Wire -> External Rendering In pratica, per DirectX9, abbiamo dirottato la CreateDevice9 cercata dall’applicazione nella versione implementata da WineD3D che a sua volta ha utilizzate le funzioni OpenGL, che a loro volta vengono intercettate dal wrapper OpenGL di DCV che serializza le chiamate, le manda sul wire ad un rendering server esterno su linux che effettua il rendering delle immagini da inviare a un Endstation. Per quanto possa sembrare entusiasmante, un procedimento così complesso si è dimostrato troppo debole per i seguenti motivi: ● Il Wine D3D è studiato principalmente per far funzionare videogiochi. Non esiste alcuna tabella di stabilità associata alle pro application e nessun tentativo concreto di farle partire. Ed infatti, la maggior parte di loro, tipo Autocad o 3DStudio, non hanno funzionato. Hanno funzionato invece i demo dell’SDK DirectX o i videogiochi supportati da WineD3D, che non sono di nostro interesse. 19 Progetto 3DCloudViz Deliverable D3 ● La traduzione eccessiva di chiamate si traduce in un calo di performance già evidente nella versione che non fa uso del wrapper OpenGL, con il wrapper opengl le performance erano così basse da rasentare la manciatina di frame nelle poche applicazioni in grado di eseguire correttamente. ● DirectX a dispetto della sua architettura macchinosa, riesce a regalare performance e features superiori rispetto a quelle di OpenGL. Creare una versione di DirectX scalata a OpenGL oltre che introdurre delle limitazioni ed un calo di performance, non sarebbe piaciuta ai client che confidano nelle features più avanzate di DirectX. Dunque, l’unica strada apparentemente percorribile per il rendering esterno di DirectX si è rivelata troppo debole per poter essere presa in considerazione. Non esistendo nessun altra alternativa, abbiamo deciso di non intraprendere questa strada. Fortunatamente, l’abbandono del rendering esterno di DirectX da parte nostra ha visto in parallelo la nascita di più tecnologie per la virtualizzazione che lo supportano in modo nativo, seppur con delle limitazioni. Tra queste tecnologie, quella di nostro interesse e che prenderemo in esame da ora in avanti riguarda NVIDIA GRID. Questa tecnologia comprende i driver vGPU che consentono ad un massimo di 8 macchine virtuali guest di utilizzare la scheda grafica della macchina host che le ospita. Anche se non passa attraverso il wire ma un canale di comunicazione interno, è la soluzione ideale per macchine virtuali windows che girano su una macchina xen di host, che può essere vista un po’ come il rendering server su linux nel caso di OpenGL I test con questa nuova tecnologia hanno regalato una semplicità di implementazione assieme a performance e stabilità tali da averci orientato definitvamente verso questa soluzione. Più avanti vedremo lo sviluppo delle tecnologie che fanno parte del pacchetto NVIDIA GRID e come il loro utilizzo ci abbia consentito di implementare un wrapper DirectX con vtable che gira su macchina fisica e su macchina virtuale con vGPU. 1.6 Avanzamenti tecnologici della piattaforma di sviluppo Inizialmente Monterey, in seguito rinominata NVIDIA GRID, la tecnologia di NVIDIA ha evoluto negli anni delle librerie che facilitano l’implementazione e rendono più performante il processo di remotizzazione e virtualizzazione delle applicazioni 3D. NvIFR La prima libreria del pacchetto che abbiamo preso in considerazione, sin dalla nascita e durante il suo sviluppo, è la NvIFR. Dato un device DirectX o un rendering context OpenGL in ingresso, la libreria NvIFR consente una cattura degli schermi più performante rispetto a quella fornita dalle rispettive librerie grafiche. NvIFRToSys. Consente la cattura di schermi su memoria di sistema. Dai nostri test, si è rivelata più performante della glReadPixels di OpenGL e della GetRenderTargetData su offscreen surface di DirectX. Per questo motivo, abbiamo deciso di usarla già da subito per la cattura degli schermi nel wrapper con vtable di DirectX (in grado di girare solo su macchina fisica) e nel wrapper di OpenGL (su macchina fisica e rendering esterno). NvIFRH264. In questa variante, gli schermi vengono catturati, compressi h264 con la GPU ed il frame dello stream video viene restituito in uscita. Di solito noi catturiamo gli schermi dalla memoria video alla memoria di sistema, li passiamo a un compressor che comprime utilizzando la codifica jpeg o zrle, con un dispendio di CPU non indifferente. La cosa migliore della compressione h264, invece, non è soltanto la compressione via hardware ma avere l’opportunità di poter comprimere al volo dallo schermo nella memoria video senza passare per la memoria di sistema, con un guadagno enorme in termini di utilizzo di CPU. L’intruduzione del formato h264 ha richiesto degli sforzi implementativi per evolvere il canale immagini di dcv e permettergli di supportare i flussi video. NvIFRH264 è stato adottato in entrambi i wrapper OpenGL e DirectX. 20 Progetto 3DCloudViz Deliverable D3 Supporto GRID in passthrough La tecnologia GPU passthrough di NVIDIA permette di creare una workstation virtuale che da agli utenti con tutti i benefici di un processore grafico dedicato sulla loro macchina. Connettendo direttamente una GPU dedidcata a una macchina virtuale attraverso l’hypervisor (VMWare ESX, XenServer, KVM), è possibile allocare l’intera GPU e le capacità di memoria grafica su una singola macchina virtuale senza compromettere alcuna risorsa. L’uso di un driver grafico nella virtual machine assicura la compatibilità col 100% delle applicazioni e l’accesso alle più recenti versioni di NVIDIA di OpenGL, DirectX, Cuda ed NVIDIA GRID. Questo ha permesso di utilizzare il wrapper DirectX nella sua versione vtable con NvIFR per h264, pur girando su una macchina virtuale. L’enorme limitazione del passthrough è che non si può creare più di una macchina virtual per GPU della macchina host, e di solito si spera di avere molte macchine virtuali che condividono le stesse risorse hardware. Fortunatamente, la tecnologia vGPU sembra far fronte al problema. vGPU La tecnologia vGPU di NVIDIA permette a più macchine virtuali di avere accesso in modo simultaneo e diretto a una singola GPU sulla macchina fisica. 21 Progetto 3DCloudViz Deliverable D3 Questo ci consente di avere più macchine virtuali windows sulla stessa macchina host, che eseguono applicazioni 3D con le versioni 9, 10 e 11 di DirectX. Le immagini sono catturate dal wrapper di DirectX con NvIFR, codificate h264 ed inviate alla endstation. Tuttavia ci sono delle limitazioni: ● ● ● ● ● E’ possibile riservare la stessa GPU fisica per un massimo di 8 macchine virtuali Cuda e OpenCL non sono supportati Funziona solo con GPU della serie NVIDIA GRID (i.e. NVIDIA K2) Per il momento gli unici hypervisor supportati sono XenServer da 6.2 SP1 in su e VMWare ESXi da 6.0 in su Linux non è supportato per il momento (NVIDIA ha annunciato di farlo in futuro) Queste limitazioni non rappresentano comunque un ostacolo al corretto funzionamento della soluzione adottata col wrapper di DirectX e NvIFRH264. NvFBC NvFBC è una API che mette a disposizione delle funzionalità interessanti per la cattura dell’intero desktop e non del frame buffer della scena 3D della singola applicazione. La possibilità di catturare l’intero desktop mette in discussione l’effettivo vantaggio di catturare gli schermi delle singole applicazioni, con tutti problemi legati alle tecnologie dei wrapper, e non l’intero desktop. Fino a tempi recenti si è deciso di catturare lo schermo delle singole applicazioni perchè la cattura dell’intero desktop è troppo costosa in termini di compressione e fino a quando si poteva comprimere con jpeg, l’invio dell’intero schermo rappresentava un uso troppo importante della banda e l’introduzione di delay lato client troppo importanti, uniti a un eccessivo uso della CPU. Era necessario avere dei criteri per non catturare, comprimere ed inviare le immagini di continuo e capire quali porzioni di schermo cambiassero per comprimere ed inviare al client solo quelle, ma non c’era nulla se non programmare degli algoritmi via software che aggiungevano complessità e un calo delle performance. NvFBC sembra fornire una soluzione anche per questo attraverso la cattura delle diffmap. Una diffmap è una mappa dove il valore associato ad ogni byte se diverso da zero rappresenta un cambiamento dei pixel in un blocco grande 128x128. Suddividendo l’intero desktop in una griglia di blocchi 128x128, è possibile capire quali pixel di questi blocchi siano effettivamente cambiati da un frame e il successivo, comprimere ed inviare al client solo questi. Ma non basta. 22 Progetto 3DCloudViz Deliverable D3 La compressione dei tile tramite jpeg è troppo onerosa se il cambiamento tra un frame e il successivo dovesse riguardare i pixel dell’intera immagine. Per ovviare a questo problema, si è possibile passare l’immagine del desktop come texture ad un device DirectX per poi comprimere in h264 via GPU tramite NvIFRH264. Questa soluzione ci ha permesso di elaborare un prototipo che cattura l’intero desktop e fa a meno dei wrapper con library interposition, col vantaggio di avere un sistema non modificato con un solo processo in background che cattura le immagini e le invia ad un client. 1.7 Analisi e implementazione di Screen Grabbing tramite NVDIA GRID Facendo uso delle tecnologie NVIDIA GRID, abbiamo proceduto ad implementare un prototipo con una architettura completamente differente dai metodi di library interposition. L’obiettivo del prototipo è di catturare l’intero schermo in modo indipendente dalle applicazioni e per tanto supportando trasparentemente applicazioni DirectX, OpenGL o anche applicazioni 2D. Per soddisfare l’obiettivo di retrocompatibilità con client esistenti, il meccanismo di cattura, compressione e trasmissione deve andare ad integrarsi con il processo VNC esistente. L’architettura che abbiamo definito è la seguente: un processo demone dcvd usa NvFBC per catturare gli schermi ed apre una finestra invisibile di overlay per permettere al client di VNC di tracciare l’intero contenuto del desktop. In base ai cambiamenti del desktop, l’immagine catturata o alcune sue parti vengono compresse con zrle o jpeg via software oppure con h264 tramite NvIFRH264. I frame compressi vengono inviati al client, decompressi e visualizzati sulla finestra del client vnc, perfettamente compatibile alla versione di dcv con wrapper OpenGL. Il diagramma che descrive il funzionamento di dcv in questo caso, è il seguente: E’ stato necessario introdurre dei criteri per ottimizzare l’utilizzo della banda, migliorare le performance e minimizzare il delay. NvFBC è in grado di catturare lo schermo se c’è stato un cambiamento nel desktop ed una mappa dei blocchi del desktop hanno subito questo cambiamento. Se un solo blocco è stato modificato, allora l’intero desktop viene catturato da uno screen grabber tramite la funzione bloccante NvFBCToSysGrabFrame assieme alla rispettiva diffmap. L’immagine e la diffmap vengono inviate dallo screen grabber ad un tiler, che decide quale tipo di compressione effettuare in base alla quantità dei cambiamenti. Se più di 9 ≤ N/10 ≤ 16 degli N blocchi dello schermo stanno cambiando, allora l’intero frame viene compresso con h264. Altrimenti dalla diffmap vengono estratti dei rettangoli (o tile) che coprono i blocchi per le porzioni di schermo che hanno subito il cambiamento, vengono compressi con jpeg e inviati sul client. Se non si verifica alcun cambiamento nel giro di 300 ms, allora viene inviato un quality update di tipo lossless con zrle per ciancun blocco che ha subito il cambiamento tra l’invio della quality update precedente e quella corrente. Nel caso dei monitor multipli, un unico desktop virtuale è composto da più schermi, uno per ogni monitor. In questo caso viene tracciata una finestra di overlay in grado di comprire l’intero dekstop virtuale. Il desktop capture di dcvd si preoccupa di enumerare i monitor e di instanzare uno screen grabber con un NvFBC per ciascun monitor. I frame h264 ed i tiles vengono 23 Progetto 3DCloudViz Deliverable D3 inviati al client sottoforma di slice da ricomporre e disegnare nelle rispettive posizioni occupate dai monitor nel desktop virtuale. Le strategie di screen capture hanno permesso di diminuire il delay, il processamento delle immagini a carico della cpu così come l’utilizzo di banda. Questo ha reso possibile la cattura dell’intero desktop anche con monitor multipli e la remotizzazione delle applicazioni 3D senza l’utilizzo dei wrapper con library injection ed interposition. I vantaggi rispetto al vecchio sistema coi wrapper sono i seguenti: ● Il sistema non è modificato da filter driver e librerie iniettate nei processi. ● Non c’è un wrapper che possa crashare l’applicazione o degradarne le performance. ● E’ possibile catturare, comprimere e inviare sul client solo ciò che si vede sull’intero desktop. Questo consente di aprire un numero arbitrario di applicazioni 3D. Col vecchio sistema, 100 dcvtest avrebbero inviato 100 immagini, saturando la cpu sul server, la banda e la cpu sul client. In questo modo, 100 dcvtest provocano al più l’invio dell’intero desktop in h264. ● Non c’è più bisogno di tenere il passo tecnologico delle librerie grafiche per tenere i wrapper aggiornati. ● E’ possibile fornire all’utente un’esperienza di remotizzazione completa su tutte le applicazioni del desktop, comprese quelle del browser. 1.8 Conclusioni ed obiettivi raggiunti Il progetto ha permesso di raggiungere i seguenti obiettivi che soddisfano pienamente i requisiti funzionali che ci eravamo prefissi: ● L’approccio basato su screen grabbing si è dimostrato il più stabile e performante: consente la remotizzazione di applicazioni OpenGL e DirectX, assicurando una compatibiltà estesa con tutte le applicazioni testate. L’uso di NVIDIA GRID permette di sfruttare la codifica h264 e il fast frame grabbing, assicurando prestazioni elevate. L’uso di GPU passthrough e vGPU consente il supporto di macchine virtuali. Infine Il prototipo implementato si integra pienamente con la versione esistente di VNC usata da NICE DCV, garantendo la compatibilità con client esistenti ● Lo studio delle tecniche di interposition per DirectX ha permesso di approfondire la nostra conoscenza sia della libreria DirectX stessa, sia delle tecniche di intercettamento a nostra disposizione: tale knowhow sarà estremamente utile in futuro sia per il debugging di applicazioni sia per implementare soluzioni in ambiti in cui la library interposition sia necessaria ● Lo studio di fattibilità sul supporto per external rendering ha permesso di mettere in evidenza una serie di limitazioni che allo stata attuale sono state considerate bloccanti; se in futuro fosse necessario intraprendere nuovamente questa strada saremmo già a conoscenza dei problemi da affrontare 24