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.
6.7 KiB
| name | description | metadata | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| schunix-usb-xhci | xHCI enumerates USB2 and USB3 devices and does control transfers; written from the spec because porting BSD's stack means porting newbus first |
|
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 fromkmain()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_RW1Coff every write. - USB3 ports arrive already enabled; USB2 ports need
PORT_PRfirst. 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 0too, 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 fromkmain()with interrupts off and beforepinit()— sosleep()has no scheduler to switch to andwakeup()would take an uninitialised ptable lock.struct xhcihas acan_sleepflag, 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->datalives in the buffer cache, not DMA memory, so everything bounces through onedma_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_eptracksslot[].max_dcinow, andusb_storage.cdeliberately opens bulk OUT before bulk IN — the formerly-broken order — so the fix stays tested. - The event ring had two unlocked consumers:
xhci_irqand the sysrq dump. Both takex->locknow;transfer_doneruns 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_commandskipped 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_unregistercompacted 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.