| Commit message (Collapse) | Author |
|
gbfs has carried I_NLINK since the beginning and nothing ever incremented it,
so a file could only have one name. SYS_LINK adds a second directory entry
pointing at the same inode: the data is stored once, and both names read it.
That makes rm's job different. It now decrements the link count and only frees
the inode and its blocks when the LAST name goes - removing one of two links
used to free blocks the other still pointed at.
Refused, with the reasons that matter here: linking a directory (it would make
a cycle the tree walkers cannot survive), an existing name, and anything under
/bin, which is ROM.
The syscall takes a request block because the trap needs HL for its dispatch
table - and ln copies both paths to the stack first, since that block is a
static living at $A000, on top of the command line it is reading.
|
|
/# ls -l /bin > b
/# cut -d \s -f 1 b | uniq -c
52 -
- touch FILE...: gbfs has no timestamps (an MBC5 cart has no RTC), so touch
does the half that means something here - make an empty file. O_APPEND is
exactly right: it creates, and it does not truncate what is already there.
- tr [-d] SET1 [SET2]: ranges (a-z), -d to delete, and a short SET2 padded
with its last character, like tr(1). ASCII only - a 128-entry map.
- rev, uniq [-c]: one line each, from a file or stdin like the other filters.
Two things the tests caught. A bare "-" is a SET, not a flag, so `tr . -`
has to skip flag-matching only when a letter follows the dash. And uniq
compared an empty "line" against the last run at EOF and flushed a phantom
blank one - a stray "1" in uniq -c output.
tr and cut also take \s \t \n \r escapes, because the shell has no quoting:
`-d ' '` arrives as three tokens, so a space or tab delimiter was simply
untypeable. Shell quoting would be the better fix; this makes the tools
usable today without touching the parser.
Banks 49-53; 10 program slots left in the 1 MB ROM.
|
|
The commands only existed in the kernel's program table, so the only way to
find out what the machine could do was to already know (or read the source).
They are now a directory in the tree:
/# ls / /# xxd -l 16 /bin/uname
bin 0000: cd 6b 43 06 00 0e 00 f7 .kC.....
readme 0008: 18 fe 47 0e 03 f7 c9 21 ..G....!
/# ls -l /bin | head -2
- 1 16384 worker
/bin is synthetic, not disk: dir_find resolves "bin" in the root to BIN_INO
and names inside it through the NameTable, so `ls /bin` lists exactly what the
shell can run - one table, one truth, no second list to keep in sync. The root
listing carries "bin" as a synthetic first entry so it is discoverable by
walking the tree.
A program image is inode BIN_FILE|progid: it stats as a 16 KiB file (a program
IS its ROM bank) and reads through rom_getb, which maps that bank, takes the
byte, and maps the CALLER'S text bank back before returning - the caller is
itself executing from $4000, so leaving the wrong bank mapped would return
into another program's code. Verified byte-for-byte against the host's view of
the blob. The tree is read-only: create/remove/write inside /bin all fail.
xxd grows -l (stop after N bytes), because a program image is 2048 dump lines
- more than a pipe's temp file can hold, let alone the screen.
Two traps worth remembering, both found the hard way here: `and BIN_FILE`
destroys A, so the inode must be reloaded before inode_ptr (missing it once
made ls -l stat inode 0 and call every file an empty non-directory); and ls
now copies its directory argument to the stack first, because stat()'s request
block is a static living at $A000 - on top of the command line it was reading.
|
|
Eight bytes per line rather than the usual sixteen: offset + 8 hex pairs +
the ASCII column is 38 of the terminal's 40 columns, where 16 would wrap
every line and make the dump unreadable. -c overrides it.
No -s (skip): there is no seek syscall, so it streams from the start - the
same reason tail(1) has to keep a ring.
/# xxd h
0000: 48 65 6c 6c 6f 20 67 62 Hello gb
0008: 6f 73 0d 0a os..
blk(1) could already peek at raw disk blocks; nothing could look inside a
file. Verified against known bytes, from stdin, with -c, through a pipe, and
on a missing file.
|
|
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 `>>`.
|
|
/# 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.
|
|
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.
|
|
sys_getb/sys_putb indexed inode.blocks[pos/256] with no limit at all, so a
file that grew past its pointers just kept walking: first through the 4
unused bytes at the tail of the 16-byte inode, then straight into the NEXT
inode, reading its type/nlink/size as block numbers.
It hides well. Reads and writes alias identically, so a big file can be
written and read back byte-for-byte and look fine - until something else
touches the neighbouring inode, after which the tail of the file is garbage
from whatever block those bytes now name. Found by serving a 7 KB file over
httpd: corruption began at exactly offset 3072, and only sometimes.
- NDIRECT 8 -> 12: bytes 4..15 are all block pointers now, which is what the
runaway indexing was already doing by accident. Files go to 3 KiB, no
on-disk layout change, no format bump.
- getb past the last direct block reports EOF; putb drops the byte like a
full disk. A capped file beats a corrupted neighbour.
count 250 > big (7142 bytes) now stops at 3072 and its neighbours survive.
|
|
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.
|
|
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).
|
|
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).
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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)
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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).
|
|
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.
|
|
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).
|
|
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.
|
|
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.
|
|
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.
|
|
- directories generalized: dir_find/dir_add/dir_remove take any dir inode;
every dir carries '.'/'..'. Path resolution (resolve / resolve_parent) walks
absolute or cwd-relative paths; open/mkdir/chdir/remove/opendir take paths.
- per-process cwd: PROC_CWD in the PCB (root by default, inherited on fork);
sys_chdir sets it, sys_opendir points ls at any directory.
- shell: 'cd' builtin + cwd-aware prompt; 'mkdir'/'ls <dir>' tools; 'exit'/'quit'
and EOF call poweroff() (clean shutdown via the $ED opcode -> reliable save).
- three HL/buffer-clobber bugs fixed along the way: resolve didn't preserve the
path cursor across dir_find; cur_cwd clobbered HL; resolve_parent set the final
name before resolve() overwrote wFsNameBuf (now copied after).
- verified: nested mkdir/cd/save, multi-level paths (cat docs/sub/b), and the
whole tree persists across a reboot.
- README: document directories + the poweroff opcode.
|
|
- fs.asm: a real block filesystem on the persistent block device - superblock,
block/inode bitmaps, a 32-entry inode table (type, size, 8 direct block
pointers -> files up to 2 KiB), and a root directory of 16-byte entries.
Metadata cached in WRAMX; one-block data cache streams file/dir blocks.
- same syscall interface (open/getb/putb/list/remove) -> tools unchanged;
ls now enumerates until flist() runs out (16 files, was hardcoded 8).
- old 8-slot WRAM FS removed; cart RAM partitioned: process banks 0-7, disk 8-11.
- two register-clobber bugs fixed: alloc_block/alloc_inode returned the
bitmap-block number (write_bitmap clobbers C); db_use loaded the wrong block
on a transition (db_flush->write_block clobbers C) -> multi-block files broke.
- verified: 10+ files, multi-block (300-byte) files, delete, and persistence
across reboots. Debug 'blk' disk tool retained (fill/peek).
- README: document the block filesystem.
|
|
- blk.asm: read_block/write_block move 256-byte blocks between battery-backed
cart-RAM banks (8-11, the 'disk') and a WRAM bounce buffer. Banking is hidden
in those two routines; they inline the 'restore my bank' step so they never
touch the stack while the disk bank is mapped over the process's $A000 window
(the stack lives there too -- a call/ret would rug-pull it).
- format-on-first-boot: superblock magic in block 0; otherwise the disk persists.
- cart RAM bumped to 128 KiB (-r 4); process banks 0-7, disk banks 8-11.
- debug tool 'blk fill/peek' + syscalls to validate round-trip and persistence.
- verified: round-trip incl. cross-bank (block 100 -> bank 11); block survives a
reboot via .sav; magic intact (no reformat on 2nd boot).
- old WRAM 8-slot FS still backs the tools until stages 2-3.
|
|
- sys_reap: nohang reap of a finished zombie child (-> pid or 0).
- shell: 'cmd &' launches in the background (prints [pid], no wait); reaps
finished jobs at the prompt ([pid done]). Foreground now waits for its own
child specifically (loops wait(), reporting bg completions meanwhile) so kill
reports the right pid.
- spin: a process that yields forever - a target for ps/kill.
- verified: spin &; ps shows it; kill <pid> removes just that one; immortal
init; self-finishing bg worker reaped with [pid done].
|
|
- PCB gains PROC_PROG (running program id), set by exec, inherited on fork.
- sys_kill(pid): reject pid 1 (immortal) and unknown/dead pids; mark the target
zombie, reparent its children to init, wake its parent if blocked in wait;
schedule away if a process kills itself. Reuses FindPcbByPid/ReparentToInit.
- sys_ps(slot,buf)->{pid,state,prog}; sys_progname(id) via NameTable reverse
lookup. NameTable gains sh/ps/kill.
- libc: kill/psget/progname; tools ps (pid state cmd) and kill <pid>.
- verified: ps lists sh(B)+ps(R); kill 1 refused; kill 42 fails.
|
|
- kernel I/O routing: PCB gains PROC_STDIN/PROC_STDOUT (default console $FF),
inherited across fork. KGetc/KPutc route read/write/putc to the console or a
file fd. New syscalls: putc(16), setin(17), setout(18), lookup(19).
read/write now go through the routing; putc/puts/nl use SYS_PUTC.
- sys_lookup + NameTable move command-name resolution into the kernel.
- shell rewritten in C (c/sh.c): tokenizes a line, parses > / < / |, and drives
fork+setin/setout+exec+wait. Pipes run as 'a > __pipe ; b < __pipe' (temp
file). asm shell + cmdtab removed; StrEqual/SkipName kept for sys_lookup.
- libc: fork/exec/wait/setin/setout/lookup wrappers.
- verified: echo>file, cat file, wc<file, cat readme|wc -l, echo ...|wc.
|
|
- fs.asm: 8-slot RAM filesystem in WRAMX ($D000-$DFFF), name[8]+len[2]+data[502];
syscalls open/close/getb/putb/list/remove, seeded with a 'readme' at boot.
- libc: open/close/fgetc/fputc/flist/fremove wrappers + O_READ/O_WRITE/NOFD.
- tools: ls, save (stdin->file, one line), rm; cat/wc/head now take a file arg.
- fix: syscall dispatch clobbers A, so putb takes its byte in E (was reading the
handler's low address byte, 0xDC); ls NUL-terminates 8-char names.
- verified: save/cat/ls/rm cycle; wc readme -> '2 7 38'; head -n 1 readme.
- README: document the filesystem + the A-clobber ABI note.
|
|
- c/libc.c: shared C helpers linked into every program - putu (print decimal),
atou (parse decimal), argv_parse (tokenize getargs() into argc/argv).
- wc: count lines/words/chars of stdin. head [n]: first n lines (drains rest to
EOF). args: argc/argv demo.
- pid now uses libc putu; build.sh links libc.c; Makefile CBLOBS + libc dep.
- README: document the new tools + argv_parse.
- verified: 'args one two three' -> argc=3/argv[..]; wc '2 3 16'; head 2.
|
|
- libc: add putc, puts, strlen, getargs (args string), EOF (=4) for readc.
- args: shell splits the command line at the first space ("cmd\0args\0" at
$A000, inherited by the child via fork); getargs() returns the arg string.
- shell: exit on Ctrl-D/EOF; $ prompt over a clean read loop.
- tools (c/): echo (argv), cat (stdin->stdout to EOF), uname, pid (decimal
print), true, false. Each built to a ROM-bank blob and registered.
- Makefile: CBLOBS list builds all C programs.
- README: document libc + the tool set.
|
|
- c/{gbos.h,crt0.s,libc.s,build.sh}: SDCC sm83 -> ROM-bank blob toolchain.
libc syscall wrappers save/restore BC/DE/HL around rst $30 (trap clobbers
them; SDCC expects them preserved).
- build via SDCC native asxxxx path (sdasgb/sdldgb), crt0 linked first so
_start is the $4000 entry; blob INCBIN'd into a ROM bank.
- chello.c: prints a message + getpid() -> runs as 'chello' shell command.
- Makefile: auto-build c/*.bin, track as a dep of programs.o; gitignore blobs.
- README: document the C toolchain + the SDCC --asm=rgbds codegen bug that
forced the native-toolchain approach.
- verified: '$ chello' -> 'hello from C on gbos!' / 'my pid is 2'.
|
|
- sys_read (SYS_READ): blocking console input via external-clock serial poll;
yields while waiting. Verified working (direct-echo cat: 'hi' -> 'hi').
- shell (PROG_SH): prompt/read/parse/fork+exec+wait; worker+hello programs;
StrEqual/SkipName helpers; command table.
- init now execs the shell.
- KNOWN BUG: storing the typed char to the line buffer at $A000 (cart RAM of an
exec'd program) corrupts the following sys_read; sys_write reading $A000 also
returns $3E. Kernel syscalls themselves are correct; isolating the cart-RAM /
exec RAM-bank interaction next.
|
|
- exit(): zombie + status, reparent orphans to init, wake blocked parent,
schedule away forever (resources reclaimed by the reaper)
- wait(): scan for a zombie child; reap (free cart-RAM bank + PCB slot) and
return pid/code; block PS_BLOCKED + yield if children still live; $FF if none
- cart-RAM banks are now a free list (wBankUsed bitmap): AllocRamBank/FreeRamBank
- scheduler: FindNextReady returns carry when nothing runnable; yield idles
instead of blindly picking slot 0
- helpers: FindPcbByPid, ReparentToInit
- demo: init forks 3 workers -> exec -> exit(pid) -> wait/reap => wwwR2R3R4!
verified bank recycling with 6 workers over 3 banks (w2w3w4w5w6w7)
- README: document the lifecycle + syscall status
|
|
- sys_exec(B=program id): look up ProgramTable, point PROC_ROMB at the program's
ROM text bank, map it into $4000-$7FFF, reset stack, jp to entry (no return)
- programs.asm: two demo programs (ping/pong) ORG'd at the SAME $4000 in banks
2 and 3 - proves execution is driven purely by PROC_ROMB
- demo: init forks; parent execs PROG_PING (bank2, '1'), child execs PROG_PONG
(bank3, '2') -> '1212...', 2000/2000 balanced, zero garbage
- README: document exec + text-in-ROM model
|
|
- sys_fork: switch to a kernel stack, bump-allocate a cart-RAM bank, copy the
parent's 8 KiB bank via a WRAM bounce buffer, plant a switch-in frame so the
child returns 0 to the post-fork PC, fill the child PCB (shared ROM text)
- add AllocRamBank (bump allocator) + FindFreeSlot helpers
- demo: init forks; parent prints P, child prints C (PCPC...); nested fork
gives three distinct pids (123...)
- README: document the fork mechanism, mark syscall status
|