2 pontos por GN⁺ 2024-09-03 | 1 comentários | Compartilhar no WhatsApp
  • paraLLEl-GS é um projeto que recria o GS (Graphics Synthesizer) do PS2 com compute em Vulkan, abordando os limites de precisão e de upscaling deixados pelo GSdx, que foi o padrão de fato por quase 20 anos
  • O GS opera com 4 MiB de VRAM e alto fill rate, mas características do pipeline de pixels como teste de alfa de destino, blending condicional e alfa/cor acima de 1.0 tornam difícil reproduzi-lo com APIs gráficas convencionais
  • A implementação rastreia a VRAM em páginas e blocos de 256 bytes, e combina snapshots de CLUT, unswizzle de texturas e agendamento de render passes para lidar com feedback de framebuffer/textura
  • Casos como Tales of the Abyss, Final Fantasy X, MGS2, Valkyrie Profile 2 e Shadow of the Colossus foram usados para comparar problemas de upscaling, UI, blending de alta precisão e feedback de textura, incluindo cenas em 8x e 16x SSAA
  • Atualmente, a validação depende principalmente da reprodução de GS dumps; há patchs hack para o PCSX2 e testes em tempo real via mkfifo, mas ainda é necessária integração com um emulador para chegar aos usuários reais

Objetivo e ponto de partida do paraLLEl-GS

  • paraLLEl-GS é um projeto que emula o GS (Graphics Synthesizer) do PlayStation 2 com compute em Vulkan
  • O mesmo autor já havia feito em 2020 o paraLLEl-RDP, que implementava o RDP do N64 com compute em Vulkan e buscava resultados próximos da precisão em nível de bit e upscaling tomando o Angrylion como referência
  • No PS2, o GSdx permaneceu por cerca de 20 anos como a implementação de ponta de fato
  • Houve por volta de 2014 uma tentativa de implementação de compute para o GS do PS2 baseada em OpenCL, mas ela não foi concluída e hoje é difícil até encontrá-la no repositório upstream
  • No PS2, o motivo para usar rasterização com compute shader é menos forte do que no N64
    • O PCSX2 já tem um renderizador de software bem otimizado e um renderizador baseado em gráficos relativamente robusto
    • O renderizador de software não oferece suporte a upscaling
    • O renderizador baseado em gráficos apresenta vários bugs e glitches, especialmente no upscaling
  • O paraLLEl-GS foca mais em evitar problemas óbvios de precisão do que em atingir precisão em nível de bit do hardware
    • O próprio renderizador de software do GSdx também não parece ser uma implementação bit-exact do hardware, então testes baseados em comparação direta logo chegam ao limite

Por que o GS do PS2 é complicado

  • O GS era, em 2000, um dispositivo com fill rate e largura de banda teoricamente acima de 1 bilhão de pixels por segundo
  • A VRAM tem apenas 4 MiB, mas foi projetada para ficar em streaming constante por meio de vários motores de DMA
  • O pipeline de pixels em si é mais simples que o RDP do N64 em alguns aspectos
    • textura única
    • combinador de ciclo único
    • antialiasing muito básico
  • Há muitos recursos difíceis de implementar com APIs gráficas comuns
    • Blending acima de 1.0: comportamento herdado do PS1, em que 0x80 é tratado como 1.0, mas valores até 0xff podem ser representados
    • Teste de alfa de destino: o alfa de destino pode ser usado como uma espécie de stencil
    • blending condicional: é possível desligar o blending condicionalmente com base no alfa
    • correção de alfa: antes de gravar o alfa, é possível aplicar OR no bit mais significativo para forçá-lo a ficar próximo de 1
    • descarte parcial no teste de alfa: dá para descartar só a cor e manter a gravação de profundidade
    • AA1: funciona no estilo coverage-to-alpha e se entrelaça com o controle de gravação de profundidade por pixel
    • Z de 32 bits em ponto fixo: o suporte a D32_UINT existe tecnicamente, mas ainda não foi visto em uso real
  • Sem blending programável, GPUs desktop de modo imediato precisam de ROVs ou barreiras por pixel, o que derruba muito o desempenho
  • A implementação em compute contorna essas limitações criando seu próprio renderizador deferred tile-based (TBDR)

