| Commit message (Collapse) | Author | Age |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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_sleep kept its entire state in three globals (wSleepTarget, wSleepAcc,
wSleepLastDiv) and yielded inside its own loop. With two processes sleeping
at once, each zeroed the other's accumulator on entry, so neither ever
reached its target: both slept ~forever.
Nothing hit it while only one program at a time paced itself. `httpd &`
(polling accept) plus a foreground `ping` is enough: ping wedged with its
echo reply already sitting in the socket buffer, unread.
Now the deadline lives in registers - i.e. on the caller's own stack across
SchedYield, which makes it per-process by construction - and it counts the
64 Hz tick (wTicks, via TimerISR) instead of accumulating DIV deltas.
ticks16 re-reads the counter until the high byte is stable, so a carry
between the two byte reads can't report a 256-unit jump.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
KEY1 prepare + STOP at KernelInit (skipped on warm reboot if already
fast). The PPU/LCD keep their normal rate; the timer and DIV double
with the CPU, compensated at the two consumers:
- TimerISR now fires at 128 Hz; a toggle byte keeps wTicks at 64 Hz
(uptime semantics unchanged)
- sys_sleep halves the DIV-delta accumulator's high byte (256 DIV
ticks = 1/128 s in double speed) so gsleep/msleep stay real-time
Measured: count 60 (scroll-heavy) 3.77s -> 1.89s wall; wTicks 64 Hz
over 4s; ping's 800ms msleep pacing unchanged (2.77s for 4 echoes).
sl0pboy already emulated KEY1/STOP + per-speed timer/PPU rates.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.)
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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 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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
KPutc's console path now renders each byte on the LCD terminal via
term_putc in addition to the serial write. Serial is kept so scripted
tests still capture output (and for link-cable/debug), while the shell,
command output, and echoed input now appear on-screen in the 40-col font
with a live cursor and scrolling. Dropped the boot-time term_demo; the
terminal starts blank and fills from real console traffic.
term_putc now saves/forces/restores SVBK=1 so it always reads and writes
the terminal buffer/state in WRAM bank 1, regardless of which process
context KPutc is invoked from (defensive; all procs currently use bank 1).
The stack lives in non-banked WRAM, so saving SVBK across the switch is safe.
Verified: a headless run of `uname; ls; echo hello world` renders each
prompt, the echoed command, and its output on the LCD (captured via the
new `--headless --shot` path), and serial capture is 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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
- 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.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
- 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
|
|
|
- fix ClearKernelRAM clobbering the kernel stack (only clear $C000-$CBFF)
- fix SyscallTrap clobbering DE (buffer arg) during table dispatch
- README: run via ~/dev/gbc --headless; note bring-up bugs
|