<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="pt" xml:base="https://viniciuscole.com/">
  <title>Vinicius Cole</title>
  <subtitle>Notas sobre as coisas que eu construo.</subtitle>
  <link href="https://viniciuscole.com/pt/feed.xml" rel="self" type="application/atom+xml"/>
  <link href="https://viniciuscole.com/pt/blog/" rel="alternate" type="text/html"/>
  <id>https://viniciuscole.com/pt/feed.xml</id>
  <updated>2026-11-01T00:00:00Z</updated>
  <author><name>Vinicius Cole</name></author>
  <entry>
    <title>Linkar não é rodar: um jogo C++ com OpenGL no navegador</title>
    <link href="https://viniciuscole.com/pt/2026/11/01/linking-is-not-running/" rel="alternate" type="text/html"/>
    <id>https://viniciuscole.com/pt/2026/11/01/linking-is-not-running/</id>
    <published>2026-11-01T00:00:00Z</published>
    <updated>2026-11-01T00:00:00Z</updated>
    <category term="webassembly" label="WebAssembly"/>
    <category term="opengl" label="OpenGL"/>
    <category term="testing" label="Testes"/>
    <content type="html">&lt;p&gt;O &lt;a href=&quot;/pt/projects/2d-graphics/&quot;&gt;jogo de rolagem lateral&lt;/a&gt; deste site foi um
trabalho de Computação Gráfica: C++, OpenGL em modo imediato, GLUT e uma arena
lida de um arquivo SVG. Portar ele foi o oposto do
&lt;a href=&quot;/pt/2026/08/16/porting-dos-assembly-to-the-browser/&quot;&gt;port do jogo da velha&lt;/a&gt;.
Lá, um emulador de 1,4 MB roda um programa DOS de 3 KB. Aqui não tem emulador:
os 181.051 bytes do &lt;code class=&quot;highlighter-rouge&quot;&gt;jogo.wasm&lt;/code&gt; são o meu C++, compilado pelo Emscripten.&lt;/p&gt;

&lt;p&gt;Compilar foi um comando só:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;em++ -std=c++11 -O2 main.cpp arena.cpp tinyxml2.cpp character.cpp \
     hero.cpp enemy.cpp shot.cpp \
     -sLEGACY_GL_EMULATION=1 -lGL -lglut \
     --preload-file arena_teste.svg -o jogo.js
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Linkou limpo. Eu quase anotei “OpenGL em modo imediato funciona”. Aí abri no
navegador e o jogo morreu no primeiro quadro:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Aborted(unsupported immediate mode 9)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Modo 9 é &lt;code class=&quot;highlighter-rouge&quot;&gt;GL_POLYGON&lt;/code&gt;, que o jogo usa para desenhar a cabeça de cada personagem.
A emulação do OpenGL antigo no Emscripten não implementa esse modo. Trocar por
&lt;code class=&quot;highlighter-rouge&quot;&gt;GL_TRIANGLE_FAN&lt;/code&gt; desenha a mesma coisa para uma forma convexa, então o patch é
de uma palavra.&lt;/p&gt;

&lt;p&gt;O patch não me incomodou. O que me incomodou foi que o compilador, o linker e
uma olhada no tamanho dos arquivos tinham dado sinal verde para um programa que
não desenhava um quadro sequer. Só rodando de verdade para pegar aquilo.&lt;/p&gt;

&lt;p&gt;Então montei uma bancada: um script que abre o jogo num Chromium de verdade
(headless, dentro da imagem Docker do Playwright), aperta teclas, clica o mouse
e confere o que aparece na tela. Quase tudo que aprendi com esse port veio dos
erros da própria bancada.&lt;/p&gt;

&lt;h2 id=&quot;não-dá-para-ler-um-canvas-webgl&quot;&gt;Não dá para ler um canvas WebGL&lt;/h2&gt;

&lt;p&gt;A primeira pergunta da bancada é “desenhou alguma coisa?”. O óbvio é ler os
pixels do canvas. Isso devolve preto, sempre, porque o WebGL joga fora o buffer
de desenho assim que o quadro vai para a tela, a menos que você peça o
contrário.&lt;/p&gt;

&lt;p&gt;O que funciona é dar a volta. Tira um screenshot pelo Playwright, carrega
num &lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;lt;img&amp;gt;&lt;/code&gt;, desenha num canvas 2D e chama &lt;code class=&quot;highlighter-rouge&quot;&gt;getImageData&lt;/code&gt;. Toda medida da
bancada passa por esse caminho.&lt;/p&gt;