Regras de rasterização, fila de vértices e layout de memória

  • Os primitivos do GS são fornecidos de forma relativamente convencional em clip space
    • O VU1 faz a transformação e o clipping, e exporta vários atributos de vértice para o GS
  • Coordenadas e atributos usam formatos específicos do GS
    • X/Y: ponto fixo sem sinal 12.4
    • Z: uint de 24 ou 32 bits
    • FOG: uint de 8 bits
    • RGBA: valores de 8 bits para iluminação por vértice
    • STQ: coordenadas de textura com correção de perspectiva
    • UV: coordenadas não normalizadas em ponto fixo 12.4 sem correção de perspectiva
  • As regras de rasterização se parecem mais com o estilo do D3D9
    • triângulos usam a regra top-left como nas GPUs modernas
    • o centro do pixel fica em coordenadas inteiras, como no D3D9
    • linhas usam o algoritmo de Bresenham, o que dificulta o upscaling, então precisam ser aproximadas por rects ou paralelogramos
    • pontos são encaixados no pixel mais próximo
    • sprites são quads simples com duas coordenadas
  • A fila de vértices do GS lembra o modo imediato do OpenGL 1.0
    • define-se RGBA, STQ e vários registradores, e a escrita no registrador XYZ forma o “kick” do vértice
    • também há suporte a TRIANGLE_FAN
  • As coordenadas de pixel do PS2 são organizadas em unidades de página
    • uma página tem 8 KiB
    • uma página é dividida em 32 blocos
    • em RGBA de 32 bits, uma página corresponde a 64×32 pixels, com 32 blocos de 8×8 embaralhados em Z-order
  • Ao renderizar em cor de 24 bits ou profundidade de 24 bits, é possível colocar texturas nos 8 bits superiores que sobram
    • os formatos 8H, 4HL e 4HH são úteis para paletas de 8 bits e 4 bits

Texturas, CLUT e TEXFLUSH

  • O texturing do GS mistura partes parecidas com APIs modernas e partes peculiares
    • o centro do texel fica no half-pixel, como nas APIs modernas
    • a precisão subtexel aparenta ser de 4 bits, e não 8 bits
    • o filtro bilinear é um bilinear comum, não uma estrutura especial como o filtro de 3 pontos do N64
  • Modos especiais de endereçamento aumentam a dificuldade de implementação
    • REGION_CLAMP permite aplicar clamp a uma região arbitrária dentro de um atlas de texturas
    • REGION_REPEAT é ainda mais difícil, porque pode aplicar operações bit a bit como (u & MASK) | FIX por coordenada
  • O mipmapping calcula LOD com base no log2 do fator Q interpolado e em um fator de escala, e não por derivadas
    • numa implementação em compute, a grande vantagem é justamente não depender de derivadas
    • esse método não permite suportar recursos como anisotropic filtering
  • A CLUT é um cache de 1 KiB que guarda a paleta atual
    • para usá-la, é preciso fazer uma cópia explícita da VRAM para o cache de CLUT
    • em cor de 32 bits, cabe uma paleta de 256 cores
    • em 16 bpp, cabem 32 paletas de 16 cores
  • TEXFLUSH é um comando explícito mais próximo de sincronização e invalidação do cache de texturas
    • no início, tentou-se usar TEXFLUSH como referência para rastrear hazards, mas no fim foi preciso ignorá-lo
    • havia jogos que esqueciam o TEXFLUSH ou o chamavam em excesso
  • A implementação final adotou uma abordagem de cache mínimo
    • assume-se efetivamente que não existe cache e os hazards são rastreados diretamente
    • loops de feedback recebem tratamento especial separado
    • o GSdx parece seguir a mesma direção

