Einführung
In dieser Reihe von Beiträgen werden wir uns mit der Portierung von Detroit: Become Human von PlayStation 4 auf den PC befassen .
Detroit: Become Human wurde im Mai 2018 auf PlayStation 4 veröffentlicht. Wir haben im Juli 2018 mit der Arbeit an der PC-Version begonnen und sie im Dezember 2019 veröffentlicht. Dies ist ein Abenteuerspiel mit drei spielbaren Charakteren und vielen Handlungssträngen. Es hat sehr hochwertige Grafiken und der größte Teil der Grafiktechnologie wurde von Quantic Dream selbst entwickelt.
Die 3D-Engine verfügt über hervorragende Funktionen:
- Realistische Darstellung von Zeichen.
- PBR-Beleuchtung.
- Hochwertige Nachbearbeitung wie Tiefenschärfe (DOF), Bewegungsunschärfe usw.
- Temporäres Anti-Aliasing.

Detroit: Mensch werden
Von Anfang an wurde die 3D-Engine des Spiels speziell für die PlayStation entwickelt, und wir hatten keine Ahnung, dass sie später andere Plattformen unterstützen würde. Daher war die PC-Version eine Herausforderung für uns.
- 3D Engine Lead Ronan Marshalot und 3D Engine Leads Nicholas Viseri und Jonathan Siret von Quantic Dream werden über das Rendern von Aspekten des portierten Spiels sprechen. Sie werden erklären, welche Optimierungen leicht von PlayStation 4 auf den PC portiert werden können und mit welchen Schwierigkeiten sie aufgrund von Unterschieden zwischen den Plattformen konfrontiert sind.
- Lou Kramer ist Technologieentwicklungsingenieur bei AMD . Sie hat uns geholfen, das Spiel zu optimieren, daher wird sie ausführlich über die ungleichmäßige Ressourcenindizierung auf dem PC und insbesondere auf AMD-Karten sprechen.
Auswählen einer Grafik-API
Wir hatten bereits eine OpenGL-Version der Engine, die wir in unseren Entwicklungstools verwendet haben.
Aber wir wollten das Spiel nicht in OpenGL veröffentlichen:
- Wir hatten viele proprietäre Erweiterungen, die nicht allen GPU-Herstellern offen standen.
- Der Motor hatte in OpenGL eine sehr geringe Leistung, obwohl er natürlich optimiert werden konnte.
- In OpenGL gibt es viele Möglichkeiten, verschiedene Aspekte zu implementieren. Daher war es ein Albtraum, verschiedene Aspekte auf allen Plattformen korrekt zu implementieren.
- OpenGL . , , .
Aufgrund des starken Einsatzes nicht verwandter Ressourcen konnten wir das Spiel nicht auf DirectX11 portieren. Es gibt nicht genügend Ressourcen-Slots, und es wäre sehr schwierig, eine anständige Leistung zu erzielen, wenn wir die Shader wiederholen müssten, um weniger Ressourcen zu verbrauchen.
Wir haben uns für DirectX 12 und Vulkan entschieden, die einen sehr ähnlichen Funktionsumfang haben. Vulkan würde es uns weiterhin ermöglichen, Unterstützung für Linux und Mobiltelefone bereitzustellen, und DirectX 12 würde Unterstützung für die Microsoft Xbox bereitstellen. Wir wussten, dass wir irgendwann Unterstützung für beide APIs implementieren müssen, aber es wäre sinnvoller, wenn sich der Port auf nur eine API konzentriert.
Vulkan unterstützt Windows 7 und Windows 8. Da wir Detroit machen wollten : Mensch werdenDies ist zu einem sehr starken Argument geworden, das möglichst vielen Spielern zugänglich ist. Die Portierung dauerte jedoch ein Jahr, und dieses Argument ist bereits unwichtig, da Windows 10 mittlerweile weit verbreitet ist!
Verschiedene Grafik-API-Konzepte
OpenGL und ältere DirectX-Versionen verfügen über ein sehr einfaches GPU-Steuerungsmodell. Diese APIs sind leicht zu verstehen und sehr gut zum Lernen geeignet. Sie weisen den Fahrer an, eine Menge Arbeit zu erledigen, die dem Entwickler verborgen bleibt. Daher wird es sehr schwierig sein, eine voll funktionsfähige 3D-Engine in ihnen zu optimieren.
Andererseits ist die PlayStation 4-API sehr leicht und sehr hardwarebezogen.
Vulkan ist irgendwo dazwischen. Es hat auch Abstraktionen, weil es auf verschiedenen GPUs läuft, aber Entwickler haben mehr Kontrolle. Angenommen, wir haben eine Aufgabe, die Speicherverwaltung oder den Shader-Cache zu implementieren. Da für den Fahrer weniger Arbeit übrig ist, müssen wir es tun! Wir haben jedoch Projekte auf PlayStation entwickelt. Daher ist es für uns bequemer, wenn wir alles steuern können.
Schwierigkeiten
Die PlayStation 4-CPU ist ein AMD Jaguar mit 8 Kernen. Offensichtlich langsamer als neuere PC-Hardware; Die PlayStation 4 bietet jedoch wichtige Vorteile, insbesondere einen sehr schnellen Zugriff auf Hardware. Wir glauben, dass die PlayStation 4-Grafik-API weitaus effizienter ist als alle APIs auf dem PC. Er ist sehr unkompliziert und verschwendet wenig Ressourcen. Dies bedeutet, dass wir eine große Anzahl von Draw Calls pro Frame erzielen können. Wir wussten, dass Anrufe mit hoher Auslosung auf langsameren PCs ein Problem sein können.
Ein weiterer wichtiger Vorteil war, dass alle Shader auf der PlayStation 4 im Voraus kompiliert werden konnten, was bedeutete, dass sie fast sofort geladen wurden. Auf einem PC muss der Treiber beim Booten Shader kompilieren: Aufgrund der großen Anzahl unterstützter GPU- und Treiberkonfigurationen kann dieser Vorgang nicht im Voraus ausgeführt werden.
Während der Entwicklung von Detroit: Become Human auf PlayStation 4 konnten Künstler einzigartige Shader-Bäume für alle Materialien erstellen. Dies führte zu einer wahnsinnigen Menge an Vertex- und Pixel-Shadern, sodass wir von Anfang an wussten, dass dies ein großes Problem sein würde.
Shader-Pipelines
Wie wir aus unserer OpenGL-Engine wissen, kann das Kompilieren von Shadern auf einem PC zeitaufwändig sein. Während der Produktion des Spiels haben wir einen Shader-Cache generiert, der auf dem GPU-Modell unserer Workstations basiert. Das Generieren eines vollständigen Shader-Caches für Detroit: Become Human hat eine ganze Nacht gedauert! Alle Mitarbeiter haben am Morgen Zugriff auf diesen Shader-Cache erhalten. Das Spiel wurde jedoch immer langsamer, da der Treiber diesen Code in den nativen Assembler-Code des GPU-Shaders konvertieren musste.
Es stellte sich heraus, dass Vulkan dieses Problem viel besser behandelt als OpenGL.
Erstens verwendet Vulkan nicht direkt eine Shader-Hochsprache wie HLSL, sondern eine Zwischen-Shader-Sprache namens SPIR-V. SPIR-V beschleunigt die Shader-Kompilierung und erleichtert die Optimierung für den Treiber-Shader-Compiler. In Bezug auf die Leistung ist es tatsächlich mit dem OpenGL-Shader-Cache-System vergleichbar.
In Vulkan müssen Shader mit der Form verknüpft sein
VkPipeline. VkPipelineSie können beispielsweise aus einem Scheitelpunkt- und Pixel-Shader erstellen. Es enthält auch Informationen zum Renderstatus (Tiefentests, Schablonen, Überblenden usw.) und Renderzielformate. Diese Informationen sind für den Treiber wichtig, damit er Shader so effizient wie möglich kompilieren kann.
In OpenGL kennt das Kompilieren von Shadern den Kontext der Verwendung von Shadern nicht. Der Treiber muss auf einen Draw-Aufruf warten, um die GPU-Binärdatei zu generieren. Daher kann der erste Draw-Aufruf mit einem neuen Shader auf der CPU lange dauern.
In Vulkan bietet die Pipeline
VkPipelineeinen Verwendungskontext, sodass der Treiber über alle Informationen verfügt, die zum Generieren der GPU-Binärdatei erforderlich sind, und der erste Draw-Aufruf keine Ressourcen verschwendet. Wir können auch VkPipelineCachebei der Erstellung aktualisieren VkPipeline.
Anfangs haben wir versucht,
VkPipelinesdas erste Mal zu erstellen, wenn wir es brauchen. Dies führte zu ähnlichen Verlangsamungen wie bei den OpenGL-Treibern. Dann wurde es VkPipelineCacheaktualisiert und die Bremse verschwand bis zum nächsten Ziehungsaufruf.
Dann haben wir vorausgesagt, dass wir
VkPipelinesbeim Booten etwas erstellen können, aber wenn es VkPipelineCacheirrelevant war, war es so langsam, dass die Strategie zum Laden im Hintergrund nicht implementiert werden konnte.
Letztendlich haben wir uns entschlossen,
VkPipelinebeim ersten Start des Spiels alles zu generieren . Dadurch wurden die Bremsprobleme vollständig beseitigt, aber jetzt stehen wir vor einer neuen Schwierigkeit: Die Generation VkPipelineCachehat sehr lange gebraucht.
Detroit: Become Human enthält ungefähr 99.500
VkPipeline! Das Spiel verwendet Forward-Rendering, sodass die Material-Shader den gesamten Beleuchtungscode enthalten. Daher kann das Kompilieren jedes Shaders lange dauern.
Wir haben verschiedene Ideen zur Optimierung des Prozesses entwickelt:
- , SPIR-V.
- SPIR-V SPIR-V.
- , CPU 100%
VkPipeline.
Jeff Boltz von NVIDIA schlug ebenfalls eine wichtige Optimierung vor, die sich in unserem Fall als sehr effektiv herausstellte.
Viele sind
VkPipelinesehr ähnlich. Beispielsweise können einige VkPipelinedie gleichen Scheitelpunkt- und Pixel-Shader haben, die sich nur in wenigen Renderzuständen unterscheiden, wie z. B. Schablonenparametern. In diesem Fall kann der Treiber sie als eine Pipeline behandeln. Wenn wir sie jedoch gleichzeitig erstellen, wird einer der Threads einfach inaktiv und wartet darauf, dass der andere die Aufgabe erledigt. Unser Prozess hat naturgemäß alle ähnlichen VkPipelinegleichzeitig übertragen. Um dieses Problem zu lösen, haben wir nur die Sortierreihenfolge geändert VkPipeline. Die "Klone" wurden am Ende platziert, und infolgedessen dauerte ihre Erstellung viel weniger Zeit.
Erstellungsleistung
VkPipelinesda gibt es große Unterschiede. Insbesondere hängt es stark von der Anzahl der verfügbaren Hardware-Threads ab. Bei AMD Ryzen Threadripper mit 64 Hardware-Threads kann dies nur zwei Minuten dauern. Auf schwachen PCs kann dieser Vorgang leider mehr als 20 Minuten dauern.
Letzteres war zu lang für uns. Leider bestand die einzige Möglichkeit, diese Zeit weiter zu verkürzen, darin, die Anzahl der Shader zu verringern. Wir müssten die Art und Weise, wie wir Materialien erstellen, ändern, damit möglichst viele davon gemeinsam genutzt werden. Für Detroit: Become Human war dies unmöglich, da die Künstler alle Materialien wiederholen mussten. Wir planen, im nächsten Spiel die richtige Materialinstanzierung zu implementieren, aber für Detroit war es zu spät : Mensch werden .
Indizierungsdeskriptoren
Um die Geschwindigkeit der Draw-Aufrufe auf dem PC zu optimieren, haben wir die Indizierung von Deskriptoren mithilfe der Erweiterung verwendet
VK_EXT_descriptor_indexing. Das Prinzip ist einfach: Wir können eine Reihe von Deskriptoren erstellen, die alle im Frame verwendeten Puffer und Texturen enthalten. Dann können wir über Indizes auf die Puffer und Texturen zugreifen. Der Hauptvorteil davon ist, dass Ressourcen nur einmal pro Frame gebunden werden, selbst wenn sie in mehreren Draw-Aufrufen verwendet werden. Dies ist der Verwendung ungebundener Ressourcen in OpenGL sehr ähnlich.
Wir erstellen Ressourcenarrays für alle Arten von verwendeten Ressourcen:
- Ein Array für alle 2D-Texturen.
- Ein Array für alle 3D-Texturen.
- Ein Array für alle kubischen Texturen.
- Ein Array für alle Materialpuffer.
Wir haben nur einen Hauptpuffer, der sich zwischen Zeichnungsaufrufen ändert (er wird als zirkulärer Puffer implementiert) und einen Deskriptorindex enthält, der sich auf den gewünschten Materialpuffer und die erforderlichen Matrizen bezieht. Jeder Materialpuffer enthält Indizes der verwendeten Texturen.

