<feed xmlns='http://www.w3.org/2005/Atom'>
<title>gbos.git/src/boot.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/boot.asm?h=master</id>
<link rel='self' href='https://git.sl0p.foo/gbos.git/atom/src/boot.asm?h=master'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/'/>
<updated>2026-07-18T20:06:35Z</updated>
<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>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>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>kernel: dmesg-style boot spew before init spawns</title>
<updated>2026-07-17T22:46:26Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-17T22:46:26Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=3fd3454212dc1d4dc396e393812833ec2e15e41a'/>
<id>urn:sha1:3fd3454212dc1d4dc396e393812833ec2e15e41a</id>
<content type='text'>
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)          &lt;- boot ROM's A/B, saved at entry
  cart: mbc5, 1M rom, 128K sram      &lt;- our own cart header ($0147-49)
  mem: 32K wram 16K vram 127B hram   &lt;- CGB constants
  proc: 8 slots, 31 programs         &lt;- link-time table sizes
  net: slip on link port, 4 sockets
  fs: gbfs v3 mounted, 120/128 blk free  &lt;- 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.
</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>kernel+sh: real anonymous pipes (concurrent, streaming, SIGPIPE)</title>
<updated>2026-07-16T23:04:16Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-16T23:04:16Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=73295e7acc1e5ec3ee8814fd4ccc45f7cee63392'/>
<id>urn:sha1:73295e7acc1e5ec3ee8814fd4ccc45f7cee63392</id>
<content type='text'>
Replace the shell's temp-file pipe hack with proper in-kernel FIFOs.

Kernel (src/pipe.asm, new):
- A small pool of bounded ring buffers (PIPE_MAX=2, 64 B each) with
  writers/readers refcounts. Pipe fds are $F0+idx*2 (read) / +1 (write);
  $FF stays "console".
- SYS_PIPE allocates one (writers=readers=1) and returns the read fd
  (write = read+1). getb/putb/close dispatch pipe fds here; sys_exit drops
  the refcounts held as PROC_STDIN/PROC_STDOUT.
- Blocking with SchedYield, which is the flow control: read blocks while
  empty with a writer (EOF once writers hit 0), write blocks while full
  with a reader, and if the last reader is gone the writer is killed
  (SIGPIPE -&gt; exit 141). Cooperative-scheduler friendly.

Shell (c/sh.c):
- run_pipeline(): split on '|', make a pipe between adjacent stages, and
  fork ALL stages concurrently (no wait between), wiring stdin/stdout;
  then wait for all. Per-stage &gt;/&lt; still honored; orphaned pipe ends are
  closed on a lookup miss so EOF/EPIPE propagate.
- Drop the __pipe temp file and its 2 KB / serialized limits.

libc: pipe(). New c/ptest.c exercises the FIFO (write, read back, EOF).

