<feed xmlns='http://www.w3.org/2005/Atom'>
<title>sl0pboy.git/src, branch master</title>
<subtitle>sl0pboy - a Game Boy Color emulator (companion to the gbos OS)</subtitle>
<id>https://git.sl0p.foo/sl0pboy.git/atom/src?h=master</id>
<link rel='self' href='https://git.sl0p.foo/sl0pboy.git/atom/src?h=master'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0pboy.git/'/>
<updated>2026-09-22T17:49:47Z</updated>
<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>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>
<entry>
<title>core: make the machine loadable without a filesystem</title>
<updated>2026-09-22T13:56:52Z</updated>
<author>
<name>gbc dev</name>
<email>gbc@localhost</email>
</author>
<published>2026-09-22T13:56:52Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0pboy.git/commit/?id=c4848bb0a114a95aaeb282ac105087f2913eb012'/>
<id>urn:sha1:c4848bb0a114a95aaeb282ac105087f2913eb012</id>
<content type='text'>
Groundwork for building the core for targets that have no files, no
sockets and no terminal (a browser, next commit). Three changes, no
behaviour change for the native binary:

  - cart_load() splits into cart_init_owned() (everything past the file
    read) plus cart_load_mem(), which takes a ROM image already in memory.
    The path version keeps owning the .sav wiring; a memory load attaches
    no battery file and leaves save_path empty, which cart_save() now
    treats as "nowhere to write".
  - gb_load_bootrom_mem() does the same for boot ROMs, so the power-on
    animation works wherever the image comes from.
  - gb_run_frame() was declared in gb.h but never existed; main.c had a
    private run_frame() instead. Implement it in gb.c and point main.c at
    it, so every host advances the machine on one identical boundary.

The link-socket layer (--serial-sock) is the only genuinely unportable
part of the core, so under __wasm__ it becomes no-op stubs: the link is
simply always down and the call sites stay untouched. The file-backed
loaders compile out there too, which keeps the libc surface tiny.

Verified: blargg cpu_instrs (11/11), instr_timing, mem_timing,
mem_timing-2 all still Passed; --shot output unchanged.
</content>
</entry>
<entry>
<title>kitty: truecolor graphics output with a layered HUD</title>
<updated>2026-09-22T12:55:45Z</updated>
<author>
<name>gbc dev</name>
<email>gbc@localhost</email>
</author>
<published>2026-09-22T12:55:45Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0pboy.git/commit/?id=44c21908b4ff6a1e41a6b9ba17e3ed78de8ec3db'/>
<id>urn:sha1:44c21908b4ff6a1e41a6b9ba17e3ed78de8ec3db</id>
<content type='text'>
Sixel is a 1987 printer format wearing a terminal costume: 256 colors per
image, no alpha, no compositing, one flat picture. --kitty [scale] takes
the other road on kitty/ghostty/WezTerm/Konsole:

  - the framebuffer ships as raw RGB888, so no per-frame palette build and
    no coarser quantization when a CGB game blows past 256 colors
  - the HUD (title, render rate, frameskip, turbo, live button readout) is
    a *separate* alpha-blended image on z-index 1, composited over the game
    by the terminal instead of being burned into the picture; 'h' toggles
    it, --no-hud starts without it
  - consecutive identical images are dropped, so a pause menu or text box
    stops the traffic dead
  - pixels go over at 1x and the terminal scales them into a cell box, so
    --kitty 6 costs the same bytes as --kitty 1 (padded to whole cells
    first, so the zoom stays exact)

Two transfers: shm hands over the name of a POSIX shared-memory object
(~60 bytes/frame) when we can tell the terminal is local, b64 inlines the
bytes for ssh and tmux (which might be attached from another machine).
--kitty-shm / --kitty-b64 force either. tmux gets DCS passthrough wrapping.
--chrome and --palette work here just like in sixel mode.

Nothing about a graphics stream is human-readable, so tools/kitty_decode.py
reassembles a capture back into images - chunked payloads, tmux wrapping
and all - and can assert a frame is pixel-identical to a --shot:

  tools/kitty_decode.py cap.bin -o frames --check shot.ppm
  -&gt; OK: image 1 matches shot.ppm exactly (160x144)
</content>
</entry>
<entry>
<title>main: keep signal handlers installed (sigaction, not signal)</title>
<updated>2026-09-22T12:55:33Z</updated>
<author>
<name>gbc dev</name>
<email>gbc@localhost</email>
</author>
<published>2026-09-22T12:55:33Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0pboy.git/commit/?id=acac856dcdac42b441d39708b1877ba532492ffd'/>
<id>urn:sha1:acac856dcdac42b441d39708b1877ba532492ffd</id>
<content type='text'>
Built with -std=c11 + _POSIX_C_SOURCE, glibc's signal() gives System V
semantics: the disposition resets to SIG_DFL the moment the handler runs.
So a second SIGTERM - a supervisor retrying, `timeout` signalling the
process group, an impatient second Ctrl-C - killed us mid-shutdown, before
term_restore() put the terminal back and before the render backend cleaned
up after itself. Wrap sigaction() in install_sig() and route SIGINT,
SIGTERM and the serial-link SIGPIPE ignore through it.

