Sfruttando il multithreading tra game loop e openGL


10

Parlando nel contesto di un gioco basato sul renderer openGL:

Supponiamo che ci siano due thread:

  1. Aggiorna la logica e la fisica del gioco, ecc. Per gli oggetti di gioco

  2. Esegue chiamate openGL per ogni oggetto di gioco in base ai dati negli oggetti di gioco (quel thread 1 continua ad aggiornarsi)

A meno che tu non abbia due copie di ciascun oggetto di gioco nello stato corrente del gioco, dovrai mettere in pausa il thread 1 mentre il thread 2 effettua le chiamate di sorteggio, altrimenti gli oggetti di gioco verranno aggiornati nel mezzo di una chiamata di sorteggio per quell'oggetto, che è indesiderabile!

Ma interrompere il thread 1 per effettuare chiamate di disegno in modo sicuro dal thread 2 uccide l'intero scopo del multithreading / concorrenza

Esiste un approccio migliore per questo oltre all'utilizzo di centinaia o migliaia o sincronizzazione di oggetti / recinzioni in modo che l'architettura multicore possa essere sfruttata per le prestazioni?

So di poter ancora utilizzare il multithreading per caricare texture e compilare shader per gli oggetti che devono ancora far parte dello stato di gioco corrente, ma come posso farlo per gli oggetti attivi / visibili senza causare conflitti con il disegno e l'aggiornamento?

Cosa succede se utilizzo un blocco sincronizzazione separato in ciascuno degli oggetti di gioco? In questo modo qualsiasi thread si bloccherebbe solo su un oggetto (caso ideale) anziché per l'intero ciclo di aggiornamento / disegno! Ma quanto costa prendere i lucchetti su ogni oggetto (il gioco può avere un migliaio di oggetti)?


1. Supponendo che ci saranno solo due thread non scalabili bene, 4 core è molto comune e aumenterà solo. 2. Risposta alla strategia delle migliori pratiche: applicare la segregazione della responsabilità dei comandi e il modello di attore / sistema di entità dei componenti. 3. Risposta realistica: basta hackerarla nel tradizionale modo C ++ con blocchi e roba.
Den,

Un archivio dati comune, un blocco.
Justin,

@justin proprio come ho detto con un blocco della sincronizzazione L'unico vantaggio del multithreading verrà brutalmente ucciso alla luce del giorno, il thread di aggiornamento dovrebbe attendere fino a quando il thread di disegno non effettua chiamate per tutti gli oggetti, quindi il thread di disegno attende fino a quando il ciclo di aggiornamento non completa l'aggiornamento ! che è peggio dell'approccio a thread singolo
Allahjane,

1
Sembra che l'approccio multithread nei giochi con API asincrone grafiche (ad esempio openGL) sia ancora argomento attivo e non esiste una soluzione standard o quasi perfetta
Allahjane

1
Il datastore comune al tuo motore e al tuo renderer non dovrebbe essere il tuo oggetto di gioco. Dovrebbe essere una rappresentazione che il renderer può elaborare il più rapidamente possibile.
Justin,

Risposte:


13

L'approccio che hai descritto, usando i blocchi, sarebbe molto inefficiente e molto probabilmente più lento rispetto all'utilizzo di un singolo thread. L'altro approccio di mantenere copie dei dati in ogni thread probabilmente funzionerebbe bene "in termini di velocità", ma con un costo di memoria e una complessità del codice proibitivi per mantenere sincronizzate le copie.

Esistono diversi approcci alternativi a questo, una soluzione popolare per il rendering multi-thread è l'utilizzo di un doppio buffer di comandi. Ciò consiste nell'eseguire il back-end del renderer in un thread separato, in cui vengono eseguite tutte le chiamate di disegno e le comunicazioni con l'API di rendering. Il thread front-end che esegue la logica di gioco comunica con il renderer back-end tramite un buffer di comando (doppio buffer). Con questa configurazione, hai solo un punto di sincronizzazione al completamento di un frame. Mentre il front-end riempie un buffer con i comandi di rendering, il back-end consuma l'altro. Se entrambi i fili sono ben bilanciati, nessuno dovrebbe morire di fame. Questo approccio è tuttavia non ottimale, poiché introduce la latenza nei frame renderizzati, inoltre, è probabile che il driver OpenGL lo stia già facendo nel suo stesso processo, quindi i guadagni delle prestazioni dovrebbero essere attentamente misurati. Utilizza anche solo due core, nella migliore delle ipotesi. Questo approccio è stato utilizzato in molti giochi di successo, come Doom 3 e Quake 3

Approcci più scalabili che fanno un uso migliore delle CPU multi-core sono quelli basati su attività indipendenti , in cui si attiva una richiesta asincrona che viene gestita in un thread secondario, mentre il thread che ha generato la richiesta continua con qualche altro lavoro. L'attività dovrebbe idealmente non avere dipendenze con gli altri thread, per evitare blocchi (evitare anche dati condivisi / globali come la peste!). Le architetture basate su attività sono più utilizzabili nelle parti localizzate di un gioco, come animazioni informatiche, pathfinding AI, generazione procedurale, caricamento dinamico di oggetti di scena, ecc. I giochi sono naturalmente pieni di eventi, la maggior parte degli eventi sono asincroni, quindi è facile farli funzionare in thread separati.

Infine, consiglio di leggere:


solo curioso! come fai a sapere che Doom 3 ha usato questo approccio? Pensavo che gli sviluppatori non facessero mai uscire le tecniche!
Allahjane,

4
@Allahjane, Doom 3 è Open Source . Nel link che ho fornito, troverai una recensione dell'architettura generale del gioco. E ti sbagli, sì, è raro trovare giochi completamente open source, ma gli sviluppatori di solito espongono le loro tecniche e trucchi in blog, documenti ed eventi come GDC .
glampert

1
Hmm. Questa è stata una lettura fantastica! grazie per avermelo informato! Ottieni il segno di spunta :)
Allahjane il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.