Pipeline de renderização com compute em Vulkan

  • O pipeline de implementação é estruturado assumindo sincronização entre as etapas
    • sincronizar a cópia de VRAM da CPU com a GPU
    • fazer upload para VRAM ou uma cópia local-to-local
    • atualizar o cache de CLUT a partir da VRAM
    • fazer unswizzle da VRAM em uma VkImage para permitir amostragem direta
    • executar a renderização
    • sincronizar de volta para a CPU a cópia de VRAM da GPU
  • O comportamento típico dos jogos combina bem com esse pipeline
    • enviar texturas para a VRAM
    • enviar paletas para a VRAM
    • atualizar o cache de CLUT
    • desenhar usando textura
    • se necessário, fazer unswizzle da VRAM para VkImage
    • agrupar lotes de primitivos em render passes
  • Se não houver hazards voltados para trás, é possível adiar lotes e sincronização
    • para ter desempenho nesse tipo de renderizador, é essencial manter os lotes
  • Os principais casos de hazard são tratados separadamente
    • copiar novamente para VRAM já escrita por outro copy
    • fazer copy para VRAM já lida por textura amostrada ou CLUT
    • amostrar como textura uma área já renderizada
    • fazer copy para VRAM já renderizada

Rastreamento por página e cache de texturas

  • A parte mais difícil da emulação do GS é lidar com hazards de read-after-write e write-after-write na VRAM
  • Os 4 MiB de VRAM são divididos primeiro em páginas
    • páginas são a unidade de framebuffer e depth buffer, então fazem mais sentido como base de rastreamento
  • O estado rastreado por página é o seguinte
    • pending frame buffer write
    • pending frame buffer read
  • Como texturas e cópias de VRAM têm alinhamento de 256 bytes, usa-se uma bitmask u32 para os 32 blocos
    • VRAM copy write
    • VRAM copy read
    • pending read para cache de CLUT ou VkImage
    • blocos sobrescritos por qualquer write
  • Ao renderizar em cor de 24 bits e amostrar os 8 bits superiores como textura, pode não haver hazard
    • para isso, a implementação rastreia separadamente a máscara de escrita do framebuffer e a máscara de leitura de textura
  • Cada página mantém uma lista de VkImages associadas
    • quando a textura da página é invalidada, a imagem é destruída e precisa ser novamente deswizzled a partir da VRAM
    • uma textura pode ocupar várias páginas, e basta uma delas ser sobrescrita para a textura inteira ser invalidada
  • Um rastreamento simples e conservador não funciona em jogos de PS2
    • o rastreamento no nível de blocos de 256 bytes e a consideração das masks de leitura/escrita são fundamentais
  • Pode haver falsos positivos por causa de texturas POT e da ausência de REGION_CLAMP
    • por exemplo, ao configurar um render target de 512×448 como textura 512×512, a área não usada pode parecer um hazard
    • a implementação usa um contorno que ignora o hazard potencial dessa “red zone”

Lotes de CLUT e unswizzle de texturas

  • Para colocar uploads de textura em lote, também é preciso colocar em lote os uploads de CLUT
  • A implementação mantém 1024 cópias da CLUT em um snapshot ring buffer
    • um workgroup percorre as atualizações e grava em um SSBO
    • isso lembra a atualização de TMEM no RDP do N64, mas as atualizações de CLUT são muito mais simples
  • Em Vulkan, uma nova VkImage é alocada, subalocada em VkDeviceMemory e depois deswizzled por compute shader
  • A implementação usa Vulkan specialization constants para especializar o formato da textura e a lógica de swizzle
  • O comportamento especial de REGION_REPEAT também é tratado na etapa de unswizzle
    • assim, o ubershader precisa menos de filtragem bilinear manual para considerar esse caso
  • Os render targets também fazem ida e volta para textura por meio da VRAM SSBO
    • concluiu-se que tentar encaminhar diretamente um render target para textura gera bugs e exceções demais

