[ BOOTING DECA-TINY-OS · KERNEL_SECTORS EQU 51 · DECA PAINT · GFX SDK · DOOM+SFX+MUSIC · PS/2 MOUSE · OPL2 MIDI · PMODE IRQ · LUA 5.4 · SMS · FAT16 32M HDD ]

Building an OS
from scratch.

A Real-Mode x86 Journey · 16-bit · 640K · FAT12 + FAT16 · C + NASM

What happens when you start with a blank NASM file and don't stop until you have a bootloader, a dual-format filesystem, a shell, a BASIC interpreter, a DOS/4GW-style protected-mode extender, a freestanding libc, a 32 MB FAT16 hard disk that boots the same kernel as the floppy, id Software's Doom playing all nine Episode 1 maps with SFX and OPL2 music ("At Doom's Gate" on E1M1), a working Lua 5.4 interpreter with real libm, a Sega Master System emulator playing Black Belt and Street Fighter 2 with PSG music, a complete Sound Blaster 16 + OPL2 FM + PC speaker audio stack, a pmode IRQ infrastructure (PIC remap + IDT + IRQ-driven SB16 streaming), a PS/2 mouse driver, a MIDI/MUS playback library that synthesises through OPL2, a reusable Graphics SDK (lines/circles/polygons/text/PCX), and Deca Paint v1 — a productivity-grade Deluxe-Paint-style drawing app with 8 tools, 16-color palette, flood-fill, undo, and cross-app PCX save/load — all from the shell? Follow along.

47 Self-tests pass
51 Kernel sectors
54+ External apps
9 Doom E1 maps
5.4 Lua version
16 IRQ vectors
27 Smoke markers
▶ Run the OS (Floppy) ▶ Boot from HDD Read the story See the timeline →
qemu-system-i386 · COM1 · Deca-Tiny-OS
Deca-Tiny-OS Bootloader · drive 0x80 · Geometry: 65c/16h/63s
Loading kernel: ................................... OK (51 sectors)
BPB parsed: FAT16 · 32 MB volume · 64991 clusters · root@LBA 513
BIOS conventional memory: 639 KiB · top 9FC0:0000
CPU level: 6 · Extended memory: 130048 KiB via INT 15h E820
A20: enabled via BIOS INT 15h AX=2401h
SELFTEST start
· memory detection ........... OK
· high runtime buffers ........ OK
· FAT format dispatcher ....... OK
· FAT16 cluster walk .......... OK
· root sector cache ........... OK
· app return trampoline ....... OK
· GDT + pmode round-trip ...... OK
· pmode app loader ............ OK
· pmem bump allocator ......... OK
· int 60h lseek ............... OK
· 32-bit handle lseek ......... OK
· thunk IF-preserve .......... OK
· 8259 PIC remap state ........ OK
· IDT loaded on pmode entry ... OK
· IRQ trampoline installed .... OK
· mouse drain stub @ IDT[0x2C] OK
SELFTEST pass=47 fail=0

Deca-Tiny-OS v0.51 · Real Mode + Pmode Extender · FAT16 HDD · DOOM+MUSIC · MOUSE · MIDI · GFX SDK · DPAINT
Conventional RAM top: 9FC0:0000 · Pmode pool: 0x200000+ (16 MiB cap)
Apps: FAT16 root, load 2000:0100, buffers 7000:9000+

] help
Shell built-ins: dir, cd, pwd, mkdir, df, stat, type,
touch, append, copy, del, ren, batch, status, where...
External apps: basic, edit, hexedit, more, head, wc, find,
pmhello, pmcat, pmsieve, pmbounce, pmpong,
chello, clibtest, cprintf, cwrite...

] clibtest
string/ctype/setjmp/malloc/stdio/qsort fixtures...
LIBTEST C PASS

] cwrite
→ fopen("CWTEST.TXT","w") · fwrite · fopen "a" · remove
CWRITE PASS

] pmpong
→ mode 13h, player vs AI, first to 7 wins
PMODE app returned.

] doom
→ DOOM 1.9 · loading DOOM1.WAD (3.6 MB / 1151 lumps)
→ SB16 DSP 4.05 · 4-ch mixer @ 11025 Hz · DMX SFX wired
→ OPL2 GM-subset synth · D_E1M1 → "At Doom's Gate" AUDIBLE
→ E1M1 Hangar · pistol + door + monster grunts AUDIBLE
PMODE app returned.

] lua -e "print(2^10, math.sqrt(2))"
1024.000000 1.414214

] sms bb.sms
→ SMS Plus GX · Z80 @ 3.58 MHz · TI VDP · 256×192 letterbox
→ SB16 PCM @ 44100 Hz · double-buffered async DMA
→ Black Belt · PSG TONE MUSIC AUDIBLE · Esc quits
PMODE app returned.

] bell
→ PC speaker · PIT counter 2 + port 0x61 · 5-note chime
BELL PASS

] fmtest
→ Yamaha OPL2 @ 0x388 · 4 voices · C-major arpeggio
FM PASS

] irqping
→ PIC remap · IDT loaded · IRQ 0 trampoline @ IDT[0x20]
→ PIT 18.2 Hz handler · 1-sec window · counter = 18
IRQPING PASS

] pcmstrm2
→ SB16 auto-init DMA · IRQ 5 refill callback · main loop idle
→ 5-second sweep · no polling · STREAM IRQ PASS
STREAM IRQ PASS

] mousetst
→ 8042 aux init · 3-byte PS/2 packet state machine
→ IRQ 12 packets=64 · dx=+27 dy=-14 · btn=LEFT
MOUSE PASS

] musictst hello.mid
→ SMF parser · 9-voice OPL2 GM-subset bank · polled tick
→ MUSIC INIT PASS · MUSIC LOAD PASS · MUSIC PLAY PASS
MUSIC PASS

] gfxtest
→ mode 13h · Bresenham + midpoint circle + convex polyfill
→ 8x8 BIOS font · cursor save/restore · PCX round-trip
GFX MODE/PRIMS/FONT/CURSOR/PCX PASS · GFX PASS

] gfxtest mouse
→ cursor follows pointer · LMB-drag draws Bresenham line
→ ESC → gfx_pcx_save("gfxdemo.pcx", 320, 200, palette, fb)
PMODE app returned.

] dpaint PAINTTUT.PCX
→ DECA PAINT v1 · tool palette + 16 colors + status bar
→ 8 tools (P L R r C c B E) · flood-fill · Z undo · S save
→ Loaded PAINTTUT.PCX · click+drag to draw · ESC quits
PMODE app returned.

] dpaint selftest
→ DPAINT INIT/TOOLS/UI/UNDO/SAVE PASS
DPAINT PASS

]

Why build an OS
from nothing?

Most people who learn programming never go lower than Python. Some go as far as C. A very few reach assembly. And then there's this project: start with a blank NASM file, a QEMU instance, and absolutely no shortcuts.

Deca-Tiny-OS is a real-mode x86 operating system whose kernel is hand-written assembly, which now also hosts a freestanding C runtime for protected-mode apps. It boots on real hardware and on QEMU, speaks both FAT12 (1.44 MB floppy) and FAT16 (32 MB hard disk) from the same kernel binary, runs external .COM apps written in either NASM or C, features a CP/M-inspired shell, and even ships a tiny BASIC interpreter — all within the classic 640K conventional memory envelope, with a 16 MiB extended-memory pool reserved for pmode apps.

"The kernel is still hand-written assembly. There's no libc under it — just the BIOS, the bare metal, and NASM. The libc lives above it, for the C apps that ride the pmode-extender ABI."

The project began with the most fundamental question in systems programming: how does a computer even start? The answer is a 512-byte boot sector, a master boot record that fits on the first sector of a disk, loaded by the BIOS at 0000:7C00. From that single sector, everything else follows.

The first working milestone was nothing glamorous: dots printing to the screen as the bootloader loaded the kernel sector by sector. The second was a flickering prompt. The third was dir actually listing files. But each of those milestones had to be earned — through BIOS geometry queries, CHS conversion bugs, FAT12 byte-order surprises, and register clobbers that crashed everything silently. Every bug is documented. Every fix is recorded.

This is a build-in-public story. The dev log runs to dozens of entries. Every feature slice has regression evidence: a SHA-256 image hash, a QEMU serial capture, a FAT cluster leak check. When it breaks, we fix it and write it down. That's the whole game.


Rules we follow
ruthlessly

Every feature slice must have: a build that passes, a QEMU smoke test that passes, a regression check that produces matching PowerShell and Python image hashes, and a dev log entry that records what changed and why. Nothing ships without evidence.

No silent regressions. When the bootloader sector count changes, the build fails at compile time. When a FAT cluster leaks, the regression script catches it. When an app overwrites the kernel, the memory guard fires. The system polices itself so that future work can be done with confidence.

boot.asm — sector gate that enforces build integrity x86 NASM
; Bootloader enforces kernel size at build time. ; If kernel.bin grows past this sector count, ; the image builder refuses to produce the image. kernel_sectors equ 51 kernel_start_lba equ 128 ; Load kernel from LBA 128, one sector at a time, ; with BIOS geometry query and CHS conversion. kernel_load_loop: call lba_to_chs mov ah, 0x02 mov al, 1 int 0x13 jc disk_retry

From blank file
to full OS

Every milestone earned through real debugging, real regressions caught, and real fixes documented. No skipping steps.

April 27, 2026 · Week 1
Boot FAT12

The Very First Boot

The journey started with a 512-byte boot sector and a kernel that printed dots to prove it was loading. Those dots were everything — each one meant a sector had crossed the BIOS disk I/O layer correctly. Getting even that working required BIOS geometry queries (int 13h ah=08h), proper LBA-to-CHS conversion, and a retry loop because early drafts corrupted their own loop counter mid-read.

The biggest early surprise: FAT12 stores bytes-per-sector as 00 02 in little-endian, but the initial validation code checked only the low byte (00) and decided every valid disk was invalid. Classic. Every shell command reported "filesystem unavailable" for days until that single-byte comparison bug was spotted.

Kernel size: 7 sectors Selftest: pass=8 fail=0 Apps: HELLO.COM, INFO.COM
April 27–28, 2026 · Week 1
Shell API

CP/M Shell & the int 60h API

With a kernel that could boot and list files, the next step was making it actually useful. A CP/M-inspired shell was born: table-driven command dispatch, case-insensitive matching, external app fallback, and VGA text scrolling so long output didn't wrap to the top and eat itself.

The int 60h kernel API was designed around this era: print string, read line, DMA buffer selection, file open/read/close. Apps don't touch kernel internals — they call a software interrupt and get a clean interface back. That design decision has paid dividends ever since. Every app written since then works the same way.

apps/hello.asm — classic first app
; Every app is a flat .COM binary loaded at 2000:0100 org APP_LOAD_OFFSET ; from memory_map.inc mov si, message mov ah, 0x00 ; int 60h: print string int 0x60 ret ; return to shell message: db 'Hello from Deca-Tiny-OS!', 13, 10, 0
API services: 12 defined VGA: Text scrolling
April 29, 2026 · Week 1
FAT12 Writes Write Safety

Writing to Disk: The Hard Part

Reading from a FAT12 disk is satisfying. Writing to it is terrifying. One wrong byte in the FAT and you've corrupted the entire filesystem. The approach taken here: build comprehensive rollback from day one, and stress-test every failure path before building on top of it.

The diskfull diagnostic fills every data cluster until the disk overflows, verifying that allocation rollback correctly releases partial chains. The rootfull diagnostic saturates all 224 root directory entries. The wrterr diagnostic injects simulated disk-write failures via a kernel countdown mechanism and verifies that no bytes leak.

The most insidious bug from this era: the FAT release function was called with the start cluster in AX, then immediately called fs_load_metadata which clobbered AX to 19. Every delete silently freed cluster 19 instead of the requested chain — which also happened to be where GFXDEMO.COM lived. That one took a while.

Write ops: create, delete, append, rename Rollback paths: data, FAT, root Diagnostics: diskfull, rootfull, wrterr, wrtfail
April 29–30, 2026 · Week 1–2
BASIC Language

Writing a BASIC Interpreter in Assembly

Here's a question that sounds absurd: can you write a BASIC interpreter in 16-bit x86 assembly, fit it in a 8KB .COM file, and have it actually be useful? The answer turned out to be yes — if you're disciplined about scope.

The interpreter was built in twelve slices: tokenizer first, then a REPL skeleton, then expression parsing, variables, input, program storage, execution, conditionals, loops, subroutines, and finally file I/O. Each slice had its own smoke test. Each slice produced a working artifact. Nothing was left in a half-finished state.

The end result: basic DEMO.BAS RUN loads a BASIC program from disk, runs it, handles loops and subroutines and user input, and returns cleanly to the shell. FOR/NEXT, GOSUB/ RETURN, IF...THEN, SAVE/LOAD — all working. All within 8KB.

samples/SUBS.BAS — bundled BASIC demo
10 PRINT "SUBS" 20 FOR I=1 TO 3 30 GOSUB 100 40 NEXT I 50 PRINT "DONE" 60 END 100 PRINT I 110 RETURN → Output: SUBS 1 2 3 DONE
BASIC.COM size: ~8KB Variables: A–Z (16-bit signed) Selftest: pass=21 fail=0
May 1, 2026 · Week 2
File Tools Text Tools

Building a Toolkit: Text Tools & File Management

An OS isn't useful if you can only run one thing at a time. The toolkit phase built out a full suite of Unix-inspired text tools — all as external .COM apps using the kernel's handle-based streaming API.

more: a paged text viewer with keyboard controls. head: shows the first N lines of a file. wc: counts bytes, lines, and words. find: case-sensitive literal search through a file. All of these stream through the file handle API — no whole-file loading, no 8KB buffer limits. They work on any size file.

The shell also grew: touch for zero-byte file creation, append file text for LF-terminated text appends from the command line, copy /overwrite with explicit overwrite semantics (default copy is always safe), stat for file metadata, df for disk usage. Every command with clear, specific error messages.

Shell commands: df, stat, touch, append, copy/overwrite Text tools: more, head, wc, find Selftest: pass=24 fail=0
May 2, 2026 · Week 2
640K Memory Segments

640K: Moving Everything Out of Segment Zero

The first 64KB of memory is precious real estate in a real-mode system. Bootloader at 0000:7C00. Interrupt vectors at 0000:0000. BIOS data. Kernel code. FAT buffers. File buffers. App load window. Stack. All competing for the same cramped space. Something had to give.

The 640K preparation arc was a methodical ten-slice migration. First, detect conventional memory via BIOS int 12h. Then centralize memory constants in kernel/memory_map.inc. Then add helper routines for far-pointer buffer operations. Then — one by one — move the disk buffer, FAT buffer, root directory buffer, and file staging buffer into high conventional memory at segment 7000h.

The payoff: apps now load at 2000:0100 with 36KB of app window. The kernel has 35KB of headroom before the app region. Runtime buffers live at 7000:9000+. Everything is segment-aware. The system is ready for the full 640KB address space.