&lt;h2 id=&quot;um-teste-que-passa-com-o-teclado-desligado&quot;&gt;Um teste que passa com o teclado desligado&lt;/h2&gt;

&lt;p&gt;A primeira versão de “apertar D move o jogador” comparava dois screenshots do
canvas inteiro, um antes da tecla e outro depois, e passava se fossem
diferentes.&lt;/p&gt;

&lt;p&gt;Eles são sempre diferentes. A arena tem sete inimigos que andam e atiram
sozinhos desde o primeiro quadro, então a imagem muda entre dois instantes
quaisquer, com ou sem teclado. O teste passaria com a entrada completamente
quebrada, o que é pior do que não ter teste, já que tudo depois dele confiava
nele.&lt;/p&gt;

&lt;p&gt;No lugar disso, a bancada mede a coisa que deveria se mexer. O herói é a única coisa
verde na tela, então a bancada acha o centro horizontal dos pixels verdes e afirma que D
empurra ele mais de 20 px para a direita e A traz de volta. E tem que medir
perto do ponto de partida: a câmera acompanha o herói, e depois de alguns
passos ele fica centralizado e o deslocamento na tela quase some.&lt;/p&gt;

&lt;p&gt;O pulo era um problema de tempo. Um toque rápido no W levanta o herói
21 px e ele aterrissa em menos de 90 ms. Minha primeira medida rodava 250 ms
depois de soltar a tecla, via o herói no chão e acusava uma falha que não
existia. Segurar W por 900 ms chega a 147 px. Hoje a bancada tira três amostras
logo depois de soltar e fica com o ponto mais alto.&lt;/p&gt;

&lt;h2 id=&quot;exit0-não-sai&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;exit(0)&lt;/code&gt; não sai&lt;/h2&gt;

&lt;p&gt;O jogo original fecha no ESC com &lt;code class=&quot;highlighter-rouge&quot;&gt;exit(0)&lt;/code&gt;. No desktop, tudo bem. Numa página
não existe para onde sair, então o port transforma o ESC em nada. A bancada
conferia isso apertando ESC e vendo se o herói continuava visível.&lt;/p&gt;

&lt;p&gt;Essa checagem não servia para nada, e só descobri quebrando a proteção de
propósito e recompilando. Com o &lt;code class=&quot;highlighter-rouge&quot;&gt;-sEXIT_RUNTIME=0&lt;/code&gt; do Emscripten, &lt;code class=&quot;highlighter-rouge&quot;&gt;exit(0)&lt;/code&gt; não
para coisa nenhuma. Ele lança uma exceção &lt;code class=&quot;highlighter-rouge&quot;&gt;ExitStatus&lt;/code&gt; não capturada dentro do
handler de tecla, a página registra um erro, e o laço de &lt;code class=&quot;highlighter-rouge&quot;&gt;requestAnimationFrame&lt;/code&gt;
que já estava agendado segue rodando. O herói nunca saiu da tela, então minha
checagem testava justamente a única coisa que a regressão não quebra.&lt;/p&gt;

&lt;p&gt;A afirmação que pega a regressão conta os erros da página antes e depois do
ESC. “O herói ainda anda depois do ESC” ficou como segunda linha de defesa, para
o caso de o build mudar um dia para &lt;code class=&quot;highlighter-rouge&quot;&gt;EXIT_RUNTIME=1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Isso virou a regra da bancada inteira: toda afirmação tinha que ser vista
falhando. Coloca o bug de volta, vê a checagem ficar vermelha, desfaz. No caso
do &lt;code class=&quot;highlighter-rouge&quot;&gt;GL_POLYGON&lt;/code&gt;, isso significou editar o patch de volta para
&lt;code class=&quot;highlighter-rouge&quot;&gt;glBegin(GL_POLYGON)&lt;/code&gt;, recompilar e ver a bancada pegar o abort. Uma checagem
nem sobreviveu à regra: “segurar W pula mais alto que tocar” comparava contra o
&lt;code class=&quot;highlighter-rouge&quot;&gt;Math.min&lt;/code&gt; de uma lista que podia vir vazia. &lt;code class=&quot;highlighter-rouge&quot;&gt;Math.min()&lt;/code&gt; de nada é &lt;code class=&quot;highlighter-rouge&quot;&gt;Infinity&lt;/code&gt;,
então qualquer altura passava.&lt;/p&gt;

