The RTL8168 is now driven by Linux's r8169, unmodified, in a process.
`make ldls-r8169` stages it from the running kernel together with libphy,
mdio-bus, mdio_devres and the Realtek PHY driver, the kernel's BTF and
the RTL PHY firmware, and etc/schinit/ldls-r8169.job starts it at boot
with its log in /var/log/ldls-r8169.log. The native driver is out of the
registry; its code, now with Linux's 8168H setup, stays as the fallback.
LDLS grows what that needed:
- ldlsd loads a comma-separated module set in dependency order, each
linking against the ones before it, with BTF read before any
init_module runs;
- a driver core (bus match, driver probe), devres, kernel printf, real
udelay/msleep/ktime and 1000 Hz jiffies, phylib's bitmaps and
link_mode_params, firmware from /lib/firmware, and the symbols whose
stub zero was a lie (list validity, try_module_get, dev_err_probe,
eth_platform_get_mac_address);
- pdev->bus is a real pci_bus, and cmovcc from per-CPU memory is
rewritten like the other %gs forms.
In QEMU all five modules load, link and initialise, and r8169 registers
and finds no device. Probe and traffic are untested until real hardware.
The kernel turns ASPM off on Realtek NIC links at boot, as r8169 does
through pci_disable_link_state, which a process cannot.
Idle CPUs enter ACPI _CST's C2 (CC6 on Zen) by port read or MWAIT when
the APIC timer keeps running, instead of only ever halting at C1.
docs/plan/gpu.md: amdgpu and nouveau through LDLS, with measurements.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
Caps, Num and Scroll Lock now light their LEDs on PS/2 and USB keyboards,
share one state between keyboards, and stop toggling on key repeat. X sets
them through evdev writes while it holds the keyboard.
Unmapping memory in a process with threads on other CPUs now flushes their
TLBs with an IPI. Without it a freed page stayed reachable through stale
translations, which corrupted Epiphany's heap on SMP.
ACPI batteries and AC adapters appear under /sys/class/power_supply.
/proc/<pid>/maps lists file mappings, and a process killed by a fault
names the file and offset it died in. r8169 gets a 256-entry RX ring.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
- Anonymous memory can be reserved without being backed: large mappings,
MAP_NORESERVE and PROT_NONE are recorded and each page appears, zeroed,
on first touch -- from trap() for the program, from uva2ka() for the
kernel on its behalf. WebKit reserves 4 GB at start and touches a sliver;
backing it all failed with ENOMEM and Epiphany aborted.
- r8169: the 8168G generation and later (the laptop's 8168H, XID 541) are
taken from the firmware's out-of-band mode, get their PLL powered, the
receive gate opened, and their PHY reached through the OCP window. The
PHY's power-down bit is cleared; the old code kept it while restarting
autonegotiation. Untested on the hardware so far.
- The network thread follows each device's link into lwIP. The link was
read once at attach, 0.02 s into boot; real autonegotiation takes
seconds, so on hardware the interface stayed down for good.
- Twelve terminals, switched with Ctrl+Alt+F1..F12 by default: the keys X
uses, so the console and the desktop agree. AltGr+F-key is gone from
the defaults (chvt -c altgr+F brings it back).
- SO_PASSCRED on Unix sockets, with SCM_CREDENTIALS delivered by recvmsg.
WebKit sets it on its IPC sockets and aborted when refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
SDL 3 has no OSS backend and SchOS has no ALSA kernel side, so sound now
goes SDL -> alsa-lib -> alsa-plugins' OSS plugin -> /dev/dsp. That path
found two bugs underneath it:
- ac97: resetting the PCM-out box also clears the descriptor list address,
and nothing set it again. Every stream after the first stop played a
"list" at physical address 0 -- silence -- and ALSA resets before each
stream. If the engine halts at the end of the queue, the run bit is set
again.
- /dev/dsp: SETFRAGMENT, GETFMTS, GETCAPS, SETTRIGGER, GETOPTR and GETODELAY
for the plugin's ring-and-pointer model, and poll(), writable once a whole
fragment fits (ALSA spun at 76% CPU otherwise). speaker-test now plays in
real time at 0% CPU, verified in a recording of QEMU's AC'97.
- /etc/asound.conf: the default device is a plug to the OSS device at 16-bit
stereo 48 kHz, what every SchOS audio driver plays.
Also:
- getstackpcs: each frame link must go up the same stack by less than
KSTACKSIZE. The chain ends at the syscall entry with the user's rbp,
which lbreakouthd uses as a register (-1024); the walk dereferenced it
under the syscall tracer and the kernel panicked inside acquire().
- fb: FBIOGET_VSCREENINFO reports nominal timings, as Linux's efifb does,
instead of zeros, which Xorg's fbdev turned into a 0 Hz RandR mode.
- procdump (Ctrl-P) shows where runnable threads are in user space.
- World: lbreakouthd instead of lbreakout2 (its stk.c passes vsnprintf a
size of 511 plus the prefix length, and Alpine's fortified headers trap on
that), plus curl, alsa-lib and alsa-plugins.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
- SIGSTOP/SIGTSTP/SIGTTIN/SIGTTOU stop, SIGCONT resumes; the parent gets
SIGCHLD and wait4 reports WUNTRACED/WCONTINUED; ps shows T. Ctrl-Z works,
and mc no longer hangs waiting for its subshell to stop.
- Blocking reads (console, pty, pipe) and sleeps take a pending stop and
carry on after SIGCONT; a caught signal without SA_RESTART ends them
with EINTR.
- nanosleep reads tv_nsec (sleep 0.5 used to return at once), writes the
remainder, and clock_nanosleep understands TIMER_ABSTIME.
- rt_sigaction uses the k_sigaction layout for Linux-ABI processes: the old
action's restorer was written 112 bytes past the caller's struct. Flags
are kept; SIGKILL and SIGSTOP cannot be caught.
- F1..F12 reach programs, as the Linux console sends them (PS/2, USB, I2C).
- TIOCLINUX subcode 6 answers "no modifiers" instead of echoing the 6 back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
ACPI, DMI, SMP, xHCI, USB, I2C, NVMe, r8169 and ramdisk progress lines are
kinfo now: in dmesg and on a verbose boot, off the screen of a working
machine. Failures stay cprintf.
vt_repaint also paints the strip right of the last column and below the
last row, which kept the bottom of the X desktop after Ctrl+Alt+F-key.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
The third micro driver: xhci.c with everything but finding a keyboard and
reading it taken out. It does not take over the live driver's state --
the kernel may have died inside that driver -- so each controller is
halted, reset and enumerated from nothing. Of the live driver it uses
the register mapping and the scratchpad pages the controller was lent
(DCBAA entry 0, read before the reset).
Root ports, and USB 2 hubs up to two deep (power, reset, route string,
transaction translator for a slow device behind a high-speed hub). A
boot-protocol keyboard interface gets Set Configuration, Set Protocol
boot, Set Idle, one interrupt IN endpoint and a Normal TRB, re-queued
after every report. No interrupts, no hot-plug, nothing that is not a
keyboard or a hub. A static 256 KB pool instead of the DMA allocator.
Checked under QEMU with the PS/2 controller switched off (i8042=off): a
USB keyboard on a root port, and one behind a hub, both type into the
GMMOS terminal. QEMU's hub is full speed, so the transaction-translator
path is not exercised.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
The second micro driver: i2c_hid.c with everything but reading a
keyboard's input reports taken out. No ACPI -- AML cannot run on a dead
kernel -- so the live driver hands over what it found at boot
(i2c_hid_keyboards): the DesignWare controller, the address, the report
size, the AMD GPIO pin if there is one, and the parsed report descriptor.
No power commands, no reset, a read-only transfer.
usb_hid.c's report-protocol rewrite is split out as hid_boot_report(),
which does nothing but rewrite, and kernel/gmmos/hidkeys.c turns boot
reports into keys for this and the USB micro drivers to come.
GMMOS now probes every micro driver and polls all that answer.
Not run: QEMU has no DesignWare I2C, and the laptops at hand have PS/2
keyboards.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
The PSOGM on the ASUS TUF A15 was a page fault in cprintf, from probe(),
in the touchpad driver's own success message. cprintf had no field widths:
"%04x" printed literally and consumed no argument, so the "%s" after it
was handed the product ID as a string pointer. "%lx" misprinted the same
way -- "0x4275838976x" for 0xFEDC2000 -- on the line before.
The driver's formats are fixed, and cprintf now steps over flags and
widths and takes l/ll/z as 64-bit, so a directive it does not render can
no longer shift every argument after it. Until now any %ld or %lu printed
the number followed by a stray letter.
The touchpad on/off entry in the boot menu has served its purpose and is
gone; the verbose step trace stays.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
A laptop's touchpad is on the chipset's I2C bus, found only through ACPI
(PNP0C50 / ACPI0C50), and SchOS had no I2C at all, so the touchpad of
every recent laptop did nothing.
kernel/acpi/i2c_hid.c drives the DesignWare I2C controller every x86
chipset uses (AMDI0010 on Ryzen) by polling, speaks HID over I2C to the
device the _DSM points at, and feeds the reports through the USB HID
parser. On AMD the interrupt pin's level is read every tick, so a report
is read exactly when one is waiting; elsewhere the input register is polled.
The touchpad is driven in mouse mode, which it starts in after a reset:
motion, clicks and taps become /dev/input/event1 like any mouse. The HID
parser now prefers a descriptor's relative pointer over an absolute one
for this, and takes descriptors from transports other than USB.
Untested on hardware: QEMU emulates no I2C touchpad.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
Xorg uses VTs only when stat("/dev/tty0") reports Linux's tty major 4.
SchOS's terminals sit under major 1, so X ran on no VT at all: the console
kept drawing over it, Ctrl+Alt+F-keys did nothing, and nothing ended the
session. stat now reports terminals under Linux's numbers.
startx picks the VT only when uname says Linux, so root's xserverrc passes
the current one itself. An xorg.conf.d file names X's input devices, since
Alpine's Xorg looks them up through udev and SchOS has none yet.
Alt+F1..F6 and Ctrl+Alt+F1..F6 now switch terminals as on Linux, alongside
the settable chord. USB keyboards gain arrows, Home/End/PgUp/PgDn/Ins/Del
and the switch keys, which the PS/2 path always had.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
Measured against the host fetching the same 67 MB from Alpine's CDN at
11.5 MB/s, SchOS managed about 120 KB/s. Three causes:
- The receive window reopened when data entered the socket buffer,
before the reader drained it; lwIP then refused segments and the
sender waited on retransmission timers. It reopens on read now.
- The e1000 receive ring held 16 frames, so a full window arriving back
to back lost everything past the sixteenth. 256 now.
- Without window scaling the window stops at 64 KB, one window per
round trip. Scaling on, 256 KB window, 512 KB socket ring (a power of
two, which the free-running indices need), copied in blocks.
Now 5.8 MB/s for the same file, and apk update in about two seconds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
Three fixes for real machines that broke at any resolution but 800x600:
- kmain() took the loader's framebuffer address as uint32, and
vga_fb_init() re-published the scanout from that and the 16-bit VBE
block. An aperture above 4 GB moved the console onto the wrong memory
right after the vga: line. Both now use the loader's 64-bit display
block, mapped with ioremap().
- The loader mirrored onto the console splitter's GOP, which forwards to
the primary itself, and onto GOPs that share the primary's aperture.
After a mode change that rewrote the screen over itself with another
mode's stride. Handles without a device path and overlapping apertures
are skipped, and a mode change re-matches the remaining mirrors.
- A mode whose reported size, stride or aperture does not add up after
SetMode is refused, and the loader returns to the previous one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
Sixty-five headers sat side by side in include/, so `#include "irq.h"`
said nothing about where irq.h was or what it belonged to, and errno.h,
stat.h and fcntl.h at the top shadowed the system's for any host tool
built with -I./include. They are in folders now -- arch/ core/ fs/
dev/ gfx/ net/ uapi/ (and ldls/ for the Linux driver layer) -- and
every include names the folder, as Linux's do: "core/proc.h".
Done mechanically and checked the hard way: the kernel built from this
tree disassembles identically to the one built from the tree before
it. The full image boots, and userland is the same set of programs.
Paths in docs and comments follow; docs/guides/layout.md has a table
of what is where, and tools/fix-includes.py rewrites old-style
includes from the folders in the tree, for code written before this.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
The fixed tables that a bigger machine than the one they were written
on reaches first, each raised to where it costs little:
NPCI 64 -> 256 a desktop with cards and Thunderbolt passes 64,
and the devices past it -- often the disk or
NIC last in bus order -- were ignored; 44 KB
NSOCK 32 -> 128 Unix sockets, which a desktop session uses by
the dozen; 4.9 KB each, 470 KB in all
NDEVICE 32 -> 128 120 bytes each
NIFDEV 16 -> 64 one dock is a hub, Ethernet, audio and a card
reader at two or three interfaces apiece
NETIF_MAX 4 -> 8
Device names took one digit and clamped there, so the tenth device of
a kind and every one after it was named "...9". Two digits now.
Checked under QEMU with two bridges of test devices: 68 functions,
all in lspci, /proc/pci and /sys/bus/pci.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
hwreport writes one folder to the first read-write FAT mount, or mounts
the first USB stick that takes vfat itself and unmounts it afterwards:
summary.txt (machine, firmware, CPU, memory, every PCI and USB device
with its driver or "-- no driver"), dmesg.txt, lspci.txt in the shape of
lspci -nnk -xxx, lsusb.txt, dmi.txt, memmap.txt, the ACPI tables as raw
.dat files iasl -d reads, and copies of /proc. Serials, the UUID and the
MSDM/SLIC tables (a Windows licence key) are left out.
It reads /sys, which is new: sysfs at Linux's paths and in Linux's
formats -- /sys/bus/pci/devices with uevent, config and resource,
/sys/bus/usb/devices, /sys/firmware/acpi/tables, dmi/tables, memmap
and efi, /sys/class/dmi/id. toybox's lspci and lsusb work unmodified
against it. The USB core now keeps a table of attached devices for it.
The machine's name comes from SMBIOS (kernel/dev/dmi.c). The UEFI loader
hands over the entry point from the configuration table, the kernel
falls back to the 0xF0000 scan, and the log gets Linux's "DMI:" line.
Two bugs found on the way:
- The loader handed over at most 40 memory-map entries, in firmware
order. A laptop's UEFI map is 60-150 descriptors with RAM above 4 GB
listed last, so that is what was dropped. The entries now have their
own area at 0x31000 with room for 256, are sorted before merging, and
the kernel's 32 usable regions keep the largest when there are more.
- MemTotal and sysinfo's totalram were PHYSTOP, an address, which counts
the PCI hole: a 6 GB machine reported 8. Now the usable RAM, summed.
usb.ids moves to /usr/share/hwdata, where lsusb looks for it, and
pci.ids joins it (`make pci-ids`, BSD-3-Clause like usb.ids).
Checked under QEMU q35: the report written to a FAT32 stick, fsck.fat
clean, DSDT.dat disassembled by iasl on the host, lspci -k and lsusb
showing names, 6 GB of RAM seen as 6137 MB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
"acpi: 0 PCI interrupts routed through _PRT" on an AMD board meant that
every PCIe bridge's devices are wired to the second I/O APIC, which is not
driven. It says so now, and that those devices need MSI.
"vga: framebuffer disagreement" on an NVIDIA card compared the GOP's
framebuffer against BAR0, a 16 MB register block big enough to pass for a
screen. The BAR that contains the framebuffer is preferred now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
From the first boot on real hardware: a Razer mouse's control transfer
failed with a babble, and an ITE controller's late endpoint-0 completion
was reported as an "unexpected event" during a command.
A failed control transfer left endpoint 0 halted (or, on a timeout, still
running with TRBs queued), so every request after it to that device failed
too. It is now reset or stopped and its dequeue pointer moved past what was
left on the ring. A STALL -- a device declining a request it does not
implement -- is logged only on a verbose boot.
HID interfaces without the boot subclass were turned away unless they had a
pointer. The report descriptor's keyboard page is now read too -- modifier
bits, a key array, an NKRO bitmap -- and each report rewritten into the boot
layout for the existing path. Checked with QEMU's usb-kbd forced into report
protocol: "echo Hi", Shift included, typed and ran.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
A native driver written from the NVMe 1.4 base specification. It resets
the controller the firmware left running, then sets up an admin queue and
one I/O queue pair. Completion is polled, with interrupts disabled at the
queue, for the same reason AHCI polls: without AML, interrupt routing on
real hardware cannot be trusted. Each active namespace with 512-byte to
4 KB sectors and no metadata is registered as nvme<C>n<N>, the name
Linux uses, and its GPT is read like any other disk's.
Tested under QEMU q35: disk.img on NVMe as the only disk boots through
OVMF, roots on nvme0n1p2, and writes read back after sync. A
4 KB-sector namespace registers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
Devices can now arrive and leave after boot. A "usbhotplug" kernel thread
handles root-port changes, which it is told about by Port Status Change
events and also checks for every quarter second. It also handles changes
behind hubs, which each hub reports on its status-change endpoint. An
unplugged device has its endpoints closed and any transfer in flight
failed. Then its drivers' disconnect hooks run, the slot is disabled and
its memory is freed. Everything behind a hub that goes goes with it. This
needed the interrupt handler to file command and endpoint-0 completions
for their waiters, so that enumeration can run while keyboards interrupt.
On real hardware: the controller is taken from the firmware through USB
Legacy Support before reset, with SMIs off. On Intel 7/8/9 series parts
the ports are switched over from EHCI, which is where firmware leaves
them.
Every interface of a device is offered to the class drivers, not just
the first, so a receiver's mouse on interface 1 gets a driver. Configure
Endpoint now copies the slot context from the controller. Before, it
rebuilt it and lost the route string and TT fields of devices behind
hubs.
HID: pointers are read in report protocol from their report descriptor.
That covers buttons, X/Y and wheel, relative or absolute, and report IDs.
Boot protocol is kept as the fallback for mice and for keyboards.
Non-pointer HID interfaces are declined rather than treated as tablets.
USB keyboards get software key repeat. Mass storage handles up to four
sticks, each unregistered from the block layer when it is pulled out,
along with its cached blocks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HH7xukt4aaiGj1nQEAoQf1
/proc/interrupts on q35 said "LOSING 526" ticks: 5 s of boot with
interrupts off. Timing each step of kmain() found two waits:
- AHCI sent a COMRESET to every empty port and waited a second for a
link. DET = 0 is "no device", so only DET = 1 is worth a reset now
(after 20 ms of grace for a drive that is still spinning up).
- ideinit() read 0xFF from a bus with no IDE controller, took the BSY
bit for a busy drive, and waited out its 1 s timeout. A status of
0xFF means no controller and is answered at once.
That is every PC with SATA disks. q35 now loses 33 ticks, under the
flag; i440fx still roots on IDE and loses none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HH7xukt4aaiGj1nQEAoQf1
q35 stopped with "no root filesystem" because its disk is SATA, like
every PC since 2008, and SchOS only had IDE. And SchFS could only sit
on a whole raw device, so nothing could boot from a disk that also
carried an ESP.
- include/blkdev.h, kernel/dev/blkdev.c: a registry of block devices
found at probe time, numbered from 4. A disk's GPT (or MBR) is read
when it registers, and each partition becomes a device whose block 0
is the partition's first sector. SchFS needs no format change to live
in a partition; a Linux SchFS driver will see the same bytes on
/dev/sdX2. SchFS has its own GPT type:
47138FA9-B79C-47A6-9F7D-AC1F7F572D5E.
- AHCI driver, written from the AHCI 1.3.1 spec: BIOS handoff,
COMRESET when the link is down, IDENTIFY, 48-bit DMA read/write,
FLUSH. Completion is polled, so the root disk does not depend on
interrupt routing.
- USB sticks also register as usb0, so their partitions are found.
- The root probe tries registered devices between USB and IDE,
SchFS-typed partitions first.
- make disk / tools/posix/mkdisk.sh: a GPT image with a 64 MB ESP and
a SchFS partition, ready to dd to a stick or a disk.
Tested in QEMU q35: the old two-disk setup roots on sata1; disk.img as
the only SATA disk roots on sata0p2; disk.img as a USB stick (firmware
boots from it) roots on usb0p2. Writes survive sync in each case.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HH7xukt4aaiGj1nQEAoQf1
PCI drivers took their interrupt from INTERRUPT_LINE. On real hardware
that is the 8259's number, not the IOAPIC pin the device is wired to
(that is in ACPI _PRT, which needs AML), so nothing ever arrived. xHCI
is entirely interrupt-driven, so the USB keyboard and a USB root would
have hung even on a clean boot. Under QEMU the two numbers agree,
which is why it never showed.
- pci_irq_attach()/pci_irq_detach(): MSI, then MSI-X if the driver
allows it, then INTx. The handler is registered before MSI is
enabled, so no edge is lost. xHCI, e1000, r8169 and rtl8139 use it.
- Lines 24-29 are MSI vectors. The LAPIC error vector moves from 19,
which is a PCI pin on Intel chipsets, to 30. Nothing is placed at or
above vector 64 (T_SYSCALL).
- A storming line is masked and its handlers are polled instead of
being left to eat the CPU. A level line that has had a handler for 2 s
and never fired is polled every tick by a kernel thread.
Tested in QEMU q35 with the root on a USB stick: xHCI and e1000e on
MSI; then MSI disabled and INTx deliberately misrouted, where xHCI got
zero interrupts and the root still mounted through the poller. i440fx
INTx and the rtl8139 hand-over are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HH7xukt4aaiGj1nQEAoQf1
The kernel's rtl8139 driver had no release hook, so /dev/pci refused
every request for the card ("the kernel would not release it") and a
hosted 8139too could never run on a machine where this driver probed
first.
release() follows e1000_release(): silence the chip before unhooking the
interrupt, reset it and clear bus mastering so it stops writing into its
buffers, take the interface out of the stack, then give the DMA buffers
back.
Tested in QEMU: ldlsd takes the card from the kernel, 8139too probes it,
DHCP binds ldls0, and ping 10.9.0.2 gets 3 of 3 replies.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HH7xukt4aaiGj1nQEAoQf1
Nexoniarz is the sole author and SchOS was never published under
Apache-2.0, so this is a straight relicence. LICENSE is the FSF GPLv2 text,
every first-party SPDX header moves to GPL-2.0-or-later, and NOTICE,
README.md, docs/licensing.md and the site say so. projects/schfs-linux
follows the rest of the tree.
The GPL gives copyleft: a fork has to ship its source. It also lifts the
old "Linux drivers, never" rule, since GPL-2.0-only code combines with
GPL-2.0-or-later. "Or later" leaves room for GPLv3, but one binary still
has to pick one version, so docs/licensing.md now keeps Apache-2.0 and
GPLv3 code out of the kernel to keep Linux code possible there. BSD
4-clause code goes from "avoid" to "no", because the advertising clause
is GPL-incompatible.
LDLS still ships no .ko. That boundary is now project policy rather than
a legal requirement, and ldls.md, devices.md and licensing.md section 6
say so. refmod declares license=GPL, and ldls-check passes (27 checks).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two NICs, and unlike the RTL8111 last commit, QEMU emulates both -- so both
are tested rather than reasoned about.
**RTL8139** is its own driver and not a mode of the 8169, because it is its own
chip. No descriptor rings: the receiver is one circular buffer the chip writes
packets into back to back behind four-byte headers, and the transmitter is four
fixed slots. Putting both programming models behind one set of function
pointers is how a mode flag ends up threaded through every path.
Three details are the whole driver. CAPR runs sixteen bytes behind the true
read position -- a quirk of the part, not an off-by-one. WRAP means the chip
runs a packet past the end of the buffer rather than splitting it, so the 8 KB
buffer is allocated as 8 KB + 16 + one maximum frame; getting that wrong is a
chip writing past its allocation at line rate. And a length field of 0xFFF0
means "still writing this one", so treating it as a length copies a partial
packet and loses the alignment for everything after it.
**The e1000 now matches the PCIe 8257x parts.** Its header refused them, with
a note saying they "differ in NVM access and initialisation, and claiming a
device this driver has not been shown to initialise is worse than not claiming
it". That was right, and the second half is answerable: QEMU emulates the
82574L, so it can be *shown* to initialise.
What differs turned out to be one register. EERD moved its address field from
bits 8-15 to bits 2-15 and its done bit from 4 to 1; the rings, RCTL, TCTL and
ICR are identical. So there is a per-instance flag for the NVM layout and seven
more ids -- not a second copy of four hundred lines of ring code. The wrong
pair of constants fails silently, reading the wrong word or waiting for a bit
that never sets, and the MAC comes out as zeroes; that is why it is named in
the header rather than papered over.
Still refused: the ICH/PCH parts (82562, 82577-82579, I217-I219), which put the
PHY behind a MAC-PHY interconnect and need a different bring-up nothing here
can test.
Tested, each booted alone: rtl8139 (10ec:8139) and 82574L (8086:10d3) both bind,
read a real MAC, complete DHCP and ping 4 of 4 with no loss. The classic
82540EM does the same, which is the regression that mattered for the NVM
change. Both supported display configurations: one boot each, no traps, no
panics, socktest and memfdtest green.
Also recorded: the Chromebox CN62 carries an RTL8111, not an Intel part, so
last commit's driver covers it too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The laptop's NIC is an RTL8111H, and the request was to add it to LDLS.
Measuring it is what changed the answer, so the measurement is the commit
message: r8169.ko (6.18.52) has **183 undefined symbols, 130 of them not
implemented by ldls/kpi/, and 35 of those 130 are phylib** --
phy_connect_direct, phy_start, mdiobus_read, genphy_soft_reset and the rest.
phylib is a subsystem with a link state machine, not a set of leaf calls, and
LDLS's usual answer for an unimplemented symbol -- a stub that reports itself
and returns zero -- is exactly wrong for it. phy_start returning zero means the
link never comes up and there is nothing to look at. The 8139too that ldls.md
uses as its model has no phylib dependency, which is why that one was
tractable and this one is not.
So: a native driver. It reaches the PHY through the chip's own MII window --
PHYAR is one register that is a whole MII bus -- and skips the subsystem. Less
work than the port, no GPL entanglement, no firmware blob, no coupling to one
Linux version, and it covers the whole RTL8111/8168 family because all of it
answers to 10EC:8168. The register layout is a fact about the hardware, taken
the way ldls.md section 3 says facts may be taken; the expression is this
tree's, in the same shape as the e1000 beside it.
**It has not run on hardware, and that is not a hedge.** QEMU emulates rtl8139
and nothing else from Realtek, and the 8139 is a different programming model --
a circular receive buffer rather than descriptors -- so it is deliberately not
in the match table. What is verified: it builds, its presence changes nothing
on a machine without the device, and with an rtl8139 on the bus it correctly
leaves it unclaimed. What is not: every register write, on a real part.
The probe therefore says everything a first bug report needs, with cprintf
rather than kinfo so it reaches the screen of a machine with no serial port:
r8169: xid 5c0, 1c:1b:b5:xx:xx:xx, irq 11, link up (bmsr 796d)
xid is the hardware version from the top of TxConfig -- the PCI device id has
been 8168 for twenty years and says nothing. bmsr is MII register 1: bit 2 is
link, bit 5 is autonegotiation complete. Those four facts separate "the chip is
not there" from "the PHY sees nothing" from "the rings are wrong".
Two constants were wrong in the first draft and are recorded because both would
have worked quietly. RxConfig had 0x7700 for "unlimited": MXDMA is bits 8-10
and RXFTH is bits 13-15, so that was one bit short of a full burst and a
half-full FIFO threshold. And CPCR bit 3 was being set under a comment calling
it "PCI multiple read/write" -- bit 3 is PCIDAC, 64-bit addressing, and bit 4
is what the comment described; neither is wanted, so the write is gone.
Both supported configurations: one boot each, no traps, no panics, socktest and
memfdtest green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A program that wants to make a sound opens /dev/dsp, writes PCM, and closes
it. That is what UNIX audio has always been and it now exists here:
tone 440 2
Three pieces, in the order a sample passes through them. **/dev/dsp** is a
character device where write() is audio and a handful of OSS ioctls say what
the samples are. **AudioEngine** is everything above a driver that is not
specific to a chip -- the format, whether there is room, and what a write does
when there is not. **A driver** is bound by class, exactly as a display driver
is: device_find_class(DEVCLASS_AUDIO).
OSS numbering rather than something new, because every UNIX audio program ever
written already knows it and the only thing a different set would achieve is
making ports harder. They are read/write ioctls, which is the part that
matters: a program asks for 44100, the device sets 48000 and writes 48000
back, the program resamples. One that ignores the answer plays 9% fast, and
that is its bug rather than the driver's.
**The engine is deliberately one stream.** Mixing several programs is a real
feature with a real design behind it, and building it before anything had
played a single sound would be exactly what docs/plan/1.0.md keeps a register
for: work that exists and is connected to nothing. The second program that
wants to play at the same time is what should cause it to be written.
**audio_wait() sleeps on the timer tick, not on the device.** That looks like
the lazy choice and is not: a ring that drains is a ring with room, so asking
again in ten milliseconds always makes progress, whereas sleeping on the card
makes audio depend on an interrupt arriving. On a machine whose line is
misrouted that is not a stutter, it is a writer that never wakes. The handler
still accounts for finished buffers -- deleting it would cost efficiency, not
correctness -- and it retires descriptors through the same `tail` pointer
queue() uses, so the two cannot disagree about what is in flight.
**AC'97 first because HDA is two unproven things at once.** It is the simplest
audio device that exists on real machines and in QEMU both. Intel HDA is what
the laptop actually has and is a much larger driver; writing it against an
interface nothing had ever played through would have meant debugging the
driver and the interface simultaneously. HDA arrives as a second
struct audio_ops and nothing above it changes -- which is the entire point of
the class.
Two things the wiring needed: PCI class 0x04 now maps to DEVCLASS_AUDIO (the
driver bound and /proc/devices showed it, but audio_get() found nothing,
because the class byte had never been translated), and include/x86.h grew
inw() -- AC'97's mixer is all 16-bit registers and reading one a byte at a
time returns the low half twice on some codecs.
**Tested by measuring what came out, not by not crashing.** QEMU's wav backend
writes the guest's audio to a file; `tone 440 2` produced 1.97 seconds of
44.1 kHz stereo with peak amplitude 14177 against the generator's table
maximum of 14178, and 880 zero crossings per second -- 440 Hz exactly. wav is
on every QEMU target rather than a host backend, because a build machine may
have no sound card and it is the only backend whose output can be checked.
Both supported configurations: one boot each, no traps, no panics, socktest
and memfdtest green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two things.
**The F8 resolution now applies to the other displays as well.** It already
reached them once vgpud started -- that was the last commit but one -- but not
before: choosing 1024x768 changed the screen it was chosen on and left the
other at 1280x800, so the menu and then the splash appeared on it as a small
rectangle in the middle of a larger black one. Which is a fair reading of "the
resolution change is not changing resolution".
The resolution offered in that menu is a statement about the machine's display,
not about one adapter of it, so both stages before vgpud now follow it:
the loader mirrors_match() puts every mirror GOP into the primary's mode,
called from mirrors_show() so it cannot be forgotten at a new
call site. Two integer comparisons in the common case.
the kernel setup_scanout() takes gfx's dimensions over GET_DISPLAY_INFO's.
Same argument as vgpud's bring_up(): the host window's size is
a fact about QEMU, and the console's mode is the one somebody
chose.
**And the greeting is no longer a race.** "Welcome to SchOS!" was printed after
every job had been started, including the shell on the console -- so which of
the two reached the screen first was up to the scheduler, and it went both ways
about evenly. A boot that sometimes greets you above the prompt and sometimes
below it does not read as two orderings of one boot; it reads as a line printed
by mistake.
schinitd now holds back the job that owns the console -- `own_tty`, which is
exactly the job whose output lands there -- until after the greeting is
written. That is enough to settle it: this process writes the line, and only
then asks PID 1 to spawn the shell, so the fork has not happened yet. Nothing
waits on it that was not already waiting.
Verified: F8 -> Resolution -> 1024x768 -> boot, with the virtio head
screendumped over QMP at three points during startup and once at the prompt --
1024x768 throughout, and byte-identical to the stdvga head. Three consecutive
boots put the greeting at the same offset, ahead of the prompt every time.
Both supported configurations: one boot each, no traps, no panics, socktest and
memfdtest green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two things, both asked for.
**The crash report no longer says which monitors it reached.** "The report is
on the virtio-gpu display too" was a line about the crash screen printed on the
crash screen. The report says what happened and where to read it again; that is
not part of it.
**And the second head now shows the F8 menu and the boot.** It used to be black
until vgpud started several seconds into userspace -- through the splash,
through the menu, through every boot message -- which from the outside is
indistinguishable from a display that does not work. Three stages now cover the
whole boot and hand over to each other without coordinating:
*The loader.* LocateProtocol() answers with the first GOP and offers no way to
ask for the rest, so the rest are found by handle and fed the primary's
framebuffer as a Blt source at the points where a screen is finished: the menu,
the resolution chooser, system information, the splash, each segment of the
kernel load. Blt rather than a second framebuffer to draw into, because a
virtio GOP does not have one -- the same fact that made a virtio-only machine
reboot-loop two commits ago.
*The kernel.* gfx remembers which rows were drawn, as one span rather than a
list of rectangles, because console output is lines and a span is what lines
make. display_mirror_console() copies that span across: a line of boot text is
one cell-row transfer, not a screen. Primed once after pci_init(), because a
display that has just appeared has a black scanout and the console it should be
showing was drawn by the loader into a framebuffer it knows nothing about.
*Then vgpud*, from the same console framebuffer, unchanged.
**Two bugs found building it, both worth the retelling.**
The mirror ran per character from inside vt_feed -- which is inside the console
lock -- and vga_disp_acquire() announces its back buffer with a cprintf. A CPU
taking a lock it already holds, with a call chain that said exactly that. It
runs at the end of consolewrite() and cprintf() now, outside the lock, which is
also the better batching: one transfer per write instead of one per character.
And "is this the console's own screen" was asked of the pointer acquire()
returned. vga_fb hands back a back buffer it copies onto the console's
framebuffer afterwards, so on a one-screen machine the answer was "a different
screen" -- and the console was mirrored onto itself through two megabytes of
intermediate, allocated for the purpose. display_ops grew scanout_phys, which
is a fact about the hardware rather than a policy a driver can get subtly
wrong, and it is asked first: it has no side effects and acquire() has two.
Verified with two heads under QEMU, screendumping the virtio head every 400 ms
from power-on: splash, splash, splash, then the console, and it stays. The F8
menu appears on it; choosing 1024x768 there brings both heads up at 1024x768
byte-identical. A panic still reaches both. Both supported configurations: one
boot each, no traps, no panics, socktest and memfdtest green.
One gap remains and is documented rather than hidden: while the kernel's driver
resets the device during probe, QEMU shows "Display output is not active" for a
fraction of a second. A device being taken over has no scanout until the new
driver sets one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"it also doesn't show crash screen on virtio gpu or just other gpu than default
vga" -- true, and on the virtio-gpu it was worse than nothing. The head kept
showing the console from a second before the panic: one screen saying the
kernel is dead, the other showing a shell prompt.
panic() painted into the console framebuffer and stopped there. Two cases were
missing, and they are not the same problem.
**The kernel still drives it.** display_acquire() hands back a buffer; the
report is copied in and presented. Ordinary, and a no-op when the display
class's buffer *is* the console's, so a one-screen machine does not do the work
twice.
**A process drives it.** The hard one, and the one hybrid.md owed an answer
for. The kernel let go of the device deliberately, and the process holding it
will never run again -- there is no scheduling after a panic -- so there is
nobody left to ask. It is not a race that can be won by being careful.
So the userspace driver says where it is scanning out *in advance*:
PCIIOC_PANICFB, registered by vgpud after bring-up and after every modeset. On
a panic the kernel paints the report into that memory and asks the kernel
driver the device was taken from to put it on screen one last time --
`panic_present` in struct pci_driver, which virtio-gpu implements by resetting
the device out from under a driver that is still nominally running. That is
safe here and nowhere else, because nothing is going to run again.
The panic path allocates nothing. It cannot: a corrupted heap is one of the
ways a machine gets here, and an allocator that deadlocks takes the crash
screen *and* the serial log's last chance to be read. So the driver keeps its
BAR mappings across the handover, rebuilds one queue from 20 KB of static
memory, and bounds every spin -- including the reset acknowledge, which is an
unbounded `while` in probe() and correct there.
**The registration is checked like the promise it is.** The range must lie
inside a block that process got from PCIIOC_DMA on that device. Without that,
a claimed device would be a way to nominate any physical address in the machine
-- page tables, kernel text -- to be overwritten with four megabytes of crash
report at the one moment nothing is left to notice. That is a worse bug than
the one this fixes. It goes away with the claim and with the process.
What it buys is worth stating plainly: a device can be evicted to userspace
without the kernel losing its ability to tell you the machine died. The
alternative was a driver boundary that is also a blind spot.
Verified both ways under QEMU with two heads, `echo c > /proc/sysrq`, and a
screendump of each over QMP:
vgpud holding the device both heads byte-identical, serial says
"The report is on the virtio-gpu display too."
kernel holding it same, via display_acquire/display_present
(/proc/pci shows 1af4:1050 [virtio-gpu])
Both supported configurations: one boot each, no traps, no panics, socktest and
memfdtest green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Booting with a virtio-gpu as the *only* display reboot-looped. Not a hang, not
a panic: the firmware came back round to the boot menu, started the loader
again, and did it again, for ever, with nothing to say why.
virtio-gpu has no linear aperture, so OVMF's virtio GOP implements Blt and
nothing else. On a machine with one and nothing else, the firmware reports a
width and a height and a FrameBufferBase of **zero**:
schos: framebuffer 0x0000000000000000 0x500x0x320
Two places took that at face value.
**kmain() tested `if (bi.ptr && ...)` with bi.ptr already run through P2V()**,
and P2V(0) is KERNBASE, which is not zero. So the guard could not fail -- and
its own comment, three lines above, said "drawing into address zero is a worse
failure than staying quiet". The intent was exactly right and the test could
never enforce it. It reads the physical value first now and tests that.
**vga_fb_init() attached the scanout regardless**, one layer down, so fixing
kmain alone just moved the same write.
Either one had gfx_clear() write four megabytes over physical 0: the real-mode
IVT, the BIOS data area, the AP trampoline at 0x7000, the early page tables.
Hence a reset with no output -- there was nothing left to run.
Both refuse now, and the refusal says what it is:
vga: firmware reported a 1280x800 display with no framebuffer address;
no graphical console
A machine with no usable linear framebuffer gets no graphical console. That is
the truth about it, and the serial line still has everything.
**It still does not reach a shell on such a machine**, and that is recorded
rather than implied: the console path assumes a framebuffer -- vt_init()
reports a terminal of `0 by 0` cells -- and that path has never been exercised.
What this fixes is a machine that destroyed low memory and reset; what it does
not do is make a headless boot work. Supported remains stdvga, with virtio-gpu
optional beside it, which is what docs/architecture/graphics.md already said.
Both supported configurations verified: stdvga alone and stdvga with
virtio-gpu, one boot each, no traps, socktest and memfdtest green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A boot printed about thirty lines before reaching a prompt. Every one was
useful once, and printed on every boot for ever they train the person in front
of the machine to stop reading their own console -- which is expensive on the
day one of them changes.
SchOS UEFI loader
Press F8 for boot options... booting
Welcome to SchOS! (F8 at boot for the full log, or dmesg once you are in)
root@schos / »
**Nothing was deleted, it was routed.** consputc() already wrote to the klog
ring before anywhere else, so a line can be kept in full while being held back
from the two places a person looks. kinfo() is cprintf() with that routing, and
`dmesg` still has the whole boot with timestamps afterwards. The rule for which
is which is not how interesting a line is -- everything was interesting to
somebody once -- but whether you would want to see it on a machine that is
*working*. `pci: 7 devices` is yes the first time and no every time after;
`e1000: no DMA memory for the rings` is yes for ever and stays a cprintf.
The verbose boot the F8 menu already offered now means something much bigger,
which is the other half of this: it is the only way to see any of it live.
Userspace follows through /proc/bootmode, one word. schinit, schinitd and vgpud
hold back their progress lines and never their failures -- a supervisor that
goes quiet when a job dies is worse than one that chatters.
Two things found by testing rather than by reading:
- schinit read /proc/bootmode *before* bootstrap() mounted /proc, so it got
"quiet" every time and the verbose boot was quiet in the one process that
runs first. It reads after the mount now.
- resolvconf announced "already exists; -f to replace it" on every boot after
the first, which is a line about having done nothing. It is quiet when the
file already says what it would have written, and speaks only when the two
disagree -- which means the nameserver changed and the machine is using the
old one.
Verified both ways: quiet boot is six lines, a forced-verbose build prints all
thirty plus the `verbose boot:` steps, and /proc/kmsg holds every line in both.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
e1000 -- Linux's, unmodified -- probes, opens, gets an address by DHCP and
carries ping traffic in a SchOS process. The kernel was driving that NIC and
gave it up, which makes it the first real handover as well as the second
driver.
None of what it needed was a missing feature. Every one was a call that
succeeded, returned zero and did nothing:
_find_next_bit "bit 0 is set" -- for_each_cpu never ended
netif_carrier_off carrier already up -- TCTL.EN never set
__x86_indirect_thunk_r8 the jump discarded -- 255 rx buffers, none made
dma_map_page_attrs bus address 0 -- NIC aimed at physical 0
so the rule from memcpy is now general: a stub returning zero is unsafe
wherever zero is a meaningful answer.
The loader grew ko_fixup_percpu(). The stack guard turned out to be one
per-CPU variable among several; `current` cannot be defused because the driver
dereferences it. Taking the %gs off instead leaves the same opcode reading the
same width from storage the host supplies -- a per-CPU variable in a
single-threaded host is just a variable. Eight bytes become seven and a nop,
and the displacement is not always last, so there is now just enough of an x86
decoder in ko.c to know how long the instruction is.
ldls/kpi/thunk.c is fifteen lines of assembly and the highest-value file here:
retpolines are control flow, and a stub cannot stand in for a jump.
The kernel side:
- ioperm.c, the TSS I/O permission bitmap. A driver's outl is an instruction,
not a call, and e1000's reset path picks it deliberately. IOPL=3 would hand
over the PIC and the disk with it, so it is per-port. Writing it found that
the I/O map base had been written at offset 64 rather than 102, leaving the
CPU checking ports 0-831 against the TSS's own RSP0.
- claim masks the device's IRQ line: 450,000 a second, otherwise.
- net_detach(), the missing half of net_attach_late(). An interface left in
the stack keeps its route and sends every frame to a driver that refuses
it, which looks exactly like a userspace driver that does not work.
- e1000_release() gives up the interface, not just the hardware.
8139too still passes 4/4; ldls-check still passes 27; four CPUs, no panic.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
H2 of docs/plan/hybrid.md -- the milestone the plan itself calls the one
that proves the whole idea, and the one that cannot be skipped.
# cat /proc/schinit
1 4 11 os.schos.vgpud
# kill 4
schinitd: restarting os.schos.vgpud
vgpud: claimed
vgpud: pid 8 drew a test pattern from userspace
The test pattern ends in a twelve-block tally of the drawing process's
pid, because a second run that redraws an identical picture is
indistinguishable from a first run that never stopped. Read off the
virtio-gpu scanout across four generations it says 4, 8, 10, 12, and the
DMA arena hands back the same physical address every time.
What it took:
* A halt the kernel can apply to a device it does not understand.
Clearing PCI_CMD_BUS_MASTER is the only one there is, and it has to
happen before the DMA goes back to the arena -- a dead driver's device
keeps reading a framebuffer the kernel is about to give to somebody
else. Claim sets the bit, release clears it; pcitest shows it in a bm
column, with a third state so a failed read is never printed as off.
* A refusal. vgpu_release() asks vga_fb_restore() whether there is a
framebuffer to put the console back on, and declines to hand over the
device if there is not. The rule that the kernel keeps a minimal
driver for everything on the path to a shell is worth writing down
only if something enforces it.
* The DMA arena not being owned by two allocators at once. This is the
bug that stopped H2: freevm() frees every mapped page below PHYSTOP
and the arena is the top 16 MB of exactly that range, so a DMA block
mapped into a driver went onto the page allocator's free list when the
driver died, while pciuser_release_all() handed the same pages back to
the arena. The restarted vgpud asked for its framebuffer, got pages
kalloc() had already given out -- including its own page tables -- and
dma_alloc()'s zeroing wiped them, so it died inside the ioctl on the
return from a syscall whose stack had stopped existing. do_dma() now
records a VMA with no_free set, the way shared memory does.
* One exit status. KeepAlive must restart a driver that crashed and must
not restart one that has looked at the machine and found its device
absent: on hardware with no virtio-gpu that was fifteen spawns in
forty-five seconds, each printing a line to the console, for ever.
SCHINIT_EXIT_NORESTART is 78, sysexits.h's EX_CONFIG. It is not a new
manifest key -- the wait status has always travelled from PID 1 to the
supervisor, and note_exit() simply stopped discarding it.
The milestone's third clause, "the compositor reattaches", is not done
and is not claimed: there is no compositor running. That is recorded in
the plan as a Schcom milestone rather than counted here.
Closes the register row "A released PCI device" in docs/plan/1.0.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
The failure this is against is not scope creep. It is abandonment by promotion:
a thing gets built to the point where it works, the next thing becomes more
interesting, and the first is left in a corner -- in the tree, not finished, not
deleted, and not in any list. Enough of those and the tree stops matching its
own documentation.
docs/plan/1.0.md now holds the register, and it is the only place anything
half-built is listed. Four sections, because the kinds are genuinely different:
built and connected to nothing (the text layer, a font from disk, socket
activation, wait_vblank); started and stopped part-way (foundations 2-5, hybrid
H2, Schcom v0, a released PCI device that nothing brings back); named in a "what
it does not do" and still true; and frozen -- scoped, unstarted, not to be
started. The rule is that nothing enters except by being finished or deleted,
and that adding a row requires closing one.
1.0.md already said "two things in flight, never three". It was right and it was
not being followed.
Then the corners themselves, because a register written over a tree full of them
is just a longer list:
- cmd/init is deleted. Nothing had execed /bin/init since initcode.S changed
to /bin/schinit; it was built and installed on every image as an unreachable
fallback, and an unreachable fallback rots rather than saves.
- projects/schwm/ and projects/schde/ are deleted. Both said "to be deleted"
in their own first paragraph, which proves only that a directory marked for
deletion is not deleted by being marked. README-only, no code lost, and the
arguments each made are moved into wayland.md where they are still true.
- projects/schcom/schcom and schcom-demo were tracked binaries. Untracked, and
.gitignore'd.
Three code comments and one document described a boot that has not happened
since 7d44eb7. cons_serial_only starts set, but kmain clears it on its first
lines now, so the splash does not survive the boot and the kernel log is on
screen from the start. That reversal was earned by a Chromebox with no serial
port freezing at boot showing nothing at all, and the comments still argued the
losing side of it.
The site:
- a download page. One ISO, what it has actually been run on -- one machine,
under test -- Ventoy and dd, what the first boot looks like step by step,
and the build if you would rather. The support table names one Chromebox
rather than listing hardware, because one Chromebox is the entire bare-metal
record and a list would be fiction.
- Assets/usb.svg, drawn to the same rule as the other marks: two tones,
geometry only, the accent spent in one place, and that place is the activity
light because it is the part that tells you something is happening.
- Download is in both navs and is the hero's primary action; a closing band
ends the front page on it.
- the uberblock-slots and drivers-outside-the-kernel figures are gone. Three
numbers left, including the fastfetch one, which stays because it is funny.
Two things caught by rendering the pages instead of assuming: .hero p was
beating .filemeta on specificity and the file line was coming out at body size,
and the dd command was one line long enough that the card clipped it at `conv=`
-- a destructive command that can scroll half of itself out of view is a good
way to take somebody's disk. It is two lines now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
docs/plan/hybrid.md's first eviction. cmd/vgpud is the virtio-gpu driver as a
process: it finds the device by vendor and device id, asks the kernel for it,
and does the whole virtio 1.0 bring-up from outside -- reset, feature
negotiation, a virtqueue in DMA memory, GET_DISPLAY_INFO, RESOURCE_CREATE_2D,
ATTACH_BACKING, SET_SCANOUT, TRANSFER and FLUSH.
vgpud: virtio-gpu at 00:03.0, currently held by virtio-gpu
virtio-gpu: released to userspace; screen is the VBE framebuffer
vgpud: claimed
vgpud: display is 1280x800
vgpud: scanout is ours, framebuffer at physical 0x3eceb000
vgpud: drew a test pattern from userspace
Verified by screendumping the virtio-gpu head: the pattern is there, with a
checkerboard that would shear if the stride were wrong. The console carries on
in VBE on the other head at the same time, which is the fallback rule from the
plan working rather than being described.
No syscall knows this is a GPU. The kernel handed over a PCI device and the
process decided what it was -- which is the property that makes this a general
mechanism, and the reason the evictions come before anything foreign.
Four things the first real driver needed that the test program had not:
- PCI config space (PCIIOC_CFG). A virtio device says where its registers
are in vendor capabilities, which live in config space, so a driver that
cannot read it cannot find its own registers.
- a way for the incumbent to let go: struct pci_driver gained a release
hook. A driver without one keeps its device, which is always safe and for
anything on the path to a shell is also correct.
- the outgoing driver freeing its DMA. Releasing the device but keeping its
4 MB framebuffer is half a handover -- the incoming driver asks for one and
is told there is no room.
- a 16 MB DMA arena. 4 MB was exactly one framebuffer at this resolution,
and either side of a handover there are two.
And one bug it reproduced on its first working run: vgpud polls the used ring,
so it registered no interrupt handler, and a virtio device interrupts on every
completion. The storm detector said so immediately --
irq 11: 77780 interrupts/second -- a device on this line is not
acknowledging it. handler 'e1000' is attached
-- naming the line, the rate and the neighbour it was drowning. One line fixes
it (VRING_AVAIL_F_NO_INTERRUPT before the queue is enabled), and that detector
was worth every minute it cost.
include/virtio.h moved out of the driver directory. Its own comment said it
should when a second reader appeared; one has.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
fastfetch reported 3 seconds of uptime after 15 real ones, `time` claimed
3.12s for a 20-second command, and ping's one-second interval was closer to
six. One cause, not three: `ticks` was advancing at ~14.5 Hz instead of 100.
It was not the timer. virtio-gpu-pci shares IRQ 11 with the e1000 and
registered no interrupt handler at all. PCI INTx is level-triggered, so the
device held the line asserted, the kernel signalled EOI, and it fired again
immediately -- 72,000 interrupts a second, 83% of CPU0, starving the tick.
The e1000's handler read ICR as zero half a million times, correctly
answering "not mine", which is what made it look like a network-free problem.
Reading virtio's ISR byte clears it and drops the line. That is the fix.
irq_dispatch now measures each line's rate against the TSC -- not ticks,
which is the first casualty of a storm -- and names the attached handlers
once above 20000/s. It reproduces the original fault as a single line.
Found along the way, and fixed because they were real:
- lapicinit loaded TICR with xv6's uncalibrated 10000000, whose own comment
admitted it was never measured. calibrate_timers() now measures the LAPIC
and the TSC against the 8254 PIT. (It came back at 999.9 MHz, so this was
not the bug -- but the next machine is not obliged to be 1 GHz.)
- microdelay() was an empty function body. Every microdelay(1000) in the
xHCI reset path and the USB hub power-on sequence returned instantly.
- HZ is a constant in param.h now, not the literal 100 written out at a
dozen ticks-to-seconds conversions. USER_HZ is separate and deliberately
does not follow it: musl answers sysconf(_SC_CLK_TCK) with 100 always, so
times() and /proc/stat scale at the syscall boundary the way Linux does.
- TIOCGWINSZ answered a hardcoded 24x80. gfx_cols()/gfx_rows() have known
the real geometry all along; at 1024x768 the console is 128x48, so every
program that lays out a screen was formatting for the wrong one.
- TCFLSH fell through to the catch-all `return 0`, claiming to discard
input without discarding any.
uptime now advances 5.01 seconds per 5 real seconds. schfs-check passes.
Full investigation, including the two wrong hypotheses and the measurement
flaw that protected them, in docs/history/kernel.md section 13.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
The network thread waited on the timer and nothing else, so a frame that landed
just after a poll sat in the ring for up to 10 ms -- the largest fixed latency in
the system. That was deliberate when the stack was new (a receive path woken by
an interrupt is much harder to debug than one that is asked) and there is now a
working stack to compare against, which is exactly when it becomes safe to do.
The frame is still *pulled* by the thread through .receive. It has to be: a call
into lwIP can reach the driver, the driver can sleep on the buffer cache, and an
interrupt handler may do neither. netdev_rx_signal() delivers nothing and reads
no ring -- it only ends the wait early.
The wait stays on the timer tick rather than moving to a channel of its own, and
that is the design. lwIP's timers must still run when the link is silent, so the
clock has to remain a floor under the wait, and sleep() cannot wait on two things
at once. Joining the tick's existing wakeup costs nothing and leaves one wait
with one predicate.
rx_signal lives under tickslock for the same reason it gets no lock of its own:
sleep() releases tickslock atomically, so a driver setting the flag and waking
under that lock cannot lose the wakeup by arriving between the predicate test and
the sleep. A private lock would not be held across the sleep, and that window
would be back. Calling it from interrupt context is safe on the same terms the
timer handler already relies on -- acquire() disables interrupts, so a CPU
holding tickslock cannot be interrupted into it.
The e1000 signals on RXT0 (a frame or burst has landed) and RXDMT0 (the ring has
fallen below its threshold, which is when waiting for the clock hurts most).
Measured rather than asserted: /proc/netif reports wake_rx and wake_tick, and
across three pings rx_frames and wake_rx both went 2 -> 6 -- every received frame
woke the thread through the interrupt. wake_tick keeps advancing as the idle
floor, which is what lwIP's timers need. socktest still passes and ping is still
0% loss.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
docs/ was ten files where a plan, a description, a guide and a bug report all
sat next to each other. It is now architecture/ (what SchOS is), plan/ (what it
will be), guides/ (how to work on it) and history/ (why it is like this), with
an index that states the one rule holding it together: when something is
finished it moves from plan/ to architecture/ and is rewritten as a description
of the thing rather than a record of building it.
A ticked checkbox says the work happened. It does not say what the work
*produced*, and six months later the checkbox is all that is left. The network
stack proved the point: finished, verified end to end, and for a month the only
description of it anywhere was a checkbox four levels of indentation deep.
So roadmap.md went from 534 lines to 181 -- nothing deleted, the done work
graduated. Seven subsystems that were finished and undescribed now have
descriptions: boot, kernel, syscalls, filesystem, networking, graphics, usb,
init. Every one ends with "What it does not do", which is the section that pays;
a document listing only strengths is marketing, and the reader who needs it most
is the one about to trip over a limitation nobody wrote down.
schfs.md still opened with "phases 1 through 3 1/2 are done, phase 4 is next"
long after all five had landed. It is a description now, with the phase record
and the hard-won bugs moved to history/.
Three corrections found on the way, all load-bearing:
* projects/schcom/README.md claimed SCM_RIGHTS was done. There is no
sendmsg/recvmsg in the kernel at all, and an entire GUI plan rested on it.
* The root README said "No TCP/IP" months after the stack landed.
* history/kernel.md sec.7 was marked "in progress" on a problem that process
groups solved.
Also adds plan/1.0.md, which draws a finish line so the open list stops being
infinite, and marks schwm/ and schde/ superseded: in Wayland the compositor is
the window manager, so that split dissolves rather than being abandoned.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
Same disease as the network stack, one subsystem over. kernel/dev/vga.c
was three things wearing one name: the owner of the framebuffer, a
bitmap drawing library, and the text console. Only the first is a
driver, and because they shared a file they shared a prefix -- so
panic.c drew the crash screen with vga_fill_rect, console.c printed with
vga_putc, /dev/fb0 handed out vga_fb_phys, and every one of those files
named a display driver it had no business knowing about.
Split three ways:
- kernel/drivers/gpu/vga/vga_fb.c owns the VBE framebuffer -- the mode
the loader programmed and handed over, the aperture, and the display
class vtable. It is a driver and now lives with the driver that
finds the card on the bus.
- kernel/dev/gfx.c draws: pixels, shapes, glyphs, the cursor, scroll,
the ANSI erase operations. It names no driver. The framebuffer
arrives through gfx_attach_scanout(), so a driver publishes what it
owns and this file draws on it without knowing what it is.
- include/gfx.h is the interface, and says why it is deliberately not
the display class: that one is about frames -- acquire, present,
vblank -- and the crash screen has to work when nothing else does.
main() no longer calls a display driver. platform_register_devices()
takes the framebuffer address and brings up whatever owns it, which is
where driver names belong; kernel/drivers/registry.c is the file whose
entire job is naming every driver in the kernel.
virtio-gpu got the e1000 treatment as well: instances in an array, the
instance hung on dev->priv by an `attached` hook, and every op reading
it rather than a file-static. It refused the second adapter outright
before, which is not hypothetical -- integrated plus discrete is an
ordinary laptop. The VGA driver deliberately has no such hook, and says
so: there is one framebuffer the loader programmed however many
VGA-class controllers the bus turns up, and a second one publishing the
same scanout is the truth rather than a bug.
Private headers moved next to their drivers: include/virtio.h and
include/xhci.h each had exactly one includer. include/ is for headers
the rest of the kernel needs.
Deleted rather than moved: consoleinit() programmed the sixteen ANSI
colours into the VGA DAC and disabled text-mode blink, by writing ports
0x3C0-0x3C9 from the console. The comment three lines above the colour
table already said this does nothing in a 32bpp linear mode -- there is
no palette to index -- so it was both dead and in the wrong file.
Verified, and this is a refactor so the evidence is that nothing moved:
- bootcheck -Fb -Net: 59 [ok], no failures. That covers the console
through every command, /dev/fb0's geometry, writing pixels through
write() and mapping the framebuffer through mmap.
- make crashshot: the crash screen renders exactly as before --
shapes, glyphs, logo and QR code -- which is the whole of gfx.c
exercised at once, in a panic, with the scanout attached by a driver.
- make schcom-test: five pixel assertions from the host oracle still
pass, so the display class and the compositor above it are unmoved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The TCP/IP stack reached straight into the first driver anyone wrote.
net.c called e1000_present(), the lwIP glue was named netif_e1000.c and
called e1000_transmit(), and include/defs.h declared the driver's
internals to the whole kernel. The second NIC driver would therefore
have arrived with a near-identical copy of the glue file and a patch to
the protocol stack, which is the arrangement include/device.h was
written to prevent -- the e1000 even said so, with `.ops = 0, // no
class vtable yet; there is no net class`.
There is now. A driver publishes a struct netdev_ops -- transmit,
receive, mac, link_up -- and is finished: it never mentions lwIP, and
nothing above it learns its name. The display class was already this
shape; this is the same shape for networking, and the two now read the
same way in kernel/dev/device.c.
What that made possible immediately:
- One netif per bound device instead of one per driver. lwIP's glue is
netif_eth.c, it takes a struct device, and it holds up to NETIF_MAX
interfaces. net.c attaches every network device it finds and starts
DHCP on each, because which interface the network answers on is not
ours to decide.
- /proc/netif lists all of them, one block of fields each, with the
interface name and the driver that claimed it.
- The ARP self-test moved from the e1000 to the class. Nothing in it
was chip-specific, and it is the first thing anyone will want to run
against the next driver. Still `echo n > /proc/sysrq`.
- Private headers live with their driver: e1000.h was in include/,
where exactly one file could use it. The driver build now adds the
driver's own directory to the include path.
The e1000 also drives more than one card. Its probe() refused the second
outright, because there was a single static instance -- so a machine
with two of them drove one and silently ignored the other. Instances are
an array now, and the instance is hung on dev->priv by a new optional
`attached` hook that the bus calls once the device exists. probe() runs
before device_add(), so there was previously nowhere to put it; that is
the whole reason the hook exists rather than a line inside probe.
QEMU gets an explicit NIC on every target (QEMU_NET). The default device
happens to be an e1000 on this machine type today, which is exactly the
sort of thing that stays true until a QEMU release changes it and the
network stops being exercised without anyone noticing.
Verified:
- bootcheck -Net passes, 48 [ok], no failures -- including the moved
self-test and two new checks that /proc/netif names both the
interface and the driver behind it, which together say the stack
went through the class rather than through a driver it was compiled
to know.
- Two NICs attached by hand (-device e1000 twice, second with its own
MAC): eth0 and eth1 both attach, each reports its own MAC in
/proc/netif, and each gets its own DHCP lease. Before this commit
the same machine produced one interface.
- make schfs-check still passes in full.
One trap worth recording: moving a header broke the build for anyone
with stale .d files -- make stopped with "No rule to make target
'include/e1000.h'". CFLAGS gains -MP, which emits a phony target per
header so a moved or deleted one can never wedge the build again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lwIP 2.2.0, fetched by ports/lwip and compiled straight into the kernel.
Not written from scratch, and the reason is not the packet formats: it is
congestion control, retransmission, window management and twenty years of
corner cases that only show up against a real peer. Not FreeBSD's
netinet either, though the licence would have allowed it -- that stack is
not a library, it reaches into mbufs, epoch(9), netisr, VNET and uma(9),
so adopting it means porting FreeBSD's kernel infrastructure first. lwIP
was written for exactly this situation: a small kernel with a raw
Ethernet driver and a port layer of a few hundred lines.
It is kernel code, so nothing is installed into the sysroot and there is
no second build system: the top-level Makefile compiles the files it
wants against our port headers. The file list is explicit rather than a
wildcard, because lwIP ships IPv6, 6LoWPAN, PPP, SLIP, DNS-SD and a dozen
apps in the same tree, and building what is not configured yields either
dead code or a link error naming a port function nobody meant to write.
Its sources are unmodified and its warnings are silenced for its files
only -- patching them would mean maintaining a fork.
The port layer is ours: a lock, a clock, the netif that hands frames to
e1000, lwipopts.h, and a shim/ directory of tiny headers so lwIP's
<string.h> and friends resolve to the kernel's functions instead of the
host's, whose prototypes disagree and would otherwise be found on the
default include path.
NO_SYS=1, so lwIP has no threads of its own and one kernel thread feeds
it. A thread rather than the interrupt handler, because a call into lwIP
can reach the driver, the driver can sleep, and an interrupt handler
cannot -- so the interrupt still only clears the cause register and the
thread polls. kthread_create is new and generally useful: a process with
a kernel-only page directory, no files, no cwd, no parent, dispatched
from forkret, which can therefore sleep and be preempted like anything
else. DHCP obtains an address at boot and the console says which one.
AF_INET went in exactly where phase 2 said it would. socknew dispatches
on the domain, kernel/net/inet.c implements the internet side on lwIP's
raw API, and not one syscall changed shape: bind, connect, listen,
accept, read and write reach the same descriptors they always did. TCP
and UDP both work. cmd/nettest opens a connection, writes, and reads the
answer back.
Two things are owed and named here so they are not discovered later:
recvfrom does not yet report the source address, and the socket paths
poll on the timer tick instead of sleeping on the socket. Both are
follow-ups, not redesigns.
NOTICE carries lwIP's BSD-3 text. Clause 3 is no-endorsement, not the
old advertising clause, so it sits with Apache-2.0 without difficulty.
Verified: the tree builds clean, and the headless boot check passes with
27 [ok] and no failures with the stack compiled in and the thread
running -- which is the part that would break first if netinit deadlocked
or the thread starved anything. An end-to-end connection test belongs in
that suite and is the next commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The last three phases of the filesystem, which were written together and
touch the same files throughout -- kmount.c, objw.c and vfs.c are each
rewritten by all three -- so they land as one commit rather than three
that could not be built separately.
Durability. A transaction that reaches the platter out of order is worse
than one that never reached it: the uberblock can name a tree whose
blocks are still in the drive's write cache. bdirty/bflush give the
buffer layer an explicit barrier, ideflush issues ATA FLUSH CACHE and
usb_storage_flush the SCSI SYNCHRONIZE CACHE that means the same thing,
and a commit now waits for the data before it writes the root that
points at it. The uberblock groups are read newest-first with fallback,
so a volume whose newest roots are torn mounts at an older one and says
which generations were lost.
Reflink. Extents carry a refcount, so two objects can name the same
blocks and neither frees them while the other holds them. Exposed as
FICLONE -- Linux's ioctl value, so `cp --reflink` works unmodified --
and as cmd/reflink for the case where there is no cp to hand.
Attributes, and the one genuinely new idea here. attr.c stores named
values on an object, reachable through setxattr/getxattr/removexattr at
Linux's syscall numbers. On top of that, schfs_index declares an index
over an attribute name and schfs_query answers "which objects carry this
value" from the B-tree rather than by walking. A query against an
unindexed attribute is refused with an error, never silently answered by
a full scan: a query that quietly becomes a scan is worse than one that
admits it cannot help. cmd/schattr drives both.
Three bugs fixed on the way, each found by the numbers being wrong
rather than by inspection:
- statfs's f_fsid was declared int64[2]. Linux's fsid_t is two *ints*,
so every field after it landed one slot late: userland read f_frsize
out of f_namelen, got 255, and df reported a 32 MB root filesystem
as 1992 KB. It hid for as long as it did because the fields used to
be invented constants, so wrong numbers looked like the usual ones.
- struct inode's nlink was a short against SchFS's uint32, so a file
past 32767 names came back negative -- and iput frees an object
whose count reaches zero. It is an int now, signed on purpose, so an
underflow lands somewhere a check can see rather than wrapping.
- filewrite split every write to fit v1's log. That limit belongs to
the journal, not to writing, and SchFS makes one transaction of
whatever it is handed: a 1 MB write was becoming 170 commits and
170 flushes for nothing. fs_ops carries max_write now, and SchFS
sets no limit.
Also here: a real mount table, so `mount -t schfs /dev/hdb /mnt` shows
up in /proc/mounts -- df looks a path up there before it calls statfs at
all, so a mount missing from that list was invisible to it -- several
volumes mounted at once, snapshots mounted by name, and /proc/fsstat
with an rdtsc-based counter, which is the only instrument fine enough to
see inside a single syscall.
Verified: `make schfs-check` passes in full, including the sweep that
crashes the volume at each of 119 points mid-transaction and requires
every one to mount with exactly its committed data, and the case where
both newest uberblocks are rubbish and the volume still comes back. The
headless boot check passes with 27 [ok] and no failures, on a SchFS root.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Groundwork for usb-tablet, which is the fix for the guest cursor and the
host's never being in the same place: a PS/2 mouse reports how far it
moved, two pointers accumulating deltas independently drift apart, and no
amount of care in the compositor can make them agree. An absolute device
reports where it is, so they coincide by construction.
- the HID driver no longer matches the boot subclass only. usb-tablet
declares subclass 0 and was refused at the door; hid_probe decides
what an interface actually is, and still declines what it cannot make
sense of.
- a non-boot HID interface is taken to be an absolute pointer and is
not asked for boot protocol, which it does not have -- that request
failing is what stopped it binding.
- tablet_report decodes the fixed 6-byte layout (buttons, 16-bit X,
16-bit Y, wheel) and scales the logical range to pixels there,
because it is the only place that knows the range.
- input_mouse_abs reports EV_ABS/ABS_X/ABS_Y so a reader can tell an
absolute position from a delta, and mouse_set_abs places the pointer
rather than nudging it.
- Schcom treats EV_ABS as authoritative and EV_REL as a nudge.
The device binds and identifies correctly: "usb-hid: tablet (absolute) on
slot 1, endpoint 0x81 every 4 ms".
It is NOT finished, and QEMU_INPUT is empty by default because of it. No
reports have yet been observed arriving on the tablet's interrupt
endpoint, so making it the default pointer would trade a cursor that
drifts for one that does not move at all. Opt in with
make qemu QEMU_INPUT='-device qemu-xhci,id=xhci -device usb-tablet,bus=xhci.0'
Next step is to find out why nothing arrives: whether the first transfer
is queued but never completes, whether a completion needs re-arming for
this endpoint, or whether QEMU is not routing motion to the tablet at all
(`info mice`).
bootcheck 108 [ok], schcom-test passes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every driver reached its registers with P2V(bar.base), which assumes the
BAR sits inside the 0-4 GB the direct map covers. Under SeaBIOS it always
does. Under OVMF it does not: firmware may place a 64-bit BAR anywhere,
and it puts virtio-gpu's at about 768 GB. P2V produced an address with no
translation behind it, so the driver's first register write took a page
fault, then a double fault, then a triple fault and a reboot loop --
which, coming from a driver bind, reads as broken hardware rather than a
missing mapping.
ioremap() carries each region on a 1 GB page in PDPT entries 4-511, which
were unused: the direct map occupies 0-3, and kpml4[0], kpml4[KERNBASE]
and every process's PML4 all share that one table, so a mapping appears
in every address space at once with no page table to allocate. The pages
are cache-disabled and write-through for the same reason the top GB of
the direct map is -- these are registers, and a write sitting in a cache
line is a write the device never sees.
A physical address below 4 GB returns the direct-map address unchanged,
so the BIOS path, where every BAR is low, is byte for byte what it was.
virtio-gpu now initialises under OVMF (1280x800, queue 64) instead of
triple faulting. e1000 and xhci had the identical latent bug; xhci was
already refusing to bind above 4 GB with a comment saying the mapping did
not exist yet, and now it does.
BIOS unchanged: bootcheck 108 [ok] with -Fb -Tty -Usb -Sock -Evdev -Ps,
e1000 and xhci both binding.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An x86-64 operating system built from xv6, now largely rewritten: SMP,
a copy-on-write root filesystem (SchFS), PCI/USB/virtio drivers, a TTF
rasteriser, and a Linux-numbered syscall ABI.
No history before this point -- the tree was never under version control.
This commit exists so the rename that follows has something to undo to.