Welches Entwurfsmuster eignet sich am besten zum Verwalten von Handles für Objekte, ohne Handles oder Manager weiterzugeben?

8

Ich schreibe ein Spiel in C ++ mit OpenGL.

Für diejenigen , die Sie mit der OpenGL API nicht kennen, machen eine Menge Anrufe , um Dinge wie glGenBuffersund glCreateShaderetc. Diese Rückgabetypen von GLuintdenen sind eindeutige Kennungen zu dem, was Sie gerade erstellt haben . Das erstellte Objekt lebt im GPU-Speicher.

Da der GPU-Speicher manchmal begrenzt ist, möchten Sie nicht zwei Dinge erstellen, die gleich sind, wenn sie von mehreren Objekten verwendet werden sollen.

Zum Beispiel Shader. Sie verknüpfen ein Shader-Programm und haben dann ein GLuint. Wenn Sie mit dem Shader fertig sind, sollten Sie anrufen glDeleteShader(oder etwas in diesem Sinne).

Nehmen wir an, ich habe eine flache Klassenhierarchie wie:

class WorldEntity
{
public:
    /* ... */
protected:
    ShaderProgram* shader;
    /* ... */
};

class CarEntity : public WorldEntity 
{
    /* ... */
};

class PersonEntity: public WorldEntity
{
    /* ... */
};

Jeder Code, den ich jemals gesehen habe, würde erfordern, dass alle Konstruktoren eine ShaderProgram*Übergabe an ihn haben, um in der gespeichert zu werden WorldEntity. ShaderProgramist meine Klasse, die die Bindung von a GLuintan den aktuellen Shader-Status im OpenGL-Kontext sowie einige andere hilfreiche Dinge, die Sie mit Shadern tun müssen, kapselt.

Das Problem, das ich damit habe, ist:

  • Es sind viele Parameter erforderlich, um ein zu WorldEntityerstellen (bedenken Sie, dass es möglicherweise ein Netz, einen Shader, eine Reihe von Texturen usw. gibt, die alle gemeinsam genutzt werden können, sodass sie als Zeiger übergeben werden).
  • Was auch immer die WorldEntityBedürfnisse schafft, muss wissen, was ShaderProgrames braucht
  • Dies erfordert wahrscheinlich eine Art Gulp- EntityManager Klasse, die weiß, welche Instanz von was ShaderPrograman verschiedene Entitäten übergeben werden soll.

Jetzt, da es eine gibt, müssen sich Managerdie Klassen entweder EntityManagerzusammen mit der benötigten ShaderProgramInstanz bei der registrieren , oder ich brauche einen Big-Ass switchim Manager, den ich für jeden neuen WorldEntityabgeleiteten Typ aktualisieren muss .

Mein erster Gedanke war, eine ShaderManagerKlasse zu erstellen (ich weiß, Manager sind schlecht), die ich als Referenz oder Zeiger auf die WorldEntityKlassen übergebe, damit sie erstellen können, was ShaderProgramsie wollen, über die ShaderManagerund die ShaderManagerbereits vorhandenen ShaderPrograms verfolgen können , damit dies möglich ist Geben Sie eine bereits vorhandene zurück oder erstellen Sie bei Bedarf eine neue.

(Ich könnte das ShaderPrograms über den Hash der Dateinamen des ShaderProgramtatsächlichen Quellcodes speichern. )

Also jetzt:

  • Ich übergebe jetzt Zeiger auf ShaderManagerstatt ShaderProgram, daher gibt es immer noch viele Parameter
  • Ich brauche keine EntityManager, die Entitäten selbst wissen, welche Instanz ShaderProgramerstellt werden soll, und ShaderManagerkümmern sich um die tatsächlichen ShaderPrograms.
  • Aber jetzt weiß ich nicht, wann ich ShaderManagersicher löschen kann, ShaderProgramwas es enthält.

Jetzt habe ich meiner ShaderProgramKlasse eine Referenzzählung hinzugefügt , die das interne GLuintVia löscht, glDeleteProgramund ich verzichte darauf ShaderManager.

Also jetzt:

  • Ein Objekt kann alles erstellen, was ShaderProgrames benötigt
  • Aber jetzt gibt es Duplikate, ShaderProgramweil kein externer Manager den Überblick behält

