aboutsummaryrefslogtreecommitdiffstats
path: root/src (follow)
Commit message (Collapse)AuthorAge
* wasm: expose the link port as host-drained byte queuesgbc dev2026-09-22
| | | | | | | | | | | | | | | | | | | | | | 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.
* wasm: run the emulator in a browsergbc dev2026-09-22
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | `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 -> 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 -> 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.
* core: make the machine loadable without a filesystemgbc dev2026-09-22
| | | | | | | | | | | | | | | | | | | | | | | | | 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.
* kitty: truecolor graphics output with a layered HUDgbc dev2026-09-22
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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 -> OK: image 1 matches shot.ppm exactly (160x144)
* main: keep signal handlers installed (sigaction, not signal)gbc dev2026-09-22
| | | | | | | | | | | | | | 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).
* render: --shrink N - downscale the half-block live display (1-4)gbc dev2026-07-18
| | | | | | | | | | | 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.
* main: allow --sock (control/debug socket) in headless modegbc dev2026-07-17
| | | | | | | | 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.
* cpu: log the $ED poweroff opcode with pc/registersgbc dev2026-07-17
| | | | | | | 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.
* gb: auto-reconnect a dead --serial-sock link instead of going silentgbc dev2026-07-17
| | | | | | | | | | | | | | | | | 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.
* main: silence stderr while the live sixel display is upgbc dev2026-07-17
| | | | | | | | 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.
* main: --serial-sock PATH - link port over a unix socket (net + live display)gbc dev2026-07-17
| | | | | | | | | | | | | The serial fds are serviced by cpu_step in every mode, so wiring them to a connected unix socket lets networking run alongside a live display (--sixel / terminal) - unlike --headless, which needs stdio for the serial. The emulator connects to PATH (retrying ~1s so a launcher can listen first) and uses it non-blocking with no EOF-exit; it overrides the headless stdio serial. Pairs with gbos tools/netboot, which listens on PATH, brings up a TUN+NAT, and bridges the socket <-> TUN. In windowed mode the OSK is the console and the link port is pure network - cleanly separated. Verified: host pings the GB 3/3 over the socket link.
* rebrand: gbc -> sl0pboy (binary, README, tooling)gbc dev2026-07-17
|
* main: options-first CLI with --help, and wire up display/boot flagsgbc dev2026-07-17
| | | | | | | | | | | | - Rewrite argument handling: the ROM is now the final argument and options precede it (parsed in argv[1..argc-2] so optional-value flags never swallow the ROM). Add grouped --help/-h, unknown-option errors, and clear 'missing value'/'no ROM' diagnostics. - Add --chrome, --palette/--green, and --bios flags; set the display/capture options before the --shot and recording paths so screenshots and recordings honor them. Make --shot dump the composited capture frame. - gbctl: pass the ROM last when spawning the emulator. - gitignore the (copyrighted) bios/ directory; document all new flags.
* record: capture the displayed frame (chrome + palette)gbc dev2026-07-17
| | | | | | | The video recorder now writes render_capture_frame() instead of the raw LCD framebuffer, so captures match what's shown: chrome frames record at their larger dimensions (written into the .gbv header) and the LCD palette is baked in. Button presses also show up lit in recordings.
* render: Game Boy chrome, LCD palette recolor, and frame capturegbc dev2026-07-17
| | | | | | | | | | | | | - --chrome: draw a DMG-style 'sl0pboy' shell around the LCD in sixel mode, with a bitmap-font silk-screen (logo, labels), speaker grille, power LED, and buttons that light up while held. - LCD palette recolor (render_set_palette): remap the LCD to a 4-shade palette by luminance for the classic monochrome look ('dmg'/'green' pea green, 'pocket'/'gray'); applies to every render path. - Refactor the sixel encoder into generic build_palette_buf/encode_sixel_buf operating on any RGB888 buffer, shared by the bare LCD and the chrome canvas. - render_capture_size/frame: produce the exact displayed frame (recolor + chrome) as an RGB buffer, for screenshots and video recording.
* gb: boot ROM (BIOS) supportgbc dev2026-07-17
| | | | | | | | | Add optional boot ROM emulation. When a boot ROM image is loaded, gb_reset starts execution at PC=0 with power-on register state instead of the documented post-boot state; the ROM overlays low memory (with the CGB 0x0100-0x01FF header hole showing the cartridge through) until the game writes 0xFF50 to hand off. gb_load_bootrom() accepts 256-byte (DMG) or 2304-byte (CGB) images.
* main/gb: --keys button-script harness + serial_no_eof for OSK testinggbc dev2026-07-16
| | | | | | | | --keys "udlrabse." plays scripted joypad taps (each char = one button held 10 frames then released 10, so the guest sees a clean edge); pairs with --headless --shot to drive on-screen input and capture the result. When --keys is active, serial-RX EOF stays pending instead of sending $04, so a shell blocked on read waits for OSK input instead of exiting.
* main: --shot with --headless dumps final LCD on exitgbc dev2026-07-16
| | | | | | | Lets scripted runs feed stdin to the serial console AND capture the resulting screen, e.g. printf 'ls\nexit\n' | gbc rom.gb --headless --shot 0 out.ppm Previously --shot ran frames with no stdin, so anything waiting on input (a shell) only showed its first prompt.
* headless: $ED opcode = clean power-off (exit + battery save)gbc dev2026-07-16
| | | | | | | | The unused SM83 opcode $ED now sets gb->poweroff, which breaks the headless loop so the normal cart_free()->cart_save() path runs. Lets a guest OS shut the emulator down cleanly (and flush its .sav) instead of relying on an external timeout/SIGTERM, which could SIGKILL before the save. run_frame() also stops on poweroff so exit is immediate.
* headless: interactive TTY mode (raw stdin) for driving a guest OS livegbc dev2026-07-16
| | | | | | | | When stdin is a terminal, --headless now disables canonical mode + local echo so keystrokes reach the guest immediately and only the guest echoes (no doubled chars). Piped/redirected stdin is untouched (scripted input still works). Keep ISIG so Ctrl-C quits; restore termios on exit. VMIN=1 so a no-data non-blocking read is EAGAIN, not a 0-byte read the serial layer would misread as EOF.
* serial: deliver EOF (0x04) on stdin end for headless consolegbc dev2026-07-16
| | | | | | read()==0 completes the external-clock transfer with SB=0x04 (EOT), so a gbos program's readc() sees EOF instead of polling forever. Enables cat/wc-style tools and lets the shell exit on Ctrl-D / pipe end.
* serial: clean RX path for headless consolegbc dev2026-07-16
| | | | | | - external-clock transfers (SC=$80) deliver a stdin byte into SB without echoing to stdout, and stay pending when no input is available -> programs can poll for input cleanly. internal-clock (SC=$81) stays pure TX.
* serial: headless mode tying serial port to stdio for OS debugginggbc dev2026-07-15
| | | | | | | | - --headless: no rendering/TUI; serial TX -> stdout, RX <- stdin (nonblocking) - generalize serial: serial_out_fd/serial_in_fd on the GB; internal-clock transfer now shifts in a real RX byte (from stdin) instead of always 0xFF - paced to native speed by default, --uncapped for turbo; pipes/scripts cleanly - README: document headless serial console
* control: video frame capture + deterministic input-movie replaygbc dev2026-07-15
| | | | | | | | | | | | | | | Two socket features for building reproducible showcase clips: - record start <path> [everyN] / record stop: append the RGB888 framebuffer of each produced frame to a flat 'GBCV' capture file (downsample with everyN). tools/gbgif.py turns it into a GIF (or PNG frames). Capture is decoupled from wall-clock/turbo/frameskip, so timing is always correct. - input play <path> [reset] / input stop: drive the joypad from a synthesized TAS-style movie (text: '<frames> [buttons...]' per line), frame-locked so replay is deterministic. 'reset' does an exact power-on (preserve cart ROM+SRAM, zero all other state) so a movie replays byte-identically -- verified by hashing two runs. tools/gbmovie.py is a Python builder for movies.
* render: no status line in sixel mode (pure graphics output only)gbc dev2026-07-15
| | | | | In --sixel mode the emulator drew a status line (title/skip/[f]ast/q:quit) under the image; drop it so sixel output is nothing but the game graphics.
* fix(control): ignore SIGPIPE so a vanishing client can't kill the emulatorgbc dev2026-07-15
| | | | | | | | | A client that connects and closes before reading (e.g. the HUD's liveness probe) made the greeting write() raise SIGPIPE, terminating the whole emulator - this was behind much of the 'it keeps dying' this session. Also: gbhud now re-resolves the live instance from the registry on every reconnect (skipping stale sockets by test-connecting), so the HUD follows respawns, and its box rendering is ANSI-width-correct.
* apu: register-level emulation of 0xFF10-0xFF3F (fixes music fades)gbc dev2026-07-15
| | | | | | | | | The APU was a black hole: reads returned 0xFF, writes were dropped. That hangs any game that fades music out by decrementing the NR50/rAUDVOL volume register until it reads 0 (Pokemon's StopMusic spin-wait) - the write never stuck, the read never hit 0, infinite loop. Store the 48 sound registers and return them with the standard hardware read-back OR-masks (NR50 mask 0x00 = exact readback). No synthesis, just correct register behavior.
* control: socket-based debug channel + gbctl CLI drivergbc dev2026-07-14
| | | | | | | | | | | | | | Replace the input-only FIFO's limitations with a Unix-domain socket control channel (--sock) that is a superset of the button vocabulary plus emulator introspection: read/write bus memory, get/set CPU context, single-step, PC breakpoints, value-change watchpoints, run/pause. Being a socket it replies to each command and broadcasts async 'event stop ...' lines; a stored last-stop record (stopinfo) lets per-request clients recover a missed event. Add gbctl: a stdlib-python CLI that spawns a tmux pane running the emulator on a ROM, auto-resolves the socket, and drives it with terse verbs (cpu/read/write/ reg/step/break/watch/continue/pause/press/hold/screen/stop). The FIFO stays as legacy input-only.
* speed control: frame skipping to decouple terminal draw from emulationgbc dev2026-07-04
| | | | | | | Emulation always runs at native rate (correct game speed); --frameskip N draws only 1 of every N+1 frames so a slow terminal no longer bottlenecks the loop. Adds --fps cap, --uncapped/--turbo, live [ ] and f keys, HUD readout, and an exit summary of emulated-vs-drawn fps. Replaces the confusing --render-fps.
* input: external control channel via FIFOgbc dev2026-07-04
| | | | | | | Adds --fifo [path] opening a named pipe that accepts button commands from any process. Supports momentary taps (a, start, name:N), deterministic hold/release (+name / -name), and 'release'. Merges with keyboard input; held state carries true key-up semantics the terminal can't provide. GBC_INPUT_DEBUG traces state.
* renderer: inter-frame diffing to minimize terminal outputgbc dev2026-07-04
| | | | | | | Only cells that changed since the previous frame are redrawn, using cursor positioning to skip unchanged runs. Static screens write nothing after the first frame (~108x reduction on acid2). SIGWINCH forces a full repaint. Verified pixel-exact against the framebuffer (11520/11520 cells, 0 mismatch).
* add status line HUD, README with architecture and test statusgbc dev2026-07-04
|
* terminal renderer (truecolor half-block), input, screenshot modegbc dev2026-07-04
|
* core emulator: SM83 CPU, MMU, timer, PPU, CGB supportgbc dev2026-07-04
| | | | Passes blargg cpu_instrs (all 11), instr_timing, mem_timing.
* scaffolding: build system, cartridge loader with MBC1/2/3/5, core typesgbc dev2026-07-04