Now works (old version couldn't): multi-stage a|b|c; streaming beyond 2 KB
(count 120 | wc = 3372 bytes through a 64 B buffer); early-exit SIGPIPE
(count 200 | true kills count instead of hanging/overflowing a file).
</content>
</entry>
<entry>
<title>osk: toggle-able on-screen keyboard on the window layer</title>
<updated>2026-07-16T14:07:01Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-16T14:07:01Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=a5557c75767f7201d5df8c6f95c162044072ef10'/>
<id>urn:sha1:a5557c75767f7201d5df8c6f95c162044072ef10</id>
<content type='text'>
A joypad-driven keyboard that costs zero permanent screen space: it lives
on the GB window layer, so SELECT just flips LCDC bit 5 (window enable) and
the terminal's background layer underneath is never disturbed.

- src/joypad.asm: read $FF00 with edge detection (wPadCur/Prev/New).
- src/osk.asm: 3x20 key grid (letters, digits, space, punctuation, plus
  Enter/Backspace) built once into spare bank-1 VRAM tiles (104+) and laid
  out on the $9C00 window map, docked to the bottom 3 rows. The highlighted
  key is a palette swap on its window attribute byte (CGB BG palette 1 =
  inverted), so moving the cursor is 1-2 attribute writes with no tile
  rebuilding. SELECT toggles, d-pad moves, A types.
- KGetc's console poll loop now polls the joypad and osk_handle each
  iteration; a key press returns its byte to the reader exactly like a
  serial byte, so the shell is oblivious to the input source.

Verified in the emulator (via --keys): SELECT shows the keyboard, the
highlight tracks the d-pad, and typing "ls"+Enter runs the command and
lists the files; a second SELECT hides it and reclaims the full 18 rows.

Known limitation: the OSK overlays the bottom 3 terminal rows, so if the
prompt has scrolled to the very bottom the current input line can be
hidden. A follow-up can add a scroll region (terminal uses rows 0-14 while
the OSK is up). Input over serial still works unchanged.
</content>
</entry>
<entry>
<title>syscall: mirror console output to the LCD terminal</title>
<updated>2026-07-16T13:45:09Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-16T13:45:09Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=7274084cc07a473c530b129aa217f586781ba70c'/>
<id>urn:sha1:7274084cc07a473c530b129aa217f586781ba70c</id>
<content type='text'>
KPutc's console path now renders each byte on the LCD terminal via
term_putc in addition to the serial write. Serial is kept so scripted
tests still capture output (and for link-cable/debug), while the shell,
command output, and echoed input now appear on-screen in the 40-col font
with a live cursor and scrolling. Dropped the boot-time term_demo; the
terminal starts blank and fills from real console traffic.

term_putc now saves/forces/restores SVBK=1 so it always reads and writes
the terminal buffer/state in WRAM bank 1, regardless of which process
context KPutc is invoked from (defensive; all procs currently use bank 1).
The stack lives in non-banked WRAM, so saving SVBK across the switch is safe.

Verified: a headless run of `uname; ls; echo hello world` renders each
prompt, the echoed command, and its output on the LCD (captured via the
new `--headless --shot` path), and serial capture is unchanged.
</content>
</entry>
<entry>
<title>term: cursor + O(1) line scrolling</title>
<updated>2026-07-16T13:35:07Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-16T13:35:07Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=61c9595901b86ca60eaeb9a65db6a7ddb1309976'/>
<id>urn:sha1:61c9595901b86ca60eaeb9a65db6a7ddb1309976</id>
<content type='text'>
Rework the terminal from a static one-tile-per-screen-position model to
LINE-BOUND tiles with an assign[] indirection, so scrolling is cheap:

- Each line-slot L owns the 20 VRAM tiles at positions [L*20..L*20+19].
- assign[screen_row] -&gt; line-slot; the tilemap points each screen row at
  its slot's tiles.
- Scroll = rotate assign[], blank+rebuild only the new bottom line (20
  tiles), and rewrite the tilemap. O(1 line) instead of rebuilding all
  360 tiles.
- term_putc: printable + \n \r \b, line wrap at col 40, cursor advance
  with scroll-on-overflow.
- Cursor: an underline OR'd into the current cell's tile via wCurMask
  (no sprites/OAM); moving it just re-renders the old and new tiles.
- term_demo drives 24 lines through term_putc to exercise scroll+cursor.

Also fixes a vicious bug this shook out: build_tile advanced its 16-bit
glyph pointers with `ld hl, wGlyphL+1 / inc [hl]`, which CLOBBERS HL --
and HL is the live VRAM destination pointer. Whenever a glyph's 8 bytes
straddled a page boundary (common with long/varied lines) the next tile
bytes were written into WRAM at $D580+, corrupting wGlyphL/wVram/wCurRow/
wCurCol and freezing the display. Now the pointers are bumped via A so HL
is preserved. (Note: the emulator build had also been silently failing on
an unrelated debug edit, masking this for a while.)

Verified by screenshot: 24 lines scroll to show the last 18 + a visible
cursor at the "ready$" prompt; serial shell and filesystem still work.
</content>
</entry>
<entry>
<title>term: 40-column text terminal on the CGB LCD (4x8 dynamic tiles)</title>
<updated>2026-07-16T11:54:23Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-16T11:54:23Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=4656a1003226709e9b3c804ec32b4913fca4c7f4'/>
<id>urn:sha1:4656a1003226709e9b3c804ec32b4913fca4c7f4</id>
<content type='text'>
Add a background-tile text terminal that packs TWO 4px-wide characters
into each 8x8 tile, giving a 40x18 grid instead of the 20x18 you'd get
from an 8x8 font. The tilemap is static (one dedicated VRAM tile per
screen position); we rebuild a tile's 16 bytes from two glyphs whenever
a character changes. 360 tiles exceed the 256 a single tilemap can
address, so positions 256-359 live in VRAM bank 1 via the CGB tilemap
attribute bank-bit -- hence CGB-only.

- Switch the ROM to CGB (rgbfix -C). Safe for the FS: every process has
  PROC_WRAMB=1, so SVBK stays on bank 1 and the WRAMX FS caches don't move.
- CGB BG palette 0 = white bg / black text via BCPS/BCPD.
- src/term.asm: term_init (palette + static tilemap + clear buffer),
  build_tile (combine two 4px glyphs -&gt; one 8x8 tile, correct VRAM bank),
  term_redraw (rebuild all tiles), term_puts, term_show, term_test.
- src/font.asm: 96-glyph 3x5-in-4x8 font, generated by tools/genfont.py.
- 40x18 text buffer at $D600 (WRAMX, above the FS caches).
- tools/ppmview.py: render a --shot PPM as ASCII so the LCD is inspectable
  from the shell during development.

Verified via emulator screenshot: "GBOS TERMINAL", the alphabet, and a
bank-1 row all render legibly at 40 columns. Serial shell + filesystem
still work unchanged.

Note: a full term_redraw builds 360 tiles and takes ~8 frames; fine for
incremental single-char updates, but scrolling needs a smarter path
(next milestone). Input (no keyboard) is also still TODO.
</content>
</entry>
</feed>
