<feed xmlns='http://www.w3.org/2005/Atom'>
<title>gbos.git/src, 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?h=master</id>
<link rel='self' href='https://git.sl0p.foo/gbos.git/atom/src?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>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: SCY ring-scroll - scroll writes ONE map row; OSK toggle is 212 cycles</title>
<updated>2026-07-18T19:50:22Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-18T19:50:22Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=9b813f2a0eb8626f84489de0f55dc6da2ed22420'/>
<id>urn:sha1:9b813f2a0eb8626f84489de0f55dc6da2ed22420</id>
<content type='text'>
The BG map's 32 rows become a ring: logical row L always lives at map
row (wMapTop + L) &amp; 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) &lt;= 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 -&gt; 35.6k T-cycles (857k pre-optimization: 24x).
OSK toggle: 45k tilemap rewrite -&gt; 212 cycles.
Regenerated demo/gbos-demo.gif on the new renderer.
</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: cache tile attrs in COL_BUF's spare stride bytes (3.5x scroll)</title>
<updated>2026-07-18T19:32:16Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-18T19:32:16Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=74825c20ada40a3bf1fffb4a35f3063b7844aea6'/>
<id>urn:sha1:74825c20ada40a3bf1fffb4a35f3063b7844aea6</id>
<content type='text'>
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 -&gt; 114k T-cycles (6.3x),
term_scroll 857k -&gt; 248k (3.5x) - a scroll drops from ~12 frames of
guest CPU to ~3.5.
</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>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>refactor: c/ -&gt; usr/, compiled program blobs out of the source tree</title>
<updated>2026-07-17T22:38:38Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-17T22:38:38Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=ba2d7ae1d7b319645b10aaa31a7f664f9f80c25c'/>
<id>urn:sha1:ba2d7ae1d7b319645b10aaa31a7f664f9f80c25c</id>
<content type='text'>
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.
</content>
</entry>
</feed>
