Eventos, estado e debugging
Separar drivers e aplicação, representar transições e localizar erros por experiências pequenas.
Uma tecla P e um tick na mesma iteração podem pausar o jogo antes ou depois de avançar o contador. A aplicação precisa de uma ordem definida, além dos drivers que recolhem os dados.
A aplicação recebe bytes, transforma-os em eventos e atualiza o estado. O desenho apresenta uma fotografia desse estado. Separar estes passos permite reproduzir uma partida com uma sequência guardada de entradas, sem repetir o movimento na máquina virtual.
Programação por eventos
Um evento pode ser um tick, uma tecla completa, um pacote de rato ou uma mensagem recebida pela UART, porque nem todo o dado bruto já é um evento útil. Um prefixo E0 precisa do byte seguinte.
A divisão de responsabilidades é:
- O driver acede a registos e recolhe os dados.
- O descodificador produz eventos completos.
- A aplicação atualiza estado e decide ações.
- O módulo gráfico desenha o estado num frame.
O driver do rato não deve decidir se um clique inicia um jogo. Isso depende da aplicação. O código do menu não deve saber a porta do i8042. Uma biblioteca é reutilizável quando o contrato esconde esses detalhes e conserva a informação de que os chamadores precisam.
No ciclo Minix, recebe a mensagem, atende todas as máscaras ativas e processa os eventos produzidos antes de voltar a bloquear. Deixar um evento numa variável para o tratar “quando não houver interrupções” pode fazer o programa bloquear antes de o consumir.
Máquinas de estados
Uma máquina de estados explicita o que cada evento faz em cada situação. Para um jogo simples, podemos ter MENU, JOGO, PAUSA e FIM.
A transição não depende apenas da tecla. Enter no menu começa, enquanto Enter no fim regressa ao menu. Um tick na pausa pode atualizar um cursor do menu, mas não deve avançar o tempo do jogo se a regra diz que o jogo está parado.
Uma máquina Mealy associa saídas à transição, dependendo de estado e entrada. Uma máquina Moore associa saídas ao estado. Ambas podem representar a mesma função, embora possam precisar de diferentes números de estados. Em código por eventos, produzir uma ação ao aceitar uma transição é uma representação frequente.
Os dados também fazem parte do estado. Um enum sozinho não representa vidas, posição ou ticks acumulados. Podes usar uma struct com o estado e os dados comuns, ou uma tagged union para dados que só existem em algumas alternativas.
Ordem das teclas e ticks
Este programa C portátil simula iterações com uma tecla e um tick cada. Implementa exatamente a máquina do diagrama: o jogo termina após três ticks ativos, porque o tick só conta em JOGO. Cada iteração aplica primeiro a tecla e depois o tick.
#include <stdio.h>
enum state { MENU, GAME, PAUSE, END };
enum key { NO_KEY, ENTER, P_KEY, ESC };
struct input { enum key key; int tick; };
struct game { enum state state; unsigned active_ticks; };
static void handle(struct game *g, struct input in) {
/* Ordem fixa desta aplicação: primeiro a tecla, depois o tick. */
switch (g->state) {
case MENU:
if (in.key == ENTER) {
g->active_ticks = 0;
g->state = GAME;
}
break;
case GAME:
if (in.key == P_KEY) g->state = PAUSE;
break;
case PAUSE:
if (in.key == P_KEY) g->state = GAME;
else if (in.key == ESC) g->state = MENU;
break;
case END:
if (in.key == ENTER) g->state = MENU;
break;
}
if (in.tick && g->state == GAME && ++g->active_ticks == 3)
g->state = END;
}
int main(void) {
struct game g = {MENU, 0};
const struct input inputs[] = {
{ENTER, 0}, {NO_KEY, 1}, {P_KEY, 1}, {NO_KEY, 1},
{P_KEY, 0}, {NO_KEY, 1}, {NO_KEY, 1}
};
const char *names[] = {"MENU", "JOGO", "PAUSA", "FIM"};
for (size_t i = 0; i < sizeof inputs / sizeof inputs[0]; ++i) {
handle(&g, inputs[i]);
printf("%s %u\n", names[g.state], g.active_ticks);
}
return 0;
}
O programa imprime:
JOGO 0
JOGO 1
PAUSA 1
PAUSA 1
JOGO 1
JOGO 2
FIM 3
Cada linha permite seguir o estado depois de uma iteração. A terceira iteração prova a ordem fixa: tecla P e tick chegam juntos em JOGO e o resultado é PAUSA 1. A tecla foi aplicada primeiro e o tick foi ignorado; se o tick fosse aplicado primeiro, o contador chegaria a 2 antes da pausa. Fixa esta ordem e usa-a nos testes, na execução local e no protocolo remoto.
O tick é aplicado ao estado que resulta da tecla, fora do switch. Assim, ENTER e tick em MENU iniciam JOGO e contam o primeiro tick. P e tick em PAUSA retomam JOGO e contam um tick. P e tick em JOGO entram em PAUSA e não contam. Tratar o tick apenas no ramo do estado anterior falharia nos dois primeiros casos.
Filas, ordem e ritmo
Se chegam vários eventos antes de os tratares, usa uma fila, com limite e política de overflow. Uma variável com o último evento não guarda toda a sequência. Quando os eventos são posições absolutas, por vezes podes substituir uma posição antiga pela mais recente; quando são cliques ou deltas, essa substituição muda o comportamento.
Há duas ordens diferentes. Dentro de cada struct input, a aplicação usa a regra fixada na secção anterior: primeiro a tecla, depois o tick aplicado ao estado resultante. Entre eventos, a fila conserva a ordem de chegada e a aplicação retira pela mesma ordem. Uma tecla que chegue depois de um tick não passa à frente dele; cada elemento é aplicado quando sai da fila.
A perda sem fila depende da representação. Se duas interrupções do timer chegarem antes de o processo as atender, o kernel entrega um só bit ativo: garantes pelo menos um tick, não dois. Se o programa guardar só a última tecla numa variável, uma tecla anterior perde-se. Já uma notificação com bits de dispositivos diferentes conserva ambos, desde que o despacho teste cada máscara com um if independente.
Atualiza a lógica ao receber entradas e desenha a um ritmo escolhido. Por exemplo, desenhar a cada dois ticks de um timer de 60 Hz dá 30 frames/s, sem redesenhar o ecrã inteiro por cada byte de scancode.
Carrega recursos gráficos antes do ciclo e liberta-os depois. Não faças parsing de XPM ou alocações grandes em cada tick. Otimiza depois de medir: copiar linhas contíguas de um pixmap pode custar menos do que chamar uma função de desenho por píxel, mas deve preservar transparência, clipping e pitch.
Inicialização e falha parcial
A inicialização precisa de um caminho inverso. Se obténs timer, teclado, pixmaps e vídeo por esta ordem, uma falha no vídeo obriga a libertar os pixmaps e as subscrições já obtidas. Uma flag de recurso adquirido, ou uma função de limpeza que tolere recursos ausentes, permite tratar todos os caminhos.
Não chames a aplicação “pronta” antes de todos os recursos obrigatórios estarem válidos. Mantém erros distinguíveis: alocação falhada, configuração VBE recusada, subscrição falhada e timeout do dispositivo pedem verificações diferentes.
Na saída normal, deixa de gerar novos eventos, repõe configurações alteradas, remove subscrições, liberta pixmaps e buffers e restaura o modo de texto. A ordem deve respeitar as dependências, para uma rotina não usar um recurso já destruído.
Debugging como experiência
Começa por uma observação reproduzível. “O cursor salta para a esquerda ao mover depressa para a direita” sugere uma hipótese concreta: um delta positivo acima de 127 foi interpretado como int8_t.
Prevê antes de alterar: para um pacote 08 C8 00, o delta correto é +200. O erro produziria -56. Simula exatamente esse pacote no descodificador. Se obténs -56, o problema está antes da atualização do cursor ou do desenho. Se obténs +200, verifica a soma, o clipping e a representação da posição.
A experiência segue estes passos:
- Reduz a entrada à menor sequência que ainda falha.
- Formula uma hipótese sobre uma operação ou estado.
- Escolhe a observação que distinguiria essa hipótese das alternativas.
- Altera uma causa, volta a correr a mesma sequência e compara.
Para uma interrupção ausente, observa configuração do dispositivo, subscrição, máscara recebida e leitura do buffer. Para imagem errada, verifica modo, pitch, offset e formato de cor. Para ligação errada, começa com bytes conhecidos antes de culpar o parser de mensagens.
Regista valores no limite entre módulos, evitando imprimir em cada ciclo de espera sem necessidade. O logging altera tempo e carga. Se um erro desaparece quando acrescentas prints, isso é uma observação sobre temporização, não prova de correção.
Depois da correção, conserva um caso que reproduza o comportamento, como uma sequência de bytes ou eventos, e verifica também o caso vizinho que poderia quebrar. O objetivo é explicar o erro e a correção por resultados observáveis.
Exercícios
Seguir um estado por eventos
Parte de MENU com zero ticks e usa a máquina do exemplo. Recebe ENTER, TICK, P_KEY, TICK, TICK, P_KEY, TICK. Quantos ticks ativos acumulou no fim?
Pista
Solução
ENTER inicia GAME, o primeiro TICK dá 1 e P_KEY pausa. Os dois TICK em PAUSE não alteram o contador. P_KEY retoma GAME e o último TICK dá 2. O estado final continua GAME.
Erros frequentes
Contar todos os eventos TICK independentemente do estado ou reiniciar o contador ao sair da pausa sem essa regra.
Limpar recursos já adquiridos
O programa subscreve timer e teclado, aloca um pixmap e depois falha ao mapear vídeo, antes de mudar o modo. Que recursos deve limpar e que ações não deve assumir que são necessárias?
Pista
Solução
Liberta o pixmap e remove as subscrições de teclado e timer, respeitando a desativação e dependências do ambiente. Não deve desmontar um mapeamento que falhou nem presumir que o modo foi mudado. A limpeza depende das operações realmente concluídas, não só da etapa que estava a ser tentada.
Confere a tua resposta:
- Listei apenas recursos efetivamente adquiridos.
- Respeitei dependências na ordem inversa.
- Não usei nem libertei recurso que falhou.
Erros frequentes
Sair imediatamente deixando recursos obtidos ou chamar indiscriminadamente destrutores sobre valores não inicializados.
Escolher uma experiência que distingue causas
O cursor às vezes anda para a esquerda num movimento para a direita.
Suspeitas de cast para int8_t. Que entrada mínima usas e
que dois resultados distinguem o bug de um erro posterior no desenho?
Pista
Solução
Alimenta o descodificador com 08 C8 00. O protocolo pede delta X +200. Uma interpretação de oito bits com sinal produz -56. Se o parser der +200, verifica a atualização e clipping da posição e o desenho. Se der -56, corrige a descodificação antes de alterar gráficos.
Confere a tua resposta:
- Escolhi um pacote que distingue nove de oito bits.
- Previ +200 e -56 antes de correr.
- Separei parser, posição e desenho.
Erros frequentes
Mover o rato aleatoriamente sem conservar os bytes ou mudar parsing e desenho ao mesmo tempo.
Fixar a ordem de dois eventos
Na máquina do exemplo em JOGO com um tick ativo, chegam PAUSE e TICK na mesma iteração. Compara aplicar pausa antes do tick e tick antes da pausa.
Pista
Solução
Pausa antes do tick: entras em PAUSE e o tick seguinte não conta, ficando um tick ativo. Tick antes da pausa: o tick conta em JOGO e só depois entras em PAUSE, ficando dois ticks ativos. A diferença decide um timeout no limite, por isso a ordem tem de estar escrita e repetida nos testes.
Confere a tua resposta:
- Apliquei a mesma ordem nas duas sequências.
- Mostrei que a ordem muda o contador.
- Conservei a regra nos testes e na execução.
Erros frequentes
Tratar eventos simultâneos como ordem irrelevante ou mudar a regra entre teste e execução.