Olá a todos!
Há uns meses fui buscar uma caixa a garagem e encontrei lá dentro coisas que já não sabia que tinha. Cartuchos de Mega Drive, um par deles com a etiqueta a descolar, dois CDs de PlayStation dentro de capas trocadas, e um cabo SCART que provavelmente já não serve para nada em casa nenhuma. Passei uma tarde a tentar perceber o que é que ainda dava para ligar a alguma coisa. A resposta, sem grande surpresa, foi quase nada.
Aquilo levou-me a uma conclusão desconfortável para quem gosta de guardar coisas: eu tinha os jogos, tinha o hardware, e mesmo assim não conseguia jogar. O que faltava não era nostalgia, era infraestrutura.
Foi por aí que cheguei ao RomM. E fico com a sensação de que muita gente ainda o arruma mentalmente na gaveta errada, a gaveta dos “gestores de ROMs”, quando ele hoje é outra coisa: um servidor que arruma, enriquece, serve e corre a vossa coleção, com clientes para telemóvel, para consolas portáteis e para Windows, e com emulação nativa a correr do lado do servidor. É, na prática, uma consola que está sempre ligada e que ninguém desliga quando uma licença expira.
Este post é o que eu gostaria de ter encontrado quando comecei: o que é, como se monta, o que vale mesmo a pena ligar, e onde é que dói e se seguem este blog a algum tempo já sabem que nas férias faço o meu post nerd habitual, mas para atividades de ferias :]

