2 puntos por GN⁺ 2024-09-03 | 1 comentarios | Compartir por WhatsApp
  • paraLLEl-GS es un proyecto que recrea el GS (Graphics Synthesizer) de PS2 con cómputo en Vulkan, abordando los límites de precisión y escalado que dejó GSdx, el estándar de facto durante casi 20 años
  • El GS funciona sobre la base de 4 MiB de VRAM y un alto fillrate, pero sus características de pipeline de píxeles —como destination alpha test, blending condicional y alfa/color por encima de 1.0— hacen difícil replicarlo con APIs gráficas convencionales
  • La implementación rastrea la VRAM por páginas y bloques de 256 bytes, y combina snapshots de CLUT, unswizzling de texturas y agrupación de render passes para manejar el feedback entre framebuffer y texturas
  • Se compararon problemas de escalado, UI, blending de alta precisión y feedback de texturas en casos como Tales of the Abyss, Final Fantasy X, MGS2, Valkyrie Profile 2 y Shadow of the Colossus, incluyendo escenas con 8x y 16x SSAA
  • Actualmente la validación depende sobre todo de la reproducción de GS dumps; existen un hack patch de PCSX2 y pruebas en tiempo real con mkfifo, pero para llegar a usuarios reales hace falta integración con un emulador

Objetivos y punto de partida de paraLLEl-GS

  • paraLLEl-GS es un proyecto para emular el GS (Graphics Synthesizer) de PlayStation 2 con cómputo en Vulkan
  • Un trabajo previo del mismo autor en 2020, paraLLEl-RDP, implementó el RDP de N64 con cómputo en Vulkan, tomando a Angrylion como referencia y apuntando a resultados cercanos a la precisión a nivel de bits junto con escalado
  • En PS2, GSdx se mantuvo como la implementación de referencia de facto durante casi 20 años
  • Hubo un intento alrededor de 2014 de implementar el GS de PS2 con OpenCL, pero no se completó, y hoy es difícil encontrarlo incluso en los repositorios upstream
  • En PS2, la necesidad de usar rasterización con compute shader es menor que en N64
    • PCSX2 cuenta con un software renderer bien optimizado y con un renderer basado en APIs gráficas relativamente sólido
    • El software renderer no soporta escalado
    • El renderer gráfico muestra varios bugs y glitches, especialmente al escalar
  • paraLLEl-GS se enfoca más en evitar problemas evidentes de precisión que en lograr precisión a nivel de bits respecto al hardware
    • El software renderer de GSdx tampoco parece ser una implementación bit exacta del hardware, así que las pruebas por comparación directa pronto llegan a su límite

Por qué el GS de PS2 es complicado

  • El GS era un dispositivo con fillrate y ancho de banda que en 2000 teóricamente podía procesar más de mil millones de píxeles por segundo
  • La VRAM es pequeña, 4 MiB, pero fue diseñada para mantenerse en streaming constante mediante varios motores DMA
  • El pipeline de píxeles en sí es en algunos aspectos más simple que el RDP de N64
    • Una sola textura
    • Un combinador de un solo ciclo
    • Antialiasing muy básico
  • Tiene muchas funciones difíciles de implementar con APIs gráficas generales
    • Blending por encima de 1.0: comportamiento heredado de PS1, donde 0x80 se trata como 1.0 y puede representarse hasta 0xff
    • Destination alpha test: permite usar el alfa de destino como una especie de stencil
    • Blending condicional: se puede desactivar el blending de forma condicional según el alfa
    • Corrección de alfa: antes de escribir alfa se puede aplicar OR al bit más significativo para forzarlo cerca de 1
    • Descarte parcial en alpha test: se puede descartar solo el color y mantener la escritura de profundidad
    • AA1: usa un esquema tipo coverage-to-alpha y está ligado al control de escritura de profundidad por píxel
    • Z fijo de 32 bits: existe soporte técnico para D32_UINT, aunque todavía no se han visto casos reales de uso
  • Sin blending programable, en GPUs de escritorio immediate mode se requieren ROV o barreras por píxel, lo que degrada mucho el rendimiento
  • La implementación con compute evita estas limitaciones construyendo su propio tile-based deferred renderer (TBDR)

