<feed xmlns='http://www.w3.org/2005/Atom'>
<title>gbos.git/include/gbos.inc, 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/include/gbos.inc?h=master</id>
<link rel='self' href='https://git.sl0p.foo/gbos.git/atom/include/gbos.inc?h=master'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/'/>
<updated>2026-07-18T19:56:21Z</updated>
<entry>
<title>term: latch SCY in VBlank - fix ring-scroll shear on live displays</title>
<updated>2026-07-18T19:56:21Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-18T19:56:21Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=bea52bc8f99ee6dbcc8154a366560fc18e7b24c8'/>
<id>urn:sha1:bea52bc8f99ee6dbcc8154a366560fc18e7b24c8</id>
<content type='text'>
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).
</content>
</entry>
<entry>
<title>term: 3 more renderer wins - 10.9x scroll, 2.7x putc vs pre-cache</title>
<updated>2026-07-18T19:42:27Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-18T19:42:27Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=554fd415d34235fda6c172fc09b19278764f00c3'/>
<id>urn:sha1:554fd415d34235fda6c172fc09b19278764f00c3</id>
<content type='text'>
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-&gt;tilemap copy runs - no per-tile
   addressing or bank test.

Measured (emulator cycle counter, ANSI test screen):
  term_scroll         857k -&gt; 248k (attr cache) -&gt; 78.5k  T-cycles
  term_write_tilemap  723k -&gt; 114k              -&gt; 45.4k
  term_putc           18.2k                     -&gt; 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).
</content>
</entry>
<entry>
<title>term: 7-color terminal (per-glyph fg+bg) + ANSI escapes; colorize irc</title>
<updated>2026-07-18T18:41:27Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-18T18:41:27Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=9e7d5f6e5e01b7473b84bc3449d7f8e0f78e7045'/>
<id>urn:sha1:9e7d5f6e5e01b7473b84bc3449d7f8e0f78e7045</id>
<content type='text'>
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&lt;&lt;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.
</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>irc: a bitchx/irssi-style IRC client</title>
<updated>2026-07-17T18:22:48Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-17T18:22:48Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=c9495d4b52b245951bf4768379b38aeb1843d60f'/>
<id>urn:sha1:c9495d4b52b245951bf4768379b38aeb1843d60f</id>
<content type='text'>
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:
&lt;nick&gt; msg, &lt;nick:#c&gt; off-channel, *nick* private, -nick- notice,
* nick action, &gt;target&lt; 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.
</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: unbreak ping against hosts that intermittently drop ICMP</title>
<updated>2026-07-17T17:45:13Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-17T17:45:13Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=4f038ecad87777bc1d74f8f93f267da8cd396446'/>
<id>urn:sha1:4f038ecad87777bc1d74f8f93f267da8cd396446</id>
<content type='text'>
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 &lt;=~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&lt;-&gt;GB ping and DNS/DHCP unaffected.
</content>
</entry>
<entry>
<title>net: DHCP client - lease the IP at boot instead of hardcoding it</title>
<updated>2026-07-17T12:11:22Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-17T12:11:22Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=a2cb756693229c7880d926d2cdde2f838c697415'/>
<id>urn:sha1:a2cb756693229c7880d926d2cdde2f838c697415</id>
<content type='text'>
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 &lt;=255 (kept small on purpose) so the per-protocol datalen math
  is unchanged. wNetTx/wNetRxBuf 256-&gt;320, SK_RXBUF 208-&gt;288.

Userland:
- c/dhcp.c: DISCOVER-&gt;OFFER-&gt;REQUEST-&gt;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-&gt;: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   -&gt; 4/4 (traffic from the leased address)
</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: UDP sockets in the kernel + a DNS resolver (real hostname lookups)</title>
<updated>2026-07-17T10:54:43Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-17T10:54:43Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=26a2ccb8368a9968b6ec16068ad68dd0edb40641'/>
<id>urn:sha1:26a2ccb8368a9968b6ec16068ad68dd0edb40641</id>
<content type='text'>
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   -&gt;  example.com -&gt; 172.66.147.243

This gives us name resolution for the TCP/HTTP demo next.
</content>
</entry>
</feed>
