O gerador de fonte 4x8 magico
Algum tempo atrás Breno Motta, que é um dos membros do grupo MSX Brasil Oficial do Facebook publicou uma foto de um pequeno programa BASIC que cria um estilo de fonte “comprimido” com 4 x 8 pixels na Tela 2 com base no texto mostrado na tela. Foi muito interessante ver como o programa foi capaz de criar uma fonte realmente perfeitamente legível com uma pegada tão pequena, e fiquei muito curioso em aprender mais sobre como funcionava, e descobri que é muito simples, mas também muito inteligente !
Sobre o código do Breno
Esta é a foto que Breno Motta publicou no grupo do Facebook no dia 1º de janeiro de 2022:
Ele também postou uma foto do código BASIC que eu digitei no site MSXPen e também estou fornecendo abaixo:
1 cor 15,4,4 2 tela 2 3 abra "grp:" para saída como #1 4 preset(0,20),15 5 print #1,"12345678901234567890123456789012" 6 for g = 0 a 7 7 for f = 0 a 255 8 a = ponto(f,g+20) 9 se a = 15 então pset(f*.5,g+48),a 10 se a = 15 então pset(128+f*.5,g+48), a 11 próximo f,g 12 vai até 12
Então, como esse código funciona? Começamos alterando as cores da tela e o modo de tela nas primeiras 2 linhas. Em seguida, na linha 3, usamos o comando ABRIR para criar um arquivo de saída identificado como #1, que é o “canal” que usamos para imprimir caracteres na Tela 2, pois essa não suporta o uso de PRINT diretamente.
A linha 4 irá usar o comando PRESET para basicamente posicionar o cursor na posição X = 0 e Y = 20, o que no modo texto poderia ser feito usando o comando LOCALIZAR Observe que um pixel branco será aceso nesta posição da tela, pois também estamos informando a cor 15 no código, mas se omitirmos esse valor o pixel não será exibido na tela sem alterar o funcionamento do código.
Apenas uma observação lateral aqui: a diferença entre PSET e PRESET é que o primeiro comando acenderá um pixel na tela usando a cor definida para o primeiro plano, e o segundo definirá o pixel usando a cor de fundo, caso nenhum valor for fornecido como cor parâmetro, então eles basicamente Setam e Resetam a cor do pixel.
A linha 5 imprimirá a string que será utilizada durante o processo de “compressão” na posição anteriormente utilizada pelo comando PRESET. É importante observar que há um motivo especial para esse local nas próximas etapas. Em seguida, criamos dois loops aninhados com G e F, para que possamos iterar G de 0 a 7 e F de 0 a 255. Ambas as variáveis serão usadas para ler a string e “imprimir os pixels” da fonte compactada como veremos mais adiante.
Agora a linha 8 é interessante. Usaremos o comando PONTO para ler a cor do pixel na posição X por Y, o que significa que basicamente estamos lendo o texto impresso na tela e anotando o status de cada um dos pixels com base em se eles estão acesos ou não. Inteligente, não é? Usando F para a posição X (coluna) e G+20 para a Y (linha), o código lerá os pixels das linhas 20 a 27 da tela, que são os 8 pixels do caractere na posição, e repetirá isso em a próxima coluna 255 vezes que é a resolução horizontal da Screen 2.
Agora a mágica da compressão acontece na linha 9. Verificamos se o valor armazenado em A pelo comando POINT é igual a 15 (White), e usamos o comando PSET para iluminar os pixels na linha 48 (G+48) e linha F *.5, que basicamente significa “multiplicar F por 0,5 e definir o pixel lá”. Como o VDP não funcionará com números fracionados, ele ignorará qualquer valor após o ponto decimal, resultando em um pixel definido na mesma posição do anterior quando o resultado da multiplicação for uma fração. Se alterarmos PSET(F*.5,G+48) por (F/2,G+48) teremos a mesma saída, mas pode haver alguma diferença de desempenho que não pude observar até agora, então sua milhagem pode variar . A linha 10 faz a mesma coisa, mas da linha 128 em diante, mostrando que a saída resultante é o dobro do número de caracteres da string original.
Então, basicamente, o que o código faz é copiar os 8 pixels da primeira coluna do caractere da linha 20 para a coluna 0 e linha 48 na tela, então plotar os próximos 8 pixels na mesma coluna 0, então mover para a coluna 1 da linha 20 e copiar a 3ª e a 4ª coluna no mesmo local, repetindo o mesmo padrão até chegar ao final da linha 20 na coluna 256. Também é interessante notar que isso funcionará para qualquer string inserida na linha 5, desde que apenas ocupe uma única linha e você não se importe em ter a mesma string repetida na linha 48!
Alterando e melhorando o código
Este código é bom e curto, mas cara, é lento! Usar o comando PSET do BASIC não é a coisa mais rápida em uma máquina MSX, então o que podemos fazer para melhorar isso? Bem, poderíamos codificar a coisa toda em conjunto!
Será tão curto quanto o programa BASIC original? Não. Tenho certeza de que poderia reduzir o programa para metade de seu tamanho usando operadores de deslocamento nas rotinas BIT e SET/RES, mas quero deixar o código legível e mais fácil de entender.
Ele vai ser mais rápido? Com certeza vai!
Será a mesma lógica? De jeito nenhum. Alguns comandos de alto nível que temos no BASIC não são facilmente recriados no ASM, então tivemos que pegar alguns atalhos, mas os resultados são 99% similares.
Vamos ver como funciona o código:
; bios chama CHGMOD: equ 0x005F ;Muda o modo de tela para o valor definido em A GRPPRT: equ 0x008D ;Rotina da BIOS para imprimir caractere em modo de tela gráfica CHGET: equ 0x009F ;Aguarda que uma tecla seja pressionada e armazena o valor em A LDIRVM : equ 0x005C ;Copiar blocos BC do endereço VRAM armazenado em DE para o endereço RAM armazenado em HL LDIRMV: equ 0x0059 ;Copiar blocos BC do endereço RAM armazenado e DE para o endereço VRAM armazenado em HL FILVRM: equ 0x0056 ;Preencher blocos BC da VRAM começando em endereços armazenados em HL com os valores de A ;System Variables ;Essas duas variáveis não estão documentadas em documentos Grauw, e eu as encontrei em ;https://www.msx.org/forum/msx-talk/development/ posicionamento-grafico-cursor-na-tela-2 GRPACX: equ 0xFCB7 ;Mostrar ou definir a posição atual da linha do cursor GRPACY: equ 0xFCB9 ;Mostrar ou definir a posição atual da coluna do cursor ; o endereço do nosso programa org 0xD000 start: ld a,2 ;Precisamos da Tela 2 para este programa chamar CHGMOD ;então chamamos CHMOD com A=2 para mudar para o modo gráfico ld hl,GRPACX ;Queremos imprimir nossa string de números na coluna 0 ld (hl),0 ;e para isso definimos GPRACX ld hl,GRPACY ;A string será impressa na linha 16 em vez de 20 como no programa BASIC ld (hl),16 ;tornando mais fácil de copiar os valores à frente ld hl,$0600 ;$0600 é a posição da VRAM que se correlaciona com a linha 48 na tela se a tabela de nomes não for modificada ld (Vaddr),hl ;então salvamos esse valor na variável Vaddr ld hl,$0200 ;$0200 é o endereço da VRAM onde encontraremos os dados padrão da string impressa na tela ld (copyStringAddr),hl ;então salvamos esse valor na variável copyStringAddr ld hl,message ;Vamos colocar o endereço da string da mensagem em HL chama printString ;e chama nossa rotina para imprimir ld bc,16 ;Com a string já impressa, definimos um contador de loops no BC e repetimos o código startLoop startLoop: push bc ;Salva BC na pilha chama copyString ;Chama a rotina copyString chama compressString ;Chama a rotina compressString que vai mudar o padrão, "comprimindo" o caractere chama pasteString ;Chama a rotina pasteString que imprimirá os novos padrões na linha 48 pop bc ;Recupera BC da pilha DEC BC ;Diminui BC ld a,c ;Carrega C em A ou b ;Então OR B com A, que levantará o sinalizador Zero em F se BC = 0 jp nz,startLoop ; repete o loop até BC = 0 chama CHGET ;Aguarda que uma tecla seja pressionada RET ;e retorna para BASIC printString: ld a, (hl) ;Esta é nossa maneira usual de imprimir strings na tela, carregando o valor apontado por HL em A cp 0 ;e verificando se o valor em A = 0, que é o nosso controle de fim de string ret z ;se o sinalizador Zero for levantado, retornar ao bloco inicial chamar GRPPRT ;Chamamos GRPPRT em vez de CHPUT porque estamos usando a graphic mode inc hl ; Em seguida, aumentamos HL para pular para o próximo valor da mensagem jr printString ; e repetir o loop copyString: ld hl,(copyStringAddr) ;Para copiar o padrão da tela vamos carregar o valor de copyStringAddr para HL ld de,stringData ;e carregar em DE a posição para stringdata ld bc,16 ;e 16 em BC para o número de bytes a ser copiado de HL para DE chame LDIRMV ;quando LDIRMV for chamado. Isto irá copiar os dados do padrão da VRAM para a RAM ld de,$0010 ;Precisamos aumentar a posição apontada por copyStringAddr, então carregamos $0010 em DE ADD HL,DE ;e adicionamos HL com DE, pois HL já tem o valor de o copyStringAddr carregou anteriormente ld (copyStringAddr),hl ;depois envie os resultados de volta para copyStringAddr ret ;e retorne ao bloco StartLoop compressString: ld ix,stringData ;Agora vem a parte complicada. Carregamos a posição stringData em IX ld bc,8 ;e configuramos um novo contador em BC para 8 loop: ;Vamos verificar o valor de cada bit de 6 a 0 (da esquerda para a direita, lembra?) ;e replicar o bit par valores para os ímpares anteriores, reduzindo 8 bits para 4 bits 6,(ix) ;Verifica o valor do bit 6 - mesmo chama nz,bit7Set ;e define o bit 7 se não for zero, resultando no bit 7 tendo os dados de ambos os bits 7 e 6 bit 5,(ix) ;Verifica o valor do bit 5 - ímpar chama z,bit6Reset ;e reseta o bit 6 se zero chama nz,bit6Set ;ou define o bit 6 se não for zero bit 4,(ix) ;Verifica o valor do bit 4 - mesmo chamar nz,bit6Set ;e definir bit6 se não for zero, resultando no bit 6 com os valores dos bits 5 e 4 bit 3,(ix) ;Verificar o valor do bit 3 - ímpar chamar z,bit5Reset ; e resetar bit 5 se zero chamar nz,bit5Set ;ou definir bit 5 se não zero bit 2,(ix) ;verificar o valor do bit 2 - mesmo chamar nz,bit5Set ;e definir bit 5 se não zero, resultando no bit 5 tendo os valores dos bits 3 e 2 bit 1,(ix) ;Verifica o valor do bit 1 - chamada ímpar z,bit4Reset ;a nd redefina o bit 4 se zero chamar nz,bit4Set ;ou defina o bit 4 se não for zero bit 0,(ix) ;Verifique o valor do bit 0 chame nz,bit4Set ;e defina o bit 4 se não for zero, resultando no bit 4 tendo o valores dos bits 1 e 0 ;O bloco acima compacta os valores de 8 bits em 4 bits, e como um novo bloco é feito de ;8x8 pixels podemos avançar e compactar os próximos 8 bits no segundo nibble de IX ;então imprimir 2 caracteres em um único tile bit 7,(ix+8) ;Faremos todo o check/reset/set novamente, mas desta vez vamos verificar a chamada de dados z,bit3Reset ;que está 8 bytes à frente de IX e definir o valores nos bits entre 3 e 0 chame nz,bit3Set ;que irá comprimir os valores do próximo padrão na posição atual apontada pelo IX bit 6,(ix+8) ;então acredito que não preciso explicar tudo novamente, certo? chamar nz,bit3Set bit 5,(ix+8) chamar z,bit2Reset chamar nz,bit2Set bit 4,(ix+8) chamar nz,bit2Set bit 3,(ix+8) chamar z,bit1Reset chamar nz,bit1Set bit 2 ,(ix+8) call nz,bit1Set bit 1,(ix+8) call z,bit0Reset call nz,bit0Set bit 0,(ix+8) call nz,bit0Set inc ix ;Quando nossa compactação é feita com o byte apontado por IX vamos pular para o próximo DEC BC ;e usar o método de diminuir BC LD A,C ;carregar C em A OR B ;Fazer A OR B que levantará a bandeira Zero em F JP NZ,loop ;e repita todo o loop novamente até BC chegar a 0 ret ;retornando a execução do programa para startLoop bit7Reset: res 7,(ix) ;Codificação preguiçosa, mas isso é tudo o que faz: reseta o bit 7 e ret ;retorna bit7Set: set 7, (ix) ;Seta o bit 7 ret ;depois retorna bit6Reset: res 6,(ix) ;Mesma coisa até chegar ao bit 0 ret bit6Set: set 6,(ix) ret bit5Reset: res 5,(ix) ret bit5Set: set 5 ,(ix) ret bit4Reset: res 4,(ix) ret bit4Set: set 4,(ix) ret bit3Reset: res 3,(ix) ret bit3Set: set 3,(ix) ret bit2Reset: res 2,(ix) ret bit2Set: conjunto 2, (ix) ret bit1Reset: res 1,(ix) ret bit1Set: set 1,(ix) ret bit0Reset: res 0,(ix) ret bit0Set: set 0,(ix) ret pasteString ;Vamos imprimir os resultados da fonte compactada ld hl,$2600 ;Já que não sabemos o que está definido na tabela de atributos ld a,$f0 ;definimos bem A para F0, que é o primeiro plano branco e o fundo transparente ld bc,$FF ;e preenchemos os 256 bytes no padrão endereço da tabela $2600 chame FILVRM ;para garantir que nosso padrão apareça na tela como um texto branco ld de,(Vaddr) ;Carregamos o valor de Vaddr em de ld hl,stringData;e o stringData em HL ld bc,8 ; então defina o tamanho do bloco para 8 chame LDIRVM ;e copie os dados da RAM para a VRAM ld de,(Vaddr) ;Para duplicar a string do lado direito da tela ld hl,$0080 ;adicionamos $80 ao valor apontado por Vaddr ADD HL,DE ;e o resultado será armazenado em HL PUSH HL ;então empurramos HL para a pilha pop DE ;e trazemos o valor de volta, mas em DE em vez disso ld hl,stringData;para que possamos carregar o stringData novamente int o HL ld bc,8 ;defina um bloco de 8 bytes em BC chame LDIRVM ;e chame LDIRVM para copiar os dados da RAM para a VRAM, visando o lado direito da tela ld de,(Vaddr); ;Agora vamos preparar o Vaddr para apontar para a tabela de padrões relacionada à próxima coluna da tela ld hl,$0008 ;carregando $8 em HL ADD HL,DE ; e adicionando DE com HL, enviando os resultados para HL novamente ld (Vaddr),HL ;que retornará o valor de volta para a posição relacionada a Vddr ret ;finalmente retornando ao bloco startLoop novamente Vaddr: ;Nossas variáveis estão definidas abaixo de dw 0 copyStringAddr: dw 0 message: ;e nossa string original também db "12345678901234567890123456789012",0 stringData: dw 0 ; use o rótulo "start" como o ponto de entrada end start
Reparem que nem uma vez eu mexo na tabela de nomes aqui, e como toda vez que mudamos para a tela 2 a BIOS vai preencher a tabela de nomes com valores de 0 a 255 três vezes, uma para cada terço da tela, estamos jogando batalha naval com os valores já definidos na VRAM pelo BIOS. Isso é ruim? Não, se você tiver em mente que não deve tocar na tabela de nomes – ou deve se quiser “mexer as coisas um pouco de lugar”.
Por que todo o esforço para fazer isso?
Porque é divertido! Aprendi muito lendo o código original e aprendi mais enquanto tentava replicar os resultados sem realmente fazer a mesma coisa. Também brinquei com comandos de deslocamento e rotação que não terminaram no artigo, mas me ajudaram a entender como é possível reduzir o tamanho do código e se livrar de comandos repetitivos, e estou satisfeito com o resultado final. É possível fazer algo melhor? Sim, é, e Breno já fez isso criando um editor de texto 64×24 para o MSX!
Por hoje é só!
Como de costume, deixe seus comentários e perguntas aqui, e compartilhe este artigo com seus amigos e familiares!
Relacionado
Há algum tempo Breno Motta, que é um dos integrantes do grupo MSX Brasil Oficial do Facebook, publicou a foto de um pequeno programa BASIC que cria um estilo de fonte “comprimido” com 4 x 8 pixels na Tela 2 baseado no texto exibido na tela . Foi muito interessante ver como o…