Reglas de rasterización, cola de vértices y disposición de memoria

  • Los primitivos del GS se entregan de forma relativamente convencional en clip space
    • VU1 realiza la transformación y el clipping, y exporta múltiples atributos de vértice al GS
  • Las coordenadas y atributos usan formatos propios del GS
    • X/Y: fixed-point sin signo 12.4
    • Z: uint de 24 o 32 bits
    • FOG: uint de 8 bits
    • RGBA: valores de 8 bits para iluminación por vértice
    • STQ: coordenadas de textura con corrección de perspectiva
    • UV: coordenadas no normalizadas en fixed-point 12.4 sin corrección de perspectiva
  • Las reglas de rasterización son más cercanas al estilo de D3D9
    • Los triángulos usan la regla top-left como en GPUs modernas
    • El centro del píxel está en coordenadas enteras, como en D3D9
    • Las líneas usan el algoritmo de Bresenham, lo que dificulta escalarlas, y deben aproximarse como rects o paralelogramos
    • Los puntos se ajustan al píxel más cercano
    • Los sprites son quads simples con dos coordenadas
  • La cola de vértices del GS se parece al immediate mode de OpenGL 1.0
    • Se configuran RGBA, STQ y varios registros, y una escritura al registro XYZ forma un “kick” de vértice
    • También se soporta TRIANGLE_FAN
  • Las coordenadas de píxel de PS2 se organizan por páginas
    • Una página ocupa 8 KiB
    • Cada página se divide en 32 bloques
    • En RGBA de 32 bits, una página corresponde a 64×32 píxeles, con 32 bloques de 8×8 swizzleados en orden Z
  • Al renderizar en color de 24 bits o profundidad de 24 bits, los 8 bits superiores sobrantes pueden usarse para texturas
    • Los formatos 8H, 4HL y 4HH son útiles para paletas de 8 y 4 bits

Texturas, CLUT y TEXFLUSH

  • El sistema de texturas del GS mezcla aspectos similares a APIs modernas con otros bastante peculiares
    • El centro del texel está en medio píxel, como en APIs modernas
    • La precisión subtexel parece ser de 4 bits, no de 8
    • El filtro bilineal es bilineal convencional, no una estructura especial como el filtro de 3 puntos de N64
  • Los modos especiales de direccionamiento aumentan la dificultad de implementación
    • REGION_CLAMP permite aplicar clamp a una región arbitraria dentro de un atlas de texturas
    • REGION_REPEAT es más difícil porque puede aplicar operaciones de bits por coordenada como (u & MASK) | FIX
  • El mipmapping calcula el LOD con el log2 del factor Q interpolado y un factor de escala, en lugar de usar derivadas
    • En una implementación con compute, no depender de derivadas es una gran ventaja
    • Este esquema no puede soportar funciones como anisotropic filtering
  • El CLUT es una caché de 1 KiB que contiene la paleta actual
    • Para usarla, hay que copiar explícitamente desde la VRAM a la caché CLUT
    • En color de 32 bits, puede contener una paleta de 256 colores
    • En 16bpp, puede contener 32 paletas de 16 colores
  • TEXFLUSH es una instrucción explícita más cercana a sincronizar e invalidar la caché de texturas
    • Al inicio se intentó usar TEXFLUSH como referencia para rastrear hazards, pero al final hubo que ignorarlo
    • Los juegos a veces olvidan llamar TEXFLUSH o lo llaman demasiado
  • La implementación final adopta un enfoque de minimal caching
    • Asume que no hay caché y rastrea los hazards directamente
    • Considera excepciones aparte para los feedback loops
    • GSdx parece seguir una dirección similar

