| 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.
|
| |
|
|
|
|
|
|
|
| |
The pattern rule listed no header dependency, so editing a constant in
include/gbos.inc rebuilt only the .asm files that had themselves changed.
Bumping MAX_SOCKS left the kernel half-built against the old value - a
socket table sized 6 in socket.asm while dmesg still printed "4 sockets" -
and the resulting misbehaviour looked exactly like a logic bug in the new
socket code, which cost real time to chase.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Reported by an outside agent (BUG-schedyield-clobbers-hl.md) while putting
gbos in a browser, where there is no link port at all. Verified here, fixed,
and re-verified.
SchedYield had two exits. The switch path preserved registers because
hSwitchTo saves and restores the full context. The "nobody else is runnable"
path was a bare `ret c` straight out of FindNextReady, which leaves HL
pointing into wProcTable (PcbPtr puts it there). Two callers - net_op_recv's
timeout loop and tcp_connect's - keep a pointer in HL across that call:
ld hl, wNetTO
inc [hl]
jr nz, .wait
call SchedYield ; HL now = &wProcTable, not &wNetTO
inc hl
inc [hl] ; stray increment into a PCB
With no DHCP server the boot-time `dhcp` spins in that loop for ~10s, and
with the shell blocked on tty input nothing else is PS_READY - so *every*
yield took the no-switch path and corrupted a byte, until a saved return
address rolled from $00xx to $01xx and RET landed on the cart entry $0100.
A clean-looking warm reboot, every ~11s, forever. It never happens with
tools/gbhub running, which is why it survived this long.
Fixed in the scheduler rather than at the two call sites, so the contract
holds for every present and future caller: SchedYield now preserves AF/BC/DE
and HL on both paths. On the switch path the pushes sit on the task's own
stack and are popped when it is rescheduled.
Evidence, with the link port unconnected (no --serial-sock; note --headless
bypasses the emulator's breakpoints, so these run on the display path):
before: breakpoint $0100 hit 5x in 40s, first at cycles=46,286,496
(matching the report), deltas ~45.5M cycles apart; HL at the
`inc hl` after the call read $C000 instead of $C9D1
after: 0 hits in 60s, HL reads $C9D1 every time, and a 60s screen watch
shows no spontaneous reboot (it reproduced at 9.9s before)
No regression: 34/34 across the tool, httpd and client suites.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
/# 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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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_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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Found by writing nc(1): a 96-byte receive buffer against a host that
answers in 200-byte segments hung the process every time.
- recv/recv_nb copied RXLEN bytes into the caller's buffer and never
looked at the caller's maxlen, so any short buffer got overrun and its
stack smashed. Deliver min(RXLEN, maxlen) and keep the tail in RXBUF
for the next call. wget only survived because its buffer (220) happens
to exceed our MSS.
- tcp_buffer_data overwrote RXBUF from offset 0 on every segment, so a
segment arriving before the app drained the previous one destroyed
unread bytes - silent, undetectable data loss. Append at RXBUF+RXLEN
instead, and drop (without advancing rcv_nxt) when it doesn't fit, so
the peer retransmits once there's room. RXLEN is one byte, so 255 is
the capacity that matters, not RXBUF's 288.
- A draining recv now clears RXLEN as well as HASRX: RXLEN is the append
offset, and a stale one made every later segment look too big to fit,
stalling the connection until the peer gave up.
- A FIN past a gap is not end-of-stream. tcp_in accepted any FIN whose
segment carried no data, without a sequence check, marking the socket
DONE and making recv report EOF at the hole - truncating a transfer
right before its last segments. Decide in-order-ness once, before
rcv_nxt moves, and re-ACK an out-of-order FIN instead.
Verified against a 6300-byte reply received while sending 831 bytes:
byte-exact (6339 = payload + banner, 303 lines, first/mid/last intact),
plus no regression in ping / nslookup / wget / irc.
|
| |
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
| |
gbdemo resolves a bare --bios to bios/sl0pboy_bios.bin when present
(else stock gbc_bios.bin). Generate the fork with the emulator's
tools/mk_sl0pboy_bios.py. The gif now opens on the SL0PBOY boot
animation - logo drop, colorize, chime - before gbos comes up.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
| |
spawn --bios shows the real power-on show (logo drop + splash) before
gbos boots; gbdemo resolves a bare --bios to the emulator's bundled
boot ROM by absolute path (the tmux pane cwd is wherever the user is).
The screen-synced waitfor absorbs the extra ~4s - only the first
prompt timeout got a bump. Regenerated with the double-speed kernel:
same demo, boots and scrolls visibly snappier.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
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 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).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The BG map's 32 rows become a ring: logical row L always lives at map
row (wMapTop + L) & 31, and SCY = (wMapTop + wViewTop)*8 places the
view. Scrolling bumps wMapTop, rebuilds the one new bottom line and
writes its single map row (map_row_one); the other 17 rows move for
free. The whole OSK view dance - shift the cursor above the keys,
restore the backlog on hide - is now term_view_update: one SCY write,
no map rewrite at all.
OSK safety: the keyboard lives on the WINDOW layer, which SCY never
moves. Stale ring rows can only appear under it: wViewTop = max(0,
wCurRow-14) <= 3 is nonzero only while the OSK is visible, and the
window covers exactly those bottom 3 rows (WX=7, WY=120). Verified:
scroll + view shift + scroll-while-OSK-up + backlog restore all
pixel-correct via VRAM screenshots; full README demo passes.
term_scroll: 78.5k -> 35.6k T-cycles (857k pre-optimization: 24x).
OSK toggle: 45k tilemap rewrite -> 212 cycles.
Regenerated demo/gbos-demo.gif on the new renderer.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
1. color_setup memoization: a call with the same (colL,colR) pair as
last time returns immediately (masks/attr still valid in WRAM).
Runs of same-colored cells - i.e. almost all text - hit this.
2. build_tile blank fast path: two spaces = bg-only planes, 8 constant
rows; term_scroll's cleared line and every blank region skip the
glyph pipeline entirely.
3. build_tile two-phase rewrite: combine both glyphs into wRowBuf with
pointers in registers (the old per-row 16-bit WRAM pointer walk was
most of the blit), then compose planes unrolled with plane-0 masks
in B/C.
4. term_write_tilemap attr pass: split each row at the pos-256 VRAM
bank boundary into two tight cache->tilemap copy runs - no per-tile
addressing or bank test.
Measured (emulator cycle counter, ANSI test screen):
term_scroll 857k -> 248k (attr cache) -> 78.5k T-cycles
term_write_tilemap 723k -> 114k -> 45.4k
term_putc 18.2k -> 6.7k
A scroll is now ~1.1 frames of guest CPU (was 12); a full 40-char line
prints in ~63ms of guest time (was ~173ms).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
term_write_tilemap's attribute pass ran color_setup - the full Fano
palette walk - for all 360 tiles on every scroll. render_tile already
computes the attr when a tile changes, so store it (COL_BUF + slot*64
+ ACACHE + tcol; the stride's 24 spare bytes were free) and the attr
pass becomes a table read. update_cursor_attr reads the cache too.
term_init now redraws before writing the tilemap so the cache is warm.
Measured entry-to-return with the emulator cycle counter, ANSI test
screen content: term_write_tilemap 723k -> 114k T-cycles (6.3x),
term_scroll 857k -> 248k (3.5x) - a scroll drops from ~12 frames of
guest CPU to ~3.5.
|
| | |
|
| |
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
| |
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.
|
| | |
|
| |
|
|
|
|
|
|
| |
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).
|
| | |
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
'/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.)
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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 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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
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).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| | |
|
| |
|
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.)
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
| |
TCP/wget)
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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")
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|