| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
tunbridge.py opens a TUN device (gbtun0, 10.0.0.1/24), NATs 10.0.0.0/24 out
to the internet, spawns the emulator, and bridges raw IP packets on the TUN
to/from SLIP frames on the link serial. The Game Boy (10.0.0.2) becomes a
genuine routed IP host: the host kernel routes its packets for real, so the
host's own `ping` command reaches it and netd answers.
Verified: `sudo python3 tools/tunbridge.py netd --test`
64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=1.61 ms
4 packets transmitted, 4 received, 0% packet loss
Two hard-won gotchas baked in:
- Under sudo, ~ expands to /root, so resolve EMU/ROM via $SUDO_USER's home.
- Only start feeding host->GB packets AFTER netd is running; raw packet
bytes delivered to gbos's shell (before netd claims the serial) corrupt
the shell and crash the emulator. gb->host draining starts immediately.
Requires /dev/net/tun (on Proxmox LXC: allow cgroup2 device c 10:200 rwm +
bind-mount /dev/net/tun into the container).
|
| |
|
|
|
|
|
|
|
|
| |
Refactor netd into a protocol dispatcher (IP -> ICMP/UDP) and add UDP echo:
swap addresses + ports and recompute the UDP checksum over the pseudo-header
(src/dst IP + proto + length) plus the datagram. This is the same pseudo-
header TCP uses, so it de-risks the next milestone.
tools/gateway.py --mode udp sends a datagram and verifies the echo.
Verified: 'hello udp gbos' echoes back with cksum=ok.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|