A Saga de programar um jogo no ZX Spectrum continua.
Como sabem, resolvi reeditar o antigo jogo Treinador de Futebol, perpetuado no World of Spectrum e editado pela Softimar, abreviatura de software Tiago e Mário (o meu primo e eu), em 1989. Jogo. Aliás que teve direito a referência recente no livro jovens programadores Portugueses.
A minha ambição levou-me a melhorias enormes face ao jogo original, mas rapidamente, e apesar de toda uma optimização de código que não existiu no jogo original, atingi os limites da máquina. E ao programar num Spectrum 128K pela facilidade de digitação (a digitação é feita letra a letra como nas máquinas modernas, e não com combinações de teclas como no 48K), sem me aperceber rapidamente alcancei os 64 KB de RAM, eliminando assim a possibilidade de suporte ao modelo 48K.
Como referi na primeira parte deste artigo, não é fácil nos dias de hoje programar com tão pouca RAM, tendo de se ter o cuidado com cada byte usado. Mas o certo é que acredito que em 1989 não nos apercebemos do quão perto o treinador de futebol ficou no limite de esgotar os recursos do 48Kb.
A realidade foi que, perante as melhorias, precisei de 64Kb, mas infelizmente atingi o limite da RAM, antes de acabar o jogo.
Dado que o que falta é muito pouco decidi explorar formas de obter mais memória. A primeira foi otimizar o código ao extremo, o que me deu a possibilidade de acabar o mercado de transferências que estava em falta. Mas os finais de época, as descidas e subidas de divisão, e o ecrã de vitória do campeonato, ficaram de fora. Precisava de mais RAM.
Ora dado que o Spectrum 48k estava excluído, porque não usar toda a RAM do 128K?
Infelizmente a coisa não é assim tão simples. O Z80 apenas endereça no máximo 64KB, pelo que aceder a mais RAM é problemático e requer paginação de memória. Uma situação complexa e avançada que é um problema de ser feita com o Basic.
Mas mesmo assim, tentei. E consegui! Mas infelizmente o método não servia. Apesar de só ter copiado para a RAM estendida um bloco de 7 KB recuperar o mesmo para a RAM vídeo era um processo de mais de 1 minuto.
Basicamente tinha de aceder à RAM adicional, ler todos os bytes um a um, e copiar de volta. Um ciclo For Next que no Basic é lento. Super lento.
Apesar de tudo consegui aumentar a capacidade de leitura para 256 bytes de cada vez, reduzindo o tempo de leitura de forma significativa, mas mesmo assim… demasiadamente lento.
A solução que me restava era só uma. Assembler.
Ora o Assembler, vocês já ouviram falar dele. É o código máquina, a linguagem de mais baixo nível que existe. E também a mais rápida que existe. Uma linguagem sobre a qual os meus conhecimentos são muito parcos.
Mas entre parcos e zero ainda vai uma grande diferença, e eu sabia que neste caso apenas precisava de dominar uma pequena parte do assembler: A leitura e cópia de dados. E nesse sentido, dediquei-me a estudar o mesmo, e após alguns dias tinha conseguido copiar uma imagem de uma RAM para a outra. O tempo? Bem, varia dependendo do banco, pois alguns são mais lentos que outros, mas mesmo assim, o caso pior, é uma ridicularia: 41 ms.
O problema com a RAM parecia ultrapassado.
Mas infelizmente não, pois eu precisava de meter na memória extra mais do que um bloco. A ideia era meter os dados para um menu, a criação do estádio onde vemos o jogo, e o ecra de celebração de vitória no campeonato. Um total de 28 KB que não seriam um problema para os restantes 64Kb. Ou seriam?
O certo é que o que funcionava com uma imagem… Não funcionava com as restantes. E descobrir os motivos, foi um berbicacho. Até porque estava perante vários problemas e não apenas um, o que demorou a perceber.
O que eu descobri foi que a paginação de memória não me troca dos 64KB originais para os outros. Dado que o Spectrum está no limite, uma mudança dessas implicava perder os valores todos das variáveis, e mesmo parte do código presente na RAM.
O que o Spectrum permite, para resolver isso, é abrir uma ranhurazinha na “parede da sala” de 64KB, para se ver a “sala do lado”, e passar dados por ela.
Mas ranhurinha ou não, com o uso de assembler, o problema estava resolvido pois a transferência era rápida, e 41ms era aceitável. Agora porque motivo não conseguia meter mais do que um bloco de dados?
O problema é que os restantes 64KB não existem. Se nos primeiros 64KB olhamos para eles como um todo pois tudo é invisível ao utilizador, os restantes 64 Kb são na realidade 4x16KB, pelo que não basta abrir a ranhura na parede. É preciso definir onde a fazemos para que se veja o compartimento que se pretende aceder.
Ou seja, a paginação é mais complexa ainda do que inicialmente parecia pois não se acede a mais 64 KB, mas sim a 4x 16 Kb.
Mas essa parte eu também acabei por conseguir superar, e consegui definir qual dos bancos de ram de 16 KB quero ver.
Mas o Spectrum com 128KB tem 8 bancos de 16 KB. A dúvida agora era saber quais são os que já estão usados e os que estão livres.
Poderiam ser de se esperar que a coisa fosse linear, mas não é. E como linear era que o Basic usasse os 4 primeiros e os restantes 4 estivessem livres (gênero, banco 0, 1, 2 e 3 usados, e 4, 5, 6 e 7 livres). Mas não! E descobrir em quais eu podia escrever sem causar danos ao meu programa foi um problema. Mas depois de descobrir, passei a usar dois bancos adicionais, o 1 e o 6. Dado que cada um tem 16 KB, e cada bloco de dados ocupa perto de 7KB, cada um pode conter dois blocos, e precisando de 4 deles, esta memória chegaria.
Mas depois tive outro problema. Dado que os bancos não são sequenciais, quais os endereços de memória de cada um?
E esta parte ainda hoje me mata. Porque há duas formas de lhes aceder, sendo que basta um pequeno erro de código, e ambas misturam-se, com resultados desagradáveis.
A primeira forma é usando os endereços reais. Basicamente os primeiros 64KB usam os endereços do zero até ao 65535, e os restantes bancos vão usar do 65536 até ao 131070. A questão aqui é que banco contém que parte desses endereços?
Ora isso é possível saber-se, mas para usar endereços reais eu preciso que o Z80 lhes aceda, e como já referi, isso obriga a largar parte da RAM base, com consequências desagradáveis para a estabilidade do programa (na maior parte dos testes, parte do meu código que estava na RAM do Basic, ficou corrompido ou desapareceu). A alternativa era usar endereços lógicos.
O que isso quer dizer? Basicamente tratar cada um dos bancos como se fosse único, com endereços iguais em todos. E isso trabalha bem melhor com a janelinha de abertura pois não causa problemas com a ram base.
Ora os endereços lógicos funcionam muito bem, mas tem um inconveniente. Algo que me demorou duas semanas a perceber. A questão é que quando eu acedo a um banco de memória, mesmo que mude para outro, o anterior mantem-se acessível, algo que eu não sabia, e que as IAs que eu consultava para auxilio me garantiam que não acontecia. Ora o que acontecia é que eu carregava dados para o banco 1, por exemplo nos endereços 40000 e 46912. Tudo corria bem, e elas estavam lá. Mas quando copiava para o banco 6, para os mesmos endereços, a janelinha para o banco 1 mantinha-se aberta, e o comando ia também para o banco 1, copiando os dados para ambos, e escrevendo por cima do que já existia no banco 1. O resultado é que só obtinha metade dos dados, e eu não estava a compreender porque. A resposta é que o que estava nos dois bancos era igual pois eu estava a escrever por cima.
Infelizmente, dado que o que pretendo passar para cada banco são dois blocos de dados que ocupam 7 KB cada um (total de 14 KB), e os bancos são de 16 KB cada um, eu enchia basicamente os bancos, pelo que não tinha possibilidade de usar endereços que não se sobrepusessem. E ao manterem-se os dois bancos ativos, a copia para o segundo banco, escreve por cima do primeiro.
Perceber a causa do problema foi um berbicacho. As IAs são uma vergonha. O Deepseek não percebe a ponta de um chavelho de ZX Spectrum, e insiste comigo que o meu código está todo mal e que não funciona. Para lhe explicar as coisas perco horas, sendo que ele acaba por reconhecer que estou correto, mas passado um bocado, volta à carga insistindo que está tudo mal.
O Gemini é sem dúvida o que mais entende de Spectrum, mas é impressionante como ele está tão atrás dos outros, cometendo sucessivamente erros básicos.
O ChatGPT é o mais avançado, mas insiste nos erros e também os comete, não tendo um conhecimento da arquitetura do Spectrum ao nível do Gemini.
O que me tem valido são duas janelas, uma com o Chat, outra com o Gemini, onde os confronto com as afirmações um do outro, até chegarem a um consenso. Mas mesmo assim há muita ignorâncias nestas IAs que falam como se tivessem certeza do que dizem.
Como auxiliares para código são os dois miseráveis, mas para me explicarem a arquitetura do Spectrum, tem sido uma ajuda preciosa, apesar de os ter de confrontar regularmente com as incongruências. Mas mesmo elas não foram capazes de me dizer o que estava a acontecer. E apenas quando eu formulei de forma insistente a hipótese de estar a escrever em simultâneo para os dois bancos é que elas reconheceram a situação. Apesar de andarem à duas semanas a negaram que isso pudesse estar a acontecer.
A realidade é que, devido a isto, até ao momento ainda não consegui um código funcional que me recupere 4 blocos de dados. Recorri a fóruns de programação do Spectrum, mas em vez de ajuda tudo o que me disseram foi que me meti num caminho que poucos conseguiram trilhar com sucesso.
E infelizmente, devido a isso decidi que vou ter de dar uma pausa ao jogo. Afastar a cabeça por uns dias a ver se vem ideias frescas. Porque estou à beira de conseguir o que pretendo, mas devo estar a cair em algum erro básico que me está a entupir e entretanto estou perto de um esgotamento.
Acréscimo de ultima hora.
Basicamente o que aconteceu foi que desisti do jogo. Estava de tal forma cansado que resolvi mandar tudo às urtigas.
O facto é que perante isso, sem stresses resolvi meter-se a ler sobre a arquitetura interna do Zx Spectrum. Basicamente deixar de acreditar em IAs e ir eu mesmo à busca da informação.
O resultado foi que ao fim de 10 minutos de leitura, estava entusiasmado com o que lia e voltei a programar. E à primeira tentativa com um novo método, estava com sucesso. Acedi a três bancos, guardei 6 blocos de código, e retornei os mesmos com sucesso.
Por outras palavras, o jogo não está morto, e o problema resolvido tendo agora acesso a toda a RAM onde leio e escrevo à vontade. Irei agora, com calma, acabar o que falta, e resolver um ou outro problema que me parece menor que ainda existe, acreditando que dentro em breve o jogo estará publicado. Dado que agora acedo à totalidade dos 128 KB, talvez tire partido disso e melhore o grafismo ainda mais um bocadinho.
Só lamento ter matado a cabeça por duas semanas à procura de uma solução recomendada pelas IAs, que se revelou um beco sem saída. Tudo devido à Estupidez artificial das mesmas.
Que aventura!!
Está a ser muito divertido e interessante de acompanhar, Mário!