Verified: staged tree builds clean (-Wall -Wextra, no warnings); added
lines are already clang-format clean (hook bypassed only because the
split commit leaves main.c partially staged).
</content>
</entry>
<entry>
<title>render: --shrink N - downscale the half-block live display (1-4)</title>
<updated>2026-07-18T18:41:19Z</updated>
<author>
<name>gbc dev</name>
<email>gbc@localhost</email>
</author>
<published>2026-07-18T18:41:19Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0pboy.git/commit/?id=fa1579cc9f209dbeb14ab277dc9c4ac741ace57c'/>
<id>urn:sha1:fa1579cc9f209dbeb14ab277dc9c4ac741ace57c</id>
<content type='text'>
Decimates the LCD N x N before the half-block encode so the monitor pane
takes 160/N cols x 72/N rows. Display-only: capture, recording and sixel
still read the full-res framebuffer.

Decimation is max-luma pick, not box-average: terminal fonts are 1px
bright strokes on dark background, and averaging dims them into grey
mush (the 'blurry font' regression). Keeping the brightest source pixel
of each block preserves stroke brightness and hue.
</content>
</entry>
<entry>
<title>main: allow --sock (control/debug socket) in headless mode</title>
<updated>2026-07-17T18:22:13Z</updated>
<author>
<name>gbc dev</name>
<email>gbc@localhost</email>
</author>
<published>2026-07-17T18:22:13Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0pboy.git/commit/?id=e8c1777a1761ebf55029589912198c83fb06aaec'/>
<id>urn:sha1:e8c1777a1761ebf55029589912198c83fb06aaec</id>
<content type='text'>
The control socket was only opened on the windowed path, so a --headless
GB couldn't be inspected. A scriptable, invisible GB is exactly the one
you most want to peek into - reading kernel WRAM over the debug socket is
how the TCP field-offset corruption got pinned down. Open it in the
headless loop too and poll it each frame; close it on exit.
</content>
</entry>
<entry>
<title>cpu: log the $ED poweroff opcode with pc/registers</title>
<updated>2026-07-17T17:47:02Z</updated>
<author>
<name>gbc dev</name>
<email>gbc@localhost</email>
</author>
<published>2026-07-17T17:47:02Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0pboy.git/commit/?id=ef07ad11bb0a20f137682e901a0a6beacfc08968'/>
<id>urn:sha1:ef07ad11bb0a20f137682e901a0a6beacfc08968</id>
<content type='text'>
The clean-exit opcode used to print nothing, so any guest crash that
walked into a stray $ED byte was indistinguishable from a silent
socket death (one of the kill vectors in the link-drop investigation).
Now it identifies itself on stderr with pc/af/bc/de/hl/sp.
</content>
</entry>
<entry>
<title>gb: auto-reconnect a dead --serial-sock link instead of going silent</title>
<updated>2026-07-17T17:46:58Z</updated>
<author>
<name>gbc dev</name>
<email>gbc@localhost</email>
</author>
<published>2026-07-17T17:46:58Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0pboy.git/commit/?id=130215c21ce4aa300abc9a276e2ffb35c8f43803'/>
<id>urn:sha1:130215c21ce4aa300abc9a276e2ffb35c8f43803</id>
<content type='text'>
Root-cause work for the 'idle link-socket drop' (see gbos
docs/link-drop-investigation.md): when the hub restarts or the bridge
dies, the link fd went dead forever - and before SIGPIPE was ignored,
the next console-echo write() would kill the whole emulator silently.

Now, in --serial-sock mode, EOF on read or EPIPE/ECONNRESET/ENOTCONN/
EBADF on either direction marks the link dead, closes the fd, and
retries connect() every 500ms from the serial poll path (the guest
polls constantly, so no extra plumbing). RX stays pending while down
(serial_no_eof semantics), so the guest just sees a quiet link that
comes back. Logs 'link socket lost / reconnected' either way.

Verified: SIGKILL the hub, restart it - every GB reconnects within ~1s
and keeps its lease.
</content>
</entry>
<entry>
<title>main: silence stderr while the live sixel display is up</title>
<updated>2026-07-17T17:45:03Z</updated>
<author>
<name>gbc dev</name>
<email>gbc@localhost</email>
</author>
<published>2026-07-17T17:45:03Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0pboy.git/commit/?id=a3dd7fa747d631831e920aa55ed03e7954ca6517'/>
<id>urn:sha1:a3dd7fa747d631831e920aa55ed03e7954ca6517</id>
<content type='text'>
Runtime chatter (link reconnect notices, input debug, cart warnings) was
landing mid-sixel-stream and corrupting the picture. Redirect fd 2 to
/dev/null for the duration of the display loop; restored on exit so the
final stats line still prints. Startup errors before the display begins
are unaffected.
</content>
</entry>
</feed>