&lt;h2 id=&quot;o-que-o-jogo-fez-com-a-página&quot;&gt;O que o jogo fez com a página&lt;/h2&gt;

&lt;p&gt;O GLUT do Emscripten escuta &lt;code class=&quot;highlighter-rouge&quot;&gt;keydown&lt;/code&gt; e &lt;code class=&quot;highlighter-rouge&quot;&gt;keyup&lt;/code&gt; na janela inteira e chama
&lt;code class=&quot;highlighter-rouge&quot;&gt;preventDefault&lt;/code&gt; em Tab, Enter e Espaço. O jogo não usa nenhuma das três.
Depois que alguém clicava em Jogar, quem navega por teclado ficava preso no
canvas: o Tab não chegava ao rodapé, nem ao botão de tema, nem ao “jogar de
novo” da tela de fim do próprio jogo. Agora um listener em captura na janela,
registrado antes de o módulo do jogo carregar, segura essas três teclas antes
de o GLUT ver.&lt;/p&gt;

&lt;p&gt;E essa tela de fim também estava quebrada. As fontes bitmap do GLUT não existem no
Emscripten, então “Você venceu” e “Você perdeu” agora são HTML por cima do
canvas. O C++ chama a página a cada quadro enquanto o jogo está acabado. Quando
o R reinicia, ele simplesmente para de chamar, e nada avisava a página para
esconder a mensagem. O overlay ficava por cima de um jogo rodando e engolia
todos os cliques. Agora o &lt;code class=&quot;highlighter-rouge&quot;&gt;restartGame()&lt;/code&gt; avisa a página quando o jogo volta.&lt;/p&gt;

&lt;p&gt;O último só apareceu no site de verdade. A bancada servia os três arquivos do
build de um diretório só. No site, a página mora em &lt;code class=&quot;highlighter-rouge&quot;&gt;/projects/...&lt;/code&gt; e o jogo em
&lt;code class=&quot;highlighter-rouge&quot;&gt;/demos/...&lt;/code&gt;. O loader do Emscripten procura o &lt;code class=&quot;highlighter-rouge&quot;&gt;jogo.data&lt;/code&gt; (a arena SVG
empacotada) relativo à página, não ao &lt;code class=&quot;highlighter-rouge&quot;&gt;jogo.js&lt;/code&gt;, então no site real era 404
toda vez. Uma opção &lt;code class=&quot;highlighter-rouge&quot;&gt;locateFile&lt;/code&gt; no loader resolveu, e a bancada ganhou um
segundo modo que carrega a página construída de verdade, porque a versão de
diretório único não tem como ver esse tipo de bug.&lt;/p&gt;

&lt;h2 id=&quot;três-patches-e-muita-bancada&quot;&gt;Três patches, e muita bancada&lt;/h2&gt;

&lt;p&gt;O jogo em si quase não mudou: três patches pequenos sobre um commit fixo do
repositório original, e o próprio &lt;code class=&quot;highlighter-rouge&quot;&gt;make&lt;/code&gt; do trabalho ainda gera a versão de
desktop com o texto original do GLUT.&lt;/p&gt;

&lt;p&gt;Entrei achando que a parte difícil seria o OpenGL. Foi a bancada.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Ensinando um programa em C a mostrar o raciocínio</title>
    <link href="https://viniciuscole.com/pt/2026/11/01/teaching-c-to-show-its-work/" rel="alternate" type="text/html"/>
    <id>https://viniciuscole.com/pt/2026/11/01/teaching-c-to-show-its-work/</id>
    <published>2026-11-01T00:00:00Z</published>
    <updated>2026-11-01T00:00:00Z</updated>
    <category term="c" label="C"/>
    <category term="webassembly" label="WebAssembly"/>
    <category term="algorithms" label="Algoritmos"/>
    <content type="html">&lt;p&gt;O &lt;a href=&quot;/pt/projects/car-routes/&quot;&gt;projeto de rotas&lt;/a&gt; começou como um trabalho de 2023
que fiz com dois colegas: ler um mapa, achar a rota mais rápida e replanejar
quando o trânsito muda no meio do caminho. É C puro. Você dá um arquivo e ele
devolve três linhas: o caminho, os quilômetros e o tempo.&lt;/p&gt;