Dank dieser Strategie konnten wir eine kleine Anzahl von Deskriptorsätzen beibehalten, die allen Zeichnungsaufrufen gemeinsam sind und alle Informationen enthalten, die zum Zeichnen des Rahmens erforderlich sind.
Aktualisieren von Deskriptorsatzaktualisierungen
Selbst mit einer kleinen Anzahl von Deskriptorsätzen war die Aktualisierung immer noch ein Engpass. Das Aktualisieren eines Deskriptorsatzes kann sehr kostspielig sein, wenn er viele Ressourcen enthält. Zum Beispiel kann es in einem Frame von Detroit: Become Human mehr als viertausend Texturen geben.
Wir haben inkrementelle Aktualisierungen der Deskriptorsätze implementiert, um Ressourcen zu verfolgen, die im aktuellen Frame sichtbar und unsichtbar werden. Darüber hinaus wird dadurch die Größe der Deskriptor-Arrays begrenzt, da diese über genügend Kapazität verfügen, um die derzeit sichtbaren Ressourcen zu verarbeiten. Das Verfolgen der Sichtbarkeit verschwendet nur wenige Ressourcen, da wir keinen teuren Algorithmus zum Berechnen von Schnittpunkten mit verwenden
O(n.log(n))... Stattdessen verwenden wir zwei Listen, eine für den aktuellen Frame und eine für die vorherige. Durch Verschieben der verbleibenden sichtbaren Ressourcen von einer Liste in eine andere und Untersuchen der verbleibenden Ressourcen in der ersten Liste können Sie feststellen, welche Ressourcen in die Sichtbarkeitspyramide eintreten und aus dieser verschwinden.
Die bei diesen Berechnungen erhaltenen Deltas werden für vier Frames gespeichert. Wir verwenden die dreifache Pufferung. Um die Bewegungsvektoren von Objekten mit Skinning zu berechnen, ist ein weiteres Frame erforderlich. Der Deskriptorsatz muss mindestens vier Frames unverändert bleiben, bevor er erneut geändert werden kann, da er für die GPU weiterhin nützlich sein kann. Daher wenden wir Deltas auf Gruppen von vier Frames an.
Letztendlich reduzierte diese Optimierung die Aktualisierungszeit für Deskriptorsätze um ein bis zwei Größenordnungen.
Primitive schlachten
Die Verwendung der Deskriptorindizierung ermöglicht es uns, mehrere Grundelemente in einem einzigen Ziehungsaufruf mit Batch zu stapeln
vkCmdDrawIndexedIndirect. Wir verwenden, gl_InstanceIDum auf die gewünschten Indizes im Hauptpuffer zuzugreifen. Grundelemente können in Stapel gruppiert werden, wenn sie denselben Deskriptorsatz, dieselbe Shader-Pipeline und denselben Scheitelpunktpuffer haben. Dies ist sehr effektiv, insbesondere bei Tiefen- und Schattenpassagen. Die Gesamtzahl der Draw Calls wird um 60% reduziert.
Damit ist der erste Teil der Artikelserie abgeschlossen. In Teil 2 wird der Technologieingenieur Lou Kramer über die heterogene Ressourcenindizierung insbesondere auf PCs und AMD-Karten sprechen.