<feed xmlns='http://www.w3.org/2005/Atom'>
<title>sl0pboy.git, branch master</title>
<subtitle>sl0pboy - a Game Boy Color emulator (companion to the gbos OS)</subtitle>
<id>https://git.sl0p.foo/sl0pboy.git/atom/?h=master</id>
<link rel='self' href='https://git.sl0p.foo/sl0pboy.git/atom/?h=master'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0pboy.git/'/>
<updated>2026-09-24T12:21:30Z</updated>
<entry>
<title>web: point at gbos's new tools in the lab header</title>
<updated>2026-09-24T12:21:30Z</updated>
<author>
<name>gbc dev</name>
<email>gbc@localhost</email>
</author>
<published>2026-09-24T12:21:30Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0pboy.git/commit/?id=56c512a5178af52b11e19a9f917165c334202b6c'/>
<id>urn:sha1:56c512a5178af52b11e19a9f917165c334202b6c</id>
<content type='text'>
gbos grew a /bin mount and a pile of userland (help, ls -l, df, mv, tail,
tee, sleep, xxd, poweroff), so the header suggests the ones that show the
machine is real rather than a canned animation: help, ls /bin, ls -l, df.
Also drops the stray path from the wget example - it takes a host only.

Checked each in the browser against the deployed ROM: `ls /bin` lists the
builtins, `help` prints the program table, `df` reports "blocks 5/128 used
(1K of 32K), inodes 2/32 used", `ls -l` shows the /bin directory entry.
Demo still assembles in ~22s with 4/4 leases, ircd chat and httpd 200 /.
</content>
</entry>
<entry>
<title>web: show what the demo is doing while it boots, and lock the controls</title>
<updated>2026-09-24T10:45:14Z</updated>
<author>
<name>gbc dev</name>
<email>gbc@localhost</email>
</author>
<published>2026-09-24T10:45:14Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0pboy.git/commit/?id=e235adb5cf9655b23d0097f8ab184e54d2d7dea0'/>
<id>urn:sha1:e235adb5cf9655b23d0097f8ab184e54d2d7dea0</id>
<content type='text'>
The page spent ~20s assembling itself with nothing to show for it but four
Game Boys sitting on banners, which reads as broken rather than busy. There's
now a boot banner with a spinner naming the current step ("GB2 is 10.0.0.4 -
joining #gb on 10.0.0.2... step 3/4"), and each machine's card pulses while
it's still being set up. When it finishes, the banner turns green and says so.

The controls are disabled for the duration - not just cosmetics: plugging in a
Game Boy mid-scenario takes the address the next step is about to be told to
connect to.

Two bugs found by testing the lock rather than assuming it: the DHCP-lease
handler re-enabled "ping across" directly instead of going through refresh(),
and refresh() itself had a leftover second assignment to the same button that
overwrote the demo-aware one. Both now go through refresh(), which is the only
thing that decides whether a control is live.

Verified: all four controls stay off across every step and come back together
at the end, exactly one card pulses at a time, and the banner reaches "demo
ready" with no exceptions.
</content>
</entry>
<entry>
<title>web: the switch builds itself a small network on arrival</title>
<updated>2026-09-23T14:59:27Z</updated>
<author>
<name>gbc dev</name>
<email>gbc@localhost</email>
</author>
<published>2026-09-23T14:59:27Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0pboy.git/commit/?id=b8fe8823c43dcd279d1376608b92303ce3455c2e'/>
<id>urn:sha1:b8fe8823c43dcd279d1376608b92303ce3455c2e</id>
<content type='text'>
An empty switch is a blank page. The lab now stands up a four-machine demo by
itself: one Game Boy runs ircd, one runs httpd, and two more use them - an irc
client that joins #gb and says hello, and a web client that wgets the server
and pings it. Cards carry a role badge so the shape is readable at a glance.

Nothing is faked: every step is a command typed into a real shell over the
link port, which is why you can take it over afterwards and keep typing. New
controls: "restart demo" and "empty the switch"; "+ plug in a Game Boy" still
works the same.

Booting four machines at real speed would be a minute of watching banners, so
each one is fast-forwarded (6 frames per tick) until it has a lease, then
drops back to real time for the part worth watching. Only one is ever boosted
at a time - the scenario is sequential - which keeps the total inside the
~12x-realtime budget a single thread has. Whole thing is ready in ~22s.

deploy.sh now also refreshes the gitignored web/roms/gbos.gb dev copy. That
copy had silently gone stale by two gbos releases, and a local demo run kept
reporting "ircd: not found" for a program that was plainly in the ROM - the
page was fine, the ROM behind it was from before ircd existed.

Verified against the live site: 4/4 leases, roles .2-.5, 60fps each, ircd logs
"&lt;gbchat&gt; hello from the other Game Boy", httpd logs "200 /", wget prints the
HTML and ping gets 4/4, no exceptions.
</content>
</entry>
<entry>
<title>web: refresh the demo ROM story - gbos now serves IRC and HTTP</title>
<updated>2026-09-23T14:12:03Z</updated>
<author>
<name>gbc dev</name>
<email>gbc@localhost</email>
</author>
<published>2026-09-23T14:12:03Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0pboy.git/commit/?id=de20598590151591faad089f74009d44452a919f'/>
<id>urn:sha1:de20598590151591faad089f74009d44452a919f</id>
<content type='text'>
gbos picked up an IRC server (and httpd before it), which is a much better
advert for the lab than `ls`: one Game Boy runs the daemon, another connects
over the virtual switch. Verified on the live site end to end - GB0 `ircd &amp;`,
GB1 `irc 10.0.0.2 gb1`, `/join #gb`, and the message lands:
`&lt;gb1&gt; hello from sl0p.foo`. The header now suggests exactly that, plus the
httpd/wget pair.

Two display fixes fell out of running it:

  - irc(1) is a colour client, so the console mirror was printing raw CSI
    sequences as "[44m[37m..." - the decoder dropped the ESC byte but kept
    the rest. It now tracks ESC / CSI state and discards the whole sequence.
    The LCD still renders the colours; only the plain-text mirror strips them.
  - the LCD is 40 columns, so full-width output (irc's status bar) arrives
    space-padded. The mirror isn't column-aligned, so it squeezes runs of
    whitespace and drops lines that were nothing but padding.

The ROM itself isn't vendored here (it's a build artifact of ~/dev/gbos that
deploy.sh ships), so this commit is the page copy and the decoder.
</content>
</entry>
<entry>
<title>wasm: cap the stack at 256KB - 16.25MB -&gt; 0.5MB per Game Boy</title>
<updated>2026-09-22T18:48:09Z</updated>
<author>
<name>gbc dev</name>
<email>gbc@localhost</email>
</author>
<published>2026-09-22T18:48:09Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0pboy.git/commit/?id=a2aa8fbb20cab296d44bcdd23c63c0f76ec52869'/>
<id>urn:sha1:a2aa8fbb20cab296d44bcdd23c63c0f76ec52869</id>
<content type='text'>
zig defaults the wasm stack to 16MB, and that lands in the module's *initial*
memory, so every instance allocated 16.25MB before executing an instruction.
With several Game Boys on one page that was the dominant cost: 123MB for six.
The machine is a static and nothing here recurses deeply, so 256KB is plenty.

Per instance: 20.5MB -&gt; 4.8MB of linear memory (six: 123MB -&gt; 29MB). Note the
spelling - zig cc rejects -Wl,--stack-size and -zstack-size, and wants
-Wl,-z,stack-size=N.

With memory no longer the constraint, the page cap goes 6 -&gt; 8 on measurement
rather than guesswork. Emulation-only, one thread: 12.5x realtime at 1, 3.1x
at 4, 1.5x at 8, and exactly 1.0x at 12 - i.e. ~12 is where a thread runs out
of road with nothing left for rendering. 8 holds a verified 60fps each in a
real browser with all eight leased.

Golden frames are still pixel-exact on cgb-acid2, dmg-acid2, cpu_instrs@600
and pky@400 with its .sav, and two Game Boys still lease and ping 4/4.
</content>
</entry>
<entry>
<title>web: selecting a Game Boy puts the caret in its console</title>
<updated>2026-09-22T17:54:55Z</updated>
<author>
<name>gbc dev</name>
<email>gbc@localhost</email>
</author>
<published>2026-09-22T17:54:55Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0pboy.git/commit/?id=ff3b97be10ad5f8cf87a43dadd71daf5e284a82f'/>
<id>urn:sha1:ff3b97be10ad5f8cf87a43dadd71daf5e284a82f</id>
<content type='text'>
Typing commands is the main thing you do in the lab, so selecting a machine
now focuses its input instead of making you click twice. Clicking anywhere on
a card selects it; the input takes the caret.

The catch is that the console swallows the keyboard - 'z' and 'x' are letters
before they are A and B - so there are two ways back to the joypad: esc, or
clicking the Game Boy's screen (which is where you'd reach for it anyway).
Both are spelled out in the header and the input's placeholder.