&lt;p&gt;Para o professor corrigir, essa resposta basta. Para uma página web, não serve
para nada. Eu queria que o leitor visse o Dijkstra abrindo e fechando nós, o
carro saindo, uma via travando e o carro mudando de ideia. O C sabia tudo isso
enquanto rodava. Só nunca falava.&lt;/p&gt;

&lt;p&gt;Reescrever o algoritmo em JavaScript transformaria o C num link para o
repositório, e animações gravadas tirariam do leitor a chance de criar o
próprio engarrafamento. Então o C ia ter que aprender a narrar.&lt;/p&gt;

&lt;h2 id=&quot;ganchos-que-não-custam-nada-quando-ninguém-escuta&quot;&gt;Ganchos que não custam nada quando ninguém escuta&lt;/h2&gt;

&lt;p&gt;O C ganhou uma struct de ponteiros de função, um por evento, todos &lt;code class=&quot;highlighter-rouge&quot;&gt;NULL&lt;/code&gt; por
padrão:&lt;/p&gt;

&lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;typedef&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pop&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;double&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;relax&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;to&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;double&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;new_time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;plan&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;double&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;eta&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;move&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;to&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;double&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;double&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;cm&quot;&gt;/* ...update, replan, unreachable, done */&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;TraceHooks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;O &lt;code class=&quot;highlighter-rouge&quot;&gt;dijkstra&lt;/code&gt; e o laço de replanejamento só chamam cada um se ele estiver
preenchido. O programa de linha de comando original nunca preenche, então para
ele a mudança custa umas poucas comparações com &lt;code class=&quot;highlighter-rouge&quot;&gt;NULL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Antes de mexer em qualquer linha, salvei a saída do programa para alguns mapas
de teste como resultado esperado. Qualquer mudança dali em diante tinha que
reproduzir esses arquivos exatamente. Na primeira execução dessa base, um deles
saiu assim:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;1;2;4;6
3.000000
00:02:60.000000
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Dois minutos e sessenta segundos. A rota está certa, são exatamente 180
segundos. Em ponto flutuante a soma dá algo como 179,99999, o código trunca com
&lt;code class=&quot;highlighter-rouge&quot;&gt;(int)&lt;/code&gt; para tirar os minutos, chega em 2, e imprime o resto, que o &lt;code class=&quot;highlighter-rouge&quot;&gt;printf&lt;/code&gt;
arredonda de volta para 60. Isso vinha sendo impresso desde 2023. A correção
arredonda para milissegundos antes de quebrar o tempo em partes. Foi a única
linha da base que o projeto teve permissão de mudar.&lt;/p&gt;

&lt;h2 id=&quot;uma-linha-por-evento&quot;&gt;Uma linha por evento&lt;/h2&gt;

&lt;p&gt;Com os ganchos no lugar, uma função nova, &lt;code class=&quot;highlighter-rouge&quot;&gt;run_trace&lt;/code&gt;, imprime uma linha para
cada coisa que acontece. Esta é a saída real para o mapa da página do projeto,
onde a via 2→4 cai para 5 km/h aos 30 segundos:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;pop 1 0.000
relax 1 2 60.000
relax 1 3 90.000
pop 2 60.000
relax 2 4 120.000
relax 2 5 210.000
...
plan 1 2 4 6 180.000
move 1 2 0.000 60.000
update 2 4 5.000 60.000
replan 2 60.000
pop 2 0.000
...
plan 2 5 6 180.000
move 2 5 60.000 210.000
move 5 6 210.000 240.000
done 240.000 4.000
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Repare nos tempos depois de &lt;code class=&quot;highlighter-rouge&quot;&gt;replan 2 60.000&lt;/code&gt;. O &lt;code class=&quot;highlighter-rouge&quot;&gt;pop 2&lt;/code&gt; seguinte diz &lt;code class=&quot;highlighter-rouge&quot;&gt;0.000&lt;/code&gt;,
o plano novo diz que a viagem leva &lt;code class=&quot;highlighter-rouge&quot;&gt;180.000&lt;/code&gt;, mas o carro chega em 240. Os dois
estão certos. Quando o carro replaneja, o Dijkstra recomeça do nó 2 com o
relógio zerado, então tudo que ele imprime é relativo àquele momento. Já
&lt;code class=&quot;highlighter-rouge&quot;&gt;move&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;update&lt;/code&gt; e &lt;code class=&quot;highlighter-rouge&quot;&gt;done&lt;/code&gt; usam o relógio de verdade. Minha própria spec errou
isso: ela tinha o segundo plano em &lt;code class=&quot;highlighter-rouge&quot;&gt;240.000&lt;/code&gt;, e só corrigi depois que o C de
verdade imprimiu 180. O JavaScript guarda o relógio do último &lt;code class=&quot;highlighter-rouge&quot;&gt;replan&lt;/code&gt; e soma
em todo evento do Dijkstra que vem depois.&lt;/p&gt;

