- Ein Entwickler mit OpenGL-Erfahrung lernte Vulkan zum ersten Mal und baute in nur etwa 3 Monaten zwei kleine Game-Demos sowie die wiederverwendbare EDBR-Engine
- Statt von Anfang an eine universelle Engine zu entwerfen, wurden zuerst kleine Spiele gebaut und anschließend nur die wirklich benötigten Teile in eine Engine überführt, was übermäßiges Design und bike-shedding reduzierte
- Die Engine umfasst 19k LoC, davon 6.7k LoC Grafikcode und 2k LoC für eine leichtgewichtige Vulkan-Abstraktion, und enthält Compute Skinning, CSM, PBR-Shading, MSAA, Post FX und UI-Rendering
- Mit
vk-bootstrap, Vulkan Memory Allocator,volk,VK_KHR_dynamic_rendering, Push Constants, Buffer Device Address und bindless Descriptors wurde der Vulkan-Boilerplate-Aufwand sowie die Nutzung von Descriptor Sets reduziert - Vulkan bot die Entfernung globaler Zustände, bessere Validierungsfehler, Shader-Debugging mit RenderDoc und mehr Konsistenz zwischen GPU und OS, aber explizite Synchronisierung muss weiterhin direkt behandelt werden
Die in 3 Monaten gebaute Vulkan-basierte EDBR-Engine
- EDBR (Elias Daler’s Bikeshed Engine) begann als Vulkan-Lernprojekt und entwickelte sich später zu einer kleinen Engine, die in weiteren Projekten wiederverwendet werden kann
- Der Code für Engine und Spiele ist im GitHub-Repository veröffentlicht
- Zum Zeitpunkt des Schreibens hatte der Code folgenden Umfang
- Engine selbst: 19k LoC
- grafikbezogener Code: 6.7k LoC
- leichtgewichtige Vulkan-Abstraktion: 2k LoC
- 3D-Katzenspiel: 4.6k LoC
- 2D-Platformer: 1.2k LoC
- Engine selbst: 19k LoC
- Ein Teil des nicht-grafischen Codes wie Eingabeverarbeitung und Audiosystem wurde aus einer früheren Engine übernommen, Grafik und mehrere Kernsysteme wurden jedoch neu geschrieben
- Die Einschätzung war, dass es besser passte, Vulkan neu zu implementieren, als es in eine bestehende OpenGL-Abstraktion einzupassen
Lernreihenfolge von Grafikprogrammierung bis Vulkan
- Wer gerade erst mit Grafikprogrammierung beginnt, fährt besser damit, mit OpenGL statt Vulkan zu starten, um nicht von der Komplexität überrollt zu werden
- Als Mindestziel wird empfohlen, ein texturiertes Modell auf dem Bildschirm darzustellen und einfache Blinn-Phong-Beleuchtung zu implementieren
- Einfaches Shadow Mapping hilft dabei zu lernen, wie man eine Szene aus einer anderen Perspektive und in ein anderes Render Target rendert und dabei eine Depth Texture sampelt
- Als Lernmaterialien für OpenGL werden folgende Ressourcen empfohlen
- Für das Lernen von Vulkan war vkguide am hilfreichsten; Einsteiger sollten es zunächst komplett durcharbeiten, auch wenn ein einfaches Spiel nicht sofort die Komplexität von „GPU driven rendering“ braucht
- Die Vulkan Lecture Series by TU Wien behandelt die Vulkan-Grundlagen, wobei insbesondere die Vorlesungen zur Synchronisierung hilfreich waren
- Im ersten Lernmonat wurden folgende Funktionen implementiert
- Laden von glTF-Modellen
- Compute Skinning
- Frustum Culling
- Shadow Mapping und Cascaded Shadow Maps
Warum Vulkan gewählt wurde und Vergleich mit WebGPU
- Ziel waren kleine 3D-Desktop-Spiele mit Fokus auf Windows und Linux, und wegen der Vorliebe für Open-Source-Technologien und offene Standards fiel die Wahl zwischen OpenGL und Vulkan
- OpenGL ist für kleine Spiele zwar ausreichend, doch es sind kaum neue Versionen zu erwarten, und auf macOS ist es deprecated, weshalb die Zukunft als unsicher angesehen wurde
- WebGPU wurde ebenfalls etwas gelernt, hatte aber folgende Einschränkungen
- noch nicht stabil und mit vergleichsweise wenigen Tutorials und Beispielen
- die WGSL-Syntax gefiel weniger als GLSL
- auf dem Desktop eher ein Wrapper über DirectX, Vulkan und Metal, wodurch sich RenderDoc-Captures je nach Plattform unterscheiden und WebGPU-Aufrufe nicht 1:1 nativen API-Aufrufen entsprechen
- keine bindless Textures und keine Push Constants
- WebGPU hat aber auch klare Vorteile
- bessere Validierungsfehler als OpenGL/WebGL und keine globalen Zustände
- in Teilen Vulkan ähnlich und daher hilfreich vor dem Lernen von Vulkan
- weniger Boilerplate nötig, um etwas auf den Bildschirm zu bringen, als bei Vulkan
- explizite Synchronisierung muss nicht direkt behandelt werden
- Spiele können im Browser ausgeführt werden
Ablauf des Frame-Renderings
- Ein Frame ist in mehrere Schritte unterteilt, die jeweils als Pipeline oder Pass implementiert sind
- In der Skinning-Phase werden Modelle mit Skeletal Animation per Compute Shader verarbeitet
- Eingaben sind ein nicht geskinntes Mesh und Joint-Matrizen
- Ausgabe ist ein Vertex Buffer, der in späteren Rendering-Schritten verwendet wird
- In den folgenden Schritten können statische und geskinte Meshes ähnlich behandelt werden
- In der CSM-Phase wird Cascaded Shadow Mapping mit einer 4096x4096-Depth-Texture und 3 Slices durchgeführt
- In der Geometry- + Shading-Phase werden Modelle gezeichnet und mithilfe von Shadow Map und Lichtinformationen schattiert
- PBR-Modelle entsprechen fast genau dem in Physically Based Rendering in Filament beschriebenen Ansatz
- Der Fragment Shader berechnet in einem Draw Call alle Lichter, die das jeweilige Mesh beeinflussen
- Alles wird zunächst in eine Multi-Sampled Texture gezeichnet und anschließend resolved
- Der Depth Resolve wird manuell im Fragment Shader durchgeführt
- Dabei werden alle Fragmente der Multi-Sample-Depth-Texture durchlaufen und der Minimalwert in eine nicht-MS-Depth-Texture geschrieben
- Die Post-FX-Phase wendet derzeit nur Depth Fog an; später sollen auch Tone Mapping und Bloom in diesem Schritt verarbeitet werden
- In der UI-Phase wird die Dialog-UI gezeichnet und in einem Draw Call verarbeitet
Bibliotheken zur Reduzierung von Vulkan-Boilerplate
vk-bootstrapreduziert Boilerplate bei der Vulkan-Initialisierung, etwa bei der Auswahl des Physical Device oder der Erstellung der Swapchain- Es ist kein vollständiger Wrapper für Vulkan-Funktionen, sondern wirkt sich vor allem auf die Initialisierungsphase aus
- Vulkan Memory Allocator sorgt dafür, dass Vulkan-Speicherallokation nicht direkt von Hand behandelt werden muss
volkvereinfacht das Laden von Extension-Funktionen- So lassen sich etwa Extension-Funktionen wie
vkSetDebugUtilsObjectNameEXTnutzen, ohne ihre Funktionszeiger selbst zu speichern
- So lassen sich etwa Extension-Funktionen wie
- Die Klasse
GfxDevicebündelt häufig genutzte Vulkan-Funktionen und -Objekte- Initialisierung des Vulkan-Kontexts
- Erstellung und Verwaltung der Swapchain
beginFrameundendFrame- Erzeugung von Images und Laden von Texturen
- Erstellung von Buffern
- Verwaltung des bindless Descriptor Set
GfxDevice.cppumfasst zum Zeitpunkt des Schreibens 714 Zeilen; stattVkDevice,VkQueue,VmaAllocatorund Ähnliches an viele Stellen weiterzureichen, ist es praktisch, einfach ein einziges Objekt zu übergeben
Shader-Build und Strategie zur Vermeidung von Descriptor Sets
- Als Shader-Sprache wurde aufgrund von OpenGL-Erfahrung GLSL gewählt
- Shader werden nicht zur Laufzeit, sondern im Build-Schritt nach SPIR-V kompiliert
- Der Code zum Laden von Shadern zur Laufzeit wird einfacher
- Keine Abhängigkeit von einem Shader-Compiler zur Laufzeit
- Shader-Fehler werden bereits im Build-Schritt gefunden
glslckann in CMakeDEPFILEangeben, sodass bei Änderungen an Shader-Includes betroffene Dateien automatisch neu kompiliert werden- In Vulkan müssen Uniforms in Descriptor Sets gebündelt werden, wodurch die Datenübergabe komplizierter wird als in OpenGL
- In der Implementierung wird die Nutzung von Descriptor Sets stark reduziert
- Es wird nur ein globales Descriptor Set für bindless Texturen und Sampler verwendet
- Der Rest wird größtenteils über push constants übergeben
- Mit Buffer Device Address werden Buffer-Adressen per Push Constants übergeben
Pipeline-Klassen und Dynamic Rendering
- Die Rendering-Schritte werden in Pipeline-Klassen wie
PostFXPipelineaufgeteilt - Jede Pipeline übernimmt normalerweise die folgenden Aufgaben
init: Shader laden,VkPipelineundVkPipelineLayoutinitialisierencleanup: Pipeline und Layout aufräumendraw: die pro Frame nötigen Eingaben entgegennehmen und den Draw Call ausführen
- Es wird angenommen, dass
drawzwischenvkCmdBeginRenderingundvkCmdEndRenderingaufgerufen wird - Welche Texture ein Render Pass beschreibt, interessiert die Pipeline intern nicht; das Render Target wird vom Aufrufer festgelegt
VK_KHR_dynamic_renderingwird durchgängig verwendet, Vulkan Render Passes und Subpasses werden nicht genutzt- Es wurde zwar gehört, dass Render Passes und Subpasses auf tile-basierten GPUs effizienter sind, aber mobile Unterstützung wird derzeit nicht berücksichtigt
- Dynamic Rendering macht die Implementierung deutlich einfacher
Einsatz von PVP, BDA und bindless Descriptors
- Für alle Meshes wird ein einziger Vertex-Typ verwendet
- Mit programmable vertex pulling lassen sich Vertex-Formatdefinitionen wie das OpenGL-VAO oder Vulkans
VkVertexInputBindingDescriptionundVkVertexInputAttributeDescriptionvermeiden - Mit buffer device address kann die Vertex Buffer-Adresse per Push Constants übergeben werden, ohne den Vertex Buffer an ein Descriptor Set zu binden
- Für Push Constants und Buffer wird das
scalar-Layout verwendet- Im Vergleich zu
std430ist die Ausrichtungsbehandlung einfacher, sodass es sich fast wie eine C++-Struktur behandeln lässt - Der Bedarf an Padding-Membern in C++-Strukturen sinkt
- Im Vergleich zu
- Bindless Descriptors werden in Form eines großen Descriptor Sets mit Arrays für Texturen und Sampler verwendet
- Wird eine neue Texture geladen, kommt sie in das
textures-Array und ihr Index wird als bindless Texture-ID verwendet - An den Shader wird die Texture-ID per Push Constants übergeben
- Wird eine neue Texture geladen, kommt sie in das
- Sampler werden von Images getrennt, gemeinsame Sampler werden beim Startup erzeugt und in das
samplers-Array gelegt - Auch im Material-Buffer werden bindless Texture-IDs verwendet
- Es wird nur die Material-ID per Push Constants übergeben, und der Fragment Shader schlägt dann im Material-Buffer nach
- So ist Texture-Zugriff pro Material mit nur einer Ganzzahl möglich, ganz ohne sperrige Descriptor Sets
- Als Referenzmaterial zu bindless Texturen wird Vulkan Bindless Texture empfohlen
Dynamische Daten, die pro Frame hochgeladen werden
- Für Daten, die pro Frame von der CPU zur GPU übertragen werden müssen, wird ein großes Array vorab allokiert und in jedem Frame ab Index 0 gefüllt
- Ein Beispiel: Alle Joint-Matrizen werden in einem großen
mat4-Array gespeichert, und pro Skinned Mesh wird der Startindex per Push Constants übergeben - Es gibt zwei Methoden
- Auf der GPU werden N Buffer angelegt und je nach Frame-in-Flight rotiert
- Auf der GPU gibt es nur einen Buffer, auf CPU-Seite aber N Staging Buffer
- In den meisten Fällen wird die erste Methode empfohlen
- Sie verbraucht mehr GPU-Speicher, erfordert aber keine manuelle Synchronisierung
- Wenn GPU-Speicher gespart werden muss, kann die zweite Methode nützlich sein
- Ein auffälliger Leistungsunterschied zwischen beiden Methoden wurde nicht festgestellt, aber beim Upload sehr großer Datenmengen pro Frame könnte es Unterschiede geben
Cleanup und Synchronisierung
- Das Deletion-Queue-Muster aus vkguide brachte in der Engine des Autors keinen großen Nutzen
- Denn pro Frame werden keine neuen Vulkan-Objekte allokiert oder zerstört
- Cleanup auf Basis von C++-Destruktoren war ebenfalls nicht bequem
- Wrapper-Klassen, Move-Konstruktoren und Move-Assignment sind nötig, was die Komplexität erhöht
- Es besteht die Gefahr, versehentlich ein mitten im Frame verwendetes Objekt durch Zerstörung eines Wrappers zu entfernen
- Derzeit werden Vulkan-Objekte durch explizite Aufrufe von
cleanupan einer Stelle bereinigt- Man kann den Aufruf leicht vergessen, aber beim Beenden weisen Vulkan-Validation-Fehler und VMA-Assertions auf fehlendes Cleanup hin
- Vulkan-Synchronisierung ist schwierig und muss explizit verwaltet werden
- OpenGL und WebGPU übernehmen die nötige Synchronisierung für Texture- oder Buffer-Lesezugriffe
- In Vulkan müssen Barriers manuell eingefügt werden, um Data Races zu vermeiden
- Aktuell werden Draws in Passes und Pipelines aufgeteilt, und dazwischen werden Barriers manuell eingefügt
- Zum Beispiel wird nach einem Skinning-Pass mit Compute Shader eine Barrier eingefügt, bevor ein Shadow-Mapping-Pass die Vertex-Daten liest
- Das ließe sich mit einem Render Graph automatisieren, aber derzeit ist man mit manueller Synchronisierung zufrieden
- Die Synchronization Validation Layer von
vkconfighilft beim Finden von Synchronisierungsfehlern
Trennung von Sprite-, Skinning- sowie Game-/Renderer-Logik
- Mit bindless Texturen lassen sich viele Sprites leicht ohne Vertex Buffer in einem Draw Call zeichnen
- Der Sprite-Vertex-Shader erzeugt über
gl_VertexIndexdie Vertex-Koordinaten und UVs des Quads - Alle Sprite-Draw-Calls werden als
SpriteDrawCommandinSpriteDrawBuffergesammelt- Transform
- UV-Bereich
- Farbe
- Texture-ID
- Shader-ID
- Der eigentliche Draw Call hat die Form
vkCmdDraw(cmd, 6, spriteDrawCommands.size(), 0, 0)- 6 Vertices pro Sprite
- so viele Instanzen wie Sprites
- Der Sprite-Renderer kann 10.000 Sprites in 315 Mikrosekunden zeichnen
- Compute Skinning nimmt bei Meshes mit Skelettanimation Eingabe-Vertices und Joint-Matrizen entgegen und erzeugt einen skinned Vertex Buffer
- Selbst drei Katzen mit demselben Mesh können unterschiedliche Animationen haben
- Der Ausgabe-Vertex-Buffer wird pro Mesh-Instanz benötigt
- Game-Logik und Renderer sind über Draw Commands getrennt
- Die Game-Logik verwendet entt
- Der Renderer kennt weder Entities noch Game Objects, sondern verarbeitet nur Lichter, Szenenparameter und Mesh-Draw-Commands
MeshDrawCommandenthältmeshId, Transform-Matrix, Bounding Sphere, einen Zeiger auf das skinned Mesh, den Startindex der Joint-Matrizen und die Angabe, ob Schatten geworfen werden
Laden von Szenen, UI, Dear ImGui
- Als Level-Editor wurde nicht eigens einer entwickelt, sondern Blender verwendet und als glTF exportiert.
- Wenn man einen Level-Editor selbst schreibt, kann das Monate oder Jahre dauern, daher spart dieser Ansatz Zeit.
- Über Node-Namen werden Prefab-Erzeugung und Physics-Shape festgelegt.
- Beispiel: Bei
Interact.Sphere.DiaryistInteractvor dem ersten Punkt der Prefab-Name. Spherewird vom Physics-System verwendet, um einen Sphere-Physics-Body zu erzeugen.- Auch
CapsuleundBoxkönnen verwendet werden; wenn nichts angegeben ist, wird die Physics-Shape aus den Mesh-Vertices erzeugt.
- Beispiel: Bei
- Komplexe Modelle werden nicht direkt in die Level-glTF eingefügt, sondern als
Empty->Arrows-Objekt platziert und dann mit einem Namen wieCat.NearStoreversehen.- Dabei wird das Prefab
Caterzeugt und mit dem TagNearStoreversehen.
- Dabei wird das Prefab
- Prefabs werden in JSON geschrieben und können externe glTFs sowie Bewegungs- und Physics-Informationen enthalten.
- Das UI-System ist von der Roblox-UI-API inspiriert.
- Ursprung
- relative Größe
- relative Position
offsetPosition,offsetSize- feste Größe
- größenabhängig vom Inhalt von Label/Bild
- Die Größe der UI-Elemente wird rekursiv berechnet, danach die Position bestimmt und anschließend vom Parent zu den Children gezeichnet.
- Dear ImGui wird für Entwicklungs- und Debugging-Tools verwendet.
- Da Dear ImGui in einem sRGB-Framebuffer falsch dargestellt wurde, wurde ein eigenes Dear-ImGui-Backend geschrieben.
- Es wurde nur der Rendering-Teil implementiert; Input-Event-Verarbeitung, Clipboard und andere Logik-/OS-Interaktionen übernimmt weiterhin das standardmäßige Dear-ImGui-SDL-Backend.
- Die Vorteile des eigenen Backends sind wie folgt.
- Es unterstützt bindless texture id, sodass sich Bilder mit
ImGui::Image(bindlessTextureId, ...)zeichnen lassen. - Lineare und nichtlineare Bilder können durch Übergabe des Formats korrekt dargestellt werden.
- Es lässt sich auf die gleiche Weise initialisieren und handhaben wie anderer Vulkan-Code der Engine.
- Es unterstützt bindless texture id, sodass sich Bilder mit
Verwendete Bibliotheken und der Effekt des Wechsels zu Vulkan
- Für die Physik wird Jolt Physics verwendet.
- Es kommt hauptsächlich für Collision Resolution und einfache Character-Movement zum Einsatz.
JPH::CharacterVirtualübernimmt grundlegende Character-Movement-Aufgaben gut.
- Als ECS wird entt verwendet.
- Durch die Nutzung einer externen Bibliothek statt eines selbstgebauten ECS reduziert sich der zu wartende Code.
- Für Audio werden openal-soft, libogg und libvorbis verwendet.
- Für Profiling wird Tracy verwendet.
- Es hilft dabei zu prüfen, wie wenig Zeit bestimmter Code tatsächlich verbraucht, und unnötiges Bike-Shedding zu vermeiden.
- Der Wechsel zu Vulkan brachte folgende Vorteile.
- Der globale Zustand von OpenGL entfällt, was Abstraktion vereinfacht.
- OpenGL-artige
shader.bind()-Aufrufe, State-Tracker und magisches RAII werden weniger benötigt. - Validation Errors sind umfangreicher als bei OpenGL.
- In RenderDoc lassen sich Vertex-Shader und Fragment-Shader direkt debuggen.
- Unterschiede im Verhalten zwischen GPU und OS fallen weniger stark ins Gewicht als bei OpenGL.
- Künftig können auch andere Shading Languages wie Slang oder Shady untersucht werden.
- Man erhält mehr Kontrolle über die einzelnen Aspekte der Grafik-Pipeline.
Was noch geplant ist
- Für die Zukunft sind folgende Arbeiten geplant.
- Signed-Distance-Field-Font-Unterstützung
- Laden vieler Bilder und parallele Mipmap-Erzeugung
- Bloom
- volumetrischer Nebel
- Animation Blending
- Render Graph
- Ambient Occlusion
- das Spiel fertigstellen
- Das Lernen von Vulkan war schwierig, aber nicht so schwer wie erwartet, und wurde zu einer Gelegenheit, Grafikprogrammierung und moderne APIs tiefer zu verstehen.
1 Kommentare
Hacker-News-Kommentare
Minimalismus ist sehr effektiv
Ich bin den umgekehrten Weg gegangen und leide deshalb ziemlich darunter. Ich baue einen Metaverse-Client in Rust, und gerade jetzt läuft auf einem anderen Bildschirm eine Szene, in der ein Avatar mit der Straßenbahn durch eine riesige Steampunk-Stadt fährt. Vor jedem neuen Pre-Release lasse ich das Ganze 12 Stunden lang laufen
Auf Vulkan habe ich WGPU und Rend3 aufgesetzt, und Rend3 bietet eine saubere API: Man erstellt Meshes, 2D-Texturen und Objekte, die Objekte referenzieren dann Meshes und Texturen und erscheinen auf dem Bildschirm. Dank Rusts Reference Counting ist die Verknüpfung konsistent und die Nutzung intuitiv
Aber mit mehr Schichten kommen Probleme. WGPU versucht Webbrowser, Vulkan, Metal, DX11 (kürzlich entfernt), DX12, Android und OpenGL zu unterstützen, braucht deshalb ein großes Entwicklerteam und ist schwer zu ändern. Auch die WGPU-API selbst ist im Wesentlichen Vulkan-ähnlich, sodass man sich weiterhin selbst um GPU-Speicherallokation und Synchronisierung kümmern muss
WGPU hat ein Kleinster-gemeinsamer-Nenner-Problem. Manche Plattformen unterstützen bestimmte Funktionen nicht, und gleichzeitige GPU-Speicherupdates aus mehreren Threads, die Vulkan unterstützt, kann WGPU nicht konfliktfrei handhaben. Für Spiele oder Clients mit großen Welten braucht man diese Funktion, um Inhalte auf die GPU zu bringen, ohne die Framerate zu ruinieren. Da die Parallelitätsbeschränkungen je nach Plattform unterschiedlich sind, kann die Performance durch Lock-Contention stark einbrechen
Rend3 sollte als brauchbarer Klebstoff für Synchronisierung und Allokation dienen, aber gerade Synchronisierung ist schwer allgemein zu lösen. Frustum Culling bringt große Performancegewinne, aber Occlusion Culling war wegen der Rechenkosten eher nachteilig. Transparenz ist ebenfalls mühsam, weil dafür Tiefensortierung nötig ist. Da ich an einer Welt mit vielen Fenstern arbeite, sind transparente Objekte unverzichtbar, damit man von außen hineinschauen kann
Die Leute rund um den Rust-3D-Stack scheinen ziemlich genervt von mir zu sein, weil ich seit drei Jahren ständig Druck mache, den Stack zu reparieren. Alle arbeiten ehrenamtlich, und Vulkan wird nur wegen Geld und Nutzerbasis gepflegt. Der Schöpfer von Rend3 hat kürzlich aufgegeben, also muss ich jetzt wohl selbst einsteigen und es reparieren. Nur wenige bauen mit WGPU etwas Komplexes; die meisten machen 2D-Spiele, die auch mit Flash gegangen wären, oder einfache statische 3D-Szenen. Kommerzielle Projekte bleiben weiter bei Unity oder UE5
Selbst wenn man direkt auf Vulkan geht, ist das ein großer Umstieg, weil man Synchronisierung, Allokation, Frustum Culling und Transparenz selbst schreiben muss
Zusätzlich hat auch Vulkano, ein Wrapper über Vulkan und Metal, das Kleinster-gemeinsamer-Nenner-Problem. Vulkan und Metal unterstützen beide gleichzeitige Updates von GPU-Assets, aber Vulkano erlaubt das nicht. Natürlich macht Apple es wieder auf seine eigene Weise
https://gpuweb.github.io/gpuweb/explainer/#multithreading
https://github.com/gpuweb/gpuweb/issues/354
OpenGL hat ebenfalls nie Threads unterstützt, daher ist das bei OpenGL nicht überraschend
Vor etwas mehr als einem Jahr wollte ich Vulkan lernen, aber ich will es nie wieder anfassen. Es ist wirklich unerquicklich, OpenGL abzuschaffen und durch etwas zu ersetzen, das selbst einfache Dinge absurd schwierig macht. Zum Beispiel braucht schon ein einzelner rotierender Würfel Hunderte Zeilen Code
OpenGL war auch nicht einfach, aber normale Menschen konnten die Grundlagen meist in relativ kurzer Zeit lernen. Man konnte in einer großen Buchhandlung ein Einsteigerbuch zur Grafikprogrammierung kaufen und nach ein, zwei Nachmittagen ein grundlegendes Rendering zum Laufen bringen. Vulkan mag in mancher Hinsicht besser sein, aber zu erwarten, dass man es schnell lernt, ist unrealistisch
Man stelle sich vor, neue Intel-/ARM-/AMD-Chips kämen heraus und man könne C oder C++ nicht mehr verwenden, sondern bekäme gesagt: „Die Unterstützung für Hochsprachen wird eingestellt, ab jetzt bitte nur noch Assembler verwenden, ihr habt mehr Kontrolle und es ist schneller.“ Das würde natürlich zu Recht als absurd gelten
Oft heißt es, Vulkan sei weniger eine High-Level-Grafik-API als vielmehr näher an einer „GPU-API“. Unter diesem Blickwinkel ist die Komplexität nicht überraschend; die Domäne selbst ist schwierig
Passender als der Vergleich mit modernen CPUs, auf denen man keine Hochsprachen mehr hätte und nur noch Assembler schreiben dürfte, ist eher der Unterschied zwischen Single-Thread-C/C++ und Multi-Thread-C/C++. Die Komplexität steigt stark an, und wenn man es nicht versteht, fliegt einem alles um die Ohren oder die Performance wird sogar schlechter, aber es ist auch ein praktikabler Weg nach vorn
OpenGL lässt sich im Allgemeinen auf Vulkan implementieren. Schade ist, dass der OpenGL-Standard nicht mehr aktiv weiterentwickelt wird, aber nichts ist für immer
Das Problem ist, dass OpenGL nicht mehr gut zu heutiger Hardware passt und bei naiver Nutzung sehr ineffiziente Performance liefert. Wenn man in OpenGL bis zu den Techniken geht, die den Treiber-Overhead beseitigen, ist Vulkan gar nicht mehr so viel schwieriger
Auch aktuelle Intel-/ARM-/AMD-Chips unterstützen C oder C++ nicht direkt. Man verwendet bereits Assembler oder Tools von Drittanbietern, die aus C/C++ übersetzen. Vulkan hat ebenfalls das Ziel, eine Low-Level-Standardschnittstelle zur GPU bereitzustellen, auf die man besser nutzbare Abstraktionen aufsetzen kann
https://github.com/google/angle
Viele Smartphones haben ANGLE inzwischen als einzige OpenGL-Unterstützung auf Vulkan-Treibern an Bord
Wenn man eine relativ einfache und portable moderne API möchte, kann man WebGPU über Rusts wgpu oder C++-dawn verwenden
Ich halte Vulkan für ausgezeichnet, aber sein Zweck ist es, fortgeschrittene GPU-Funktionen maximal auszunutzen. Wenn man fortgeschrittene GPU-Funktionen verwendet, kann es auch bessere Performance liefern als OpenGL
Wenn man keine fortgeschrittenen Rendering-Techniken anstrebt, wirkt OpenGL in der Regel weiterhin wie der empfohlene Weg
Es gibt auch heute noch viele 2D-, Low-Poly- und PS1-artige Grafikspiele, und für solche Spiele braucht man kein Vulkan
Vulkan ist ein Beispiel dafür, dass sich die AAA-Spielindustrie stark auf Rendering-Qualität und Optik verlagert hat. AAA-Studios rechtfertigen ihre Budgets mit sehr fortgeschrittenen Engines und Inhalten, aber da Spieler davon ermüden und merken, dass sie eher Gameplay als Grafik wollen, wächst der Markt für 2D-/Low-Poly-Spiele
Als Spieleentwickler möchte man sich wohl eher auf Gameplay und Funktionen konzentrieren als auf Rendering-Qualität
Es gibt keinen globalen Zustand, man kann die zur Laufzeit zu nutzende GPU auswählen, und die Fehlerbehandlung in OpenGL ist grauenhaft. Es gibt Validierungsebenen, das offizielle Artwork ist cool, und die Dokumentation ist hervorragend. Auch das asynchrone Hochladen von Daten auf die GPU aus einem zweiten Thread ist sinnvoller möglich, und es gibt fortgeschrittene GPU-Funktionen wie Mesh Shader oder RTX
Würden Verbraucher Indie-Spiele mit AAA-Grafik ablehnen? Wohl kaum. Dass es davon wenige gibt, liegt daran, dass sie finanziell schwer tragfähig sind und der Markt groß genug ist, um stärker stilisierte und weniger hochfidele Grafik zu akzeptieren
Gleichzeitig bekommt man Zugang zu modernen Funktionen wie Compute Shadern, die in WebGL nicht nutzbar sind, und es gibt nicht so viel Legacy-Ballast wie bei OpenGL, wo sich mehrere Wege für dieselbe Aufgabe angesammelt haben. Der Hauptvorteil ist, dass es neu ist, aber deshalb gibt es auch viel weniger Tutorials, und das ist ein ziemlich gravierender Nachteil
Es gibt endlos viele Frameworks und Engines, aber die meisten sind unvollständig, und alle haben starke Meinungen zur Code-Struktur. So nach dem Motto: „Baue einen Scene Graph für Callbacks. Nein, schreibe Entities und Komponentenklassen und Objekte. Moment, jetzt ist alles Immediate Mode, funktional und zustandslos.“
Dazu kommt noch das Plattform-Chaos. Nimmt man Push-Benachrichtigungen für Mobile Games, In-App-Käufe und die verpflichtende Signierung über Xcode dazu, ist es ein völliges Durcheinander. Es gibt einen Grund, warum Unity Marktanteile hat, und der ist nicht, dass es großartige Software wäre. Cross-Platform jenseits schicker Webviews ist immer noch ein enormer Schmerz
Wenn man ohnehin ständig GPU-/Treiber-spezifischen Code schreiben muss, verliert die Abstraktionsschicht ihren Sinn. Es ist besser, Low-Level-Treiber und High-Level-Bibliotheken zu trennen
Dieser Artikel enthält viel guten Rat. Besonders hervor stach „Implementiere es nicht, wenn du es jetzt nicht brauchst“.
Genau daran gerät man ständig mit Junior-Programmierern aneinander, die zwar schon ein paar Jahre Erfahrung haben, aber noch im Wachstum sind. Sie hängen oft an „Best Practices“ und angesagten, glänzenden neuen Tools, tun sich aber schwer damit, vom tatsächlich zu lösenden Problem auszugehen und sich auf das Minimum zu konzentrieren, das nötig ist, um es zu lösen
In mittelgroßen bis großen Organisationen ist das meist nicht so. In der Regel geht man zur nächsten Aufgabe über und hat kaum Zeit, noch einmal zurückzukommen. Leider muss man es deshalb von Anfang an richtig bauen und den Spielraum für Bugs oder Nebenwirkungen klein halten
In zu vielen Codebases wurden neue Features überhastet eingebaut, bis der Code zu einem Minenfeld wurde, in dem man für jede kleine Änderung alle Funktionen manuell prüfen und den gesamten Anwendungskontext im Kopf haben musste
Meiner Erfahrung nach ist die früher gebaute generische Lösung ohnehin völlig ungeeignet, wenn später neue Anforderungen kommen, sodass man sie am Ende doch neu bauen muss. Man sollte das Problem vor sich lösen, nicht ein unbekanntes Problem der Zukunft
Daher überrascht es nicht, dass Leute bei jeder sich bietenden Gelegenheit hier und da etwas selbst schreiben wollen. Solche digitalen Klempnerarbeiten laugen einen aus, also möchte man, um etwas länger bei Verstand zu bleiben, gelegentlich auch etwas Angenehmeres dazwischen schieben
Die Seite scheint unter dem Ansturm zusammengebrochen zu sein, also hier ein Cache: https://web.archive.org/web/20240606103630/https://edw.is/le...
Großartiger Artikel. Ich habe Vulkan selbst gelernt, um eine Engine für wissenschaftliche Datenvisualisierung zu bauen: https://datoviz.org/ Sie ist noch ziemlich experimentell, und bald soll eine neue Version erscheinen
Ich hatte vorher schon OpenGL-Kenntnisse, aber Vulkan zu lernen war wirklich schwer. Vor 5 Jahren waren die Lernmaterialien auch nicht besonders gut. Trotzdem habe ich es gewagt, und es hat großen Spaß gemacht
Es hat Monate gedauert, die Rolle der Dutzenden Abstraktionen zu verstehen, und dabei habe ich auch einen kleinen Wrapper geschrieben, um Vulkan etwas weniger schmerzhaft zu machen: https://datoviz.org/api/vklite/
Dieser Wrapper unterstützt nur einen Teil der Funktionen, die für wissenschaftliche Visualisierung am wichtigsten sind
Mit Vulkan bessere Performance als mit OpenGL zu erreichen, ist nicht einfach. Im Vulkan-Treiber fehlen ungefähr 20.000 Zeilen Code, die beim OpenGL-Treiber die Einrichtung der Rendering-Pipeline und der Render-Targets übernehmen
Dieser Code existiert bereits im OpenGL-Treiber und wurde seit über 20 Jahren von den besten Leuten der Branche optimiert
Wenn man also naiv etwas auf Vulkan zusammenschraubt, das nur ungefähr dem entspricht, was OpenGL standardmäßig mitliefert, bekommt man nicht auf magische Weise gute Performance. Es braucht mehr Arbeit, und die echten Probleme fangen erst an, etwa die richtigen Fences und Synchronisationsprimitive einzubauen
Erst wenn man wirklich weiß, was man tut, und Rendering mit guter Parallelität und korrekter Synchronisation ausführen kann, darf man von den Performance-Vorteilen von Vulkan träumen
Als Hobbyentwickler nutze ich wegen der Einfachheit OpenGL ES3. Für mich ist das schon gut genug, und ich habe Dringenderes zu tun, als mich mit lästigen Vulkan-Vertex-Descriptor-Descriptor-Descriptorn herumzuschlagen
Meine Engine gibt es übrigens hier: https://github.com/ensisoft/detonator
Ich habe gehört, dass Vulkan inzwischen Bindless Textures erlaubt, also ist das Descriptor-Chaos vermutlich weniger schlimm als früher
Vulkan ist reizvoll, aber die hohen Anfangskosten möchte ich nicht bezahlen
Und dann ersetzt man diese Bibliothek nach und nach durch Routinen, die für den jeweiligen Anwendungsfall optimiert sind
Es ist gut, dass es mehr Vulkan-Material gibt, aber auch hier tritt dasselbe Problem auf, das ich bei allen Vulkan-Materialien gesehen habe, die irgendetwas auf dem Bildschirm anzeigen.
Alle führen noch vor dem einfachsten Fall eine weitere Abstraktionsschicht über Vulkan ein. Ständig heißt es, man solle
vk-bootstrap,volk,vmaoder andere Bibliotheken verwenden.Ich weiß nicht, ob es irgendwo überhaupt Material gibt, das Beispiele mit manueller Speicherverwaltung zeigt. Es scheint nur die Wahl zu geben,
vmazu benutzen oder sich direkt durch die Spezifikation zu graben. Ist es wirklich zu viel verlangt, nach dem grundlegendsten Beispiel zu fragen, das außer dem Vulkan SDK selbst keinerlei zusätzliche Bibliotheken einbindet?Die meisten Spiele haben grob drei Lebensdauern: permanent/beim Start, pro Level und pro Frame.
Diese Lebensdauern sind verschachtelt, daher kommt man mit einem einzigen Stack-Allocator ziemlich weit. Wenn ein Frame oder ein Level endet, setzt man einfach auf die Startposition zurück.
Es gibt auch komplexere Muster, aber schon dieser Ansatz ist ziemlich nützlich und kann sowohl auf der CPU als auch auf der GPU verwendet werden
Insofern ist der breite Einsatz von Bibliotheken darüber völlig normal. Das ist so, als würde man moderne Software nicht direkt gegen Systemaufrufe schreiben
Wenn man die Vulkan-Initialisierungsanleitung von irgendwem mit dem Code in den Repositories der Khronos Group vergleicht und dann die Vulkan-1.3-Spezifikation liest, wird spätestens in dem Moment klar, dass sie gescheitert sind, in dem man merkt, dass man die Spezifikation nicht der Reihe nach, sondern durcheinander lesen muss, um überhaupt etwas zu tun.
Sie sind gescheitert. Auch nach anderen Maßstäben ist das schlechte Arbeit. Nur beschweren sich Experten nicht allzu sehr, weil man es einmal macht und den Großteil danach wieder vergessen kann.
In einem anderen Kommentar in diesem Thread habe ich einen Teil des Quellcodes hinterlassen, mit Kommentaren zu Kapiteln und Abschnitten der Spezifikation. Das ist eine allgemeine Implementierung, die sich zusammen mit SDL usw. verwenden lässt.
Zum Zeitpunkt des Schreibens ist der Standardansatz, VMA und Volk zu verwenden, die im offiziellen Vulkan SDK enthalten sind. Das allein sagt schon genug über den aktuellen Zustand aus
Ich habe über einige Jahre hinweg immer wieder versucht, Vulkan zu lernen. Früher kannte ich OpenGL ES 2 und 3 ziemlich gut.
Eine der schwierigen Stellen war, zu verstehen, wie man es in einer echten Engine verwendet und nicht nur in Samples. Viele Samples allozieren entweder genau so viel wie nötig oder gleich Hunderte, damit es nie knapp wird.
Beim Lernen von DirectX hat mir Microsofts MiniEngine geholfen. Sie war nicht zu komplex und hatte Dinge wie
DescriptorAllocator, das die Descriptor-Allokation verwaltet. Ich frage mich, ob es für Vulkan etwas Ähnliches gibt.Eine weitere Schwierigkeit ist zu wissen, wie man gute Abstraktionen für Materialien, Meshes und Render-Reihenfolge baut. Gibt es eine gute Engine oder ein Framework, das man sich ansehen kann, um über Tutorials hinauszukommen?
Bei der Allokation von Descriptor-Sets ergibt für mich nur ein Muster Sinn. Man muss davon ausgehen, dass Pools kurzlebig sind und in großer Zahl entstehen. Wenn die Allokation aus dem aktuellen Pool fehlschlägt, erstellt man einfach einen neuen Pool, statt selbst Zähler für die Anzahl der Descriptors zu pflegen. Der Standard erlaubt allerlei Pool-Verhalten, das sich von einfachem Zählen unterscheidet. Alte Pools kann man verwerfen, nachdem der letzte Command Buffer, der auf sie verweist, abgeschlossen ist
Pipeline-Barriers und Image-Layouts sind wirklich lästig. Am besten abstrahiert man sie als Schicht, die die letzte Verwendung und das letzte Format aller Ressourcen verfolgt und die nötigen Barriers ergänzt. Das kann komplex werden, ist aber den Aufwand wert, sobald kompliziertere Situationen auftreten, etwa optionale Passes oder veränderbare Pass-Reihenfolgen
Meshes, Materialien und Render-Reihenfolge lassen sich in einem einzelnen HN-Kommentar schwer zusammenfassen und hängen stark von der Wahl des Rendering-Algorithmus ab. Ich halte den enormen Aufwand, der für eine wirklich stark verallgemeinerte Lösung nötig wäre, nicht für lohnend
Wer sich als eher gelegentlicher Leser fragt, was man braucht, um in Vulkan 1.3 „Hello, Triangle!“ zu schreiben, kann sich das hier ansehen: https://github.com/Planimeter/game-engine-3d/blob/main/src/g...
vk-bootstrapist es immerhin mit 600 Zeilen Code etwas besser: https://github.com/charles-lunarg/vk-bootstrap/blob/main/exa...Die Vulkan-Initialisierung und das grundlegende Swapchain-Management sind sehr ausschweifend, aber wenn man es einmal gemacht hat und sich später komfortable Abstraktionen für die Pipeline-Erzeugung und -Verwaltung baut, wird es deutlich besser