Switching machines also releases any button still held on the one you left,
which previously could stick down across a focus change.

Also lands the page swap: the network lab is the landing page and the
single-Game-Boy player moved to /play.html, with /net.html 301'd in nginx.

Verified in headless Chrome: focus follows add and click, the screen and esc
both blur back, SELECT still opens the on-screen keyboard afterwards
(20 -&gt; 491 lit pixels), and typing 'uptime' into a card still answers.
</content>
</entry>
<entry>
<title>web: network lab - several Game Boys on a virtual switch, in the page</title>
<updated>2026-09-22T17:50:10Z</updated>
<author>
<name>gbc dev</name>
<email>gbc@localhost</email>
</author>
<published>2026-09-22T17:50:10Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0pboy.git/commit/?id=c1ce018c20c8b9f10b849c1b10c6862b666ea2f6'/>
<id>urn:sha1:c1ce018c20c8b9f10b849c1b10c6862b666ea2f6</id>
<content type='text'>
/net.html plugs Game Boys into a switch that leases them addresses and routes
between them: add one, add another, and they ping each other. Unplug either
and its address goes back to the pool for the next one.

web/gbhub.js is tools/gbhub (gbos) ported to the browser - SLIP framing, DHCP
OFFER/ACK, unicast routing, broadcast flooding and ICMP echo for the gateway.
It deliberately has no uplink: NAT needs a TUN device and a tab has no raw
sockets, so packets addressed off-net are dropped and logged as "no route"
rather than pretending.