&lt;p&gt;O trace também mostra coisas que o código não diz em lugar nenhum. O nó 5 é
relaxado duas vezes, com 210 e depois com 156, porque o nosso heap não tem
decrease-key: ele só insere o nó de novo e deixa a cópia velha na fila. E o
&lt;code class=&quot;highlighter-rouge&quot;&gt;pop 4 720.000&lt;/code&gt; aparece depois de o destino já estar fechado, porque o laço não
para antes. Os dois comportamentos sempre estiveram no código. Eu só vi quando
eles apareceram impressos, um por linha.&lt;/p&gt;

&lt;h2 id=&quot;lendo-um-arquivo-que-não-é-arquivo&quot;&gt;Lendo um arquivo que não é arquivo&lt;/h2&gt;

&lt;p&gt;O programa original lê o mapa com &lt;code class=&quot;highlighter-rouge&quot;&gt;fscanf&lt;/code&gt; de um &lt;code class=&quot;highlighter-rouge&quot;&gt;FILE *&lt;/code&gt;. Uma página web não
tem arquivo nenhum para entregar, e eu não queria um segundo parser só para o
navegador. O &lt;code class=&quot;highlighter-rouge&quot;&gt;fmemopen&lt;/code&gt; resolveu: ele abre uma string como &lt;code class=&quot;highlighter-rouge&quot;&gt;FILE *&lt;/code&gt;, então o
&lt;code class=&quot;highlighter-rouge&quot;&gt;run_trace&lt;/code&gt; recebe o mapa como texto e passa para as mesmas três funções de
leitura que o programa de linha de comando usa. O único custo foi compilar com
&lt;code class=&quot;highlighter-rouge&quot;&gt;-std=gnu99&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Para o navegador, o Emscripten compila todos os arquivos menos o &lt;code class=&quot;highlighter-rouge&quot;&gt;main.c&lt;/code&gt;. Sem
&lt;code class=&quot;highlighter-rouge&quot;&gt;main&lt;/code&gt;, o módulo carrega e fica esperando. A página chama &lt;code class=&quot;highlighter-rouge&quot;&gt;run_trace&lt;/code&gt; com o
mapa em forma de string, junta tudo que ele imprime pelo callback &lt;code class=&quot;highlighter-rouge&quot;&gt;print&lt;/code&gt; do
módulo e recebe a execução inteira como texto. São 49.687 bytes de
WebAssembly, carregados uma vez por página, e uma execução nesses mapas leva
poucos milissegundos. Quando você cria um engarrafamento no editor de trânsito,
a página monta uma entrada nova e chama o C de novo.&lt;/p&gt;

&lt;h2 id=&quot;entrada-de-desconhecidos&quot;&gt;Entrada de desconhecidos&lt;/h2&gt;

&lt;p&gt;Antes do editor de trânsito, a única entrada que esse programa já tinha lido
eram arquivos que nós três escrevemos. Agora ele lê o que qualquer leitor
monta clicando. Três coisas
quebraram na hora:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;o &lt;code class=&quot;highlighter-rouge&quot;&gt;read_updates&lt;/code&gt; lia cada linha num &lt;code class=&quot;highlighter-rouge&quot;&gt;char aux[50]&lt;/code&gt;, então uma linha comprida
escrevia além do fim do buffer;&lt;/li&gt;
  &lt;li&gt;uma atualização para uma via que não existe andava a lista de adjacência até
passar do fim e derrubava o programa;&lt;/li&gt;
  &lt;li&gt;um destino inalcançável também derrubava, em vez de avisar.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Teve uma quarta, mais quieta. O &lt;code class=&quot;highlighter-rouge&quot;&gt;dijkstra&lt;/code&gt; dimensiona os vetores pelo número de
