aboutsummaryrefslogtreecommitdiffstats
Commit message (Collapse)AuthorAgeFilesLines
* demo: boot the SL0PBOY-forked BIOS in the README gifHEADmasteruser3 days3-4/+6
| | | | | | | gbdemo resolves a bare --bios to bios/sl0pboy_bios.bin when present (else stock gbc_bios.bin). Generate the fork with the emulator's tools/mk_sl0pboy_bios.py. The gif now opens on the SL0PBOY boot animation - logo drop, colorize, chime - before gbos comes up.
* term+libc: batched write rendering - one tile render per write(), 2.3x linesuser3 days5-10/+151
| | | | | | | | | | | | | | | | | sys_write sets wBatch; render_cursor_tile - the single choke point all of term_putc's visible mutations pass through - then marks a dirty span (per LINE-SLOT, so spans survive scroll rotation for free) instead of rendering. term_write_end renders every dirty tile exactly once, then the cursor. Scrolls stay immediate (blank rebuild is cheap and artifact-free); file-bound writes flush for free. The enabler is in libc: puts() was a SYS_PUTC trap PER CHARACTER, so almost no console output ever went through sys_write. It now issues one SYS_WRITE for the whole string - one trap, one batch, and every line-printer (echo, irc, the shell) gets the batched path. Measured: 37-char write = 128k cycles (3.5k/char) vs ~8k/char down the per-char trap path. Full demo passes; GIF regenerated.
* demo: boot through the CGB BIOS + double-speed ROM in the README gifuser3 days3-3/+10
| | | | | | | | | spawn --bios shows the real power-on show (logo drop + splash) before gbos boots; gbdemo resolves a bare --bios to the emulator's bundled boot ROM by absolute path (the tmux pane cwd is wherever the user is). The screen-synced waitfor absorbs the extra ~4s - only the first prompt timeout got a bump. Regenerated with the double-speed kernel: same demo, boots and scrolls visibly snappier.
* kernel: CGB double-speed mode - 2x everything (1.99x measured)user3 days4-6/+31
| | | | | | | | | | | | | | KEY1 prepare + STOP at KernelInit (skipped on warm reboot if already fast). The PPU/LCD keep their normal rate; the timer and DIV double with the CPU, compensated at the two consumers: - TimerISR now fires at 128 Hz; a toggle byte keeps wTicks at 64 Hz (uptime semantics unchanged) - sys_sleep halves the DIV-delta accumulator's high byte (256 DIV ticks = 1/128 s in double speed) so gsleep/msleep stay real-time Measured: count 60 (scroll-heavy) 3.77s -> 1.89s wall; wTicks 64 Hz over 4s; ping's 800ms msleep pacing unchanged (2.77s for 4 echoes). sl0pboy already emulated KEY1/STOP + per-speed timer/PPU rates.
* term: latch SCY in VBlank - fix ring-scroll shear on live displaysuser3 days4-6/+27
| | | | | | | | | | | | | | The ring-scroll wrote SCY mid-frame; SCY is sampled per scanline (on hardware and in sl0pboy's PPU), so a scroll landing mid-frame rendered the top of the frame at the old offset and the bottom at the new one - a one-frame shear, invisible in frame-sampled GIF captures but ugly on a live sixel view. term_view_update now writes hSCY (HRAM) and a transparent VBlank ISR (push af / apply / pop af / reti, same profile as TimerISR) copies it to rSCY, so the view only ever moves at frame boundaries. The scroll's tile+map writes stay immediate: the new map row is invisible at the old SCY by construction (it's the ring row one past the visible 18).
* term: SCY ring-scroll - scroll writes ONE map row; OSK toggle is 212 cyclesuser3 days3-77/+112
| | | | | | | | | | | | | | | | | | | | | The BG map's 32 rows become a ring: logical row L always lives at map row (wMapTop + L) & 31, and SCY = (wMapTop + wViewTop)*8 places the view. Scrolling bumps wMapTop, rebuilds the one new bottom line and writes its single map row (map_row_one); the other 17 rows move for free. The whole OSK view dance - shift the cursor above the keys, restore the backlog on hide - is now term_view_update: one SCY write, no map rewrite at all. OSK safety: the keyboard lives on the WINDOW layer, which SCY never moves. Stale ring rows can only appear under it: wViewTop = max(0, wCurRow-14) <= 3 is nonzero only while the OSK is visible, and the window covers exactly those bottom 3 rows (WX=7, WY=120). Verified: scroll + view shift + scroll-while-OSK-up + backlog restore all pixel-correct via VRAM screenshots; full README demo passes. term_scroll: 78.5k -> 35.6k T-cycles (857k pre-optimization: 24x). OSK toggle: 45k tilemap rewrite -> 212 cycles. Regenerated demo/gbos-demo.gif on the new renderer.
* term: 3 more renderer wins - 10.9x scroll, 2.7x putc vs pre-cacheuser3 days2-101/+144
| | | | | | | | | | | | | | | | | | | | | | | 1. color_setup memoization: a call with the same (colL,colR) pair as last time returns immediately (masks/attr still valid in WRAM). Runs of same-colored cells - i.e. almost all text - hit this. 2. build_tile blank fast path: two spaces = bg-only planes, 8 constant rows; term_scroll's cleared line and every blank region skip the glyph pipeline entirely. 3. build_tile two-phase rewrite: combine both glyphs into wRowBuf with pointers in registers (the old per-row 16-bit WRAM pointer walk was most of the blit), then compose planes unrolled with plane-0 masks in B/C. 4. term_write_tilemap attr pass: split each row at the pos-256 VRAM bank boundary into two tight cache->tilemap copy runs - no per-tile addressing or bank test. Measured (emulator cycle counter, ANSI test screen): term_scroll 857k -> 248k (attr cache) -> 78.5k T-cycles term_write_tilemap 723k -> 114k -> 45.4k term_putc 18.2k -> 6.7k A scroll is now ~1.1 frames of guest CPU (was 12); a full 40-char line prints in ~63ms of guest time (was ~173ms).
* term: cache tile attrs in COL_BUF's spare stride bytes (3.5x scroll)user3 days1-28/+38
| | | | | | | | | | | | | | term_write_tilemap's attribute pass ran color_setup - the full Fano palette walk - for all 360 tiles on every scroll. render_tile already computes the attr when a tile changes, so store it (COL_BUF + slot*64 + ACACHE + tcol; the stride's 24 spare bytes were free) and the attr pass becomes a table read. update_cursor_attr reads the cache too. term_init now redraws before writing the tilemap so the cache is warm. Measured entry-to-return with the emulator cycle counter, ANSI test screen content: term_write_tilemap 723k -> 114k T-cycles (6.3x), term_scroll 857k -> 248k (3.5x) - a scroll drops from ~12 frames of guest CPU to ~3.5.
* docs: drop the link-drop investigation writeupuser3 days2-138/+0
|
* hooks: clang-format pre-commit (changed lines only) + .clang-formatuser3 days3-0/+69
| | | | | | | | | Same setup as sl0pboy: git-clang-format scopes formatting to the lines each commit touches, re-adds the formatted files, and refuses partially-staged C files. Style file matches the house style (4-space, attached braces, 100 cols, short cases/ifs inline, no include sorting - order is load-bearing for SDCC userland). Enable once: git config core.hooksPath hooks
* docs: concise README (networking + demo gif up front); internals.mduser3 days2-344/+308
| | | | | | | | | README is now pitch -> proof (real capture gif) -> quick start -> docs pointers, with the network stack and gbhub/gbjoin/gbtype getting explicit billing. The full technical breakdown (memory model, syscall ABI, fork/exec/context switch, FS layout, toolchain war stories) moves to docs/internals.md, plus previously-undocumented sections on the network stack and the terminal/OSK.
* tools: gbdemo - scripted, screen-synced demo driver (+ the README demo)user3 days3-0/+353
| | | | | | | | | | | | | Runs a step file: spawns a GB on the gbhub network, records the LCD to a .gbv over the emulator control socket, injects console text (hub ctl, like gbtype) and button events (gbctl) at the right moments, then renders a GIF (gbgif.py). Sync is waitfor/waitgone on the terminal's WRAM shadow (TERM_BUF $D600 + wAssign row map) read via the emulator's debug socket - steps key off real screen output, so boot/DHCP/IRC timing can vary without breaking the choreography. demo/readme.gbd is the README GIF: cold boot -> uname -> ping -> IRC session on 10.0.0.1.
* term: 7-color terminal (per-glyph fg+bg) + ANSI escapes; colorize ircuser3 days9-91/+907
| | | | | | | | | | | | | Fano-plane palette scheme: the 7 colors map onto 7 CGB BG palettes so any color pair shares one palette; a tile's palette is picked from the set of colors its two cells need (tables generated by tools/gencolor.py). Per cell a packed (bg<<4)|fg byte lives alongside the char shadow, and the glyph blitter steers glyph/empty pixels to each cell's fg/bg color slots via plane masks. An ANSI-ish CSI parser (ESC [ .. m) drives it; usr/ansi.c demos it and the irc client now renders hashed nick colors, status dimming and a channel-activity bar.
* kernel: a real timer source (64 Hz tick IRQ) + uptime(1)user3 days13-10/+132
| | | | | | | | | | | | | | | | | | | | | | | | | | The kernel had no clock: TimerISR was a reti stub, IEF_TIMER masked, and IME was never enabled - the vectors were decorative. sys_sleep just polls DIV deltas per-process; nothing counted globally. Now: TAC runs the hardware timer at 16384 Hz with TMA=0, so TIMA overflows at exactly 64 Hz; TimerISR increments a monotonic 32-bit wTicks (wraps after ~2.1 years). Scheduling stays cooperative - the ISR is transparent. Enabling IME in a kernel written for zero interrupts needs care wherever SP points into memory whose bank is being switched (an IRQ pushes onto SP): - read_block/write_block map the disk bank over the $A000 window that holds the caller's stack -> di/ei around the transfer (~2ms, well under the 15.6ms tick period, so no tick is ever lost) - hSwitchTo switches SVBK + cart-RAM banks under the outgoing stack -> di on entry, ei once the incoming stack is mapped - fork already runs on KSTACK_TOP2 (fixed WRAM) - safe as-is - term_putc's SVBK switch only remaps $Dxxx, stacks live in $Axxx/$Cxxx SYS_UPTIME (36) copies the counter (4B LE, di/ei so the read can't tear) to a user buffer; libc gticks(); usr/uptime.c formats 'up [Nd] H:MM:SS'. uptime avoids SDCC long div/shift entirely: sm83.lib modules link into their own areas that land in the $A000 RAM window (latent build.sh trap, documented there) - bytewise >>6 plus bounded subtraction loops instead.
* kernel: dmesg-style boot spew before init spawnsuser4 days5-2/+330
| | | | | | | | | | | | | | | | | | | Print everything the kernel knows implicitly at boot, Linux-flavored, on the LCD console and mirrored over the link (gbhub logs each GB's boot): gbos sm83 microkernel console: CGB (boot a=$11) <- boot ROM's A/B, saved at entry cart: mbc5, 1M rom, 128K sram <- our own cart header ($0147-49) mem: 32K wram 16K vram 127B hram <- CGB constants proc: 8 slots, 31 programs <- link-time table sizes net: slip on link port, 4 sockets fs: gbfs v3 mounted, 120/128 blk free <- bitmap popcount; 'formatted' tty: 40x18 console, SELECT = osk on first boot init: spawning pid 1 New src/dmesg.asm with kernel print helpers (kputc/kputs/kputhex/kputdec) that bypass KPutc (no process context at boot). term_init now runs before net/fs init so the spew is visible as subsystems come up.
* refactor: c/ -> usr/, compiled program blobs out of the source treeuser4 days41-56/+57
| | | | | | | Userland sources live in usr/ (fits the Unix theme better than 'c'). SDCC output .bin blobs land in build/usr/ with the other build artifacts instead of littering the source dir; programs.asm INCBINs them from there. Byte-identical ROM.
* tools: gbtype prints hub errors to stderr and exits 1user4 days1-1/+5
|
* net: reject out-of-order TCP segments (accept only seq == rcv_nxt, re-ACK dups)user4 days1-0/+46
| | | | | | | | Retransmitted segments (GB ACKs lag behind slow LCD rendering, so real servers do retransmit) were accepted as fresh data: the same line rendered again on every retransmit, and rcv_nxt over-advanced so every later outgoing segment carried an ACK beyond the peer's snd_nxt - which real stacks drop, silently wedging the session ('/join does nothing' until reconnect).
* term: dark mode - black background, white text (OSK highlight inverts to match)user4 days2-12/+17
|
* irc: keep the keyboard live during RX floods (poll input every pass)user4 days1-7/+13
| | | | | | | | | | | | | | | | | | | | | | '/join #sl0p right after connect' looked broken: the main loop only polled input when net_recv_nb returned nothing, and 'continue'd on every received segment. During a big MOTD (irc.sl0p.foo's is ~60 lines of color art) that starved input for the whole ~20s flood - the OSK wouldn't even open, and keystrokes were dropped. gbtype input fared a bit better (buffered in the 64-byte console ring) but still wasn't serviced until the flood ended. Poll pollin()/pollcon() once every loop iteration regardless of RX, and only idle-sleep when a pass both received nothing and read no key. The protocol/join logic was already correct (verified: JOIN #sl0p is sent, accepted, and the client switches to [#sl0p] with topic + names) - this just makes typing responsive while messages are streaming. Verified against the live server: injecting /join #sl0p *during* the MOTD flood now joins immediately instead of waiting it out. (Note: '#' is on the OSK - third row, second from the end: 10,8,":;,=+*_!?()[]<>@#~" - just not obvious.)
* irc: strip mIRC formatting, drop 004/005 noise, fix a RAM-bank-swap landmineuser4 days1-1/+42
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Against the real irc.sl0p.foo (via a TLS-stripping socat proxy) the client's screen filled with garbage after connect. Two causes: 1. Color/format codes. The MOTD is a mIRC-color ASCII-art logo. We dropped the \x03 control byte (term ignores <32) but left its numeric *arguments*, so the logo rendered as digit soup ('09> stream the work', '030903 030903 ...'). strip_fmt() now removes \x03 color (+its fg[,bg] digits), \x04 hex color, and the \x02/\x0f/\x11/\x16/\x1d/ \x1e/\x1f toggles, applied to the trailing text of every line (leaving \x01 for CTCP). Standalone digits like '3 apples' are untouched. 2. The real bug: handle() defaulted pfx/txt to a read-only "" literal, and both bang(pfx) and strip_fmt(txt) write a NUL terminator into it. That literal lives in ROM (000-FFF); on MBC5 a write there is a RAM-bank-select, silently swapping the cart-RAM bank - where every static lives - out from under the program. One prefix-less or empty-trailing server line (real ircds send them: NOTICE AUTH, ERROR, registration PING) and all state turns to garbage. Fixed with a writable 1-byte 'empty' default (zeroed in main; gbos doesn't clear BSS). Also suppress 004 (MYINFO) and 005 (ISUPPORT) - pure noise on 40 cols, and InspIRCd splits ISUPPORT across several lines. Verified end to end against the live server: full InspIRCd MOTD (logo, LUSERS, links) renders clean and stable; host unit tests cover color/ bold/CTCP-action stripping and prefix-less NOTICE/ERROR/PING.
* gbhub: control socket + gbtype to inject shell input without the OSKuser4 days2-3/+153
| | | | | | | | | | | | | | | | | | | | | | | | | | | | Typing into a Game Boy meant driving the on-screen keyboard by hand. But the link port already *is* the shell's console: bytes the hub sends un-SLIP-framed land straight on stdin (that's what GB.type() has always done for spawn-mode command scripting). This just exposes it. gbhub now serves a control socket (/tmp/gbhub.ctl, world-connectable like the join socket) accepting two commands: 'list' and 'type [idx]' followed by a payload (delimited by the client's half-close). Injection is chunked and paced to respect the 64-byte kernel console ring, so arbitrarily long input - even multi-line scripts - reaches the shell without overflow. The server runs in both daemon and spawn modes. tools/gbtype is the client: tools/gbtype 'ls -l' run a command on GB0 (Enter appended) tools/gbtype -g 1 ps target the Nth joined Game Boy tools/gbtype -n abc no trailing Enter tools/gbtype -r just press Enter tools/gbtype -l list connected Game Boys printf 'ls\nuname\n' | tools/gbtype pipe a script via stdin Options precede the command; the first non-option word starts literal text, so 'gbtype ls -l' needs no quoting. No root required. Verified end to end: injected list/uname/echo through the real CTL protocol and saw the shell execute each and echo output back.
* irc: a bitchx/irssi-style IRC clientuser4 days4-1/+384
| | | | | | | | | | | | | | | | | | | | | | | irc HOST [NICK] - connects over the kernel TCP stack (DNS-resolves the host), registers, and runs a live client on the 40x18 LCD. UI, within the terminal's means (no cursor addressing - just \r + \b): messages scroll above a fixed irssi-style input line '[#chan] text_' redrawn in place; long input scrolls horizontally. The elders' formats: <nick> msg, <nick:#c> off-channel, *nick* private, -nick- notice, * nick action, >target< outbound, -!- server/status. Keys come from both the OSK (SELECT) and the console ring (pollcon), so a hub can drive it. Commands: /join /part /msg /me /nick /quit /raw, plus bare text to the current channel. Handles PING (PONG + a wink), CTCP ACTION/VERSION, JOIN/PART/QUIT/KICK/NICK, 332/353 topic+names, and 433 nick-in-use (auto-appends _). Registers on bank 32 as program id 30. Note: gbos doesn't zero C statics, so main() inits its state explicitly; the local TCP port is randomized (DIV) to dodge a stale server-side half-open from an unclean prior exit. Tested end to end against a small ircd through gbhub: full MOTD burst, join, channel + private messages, actions, and bot replies all render correctly.
* net: SYS_POLLCON + larger console ring for link-injected inputuser4 days6-5/+30
| | | | | | | | | | | | | | | Programs that own their main loop (the IRC client) need to poll for typed input without blocking. pollin() only sees the on-screen keyboard; bytes injected over the link (gbhub 'type', for scripted/hub-driven sessions) land in the kernel console ring, previously only drained by the blocking KGetc path. Add SYS_POLLCON: a non-blocking con_pop for userland. Also enlarge that console ring 16 -> 64. One net_pump drains an entire serial burst into the ring at once, so a whole injected command line has to fit or bytes are dropped and lines merge (a 20-char command came out truncated and glued to the next). 64 covers a full line; mask stays a power of two.
* net: fix truncated TCP field offsets corrupting multi-segment recvuser4 days1-4/+10
| | | | | | | | | | | | | | | | | | | | | SK_STATE/SK_SND/SK_RCV sat at struct offsets 305/306/310, past the 288-byte SK_RXBUF. But the field accessors add the offset as an 8-bit immediate (add SK_x; ld l,a), so rgbasm truncated 305->49, 306->50, 310->54 (the -Wtruncation warnings we'd been ignoring) - putting STATE and the sequence numbers *inside* RXBUF at offsets 49/50/54. Any TCP segment with >=33 payload bytes therefore overwrote the connection's own STATE and rcv_nxt/snd_nxt with message text. The first segment of a stream landed, its data clobbered STATE to a garbage value, and every subsequent segment was dropped because tcp_in no longer saw ESTABLISHED - so multi-segment TCP receives (an IRC MOTD, any HTTP body past one segment) silently stalled. wget appeared to 'work' only because a single-segment reply plus a never-honored FIN still printed once. Fix: reorder the struct so STATE/SND/RCV precede the big RXBUF, keeping every field offset < 256 (SK_SIZE unchanged at 314, all accessors are symbolic). Zero -Wtruncation warnings remain. Verified an 11-line IRC registration burst now arrives intact.
* docs: link-drop investigation resolved - drops are self-healing nowuser4 days1-160/+106
| | | | | | | | | | | | | Rewrite the notes from 'open / not root-caused' to the resolution: the socket never mysteriously dies; every producible drop signature is the emulator process dying silently (SIGPIPE on console echo after a hub restart, terminal death/HUP/stray quit keys in windowed mode - which exactly explains the 'only when idle' correlation - or a guest $ED poweroff), plus observation artifacts in the original session's tooling. Records the fixes shipped on both sides (emulator link auto-reconnect + SIGPIPE ignore + $ED logging; hub eof-vs-error logging, kept stderr, EBUSY guard), the honest spawn-mode caveat, and what's still worth doing.
* gbhub: make GB drops attributable, not spookyuser4 days1-5/+15
| | | | | | | | | | | | Link-drop investigation fallout - every drop looked identical because the hub discarded all the evidence: - gb_reader logs *how* a GB left (clean eof vs the exception) with timestamps on join/leave, for correlating against SSH/terminal events. - Spawn mode keeps each emulator's stderr in /tmp/gbhub_<n>.err instead of devnull - postmortems get the emulator's side of the story. - A second gbhub exits with a clear 'already running?' message instead of an ioctl traceback (and can't damage the live hub's NAT/socket).
* net: unbreak ping against hosts that intermittently drop ICMPuser4 days4-4/+77
| | | | | | | | | | | | | | | | | | | | | | | | | Pinging sl0p.foo through gbhub looked hung: DNS resolved, then nothing. Packet-tracing showed the echo request leaving the hub's TUN and eth0 correctly NATed every time - but 80.78.19.56 blackholes ICMP for all ids in windows of tens of seconds (provider rate limiting). One lost reply wedged ping for minutes because net_recv's pump-counted timeout is effectively unbounded at native emulation speed. Two fixes: - NET_RECVNB (op 8): non-blocking recv - one RX pump, $FE if nothing buffered, same delivery/EOF semantics as NET_RECV otherwise. ping now waits <=~2s per seq (net_recv_nb + msleep loop), prints 'seq=N timeout' and moves on, like real ping. - ICMP echo id was hardcoded $1234 for every GB, every boot, so all sessions produced byte-identical flows - hostile to NAT conntrack (keyed on icmp id). wNetEchoId is now our host octet + rDIV timing noise sampled at DHCP lease, distinct per GB and per boot. Verified: 8 back-to-back native-speed gbhub runs, zero hangs; a run that hit a blackhole window printed seq=1 timeout then recovered to 3/4 received. GB<->GB ping and DNS/DHCP unaffected.
* docs: investigation notes for the idle link-socket drop (open bug)user4 days1-0/+190
|
* net: ping resolves hostnames (ping sl0p.foo, not just ping 1.2.3.4)user4 days1-21/+15
| | | | | | | | | | | | ping used parse_ip only, so a hostname gave "bad address". It now uses resolve.h (like wget/nslookup): dotted-quad is used as-is, a name is looked up via DNS first. Copies the arg to a safe buffer before resolving (the 0xA000 arg/_DATA overlap dance). Verified through DHCP + NAT: /# ping sl0p.foo PING 80.78.19.56 reply from 80.78.19.56 seq=1 ... -- 4/4 received
* net: pump the network from the console wait (answer pings at the prompt)user4 days3-42/+85
| | | | | | | | | | | | | | | A Game Boy sitting at the shell prompt now services the network instead of being deaf until you run netd: KGetc (the console input wait) pumps net_pump each poll, so inbound pings are auto-answered while idle. net_pump gained an in-frame model and routes any non-framed link bytes to a small console-input ring (con_push/con_pop), so a headless-injected command still reaches the shell while SLIP frames go to the stack. Removes the old SLIP-skip-in-KGetc hack. Verified: two Game Boys on the hub, GB0 idle at the prompt (no netd), GB1 `ping 10.0.0.2` -> 4/4 replies, routed GB1->hub->GB0->hub->GB1. (Separate, pre-existing: gbhub *spawn* mode and windowed gbjoin can drop an idle link socket - under investigation; daemon mode + headless is solid.)
* tools: gbhub --daemon + gbjoin - interactive multi-Game-Boy networkinguser4 days2-20/+87
| | | | | | | | | | | | | | | | | | | gbhub gains a --daemon mode: instead of spawning headless emulators, it just runs the switch/router/DHCP and accepts link connections, handing each a lease from a pool (10.0.0.2..) and freeing it on disconnect. The link socket is made world-connectable so unprivileged emulators can join. gbjoin (no root) launches a *windowed* Game Boy and plugs its link into the running hub, with its own ROM copy so battery saves don't clash. So you get real interactive Game Boys on one network: term 1: sudo tools/gbhub --daemon term 2: tools/gbjoin # windowed GB -> 10.0.0.2 term 3: tools/gbjoin # windowed GB -> 10.0.0.3 (on each: SELECT for the OSK, then e.g. ping 10.0.0.3) Verified two GBs join the daemon and get distinct leases; routing is the same code path as the (already-verified) spawn-mode GB<->GB ping.
* tools: gbhub - many Game Boys on one network (DHCP pool + inter-GB routing)user4 days1-0/+166
| | | | | | | | | | | | | | | | | A virtual switch/router/DHCP for N emulators. It leases each Game Boy a distinct address (10.0.0.2, 10.0.0.3, ...), routes packets between them by destination IP, floods broadcasts, and NATs external traffic out a TUN. No kernel changes are needed: the GB just sends to a dst IP over its one link and the hub decides GB<->GB vs GB<->internet. sudo tools/gbhub 2 netd "ping 10.0.0.2" Verified two Game Boys talking to each other: [GB0] dhcp: leased 10.0.0.2 [GB1] dhcp: leased 10.0.0.3 [GB0] netd: up (10.0.0.2) [GB1] ping 10.0.0.2 -> 4/4 received DHCP is keyed per connection (not MAC), so identical GBs still get unique IPs; each emulator gets its own ROM copy so battery saves don't clash.
* cleanup: drop c/tcp.c (old userland-SLIP TCP client, superseded by kernel ↵user4 days1-107/+0
| | | | TCP/wget)
* net: DHCP client - lease the IP at boot instead of hardcoding ituser4 days10-34/+360
| | | | | | | | | | | | | | | | | | | | | | | | | | | | The address is no longer baked in. net_init starts at 0.0.0.0; a DHCP client runs as the first thing on boot (init/shell forks it and waits), and only once it has a lease (or gives up) does the prompt appear. Kernel: - IP is configurable: 0.0.0.0 until leased; NET_SETIP op stores it. - 16-bit frame length. DHCP/BOOTP packets are ~272 bytes, over the old 255-byte frame cap, so net_slip_send takes a 16-bit length, net_pump assembles into a 320-byte buffer with a 16-bit wNetRxLen, and udp_send writes a 16-bit IP total. Payloads stay <=255 (kept small on purpose) so the per-protocol datalen math is unchanged. wNetTx/wNetRxBuf 256->320, SK_RXBUF 208->288. Userland: - c/dhcp.c: DISCOVER->OFFER->REQUEST->ACK over a UDP socket, then net_setip(); times out gracefully (shell still boots) if there's no server. sh.c runs it before the prompt. Bridge (self-contained DHCP server, no dnsmasq): - tunbridge.py + netboot intercept UDP->:67 and answer OFFER/ACK leasing 10.0.0.2 (gateway 10.0.0.1); everything else is bridged/NATed as before. Regression-tested ICMP/UDP/TCP after the 16-bit change. Verified end to end: dhcp: discovering dhcp: leased 10.0.0.2 /# ping 10.0.0.1 -> 4/4 (traffic from the leased address)
* kernel: sys_sleep - a cooperative delay off the DIV timer (+ ping pacing)user4 days7-2/+67
| | | | | | | | | | | | | | There was no time source at all (IRQ vectors just reti; scheduler is purely cooperative). The DIV register (FF04) free-runs at 16384 Hz regardless of interrupts, so sys_sleep accumulates DIV deltas across SchedYields (other procs keep running) until the requested number of 1/64-second units elapse. - SYS_SLEEP(34): B = 1/64s units; libc gsleep(units) / msleep(ms) wrappers. - ping now msleep(800) between echoes, so it paces like real ping instead of blasting all four at once. Verified real-time (capped emulator): replies land ~0.85s apart. In --uncapped runs the delay is GB-time (fast wall-clock), as expected.
* net: keep network packets out of the console (fixes shell garbage on netboot)user4 days4-0/+28
| | | | | | | | | | | | | | | | | The link port is both the console-input fallback and the network. At the shell prompt the host's multicast (mDNS/LLMNR/IGMP) was fed to the serial and KGetc read those packet bytes as console input -> garbage in the shell, OSK unusable. Two-part fix: - KGetc now skips SLIP frames (0xC0-delimited) on the serial console path, so inbound packets never surface as console input. Plain injected bytes (the headless tunbridge command path) still pass through. New wConInSkip flag. - netboot/tunbridge only forward IP packets destined to 10.0.0.2 (drop the multicast noise at the bridge). Verified: GB sits at a clean "/#" prompt while all packets (noise included) are forwarded; being-pinged (3/3) and wget still work. On the emulator, the OSK is SELECT=space, START=enter, A=z, B=x, d-pad=WASD/arrows.
* tools: netboot - interactive networked emulator (TUN + NAT + --serial-sock)user4 days1-0/+128
| | | | | | | | | | | | | | Brings up gbtun0 (10.0.0.1/24) + NAT, then launches sl0pboy with your own args and the link port bridged to the TUN over a unix socket (the emulator's new --serial-sock). Unlike headless tunbridge, the LCD + on-screen keyboard stay live, so it's a fully interactive networked Game Boy: the OSK is the console, the link port is the network. sudo tools/netboot --sixel --chrome gbos.gb # on the GB: SELECT for the OSK, then type: wget example.com / ping 1.1.1.1 GB is 10.0.0.2, host is 10.0.0.1. Requires /dev/net/tun + root. Ctrl-C or quit the emulator to tear down. tunbridge stays for headless scripted runs.
* net: wget-by-hostname + shared DNS resolver (a Game Boy fetches example.com)user4 days3-96/+109
| | | | | | | | | | | | | | | | | | | | | | | | | | resolve.h: shared helper that turns a dotted-quad or a hostname into an IPv4 address (DNS A query over a UDP socket to 1.1.1.1). nslookup now uses it and slims down; wget uses it so you can name a host directly. wget HOST|IP now: resolve -> TCP connect -> "GET / HTTP/1.0" with the real Host header -> stream the body to the LCD/console until EOF. One command does DNS + TCP + HTTP, all on the kernel socket layer. Verified over tunbridge's NAT against the real internet: /# wget example.com connecting 172.66.147.243 HTTP/1.1 200 OK Server: cloudflare <!doctype html><html lang="en"><head><title>Example Domain</title>... </body></html> [eof] The full HTML page arrives across many TCP segments and is reassembled and printed - real DNS, real TCP, real HTTP, fetched by a Game Boy by name. (tunbridge.py already sets up + tears down the NAT, so the demo is a one-liner: sudo python3 tools/tunbridge.py "wget example.com")
* net: TCP sockets in the kernel - a Game Boy fetches HTTP over real TCPuser4 days2-22/+720
| | | | | | | | | | | | | | | | | | | | | | | | | Kernel TCP client on the socket layer: net_connect(SOCK_TCP) runs the 3-way handshake in-kernel, send()/recv() drive the byte stream, close() does the FIN. - Socket gains state + 32-bit snd_nxt/rcv_nxt (big-endian, add-with-carry-fold). - tcp_send_seg builds IP+TCP with a pseudo-header checksum; the SYN carries an MSS option (200) so the peer never sends a segment larger than our 256-byte frame buffer (we don't do IP reassembly). - tcp_in state machine: SYN_SENT->ESTABLISHED on SYN-ACK, buffers in-order data and ACKs it, handles FIN -> recv() returns 0 (EOF). - No retransmission: the GB<->host link is lossless and the host's real TCP owns the internet side - which removes TCP's hardest part. - net_pump now processes one frame per call so recv drains each segment before the next arrives (single rx slot, no overwrite). wget.c is now a thin socket client: connect -> send "GET / HTTP/1.0" -> recv to EOF -> print. Verified end to end against a host HTTP server: /# wget 10.0.0.1 HTTP/1.0 200 OK Hello from a real HTTP server, fetched by a Game Boy! with a clean SYN/SYN-ACK/ACK ... PSH ... FIN/ACK trace on the wire.
* net: UDP sockets in the kernel + a DNS resolver (real hostname lookups)user4 days6-22/+424
| | | | | | | | | | | | | | | | | | | | Adds UDP to the kernel socket layer on top of the ICMP core: - net_sum() (raw folded sum) split out of net_cksum() so a UDP pseudo-header (src/dst IP + proto + length) can seed the segment checksum. - udp_send: builds IP+UDP with the pseudo-header checksum; NET_BIND sets the local/source port; net_connect sets the peer. - udp_in: demuxes inbound UDP by destination port to the bound socket (net_find_udp), delivers the payload + source addr. Also fixes a real recv bug: net_pump clobbers BC/DE/HL, so the old recv timeout counted in registers and was effectively random. recv now counts in WRAM. New `nslookup HOST` (PROG_NSLOOKUP=28, bank 30): builds a DNS A query and parses the answer (with 0xC0 name-compression) entirely in userland over a UDP socket - the kernel never sees DNS, just UDP. Verified through the bridge NAT: /# nslookup example.com -> example.com -> 172.66.147.243 This gives us name resolution for the TCP/HTTP demo next.
* net: proper socket API in the kernel (SYS_NET) - no more SLIP in userlanduser4 days9-140/+872
| | | | | | | | | | | | | | | | | | | | | | | | The network stack moves into the kernel. src/socket.asm owns SLIP framing, IPv4, RFC1071 checksums, and ICMP; programs now speak a socket API through one syscall (SYS_NET, DE=&netreq dispatched by op): net_socket/connect/send/recv/ close/poll (c/sock.h). No program touches SLIP, IP headers, or checksums. - Socket table (4 sockets) + tx/rx buffers in WRAM0; our IP = 10.0.0.2. - net_pump: drains the link, reassembles SLIP frames, demuxes IPv4. Inbound ICMP echo requests are auto-answered in-kernel, so the GB replies to pings whenever any process pumps RX. - ICMP sockets: send() emits an echo request to the connected peer; recv() returns the matching reply (with a spin/yield timeout). ping.c is now a ~15-line socket client; netd.c is just `for(;;){net_poll(); yield();}`. Verified over tunbridge: /# ping 1.1.1.1 -> replies from the real internet (kernel builds it all) host# ping 10.0.0.2 -> 4/4, 0% loss (kernel auto-answers) Gotchas recorded: gbos.inc isn't a make dep (touch asm after editing); this crt0 doesn't copy initializers (fill arrays at runtime); and the arg string at 0xA000 overlaps _DATA, so parse targets must sit past it (big buffer first). UDP and TCP sockets build on this same core next.
* net: ping - GB-originated ICMP echo (a Game Boy pings the real internet)user4 days4-1/+109
| | | | | | | | | | | | | | | New `ping [A.B.C.D]` program (PROG_PING=27, bank 29): builds and sends ICMP echo requests from 10.0.0.2, then reads replies off the link port. It keeps reading SLIP frames until it finds *our* echo reply, skipping the IGMP/mDNS/ SSDP multicast noise that shares 10.0.0.0/24. Reply wait uses a generous srecv_nb spin budget since the emulator runs uncapped (no timer syscall yet). Verified over the tunbridge (with NAT): /# ping 10.0.0.1 -> 4/4 received, ttl=64 (the SLIP peer/host) /# ping 1.1.1.1 -> 4/4 received, ttl=56 (Cloudflare, real net!) ttl=56 is a real internet round trip (64 minus the hops). Combined with the host being able to ping the GB, the Game Boy is now a full two-way ICMP host.
* net: real routed IP via SLIP<->TUN bridge (you can *ping* a Game Boy)user5 days1-0/+127
| | | | | | | | | | | | | | | | | | | | | tunbridge.py opens a TUN device (gbtun0, 10.0.0.1/24), NATs 10.0.0.0/24 out to the internet, spawns the emulator, and bridges raw IP packets on the TUN to/from SLIP frames on the link serial. The Game Boy (10.0.0.2) becomes a genuine routed IP host: the host kernel routes its packets for real, so the host's own `ping` command reaches it and netd answers. Verified: `sudo python3 tools/tunbridge.py netd --test` 64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=1.61 ms 4 packets transmitted, 4 received, 0% packet loss Two hard-won gotchas baked in: - Under sudo, ~ expands to /root, so resolve EMU/ROM via $SUDO_USER's home. - Only start feeding host->GB packets AFTER netd is running; raw packet bytes delivered to gbos's shell (before netd claims the serial) corrupt the shell and crash the emulator. gb->host draining starts immediately. Requires /dev/net/tun (on Proxmox LXC: allow cgroup2 device c 10:200 rwm + bind-mount /dev/net/tun into the container).
* net: IP stack milestone B - UDP echo (pseudo-header checksum)user5 days2-31/+88
| | | | | | | | | | Refactor netd into a protocol dispatcher (IP -> ICMP/UDP) and add UDP echo: swap addresses + ports and recompute the UDP checksum over the pseudo-header (src/dst IP + proto + length) plus the datagram. This is the same pseudo- header TCP uses, so it de-risks the next milestone. tools/gateway.py --mode udp sends a datagram and verifies the echo. Verified: 'hello udp gbos' echoes back with cksum=ok.
* net: real IP stack, milestone A - ICMP echo (you can ping a Game Boy)user5 days5-36/+190
| | | | | | | | | | | | | | Start of an actual TCP/IP stack on gbos (TLS stays in a proxy). netd is a userland IP responder over SLIP: our address is 10.0.0.2, the SLIP peer 10.0.0.1. It parses IPv4 headers, answers ICMP echo requests, and rebuilds the packet with correct IP + ICMP checksums (RFC 1071 one's-complement sum, carry-folded - works fine on the SM83). c/netd.c + register; tools/gateway.py gains --mode ping: it crafts ICMP echo requests over SLIP and verifies the replies. Verified: `netd` answers 4 pings, gateway reports reply from 10.0.0.2 with cksum=ok for each. Next: UDP, then TCP.
* net: chat client - async receive + OSK send over the link portuser5 days9-5/+127
| | | | | | | | | | | | | | | | | | | | | | | The application layer of the link-port demo, and it ties the whole system together: the LCD terminal displays, the on-screen keyboard types, and the link port carries a live chat. Kernel: sys_srecv_nb (non-blocking link receive; A=byte, CF=none) and sys_pollin (poll the OSK for a typed char without blocking) - syscalls 31/32. Both are what a poll loop needs to receive and type at once. Userland: c/chat.c runs a poll loop - it feeds non-blocking bytes through a SLIP receive state machine and prints whole incoming frames as messages, while pollin() drives the on-screen keyboard; SELECT shows the keys, type a line, START sends it as a frame. libc srecv_nb()/pollin(). Host: tools/gateway.py --mode chat is a simple bot peer (echoes each GB message and injects a few async ones); --keys can drive the OSK for tests. Verified: the gateway pushes 'welcome', '<alice> hey gameboy!', '<bob> nice link cable' unprompted and the GB displays all three (async receive); typing 'hi' on the OSK echoes it and emits the SLIP frame \xC0hi\xC0 (send). A Game Boy in the chat, keyboard on screen, over the link cable.
* net: wget - real HTTP over the link port via the gatewayuser5 days7-54/+112
| | | | | | | | | | | | | | | | Grow the link-port demo from echo to actual network access. The Game Boy still only does SLIP framing + display; the host gateway does DNS/TCP/HTTP. - c/netlib.h: SLIP framing factored out (header-only, per-program copy). necho.c now uses it too. - c/wget.c: `wget URL` frames the URL, then prints the reply body. The gateway streams the body back as typed frames: 'D'<chunk> ... 'E'. - tools/gateway.py: add --mode http (urlopen the frame as a URL, cap the body, chunk it) alongside --mode echo; --cmd runs any gbos command. Verified: `wget example.com` streams back the full Example Domain HTML onto the terminal; `wget sl0p.foo` fetches the real page. A Game Boy on the web, over the link cable.
* net: SLIP framing over the link port + a host gateway (echo round-trip)user5 days9-3/+184
| | | | | | | | | | | | | | | | | | | | First step of link-port networking. The LCD terminal + OSK freed the serial port from console duty, so it can be the network link. Kernel (src/net.asm): raw link-port serial that bypasses the console/ terminal - sys_ssend (transmit, GB drives the clock) and sys_srecv (receive, GB slave, blocks by yielding). Syscalls 29/30. Userland: libc ssend()/srecv(); c/necho.c does SLIP (RFC 1055) framing over them - send a packet, receive the reply, print it. Host: tools/gateway.py wraps the emulator, owns its link serial, speaks SLIP, and (for now) echoes every frame back - the "link cable adapter". Console (ASCII) bytes on the same channel are printed for visibility. Verified: `necho` sends a SLIP frame, the gateway decodes+echoes it, and gbos prints the reply - a real framed round-trip over the Game Boy link port. Next: swap the echo for actual network ops (DNS/HTTP or IRC/chat).
* pipe: PIPE_MAX 2->4 (5-stage pipelines) + fix cross-yield byte corruptionuser5 days2-10/+23
| | | | | | | | | | | | | | | | | | | Bump PIPE_MAX to 4 so a|b|c|d|e (4 pipes) works. This stays within the single-byte buffer-offset math (idx*64+pos <= 3*64+63 = 255) and the fd space ($F0..$F7, clear of $FF console). The bump exposed a data-corruption bug that also affected the 2-pipe case (just invisibly - a wc-only test can't see mangled bytes): pipe_write kept the byte-to-write in the SHARED wPipeByte global across its SchedYield (buffer full), so a concurrent pipe op clobbered it and the writer then stored the wrong byte. Now the byte is held in D across the yield, and pipe_bufptr no longer clobbers D; pipe_read/pipe_write also push their idx across SchedYield rather than assume the yield preserves registers. Verified: count N | cat now streams EXACT content (no 'linn'/'llne' corruption); count 60 | cat | cat | wc = 60 180 1671; 5-stage count 4 | cat | cat | cat | cat prints line 1..4; SIGPIPE (count 200|true) and count 100|wc still fine, no hangs.