web/gbcore.js wraps one wasm instantiation as a GameBoy object. Several Game
Boys are several instantiations of the same compiled Module - the core keeps
its machine in a static, so each gets its own linear memory and is isolated
for free. Measured 12.3x realtime per instance, so the page caps at 6.

Each card gets a text input, because unframed link bytes are console I/O: it
lands on the shell's stdin like tools/gbtype, so you can run `ping 10.0.0.3`
without the on-screen keyboard.

Verified headless (node, two Game Boys through the hub: 2/2 leases, ping
4/4 received) and in headless Chrome against the live site: 4 Game Boys at
60fps each, leases .2-.5, typing into a card's console pings another, unplug
frees the address and a replug reclaims it, no exceptions.
</content>
</entry>
<entry>
<title>wasm: expose the link port as host-drained byte queues</title>
<updated>2026-09-22T17:49:47Z</updated>
<author>
<name>gbc dev</name>
<email>gbc@localhost</email>
</author>
<published>2026-09-22T17:49:47Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0pboy.git/commit/?id=1537eff38cd86e1d0b1980bf8f4412dfa2a11056'/>
<id>urn:sha1:1537eff38cd86e1d0b1980bf8f4412dfa2a11056</id>
<content type='text'>
A browser has no file descriptors, so the wasm build's link port was inert.
Give it a pair of ring buffers instead: the guest's transmitted bytes pile up
in one, the host pushes received bytes into the other, and the external-clock
RX path pops from it (staying pending when empty, exactly like a quiet link).
gbw_serial_drain / gbw_serial_push hand them to JS through a fixed staging
buffer, so nothing allocates per frame.

That is enough to be a real link. SLIP-framed bytes are IP packets; unframed
bytes are console I/O - the same split tools/gbhub makes natively, which is
what lets a page type into the link port and reach the shell's stdin.