nós da primeira linha do arquivo, então um cabeçalho dizendo que o mapa tem um
bilhão de nós estouraria a pilha. O &lt;code class=&quot;highlighter-rouge&quot;&gt;run_trace&lt;/code&gt; recusa qualquer coisa acima de
um milhão.&lt;/p&gt;

&lt;p&gt;Nenhum desses era bug para o trabalho da faculdade, porque a gente nunca deu
arquivo ruim para ele. Hoje cada um tem um teste, e a saída do programa para
os mapas originais continua batendo byte a byte com a base, fora aquela linha.&lt;/p&gt;

&lt;h2 id=&quot;reproduzindo&quot;&gt;Reproduzindo&lt;/h2&gt;

&lt;p&gt;No lado do JavaScript, o trace passa por um reducer que transforma cada evento
num retrato completo: quais nós estão abertos ou fechados, o plano atual, onde
está o carro. Todos são calculados de uma vez, então tocar, pausar, avançar um
passo e mudar a velocidade são só uma posição num array. O canvas lê as cores
das variáveis CSS do site a cada quadro, e é por isso que o grafo acompanha a
troca de tema claro/escuro no meio da animação sem código extra.&lt;/p&gt;

&lt;p&gt;O C em si ainda imprime três minutos como &lt;code class=&quot;highlighter-rouge&quot;&gt;00:03:0.000000&lt;/code&gt;, com os segundos
sem o zero à esquerda. O README documenta isso agora, e vou deixar assim.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Portando assembly de DOS para o navegador</title>
    <link href="https://viniciuscole.com/pt/2026/08/16/porting-dos-assembly-to-the-browser/" rel="alternate" type="text/html"/>
    <id>https://viniciuscole.com/pt/2026/08/16/porting-dos-assembly-to-the-browser/</id>
    <published>2026-08-16T00:00:00Z</published>
    <updated>2026-08-16T00:00:00Z</updated>
    <category term="assembly" label="Assembly"/>
    <category term="emulation" label="Emulação"/>
    <category term="webassembly" label="WebAssembly"/>
    <content type="html">&lt;p&gt;Meu jogo da velha é escrito em assembly x86 de 16 bits, em modo real. Não existe
compilador que transforme isso em WebAssembly. O conjunto de instruções, o
modelo de memória segmentada e as interrupções do DOS não têm alvo equivalente.&lt;/p&gt;

&lt;p&gt;Então o plano é emulação: montar o código original com o NASM, linkar num
executável DOS e rodar esse executável dentro do DOSBox compilado para
WebAssembly. O WebAssembly aqui é o emulador, não o meu código, e eu acho que
essa honestidade importa mais do que o rótulo.&lt;/p&gt;

&lt;p&gt;O build já funciona: o &lt;code class=&quot;highlighter-rouge&quot;&gt;nasm -f obj&lt;/code&gt; monta os dois arquivos-fonte sem uma única
mudança, e o linker do Open Watcom produz um executável DOS de 3.244 bytes.&lt;/p&gt;

&lt;h2 id=&quot;o-que-o-jogo-realmente-faz&quot;&gt;O que o jogo realmente faz&lt;/h2&gt;

&lt;p&gt;São 1.740 linhas em dois arquivos. O &lt;code class=&quot;highlighter-rouge&quot;&gt;vca.asm&lt;/code&gt; guarda o jogo: o interpretador de
comandos, o tabuleiro, a detecção de vitória. O &lt;code class=&quot;highlighter-rouge&quot;&gt;draw.asm&lt;/code&gt; guarda as primitivas
gráficas.&lt;/p&gt;

&lt;p&gt;O estado inteiro do jogo são nove bytes:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;tabuleiro    db    &#39;         &#39;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Nove espaços ASCII. Uma jogada sobrescreve um deles com &lt;code class=&quot;highlighter-rouge&quot;&gt;X&lt;/code&gt; ou &lt;code class=&quot;highlighter-rouge&quot;&gt;C&lt;/code&gt;, e uma partida
nova são nove espaços de novo. Não tem struct, não tem bitboard, não tem
empacotamento. O tabuleiro é literalmente a string que você desenharia no papel.&lt;/p&gt;

