← voltar ao blog
ACERTOjogos6min de leituramin

Saint Seiya: The Hades em 16:9 sem esticar nada

Como coloquei widescreen de verdade no port nativo de um jogo de PS2: achar a câmera no código MIPS, abrir o campo de visão, manter HUD e menus em 4:3 e preencher as artes de tela cheia.

Matheus Nunes PereiraEngenheiro de Software

O port nativo de Saint Seiya: The Hades é uma recompilação estática: cada uma das 13.855 funções do executável do PS2 é traduzida de MIPS R5900 para C e compilada para o PC. Em volta disso existe uma camada que faz o papel do hardware do console: o GS (o chip gráfico), o VU1, o processador de som e o resto. O jogo roda inteiro, mas até agora rodava como no PS2: uma imagem 4:3 com faixas pretas dos lados de um monitor 16:9.

Esticar a imagem até a borda resolveria em dois minutos, mas deixaria tudo deformado: personagens largos, círculos virando ovais, texto achatado. Eu queria o jogo ocupando a tela com tudo nas proporções certas.

AntesSeiya na abertura da luta em 4:3, com faixas pretas dos lados
DepoisA mesma cena em 16:9, com mais do templo aparecendo dos lados e o Seiya do mesmo tamanho
A mesma cena: o personagem não muda de tamanho, a câmera é que enxerga mais

Achar a câmera no código do jogo

Para mostrar mais cenário dos lados, quem precisa mudar é a projeção da câmera, e ela está em algum lugar das 13 mil funções. O executável não tem símbolos, então comecei pelo que toda projeção perspectiva usa: a tangente do campo de visão. Só três funções chamam tanf, e duas delas montam a câmera.

Seguindo essas duas, cheguei a um método que todas as câmeras chamam pela tabela virtual. Pelos argumentos, ele recebe o mesmo que o sceVu0ViewScreenMatrix da biblioteca da Sony: distância da tela, escala horizontal (ax), escala vertical (ay), centro e planos de recorte. E faz duas coisas com eles:

0x002A0E50 · set_view_screen (trecho)
lwc1 $f1, 0xF8($a0) # largura da tela da câmera
div.s $f1, $f1, $f26 # largura / ax
div.s $f0, $f0, $f25 # altura / ay
jal func_002A38D0 # matriz de clip: o culling do jogo usa esta
...
jal func_002A1010 # matriz view-screen: a que vai para o VU1

Isso facilitou bastante. Se eu mudar o ax na entrada dessa função, as duas matrizes mudam juntas: o 3D fica com 3/4 da largura e, com a imagem mostrada em 16:9, volta às proporções certas, agora com mais cenário. Como o culling usa a mesma conta, nada que está visível nas bordas é descartado.

Um gancho no recompilador

O recompilador já sabia trocar uma função do jogo por uma nativa (é assim que leitura de disco e memory card funcionam). Aqui eu não queria trocar nada, só mexer em um argumento e deixar o código original seguir. Então ele ganhou um terceiro campo na tabela de funções nativas:

port/hle.txt
0x002A0E50 hle_widescreen_view before # set_view_screen da câmera: ax * 3/4 para 16:9

O recompilador gera a chamada nativa logo na entrada da função traduzida, e o resto continua sendo o código do jogo:

port/hle/widescreen.c
void hle_widescreen_view(Ctx *c) {
if (widescreen_on()) F(13) *= 0.75f; // $f13 é o ax
}

O HUD também ficou largo

Com o 3D certo, o problema passou para o 2D. Barra de vida, retratos, menus e legendas são desenhados em coordenadas de tela, e mostrar a imagem em 16:9 alargou tudo isso. A correção é desenhar só o 2D com 3/4 da largura, em volta do centro. O difícil é saber o que é 2D, porque o GS só recebe triângulos e não sabe para que eles servem.

Registrei todos os desenhos de um quadro de luta e o padrão ficou claro. O 3D chega pelo VU1, que transforma os modelos e manda os triângulos direto para o GS. O HUD e os menus chegam pelo processador principal, por outro caminho. Os poucos desenhos 2D que não podem ser comprimidos têm marcas próprias:

  1. Veio do VU1?

    Então é 3D e fica como está.
  2. Usa como textura uma imagem que o próprio jogo desenhou?

    É pós-processamento (brilho, desfoque) e cobre a tela toda.
  3. Ocupa a largura inteira sem textura?

    É fade ou limpeza de tela.
  4. Recortado para uma parte da tela?

    É o mapa de sombra sendo gerado num canto do buffer.
  5. E o resto?

    É HUD e menu, desenhado com 3/4 da largura e centralizado.