One machine per wasm instance, so the queues are plain statics: several Game
Boys on a page are several instantiations of the one compiled module, each
with its own linear memory.

The native link is untouched: the fd paths sit in the #else and, preprocessed
for a native target, gb.c comes out byte-identical to the previous commit
(verified with gcc -E). blargg cpu_instrs still passes and the golden-frame
tests are still pixel-exact.
</content>
</entry>
<entry>
<title>web: boot a demo ROM on arrival, and a deploy script for gbc.sl0p.foo</title>
<updated>2026-09-22T17:29:28Z</updated>
<author>
<name>gbc dev</name>
<email>gbc@localhost</email>
</author>
<published>2026-09-22T17:29:28Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0pboy.git/commit/?id=69ce132b9edb355ecf77199f4b2ee5b339c39a30'/>
<id>urn:sha1:69ce132b9edb355ecf77199f4b2ee5b339c39a30</id>
<content type='text'>
The page fetched nothing on its own, so a visitor met an empty canvas and a
file picker. It now loads roms/gbos.gb on arrival - gbos being a Unix-ish
microkernel for the GBC, which shows off rather more of the emulator than a
title screen - with a note pointing at SELECT for its on-screen keyboard and
warning that its networking rides a link cable a browser tab doesn't have.
Dropping your own ROM still replaces it (and hides the blurb).

web/deploy.sh builds the wasm, stages index + wasm + the demo ROM, rsyncs to
/opt/sl0p/gbc and curls the three URLs back. The ROM is a build artifact of
~/dev/gbos and deliberately not vendored here; if it's missing the page falls
back to "drop in a ROM" on the 404, so the deploy still works.

Also fixes a save-path bug that would have bitten the first 128KB cart:
String.fromCharCode(...sram) throws RangeError on a spread that big, killing
the autosave. Chunked, and the whole thing is wrapped for quota errors.
</content>
</entry>
<entry>
<title>wasm: run the emulator in a browser</title>
<updated>2026-09-22T13:57:06Z</updated>
<author>
<name>gbc dev</name>
<email>gbc@localhost</email>
</author>
<published>2026-09-22T13:57:06Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0pboy.git/commit/?id=6fb4fbbce99c08bd535d1b7e0f356864fb0c38a0'/>
<id>urn:sha1:6fb4fbbce99c08bd535d1b7e0f356864fb0c38a0</id>
<content type='text'>
`make wasm` builds the core to WebAssembly (~44KB stripped) and web/ is a
front-end for it: drag-drop a ROM, pixelated canvas at any zoom, keyboard
mapped like the native build, battery saves in localStorage.

Built with zig cc, which carries its own wasm backend, linker and
wasi-libc - so no emscripten dependency, just zig on PATH. Only the
portable core goes in; render/kitty/control/main are terminal- and
POSIX-bound and stay out. src/wasm.c is the browser's whole host layer:
no terminal, no sockets, no files, and no frame pacing (the page's
requestAnimationFrame owns that). The machine is static, the RGB888 -&gt;
RGBA conversion for putImageData happens in wasm, and nothing allocates
per frame.

The entire host interface is four WASI fd_* imports, pulled in by the
cart info banner's fprintf; the page shims them to console.log in ~20
lines. Cartridge RAM *is* the save file here, so it's exported for the
page to persist per cart title (every 5s and on unload).

Because both builds now advance on the same frame boundary, the port is
checked byte-for-byte against the native binary rather than by eyeballing
a canvas - tools/wasm_check.mjs runs N frames in node and diffs the
framebuffer against a --shot PPM:

  cgb-acid2 @60, dmg-acid2 @60, cpu_instrs @600, mem_timing-2 @700,
  pky.gbc @400 with an imported .sav, cgb-acid2 @120 through the full
  CGB BIOS boot animation   -&gt; all pixel-identical

Also smoke-tested in headless chromium over CDP: core instantiates, ROM
loads through the file input, canvas animates (7 distinct frames over
5.6s), keyboard reaches the joypad, 60fps, and a 32KB save lands in
localStorage.

No audio: the APU is still stubbed.
</content>
</entry>
</feed>