&lt;p&gt;A detecção de vitória é toda desenrolada. Oito linhas vencedoras, dois jogadores,
dezesseis blocos de &lt;code class=&quot;highlighter-rouge&quot;&gt;cmp byte [tabuleiro + n], al&lt;/code&gt; escritos um atrás do outro,
sem laço e sem tabela de índices. Essa decisão sozinha responde por boa parte das
1.187 linhas do &lt;code class=&quot;highlighter-rouge&quot;&gt;vca.asm&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Tudo que aparece na tela chega por um de dois caminhos, e os dois não têm nada em
comum. Os rótulos de posição são texto da BIOS: &lt;code class=&quot;highlighter-rouge&quot;&gt;int 10h&lt;/code&gt; com &lt;code class=&quot;highlighter-rouge&quot;&gt;AH=02h&lt;/code&gt; para
posicionar o cursor e &lt;code class=&quot;highlighter-rouge&quot;&gt;AH=09h&lt;/code&gt; para escrever um caractere na fonte da ROM, direto
sobre uma tela em modo gráfico. O tabuleiro, os xis e os círculos são pixels,
traçados um a um pelas rotinas de Bresenham que o próprio jogo implementa.&lt;/p&gt;

&lt;h2 id=&quot;uma-interrupção-por-pixel&quot;&gt;Uma interrupção por pixel&lt;/h2&gt;

&lt;p&gt;O &lt;code class=&quot;highlighter-rouge&quot;&gt;draw.asm&lt;/code&gt; tem 553 linhas e exatamente três chamadas &lt;code class=&quot;highlighter-rouge&quot;&gt;int 10h&lt;/code&gt;. A terceira é a
camada gráfica inteira:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;plot_xy:
        ; ... prologo: salva o bp e todos os registradores que ele toca
        mov     ah,0ch
        mov     al,[cor]
        mov     bh,0
        mov     dx,479
        sub     dx,[bp+4]
        mov     cx,[bp+6]
        int     10h
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;AH=0Ch&lt;/code&gt; é o serviço de escrever pixel da BIOS. Toda linha, todo círculo, todo X
do tabuleiro passa por ele, um pixel por interrupção. Nada é escrito direto na
memória de vídeo em &lt;code class=&quot;highlighter-rouge&quot;&gt;A000h&lt;/code&gt;, e nenhum dos truques de planos da VGA que faziam o
modo 12h ser rápido aparece no fonte.&lt;/p&gt;

&lt;p&gt;O &lt;code class=&quot;highlighter-rouge&quot;&gt;mov dx,479 / sub dx,[bp+4]&lt;/code&gt; é o outro detalhe de que eu gosto. A VGA conta as
linhas de cima para baixo; o jogo pensa a partir do canto inferior esquerdo, como
papel quadriculado. Em vez de converter em cada ponto de chamada, ele inverte o
eixo uma vez só, dentro da única rotina que toca num pixel.&lt;/p&gt;

&lt;p&gt;O custo é fácil de medir. A grade vazia são duas linhas horizontais de 313 pixels
e duas verticais de 297: cerca de 1.220 chamadas à BIOS antes de qualquer jogada.
Um círculo de raio 40 são mais uns 250. No navegador, cada uma dessas
interrupções cai dentro do emulador em vez de cair numa ROM de verdade.&lt;/p&gt;

&lt;p&gt;Não faz diferença (um tabuleiro de jogo da velha são alguns milhares de pixels,
e a coisa desenha na hora), mas deixa claro o que a emulação está comprando aqui.
Não é velocidade. É a fidelidade de uma BIOS que não existe em hardware há
décadas.&lt;/p&gt;

&lt;h2 id=&quot;o-formato-do-bundle&quot;&gt;O formato do bundle&lt;/h2&gt;

&lt;p&gt;O js-dos recebe um arquivo &lt;code class=&quot;highlighter-rouge&quot;&gt;.jsdos&lt;/code&gt;, que no fim das contas é um zip comum. O meu
tem dois arquivos: o &lt;code class=&quot;highlighter-rouge&quot;&gt;VCA.EXE&lt;/code&gt; e um &lt;code class=&quot;highlighter-rouge&quot;&gt;.jsdos/dosbox.conf&lt;/code&gt; de 77 bytes que manda o
DOSBox subir a máquina como &lt;code class=&quot;highlighter-rouge&quot;&gt;vgaonly&lt;/code&gt;, montar o bundle como &lt;code class=&quot;highlighter-rouge&quot;&gt;C:&lt;/code&gt; e rodar o
executável. É esse o passo de empacotamento inteiro.&lt;/p&gt;