Pipeline de renderizado por cómputo en Vulkan

  • El pipeline de implementación se organiza asumiendo sincronización entre cada etapa
    • Sincronizar con la GPU la copia de VRAM del CPU
    • Realizar upload a VRAM o copia local-to-local
    • Actualizar la caché CLUT desde VRAM
    • Hacer unswizzle de la VRAM a VkImage para permitir muestreo directo
    • Ejecutar el renderizado
    • Volver a sincronizar la copia de VRAM de la GPU con el CPU
  • El comportamiento típico de los juegos encaja bien con este pipeline
    • Subir la textura a VRAM
    • Subir la paleta a VRAM
    • Actualizar la caché CLUT
    • Dibujar con la textura
    • Si hace falta, hacer unswizzle desde VRAM a VkImage
    • Agrupar lotes de primitivos en render passes
  • Si no hay hazards hacia atrás, se puede retrasar la agrupación y la sincronización
    • Para obtener rendimiento en este tipo de renderer es clave mantener el batching
  • Los principales casos de hazard se manejan por separado
    • Copiar otra vez a una zona de VRAM ya escrita por una copia
    • Copiar a VRAM que fue leída por una textura o por CLUT muestreado
    • Muestrear como textura una región ya renderizada
    • Copiar sobre VRAM ya renderizada

Seguimiento por páginas y caché de texturas

  • La parte más difícil en la emulación del GS es manejar los hazards de VRAM de tipo read-after-write y write-after-write
  • La VRAM de 4 MiB se divide primero por páginas
    • Las páginas son la unidad del framebuffer y del depth buffer, así que son la base de seguimiento más significativa
  • El estado rastreado por página es el siguiente
    • pending frame buffer write
    • pending frame buffer read
  • Como las texturas y las copias de VRAM están alineadas a 256 bytes, se usa una máscara de bits u32 para los 32 bloques
    • VRAM copy write
    • VRAM copy read
    • pending read hacia caché CLUT o hacia VkImage
    • bloques sobrescritos por cualquier write
  • Si se renderiza en color de 24 bits y al mismo tiempo se muestrean como textura los 8 bits superiores, puede no haber hazard
    • Para esto se rastrean por separado la frame buffer write mask y la texture read mask
  • Cada página mantiene una lista de VkImage asociados
    • Si se invalida la textura de una página, la imagen se destruye y debe hacerse unswizzle de nuevo desde VRAM
    • Una sola textura puede abarcar varias páginas, y si se sobrescribe solo una de ellas, la textura se invalida
  • Un seguimiento simple y conservador no funciona en juegos de PS2
    • El seguimiento a nivel de bloques de 256 bytes y considerar las máscaras de lectura/escritura es esencial
  • Puede haber falsos positivos por texturas POT y por no usar REGION_CLAMP
    • Por ejemplo, si un render target de 512×448 se configura como una textura de 512×512, la zona no usada puede parecer un hazard
    • La implementación usa una solución práctica que ignora los posibles hazards en esa “red zone”

Batching de CLUT y unswizzle de texturas

  • Para agrupar uploads de texturas, también hay que agrupar los uploads de CLUT
  • La implementación mantiene 1024 copias del CLUT en un snapshot ring buffer
    • Un solo workgroup recorre las actualizaciones y escribe en un SSBO
    • Se parece a las actualizaciones de TMEM del RDP de N64, aunque las actualizaciones de CLUT son mucho más simples
  • En Vulkan, se asigna un nuevo VkImage, se subasigna desde VkDeviceMemory y luego se hace el unswizzle con compute shaders
  • Se usan Vulkan specialization constants para especializar el formato de textura y la lógica de swizzle
  • El comportamiento especial de REGION_REPEAT también se procesa en la etapa de unswizzle
    • Así el ubershader ya no necesita manejar tanto filtrado bilineal manual para ese caso
  • Los render targets también van y vuelven como texturas a través del SSBO de VRAM
    • Se concluyó que intentar reenviar directamente un render target como textura genera demasiados bugs y excepciones

