- paraLLEl-GS bildet den GS (Graphics Synthesizer) der PS2 per Vulkan Compute nach und adressiert Genauigkeits- und Upscaling-Grenzen, die GSdx, fast 20 Jahre lang der De-facto-Standard, hinterlassen hat
- Der GS arbeitet auf Basis von 4 MiB VRAM und hoher Fillrate, ist aber wegen Eigenschaften der Pixel-Pipeline wie Destination-Alpha-Test, bedingtem Blending sowie Alpha-/Farbwerten über 1,0 mit üblichen Grafik-APIs schwer exakt nachzubilden
- Die Implementierung verfolgt VRAM auf Seiten- und 256-Byte-Block-Ebene und kombiniert CLUT-Snapshots, Texture-Unswizzling und Render-Pass-Batching, um Framebuffer-/Texture-Feedback zu behandeln
- An Beispielen wie Tales of the Abyss, Final Fantasy X, MGS2, Valkyrie Profile 2 und Shadow of the Colossus werden Probleme bei Upscaling, UI, hochpräzisem Blending und Texture-Feedback verglichen; auch 8x- und 16x-SSAA-Szenen werden behandelt
- Die aktuelle Verifikation stützt sich vor allem auf die Wiedergabe von GS dumps; es gibt zwar PCSX2-Hack-Patches und mkfifo-Livetests, aber um echte Nutzer zu erreichen, ist eine Emulator-Integration nötig
Ziele und Ausgangspunkt von paraLLEl-GS
- paraLLEl-GS ist ein Projekt zur Emulation des PlayStation-2-GS (Graphics Synthesizer) mit Vulkan Compute
- Die Arbeit desselben Autors aus dem Jahr 2020, paraLLEl-RDP, implementierte den N64-RDP mit Vulkan Compute und zielte auf Ergebnisse nahe an Bitgenauigkeit und auf Upscaling ab, gemessen an Angrylion
- Auf der PS2 blieb GSdx etwa 20 Jahre lang praktisch der Stand der Technik
- Um 2014 gab es einen Versuch einer OpenCL-basierten Compute-Implementierung des PS2-GS, der jedoch nicht abgeschlossen wurde und heute auch im Upstream-Repository schwer zu finden ist
- Die Gründe, auf der PS2 Compute-Shader-Rasterisierung zu verwenden, sind schwächer als beim N64
- PCSX2 verfügt über einen gut optimierten Software-Renderer und relativ robuste grafikbasierte Renderer
- Der Software-Renderer unterstützt kein Upscaling
- Grafikbasierte Renderer zeigen insbesondere beim Upscaling diverse Bugs und Glitches
- paraLLEl-GS konzentriert sich weniger auf Bitgenauigkeit gegenüber der Hardware als darauf, offensichtliche Genauigkeitsprobleme zu vermeiden
- Auch der GSdx-Software-Renderer wirkt nicht wie eine bitgenaue Hardware-Implementierung, sodass direkt vergleichsbasierte Tests schnell an Grenzen stoßen
Warum der PS2-GS schwierig ist
- Der GS war im Jahr 2000 ein Gerät mit einer theoretischen Fillrate und Bandbreite von mehr als einer Milliarde Pixeln pro Sekunde
- Der VRAM ist mit 4 MiB klein, aber darauf ausgelegt, über mehrere DMA-Engines kontinuierlich gestreamt zu werden
- Die Pixel-Pipeline selbst ist in mancher Hinsicht einfacher als der N64-RDP
- Eine einzige Textur
- Ein Single-Cycle-Combiner
- Sehr einfaches Anti-Aliasing
- Viele Funktionen sind mit gewöhnlichen Grafik-APIs schwer umzusetzen
- Blending über 1,0 hinaus: Ein vom PS1 übernommenes Verhalten, bei dem 0x80 wie 1,0 behandelt wird und Werte bis 0xff darstellbar sind
- Destination-Alpha-Test: Das Ziel-Alpha kann ähnlich wie ein Stencil verwendet werden
- Bedingtes Blending: Blending kann abhängig vom Alpha bedingt abgeschaltet werden
- Alpha-Korrektur: Vor dem Schreiben von Alpha kann das MSB per OR gesetzt werden, um den Wert zwangsweise nahe an 1 zu bringen
- Teilweises Verwerfen beim Alpha-Test: Es sind Verhaltensweisen möglich, bei denen nur Farbe verworfen wird, Depth-Schreibvorgänge aber erhalten bleiben
- AA1: Ein coverage-to-alpha-Verfahren, das mit pixelweiser Steuerung von Depth-Schreibvorgängen verflochten ist
- 32-Bit-Festkomma-Z: D32_UINT-Unterstützung existiert technisch, reale Einsatzfälle wurden bisher aber nicht gesehen
- Ohne programmierbares Blending wären auf Immediate-Mode-Desktop-GPUs ROVs oder pixelweise Barrieren nötig, was die Performance stark verschlechtert
- Die Compute-Implementierung baut einen eigenen tile-basierten Deferred Renderer (TBDR), um diese Einschränkungen zu umgehen
Rasterregeln, Vertex-Queue und Speicherlayout
- Die Primitives des GS werden im Clip-Space vergleichsweise gewöhnlich bereitgestellt
- VU1 führt Transformation und Clipping aus und gibt mehrere Vertex-Attribute an den GS aus
- Koordinaten und Attribute haben GS-spezifische Formate
- X/Y: 12.4 unsigned fixed-point
- Z: 24-Bit- oder 32-Bit-uint
- FOG: 8-Bit-uint
- RGBA: 8-Bit-Werte für Beleuchtung pro Vertex
- STQ: perspektivisch korrigierte Texturkoordinaten
- UV: nicht normalisierte 12.4-Festkomma-Koordinaten ohne Perspektivkorrektur
- Die Rasterregeln ähneln dem D3D9-Stil
- Dreiecke verwenden wie moderne GPUs die Top-Left-Rasterregel
- Pixelzentren liegen wie bei D3D9 auf Ganzzahlkoordinaten
- Linien verwenden den Bresenham-Algorithmus, was Upscaling erschwert; sie müssen als rect oder parallelogram angenähert werden
- Punkte werden auf den nächstgelegenen Pixel gesnappt
- Sprites sind einfache Quads mit zwei Koordinaten
- Die GS-Vertex-Queue ähnelt dem Immediate Mode von OpenGL 1.0
- RGBA, STQ und verschiedene Register werden gesetzt, und ein Schreibzugriff auf das XYZ-Register bildet einen Vertex-„Kick“
- TRIANGLE_FAN wird ebenfalls unterstützt
- PS2-Pixelkoordinaten sind in Seiten angeordnet
- Eine Seite hat 8 KiB
- Eine Seite ist in 32 Blöcke unterteilt
- Bei 32-Bit-RGBA entspricht eine Seite 64×32 Pixeln, und die 32 Blöcke zu 8×8 werden in Z-Order geswizzelt
- Beim Rendern in 24-Bit-Farbe oder 24-Bit-Depth können Texturen in den verbleibenden oberen 8 Bit liegen
- Die Formate 8H, 4HL und 4HH sind nützlich für 8-Bit- und 4-Bit-Paletten
Texturen, CLUT und TEXFLUSH
- Das GS-Texturing kombiniert Teile, die modernen APIs ähneln, mit eigenwilligen Besonderheiten
- Texelzentren liegen wie bei modernen APIs auf half-pixel
- Die Subtexel-Präzision scheint 4 Bit statt 8 Bit zu betragen
- Der bilineare Filter ist ein gewöhnlicher bilinearer Filter und keine Spezialkonstruktion wie der 3-Point-Filter des N64
- Spezielle Adressierungsmodi erhöhen den Implementierungsaufwand
- REGION_CLAMP kann Clamping auf beliebige Bereiche innerhalb eines Texturatlas anwenden
- REGION_REPEAT kann pro Koordinate Bitoperationen wie
(u & MASK) | FIXanwenden und ist daher schwieriger zu implementieren
- Mipmapping berechnet den LOD nicht aus Ableitungen, sondern aus dem log2 des interpolierten Q-Factors und einem Skalierungsfaktor
- Für eine Compute-Implementierung ist es ein großer Vorteil, nicht von Ableitungen abhängig zu sein
- Dieses Verfahren kann Funktionen wie anisotropic filtering nicht unterstützen
- CLUT ist ein 1-KiB-Cache, der die aktuelle Palette enthält
- Um ihn nutzen zu können, muss explizit aus dem VRAM in den CLUT-Cache kopiert werden
- Bei 32-Bit-Farbe passt eine Palette mit 256 Farben hinein
- Bei 16 bpp passen 32 Paletten mit je 16 Farben hinein
- TEXFLUSH ist ein expliziter Befehl, der der Synchronisierung und Invalidierung des Texture-Cache nahekommt
- Anfangs sollte TEXFLUSH als Grundlage für Hazard-Tracking dienen, letztlich musste es aber ignoriert werden
- Spiele vergaßen TEXFLUSH oder riefen es viel zu oft auf
- Die finale Implementierung wählt einen minimal caching-Ansatz
- Sie nimmt an, dass es keinen Cache gibt, und verfolgt Hazards direkt
- Für Feedback-Loops wird eine separate Ausnahmebehandlung erwogen
- GSdx scheint in dieselbe Richtung zu gehen
Vulkan-Compute-Rendering-Pipeline
- Die Implementierungspipeline setzt Synchronisierung zwischen den einzelnen Schritten voraus
- VRAM-Kopie der CPU mit der GPU synchronisieren
- VRAM-Upload oder local-to-local copy ausführen
- CLUT-Cache aus dem VRAM aktualisieren
- VRAM in ein VkImage unswizzeln und so direkt samplebar machen
- Rendering ausführen
- GPU-VRAM-Kopie zurück zur CPU synchronisieren
- Typisches Spielverhalten passt gut zu dieser Pipeline
- Texturen in den VRAM hochladen
- Paletten in den VRAM hochladen
- CLUT-Cache aktualisieren
- Mit Textur zeichnen
- Bei Bedarf aus dem VRAM in ein VkImage unswizzeln
- Primitive-Batches als Render-Passes organisieren
- Wenn es keine rückwärts gerichteten Hazards gibt, können Batching und Synchronisierung verzögert werden
- Um bei einem Renderer dieser Art Performance zu erzielen, ist Batch-Erhaltung wichtig
- Wichtige Hazard-Fälle werden separat behandelt
- Erneutes Kopieren in VRAM, in den bereits ein copy geschrieben hat
- copy in VRAM, den eine gesampelte Textur oder CLUT gelesen hat
- Sampling eines gerenderten Bereichs als Textur
- copy in gerenderten VRAM
Seiten-Tracking und Texture-Cache
- Der schwierigste Teil der GS-Emulation ist der Umgang mit read-after-write- und write-after-write-Hazards im VRAM
- Der 4-MiB-VRAM wird zuerst in Seiten aufgeteilt
- Seiten sind die Einheit von Framebuffer und Depth-Buffer und damit die sinnvollste Tracking-Grundlage
- Pro Seite werden folgende Zustände verfolgt
- pending frame buffer write
- pending frame buffer read
- Texturen und VRAM-copy haben 256-Byte-Alignment, daher wird eine u32-Bitmaske für die 32 Blöcke verwendet
- VRAM copy write
- VRAM copy read
- pending read in den CLUT-Cache oder ein VkImage
- Blöcke, die von irgendeinem write überschrieben wurden
- Beim Rendern in 24-Bit-Farbe und gleichzeitigem Sampling der oberen 8 Bit als Textur kann es sein, dass kein Hazard besteht
- Dafür werden Framebuffer write mask und Texture read mask getrennt verfolgt
- Jede Seite besitzt eine Liste verknüpfter VkImages
- Wird eine Seiten-Textur invalidiert, wird das Image zerstört und muss erneut aus dem VRAM unswizzelt werden
- Eine Textur kann sich über mehrere Seiten erstrecken; wird auch nur eine davon überschrieben, wird die Textur invalidiert
- Einfaches und konservatives Tracking allein funktioniert bei PS2-Spielen nicht
- Tracking auf Ebene von 256-Byte-Blöcken und die Berücksichtigung von write/read masks sind wichtig
- Durch POT-Texturen und nicht genutztes REGION_CLAMP können False Positives entstehen
- Wenn etwa ein 512×448-Render-Target als 512×512-Textur eingerichtet wird, kann der ungenutzte Bereich wie ein Hazard aussehen
- Die Implementierung verwendet einen Workaround, der potenzielle Hazards in dieser „red zone“ ignoriert
CLUT-Batching und Texture-Unswizzling
- Um Texture-Uploads zu batchen, müssen auch CLUT-Uploads mitgebatcht werden
- Die Implementierung hält 1024 Kopien der CLUT als Snapshot-Ringpuffer vor
- Eine Workgroup durchläuft die Updates und schreibt sie in ein SSBO
- Das ähnelt TMEM-Updates beim N64-RDP, CLUT-Updates sind aber deutlich einfacher
- In Vulkan wird ein neues VkImage alloziert, aus VkDeviceMemory suballoziert und anschließend per Compute-Shader unswizzelt
- Vulkan specialization constants werden genutzt, um Texturformat und Swizzle-Logik zu spezialisieren
- Auch das Spezialverhalten von REGION_REPEAT wird in der Unswizzle-Phase behandelt
- Danach muss der ubershader diesen Fall seltener berücksichtigen, um bilineares Filtering manuell durchzuführen
- Auch Render-Targets gehen über das VRAM-SSBO zur Textur und wieder zurück
- Versuche, Render-Targets direkt als Textur weiterzureichen, wurden als zu fehler- und ausnahmereich eingeschätzt
Triangle-Setup, Binning und Ubershader
- paraLLEl-GS ist wie paraLLEl-RDP ein tile-basierter Renderer
- Vor dem Binning wird das Triangle-Setup ausgeführt; die Eingabe ist in drei Arrays unterteilt
- Position
- Attribute pro Vertex
- Attribute pro Primitive
- Der Rasterizer ist baryzentrisch basiert und stark von Fabian Giesens Artikeln zur Grafikpipeline sowie dem in Pineda 1988 beschriebenen Verfahren zur parallelen Rasterisierung beeinflusst
- Der echte PS2-GS ist ein DDA-, also Scanline-Rasterizer; da keine bitgenaue Beschreibung des GS-DDA bekannt ist, wird ein baryzentrischer Ansatz verwendet
- Für die Implementierung von wide lines und sprites werden auch Parallelogramme unterstützt
- inv_area wird mit einem eigenen Festkomma-RCP berechnet
- Standard-GPU-RCP ist je nach Implementierung wenig konsistent und hat nur etwa 22,5 Bit Genauigkeit, daher wird er vermieden
- Der eigene RCP zielt auf etwa 24,0 Bit Genauigkeit ab
- Binning verwendet üblicherweise 32×32-Pixel-Blöcke
- Die maximale Zahl von Primitives pro Render-Pass beträgt wegen u16-Indizes 64k
- Die beobachteten großen Render-Passes lagen meist im Bereich von 10k bis 30k Primitives
- Der PS2-GS hat eine hohe Fillrate und niedrige Komplexität pro Pixel, wodurch ein reiner ubershader möglich ist
- Anders als beim N64 kann bindless genutzt werden, was auch die Texturing-Komplexität reduziert
- Der ubershader nutzt early Z, deferred on-tile shading und lazy pixel shading
- Tatsächliches Shading wird nur ausgeführt, wenn ein Pixel von einem vorherigen Ergebnis abhängt
- Alpha test, color write mask, alpha blending und Ähnliches erzeugen solche Abhängigkeiten
- Die finale Framebuffer-Farbe und Depth werden in ein SSBO geschrieben, was die GPU-Bandbreitennutzung reduziert
Supersampling und Milderung von Upscaling-Artefakten
- Nur Single-Sample-Rendering wäre für diesen Renderer nicht nützlich genug
- Bei 8x SSAA werden zum Beispiel 10 Versionen des VRAM auf der GPU gehalten
- Eine Single-Sample-VRAM-Version
- Ein reference-Wert der Single-Sample-VRAM-Version
- Acht Supersamples
- Beim Rendering wird die Supersample-Version geladen, wenn Single-Sample-VRAM und reference übereinstimmen
- Das ist wichtig für inkrementelles Rendering
- Beim Abschluss eines Tiles wird per clustered subgroup operation ein Multisample-Resolve ausgeführt, und Supersample- sowie Single-Sample-Kopien werden geschrieben
- Supersampling erzeugt weniger Treppchen als einfaches Upscaling und sorgt für einen konsistenteren Auflösungseindruck zwischen 3D-Elementen und UI-Elementen
- Sprite-Primitives müssen immer single-rate gerendert werden
- Meist handelt es sich um UI oder ähnliche Elemente; beim Upscaling könnten sie außerhalb des vorgesehenen rect samplen oder durch bilineares Filtering zu stark verschwimmen
- Da viele UIs als normale Dreiecke gezeichnet werden, wird bei manchen flat primitives die Attributinterpolation auf single-pixel-Koordinaten reduziert
- Selbst bei Verwendung von perspective werden Primitives als flat UI primitives vermutet, wenn alle Vertices dasselbe Q und dasselbe Z haben
- Es kann False Positives geben, in den getesteten Spielen funktionierte dies aber ausreichend gut
Ergebnisse pro Spiel und schwierige Fälle
- In Tales of the Abyss zeigt das Upscaling des PCSX2-Vulkan-Backends eine Fehlanordnung von Yuris Bloom und ein quadratisches Muster
- Beim 8x SSAA von paraLLEl-GS treten typische Probleme schlechten Upscalings kaum hervor
- Auf den betreffenden Screenshot wird zusätzlich FSR1-Post-Process-Upscaling angewandt
- Die UI von Final Fantasy X zeigt beim Vergleich von native resolution und 4x upscale Upscaling-Probleme
- Der MSAA snap trick ist wirksam, um Artefakte zu vermeiden
- Das Grundprinzip lautet, UI nicht über nearest neighbor mit ganzzahligem Faktor hinaus hochzuskalieren
- MGS2 enthält Fälle, die in PCSX2 hohe blending accuracy erfordern
- In PCSX2 fügt der programmable-blending-Pfad pro Primitive eine Barriere ein, was die Performance stark senkt
- paraLLEl-GS ist so aufgebaut, dass es immer mit 100 % blend accuracy arbeitet; eine 16x-SSAA-Szene auf einer RX 7600 wird mit 25 W und 17 % GPU utilization angegeben
- Valkyrie Profile 2 enthält einen Fall, in dem das Alpha des eigenen Pixels als Palettenindex gesampelt wird
- paraLLEl-GS erkennt dies, macht den Texture-Index zu einem Spezialwert und referenziert die in-register framebuffer color
- Durch diese Optimierung sinkt die Zahl der render pass barriers von über 500 auf 18
- Der Camo-Effekt im Intro von MGS2 sampelt den Framebuffer als Textur, verwendet aber überlappende Koordinaten, die nicht pixelgenau ausgerichtet sind
- PCSX2 scheint hier ebenfalls keine Barriere hinzuzufügen, und paraLLEl-GS behandelt es genauso
- Shadow of the Colossus dient als starker Stresstest
- Bei maximaler blend accuracy in PCSX2 fällt die GPU im Intro schon bei 2x upscale auf 24 FPS
- paraLLEl-GS bietet auch bei 8x SSAA noch ordentliche Performance, doch in dieser Szene steigt die Last deutlich
- In diesem Fall liegt der Flaschenhals eher im geometry processing der CPU als bei der GPU
Aktueller Stand und nächste Schritte
- Die praktisch wichtigste Testmethode ist derzeit die Nutzung von GS dumps
- Es gibt einen hack-patch, mit dem PCSX2 rohe GS-Traces dumpen kann
- Auch grobe Livetests über mkfifo sind möglich
- Damit es für Endnutzer nützlich wird, ist in irgendeiner Form eine Emulator-Integration nötig
- Da die PS2-Bibliothek sehr groß ist, dürften noch viele Bugs verborgen sein
- Wegen des Charakters als standalone library gibt es auch potenzielle Einsatzfälle wie bei einer Rendering-API alten Stils
1 Kommentare
Hacker-News-Kommentare
Ich hatte schon befürchtet, ich müsste eine Weile suchen, bis in dem Artikel die Abkürzung GS ausgeschrieben wird. Der Inhalt ist interessant, aber an der Stelle fühlt man sich etwas abgehängt
„Bete, dass es programmierbares Blending gibt“ – seit ich Anfang der 2000er erstmals Pixel Shader gelernt habe, wünsche ich mir programmierbares Blending über „Blending Shader“
Nebenbei wollte ich auch programmierbares Texture-Decoding über „Texture Shader“, nützlich für eigene Texture-Formate/-Kompression, Texture-Compositing usw.
Irgendwie haben GPUs Raytracing vor programmierbarem Blending bekommen; Ersteres war wie ein Sommernachtstraum, Letzteres fühlte sich eher so an, als würde man nur einen weiteren Fixed-Function-Block durch einen programmierbaren Block ersetzen. Auf Texture Shader warte ich immer noch
https://medium.com/pocket-gems/programmable-blending-on-ios-...
https://developer.apple.com/videos/play/tech-talks/605
Es gibt ein gutes Beispiel dafür: https://vulkan.org/user/pages/09.events/vulkanised-2024/vulk...
Ich denke, es ist bis zu einem gewissen Grad machbar, aber ohne überzeugende Use Cases lässt sich der Implementierungsaufwand schwer rechtfertigen
Mein Lieblingsteil am GS war die absurde Größenordnung der Busarchitektur. Insgesamt 2560 Bit breit, und die Cache-Aufteilung war ebenfalls clever
Die PS3 fühlte sich in mancher Hinsicht wie ein Rückschritt an, besonders beim Blending
Ich frage mich, wie sich dieser Ansatz mit Dolphins Ubershader vergleichen lässt
Tatsächlich war das schon eine alte Technik, als Dolphin sie eingeführt hat. Dieses Projekt ist ein vollständiger Renderer einschließlich Rasterizer und enthält, wie im Artikel zu sehen, auch einen Ubershader fürs Blending
Der Shader zeichnet keine Dreiecke, sondern wird für jeden Punkt innerhalb eines Dreiecks aufgerufen, erhält einige Eingaben und bestimmt die Farbe dieses Punkts. Das ist entfernt vergleichbar damit, einen vollständigen CPU-Emulator mit einer Implementierung nur der ADD/MUL-Befehle zu vergleichen
Ich frage mich, was top-left raster bedeutet