Schließlich komme ich, um eine von zwei Entscheidungen zu treffen:

1. Statische Klasse

A static class, das aufgerufen wird, um ShaderPrograms zu erstellen . Es führt eine interne Verfolgung von ShaderPrograms basierend auf einem Hash der Dateinamen durch - dies bedeutet, dass ich keine Zeiger oder Verweise mehr auf ShaderPrograms oder ShaderManagers übergeben muss, also weniger Parameter - WorldEntitiesdie alle Kenntnisse über die Instanz haben, die ShaderProgramsie erstellen möchten

Dieses neue static ShaderManagermuss:

  • Zählen Sie, wie oft a verwendet ShaderProgramwird, und ich mache ShaderProgramkein kopierbares ODER
  • ShaderPrograms zählen ihre Referenzen und rufen glDeleteProgramihren Destruktor nur auf, wenn die Zählung 0UND ist, und suchen ShaderManagerregelmäßig nach ShaderProgram's mit einer Zählung von 1 und verwerfen sie.

Die Nachteile dieses Ansatzes, die ich sehe, sind:

  1. Ich habe eine globale statische Klasse, die ein Problem sein könnte. Der OpenGL-Kontext muss vor dem Aufrufen von glXFunktionen erstellt werden. Möglicherweise wird also ein WorldEntityerstellt und versucht, ShaderProgramvor der OpenGL-Kontexterstellung einen zu erstellen , was zu einem Absturz führt.

    Der einzige Weg, dies zu umgehen, besteht darin, alles als Zeiger / Referenzen weiterzugeben oder eine globale GLContext-Klasse zu haben, die abgefragt werden kann, oder alles in einer Klasse zu halten, die den Kontext bei der Erstellung erstellt. Oder vielleicht nur ein globaler Boolescher Wert IsContextCreated, der überprüft werden kann. Aber ich mache mir Sorgen, dass ich dadurch überall hässlichen Code bekomme.

    Was ich sehen kann, ist:

    • Die große EngineKlasse, in der jede andere Klasse versteckt ist, damit sie die Bau- / Dekonstruktionsreihenfolge angemessen steuern kann. Dies scheint ein großes Durcheinander von Schnittstellencode zwischen dem Benutzer der Engine und der Engine zu sein, wie ein Wrapper über einem Wrapper
    • Eine ganze Reihe von "Manager" -Klassen, die Instanzen verfolgen und Dinge löschen, wenn dies erforderlich ist. Dies könnte ein notwendiges Übel sein?

UND

  1. Wann soll man eigentlich ShaderPrograms aus dem räumen static ShaderManager? Alle paar Minuten? Jede Spielschleife? Ich kümmere mich ordnungsgemäß um das Neukompilieren eines Shaders für den Fall, dass ein Shader ShaderProgramgelöscht wurde, aber dann ein neuer ihn WorldEntityanfordert. aber ich bin sicher, es gibt einen besseren Weg.

2. Eine bessere Methode

Darum bitte ich hier

NeomerArcana
quelle
2
Wenn Sie sagen "Es sind viele Parameter erforderlich, um eine WorldEntity zu erstellen", fällt Ihnen ein, dass für die Verkabelung ein Fabrikmuster erforderlich ist. Außerdem sage ich nicht, dass Sie hier unbedingt eine Abhängigkeitsinjektion wünschen, aber wenn Sie diesen Pfad noch nicht durchgesehen haben, finden Sie ihn möglicherweise aufschlussreich. Die "Manager", von denen Sie hier sprechen, klingen ähnlich wie lebenslange Scope-Handler.
J Trana
Nehmen wir also an, ich implementiere eine Factory-Klasse, um WorldEntitys zu konstruieren . Verschiebt das nicht einen Teil des Problems? Denn jetzt muss die WorldFactory-Klasse jeder WolrdEntity das richtige ShaderProgramm übergeben.
NeomerArcana
Gute Frage. Oft nein - und hier ist warum. In vielen Fällen müssen Sie kein bestimmtes ShaderProgramm haben, oder Sie möchten möglicherweise ändern, welches instanziiert ist, oder Sie möchten einen Komponententest mit einem vollständig simulierten ShaderProgramm schreiben. Eine Frage, die ich stellen würde, ist: Ist es für diese Entität wirklich wichtig, welches Shader-Programm sie hat? In einigen Fällen kann dies der Fall sein, aber da Sie einen ShaderProgram-Zeiger anstelle eines MySpecificShaderProgram-Zeigers verwenden, ist dies möglicherweise nicht der Fall. Außerdem kann sich das Problem des ShaderProgram-Bereichs jetzt auf die Werksebene verschieben, sodass Änderungen zwischen Singletons usw. problemlos möglich sind.
J Trana