Setup de triángulos, binning y ubershader

  • paraLLEl-GS es un tile-based renderer como paraLLEl-RDP
  • Antes del binning se hace el setup de triángulos, y la entrada se divide en tres arreglos
    • posición
    • atributos por vértice
    • atributos por primitivo
  • El rasterizador es barycentric-based y está muy influido por los textos sobre pipelines gráficos de Fabian Giesen y por el método de rasterización paralela descrito en el paper de Pineda de 1988
  • El GS real de PS2 usa DDA, es decir, un scanline rasterizer, pero como no se conoce una descripción bit exacta del DDA del GS, se usa un enfoque baricéntrico
  • También se soportan paralelogramos para implementar wide lines y sprites
  • inv_area se calcula con un RCP custom en fixed-point
    • Se evita el RCP estándar de GPU porque su consistencia depende de la implementación y su precisión ronda los 22.5 bits
    • El RCP custom apunta a unos 24.0 bits de precisión
  • El binning normalmente usa bloques de 32×32 píxeles
    • El máximo de primitivos por render pass es 64k debido a índices u16
    • Los render passes más comunes observados estuvieron sobre todo en el rango de 10k a 30k primitivos
  • El GS de PS2 tiene alto fillrate y baja complejidad por píxel, lo que hace viable un ubershader puro
    • A diferencia de N64, aquí puede aprovecharse bindless, reduciendo también la complejidad del texturing
  • El ubershader usa early Z, deferred on-tile shading y lazy pixel shading
    • Solo se hace shading real cuando el píxel depende de un resultado previo
    • Dependencias como alpha test, color write mask y alpha blending provocan esos casos
    • El color final del framebuffer y la profundidad se escriben en SSBO, reduciendo el uso de ancho de banda de GPU

Supersampling y mitigación de artefactos de escalado

  • Renderizar con una sola muestra no aprovecha lo suficiente este renderer
  • Por ejemplo, con 8x SSAA se mantienen 10 versiones de VRAM en la GPU
    • Una VRAM single-sample
    • Un valor de referencia de esa VRAM single-sample
    • Ocho supersamples
  • Al renderizar, si la VRAM single-sample y la referencia coinciden, se carga la versión supersample
    • Esto es importante para el renderizado incremental
  • Al terminar un tile, se hace el resolve multisample con operaciones clustered subgroup, y se escriben tanto los supersamples como la copia single-sample
  • El supersampling produce menos dientes de sierra que un simple upscale y da una sensación de resolución más consistente entre elementos 3D y elementos de UI
  • Los primitivos sprite siempre deben renderizarse a single-rate
    • La mayoría corresponden a UI o elementos similares, y escalarlos puede hacer que se muestree fuera del rect previsto o que el filtrado bilineal los vuelva demasiado borrosos
  • Como mucha UI se dibuja con triángulos normales, algunos flat primitives reducen la interpolación de atributos a coordenadas single-pixel
    • Incluso si usan perspectiva, si todos los vértices tienen el mismo Q y el mismo Z, se asumen como primitivos planos de UI
    • Puede haber falsos positivos, pero en los juegos probados funciona suficientemente bien

Resultados por juego y casos difíciles

  • En Tales of the Abyss, el upscale del backend Vulkan de PCSX2 muestra desalineación del bloom en el vidrio y un patrón cuadriculado
    • En 8x SSAA con paraLLEl-GS, no se notan tanto los problemas típicos de un mal escalado
    • En esa captura también se aplicó upscale de posprocesado con FSR1
  • La UI de Final Fantasy X muestra problemas de escalado al comparar native resolution con 4x upscale
    • El truco de MSAA snap ayuda a evitar artefactos
    • El principio clave es no escalar la UI por encima de un factor entero con nearest neighbor
  • MGS2 es un caso que en PCSX2 requiere alta precisión de blending
    • En la ruta de blending programable de PCSX2 se insertan barreras por primitivo, lo que baja mucho el rendimiento
    • paraLLEl-GS funciona siempre con 100% de precisión de blending, y se muestra una escena con 16x SSAA en una RX 7600 usando 25 W y 17% de utilización de GPU
  • Valkyrie Profile 2 incluye un caso donde se muestrea el alfa del propio píxel como índice de paleta
    • paraLLEl-GS detecta esto, convierte el índice de textura en un valor especial y consulta el color del framebuffer que ya está en registros
    • Esta optimización reduce las barreras de render pass de más de 500 a 18
  • El efecto de camuflaje de la intro de MGS2 muestrea el framebuffer como textura, pero usa coordenadas superpuestas sin alineación exacta de píxel
    • PCSX2 aparentemente tampoco agrega una barrera aquí, y paraLLEl-GS lo maneja de la misma manera
  • Shadow of the Colossus funciona como una prueba de estrés fuerte
    • Con la máxima precisión de blending en PCSX2, en la intro la GPU baja hasta 24 FPS incluso con solo 2x upscale
    • paraLLEl-GS mantiene buen rendimiento incluso con 8x SSAA, aunque esa escena sigue siendo pesada
    • En este caso, el cuello de botella está en el procesamiento de geometría del CPU más que en la GPU