Isso deixou o HUD certo, mas trouxe uma surpresa: apareceram textos como “Blast!!” colados nas bordas. São elementos que o jogo deixa esperando fora da tela 4:3 e faz deslizar para dentro na hora certa. Comprimidos, eles caíam dentro da área visível. A correção foi comprimir também o recorte (o scissor) desses desenhos, para que o que estava fora da tela 4:3 continue fora.

Luta entre Seiya e Shiryu em 16:9, com o templo ocupando a tela e a barra de vida nas proporções originais
Luta em 16:9: o cenário ocupa a tela toda e o HUD fica como no original.
Tela VS com os retratos de Seiya e Shiryu no centro e o templo 3D aparecendo nas laterais
Na tela VS a arte 2D fica no centro e o templo 3D ao fundo ocupa as laterais.

Artes 4:3 de tela cheia

Tela de título, logos e as cenas desenhadas do modo história são imagens 4:3 prontas. Não existe nada além das bordas delas, então ou elas ficam com faixas pretas ou são esticadas. Preferi um meio-termo: nos quadros em que nada foi desenhado nas laterais, elas mostram a própria imagem ampliada e desfocada. A arte continua nítida e intacta no centro.

Para decidir quando fazer isso, cada desenho é marcado se pinta alguma coisa fora dos 3/4 do meio. Numa luta o 3D marca as laterais e nada muda. Numa cena do modo história nada marca, e o desfoque entra. O desfoque reaproveita o shader que já fazia a média das amostras do supersampling, só que espalhando as amostras por uma área bem maior.

Tela de título de Saint Seiya: The Hades com a arte nítida no centro e as laterais preenchidas pela mesma arte desfocada
Tela de título: a arte original no centro e as laterais com ela mesma, desfocada.

Resolução que acompanha a janela

Junto com o widescreen veio outro ajuste. A escala interna de renderização era escolhida uma vez, pela tela do monitor. Agora ela acompanha a altura da imagem na janela e muda na hora quando a janela muda de tamanho. Em 1080p o 3D é desenhado em 2048×1792 e reduzido com supersampling, o que deixa as bordas lisas. Na troca de escala o que já estava desenhado é copiado para a resolução nova, então a imagem não pisca.

Próximo passo: texturas em HD

A resolução maior deixa o 3D nítido, mas não cria detalhe nas texturas, que continuam pequenas como no disco. Para isso o port ganhou pacotes de texturas. Com SSTHEH_TEX_DUMP=1 ele salva cada textura como PNG, com um nome tirado do próprio conteúdo, que se repete de uma execução para outra. Um PNG com esse nome, em qualquer resolução, entra no lugar da original. Testei trocando as 380 texturas que aparecem até uma luta por versões quatro vezes maiores. O próximo passo é gerar um pacote ampliado com IA e mostrar o resultado aqui.

O que aprendi

  1. Num executável sem símbolos, comece pelo que o código obrigatoriamente usa. Para achar a câmera, bastou listar quem chama tanf.
  2. Mudar o jogo na origem rende mais do que corrigir o resultado. Um argumento na entrada da função ajustou a imagem e o culling juntos.
  3. A GPU não sabe o que é HUD, mas o caminho por onde cada desenho chega sabe. Registrar um quadro inteiro e olhar os padrões resolveu mais do que qualquer heurística pensada antes.
  4. Teste com o jogo de verdade, não só com a tela que você está olhando. Os elementos fora da tela só apareceram numa luta.

Tudo isso liga e desliga com F2, e o 4:3 original continua a um toque de distância. O código está no repositório do port, que não distribui nada do jogo: cada um compila a partir do próprio disco.

Neste post
Escrito por Matheus Nunes PereiraEngenheiro de Software. Sistemas distribuídos, telemetria em tempo real e software que conversa com hardware. Se tiver um problema assim, vamos conversar.Engenheiro de Software

Sistemas distribuídos, telemetria em tempo real e software que conversa com hardware. Se tiver um problema assim, vamos conversar.