Antworten:

4
  1. Eine bessere Methode Das ist es, wonach ich hier frage

Entschuldigung für die Nekromantie, aber ich habe so viele gesehen, die über ähnliche Probleme bei der Verwaltung von OpenGL-Ressourcen gestolpert sind, einschließlich mir in der Vergangenheit. Und so viele der Schwierigkeiten, mit denen ich zu kämpfen hatte und die ich bei anderen erkenne, sind auf die Versuchung zurückzuführen, die OGL-Ressourcen, die für das Rendern einer analogen Spieleinheit erforderlich sind, zu verpacken und manchmal zu abstrahieren und sogar zu kapseln.

Und der "bessere Weg", den ich gefunden habe (zumindest einer, der meine besonderen Kämpfe dort beendet hat), war, Dinge anders herum zu machen. Das heißt, beschäftigen Sie sich beim Entwerfen Ihrer Spielentitäten und -komponenten nicht mit den Aspekten von OGL auf niedriger Ebene und entfernen Sie sich von solchen Ideen, die Sie Modelwie Dreiecks- und Scheitelpunktprimitive in Form von Objekten speichern müssen, die umhüllen oder sogar VBOs abstrahieren.

Rendering-Bedenken vs. Game-Design-Bedenken

Es gibt Konzepte auf etwas höherer Ebene als GPU-Texturen, zum Beispiel mit einfacheren Verwaltungsanforderungen wie CPU-Images (und diese benötigen Sie zumindest vorübergehend, bevor Sie überhaupt eine GPU-Textur erstellen und binden können). Fehlende Bedenken hinsichtlich des Renderns eines Modells können ausreichen, um nur eine Eigenschaft zu speichern, die den Dateinamen angibt, der für die Datei verwendet werden soll, die die Daten für das Modell enthält. Sie können eine "Material" -Komponente haben, die übergeordneter und abstrakter ist und die Eigenschaften dieses Materials beschreibt als ein GLSL-Shader.

Und dann gibt es nur einen Platz in der Codebasis, der sich mit Dingen wie Shadern und GPU-Texturen sowie VAOs / VBOs und OpenGL-Kontexten befasst, und das ist die Implementierung des Renderingsystems . Das Rendering-System durchläuft möglicherweise die Entitäten in der Spielszene (in meinem Fall durchläuft es einen räumlichen Index, aber Sie können dies leichter verstehen und mit einer einfachen Schleife beginnen, bevor Sie Optimierungen wie das Ausstoßen von Kegelstumpf mit einem räumlichen Index implementieren) entdeckt Ihre übergeordneten Komponenten wie "Materialien" und "Bilder" und Modelldateinamen.

Und seine Aufgabe ist es, die übergeordneten Daten, die nicht direkt mit der GPU zu tun haben, zu nehmen und die erforderlichen OpenGL-Ressourcen zu laden / zu erstellen / zuzuordnen / zu binden / zu verwenden / zu trennen / zu zerstören, basierend auf dem, was sie in der Szene entdecken und was mit der passiert Szene. Und das beseitigt die Versuchung, Dinge wie Singletons und statische Versionen von "Managern" zu verwenden und was nicht, denn jetzt ist Ihre gesamte OGL-Ressourcenverwaltung auf ein System / Objekt in Ihrer Codebasis zentralisiert (obwohl Sie es natürlich in weitere gekapselte Objekte zerlegen könnten vom Renderer, um den Code übersichtlicher zu gestalten). Es vermeidet natürlich auch einige Auslösepunkte, wenn beispielsweise versucht wird, Ressourcen außerhalb eines gültigen OGL-Kontexts zu zerstören.

Designänderungen vermeiden

