aboutsummaryrefslogtreecommitdiffstats
path: root/usr (follow)
Commit message (Collapse)AuthorAge
* kernel: stat/fsstat/isatty syscalls, and O_APPENDuser2026-09-24
| | | | | | | | | | | | | | | | | | | Four things userland had no way to ask for: - SYS_STAT: type, link count, size and inode for a path. The trap needs HL for its dispatch table, so a call taking both a path and an output buffer passes a request block - the shape SYS_NET already established. usr/stat.h wraps it, header-only like sock.h, because the block must live in the caller's RAM. - SYS_FSSTAT: blocks/inodes, total and free. The bitmaps are already cached write-through in fs_bmbuf, so this is the popcount boot_fs already prints. - SYS_ISATTY: is this process's stdin the console? The shell needs it to tell an interactive session from a script. - O_APPEND: open a file positioned at EOF. There is no seek syscall, so this is the only way to add to a file - without it nothing running ON the Game Boy can build a multi-line file, shell scripts included. Together these back ls -l, df, and `>>`.
* usr: ircd - an IRC server running on the Game Boyuser2026-09-22
| | | | | | | | | | | | | | | | | | | | | | | | | /# ircd & host$ irssi -c 10.0.0.2 /join #gbos other GB$ irc 10.0.0.2 gb2 /join #gbos Implements the subset a real client needs to get in and talk: NICK, USER, PING/PONG, JOIN, PART, PRIVMSG/NOTICE (to a channel or to a nick), NAMES and QUIT, with the 001/004/375/372/376 numerics clients wait for on registration, 353/366 on join, and 421 for anything else. One channel per client keeps the bookkeeping in fixed-size tables; this is a handheld with 8 KiB of task RAM. MAXCL is 3, bounded by the kernel socket table (listener + client each take a slot, the rest left for other programs). A fourth client is refused and the daemon keeps serving - which is how the ~30s close() stall turned up. Every event is logged to the LCD, so the Game Boy shows its own server traffic: "+ alice", "alice joined", "<alice> hello". Verified with real clients over the hub: two and three simultaneous hosts registering, joining, channel relay (sender not echoed to itself), private messages by nick, PING/PONG, unknown-command numerics, QUIT propagation, the overflow refusal, and a second Game Boy running gbos's own irc(1) in the same channel as a laptop client - messages crossing both ways between them.
* net: one socket per client - four-tuple demux, spawning accept, bounded closeuser2026-09-22
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | A server could only ever hold ONE connection, which makes an ircd pointless: listen() turned the listener into the connection, and net_find_tcp demuxed on local port alone, so two clients on 6667 were indistinguishable. - net_find_tcp now matches an established socket on the full four-tuple (local port + peer IP + peer port) and only falls back to a LISTENing socket when none matches. - A SYN at a listener no longer consumes it: tcp_spawn_conn clones a new socket (local port + owning pid, so exit/kill still reclaims it) and the listener keeps listening. accept() returns that socket; closing it leaves the listener alone. A full table drops the SYN, and the client's retransmit is taken once a slot frees. - tcp_peer_is_current preserves BC: net_find_tcp scans with the socket index there, and the compare needs BC for the rx buffer base. Without this the scan died at the first mismatching socket, so a second client's handshake ACK never reached its socket and it hung in SYNRCVD - tcpdump showed our SYN+ACK going out and the client's NICK retransmitted five times into silence. - listen() only conflicts with another LISTENER on the port; connections share it by design now. - MAX_SOCKS 4 -> 6 (listener + 3 clients + 2 spare; 537 bytes of WRAM0 still free), and SK_ACC marks a spawned connection as not yet accepted. tcp_close's settle is also time-boxed to ~250 ms of real time. It counted PUMPS (8 x 8000), which is ~30s of wall clock when the peer never answers - invisible with well-behaved peers, but a daemon refusing a client that stayed connected froze itself, and every other client with it, for half a minute. httpd keeps its listener open across requests now, so the next client's SYN is accepted immediately instead of being dropped between close and re-listen: three parallel clients go 0.1/1.1/2.1s -> 0.4/1.0/0.1s.
* usr: httpd - the Game Boy serves its filesystem over HTTPuser2026-09-22
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | /# httpd & host$ curl http://10.0.0.2/ -> a listing of the cart RAM host$ curl http://10.0.0.2/readme -> the file, from battery-backed SRAM GET only. "/" serves /index.html if present, else a generated listing; any directory lists with every entry linked, so you can click around a 1997 handheld's disk from a browser. Content type from the extension. No Content-Length: HTTP/1.0 + Connection: close delimits the body by closing, which suits a stack with one in-flight segment. All output goes through one 200-byte buffer, so each net_send is a full segment instead of one tiny segment per string. It drains the whole request, not just the request line. That is not politeness: leaving the rest unread means those segments are never acknowledged, so the client retransmits headers for a minute, never sends its FIN, and its dead connection keeps arriving at port 80 long after we re-listen. One curl with 1.4 KB of headers stalled every following request for 30-120s. It deliberately does NOT read the keyboard either. Backgrounded - the useful way to run it - every key it polled would be a key stolen from the shell's prompt. It is a daemon: kill(1) stops it, and the kernel reclaims its socket. Verified with curl: index, byte-exact file bodies, nested paths, directory listings, 404, 501, a 1.4 KB request spanning segments, browser user-agents, three parallel clients (0.1/1.1/2.1s), the shell staying usable alongside it, and six kill/restart cycles - more than there are sockets.
* net: passive open (listen/accept) + own sockets + close promptlyuser2026-09-22
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The stack could only ever dial out. Two new ops give it an ear: - listen(sock, port): sockets demux on LOCAL PORT alone, so the listener IS the connection - the SYN fills in the peer and the socket becomes the connection. One at a time, re-armed by listening again after close, which is all a 4-socket table can honestly promise. A second client's SYN is dropped in silence; its retransmit is taken once we are free (~1s), so three parallel clients complete in 0.1/1.1/2.1s. - accept(sock): one RX pump per call, never blocks (like recv_nb), so a server keeps its own main loop and stays killable. - TCP_LISTEN/TCP_SYNRCVD in tcp_in: answer a SYN with SYN+ACK, become established on the handshake's ACK, and process any data that ACK carries. tcp_peer_is_current tells a peer's retransmitted SYN from a newcomer's. Without it, "any SYN in SYNRCVD is a retransmission" let a second client *hijack* a connection mid-request: the first client got its SYN+ACK, sent its GET twice, and was never answered (found in a tcpdump of two parallel curls). listen() also refuses a port another socket already holds - two sockets on one port silently black-hole the second listener, since demux takes the first match. Sockets now record the pid that opened them (SK_OWNER) and exit *and* kill release them: with 4 slots, a few killed irc/nc/httpd runs used to leave the box unable to open a socket at all until reboot. tcp_close stops settling the moment the peer's FIN/ACK arrives instead of burning its whole pump budget. It otherwise sits in FINWAIT for about a second, and a server that closes and re-listens drops the next client's SYN in that window: every request after the first paid a SYN retransmit. 1.1s -> 0.1s per request.
* usr: grep, more, cp, nc - and make pollcon pump the linkuser2026-09-22
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | The shell has had pipes for a while with nothing to pipe *through*, and an 18-row screen with no way to stop output scrolling off it. - grep [-vin] PAT [file]: substring match (no regex), stdin or a file, grep(1)'s exit convention. The other end of the pipe, at last. - more [file]: pages 17 lines at a time, --More-- bar in white-on-blue, any key pages, q quits. Counts *screen* rows, so wrapped lines pay their real cost. Keys come from pollin/pollcon, not stdin, so `cat big | more` still has a keyboard. - cp SRC DST: the fs has rm/mkdir but no way to duplicate a file. mv is this plus rm (there is no rename). Rejects SRC == DST, which would otherwise truncate the source via O_WRITE before reading a byte. - nc [-s] HOST PORT: raw TCP. Interactive (START sends the line with a CRLF, /q quits) or -s to pump stdin and print the reply with its own wall-clock idle timeout, because the kernel's blocking recv counts pumps, not seconds. Generalizes what chat/irc hardcode: `nc towel.blinkenlights.nl 23` works. sys_pollcon now pumps the link before draining the console ring. The ring is filled *by* net_pump, so a program that never touches the network polled a ring nothing would ever fill: host-injected keys (gbtype/gbdemo) hung `more` forever, and the Game Boy stopped answering pings for as long as it ran. KGetc already pumps for this reason. New tools take banks 35-38. Locals over statics in all four: a program's _DATA starts at $A000, where the shell leaves the command line.
* sh: background dhcp at boot so the prompt is never blockeduser2026-09-22
| | | | | | | | dhcp's recv times out only after ~250*65536 pumps x3 retries, so fork()+wait() left the screen stuck at 'dhcp: discovering' with no prompt whenever no DHCP server answered (raw emulator, no bridge). Fork without waiting: the prompt is immediate, the lease (or 'no offer') lands a moment later, and the job is reaped by the main loop.
* term+libc: batched write rendering - one tile render per write(), 2.3x linesuser2026-07-18
| | | | | | | | | | | | | | | | | 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.
* term: 7-color terminal (per-glyph fg+bg) + ANSI escapes; colorize ircuser2026-07-18
| | | | | | | | | | | | | 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)user2026-07-18
| | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* refactor: c/ -> usr/, compiled program blobs out of the source treeuser2026-07-18
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.