Setup de triângulos, binning e ubershader

  • O paraLLEl-GS é um renderizador tile-based como o paraLLEl-RDP
  • Antes do binning, é feito o setup dos triângulos, com entrada dividida em três arrays
    • posições
    • atributos por vértice
    • atributos por primitivo
  • O rasterizador é baseado em coordenadas baricêntricas e fortemente influenciado pelos textos de Fabian Giesen sobre pipeline gráfico e pelo método de rasterização paralela descrito no artigo de Pineda de 1988
  • O GS real do PS2 usa DDA, ou seja, um rasterizador por scanline, mas como não se conhece uma descrição bit-exact do DDA do GS, foi adotada a abordagem baricêntrica
  • Também há suporte a paralelogramos para implementar linhas largas e sprites
  • O inv_area é calculado com um RCP customizado em ponto fixo
    • o RCP padrão de GPU varia demais entre implementações e tem cerca de 22,5 bits de precisão, então foi evitado
    • o RCP customizado mira algo em torno de 24,0 bits de precisão
  • O binning normalmente usa blocos de 32×32 pixels
    • o número máximo de primitivos por render pass é 64k por causa dos índices u16
    • os principais render passes observados ficaram em geral na faixa de 10k a 30k primitivos
  • O GS do PS2 tem fill rate alto e baixa complexidade por pixel, então um ubershader puro é viável
    • ao contrário do N64, é possível usar bindless, o que reduz também a complexidade do texturing
  • O ubershader usa early Z, deferred on-tile shading e lazy pixel shading
    • o sombreamento real só é feito quando o pixel depende do resultado anterior
    • alpha test, color write mask e alpha blending criam esse tipo de dependência
    • a cor final do framebuffer e a profundidade são gravadas em SSBO para reduzir o uso de largura de banda da GPU

Supersampling e mitigação de artefatos de upscaling

  • Renderização com amostra única não aproveita bem o potencial desse renderizador
  • Por exemplo, em 8x SSAA, são mantidas 10 versões de VRAM na GPU
    • 1 VRAM single-sample
    • 1 valor de referência da VRAM single-sample
    • 8 versões supersampled
  • Ao renderizar, se a VRAM single-sample e a referência coincidirem, a versão supersampled é carregada
    • isso é importante para renderização incremental
  • Quando um tile termina, um resolve multisample é feito com operações clustered subgroup, e então são gravadas as cópias supersampled e single-sample
  • O supersampling gera menos serrilhado que um upscaling simples e deixa mais consistente a percepção de resolução entre elementos 3D e de UI
  • Primitivos de sprite devem sempre ser renderizados em single-rate
    • na maioria dos casos são UI ou elementos parecidos, e fazer upscale pode amostrar fora do rect pretendido ou desfocar demais por causa da filtragem bilinear
  • Como muita UI é desenhada com triângulos comuns, alguns primitivos flat têm a interpolação de atributos reduzida para coordenadas de pixel único
    • mesmo usando perspective, se todos os vértices têm o mesmo Q e o mesmo Z, o primitivo é estimado como UI flat
    • pode haver falsos positivos, mas nos jogos testados isso funcionou bem o bastante

Resultados por jogo e casos difíceis

  • Em Tales of the Abyss, o upscaling do backend Vulkan do PCSX2 mostra desalinhamento do bloom no vidro e padrões quadriculados
    • no 8x SSAA do paraLLEl-GS, os problemas típicos de upscaling ruim quase não aparecem
    • no screenshot citado também foi aplicado upscaling de pós-processamento com FSR1
  • A UI de Final Fantasy X mostra claramente problemas de upscaling ao comparar resolução nativa com upscale 4x
    • o truque de snap com MSAA ajuda a evitar artefatos
    • o princípio central é não aplicar upscale na UI além de um fator inteiro com nearest neighbor
  • MGS2 é um caso no PCSX2 que exige alta precisão de blending
    • no caminho de blending programável do PCSX2, há uma barreira por primitivo, o que derruba fortemente o desempenho
    • o paraLLEl-GS opera sempre com 100% de precisão de blending por construção, e uma cena em 16x SSAA foi mostrada em uma RX 7600 com 25 W e 17% de uso de GPU
  • Valkyrie Profile 2 tem um caso em que o alfa do próprio pixel é amostrado como índice de paleta
    • o paraLLEl-GS detecta isso, transforma o índice de textura em um valor especial e consulta a cor do framebuffer que está em registrador
    • essa otimização reduz as barreiras de render pass de mais de 500 para 18
  • O efeito de camuflagem da introdução de MGS2 amostra o framebuffer como textura, mas usa coordenadas sobrepostas sem alinhamento de pixel
    • o PCSX2 aparentemente também não adiciona barreira nesse caso, e o paraLLEl-GS segue a mesma abordagem
  • Shadow of the Colossus funciona como um forte teste de estresse
    • com precisão máxima de blending no PCSX2, a GPU cai para 24 FPS já com upscale 2x na introdução
    • o paraLLEl-GS mantém desempenho razoável até em 8x SSAA, embora essa cena ainda seja pesada
    • nesse caso, o gargalo está mais no processamento de geometria pela CPU do que na GPU

