- paraLLEl-GS reproduit le GS (Graphics Synthesizer) de la PS2 en Vulkan compute, afin de traiter les limites de précision et d’upscaling laissées par GSdx, qui a été de fait la référence pendant près de 20 ans
- Le GS fonctionne avec 4 Mio de VRAM et un fillrate élevé, mais ses caractéristiques de pipeline de pixels — test alpha de destination, blending conditionnel, alpha/couleurs au-delà de 1,0, etc. — le rendent difficile à faire correspondre aux API graphiques classiques
- L’implémentation suit la VRAM par pages et par blocs de 256 octets, et combine instantanés de CLUT, déswizzling de textures et regroupement en passes de rendu pour gérer le feedback framebuffer/texture
- Des cas comme Tales of the Abyss, Final Fantasy X, MGS2, Valkyrie Profile 2 et Shadow of the Colossus comparent les problèmes d’upscaling, d’UI, de blending haute précision et de feedback de textures, avec aussi des scènes en SSAA 8x et 16x
- La validation actuelle repose surtout sur la lecture de GS dumps ; il existe un patch de hack pour PCSX2 et des tests temps réel via mkfifo, mais une intégration dans un émulateur est nécessaire pour atteindre les utilisateurs finaux
Objectifs et point de départ de paraLLEl-GS
- paraLLEl-GS est un projet qui émule le GS (Graphics Synthesizer) de la PlayStation 2 en Vulkan compute
- paraLLEl-RDP, travail du même auteur datant de 2020, implémentait le RDP de la N64 en Vulkan compute, avec pour objectif des résultats proches de l’exactitude bit à bit par rapport à Angrylion, ainsi que l’upscaling
- Sur PS2, GSdx est resté pendant environ 20 ans l’implémentation de pointe de facto
- Une tentative d’implémentation compute du GS de la PS2 basée sur OpenCL a existé vers 2014, mais elle n’a pas été achevée et elle est difficile à retrouver dans le dépôt upstream actuel
- Les raisons d’utiliser un rasterizer en compute shader sur PS2 sont moins fortes que sur N64
- PCSX2 dispose d’un renderer logiciel bien optimisé et d’un renderer basé sur l’API graphique relativement robuste
- Le renderer logiciel ne prend pas en charge l’upscaling
- Le renderer basé sur l’API graphique montre plusieurs bugs et glitches, en particulier avec l’upscaling
- paraLLEl-GS se concentre davantage sur l’évitement de problèmes manifestes de précision que sur une exactitude bit à bit vis-à-vis du matériel
- Le renderer logiciel GSdx ne semblant pas non plus être une implémentation bit-exacte du matériel, les tests par comparaison directe atteignent vite leurs limites
Pourquoi le GS de la PS2 est difficile
- Le GS était, en 2000, un dispositif doté d’un fillrate et d’une bande passante capables en théorie de traiter plus d’un milliard de pixels par seconde
- La VRAM est limitée à 4 Mio, mais elle est conçue pour être alimentée en continu via plusieurs moteurs DMA
- Le pipeline de pixels lui-même est, sur certains aspects, plus simple que le RDP de la N64
- Une seule texture
- Un combiner à cycle unique
- Un antialiasing très basique
- De nombreuses fonctionnalités sont difficiles à implémenter avec une API graphique classique
- Blending au-delà de 1,0 : comportement hérité de la PS1, où 0x80 est traité comme 1,0 et les valeurs jusqu’à 0xff sont représentables
- Test alpha de destination : l’alpha de destination peut être utilisé comme une sorte de stencil
- Blending conditionnel : le blending peut être désactivé conditionnellement selon l’alpha
- Correction alpha : avant l’écriture de l’alpha, le bit de poids fort peut être forcé par OR pour le rapprocher de 1
- Rejet partiel du test alpha : il est possible de rejeter seulement la couleur tout en conservant l’écriture de profondeur
- AA1 : approche coverage-to-alpha, liée au contrôle de l’écriture de profondeur par pixel
- Z en virgule fixe 32 bits : la prise en charge de D32_UINT existe techniquement, mais aucun cas d’usage réel n’a encore été observé
- Sans blending programmable, les GPU desktop en mode immédiat nécessitent des ROV ou des barrières par pixel, ce qui dégrade fortement les performances
- L’implémentation compute contourne ces contraintes en construisant son propre renderer différé à tuiles (TBDR)
Règles de rasterisation, file de sommets et organisation mémoire
- Les primitives du GS sont fournies de manière relativement classique dans l’espace de clipping
- Le VU1 effectue les transformations et le clipping, puis émet plusieurs attributs de sommets vers le GS
- Les coordonnées et attributs ont des formats propres au GS
- X/Y : virgule fixe non signée 12.4
- Z : uint 24 bits ou 32 bits
- FOG : uint 8 bits
- RGBA : valeurs 8 bits pour l’éclairage par sommet
- STQ : coordonnées de texture avec correction de perspective
- UV : coordonnées non normalisées en virgule fixe 12.4, sans correction de perspective
- Les règles de rasterisation sont proches du style D3D9
- Les triangles utilisent la règle de rasterisation top-left comme les GPU modernes
- Les centres de pixels se trouvent sur des coordonnées entières, comme en D3D9
- Les lignes utilisent l’algorithme de Bresenham, ce qui rend l’upscaling difficile ; il faut les approximer par des rectangles ou des parallélogrammes
- Les points sont alignés sur le pixel le plus proche
- Les sprites sont de simples quads définis par deux coordonnées
- La file de sommets du GS ressemble au mode immédiat d’OpenGL 1.0
- On définit RGBA, STQ et divers registres, puis une écriture dans le registre XYZ forme un « kick » de sommet
- TRIANGLE_FAN est également pris en charge
- Les coordonnées de pixels de la PS2 sont organisées par pages
- Une page fait 8 Kio
- Une page est divisée en 32 blocs
- En RGBA 32 bits, une page fait 64×32 pixels, et ses 32 blocs de 8×8 sont swizzlés en ordre Z
- Lors du rendu en couleur 24 bits ou en profondeur 24 bits, les 8 bits supérieurs restants peuvent contenir une texture
- Les formats 8H, 4HL et 4HH sont utiles pour les palettes 8 bits et 4 bits
Textures, CLUT et TEXFLUSH
- Le texturing du GS mélange des aspects proches des API modernes et des particularités
- Le centre des texels se trouve sur le demi-pixel, comme dans les API modernes
- La précision sub-texel semble être de 4 bits plutôt que 8 bits
- Le filtrage bilinéaire est un bilinéaire classique, sans structure spéciale comme le filtre 3 points de la N64
- Des modes d’adressage spéciaux augmentent la difficulté d’implémentation
- REGION_CLAMP peut appliquer un clamp à une zone arbitraire au sein d’un atlas de textures
- REGION_REPEAT peut appliquer des opérations bit à bit du type
(u & MASK) | FIXà chaque coordonnée, ce qui est plus difficile à implémenter
- Le mipmapping calcule le LOD avec le log2 du facteur Q interpolé et un facteur d’échelle, plutôt qu’avec des dérivées
- Dans une implémentation compute, le fait de ne pas dépendre des dérivées est un grand avantage
- Cette approche ne peut pas prendre en charge des fonctionnalités comme l’anisotropic filtering
- La CLUT est un cache de 1 Kio contenant la palette courante
- Une copie explicite depuis la VRAM vers le cache CLUT est nécessaire pour l’utiliser
- En couleurs 32 bits, il peut contenir une palette de 256 couleurs
- En 16 bpp, il peut contenir 32 palettes de 16 couleurs
- TEXFLUSH est une commande explicite proche d’une synchronisation et invalidation du cache de textures
- Au début de l’implémentation, TEXFLUSH devait servir de base au suivi des hazards, mais il a finalement fallu l’ignorer
- Certains jeux oublient TEXFLUSH ou l’appellent trop souvent
- L’implémentation finale adopte une approche de minimal caching
- Elle considère qu’il n’y a pas de cache et suit directement les hazards
- Un traitement d’exception séparé est envisagé pour les boucles de feedback
- GSdx semble aller dans la même direction
Pipeline de rendu Vulkan compute
- Le pipeline d’implémentation repose sur une synchronisation entre chaque étape
- Synchroniser la copie CPU de la VRAM avec le GPU
- Téléverser la VRAM ou effectuer une copie local-to-local
- Mettre à jour le cache CLUT depuis la VRAM
- Déswizzler la VRAM en VkImage pour permettre l’échantillonnage direct
- Effectuer le rendu
- Resynchroniser la copie GPU de la VRAM vers le CPU
- Le fonctionnement habituel des jeux correspond bien à ce pipeline
- Téléverser une texture en VRAM
- Téléverser une palette en VRAM
- Mettre à jour le cache CLUT
- Dessiner avec la texture
- Si nécessaire, déswizzler depuis la VRAM vers une VkImage
- Composer les lots de primitives en passes de rendu
- En l’absence de hazards vers l’arrière, le batching et la synchronisation peuvent être différés
- Pour obtenir de bonnes performances avec ce type de renderer, il est important de préserver les batches
- Les principaux cas de hazard sont traités séparément
- Recopier dans une zone de VRAM déjà écrite par une copy
- Copier dans une zone de VRAM lue par une texture échantillonnée ou une CLUT
- Échantillonner comme texture une zone qui vient d’être rendue
- Copier dans une VRAM rendue
Suivi des pages et cache de textures
- La partie la plus difficile de l’émulation du GS consiste à gérer les hazards read-after-write et write-after-write dans la VRAM
- Les 4 Mio de VRAM sont d’abord divisés par pages
- Les pages sont l’unité des framebuffers et des depth buffers, donc le niveau de suivi le plus pertinent
- Les états suivis par page sont les suivants
- pending frame buffer write
- pending frame buffer read
- Les textures et les copies VRAM étant alignées sur 256 octets, un masque de bits u32 est utilisé pour les 32 blocs
- VRAM copy write
- VRAM copy read
- pending read vers le cache CLUT ou une VkImage
- bloc recouvert par n’importe quelle écriture
- Lorsqu’un rendu est effectué en couleur 24 bits tout en échantillonnant les 8 bits supérieurs comme texture, il peut ne pas y avoir de hazard
- Pour cela, le framebuffer write mask et le texture read mask sont suivis séparément
- Chaque page possède une liste de VkImage associées
- Lorsqu’une texture de page est invalidée, l’image est détruite et doit être redéswizzlée depuis la VRAM
- Une texture peut couvrir plusieurs pages, et il suffit que l’une d’elles soit recouverte pour que la texture soit invalidée
- Un suivi simple et conservateur ne suffit pas pour les jeux PS2
- Le suivi au niveau des blocs de 256 octets et la prise en compte des write/read masks sont importants
- Des faux positifs peuvent apparaître à cause des textures POT et de la non-utilisation de REGION_CLAMP
- Par exemple, si une cible de rendu 512×448 est configurée comme texture 512×512, la zone inutilisée peut apparaître comme un hazard
- L’implémentation utilise un contournement qui ignore les hazards potentiels de cette « red zone »
Batching de CLUT et déswizzling de textures
- Pour batcher les uploads de textures, il faut aussi batcher les uploads de CLUT
- L’implémentation conserve 1024 copies de la CLUT dans un ring buffer d’instantanés
- Un workgroup parcourt les mises à jour et les écrit dans un SSBO
- C’est similaire aux mises à jour de TMEM du RDP de la N64, mais les mises à jour de CLUT sont beaucoup plus simples
- Dans Vulkan, une nouvelle VkImage est allouée, sous-allouée depuis VkDeviceMemory, puis déswizzlée par un compute shader
- Des specialization constants Vulkan servent à spécialiser le format de texture et la logique de swizzle
- Le comportement spécial de REGION_REPEAT est également traité à l’étape de déswizzling
- Cela réduit ensuite le besoin, pour l’ubershader, de tenir compte de ce cas avec un filtrage bilinéaire manuel
- Les render targets font elles aussi l’aller-retour via la VRAM SSBO vers des textures
- Les tentatives de forwarder directement les render targets comme textures ont été jugées trop pleines de bugs et d’exceptions
Setup de triangles, binning et ubershader
- Comme paraLLEl-RDP, paraLLEl-GS est un renderer à tuiles
- Le setup des triangles est effectué avant le binning, avec une entrée divisée en trois tableaux
- Positions
- Attributs par sommet
- Attributs par primitive
- Le rasterizer est basé sur les coordonnées barycentriques et fortement influencé par la méthode de rasterisation parallèle décrite dans les articles de Fabian Giesen sur le pipeline graphique et dans l’article de Pineda de 1988
- Le véritable GS de la PS2 est un rasterizer DDA, c’est-à-dire à scanlines, mais faute de description bit-exacte connue du DDA du GS, l’implémentation utilise une approche barycentrique
- Les parallélogrammes sont également pris en charge pour implémenter les wide lines et les sprites
- inv_area est calculé avec un RCP en virgule fixe personnalisé
- Le RCP standard des GPU est évité car sa cohérence varie selon les implémentations et sa précision est d’environ 22,5 bits
- Le RCP personnalisé vise une précision d’environ 24,0 bits
- Le binning utilise généralement des blocs de 32×32 pixels
- Le nombre maximal de primitives par passe de rendu est de 64k à cause des indices u16
- Les principales passes de rendu observées se situaient généralement entre 10k et 30k primitives
- Le GS de la PS2 ayant un fillrate élevé et une faible complexité par pixel, un ubershader pur est possible
- Contrairement à la N64, bindless peut être exploité, ce qui réduit aussi la complexité du texturing
- L’ubershader utilise early Z, deferred on-tile shading et lazy pixel shading
- Le shading réel n’est effectué que lorsqu’un pixel dépend d’un résultat précédent
- Alpha test, color write mask et alpha blending créent ce type de dépendance
- La couleur finale du framebuffer et la profondeur sont écrites dans un SSBO afin de réduire l’utilisation de bande passante GPU
Supersampling et atténuation des artefacts d’upscaling
- Le rendu à échantillon unique ne suffit pas à rendre ce renderer vraiment utile
- Par exemple, en SSAA 8x, le GPU conserve 10 versions de la VRAM
- Une VRAM à échantillon unique
- Une valeur de référence de la VRAM à échantillon unique
- Huit versions supersamplées
- Lors du rendu, si la VRAM à échantillon unique correspond à la référence, la version supersamplée est chargée
- C’est important pour le rendu incrémental
- À la fin d’une tuile, les opérations clustered subgroup effectuent le resolve multisample et écrivent les copies supersamplée et à échantillon unique
- Le supersampling produit moins d’aliasing qu’un simple upscaling et rend plus cohérente la perception de résolution entre éléments 3D et éléments d’UI
- Les primitives de sprites doivent toujours être rendues en single-rate
- Il s’agit surtout d’éléments d’UI ou similaires ; en les upscalant, on risque d’échantillonner hors du rect prévu ou de les rendre excessivement flous par filtrage bilinéaire
- Comme de nombreuses UI sont dessinées avec des triangles ordinaires, certaines primitives plates réduisent l’interpolation des attributs à des coordonnées de pixel unique
- Même avec la perspective, si tous les sommets ont le même Q et le même Z, la primitive est considérée comme une primitive d’UI plate
- Des faux positifs sont possibles, mais cela a suffisamment bien fonctionné dans les jeux testés
Résultats par jeu et cas difficiles
- Dans Tales of the Abyss, l’upscaling du backend Vulkan de PCSX2 montre un mauvais alignement du bloom du verre et un motif rectangulaire
- Avec le SSAA 8x de paraLLEl-GS, les problèmes typiques d’un mauvais upscaling sont peu visibles
- Le screenshot correspondant applique aussi un upscaling de post-traitement FSR1
- L’UI de Final Fantasy X révèle des problèmes d’upscaling dans la comparaison entre résolution native et upscale 4x
- Le MSAA snap trick est efficace pour éviter les artefacts
- Le principe clé est de ne pas upscaler l’UI au-delà d’un facteur entier en nearest neighbor
- MGS2 comporte des cas qui exigent une haute blending accuracy dans PCSX2
- Dans PCSX2, le chemin de blending programmable insère une barrière pour chaque primitive, ce qui dégrade fortement les performances
- paraLLEl-GS est structuré pour fonctionner en permanence avec 100 % de blend accuracy, et une scène en SSAA 16x est présentée sur RX 7600 avec 25 W et 17 % d’utilisation GPU
- Valkyrie Profile 2 contient un cas où l’alpha du pixel lui-même est échantillonné comme index de palette
- paraLLEl-GS le détecte, transforme l’index de texture en valeur spéciale et référence la couleur du framebuffer en registre
- Cette optimisation réduit le nombre de render pass barriers de plus de 500 à 18
- L’effet de camouflage de l’intro de MGS2 échantillonne le framebuffer comme texture, mais utilise des coordonnées superposées qui ne sont pas alignées sur les pixels
- PCSX2 semble ne pas ajouter de barrière ici non plus, et paraLLEl-GS traite le cas de la même manière
- Shadow of the Colossus sert de test de stress important
- Avec le niveau maximal de blend accuracy de PCSX2, dans l’intro, un simple upscale 2x fait chuter le GPU à 24 FPS
- paraLLEl-GS conserve de bonnes performances même en SSAA 8x, mais cette scène reste exigeante
- Dans ce cas, le goulot d’étranglement se situe davantage dans le geometry processing CPU que dans le GPU
État actuel et prochaines étapes
- La méthode pratique actuelle de test repose sur les GS dumps
- Il existe un hack-patch permettant de dumper une trace GS brute depuis PCSX2
- Des tests temps réel rudimentaires via mkfifo sont également possibles
- Pour être utile aux utilisateurs finaux, une intégration dans un émulateur sous une forme ou une autre sera nécessaire
- La bibliothèque PS2 étant très vaste, il est très probable que de nombreux bugs restent cachés
- Son caractère de standalone library ouvre aussi des cas d’usage potentiels, comme une API de rendu à l’ancienne
1 commentaires
Commentaires sur Hacker News
J’ai l’impression qu’il faut chercher un bon moment dans l’article avant que l’acronyme GS soit explicité. Le contenu est intéressant, mais sur ce point j’ai eu le sentiment d’être un peu largué
« Priez pour avoir du blending programmable » : depuis que j’ai découvert les pixel shaders au début des années 2000, j’ai toujours espéré du blending programmable via des « blending shaders »
À titre de comparaison, j’aurais aussi voulu du décodage de textures programmable via des « texture shaders », utile pour les formats/compressions de textures personnalisés, la composition de textures, etc.
Au final, les GPU ont obtenu le ray tracing avant le blending programmable : le premier ressemblait à un rêve d’une nuit d’été, tandis que le second donnait plutôt l’impression de remplacer un bloc à fonction fixe de plus par un bloc programmable. J’attends toujours les texture shaders
https://medium.com/pocket-gems/programmable-blending-on-ios-...
https://developer.apple.com/videos/play/tech-talks/605
Il y a un bon exemple de son utilisation ici : https://vulkan.org/user/pages/09.events/vulkanised-2024/vulk...
Je pense que c’est faisable dans une certaine mesure, mais sans cas d’utilisation convaincant, il est difficile de justifier le travail d’implémentation
Ce que je préfère dans le GS, c’est l’échelle absurde de son architecture de bus. Une largeur totale de 2 560 bits, avec en plus une séparation des caches assez astucieuse
La PS3 m’a semblé être un recul sur certains aspects, notamment pour le blending
Je me demande comment cette approche se compare aux ubershaders de Dolphin
En réalité, même lorsque Dolphin les a introduits, c’était déjà une technique ancienne. Ce projet, lui, est un moteur de rendu complet, rasterizer inclus, et comme on le voit dans l’article, il comprend aussi un ubershader pour le blending
Le shader ne dessine pas des triangles ; il est appelé pour chaque point à l’intérieur d’un triangle, reçoit quelques entrées et détermine la couleur de ce point. C’est un peu comme comparer un émulateur CPU complet avec une implémentation ne couvrant que les instructions ADD/MUL
Je me demande ce que signifie top-left raster