Darüber hinaus bietet dies viel Raum zum Atmen, um kostspielige zentrale Designänderungen zu vermeiden, da Sie im Nachhinein feststellen, dass für einige Materialien mehrere Rendering-Durchgänge (und mehrere Shader) zum Rendern erforderlich sind, z. B. ein Streuungsdurchlauf unter der Oberfläche und ein Shader für Hautmaterialien, während Sie dies zuvor getan haben wollte ein Material mit einem einzigen GPU-Shader zusammenführen. In diesem Fall gibt es keine kostspielige Designänderung an zentralen Schnittstellen, die von vielen Dingen verwendet werden. Sie aktualisieren lediglich die lokale Implementierung des Renderingsystems, um diesen zuvor unerwarteten Fall zu behandeln, wenn Skineigenschaften in Ihrer übergeordneten Materialkomponente auftreten.

Die Gesamtstrategie

Und das ist die Gesamtstrategie, die ich jetzt verwende, und sie wird umso hilfreicher, je komplexer Ihre Rendering-Bedenken sind. Als Nachteil erfordert es ein bisschen mehr Vorarbeit als das Injizieren von Shadern und VBOs und ähnlichen Dingen in Ihre Spieleinheiten, und es koppelt Ihren Renderer mehr an Ihre spezielle Spiel-Engine (oder deren Abstraktionen, obwohl im Gegenzug die höhere Ebene) Spielentitäten und -konzepte werden vollständig von Rendering-Problemen auf niedriger Ebene entkoppelt. Und Ihr Renderer benötigt möglicherweise Rückrufe, um ihn zu benachrichtigen, wenn Entitäten zerstört werden, damit er alle ihm zugeordneten Daten trennen und zerstören kann (Sie können hier oder hier die Nachzählung verwendenshared_ptrfür gemeinsam genutzte Ressourcen, jedoch nur lokal im Renderer). Möglicherweise möchten Sie eine effiziente Methode zum Zuordnen und Aufheben der Zuordnung aller Arten von Rendering-Daten zu beliebigen Entitäten in konstanter Zeit (ein ECS bietet dies in der Regel jedem System auf Anhieb an, wie Sie neue Komponententypen im laufenden Betrieb zuordnen können, wenn dies der Fall ist ein ECS - wenn nicht, sollte es auch nicht zu schwierig sein) ... aber auf der anderen Seite werden all diese Dinge wahrscheinlich für andere Systeme als den Renderer nützlich sein.

Zugegeben, die reale Implementierung wird viel nuancierter und verwischt diese Dinge möglicherweise ein bisschen mehr, als wenn Ihre Engine Dinge wie Dreiecke und Eckpunkte in anderen Bereichen als dem Rendern behandeln möchte (z. B. die Physik möchte, dass solche Daten eine Kollisionserkennung durchführen ). Aber wo das Leben (zumindest für mich) viel einfacher wurde, war es, diese Art der Umkehrung in Denkweise und Strategie als Ausgangspunkt zu nehmen.

Das Entwerfen eines Echtzeit-Renderers ist meiner Erfahrung nach sehr schwierig - das Schwierigste, das ich je entworfen habe (und das ich immer wieder neu entwerfe), mit schnellen Änderungen an Hardware, Schattierungsfunktionen und entdeckten Techniken. Dieser Ansatz beseitigt jedoch die unmittelbare Sorge, wann GPU-Ressourcen durch Zentralisierung all dessen für die Rendering-Implementierung erstellt / zerstört werden können, und noch vorteilhafter für mich ist, dass er die ansonsten kostspieligen und kaskadierenden Designänderungen (die sich auswirken könnten) verschoben hat Code, der sich nicht unmittelbar mit dem Rendern befasst), sondern nur mit der Implementierung des Renderers selbst. Und diese Reduzierung der Änderungskosten kann zu enormen Einsparungen führen, da sich die Anforderungen jedes oder jedes zweite Jahr so ​​schnell ändern wie beim Echtzeit-Rendering.

Ihr Schattierungsbeispiel

Ich gehe Ihr Schattierungsbeispiel so an, dass ich mich nicht mit Dingen wie GLSL-Shadern in Dingen wie Auto- und Personeneinheiten beschäftige. Ich beschäftige mich mit "Materialien", die sehr leichte CPU-Objekte sind, die nur Eigenschaften enthalten, die beschreiben, um welche Art von Material es sich handelt (Haut, Autolack usw.). In meinem eigentlichen Fall ist es etwas raffiniert, da ich ein DSEL habe, das Unreal Blueprints ähnelt, um Shader in einer visuellen Sprache zu programmieren, aber in Materialien werden keine GLSL-Shader-Handles gespeichert.