Estado atual e próximos passos

  • Hoje, a forma prática de testar é usando GS dumps
  • Há um hack-patch que permite despejar raw GS trace a partir do PCSX2
  • Também existe teste em tempo real rudimentar via mkfifo
  • Para se tornar útil ao usuário final, será necessária integração com um emulador de alguma forma
  • Como a biblioteca do PS2 é enorme, ainda é muito provável que existam muitos bugs escondidos
  • Por ser uma biblioteca standalone, também há um uso potencial como uma API de renderização em estilo antigo

1 comentários

 
GN⁺ 2024-09-03
Comentários do Hacker News
  • Fiquei um tempão procurando em que momento a sigla GS seria escrita por extenso neste texto. O conteúdo é interessante, mas isso me deixou meio perdido

    • É a sigla de Graphics Synthesizer, nome que a Sony deu à “GPU” do PS2
  • “Reze para existir blending programável”: desde que aprendi pela primeira vez sobre pixel shaders no começo dos anos 2000, venho querendo blending programável por meio de “blending shaders”
    Aliás, também queria decodificação programável de texturas via “texture shaders”, útil para formatos/compressão de textura personalizados, composição de texturas etc.
    Por acaso, as GPUs ganharam ray tracing antes de blending programável; o primeiro parecia um sonho de uma noite de verão, e o segundo pareceu mais como transformar outro bloco de função fixa em um bloco programável. Ainda estou esperando pelos texture shaders

    • As GPUs mobile herdadas da família PowerVR têm esse tipo de recurso
      https://medium.com/pocket-gems/programmable-blending-on-ios-...
      https://developer.apple.com/videos/play/tech-talks/605
    • Da última vez que mexi com isso, GPUs mobile PowerVR tinham blending programável e, na verdade, esse era o único tipo de blending que existia nelas. No PS Vita, mudar o estado de blend levava cerca de 1 ms, então não era lá muito bonito
    • Acho que VK_EXT_fragment_shader_interlock é uma espécie de blending programável. O mesmo vale para Raster-order-views no lado do DirectX
      Há um bom exemplo de uso disso aqui: https://vulkan.org/user/pages/09.events/vulkanised-2024/vulk...
    • Hoje em dia também existem mesh shaders, work graphs, CUDA e shaders C++ genéricos. A OTOY agora faz toda a renderização em compute
    • A maior parte disso provavelmente daria para emular em Vulkan ou DX12; sobre outras APIs, não sei. Mas o que eu realmente queria saber é qual seria o caso de uso
      Acho que em certo nível dá para implementar, mas sem um caso de uso convincente é difícil justificar o trabalho
  • Minha parte favorita do GS sempre foi a escala absurda da arquitetura de barramento. No total eram 2560 bits de largura, e a divisão de cache também era inteligente
    O PS3, em alguns aspectos, pareceu um retrocesso, especialmente no blending

  • Fico curioso sobre como essa abordagem se compara ao ubershader do Dolphin

    • Basicamente, quase não dá para comparar. O ubershader do Dolphin faz uma coisa só: imitar blending/texturing de função fixa com hardware moderno e flexível
      Na verdade, já era uma técnica antiga mesmo quando o Dolphin a adotou. Este projeto é um renderizador completo, incluindo o rasterizador, e como o texto mostra, também inclui um ubershader para blending
      O shader não desenha triângulos; em vez disso, ele é chamado para cada ponto dentro do triângulo, recebe algumas entradas e decide a cor daquele ponto. É vagamente parecido com comparar um emulador completo de CPU com algo que só implementa instruções ADD/MUL
  • Gostaria de saber o que significa top-left raster