O que o RomM e o que faz?
A ideia base é simples. Vocês apontam-lhe uma pasta com ROMs organizadas por plataforma. Ele faz o scan, identifica os jogos, vai buscar capas, screenshots, descrições, datas e classificações a várias bases de dados, e apresenta tudo numa interface que se navega com rato, com dedo, com teclado ou com comando. A partir daí, cada jogo tem uma página, e nessa página há um botão para jogar.
O projeto atualmente suporta mais de 400 plataformas, o que inclui a lista óbvia e também sistemas que quase ninguém emula por vontade própria. A interface foi refeita de raiz na versão 5, com um sistema de desenho novo, e nota-se: a versão anterior do frontend ficou congelada no repositório, com verificações automáticas que rejeitam alterações a esse código. Foi um corte a sério, não uma camada de tinta.
Corre em Docker, guarda os dados numa base de dados relacional (MariaDB por omissão, com MySQL e PostgreSQL suportados), usa Valkey para as tarefas em segundo plano, e serve tudo através de nginx e gunicorn dentro do mesmo contentor. Nada de exótico. É deliberadamente aborrecido na arquitetura, o que eu considero um elogio.
Montar isto em quinze minutos
O ponto de partida é um docker-compose.yml. Este é o que uso, alinhado com a documentação da versão 5.1:
volumes:
mysql_data:
romm_resources:
romm_redis_data:
services:
romm:
image: rommapp/romm:latest
container_name: romm
restart: unless-stopped
environment:
- DB_HOST=romm-db
- DB_NAME=romm
- DB_USER=romm-user
- DB_PASSWD= # igual ao MARIADB_PASSWORD
- ROMM_AUTH_SECRET_KEY= # gerar com: openssl rand -hex 32
- SCREENSCRAPER_USER=
- SCREENSCRAPER_PASSWORD=
- RETROACHIEVEMENTS_API_KEY=
- STEAMGRIDDB_API_KEY=
- HASHEOUS_API_ENABLED=true
volumes:
- romm_resources:/romm/resources # capas, screenshots e afins
- romm_redis_data:/redis-data # cache das tarefas em segundo plano
- /caminho/para/library:/romm/library
- /caminho/para/assets:/romm/assets
- /caminho/para/config:/romm/config
ports:
- 8080:8080
depends_on:
romm-db:
condition: service_healthy
restart: true
romm-db:
image: mariadb:latest
container_name: romm-db
restart: unless-stopped
environment:
- MARIADB_ROOT_PASSWORD=
- MARIADB_DATABASE=romm
- MARIADB_USER=romm-user
- MARIADB_PASSWORD=
volumes:
- mysql_data:/var/lib/mysql
healthcheck:
test: [CMD, healthcheck.sh, --connect, --innodb_initialized]
start_period: 30s
start_interval: 10s
interval: 10s
timeout: 5s
retries: 5
Três notas antes de arrancarem.
Gerem a chave de autenticação com openssl rand -hex 32 e guardem-na num sítio a sério. Se a perderem, perdem as sessões todas. Be very warned. Ask me how i know…
Aquele volume para /redis-data parece estranho e tem explicação: a imagem normal traz o Valkey lá dentro, e é ali que ele guarda o estado. Se preferirem um Redis ou Valkey externo, também é suportado, e para instalações maiores é o que eu faria.
E se já tiverem uma base de dados fora do stack, usem-na. Só tenham atenção às diferenças de compatibilidade entre MariaDB e MySQL, que continuam a morder de vez em quando.
Depois é o costume:
docker compose up -d
docker ps -f name=romm
Ao primeiro arranque não é preciso adivinhar nada. A aplicação leva-vos a um assistente de configuração, e o primeiro utilizador criado fica automaticamente com o papel de administrador.
A pasta manda
O RomM identifica plataformas pelo nome da pasta, portanto a estrutura da biblioteca é meio caminho andado para um scan que corre bem:
library/
└── roms/
├── nes/
├── snes/
├── n64/
├── gc/
├── genesis/
├── dreamcast/
├── psx/
├── ps2/
└── psp/
Se já usam o EmuDeck, o Batocera ou coisa parecida, é provável que a vossa organização atual já sirva sem tocar em nada. Quem tiver a biblioteca arrumada de outra maneira, com pastas por fabricante, vai ter de a achatar ou definir plataformas personalizadas.
Uma melhoria recente que vale ouro em bibliotecas grandes: quando renomeiam ou movem uma ROM, o sistema reassocia-a à entrada existente através do hash do ficheiro, em vez de a marcar como desaparecida e criar uma entrada nova ao lado. Poupa aquelas tardes em que se passa mais tempo a arrumar duplicados do que a jogar.
Metadados: parem de pensar em nomes de ficheiro
Esta é a parte onde o RomM se distanciou mais da concorrência, e onde eu vejo mais gente a configurar mal por hábito antigo.
A abordagem tradicional dos scrapers é adivinhar o jogo pelo nome do ficheiro. Funciona até deixar de funcionar, e deixa de funcionar exatamente nos casos irritantes: expansões autónomas confundidas com o jogo base, versões regionais trocadas, edições especiais tratadas como o original.
A abordagem que o projeto recomenda hoje é outra: calcular hashes e perguntar a bases de dados que respondem por hash. Os fornecedores recomendados no ficheiro de exemplo são o ScreenScraper, o RetroAchievements, o SteamGridDB e o Hasheous, este último ativado apenas com uma variável de ambiente, sem chave nenhuma. Há ainda o Playmatch, que agrega correspondências de várias fontes e que pode ser auto-hospedado, o LaunchBox, e o repositório de thumbnails do libretro como fonte de artwork de primeira classe. O IGDB continua disponível para quem quiser, com credenciais obtidas através da Twitch, mas já não é o centro da história nem vem preenchido por omissão.
Aquela fonte do libretro resolve um chato antigo de quem vive na Europa: uma ROM marcada como “(Europe)” apanha a capa PAL, e não a caixa americana com o logótipo diferente e a classificação errada.
Também dá para definir prioridades por campo, o que é uma finura que eu andava a pedir há muito. As capas podem vir de uma fonte, os screenshots de outra, os manuais de uma terceira:
scan:
priority:
cover:
- igdb
- ss
screenshot:
- ss
- igdb
manual:
- launchbox
region_mode: prefer_rom_tags
Aquele region_mode faz com que as etiquetas de região no nome do ficheiro ganhem à lista de prioridades, que é quase sempre o que se quer numa biblioteca europeia.
Quem detesta editar YAML à mão pode fazer isto tudo pela interface: existe agora um ecrã de definições de scan que edita a secção scan.* inteira do config.yml, acessível pelo menu do utilizador.
O primeiro scan de uma biblioteca grande é um evento, não um detalhe. Calcular hashes de arquivos comprimidos, de imagens de disco, de CHD e de contentores de PSP custa I/O e custa CPU. A boa notícia é que é uma vez.
Jogar no browser
A forma mais simples de jogar é aquela que não exige instalar nada: o EmulatorJS, que é o RetroArch compilado para WebAssembly, a correr dentro do browser de quem está a jogar. Carrega-se em jogar na página do jogo e está feito, seja no portátil, no tablet ou no telemóvel.
Há dois motores adicionais para casos específicos. O Ruffle, para conteúdos Flash, e um caminho próprio para jogos de MS-DOS. Quem tem uma pasta esquecida de jogos de DOS vai gostar.
O ponto fraco é previsível: quem faz o trabalho é a máquina do cliente. Para 8 e 16 bits é irrepreensível. Para a primeira PlayStation dá-se bem. A partir daí começa a sofrer, e num telemóvel modesto sofre depressa. Again… ask me how i know…
Depois há duas coisas que são tecnicamente supérfluas e que eu adoro na mesma. Um shader CRT, que simula o aspeto daqueles ecrãs antigos, e capas em 3D interativo, que se rodam com o rato sem qualquer razão prática. Sobre o shader, há aqui um argumento sério escondido na brincadeira: a fidelidade de uma emulação não se esgota na precisão do processador emulado. Aqueles jogos foram desenhados por pessoas que sabiam como é que uma televisão da altura misturava as cores. Uma grelha de píxeis perfeitos é, em certo sentido, menos fiel ao original do que um bocadinho de sujidade simulada.
Ah, e as bandas sonoras guardadas na pasta soundtrack do jogo tocam diretamente na página de detalhes. Coisa pequena. Usa-se mais do que se esperaria.
Emulação do lado do servidor, a novidade a sério
Aqui está a funcionalidade que muda a categoria do projeto.
Em vez de correr o emulador no browser, o RomM consegue lançar o jogo num emulador nativo, dentro de um contentor próprio, e enviar imagem, som e o caminho de volta do input para o browser através de WebRTC. A emulação corre no servidor, com binários reais. O portátil da sala passa a ser um ecrã.
Na primeira leva vêm três integrações: PCSX2 para PS2, Dolphin para GameCube, Wii e Wii U, e xemu para a Xbox original. Cada emulador vive num contentor baseado nas imagens da linuxserver, com um stream Selkies e um pequeno serviço HTTP ao lado, o broker, que é com quem o servidor fala para mandar lançar, gravar ou mexer no volume. Há mais integrações anunciadas, incluindo rpcs3 para a PS3.
Agora a parte menos boa….
Isto está declaradamente em desenvolvimento inicial. O endereço de cada contentor tem de estar acessível aos clientes e servido por HTTPS, porque o WebRTC exige contexto seguro. Ou se usa o certificado self-signed que vem na caixa, ou se põe um reverse proxy com TLS à frente. Sem isso resolvido, nem vale a pena começar. O broker, esse, é chamado do lado do servidor, e aí HTTP chega perfeitamente; se os contentores partilharem a rede Docker, basta usar o nome do contentor.
Duas limitações que convém interiorizar antes de se entusiasmarem. O broker lança as ROMs como ficheiros diretos, o que significa que não há extração de arquivos: se a vossa biblioteca de PS2 está toda em .zip ou .7z, isto não vos serve como está. E só existe uma sessão por plataforma de cada vez, porque atrás de cada plataforma há um único contentor. As sessões ficam no Valkey com uma reserva atómica, pertencem a quem as abriu, e um administrador pode forçar a libertação quando alguém fecha o separador sem sair em condições.
Os save states desta modalidade também não são os mesmos das sessões de browser. Vivem dentro do contentor do emulador, com um número fixo de slots manuais e um slot reservado ao “gravar e sair”, que é sobrescrito na saída seguinte. Coexistem dois universos de saves, e convém não os confundir.
A minha opinião sem diplomática: para a maior parte das pessoas, isto ainda não é a primeira coisa a instalar. Dá trabalho, exige CPU disponível no anfitrião e é a primeira iteração de um sistema que ainda vai mudar de forma. Para quem tem um servidor com folga e sempre quis jogar Wind Waker a partir do sofá sem instalar nada, é a coisa mais interessante a acontecer neste espaço nos últimos anos. Escolham o vosso lado com honestidade sobre o hardware que têm.
Os saves que nos seguem
Esta parte parece burocrática e é, na verdade, o que faz a diferença entre uma experiência agradável e uma coleção de ficheiros soltos.
O servidor tem um motor de sincronização de saves. Os dispositivos, sejam apps, consolas portáteis ou browsers, registam-se com um token e passam a ser identificados individualmente. O servidor sabe qual deles tem a versão mais recente de cada save, sinaliza conflitos quando dois dispositivos mexem no mesmo ficheiro, e sincroniza de várias maneiras: a pedido, por pasta vigiada, ou por tarefa agendada em segundo plano. Há também registo de sessões de jogo, com o tempo de janela e o tempo de ecrã ligado contabilizados em separado, porque um dispositivo suspenso não está a jogar.
Traduzindo para efeitos de vida do dia a dia: começam um RPG no portátil ao domingo, continuam na consola portátil no comboio à segunda, e ninguém perde nada pelo caminho.
Além disso, os saves e savestates podem ser partilhados com outros utilizadores, o que serve para speedruns, para desbloqueios, para new game plus, ou simplesmente para mostrar onde é que se chegou.
Há ainda integração com o RetroAchievements, para quem gosta de troféus em jogos que nunca os tiveram, e um patcher de ROMs do lado do servidor: aplicam-se patches guardados ou carregados na hora, sem o ritual de encontrar a ferramenta certa, correr o patcher e rezar para os checksums baterem certo.
Netplay*
Dá para jogar com outras pessoas em tempo real nas sessões de browser. O anfitrião cria uma sala, opcionalmente com palavra-passe, e até mais três jogadores entram como jogadores 2 a 4, conforme o core suportar. Cada um usa o seu comando localmente, os inputs vão para o anfitrião, que corre o jogo e distribui o vídeo.
Agora as ressalvas, que são muitas e vêm da própria documentação. O netplay anda instável nas builds nightly do EmulatorJS, com ligações falhadas e dessincronizações à espera de correções a montante. Precisa de servidores ICE configurados, STUN e TURN, sem os quais só funciona se estiverem todos na mesma rede local. E os cores mais testados são poucos: SNES9x, Mupen64Plus, Mednafen PSX e Genesis Plus GX.
A latência típica anda nos 50 a 150 milissegundos, mais em saltos transatlânticos ou quando o tráfego passa por TURN. Traduzindo para linguagem de sala: dá para um Mario Kart 64 com os amigos numa sexta à noite. Não dá para levar a sério um jogo de luta, e não há vergonha nenhuma em usar o FightCade para isso.
Partilhar a biblioteca
Se são a única pessoa a usar o servidor, podem saltar esta secção. Se não são, leiam com atenção, porque o modelo de permissões é fino.
Existem grupos, permissões por utilizador e por grupo, e um grupo por omissão configurável para novos registos. Por cima do grupo dá para conceder ou revogar permissões individualmente, e há ainda sobreposições por entidade, o que permite controlar quem vê o quê ao nível da plataforma ou da coleção. Há registo por convite, autenticação nativa e suporte para OIDC, com guias para Authelia, Authentik, Keycloak, PocketID, Zitadel e VoidAuth.
Duas recomendações práticas que dou sempre. Ponham um reverse proxy à frente com TLS a sério, e criem contas separadas com permissões mínimas para integrações, em vez de andarem a distribuir as credenciais principais. Isto é particularmente relevante no plugin de Windows, que guarda a palavra-passe em texto simples, como a própria documentação avisa.
Os clientes: o servidor não é o fim da história
Uma das coisas que mais me convenceu foi perceber que a equipa mantém clientes próprios, em vez de deixar tudo entregue à sorte da comunidade.
Para Android existe o Argosy Launcher. Navega a biblioteca, autentica-se por tokens de API emparelhados através de um código curto ou de um código QR (para não terem de escrever à mão uma cadeia de 44 caracteres), descarrega ROMs a pedido e lança o RetroArch ou o emulador configurado, com sincronização opcional dos saves no fim da sessão.
Para consolas portáteis com Linux, tipo muOS e NextUI, existe o Grout, escrito em Go. Puxa ROMs para o cartão SD já na estrutura de pastas que essas interfaces esperam, empurra saves e states de volta, e corre por agenda, ao fim da sessão ou quando o dispositivo fica em repouso. Reparem no detalhe que torna isto útil de verdade: entre sincronizações, a consola funciona offline. Não precisa do servidor à mão para jogar no comboio.
Para Windows há um plugin do Playnite, que importa a biblioteca para dentro do gestor onde já estão os jogos da Steam, da Epic e da GOG. Carrega-se em instalar, a ROM é descarregada, e o jogo abre pelo emulador que já estava configurado. Os jogos de Mega Drive aparecem ao lado dos jogos comprados no mês passado, o que tem qualquer coisa de reconfortante.
Há também clientes de terceiros na comunidade, e há exportação de metadados para formatos de outras interfaces, incluindo gamelist.xml e o formato do Pegasus. Este último ponto é mais importante do que parece: os metadados que o servidor recolheu não ficam presos ao servidor. Se um dia quiserem sair, saem com a casa arrumada. Para mim isto é quase um critério de escolha em software self-hosted, e é surpreendentemente raro.
Quanto ferro é preciso para isto tudo correr?
Para uso normal, um servidor modesto chega bem. Houve um trabalho sério de desempenho nas versões recentes, com índices novos, colunas geradas, materialização das coleções virtuais e agregação das estatísticas a partir de tabelas próprias, e isso nota-se em bibliotecas com muitos milhares de entradas. Em repouso, o serviço é discreto, na casa das poucas centenas de megabytes de RAM.
As cargas que mudam a conta são duas. O scan inicial, com o cálculo de hashes, que é pesado em I/O e em CPU mas acontece uma vez. E o streaming de emuladores, que é permanente: um contentor do Dolphin a emular Wii não é um processo de fundo simpático, e ainda por cima quer aceleração gráfica no anfitrião e largura de banda de subida se a sessão for consumida fora de casa. É a diferença entre alojar uma biblioteca e alojar uma consola.
Manutenção: Lições aprendidas
Este projeto tem um ritmo de lançamentos alto, o que é bom para as funcionalidades e exigente para quem opera. Estas são as coisas que eu queria ter sabido antes.
Leiam as notas de lançamento antes de subir de versão maior. Sério. E façam cópia da base de dados primeiro.
MariaDB, triggers e binary logging. As versões recentes usam tabelas mantidas por triggers para acelerar filtros e coleções. Se tiverem binary logging ligado e o utilizador da aplicação não tiver SUPER, o MariaDB recusa a criação dos triggers e a migração falha. Corre-se isto como root ou administrador da base de dados, não como utilizador da aplicação:
SET GLOBAL log_bin_trust_function_creators = 1;
Como um SET GLOBAL se perde no reinício, ou se acrescenta ao my.cnf na secção [mysqld]:
log_bin_trust_function_creators = 1
Ou se dá a permissão diretamente ao utilizador da aplicação, em MariaDB 10.5 ou superior:
GRANT BINLOG ADMIN ON *.* TO 'romm'@'%';
O reverse proxy e a página em branco. Se o vosso proxy faz cache das respostas por conta própria, ou reescreve o Cache-Control, acaba a servir um index.html antigo que aponta para ficheiros de JavaScript que a imagem nova já não traz. O sintoma é uma página em branco depois de atualizar, e a solução é limpar a cache do proxy ou da CDN uma vez.
Revejam as permissões depois de atualizações grandes. O modelo mudou o suficiente nas versões recentes para que assumir continuidade seja uma má ideia.
E a regra que vale para tudo o resto: não atualizem a meio de uma sessão de jogo de outra pessoa. Sim, já fiz isso. Sim, ouvi sobre o assunto.
Onde é que notei que isto ainda tem caminho para melhorar
Não quero acabar com uma carta de amor sem contas a pagar, portanto aqui vão as reservas.
A superfície de configuração cresceu muito. Entre variáveis de ambiente, config.yml, definições por utilizador e prioridades por campo, há hoje várias formas de configurar a mesma coisa, e nem sempre é óbvio qual é que ganha. A interface nova de definições de scan ajuda, mas ainda não cobre tudo.
O ritmo de desenvolvimento traz o reverso da medalha habitual. Funcionalidades novas chegam depressa, e algumas chegam claramente na primeira iteração. O streaming é o exemplo maior, o netplay é o exemplo mais frustrante.
E há a dependência de serviços externos para os metadados. O sistema tolera falhas melhor do que tolerava, com fallback entre fornecedores quando uma quota se esgota, mas se as fontes estiverem em baixo, o enriquecimento sofre. O que se joga continua a jogar-se.
Legalize….
O RomM é uma ferramenta legítima de organização e emulação. A legalidade das ROMs é outra conversa e depende do que vocês possuem. Usem cópias de jogos que tenham, ou material em domínio público.
A emulação é uma forma séria de preservação de um património cultural que, na maioria dos casos, os detentores dos direitos não estão a preservar. Isso é um argumento a favor da emulação, e não uma desculpa para descarregar tudo o que mexe. Vale a pena manter os dois pensamentos na cabeça ao mesmo tempo.
Por onde começar
Se estão a ler isto e têm um servidor em casa, façam a versão pequena primeiro. Compose acima, um fornecedor de metadados configurado, uma plataforma só, um scan. Vinte minutos. Vejam a biblioteca aparecer com capas e sinopses e decidam se aquilo vos diz alguma coisa.
Se disser, o resto vem naturalmente: mais plataformas, um reverse proxy com TLS, contas para a família, uma app no telemóvel, sincronização de saves. E se um dia tiverem CPU a mais e vontade de complicar, os contentores de emulação nativa estão ali à espera.
Aquela caixa do arrumo continua no arrumo. Os jogos, esses, andam agora comigo.
Abraço e Boas Férias!
Nuno
P.S.: o de sempre. Respeitem as leis de direitos de autor e utilizem apenas ROMs de jogos que possuam legalmente ou que estejam em domínio público. Isto é sobre preservar história, não sobre pirataria.
Links úteis
- Site oficial do RomM
- Documentação
- Guia de arranque rápido
- Emulator Streaming
- Apps oficiais: Argosy, Grout e plugin de Playnite
- Notas de lançamento
- Repositório no GitHub
Este post foi escrito com auxilio a um LLM privado.