aboutsummaryrefslogtreecommitdiffstats
Commit message (Collapse)AuthorAgeFilesLines
* docs: drop the link-drop investigation writeupuser10 days2-138/+0
|
* hooks: clang-format pre-commit (changed lines only) + .clang-formatuser10 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.mduser10 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)user10 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 ircuser10 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)user10 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 spawnsuser11 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 treeuser11 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 1user11 days1-1/+5
|
* net: reject out-of-order TCP segments (accept only seq == rcv_nxt, re-ACK dups)user11 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)user11 days2-12/+17
|
* irc: keep the keyboard live during RX floods (poll input every pass)user11 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 landmineuser11 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 OSKuser11 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 clientuser11 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 inputuser11 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 recvuser11 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 nowuser11 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 spookyuser11 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 ICMPuser11 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)user11 days1-0/+190
|
* net: ping resolves hostnames (ping sl0p.foo, not just ping 1.2.3.4)user11 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)user11 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 networkinguser11 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)user11 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 ↵user11 days1-107/+0
| | | | TCP/wget)
* net: DHCP client - lease the IP at boot instead of hardcoding ituser11 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)user11 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)user11 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)user11 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)user11 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 TCPuser11 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)user11 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 userlanduser11 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)user11 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)user11 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)user12 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)user12 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 portuser12 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 gatewayuser12 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)user12 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 corruptionuser12 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.
* kernel+sh: real anonymous pipes (concurrent, streaming, SIGPIPE)user12 days11-15/+349
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | Replace the shell's temp-file pipe hack with proper in-kernel FIFOs. Kernel (src/pipe.asm, new): - A small pool of bounded ring buffers (PIPE_MAX=2, 64 B each) with writers/readers refcounts. Pipe fds are $F0+idx*2 (read) / +1 (write); $FF stays "console". - SYS_PIPE allocates one (writers=readers=1) and returns the read fd (write = read+1). getb/putb/close dispatch pipe fds here; sys_exit drops the refcounts held as PROC_STDIN/PROC_STDOUT. - Blocking with SchedYield, which is the flow control: read blocks while empty with a writer (EOF once writers hit 0), write blocks while full with a reader, and if the last reader is gone the writer is killed (SIGPIPE -> exit 141). Cooperative-scheduler friendly. Shell (c/sh.c): - run_pipeline(): split on '|', make a pipe between adjacent stages, and fork ALL stages concurrently (no wait between), wiring stdin/stdout; then wait for all. Per-stage >/< still honored; orphaned pipe ends are closed on a lookup miss so EOF/EPIPE propagate. - Drop the __pipe temp file and its 2 KB / serialized limits. libc: pipe(). New c/ptest.c exercises the FIFO (write, read back, EOF). Now works (old version couldn't): multi-stage a|b|c; streaming beyond 2 KB (count 120 | wc = 3372 bytes through a 64 B buffer); early-exit SIGPIPE (count 200 | true kills count instead of hanging/overflowing a file).
* term: compute the OSK view offset from the cursor row (keep input visible)user12 days3-17/+34
| | | | | | | | | | | | | | | | | A fixed offset of OSK_ROWS pushed the cursor off the top of the screen when there was little backlog (e.g. a fresh terminal: cursor at logical row 0 -> screen row -3, hidden). Now the offset is max(0, wCurRow - 14): 0 when the cursor is within the visible area, growing only enough to keep the cursor line at the bottom visible row (14) once the terminal has filled past it. compute_offset (from wOskVisible + wCurRow) runs at the top of term_write_tilemap; cursor_down re-runs the tilemap when the cursor changes rows while the OSK is up; osk_toggle just re-runs it. Shared OSK_DOCK constant in gbos.inc keeps term.asm and osk.asm in sync. Verified: fresh terminal + OSK shows the input at row 0; filling past row 14 shifts the view to hold the input at row 14; full screen + OSK puts it at row 14; toggle on+off is still lossless.
* term/osk: view-offset scroll region (lossless backlog on OSK toggle)user12 days2-62/+35
| | | | | | | | | | | | | | Replace the shrink-and-scroll region with a view offset. The terminal is always 18 logical rows; term_write_tilemap maps screen row -> logical row + wViewTop (rows the OSK covers clamp to the cursor line). Showing the OSK sets the offset to OSK_ROWS so the cursor line lands at row 14 (above the keyboard) and hiding it sets the offset back to 0 - now purely a remap, so nothing is scrolled off or destroyed. term_set_view replaces term_set_rows; term_scroll/cursor_down are back to plain 18-row scrolling. Result: full 18-row backlog when the OSK is hidden, a shifted 15-row window when shown, and toggling is lossless (verified: OSK-shown view == baseline rows 3..17, and toggle on+off == baseline exactly).
* term/osk: scroll region so the OSK never hides the input lineuser12 days6-13/+87
| | | | | | | | | | | | | | | | | | | The terminal now has a variable usable height (wTermRows). term_scroll and cursor_down operate within [0 .. wTermRows-1], and term_set_rows(n) shrinks or grows that region, scrolling the cursor up into view when it would fall outside. osk_toggle shrinks the terminal to the rows above the keyboard on show (18 - OSK_ROWS = 15) and restores full height on hide, so the active line is always visible just above the docked keyboard, and hiding the OSK reclaims all 18 rows. Also add c/count.c ("count [n]", default 25): prints n numbered lines to observe/debug scrolling and the scroll-region behavior. Registered as program id 21 / bank 23. Fixed the arg to read argv[0] (getargs returns the string after the command name). Verified: filling the screen then opening the OSK scrolls the latest line to row 14 (visible) with the keyboard at 15-17; closing it restores rows 15-17; count N prints exactly N lines.
* osk: bind START to Enter (CR)user12 days1-0/+7
| | | | | START now returns $0A regardless of cursor position, so you can submit a command without navigating to the CR key. The CR key still works too.
* sh: handle backspace in readline; prompt is now '#'user12 days2-2/+19
| | | | | | | | | | | | readc() returning $08/$7F now drops the last char from the command buffer (and emits one $08 so the terminal erases it) instead of storing the raw byte. Previously "lz<bksp>s" left the buffer as "lz\x08s", so the command "ls" was reported "not found" even though the screen showed "ls". Also change the shell prompt from '$' to '#'. osk: B button types the selected key SHIFTED (uppercase a-z). A types the key as shown (lowercase/digit/symbol); B on a letter subtracts 32 for the uppercase glyph (the font already has A-Z). Non-letters are unchanged.
* osk: wrap the cursor around all four edgesuser12 days1-19/+27
| | | | | | Left at column 0 wraps to the last column (and vice-versa); up at the top row wraps to the bottom (and vice-versa). Makes reaching far keys quicker - e.g. the Enter/space area is one UP away from the top row.
* osk: toggle-able on-screen keyboard on the window layeruser12 days7-7/+412
| | | | | | | | | | | | | | | | | | | | | | | | | | A joypad-driven keyboard that costs zero permanent screen space: it lives on the GB window layer, so SELECT just flips LCDC bit 5 (window enable) and the terminal's background layer underneath is never disturbed. - src/joypad.asm: read $FF00 with edge detection (wPadCur/Prev/New). - src/osk.asm: 3x20 key grid (letters, digits, space, punctuation, plus Enter/Backspace) built once into spare bank-1 VRAM tiles (104+) and laid out on the $9C00 window map, docked to the bottom 3 rows. The highlighted key is a palette swap on its window attribute byte (CGB BG palette 1 = inverted), so moving the cursor is 1-2 attribute writes with no tile rebuilding. SELECT toggles, d-pad moves, A types. - KGetc's console poll loop now polls the joypad and osk_handle each iteration; a key press returns its byte to the reader exactly like a serial byte, so the shell is oblivious to the input source. Verified in the emulator (via --keys): SELECT shows the keyboard, the highlight tracks the d-pad, and typing "ls"+Enter runs the command and lists the files; a second SELECT hides it and reclaims the full 18 rows. Known limitation: the OSK overlays the bottom 3 terminal rows, so if the prompt has scrolled to the very bottom the current input line can be hidden. A follow-up can add a scroll region (terminal uses rows 0-14 while the OSK is up). Input over serial still works unchanged.