<feed xmlns='http://www.w3.org/2005/Atom'>
<title>gbos.git/src/syscall.asm, branch master</title>
<subtitle>gbos - a tiny Unix-flavored OS for the Game Boy Color (kernel, shell, coreutils in C, RAM filesystem)</subtitle>
<id>https://git.sl0p.foo/gbos.git/atom/src/syscall.asm?h=master</id>
<link rel='self' href='https://git.sl0p.foo/gbos.git/atom/src/syscall.asm?h=master'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/'/>
<updated>2026-07-18T20:19:59Z</updated>
<entry>
<title>term+libc: batched write rendering - one tile render per write(), 2.3x lines</title>
<updated>2026-07-18T20:19:59Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-18T20:19:59Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=c2b512e803285223959e0a97601c017730d1b0bb'/>
<id>urn:sha1:c2b512e803285223959e0a97601c017730d1b0bb</id>
<content type='text'>
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.
</content>
</entry>
<entry>
<title>kernel: CGB double-speed mode - 2x everything (1.99x measured)</title>
<updated>2026-07-18T20:06:35Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-18T20:06:35Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=4425150cb6f52d15f513690a75645639358a2030'/>
<id>urn:sha1:4425150cb6f52d15f513690a75645639358a2030</id>
<content type='text'>
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 -&gt; 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.
</content>
</entry>
<entry>
<title>kernel: a real timer source (64 Hz tick IRQ) + uptime(1)</title>
<updated>2026-07-18T08:10:17Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-18T08:10:04Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=0cc9b0a8bdf774ee376937d5a6767ccdced51245'/>
<id>urn:sha1:0cc9b0a8bdf774ee376937d5a6767ccdced51245</id>
<content type='text'>
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 -&gt; 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 -&gt;
    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 &gt;&gt;6 plus bounded subtraction loops instead.
</content>
</entry>
<entry>
<title>net: SYS_POLLCON + larger console ring for link-injected input</title>
<updated>2026-07-17T18:22:34Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-17T18:22:34Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=bf647581fbad727ad22021d345d3955abf44c2f3'/>
<id>urn:sha1:bf647581fbad727ad22021d345d3955abf44c2f3</id>
<content type='text'>
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 -&gt; 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.
</content>
</entry>
<entry>
<title>net: pump the network from the console wait (answer pings at the prompt)</title>
<updated>2026-07-17T13:12:11Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-17T13:12:11Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=f68fc872755189bc287a5f7ca838dbd9f45c8353'/>
<id>urn:sha1:f68fc872755189bc287a5f7ca838dbd9f45c8353</id>
<content type='text'>
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` -&gt; 4/4 replies, routed GB1-&gt;hub-&gt;GB0-&gt;hub-&gt;GB1.

(Separate, pre-existing: gbhub *spawn* mode and windowed gbjoin can drop an idle
link socket - under investigation; daemon mode + headless is solid.)
</content>
</entry>
<entry>
<title>kernel: sys_sleep - a cooperative delay off the DIV timer (+ ping pacing)</title>
<updated>2026-07-17T11:43:10Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-17T11:43:10Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=b52f37602968768901d2dd83ccf966d953a532f0'/>
<id>urn:sha1:b52f37602968768901d2dd83ccf966d953a532f0</id>
<content type='text'>
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.
</content>
</entry>
<entry>
<title>net: keep network packets out of the console (fixes shell garbage on netboot)</title>
<updated>2026-07-17T11:29:42Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-17T11:29:42Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=e442252d72f1aab4c332a76983cd3924ce2b492b'/>
<id>urn:sha1:e442252d72f1aab4c332a76983cd3924ce2b492b</id>
<content type='text'>
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 -&gt; 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.
</content>
</entry>
<entry>
<title>net: proper socket API in the kernel (SYS_NET) - no more SLIP in userland</title>
<updated>2026-07-17T10:39:01Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-17T10:39:01Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=09ae37313a8b6d567a05a3ae886de78e88cd1afe'/>
<id>urn:sha1:09ae37313a8b6d567a05a3ae886de78e88cd1afe</id>
<content type='text'>
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=&amp;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     -&gt; replies from the real internet (kernel builds it all)
    host# ping 10.0.0.2 -&gt; 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.
</content>
</entry>
<entry>
<title>net: chat client - async receive + OSK send over the link port</title>
<updated>2026-07-16T23:39:29Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-16T23:39:29Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=c4b9a1b1c4f8c211f765a089b34b66a2d046f3be'/>
<id>urn:sha1:c4b9a1b1c4f8c211f765a089b34b66a2d046f3be</id>
<content type='text'>
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', '&lt;alice&gt; hey gameboy!', '&lt;bob&gt; 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.
</content>
</entry>
<entry>
<title>net: SLIP framing over the link port + a host gateway (echo round-trip)</title>
<updated>2026-07-16T23:28:23Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-16T23:28:23Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=b5544e84232b80351e7c58501c1632080a354d12'/>
<id>urn:sha1:b5544e84232b80351e7c58501c1632080a354d12</id>
<content type='text'>
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).
</content>
</entry>
</feed>