ShaderPrograms zählen ihre Referenzen und rufen glDeleteProgram in ihrem Destruktor nur auf, wenn die Anzahl 0 ist. ShaderManager sucht regelmäßig nach ShaderPrograms mit einer Anzahl von 1 und verwirft sie.

Ich habe ähnliche Dinge getan, als ich diese Ressourcen außerhalb des Renderers gespeichert und verwaltet habe, weil meine frühesten naiven Versuche, die nur versuchten, diese Ressourcen in einem Destruktor direkt zu zerstören, oft versuchten, diese Ressourcen außerhalb von a zu zerstören gültiger GL-Kontext (und manchmal habe ich sogar versehentlich versucht, sie in einem Skript oder etwas anderem zu erstellen, wenn ich mich nicht in einem gültigen Kontext befand), daher musste ich die Erstellung und Zerstörung auf Fälle verschieben, in denen ich garantieren konnte, dass ich mich in einem gültigen Kontext befand die zu ähnlichen "Manager" -Designs führen, die Sie beschreiben.

All diese Probleme verschwinden, wenn Sie eine CPU-Ressource an ihrer Stelle speichern und der Renderer sich mit den Bedenken der GPU-Ressourcenverwaltung befasst. Ich kann einen OGL-Shader nirgendwo zerstören, aber ich kann ein CPU-Material überall zerstören und einfach verwenden shared_ptrund so weiter, ohne mich in Schwierigkeiten zu bringen.

Wann müssen ShaderPrograms tatsächlich aus dem statischen ShaderManager gelöscht werden? Alle paar Minuten? Jede Spielschleife? Ich kümmere mich ordnungsgemäß um das Neukompilieren eines Shaders, wenn ein ShaderProgramm gelöscht wurde, aber dann eine neue WorldEntity es anfordert. aber ich bin sicher, es gibt einen besseren Weg.

Jetzt ist dieses Problem selbst in meinem Fall schwierig, wenn Sie die GPU-Ressourcen effizient verwalten und auslagern möchten, wenn sie nicht mehr benötigt werden. In meinem Fall kann ich mich mit massiven Szenen befassen und arbeite eher in VFX als in Spielen, in denen Künstler möglicherweise besonders intensive Inhalte haben, die nicht für das Echtzeit-Rendering optimiert sind (epische Texturen, Modelle, die Millionen von Polygonen umfassen usw.).

Für die Leistung ist es sehr nützlich, nicht nur das Rendern zu vermeiden, wenn sie sich außerhalb des Bildschirms befinden (außerhalb des Betrachtungsstumpfs), sondern auch die GPU-Ressourcen zu entladen, wenn sie für eine Weile nicht mehr benötigt werden (sagen wir, der Benutzer schaut nicht auf etwas in der Ferne Platz für eine Weile).

Die Lösung, die ich am häufigsten verwende, ist die Art der "Zeitstempel" -Lösung, obwohl ich nicht sicher bin, wie sie bei Spielen anwendbar ist. Wenn ich anfange, Ressourcen zum Rendern zu verwenden / zu binden (z. B. sie bestehen den Kegelstumpf-Keulungstest), speichere ich die aktuelle Zeit bei ihnen. Dann wird regelmäßig überprüft, ob diese Ressourcen eine Weile nicht verwendet wurden, und wenn ja, werden sie entladen / zerstört (obwohl die ursprünglichen CPU-Daten, die zum Generieren der GPU-Ressource verwendet wurden, beibehalten werden, bis die tatsächliche Entität, die diese Komponenten speichert, zerstört ist oder bis diese Komponenten aus der Entität entfernt werden). Wenn die Anzahl der Ressourcen zunimmt und mehr Speicher verwendet wird, wird das System aggressiver beim Entladen / Zerstören dieser Ressourcen (die für einen alten Benutzer zulässige Leerlaufzeit).

