Profiter du multithreading entre la boucle de jeu et openGL

10

Parler dans le cadre d'un jeu basé sur le rendu openGL:

Supposons qu'il existe deux fils:

  1. Met à jour la logique et la physique du jeu, etc. pour les objets du jeu

  2. Effectue des appels de dessin openGL pour chaque objet de jeu en fonction des données dans les objets de jeu (ce thread 1 continue de se mettre à jour)

À moins que vous ayez deux copies de chaque objet de jeu dans l'état actuel du jeu, vous devrez mettre en pause Thread 1 pendant que Thread 2 effectue les appels de tirage sinon les objets de jeu seront mis à jour au milieu d'un appel de tirage pour cet objet, ce qui est indésirable!

Mais arrêter le thread 1 pour effectuer des appels en toute sécurité depuis le thread 2 tue tout le but du multithreading / simultané

Existe-t-il une meilleure approche pour cela autre que l'utilisation de centaines ou de milliers ou la synchronisation d'objets / clôtures afin que l'architecture multicœur puisse être exploitée pour les performances?

Je sais que je peux toujours utiliser le multithreading pour charger des textures et compiler des shaders pour les objets qui ne font pas encore partie de l'état actuel du jeu, mais comment le faire pour les objets actifs / visibles sans provoquer de conflit avec le dessin et la mise à jour?

Que se passe-t-il si j'utilise un verrouillage de synchronisation distinct dans chacun des objets du jeu? De cette façon, n'importe quel thread ne bloquerait qu'un seul objet (cas idéal) plutôt que pour tout le cycle de mise à jour / dessin! Mais combien coûte la prise de verrous sur chaque objet (le jeu peut avoir mille objets)?

Allahjane
la source
1. En supposant qu'il n'y aura que deux threads, l'échelle n'est pas bonne, 4 cœurs sont très courants et ne feront qu'augmenter. 2. Réponse de la stratégie des meilleures pratiques: appliquer la séparation de la responsabilité de la requête de commande et le modèle du modèle d'acteur / système d'entité de composant. 3. Réponse réaliste: il suffit de le pirater de la manière traditionnelle C ++ avec des verrous et d'autres choses.
Den
Un magasin de données commun, un verrou.
Justin
@justin juste comme je l'ai dit avec un verrou de synchronisation ! ce qui est pire qu'une approche à un seul
fil
1
Il semble que l'approche multithread dans les jeux avec des API graphiques asynchrones (par exemple openGL) soit toujours un sujet actif et il n'y a pas de solution standard ou presque parfaite
Allahjane
1
Le magasin de données commun à votre moteur et à votre moteur de rendu ne doit pas être vos objets de jeu. Il doit s'agir d'une représentation que le moteur de rendu peut traiter le plus rapidement possible.
Justin

Réponses:

13

L'approche que vous avez décrite, en utilisant des verrous, serait très inefficace et probablement plus lente que l'utilisation d'un seul thread. L'autre approche consistant à conserver des copies de données dans chaque thread fonctionnerait probablement bien "en termes de vitesse", mais avec un coût de mémoire prohibitif et une complexité de code pour garder les copies synchronisées.

Il existe plusieurs approches alternatives à cela, une solution populaire pour le rendu multi-thread est d'utiliser un double tampon de commandes. Cela consiste à exécuter le backend du moteur de rendu dans un thread séparé, où tous les appels de dessin et la communication avec l'API de rendu sont effectués. Le thread frontal qui exécute la logique du jeu communique avec le moteur de rendu principal via un tampon de commande (à double tampon). Avec cette configuration, vous n'avez qu'un seul point de synchronisation à la fin d'une trame. Alors que le front-end remplit un tampon avec des commandes de rendu, le back-end consomme l'autre. Si les deux fils sont bien équilibrés, aucun ne doit mourir de faim. Cette approche n'est cependant pas optimale, car elle introduit une latence dans les images rendues, de plus, le pilote OpenGL est susceptible de le faire déjà dans son propre processus, de sorte que les gains de performances devraient être soigneusement mesurés. Il n'utilise également que deux cœurs, au mieux. Cette approche a cependant été utilisée dans plusieurs jeux à succès, tels que Doom 3 et Quake 3.

Les approches plus évolutives qui font un meilleur usage des processeurs multicœurs sont celles basées sur des tâches indépendantes , où vous déclenchez une demande asynchrone qui est traitée dans un thread secondaire, tandis que le thread qui a déclenché la demande continue avec d'autres travaux. La tâche devrait idéalement ne pas avoir de dépendances avec les autres threads, pour éviter les verrous (évitez également les données partagées / globales comme la peste!). Les architectures basées sur des tâches sont plus utilisables dans des parties localisées d'un jeu, telles que les animations informatiques, la recherche d'IA, la génération de procédures, le chargement dynamique des accessoires de scène, etc. Les jeux sont naturellement riches en événements, la plupart des types d'événements sont asynchrones, de sorte qu'il est facile à faire fonctionner dans des threads séparés.

Enfin, je recommande la lecture:

glampert
la source
juste curieux! comment savez-vous que Doom 3 a utilisé cette approche? Je pensais que les développeurs ne laissaient jamais tomber les techniques!
Allahjane
4
@Allahjane, Doom 3 est Open Source . Dans le lien que j'ai fourni, vous trouverez une revue de l'architecture globale du jeu. Et vous vous trompez, oui, il est rare de trouver des jeux entièrement Open Source, mais les développeurs exposent généralement leurs techniques et astuces dans des blogs, des articles et des événements tels que GDC .
glampert
1
Hmm. Ce fut une lecture impressionnante! merci de m'en avoir informé! Vous obtenez la tique :)
Allahjane