Estado actual y siguientes pasos

  • El método práctico principal de prueba hoy es usar GS dumps
  • Existe un hack patch para poder volcar raw GS traces desde PCSX2
  • También hay pruebas en tiempo real algo rudimentarias mediante mkfifo
  • Para que esto sea útil a usuarios finales, hace falta integrarlo con un emulador de alguna forma
  • Como la biblioteca de PS2 es enorme, es muy probable que todavía haya muchos bugs ocultos
  • Por su naturaleza de biblioteca standalone, también podría tener usos potenciales como una API de renderizado de estilo antiguo

1 comentarios

 
GN⁺ 2024-09-03
Comentarios en Hacker News
  • Me costó un buen rato encontrar en qué momento explicaban la abreviatura GS. El contenido está interesante, pero ahí sí sentí que me iba quedando atrás

    • Es la abreviatura de Graphics Synthesizer, el nombre que Sony le puso a la “GPU” de la PS2
  • Eso de “reza para que exista blending programable”: desde que aprendí por primera vez sobre pixel shaders a inicios de los 2000, he querido blending programable mediante un “blending shader”
    De paso, también quería decodificación de texturas programable mediante un “texture shader”, útil para formatos/compresión de textura personalizados, composición de texturas y demás
    Por cosas de la vida, las GPU terminaron teniendo ray tracing antes que blending programable; lo primero se sentía como un sueño de una noche de verano, y lo segundo como apenas convertir otro bloque de función fija en uno programable. Sigo esperando los texture shaders

    • Los GPU móviles que heredaron la línea PowerVR sí tienen algo así
      https://medium.com/pocket-gems/programmable-blending-on-ios-...
      https://developer.apple.com/videos/play/tech-talks/605
    • La última vez que lo toqué, los GPU PowerVR móviles tenían blending programable y, de hecho, era la única forma de blending que había. En PS Vita cambiar el estado de blending tardaba como 1 ms, así que no se veía muy bien
    • Siento que VK_EXT_fragment_shader_interlock es una especie de blending programable. También los Raster-order-views del lado de DirectX
      Hay un buen ejemplo usándolo aquí: https://vulkan.org/user/pages/09.events/vulkanised-2024/vulk...
    • Hoy en día también existen mesh shaders, work graphs, CUDA e incluso shaders generales en C++. OTOY ahora hace todo el renderizado con cómputo
    • La mayor parte de eso probablemente se podría emular en Vulkan o DX12; de otras API no sé bien. Pero sí me pregunto cuál sería el caso de uso real
      Creo que hasta cierto punto se puede implementar, pero sin un caso de uso convincente cuesta justificar el trabajo de implementación
  • Lo que más me gustaba del GS era la escala absurda de su estructura de buses. En total eran 2560 bits de ancho y además la partición de caché estaba muy bien pensada
    En algunos aspectos la PS3 se sintió como un retroceso, especialmente en blending

  • Me pregunto cómo se compara este enfoque con el ubershader de Dolphin

    • En esencia, casi no son comparables. El ubershader de Dolphin hace una sola cosa: imitar blending/texturizado de función fija con hardware moderno y flexible
      De hecho, incluso cuando Dolphin lo introdujo ya era una técnica vieja. Este proyecto es un renderizador completo, incluyendo el rasterizador y, como se ve en el artículo, también incluye un ubershader para blending
      El shader no dibuja triángulos, sino que se invoca para cada punto dentro del triángulo, recibe algunas entradas y decide el color de ese punto. Se parece vagamente a comparar un emulador completo de CPU con algo que solo implementa instrucciones ADD/MUL
  • Me pregunto qué significa top-left raster