App window: 2000:0100 → 36KB Buffer segment: 7000:9000+ Kernel region limit: 0000:E000 Selftest: pass=30 fail=0
May 2–3, 2026 · Week 2
Pmode Extender DOS/4GW-style

Protected Mode, Without Leaving Real Mode

The kernel still lives in 16-bit real mode. But individual apps now have the option to run as 32-bit protected-mode programs, with access to the full 4 GiB linear address space and a 16 MiB extended-memory bump pool. The classic DOS/4GW playbook — and a 4-epic, 13-slice arc to land it.

Prerequisites first: CPU level detection (NT-bit flip for ≥386, CPUID for ≥586), an INT 15h E820 → E801 → AH=88h fallback chain for extended-memory discovery, and a four-step A20 enable (BIOS INT 15h AX=2401h → fast A20 via port 0x92 → KBC via 0x60/0x64), each verified with a wraparound check at 0xFFFF:0x0510.

Then the launcher: a 5-entry GDT (null, code32/data32, code16/data16), a CR0.PE flip behind a far-jmp, a pmode32 round-trip selftest that writes 0xDECA32 at linear 0x100000 and reads it back, and finally program_launch_pmode_app — recognises any .com file whose first 8 bytes are 'DECA32',0,0, copies it to 0x100000, mode-switches in, and returns through a pre-pushed exit thunk. Pmode apps reach kernel services via a syscall thunk that bounce-buffers strings through 7000:C000, dispatches int 60h in real mode, and preserves all eight 32-bit GP regs across the call with pushad/popad.

Pmode apps: pmhello, pmcat, pmsieve Extended-mem pool: 16 MiB @ 0x200000 Selftest: pass=40 fail=0
May 3, 2026 · Week 2
Game Mode 13h

pmpong: A Playable Pong on the Pmode Extender

With the pmode extender in place, the next obvious question: can a 32-bit app actually drive VGA mode 13h with playable input and frame pacing, end-to-end, with zero kernel changes? Answer: yes — first as pmbounce (a 591-byte bouncing-ball proof), then as a full 8-slice / 3-epic Pong arc.

pmpong is player vs AI: UP/DOWN moves the left paddle, the right paddle tracks the ball with a 4-pixel deadzone at half the player's speed (so the AI is deliberately beatable), AABB collision keeps the ball out of paddles, top/bottom walls bounce, balls past either side score and re-serve toward whoever scored. A 5×7 bitmap font scaled 4× renders the score; first to 7 freezes the playfield with the final scoreboard until ESC quits.

Direct VGA writes to 0xA0000, port-0x60 keyboard polling (the masked-IRQ BIOS keyboard buffer is unreliable in pmode), and PIT-based ~30 fps frame pacing — the same primitives proven by pmbounce. Notable: across the entire 8-slice arc, the kernel binary stayed at exactly 21767 B / 43 sectors — the pmode-extender ABI absorbed every requirement, no new int 60h services and no new thunk dispatch cases. Pre-merge bug count for the arc: zero.

pmpong.com: 1569 bytes Win condition: first to 7 Frame pacing: ~30 fps via PIT Selftest: pass=40 fail=0
May 4–5, 2026 · Week 3
C Toolchain libc

A Freestanding libc for Pmode Apps

The pmode-extender ABI was already C-runtime-shaped — flat 32-bit segments, a function-pointer-style syscall thunk in a kinfo struct, register preservation across the call. The C toolchain arc made that potential concrete: a .c file in apps/c/<name>/ now becomes a .COM binary identifiable by the same 'DECA32',0,0 magic as hand-written NASM pmode apps, compiled by i686-elf-gcc 13.2.0, linked at flat 0x100000 via a small pmapp.ld, and dead-stripped to per-function granularity through -ffunction-sections + --gc-sections.

~50 libc functions across six headers, all hand-rolled. <string.h> mem*/str* family. <ctype.h> table-driven predicates. <stdlib.h> with a real freelist malloc over a 1 MiB arena drawn from the pmem bump pool, plus qsort and exit/abort. <setjmp.h> in ~50 lines of NASM (System V i386 jmp_buf layout — six dwords). <stdio.h> with a 270-line vsnprintf core feeding printf / fopen / fread / fwrite / fseek / remove. <time.h> via direct PIT polling and CMOS RTC reads. The libc never reaches around apps/c/lib/pm_syscall.h — DIP held; porting to a different host platform is a one-file swap.

Five C apps shipped: cstub (empty smoke), chello (494 B), clibtest (11.2 KB — exercises the entire libc surface), cprintf (3.6 KB — formats SAMPLES.TXT with line numbers), and cwrite (4.5 KB — fwrite/append/remove round-trip). One new int 60h opcode (AH=0x19 lseek) and six new pmode thunk dispatch cases. Real-mode apps and the existing pmode NASM apps were untouched — the arc was purely additive.

apps/c/chello/chello.c — first C app on the pmode extender
#include "pm_syscall.h" #include <stdio.h> int main(int argc, char **argv) { (void)argc; (void)argv; printf("CHELLO PASS\n"); return 0; } // $ chello → CHELLO PASS · returns to shell
C apps: chello, cstub, clibtest, cprintf, cwrite libc: ~50 fns / 6 headers Toolchain: i686-elf-gcc 13.2.0 Selftest: pass=41 fail=0
May 4–5, 2026 · Week 3
FAT16 HDD Boot 32 MB

HDD + FAT16: Same Kernel, Two Image Flavours

The 1.44 MB floppy ceiling lifts. A new os-hdd.img (32 MB, FAT16) now boots the same kernel binary as the floppy, with the format detected at runtime from the on-disk BPB. No recompilation, no per-flavour kernels — every layout constant the FS layer used to hardcode (FAT LBA, sectors-per-FAT, root LBA, root entry count, cluster→LBA base, cluster cap, end-of-chain marker) is now read from the BPB at boot.

Structurally the biggest single arc in the project. kernel/fs.asm split into a format-agnostic dispatcher plus a new kernel/fs_fat12.asm (8 routines, ~225 lines, lifted verbatim from fs.asm and renamed) and a fresh kernel/fs_fat16.asm (6 routines, ~250 lines). Six dispatcher entry points route by [fs_format]; kernel/api.asm callers don't know which format is mounted. FAT16's 130 KB FAT can't fit in the conventional-memory FAT buffer — so a one-sector-at-a-time FAT cache pages on demand, and a parallel root-directory sector cache handles the FAT16 32-sector root.

The single most fun quirk: the kernel sits at LBA 128, which lands inside FAT1 on the 32 MB image. Writing the kernel to disk overwrites FAT1 sectors 127..173. The build script caps allocation at MAX_SAFE_CLUSTER=32000 so no app's cluster lookup ever reads a corrupted FAT1 entry; FAT2 stays clean and serves as the source of truth for the leak check. Both PowerShell and Python builders agree byte-identically on both image kinds; both regression and smoke gate both flavours end-to-end.

Kernel: 23697 B / 47 sectors FAT16 image: 32 MB / 64991 clusters Dispatchers: 6 · routes by [fs_format] Selftest: pass=41 fail=0 (both kinds)
May 5, 2026 · Mini-slice
Shell polish Format-aware

where Tells the Truth About the Filesystem

Caught while exploring the freshly-merged FAT16 HDD image: the where shell command was happily printing Apps: FAT12 root, ... even when the kernel had auto-detected and mounted FAT16 from the BPB. The string was a single hardcoded literal back when FAT12 was the only option. Twelve lines of asm in kernel/memory.asm later, where branches on [fs_format] and prints the right label — FAT12 floppy still says FAT12 root, FAT16 HDD now says FAT16 root, and the smoke harness's existing FAT12 markers continue to match without modification.

Kernel: 23754 B / 47 sectors Selftest: pass=41 fail=0
May 6, 2026 · Mini-arc (3 slices)
FS Kernel ABI

32-bit File API: Lifting the 64 KiB Position Cap

Slice D5.1 wired the WAD-bundling pipeline, then immediately surfaced a hard blocker for the Doom port: the kernel's per-handle file size and position were both 16-bit. Any fseek past 65535 truncated; a multi-megabyte DOOM1.WAD was unreachable through libc stdio. The 32-bit File API mini-arc opened, lifted, and closed in a single day — three slices (F0 doc kickoff, F1 atomic implementation, F2 arc-close docs).

F1 widened api_handle_size / api_handle_pos from word arrays to dword arrays, lifted api_return_cx to a dword, rewrote api_open_handle_slot to read all 4 bytes of the FAT directory size field, rewrote api_lseek_handle to take a signed 32-bit offset packed in DX:CX, dropped the legacy cmp word [es:si+30], 0 append-rejection that capped append targets at 64 KiB, and lifted fseek's 32767 cap in libc. Six call sites updated for the new fs_create_root_entry signature; one new selftest opens DOOM.COM and seeks past 64 KiB to prove the path. Behaviour-preserving for the 13 existing apps — they all live below 64 KiB and the 32-bit math reduces to the old 16-bit behaviour for files that small by construction.

Kernel: 24496 B / 48 sectors Selftest: pass=42 fail=0 Cap: 64 KiB → 4 GiB
May 6, 2026 · Doom Arc Closes
Pmode C App FAT16 HDD

DOOM. All Nine E1 Maps. From the Shell.

doomgeneric's six platform callbacks (DG_Init, DG_DrawFrame, DG_GetKey, DG_GetTicksMs, DG_SleepMs, DG_SetWindowTitle) are wired to the OS as a verbatim drop of upstream under apps/c/doom/engine/ plus a tiny apps/c/doom/shim/ layer. Mode 13h via pm_set_video_mode; framebuffer at 0xA0000 via direct pmode write; PLAYPAL uploaded to the VGA DAC via raw out 0x3C8 / 0x3C9; keyboard via raw port-0x60 polling; tic timing via 8254 PIT counter 0 polling. Doom's Z_Malloc zone is backed by a single pm_alloc(8 * 1024 * 1024) at startup, separate from libc's malloc arena. PMAPP_MEM_SIZE(2 * 1024 * 1024) covers the binary's working memory; the 8 MiB Z_Malloc pool sits separately in the 16 MiB pmem pool.

The launchable artefact is a 290 KB DOOM.COM identifiable by the same 'DECA32',0,0 magic as every other pmode app. The OS ships with a stripped 3.6 MB DOOM1.WAD (1151 lumps, via tools/strip-wad.py --keep-music --keep-all-e1) — no SFX, no music, no demo lumps, but every Episode 1 map intact. Drop any unmodified Doom 1.9 IWAD (shareware DOOM1.WAD or retail Ultimate Doom) into apps/c/doom/data/ instead and the build picks it up via the existing samples-glob path; the engine identifies the version from its own hard-coded filename list. FAT16-HDD-only — the 1.44 MB FAT12 floppy can't fit a multi-MB WAD.

From a fresh boot of os-hdd.img: type doom, wait ~25 seconds for W_Init to cache lumps, the title screen fades in with the correct PLAYPAL palette, the menu navigates with arrows + ENTER, all nine maps (E1M1 Hangar, E1M2 Nuclear Plant, E1M3 Toxin Refinery, E1M4 Command Control, E1M5 Phobos Lab, E1M6 Central Processing, E1M7 Computer Station, E1M8 Phobos Anomaly, plus secret level E1M9 Military Base) load and play smoothly with no lag on QEMU TCG. ESC quits cleanly back to the shell. Eight prior arcs (FAT16, big-app loader, per-app mem-size, -flto, vendored doomgeneric source, Z_Malloc bridge, mode-13h DG_DrawFrame, PLAYPAL DAC upload, raw-port input, PIT timing, WAD bundling, 32-bit file API) all fed this one.

DOOM.COM: 290528 B DOOM1.WAD: 3.6 MB / 1151 lumps Z_Malloc zone: 8 MiB @ pmem Maps: E1M1–E1M9 ✓ Selftest: pass=42 fail=0
May 7, 2026 · Lua Arc · 8 slices in a day
Pmode C App Scripting

Lua 5.4 Runs on Deca-Tiny-OS.

lua is now a runnable shell command on both image kinds. lua -e "print(1+2)" evaluates inline, lua HELLO.LUA runs a script from disk, bad syntax prints a clean diagnostic and returns rc=1 to the shell. Lua 5.4.7 vendored verbatim under apps/c/lua/engine/ at SHA-256 9fbf5e28…; 28 .c files, 27 headers, with five sources deliberately excluded (lua.c/luac.c we replace, liolib.c/loslib.c/loadlib.c we drop per locked decisions). Five edits to upstream all documented in UPSTREAM.md: four luaconf.h overrides (LUA_USE_C89, LUA_32BITS=1, l_signalT=int, lua_getlocaledecpoint='.') plus one linit.c table trim dropping io/os/loadlib. Same "verbatim engine + thin shim" pattern that worked for Doom — second instance in the repo, now reusable.

lua_Number is LUA_FLOAT_FLOAT (32-bit float), lua_Integer is long (32-bit), and LUAI_NUMFMT is overridden from "%.14g" to "%f" to stay inside the printf-float subset shipped in slice L1.2 of this same arc. Heap is libc malloc behind a ~20-line deca_alloc shim that honours Lua's lua_Alloc contract. PMAPP_MEM_SIZE(2 * 1024 * 1024) covers stack + .bss + a 1 MiB libc malloc arena with slack. Stdlibs included: base, coroutine, table, string, math (stubbed at this point — real libm follows in the next arc), utf8, debug.

Five sample fixtures in samples/ exercise the language end-to-end: HELLO.LUA (print + the pm_print_char path), FIB.LUA (recursive Fibonacci 1..10), TABLE.LUA (table.insert + ipairs + table.sort), COROUTI.LUA (coroutine.create + resume + yield producer-consumer), STRING.LUA (string.upper + string.sub + string.format). Each emits a unique LUA <CASE> PASS smoke marker. Eight prior arcs (C toolchain, HDD/FAT16, big-app loader, per-app mem-size, -flto, 32-bit file API, plus L1+L2 epics) all required for this single line of Lua to evaluate. Arc opened, vendored, debugged, and closed in a single day.

LUA.COM: 123,922 B Engine: Lua 5.4.7 verbatim Stdlibs: 7 included · 3 dropped Fixtures: 5 (HELLO/FIB/TABLE/COROUTI/STRING) Slices: 8 in 1 day Selftest: pass=42 fail=0
May 7, 2026 · libm Arc · same day
Pmode libc Numerics

Real Math: FreeBSD msun Replaces the Stubs.

The Lua port shipped earlier the same day with deliberately-wrong constant-return libm stubs (sin→0, cos→1, sqrt→x identity, pow→1, all logs→0) so the arc could close on schedule. The follow-on libm arc — opened and closed the same day — replaces every stub with a real public-domain implementation vendored verbatim from FreeBSD msun (lib/msun/src/) at pinned commit 640af0d9067bee6e8f300c158f0cf928e666977c. BSD-2-Clause throughout. 38 .c files + 3 helper headers, one upstream file per function — perfect match for our --gc-sections discipline.