Ich kann mir vorstellen, dass es sehr von Ihrem Spieldesign abhängt. Wenn Sie ein Spiel mit einem stärker segmentierten Ansatz mit kleineren Levels / Zonen haben, können Sie möglicherweise (und die einfachste Zeit, um die Frameraten stabil zu halten) alle für dieses Level erforderlichen Ressourcen im Voraus laden und sie entladen, wenn die Benutzer geht zum nächsten Level. Wenn Sie ein massives Open-World-Spiel haben, das auf diese Weise nahtlos ist, benötigen Sie möglicherweise eine viel ausgefeiltere Strategie, um zu steuern, wann diese Ressourcen erstellt und zerstört werden sollen, und es besteht möglicherweise eine größere Herausforderung, dies alles ohne Stottern zu tun. In meiner VFX-Domain ist ein kleiner Schluckauf bei den Frameraten keine so große Sache (ich versuche, sie innerhalb eines vernünftigen Rahmens zu beseitigen), da der Benutzer dadurch nicht überspielen wird.

All diese Komplexität ist in meinem Fall immer noch auf das Rendering-System beschränkt, und obwohl ich Klassen und Code zur Implementierung verallgemeinert habe, gibt es keine Bedenken hinsichtlich gültiger GL-Kontexte und Versuchungen, Globals oder ähnliches zu verwenden.

Drachenenergie
quelle
1

Anstatt die Referenzzählung in der ShaderProgramKlasse selbst durchzuführen, ist es besser, diese an eine Smart-Pointer-Klasse zu delegieren, wie z std::shared_ptr<>. Auf diese Weise stellen Sie sicher, dass jede Klasse nur einen einzigen Job zu erledigen hat.

Um zu vermeiden, dass Ihre OpenGL-Ressourcen versehentlich erschöpft werden, können Sie sie ShaderProgramnicht kopierbar machen (privater / gelöschter Kopierkonstruktor und Kopierzuweisungsoperator).
Um ein zentrales Repository mit ShaderProgramInstanzen zu verwalten, die gemeinsam genutzt werden können, können Sie Folgendes verwenden SharedShaderProgramFactory(ähnlich wie bei Ihrem statischen Manager, jedoch mit einem besseren Namen):

class SharedShaderProgramFactory {
private:
  std::weak_ptr<ShaderProgram> program_a;

  std::shared_ptr<ShaderProgram> get_progam_a()
  {
    shared_ptr<ShaderProgram> temp = program_a.lock();
    if (!temp)
    {
      // Requested program does not currently exist, so (re-)create it
      temp = new ShaderProgramA();
      program_a = temp; // Save for future requests
    }
    return temp;
  }
};

Die Factory-Klasse kann als statische Klasse, als Singleton oder als Abhängigkeit implementiert werden, die bei Bedarf übergeben wird.

Bart van Ingen Schenau
quelle
-3

Opengl wurde als C-Bibliothek konzipiert und weist die Merkmale von prozeduraler Software auf. Eine der OpenGL-Regeln, die aus einer C-Bibliothek stammen, sieht folgendermaßen aus:

"Wenn die Komplexität Ihrer Szene zunimmt, haben Sie mehr Handles, die um den Code herum übergeben werden müssen."

Dies ist ein Merkmal der opengl-API. Grundsätzlich wird davon ausgegangen, dass sich Ihr gesamter Code in der Funktion main () befindet und alle diese Handles über die lokalen Variablen von main () übergeben werden.

Die Konsequenzen dieser Regel lauten wie folgt:

  1. Sie sollten nicht versuchen, einen Typ oder eine Schnittstelle in den Datenübergabepfad einzufügen. Grund dafür ist, dass dieser Typ instabil ist und ständige Änderungen erfordert, wenn die Komplexität Ihrer Szene zunimmt.
  2. Der Datenübergabepfad sollte in der Funktion main () sein.
tp1
quelle
Wenn Sie interessiert sind, warum dies zu Abstimmungen führt. Als jemand, der mit dem Thema nicht vertraut ist, wäre es hilfreich zu wissen, was mit dieser Antwort falsch ist.
RubberDuck
1
Ich habe hier nicht herabgestimmt und war auch neugierig, aber vielleicht mainscheint mir die Idee, dass OGL auf dem gesamten Code basiert, der sich darin befindet , etwas schwierig (zumindest in der Formulierung).
Dragon Energy