&lt;p&gt;O bundle inteiro tem 2.236 bytes. O emulador que roda ele são 1,4 MB de
&lt;code class=&quot;highlighter-rouge&quot;&gt;wdosbox.wasm&lt;/code&gt; mais 315 KB de carregador. Só o WebAssembly é 450 vezes o tamanho
do programa de 3.244 bytes que ele existe para rodar. É por causa dessa
assimetria que a demo carrega sob clique e não junto com a página: quem veio ler
sobre o projeto não deveria pagar por um DOS que nunca pediu para ligar.&lt;/p&gt;

&lt;h2 id=&quot;duas-coisas-que-a-metade-do-navegador-me-custou&quot;&gt;Duas coisas que a metade do navegador me custou&lt;/h2&gt;

&lt;p&gt;O js-dos usa a CDN dele como padrão do &lt;code class=&quot;highlighter-rouge&quot;&gt;pathPrefix&lt;/code&gt;. Deixe como está e a página
baixa 1,4 MB de WebAssembly do servidor de um terceiro, que é exatamente o que
este site foi feito para não fazer. A falha é silenciosa: tudo funciona, e a
página só não é o que eu disse que ela era. A minha verificação de requisições a
terceiros lia atributos &lt;code class=&quot;highlighter-rouge&quot;&gt;src&lt;/code&gt; e &lt;code class=&quot;highlighter-rouge&quot;&gt;href&lt;/code&gt; do HTML gerado, então ela era cega por
construção: essa requisição nasce em tempo de execução, dentro do JavaScript. A
verificação nova varre o JavaScript publicado atrás de URLs fora da nossa origem.&lt;/p&gt;

&lt;p&gt;A segunda foi mais barulhenta. O js-dos traz uma folha de estilo, e essa folha
abre com um reset completo do Tailwind: &lt;code class=&quot;highlighter-rouge&quot;&gt;h1..h6{font-size:inherit}&lt;/code&gt;,
&lt;code class=&quot;highlighter-rouge&quot;&gt;a{color:inherit;text-decoration:inherit}&lt;/code&gt;. Ela carrega depois da minha, com a
mesma especificidade. Clicar em Jogar achatava todos os títulos e apagava a cor
de todos os links da página de uma vez só. A correção foi parar de servir aqueles
118 KB e escrever à mão as poucas regras de que o DOM do emulador realmente
precisa, escopadas sob &lt;code class=&quot;highlighter-rouge&quot;&gt;.demo-screen&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;o-readme-estava-errado&quot;&gt;O readme estava errado&lt;/h2&gt;

&lt;p&gt;O readme do jogo manda digitar &lt;code class=&quot;highlighter-rouge&quot;&gt;OLC&lt;/code&gt; para jogar o círculo. O código compara com
&lt;code class=&quot;highlighter-rouge&quot;&gt;&#39;C&#39;&lt;/code&gt;, no &lt;code class=&quot;highlighter-rouge&quot;&gt;vca.asm:80&lt;/code&gt;, e foi &lt;code class=&quot;highlighter-rouge&quot;&gt;C22&lt;/code&gt; que funcionou quando rodei o jogo pela
primeira vez no DOSBox. As mensagens nunca mudaram: o jogo ainda escreve
&lt;code class=&quot;highlighter-rouge&quot;&gt;O Venceu&lt;/code&gt; e &lt;code class=&quot;highlighter-rouge&quot;&gt;Vez do O&lt;/code&gt;. Em algum momento a tela ficou com o &lt;code class=&quot;highlighter-rouge&quot;&gt;O&lt;/code&gt; e o parser ficou
com o &lt;code class=&quot;highlighter-rouge&quot;&gt;C&lt;/code&gt;, e o readme seguiu a metade que era mais fácil de ver.&lt;/p&gt;

&lt;p&gt;Então a demo documenta &lt;code class=&quot;highlighter-rouge&quot;&gt;C&lt;/code&gt;, e existe um teste que falha se alguém “corrigir” isso
de volta para &lt;code class=&quot;highlighter-rouge&quot;&gt;O&lt;/code&gt; lendo o readme. É uma coisa pequena, mas é o tipo de coisa que
só aparece quando se roda o programa em vez de descrever ele. Sete anos depois,
é para isso que esse porte serviu.&lt;/p&gt;
</content>
  </entry>
</feed>
