Carry project memory in the repo, with tools/memory-sync.sh
Claude Code keeps per-project memory at <home>/.claude/projects/<key>/,
where <key> is the project's absolute path with every separator and the
drive colon turned into a dash. That makes it both rename-fragile and
machine-specific, and this project has hit each of those once:
- D:\DEV\Schunix -> D:\DEV\SchOS changed the key, and 43 memories were
invisible until moved across by hand.
- Development happens from Windows and from Linux, and each looks in a
different place for the same checkout.
Committing .claude/memory/ fixes both -- git carries it across renames
and between hosts -- and `import` puts it wherever the local machine
expects. .claude/settings.local.json stays ignored: those are per-machine
permissions.
The colon maps to a dash rather than being dropped. Deleting it gives
D-DEV-SchOS, which matches nothing and silently finds zero memories; that
was the first attempt. On Git Bash the key comes from `cygpath -w`,
because Claude Code is a Windows program and sees D:\DEV\SchOS, not
/d/DEV/SchOS.
This commit is contained in:
parent
cda5908660
commit
3182da3965
47 changed files with 2810 additions and 0 deletions
44
.claude/memory/MEMORY.md
Normal file
44
.claude/memory/MEMORY.md
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
- [SchOS rename + git](schos-rename-and-git.md) — Schunix is SchOS and the kernel is SchIFT; the tree is under local git, and the two bugs the rename caused.
|
||||||
|
- [Cross-platform build](schos-cross-platform-build.md) - Windows, Linux, macOS and BSD all build it; clang everywhere, and the four host assumptions that used to break.
|
||||||
|
- [Memory sync](schos-memory-sync.md) - memory lives in the repo at .claude/memory/; tools/memory-sync.sh moves it between machines.
|
||||||
|
- [Schunix build environment](schunix-build-environment.md) — builds natively on Windows with clang/lld + mingw + Git Bash; WSL is gone, and the five latent bugs the move exposed.
|
||||||
|
- [Syscalls: one table](schunix-syscall-single-table.md) — declare in kernel/sys/table.inc only; linux_abi.c is generated, never hand-edited.
|
||||||
|
- [Linux ABI decision](schunix-linux-abi-decision.md) — the native syscall ABI is being dropped; Linux numbering becomes the only ABI.
|
||||||
|
- [Rewrite priorities](schunix-rewrite-priorities.md) — what actually blocks the OS, in order, plus a confirmed SMP bug in the syscall entry path.
|
||||||
|
- [SMP and RAM-floor bugs](schunix-smp-and-memory-bugs.md) — SMP works (five bugs, and why __thread is wrong for per-CPU data); the 224 MB floor is gone.
|
||||||
|
- [SchFS and ramdisk root](schfs-v2-and-ramdisk.md) — the agreed BeFS/ZFS/btrfs synthesis, and the completed `make ramroot` + mkfs dedup work.
|
||||||
|
- [SchFS B-tree, phase 1](schfs-btree-phase1.md) — the CoW B-tree is built and passing; run `make schfs-test` after touching it.
|
||||||
|
- [SchFS phase 2: real mount](schfs-phase2-mount.md) — mkschfs writes images, the kernel mounts them read-only; `make schfs-check`, plus four traps worth remembering.
|
||||||
|
- [SchFS phase 3: writes](schfs-phase3-writes.md) — full write API, free map derived not stored, and `mount -t schfs` works; the VFS now dispatches through `struct fs_ops`.
|
||||||
|
- [DIRSIZ 14 -> 30](schunix-dirsiz-14.md) — filenames used to truncate silently; fixed by holding the HTree bucket at 256 bytes.
|
||||||
|
- [Linux open flags](schunix-linux-open-flags.md) — include/fcntl.h must hold Linux's values; a mismatch made `>>` truncate.
|
||||||
|
- [Shared memory and mmap](schunix-shm-and-mmap.md) — shm + file-backed mmap work; pages you don't own must never reach kfree.
|
||||||
|
- [Wall clock](schunix-wall-clock.md) — the RTC was never read, so `date` said 1970; kernel/core/time.c fixes it, plus the stale-image trap.
|
||||||
|
- [TTF rasteriser](schunix-ttf-rasteriser.md) — glyf outlines (not CFF, and why); the composite-glyph trap that loses the letter "i".
|
||||||
|
- [Text rendering](schunix-text-rendering.md) — the layer replacing the 8x16 cell grid; metrics, surfaces, coverage blending, UTF-8.
|
||||||
|
- [Console and TTY](schunix-console-tty.md) — ANSI parser, termios, UTF-8; the font is CP437 not Latin-1, plus five bugs that masqueraded as broken commands.
|
||||||
|
- [Build speed](schunix-build-speed.md) — no-op make is ~2s now; what made it 1m48 and the traps that would undo it.
|
||||||
|
- [PCI and drivers](schunix-pci-drivers.md) — bus enumeration, and where a driver that finds its own hardware goes.
|
||||||
|
- [ISO boot](schunix-iso-boot.md) — `make iso`; needs El Torito no-emulation + ATAPI because the loader never uses INT 13h.
|
||||||
|
- [Ramdisk root sizing](schunix-ramdisk-root-sizing.md) — the "disk full" was DOOM WADs, not FSSIZE; three Makefile knobs, and why excluding them is a licensing rule.
|
||||||
|
- [Framebuffer device](schunix-framebuffer-device.md) — /dev/fb0 is real and speaks Linux fbdev; never assume where the framebuffer lives.
|
||||||
|
- [Crash screen](schunix-crash-screen.md) — PSOGM lives in kernel/core/panic.c; `echo c > /proc/sysrq` to trigger, `make crashshot` to photograph.
|
||||||
|
- [QR encoder](schunix-qr-encoder.md) — kernel/core/qr.c for crash reports; `make qrcheck`, and the segno padding bug that is not ours.
|
||||||
|
- [Boot splash and logo](schunix-boot-splash-logo.md) — the loader draws the logo, not the kernel; STAGE2_SECTORS was in five places and is now in one.
|
||||||
|
- [SchAPI direction](schunix-schapi-direction.md) — Schunix as a macOS-shaped *system*; the stable ABI is a library, not the syscall table.
|
||||||
|
- [Roadmap and projects/](schunix-roadmap-and-projects.md) — the agreed order through to Schcom/SchWM/SchDE; the GUI and installer live outside the OS tree.
|
||||||
|
- [Driver infrastructure](schunix-driver-infrastructure.md) — dynamic IRQ registration, a DMA arena, PCI capabilities; PCI interrupts are level/active-low.
|
||||||
|
- [Network stack choice](schunix-network-stack-choice.md) — don't write TCP; lwIP over FreeBSD over NetBSD, with the licence evidence.
|
||||||
|
- [e1000 and driver layout](schunix-e1000-and-driver-layout.md) — the NIC driver, drivers/ split by device class, and virtio-gpu as the QEMU default.
|
||||||
|
- [virtio-gpu](schunix-virtio-gpu.md) — the display class's second implementation; run with `-vga std -device virtio-gpu-pci`.
|
||||||
|
- [Device model](schunix-device-model.md) — one bus-agnostic list at /proc/devices; bus is provenance, class is capability, and the settled Linux-driver decision.
|
||||||
|
- [usb.ids](schunix-usb-ids.md) — shipped as /usr/share/usb.ids, BSD option of a dual licence; cached, not fetched per build.
|
||||||
|
- [USB and xHCI](schunix-usb-xhci.md) — enumeration and control transfers work; written from the spec, because porting BSD's stack means porting newbus first.
|
||||||
|
- [Licensing](schunix-licensing.md) — Apache-2.0, copyright Nexoniarz; BSD drivers are usable and Linux ones are not.
|
||||||
|
- [Layout migration](schunix-layout-migration.md) — the bt/ → ports/ + lib/ + cmd/ restructure and the two Makefile traps it exposed.
|
||||||
|
- [Verify by driving the shell](schunix-verify-by-driving-shell.md) — booting to a prompt proves nothing; run real commands over serial with paced input.
|
||||||
|
- [/dev and /proc](schunix-devfs-procfs.md) — how both are built; ps and top work now, one cosmetic gap left.
|
||||||
|
- [TTY foreground pgrp coupling](schunix-tty-fg-pgrp-coupling.md) — TIOCGPGRP and getpgrp() must agree or the shell spins forever; how to spot that hang.
|
||||||
|
- [Sockets and evdev](schunix-sockets-and-evdev.md) — AF_UNIX with one table lock, /dev/input/eventN, and why keycodes need a table for USB but not PS/2.
|
||||||
|
- [The removed acceptance test](schunix-doom-acceptance-test.md) — deleted 2026-08-08 with its trademark references; what it found and the two techniques worth keeping.
|
||||||
|
- [Sharing preference](nexoniarz-sharing-preference.md) — never push or publish; unfinished work stays local.
|
||||||
21
.claude/memory/nexoniarz-sharing-preference.md
Normal file
21
.claude/memory/nexoniarz-sharing-preference.md
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
---
|
||||||
|
name: nexoniarz-sharing-preference
|
||||||
|
description: Does not want unfinished work shared or published anywhere
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: user
|
||||||
|
originSessionId: d1a1ea76-f919-411c-b0ef-0e1bc5064563
|
||||||
|
modified: 2026-08-01T17:16:58.232Z
|
||||||
|
---
|
||||||
|
|
||||||
|
Strongly does not want to share work that isn't finished. Stated while
|
||||||
|
explaining an aversion to git ("you need to login to github or gitlab to use
|
||||||
|
it and this is stupid... I don't like to share stuff that aren't done").
|
||||||
|
|
||||||
|
**Why:** it's about publishing, not about version control — the objection was
|
||||||
|
based on believing git requires a hosted account.
|
||||||
|
|
||||||
|
**How to apply:** never push, publish, or otherwise send this work anywhere
|
||||||
|
without being asked. Local-only git is fine and was accepted once clarified
|
||||||
|
that `git init` needs no account and uploads nothing — but do not assume that
|
||||||
|
extends to any remote. Ask before anything leaves the machine.
|
||||||
42
.claude/memory/schfs-btree-phase1.md
Normal file
42
.claude/memory/schfs-btree-phase1.md
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
---
|
||||||
|
name: schfs-btree-phase1
|
||||||
|
description: "SchFS phase 1 (the CoW B-tree) is built and passing; run `make schfs-test` after touching it"
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: a665e14b-86bd-4b41-a228-51377f3e9225
|
||||||
|
modified: 2026-08-01T20:34:28.236Z
|
||||||
|
---
|
||||||
|
|
||||||
|
The filesystem is called **SchFS** — not "SchFS v2". Design: `docs/architecture/schfs.md`.
|
||||||
|
|
||||||
|
**Phase 1 is done** (2026-08-01): the copy-on-write B-tree exists and passes its
|
||||||
|
torture harness.
|
||||||
|
|
||||||
|
- `include/schfs.h` — on-disk format: 4096-byte blocks, 24-byte
|
||||||
|
`(objectid, type, offset)` keys, node header with a self-pointer, uberblock ring.
|
||||||
|
- `kernel/fs/schfs/btree.{c,h}` — portable C against an abstract
|
||||||
|
`struct schfs_bdev`, so the same code runs on a host file and in the kernel.
|
||||||
|
Not yet linked into the kernel.
|
||||||
|
- `tools/schfs_test.c` — the harness. **`make schfs-test`** builds and runs it.
|
||||||
|
|
||||||
|
Measured geometry: max inline value 1016 bytes, fanout 84 (internal) / 127 (leaf).
|
||||||
|
|
||||||
|
**Run `make schfs-test` after any change to btree.c.** It checks against a
|
||||||
|
reference model, verifies structure, simulates a crash by discarding
|
||||||
|
uncommitted writes, and confirms both a bit flip and a misdirected write are
|
||||||
|
detected.
|
||||||
|
|
||||||
|
**A test-design trap worth remembering:** the harness originally used
|
||||||
|
`schfs_key_cmp` inside its own reference model. Deliberately breaking that
|
||||||
|
function then broke the tree *and* the oracle identically, and every check
|
||||||
|
still passed — the suite could not fail. The model now has its own
|
||||||
|
`ref_key_cmp` written out longhand. Mutation-test any new oracle the same way
|
||||||
|
before trusting it.
|
||||||
|
|
||||||
|
Known and deliberate: delete does not merge or rebalance underfull nodes. The
|
||||||
|
tree stays correct and sorted; merging is phase 3.
|
||||||
|
|
||||||
|
Next: phase 2 (read-only mount of a v2 image), then writes/transaction groups,
|
||||||
|
then extents. Per-pid `/proc` for ps/top should ride along with that VFS work —
|
||||||
|
see [[schunix-devfs-procfs]].
|
||||||
45
.claude/memory/schfs-phase2-mount.md
Normal file
45
.claude/memory/schfs-phase2-mount.md
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
---
|
||||||
|
name: schfs-phase2-mount
|
||||||
|
description: "SchFS phase 2 is done — the kernel mounts real images read-only; run `make schfs-check` after touching kernel/fs/schfs"
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: a665e14b-86bd-4b41-a228-51377f3e9225
|
||||||
|
modified: 2026-08-03T12:37:43.088Z
|
||||||
|
---
|
||||||
|
|
||||||
|
**Phase 2 done (2026-08-02):** the kernel mounts a SchFS volume read-only at
|
||||||
|
boot and it is verified end to end. Phase 1 notes: [[schfs-btree-phase1]].
|
||||||
|
|
||||||
|
Layout — everything but `kmount.c` compiles into *both* the kernel and the host
|
||||||
|
tools, so writer and reader cannot drift:
|
||||||
|
|
||||||
|
- `kernel/fs/schfs/vol.c` uberblock ring, `obj.c` read side, `objw.c` write side
|
||||||
|
(host only until phase 3), `kmount.c` buffer-cache bdev + boot mount.
|
||||||
|
- `tools/mkschfs.c` builds images, `tools/schfs_fsck.c` verifies/lists/extracts.
|
||||||
|
|
||||||
|
**Run `make schfs-check`** (B-tree + round-trip) after any change under
|
||||||
|
`kernel/fs/schfs`. `tools/mutate.sh` injects six bugs and fails if any survives.
|
||||||
|
|
||||||
|
Boot-testing: `make ramroot` first (root from RAM leaves device 1 free), then
|
||||||
|
`make qemu-schfs`, then `cat /proc/schfs`. Unattended:
|
||||||
|
`tools\bootcheck.ps1 -SchFS` drives the shell over serial and asserts on the
|
||||||
|
transcript — see [[schunix-verify-by-driving-shell]].
|
||||||
|
|
||||||
|
**Traps that cost real time, all fixed:**
|
||||||
|
|
||||||
|
- The root's object id must be *reserved*; starting `next_objectid` at it made
|
||||||
|
the first directory become the root, so it contained itself.
|
||||||
|
- Range callbacks get pointers into the cursor's block buffer, which any nested
|
||||||
|
lookup overwrites. Directory iteration copies each entry out first.
|
||||||
|
- A 4096-byte block will not fit on the 4096-byte kernel stack (KSTACKSIZE).
|
||||||
|
The kstack canary catches it as "kstack overflow".
|
||||||
|
- Extents only merge if the whole run is allocated before any tree write.
|
||||||
|
|
||||||
|
Full write-up, including why the design doc's phase list changed, is in
|
||||||
|
`docs/architecture/schfs.md` §8-9.
|
||||||
|
|
||||||
|
**Superseded on 2026-08-03 by [[schfs-phase3-writes]]** — writes are done, and
|
||||||
|
free space is *derived from the tree at mount*, not tracked as tree keys as
|
||||||
|
planned here. The VFS work is still outstanding, and also unblocks per-pid
|
||||||
|
`/proc` for ps/top ([[schunix-devfs-procfs]]).
|
||||||
194
.claude/memory/schfs-phase3-writes.md
Normal file
194
.claude/memory/schfs-phase3-writes.md
Normal file
|
|
@ -0,0 +1,194 @@
|
||||||
|
---
|
||||||
|
name: schfs-phase3-writes
|
||||||
|
description: "SchFS phase 3 is done — full write API, in-RAM allocation map derived from the tree; the only gap left is VFS integration"
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: a665e14b-86bd-4b41-a228-51377f3e9225
|
||||||
|
modified: 2026-08-03T13:59:08.496Z
|
||||||
|
---
|
||||||
|
|
||||||
|
**Phase 3 done (2026-08-03), including VFS integration.** SchFS is a complete,
|
||||||
|
mountable, read-write filesystem. Follows [[schfs-phase2-mount]].
|
||||||
|
|
||||||
|
```
|
||||||
|
mkdir /mnt && mount -t schfs /dev/hdb /mnt
|
||||||
|
```
|
||||||
|
|
||||||
|
then ordinary `ls`/`cat`/`>`/`>>`/`mkdir`/`cp`/`rm`/`rmdir` all work on it.
|
||||||
|
|
||||||
|
**The VFS now has a real dispatch layer**: `struct fs_ops` in `include/file.h`,
|
||||||
|
one table per filesystem, chosen by `iget()` from the device number and hung off
|
||||||
|
the inode. v1's implementations kept their bodies and are named `v1_*`; SchFS's
|
||||||
|
live in `kernel/fs/schfs/vfs.c`. **Adding a filesystem is a new table, not
|
||||||
|
another branch in fifteen functions.** `getdents64` is an fs_op for the same
|
||||||
|
reason — v1's `struct dirent` (16-bit inum, 14-byte name) cannot represent a
|
||||||
|
SchFS entry, so each filesystem emits the record stream itself.
|
||||||
|
|
||||||
|
Mounting is a substitution during path resolution (`mount_add`/`mount_cross` in
|
||||||
|
`fs.c`), recorded nowhere on disk. `..` at a mounted root has to be redirected
|
||||||
|
out explicitly, or the mount becomes a subtree you can enter and never leave.
|
||||||
|
|
||||||
|
**Two design decisions that contradict the older design doc — the doc is now
|
||||||
|
right, older notes are not:**
|
||||||
|
|
||||||
|
- *Free space is NOT tracked as tree keys.* Allocating from a tree-resident map
|
||||||
|
mutates the tree, which allocates, which changes the map. Instead the map is
|
||||||
|
**derived** at mount by walking the tree (`kernel/fs/schfs/space.c`) and lives
|
||||||
|
only in RAM. It cannot disagree with the filesystem because it is computed
|
||||||
|
from it. This is what caps a mounted volume at 64 GiB (`SCHFS_SPACE_PAGES`).
|
||||||
|
- *Data blocks carry a generation too* (`schfs_extent.gen`, 64-bit). Same rule
|
||||||
|
as tree nodes: a run stamped with the group being built may be overwritten in
|
||||||
|
place. Without it every 100-byte write copied a 4 KB block and split an
|
||||||
|
extent.
|
||||||
|
|
||||||
|
**Testing.** `make schfs-check` now runs three suites — add `schfs-objtest`
|
||||||
|
(`tools/schfs_objtest.c`, memory-backed, covers files/dirs/space accounting) to
|
||||||
|
the two that existed. `sh tools/mutate.sh` injects nine bugs; all nine must be
|
||||||
|
caught.
|
||||||
|
|
||||||
|
**The two tests that actually earn their keep**, both found by mutation testing
|
||||||
|
after everything else passed:
|
||||||
|
|
||||||
|
- Filling and emptying the volume *three times on one mount* and checking each
|
||||||
|
round stores the same amount. A leaked bitmap heals on remount, so a
|
||||||
|
remount-based check cannot see it; only spending the space again can.
|
||||||
|
- Overwriting a block **after a commit**. Within one transaction group both the
|
||||||
|
tree and the data rewrite in place, so a test that never commits never runs
|
||||||
|
the copy-on-write path at all.
|
||||||
|
|
||||||
|
`struct schfs_vol` is **not copyable** — `tree.dev` points at its own `dev`
|
||||||
|
field. Open into the storage it will live in.
|
||||||
|
|
||||||
|
## Migrating root off the xv6 legacy FS (2026-08-09)
|
||||||
|
|
||||||
|
Nexoniarz: kill the legacy FS, keep SchFS ("SchFS is goat"). Agreed — and it is
|
||||||
|
a **migration, not a rewrite**: SchFS is 4,204 lines across 7 files against
|
||||||
|
`kernel/fs/fs.c`'s 1,830, and it already builds images, mounts, and dispatches
|
||||||
|
through `struct fs_ops`. Every xv6 legacy problem (DIRSIZ 14, the 16-byte
|
||||||
|
dirent, the HTree byte layout, `ushort inum`) lives below a filesystem that has
|
||||||
|
already been replaced but never promoted.
|
||||||
|
|
||||||
|
**The one line holding xv6 in charge:**
|
||||||
|
|
||||||
|
kernel/fs/fs.c:1642: ip = (*spath == '/') ? iget(ROOTDEV, ROOTINO) : idup(start);
|
||||||
|
|
||||||
|
**Blocker 1, now cleared: `mkschfs` would not build on Windows.** It needed
|
||||||
|
`sys/sysmacros.h` (major/minor) and `lstat`/`readlink`/`S_ISLNK`, none of which
|
||||||
|
mingw has. Stubbed under `__MINGW32__` — honest here because Windows has no
|
||||||
|
mknod and `find sysroot -type l` returns nothing, so neither device nodes nor
|
||||||
|
symlinks can appear in the tree being imaged. **If symlinks are ever staged**
|
||||||
|
(procfs wants `/proc/<pid>/exe` to be one) this host can no longer build a
|
||||||
|
faithful image and the stub must become a real reparse-point read.
|
||||||
|
`make build/schfs.img` now works: 586 of 8000 blocks, 2344 KB, generation 2.
|
||||||
|
|
||||||
|
**Blocker 2, confirmed and not started:** booting with schfs.img as the root
|
||||||
|
disk panics `fsinit: invalid file system` — v1 `fsinit()` probes ROOTDEV for a
|
||||||
|
v1 superblock and halts. So the order is:
|
||||||
|
|
||||||
|
1. `fsinit`/`namei` reach the root through `fs_ops` instead of
|
||||||
|
`iget(ROOTDEV, ROOTINO)`.
|
||||||
|
2. A `ramroot`-equivalent SchFS image, so root can be swapped without a disk.
|
||||||
|
3. Point `ROOTDEV` at the SchFS volume; keep v1 mountable until it is proven.
|
||||||
|
4. Delete `fs.c`.
|
||||||
|
|
||||||
|
**Do this with a fresh session.** Swapping the root filesystem is the change
|
||||||
|
most able to leave an unbootable image, and step 1 wants room to boot, fail and
|
||||||
|
retry.
|
||||||
|
|
||||||
|
|
||||||
|
### Sizing step 1 (2026-08-09) -- it is small
|
||||||
|
|
||||||
|
`ops_for(dev)` in `kernel/fs/fs.c:278` already dispatches the whole VFS:
|
||||||
|
|
||||||
|
if (dev == SCHFS_DEV) return &schfs_ops;
|
||||||
|
if (dev == PROCFS_DEV) return &procfs_ops;
|
||||||
|
return &v1_ops; // legacy, only because it is the default
|
||||||
|
|
||||||
|
`struct fs_ops` (include/file.h:35) is a complete vtable -- iload, istore,
|
||||||
|
ifree, read, write, truncate, dirops -- and **SchFS implements all of it**. So
|
||||||
|
`fs.c` is no longer "the filesystem"; it is the fallback branch. Killing it is
|
||||||
|
three edits, not a rewrite:
|
||||||
|
|
||||||
|
1. `fsinit()` stops requiring a v1 superblock on ROOTDEV; call `schfs_kmount()`.
|
||||||
|
2. `ROOTDEV` becomes `SCHFS_DEV` so `ops_for` returns `schfs_ops` for `/`.
|
||||||
|
3. `namei`'s `iget(ROOTDEV, ROOTINO)` uses `schfs_kroot()` for the root inode.
|
||||||
|
|
||||||
|
Then `v1_ops` has no caller and `fs.c` can go. Keep a build flag or a second
|
||||||
|
disk to boot v1 until SchFS has carried `/` through a full bootcheck run.
|
||||||
|
|
||||||
|
Nexoniarz calls the legacy one "ShitFS v1" -- worth knowing that "SchFS v1" in
|
||||||
|
older notes means the *xv6-derived* filesystem, not early SchFS.
|
||||||
|
|
||||||
|
### The recycle bin, removed 2026-08-09
|
||||||
|
|
||||||
|
`sys_delete` (syscall 51, NO_LINUX) set an on-disk inode flag `showmode = 0`,
|
||||||
|
and `fileread`/`filewrite` then refused every operation with a printf. A
|
||||||
|
"deleted" file still existed, still had its name, still held its blocks, and
|
||||||
|
answered read() with -1 -- and since nothing was freed, deleting filled the disk
|
||||||
|
faster. Both gates removed; the syscall entry and the on-disk field stay only
|
||||||
|
because pulling them touches the inode layout, and both die with fs.c.
|
||||||
|
|
||||||
|
|
||||||
|
### SchFS is the root filesystem -- default since 2026-08-09
|
||||||
|
|
||||||
|
`make` builds it, `make qemu` boots it, `tools/bootcheck.ps1` tests it. There is
|
||||||
|
no opt-in switch any more: `schfsroot`, `-DSCHFS_ROOT` and the `-SchFS` /
|
||||||
|
`-SchFSRoot` bootcheck switches are all gone. schunix.img depends on
|
||||||
|
`build/schfs.img`, and a kernel handed anything else on device 1 panics in
|
||||||
|
forkret().
|
||||||
|
|
||||||
|
Full suite green -- base, -Tty, -Ps, -Sock, -Fb, -Usb, -Evdev.
|
||||||
|
|
||||||
|
Three edits carried `/` over:
|
||||||
|
|
||||||
|
1. `kernel/proc/proc.c` forkret: `schfs_kmount(ROOTDEV)` + `schfs_kmounted()`
|
||||||
|
check, replacing `fsinit(ROOTDEV)`. Mounted there and not in main() because
|
||||||
|
mounting reads blocks, bread() sleeps, and sleeping needs a process context.
|
||||||
|
`schfs_kmount` returns **void** -- it reports failure by leaving the volume
|
||||||
|
unmounted, because elsewhere it is a probe.
|
||||||
|
2. `kernel/fs/fs.c` namei: root is `iget(SCHFS_DEV, schfs_kroot())`, so
|
||||||
|
`ops_for()` routes `/` through `schfs_ops`. SCHFS_DEV is 64 -- a synthetic
|
||||||
|
number for ops dispatch, not the disk the volume lives on.
|
||||||
|
3. `kernel/fs/log.c`: `begin_op`/`end_op` return immediately, the rest is
|
||||||
|
`#if 0`. The v1 log made multi-block v1 updates atomic; SchFS gets that from
|
||||||
|
CoW. They stay as no-ops because sys_fs.c calls them in dozens of places.
|
||||||
|
|
||||||
|
**The bug that cost the most, and would cost it again:** `sinit()` sets pid 1's
|
||||||
|
cwd with `namei("/")` in `main()`, *before* forkret mounts anything. The old v1
|
||||||
|
root survived that because `iget(ROOTDEV, ROOTINO)` is two compile-time
|
||||||
|
constants that touch no disk -- the inode fills in lazily at the first ilock.
|
||||||
|
SchFS reads its root object id off the uberblock, so pid 1 got a cwd built from
|
||||||
|
an unmounted volume. Absolute paths never consult cwd, so `exec("/bin/init")`
|
||||||
|
worked and everything looked healthy; the first *relative* lookup is init's
|
||||||
|
`open("console")`, which failed, so init had no fd 0/1/2, so every shell it
|
||||||
|
spawned exited at once and init respawned it forever. **Silent boot, no
|
||||||
|
`init: ready`, indistinguishable from a hang.** Fixed by re-pointing
|
||||||
|
`myproc()->cwd` after the mount.
|
||||||
|
|
||||||
|
Also: the kernel execs `/bin/init` (`lib/initcode.S`), not `/init`. Probing the
|
||||||
|
wrong path made lookup look broken when it was not.
|
||||||
|
|
||||||
|
**Still to do -- the v1 format code is compiled but unreachable from `/`.**
|
||||||
|
`fs.c` cannot simply be deleted: iget/ilock/iput/namei/readi, the whole VFS
|
||||||
|
layer, live in it too. Only the v1 on-disk format goes -- fsinit, the
|
||||||
|
superblock, bmap/balloc, `v1_ops`, and the third-party HTree/extent code -- plus
|
||||||
|
log.c, tools/mkfs.c and build/fs.img. The MIT attribution in NOTICE stays
|
||||||
|
regardless ([[schunix-licensing]]).
|
||||||
|
fs.img is still built: the -Usb suite uses it as the USB stick.
|
||||||
|
|
||||||
|
### Two harness bugs found doing this, both of which faked OS failures
|
||||||
|
|
||||||
|
- **bootcheck slept a flat 4s between commands.** `ls /bin | wc -l` overran it,
|
||||||
|
so the next command was typed while the shell was busy -- the tty echoed it,
|
||||||
|
no reader took it, and it landed concatenated onto the following one. The
|
||||||
|
transcript read `cat /proc/pcicat /proc/schfs` and three PCI checks failed,
|
||||||
|
looking exactly like a broken PCI driver. Now it waits for the prompt by
|
||||||
|
polling the logfile QEMU is writing. (It still must not read QEMU's stdout --
|
||||||
|
that deadlocks, see the script header.)
|
||||||
|
- **The USB stick was `$hdb`**, so the root volume was attached twice, once as
|
||||||
|
a disk and once over USB. That failed the storage checks *and* the xHCI
|
||||||
|
interrupt check. The stick is pinned to `build/fs.img` now and -Usb is green.
|
||||||
|
|
||||||
|
**The general lesson, twice over: a red check is not evidence until the harness
|
||||||
|
is ruled out.** Both of these looked like driver bugs.
|
||||||
39
.claude/memory/schfs-v2-and-ramdisk.md
Normal file
39
.claude/memory/schfs-v2-and-ramdisk.md
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
---
|
||||||
|
name: schfs-v2-and-ramdisk
|
||||||
|
description: The agreed SchFS direction and the ramdisk-root work completed 2026-08-01
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: d1a1ea76-f919-411c-b0ef-0e1bc5064563
|
||||||
|
modified: 2026-08-01T17:52:30.145Z
|
||||||
|
---
|
||||||
|
|
||||||
|
Agreed plan for Schunix's filesystem and installer, and step 1 is done.
|
||||||
|
|
||||||
|
**Order agreed 2026-08-01:** ramdisk root → `struct proc`/exit codes/pgrp →
|
||||||
|
SchFS → mkfs into `cmd/` → installer + framebuffer GUI.
|
||||||
|
|
||||||
|
**SchFS** keeps the name and is designed in `docs/architecture/schfs-v2.md`:
|
||||||
|
copy-on-write + checksum-in-parent-pointer + uberblock array (ZFS), one CoW
|
||||||
|
B-tree reused for every key type + extents with refcounts (btrfs), attribute
|
||||||
|
indexes for the "SQL-like" query feel (BeFS). Explicitly NOT doing: storage
|
||||||
|
pools/RAID-Z, online format conversion, dedup, or an actual query language —
|
||||||
|
the useful part of "database filesystem" is the index, not SQL. WinFS is the
|
||||||
|
cautionary tale; BeFS is the scale model.
|
||||||
|
|
||||||
|
**Done — ramdisk root.** `make ramroot` links a full SCHFS image into the
|
||||||
|
kernel and boots to a shell with no data disk attached (verified). Plain `make`
|
||||||
|
is unchanged. `kernel/dev/ramdisk.c` + a `bdevrw()` dispatcher in `bio.c`.
|
||||||
|
|
||||||
|
**Also done — `tools/mkfs` content dedup**, which is what made the ramdisk
|
||||||
|
practical: identical files now share one inode with real hard links
|
||||||
|
(`nlink > 1`). The 57 toybox applet copies collapsed from ~44 MB to one copy —
|
||||||
|
inodes 73 → 19, used blocks ~47,000 → ~3,400. `mkfs` also gained `--size
|
||||||
|
<blocks>` so one tool builds both the big root image and the small ramdisk one.
|
||||||
|
|
||||||
|
**Why it is opt-in:** stage2 loads the kernel by PIO one sector at a time, so a
|
||||||
|
6 MB ramroot kernel is visibly slower to boot than the 350 KB default. Use
|
||||||
|
`make ramroot`, never `make RAMROOT=1` — the flag changes CFLAGS, which make
|
||||||
|
cannot see, so stale objects would silently keep the wrong `ROOTDEV`.
|
||||||
|
|
||||||
|
See [[schunix-rewrite-priorities]], [[schunix-build-environment]].
|
||||||
58
.claude/memory/schos-cross-platform-build.md
Normal file
58
.claude/memory/schos-cross-platform-build.md
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
---
|
||||||
|
name: schos-cross-platform-build
|
||||||
|
description: "The build and tools support Windows, Linux, macOS and BSD; clang everywhere, and the four host assumptions that used to break"
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: 9d8a2aef-bcf0-47a0-96c6-f706efab4a95
|
||||||
|
modified: 2026-08-09T12:26:54.787Z
|
||||||
|
---
|
||||||
|
|
||||||
|
**Nexoniarz develops from both Windows and Linux and switches between them**, so
|
||||||
|
nothing in the build may assume a host. As of 2026-08-09 all four are supported:
|
||||||
|
Windows (Git Bash/MSYS), Linux, macOS, and the BSDs. See
|
||||||
|
[[schunix-build-environment]] for why WSL is not an option on the Windows box.
|
||||||
|
|
||||||
|
**One toolchain, not two.** clang is a cross-compiler by construction, so a
|
||||||
|
single LLVM install produces every target the tree needs — bare-metal ELF, the
|
||||||
|
16/32-bit code below 1 MB, and Linux/musl userland — from any host. The Makefile
|
||||||
|
picks clang+lld whenever both are on PATH; `USE_GNU=1` forces the GNU fallback.
|
||||||
|
The old GNU branch had rotted: it passed `--no-warn-rwx-segments`, which is a
|
||||||
|
**hard error on lld**, and lld is the default linker on FreeBSD and macOS. It
|
||||||
|
also wanted a brewed `x86_64-elf-gcc` before a Mac could build at all.
|
||||||
|
|
||||||
|
**Host tools use the host's own `cc`.** Only mingw needs naming explicitly, and
|
||||||
|
only because clang defaults to an MSVC target there while `tools/mkfs.c` needs
|
||||||
|
`<unistd.h>` and `<dirent.h>`.
|
||||||
|
|
||||||
|
**The four things that were actually host-specific**, all fixed — worth
|
||||||
|
recognising the pattern before adding a fifth:
|
||||||
|
|
||||||
|
1. `<sys/sysmacros.h>` is glibc/musl only. macOS and the BSDs have no such
|
||||||
|
header and take `major()`/`minor()` from `<sys/types.h>`. Guarded on
|
||||||
|
`__linux__` now.
|
||||||
|
2. `sha256sum -c -` exists on Linux and Git Bash only. macOS has `shasum -a
|
||||||
|
256`, FreeBSD/NetBSD `sha256`, OpenBSD `cksum -a sha256`. `tools/sha256check.sh`
|
||||||
|
tries all five and compares digests itself rather than trusting a `-c` flag,
|
||||||
|
whose input format also differs.
|
||||||
|
3. `nproc` is coreutils — absent on macOS and BSD, where it is
|
||||||
|
`sysctl -n hw.ncpu`.
|
||||||
|
4. **`Join-Path $root 'build\x'` produces a filename containing a backslash on
|
||||||
|
a Unix host.** PowerShell 7 runs on Linux/macOS/BSD, so the .ps1 tools are
|
||||||
|
usable there, but only with forward slashes throughout and QEMU found on
|
||||||
|
PATH instead of a hardcoded scoop location.
|
||||||
|
|
||||||
|
**Do not blanket-replace backslashes in the .ps1 tools.** `bootcheck.ps1` sends
|
||||||
|
`printf 'caf\303\251'` to the guest to test UTF-8; rewriting those octal escapes
|
||||||
|
changes what the test asserts without failing it. The sweep is restricted to
|
||||||
|
known directory prefixes for exactly this reason.
|
||||||
|
|
||||||
|
**Ports do not build on Windows** — `make ports` skips with a message and the
|
||||||
|
tree relies on `sysroot/` being pre-staged. Building musl/toybox/mksh from
|
||||||
|
source needs a Linux host. That is a real reason to keep the Linux path working,
|
||||||
|
not a theoretical one.
|
||||||
|
|
||||||
|
**Unverified, and say so when it matters:** the Linux, macOS and BSD paths are
|
||||||
|
written from documented behaviour and have **never been executed** — this
|
||||||
|
machine has no WSL and no second host. Windows is the only host where the build
|
||||||
|
and `tools/bootcheck.ps1` have actually been run green.
|
||||||
46
.claude/memory/schos-memory-sync.md
Normal file
46
.claude/memory/schos-memory-sync.md
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
---
|
||||||
|
name: schos-memory-sync
|
||||||
|
description: Project memory lives in the repo at .claude/memory/ and is moved in and out with tools/memory-sync.sh
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: 9d8a2aef-bcf0-47a0-96c6-f706efab4a95
|
||||||
|
modified: 2026-08-09T12:27:06.629Z
|
||||||
|
---
|
||||||
|
|
||||||
|
**The memory is committed to the repo, at `.claude/memory/`.** Move it with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
tools/memory-sync.sh status # show both sides, change nothing
|
||||||
|
tools/memory-sync.sh export # this machine -> .claude/memory/ (then commit)
|
||||||
|
tools/memory-sync.sh import # .claude/memory/ -> this machine (after clone/pull)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why it has to be in the repo.** Claude Code stores memory outside the tree at
|
||||||
|
`<home>/.claude/projects/<key>/memory/`, where `<key>` is the project's absolute
|
||||||
|
path with every separator *and the drive colon* turned into a dash:
|
||||||
|
|
||||||
|
- `D:\DEV\SchOS` → `D--DEV-SchOS`
|
||||||
|
- `/home/me/dev/SchOS` → `-home-me-dev-SchOS`
|
||||||
|
|
||||||
|
Two consequences, both of which have already happened here:
|
||||||
|
|
||||||
|
- **Renaming the directory orphans the memory.** `D:\DEV\Schunix` →
|
||||||
|
`D:\DEV\SchOS` changed the key, and everything under the old one was invisible
|
||||||
|
until moved across by hand. See [[schos-rename-and-git]].
|
||||||
|
- **The key is machine-specific**, so memory written on Windows is not where
|
||||||
|
Linux looks, even for the same checkout. Nexoniarz uses both.
|
||||||
|
|
||||||
|
Git carries the memory across renames and between hosts; `import` puts it where
|
||||||
|
the local machine expects it.
|
||||||
|
|
||||||
|
**The colon becomes a dash, it is not dropped.** Deleting it yields
|
||||||
|
`D-DEV-SchOS`, which matches nothing — that was the first attempt and it
|
||||||
|
silently found zero memories.
|
||||||
|
|
||||||
|
On Git Bash the key must be derived from the **Windows** path (`cygpath -w`),
|
||||||
|
not the MSYS one: Claude Code is a Windows program and sees `D:\DEV\SchOS`, not
|
||||||
|
`/d/DEV/SchOS`.
|
||||||
|
|
||||||
|
Related: [[nexoniarz-sharing-preference]] still applies — this repo is local,
|
||||||
|
never pushed anywhere.
|
||||||
76
.claude/memory/schos-rename-and-git.md
Normal file
76
.claude/memory/schos-rename-and-git.md
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
---
|
||||||
|
name: schos-rename-and-git
|
||||||
|
description: "Schunix is now SchOS, the kernel is SchIFT; the tree is under local git as of 2026-08-09"
|
||||||
|
metadata:
|
||||||
|
type: project
|
||||||
|
---
|
||||||
|
|
||||||
|
**On 2026-08-09 the project was renamed from Schunix to SchOS**, to drop the
|
||||||
|
"-unix" element and any trademark exposure to The Open Group. Nexoniarz's
|
||||||
|
ecosystem layout:
|
||||||
|
|
||||||
|
| Layer | Name |
|
||||||
|
|---|---|
|
||||||
|
| Operating system | **SchOS** |
|
||||||
|
| Kernel | **SchIFT** |
|
||||||
|
| Desktop environment | SchDE |
|
||||||
|
| Window manager | SchWM |
|
||||||
|
| Compositor | SchCom |
|
||||||
|
| API / subsystem | SchAPI |
|
||||||
|
| File system | SchFS (unchanged) |
|
||||||
|
|
||||||
|
**Every memory written before this date says "Schunix" and is filed under a
|
||||||
|
`schunix-` slug. That is the old name for this same project -- read it as
|
||||||
|
SchOS.** They were deliberately not renamed: 42 files and every `[[link]]`
|
||||||
|
between them, for no gain.
|
||||||
|
|
||||||
|
**SchOS vs SchIFT is a real distinction, not a synonym.** Nexoniarz chose the
|
||||||
|
split explicitly. SchIFT appears *only* where a Unix names its kernel:
|
||||||
|
|
||||||
|
- `uname(2)` sysname -> `SchIFT` (nodename is `schos`, it is the host)
|
||||||
|
- `/proc/version` -> `SchIFT version 0.1.0 (schift) #1 SMP x86_64`
|
||||||
|
- the two licence sentences that say "kernel" outright, in LICENSE.MIT and
|
||||||
|
NOTICE
|
||||||
|
|
||||||
|
Everything a user reads as the product is SchOS: panic screen, boot menu,
|
||||||
|
error prefixes, docs, LICENSE, NOTICE. `schunix.img` is now `schos.img`;
|
||||||
|
`__SCHUNIX__` is `__SCHOS__`. The kernel source stays at `kernel/`, not
|
||||||
|
`schift/` -- asked and declined, to keep the diff small.
|
||||||
|
|
||||||
|
The `Assets/SchOS * Logo.*` files are renamed but the **artwork inside the
|
||||||
|
Full and Text logos may still spell the old name as vector paths**. That is
|
||||||
|
design work, not a code change.
|
||||||
|
|
||||||
|
## The tree is under git now -- local only
|
||||||
|
|
||||||
|
`git init` on 2026-08-09, first commit `d0f7acf` = the tree as it stood before
|
||||||
|
the rename, second `e04ca28` = the rename. Before this there was **no version
|
||||||
|
control at all**, which had already cost real damage: a blanket `sed` rewrote
|
||||||
|
`#include` lines and string literals with no way to undo it.
|
||||||
|
|
||||||
|
`core.autocrlf` is **false** and `.gitattributes` pins `* text eol=lf`. This
|
||||||
|
matters: the build runs under Git Bash, and a Makefile recipe or `.S` file
|
||||||
|
checked out with CRLF fails in ways that point nowhere near the cause.
|
||||||
|
|
||||||
|
`.gitignore` excludes `build/`, `sysroot/`, `dist/` and the images -- `dist/`
|
||||||
|
alone is 61 MB of upstream tarballs the build re-fetches.
|
||||||
|
|
||||||
|
**There is no remote and there must not be one.** See
|
||||||
|
[[nexoniarz-sharing-preference]] -- the standing rule is that unfinished work
|
||||||
|
never leaves the machine. Local commits are fine and wanted; `git push` is not.
|
||||||
|
|
||||||
|
## The rename broke two things, both the same mistake
|
||||||
|
|
||||||
|
Both found by `tools/bootcheck.ps1`, neither visible at compile time:
|
||||||
|
|
||||||
|
- `cmd/shmtest/main.c` wrote `"SCHUNIX-MMAP-OK"` with a hardcoded length of
|
||||||
|
`15` and compared it back by hand-counted offsets (`m[3]=='U'`,
|
||||||
|
`m[14]=='K'`). The shorter marker made the write run two bytes past the
|
||||||
|
literal and the compare read past the data -- **failing a file-backed mmap
|
||||||
|
that was working perfectly.** Now one constant plus `sizeof`.
|
||||||
|
- bootcheck asserted a 31-byte file after appending to `"hello-from-schunix"`;
|
||||||
|
the shorter string makes it 29.
|
||||||
|
|
||||||
|
**The lesson for any future rename: grep for string literals whose length is
|
||||||
|
written out separately from the literal.** A compiler cannot catch it and the
|
||||||
|
failure looks like a broken subsystem.
|
||||||
61
.claude/memory/schunix-boot-splash-logo.md
Normal file
61
.claude/memory/schunix-boot-splash-logo.md
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
---
|
||||||
|
name: schunix-boot-splash-logo
|
||||||
|
description: "The boot logo pipeline, why the loader draws it rather than the kernel, and the STAGE2_SECTORS trap that consolidating it exposed."
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: 9d8a2aef-bcf0-47a0-96c6-f706efab4a95
|
||||||
|
modified: 2026-08-08T12:04:49.027Z
|
||||||
|
---
|
||||||
|
|
||||||
|
The boot splash shows the `Assets/Schunix Small Logo.png` mark (an SX monogram in
|
||||||
|
a ring) above the `S C H U N I X` wordmark and its accent rule, all centred as
|
||||||
|
one group. Verified rendering correctly on 2026-08-08.
|
||||||
|
|
||||||
|
**The logo is drawn by the bootloader, not the kernel.** This is the
|
||||||
|
counter-intuitive part and it is a timing fact, not a preference: the kernel does
|
||||||
|
not reach `vga_init()` until roughly the last 300ms of a ~2.8s boot, so a
|
||||||
|
kernel-drawn logo appeared for about one frame before `console_takeover()` wiped
|
||||||
|
it. Drawn from `splash()` in `boot/stage2main.c` it is up for the whole boot.
|
||||||
|
`kernel/dev/vga.c` keeps the same drawing code against the same mask
|
||||||
|
(`vga_draw_logo`, `vga_logo_centered`) for things that want the mark *after*
|
||||||
|
boot — the crash screen, eventually `schinit`. Don't "fix" the duplication by
|
||||||
|
deleting the loader's copy.
|
||||||
|
|
||||||
|
**Pipeline:** `tools/mklogo.py` (host Python, has its own PNG inflate +
|
||||||
|
un-filter) crops to the mark, box-downscales, normalises to full strength, packs
|
||||||
|
to a 4-bit coverage mask, and writes `include/logo.h`. Run `make logo` — it is
|
||||||
|
*not* a build dependency, and `include/logo.h` is committed, on the same
|
||||||
|
reasoning as `include/font.h`. The mask is coverage, not colour, so the palette
|
||||||
|
stays in [[schunix-console-tty]]'s `include/theme.h`; the loader composites it in
|
||||||
|
`SCH_FG` because the source art is a silver gradient and the accent is reserved
|
||||||
|
for the one rule. 4 bits rather than 8 because the header is included by the
|
||||||
|
*bootloader*, where size is a boot constraint.
|
||||||
|
|
||||||
|
**`STAGE2_SECTORS` was encoded in five places, not the three `docs/layout.md`
|
||||||
|
claimed.** Raising it 24 → 48 to fit the 8 KiB mask proved it: the `dd seek=25`
|
||||||
|
offset stayed behind and the kernel got written straight over stage2's tail.
|
||||||
|
The five were `boot/bootasm.S`, `boot/stage2main.c`, the `dd seek=`, and *twice*
|
||||||
|
inside `tools/mkiso.sh` (its size assertion and `-boot-load-size`). It now lives
|
||||||
|
only in the root Makefile, which passes `-DSTAGE2_SECTORS` and exports it to
|
||||||
|
mkiso; the `#define`s left in the boot sources are `#ifndef` fallbacks. Hard
|
||||||
|
ceiling is 65 sectors — stage2 loads at `0x7E00` and must not reach the
|
||||||
|
multiboot kernel-search buffer at `0x10000` — and `stage2main.c` now
|
||||||
|
static-asserts that. See also [[schunix-iso-boot]].
|
||||||
|
|
||||||
|
**`tools/splashshot.ps1`** captures a burst of QEMU screendumps across early
|
||||||
|
boot and reports each frame's distinct-colour count, because the splash window
|
||||||
|
is too short to hit with a single guessed delay. It uses `-snapshot` plus
|
||||||
|
`file.locking=off` so it runs while an interactive QEMU has the images open —
|
||||||
|
booting from copies also works but means duplicating all 153 MB of
|
||||||
|
`build/fs.img` per run. Colour counts are the
|
||||||
|
assertion: ~2 means text console or blank, 18 means the 4bpp logo is up. If it
|
||||||
|
ever reads 156 again, something is drawing an 8-bit mask.
|
||||||
|
|
||||||
|
**Two traps, both hit while doing this:**
|
||||||
|
- `$(shell expr $(N) \* 512)` in a Makefile does not work — make passes the
|
||||||
|
backslash through, sh globs the `*`, and expr receives a directory listing.
|
||||||
|
Make silently carries on with nonsense. Use `$$(( ))` in the recipe.
|
||||||
|
- `make iso` was failing when this was written, for unrelated reasons since
|
||||||
|
fixed — see [[schunix-ramdisk-root-sizing]]. `-boot-load-size 49` is now
|
||||||
|
verified: the ISO boots from optical media only.
|
||||||
54
.claude/memory/schunix-build-environment.md
Normal file
54
.claude/memory/schunix-build-environment.md
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
---
|
||||||
|
name: schunix-build-environment
|
||||||
|
description: Schunix builds natively on Windows with clang/lld + mingw + Git Bash; WSL is gone because WSL2 needs Hyper-V
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: 9d8a2aef-bcf0-47a0-96c6-f706efab4a95
|
||||||
|
modified: 2026-08-07T17:19:34.188Z
|
||||||
|
---
|
||||||
|
|
||||||
|
**Builds natively on Windows. WSL is no longer used at all.** WSL2 runs a real
|
||||||
|
Linux kernel in a Hyper-V VM, so it hard-requires virtualisation, and enabling
|
||||||
|
that puts the Hyper-V hypervisor under the whole machine — which demotes VMware
|
||||||
|
to running nested and slows the box down permanently. Nexoniarz will not enable
|
||||||
|
it, and that is a reasonable call.
|
||||||
|
|
||||||
|
`make` from **Git Bash** does everything. `run.ps1` builds natively too. The
|
||||||
|
toolchain, all already installed:
|
||||||
|
|
||||||
|
- **clang + ld.lld + llvm-objcopy/objdump** (`C:\Program Files\LLVM\bin`) —
|
||||||
|
clang is a cross-compiler by construction, so `--target=x86_64-elf` builds the
|
||||||
|
kernel and `--target=i386-elf` the bootloader. No cross-binutils needed.
|
||||||
|
- **mingw-w64 gcc** (`scoop install mingw`) for host tools — `tools/mkfs.c`
|
||||||
|
needs `<unistd.h>`/`<dirent.h>`, which the Windows SDK lacks.
|
||||||
|
- **make** (`scoop install make`), plus Git Bash's perl/sed/dd/sha256sum.
|
||||||
|
|
||||||
|
`run.ps1` prepends all three directories itself, so it works from any shell.
|
||||||
|
The Makefile detects Windows via `uname -s` matching `MINGW*` and sets
|
||||||
|
`WINHOST`. Ports (musl/mksh/toybox) are **not** rebuilt on Windows — they are
|
||||||
|
Linux userland built by configure scripts, already staged in `sysroot/`, and
|
||||||
|
the Makefile says so out loud rather than skipping silently. Everything is a
|
||||||
|
no-op on Linux, so a Linux/VM build still works identically.
|
||||||
|
|
||||||
|
**Five latent bugs the move exposed**, all real, none Windows-specific — see
|
||||||
|
`docs/architecture/kernel-history.md` §8 for the two hardest:
|
||||||
|
|
||||||
|
1. `kernel.ld` named no `.ltext`/`.ldata`/`.lbss` — with `-mcmodel=large`, LLVM
|
||||||
|
and GCC 13+ put nearly everything there, and orphan placement put them past
|
||||||
|
`end`, so `kinit1(end,...)` handed the allocator live `.lbss`.
|
||||||
|
2. `uint64` was `unsigned long` — 64-bit on Linux, **32-bit on Windows** (LLP64).
|
||||||
|
Host tools include the on-disk structs, so this would have silently written
|
||||||
|
images with half-width inode fields. Now data-model-aware + `_Static_assert`.
|
||||||
|
3. `syscall_entry` used `%fs:(0)` as scratch — that is the **TCB self-pointer**.
|
||||||
|
GCC's direct `%fs:-8` TLS never reads it; clang materialises the TP from it.
|
||||||
|
4. `_start` read `argc`/`argv` via `register ... asm("rdi")`, which is only
|
||||||
|
guaranteed for inline-asm operands. clang read uninitialised stack. Now
|
||||||
|
`lib/start.S`.
|
||||||
|
5. lld needs `--image-base=0` for anything linked below 0x400000, and
|
||||||
|
`-m elf_x86_64` on `ld -r -b binary`.
|
||||||
|
|
||||||
|
**When a symptom moves as you add printfs, stop printing and use the
|
||||||
|
watchpoint.** QEMU `-S -gdb tcp::26000` plus mingw's gdb (`hbreak`, then
|
||||||
|
`watch`) named the offending instruction on the first run after three rounds of
|
||||||
|
cprintf had failed. See [[schunix-verify-by-driving-shell]].
|
||||||
50
.claude/memory/schunix-build-speed.md
Normal file
50
.claude/memory/schunix-build-speed.md
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
---
|
||||||
|
name: schunix-build-speed
|
||||||
|
description: The build is fast now (no-op ~2s); the four things that made it slow and the traps that will make it slow again
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: a665e14b-86bd-4b41-a228-51377f3e9225
|
||||||
|
modified: 2026-08-04T17:27:56.099Z
|
||||||
|
---
|
||||||
|
|
||||||
|
**A no-op `make` went from 1m48s to ~2s on 2026-08-04**, and a full kernel
|
||||||
|
rebuild from ~110s to ~7s. Almost none of it was compilation — with only 2.4s
|
||||||
|
of CPU in that 1m48, it was I/O across WSL's `/mnt/d` boundary. Four causes,
|
||||||
|
all still worth not reintroducing:
|
||||||
|
|
||||||
|
- **`tools/mkfs` built the image on the file**, one `lseek` + one 512-byte
|
||||||
|
`write` per block: 300,000 syscall pairs, 150,000 of them just to zero it.
|
||||||
|
It now assembles the image in RAM and writes once. It also stops at the last
|
||||||
|
non-zero block and `ftruncate`s, so ~2 MB is written instead of 147 MB.
|
||||||
|
- **The image was rebuilt on every invocation.** `sysroot-install` is phony, so
|
||||||
|
everything depending on it always ran. There is now a `build/sysroot.stamp`
|
||||||
|
whose mtime only advances when a `find`-based summary of the staged tree
|
||||||
|
actually changes (the `cmp -s ... || mv` idiom — **do not add a `touch`, and
|
||||||
|
note `touch -c` still updates an existing file's mtime**, which is exactly
|
||||||
|
the bug I wrote and had to remove).
|
||||||
|
- **`ports/toybox` re-linked its 236 applets every build**, because port.mk
|
||||||
|
declares `install` .PHONY for every port. It hangs off
|
||||||
|
`$(BUILDDIR)/toybox.installed` now. This was the sneaky one: it bumped the
|
||||||
|
mtime of 236 files in `sysroot/` and so invalidated the stamp above.
|
||||||
|
- **mkfs and mkschfs re-read every file to dedup it.** They now check
|
||||||
|
`(st_dev, st_ino)` first — toybox's applets are real hard links, so 236 of
|
||||||
|
them are settled by one `fstat` instead of ~500 MB of reads.
|
||||||
|
|
||||||
|
Two real bugs found in the Makefile while doing this:
|
||||||
|
|
||||||
|
- **`$(BUILD)/kernel/**/*.d` only matched one directory level.** make's wildcard
|
||||||
|
has no recursive `**`. `arch/x86_64/` and `fs/schfs/` therefore had **no
|
||||||
|
header dependency tracking at all** — editing `include/schfs.h` left stale
|
||||||
|
objects that linked fine. It uses `$(shell find ...)` now.
|
||||||
|
- **`CFLAGS` is recursively expanded and had a `$(shell)` appended**, so the
|
||||||
|
stack-protector probe re-ran on every expansion: one extra gcc per object
|
||||||
|
file. Probed once into `NO_SSP :=` now.
|
||||||
|
|
||||||
|
`schunix.img` is no longer `.PHONY` (it is a real file), and `MAKEFLAGS +=
|
||||||
|
-j$(nproc)` is on by default — use `make -j1` for readable output from a
|
||||||
|
failing build.
|
||||||
|
|
||||||
|
Verify with `make schfs-check` plus [[schunix-verify-by-driving-shell]]; the
|
||||||
|
superblock checksum is printed by mkfs and should not change when only build
|
||||||
|
mechanics change.
|
||||||
67
.claude/memory/schunix-console-tty.md
Normal file
67
.claude/memory/schunix-console-tty.md
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
---
|
||||||
|
name: schunix-console-tty
|
||||||
|
description: "The console has an ANSI parser, real termios and UTF-8; the font is CP437 not Latin-1, and five bugs that looked like broken commands"
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: a665e14b-86bd-4b41-a228-51377f3e9225
|
||||||
|
modified: 2026-08-04T21:10:44.835Z
|
||||||
|
---
|
||||||
|
|
||||||
|
**`kernel/core/console.c` was rewritten on 2026-08-04**: ANSI escape parser,
|
||||||
|
real termios, signals, and UTF-8.
|
||||||
|
|
||||||
|
**The serial line and the framebuffer are treated differently on purpose.**
|
||||||
|
Escapes are passed through to serial verbatim -- a real terminal emulator is
|
||||||
|
on the other end and understands them better than we ever will -- and
|
||||||
|
*interpreted* only for the framebuffer. So a serial transcript can never tell
|
||||||
|
you whether the escape parser works; it holds the raw bytes either way. Use
|
||||||
|
`pwsh -File tools/ttyshot.ps1`, which drives colour/cursor/erase/UTF-8 and
|
||||||
|
screendumps the result.
|
||||||
|
|
||||||
|
**The font is CP437, not Latin-1.** `include/font.h`'s per-glyph comments
|
||||||
|
label index N with Unicode codepoint N, which is fiction above 127: index 0xE9
|
||||||
|
is annotated "e-acute" and draws a capital theta. Trusting it turned "café"
|
||||||
|
into "caf" + theta. `console.c` maps Unicode through `latin1_to_cp437[]` plus
|
||||||
|
a small table of box-drawing characters. **Verify glyphs by rendering the
|
||||||
|
bitmap, never by reading the comment.**
|
||||||
|
|
||||||
|
`font_data` was also declared `[128][16]` while containing 256 glyphs, so the
|
||||||
|
compiler silently discarded the entire upper half -- the wall of "excess
|
||||||
|
elements in array initializer" warnings was that.
|
||||||
|
|
||||||
|
**Five bugs that all presented as broken commands, none of which were:**
|
||||||
|
|
||||||
|
- `ls /bin | wc -l` said 48 instead of 240 -- `sys_ioctl` answered *every*
|
||||||
|
unrecognised request with 0, so `TIOCGWINSZ` on a pipe "succeeded" and every
|
||||||
|
program thought pipes were terminals. It returns `-ENOTTY` now. isatty() is
|
||||||
|
built on this call, so the blast radius was everything.
|
||||||
|
- `yes | head -2` failed with "Invalid argument" -- `readv`/`writev` capped
|
||||||
|
iovcnt at 16 (POSIX's minimum) and toybox's `yes` builds 256. Now
|
||||||
|
`UIO_MAXIOV` = 1024, Linux's value.
|
||||||
|
- `cat big | head -c 4` killed the shell at `rip 0x1` -- **SIG_IGN is handler
|
||||||
|
value 1**, and `deliver_signals()` only skipped 0 (SIG_DFL), so it jumped to
|
||||||
|
address 1. Nothing sent SIGPIPE until pipes learned to, at which point every
|
||||||
|
shell that ignores SIGPIPE died.
|
||||||
|
- Pipes returned a bare `-1` with no signal on a closed read end; now SIGPIPE
|
||||||
|
plus `-EPIPE`.
|
||||||
|
- Ctrl-C did `proc->killed = 1`, and `proc` in an interrupt handler is
|
||||||
|
whatever was running on that CPU -- almost never the foreground job. It
|
||||||
|
sends a real SIGINT to `tty_fg_pgrp` now.
|
||||||
|
|
||||||
|
**Control characters must never reach the glyph path.** `vga_putc` had no tab
|
||||||
|
case, so `'\t'` fell through to `vga_draw_char(9)` -- CP437 glyph 9 is a
|
||||||
|
hollow circle, so pressing Tab printed a circle. The font has a picture for
|
||||||
|
every control code and none of them is what the character means. Tab now
|
||||||
|
advances to the next eight-column stop (erasing as it goes, so it overwrites
|
||||||
|
rather than skipping); everything else below 0x20 is dropped.
|
||||||
|
|
||||||
|
termios is real: TCGETS returns what was last set, TCSETS is honoured, and the
|
||||||
|
console implements ICANON, ECHO, ECHOE, ECHONL, ISIG, ICRNL, INLCR, IGNCR,
|
||||||
|
ISTRIP and VMIN. Raw mode returns bytes as they arrive rather than waiting for
|
||||||
|
a newline -- that is what zsh and any line editor need. EOF is a flag, not a
|
||||||
|
`^D` byte stuffed into the buffer.
|
||||||
|
|
||||||
|
Check with `pwsh -File tools/bootcheck.ps1 -Tty` (six assertions, including a
|
||||||
|
real Ctrl-C against a running `sleep`). See
|
||||||
|
[[schunix-verify-by-driving-shell]] and [[schunix-tty-fg-pgrp-coupling]].
|
||||||
57
.claude/memory/schunix-crash-screen.md
Normal file
57
.claude/memory/schunix-crash-screen.md
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
---
|
||||||
|
name: schunix-crash-screen
|
||||||
|
description: "PSOGM crash screen lives in kernel/core/panic.c; test it with `echo c > /proc/sysrq` and tools/crashshot.ps1"
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: a665e14b-86bd-4b41-a228-51377f3e9225
|
||||||
|
modified: 2026-08-03T15:09:42.189Z
|
||||||
|
---
|
||||||
|
|
||||||
|
**The crash screen is `kernel/core/panic.c`** (rewritten 2026-08-03; it used to
|
||||||
|
live inside `console.c`). **PSOGM — Purple Screen of Guru Meditation.** Solid
|
||||||
|
purple, big white `:(` drawn from primitives, Windows-style left-aligned
|
||||||
|
headline and field table, then a raw call-chain dump. Nexoniarz picked this
|
||||||
|
after a first attempt that dimmed the desktop and floated a dark panel with a
|
||||||
|
cartoon emoji over it — **the emoji and the muted palette read as out of
|
||||||
|
place**; the name says purple screen, so it is one.
|
||||||
|
|
||||||
|
A "Stop code" field is derived from the panic message (uppercased up to the
|
||||||
|
first colon: `"sysrq: crash requested"` → `SYSRQ`), so no call site has to
|
||||||
|
supply one.
|
||||||
|
|
||||||
|
**To see it, crash on purpose:**
|
||||||
|
|
||||||
|
```
|
||||||
|
echo c > /proc/sysrq # from inside Schunix
|
||||||
|
```
|
||||||
|
|
||||||
|
**To photograph it:** `pwsh -File tools/crashshot.ps1` (or `make crashshot`)
|
||||||
|
after `make ramroot`. It boots, fills the screen with real output, crashes,
|
||||||
|
and pulls a PPM out through QEMU's monitor on TCP 55555. Convert with
|
||||||
|
`convert build/crashscreen.ppm build/crashscreen.png` — ImageMagick is in the
|
||||||
|
WSL Fedora box as `convert`, not `magick`.
|
||||||
|
|
||||||
|
Why the trigger exists at all: **the crash screen is the one part of the system
|
||||||
|
you cannot look at by using the system**, so without a deliberate trigger it is
|
||||||
|
only ever seen when something is already wrong, and it quietly rots.
|
||||||
|
|
||||||
|
**Three things that will bite anyone editing it:**
|
||||||
|
|
||||||
|
- There is no libm. `isqrt` in panic.c uses Newton's method iterated *while the
|
||||||
|
estimate decreases*; the obvious "stop when it stops changing" version does
|
||||||
|
not terminate, because on a non-square it settles into a two-cycle
|
||||||
|
(`isqrt(3)` alternates 1, 2, 1, 2...). That shipped once and hung the panic
|
||||||
|
handler with the screen half drawn — a hang with no way to find out why.
|
||||||
|
|
||||||
|
- `vga_draw_char` fills the *background* of every glyph with `vga_bg`. Drawing
|
||||||
|
panel text without setting `vga_set_bg(PSOGM_PANEL)` first puts every line in
|
||||||
|
a cream-coloured box.
|
||||||
|
- `cprintf` writes to the framebuffer as well as the serial line, so the serial
|
||||||
|
report scrolls straight over the panel. `cons_panic_mode()` sets
|
||||||
|
`cons_serial_only`, which is what keeps the two apart — paint order alone
|
||||||
|
does not.
|
||||||
|
|
||||||
|
The serial copy is the one that survives, and it is what `tools/bootcheck.ps1`
|
||||||
|
reads. It is also all you get when the panic happens before there is a
|
||||||
|
framebuffer. See [[schunix-verify-by-driving-shell]].
|
||||||
49
.claude/memory/schunix-devfs-procfs.md
Normal file
49
.claude/memory/schunix-devfs-procfs.md
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
---
|
||||||
|
name: schunix-devfs-procfs
|
||||||
|
description: "How /dev and /proc work in Schunix; ps and top work as of 2026-08-08"
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: a665e14b-86bd-4b41-a228-51377f3e9225
|
||||||
|
modified: 2026-08-08T16:24:48.813Z
|
||||||
|
---
|
||||||
|
|
||||||
|
**/dev** nodes are created by `cmd/init/main.c` with `mknod()` at every boot,
|
||||||
|
and served by `kernel/dev/cdev.c`'s `devsw[major]` table. Minor numbers under
|
||||||
|
major 1 follow Linux's mem-major layout where free: null=2, zero=5, full=7,
|
||||||
|
random=8, urandom=9. `/dev/hole` (minor 4) was deleted 2026-08-01 — an
|
||||||
|
abandoned experiment nothing referenced.
|
||||||
|
|
||||||
|
**/proc** (`kernel/dev/procfs.c`, major 6) synthesises eight global files:
|
||||||
|
meminfo, uptime, cpuinfo, version, stat, loadavg, mounts, filesystems. Each is
|
||||||
|
a device node with its own minor; reading renders the whole text and returns
|
||||||
|
the slice at the caller's offset.
|
||||||
|
|
||||||
|
**Two gotchas that shaped it:**
|
||||||
|
|
||||||
|
1. `devsw[].read()` used to be called as `read(ip, 1, addr, n)` — every caller
|
||||||
|
passed the literal `1` where an offset belongs, so a device could never
|
||||||
|
know where a read started. Fine for a stream like the console, fatal for a
|
||||||
|
/proc file: without an offset every read returns the whole text again and
|
||||||
|
`cat` never terminates. `kernel/fs/file.c` now passes `f->off` and advances
|
||||||
|
it by the amount returned.
|
||||||
|
|
||||||
|
2. `/proc/mounts` must list `/` **last**. df keeps the last mount point that
|
||||||
|
prefixes the path it was asked about, so listing root first made `df /`
|
||||||
|
report /proc's numbers.
|
||||||
|
|
||||||
|
**Per-process `/proc/<pid>/` directories now exist and `ps`/`top` work** —
|
||||||
|
re-verified 2026-08-08 with `tools/bootcheck.ps1 -Ps`: ps finds init and the
|
||||||
|
shell, `-o ppid` shows the real parent, top reads `/proc/meminfo`. This entry
|
||||||
|
used to say they were impossible without a VFS rewrite; that was overtaken by
|
||||||
|
`kernel/fs/procfs.c`, which implements /proc as a real filesystem behind
|
||||||
|
`struct fs_ops` with synthetic inodes (`pid<<8` encoding) rather than touching
|
||||||
|
`fs.c`. See [[schfs-phase3-writes]] for the `fs_ops` dispatch.
|
||||||
|
|
||||||
|
One cosmetic gap remains: `ps -o comm` shows `[init]` in brackets, because
|
||||||
|
toybox `readlink()`s `/proc/<pid>/exe` and ours is a regular file, not a
|
||||||
|
symlink. Needs symlink support in procfs.
|
||||||
|
|
||||||
|
`sysinfo(2)` also reports real memory now (`PHYSTOP` and
|
||||||
|
`kalloc_free_bytes()`); it used to return hardcoded 256MB/128MB, which is what
|
||||||
|
`free` reads.
|
||||||
92
.claude/memory/schunix-device-model.md
Normal file
92
.claude/memory/schunix-device-model.md
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
---
|
||||||
|
name: schunix-device-model
|
||||||
|
description: "The bus-agnostic device model behind /proc/devices, and the settled decision on how Linux drivers may be used."
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: 9d8a2aef-bcf0-47a0-96c6-f706efab4a95
|
||||||
|
modified: 2026-08-08T18:05:13.551Z
|
||||||
|
---
|
||||||
|
|
||||||
|
Built 2026-08-08. `include/device.h` + `kernel/dev/device.c`, readable at
|
||||||
|
`/proc/devices`. Written up in `docs/drivers.md`. Supersedes the two separate
|
||||||
|
arrays in `kernel/drivers/registry.c` as the answer to "what is attached".
|
||||||
|
|
||||||
|
**Bus is provenance, class is capability** — keep them separate. `bus` is
|
||||||
|
`platform`/`pci`/`usb`; `class` is `display`/`input`/`storage`/`net`/`bus`. A USB
|
||||||
|
disk and a SATA disk are both `storage`. `device_find_class()` is how a consumer
|
||||||
|
asks for "the display" without naming a driver, which is what stops the next
|
||||||
|
display driver requiring edits to every consumer.
|
||||||
|
|
||||||
|
Where a driver claims a device, **the driver's opinion beats the descriptor's**:
|
||||||
|
`struct usb_driver` gained a `devclass` field distinct from the USB interface
|
||||||
|
class, because the descriptor says what the device claims to be and the driver
|
||||||
|
knows what it made of it. A USB driver that forgets to set it lands in `other`.
|
||||||
|
|
||||||
|
Registration is automatic in `pci_bind()` and `usb_bind_all()` so a driver
|
||||||
|
cannot forget; platform devices are registered by hand in
|
||||||
|
`platform_register_devices()` in registry.c, in one readable block, matching
|
||||||
|
that file's existing "a list you can read beats a list assembled by side
|
||||||
|
effects" reasoning.
|
||||||
|
|
||||||
|
Flat list with a bus tag, **not a device tree** — nothing walks a hierarchy yet.
|
||||||
|
No lock: everything is added during single-threaded boot. **USB hotplug would
|
||||||
|
break both assumptions** and needs a spinlock plus a reader lock in procfs.
|
||||||
|
|
||||||
|
## The display class
|
||||||
|
|
||||||
|
`include/display.h` + ops in `kernel/dev/vga.c`, published on the device by
|
||||||
|
`vga_pci_driver.ops`. `struct pci_driver` gained an `ops` field for this —
|
||||||
|
**it has to live on the driver, not be set inside probe()**, because
|
||||||
|
`pci_bind()` registers the device *after* probe returns.
|
||||||
|
|
||||||
|
Ops: `mode`, `acquire`, `present(x,y,w,h)`, `wait_vblank`. The wrappers in
|
||||||
|
device.c null-check every op so consumers are not each written around a
|
||||||
|
maybe-absent vtable.
|
||||||
|
|
||||||
|
- **Back buffer comes from `dma_alloc()`, which is the wrong pool** and is used
|
||||||
|
because it is the only *physically contiguous* allocator in the kernel —
|
||||||
|
kalloc hands out single pages. 1.9 MB of a 4 MB arena meant for device rings.
|
||||||
|
Allocated lazily on first `acquire()`, never at boot. The real fix travels
|
||||||
|
with the E820 work ([[schunix-smp-and-memory-bugs]]).
|
||||||
|
- **vblank is the 1987 VGA port 0x3DA bit 3**, not VBE — VBE's protected-mode
|
||||||
|
interface for this is optional and usually absent. Two loops (wait for the
|
||||||
|
current retrace to *end*, then for the next to begin), or you return
|
||||||
|
immediately having just missed one. Both loops are bounded so hardware that
|
||||||
|
never reports retrace cannot hang a caller holding the console lock.
|
||||||
|
|
||||||
|
`echo d > /proc/sysrq` runs `display_selftest()`; `tools/dispshot.ps1`
|
||||||
|
photographs it to `build/dispshot.ppm` — it was derived from crashshot.ps1 and
|
||||||
|
originally kept its output path, so the two silently overwrote each other and
|
||||||
|
`crashqr.py` then failed on a capture that was not a crash screen. The pattern is designed to fail visibly: quadrants slant on a
|
||||||
|
stride error, change colour on a bpp error, and the 1px inset frame clips on an
|
||||||
|
off-by-one in the present rectangle. Verified 2026-08-08.
|
||||||
|
|
||||||
|
The console still writes *directly* to the framebuffer; the two paths coexist
|
||||||
|
deliberately. Routing text through the back buffer would mean presenting per
|
||||||
|
character.
|
||||||
|
|
||||||
|
## Linux drivers — settled 2026-08-08
|
||||||
|
|
||||||
|
Nexoniarz's decision, and the reasoning is correct: **not shipped, ever**; the
|
||||||
|
user installs one themselves through a device manager and Schunix loads it.
|
||||||
|
|
||||||
|
- *Distribution* is what triggers the GPL. A user obtaining and loading a driver
|
||||||
|
on their own machine is their act, not ours.
|
||||||
|
- Implementing a *loader* from published documentation is clean-room and fine —
|
||||||
|
it stays clean-room only if written from docs, not transcribed from Linux
|
||||||
|
source. See [[schunix-licensing]].
|
||||||
|
|
||||||
|
Also worth keeping separate when this resumes: "loadable modules" (a stable ABI
|
||||||
|
plus a linker/loader, on top of this model) and "drivers out of kernel"
|
||||||
|
(user-space processes, DriverKit-style — needs IPC and mmap, both of which
|
||||||
|
already exist). They get bundled together in conversation and are different
|
||||||
|
projects.
|
||||||
|
|
||||||
|
## Adding /proc files
|
||||||
|
|
||||||
|
Three places, all needed: a `PROC_*` id and a case in `proc_gen_global()` in
|
||||||
|
`kernel/dev/procfs.c`, and a row in the `globals[]` table in
|
||||||
|
`kernel/fs/procfs.c`. Miss the third and the file exists but cannot be opened by
|
||||||
|
name. `sb_pad()` exists there now for column layout — cprintf has no width
|
||||||
|
specifier.
|
||||||
48
.claude/memory/schunix-dirsiz-14.md
Normal file
48
.claude/memory/schunix-dirsiz-14.md
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
---
|
||||||
|
name: schunix-dirsiz-14
|
||||||
|
description: DIRSIZ was 14 and truncated silently; raised to 30 on 2026-08-09 by holding the HTree bucket at 256 bytes.
|
||||||
|
metadata:
|
||||||
|
type: project
|
||||||
|
---
|
||||||
|
|
||||||
|
Found 2026-08-09 while staging fonts. `DIRSIZ` in `include/fs.h` is **14**, an
|
||||||
|
xv6 inheritance, and the v1 filesystem truncates to it **silently**.
|
||||||
|
|
||||||
|
`JetBrainsMono-Regular.ttf` was stored as `JetBrainsMono-`. Two consequences,
|
||||||
|
the second much worse than the first:
|
||||||
|
|
||||||
|
- The file cannot be opened by its real name, and `stat` fails on it — `ls -l`
|
||||||
|
showed `?` for every field, which is what tipped this off.
|
||||||
|
- **`JetBrainsMono-Regular.ttf` and `JetBrainsMono-Bold.ttf` both truncate to
|
||||||
|
`JetBrainsMono-`.** Any set of files sharing a 14-character prefix collides.
|
||||||
|
That is silent data loss, not a display limitation.
|
||||||
|
|
||||||
|
## Fixed the same day: DIRSIZ is 30
|
||||||
|
|
||||||
|
**The trick that made it safe: hold the HTree bucket at 256 bytes.**
|
||||||
|
|
||||||
|
The directory hash tree is laid out in *bytes*, not entries. A bucket was 16
|
||||||
|
slots x 16 bytes = 256, and `HASHRANGE`, `OVERFLOW_DIR_START` (113 * BSIZE),
|
||||||
|
`OVERFLOW_DIR_END` and `MAXFSIZE` are all derived from that. Raising DIRSIZ
|
||||||
|
naively to 62 would have made buckets 1024 bytes and pushed the primary area
|
||||||
|
from 117 KB to 470 KB, straight through the overflow region -- silent corruption
|
||||||
|
of every directory.
|
||||||
|
|
||||||
|
DIRSIZ 30 makes `struct dirent` exactly **32 bytes**, so a 256-byte bucket holds
|
||||||
|
**8** slots instead of 16 and **every byte offset in the layout is unchanged**.
|
||||||
|
`DIRENT_BUCKET_BYTES` in `include/fs.h` is now the constant the maths is written
|
||||||
|
against; `kernel/fs/fs.c` and `tools/mkfs.c` both use it instead of
|
||||||
|
`16 * sizeof(struct dirent)`. Cost: more overflow use in heavily collided
|
||||||
|
directories.
|
||||||
|
|
||||||
|
**Verified in the running system**: `/usr/share/fonts` lists
|
||||||
|
`JetBrainsMono-Regular.ttf` in full, and `/tmp/JetBrainsMono-Regular.txt` and
|
||||||
|
`/tmp/JetBrainsMono-Bold.txt` coexist with distinct contents -- the exact case
|
||||||
|
that used to collapse into one entry. All four bootcheck modes pass.
|
||||||
|
|
||||||
|
**Still not POSIX's 255.** Getting there means reshaping the directory format,
|
||||||
|
which wants the on-disk version policy SchFS is getting first. 30 covers real
|
||||||
|
filenames; it is not a general solution.
|
||||||
|
|
||||||
|
`sysroot/bin`'s 244 toybox links were checked for collisions at the old limit:
|
||||||
|
there were none, so nothing was silently broken before this.
|
||||||
46
.claude/memory/schunix-doom-acceptance-test.md
Normal file
46
.claude/memory/schunix-doom-acceptance-test.md
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
---
|
||||||
|
name: schunix-doom-acceptance-test
|
||||||
|
description: "The full-stack acceptance test (a ported 1993 shooter) was removed on 2026-08-08 — what it found, why it went, and what to do instead."
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: 9d8a2aef-bcf0-47a0-96c6-f706efab4a95
|
||||||
|
modified: 2026-08-08T12:19:38.682Z
|
||||||
|
---
|
||||||
|
|
||||||
|
**Removed 2026-08-08 at Nexoniarz's request** ("we don't need it anymore because
|
||||||
|
we know that doom works"). Do not re-add it, and do not reintroduce the name into
|
||||||
|
comments — they also asked for trademark words to be cleared from the tree, and
|
||||||
|
it is now at zero references outside `dist/`.
|
||||||
|
|
||||||
|
Deleted: `ports/fbdoom/`, `tools/doomtest.ps1`, `tools/doomtest2.ps1`, the
|
||||||
|
`doom`/`doomtest`/`doom-uninstall` Makefile targets, `NOTPORTS := fbdoom`.
|
||||||
|
**The WADs were moved, not deleted** — `dist/wads/` — because `doom1.wad` was a
|
||||||
|
shareware download that existed nowhere else in the tree, while `freedoom1.wad`
|
||||||
|
is recoverable from `dist/freedoom-0.13.0.zip`.
|
||||||
|
|
||||||
|
**What it was worth, because the shape is worth repeating.** A large program
|
||||||
|
somebody else wrote, exercising `/dev/fb0`, evdev, `mmap`, the ELF loader,
|
||||||
|
`gettimeofday` and a fixed-rate tic loop at once, is harsher than any in-house
|
||||||
|
test *because it was not written against our bugs*. It found: the
|
||||||
|
`fcntl(F_SETFL, O_NONBLOCK)` no-op; the evdev backlog replayed to each new
|
||||||
|
opener; and the missing `EVIOCGRAB`. All fixed, all now covered by
|
||||||
|
`tools/bootcheck.ps1 -Evdev` / `-Sock` — which are narrower and do **not** prove
|
||||||
|
the whole stack composes. That is the honest cost of the removal.
|
||||||
|
|
||||||
|
**Why it went beyond being asked:** ~100k lines of GPL-2.0 engine on
|
||||||
|
non-redistributable shareware data, whose safety depended on two separate
|
||||||
|
opt-outs (`NOTPORTS` and `RAMDISK_EXCLUDE`) both staying correct — and the
|
||||||
|
ramdisk is linked *into the kernel binary*, so a slip there makes the kernel
|
||||||
|
undistributable, not just the image. See [[schunix-ramdisk-root-sizing]] and
|
||||||
|
[[schunix-licensing]].
|
||||||
|
|
||||||
|
**Two techniques from it that transfer:**
|
||||||
|
- Assert on *pixels*, not log lines. A log-only harness once passed on a blank
|
||||||
|
screen. `tools/splashshot.ps1` counts distinct colours for this reason.
|
||||||
|
- For a crash inside a program you did not write, run `llvm-nm` against the
|
||||||
|
faulting `rip` to get a function name, then use the fault offset to identify
|
||||||
|
the exact field. "Null pointer plus 24" alone is nearly useless.
|
||||||
|
|
||||||
|
Both lessons are written up in `docs/roadmap.md` Phase 7 and
|
||||||
|
`docs/architecture/kernel-history.md` §10 and §12, with the program genericised.
|
||||||
60
.claude/memory/schunix-driver-infrastructure.md
Normal file
60
.claude/memory/schunix-driver-infrastructure.md
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
---
|
||||||
|
name: schunix-driver-infrastructure
|
||||||
|
description: "Dynamic IRQ registration, a DMA arena and PCI capabilities exist now; PCI interrupts are level/active-low and the old ioapicenable only did edge/high"
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: a665e14b-86bd-4b41-a228-51377f3e9225
|
||||||
|
modified: 2026-08-04T22:21:01.983Z
|
||||||
|
---
|
||||||
|
|
||||||
|
**Built 2026-08-04**, before USB, because xHCI, any NIC and virtio-gpu all
|
||||||
|
need the same three things and none of them existed.
|
||||||
|
|
||||||
|
- **`kernel/dev/irq.c` + `include/irq.h`** — `irq_register(irq, fn, arg,
|
||||||
|
name, level_low)`. `trap.c` used to be a switch listing every interrupt by
|
||||||
|
name, which cannot work for a PCI device: its line comes out of config
|
||||||
|
space at probe time, so there is no constant to write a `case` label for.
|
||||||
|
Chains of up to 4 handlers per line, because PCI INTx pins are shared and a
|
||||||
|
handler must recognise that its own device is idle and return.
|
||||||
|
Registration publishes an entry then bumps `n` with a barrier, so dispatch
|
||||||
|
needs no lock. `/proc/interrupts` (minor 12) shows counts and trigger mode.
|
||||||
|
- **`kernel/core/dma.c` + `include/dma.h`** — `dma_alloc(size, align)` /
|
||||||
|
`dma_free(p, size)` (Linux's contract: caller remembers the size). Carved
|
||||||
|
off the top of physical memory in `main.c` *before* `kinit1`, because
|
||||||
|
kalloc's free list makes no contiguity promise. x86 PCI is cache coherent
|
||||||
|
so no PAT games are needed — but ordering is still your problem.
|
||||||
|
- **`pci_find_cap()` and `pci_msi_enable()`** in `kernel/dev/pci.c`.
|
||||||
|
|
||||||
|
**Four traps worth remembering:**
|
||||||
|
|
||||||
|
1. **PCI interrupts are level-triggered, active low.** `ioapicenable()`
|
||||||
|
hardcoded edge/active-high, which is right for every ISA-era device and
|
||||||
|
wrong for every PCI one — and it fails *silently*: you get exactly one
|
||||||
|
interrupt, then the device holds the line asserted, no further edge ever
|
||||||
|
comes, and it is never heard from again. `ioapicroute(irq, cpu,
|
||||||
|
level_low)` is the real call now.
|
||||||
|
2. **`IRQ_ERROR` (19) and `IRQ_SPURIOUS` (31) are LAPIC vectors that xv6
|
||||||
|
numbered as if they were IOAPIC pins**, so `T_IRQ0+19` is already taken. A
|
||||||
|
PCI device routed to pin 19 would collide. `irq_register` refuses them.
|
||||||
|
3. **`cprintf` has no field widths.** `%04x` is not a directive to it: it
|
||||||
|
prints `%` and `0` literally, consumes no argument, and every argument
|
||||||
|
after it lands in the wrong place. It understands only
|
||||||
|
`%d %u %l %x %p %b %s %%`.
|
||||||
|
4. **Unaligned MMIO reads return garbage.** Reading a uint32 at BAR offset 2
|
||||||
|
to get xHCI's HCIVERSION returned zero. Read the aligned dword and shift.
|
||||||
|
5. **Never let a device raise an interrupt before its line is claimed.** This
|
||||||
|
one cost real time. xHCI transfers completed, completion events sat in the
|
||||||
|
event ring, `USBSTS.EINT` and `IMAN.IP` were both set -- and the interrupt
|
||||||
|
count stayed at zero. Cause: the controller asserted INTx during
|
||||||
|
enumeration, while the IOAPIC entry was still masked. **An interrupt that
|
||||||
|
arrives at a masked pin is dropped, not deferred** (the 82093AA manual says
|
||||||
|
so, and QEMU implements it), and INTx is a *level* -- so the line then sits
|
||||||
|
high forever with no further transition for anyone to notice. Unmasking
|
||||||
|
afterwards finds nothing pending. The fix is ordering: enumerate with
|
||||||
|
controller interrupts off, then `irq_register`, then clear the device's
|
||||||
|
pending status so it drives the line low, and only then enable. The next
|
||||||
|
event is a genuine low-to-high transition.
|
||||||
|
|
||||||
|
Verify with `pwsh -File tools/bootcheck.ps1 -Usb`. See [[schunix-usb-xhci]],
|
||||||
|
[[schunix-pci-drivers]].
|
||||||
114
.claude/memory/schunix-e1000-and-driver-layout.md
Normal file
114
.claude/memory/schunix-e1000-and-driver-layout.md
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
---
|
||||||
|
name: schunix-e1000-and-driver-layout
|
||||||
|
description: "The e1000 NIC driver, the drivers/ reorganisation by device class, and virtio-gpu as the QEMU default."
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: 9d8a2aef-bcf0-47a0-96c6-f706efab4a95
|
||||||
|
modified: 2026-08-08T20:17:14.134Z
|
||||||
|
---
|
||||||
|
|
||||||
|
2026-08-08. Three changes, all requested by Nexoniarz in one go.
|
||||||
|
|
||||||
|
## drivers/<class>/<driver>/
|
||||||
|
|
||||||
|
kernel/drivers/
|
||||||
|
registry.c the one list of what exists
|
||||||
|
gpu/vga/ gpu/virtio_gpu/
|
||||||
|
net/e1000/
|
||||||
|
bus/xhci/
|
||||||
|
usb/core/ usb/hid/ usb/storage/ usb/hub/
|
||||||
|
|
||||||
|
One directory per *driver*, not per class (Nexoniarz asked for this
|
||||||
|
specifically), so a driver that grows a register header, a second source file or
|
||||||
|
a firmware blob has somewhere to put it.
|
||||||
|
|
||||||
|
Matches `DEVCLASS_*` in `include/device.h`. Flat was eight files with four
|
||||||
|
called `usb_something`, and each driver added made it worse.
|
||||||
|
|
||||||
|
**Makefile note:** the compile rule is now
|
||||||
|
`@mkdir -p $(dir $@)`, not a fixed path — a hardcoded mkdir only creates the top
|
||||||
|
directory and every subdirectory build fails with "unable to open output file".
|
||||||
|
`KERN_DRIVERS` is built with `+=` per class rather than one long
|
||||||
|
`$(addprefix ...)`; scripted edits kept collapsing the line continuations.
|
||||||
|
|
||||||
|
## virtio-gpu is the QEMU default
|
||||||
|
|
||||||
|
`QEMU_DISPLAY = -vga std -device virtio-gpu-pci` in the Makefile, used by all
|
||||||
|
three qemu targets. **Keep `-vga std`** — the bootloader draws the splash
|
||||||
|
through VBE in 16-bit real mode and there is no virtio path that early, so a
|
||||||
|
machine with only virtio-gpu boots blind until the kernel is up.
|
||||||
|
|
||||||
|
## FreeBSD reference tree
|
||||||
|
|
||||||
|
`Refrence/freebsd-src` (spelled that way on disk) is a full FreeBSD checkout,
|
||||||
|
re-cloned 2026-08-08 after the NetBSD one was deleted for space. Never built,
|
||||||
|
never in an image; recorded in `docs/layout.md` and `docs/licensing.md` §4.
|
||||||
|
|
||||||
|
**It is 2.1 GB** — measured, and the same reason the NetBSD one was deleted.
|
||||||
|
Only `sys/dev`, `sys/net` and `sys/netinet` have ever been read, so a sparse
|
||||||
|
checkout (`--filter=blob:none --sparse`, then
|
||||||
|
`sparse-checkout set sys/dev sys/net sys/netinet`) is ~100 MB and loses nothing.
|
||||||
|
Single-file fetches from `raw.githubusercontent.com` answered every licence
|
||||||
|
question so far and cost nothing at all.
|
||||||
|
|
||||||
|
**`sys/dev/e1000/` is Intel BSD-3-clause with zero advertising clauses** —
|
||||||
|
checked. Its *device ID table* was transcribed into `include/e1000.h`, taking
|
||||||
|
the driver from binding one card (QEMU's 82540EM) to the whole 8254x family:
|
||||||
|
82540/41/44/45/46/47, ~27 IDs. That is the shape of adoption that works —
|
||||||
|
**data, not logic**. The driver stays written from Intel manual 317453, and
|
||||||
|
`NOTICE` carries the Intel attribution.
|
||||||
|
|
||||||
|
The PCIe 8257x parts (82571-82574) were deliberately *excluded*: similar
|
||||||
|
register map, different NVM access and init. Claiming a device the driver cannot
|
||||||
|
initialise is worse than not claiming it — it binds, reports a nonsense MAC, and
|
||||||
|
fails for reasons that look nothing like the cause.
|
||||||
|
|
||||||
|
## e1000
|
||||||
|
|
||||||
|
`include/e1000.h` + `kernel/drivers/net/e1000.c`, from Intel's public manual
|
||||||
|
317453. Worked on the first boot:
|
||||||
|
|
||||||
|
e1000: 52:54:0:12:34:56, link up, irq 11
|
||||||
|
|
||||||
|
**Proven to carry real traffic**, 2026-08-08: `echo n > /proc/sysrq` runs
|
||||||
|
`e1000_selftest()`, which hand-builds an ARP request for 10.0.2.2 and waits for
|
||||||
|
the reply. Under QEMU user-mode networking:
|
||||||
|
|
||||||
|
e1000: asking who has 10.0.2.2
|
||||||
|
e1000: 64 bytes from 52:55:a:0:2:2 type 806
|
||||||
|
e1000: ARP reply -- 10.0.2.2 is at 52:55:a:0:2:2
|
||||||
|
e1000: self-test passed, 1 tx 1 rx
|
||||||
|
|
||||||
|
That one exchange validates the whole path: transmit descriptor format, the DD
|
||||||
|
handshake, TDT/RDT bookkeeping, DMA addresses, the receive-ring handback, and
|
||||||
|
**the MAC filter** -- an ARP reply is unicast to us, so a wrong RAL/RAH means it
|
||||||
|
never arrives. SLIRP is silent until spoken to, which is why the test has to
|
||||||
|
transmit before it can expect to receive. Do this kind of check *before*
|
||||||
|
layering a protocol stack on a driver: otherwise the stack's first failure is
|
||||||
|
indistinguishable from its own bugs.
|
||||||
|
|
||||||
|
Run with `-netdev user,id=n0 -device e1000,netdev=n0`. Provides `e1000_transmit()`,
|
||||||
|
`e1000_receive()` and the MAC — **no IP stack yet, deliberately**: a stack
|
||||||
|
written before a driver has nothing to test against and every bug in it looks
|
||||||
|
like a driver bug.
|
||||||
|
|
||||||
|
Details that are easy to get wrong and are commented in the source:
|
||||||
|
- **Set `E1000_TXD_STAT_DD` on every transmit descriptor at init.** The hardware
|
||||||
|
only sets it after *using* one, so a driver that waits for a bit the device
|
||||||
|
has never had reason to write waits forever.
|
||||||
|
- **RDT starts at NRX-1, not NRX.** One descriptor must always be held back or
|
||||||
|
"ring full" and "ring empty" are indistinguishable.
|
||||||
|
- **Clear the multicast table.** It is undefined after reset, so left alone the
|
||||||
|
device accepts multicast for addresses nobody asked for.
|
||||||
|
- **Read the MAC from EEPROM *and* fall back to RAL/RAH.** Some cards leave the
|
||||||
|
EEPROM path unimplemented and you end up with all zeroes.
|
||||||
|
- **`irq_register(..., level_low = 1)`** — PCI interrupts are level-triggered,
|
||||||
|
active low. Edge-configured, it fires once then wedges.
|
||||||
|
- Rings and buffers come from `dma_alloc`; the device reads them by physical
|
||||||
|
address and kalloc pages are contiguous only by accident.
|
||||||
|
- Enable the receiver *after* the ring base is set, or it DMAs into physical 0,
|
||||||
|
which on this kernel is the boot page table.
|
||||||
|
|
||||||
|
`ops` is null on the pci_driver: there is no net class in the device model yet.
|
||||||
|
Adding one is the natural first step when the IP stack arrives.
|
||||||
55
.claude/memory/schunix-framebuffer-device.md
Normal file
55
.claude/memory/schunix-framebuffer-device.md
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
---
|
||||||
|
name: schunix-framebuffer-device
|
||||||
|
description: /dev/fb0 is a real device with the Linux fbdev ABI; nothing may assume where the framebuffer is. Test with bootcheck.ps1 -Fb and tools/fbshot.ps1
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: a665e14b-86bd-4b41-a228-51377f3e9225
|
||||||
|
modified: 2026-08-04T16:16:17.735Z
|
||||||
|
---
|
||||||
|
|
||||||
|
**`/dev/fb0` (major 2) is a real framebuffer device** as of 2026-08-04:
|
||||||
|
`kernel/dev/fb.c` (read/write by byte offset, plus `fb_ioctl`), declared in
|
||||||
|
`include/fb.h`. Before this it was wired to `{ nullread, nullwrite }` — writing
|
||||||
|
pixels to it succeeded and did nothing.
|
||||||
|
|
||||||
|
**`include/fb.h` holds Linux's fbdev request numbers and struct layouts**
|
||||||
|
(`FBIOGET_VSCREENINFO` 0x4600, `fb_var_screeninfo` 160 bytes,
|
||||||
|
`fb_fix_screeninfo` 80 bytes). Same reasoning as
|
||||||
|
[[schunix-linux-open-flags]]: a shared number must mean the shared thing, or a
|
||||||
|
program gets a differently-shaped answer to a request it thought it
|
||||||
|
understood.
|
||||||
|
|
||||||
|
**Nothing outside `vga.c` may assume where the framebuffer is.** Ask
|
||||||
|
`vga_fb_phys()`, `vga_fb_size()`, `vga_fb_stride()`, `vga_fb_bpp()`,
|
||||||
|
`vga_fb_ptr()`. Three places used to hardcode `0xfd000000`, two of them also
|
||||||
|
hardcoding `800*600*4` — right only on QEMU stdvga at one resolution.
|
||||||
|
|
||||||
|
Two traps that cost time here:
|
||||||
|
|
||||||
|
- **`setupkvm()` runs before `vga_init()`**, so it cannot know the framebuffer
|
||||||
|
address. It used to identity-map one anyway, user-writable, into every
|
||||||
|
process ever created. That mapping is now gone entirely — nothing used it,
|
||||||
|
since userspace reaches the framebuffer through `mmap("/dev/fb0")`.
|
||||||
|
- **`sys_ioctl` answers every non-tty request with 0.** Any new device ioctl
|
||||||
|
must be routed *before* that catch-all, or the caller is told "success" and
|
||||||
|
reads an untouched buffer — a 0x0 screen that looks like a driver bug rather
|
||||||
|
than a missing one.
|
||||||
|
|
||||||
|
**To check it:**
|
||||||
|
|
||||||
|
```
|
||||||
|
pwsh -File tools/bootcheck.ps1 -Fb # 9 assertions on the reported geometry
|
||||||
|
pwsh -File tools/fbshot.ps1 -Mode fill # gradient via mmap, screendumped
|
||||||
|
pwsh -File tools/fbshot.ps1 -Mode bars # colour bars via write()
|
||||||
|
```
|
||||||
|
|
||||||
|
`cmd/fbinfo` is the program those drive. `fill` goes through mmap and `bars`
|
||||||
|
through the device's `write()` **on purpose**: when one works and the other
|
||||||
|
does not, the failure says which half broke.
|
||||||
|
|
||||||
|
Serial output cannot catch a stride bug — a framebuffer written with the wrong
|
||||||
|
row pitch reports every byte accepted and merely shears the picture. That is
|
||||||
|
why fbshot takes an actual screenshot; see
|
||||||
|
[[schunix-verify-by-driving-shell]]. Convert with `convert` in the WSL box,
|
||||||
|
not `magick`.
|
||||||
49
.claude/memory/schunix-iso-boot.md
Normal file
49
.claude/memory/schunix-iso-boot.md
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
---
|
||||||
|
name: schunix-iso-boot
|
||||||
|
description: "`make iso` builds a bootable CD; it needs El Torito no-emulation plus ATAPI because the loader never uses INT 13h"
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: a665e14b-86bd-4b41-a228-51377f3e9225
|
||||||
|
modified: 2026-08-04T17:28:16.714Z
|
||||||
|
---
|
||||||
|
|
||||||
|
**`make iso` produces a bootable `schunix.iso`** (added 2026-08-04). Always a
|
||||||
|
RAMROOT build — a disc that needed a second disk for its root would not be an
|
||||||
|
install medium, and read-only media cannot host a writable root anyway.
|
||||||
|
|
||||||
|
**Why it is built the way it is.** Schunix's loader never calls INT 13h: it
|
||||||
|
drives the IDE registers directly, and stage2 has already switched to
|
||||||
|
protected mode before it reads the kernel. That rules out both El Torito
|
||||||
|
emulation modes (hard-disk emulation exists only through the BIOS) and floppy
|
||||||
|
emulation (2.88 MB cap). So:
|
||||||
|
|
||||||
|
- `boot/cdboot.S` — a separate stage 1 for optical media. `tools/mkiso.sh`
|
||||||
|
passes `-boot-load-size 25`, so the BIOS loads cdboot *and* all 24 sectors of
|
||||||
|
stage2 before anything of ours runs, putting stage2 at 0x7E00 exactly where
|
||||||
|
`bootasm.S` would have. cdboot only records the medium and jumps.
|
||||||
|
- The medium flag is one byte past the verbose flag, at `VBE_INFO_ADDR + 0x101`.
|
||||||
|
**`bootasm.S` must clear it** or a warm reboot from a disc sends a hard-disk
|
||||||
|
boot looking for an optical drive.
|
||||||
|
- `stage2main.c` gained an ATAPI reader (PACKET + SCSI READ(12), 2048-byte
|
||||||
|
blocks with a one-block cache) and finds where it was loaded from by reading
|
||||||
|
the El Torito boot catalog: LBA 17, pointer at offset 0x47, then Load RBA at
|
||||||
|
catalog offset 40.
|
||||||
|
- **Do not pass `-boot-info-table`** to xorriso — it patches 56 bytes over
|
||||||
|
offset 8 of the boot image, which is the middle of cdboot's code. That is
|
||||||
|
why the boot catalog is parsed instead.
|
||||||
|
|
||||||
|
**The trap that cost the most time here:** ATAPI's 0x14/0xEB signature is only
|
||||||
|
valid until something issues a command, and the BIOS has just finished reading
|
||||||
|
the disc to load us — so on a CD boot the signature is *always* stale.
|
||||||
|
`atapi_reset()` (SRST via the control port at `io + 0x206`) is what makes the
|
||||||
|
probe work.
|
||||||
|
|
||||||
|
Two unbounded waits were hiding behind that, both of which hung with no
|
||||||
|
message on a machine with no ATA disk: `waitdisk()` in stage2main.c and
|
||||||
|
`idewait()` in kernel/dev/ide.c. Both are bounded now, `ideinit()` tolerates an
|
||||||
|
empty channel, and anything that *probes* a disk must call
|
||||||
|
`ide_disk_present()` first — `iderw()` rightly panics on an absent drive.
|
||||||
|
|
||||||
|
Check it with `pwsh -File tools/bootcheck.ps1 -Iso -Fb`, which boots with no
|
||||||
|
hard disk attached at all. See [[schunix-verify-by-driving-shell]].
|
||||||
44
.claude/memory/schunix-layout-migration.md
Normal file
44
.claude/memory/schunix-layout-migration.md
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
---
|
||||||
|
name: schunix-layout-migration
|
||||||
|
description: "The bt/ -> ports/ + lib/ + cmd/ restructure done 2026-08-01, and the two build traps it exposed"
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: d1a1ea76-f919-411c-b0ef-0e1bc5064563
|
||||||
|
modified: 2026-08-01T17:17:12.984Z
|
||||||
|
---
|
||||||
|
|
||||||
|
Restructured Schunix's tree on 2026-08-01. `bt/` became `ports/` (third-party
|
||||||
|
only, Makefile + patches, nothing else), first-party userland split into `lib/`
|
||||||
|
(runtime glue) and `cmd/<name>/main.c` (programs), tarballs to `dist/`, and all
|
||||||
|
generated output to `build/`. Documented in `docs/layout.md`. Verified: full
|
||||||
|
build passes and the image boots to `init: ready (pid 1)`.
|
||||||
|
|
||||||
|
**Two traps that bit during this and will bite again:**
|
||||||
|
|
||||||
|
1. `ld -r -b binary` derives symbol names from the input file's **path**. The
|
||||||
|
kernel embeds `initcode` and `entryother` this way, and the root Makefile
|
||||||
|
has `--redefine-sym _binary_build_lib_initcode_start=...` lines. Move either
|
||||||
|
blob's build location and the link breaks.
|
||||||
|
|
||||||
|
2. **A stamp file inside the directory it guards is always out of date.**
|
||||||
|
`ports/musl`'s `$(SRCDIR)/.patched` rule depended on `$(SRCDIR)`, but
|
||||||
|
creating the stamp bumps that directory's mtime, so the rule re-ran on every
|
||||||
|
build and failed (a patch does not apply twice). Same class of bug in
|
||||||
|
toybox and mksh. Fixed by making `$(SRCDIR)` an order-only prerequisite
|
||||||
|
(`| $(SRCDIR)`) in all three ports.
|
||||||
|
|
||||||
|
**A real thing that was silently lost-in-waiting:** musl's patch file claimed
|
||||||
|
two modified files; the build tree actually had five. The three undocumented
|
||||||
|
ones were in mallocng — two are inert debug tracing under
|
||||||
|
`#ifdef SCHUNIX_MALLOC_DEBUG`, but `glue.h` had a **required** build fix
|
||||||
|
(forward declarations for `__mmap`/`__madvise`/`__mremap`) that existed only as
|
||||||
|
a hand-edit in the build tree. Any `make distclean` would have silently lost
|
||||||
|
it. The patch was regenerated from a real diff against pristine musl 1.2.5 and
|
||||||
|
dry-run verified.
|
||||||
|
|
||||||
|
Backups from that session (delete when confident): the pre-migration tarball
|
||||||
|
`~/schunix-backup-20260801-1856.tar.gz` and `~/toybox-known-good`, both inside
|
||||||
|
WSL.
|
||||||
|
|
||||||
|
See [[schunix-build-environment]], [[schunix-rewrite-priorities]].
|
||||||
68
.claude/memory/schunix-licensing.md
Normal file
68
.claude/memory/schunix-licensing.md
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
---
|
||||||
|
name: schunix-licensing
|
||||||
|
description: "Schunix is Apache-2.0, copyright Nexoniarz; BSD drivers yes, Linux no, and ports/ licences bind regardless"
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: a665e14b-86bd-4b41-a228-51377f3e9225
|
||||||
|
modified: 2026-08-08T16:18:39.569Z
|
||||||
|
---
|
||||||
|
|
||||||
|
**`LICENSE` is Apache-2.0**, copyright *Nexoniarz* (not "Schunix Contributors" —
|
||||||
|
one author, and a fictional collective makes future re-licensing harder).
|
||||||
|
Chosen over MIT/BSD for the express patent grant.
|
||||||
|
|
||||||
|
**Settled, do not reopen:** the "Apache-2.0 but shared-source" question was
|
||||||
|
discussed at length in 2026-08 and **on 2026-08-08 Nexoniarz asked for it to be
|
||||||
|
dropped from the docs entirely** — `docs/licensing.md` is about licensing, not
|
||||||
|
about that debate. All references are gone from `docs/`, `LICENSE` and `NOTICE`;
|
||||||
|
don't reintroduce them. The substance that survives, in one line inside §1:
|
||||||
|
Apache-2.0 §2 is an irrevocable grant, so a restriction stated anywhere else
|
||||||
|
does not narrow it, and restricting distribution would be a licence change
|
||||||
|
(bespoke proprietary, or BUSL-1.1) rather than a wording change.
|
||||||
|
|
||||||
|
The decision that actually mattered to them is **when to publish**, not the
|
||||||
|
licence; the standing rule is still never ([[nexoniarz-sharing-preference]]).
|
||||||
|
|
||||||
|
**Binding regardless of what they pick:**
|
||||||
|
- The kernel derives from xv6 (MIT). `LICENSE.MIT` was *missing* — it was
|
||||||
|
referenced as `bk/LICENSE.MIT` and lost in the `bk/` → `kernel/` migration.
|
||||||
|
Recreated; must not be deleted.
|
||||||
|
- `ports/` is other people's code, redistributed in every image: musl (MIT),
|
||||||
|
toybox (0BSD), mksh (MirOS). `NOTICE` carries the attribution musl and mksh
|
||||||
|
require. Patches under `ports/*/patches/` inherit *their* licences.
|
||||||
|
|
||||||
|
New first-party files carry `// SPDX-License-Identifier: Apache-2.0`.
|
||||||
|
|
||||||
|
**Drivers: BSD yes, Linux no** (asked 2026-08-04, "for drivers we can try get
|
||||||
|
it from like Linux?"). Linux is GPL-2.0-**only**, and Apache-2.0's patent and
|
||||||
|
indemnity clauses are "further restrictions" under GPLv2 §6 — the two cannot
|
||||||
|
be combined and redistributed. Using a Linux driver means relicensing all of
|
||||||
|
Schunix to GPLv2 and dropping Apache-2.0; you cannot keep both. FreeBSD /
|
||||||
|
NetBSD / OpenBSD are BSD-2/3-clause and **are** compatible: keep the notice,
|
||||||
|
add to `NOTICE`, done. Reading Linux to understand hardware is fine;
|
||||||
|
translating a driver function-by-function produces a derivative work whatever
|
||||||
|
the identifiers say. Best source of all is the public specification — xHCI,
|
||||||
|
virtio, AHCI, NVMe, e1000 — which carries no licence on what you write from
|
||||||
|
it. Recorded in `docs/roadmap.md` §1. See [[schunix-roadmap-and-projects]].
|
||||||
|
|
||||||
|
|
||||||
|
## fs.c is third-party, and was unattributed until 2026-08-09
|
||||||
|
|
||||||
|
`kernel/fs/fs.c` (and parts of `tools/mkfs.c`) derive from **"New File System
|
||||||
|
For XV6"** by Qi Ma, Leyi Pan, Ao Sun and Peiran Xu -- **MIT License**. Nexoniarz
|
||||||
|
mentioned the provenance in passing; the tree had **no attribution at all**,
|
||||||
|
only a comment crediting xv6. That was a live compliance gap, since MIT requires
|
||||||
|
the notice be retained in substantial portions.
|
||||||
|
|
||||||
|
Their work is what the HTree directory index (`BKDRHash`, `HASHRANGE`,
|
||||||
|
`OVERFLOW_DIR_START`), the Ext4-style extents (`T_EXTENT`), the inode bitmap and
|
||||||
|
`divesymlink` all are. Now credited in `NOTICE` and in fs.c's header.
|
||||||
|
|
||||||
|
**The obligation does not go away when fs.c does.** It attaches to every copy
|
||||||
|
already made, so the NOTICE entry stays even after the SchFS migration deletes
|
||||||
|
the file.
|
||||||
|
|
||||||
|
**The general lesson:** ask where code came from before assuming it is
|
||||||
|
first-party. "It is in our tree and looks like xv6" was wrong here, and the only
|
||||||
|
reason it surfaced was an offhand remark.
|
||||||
30
.claude/memory/schunix-linux-abi-decision.md
Normal file
30
.claude/memory/schunix-linux-abi-decision.md
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
---
|
||||||
|
name: schunix-linux-abi-decision
|
||||||
|
description: Decided 2026-08-01 — Schunix drops its native syscall ABI and keeps only the Linux one
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: d1a1ea76-f919-411c-b0ef-0e1bc5064563
|
||||||
|
modified: 2026-08-01T17:16:39.021Z
|
||||||
|
---
|
||||||
|
|
||||||
|
Decided 2026-08-01: **Schunix will use the Linux syscall ABI as its only ABI.**
|
||||||
|
The native Schunix numbering gets deleted. Not yet implemented as of that date.
|
||||||
|
|
||||||
|
**Why:** the kernel currently carries two ABIs — 185 native entries in
|
||||||
|
`kernel/sys/table.inc` and 157 Linux numbers mapped in `kernel/sys/linux_abi.c`
|
||||||
|
— with a `proc->syscall_map` indirection on every syscall to pick between them.
|
||||||
|
100% of the shipping userland (musl, mksh, toybox) is Linux-ABI. Exactly one
|
||||||
|
program used the native ABI (`cmd/init/main.c`), and `exec.c` chose the ABI by
|
||||||
|
*guessing from the ELF entry address* (`>= 0x100000`).
|
||||||
|
|
||||||
|
**How to apply:** rebuild init against musl like every other program, then
|
||||||
|
delete `lib/usys.S`, `sys_enable_linux_abi`, the `syscall_map` indirection, the
|
||||||
|
entry-address heuristic in `exec.c`, and the duplicate native `struct stat`
|
||||||
|
path (`stati_linux` vs native). Roughly −500 lines.
|
||||||
|
|
||||||
|
The user was uneasy that this makes Schunix "not a real UNIX" — it does not.
|
||||||
|
FreeBSD (linuxulator), illumos (lx zones), and WSL1 all implement the Linux
|
||||||
|
ABI. The ABI is a calling convention, not the OS.
|
||||||
|
|
||||||
|
See [[schunix-rewrite-priorities]], [[schunix-build-environment]].
|
||||||
37
.claude/memory/schunix-linux-open-flags.md
Normal file
37
.claude/memory/schunix-linux-open-flags.md
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
---
|
||||||
|
name: schunix-linux-open-flags
|
||||||
|
description: "include/fcntl.h must hold Linux's open-flag values — the userland is all Linux-ABI, and a mismatch silently corrupts files"
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: a665e14b-86bd-4b41-a228-51377f3e9225
|
||||||
|
modified: 2026-08-03T13:59:19.392Z
|
||||||
|
---
|
||||||
|
|
||||||
|
**`include/fcntl.h` holds Linux x86-64 open-flag values, and must keep holding
|
||||||
|
them.** Every program in the userland is built against a libc that sends Linux
|
||||||
|
values; the kernel reads them with these constants. Do not "tidy" them into a
|
||||||
|
neat sequence — they are an external interface.
|
||||||
|
|
||||||
|
They disagreed until 2026-08-03, and the symptoms looked like filesystem
|
||||||
|
corruption rather than a header mistake:
|
||||||
|
|
||||||
|
- Linux `O_APPEND` (0x400) landed on Schunix's `O_TRUNC`, so **`cmd >> file`
|
||||||
|
truncated** the file it was supposed to extend.
|
||||||
|
- Linux `O_TRUNC` (0x200) landed on `O_CREATE`, so **`cmd > file` did not
|
||||||
|
truncate** and left whatever was past the new end in place.
|
||||||
|
|
||||||
|
Found by making `tools/bootcheck.ps1` do ordinary shell work — write a file,
|
||||||
|
append to it, `wc -c` the result — instead of only booting to a prompt. The
|
||||||
|
byte count was the only thing that said which failure had happened. See
|
||||||
|
[[schunix-verify-by-driving-shell]].
|
||||||
|
|
||||||
|
Two related traps fixed at the same time, both worth re-checking after any
|
||||||
|
syscall work:
|
||||||
|
|
||||||
|
- `sys_openat` silently dropped `O_TRUNC` and `O_APPEND` while `sys_open`
|
||||||
|
honoured both, so the same redirection behaved differently depending on which
|
||||||
|
entry point libc called. **Any flag handled in one must be handled in both.**
|
||||||
|
- `rmdir` was `NO_LINUX` in `kernel/sys/table.inc`, so every Linux-ABI program
|
||||||
|
got ENOSYS. A syscall with no Linux number is unreachable from userland —
|
||||||
|
see [[schunix-syscall-single-table]].
|
||||||
61
.claude/memory/schunix-network-stack-choice.md
Normal file
61
.claude/memory/schunix-network-stack-choice.md
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
---
|
||||||
|
name: schunix-network-stack-choice
|
||||||
|
description: "Don't write TCP from scratch — and don't take NetBSD's either; the licence and porting evidence for lwIP vs FreeBSD."
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: 9d8a2aef-bcf0-47a0-96c6-f706efab4a95
|
||||||
|
modified: 2026-08-08T20:25:37.412Z
|
||||||
|
---
|
||||||
|
|
||||||
|
Decided 2026-08-08. Nexoniarz: "we should take FreeBSD or NetBSD and their
|
||||||
|
Network stuff because it's going to be hell making own TCP/IP." **Agreed on the
|
||||||
|
premise** — TCP is the one part of a network stack where writing it yourself is
|
||||||
|
a bad trade. It is not the packet format that is hard; it is congestion control,
|
||||||
|
retransmission, window management and twenty years of corner cases that only
|
||||||
|
appear against a real peer.
|
||||||
|
|
||||||
|
**But the source matters, and NetBSD is the wrong one.**
|
||||||
|
|
||||||
|
## The licence evidence (all verified, not assumed)
|
||||||
|
|
||||||
|
| | advertising clause? |
|
||||||
|
|---|---|
|
||||||
|
| NetBSD `sys/netinet/tcp_input.c`, `tcp_output.c` | **Yes** — the WIDE Project's 4-clause |
|
||||||
|
| FreeBSD `sys/netinet/tcp_input.c` | **No** — UC, Swinburne, FreeBSD Foundation, Juniper |
|
||||||
|
| lwIP | **No** — BSD-3, Swedish Institute of Computer Science |
|
||||||
|
|
||||||
|
The NetBSD clause belongs to **WIDE**, not Berkeley, so the 1999 rescission does
|
||||||
|
not reach it. Checked FreeBSD's file directly from `raw.githubusercontent.com`;
|
||||||
|
zero matches for "All advertising materials". Checked lwIP's `COPYING`; likewise
|
||||||
|
zero. This **reverses** the older assumption in `docs/roadmap.md` that NetBSD
|
||||||
|
was the intended starting point.
|
||||||
|
|
||||||
|
## The porting evidence, which matters more
|
||||||
|
|
||||||
|
FreeBSD's `sys/netinet` is not a library. `tcp_input.c` alone is 122 KB and
|
||||||
|
reaches into mbufs, `epoch(9)`, netisr, VNET, `uma(9)`, sockbufs, inpcb hash
|
||||||
|
tables and FreeBSD's locking. Taking it means porting FreeBSD's kernel
|
||||||
|
infrastructure first and finding the TCP underneath — the same conclusion
|
||||||
|
`docs/roadmap.md` §1 already reached about `sys/dev/usb`.
|
||||||
|
|
||||||
|
**lwIP was written for the situation Schunix is actually in:** a small OS with a
|
||||||
|
raw Ethernet driver and no infrastructure. Port layer is a handful of files, no
|
||||||
|
dependency on the host allocator or locking, and it brings ARP, IPv4, ICMP, UDP,
|
||||||
|
TCP, DHCP and DNS.
|
||||||
|
|
||||||
|
**Recommendation: lwIP first, FreeBSD as the reference to check against.**
|
||||||
|
|
||||||
|
## Practical notes
|
||||||
|
|
||||||
|
- Nexoniarz deleted the NetBSD checkout (`src-trunk/`) for space and said it can
|
||||||
|
be re-fetched. **Do not clone a whole BSD tree** — a sparse checkout of
|
||||||
|
`sys/net` and `sys/netinet` is all that has ever been needed, and the space
|
||||||
|
complaint was legitimate.
|
||||||
|
- One-file fetches from `raw.githubusercontent.com` are enough for a licence
|
||||||
|
check and cost nothing. That is how both findings above were made.
|
||||||
|
- Shell trap hit while doing this: `grep -c` **exits 1 when it finds zero
|
||||||
|
matches**, so `cmd && grep -c ... || echo "failed"` prints the failure message
|
||||||
|
on a perfectly successful run. Nearly reported "no network access" when the
|
||||||
|
fetch had worked.
|
||||||
|
- Whichever is chosen, it needs a `NOTICE` entry — see [[schunix-licensing]].
|
||||||
45
.claude/memory/schunix-pci-drivers.md
Normal file
45
.claude/memory/schunix-pci-drivers.md
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
---
|
||||||
|
name: schunix-pci-drivers
|
||||||
|
description: PCI enumeration lives in kernel/dev/pci.c; drivers that find their own hardware go in kernel/drivers/ and are listed in registry.c
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: a665e14b-86bd-4b41-a228-51377f3e9225
|
||||||
|
modified: 2026-08-04T17:28:31.246Z
|
||||||
|
---
|
||||||
|
|
||||||
|
**`kernel/dev/pci.c` enumerates the PCI bus** (added 2026-08-04), through the
|
||||||
|
old 0xCF8/0xCFC config mechanism — the one that works without first finding an
|
||||||
|
ACPI table. The scan recurses through bridges rather than sweeping all 256
|
||||||
|
buses flat, so it also knows what sits behind what.
|
||||||
|
|
||||||
|
BAR sizes are found by writing all ones and reading back the bits the device
|
||||||
|
does not decode; **the original value must be restored**, since that briefly
|
||||||
|
points a live device's decoder at the top of the address space. 64-bit BARs
|
||||||
|
consume the following slot. Only header type 0 has six BARs — a bridge is
|
||||||
|
type 1 with a completely different layout.
|
||||||
|
|
||||||
|
**`kernel/drivers/` is the folder for drivers that have to *find* their
|
||||||
|
hardware.** Fixed-address legacy devices (console, keyboard, the IDE ports)
|
||||||
|
stay in `kernel/dev/`. To add one: write `kernel/drivers/<name>.c` exporting a
|
||||||
|
`struct pci_driver`, declare it in `kernel/drivers/registry.c`, add it to the
|
||||||
|
array there, and add the object to `KERN_DRIVERS` in the Makefile.
|
||||||
|
|
||||||
|
The registry is a plain array rather than the linker-section trick, on
|
||||||
|
purpose: it keeps the list of what is in the kernel somewhere you can read.
|
||||||
|
A driver silently dropped by the linker looks identical to one never written.
|
||||||
|
|
||||||
|
`kernel/drivers/vga_pci.c` is the first driver and the template. It draws
|
||||||
|
nothing — vga.c still owns the framebuffer — but it cross-checks the aperture
|
||||||
|
it finds on the bus against the address VBE reported. That disagreement is
|
||||||
|
exactly the symptom that would have surfaced first on hardware where the old
|
||||||
|
hardcoded `0xfd000000` was wrong. See [[schunix-framebuffer-device]].
|
||||||
|
|
||||||
|
**`cat /proc/pci`** lists everything found, lspci-shaped, with the binding
|
||||||
|
driver in brackets (`[-]` when unclaimed). Under QEMU's default i440fx it
|
||||||
|
should show six devices; `tools/bootcheck.ps1` asserts on the host bridge and
|
||||||
|
the IDE controller by ID, and on `[vga]` having bound.
|
||||||
|
|
||||||
|
`inl`/`outl` were added to include/x86.h for this — config space is only
|
||||||
|
reachable in dwords, so `pci_read16`/`pci_read8` shift and mask the containing
|
||||||
|
dword rather than doing a narrower bus cycle.
|
||||||
71
.claude/memory/schunix-qr-encoder.md
Normal file
71
.claude/memory/schunix-qr-encoder.md
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
---
|
||||||
|
name: schunix-qr-encoder
|
||||||
|
description: "kernel/core/qr.c — the crash-report QR encoder, how it's verified, and the segno padding bug that is NOT ours."
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: 9d8a2aef-bcf0-47a0-96c6-f706efab4a95
|
||||||
|
modified: 2026-08-08T16:22:01.468Z
|
||||||
|
---
|
||||||
|
|
||||||
|
Built 2026-08-08. `kernel/core/qr.c` + `include/qr.h`, the Phase 8 crash channel
|
||||||
|
(see [[schunix-crash-screen]] and `docs/roadmap.md`).
|
||||||
|
|
||||||
|
**Wired into the crash screen the same day.** `panic.c` builds a compact payload
|
||||||
|
and paints a white card on the right of PSOGM. Verified end-to-end with
|
||||||
|
`make crashshot && make crashqr`, which decodes the symbol out of the captured
|
||||||
|
framebuffer — not out of the encoder. Payload format `SXPSOGM1`: magic, reason,
|
||||||
|
`c<cpu> p<pid> t<ticks> <name>`, `b<kernel base>`, then call-chain frames as
|
||||||
|
**offsets from the kernel base** (8 hex digits, not 16 — that is what makes a
|
||||||
|
useful number of frames fit in 134 bytes). The reason is written against a
|
||||||
|
buffer end held back by 34 bytes, so a long message costs frames but never all
|
||||||
|
of them. Deliberately not a URL: it encodes the report itself and reaches
|
||||||
|
nothing.
|
||||||
|
|
||||||
|
**Scope, deliberately narrow:** byte mode only, EC level L only, versions 1–6
|
||||||
|
only (max 134 bytes). Version 7 is where QR starts needing an 18-bit
|
||||||
|
version-information block in two extra places; version 10 is where the character
|
||||||
|
count grows to 16 bits and the alignment grid becomes 2-D. Stopping at 6 avoids
|
||||||
|
both. If a payload ever needs more, shorten the report rather than add the
|
||||||
|
tables. Freestanding: no allocation, no libc beyond memset/memmove, GF(256)
|
||||||
|
multiply by shift-and-add so there is no table init to depend on — all of which
|
||||||
|
matters because the caller is a panic handler.
|
||||||
|
|
||||||
|
**How it is verified — this is the important part.** A wrong QR still looks
|
||||||
|
exactly like a QR, so "it renders" proves nothing.
|
||||||
|
|
||||||
|
- `make qrtest` — self-checks (table arithmetic, RS generator polynomials,
|
||||||
|
finder/timing placement) and writes `build/qr.png` plus a terminal rendering
|
||||||
|
a phone can read straight off the screen. `-q` suppresses the ANSI art,
|
||||||
|
`-m TEXT` dumps the module matrix, `-f TEXT` dumps the function-pattern map.
|
||||||
|
- `make qrcheck` (`tools/qrcheck.py`) — the real oracle. Decodes with OpenCV,
|
||||||
|
*and* compares module-for-module against segno. Needs
|
||||||
|
`pip install --target <dir> opencv-python-headless numpy segno` and
|
||||||
|
PYTHONPATH; not wired into `make`.
|
||||||
|
|
||||||
|
**Two traps found doing this, both worth remembering:**
|
||||||
|
|
||||||
|
1. **segno adds a spurious zero pad codeword to every byte-mode symbol.** Its
|
||||||
|
`write_padding_bits()` does `extend([0] * (8 - (length % 8)))`, which emits a
|
||||||
|
whole byte when the stream is already codeword-aligned — and in byte mode it
|
||||||
|
always is (8L+16 bits after the terminator). ISO/IEC 18004 makes it
|
||||||
|
conditional and segno quotes that exact sentence above the line. Harmless
|
||||||
|
(decoders read `count` bytes and ignore padding) but it means the two
|
||||||
|
encoders **cannot** agree bit-for-bit on any padded payload. qrcheck compares
|
||||||
|
full matrices only for exact-fill payloads and decodes the rest. Our
|
||||||
|
behaviour is correct; do not "fix" it to match.
|
||||||
|
2. **OpenCV's QRCodeDetector fails on perfectly valid symbols**, depending on
|
||||||
|
scale and quiet-zone width. qrcheck tries six renderings, and if one still
|
||||||
|
fails it re-tests segno's symbol for the same payload — if that fails too it
|
||||||
|
reports `[detector]`, not `[FAIL]`. Without that step the check produces
|
||||||
|
false failures, which is worse than not running it.
|
||||||
|
|
||||||
|
**Debugging technique that cracked it:** dump the module matrix *and* the
|
||||||
|
function map, run the zigzag backwards in Python to recover the codewords from
|
||||||
|
both encoders, and diff those. "First differing codeword index 18" localised it
|
||||||
|
instantly; comparing matrices only ever said "108 of 441 modules differ". Note
|
||||||
|
the mask must be read from the format bits and un-applied first — un-masking
|
||||||
|
with the wrong mask makes your own output look like noise, which cost a round.
|
||||||
|
|
||||||
|
Windows note: `subprocess` cannot launch a relative path with forward slashes;
|
||||||
|
`os.path.abspath` it.
|
||||||
55
.claude/memory/schunix-ramdisk-root-sizing.md
Normal file
55
.claude/memory/schunix-ramdisk-root-sizing.md
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
---
|
||||||
|
name: schunix-ramdisk-root-sizing
|
||||||
|
description: "Why `make iso` was failing with \"disk full\" — DOOM WADs in the ramdisk, not FSSIZE — and the three Makefile knobs that actually control that image."
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: 9d8a2aef-bcf0-47a0-96c6-f706efab4a95
|
||||||
|
modified: 2026-08-08T12:04:27.952Z
|
||||||
|
---
|
||||||
|
|
||||||
|
Fixed 2026-08-08. `make iso` died with `mkfs: disk full -- used 6000 of 6000
|
||||||
|
blocks` and the advice "Increase FSSIZE in param.h". **`FSSIZE` was 150000 and
|
||||||
|
entirely innocent.** The ramdisk root's size is `RAMDISK_BLOCKS` in the Makefile.
|
||||||
|
The error message has been rewritten to name the right knob per image and print
|
||||||
|
the metadata breakdown; don't trust the old advice if it resurfaces anywhere.
|
||||||
|
|
||||||
|
**The actual cause: `make doom` stages 33 MB of WADs into `sysroot/`**
|
||||||
|
(`freedoom1.wad` 28.8 MB, `doom1.wad` 4.2 MB) and the ramdisk was trying to
|
||||||
|
swallow them into 6 MB. Real userland is only ~1.9 MB — the 244 `/bin` entries
|
||||||
|
are *hardlinks* to one 822 KB toybox, which is why `du` says 35 MB while the
|
||||||
|
file sizes sum to 229 MB.
|
||||||
|
|
||||||
|
**This is a licensing rule, not just a size one, and that is the reason it is
|
||||||
|
enforced in the build rather than by remembering to run `make doom-uninstall`.**
|
||||||
|
The ramdisk is linked *into the kernel binary*; `doom1.wad` is
|
||||||
|
non-redistributable shareware, so a kernel containing it could not be
|
||||||
|
distributed. See [[schunix-doom-acceptance-test]] and [[schunix-licensing]].
|
||||||
|
|
||||||
|
Three Makefile knobs, none of them FSSIZE:
|
||||||
|
- `RAMDISK_BLOCKS` (6000) — image size, BSIZE is 1024.
|
||||||
|
- `RAMDISK_NINODES` (2000) — **the inode table is preallocated.** mkfs's default
|
||||||
|
12000 inodes cost 1501 of 6000 blocks, a quarter of the image, for ~260 files.
|
||||||
|
Metadata went 1596 → 345 blocks.
|
||||||
|
- `RAMDISK_EXCLUDE` (`usr/share/doom bin/doom`) — `mkfs --exclude <relpath>`,
|
||||||
|
repeatable, drops a whole subtree and prints what it skipped.
|
||||||
|
|
||||||
|
`mkfs` gained `--ninodes` and `--exclude`. Its option parser used to handle
|
||||||
|
exactly one flag then `break`, silently ignoring a second — now it compacts argv
|
||||||
|
properly, but both modes still identify themselves *positionally* (argv[2] is
|
||||||
|
`--sysroot` or the first file), so keep that contract if adding options.
|
||||||
|
|
||||||
|
**Verified:** ramroot boots with no disk attached at all, and the ISO boots from
|
||||||
|
optical media only (`ide: no drive on the primary channel`) to a full shell with
|
||||||
|
243 commands — 243 not 244 because `doom` is correctly excluded.
|
||||||
|
|
||||||
|
**`make iso` needs `mkisofs`, not xorriso, on this host.** There is no xorriso
|
||||||
|
package in scoop; cdrtools provides `mkisofs.exe` and was already installed.
|
||||||
|
`tools/mkiso.sh` now takes either — the El Torito flags are argument-compatible.
|
||||||
|
This also finally verified `-boot-load-size 49` (see
|
||||||
|
[[schunix-boot-splash-logo]]), which was untestable while xorriso was missing.
|
||||||
|
|
||||||
|
**Trap:** `make iso` builds the ramroot variant and does `rm -rf build/kernel`
|
||||||
|
first, so a failed run leaves objects compiled with `-DRAMDISK_ROOT` and the next
|
||||||
|
plain `make` dies on `_binary_ramdisk_size`. Recover with
|
||||||
|
`rm -rf build/kernel build/lib build/cmd`.
|
||||||
45
.claude/memory/schunix-rewrite-priorities.md
Normal file
45
.claude/memory/schunix-rewrite-priorities.md
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
---
|
||||||
|
name: schunix-rewrite-priorities
|
||||||
|
description: "The ordered list of what actually blocks Schunix, established by a full read of the kernel on 2026-08-01"
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: d1a1ea76-f919-411c-b0ef-0e1bc5064563
|
||||||
|
modified: 2026-08-01T19:01:25.126Z
|
||||||
|
---
|
||||||
|
|
||||||
|
Assessed the whole Schunix kernel (~12k hand-written lines) on 2026-08-01.
|
||||||
|
Code quality is good; the problem is that xv6 lacks the *concepts* a real UNIX
|
||||||
|
needs, so the fix is missing struct fields and subsystems, not bug fixing.
|
||||||
|
|
||||||
|
Order to work in (each item unblocks the next):
|
||||||
|
|
||||||
|
1. ~~**`struct proc` fields.**~~ **DONE 2026-08-01.** `xstate`, `killsig`,
|
||||||
|
`pgid`, `sid`, and uid/euid/suid + gid/egid/sgid added; `exit()` takes a
|
||||||
|
wait(2)-encoded status; waitpid() reports it. Verified: `sh -c "exit 42"`
|
||||||
|
yields `rc=42`, `id` prints `uid=0(root)`. Also required coupling
|
||||||
|
TIOCGPGRP to the real pgid — see [[schunix-tty-fg-pgrp-coupling]].
|
||||||
|
2. **Signal delivery on the trap return path.** Currently only fires on
|
||||||
|
syscall return (`kernel/sys/syscall.c`), so a process spinning in userland
|
||||||
|
never receives a signal.
|
||||||
|
3. **Fault-recoverable `copy_from_user`/`copy_to_user` with an exception
|
||||||
|
table.** `uva2ka()` returns a P2V alias valid for exactly one page; two
|
||||||
|
separate historical crashes trace to that single design flaw.
|
||||||
|
4. **A page-fault handler.** `trap.c` has no `T_PGFLT` case at all — hence no
|
||||||
|
demand paging, no COW, no lazy stack growth.
|
||||||
|
5. **SCHFS directory format.** Every directory is ≥113KB on disk, capped at
|
||||||
|
119KB; `DIRSIZ` is 14; `dirent.inum` is a `ushort` (65535-inode ceiling).
|
||||||
|
Disk-format break, so decide before accumulating images worth keeping.
|
||||||
|
6. **Then** the Ring 0A/0B split. Its own Phase 1 is "just file moves" — doing
|
||||||
|
it before 1-5 means moving the same files twice.
|
||||||
|
|
||||||
|
Separately, a real confirmed bug: `kernel/arch/x86_64/trapasm.S` saves
|
||||||
|
`rax/rcx/rdx` to three **global** scratch words (`_sc_rax` etc. in `vm.c`) with
|
||||||
|
a comment claiming "IF is off, no race" — but the Makefile runs `-smp 2`, and
|
||||||
|
`cli` only masks the local CPU. Two CPUs in `syscall_entry` clobber each
|
||||||
|
other's saved user `rip`. Use per-CPU storage.
|
||||||
|
|
||||||
|
Also: `KSTACKSIZE` is 4096 (one page) on x86-64; Linux uses 16K. The stack
|
||||||
|
canary exists because it really did overflow.
|
||||||
|
|
||||||
|
See [[schunix-linux-abi-decision]], [[schunix-layout-migration]].
|
||||||
80
.claude/memory/schunix-roadmap-and-projects.md
Normal file
80
.claude/memory/schunix-roadmap-and-projects.md
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
---
|
||||||
|
name: schunix-roadmap-and-projects
|
||||||
|
description: "The agreed roadmap through USB, GPU, fonts, network and the GUI; Schcom/SchWM/SchDE and the installer live in projects/, outside the OS tree"
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: a665e14b-86bd-4b41-a228-51377f3e9225
|
||||||
|
modified: 2026-08-04T23:05:57.185Z
|
||||||
|
---
|
||||||
|
|
||||||
|
**Set out 2026-08-04.** Recorded in `docs/roadmap.md`; the naming and the
|
||||||
|
split are Nexoniarz's.
|
||||||
|
|
||||||
|
Order asked for: USB → GPU → OTF/TTF → network + sockets → GUI → installer.
|
||||||
|
Kept, with one dependency fix: shared driver infrastructure came first
|
||||||
|
([[schunix-driver-infrastructure]]), because it was the first step of USB
|
||||||
|
rather than a detour from it.
|
||||||
|
|
||||||
|
**The GUI stack, all three named by Nexoniarz and all in `projects/`:**
|
||||||
|
|
||||||
|
- **Schcom** — Schunix Compositor. Owns the display and input; composites
|
||||||
|
client buffers. Policy is *not* its job.
|
||||||
|
- **SchWM** — window manager, a privileged Schcom client.
|
||||||
|
- **SchDE** — desktop environment: panel, launcher, settings.
|
||||||
|
- **schinst** — installer, installation environment, and `.sip` tooling.
|
||||||
|
|
||||||
|
`projects/` is at the repo root and is **not part of the kernel build** —
|
||||||
|
things that run *on* Schunix, not things that *are* Schunix. They link against
|
||||||
|
the sysroot libc and never against kernel headers. The reason is that each is
|
||||||
|
larger than the kernel will be for a long time, and grown in-tree the
|
||||||
|
boundary would blur until "does Schunix boot" stopped having a clear answer.
|
||||||
|
|
||||||
|
**The two hard blockers before Schcom can start**, both kernel work:
|
||||||
|
|
||||||
|
1. **Unix domain sockets.** `socket()` is still a `NO_LINUX` stub. Needed for
|
||||||
|
the client protocol and for `SCM_RIGHTS` — a pipe cannot carry a buffer
|
||||||
|
handle. Build these *before* TCP/IP; they are simpler and they are what
|
||||||
|
the socket layer's shape should be designed around.
|
||||||
|
2. **Raw input events** — an evdev-style `/dev/input`. Input only arrives
|
||||||
|
cooked through the tty line discipline, line-buffered to one foreground
|
||||||
|
process, which is not usable by a compositor.
|
||||||
|
|
||||||
|
shm and framebuffer mmap are already done ([[schunix-shm-and-mmap]]).
|
||||||
|
|
||||||
|
**`.sip`** = Schunix Installation Package, modelled on WIM: file-based rather
|
||||||
|
than block-based, so one file holds a whole tree, applies to any partition
|
||||||
|
size, and dedups across images. Most of it exists — `tools/mkschfs.c` and
|
||||||
|
`tools/mkfs.c` already do content dedup with an `(st_dev, st_ino)` fast path.
|
||||||
|
`mksip` should share that code.
|
||||||
|
|
||||||
|
**Confirmed order (2026-08-05):** finish USB, then sockets + raw input, then
|
||||||
|
virtio-gpu.
|
||||||
|
|
||||||
|
**Doom moved (2026-08-05).** It was "explicitly last"; it is now the
|
||||||
|
**acceptance test** — "an OS that doesn't run Doom is not a real OS".
|
||||||
|
fbDOOM at that. It is a real load test: framebuffer mmap, raw termios,
|
||||||
|
file I/O, allocator and timing, all at once. Everything on that list already
|
||||||
|
exists except possibly `gettimeofday`/`nanosleep` resolution, so **it could
|
||||||
|
be attempted today** — it needs no sockets, USB, virtio-gpu or fonts.
|
||||||
|
**It is a test, not a component** — clarified by Nexoniarz: built and run to
|
||||||
|
prove things work, never shipped in the system. That disposes of licensing
|
||||||
|
entirely, since nothing is redistributed. Belongs with the test tooling (a
|
||||||
|
`make doomtest`, same shape as `make crashshot`), not in `cmd/` or the
|
||||||
|
sysroot. *If* it were ever shipped the licensing returns: engine is GPL-2.0
|
||||||
|
(fine in `ports/`, never `cmd/`), and the WAD would have to be **Freedoom**
|
||||||
|
(BSD-3-clause) because `doom1.wad` is not redistributable.
|
||||||
|
|
||||||
|
**Sized honestly and not to be underestimated:** OTF/TTF is a rasteriser plus
|
||||||
|
a variable-width glyph cache, and the console assumes a fixed 8x16 cell grid
|
||||||
|
throughout — proportional text is a new rendering layer, not a font swap.
|
||||||
|
|
||||||
|
**Fonts must be public domain or very permissive** — "PD0", clarified
|
||||||
|
2026-08-05 (the earlier note read it as "CY-NO", which was never a real
|
||||||
|
thing). CC0 is ideal; MIT/BSD/Apache fine. SIL OFL is usable but carries a
|
||||||
|
rename-if-modified obligation, so read it first. A font ships inside every
|
||||||
|
image, so its licence attaches to the image — see [[schunix-licensing]].
|
||||||
|
|
||||||
|
First GPU driver should be **virtio-gpu**, not Intel/AMD: public spec, QEMU
|
||||||
|
implements it, and it teaches the resource/flush model that a raw framebuffer
|
||||||
|
never will. Doom stays last. See [[nexoniarz-sharing-preference]].
|
||||||
46
.claude/memory/schunix-schapi-direction.md
Normal file
46
.claude/memory/schunix-schapi-direction.md
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
---
|
||||||
|
name: schunix-schapi-direction
|
||||||
|
description: "Schunix is aimed at being a NeXTSTEP/macOS-shaped system with a native SchAPI over a POSIX personality, with permanent Windows-grade compatibility"
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: 9d8a2aef-bcf0-47a0-96c6-f706efab4a95
|
||||||
|
modified: 2026-08-07T16:29:50.730Z
|
||||||
|
---
|
||||||
|
|
||||||
|
**Agreed 2026-08-06.** Schunix is no longer "a Unix-like kernel with a userland
|
||||||
|
attached". It is aimed at being a **system**: NeXTSTEP/macOS-shaped layering,
|
||||||
|
with POSIX/Unix kept complete but demoted from identity to *personality*, and a
|
||||||
|
native higher-level API called **SchAPI** above it. Written up in
|
||||||
|
`docs/architecture/schapi.md`.
|
||||||
|
|
||||||
|
The architecture is borrowed from macOS; the compatibility policy is borrowed
|
||||||
|
from **Windows** (nothing is ever removed after 1.0). Those are two different
|
||||||
|
systems — macOS drops things aggressively — and the mix is deliberate.
|
||||||
|
|
||||||
|
**The load-bearing mechanism:** the stable ABI is a *library*
|
||||||
|
(`libschunix`), not the syscall table. That is why macOS can renumber syscalls
|
||||||
|
freely while old binaries keep working, and why Linux is stuck with every
|
||||||
|
syscall mistake it ever made. Syscalls become private and renumberable; only
|
||||||
|
libschunix may issue them. This does **not** contradict
|
||||||
|
[[schunix-linux-abi-decision]] — Linux numbering stays as the Linux
|
||||||
|
personality's entry path, frozen because Linux froze it, not because we did.
|
||||||
|
|
||||||
|
Everything today is tier `provisional`; the freeze starts at 1.0 and covers
|
||||||
|
only what is explicitly marked `stable` then. Committing now would enshrine
|
||||||
|
things like the `fcntl(F_SETFL)` no-op fixed in [[schunix-doom-acceptance-test]].
|
||||||
|
|
||||||
|
`compat/` holds **shims written in terms of the current API**, not parallel
|
||||||
|
implementations. If an old entry point cannot be expressed as a translation,
|
||||||
|
that is a defect in the new API to fix *before* freezing.
|
||||||
|
|
||||||
|
`src-trunk/` is a NetBSD checkout — reference material and a parts bin, never
|
||||||
|
built, never in an image. Highest-value pieces: `sys/net`+`sys/netinet` (the
|
||||||
|
TCP/IP stack, for the roadmap's Phase 2), `subr_pool.c` (slab allocator), and
|
||||||
|
the autoconf driver model. **NetBSD is not uniformly licensed** — a minority of
|
||||||
|
files still carry the 4-clause advertising clause and Berkeley's 1999
|
||||||
|
rescission only covers UC-copyrighted files. Check every file before copying:
|
||||||
|
`grep -l "All advertising materials"`. See [[schunix-licensing]].
|
||||||
|
|
||||||
|
Ordering is unchanged: this is a direction, not a task. The native Windows
|
||||||
|
build has to boot first — see [[schunix-build-environment]].
|
||||||
48
.claude/memory/schunix-shm-and-mmap.md
Normal file
48
.claude/memory/schunix-shm-and-mmap.md
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
---
|
||||||
|
name: schunix-shm-and-mmap
|
||||||
|
description: Shared memory and file-backed mmap work now; the rule is that pages a process does not own must never reach kfree
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: a665e14b-86bd-4b41-a228-51377f3e9225
|
||||||
|
modified: 2026-08-04T18:45:47.215Z
|
||||||
|
---
|
||||||
|
|
||||||
|
**Shared memory and file-backed mmap were implemented on 2026-08-04**, in
|
||||||
|
`kernel/sys/sys_mem.c`. These were the two syscall gaps blocking a display
|
||||||
|
server: without shm, every frame is a copy through a pipe.
|
||||||
|
|
||||||
|
- `shmget`/`shmat`/`shmdt` — up to 16 segments of 2 MB. Pages are allocated up
|
||||||
|
front and zeroed (a fresh segment handing out the last tenant's bytes would
|
||||||
|
leak them between processes). A segment lives until its last attachment
|
||||||
|
goes. **Their table.inc entries were `NO_LINUX`**, so a musl-linked program
|
||||||
|
never reached them at all; they now carry Linux's numbers (29/30/67).
|
||||||
|
- `mmap` of a regular file reads the contents in. It used to silently return
|
||||||
|
zeroed anonymous pages -- success-shaped, and wrong. **Writable `MAP_SHARED`
|
||||||
|
on a file returns `-ENODEV`** rather than faking it: its whole contract is
|
||||||
|
that stores reach the file, and there is no page cache that could make that
|
||||||
|
true.
|
||||||
|
|
||||||
|
**The rule that matters: pages a process does not own must never reach
|
||||||
|
kfree().** Three places had to learn it, and each was a real hazard:
|
||||||
|
|
||||||
|
- `vm_unmap_range()` kfree's everything it unmaps. Use
|
||||||
|
`vm_unmap_norelease()` for device memory and shared segments.
|
||||||
|
- `freevm()` frees any mapped page below `PHYSTOP`. The framebuffer survived
|
||||||
|
only by accident (0xfd000000 is above PHYSTOP); shm pages are kalloc'd and
|
||||||
|
sit below it. `proc_release_shared()` runs before freevm in both `exit()`
|
||||||
|
and `exec()` -- and in exec it must run **before** the `memset(proc->vma)`,
|
||||||
|
which erases the record of which mappings were shared.
|
||||||
|
- `fork()` must map the *same* physical pages and bump the attachment count.
|
||||||
|
`copy_vma_range()`'s share branch now copies the parent's PTEs rather than
|
||||||
|
special-casing the framebuffer address.
|
||||||
|
|
||||||
|
`struct vma`'s `is_fb` became `no_free` + `shmid` for this reason.
|
||||||
|
|
||||||
|
**Test with `shmtest`** (`cmd/shmtest`, nine checks) — it forks and has the
|
||||||
|
child write where the parent reads, which is the only convincing proof. Run
|
||||||
|
via `pwsh -File tools/bootcheck.ps1 -Tty`.
|
||||||
|
|
||||||
|
Still missing for a GUI: raw keyboard/mouse events that bypass the tty line
|
||||||
|
discipline, and unix domain sockets (`socket()` is still a NO_LINUX stub).
|
||||||
|
See [[schunix-console-tty]] and [[schunix-framebuffer-device]].
|
||||||
98
.claude/memory/schunix-smp-and-memory-bugs.md
Normal file
98
.claude/memory/schunix-smp-and-memory-bugs.md
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
---
|
||||||
|
name: schunix-smp-and-memory-bugs
|
||||||
|
description: "SMP works as of 2026-08-08 — the five bugs it took, the __thread lesson, and the 224 MB RAM floor that is also gone."
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: 9d8a2aef-bcf0-47a0-96c6-f706efab4a95
|
||||||
|
modified: 2026-08-08T19:21:20.756Z
|
||||||
|
---
|
||||||
|
|
||||||
|
Both of Nexoniarz's 2026-08-08 reports are **fixed**. `docs/roadmap.md` §3b has
|
||||||
|
the full write-up.
|
||||||
|
|
||||||
|
## SMP works
|
||||||
|
|
||||||
|
One, two and four processors boot; every CPU prints `cpu%d: starting` and
|
||||||
|
`/proc/cpuinfo` lists them all. `smp_start_aps` in `kernel/core/main.c` is 1.
|
||||||
|
Verified on QEMU at `-smp 1/2/4`, with all four `bootcheck` modes passing.
|
||||||
|
|
||||||
|
**Five bugs, each hidden behind the one before it.** All were invisible under
|
||||||
|
QEMU, which publishes only one processor in the legacy MP table; VMware
|
||||||
|
published the real count, which is why the triple fault only ever appeared
|
||||||
|
there.
|
||||||
|
|
||||||
|
1. **Enumeration** — `mpinit()` read the 1994 Intel MP table.
|
||||||
|
`kernel/arch/x86_64/acpi.c` now reads the ACPI MADT (RSDP → RSDT/XSDT →
|
||||||
|
MADT type 0). Do this *first*: nothing else is reproducible until QEMU
|
||||||
|
reports more than one CPU.
|
||||||
|
2. **Triple fault** — `entry64high` in `entry.S` had **no AP branch at all**;
|
||||||
|
the check was a comment saying it had never been written. Every AP fell into
|
||||||
|
the BSP's path, took its stack, and ran `kmain()` twice. Now tests `EBX == 1`,
|
||||||
|
which `entryother.S` had always been setting.
|
||||||
|
3. **`panic("acquire")`** — `seginit()` called `kalloc()` for its per-CPU page.
|
||||||
|
kalloc locks, `acquire()` calls `holding()`, `holding()` reads `cpu` — which
|
||||||
|
`seginit()` was about to set up. `startothers()` now preallocates each AP's
|
||||||
|
page on the BSP.
|
||||||
|
4. **`panic("sched locks")`** — `kernel_fs_base` was **one global** written by
|
||||||
|
every CPU and read by `trapasm.S` on every entry from user mode, so all CPUs
|
||||||
|
shared one per-CPU block. `RESTORE_KERNEL_FS` now reads `%gs:0`; `seginit()`
|
||||||
|
sets GS base per CPU. No swapgs, because `sys_arch_prctl()` implements only
|
||||||
|
ARCH_SET_FS/GET_FS and musl uses FS for TLS — **if ARCH_SET_GS is ever added
|
||||||
|
this must become a swapgs pair.**
|
||||||
|
5. **Null `proc` in `sleep()`, then `#GP` in `syscall_entry`** — see below.
|
||||||
|
|
||||||
|
## The lesson: `__thread` is wrong for per-CPU kernel data
|
||||||
|
|
||||||
|
`cpu` and `proc` were `__thread`. That means per *thread*, and the compiler is
|
||||||
|
entitled to cache the thread pointer across calls on that basis:
|
||||||
|
|
||||||
|
movq %fs:0x0, %rax # read the thread pointer
|
||||||
|
movq %rax, -0x18(%rbp) # spill it
|
||||||
|
callq sched # <-- the CPU can change here
|
||||||
|
movq -0x18(%rbp), %rax # reload the OLD pointer
|
||||||
|
movq (%rax,%rcx), %rax # proc, from the previous CPU's block
|
||||||
|
movq $0x0, 0x40(%rax) # NULL, because that CPU set proc = 0
|
||||||
|
|
||||||
|
But `%fs` here is per *CPU*, and a kernel thread changes CPU at every `sched()`.
|
||||||
|
Now `mycpu()`/`myproc()` — inline `volatile` asm reading `%gs` at each use, so
|
||||||
|
nothing can be hoisted across a call. Named that way (as xv6-riscv does) and
|
||||||
|
**not** `cpu`/`proc`, because a macro by those names would also rewrite every
|
||||||
|
`struct cpu` and `struct proc`; macros have no namespace, tags do.
|
||||||
|
|
||||||
|
**Per-CPU block layout — shared with assembly, do not reorder:**
|
||||||
|
|
||||||
|
+0 self-pointer x86-64 TLS ABI (%fs:0)
|
||||||
|
+8 syscall scratch trapasm.S parks %rsp here
|
||||||
|
+16 struct cpu * PERCPU_CPU
|
||||||
|
+24 struct proc * PERCPU_PROC
|
||||||
|
|
||||||
|
**Offset 8 is not free.** `syscall_entry` uses `%fs:(8)` as scratch, and since
|
||||||
|
FS base and GS base are the same address, `%fs:8` *is* `%gs:8`. Putting `cpu`
|
||||||
|
there made every syscall overwrite it with `%rsp` — a `#GP` inside
|
||||||
|
`syscall_entry`. That was bug 5.
|
||||||
|
|
||||||
|
## The 224 MB RAM floor is gone
|
||||||
|
|
||||||
|
`kernel/arch/x86_64/e820.c` reads the firmware map and sets `phystop`.
|
||||||
|
**Floor is 32 MB, was 224 MB**; 512 MB in gives `MemTotal: 524160 kB`. Take the
|
||||||
|
end of the usable region *containing the kernel* (`EXTMEM`), not the global
|
||||||
|
maximum — `freerange()` walks from the kernel's end to `phystop`, so jumping a
|
||||||
|
reserved hole puts firmware tables on the free list. Ceiling `PHYSTOP_MAX` is
|
||||||
|
3 GB: `kvmalloc()` maps 0–4 GB as four 1 GB pages with the top one uncached for
|
||||||
|
PCI BARs.
|
||||||
|
|
||||||
|
## Techniques worth reusing
|
||||||
|
|
||||||
|
- `qemu -d int,cpu_reset -D log` printed `v=0e` with `SP=ffff80000000ffb0` — a
|
||||||
|
stack pointer in low memory, which said instantly the AP was not on its own
|
||||||
|
stack and pointed at the entry path rather than anything in C.
|
||||||
|
- The symbol table is too sparse for nearest-preceding-symbol. Use
|
||||||
|
`build/kernel/kernel.asm` and
|
||||||
|
`awk '/^[0-9a-f]+ <.*>:/{fn=$0} index($0,"<addr>:")==1{print fn; print; exit}'`
|
||||||
|
— gives the enclosing function *and* the exact instruction.
|
||||||
|
- **Do not blanket-`sed` an identifier rename.** Doing that for `proc`/`cpu`
|
||||||
|
rewrote `#include "proc.h"`, string literals and declarations, and this is not
|
||||||
|
a git repo so there was no undo. The recovery that worked: revert the whole
|
||||||
|
substitution, reapply only the safe `proc->` → `myproc()->` form, then let the
|
||||||
|
compiler name each remaining site.
|
||||||
52
.claude/memory/schunix-sockets-and-evdev.md
Normal file
52
.claude/memory/schunix-sockets-and-evdev.md
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
---
|
||||||
|
name: schunix-sockets-and-evdev
|
||||||
|
description: "Unix domain sockets and /dev/input evdev both work; the one lock rule, the names-not-in-the-VFS tradeoff, and where the syscall numbers went"
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: 9d8a2aef-bcf0-47a0-96c6-f706efab4a95
|
||||||
|
modified: 2026-08-05T07:12:20.950Z
|
||||||
|
---
|
||||||
|
|
||||||
|
Unix domain sockets (`kernel/net/unix.c`, `include/socket.h`, `kernel/sys/sys_net.c`)
|
||||||
|
and raw input events (`kernel/dev/input.c`, `include/input.h`) are both done and
|
||||||
|
verified — `tools\bootcheck.ps1 -Sock` and `-Evdev`.
|
||||||
|
|
||||||
|
**One lock for the whole socket table, deliberately.** A pipe gets a lock per
|
||||||
|
pipe because every operation touches one object; a socket operation touches
|
||||||
|
two, and per-socket locks need an ordering rule every path obeys or they
|
||||||
|
deadlock. Worse, a reader testing `peer_closed` under its own lock while the
|
||||||
|
closer sets it under the peer's has no interlock and sleeps through its own
|
||||||
|
wakeup. Don't "optimise" this into per-socket locks.
|
||||||
|
|
||||||
|
**Socket names live in a table, not the filesystem.** `ls -l /tmp/schcom` will
|
||||||
|
not show an `s` and `unlink()` of a socket path returns ENOENT. Fixing it
|
||||||
|
properly means a new inode type through both filesystems, iget, dirlookup and
|
||||||
|
stat — a VFS change, not a socket change. Everything that matters works.
|
||||||
|
|
||||||
|
`connect()` deliberately does **not** wait for `accept()`. A queued connection
|
||||||
|
is already established on Unix; blocking would deadlock any server that
|
||||||
|
connects to itself.
|
||||||
|
|
||||||
|
The syscalls had real Linux numbers assigned (41/49/50/43/42 and friends).
|
||||||
|
They were `NO_LINUX` stubs before, which meant a musl program calling
|
||||||
|
`socket()` got "bad syscall" from the ABI translation and never reached the
|
||||||
|
stub at all — same trap as [[schunix-shm-and-mmap]]. `shutdown` is registered
|
||||||
|
as `sockshutdown` because Schunix already has a native `shutdown` that powers
|
||||||
|
the machine off.
|
||||||
|
|
||||||
|
**evdev:** `/dev/input/event0` (keyboard) and `event1` (mouse), major 7, Linux
|
||||||
|
`struct input_event` layout. Fed by the PS/2 keyboard, the PS/2 mouse and USB
|
||||||
|
HID. The console keeps its own path — both consumers see every key, so keys
|
||||||
|
pressed during a test also land on the shell's input line.
|
||||||
|
|
||||||
|
Two things that are not obvious:
|
||||||
|
- Below 0x54 a Linux keycode **is** the AT set-1 scancode, so the PS/2 path
|
||||||
|
needs no table. USB HID does need one (`hid_keycode[]`), because HID numbers
|
||||||
|
keys by position and the AT set numbers them by matrix wiring order.
|
||||||
|
- A full event ring drops the **oldest** event. Dropping the newest is the
|
||||||
|
obvious choice and it is wrong: the event most likely lost is a key release,
|
||||||
|
and a lost release leaves the key held down forever.
|
||||||
|
|
||||||
|
See also [[schunix-console-tty]], [[schunix-framebuffer-device]],
|
||||||
|
[[schunix-verify-by-driving-shell]].
|
||||||
36
.claude/memory/schunix-syscall-single-table.md
Normal file
36
.claude/memory/schunix-syscall-single-table.md
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
---
|
||||||
|
name: schunix-syscall-single-table
|
||||||
|
description: Syscalls are declared once in kernel/sys/table.inc — never hand-edit linux_abi.c
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: d1a1ea76-f919-411c-b0ef-0e1bc5064563
|
||||||
|
modified: 2026-08-01T19:34:55.691Z
|
||||||
|
---
|
||||||
|
|
||||||
|
Since 2026-08-01 a Schunix syscall is declared in exactly **one** place:
|
||||||
|
|
||||||
|
```
|
||||||
|
SYSCALL(nr, linux_nr, name) // kernel/sys/table.inc
|
||||||
|
SYSCALL_ALIAS(linux_nr, name) // extra Linux number -> existing handler
|
||||||
|
```
|
||||||
|
|
||||||
|
and implemented as `SYSCALL_DEFINE(name) { ... }` in a `kernel/sys/sys_*.c`.
|
||||||
|
|
||||||
|
Generated from that one line: the `SYS_*` enum (`include/syscall.h`), the
|
||||||
|
dispatch table (`kernel/sys/syscall.c`), and the Linux ABI translation map
|
||||||
|
(`kernel/sys/linux_abi.c`).
|
||||||
|
|
||||||
|
**Why:** it used to take three edits in two files — a `table.inc` line, a
|
||||||
|
`#define LINUX_foo N`, and a map entry. Forgetting the second file left the
|
||||||
|
syscall unreachable from every musl-linked program while looking correctly
|
||||||
|
registered. `linux_abi.c` went from 361 hand-written lines to 34 generated ones.
|
||||||
|
|
||||||
|
**How to apply:** never add anything to `linux_abi.c` — there is nothing to
|
||||||
|
edit there. `NO_LINUX` marks a syscall with no Linux equivalent; all such
|
||||||
|
entries collide on scratch index 511, which is never dispatched.
|
||||||
|
|
||||||
|
184 syscalls, 150 with Linux numbers, 7 aliases (vfork→fork, preadv→readv, …).
|
||||||
|
|
||||||
|
See [[schunix-linux-abi-decision]] — dropping the native ABI entirely is still
|
||||||
|
the plan; this refactor is the prerequisite, not a replacement for it.
|
||||||
84
.claude/memory/schunix-text-rendering.md
Normal file
84
.claude/memory/schunix-text-rendering.md
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
---
|
||||||
|
name: schunix-text-rendering
|
||||||
|
description: "The text layer that replaces the 8x16 character-cell grid — glyph metrics, surfaces, coverage blending, UTF-8."
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: 9d8a2aef-bcf0-47a0-96c6-f706efab4a95
|
||||||
|
modified: 2026-08-08T22:45:34.934Z
|
||||||
|
---
|
||||||
|
|
||||||
|
Built 2026-08-09. `include/text.h` + `kernel/dev/text.c`. Nexoniarz: "using ANSI
|
||||||
|
fonts in 2026 is a not funny joke."
|
||||||
|
|
||||||
|
**The font was never the problem — the grid was.** The console indexes
|
||||||
|
`font_data[256][16]` and steps the cursor eight pixels. A grid cannot do
|
||||||
|
variable-width glyphs, cannot position text off a multiple of eight, cannot
|
||||||
|
anti-alias, and cannot represent a codepoint above 255. A nicer bitmap fixes
|
||||||
|
none of that.
|
||||||
|
|
||||||
|
Three decisions that make the layer worth having:
|
||||||
|
|
||||||
|
- **Glyphs carry metrics** (`w`, `h`, `bearing_x/y`, `advance`). Variable-width
|
||||||
|
text *is* that; everything else follows. `text_measure()` exists because you
|
||||||
|
cannot centre or lay out anything without it.
|
||||||
|
- **Draws into a `struct surface`, not the screen.** `{pixels, stride, width,
|
||||||
|
height}`. A compositor draws off-screen and presents; rendering that writes
|
||||||
|
straight to the VGA aperture is useless to it. Works on the virtio-gpu back
|
||||||
|
buffer, a window buffer, or the VGA aperture, none of which know about each
|
||||||
|
other.
|
||||||
|
- **`struct font_ops` behind an interface, and glyphs carry 8-bit *coverage*,
|
||||||
|
not bits.** The bitmap font fills it with 0 and 255 only, which costs nothing
|
||||||
|
— and a TrueType rasteriser then plugs in with no caller changing, because it
|
||||||
|
already has somewhere to put anti-aliased edges. `blend_px()` reads the
|
||||||
|
destination for the same reason.
|
||||||
|
|
||||||
|
Pen position is on the **baseline**, as every non-terminal text API does, so
|
||||||
|
glyphs of different heights sit on a common line.
|
||||||
|
|
||||||
|
**Verified** with `echo t > /proc/sysrq` + `tools/textshot.ps1`: text at x=61
|
||||||
|
and x=63, a checkerboard background visible *around* the glyphs (no cell boxes),
|
||||||
|
`┌─┐ █▓▒░ ° ± → •` above U+00FF, and a string measured at 160 px then centred on
|
||||||
|
it. The old console renders in the same screenshot with solid cell backgrounds —
|
||||||
|
a useful side-by-side.
|
||||||
|
|
||||||
|
## Traps hit
|
||||||
|
|
||||||
|
- `include/font.h` **defines** `font_data` rather than declaring it, so
|
||||||
|
including it from a second file duplicates the symbol under `-fno-common`.
|
||||||
|
text.c declares `extern unsigned char font_data[256][16];` instead; the header
|
||||||
|
is generated data and is better left alone.
|
||||||
|
- The built-in font is **CP437, not Latin-1** (known trap in this codebase).
|
||||||
|
`to_cp437()` maps the ~25 codepoints worth having — box drawing, blocks,
|
||||||
|
arrows, degree. **That table exists to be deleted** when a real font lands; it
|
||||||
|
is a bridge, not a design.
|
||||||
|
- A missing glyph draws U+25A1 or `?`, never nothing. Silently shorter text
|
||||||
|
hides the fault.
|
||||||
|
- `utf8_next()` always advances, even on malformed input — a decoder that does
|
||||||
|
not is a hang, not a rendering artefact. Rejects surrogates and overlong
|
||||||
|
encodings.
|
||||||
|
|
||||||
|
## The typefaces, decided 2026-08-09
|
||||||
|
|
||||||
|
- **Roboto** for the DE / WM / compositor. **Apache-2.0** -- the same licence as
|
||||||
|
Schunix, so it needs only a NOTICE entry.
|
||||||
|
- **JetBrains Mono** for the terminal / TTY. **SIL OFL 1.1**, and it declares
|
||||||
|
**no Reserved Font Name** (checked: the copyright line names none; the only
|
||||||
|
"reserved font name" hit in OFL.txt is the licence defining the term). So a
|
||||||
|
modified version may keep the name -- which matters, because pre-rasterising
|
||||||
|
a font into bitmaps at build time *is* a modification and an RFN would have
|
||||||
|
forbidden keeping the name on the result.
|
||||||
|
|
||||||
|
OFL keeps the font under OFL inside an Apache-2.0 system, does not reach
|
||||||
|
anything else, and only forbids selling the font on its own. Both fetched and
|
||||||
|
verified at source, not assumed. Full note in `docs/licensing.md` ("Fonts").
|
||||||
|
|
||||||
|
Neither is in the tree yet -- there is no rasteriser to use them, and shipping
|
||||||
|
a TTF nothing can read would be dead weight.
|
||||||
|
|
||||||
|
## Not done
|
||||||
|
|
||||||
|
The console still uses the old path; nothing is ported to this yet. Porting
|
||||||
|
`vga_putc` onto it is the obvious follow-up and would delete the cell grid for
|
||||||
|
good. A TTF rasteriser is a separate provider behind `font_ops` — the layer is
|
||||||
|
what was blocking it, and it no longer is.
|
||||||
86
.claude/memory/schunix-ttf-rasteriser.md
Normal file
86
.claude/memory/schunix-ttf-rasteriser.md
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
---
|
||||||
|
name: schunix-ttf-rasteriser
|
||||||
|
description: The TrueType rasteriser — why glyf and not CFF, and the composite-glyph trap that loses the letter "i".
|
||||||
|
metadata:
|
||||||
|
type: project
|
||||||
|
---
|
||||||
|
|
||||||
|
Built 2026-08-09. `include/ttf.h` + `kernel/dev/ttf.c`, the second `font_ops`
|
||||||
|
provider behind [[schunix-text-rendering]]. Host harness: `tools/ttftest.c` →
|
||||||
|
`build/tools/ttftest font.ttf px "text" out.pgm`.
|
||||||
|
|
||||||
|
Renders real JetBrains Mono at 32 px: correct advances, anti-aliased edges,
|
||||||
|
the slashed zero, dots on i and j.
|
||||||
|
|
||||||
|
## glyf, not CFF — and that was checked, not assumed
|
||||||
|
|
||||||
|
`.ttf` and `.otf` are both sfnt containers with the same table directory, cmap
|
||||||
|
and hmtx. Only the *outlines* differ: TrueType puts quadratic B-splines in
|
||||||
|
`glyf`; OpenType/PostScript puts cubic béziers in `CFF ` as Type 2 charstrings,
|
||||||
|
a stack-based interpreted format and a separate project.
|
||||||
|
|
||||||
|
**Both chosen fonts ship `glyf` and neither ships `CFF `** — verified by dumping
|
||||||
|
JetBrains Mono's table directory before writing a line. So CFF buys nothing
|
||||||
|
today. `ttf_open()` rejects `OTTO` explicitly rather than failing later with an
|
||||||
|
empty glyph and no explanation.
|
||||||
|
|
||||||
|
## The trap: composite glyphs are not an edge case
|
||||||
|
|
||||||
|
First working render produced **"Schun x"** — the `i` was missing. `i` is a
|
||||||
|
*composite* glyph: dotless-i plus the dot, assembled from two other glyphs at
|
||||||
|
offsets. Skipping composites (`numberOfContours < 0`) does not just lose
|
||||||
|
accented characters, it loses a letter from the project's own name.
|
||||||
|
|
||||||
|
Now assembled: parse the component list (flags, glyph index, dx/dy, optional
|
||||||
|
scale), recurse, merge contours with the offset applied. Only the
|
||||||
|
**translation** case is handled — components with a scale or 2×2 matrix are
|
||||||
|
skipped, because applying a transform wrongly is worse than not applying one.
|
||||||
|
|
||||||
|
## Other things that bite
|
||||||
|
|
||||||
|
- Every sfnt field is **big-endian**; read byte by byte, never cast. The file is
|
||||||
|
not guaranteed aligned and a cast byte-swaps everything on x86.
|
||||||
|
- Glyph flags are **run-length encoded** (bit 3 = "next byte is a repeat
|
||||||
|
count"). Missing that turns any glyph with a repeated flag into garbage.
|
||||||
|
- Coordinates are **deltas** in one of three encodings per axis, selected by two
|
||||||
|
flag bits — including "same as previous", which occupies no bytes.
|
||||||
|
- TrueType **omits the on-curve point between two consecutive off-curve
|
||||||
|
points**; it must be synthesised at the midpoint or curves render as corners.
|
||||||
|
- `hmtx` stores `numberOfHMetrics` full entries then bearings only — clamp the
|
||||||
|
index.
|
||||||
|
- cmap format 4's `idRangeOffset` is a byte offset **from its own address**,
|
||||||
|
which is why that arithmetic looks the way it does.
|
||||||
|
|
||||||
|
## Glyph cache (added the same day)
|
||||||
|
|
||||||
|
Direct-mapped over printable ASCII 32..126 into a 48 KB pool, filled linearly,
|
||||||
|
never freed. No hashing, no eviction policy, no allocation — the contents are a
|
||||||
|
pure function of (codepoint, size), so nothing ever goes stale. A **size change
|
||||||
|
drops the whole cache**, which is the only event that invalidates it.
|
||||||
|
|
||||||
|
It fixes two things, not one: the obvious cost of re-rasterising every glyph,
|
||||||
|
and the contract — the single `ttf_cov` scratch buffer meant a glyph was valid
|
||||||
|
only until the next call, so a caller could not hold two at once. Cached glyphs
|
||||||
|
now point into the pool. Codepoints outside ASCII, and anything that arrives
|
||||||
|
after the pool is full, still render correctly through the scratch buffer under
|
||||||
|
the old contract.
|
||||||
|
|
||||||
|
`tools/ttftest.c` calls through `fo->ops->glyph`, not `ttf_render`, so the cache
|
||||||
|
is on the tested path rather than bypassed by the harness.
|
||||||
|
|
||||||
|
## Still not done
|
||||||
|
|
||||||
|
- **No font loaded from disk.** The kernel compiles the rasteriser but no `.ttf`
|
||||||
|
is in the tree and nothing reads one. This is what blocks pointing the console
|
||||||
|
at it.
|
||||||
|
- **Hinting ignored** (`fpgm`/`prep`/`cvt ` skipped). Fine above ~16 px, blurry
|
||||||
|
below. Real hinting is a bytecode interpreter — a project, not a fix.
|
||||||
|
- **No GSUB**, so ligatures do not form: JetBrains Mono's `==` renders as two
|
||||||
|
glyphs. Correct, just not what the font can do.
|
||||||
|
- **CFF/OTF outlines.** Requested 2026-08-09 for people who want a non-TrueType
|
||||||
|
font in the terminal. Not started, and it is not "more of the same": Type 2
|
||||||
|
charstrings are a stack-based bytecode to *interpret* — INDEX/DICT structures,
|
||||||
|
charset, local and global subrs with the biasing rule, ~40 operators, cubic
|
||||||
|
béziers. Roughly the size of everything else here, and a subtly wrong
|
||||||
|
interpreter draws plausible-looking wrong glyphs. It plugs in as a third
|
||||||
|
`font_ops` provider, so nothing above it changes.
|
||||||
34
.claude/memory/schunix-tty-fg-pgrp-coupling.md
Normal file
34
.claude/memory/schunix-tty-fg-pgrp-coupling.md
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
---
|
||||||
|
name: schunix-tty-fg-pgrp-coupling
|
||||||
|
description: TIOCGPGRP and getpgrp() must agree or the shell spins forever at startup
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: d1a1ea76-f919-411c-b0ef-0e1bc5064563
|
||||||
|
modified: 2026-08-01T19:01:09.569Z
|
||||||
|
---
|
||||||
|
|
||||||
|
`tcgetpgrp()` (TIOCGPGRP in `kernel/sys/sys_fs.c`) and `getpgrp()` must return
|
||||||
|
the same value, or mksh hangs at startup in its standard loop:
|
||||||
|
|
||||||
|
```c
|
||||||
|
while (tcgetpgrp(tty) != getpgrp()) kill(0, SIGTTIN);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why this is a trap:** both used to answer `proc->pid` independently, so they
|
||||||
|
agreed *by accident*. The moment `getpgrp()` started reporting a real process
|
||||||
|
group (which a process inherits from its parent, so pid != pgid), they
|
||||||
|
disagreed permanently and the shell spun — RUNNING, never printing a prompt,
|
||||||
|
never exiting, with no error anywhere.
|
||||||
|
|
||||||
|
Fixed 2026-08-01 by adding `tty_fg_pgrp` (defined in `kernel/core/console.c`,
|
||||||
|
declared in `defs.h`), initialised to 1 so the first shell — which inherits
|
||||||
|
init's group — sees itself as foreground. TIOCGPGRP reads it, TIOCSPGRP writes
|
||||||
|
it. Anything that changes process-group semantics must keep these coupled.
|
||||||
|
|
||||||
|
**Diagnosing this class of hang:** Ctrl-P over the serial console dumps the
|
||||||
|
process table (`procdump`, wired up in `console.c`). A shell shown as `run`
|
||||||
|
rather than `sleep` means a busy loop in userland, not a crash — and if
|
||||||
|
`exit()` is never reached, nothing is dying, so look for a spin, not a fault.
|
||||||
|
|
||||||
|
See [[schunix-verify-by-driving-shell]], [[schunix-rewrite-priorities]].
|
||||||
45
.claude/memory/schunix-usb-ids.md
Normal file
45
.claude/memory/schunix-usb-ids.md
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
---
|
||||||
|
name: schunix-usb-ids
|
||||||
|
description: "usb.ids ships as /usr/share/usb.ids, cached in dist/ and refreshed with `make usb-ids` — deliberately not on every build."
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: 9d8a2aef-bcf0-47a0-96c6-f706efab4a95
|
||||||
|
modified: 2026-08-08T22:10:41.961Z
|
||||||
|
---
|
||||||
|
|
||||||
|
Added 2026-08-09 at Nexoniarz's request. `dist/usb.ids` →
|
||||||
|
`sysroot/usr/share/usb.ids`, 730 KB, 25,673 lines, version 2026.06.26.
|
||||||
|
|
||||||
|
**Licence: dual GPL-2.0-or-later OR 3-clause BSD.** The file's own header says
|
||||||
|
nothing; the statement is on <http://www.linux-usb.org/usb-ids.html>: "under the
|
||||||
|
terms of either the GNU General Public License (version 2 or later) or of the
|
||||||
|
3-clause BSD License." Schunix takes the **BSD** option — the GPL one could not
|
||||||
|
be exercised, since Apache-2.0 and GPL-2.0-only cannot be combined. `NOTICE`
|
||||||
|
carries the attribution and says which option was taken.
|
||||||
|
|
||||||
|
## Two places I deliberately did not do what was asked
|
||||||
|
|
||||||
|
**"Update every compilation."** It is cached in `dist/` and refreshed only by
|
||||||
|
`make usb-ids`. Fetching per build would break offline builds, make the same
|
||||||
|
source produce different images on different days, and put a third-party web
|
||||||
|
server on the critical path of `make` — and `dist/` exists precisely so a
|
||||||
|
rebuild never needs the network (stated in `docs/layout.md`). The file is
|
||||||
|
committed and carries its own version and date, so what shipped is always
|
||||||
|
knowable. The update target validates the download (`grep -q "^# Version:"`)
|
||||||
|
before replacing the cached copy, so a truncated file or an error page cannot
|
||||||
|
silently destroy a good database.
|
||||||
|
|
||||||
|
**It is not in the kernel.** 730 KB against a ~770 KB kernel. Linux does not do
|
||||||
|
this either — the kernel reports vendor:product and `lsusb` resolves names from
|
||||||
|
`/usr/share/hwdata/usb.ids` in userspace. Same split here: `/proc/devices` and
|
||||||
|
the USB core report IDs; a future `lsusb` reads the database.
|
||||||
|
|
||||||
|
Also added to `RAMDISK_EXCLUDE` — 730 KB is 12% of the 6 MB ramdisk root and
|
||||||
|
nothing needs it to boot. Verified: `mkfs: excluding usr/share/usb.ids`, ramroot
|
||||||
|
metadata unchanged at 345 blocks. This is the second real use of that mechanism
|
||||||
|
(see [[schunix-ramdisk-root-sizing]]).
|
||||||
|
|
||||||
|
**Still to do:** nothing reads it yet. An `lsusb` in `cmd/` that walks the USB
|
||||||
|
core's device list and looks names up is the natural next step, and would make
|
||||||
|
`/proc/devices` far more legible than `1234:1111`.
|
||||||
118
.claude/memory/schunix-usb-xhci.md
Normal file
118
.claude/memory/schunix-usb-xhci.md
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
---
|
||||||
|
name: schunix-usb-xhci
|
||||||
|
description: "xHCI enumerates USB2 and USB3 devices and does control transfers; written from the spec because porting BSD's stack means porting newbus first"
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: a665e14b-86bd-4b41-a228-51377f3e9225
|
||||||
|
modified: 2026-08-04T23:06:09.296Z
|
||||||
|
---
|
||||||
|
|
||||||
|
**`kernel/drivers/xhci.c`, 2026-08-04.** Working: controller reset, DCBAA,
|
||||||
|
command ring, event ring + ERST, scratchpad buffers, port reset, Enable Slot,
|
||||||
|
Address Device, and control transfers. Three QEMU devices (usb-kbd, usb-mouse,
|
||||||
|
usb-storage) enumerate and answer `GET_DESCRIPTOR`.
|
||||||
|
|
||||||
|
**Written from the xHCI 1.2 spec, not ported from BSD** — and that was the
|
||||||
|
right call, not laziness. FreeBSD's `sys/dev/usb` is ~100k lines welded to
|
||||||
|
newbus, devclass, `bus_dma`, callouts and taskqueues; porting it means porting
|
||||||
|
FreeBSD's device framework first. BSD is worth far more as the reference you
|
||||||
|
check yourself against. (Licensing agrees: BSD is Apache-compatible, Linux is
|
||||||
|
not — [[schunix-licensing]].)
|
||||||
|
|
||||||
|
**Things that would cost hours to rediscover:**
|
||||||
|
|
||||||
|
- **Enumeration polls the event ring on purpose.** `pci_bind_all()` runs from
|
||||||
|
`kmain()` with interrupts off and no scheduler; a probe that waited for an
|
||||||
|
interrupt would wait forever.
|
||||||
|
- **Context size is 32 *or* 64 bytes**, per `HCCPARAMS1.CSZ`. Hardcoding 32 on
|
||||||
|
a 64-byte controller puts every field at half the right offset and commands
|
||||||
|
are rejected with no explanation.
|
||||||
|
- **PORTSC's change bits and PED are write-1-to-clear.** Writing back a value
|
||||||
|
you just read clears whatever happened. Mask `PORT_RW1C` off every write.
|
||||||
|
- **USB3 ports arrive already enabled; USB2 ports need `PORT_PR` first.**
|
||||||
|
Check PED rather than trying to work out which is which.
|
||||||
|
- **Full-speed devices' EP0 max packet size is unknowable until you ask.**
|
||||||
|
Start at 8, read 8 bytes of the device descriptor, then Evaluate Context.
|
||||||
|
- The link TRB's Toggle Cycle bit is what makes a fixed ring endless; set the
|
||||||
|
cycle bit *last* when pushing a TRB.
|
||||||
|
- A No-Op command before enumerating proves the ring and event path, so a
|
||||||
|
failure points at our plumbing rather than at a device.
|
||||||
|
|
||||||
|
**The USB core and HID landed 2026-08-05.** `kernel/drivers/usb.c` reads the
|
||||||
|
configuration, selects it, and offers the device to a class-driver table in
|
||||||
|
`registry.c` (same one-table-you-can-read arrangement as the PCI drivers);
|
||||||
|
`kernel/drivers/usb_hid.c` does boot-protocol keyboard and mouse. A command
|
||||||
|
typed on an emulated USB keyboard runs in the shell.
|
||||||
|
|
||||||
|
- **Boot protocol, not report descriptors.** A device claiming the boot
|
||||||
|
subclass promises a fixed 8-byte keyboard / 3-byte mouse layout, so no
|
||||||
|
report-descriptor parser is needed. It still starts in *report* protocol --
|
||||||
|
`SET_PROTOCOL(0)` is required or the reports arrive in an unknown shape.
|
||||||
|
- `SET_IDLE 0` too, or a keyboard resends its state every few ms forever.
|
||||||
|
- The keyboard report is a *state*, not an event: a held key appears in every
|
||||||
|
report. Emit only usages absent from the previous report.
|
||||||
|
- Characters go into a small queue that `consoleintr()` then drains through
|
||||||
|
its getc argument -- that reuses the whole line discipline instead of
|
||||||
|
reimplementing it. `mouse_inject()` in mouse.c is the same idea for the
|
||||||
|
pointer; HID's Y axis points down and PS/2's points up.
|
||||||
|
- A configuration descriptor is a *chain* of descriptors of varying types.
|
||||||
|
Walk it by each one's length byte; class-specific ones you have never heard
|
||||||
|
of sit in the middle and must be skipped, not tripped over.
|
||||||
|
|
||||||
|
**The interrupt bug that cost the most time is in
|
||||||
|
[[schunix-driver-infrastructure]]** -- transfers completed perfectly, events
|
||||||
|
sat in the ring, and the IRQ count stayed at zero.
|
||||||
|
|
||||||
|
**Mass storage landed 2026-08-05** (`kernel/drivers/usb_storage.c`): bulk
|
||||||
|
endpoints, bulk-only transport, SCSI INQUIRY / READ CAPACITY / READ(10) /
|
||||||
|
WRITE(10), and a block device at `USBDISK_DEV` (3) routed from `bdevrw()` in
|
||||||
|
bio.c. A real superblock reads back through `bread()`.
|
||||||
|
|
||||||
|
- **A bulk transfer at probe time must not sleep.** This wedged the machine
|
||||||
|
mid-enumeration with no message at all. `pci_bind_all()` runs from
|
||||||
|
`kmain()` with interrupts off and *before* `pinit()` — so `sleep()` has no
|
||||||
|
scheduler to switch to and `wakeup()` would take an uninitialised ptable
|
||||||
|
lock. `struct xhci` has a `can_sleep` flag, set once probing is over; until
|
||||||
|
then bulk transfers poll the event ring, exactly as enumeration does.
|
||||||
|
- Bulk-only transport is strictly three phases: 31-byte CBW out, data,
|
||||||
|
13-byte CSW in. **The CSW is not optional** — skipping it leaves the
|
||||||
|
endpoint out of phase and every later command fails.
|
||||||
|
- SCSI is big-endian everywhere, and READ CAPACITY returns the address of the
|
||||||
|
*last* sector, not a count.
|
||||||
|
- No GET MAX LUN: single-LUN devices answer it with a STALL, and recovering
|
||||||
|
from an EP0 stall needs a Reset Endpoint command that is not written yet.
|
||||||
|
- `b->data` lives in the buffer cache, not DMA memory, so everything bounces
|
||||||
|
through one `dma_alloc`'d block.
|
||||||
|
|
||||||
|
**Bugs found in a deliberate audit pass (2026-08-05), all fixed:**
|
||||||
|
|
||||||
|
- **Context Entries must be the highest DCI in use, not the one being added.**
|
||||||
|
Writing the new endpoint's own DCI works only while endpoints are opened in
|
||||||
|
increasing order; open a higher DCI then a lower one and the field shrinks,
|
||||||
|
silently invalidating the endpoint that already worked. `xhci_open_ep`
|
||||||
|
tracks `slot[].max_dci` now, and `usb_storage.c` deliberately opens bulk
|
||||||
|
OUT before bulk IN — the formerly-broken order — so the fix stays tested.
|
||||||
|
- **The event ring had two unlocked consumers**: `xhci_irq` and the sysrq
|
||||||
|
dump. Both take `x->lock` now; `transfer_done` runs with it held.
|
||||||
|
- **A failed block read left B_VALID clear and returned the stale buffer**,
|
||||||
|
which `bread()` hands straight to its caller as though it were disk
|
||||||
|
content. Three retries, then panic.
|
||||||
|
- **`bot_command` skipped the CSW when the data phase failed**, leaving the
|
||||||
|
device holding a status nobody asked for — so the *next* command's data
|
||||||
|
phase would receive this one's status, turning one failed read into every
|
||||||
|
later read failing. The CSW is now always read.
|
||||||
|
- **The shared descriptor scratch buffer was not cleared between devices**, so
|
||||||
|
a short reply left the previous device's descriptor visible past the end of
|
||||||
|
its own and the parser walked into it.
|
||||||
|
- Max ESIT Payload was being set for bulk endpoints; it is periodic-only.
|
||||||
|
- `irq_unregister` compacted a chain a dispatcher could be walking, and the
|
||||||
|
comment above the lock claimed nothing was ever removed. It masks the line
|
||||||
|
for the rewrite now.
|
||||||
|
|
||||||
|
**Two debug commands:** `echo u > /proc/sysrq` dumps controller state and
|
||||||
|
drains the event ring by hand — that is what separates "no event" from
|
||||||
|
"event, no interrupt". `echo s > /proc/sysrq` reads block 1 off the USB disk
|
||||||
|
through the buffer cache.
|
||||||
|
|
||||||
|
**Still not done:** hubs. See `docs/roadmap.md` phase 1.
|
||||||
34
.claude/memory/schunix-verify-by-driving-shell.md
Normal file
34
.claude/memory/schunix-verify-by-driving-shell.md
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
---
|
||||||
|
name: schunix-verify-by-driving-shell
|
||||||
|
description: "Boots to a prompt" is not a passing test for Schunix — drive real commands over the serial console
|
||||||
|
metadata:
|
||||||
|
type: feedback
|
||||||
|
---
|
||||||
|
|
||||||
|
Reaching `init: ready (pid 1)` and a `#` prompt proves almost nothing about
|
||||||
|
Schunix. A filesystem change shipped looking "verified" that way and the user
|
||||||
|
found the breakage by typing one command.
|
||||||
|
|
||||||
|
**Why:** nothing in the boot path opens a file from `/bin` or reads a
|
||||||
|
non-root inode. An image with corrupt inodes boots to a perfectly healthy
|
||||||
|
prompt and dies on the first `ls`.
|
||||||
|
|
||||||
|
**How to apply:** after any change to `tools/mkfs.c`, the on-disk format, or
|
||||||
|
`kernel/fs/*`, drive actual commands and read the output. QEMU's serial line is
|
||||||
|
an input console, but it has no flow control — a burst of piped input loses
|
||||||
|
about half its characters, and input sent before the shell is up is discarded
|
||||||
|
entirely. Both problems go away with: wait ~20s for boot, then send one
|
||||||
|
character every ~0.12s.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
send() { s="$1"; i=0
|
||||||
|
while [ $i -lt ${#s} ]; do
|
||||||
|
printf '%s' "$(expr substr "$s" $((i+1)) 1)"; sleep 0.12; i=$((i+1)); done
|
||||||
|
printf '\n'; sleep 7; }
|
||||||
|
sleep 20; send "ls"; send "ls /bin"; send "cat /etc/motd"
|
||||||
|
```
|
||||||
|
|
||||||
|
Pipe that into `qemu ... -serial stdio -display none` and read the log. `ls`,
|
||||||
|
`ls /bin` and `cat` on a real file are the minimum bar.
|
||||||
|
|
||||||
|
See [[schfs-v2-and-ramdisk]], [[schunix-build-environment]].
|
||||||
59
.claude/memory/schunix-virtio-gpu.md
Normal file
59
.claude/memory/schunix-virtio-gpu.md
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
---
|
||||||
|
name: schunix-virtio-gpu
|
||||||
|
description: "virtio-gpu 2D driver — the display class's second implementation; how to run it and the transport details that matter."
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: 9d8a2aef-bcf0-47a0-96c6-f706efab4a95
|
||||||
|
modified: 2026-08-08T19:59:27.522Z
|
||||||
|
---
|
||||||
|
|
||||||
|
Written 2026-08-08 from the OASIS virtio 1.1 specification (chapters 2, 4.1,
|
||||||
|
5.7). `include/virtio.h` + `kernel/drivers/virtio_gpu.c`. Worked on the first
|
||||||
|
boot.
|
||||||
|
|
||||||
|
virtio-gpu: 1280x800, 4000 KB framebuffer, queue 64
|
||||||
|
display: 1280x800 32bpp stride 5120
|
||||||
|
display: presented a full frame
|
||||||
|
|
||||||
|
**Run it with:** `-vga std -device virtio-gpu-pci`. Keep `-vga std` — the
|
||||||
|
bootloader draws the splash through VBE and there is no virtio path that early,
|
||||||
|
so a machine with *only* virtio-gpu boots blind until the kernel is up. Both
|
||||||
|
register as `DEVCLASS_DISPLAY`; `registry.c` lists virtio-gpu **first** so
|
||||||
|
`device_find_class()` returns it, and vga_pci still claims the stdvga card
|
||||||
|
underneath for the splash and text console.
|
||||||
|
|
||||||
|
**Why it matters over VBE:** presenting is explicit. VBE is a flat aperture with
|
||||||
|
no notion of a finished frame, so a compositor races the scanout. Here you
|
||||||
|
`TRANSFER_TO_HOST_2D` a rectangle then `RESOURCE_FLUSH` it, and the host never
|
||||||
|
shows a half-drawn frame. `acquire()` returns the guest-side framebuffer
|
||||||
|
directly — no separate back buffer is needed, because the host cannot see it
|
||||||
|
until the transfer. `wait_vblank` is deliberately null: whole rectangles are
|
||||||
|
atomic, so there is nothing to synchronise against, and the wrappers in
|
||||||
|
device.c handle a null op.
|
||||||
|
|
||||||
|
**Transport details that are easy to get wrong:**
|
||||||
|
- Structures live at an offset inside *whichever BAR the device names* in a
|
||||||
|
vendor-specific PCI capability (`PCI_CAP_ID_VNDR`, 0x09). Assuming BAR 0 works
|
||||||
|
on QEMU and is exactly the assumption that breaks on the next device.
|
||||||
|
- The status handshake order is not negotiable: reset → ACK → DRIVER → read
|
||||||
|
features → write features → FEATURES_OK → **read FEATURES_OK back** (the
|
||||||
|
device clears it to reject, and that readback is the only way it ever tells
|
||||||
|
you) → queues → DRIVER_OK.
|
||||||
|
- `VIRTIO_F_VERSION_1` is bit 32, so it lives in the *second* 32-bit window —
|
||||||
|
hence the feature-select registers.
|
||||||
|
- Notify address is `notify_base + queue_notify_off * notify_off_multiplier`;
|
||||||
|
the multiplier is a trailing field of the notify capability only.
|
||||||
|
- MMIO accessed at exact widths. The common config has 8/16/32-bit fields
|
||||||
|
adjacent, and a device may ignore a wrong-width access silently.
|
||||||
|
- 64-bit queue address registers are written as two 32-bit halves, low first.
|
||||||
|
|
||||||
|
Polled, not interrupt-driven: every caller wants the answer before continuing,
|
||||||
|
and the wait is microseconds. The poll loop is bounded so a wedged device
|
||||||
|
cannot hang boot.
|
||||||
|
|
||||||
|
`/proc/devices` name and driver columns are 14 wide because `virtio-gpu0` is
|
||||||
|
eleven characters and overran ten.
|
||||||
|
|
||||||
|
**Not done:** cursor queue, multiple scanouts, mode setting (the resolution is
|
||||||
|
whatever the host reports), resource reuse on resolution change, and 3D/virgl.
|
||||||
51
.claude/memory/schunix-wall-clock.md
Normal file
51
.claude/memory/schunix-wall-clock.md
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
---
|
||||||
|
name: schunix-wall-clock
|
||||||
|
description: The clock read 1970 because nothing ever called cmostime(); kernel/core/time.c now reads the RTC at boot.
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: project
|
||||||
|
originSessionId: 9d8a2aef-bcf0-47a0-96c6-f706efab4a95
|
||||||
|
modified: 2026-08-08T19:44:10.022Z
|
||||||
|
---
|
||||||
|
|
||||||
|
Fixed 2026-08-08. Nexoniarz reported "time has stopped, or it's just too slow —
|
||||||
|
type `date`, wait 10 seconds, it's still the same or pretty close."
|
||||||
|
|
||||||
|
**The clock was not stopped.** `ticks` advanced correctly (measured: uptime
|
||||||
|
19.81 → 34.51 over ~14 real seconds, identical at `-smp 1` and `-smp 2`). The
|
||||||
|
bug was the *origin*: `sys_gettimeofday`, `sys_clock_gettime` and `sys_time` all
|
||||||
|
returned `ticks / 100` — seconds since **boot**, presented as seconds since the
|
||||||
|
**epoch**. So every machine believed it was 1 Jan 1970 and the date crawled at
|
||||||
|
the rate of its own uptime. `cmostime()` in `kernel/arch/x86_64/lapic.c` could
|
||||||
|
read the hardware clock the whole time and had no caller.
|
||||||
|
|
||||||
|
`kernel/core/time.c` now reads the RTC once in `main()` (after the timer is
|
||||||
|
running, before userspace), converts it with Howard Hinnant's
|
||||||
|
`days_from_civil`, and stores `boot_epoch`. `wallclock(&sec, &usec)` is the one
|
||||||
|
place that answers "what time is it", so the three syscalls cannot disagree.
|
||||||
|
|
||||||
|
- **`days_from_civil`, not a loop over years** — branchless, exact for any year,
|
||||||
|
no table. Accumulating 365/366 since 1970 gets leap centuries wrong unless you
|
||||||
|
remember 1900 was not a leap year and 2000 was.
|
||||||
|
- **RTC read once, not per call** — it is polled over two I/O ports and must be
|
||||||
|
sampled twice to catch it mid-update.
|
||||||
|
- **Interpreted as UTC.** A PC that has had Windows on it usually keeps the RTC
|
||||||
|
in local time, but there is no timezone store and nowhere to ask. `date` shows
|
||||||
|
UTC; Nexoniarz is UTC+2 in summer, so it reads two hours behind local. When a
|
||||||
|
settings store exists, the offset goes in `time_init()`.
|
||||||
|
- **`CLOCK_MONOTONIC` (clkid 1) subtracts `boot_epoch`**; everything else is
|
||||||
|
treated as realtime.
|
||||||
|
- Implausible CMOS readings leave `boot_epoch` at 0, i.e. the old behaviour,
|
||||||
|
rather than putting the machine in the Bronze Age.
|
||||||
|
|
||||||
|
Verified: `Sat Aug 8 19:36:43 UTC 2026`, advancing 14s in 14 real seconds, host
|
||||||
|
at 21:37 CEST.
|
||||||
|
|
||||||
|
## The stale-image trap, now guarded
|
||||||
|
|
||||||
|
A running QEMU holds `build/fs.img` open; on Windows `rm` then fails with
|
||||||
|
"Device or resource busy", and **make stops after the kernel has already
|
||||||
|
relinked**. The next test boots the *previous* kernel and the change looks like
|
||||||
|
it did nothing. This cost several cycles across the session. The `fs.img` rule
|
||||||
|
now prints what is wrong and how to clear it. When a fix appears to have no
|
||||||
|
effect, check `schunix.img`'s timestamp before debugging the code.
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -22,3 +22,8 @@ dist/
|
||||||
*.log
|
*.log
|
||||||
*.ppm
|
*.ppm
|
||||||
*.png.tmp
|
*.png.tmp
|
||||||
|
|
||||||
|
# Claude Code's per-machine settings (permissions granted on this host).
|
||||||
|
# .claude/memory/ IS tracked -- it is the project memory, and committing it is
|
||||||
|
# how it survives a directory rename and reaches the other machines.
|
||||||
|
.claude/settings.local.json
|
||||||
|
|
|
||||||
111
tools/memory-sync.sh
Normal file
111
tools/memory-sync.sh
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# Move Claude Code's project memory between this repo and the local machine.
|
||||||
|
#
|
||||||
|
# tools/memory-sync.sh export memory on this machine -> .claude/memory/
|
||||||
|
# tools/memory-sync.sh import .claude/memory/ -> this machine
|
||||||
|
# tools/memory-sync.sh status show both sides, change nothing
|
||||||
|
#
|
||||||
|
# Why this exists
|
||||||
|
# ---------------
|
||||||
|
# Claude Code keeps per-project memory outside the repo, under
|
||||||
|
#
|
||||||
|
# <home>/.claude/projects/<key>/memory/
|
||||||
|
#
|
||||||
|
# where <key> is the project's absolute path with the separators replaced by
|
||||||
|
# dashes: D:\DEV\SchOS becomes D--DEV-SchOS, /home/me/dev/SchOS becomes
|
||||||
|
# -home-me-dev-SchOS. Two consequences follow, and both have already bitten
|
||||||
|
# this project once:
|
||||||
|
#
|
||||||
|
# * Renaming the directory orphans the memory. D:\DEV\Schunix -> D:\DEV\SchOS
|
||||||
|
# changed the key, and everything written under the old one became
|
||||||
|
# invisible until it was moved across by hand.
|
||||||
|
# * The key is machine-specific, so memory written on Windows is not where
|
||||||
|
# Linux looks for it, even for the same checkout.
|
||||||
|
#
|
||||||
|
# Committing the memory to the repo fixes both: git carries it between hosts
|
||||||
|
# and across renames, and `import` puts it wherever this particular machine
|
||||||
|
# expects to find it. Run `export` after a session that learned something and
|
||||||
|
# `import` after cloning or pulling on another machine.
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "usage: $0 {export|import|status}" >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
[ $# -eq 1 ] || usage
|
||||||
|
|
||||||
|
# Repo root: this script lives in tools/, so the parent of its directory.
|
||||||
|
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||||
|
repo=$(CDPATH= cd -- "$script_dir/.." && pwd)
|
||||||
|
repo_mem="$repo/.claude/memory"
|
||||||
|
|
||||||
|
# The absolute path Claude Code itself sees. Under Git Bash `pwd` gives
|
||||||
|
# /d/DEV/SchOS, but Claude Code is a Windows program and uses D:\DEV\SchOS, so
|
||||||
|
# the key must be derived from the Windows form or it will not match.
|
||||||
|
native_path="$repo"
|
||||||
|
case "$(uname -s)" in
|
||||||
|
MINGW* | MSYS* | CYGWIN*)
|
||||||
|
if command -v cygpath >/dev/null 2>&1; then
|
||||||
|
native_path=$(cygpath -w "$repo")
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Encode: every separator becomes a dash, and so does the drive colon.
|
||||||
|
#
|
||||||
|
# D:\DEV\SchOS -> D--DEV-SchOS
|
||||||
|
# /home/me/dev/SchOS -> -home-me-dev-SchOS
|
||||||
|
#
|
||||||
|
# The colon maps to a dash rather than being deleted -- that is where the
|
||||||
|
# doubled dash in the Windows form comes from, and dropping it instead yields
|
||||||
|
# D-DEV-SchOS, which matches nothing.
|
||||||
|
key=$(printf '%s' "$native_path" | tr '/\\:' '---')
|
||||||
|
|
||||||
|
home_dir=${HOME:-$USERPROFILE}
|
||||||
|
host_mem="$home_dir/.claude/projects/$key/memory"
|
||||||
|
|
||||||
|
count() { [ -d "$1" ] && find "$1" -name '*.md' 2>/dev/null | wc -l | tr -d ' ' || echo 0; }
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
status)
|
||||||
|
echo "repo: $repo"
|
||||||
|
echo "as Claude sees it: $native_path"
|
||||||
|
echo "project key: $key"
|
||||||
|
echo
|
||||||
|
echo "in repo: $repo_mem"
|
||||||
|
echo " $(count "$repo_mem") memories"
|
||||||
|
echo "on this host: $host_mem"
|
||||||
|
echo " $(count "$host_mem") memories"
|
||||||
|
;;
|
||||||
|
|
||||||
|
export)
|
||||||
|
if [ ! -d "$host_mem" ]; then
|
||||||
|
echo "memory-sync: nothing to export -- no memory at $host_mem" >&2
|
||||||
|
echo " (is the project key right? run: $0 status)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
mkdir -p "$repo_mem"
|
||||||
|
# Copy rather than mirror: deleting from the repo is a git operation and
|
||||||
|
# should be visible in a diff, not a side effect of running a sync script.
|
||||||
|
cp "$host_mem"/*.md "$repo_mem"/ 2>/dev/null || true
|
||||||
|
echo "exported $(count "$repo_mem") memories to .claude/memory/"
|
||||||
|
echo "commit them to carry them to your other machines."
|
||||||
|
;;
|
||||||
|
|
||||||
|
import)
|
||||||
|
if [ ! -d "$repo_mem" ]; then
|
||||||
|
echo "memory-sync: nothing to import -- no .claude/memory/ in this repo" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
mkdir -p "$host_mem"
|
||||||
|
cp "$repo_mem"/*.md "$host_mem"/ 2>/dev/null || true
|
||||||
|
echo "imported $(count "$host_mem") memories to:"
|
||||||
|
echo " $host_mem"
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
esac
|
||||||
Loading…
Reference in a new issue