All 26 stub functions from L2.1 are now numerically correct: sqrt, floor/ceil, fmod, modf, frexp/ldexp, sin/cos/tan, asin/acos/atan/atan2, exp/log/log2/log10, pow — plus the matching *f single-precision twins. math.sqrt(2)1.4142136; math.sin(math.pi/2)1; 2^10 = 1024; math.exp(1)2.7182818. Two boundary shims (sys/cdefs.h with no-op stand-ins for __always_inline / __strong_reference; machine/endian.h) plus one in-tree edit (rnintl long-double helper removed because 0x1.8p doesn't lex on gcc 13.2.0) absorbed all msun source assumptions without touching the engine bodies.

Verified end-to-end via a new MATHFIX.LUA sample fixture: 25 Lua-side math.* assertions across math.pi, sqrt, floor/ceil, fmod, sin/cos/tan (including at math.pi/2, math.pi, math.pi/4), atan/asin/acos, exp/log (one-arg + two-arg base form), and the ^ operator. Each call validated through a tolerance helper (1e-10 double, 1e-5 float for irrational args). On success prints LUA MATH PASS as a smoke marker. C-side: 96 libm fixtures across sqrt, decomp, trig, inverse trig, exp/log, and pow families in clibtest — all rolled into the existing LIBTEST C PASS marker.

Vendor: FreeBSD msun · BSD-2-Clause Files: 38 .c + 3 .h Functions: 26 stubs → real C fixtures: 96 Lua assertions: 25 (LUA MATH PASS) LUA.COM: 140,800 B
May 7–8, 2026 · SMS Arc · 11 slices, 2 days
Pmode C App Emulator

Sega Master System on Deca-Tiny-OS.

sms ROMNAME.SMS from the shell loads a real SMS / Game Gear / SG-1000 ROM, enters mode 13h, runs a Z80 @ 3.58 MHz against a TI VDP at frame-paced 60 fps, responds to PS/2 input, and exits cleanly to the shell on Esc. Single binary handles all three consoles via ROM-extension dispatch (.SMS/.GG/.SG). SMS Plus GX (libretro fork) vendored verbatim under apps/c/sms/engine/ at pinned commit 6dc7119f…. 35 source files (14 .c + 21 .h, ~12 K LOC) flat under engine/ with subdirectory structure flattened at vendor time so the build pipeline's default -Iapps/c/lib resolves the upstream's flat-path includes without per-app -I noise. License stack: GPLv2+ for the engine + sound glue + FM + PSG + BSD-3-Clause for the Z80 CPU core; combined sms.com is GPL-2 — same shape as Doom.

Per-concern shim layout under apps/c/sms/shim/: 7 files, one concern each. init.c enters mode 13h via pm_set_video_mode(1). draw.c palette-uploads and per-row letterbox-memcpys the VDP screen buffer into 0xA0000 with a 32-px left + 4-px top centre offset for SMS (and 80 + 28 for GG). input.c polls PS/2 port 0x60 and maps cursor keys → d-pad, Z → btn1, X → btn2, Enter → pause/start. timing.c 8254 PIT counter-0 polling + 60 fps frame pacing. palette.c translates SMS 6-bit-RGB → VGA DAC via raw out 0x3C8 / 0x3C9. audio.c is a no-op stub — same precedent as Doom; SN76489 PSG callbacks no-op, v1 ships silent. allocator.c routes engine malloc through libc + pm_alloc for the cart ROM buffer.

Three-iteration real-ROM bring-up at slice S5.2 mirrors Doom's WAD bring-up cycle. (iter 1) Cart loaded but the screen stayed black — Z80 PAIR-union register access reads the wrong byte halves on i386 without LSB_FIRST set. Fix: in-tree edit to shared.h, #define LSB_FIRST 1 (the literal 1 matters because render.c:306 uses #if LSB_FIRST not #ifdef). Cart now ran but (iter 2) sprites had a pink/magenta overlay against correctly-coloured backgrounds — engine/render.c:258 stamps bit 0x40 on sprite pixels as an internal "this-is-a-sprite" marker, the marker bit was leaking through to mode 13h DAC indices 0x500x5F (default VGA pink/magenta strip). Fix: shim/draw.c per-row loop now masks each byte with PIXEL_MASK (0x1F). (iter 3) FAT12 floppy diskfull-smoke timed out after a 128 KiB user-supplied homebrew ROM bloated the floppy past its known starting state. Fix: build pipeline now always skips .sms/.gg/.sg on FAT12 (category gate, not size gate); FAT16 HDD bundles them all.

Multi-ROM Epic S5 checkpoint verified 8 of 9 ROMs across a 128 KiB–800 KiB library. Working: bb.sms (Black Belt, headline demo), ab.sms, dd.sms (Double Dragon), ds.sms, gauntlet.sms, sb2.sms, sd.sms, sf2.sms (Street Fighter 2, attract demo). Known limitation: zool.sms loads + plays from title screen but hangs partway into level 1 — likely a v1-out-of-scope item (CodeMasters mapper / FM unit / timing-sensitive code), not a regression in the engine bind. Eight prior arcs (FAT16, big-app loader, per-app mem-size, -flto, 32-bit file API, doomgeneric port, Lua 5.4 port, libm port) plus all 9 of this arc's implementation slices fed the final demo.

SMS.COM: 91,908 B Engine: SMS Plus GX verbatim Files: 35 vendored (~12 K LOC) Consoles: SMS + GG + SG-1000 ROMs working: 8 / 9 In-tree edits: 6 documented Smoke marker: SMS CORE PASS (#14) Selftest: pass=42 fail=0
May 8–11, 2026 · Sound Arc · 11 slices, 4 days
Pmode Audio Drivers SB16 · OPL2 · PC Spkr

Audio: Doom Has SFX, SMS Has Music.

A complete DOS-era audio stack lands as a pure libc extension — no kernel changes, no new int 60h services, selftest stays at pass=42 fail=0. Apps drive hardware directly at CPL=0, the same pattern Doom uses for the VGA DAC at 0x3C8 and the PS/2 keyboard at 0x60. Three subsystems land: PC speaker via PIT counter 2 + port 0x61 (audio_pcspk.c, 95 LOC); Sound Blaster 16 via DSP at 0x220 + 8237 DMA channel 1 + 8-bit unsigned mono PCM at 5–44 kHz with single-shot, polled auto-init streaming, and a 4-channel software mixer (audio_sb16.c, 519 LOC); Yamaha OPL2 at 0x388/0x389 with detection, register write, and 9-voice abstraction including frequency-to-fnum/block conversion (audio_opl.c, 265 LOC). Total ~1156 LOC of new libc plus a single audio.h public surface header.

Seven sample apps demonstrate each primitive end-to-end, each with its own smoke marker: bell (PC-speaker chime, BELL PASS), sbinfo (DSP version query, SB16 INIT PASS — QEMU reports DSP 4.05), pcmplay (1-second 220 Hz single-shot sine, PCM PASS), opltest (1-second A4 FM tone, OPL TONE PASS), fmtest (C-major arpeggio across 4 OPL2 voices, FM PASS), pcmstrm (5-second frequency-sweep via auto-init DMA, STREAM PASS), mixtest (4 overlapping waveforms via software mixer, MIX PASS). Smoke-marker count goes 14 → 21.

Then the two headline re-emergences. Epic A5 rewrites apps/c/sms/shim/audio.c (was an 18-line empty stub from SMS arc S2.2): per-frame engine snd.output[] samples route into a double-buffered async chunked-DMA pattern at 44100 Hz mono. (The streaming pattern from A4.1 broke VGA mode 13h rendering on QEMU; switched to chunked async to keep DMA bursty with idle windows for VGA updates — new audio_sb16_play_8m_async + audio_sb16_wait_8m driver helpers landed for that.) Black Belt's PSG tone-channel music is now audible. Noise-channel SFX (snare/punch/kick) shelved as a known limitation — diagnostic chain ruled out inter-chunk gaps, int16→uint8 quantisation, and QEMU DAC filtering; remaining theory is engine-side tone-vs-noise balance in the per-scanline mixer.

Epic A6 adds a new apps/c/doom/shim/sound.c (~300 LOC) implementing Doom's I_StartSound / I_StopSound / I_UpdateSound by routing DMX-format WAD-lump SFX (the DS*-prefix lumps) through the 4-channel mixer + double-buffered async chunked DMA at 11025 Hz mono. Gun fire, shotgun, chainsaw, doors, item pickups, monster grunts, explosions — all audible during gameplay, up to 4 simultaneous SFX. Two minor edits: i_sound.c SDL_mixer.h include removed, plus -DFEATURE_SOUND added to the toolchain flags so Doom's sound_modules[] links our module. Doom MUS music stays silent — MUS/MIDI synth is a separate future arc. The Doom-SFX-clean result narrowed the SMS-PSG noise diagnosis from "generic int16→uint8 issue" to "SMS engine mixer tone-vs-noise balance."

Audio libc: 1156 LOC across 4 files Hardware: PC Spkr + SB16 + OPL2 Sample apps: 7 new Smoke markers: 14 → 21 SMS.COM: 91,908 → 92,580 B DOOM.COM: 293,088 → 293,152 B Kernel: unchanged · pass=42 fail=0
May 12, 2026 · IRQ Arc · 7 slices, 1 day
Pmode 8259 + IDT IRQ-driven SB16

Pmode IRQs: the kernel finally takes interrupts.

Every audio path through Sound A6 was polled — the pmode extender masked interrupts on CR0.PE flip and never re-enabled them. The IRQ arc lands the missing piece in 7 slices in one day: (I1.1) pushf/popf around all four CR0-toggle sites so a pmode app's sti survives its first syscall; (I1.2) per-pmode-hop 8259 PIC remap from BIOS default to standard 0x20–0x2F, undone on every pmode exit so BIOS shell ISRs that out 0x20, 0x20 as a literal EOI keep working; (I1.3) a 256-entry pmode IDT in kernel BSS loaded via lidt on entry, with spurious-IRQ-aware default stubs (poll master ISR via out 0x0B; in 0x20 before EOI) and two trampoline templates — master-only EOI for IRQ 0–7, slave + master cascade for IRQ 8–15; (I1.4) three new int 60h services (AH=0x22 set_irq_handler, 0x23 irq_unmask, 0x24 irq_mask) and a libc surface in apps/c/lib/pm_irq.{h,c} giving apps pm_irq_register(int irq, void (*)(void)), _unmask, _mask, _sti, _cli. Then (I2) the consumer: a new audio_sb16_stream_start_irq(buf, size, rate, refill_cb) alongside the polled streaming API. CPU exceptions land in halt stubs with a visible serial marker instead of triple-faulting the box.

Two sample apps prove the chain. irqping registers an IRQ-0 (PIT) handler that increments a counter, sleeps 1 second, and asserts counter > 10 (BIOS PIT default ~18.2 Hz) — emits IRQPING PASS. pcmstrm2 kicks off the SB16 5-second sweep via the new IRQ-driven refill, sleeps 5 seconds with a literally idle main loop, stops, emits STREAM IRQ PASS. First non-polled hardware driver in the system. Real-mode shell still works after pmode return because the PIC remap is undone on exit. Pre-merge bug caught in I1.4: a 2 KiB initialised idt_table declaration pushed kernel.bin past the boot-loader's 53-sector safe limit; fix was to declare the table as a bare label past all initialised data so NASM truncates the flat-binary output. An I1.4 follow-up also added an irq_keyboard_drain_stub at IDT[0x21] to drain port 0x60 during pmode hops — keyboard activity during a pmode app no longer wedges the 8042. Same arc retired the FAT12 floppy: every smoke gate is now FAT16-HDD-only (regression PS↔Python parity still runs both kinds to catch builder drift).

Kernel: 24,496 → 25,960 B (48 → 51 sects) Selftest: pass=42 → 46 fail=0 Smoke markers: 21 → 23 Sample apps: irqping + pcmstrm2 New libc: pm_irq.{h,c} ~140 LOC FAT12: retired
May 12, 2026 · PS/2 Mouse Arc · 5 slices, 1 day
Input 8042 aux IRQ 12

Pointer input: PS/2 mouse via IRQ 12.

The first consumer of the IRQ infrastructure outside the SB16 streaming demo. PS/2 mouse fires IRQ 12 on the 8042 keyboard controller's auxiliary port, so the arc is mostly an app-side affair — apps/c/lib/pm_mouse.{h,c} (~280 LOC combined) owns the full 8042-aux init sequence (enable aux via 0xA8 → 0x64, set the aux-IRQ-enable config bit, mouse reset 0xD4 0xFF, enable streaming 0xD4 0xF4), the 3-byte standard PS/2 packet state machine (byte 0 sync-bit check, signed dx/dy in bytes 1 and 2, restart on sync failure), and a static IRQ thunk that calls back into the app's void(uint8_t buttons, int8_t dx, int8_t dy) handler. Public surface: pm_mouse_init, pm_mouse_register(cb), pm_mouse_stop. Kernel side grows by exactly one drain stub (irq_mouse_drain_stub at IDT[0x2C], slave + master cascade EOI) plus one selftest case (selftest_mouse_drain_stub_installed) — closes the latent IRQ-arc-era gap where the default irq_slave_eoi_stub did not drain port 0x60 and a stray mouse byte during a pmode hop would wedge the 8042 controller.

mousetst initialises the mouse, registers a packet-counting handler, pm_irq_stis, audio_delay_ms(2000) — wiggle the mouse during the window — stops, reports packet count + cumulative dx/dy + last button state, emits MOUSE PASS. Interactive QEMU produces 20–100+ packets for moderate wiggle, with the correct button bit when a button is held. Shell remains responsive afterwards (verifies the mouse drain stub and the I1.4 keyboard drain coexist cleanly). Mouse-driven paint upgrade and mouse-look for Doom are queued as follow-ons.

Kernel: 25,960 → 26,064 B (+104 B, 51 sects) Selftest: pass=46 → 47 fail=0 Smoke marker: MOUSE PASS (#24) libc: pm_mouse.{h,c} ~280 LOC Sample: mousetst (4,164 B) Pre-merge bugs: 0
May 13, 2026 · MIDI / MUS Arc · 7 slices, 1 day
Pmode OPL2 GM-subset MUS + SMF

Doom music. "At Doom's Gate" plays through OPL2.

For six days DG_music_module in apps/c/doom/shim/sound.c was a no-op stub — Doom played SFX from the Sound A6 arc but the soundtrack was silent. The MUS/MIDI arc closes that. Three new libc layers ship in one day with zero kernel changes: (M1) apps/c/lib/audio_opl_gm.{h,c} — a 16-patch hand-crafted GM-subset bank baked as static const data, a 9-voice allocator with note-stealing on the lowest-priority channel, MIDI-note-to-freq conversion, velocity-to-carrier-level mapping, plus a thin audio_midi_note_on / _note_off / _program_change / _controller surface on top of the existing audio_opl primitives. (M2) pm_music.{h,c} — a format-agnostic playback engine driven by pm_music_poll(uint32_t ms_now) with a shared 24,576-entry event pool. (M4) smf_parser.c — adds Standard MIDI File support behind pm_music_load_smf: header + track chunks, variable-length quantities, running status, tempo meta events, and an SMF Format 1 multi-track merge by absolute ms time at load.

Two visible payoffs. Standalone: musictst embeds a ~200-byte MUS payload as static const uint8_t, plays a ~5-second pattern through OPL2, emits the full MUSIC INIT PASS → LOAD PASS → PLAY PASS → MUSIC PASS marker chain. musictst hello.mid exercises the SMF path through samples/HELLO.MID. Headline: M5 wires DG_music_module to the new libc — RegisterSong → pm_music_load_mus, PlaySong → pm_music_play, etc. — and Doom's I_UpdateSound hook calls pm_music_poll(DG_GetTicksMs()) once per gametick. With a --keep-music-stripped DOOM1.WAD, E1M1 plays "At Doom's Gate" through OPL2 in-game. E1M2 plays "The Imp's Song". Pause/resume via Doom's menu cuts sustained voices + rebases the poll clock. Pre-merge bug caught at the M5 interactive checkpoint: PM_MUSIC_MAX_EVENTS = 8192 overflowed on D_E1M2 (8,372 events) and D_E1M8 (16,742). Bumped the ceiling to 24,576 and added tools/mus-event-count.py for future audits.

Kernel: 26,064 → 26,072 B (+8 B for help_msg) Selftest: pass=47 fail=0 (unchanged) Smoke marker: MUSIC PASS (#25) DOOM.COM: 293,152 → 296,736 B (+3,584 B) MUSICTST.COM: 6,825 B FAT16 SHA: f796471d…
May 13, 2026 · Graphics SDK Arc · 7 slices, 1 day
Pmode VGA mode 13h PCX + Bresenham + Font

A real graphics library. Lines, circles, text, PCX, and a mouse cursor.

For weeks every C app that wanted graphics rewrote framebuffer access from scratch — Doom, SMS, the old paint.asm / snake.asm / gfxdemo.asm, even mousetst printed text-mode-only. No shared C primitive existed for Bresenham lines, midpoint circles, polygon fill, text overlay in graphics mode, mouse cursor rendering, or image-file I/O. The Graphics SDK arc closes that gap in 7 slices in one day — structurally a pure libc extension, same shape as the libm and music arcs. Zero kernel changes; selftest stays at pass=47.

Three new files in apps/c/lib/: gfx.{h,c} (~400 LOC) owns drawing primitives — direct framebuffer write at linear 0xA0000, rect-fill via memset, Bresenham line with octant dispatch, midpoint circle, scanline convex-polygon fill, VGA DAC palette write via ports 0x3C8/0x3C9 with vertical-retrace sync, and a cursor save/draw/restore trio (gfx_cursor_save_bg / _draw_sprite / _restore_bg). gfx_font.c (~120 LOC) embeds a public-domain 8×8 BIOS-style font as static const uint8_t font_8x8[128][8], ASCII 0x20-0x7F (96 glyphs ≈ 1 KiB strippable), with gfx_text(x, y, fg, str). gfx_pcx.c (~220 LOC) is a full PCX 256-color v5 encoder/decoder — 128-byte header + RLE scanlines + 769-byte palette appendix — files are byte-compatible with GIMP, ImageMagick, and IrfanView out of the box.

gfxtest proves every primitive end-to-end. Plain gfxtest runs a deterministic visual demo (palette ramp, lines, circles, polygons, text banner, cursor blit round-trip, PCX encode/decode round-trip with pixel-compare) and emits the GFX MODE → PRIMS → FONT → CURSOR → PCX → PASS lifecycle marker chain. gfxtest mouse is the headline checkpoint: a cursor sprite follows the PS/2 mouse pointer in real time via the IRQ-12 packet callback from the mouse arc; left-button-drag draws a Bresenham line directly into the framebuffer; ESC saves the canvas as gfxdemo.pcx. gfxtest gfxdemo.pcx reloads and displays the saved image. The bundled samples/GFXDEMO.PCX (17 KiB hand-painted) ships so the app does something interesting from a fresh boot. Three pre-merge fixes at the mouse-mode checkpoint: replaced pm_poll_key() (which round-tripped through real-mode and dropped mouse IRQs) with a direct-port kbd_poll_esc(); masked IRQ 1 during mouse mode so the kernel keyboard-drain stub wouldn't eat the ESC scancode; gave the PCX viewer the same treatment.

Kernel: 26,072 → 26,080 B (+8 B, 51 sects) Selftest: pass=47 fail=0 (unchanged) Smoke marker: GFX PASS (#26) New libc: gfx + gfx_font + gfx_pcx ~740 LOC GFXTEST.COM: 9,272 B GFXDEMO.PCX: 17,113 B bundled
May 14, 2026 · Deca Paint v1 Arc · 7 slices, 1 day
Productivity Mouse UI PCX save/load

Deca Paint v1. The first genuinely productive end-user app.

Every prior app shipped is either a demo, a developer tool, a port (Doom / Lua / SMS), or infrastructure. None of them are something a user opens to actually make something. Deca Paint v1 changes that. It's a Deluxe-Paint-style drawing app that turns the just-shipped Graphics SDK from "primitives library + tech demo" into a real productivity tool you can pick up, work in, save your output from, and reopen later with everything preserved. Single-binary C app, ~700 LOC of pure UI + tool logic. Zero kernel changes, zero libc additions — selftest stays at pass=47; the entire arc rides on the SDK + the mouse driver + the PCX codec from the prior two arcs.

The UI is fixed-layout 320×200: 8-pixel status bar at the top, 16-pixel tool palette down the left edge (8 tool icons with single-char glyphs P/L/R/r/C/c/B/E), 304×184 canvas in the middle, 16-swatch color palette across the bottom. 8 drawing tools: pencil (Bresenham-interpolated drag), line / rect / filled-rect / circle / filled-circle (press → release), flood-fill bucket (explicit-stack scanline BFS with a 1024-span cap), eraser. 16-named-color palette shared byte-for-byte with gfxtest so PCX files interop cross-app. Mouse-driven UI — clicking a tool icon updates the active tool (yellow highlight moves), clicking a swatch updates the active color (outline moves). Full keyboard shortcut coverage: 1-8 colors, P/L/R/Shift+R/C/Shift+C/B/E tools, Z undo, S save, ESC quit. Single-level undo via a static 64 KiB BSS snapshot captured before each tool application. Save/load via command-tail filename — dpaint MYPAINT.PCX both loads (if the file exists) and saves to that name.

Four pre-merge bugs caught at the headline P2 checkpoint — all four are 8042/IRQ interactions worth knowing about. (1) redraw_chrome internally called clear_canvas, wiping the loaded PCX right after try_load_pcx blitted it. (2) Lowercase filenames silently failed to load with no flash — fix: uppercase argv[1] in main, explicit "No file: NAME" on miss. (3) Slow PCX load + still-held Enter key meant a keyboard scancode landed in port 0x60 just as pm_mouse_init was reading mouse responses; the kernel's irq_keyboard_drain_stub consumed the ACK byte; init failed silently. Fix: pm_irq_cli() + drain 8042 manually before mouse init. (4) The ~400-syscall save sequence had pmode↔real PIC remaps; mouse IRQ 12 firing during a BIOS-mode window landed at vector 0x74 with no installed handler; ICW1 re-init on pmode return + the edge-triggered PIC dropped the still-asserted line; 8042's single output buffer blocked all subsequent keyboard + mouse IRQs (total freeze with the "Saved" flash stuck on screen). Fix: new drain_8042_post_syscall() helper at the end of try_save_pcx + try_load_pcx. Bundled samples/PAINTTUT.PCX (6 KiB) ships as the first-launch experience. Cross-app compatibility verified — gfxtest PAINTTUT.PCX opens the same file the paint app saves.

Kernel: 26,080 → 26,088 B (+8 B, 51 sects) Selftest: pass=47 fail=0 (unchanged) Smoke marker: DPAINT PASS (#27) DPAINT.COM: 14,592 B PAINTTUT.PCX: 6,010 B bundled Tools: 8 · Colors: 16 · Undo: 1-level

The lessons that
cost the most

The bugs that took the longest. The design decisions that mattered most. The moments where everything clicked — or broke spectacularly.

Deep Dive · FAT12 · Debugging
The Register That Ate My Filesystem
For days, every file delete appeared to succeed but left the filesystem in a corrupted state. The cause: fs_load_metadata was clobbering AX — the very register that carried the cluster number to free. Every delete was silently freeing cluster 19. Every delete. That cluster happened to contain GFXDEMO.COM. The system "worked" until you ran the graphics demo and it crashed with garbage on screen. This is why you write regression scripts that walk the FAT and check every chain.
April 29, 2026 · FAT12 · Assembly · Debugging
The fix: preserve AX across the metadata call
; BROKEN: AX (start cluster) clobbered here fs_release_cluster_chain: call fs_load_metadata ; AX → 19 ! ; now walks and frees cluster 19, not ours
; FIXED: save start cluster before metadata load fs_release_cluster_chain: push ax ; save cluster call fs_load_metadata pop bx ; restore → walk this ; now correctly walks requested chain
System Design · API · Architecture
Why int 60h Was the Right Call
The entire app ecosystem uses a software interrupt to talk to the kernel. int 60h, AH=service, arguments in registers. It's the CP/M BDOS model, adapted for this tiny OS. The payoff: every app ever written works the same way. Moving apps to a new segment? The API handles the segment-boundary crossing. Changing buffer locations? Apps never knew where they were anyway. The abstraction earns its cost at every feature slice.
April 27, 2026 · 26 int 60h services + pmode thunk
Build System · Regression · Automation
Serial-Driven QEMU Smoke Tests
Every significant change gets verified by scripts/smoke-qemu.ps1: a PowerShell script that launches QEMU with COM1 exposed over TCP, waits for the selftest marker, then drives the shell by typing commands over the serial port and watching for expected output. Fresh image, mutated image, SHA-256 parity between PowerShell and Python builders. If any check fails, the build doesn't pass. This is what makes confident development possible.
April 29, 2026 · Automation · Testing
Assembly · BASIC · Interpreter
8KB Is Enough for a BASIC Interpreter
The constraint was the app load region: fit in 8KB, use only int 60h services, no kernel modifications. The approach: a tokenizer include shared with a diagnostic tool, a minimal recursive-descent expression parser (signed 16-bit, A–Z variables, full precedence), a bounded 12-line program buffer, and execution state for loops and subroutines. Tight but complete. Save and load work via the FAT12 temp-first write model.
April 29–30, 2026 · 12 development slices
Memory · Architecture · x86
Escaping the First 64KB
Real mode means every data access uses a segment:offset pair, and most early real-mode code defaults to segment zero for everything. The 640K arc was about making the kernel deliberately segment-aware: moving FAT buffers to 7000:9000, the file staging buffer to 7000:C000, apps to 2000:0100. The kernel API now marshals pointer arguments across segment boundaries so apps never know where any of it lives.
May 2, 2026 · 10-slice migration
Pmode · Extender · ABI
EBX Is Reserved (and Other Pmode-Extender Rules)
The DOS/4GW-style syscall thunk dispatches through call dword [ebx]: the kinfo struct's first field is the thunk's own linear address. So EBX is reserved for the life of every pmode app. pmsieve learned this the hard way — using BL as a byte counter silently corrupted the low byte of EBX, sending the next syscall into garbage and hanging the app. Fix: use DL. Then codify the rule. Two more rules from the same arc: imul eax, imm not mul reg (preserves EDX as a row counter), and direct port-0x60 keyboard reads not BIOS keyboard buffer (the buffer is unreliable when interrupts are masked).
May 3, 2026 · Pmode · Bug discipline
FAT16 · Dispatcher · SOLID
The Kernel That Sits Inside Its Own FAT
The 32 MB FAT16 image places the kernel at LBA 128 — a perfectly fine home for the kernel, except that LBA 128 also happens to be sector 127 of FAT1. Writing the kernel to disk overwrites FAT1 entries for clusters 32512..44543. The fix isn't relocating the kernel (that breaks the floppy boot path) — it's a build-time allocation cap (MAX_SAFE_CLUSTER = 32000) that keeps every app's cluster number below the corrupted region. FAT2 stays clean, the kernel never reads its FAT1 home, and the regression script's leak check walks chains through FAT2 to confirm nothing leaks. Six dispatcher entry points route by [fs_format]; fs_fat12.asm and fs_fat16.asm are interchangeable behind the same signatures.
May 4–5, 2026 · FAT16 · Dispatch · Build discipline
C Toolchain · libc · printf
Hand-Rolling vsnprintf in 270 Lines
printf is famously fiddly — width interaction with precision, sign handling, %x zero-padding, the %.0d-with-zero edge case, %*d variadic width fetch, and the %p pointer convention all have wrong defaults waiting to be picked. The implementation here uses a small (buf, pos, cap) output struct so truncation is centralised: put bounds the actual store but always increments pos, and the function returns what would have been written if the buffer were unbounded. Numeric conversions render into a 32-byte scratch buffer from the trailing end, then a shared emit_number handles sign + precision + width + flags. The 270 lines support %d %u %x %X %s %c %ld %lu %lx %p %% with width, precision, -, 0, and *. That is enough printf to print a Doom WAD header.
May 4, 2026 · libc · vsnprintf · printf-family
Shell · FAT12 · Directories
Adding Directories Without Breaking Everything
FAT12 directories are files that happen to contain 32-byte directory entries. Adding mkdir meant: allocate a cluster, write initialized . and .. entries, create a root entry with attribute 0x10, and — critically — make sure the existing file API never treats a directory entry as a regular file. That last rule required splitting root lookup into "any entry" and "regular file only" variants and plumbing that distinction everywhere.
May 1, 2026 · Directories · FAT12

The full application
suite

54+ external applications, written in x86 NASM or C. Real-mode apps talk to the kernel through int 60h; pmode apps (NASM and C alike) talk through a syscall thunk in the kinfo struct at EBX. IRQ-capable apps install handlers via pm_irq_register and the new int 60h AH=0x22/0x23/0x24 trio. Graphics-mode apps share the C-side gfx.h SDK (lines, circles, polygons, 8×8 font, cursor blit, PCX save/load).

basic — BASIC Interpreter
Full Tiny BASIC REPL with variables, loops, subroutines, conditionals, file save/load, and command-tail autorun. 8KB.
✏️
edit — Text Editor
Line-oriented text editor with temp-first save (EDITSAVE.TMP → rename), so the original file is never lost on failure.
🔬
hexedit — Binary Patcher
Stream-based binary patch tool: verify, patch, pair-patch, undo checkpoint, and scratch recovery. Never loads the whole file.
📄
more / head / wc / find
Unix-inspired text tools: paged viewer, first-N-lines, byte/line/word counts, literal search. All streaming, no size limits.
🖥️
gfxdemo / paint / snake
VGA mode 13h graphics apps: color bar demo, arrow-key drawing, and a keyboard-driven snake game. All using int 60h graphics API.
🔍
hexview — Hex Viewer
Read-only paged hex viewer with optional filename and offset arguments. N for next page, Q to quit.
⚖️
cmp — File Compare
Streaming two-file compare using two concurrent read handles. Reports CMP match, CMP differ size, or CMP differ offset=N.
💾
Diagnostic Suite
diskfull, rootfull, wrtfail, wrterr, handles, libtest — a complete set of stress diagnostics for every failure path.
📜
batch — Shell Scripts
Run .BAT scripts from the shell with comment handling, blank-line skip, stop-on-error mode, and AUTOEXEC.BAT at startup.
🛡️
pmhello / pmcat / pmsieve [PMODE]
32-bit protected-mode apps. pmhello proves the syscall thunk; pmcat streams a file via the DMA bounce buffer; pmsieve allocates 64 KiB out of the 16 MiB extended-memory bump pool.
🎮
pmpong [PMODE]
Player-vs-AI Pong in mode 13h. 5×7 bitmap score font, AABB paddle collision, first to 7 wins. Direct VGA writes, port-0x60 keyboard polling, PIT-based ~30 fps. 1569 bytes.
pmbounce [PMODE]
First proof-of-concept that pmode apps can drive mode-13h graphics + interactive input + frame pacing — the primitives pmpong was built on. 591 bytes.
⚙️
chello / cstub [C · PMODE]
First C apps on the pmode extender, compiled by i686-elf-gcc 13.2.0 and linked at flat 0x100000. chello calls printf; cstub is the empty smoke that proves crt0 works. 494 B / 404 B.
🧪
clibtest [C · PMODE]
Exercises the entire libc surface: string/ctype/setjmp/malloc/qsort/stdio fixtures, including the freelist-coalesce stress and fseek/ftell round-trips. Prints LIBTEST C PASS. 11.2 KB.
🖨️
cprintf [C · PMODE]
Streams SAMPLES.TXT through fread, prefixes each line with a right-aligned 2-digit number via printf("[%2d] ", n), and ends with a byte-total summary. End-to-end proof that printf + fread work. 3.6 KB.
📝
cwrite [C · PMODE]
Creates CWTEST.TXT via fwrite, reopens in "a" mode and appends, reads back via fread, verifies the round-trip, then removes the file. Prints CWRITE PASS. 4.5 KB.
🎮
doom [C · PMODE]
id Software's Doom 1.9, all 9 Episode 1 maps. doomgeneric verbatim engine + thin shim: mode 13h framebuffer, raw port-0x60 keys, PIT timing, 8 MiB Z_Malloc zone. DMX SFX audible via SB16 4-channel mixer @ 11025 Hz — gun, shotgun, doors, monsters. MUS music audible via OPL2 GM-subset synth — E1M1 "At Doom's Gate", E1M2 "The Imp's Song". 297 KB binary + 3.6 MB stripped DOOM1.WAD. FAT16-HDD-only.
🌙
lua [C · PMODE]
Lua 5.4.7 verbatim under apps/c/lua/engine/. lua -e "code" evaluates inline; lua HELLO.LUA runs scripts from disk. LUA_FLOAT_FLOAT + LUA_INT_LONG. Stdlibs: base/coroutine/table/string/math/utf8/debug. 140 KB with real msun-vendored libm.
🕹️
sms [C · PMODE]
Sega Master System + Game Gear + SG-1000 emulator. SMS Plus GX vendored under apps/c/sms/engine/ (35 files, ~12 K LOC). Z80 @ 3.58 MHz, TI VDP, mode 13h letterbox, 60 fps, PS/2 d-pad. PSG tone-channel music audible via async chunked DMA @ 44100 Hz. 8/9 ROMs play cleanly. 92 KB binary.
🔔
bell [C · PMODE]
5-note chime through the PC speaker via PIT counter 2 + port 0x61. The simplest audio path on the OS, ~95 LOC of driver in audio_pcspk.c. BELL PASS. 661 B.
🎚️
pcmplay / pcmstrm / mixtest [C · PMODE]
Sound Blaster 16 PCM: pcmplay single-shot 220 Hz sine, pcmstrm auto-init DMA frequency sweep, mixtest 4-channel software mixer with overlapping waveforms. DSP @ 0x220, DMA channel 1, 8-bit mono, 5–44 kHz. PCM PASS / STREAM PASS / MIX PASS.
🎹
opltest / fmtest [C · PMODE]
Yamaha OPL2 FM synthesis via ports 0x388/0x389. opltest plays a 1-second A4 sine; fmtest arpeggiates a C-major chord across 4 of the 9 melodic voices. Frequency-to-fnum/block conversion via log2f from msun. OPL TONE PASS / FM PASS.
⏱️
irqping [C · PMODE]
First IRQ-driven sample. Installs a PIT (IRQ 0) handler via pm_irq_register(0, ...), unmasks, pm_irq_sti, sleeps 1 second while the handler counts ticks asynchronously, asserts counter > 10 (BIOS default ~18.2 Hz). Proves PIC remap + IDT + trampoline + EOI all work end-to-end. IRQPING PASS.
🎚️
pcmstrm2 [C · PMODE]
First non-polled hardware driver in deca-tiny-os. SB16 5-second frequency sweep via audio_sb16_stream_start_irq(buf, size, rate, refill_cb) — IRQ 5 fires on every DMA half-buffer drain and the refill callback walks the next sweep chunk into place. Main loop is literally audio_delay_ms(5000). STREAM IRQ PASS.
🖱️
mousetst [C · PMODE]
PS/2 mouse on the 8042 aux port via IRQ 12. pm_mouse_init runs the full 8042 dance (aux enable, IRQ-enable bit, reset, streaming), pm_mouse_register(cb) installs the packet callback, 2-second window — wiggle the mouse — reports packet count + cumulative dx/dy + last button. 3-byte standard PS/2 protocol with sync-bit recovery. MOUSE PASS. 4.2 KB.
🎼
musictst [C · PMODE]
MUS + Standard MIDI File playback through OPL2 GM-subset synth. musictst alone plays a ~200-byte embedded MUS pattern; musictst hello.mid parses an SMF file from disk. 9-voice allocator with note-stealing, 16-patch hand-crafted bank, polled tempo via pm_music_poll(ms_now). MUSIC INIT PASS / LOAD PASS / PLAY PASS / MUSIC PASS. 6.8 KB.
🖼️
gfxtest [C · PMODE]
End-to-end demo of the gfx.h SDK. Plain gfxtest draws Bresenham lines, midpoint circles, convex polygon fills, banner text via the 8×8 BIOS font, a cursor sprite round-trip via save/draw/restore, and a PCX encode/decode pixel-compare. gfxtest mouse: cursor follows the PS/2 pointer, LMB-drag draws lines, ESC saves the canvas to gfxdemo.pcx. gfxtest <file>.pcx loads and displays. GFX MODE/PRIMS/FONT/CURSOR/PCX → GFX PASS. 9.3 KB + 17 KB bundled GFXDEMO.PCX.
🎨
dpaint — Deca Paint v1 [C · PMODE]
First productivity-grade end-user app. Deluxe-Paint-style drawing tool with 8 brushes (pencil / line / rect / filled-rect / circle / filled-circle / flood-bucket / eraser), 16-named-color palette, mouse-driven UI chrome (tool palette column + color swatches + status bar), keyboard shortcuts (1-8 colors, P/L/R/Shift+R/C/Shift+C/B/E tools, Z undo, S save, ESC quit), single-level undo (64 KiB BSS snapshot), and PCX save/load via command-tail filename. dpaint PAINTTUT.PCX opens the bundled tutorial. Cross-app PCX compatibility with gfxtest. DPAINT INIT/TOOLS/UI/UNDO/SAVE → DPAINT PASS. 14.6 KB + 6 KB bundled PAINTTUT.PCX.

Where everything
lives in memory

640KB of conventional real-mode memory, carefully partitioned. After the 640K arc, everything has a permanent home — and the layout is identical whether the kernel was loaded from a FAT12 floppy or a FAT16 hard disk.

0000:0000
Interrupt Vector Table / BDA
0000:1000
KERNEL (~26KB, 51 sectors) — GDT + pmode launcher + FS dispatcher (32-bit handles) + 256-entry pmode IDT
0000:F000
Stack (grows down)
2000:0100
APP WINDOW (36KB) ← apps load here
2000:8F00
Return trampoline
7000:9000
Disk buffer (512B)
7000:9200
FAT buffer (512B)
7000:A400
Root directory buffer (3.5KB)
7000:C000
File / DMA staging buffer (8KB)
9FC0:0000
EBDA / Conventional memory top
A000:0000
VGA framebuffer (mode 13h graphics)

Above the 1 MiB line is where the pmode extender lives. After A20 enable, the kernel can address the full extended-memory range; the pmode launcher copies app images here and hands them out via a per-launch bump allocator.

0x100000
Pmode app image (loaded at linear 1 MiB)
0x200000
Extended-memory bump pool (16 MiB cap, reset per launch)
0x1200000
Pool top — clipped by detected extended memory

And the on-disk layout of the new 32 MB FAT16 image. The kernel sits at LBA 128 — which lands inside FAT1's region — so the build script writes the kernel last, capping app cluster allocation at 32000 so no live cluster ever lives inside the corrupted FAT1 sectors. FAT2 stays pristine and is what the leak check walks.

LBA 0
boot-fat16.bin — boot sector + FAT16 BPB
LBA 1..256
FAT1 (256 sectors / 130 KB) — kernel overlays sectors 127..178
LBA 128
KERNEL (51 sectors) — boot sector reads from this absolute LBA
LBA 257..512
FAT2 (256 sectors) — clean source-of-truth for the leak check
LBA 513..544
Root directory (32 sectors / 512 entries · sector cache)
LBA 545+
Data area · ~64991 clusters @ 1 sector/cluster · alloc cap 32000

Recent commits,
real evidence

Every feature slice includes a SHA-256 image hash, a QEMU serial capture, and a FAT cluster leak check. This is what "done" means.

May 14, 2026
arc-close · ✓ Deca Paint v1
Deca Paint v1 shipped — first productivity-grade end-user app on the OS
7 slices in a single day (May 14): P0 doc kickoff, P1 feature/dpaint-scaffold-chrome — new apps/c/dpaint/main.c (~250 LOC) declares PMAPP_MEM_SIZE(2 * 1024 * 1024), installs the 16-named palette, renders the fixed-layout UI (8-px status bar + 16-px tool column + 320×8 color swatches + 304×184 canvas), sets up mouse-cursor follow via the IRQ-12 callback from the mouse arc, IRQ-1-mask on entry / unmask on exit, emits DPAINT INIT PASS → DPAINT PASS in selftest mode. P2 feature/dpaint-drawing-tools adds 7 of 8 tools (pencil / line / rect / filled-rect / circle / filled-circle / eraser) via an apply_tool() enum dispatcher, click-and-drag pencil with Bresenham-interpolation between consecutive cursor frames, press-release semantics for the shape tools, full keyboard shortcuts. P3 feature/dpaint-ui-interaction wires hit-testing for tool-icon clicks + swatch clicks with yellow-highlight feedback; Epic P1 closes; interactive checkpoint #1 cleared. P4 feature/dpaint-fill-undo adds the flood-fill bucket tool (explicit-stack scanline BFS, 1024-span cap with graceful-fallback warning) and single-level undo (save_snapshot before each tool, Zrestore_snapshot + "Undone" flash). P5 feature/dpaint-save-load parses argv[1] as the active filename, loads on init if present, saves on S; new tools/gen-painttut-pcx.py generates the bundled samples/PAINTTUT.PCX tutorial image (6,010 B). Epic P2 closes; headline interactive checkpoint: dpaint PAINTTUT.PCX loads the tutorial, lets you draw with all 8 tools, flood-fill enclosed regions, undo, save, exit, re-launch — edits persist; gfxtest PAINTTUT.PCX opens the same file (cross-app PCX compatibility verified). 4 pre-merge bugs caught at the P2 checkpoint, all 8042/IRQ-related: (1) chrome- redraw wiped the loaded canvas; (2) lowercase filenames silently missed — uppercased argv[1] + "No file: NAME" flash; (3) keyboard-IRQ-1 ate the mouse ACK byte during slow PCX load — pm_irq_cli() + manual 8042 drain before pm_mouse_init; (4) mouse IRQ 12 firing during the ~400-syscall save's BIOS-mode windows wedged the 8042's single output buffer — new drain_8042_post_syscall() helper at the end of save + load. P6 doc-only close.
kernel 26,080 → 26,088 B (+8 B, 51 sects) · pass=47 unchanged · +DPAINT PASS (#27) · DPAINT.COM 14,592 B · PAINTTUT.PCX 6,010 B
May 13, 2026
arc-close · ✓ Graphics SDK
Graphics SDK shipped — gfx.h primitives + 8×8 font + PCX codec + mouse cursor
7 slices in a single day (May 13): G0 doc kickoff, G1 feature/gfx-primitives-font ships new apps/c/lib/gfx.{h,c} (~400 LOC) — direct framebuffer write at linear 0xA0000, rect-fill, Bresenham line with octant dispatch, midpoint circle, scanline convex polygon fill, VGA DAC palette via 0x3C8/0x3C9 with vertical-retrace sync, cursor save/draw/restore trio — plus apps/c/lib/gfx_font.c (~120 LOC) with a public-domain 8×8 BIOS-style font embedded as static const uint8_t font_8x8[128][8] covering ASCII 0x20-0x7F. G2 feature/gfx-pcx-format adds apps/c/lib/gfx_pcx.c (~220 LOC) — PCX 256-color v5 encode + decode, 128-byte header + RLE scanlines + 769-byte palette appendix, files byte-compatible with GIMP / ImageMagick / IrfanView. G3 ships apps/c/gfxtest/main.c (~180 LOC) running the deterministic visual demo (palette ramp → lines → circles → polygons → text banner → cursor blit round-trip → PCX encode/decode pixel-compare) and emitting the GFX MODE → PRIMS → FONT → CURSOR → PCX → PASS lifecycle marker chain. Epic G1 closes; interactive checkpoint #1 cleared. G4 feature/gfx-mouse-cursor extends gfxtest with a mouse command-tail mode — cursor follows the PS/2 pointer in real time via the IRQ-12 callback, LMB-drag draws Bresenham lines into the framebuffer, ESC saves the canvas as gfxdemo.pcx; gfxtest <file>.pcx reloads and displays. Bundled samples/GFXDEMO.PCX (17 KiB hand-painted) ships so the app does something interesting from a fresh boot. Epic G2 closes; headline interactive checkpoint: cursor + drag-to-draw + save + reload + the bundled-image viewer all work end-to-end. 3 pre-merge bugs at the G4 checkpoint: (1) pm_poll_key() round-tripped through real-mode with the PIC remapped, dropping mouse IRQs — replaced with a direct-port kbd_poll_esc() inside a pm_irq_cli/sti bracket; (2) the kernel keyboard-drain stub at IDT[0x21] ate the ESC scancode — fix: mask IRQ 1 on mouse-mode entry, unmask on exit; (3) same root cause for the PCX viewer — applied the same pattern + a new kbd_poll_any_press helper. G5 doc-only close. Zero kernel changes across the whole arc; selftest stays at pass=47; the entire arc is a pure libc extension + one app.
kernel 26,072 → 26,080 B (+8 B, 51 sects) · pass=47 unchanged · +GFX PASS (#26) · GFXTEST.COM 9,272 B · GFXDEMO.PCX 17,113 B · FAT16 SHA: d6344a13…
May 13, 2026
arc-close · ✓ MIDI / MUS
MIDI/MUS arc closed — Doom plays "At Doom's Gate" through OPL2
7 slices across 3 epics in a single day (May 13): M0 doc kickoff, M1 audio_opl_gm.{h,c} — 16-patch hand-crafted GM-subset bank baked as static const, 9-voice allocator with note-stealing, MIDI-note-to-freq conversion, velocity-to-carrier-level mapping, thin audio_midi_note_on / _note_off / _program_change / _controller surface on top of the Sound A3 OPL2 primitives. M2 pm_music.{h,c} — opaque pm_music_handle_t, format-agnostic playback engine driven by pm_music_poll(uint32_t ms_now), shared 24,576-entry event pool. MUS header decoder + variable-length time delta + 6 event-type dispatcher. M3 musictst sample app — embeds a ~200-byte MUS payload as static const uint8_t, plays the test pattern, emits the MUSIC INIT PASS → LOAD PASS → PLAY PASS → MUSIC PASS marker chain. Epic M1 closes; interactive checkpoint #1 passed (audible OPL2 chord + arpeggio). M4 smf_parser.c — Standard MIDI File support behind pm_music_load_smf: header + track chunks, variable-length quantities, running status, tempo meta events, SMF Format 1 multi-track merge by absolute ms time at load. samples/HELLO.MID bundled. M5 apps/c/doom/shim/sound.c — replace the no-op DG_music_module with real wiring: RegisterSong → pm_music_load_mus, PlaySong → pm_music_play, StopSong → pm_music_stop, I_UpdateSound → pm_music_poll(DG_GetTicksMs()) per gametick. Epic M2 closes; interactive headline demo: launch doom on a --keep-music-stripped DOOM1.WAD — E1M1 plays "At Doom's Gate", E1M2 plays "The Imp's Song", pause/resume via Doom's menu cuts sustained voices + rebases the poll clock. Pre-merge bug fixed at the checkpoint: PM_MUSIC_MAX_EVENTS = 8192 overflowed on D_E1M2 (8,372 events) and D_E1M8 (16,742); bumped ceiling to 24,576 and added tools/mus-event-count.py. M6 doc-only close. Zero kernel changes: entire arc lives in libc + sample + Doom shim. MPU-401, GENMIDI parser, OPL3, pitch bend deferred to v2.
FAT16 SHA: f796471d… · DOOM.COM 293,152 → 296,736 B (+3,584 B) · MUSICTST.COM 6,825 B · pass=47 fail=0 · 25 smoke markers
May 12, 2026
arc-close · ✓ PS/2 Mouse
PS/2 mouse arc closed — IRQ 12 packets, wiggle in QEMU works
5 slices in a single day (May 12) right after the IRQ arc landed: M0 doc, M1 new irq_mouse_drain_stub at IDT[0x2C] in kernel/idt.asm (~12 LOC) + idt_init Phase-3b override + selftest_mouse_drain_stub_installed (selftest pass=46 → 47) — closes the latent IRQ-arc-era gap where the default irq_slave_eoi_stub didn't drain port 0x60 (a stray mouse byte during a pmode hop would wedge the 8042). M2 new apps/c/lib/pm_mouse.{h,c} (~280 LOC combined) — public surface pm_mouse_init / _register(cb) / _stop, full 8042 aux init sequence (enable aux 0xA8 → 0x64, set aux-IRQ-enable config bit, mouse reset 0xD4 0xFF, set defaults 0xD4 0xF6, enable streaming 0xD4 0xF4), 3-byte standard PS/2 packet state machine in BSS with sync-bit-recovery, IRQ thunk that drains 0x60 and invokes the user callback. M3 apps/c/mousetst/main.c (~75 LOC) — packet-count + cumulative dx/dy + button state handler, 2-second sleep window, emits MOUSE INIT PASS then MOUSE PASS. Interactive checkpoint cleared: wiggle produces 20–100+ packets, button bit set when held; shell still responsive afterwards (drain stub coexists cleanly with the keyboard drain at IDT[0x21]). M4 doc-only close. Zero pre-merge bugs across all 3 implementation slices. Mouse-driven paint upgrade, mouse-look for Doom, and IntelliMouse 4-byte wheel packet deferred to v2.
kernel 25,960 → 26,064 B (+104 B, 51 sects) · pass=46 → 47 · MOUSE PASS (#24) · MOUSETST 4,164 B
May 12, 2026
arc-close · ✓ Pmode IRQ
Pmode IRQ infrastructure shipped — PIC remap + IDT + IRQ-driven SB16 streaming
7 slices across 3 epics in a single day (May 12): I0 doc, I1.1 feature/irq-thunk-iflags-preserve audits all four CR0-toggle sites (pmode_roundtrip_test, pmode_copy_linear, program_launch_pmode_app, pmode_syscall_thunk) and wraps each cli/sti window in pushf/popf — without this an app's sti would not survive its first syscall. I1.2 kernel/pic.asm — per-pmode-hop 8259 PIC remap from BIOS default to standard 0x20–0x2F, undone on every pmode exit so BIOS shell ISRs that out 0x20, 0x20 as a literal EOI keep working. I1.3 kernel/idt.asm — 256-entry pmode IDT in kernel BSS loaded via lidt on pmode entry, replaced by real-mode IVT (base=0 limit=0x3FF) on exit. Spurious-IRQ-aware default stubs poll the master ISR via out 0x0B; in 0x20 before EOI. Two trampoline templates: master-only EOI for IRQ 0–7, slave + master cascade for IRQ 8–15. CPU exceptions 0x00–0x1F land in halt stubs that emit a serial marker instead of triple-faulting. I1.4 kernel/api.asm — 3 new int 60h services (AH=0x22 set_irq_handler, 0x23 irq_unmask, 0x24 irq_mask), libc surface apps/c/lib/pm_irq.{h,c} (~140 LOC) gives pm_irq_register / _unmask / _mask / _sti / _cli. Sample irqping registers an IRQ-0 PIT handler, sleeps 1 second, asserts counter > 10, emits IRQPING PASS. Pre-merge bug caught: 2 KiB initialised idt_table declaration pushed kernel.bin past the boot-loader's 53-sector safe limit; fix was a bare label past all initialised data so NASM truncates the flat-binary output. Follow-up after Epic I1 close: irq_keyboard_drain_stub at IDT[0x21] drains port 0x60 during pmode hops. I2 audio_sb16_stream_start_irq + _stream_stop_irq (~140 LOC additions to audio_sb16.c) plus pcmstrm2 sample app (~110 LOC) — same 5-second sweep as pcmstrm but main loop is literally audio_delay_ms(5000), no polling. First non-polled hardware driver in deca-tiny-os. STREAM IRQ PASS. I3 doc-only close. Same arc retired the FAT12 floppy: all smoke gates are now FAT16-HDD-only; regression PS↔Python parity still runs both kinds.
kernel 24,496 → 25,960 B (48 → 51 sects) · pass=42 → 46 fail=0 · +IRQPING PASS · +STREAM IRQ PASS · FAT12 retired
May 11, 2026
arc-close · ✓ Sound
Sound arc closed — PC speaker + SB16 PCM + OPL2 FM + SMS music + Doom SFX
11 slices across 7 epics over 4 days (May 8–11): A0 doc kickoff, A1 PC speaker driver + bell, A2.1 SB16 detection + sbinfo, A2.2 single-shot 8-bit mono PCM via DSP cmd 0xC0 + pcmplay, A3.1 OPL2 detection + register write + opltest, A3.2 OPL2 voice abstraction (note_on/off, freq→fnum/block, SBI patch loader) + fmtest, A4.1 streaming PCM via auto-init DMA (DSP cmd 0xC6) + pcmstrm, A4.2 4-channel software mixer + mixtest, A5 SMS audio re-emergence with PSG tone-channel music audible via double-buffered async chunked DMA at 44100 Hz, A6 Doom audio re-emergence with DMX-format WAD SFX routed through 4-channel mixer at 11025 Hz, A7 doc-only close. All-app-side libc design — apps drive hardware at CPL=0 with raw port I/O, no kernel changes, no new int 60h services, selftest unchanged at pass=42 fail=0. audio.h (277 LOC) + audio_pcspk.c (95) + audio_sb16.c (519) + audio_opl.c (265) = 1156 LOC of new libc. 7 new smoke markers (BELL/SB16 INIT/PCM/OPL TONE/FM/STREAM/MIX) bring the total from 14 → 21. QEMU launch flags extended to attach SB16 + AdLib on i440fx (modern QEMU doesn't auto-attach). No vendored upstream — driver code written from scratch against Creative's SB Hardware Programming Guide + Yamaha YM3812 datasheet + OSDev wiki.
audio libc 1156 LOC · 7 sample apps · 21 smoke markers · kernel unchanged · pass=42 fail=0
May 11, 2026
Epic A6 · ✓ Doom SFX
Doom SFX wired — gun, shotgun, doors, monsters all audible
feature/sound-doom-emerge adds new apps/c/doom/shim/sound.c (~300 LOC) implementing Doom's I_StartSound / I_StopSound / I_UpdateSound by routing DMX-format SFX from the WAD's DS*-prefix lumps through audio_sb16_mixer_play at 11025 Hz mono. One mixer channel per Doom sound channel up to 4; oldest-stolen on overflow. DMX format is well-documented (8-bit unsigned mono PCM with a 16-byte header); upstream's i_sound.c already parses it — we just hook the platform side. Audible during gameplay: pistol fire, shotgun, chainsaw, doors opening, item pickups, imp/zombie/cacodemon grunts, barrel explosions, all mixing cleanly up to 4 simultaneous SFX. Two minor edits to vendored Doom: (1) i_sound.c SDL_mixer.h include removed (we don't have SDL), (2) build flag -DFEATURE_SOUND added to scripts/c-toolchain-flags.txt so the engine's sound_modules[] table links our module. Music stays no-op stub — MUS / MIDI synth is a separate future arc. Headline finding: Doom DMX works cleanly through the same async-chunked-DMA path SMS uses, narrowing the SMS-PSG noise diagnosis from "generic int16→uint8 issue" to "SMS engine mixer tone-vs-noise balance."
DOOM.COM 293,088 → 293,152 B (+64 B · --gc-sections) · 4-ch mixer @ 11025 Hz · DMX SFX wired · Epic A6 closes
May 10, 2026
Epic A5 · ✓ SMS music
SMS music re-emerged — Black Belt's PSG tone channels now audible
feature/sound-sms-emerge rewrites apps/c/sms/shim/audio.c (was an 18-line empty stub from SMS arc S2.2). Engine's snd.output[] per-scanline samples (already-mixed mono PCM) route into the SB16 PCM driver at 44100 Hz mono via double-buffered async chunked DMA. Why not the A4.1 streaming pattern: initial pass used auto-init DMA streaming, but the continuous DMA bus traffic starved QEMU's VGA mode 13h refresh path on TCG — frames stuttered visibly while audio played. Switched to chunked async with idle windows between chunks; VGA updates fit cleanly in the gaps. New audio_sb16_play_8m_async + audio_sb16_wait_8m driver helpers added to audio_sb16.c for the bursty pattern. No engine source changes. PSG noise-channel SFX shelved as a known limitation — Black Belt's snare / punch / kick "rashbang" feels suppressed; diagnostic chain ruled out inter-chunk gap, int16→uint8 quantisation, and QEMU DAC filtering. Remaining theory: the engine's per-scanline mixer dominates noise content with tone-channel energy during the L+R→mono average. Documented in docs/issues-and-fixes.md; not a regression — same engine bind that powered the silent SMS arc, plus a working audio bridge for the parts that survive the mix.
SMS.COM 91,908 → 92,580 B (+672 B) · 44100 Hz mono PCM · async chunked DMA · Epic A5 closes
May 08, 2026
arc-close · ✓ SMS
SMS port arc closed — Black Belt, Double Dragon, SF2 all playable
Slice S6 ships the doc-only close: docs/sms-port-roadmap.md Arc Closure section (12-row as-shipped table + 5 SOLID landings + 6 reusable patterns + 7 deferred items), apps/README.md new sms row alongside doom + lua, docs/milestone-checks.md new "SMS Port Arc Baseline" block. SMS Plus GX vendored at pinned commit 6dc7119f… — 35 source files (~12 K LOC), license stack GPLv2+ for engine + sound glue + FM + PSG, BSD-3-Clause for the Z80 CPU core; combined sms.com is GPL-2. Single binary handles SMS + Game Gear + SG-1000 via ROM-extension dispatch. 6 in-tree edits documented in UPSTREAM.md: shared.h platform-include strip + 4 no-ops, PSG variant swap (mame_sn76489 → crabemu_sn76489), signal.h empty libc stub, loadrom.c cart.rom alloc routed through sms_shim_alloc, _8BPP_COLOR palette path, LSB_FIRST 1 for PAIR-union endianness on i386. 11 slices across 2 days (S0–S2.2 + S3.1–S3.2 on May 7; S4 + S5.x + S6 on May 8). Multi-ROM Epic S5 checkpoint: 8/9 ROMs working (bb.sms Black Belt, ab.sms, dd.sms Double Dragon, ds.sms, gauntlet.sms, sb2.sms, sd.sms, sf2.sms Street Fighter 2 attract demo); zool.sms hangs partway into level 1, parked as v1-out-of-scope (CodeMasters / FM / timing).
FAT12 SHA: c5f0cca0… · FAT16 SHA: 4142e51a… · SMS.COM 91,908 B · pass=42 fail=0 · 14 fixture markers
May 08, 2026
real-ROM · ✓ Epic S5
SMS S5.2: three-iteration bring-up — blank screen → pink sprites → diskfull
apps/c/sms/main.c::run_rom replaces S5.1's 30-silent-frames sanity check with a real interactive frame loop after system_poweron: each iteration polls input (sms_shim_poll_input updates input.pad[0] / input.system; returns 1 on Esc), runs system_frame(0) (engine renders ~262 scanlines into bitmap.data with the VDP fully active), sms_shim_draw_frame (palette upload + letterbox memcpy), sms_shim_pace_frame(16) (~60 fps). Esc breaks the loop and runs sms_shim_shutdown + system_shutdown. Three-iteration bring-up: (iter 1) cart loaded but screen black — Z80 PAIR union .b.l/.b.h byte halves point at wrong offsets without LSB_FIRST on i386. Fix: in-tree edit to shared.h adds #define LSB_FIRST 1. (iter 2) cart ran fine but sprites had pink/magenta overlay — engine/render.c:258 stamps bit 0x40 on sprite pixels as a "this-is-a-sprite" marker; the marker bit leaked through to DAC indices 0x500x5F (default VGA pink/magenta strip). Fix: shim/draw.c per-row loop masks each byte with PIXEL_MASK (0x1F). (iter 3) FAT12 diskfull-smoke timed out after a 128 KiB user homebrew ROM bloated the floppy past its known starting state. Fix: build pipeline now always skips .sms/.gg/.sg on FAT12 (category gate, not size gate); FAT16 32 MiB partition bundles them all. Black Belt is the third-iteration headline demo.
SMS.COM 91,940 → 91,908 B (-32) · in-tree edit count: 5 → 6 · 8/9 ROMs · pass=42 fail=0
May 07, 2026
arc-close · ✓ libm
libm port arc closed — every Lua math.* call now numerically correct
Slice M5 deletes apps/c/lib/math.c entirely (the last holdout was fabs/fabsf, both vendored from msun's s_fabs.c/s_fabsf.c in this slice). math.h prologue refreshed to remove the "STUBS" warning and point at libm/UPSTREAM.md. New samples/MATHFIX.LUA (~70 lines) end-to-end fixture exercises math.pi, sqrt, floor/ceil, fmod, sin/cos/tan (at π/2, π, π/4), atan/asin/acos, exp/log (one-arg + two-arg), and the ^ operator. Each via a tolerance helper; emits LUA MATH PASS as the smoke marker. libm port arc closes after 8 slices in a single day graduating the captured "real-libm follow-on" idea from the Lua arc. All 26 stub functions from L2.1 are now real: sqrt, floor, ceil, fmod, modf, frexp, ldexp, sin, cos, tan, asin, acos, atan, atan2, exp, log, log2, log10, pow + their *f twins. 38 .c files + 3 helpers vendored at FreeBSD-src commit 640af0d9…. 96 C-side libm fixtures + 25 Lua-side math.* assertions. Precision: 1e-10 (double) / 1e-5 (float for irrational args).
vendor: FreeBSD msun · BSD-2-Clause · LUA.COM 140,800 B · 13 fixture markers · pass=42 fail=0
May 07, 2026
libm vendor
libm M1–M4: vendor scaffold + sqrt/decomp/trig/inverse-trig/exp/log/pow families
Five implementation slices that fed M5: M1 shipped the vendor scaffold + sqrt/sqrtf (e_sqrt.c 455 lines bit-by-bit double sqrt), the new apps/c/lib/libm/ directory, UPSTREAM.md, plus boundary shims sys/cdefs.h + machine/endian.h. M2 vendored the decomposition family (12 sources): floor/ceil/fmod/modf/frexp/ldexp + their *f twins, plus scalbn/scalbnf (msun aliases ldexp→scalbn via Symbol.map; trivial 4-line wrappers replace the alias). M3.1 vendored the trig core (15 sources including 6 k_* kernel helpers + 3 range-reduction helpers with a 256-entry ipio2[] table for high-precision argument reduction of large angles). M3.2 vendored inverse trig (8 sources): atan/atan2/asin/acos + their *f twins. M4.1 vendored the exp/log family (8 sources + k_log.h/k_logf.h kernel-helper headers). M4.2 vendored pow/powf — the most edge-case-heavy libm function — handling pow(±0, n), pow(±1, ±Inf), integer vs non-integer n, fractional bases via exp/log composition. 9 pre-merge link-bring-up iterations across M3.1+M3.2+M4.2 caught and fixed (__always_inline shim, M_* BSD constants, __strong_reference no-op, multiple-definition for sin/cos/tan from leftover L2.1 stubs, spurious k_exp.c/k_expf.c imports that #include <complex.h>).
38 vendored .c files · LUA.COM 124,370 → 140,864 B across M1..M4 · DOOM.COM unchanged · pass=42 fail=0
May 07, 2026
arc-close · ✓ Lua
Lua 5.4 port arc closed — `lua` is a runnable shell command on both image kinds
8 slices in a single day: L0 doc kickoff, L1.1 strtoul/strtod/strtof, L1.2 %f/%g in vsnprintf, L2.1 libm stub-body surface (deliberately-wrong constant-return bodies with a loud header comment), L2.2 Lua 5.4.7 verbatim vendor drop with five upstream edits (4 luaconf.h overrides + 1 linit.c table trim) all documented in UPSTREAM.md, L3.1 -e "code" evaluation via luaL_dostring, L3.2 file-based execution + samples/HELLO.LUA/FIB.LUA/TABLE.LUA/COROUTI.LUA/STRING.LUA, L4 doc-only close. LUA.COM 123,922 B at L4. Per locked decisions: lua_Number = LUA_FLOAT_FLOAT (32-bit), lua_Integer = long (32-bit), LUAI_NUMFMT = "%f", stdlibs included (base/coroutine/table/string/math/utf8/debug), stdlibs dropped (io/os/loadlib), PMAPP_MEM_SIZE(2 MiB). 4 pre-merge link-bring-up iterations across L2.2+L3.1 caught and fixed (signal.h, BUFSIZ, locale.h, then strerror/strspn/strpbrk/memchr/strcoll/freopen). Eight prior arcs (C toolchain, HDD/FAT16, big-app loader, per-app mem-size, -flto, 32-bit file API, plus L1+L2 epics) all required.
FAT16 SHA: 4a140cc4… · LUA.COM 123,922 B · 11 fixture markers · pass=42 fail=0 · 5 in-tree edits documented
May 07, 2026
libc gap-fill
printf %f / %g + strtoul / strtod — libc primitives Lua's lexer needs
Lua arc slices L1.1 + L1.2 land the libc gap-fill that L2.x's link bring-up depends on. L1.1: new apps/c/lib/strtoul.c mirrors strtol.c with an unsigned long overflow clamp at ULONG_MAX; the existing atof stub replaced by a real strtod in new strtod.c (integer + fractional + optional decimal exponent — hex floats 0x1p-3 documented as out of scope). strtof follows in L2.2 as a single-precision wrapper for Lua's lua_str2number under LUA_FLOAT_FLOAT. clibtest gains LIBTEST_STRTOUL + LIBTEST_STRTOD fixtures. L1.2: vsnprintf.c grows a new emit_double() helper (~95 lines) plus case 'f'/'F'/'g'/'G' arms. Strategy: round via + 0.5 * 10^-precision, split into integer + fractional parts, render integer via the existing u_to_str, extract fractional digits one-at-a-time. Default precision 6 per C standard. %g trims trailing fractional zeros + the lone trailing dot. cprintf gains 6 fixtures, asserts CPRINTF FLOAT PASS. Both gap-fills benefit every future C app, not just Lua — doom.com picked up the float-printf path through -flto reachability and grew +736 B with no behaviour change.
cprintf: 3140 → 4324 B (+1184 B) · doom: 290528 → 291264 B · CPRINTF FLOAT PASS · pass=42 fail=0
May 06, 2026
arc-close · ✓ DOOM
DOOM end-to-end: E1M1–E1M9 playable from the shell
doomgeneric port slice D5.2 (feature/doom-wad-load) graduates from infra-only to end-to-end runtime verified. Dropped a real shareware DOOM1.WAD (4196020 B / 1264 lumps) into apps/c/doom/data/, ran tools/strip-wad.py --keep-music --keep-all-e1 to produce a 3611776 B / 1151-lump tiny WAD, built os-hdd.img, launched QEMU. Engine init takes ~25 s on QEMU TCG (W_Init + lump caching), then TITLEPIC renders cleanly with the correct PLAYPAL palette, menu navigates with arrows + ENTER, and all nine Episode 1 maps (E1M1 Hangar through E1M8 Phobos Anomaly + secret E1M9 Military Base) load and play smoothly with no lag. ESC quits cleanly to the shell. Custom stripped DOOM1.WAD ships alongside the engine (no SFX, no music, no demo lumps); any unmodified Doom 1.9 IWAD also works because Doom's D_IdentifyVersion uses a hard-coded filename list. FAT16-HDD-only — the 1.44 MB FAT12 floppy can't fit a 3.6 MB WAD. In-tree patch to upstream engine/d_main.c::D_DoAdvanceDemo substitutes TITLEPIC for cases 1/3/5/6 so the title loop never tries to load stripped demo lumps. Four pre-merge debug iterations (filename mismatch, missing music, missing demo lump, demo-version mismatch) all written up.
FAT16 SHA: ccb41d14… · pass=42 fail=0 · DOOM.COM 290528 B · DOOM1.WAD 3.6 MB · 9/9 maps
May 06, 2026
arc · 32-bit FS
32-bit File API mini-arc — lifting the 64 KiB position cap
Mid-doomgeneric arc, slice D5.1 surfaced a hard blocker: the kernel's per-handle file size and position were both 16-bit, so fseek(stdc_wad->fstream, infotableofs, SEEK_SET) against a multi-MB WAD truncated. Mini-arc opened, lifted, closed in one day across three slices (F0 doc / F1 atomic implementation / F2 arc-close docs). F1 widened api_handle_size / api_handle_pos / api_return_cx from words to dwords, rewrote api_open_handle_slot to read the full 4-byte FAT directory size, rewrote api_lseek_handle to take a signed 32-bit offset packed in DX:CX, dropped the legacy cmp word [es:si+30], 0 append-rejection that capped append targets at 64 KiB, and lifted fseek's 32767 cap in libc. New selftest opens DOOM.COM and seeks past 64 KiB to gate the path. Behaviour-preserving for the 13 existing apps — they all live below 64 KiB and the 32-bit math reduces to the old 16-bit behaviour for files that small by construction.
FAT16 SHA: ccb41d14… · pass=42 fail=0 · kernel 24496 B / 48 sectors · cap 64 KiB → 4 GiB
May 06, 2026
doom infra · 8 slices
doomgeneric infra: D1–D5 — multifile builds, libc gap-fill, vendored engine, mode 13h, palette, input, timing, WAD bundling
The eight slices that fed the final D5.2 verification, all shipped on the same day: D1.1 feature/build-multi-file-app teaches both PS and Python builders to glob apps/c/<name>/**/*.c recursively; D1.2 feature/libc-doom-gaps adds errno, <assert.h>, strdup, strncasecmp, strcasecmp with clibtest fixtures; D2.1 feature/doom-vendor-source drops doomgeneric verbatim under apps/c/doom/engine/; D2.2 feature/doom-zmalloc wires Z_Init to a single pm_alloc(8 * 1024 * 1024); D3.1 feature/doom-drawframe implements DG_DrawFrame via 8-bit indexed framebuffer memcpy to 0xA0000; D3.2 feature/doom-palette wires PLAYPAL → VGA DAC via raw out 0x3C8 / 0x3C9; D4.1 feature/doom-input wires DG_GetKey to the PS/2 keyboard via raw port-0x60 polling with set-1 + extended scancode lookup tables; D4.2 feature/doom-timing wires DG_GetTicksMs / DG_SleepMs to the 8254 PIT counter 0; D5.1 feature/doom-tiny-wad ships tools/strip-wad.py + the build-script glob for apps/c/<name>/data/*.WAD. Three pre-Doom infra arcs (FAT16 32M HDD, big-app loader to 4 MiB, per-app PMAPP_MEM_SIZE override, optional -flto) shipped earlier the same day to unblock all of the above.
DOOM.COM grew 0 → 290528 B across 8 slices · pass=41 fail=0 (pre-F1) · all PS↔Python parity green
May 05, 2026
polish
where dynamic format label — FAT12 vs FAT16
Mini-slice caught while exploring the freshly-merged FAT16 HDD image: where was printing Apps: FAT12 root, ... even on FAT16. Twelve lines of asm in kernel/memory.asm split the prefix and branched on [fs_format] to pick where_apps_fat12_label, where_apps_fat16_label, or where_apps_fat_unknown. Behaviour-preserving for the FAT12 floppy; FAT16 HDD now reports FAT16 root. Smoke confirms 4× FAT12 root on floppy, 2× FAT16 root on HDD.
FAT12 SHA: 4f… · FAT16 SHA: 2aff2082… · pass=41 fail=0 · kernel 23754 B / 47 sectors
May 05, 2026
arc-close
HDD + FAT16 arc closed — same kernel, two image flavours
Closed the entire HDD + FAT16 arc (16 slices, 6 epics, 4 interactive-demo points, 2 pre-merge bugs caught and documented). Same kernel binary now boots from a 1.44 MB FAT12 floppy or a 32 MB FAT16 HDD image; the runtime auto-detects format from the BPB; six dispatcher entry points route format-specific code to kernel/fs_fat12.asm (8 routines) or kernel/fs_fat16.asm (6 routines). FAT16 32 MB activates one-sector caches for both FAT and root since neither fits in the conventional-memory buffers. Both PS↔Python builders agree byte-identically on both kinds; both regression and smoke gate both kinds. Doom-port unblocked: the 32 MB partition fits the full Ultimate Doom WAD comfortably.
FAT12 SHA: 187d7f53… · FAT16 SHA: e3c86c5e… · pass=41 fail=0 · kernel 23697 B / 47 sectors
May 05, 2026
arc-close
C toolchain + libc arc closed — 5 C apps, ~50 libc fns
Largest single arc to date (19 slices, 6 epics, 5 interactive-demo points, 3 pre-merge bugs caught and documented). Shipped: i686-elf-gcc 13.2.0 host integration, a 27-line linker script, a hand-rolled crt0.asm emitting the 'DECA32',0,0 magic, and a freestanding libc subset (~50 functions across <string.h>, <ctype.h>, <stdlib.h>, <setjmp.h>, <stdio.h>, <time.h>) backed by 6 new pmode thunk dispatch cases plus 1 new int 60h opcode (AH=0x19 lseek). 5 C apps: chello, cstub, clibtest, cprintf, cwrite. Real-mode + pmode NASM apps untouched; arc was purely additive.
SHA-256: 92493fc1f65c3f958230e49b8f260c02fbbde8d3fd1a83e30d54ba42782f9660 · pass=41 fail=0 · kernel 22441 B / 44 sectors
May 04, 2026
libc
vsnprintf core — 270 lines feeding the printf family
Largest single C source file in the arc. %d %u %x %X %s %c %ld %lu %lx %p %% with width and precision (both supporting * for variadic fetch), - left-align, 0 zero-pad, length modifier l. Truncation contract: always NUL-terminates when cap > 0; returns the count that would have been written if unbounded. printf, snprintf, fprintf, vprintf, vfprintf wrappers all dispatch through it. Stdout-side stdio bundled in slice 3.5; cprintf exercises end-to-end against SAMPLES.TXT.
pass=41 fail=0 · cprintf 3620 B · clibtest 11200 B
May 04, 2026
kernel
int 60h AH=0x19 lseek — first kernel-side change of the C arc
New per-handle api_handle_start_cluster array preserves the file's first cluster across reads (the existing api_handle_cluster field gets overwritten as reads progress). api_lseek_handle validates handle, computes new pos per SEEK_SET / SEEK_CUR / SEEK_END, range-checks, then re-walks the FAT chain forward pos / 512 steps via fs_get_next_cluster. Kernel grew past 43 sectors; recurring three-location lockstep applied (boot.asm + kernel/memory.asm + smoke marker). Selftest pass=40 → pass=41.
pass=41 fail=0 · kernel 22441 B / 44 sectors
May 03, 2026
evening
pmpong arc closed — playable Pong, zero kernel changes
Closed Epic 3 with score render, scoring, and frozen game-over state. Across the entire 8-slice arc, the kernel binary stayed at exactly 21767 B / 43 sectors — the pmode-extender ABI absorbed every requirement, no new int 60h services. Pre-merge bug count for the arc: zero (the EBX-reserved + imul-not-mul + DL-not-BL rules from prior pmode arcs front-loaded the discipline). pmpong.com: 1569 bytes.
SHA-256: 2759b2a90293f1a2fc2533545bb37e00d6acec5a8c8f0c24368d21c620dde766 · pass=40 fail=0 · 118 clusters, no leaks
May 03, 2026
afternoon
Pmode extender arc closed — 16 MiB bump pool, three pmode apps
Wired up the syscall thunk file/handle path through a DMA bounce buffer, added a per-launch bump allocator over a 16 MiB extended-memory pool at 0x200000, and shipped pmcat (file viewer) and pmsieve (64 KiB pmem write/read pass) alongside pmhello. Caught one register-clobber bug pre-merge: the verify loop in pmsieve used BL as a byte counter, silently corrupting the kinfo struct pointer in EBX — switched to DL and codified the rule.
SHA-256: 8162075cbef96ac09212006e388f1a7e7befe364e124cc7b25cc1382b77225bb · pass=40 fail=0 · 112 clusters, no leaks
May 03, 2026
morning
Pmode app loader + syscall thunk — first end-to-end pmode app
program_launch_pmode_app recognises the in-band 8-byte 'DECA32',0,0 magic, copies the image to linear 0x100000, builds the kinfo struct, mode-switches via GDT_CODE32_SELECTOR, and jmp [entry]s into the app. App's ret pops a pre-pushed exit-thunk address that mode-switches back. pmhello prints "PMHELLO PASS" via the new syscall thunk (mode-switch back to real, dispatch int 60h AH=0x00, mode-switch forward, return).
pass=39 fail=0 · pmhello.com: 110 bytes
May 02, 2026
night
GDT + pmode round-trip — first time in 32-bit mode
Built a 5-entry GDT (null, code32, data32, code16, data16) in kernel/pmode.asm, ran pmode_init at boot. pmode_roundtrip_test is the first code in the project to actually enter protected mode: cli, save SS:SP, set CR0.PE, far-jmp to GDT_CODE32_SELECTOR, write 0xDECA32 at linear 0x100000, read it back, mode-switch home, restore. Interrupts stay masked the entire time (no IDT in v1).
pass=35 fail=0 · kernel 19996 B / 40 sectors
May 02, 2026
afternoon
A20 enable — four-step fallback chain
CPU level detection (NT-bit flip for ≥386, CPUID family for ≥586), INT 15h E820 → E801 → AH=88 extended-memory map, then a four-step A20 enable: BIOS INT 15h AX=2401h → fast A20 (port 0x92) → KBC (port 0x60/0x64), each followed by a wraparound check at 0xFFFF:0x0510. meminfo now reports CPU level, extended memory, and the A20 enable method.
pass=33 fail=0 · kernel 19580 B / 39 sectors
May 02, 2026
11:47 AM
640K smoke & doc polish — real-mode arc closed
Hardened serial smoke with exact meminfo and where markers for every memory region. Confirmed the real-mode 640K arc complete; decided shortly afterward to open the pmode-extender arc rather than continue deeper memory work.
SHA-256: 1a15ccffac774c13746892ca9d1c3cf40333afb3b229eac645d660eb45e93a2d · pass=30 fail=0 · 108 clusters, 41 root files
May 02, 2026
09:22 AM
App segment migration — apps now at 2000:0100
Moved APP_LOAD_SEGMENT to 2000h. Updated int 60h to marshal app DS:SI pointers across the segment boundary. Added KERNEL_REGION_LIMIT 0xE000 as an independent build guard.
SHA-256: 6eaec9c6ae79a310b67ec397c348e22b128160b09cbaee99a47827f4727595ac · pass=30 fail=0
May 02, 2026
06:15 AM
Streamed app loader — cluster-direct into app window
Removed whole-file staging buffer dependency for app loading. FAT clusters now stream directly to APP_LOAD_SEGMENT:APP_LOAD_OFFSET. Added BIGLOAD.COM (8238 bytes) to prove loading beyond the old 8KB file-buffer cap.
SHA-256: c9b484e0b87a131e68c465a59ec0e94f941c47326e2787dc97f24fcda2fef1c7 · pass=29 fail=0
May 01, 2026
04:30 PM
Subdirectory cd — one-level navigation working
cd name enters a root-level directory, cd .. returns to root. Extended the transient mkdir selftest to navigate into MKDIRTST, assert cwd_depth=1, print PWD /MKDIRTST, and return.
SHA-256: cb57f5eb5115c6384722319ed037267d4fcfd3312bc886d2df35769706fb2aca · pass=24 fail=0
May 01, 2026
01:15 PM
mkdir root — first FAT12 directory creation
Added mkdir name: allocate cluster, write initialized . and .. entries, create root entry with attribute 0x10. Split root lookup into any-entry vs regular-file-only paths.
SHA-256: cad64b258c2f3dab183dfc64d4de53a9e2ddfa254211c5da8be2d8313bec33cd · pass=24 fail=0 · 89 clusters, no leaks
April 30, 2026
09:45 AM
BASIC v1 complete — GOSUB, FOR/NEXT, SAVE/LOAD
Closed the BASIC interpreter arc with bounded four-frame GOSUB/RETURN and FOR/NEXT stacks, plain-text SAVE/LOAD via temp-first FAT12, command-tail autorun, and a bundled BASIC demo pack with SIGN.BAS, SQUARE.BAS, SUM3.BAS.
SHA-256: 3d3e2cf29af25bb0817493ac7a135da0967ece86f927dc6981e0af4b674914a5 · pass=21 fail=0

What comes
next

The project runs on deterministic feature arcs. Each arc is documented before it starts, and completed with regression evidence before the next begins. Here's the frontier.

S1
Sidequest: MBR + partition table
Promote os-hdd.img from a bare-FAT16 partition to a real MBR layout: 446-byte stage-1 boot in LBA 0, 4-entry partition table, one active primary FAT16 partition starting at LBA 63. The VBR (relocated boot-fat16.asm) reads the kernel via partition-relative LBA arithmetic. Bare-partition path stays shipped; MBR path is opt-in via a new -Mbr build flag. ~7 slices / 2 epics. Standalone — depends only on the shipped HDD arc.
Sidequest: doomgeneric port — SHIPPED
Doom plays. All nine Episode 1 maps (E1M1 Hangar through E1M9 Military Base) load and run smoothly from a fresh os-hdd.img boot. Stripped 3.6 MB DOOM1.WAD ships alongside DOOM.COM; any unmodified Doom 1.9 IWAD also works. Follow-ons: PC-speaker driver, save games, doom -iwad MYWAD.WAD mod-loader, second engine port (Wolf3D? Hexen?). All deferred until a downstream demands them.
Sidequest: Lua 5.4 port — SHIPPED
Lua runs. lua -e "code" evaluates inline; lua HELLO.LUA runs scripts from disk. Five sample fixtures (HELLO/FIB/TABLE/COROUTI/STRING) all pass on both image kinds. Stdlibs included: base, coroutine, table, string, math, utf8, debug. Follow-ons: line-edited interactive REPL (-i flag), io.* over libc stdio, os.clock/os.time, Lua bytecode cache (.luc), promote .LUA scripts as a shell-auto-run type, second scripting language port (Wren? Tcl? MicroPython?).
Sidequest: real libm — SHIPPED
Math is real. All 26 stub libm functions vendored verbatim from FreeBSD msun (BSD-2-Clause) at pinned commit 640af0d9…. math.sqrt(2)1.4142136; math.sin(math.pi/2)1; 2^10 = 1024; math.exp(1)2.7182818. 96 C-side fixtures + 25 Lua-side math.* assertions verified end-to-end via MATHFIX.LUA. Follow-ons: hypot/cbrt/asinh on demand, real errno reporting from libm calls.
Sidequest: SMS emulator — SHIPPED
SMS plays — now with music. SMS Plus GX vendored verbatim at pinned commit 6dc7119f…. Single sms binary handles Master System + Game Gear + SG-1000 via ROM-extension dispatch. Z80 @ 3.58 MHz + TI VDP + SN76489 PSG, 256×192 letterboxed into mode 13h, frame-paced 60 fps, PS/2 d-pad. Audio re-emerged in Sound arc Epic A5 — PSG tone-channel music audible in Black Belt etc. via double-buffered async chunked DMA at 44100 Hz. 8 of 9 ROMs working; Zool hangs partway level 1. PSG noise-channel SFX shelved (engine-mixer balance limitation).
Sidequest: audio stack — SHIPPED
The OS makes noise now. Complete DOS-era audio stack as a pure libc extension — PC speaker (PIT counter 2 + port 0x61), Sound Blaster 16 (DSP 0x220 + 8237 DMA + 5–44 kHz 8-bit mono PCM, single-shot + auto-init streaming + 4-channel software mixer), Yamaha OPL2 FM (0x388 + 9 voices). 1156 LOC of new libc, 7 sample apps (bell, sbinfo, pcmplay, opltest, fmtest, pcmstrm, mixtest), 7 new smoke markers (14 → 21 total). Doom plays with SFX; SMS plays with music. Zero kernel changes.
Sidequest: pmode IRQ infrastructure — SHIPPED
The kernel takes interrupts now. Per-pmode-hop 8259 PIC remap (BIOS default ↔ 0x20–0x2F), 256-entry pmode IDT with spurious-aware default stubs + slave-cascade EOI trampolines, syscall thunk with pushf/popf IF preservation, CPU-exception halt stubs. 3 new int 60h services (AH=0x22/0x23/0x24) and libc surface pm_irq.{h,c}. irqping proves the chain via PIT IRQ 0; pcmstrm2 is the first non-polled hardware driver in the system — SB16 5-second sweep with a literally idle main loop. Kernel 48 → 51 sectors. Follow-ons: MPU-401 UART driver, keyboard IRQ ringbuffer, async pmode console.
Sidequest: PS/2 mouse driver — SHIPPED
Pointer input works. 8042 aux-port init, IRQ 12 registered via pm_irq_register, 3-byte standard PS/2 packet state machine with sync-bit recovery, pm_mouse_init / _register(cb) / _stop libc surface. mousetst reports packet count + cumulative dx/dy + button state across a 2-second wiggle window. New irq_mouse_drain_stub at IDT[0x2C] closes the IRQ-arc-era gap where a stray mouse byte could wedge the 8042. Follow-ons: mouse-driven paint upgrade, mouse-look for Doom, IntelliMouse 4-byte wheel packet, cursor compositor, game-port joystick.
Sidequest: MIDI / MUS playback — SHIPPED
Doom has music. 16-patch hand-crafted OPL2 GM-subset synth (audio_opl_gm.{h,c}), format-agnostic playback engine driven by pm_music_poll(ms_now) with a 24,576-entry event pool, MUS parser (Doom's compact 140 Hz format, 6 event types) and Standard MIDI File parser (variable-length quantities, running status, tempo meta events, SMF Format 1 multi-track merge by absolute ms time). musictst plays an embedded MUS pattern; musictst hello.mid plays a bundled SMF. Doom shim wires DG_music_modulepm_music_*; E1M1 plays "At Doom's Gate", E1M2 plays "The Imp's Song" through OPL2. Zero kernel changes.
Sidequest: Graphics SDK — SHIPPED
One library, every graphics app. apps/c/lib/gfx.{h,c} + gfx_font.c + gfx_pcx.c (~740 LOC across three SRP-split files): direct framebuffer write at 0xA0000, Bresenham lines, midpoint circles, scanline convex polygon fill, VGA DAC palette with retrace sync, cursor save/draw/restore, embedded 8×8 BIOS-style font (96 ASCII glyphs), and a full PCX 256-color v5 encoder/decoder (files round-trip cleanly with GIMP and ImageMagick). The gfxtest consumer proves every primitive end-to-end with a deterministic marker chain; gfxtest mouse shows cursor + LMB-drag-to-draw + ESC-to-PCX-save via the mouse-arc driver. Bundled samples/GFXDEMO.PCX ships a 17 KiB hand-painted demo image. Zero kernel changes; entire arc is a pure libc extension + one app.
Sidequest: Deca Paint v1 — SHIPPED
First productivity-grade end-user app. A Deluxe-Paint-style drawing app riding on the Graphics SDK + the mouse + the PCX codec. 8 tools (pencil / line / rect / filled-rect / circle / filled-circle / flood-bucket / eraser), 16-named-color palette shared byte-for-byte with gfxtest (cross-app PCX compatibility), mouse-driven UI chrome with click-to-select tool and color, keyboard shortcuts on every action (1-8 colors, P/L/R/Shift+R/C/Shift+C/B/E tools, Z undo, S save, ESC quit), single-level undo via 64 KiB BSS snapshot, command-tail-filename save/load (dpaint MYPAINT.PCX). Bundled samples/PAINTTUT.PCX tutorial. Zero kernel changes, zero libc additions — purely an app riding on the prior arcs. Follow-ons: multi-level undo, in-app filename dialog (blocked by pm_kbd_register kernel API), palette editor, text tool, color cycling, BMP/IFF LBM formats, selection tools.
01
feature/cwd-file-commands
Resume the shell-directories arc. Route touch, append, del, stat, and streamed type through the current working directory so subdirectory-aware file operations actually work.
02
feature/path-qualified-read
Add dir name/file, type name/file, and stat name/file path-qualified reads for one-level subdirectory access.
03
feature/rmdir-empty-dir
rmdir name for empty root-level directories with clean failure for non-empty targets. FAT cluster release and root entry removal with full rollback through the new format dispatcher.
04
After-arcs: FAT32 / LBA-mode / real iron
The dispatcher leaves FAT32 as a future module behind one new dispatch arm. INT 13h Extended (LBA) lifts the CHS cylinder-1024 ceiling for partitions > 504 MB. Real-hardware boot validation via USB-stick install (gated on MBR sidequest). All deferred until a downstream demands them.

Follow the build.

Every commit is documented. Every bug is written up. Every regression is caught before it ships. This is what building an OS in public looks like.