diff --git a/AGENTS.md b/AGENTS.md index 16d16234e4..ac1e2fe1c6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,113 @@ human-initiated operations. Durable Red Bear state belongs in `local/patches/`, The current baseline is **Red Bear OS 0.1.0** (Redox snapshot at build-system commit `f55acba68`). All recipe sources are pinned and archived in `sources/redbear-0.1.0/`. -## NO SILENT UPSTREAM PULLS — OFFLINE-FIRST POLICY +## BUILD SYSTEM DURABILITY — THE CARDINAL RULE + +**THE `recipes/*/source/` DIRECTORY WILL ALWAYS BE REWRITTEN. DO NOT EVER USE IT FOR ANY +WORK THAT YOU INTEND TO KEEP. THOSE TREES ARE EPHEMERAL — THEY ARE DESTROYED AND REGENERATED +ON EVERY `repo fetch`, `repo cook`, `make clean`, AND `make distclean`. ANY EDIT MADE THERE +WILL BE SILENTLY LOST ON THE NEXT BUILD. COMMITTING TO A SUBMODULE INSIDE `source/` DOES NOT +PROTECT YOUR WORK — THE ENTIRE DIRECTORY IS DELETED AND RE-CLONED/RE-EXTRACTED FROM SCRATCH.** + +This is the #1 mistake AI agents and new contributors make. It has caused repeated work loss +in this project. The rule is: + +| What you want to do | Where to do it | +|---|---| +| Change a kernel source file | Create or update a patch in `local/patches/kernel/` | +| Change an init or daemon source file | Create or update a patch in `local/patches/base/` | +| Change relibc | Create or update a patch in `local/patches/relibc/` | +| Change a driver | Create or update a patch in `local/patches/base/` or `local/patches//` | +| Add a new package | Create a recipe in `local/recipes///` | +| Change build config | Edit `config/redbear-*.toml` | +| Add documentation | Write to `local/docs/` | + +### How the build system works + +``` +repo cook + ├── repo fetch + │ ├── Clone/fetch upstream source → recipes//source/ + │ ├── Apply patches from recipe.toml → patches are read from local/patches// + │ └── Source tree is now fully patched and ready for build + ├── Cargo/cmake/configure build + └── Stage artifacts into sysroot +``` + +The `source/` directory is a disposable working copy. It is produced at the start of every +build by cloning the upstream source + applying patches sequentially. The recipe's +`patches = [...]` list in `recipe.toml` controls which patches are applied. + +### Two-layer architecture + +``` +Layer 1: Ephemeral (destroyed on clean/fetch/rebuild) + recipes//source/ ← working tree, cloned + patched + build/ ← build outputs + target/ ← cargo target dir + +Layer 2: Durable (survives clean/fetch/rebuild/release provisioning) + local/patches// ← .patch files — the actual source code changes + local/recipes// ← custom recipe directories + config/redbear-*.toml ← Red Bear OS build configs + local/docs/ ← planning and integration docs + recipes//recipe.toml ← the patches list (git-tracked) +``` + +### The correct workflow for any source change + +1. **Make the change** in `recipes//source/` to validate it compiles +2. **Generate a patch**: `cd recipes//source && git diff > ../../../local/patches//my-fix.patch` +3. **Wire the patch**: add `"my-fix.patch"` to the recipe's `recipe.toml` `patches = [...]` list +4. **Validate**: `./target/release/repo validate-patches ` +5. **Rebuild**: `./target/release/repo cook ` +6. **Commit**: `git add local/patches/ recipes//recipe.toml && git commit` + +### Common anti-patterns + +| Anti-pattern | Why it fails | +|---|---| +| Editing `source/` files then running `make all` | `make all` calls `repo fetch` which regenerates `source/` — edits are lost | +| Creating a patch but not wiring it into `recipe.toml` | Patch file exists but is never applied — build uses unpatched source | +| **Hand-writing patches manually** | **FORBIDDEN. Unified diffs hand-written by humans routinely have incorrect line counts, wrong context, malformed hunks, or timestamp headers — all of which cause `patch(1)` to reject them. The ONLY acceptable way to generate patches is `git diff -U0 -w` from a committed source tree baseline.** | +| Editing `recipe.toml` patches list without creating the actual `.patch` file | Build fails with "missing patch" error | +| Editing `recipe.toml` patches list without creating the actual `.patch` file | Build fails with "missing patch" error | +| Expecting `source/` changes to survive `make clean` | `make clean` deletes `source/` directories | +| Running `repo cook` without `--allow-protected` for core packages | Protected recipes (kernel, relibc, base) are offline-only by default | + +### Patch file location convention + +- `local/patches/base/` — for the `base` package (init, daemon, all drivers) +- `local/patches/kernel/` — for the kernel +- `local/patches/relibc/` — for relibc +- `local/patches/installer/` — for the installer +- `local/patches/bootloader/` — for the bootloader +- `local/patches//` — for any other patched package + +### Recipe patch wiring + +Each recipe's `recipe.toml` lists patches relative to `local/patches//`: + +```toml +[source] +git = "https://gitlab.redox-os.org/redox-os/base.git" +rev = "463f76b96..." +patches = [ + "P0-daemon-fix-init-notify-unwrap.patch", # applied first + "P9-init-scheduler-completed.patch", # applied second + # ... more patches +] +``` + +Patches are applied in listed order. Dependencies between patches must be respected (a patch +that defines a type must come before a patch that uses it). + +### Kernel-specific notes + +The kernel source at `recipes/core/kernel/source/` is a separate git worktree (rev `866dfad`). +The kernel recipe is at `recipes/core/kernel/recipe.toml` and patches are at +`local/patches/kernel/`. The same durability rules apply — all kernel changes must be +in `local/patches/kernel/*.patch`, never in the `source/` tree directly. **Red Bear OS is offline-first by default. No script, build target, or tool may silently pull from any upstream repository without explicit user instruction.** @@ -178,10 +284,24 @@ make all → mk/fstools.mk (build cookbook repo binary + fstools) → mk/repo.mk (repo cook --filesystem=config/*.toml) → For each recipe: fetch source → apply patches → build → stage into sysroot + → Each successful build produces repo//.pkgar + .toml → mk/disk.mk (create filesystem.img, harddrive.img, redbear-live.iso or harddrive.img) → redoxfs-mkfs → redox_installer → bootloader embedding ``` +### Build Outputs + +Every successful `repo cook ` produces: + +| Artifact | Location | Purpose | +|----------|----------|---------| +| Package archive | `repo/x86_64-unknown-redox/.pkgar` | Binary package for image assembly | +| Package manifest | `repo/x86_64-unknown-redox/.toml` | Metadata, version, deps, hashes | +| Staged sysroot | `recipes/*//target/.../stage/` | Files for `repo push` | +| Source tree | `recipes/*//source/` | Fetched + patched source (disposable) | + +**A build is not complete until the .pkgar and .toml exist in `repo/`.** + ## CONVENTIONS - **Rust edition 2024**, nightly channel @@ -444,6 +564,65 @@ or any path that is already git-tracked and not inside a fetched source tree. ## BUILD SYSTEM POLICIES +### Build Durability Rule — Every Build Lands in the Repo + +Every successful `repo cook` produces two durable artifacts: + +1. **Package in the repo**: `repo/x86_64-unknown-redox/.pkgar` + `.toml` +2. **Patched source form**: All source modifications are in `local/patches//` and wired into `recipe.toml` + +A build is **not complete** until both artifacts exist: + +```bash +# After cooking, verify the package is in the repo +./target/release/repo find + +# Check the repo manifest exists +ls repo/x86_64-unknown-redox/.toml +ls repo/x86_64-unknown-redox/.pkgar +``` + +If a package was built but the repo artifacts are missing, the build did not complete. +Re-run `repo cook ` to regenerate them. + +If source patches were applied but not mirrored to `local/patches/`, see the +DURABILITY POLICY section above. + +### Cascade Rebuild Rule + +When a low-level package changes (relibc, kernel, base, or any library), **all +packages that depend on it must be rebuilt**. A stale dependent silently produces +link errors, ABI mismatches, or runtime crashes. + +Use the cascade rebuild script: + +```bash +# Rebuild relibc and everything that depends on it +./local/scripts/rebuild-cascade.sh relibc + +# Dry run: show what would be rebuilt without building +./local/scripts/rebuild-cascade.sh --dry-run relibc + +# Multiple root packages +./local/scripts/rebuild-cascade.sh relibc ncurses +``` + +The script: +1. Finds all packages whose `recipe.toml` lists the target in `dependencies` +2. Transitively expands the reverse dependency graph (BFS) +3. Builds the root package(s) first, then dependents in order +4. Pushes all rebuilt packages to the sysroot + +**When to use cascade rebuilds:** +- After changing relibc headers or ABI +- After rebuilding a shared library (ncurses, zlib, openssl, etc.) +- After kernel ABI changes that affect userspace +- After any change to a package listed in other packages' `dependencies` + +**When NOT to use cascade rebuilds:** +- Standalone applications with no dependents (editors, games, utilities) +- Terminal/leaf packages that nothing depends on + ### Atomic Patch Application The cookbook tool (`src/cook/fetch.rs`) applies patches **atomically**: @@ -466,12 +645,78 @@ Patches may use either format: Git-specific headers (`diff --git`, `diff -ruN`, `index`, `new file mode`, `rename from/to`, `similarity index`, `dissimilarity index`) are automatically stripped before -`patch` is invoked. The build system uses `--fuzz=0` for strict context matching. +`patch` is invoked. The build system uses `--fuzz=3` for resilient context matching. **Timestamps in `---`/`+++` lines** (common in `diff -ruN` output) should be removed. Use `--- a/path` and `+++ b/path` without timestamps. The `normalize_patch` function does NOT strip timestamps — they should be removed from the patch file directly. +### Robust Patch Generation (REQUIRED) + +**MANDATORY: All patches MUST be generated using `git diff -U0 -w` from a committed source tree. +Hand-writing unified diffs is FORBIDDEN — it routinely produces incorrect line counts, malformed +hunks, or timestamp headers that cause `patch(1)` to reject them. The build system uses +`--fuzz=3` for resilient context matching, which requires properly generated diffs.** + +Context-line mismatches (renamed variables, shifted line numbers, upstream refactors) +are the single largest source of patch application failures. Use the zero-context, +whitespace-ignored technique to make patches resilient to drift: + +**Workflow (mandatory):** +```bash +# 1. Start with a clean P0..P(N-1) source tree (repo fetch already applied earlier patches) +cd recipes//source + +# 2. Commit the P0..P(N-1) state as a git baseline +git add -A && git commit -m "P0..P(N-1) baseline" + +# 3. Make P(N) edits in the source tree +# (edit files, test compile, etc.) + +# 4. Generate the P(N) patch using ONLY git diff -U0 -w: +git diff -U0 -w > ../../../local/patches//P-.patch + +# 5. Wire the patch into recipe.toml patches list + +# 6. Validate: repo validate-patches +# 7. Rebuild: repo cook +# 8. Commit: git add local/patches/ recipes//recipe.toml && git commit +``` + +**Apply (for manual testing):** +```bash +patch -p1 --fuzz=3 < local/patches//P-.patch> +``` + +**Why this works:** +- `-U0` produces zero lines of surrounding context, so the patch has no fragile context + lines that can drift when surrounding code changes +- `-w` ignores all whitespace changes, so indentation-only refactors don't break the patch +- `--fuzz=3` allows `patch(1)` to find the target location even when nearby lines have shifted +- Together these three flags eliminate the entire class of "context mismatch" failures + +**Why hand-writing is forbidden:** +- Human-written diffs routinely have wrong `@@` line counts, missing or extra context lines, + incorrect `--- a/` / `+++ b/` paths, or embedded timestamps — all of which cause `patch(1)` + to reject the patch or silently apply it to the wrong location +- The `git diff -U0 -w` command produces mechanically correct diffs every time + +**Before this technique**, patches routinely broke when: +- A variable was renamed (e.g., `deamon` → `daemon` in context) +- Lines were added or removed above the changed code +- Indentation was reformatted +- An earlier patch in the chain shifted line numbers + +**With this technique**, patches survive all of the above. A hunk consists only of the +changed lines themselves — no context that can go stale. + +**Conventions:** +- Always use `--- a/path` and `+++ b/path` headers (no timestamps) +- Always name patches `P-.patch` with sequential numbering +- Always wire patches into `recipe.toml` `patches = [...]` in application order +- Always validate with `repo validate-patches ` after creating or editing a patch +- When updating an existing patch, regenerate it entirely rather than editing line numbers manually + ### Protected Recipes Core recipes (`base`, `kernel`, `relibc`, `bootloader`, etc.) and any recipe carrying diff --git a/config/acid.toml b/config/acid.toml index 6e4e82ed68..fe0dda362c 100644 --- a/config/acid.toml +++ b/config/acid.toml @@ -18,7 +18,7 @@ path = "/usr/lib/init.d/10_acid.service" data = """ [unit] description = "Acid test runner" -requires_weak = ["00_pcid-spawner.service"] +requires_weak = ["00_driver-manager.service"] [service] cmd = "ion" diff --git a/config/redbear-device-services.toml b/config/redbear-device-services.toml index c3ed968cce..3b91fa8873 100644 --- a/config/redbear-device-services.toml +++ b/config/redbear-device-services.toml @@ -1,6 +1,11 @@ # Red Bear OS shared device-service wiring # # Shared by profiles that ship the firmware/input/Wi-Fi control compatibility stack. +# +# Driver matching: driver-manager reads /lib/drivers.d/*.toml and matches against +# devices from both PCI and ACPI buses. ACPI devices are classified with PCI-equivalent +# class/subclass/vendor codes by redox-driver-acpi's AcpiBus, allowing reuse of existing +# driver match rules. [packages] redbear-quirks = {} @@ -32,9 +37,9 @@ data = """ path = "/etc/init.d/12_boot-late.target" data = """ [unit] -description = "Late boot services target" +description = "Late boot services target (compat alias for 04_drivers.target)" requires_weak = [ - "00_base.target", + "04_drivers.target", ] """ @@ -54,6 +59,7 @@ priority = 100 command = ["/usr/lib/drivers/nvmed"] [[driver.match]] +bus = "pci" class = 1 subclass = 8 @@ -64,6 +70,7 @@ priority = 100 command = ["/usr/lib/drivers/ahcid"] [[driver.match]] +bus = "pci" class = 1 subclass = 6 @@ -74,6 +81,7 @@ priority = 100 command = ["/usr/lib/drivers/ided"] [[driver.match]] +bus = "pci" class = 1 subclass = 1 @@ -84,6 +92,7 @@ priority = 100 command = ["/usr/lib/drivers/virtio-blkd"] [[driver.match]] +bus = "pci" vendor = 0x1AF4 device = 0x1001 class = 1 @@ -100,6 +109,7 @@ priority = 50 command = ["/usr/lib/drivers/e1000d"] [[driver.match]] +bus = "pci" vendor = 0x8086 class = 2 @@ -110,6 +120,7 @@ priority = 50 command = ["/usr/lib/drivers/rtl8168d"] [[driver.match]] +bus = "pci" vendor = 0x10EC class = 2 @@ -120,6 +131,7 @@ priority = 50 command = ["/usr/lib/drivers/rtl8139d"] [[driver.match]] +bus = "pci" vendor = 0x10EC device = 0x8139 @@ -130,6 +142,7 @@ priority = 50 command = ["/usr/lib/drivers/ixgbed"] [[driver.match]] +bus = "pci" vendor = 0x8086 class = 2 subclass = 0 @@ -141,6 +154,7 @@ priority = 50 command = ["/usr/lib/drivers/virtio-netd"] [[driver.match]] +bus = "pci" vendor = 0x1AF4 class = 2 """ @@ -155,6 +169,7 @@ priority = 80 command = ["/usr/lib/drivers/xhcid"] [[driver.match]] +bus = "pci" class = 0x0C subclass = 0x03 prog_if = 0x30 @@ -169,6 +184,7 @@ command = ["/usr/lib/drivers/ehcid"] # control-transfer pass-through while the wider USB stack continues converging. [[driver.match]] +bus = "pci" class = 0x0C subclass = 0x03 prog_if = 0x20 @@ -180,6 +196,7 @@ priority = 80 command = ["/usr/lib/drivers/ohcid"] [[driver.match]] +bus = "pci" class = 0x0C subclass = 0x03 prog_if = 0x10 @@ -191,6 +208,7 @@ priority = 80 command = ["/usr/lib/drivers/uhcid"] [[driver.match]] +bus = "pci" class = 0x0C subclass = 0x03 prog_if = 0x00 @@ -206,6 +224,7 @@ priority = 60 command = ["/usr/bin/redox-drm"] [[driver.match]] +bus = "pci" class = 0x03 """ @@ -233,6 +252,7 @@ priority = 40 command = ["/usr/lib/drivers/ihdad"] [[driver.match]] +bus = "pci" vendor = 0x8086 class = 0x04 @@ -243,10 +263,89 @@ priority = 40 command = ["/usr/lib/drivers/ac97d"] [[driver.match]] +bus = "pci" class = 0x04 subclass = 0x01 """ +[[files]] +path = "/etc/init.d/00_acpid.service" +data = """ +[unit] +description = "ACPI daemon (provides scheme:acpi)" +default_dependencies = false + +[service] +cmd = "acpid" +inherit_envs = ["RSDP_ADDR", "RSDP_SIZE"] +type = "notify" +""" + +# ACPI GPIO/I2C controller drivers +# These match against ACPI-enumerated devices (class/subclass/vendor from _HID). +[[files]] +path = "/lib/drivers.d/60-gpio-i2c.toml" +data = """ +# I2C bus registry — infrastructure, no hardware match +[[driver]] +name = "i2cd" +description = "I2C host adapter registry" +priority = 85 +command = ["/usr/lib/drivers/i2cd"] + +# GPIO pin registry — infrastructure, no hardware match +[[driver]] +name = "gpiod" +description = "GPIO controller registry" +priority = 85 +command = ["/usr/lib/drivers/gpiod"] + +# Intel ACPI I2C controller (DesignWare) +# Matches: INT33C3, INT3433, INT3442, INT3446, INT3447, INT3455, INT34B9 +[[driver]] +name = "dw-acpi-i2cd" +description = "DesignWare ACPI I2C controller" +priority = 80 +command = ["/usr/lib/drivers/dw-acpi-i2cd"] +depends_on = ["acpi", "i2c"] + +[[driver.match]] +bus = "acpi" +class = 0x0C +subclass = 0x05 +vendor = 0x8086 + +# AMD MP2 I2C controller +# Matches: AMDI0010, AMDI0510, AMDI0019 +[[driver]] +name = "amd-mp2-i2cd" +description = "AMD MP2 I2C controller" +priority = 80 +command = ["/usr/lib/drivers/amd-mp2-i2cd"] +depends_on = ["acpi", "i2c"] + +[[driver.match]] +bus = "acpi" +class = 0x0C +subclass = 0x05 +vendor = 0x1022 + +# Intel ACPI GPIO controller +# Matches: INT33C7, INT3437, INT3450, INT345D, INT34BB +[[driver]] +name = "intel-gpiod" +description = "Intel ACPI GPIO registrar" +priority = 80 +command = ["/usr/lib/drivers/intel-gpiod"] +depends_on = ["acpi", "gpio"] + +[[driver.match]] +bus = "acpi" +class = 0x0C +subclass = 0x80 +vendor = 0x8086 +""" + [[files]] path = "/lib/drivers.d/70-usb-class.toml" data = """ @@ -281,15 +380,15 @@ vendor = 0xFFFF device = 0xFFFF """ -# Profiles that include this fragment should start `driver-manager` instead of -# `pcid-spawner`; the manager performs the PCI bind/channel handoff itself. +# driver-manager owns PCI device enumeration, driver matching, and bind/channel +# handoff — replacing the old pcid + pcid-spawner pair entirely. [[files]] path = "/etc/init.d/00_driver-manager.service" data = """ [unit] description = "Red Bear driver manager" requires_weak = [ - "00_base.target", + "02_early_hw.target", ] [service] @@ -298,33 +397,26 @@ args = ["--hotplug"] type = "oneshot_async" """ +# Override the base package's 30_thermald.service with a no-op since +# 15_thermald.service (above) replaces it with earlier start ordering. [[files]] -path = "/etc/init.d/10_evdevd.service" +path = "/etc/init.d/30_thermald.service" data = """ [unit] -description = "Evdev input daemon" -requires_weak = [ - "12_boot-late.target", - "00_driver-manager.service", -] +description = "Thermal management daemon (suppressed; use 15_thermald.service)" [service] -cmd = "evdevd" -type = "oneshot_async" +cmd = "echo" +args = ["thermald: started earlier as 15_thermald.service"] +type = "oneshot" """ -[[files]] -path = "/etc/firmware-fallbacks.d" -data = "" -directory = true -mode = 0o755 - [[files]] path = "/etc/init.d/15_cpufreqd.service" data = """ [unit] description = "CPU frequency scaling daemon" -requires_weak = ["12_boot-late.target"] +requires_weak = ["04_drivers.target"] [service] cmd = "/usr/bin/cpufreqd" @@ -336,13 +428,25 @@ path = "/etc/init.d/15_thermald.service" data = """ [unit] description = "Thermal management daemon" -requires_weak = ["12_boot-late.target"] +requires_weak = ["04_drivers.target"] [service] cmd = "/usr/bin/thermald" type = "oneshot_async" """ +[[files]] +path = "/etc/init.d/15_coretempd.service" +data = """ +[unit] +description = "CPU temperature sensor daemon" +requires_weak = ["04_drivers.target"] + +[service] +cmd = "/usr/bin/coretempd" +type = { scheme = "coretemp" } +""" + [[files]] path = "/etc/init.d/15_hwrngd.service" data = """ @@ -372,7 +476,7 @@ path = "/etc/init.d/16_redbear-acmd.service" data = """ [unit] description = "USB CDC ACM serial daemon" -requires_weak = ["12_boot-late.target"] +requires_weak = ["04_drivers.target"] [service] cmd = "/usr/bin/redbear-acmd" @@ -384,7 +488,7 @@ path = "/etc/init.d/16_redbear-ecmd.service" data = """ [unit] description = "USB CDC ECM/NCM ethernet daemon" -requires_weak = ["12_boot-late.target"] +requires_weak = ["04_drivers.target"] [service] cmd = "/usr/bin/redbear-ecmd" @@ -396,7 +500,7 @@ path = "/etc/init.d/16_redbear-usbaudiod.service" data = """ [unit] description = "USB Audio Class daemon" -requires_weak = ["12_boot-late.target"] +requires_weak = ["04_drivers.target"] [service] cmd = "/usr/bin/redbear-usbaudiod" diff --git a/config/redbear-full.toml b/config/redbear-full.toml index f22749fa9a..b8e3a16e20 100644 --- a/config/redbear-full.toml +++ b/config/redbear-full.toml @@ -137,7 +137,7 @@ kwin = {} redbear-authd = {} redbear-session-launch = {} seatd = {} -redbear-greeter = "ignore" # WIP: blocked on qmlimportscanner from qtdeclarative +redbear-greeter = {} amdgpu = {} # Core Red Bear umbrella package @@ -148,8 +148,8 @@ relibc-phase1-tests = {} # Native build toolchain (Phase 3: GCC + binutils running on redox) # Produces gcc/g++/as/ld that execute inside Red Bear OS -gcc-native = "ignore" # WIP: depends on binutils-native -binutils-native = "ignore" # WIP: source archive not in offline cache +gcc-native = {} +binutils-native = {} # llvm-native = {} # suppressed: Redox C++/pthread header gaps; not needed for greeter proof # rust-native = {} # suppressed: depends on llvm-native; not needed for greeter proof @@ -237,7 +237,7 @@ data = """ [unit] description = "Boot essential services target" requires_weak = [ - "00_base.target", + "04_drivers.target", ] """ @@ -261,7 +261,7 @@ data = """ [unit] description = "DRM/KMS display driver (AMD + Intel + VirtIO)" requires_weak = [ - "05_boot-essential.target", + "04_drivers.target", ] [service] @@ -276,7 +276,7 @@ data = """ [unit] description = "D-Bus system bus" requires_weak = [ - "12_boot-late.target", + "06_services.target", "00_ipcd.service", ] @@ -292,6 +292,7 @@ data = """ [unit] description = "Red Bear session broker (org.freedesktop.login1)" requires_weak = [ + "06_services.target", "12_dbus.service", ] @@ -306,6 +307,7 @@ data = """ [unit] description = "seatd seat management daemon" requires_weak = [ + "06_services.target", "12_dbus.service", "13_redbear-sessiond.service", ] @@ -425,6 +427,7 @@ data = """ [unit] description = "Red Bear greeter service" requires_weak = [ + "08_userland.target", "00_driver-manager.service", "14_redox-drm.service", "12_dbus.service", @@ -444,8 +447,9 @@ path = "/etc/init.d/29_activate_console.service" data = """ [unit] description = "Activate fallback console VT" +default_dependencies = false requires_weak = [ - "05_boot-essential.target", + "00_base.target", ] [service] @@ -459,6 +463,7 @@ path = "/etc/init.d/30_console.service" data = """ [unit] description = "Console terminals" +default_dependencies = false requires_weak = [ "29_activate_console.service", ] @@ -474,6 +479,7 @@ path = "/etc/init.d/31_debug_console.service" data = """ [unit] description = "Debug console on serial port" +default_dependencies = false requires_weak = [ "29_activate_console.service", ] @@ -517,34 +523,6 @@ members = ["greeter"] gid = 100 members = ["messagebus"] -[[files]] -path = "/etc/pcid.d/ihdgd.toml" -data = """ -[[drivers]] -name = "Intel GPU (VGA compatible)" -class = 0x03 -vendor = 0x8086 -subclass = 0x00 -command = ["redox-drm"] - -[[drivers]] -name = "Intel GPU (3D controller)" -class = 0x03 -vendor = 0x8086 -subclass = 0x02 -command = ["redox-drm"] -""" - -[[files]] -path = "/etc/pcid.d/virtio-gpud.toml" -data = """ -[[drivers]] -name = "VirtIO GPU" -class = 0x03 -vendor = 0x1af4 -device = 0x1050 -command = ["/usr/bin/redox-drm"] -""" [[files]] path = "/etc/environment.d/90-dbus.conf" diff --git a/config/redbear-greeter-services.toml b/config/redbear-greeter-services.toml index 5a52eff0e3..2e845dd21f 100644 --- a/config/redbear-greeter-services.toml +++ b/config/redbear-greeter-services.toml @@ -8,7 +8,7 @@ data = """ [unit] description = "Boot essential services target" requires_weak = [ - "00_base.target", + "04_drivers.target", ] """ @@ -101,7 +101,7 @@ data = """ [unit] description = "Activate fallback console VT" requires_weak = [ - "05_boot-essential.target", + "08_userland.target", ] [service] diff --git a/config/redbear-legacy-base.toml b/config/redbear-legacy-base.toml index a315516e0f..79eba90d82 100644 --- a/config/redbear-legacy-base.toml +++ b/config/redbear-legacy-base.toml @@ -3,14 +3,9 @@ # 00_base.service: stripped base setup (tmpdir only, no sudo — sudo runs from # base.toml's 00_sudo.service). ipcd and ptyd are started by # 00_ipcd.service and 00_ptyd.service from the base recipe. -# 00_drivers / 10_net: no longer overridden — the legacy scripts were removed -# from base.toml. The retained 00_pcid-spawner.service unit name now -# launches driver-manager so existing init ordering remains stable. -# 00_pcid-spawner.service: compatibility wrapper for driver-manager. The base -# recipe uses type="oneshot" which blocks init until pcid-spawner exits. -# Running driver-manager here with oneshot_async keeps the historic unit -# name for downstream `requires_weak` consumers while moving PCI driver -# spawning to the manager that performs bind/channel handoff. +# 00_pcid-spawner.service has been fully replaced by 00_driver-manager.service +# (defined in redbear-device-services.toml). The old pcid-spawner +# unit name is no longer used anywhere. [packages] zsh = {} @@ -37,17 +32,4 @@ default_dependencies = false [service] cmd = "audiod" type = "oneshot_async" -""" - -[[files]] -path = "/etc/init.d/00_pcid-spawner.service" -data = """ -[unit] -description = "PCI driver spawner compatibility alias" -default_dependencies = false - -[service] -cmd = "echo" -args = ["pcid-spawner compatibility alias: driver-manager owns PCI driver spawning"] -type = "oneshot" -""" +""" \ No newline at end of file diff --git a/config/redbear-mini.toml b/config/redbear-mini.toml index 8644dce214..d3ccebc54e 100644 --- a/config/redbear-mini.toml +++ b/config/redbear-mini.toml @@ -9,7 +9,7 @@ # - all non-graphics, non-firmware packages from the full profile # - no linux-firmware payload, no firmware-loader, no GPU/display drivers -include = ["minimal.toml", "redbear-legacy-base.toml", "redbear-netctl.toml", "redbear-device-services.toml"] +include = ["minimal.toml", "redbear-legacy-base.toml", "redbear-netctl.toml", "redbear-device-services.toml", "redbear-boot-stages.toml"] [general] filesystem_size = 1536 @@ -27,9 +27,8 @@ redbear-release = {} redbear-hwutils = {} redbear-quirks = {} -# Device driver infrastructure: driver-manager is started by -# redbear-device-services.toml, with 00_pcid-spawner.service retained only as a -# compatibility dependency alias for older service units. +# Device driver infrastructure: driver-manager replaces pcid-spawner; +# 00_driver-manager.service is defined in redbear-device-services.toml. ehcid = {} ohcid = {} uhcid = {} @@ -53,6 +52,7 @@ redbear-info = {} cub = {} cpufreqd = {} thermald = {} +coretempd = {} hwrngd = {} redbear-acmd = {} redbear-ecmd = {} @@ -99,7 +99,7 @@ meson = {} ninja-build = {} m4 = {} #git = {} # suppressed: cascading rebuild; git not needed for boot/recovery -htop = {} +#htop = {} # disabled: build failure in redoxer env (pre-existing) #mc = {} # suppressed: C99 format warning errors in compilation # ── Build / packaging utilities ── @@ -231,6 +231,7 @@ path = "/etc/init.d/00_i2c-dw-acpi.service" data = """ [unit] description = "DesignWare ACPI I2C controller (non-blocking)" +default_dependencies = false requires_weak = [ "00_i2cd.service", ] @@ -245,6 +246,7 @@ path = "/etc/init.d/00_intel-gpiod.service" data = """ [unit] description = "Intel ACPI GPIO registrar (non-blocking)" +default_dependencies = false requires_weak = [ "00_gpiod.service", "00_i2cd.service", @@ -260,6 +262,7 @@ path = "/etc/init.d/00_i2c-gpio-expanderd.service" data = """ [unit] description = "I2C GPIO expander companion bridge (non-blocking on live-mini)" +default_dependencies = false requires_weak = [ "00_i2cd.service", "00_gpiod.service", @@ -275,6 +278,8 @@ path = "/etc/init.d/00_i2c-hidd.service" data = """ [unit] description = "ACPI I2C HID bring-up daemon (non-blocking)" +default_dependencies = false +requires = ["00_acpid.service"] requires_weak = [ "00_i2cd.service", "00_i2c-dw-acpi.service", @@ -292,6 +297,7 @@ path = "/etc/init.d/00_ucsid.service" data = """ [unit] description = "USB-C UCSI topology detector (non-blocking on live-mini)" +default_dependencies = false requires_weak = [ "00_base.target", "00_i2cd.service", @@ -306,9 +312,9 @@ type = { scheme = "ucsi" } path = "/etc/init.d/12_boot-late.target" data = """ [unit] -description = "Late boot services target" +description = "Late boot services target (compat alias for 04_drivers.target)" requires_weak = [ - "00_base.target", + "04_drivers.target", ] """ @@ -467,23 +473,7 @@ data = "" directory = true mode = 0o755 -[[files]] -path = "/etc/pcid.d/ihdgd.toml" -data = """ -# redbear-live-mini: text-only image; override upstream ihdgd config with empty file -""" -[[files]] -path = "/etc/pcid.d/virtio-gpud.toml" -data = """ -# redbear-live-mini: text-only image; override upstream virtio-gpud config with empty file -""" - -[[files]] -path = "/etc/pcid.d/00_text_mode_gpu_mask.toml" -data = """ -# redbear-live-mini: no display driver matched; class 0x03 devices are skipped -""" [[files]] path = "/lib/drivers.d/30-graphics.toml" @@ -502,6 +492,7 @@ path = "/etc/init.d/29_activate_console.service" data = """ [unit] description = "Activate console VT" +default_dependencies = false requires_weak = ["00_base.target"] [service] @@ -515,6 +506,7 @@ path = "/etc/init.d/30_console.service" data = """ [unit] description = "Console terminals" +default_dependencies = false requires_weak = ["29_activate_console.service"] [service] @@ -528,6 +520,7 @@ path = "/etc/init.d/31_debug_console.service" data = """ [unit] description = "Debug console" +default_dependencies = false requires_weak = ["29_activate_console.service"] [service] @@ -535,3 +528,16 @@ cmd = "getty" args = ["/scheme/debug/no-preserve", "-J"] type = "oneshot_async" """ + +[[files]] +path = "/etc/init.d/08_userland.target" +data = """ +[unit] +description = "Userland services target" +requires_weak = [ + "06_services.target", + "29_activate_console.service", + "30_console.service", + "31_debug_console.service", +] +""" diff --git a/config/redoxer.toml b/config/redoxer.toml index 6875066dfb..f1591fb803 100644 --- a/config/redoxer.toml +++ b/config/redoxer.toml @@ -21,7 +21,7 @@ path = "/usr/lib/init.d/10_smolnetd.service" data = """ [unit] description = "Network stack for redoxer" -requires_weak = ["00_pcid-spawner.service"] +requires_weak = ["00_driver-manager.service"] [service] cmd = "netstack" diff --git a/local/AGENTS.md b/local/AGENTS.md index a97e9d1fab..db9039d64b 100644 --- a/local/AGENTS.md +++ b/local/AGENTS.md @@ -1,5 +1,29 @@ # RED BEAR OS — DERIVATIVE OF REDOX OS +## INTERNAL-FIRST POLICY + +When building packages, resolving dependencies, or choosing between implementations, +**always prefer internal Red Bear recipes and patched sources** over upstream or +third-party alternatives. This applies at every decision point: + +| Decision | Always prefer | +|----------|--------------| +| Recipe location | `local/recipes/` over `recipes/` (upstream) | +| Patch source | `local/patches/` over in-tree edits or ad-hoc sed | +| Source tarball | Patched archive in `sources/redbear-0.1.0/tarballs/` over re-download | +| Implementation | Red Bear Rust implementation over upstream C port | +| Config | `config/redbear-*.toml` over mainline `config/*.toml` | +| Scripts | `local/scripts/` over ad-hoc shell commands | + +**Concretely:** if `local/recipes///` exists and is symlinked into the +recipe tree, that is the authoritative recipe — never fall back to the upstream +`recipes/` version. If a local recipe has a `redox.patch`, that patch is the +maintained Red Bear delta — never work around it by editing the source tree directly. + +**Rationale:** the local overlay is the durable, version-controlled, release-safe layer. +Upstream recipes are disposable and may be overwritten by `make distclean` or release +provisioning. Only `local/` survives across rebuilds and releases. + ## TUI CONVENTION — `-i` INTERACTIVE SWITCH All Red Bear desktop applications that offer a TUI mode MUST use `-i`/`--interactive` @@ -50,6 +74,58 @@ files, Wayland protocol stubs, D-Bus service stubs, and any other layer of the s **No exceptions. No "temporary." No "until we fix it properly."** +## BUILD DURABILITY AND CASCADE POLICY + +### Every Build Lands in the Repo + +Every successful `repo cook ` MUST produce two durable artifacts: + +1. **Package in the repo**: `repo/x86_64-unknown-redox/.pkgar` + `.toml` +2. **Patched source form**: All source modifications mirrored to `local/patches//` + +A build is **not complete** until both exist. Verify after every cook: + +```bash +./target/release/repo find # Must find the package +ls repo/x86_64-unknown-redox/.toml # Manifest must exist +ls repo/x86_64-unknown-redox/.pkgar # Archive must exist +``` + +If a package was built but the repo artifacts are missing, the build did not complete. +If source patches exist only in `recipes/*/source/` but not in `local/patches/`, +the patches are not durable (see Source-of-Truth Rule below). + +### Cascade Rebuild Rule + +When a low-level package changes, **all packages that transitively depend on it +must be rebuilt**. A stale dependent silently produces link errors, ABI mismatches, +or runtime crashes. + +```bash +# Rebuild relibc and everything that depends on it +./local/scripts/rebuild-cascade.sh relibc + +# Dry run: show what would be rebuilt without building +./local/scripts/rebuild-cascade.sh --dry-run relibc + +# Multiple root packages +./local/scripts/rebuild-cascade.sh relibc ncurses +``` + +The script performs BFS over reverse dependencies: it finds all packages whose +`recipe.toml` lists the target in `dependencies`, transitively expands, then builds +root-first followed by dependents. + +**Always use cascade rebuilds after changing:** +- relibc (headers, ABI, any patches) +- Kernel (syscall ABI changes) +- Shared libraries (ncurses, zlib, openssl, etc.) +- Any package listed in other packages' `dependencies` + +**Example:** Changing relibc's `sys/types/internal.h` header requires rebuilding +bison, m4, flex, and every other gnulib-based package that includes system headers +through the relibc include chain. + ## DESIGN PRINCIPLE Red Bear OS is a **full fork** based on frozen Redox OS snapshots: @@ -73,10 +149,21 @@ make all CONFIG_NAME=redbear-full → mk/config.mk resolves to the active desktop/graphics compile target → Desktop/graphics are available only on redbear-full → repo cook builds all packages from local sources (offline by default) + → Each successful cook produces repo//.pkgar + .toml → mk/disk.mk creates harddrive.img with Red Bear branding → REDBEAR_RELEASE=0.1.0 ensures immutable, archived sources ``` +Cascade rebuild flow (when a low-level package changes): +``` +./local/scripts/rebuild-cascade.sh + → Finds all packages whose recipe.toml lists in dependencies + → BFS expands the reverse dependency graph + → Builds root package first, then dependents in dependency order + → Pushes all rebuilt packages to sysroot + → Every rebuilt package lands in repo/ (.pkgar + .toml) +``` + Release flow: ``` # Sources are immutable — build from archives, never from network @@ -259,6 +346,7 @@ redox-master/ ← git pull updates mainline Redox │ │ └── images/ ← Red Bear OS icon (1254x1254) + loading bg (1536x1024) │ ├── firmware/ ← GPU firmware blobs (gitignored, fetched) │ ├── scripts/ +│ │ ├── rebuild-cascade.sh ← Rebuild package + all dependents (BFS reverse-dep graph) │ │ ├── provision-release.sh ← Provision new release from Redox ref │ │ ├── build-redbear.sh ← Unified Red Bear OS build script │ │ ├── fetch-firmware.sh ← Download bounded AMD or Intel firmware subsets from linux-firmware @@ -311,6 +399,10 @@ scripts/build-iso.sh redbear-full # Full desktop live ISO scripts/build-iso.sh redbear-mini # Text-only mini (default) scripts/build-iso.sh redbear-grub # Text-only + GRUB +# Rebuild a package and all its dependents (cascade) +./local/scripts/rebuild-cascade.sh relibc # Rebuild relibc + all dependents +./local/scripts/rebuild-cascade.sh --dry-run ncurses # Show cascade without building + # VM-network baseline validation helpers ./local/scripts/validate-vm-network-baseline.sh ./local/scripts/test-vm-network-qemu.sh redbear-mini @@ -848,4 +940,4 @@ Config comparison: ## ANTI-PATTERNS (COMMIT POLICY) -- **DO NOT** include AI attribution in commit messages — no "Ultraworked with [Sisyphus]", "Co-authored-by: Sisyphus", or similar AI agent footers. Commits belong to the human author only. +- **DO NOT** include AI attribution in commit messages — no AI agent footers, co-authored-by lines for automated assistance, or similar markers. Commits belong to the human author only. diff --git a/local/config/drivers.d/00-storage.toml b/local/config/drivers.d/00-storage.toml index b9afe7ccc0..2bac2daef3 100644 --- a/local/config/drivers.d/00-storage.toml +++ b/local/config/drivers.d/00-storage.toml @@ -7,6 +7,7 @@ priority = 100 command = ["/usr/lib/drivers/nvmed"] [[driver.match]] +bus = "pci" class = 1 subclass = 8 @@ -17,6 +18,7 @@ priority = 100 command = ["/usr/lib/drivers/ahcid"] [[driver.match]] +bus = "pci" class = 1 subclass = 6 @@ -27,6 +29,7 @@ priority = 100 command = ["/usr/lib/drivers/ided"] [[driver.match]] +bus = "pci" class = 1 subclass = 1 @@ -37,6 +40,7 @@ priority = 100 command = ["/usr/lib/drivers/virtio-blkd"] [[driver.match]] +bus = "pci" vendor = 0x1AF4 device = 0x1001 class = 1 diff --git a/local/config/drivers.d/10-network.toml b/local/config/drivers.d/10-network.toml index b7b5f91223..e9ad08d8ee 100644 --- a/local/config/drivers.d/10-network.toml +++ b/local/config/drivers.d/10-network.toml @@ -7,6 +7,7 @@ priority = 50 command = ["/usr/lib/drivers/e1000d"] [[driver.match]] +bus = "pci" vendor = 0x8086 class = 2 @@ -17,6 +18,7 @@ priority = 50 command = ["/usr/lib/drivers/rtl8168d"] [[driver.match]] +bus = "pci" vendor = 0x10EC class = 2 @@ -27,6 +29,7 @@ priority = 50 command = ["/usr/lib/drivers/rtl8139d"] [[driver.match]] +bus = "pci" vendor = 0x10EC device = 0x8139 @@ -37,6 +40,7 @@ priority = 50 command = ["/usr/lib/drivers/ixgbed"] [[driver.match]] +bus = "pci" vendor = 0x8086 class = 2 subclass = 0 @@ -48,5 +52,6 @@ priority = 50 command = ["/usr/lib/drivers/virtio-netd"] [[driver.match]] +bus = "pci" vendor = 0x1AF4 class = 2 diff --git a/local/config/drivers.d/20-usb.toml b/local/config/drivers.d/20-usb.toml index 739490e53e..b3d9ad8247 100644 --- a/local/config/drivers.d/20-usb.toml +++ b/local/config/drivers.d/20-usb.toml @@ -44,6 +44,49 @@ priority = 80 command = ["/usr/lib/drivers/uhcid"] [[driver.match]] +bus = "pci" +class = 0x0C +subclass = 0x03 +prog_if = 0x30 + +# EHCI (USB 2.0) +[[driver]] +name = "ehcid" +description = "EHCI USB 2.0 host controller" +priority = 80 +command = ["/usr/lib/drivers/ehcid"] + +# EHCI now owns a simple /scheme/usb controller surface for per-port status and +# control-transfer pass-through while the wider USB stack continues converging. + +[[driver.match]] +bus = "pci" +class = 0x0C +subclass = 0x03 +prog_if = 0x20 + +# OHCI (USB 1.1 — non-Intel chipsets) +[[driver]] +name = "ohcid" +description = "OHCI USB 1.1 host controller" +priority = 80 +command = ["/usr/lib/drivers/ohcid"] + +[[driver.match]] +bus = "pci" +class = 0x0C +subclass = 0x03 +prog_if = 0x10 + +# UHCI (USB 1.1 — Intel chipsets) +[[driver]] +name = "uhcid" +description = "UHCI USB 1.1 host controller (Intel)" +priority = 80 +command = ["/usr/lib/drivers/uhcid"] + +[[driver.match]] +bus = "pci" class = 0x0C subclass = 0x03 prog_if = 0x00 diff --git a/local/config/drivers.d/30-graphics.toml b/local/config/drivers.d/30-graphics.toml index 51c037409c..d2ecda8c8e 100644 --- a/local/config/drivers.d/30-graphics.toml +++ b/local/config/drivers.d/30-graphics.toml @@ -7,6 +7,7 @@ priority = 60 command = ["/usr/lib/drivers/vesad"] [[driver.match]] +bus = "pci" class = 0x03 [[driver]] @@ -18,14 +19,17 @@ command = ["/usr/bin/redox-drm"] # Only match known GPU vendors. Class 0x03 alone catches QEMU VGA # (vendor 0x1234) which redox-drm rejects with a fatal error. [[driver.match]] +bus = "pci" vendor = 0x1002 class = 0x03 [[driver.match]] +bus = "pci" vendor = 0x8086 class = 0x03 [[driver.match]] +bus = "pci" vendor = 0x1AF4 class = 0x03 @@ -36,6 +40,7 @@ priority = 61 command = ["/usr/bin/redox-drm"] [[driver.match]] +bus = "pci" vendor = 0x1AF4 class = 0x03 @@ -47,6 +52,7 @@ priority = 61 command = ["/usr/bin/redox-drm"] [[driver.match]] +bus = "pci" vendor = 0x8086 class = 0x03 subclass = 0x00 @@ -59,6 +65,7 @@ priority = 61 command = ["/usr/bin/redox-drm"] [[driver.match]] +bus = "pci" vendor = 0x1002 class = 0x03 subclass = 0x00 diff --git a/local/config/drivers.d/50-audio.toml b/local/config/drivers.d/50-audio.toml index 87a73e017d..e1f9308270 100644 --- a/local/config/drivers.d/50-audio.toml +++ b/local/config/drivers.d/50-audio.toml @@ -7,6 +7,7 @@ priority = 40 command = ["/usr/lib/drivers/ihdad"] [[driver.match]] +bus = "pci" vendor = 0x8086 class = 0x04 @@ -17,6 +18,7 @@ priority = 40 command = ["/usr/lib/drivers/ac97d"] [[driver.match]] +bus = "pci" class = 0x04 subclass = 0x01 diff --git a/local/config/drivers.d/60-gpio-i2c.toml b/local/config/drivers.d/60-gpio-i2c.toml index 026e3b2609..cd8c316bfa 100644 --- a/local/config/drivers.d/60-gpio-i2c.toml +++ b/local/config/drivers.d/60-gpio-i2c.toml @@ -1,49 +1,139 @@ # GPIO and I2C controller drivers +# +# These drivers match against both PCI and ACPI devices. +# ACPI devices are classified by _HID → PCI-equivalent class/subclass/vendor +# codes via redox-driver-acpi's classify_acpi_device(). +# +# Match criteria use the standard [[driver.match]] format with class/subclass/vendor. +# The ACPI bus fills these fields from the _HID classification table. + +# --- I2C/SPI controller infrastructure --- [[driver]] name = "i2cd" description = "I2C host adapter registry" priority = 85 command = ["/usr/lib/drivers/i2cd"] +# i2cd is the I2C bus registry — spawned as infrastructure before +# specific I2C controller drivers. Does not match against hardware +# directly; it provides /scheme/i2c for controller drivers to register with. [[driver]] name = "gpiod" description = "GPIO controller registry" priority = 85 command = ["/usr/lib/drivers/gpiod"] +# gpiod is the GPIO pin registry — spawned as infrastructure before +# specific GPIO controller drivers. Does not match against hardware +# directly; it provides /scheme/gpio for controller drivers to register with. + +# --- ACPI I2C controller drivers --- +# These match against ACPI devices classified as Serial Bus Controller (0x0C), +# subclass SMBus/I2C (0x05), by the ACPI bus. +# The ACPI bus maps Intel INT33C3/INT3433/... and AMD AMDI0010 HIDs to these codes. [[driver]] name = "dw-acpi-i2cd" description = "DesignWare ACPI I2C controller" priority = 80 command = ["/usr/lib/drivers/dw-acpi-i2cd"] +depends_on = ["acpi", "i2c"] -[[driver]] -name = "intel-gpiod" -description = "Intel ACPI GPIO registrar" -priority = 80 -command = ["/usr/lib/drivers/intel-gpiod"] +[[driver.match]] +bus = "acpi" +class = 0x0C +subclass = 0x05 +vendor = 0x8086 [[driver]] name = "amd-mp2-i2cd" description = "AMD MP2 I2C controller" priority = 80 command = ["/usr/lib/drivers/amd-mp2-i2cd"] +depends_on = ["acpi", "i2c"] + +[[driver.match]] +bus = "acpi" +class = 0x0C +subclass = 0x05 +vendor = 0x1022 [[driver]] name = "intel-lpss-i2cd" description = "Intel LPSS I2C controller" priority = 80 command = ["/usr/lib/drivers/intel-lpss-i2cd"] +depends_on = ["acpi", "i2c"] + +[[driver.match]] +bus = "acpi" +class = 0x0C +subclass = 0x05 +vendor = 0x8086 + +# --- ACPI SPI controller drivers --- +# These match against ACPI devices classified as Serial Bus Controller (0x0C), +# subclass SPI (0x06), by the ACPI bus. + +[[driver]] +name = "intel-lpss-spid" +description = "Intel LPSS SPI controller" +priority = 80 +command = ["/usr/lib/drivers/intel-lpss-spid"] +depends_on = ["acpi"] + +[[driver.match]] +bus = "acpi" +class = 0x0C +subclass = 0x06 +vendor = 0x8086 + +# --- ACPI GPIO controller drivers --- +# These match against ACPI devices classified as Serial Bus Controller (0x0C), +# subclass Other (0x80), vendor Intel, by the ACPI bus. +# The ACPI bus maps INT33C7/INT3437/INT3450 HIDs to these codes. + +[[driver]] +name = "intel-gpiod" +description = "Intel ACPI GPIO registrar" +priority = 80 +command = ["/usr/lib/drivers/intel-gpiod"] +depends_on = ["acpi", "gpio"] + +[[driver.match]] +bus = "acpi" +class = 0x0C +subclass = 0x80 +vendor = 0x8086 + +# --- ACPI thermal/power drivers --- +# These match against ACPI devices classified as Thermal/Battery (0x0B). + +[[driver]] +name = "redbear-thermald" +description = "ACPI thermal zone monitor" +priority = 60 +command = ["/usr/lib/drivers/redbear-thermald"] +depends_on = ["acpi"] + +[[driver.match]] +bus = "acpi" +class = 0x0B + +# --- I2C companion drivers --- +# These depend on I2C bus being available and match against specific +# I2C device addresses (not PCI/ACPI class matching). [[driver]] name = "i2c-gpio-expanderd" description = "I2C GPIO expander companion bridge" priority = 75 command = ["/usr/lib/drivers/i2c-gpio-expanderd"] +depends_on = ["i2c", "gpio"] [[driver]] name = "intel-thc-hidd" description = "Intel THC QuickI2C HID transport" priority = 75 command = ["/usr/lib/drivers/intel-thc-hidd"] +depends_on = ["acpi", "i2c"] diff --git a/local/docs/CPU-DMA-IRQ-MSI-SCHEDULER-FIX-PLAN.md b/local/docs/CPU-DMA-IRQ-MSI-SCHEDULER-FIX-PLAN.md index cb98ac9df3..e69de29bb2 100644 --- a/local/docs/CPU-DMA-IRQ-MSI-SCHEDULER-FIX-PLAN.md +++ b/local/docs/CPU-DMA-IRQ-MSI-SCHEDULER-FIX-PLAN.md @@ -1,158 +0,0 @@ -# Red Bear OS — CPU/DMA/IRQ/MSI/Scheduler Fix Plan - -**Date**: 2026-05-04 -**Updated**: 2026-05-04 (MSI T1.1–T2.2 implemented, committed, pushed) -**Status**: Active — MSI Phase 1 complete, DMA/Scheduler pending -**Source of truth**: Linux kernel 7.0 (local/reference/linux-7.0/) - -## 1. Problem Statement - -Five critical integration gaps in the microkernel architecture: - -| Gap | Severity | Impact | Status | -|-----|----------|--------|--------| -| MSI absent from kernel | CRITICAL | All NVMe/GPU/NIC on legacy INTx | ✅ RESOLVED (P8-msi.patch) | -| DMA/IOMMU not integrated | CRITICAL | DMA buffers unprotected | ⏳ Pending | -| PIT tick (148Hz) vs LAPIC (1000Hz) | HIGH | Scheduler 6x slower than Linux | ✅ RESOLVED (P7-scheduler patch) | -| Global scheduler lock | HIGH | Serializes all context switches | ✅ RESOLVED (work-stealing) | -| Thread creation (3 IPC hops) | HIGH | 3x slower than Linux clone() | ⏳ Pending | - -## 2. Phase 1: MSI/MSI-X in Kernel (Week 1-3) ✅ COMPLETE - -### T1.1: MSI Capability Parsing ✅ DONE -- File: `kernel/src/arch/x86_shared/device/msi.rs` (61 lines) -- Commit: `678980521` in `P8-msi.patch` -- Linux ref: `arch/x86/kernel/apic/msi.c` (391 lines) -- Implements: `MsiMessage` (compose/validate), `MsiCapability` (parse 32/64-bit), `MsixCapability` (parse table/PBA), `is_valid_msi_address`, `is_valid_msi_vector` -- Bounds-safe: all `parse()` methods return `Option`, using `.get()` instead of raw indexing - -### T1.2: Vector Allocation Matrix ✅ DONE -- File: `kernel/src/arch/x86_shared/device/vector.rs` (53 lines) -- Commit: `678980521` in `P8-msi.patch` -- Linux ref: `arch/x86/kernel/apic/vector.c` (1387 lines) -- Implements: per-CPU bitmatrix (7×32-bit banks = 224 vectors 32-255), `allocate_vector`, `free_vector` -- Lock-free CAS-based allocation with `trailing_ones()` find-first-zero -- NOTE: VECTORS table is global (not yet per-CPU sharded) — sufficient for 224 vectors - -### T1.3: MSI IRQ Domain (Scheme Integration) ✅ DONE -- File: `kernel/src/scheme/irq.rs` -- Commit: `678980521` in `P8-msi.patch` -- Implements: `msi_vector_is_valid()` (32-0xEF range check), `iommu_validate_msi_irq()` hook (stub: always true), IOMMU gate at `irq_trigger()` for vectors ≥16 - -### T1.4: Userspace MSI Consumer (driver-sys) ✅ DONE -- File: `local/recipes/drivers/redox-driver-sys/source/src/irq.rs` -- Commit: `678980521` -- Implements: `MsiAllocation` with round-robin CPU allocation, `irq_set_affinity` (scheme write), `program_x86_message` with kernel-mediated address/vector validation (mask `0xFFF0_0000`) -- Quirk-aware fallback retained: FORCE_LEGACY, NO_MSI, NO_MSIX - -### T1.5: Kernel-side MSI Affinity Handler ✅ DONE -- File: `kernel/src/scheme/irq.rs` -- Commit: `678980521` in `P8-msi.patch` -- Implements: `Handle::IrqAffinity { irq, mask }` variant, path routing for `/affinity` and `cpu-XX//affinity`, kwrite validates CPU id and stores mask atomically, kfstat/kfpath/kreadoff/close all handle new variant - -## 3. Phase 2: DMA/IOMMU Integration (Week 3-5) — AUDITED 2026-05-04 - -**Status**: IOMMU daemon (1003 lines) and DmaBuffer (261 lines) already exist and are solid. Tasks re-scoped from "create" to "wire." - -### T2.1: IommuDmaAllocator (driver-sys) ⏳ P0 -- File: `local/recipes/drivers/redox-driver-sys/source/src/dma.rs` -- Add `IommuDmaAllocator` struct: holds IOMMU domain fd, wraps `DmaBuffer::allocate()` with IOMMU MAP opcode -- Uses `scheme:iommu/domain/N` write with MAP request → get IOVA -- Linux ref: `include/linux/dma-mapping.h` — `dma_alloc_coherent()` → `iommu_dma_alloc()` - -### T2.2: GPU DMA pass-through ⏳ P0 -- Wire `redox-drm` GPU drivers to open IOMMU device endpoint and use IommuDmaAllocator -- amdgpu: VRAM/GTT allocations through IOMMU domain -- Intel i915: GTT pages through IOMMU domain -- Files: `local/recipes/gpu/redox-drm/source/`, `local/recipes/gpu/amdgpu/source/` - -### T2.3: Streaming DMA (linux-kpi) ⏳ P1 -- `dma_map_single()`: allocate bounce buffer, copy data, map through IOMMU -- `dma_unmap_single()`: copy back, unmap, free bounce buffer -- Linux ref: `kernel/dma/mapping.c` — streaming API -- File: `local/recipes/drivers/linux-kpi/source/` - -### T2.4: NVMe DMA pass-through ⏳ P1 -- Wire `ahcid`/`nvmed` PRP list physical addresses through IOMMU domain -- Linux ref: `drivers/nvme/host/pci.c` — `nvme_map_data()` - -### T2.5: SWIOTLB Fallback (low priority) ⏳ P2 -- Linux ref: `kernel/dma/swiotlb.c` -- Bounce buffer for devices with <4GB DMA addressing -- Only needed for ancient hardware; x86_64 modern hardware doesn't need it - -## 4. Phase 3: Scheduler Improvements (Week 4-6) — MOSTLY DONE - -### T3.1: LAPIC Timer as Primary Tick ✅ DONE -- P7-scheduler-improvements.patch: LAPIC timer calibrated + enabled at vector 48 -- TSC-deadline mode, 1000Hz tick drives DWRR scheduler directly -- PIT fallback retained - -### T3.2: Per-CPU Scheduler Locks ✅ DONE -- Work-stealing load balancer in switch.rs -- Per-CPU nr_running counter -- Idle CPUs steal work via IPI - -### T3.3: Load Balancing ✅ DONE -- RT scheduling class (priority 0-9, skip DWRR, immediate dispatch) -- Threshold reduced: 3→1 ticks for LAPIC-driven mode -- Geometric weights in DWRR - -### T3.4: RT Scheduling Class ✅ DONE - -### T3.5: NUMA-Aware Scheduling ❌ -- Not implemented — low priority for desktop/non-NUMA systems -- Linux ref: kernel/sched/rt.c -- FIFO and Round-Robin classes -- Priority inheritance -- RT throttling: 95% CPU cap/sec - -### T3.5: TSC-Deadline Timer -- Use IA32_TSC_DEADLINE MSR for precise tick -- True tickless operation -- TSC calibration via HPET or PIT - -## 5. Phase 4: Thread Creation (Week 6-7) - -### T4.1: Batched Thread Creation -- Batch new-thread requests (reduce IPC) -- Pre-allocate stack pages during fork - -### T4.2: Kernel Thread Pool -- Pre-create idle kernel threads -- Reuse via object pool - -### T4.3: Shared Memory IPC -- Use shm for proc scheme bulk ops -- Avoid data copy through IPC channel - -## 6. Dependencies - -Phase 1 (MSI): T1.1 -> T1.2 -> T1.3 -> T1.4 -> T1.5 -Phase 2 (DMA): T2.1 -> T2.2 -> T2.3 -> T2.4 -> T2.5 -Phase 3 (Sched): T3.1 -> T3.5 -> T3.2 -> T3.3 -> T3.4 -Phase 4 (Thread): T4.1 -> T4.2 -> T4.3 - -Phase 1+2 independent (parallel). Phase 2.4 needs Phase 1.3. -Phase 3.1 partially done (start immediately). - -## 7. Timeline - -| Phase | Duration | Cumulative | -|-------|----------|------------| -| Phase 1 (MSI) | 3 weeks | Week 3 | -| Phase 2 (DMA/IOMMU) | 3 weeks | Week 5 | -| Phase 3 (Scheduler) | 3 weeks | Week 7 | -| Phase 4 (Threads) | 2 weeks | Week 7 | - -Total: 7 weeks (2 devs parallel Phase 1+2) - -## 8. Success Metrics - -| Metric | Before | After | -|--------|--------|-------| -| Scheduler tick | 148Hz (PIT) | 1000Hz (LAPIC) | -| NVMe throughput | INTx shared | MSI-X 4+ queues | -| Context switch | ~6.75ms | ~1ms | -| Thread create | 3 IPC hops | 2 IPC hops | -| DMA safety | Unprotected | IOMMU-mapped | diff --git a/local/docs/IMPLEMENTATION-MASTER-PLAN.md b/local/docs/IMPLEMENTATION-MASTER-PLAN.md index 5405f60a66..e69de29bb2 100644 --- a/local/docs/IMPLEMENTATION-MASTER-PLAN.md +++ b/local/docs/IMPLEMENTATION-MASTER-PLAN.md @@ -1,385 +0,0 @@ -# Red Bear OS — Master Implementation Plan - -**Date**: 2026-05-04 -**Status**: Authoritative — supersedes CHANGELOG-DRIVER-IMPROVEMENT-PLAN.md, COMPREHENSIVE-DRIVER-AUDIT-2026-05-04.md, and HARDWARE-VALIDATION-MATRIX.md -**Source of truth**: Linux kernel 7.0 (`local/reference/linux-7.0/`) - ---- - -## 1. Authority & Scope - -### 1.1 Relationship to Existing Plans - -This plan is the **master execution document**. It delegates subsystem authority to specialized plans: - -| Plan | Subsystem | Relationship | -|------|-----------|-------------| -| `ACPI-IMPROVEMENT-PLAN.md` | ACPI sleep, thermal, EC, power | **Authoritative** for ACPI | -| `IRQ-AND-LOWLEVEL-CONTROLLERS-ENHANCEMENT-PLAN.md` | PCI IRQ, MSI-X, IOMMU, controllers | **Authoritative** for IRQ/PCI | -| `USB-IMPLEMENTATION-PLAN.md` | xHCI, EHCI, device lifecycle | **Authoritative** for USB | -| `DRM-MODERNIZATION-EXECUTION-PLAN.md` | GPU/DRM, KMS, Mesa | **Authoritative** for GPU | -| `BLUETOOTH-IMPLEMENTATION-PLAN.md` | BT host/controller | **Authoritative** for BT | -| `WIFI-IMPLEMENTATION-PLAN.md` | Wi-Fi control plane | **Authoritative** for Wi-Fi | -| `CONSOLE-TO-KDE-DESKTOP-PLAN.md` | Desktop/KDE path | **Authoritative** for desktop | - -**This master plan covers**: storage, network, audio, input drivers, cross-cutting quality, CPU/power, virtio, and kernel substrate (CPU/SMP/timers/DMA/memory). - -### 1.2 Validation Levels - -- **builds** — compiles without error -- **enumerates** — discovers hardware via scheme interfaces -- **usable** — works in bounded scenario (QEMU or bare metal) -- **validated** — passes explicit acceptance tests with evidence -- **hardware-validated** — proven on real bare metal - ---- - -## 2. Phase 0: Cross-Cutting Driver Quality (Week 1-2) ⏳ IMPLEMENTED - -### T0.1: Driver Error Handling ✅ - -**Status**: DONE. All 5 critical driver main.rs files have zero `unwrap()` calls. 165-line durable patch at `local/patches/base/P6-driver-main-fixes.patch`. - -**Files**: ahcid, e1000d, rtl8168d, ihdad, ac97d main.rs - -### T0.2: Driver Logging - -Not started. Drivers use inconsistent logging. - -### T0.3: Driver Lifecycle Documentation - -Not started. - ---- - -## 3. Phase 1: Storage Drivers (Week 2-6) ⏳ STRUCTURE EXISTING - -### T1.1: AHCI NCQ ✅ (71 lines, wired) - -**Status**: DONE. `ahci/src/ahci/ncq.rs` (71 lines) with tag alloc, FIS construction, completion processing, NCQ enable/issue. Wired via `pub mod ncq` in mod.rs. - -**Linux ref**: `drivers/ata/libata-sata.c` — `ata_qc_issue()` - -**Remaining work**: Wire into port interrupt handler, runtime test with QEMU AHCI + NCQ. - -### T1.2: AHCI Power Management ❌ - -**Linux ref**: `drivers/ata/libata-eh.c:3682` — `ata_eh_handle_port_suspend()` - -### T1.3: AHCI TRIM/Discard ❌ - -**Linux ref**: `drivers/ata/libata-scsi.c` — `ata_scsi_unmap_xlat()` - -### T1.4: NVMe Multiple Queues ❌ - -**Linux ref**: `drivers/nvme/host/pci.c` — `nvme_reset_work()` - ---- - -## 4. Phase 2: Network Drivers (Week 4-8) ⏳ STRUCTURE EXISTING - -### T2.1: e1000 ITR + Checksum ✅ (33 lines, wired) - -**Status**: DONE. `e1000d/src/itr.rs` (33 lines) with ITR state machine, set_itr, configure_default, enable_rx_checksum, enable_tso. Wired via `pub mod itr` in main.rs. - -**Linux ref**: `e1000e/netdev.c:4200` — `e1000_configure_itr()` - -### T2.2: e1000 TSO ❌ - -### T2.3: r8169 PHY ✅ (34 lines, wired) - -**Status**: DONE. `rtl8168d/src/phy.rs` (34 lines) with chip detection (12 variants), PHY registers, link detect, reset, autoneg + gigabit init. Wired via `pub mod phy` in main.rs. - -**Linux ref**: `r8169_phy_config.c` (1,354 lines) - -### T2.4: Jumbo Frames ❌ - ---- - -## 5. Phase 3: Audio Drivers (Week 6-10) ⏳ STRUCTURE EXISTING - -### T3.1: HDA Codec Detection ✅ (STRUCTURE) - -**Status**: DONE. `ihdad/src/hda/codec.rs` (18 lines) + `jack.rs` (4 lines). Both wired. 12 known codec table. Jack sense with pin config parsing. - -### T3.2: HDA Jack Detection ✅ (STRUCTURE) - -**Status**: `ihdad/src/hda/jack.rs` exists. Jack sense, unsolicited response. - -### T3.3: HDA Stream Setup - -Stream.rs exists (387 lines). NOT runtime-validated. - -### T3.4: AC97 Multiple Codec ❌ - ---- - -## 6. Phase 4: Input Drivers (Week 3-5) ⏳ PARTIAL - -### T4.1: PS/2 Controller Reset ❌ - -**Linux ref**: `drivers/input/serio/i8042.c:522` - -### T4.2: Touchpad Protocols ❌ - -**Linux ref**: `drivers/input/mouse/synaptics.c` - ---- - -## 7. Phase 5: Validation (Week 1-12, parallel) ⏳ IMPLEMENTED - -### T5.1: Test Harnesses ✅ - -`local/scripts/test-storage-qemu.sh` and `test-network-qemu.sh` exist. - -### T5.2: Hardware Validation Matrix ✅ - -`local/docs/HARDWARE-VALIDATION-MATRIX.md` — 28 lines tracking 18 components. - ---- - -## 8. Kernel Substrate (Addendum A findings) - -### K1: CPU / SMP / Timer (T0 priority) - -| Gap | Linux Ref | Lines | -|-----|-----------|-------| -| BSP/AP handoff | `arch/x86/kernel/smpboot.c:895` | 1,511 | -| CPU hotplug | `smpboot.c:1312` | — | -| TSC calibration | `arch/x86/kernel/tsc.c:1186` | 1,612 | -| APIC timer calibration | `arch/x86/kernel/apic/apic.c:294` | 2,694 | -| Vector allocation | `arch/x86/kernel/apic/vector.c` | 1,387 | -| MSI/MSI-X | `arch/x86/kernel/apic/msi.c` | 391 | ✅ DONE — P8-msi.patch (msi.rs, vector.rs, scheme/irq.rs, driver-sys) | - -### K2: DMA / IOMMU (Audited 2026-05-04) - -**Current State — Thorough Audit:** - -| Component | Location | Lines | Status | -|---|---|---|---| -| IOMMU scheme daemon | `local/recipes/system/iommu/source/src/lib.rs` | 1,003 | ✅ REAL — full AMD-Vi protocol: domain CRUD, MAP/UNMAP/TRANSLATE, device assignment, event drain, IRQ remapping. Host-runnable tests pass. | -| AMD-Vi unit driver | `local/recipes/system/iommu/source/src/amd_vi.rs` | 427 | ✅ REAL — IVRS parsing, MMIO mapping, device table programming, command buffer, event log, page table init | -| Domain page tables | `local/recipes/system/iommu/source/src/page_table.rs` | — | ✅ REAL — multi-level page table, IOVA allocation, mapping flags (R/W/X/coherent/user) | -| DMA buffer (alloc+phys) | `local/recipes/drivers/redox-driver-sys/source/src/dma.rs` | 261 | ✅ REAL — `DmaBuffer` with physically contiguous allocation via scheme:memory, virt-to-phys translation, heap fallback | -| linux-kpi DMA headers | `local/recipes/drivers/linux-kpi/source/` | — | ✅ dma-mapping.h, dma-direction.h, scatterlist.h ported | -| IOMMU←→driver wiring | — | — | ❌ **GAP** — `DmaBuffer` does NOT pass through IOMMU domains. GPU/NIC/NVMe drivers allocate DMA directly, not through IOMMU-isolated domains | -| Streaming DMA | — | — | ❌ **GAP** — no `dma_map_single`/`dma_unmap_single` for bounce-buffer ops | -| SWIOTLB | — | — | ❌ **GAP** — no bounce buffer for devices with limited DMA range | - -**Implementation Plan — DMA/IOMMU Integration (Week 3-5):** - -| Task | Description | Lines | Priority | -|---|---|---|---| -| **D2.1: IommuDmaAllocator** | New type in driver-sys: takes an IOMMU domain handle, allocates DmaBuffer through it. Uses `scheme:iommu/domain/N` MAP opcode. | ~150 | P0 | -| **D2.2: GPU DMA pass-through** | Wire `redox-drm` to use `IommuDmaAllocator` for GTT/VRAM allocations. Requires amdgpu/ihdgd to open IOMMU device handle. | ~80 | P0 | -| **D2.3: NVMe DMA pass-through** | Wire `ahcid`/`nvmed` PRP lists through `IommuDmaAllocator`. | ~60 | P1 | -| **D2.4: Streaming DMA** | `dma_map_single`/`dma_unmap_single` in linux-kpi. Allocates temp buffer, copies data, maps through IOMMU. | ~120 | P1 | -| **D2.5: SWIOTLB** | Bounce buffer allocation for DMA-limited devices. Linux ref: `kernel/dma/swiotlb.c`. | ~200 | P2 | - -**Linux Reference Summary (from `local/reference/linux-7.0/`):** - -| Linux API | Purpose | Red Bear Equivalent | -|---|---|---| -| `dma_alloc_coherent()` | Allocate physically contiguous, uncached DMA buffer | `DmaBuffer::allocate()` + `IommuDmaAllocator` (planned) | -| `dma_map_single()` | Map a single buffer for device DMA (cache sync) | Not yet — D2.4 | -| `dma_map_sg()` | Map scatter-gather list | Not yet | -| `iommu_domain_alloc()` | Create IOMMU translation domain | `IommuScheme` CREATE_DOMAIN opcode | -| `iommu_map()` | Map physical pages into domain | `IommuScheme` MAP opcode | -| `iommu_attach_device()` | Assign device to domain | `IommuScheme` ASSIGN_DEVICE opcode | - -### K2b: Thread Creation / fork() (Audited 2026-05-04) - -**Current State:** - -| Component | Location | Lines | Status | -|---|---|---|---| -| Kernel `context::spawn` | `recipes/core/kernel/source/src/context/mod.rs:217` | ~25 | ✅ Creates new context with NEW address space, kernel stack, initial call frame | -| `scheme:user` process spawn | `recipes/core/kernel/source/src/scheme/user.rs:723` | — | ✅ Userspace writes process params → kernel spawns | -| relibc `rlct_clone` | `recipes/core/relibc/source/src/platform/redox/mod.rs:1154` | ~10 | ✅ Thread creation via `redox_rt::thread::rlct_clone_impl` — lightweight: shares address space, TCB, signal state | -| `pthread_create` | `recipes/core/relibc/source/src/pthread/mod.rs:105` | ~100 | ✅ Allocates stack via mmap, creates TCB, calls rlct_clone | -| Thread stack allocation | mmap-based (line 130-143) | — | ✅ MAP_PRIVATE | MAP_ANONYMOUS, correct | - -**Gap Analysis:** - -| Gap | Severity | Detail | -|---|---|---| -| No `clone()` syscall | MEDIUM | Redox uses `rlct_clone` for threads and `scheme:user` for processes. This is architecturally correct for a microkernel — no gap. | -| No `CLONE_VM` flag | N/A | `rlct_clone` implicitly shares address space (it's a THREAD clone, not a process clone). Process creation via `scheme:user` creates new address space. Correct semantics. | -| No `CLONE_FILES` | N/A | File descriptors are shared via the `scheme:user` write protocol. Re-layout possible but functional. | -| "3 IPC hops" slower than Linux | LOW | Measured: 1) mmap stack, 2) rlct_clone syscall, 3) synchronization mutex unlock. Linux `clone()` does all three in kernel. Acceptable for a microkernel. | -| No `posix_spawn()` fast-path | MEDIUM | Currently goes through `fork`-equivalent → `exec`. Linux has `posix_spawn` via `vfork`+`exec`. Not yet in Redox. | - -**Overall verdict on DMA/IOMMU**: IOMMU daemon is the most complete userspace component — it needs wiring, not rewriting. DmaBuffer exists but is IOMMU-unaware. The implementation tasks (D2.1-D2.5) are wiring tasks connecting an already-working IOMMU to already-working driver allocators. - -### K3: Virtio - -| Gap | Linux Ref | Lines | -|-----|-----------|-------| -| Modern PCI transport | `drivers/virtio/virtio_pci_modern.c` | 1,301 | -| Packed virtqueue | `drivers/virtio/virtio_ring.c` | 3,940 | -| Multiqueue | `drivers/net/virtio_net.c` | 7,256 | - -### K4: CPU Frequency / Thermal - -| Component | Lines | Status | -|-----------|-------|--------| -| cpufreqd | 26 | STUB — needs MSR/governor implementation | -| thermald | 837 | REAL — needs trip points, fan control | - -### K5: Block Layer - -No shared block layer exists. Each storage driver reinvents I/O dispatch. Linux: `block/blk-mq.c` (5,309 lines). - ---- - -## 9. ACPI Gaps (delegated to ACPI-IMPROVEMENT-PLAN.md) - -| Linux File | Lines | Feature | Status | -|------------|-------|---------|--------| -| `drivers/acpi/sleep.c` | 1,152 | S3/S4 suspend | ❌ | -| `drivers/acpi/thermal.c` | 1,067 | Thermal zones | ❌ | -| `drivers/acpi/battery.c` | 1,331 | Battery status | ❌ | -| `drivers/acpi/ec.c` | 2,380 | EC runtime | ❌ | -| `drivers/acpi/fan.c` | ~400 | Fan control | ❌ | -| `arch/x86/kernel/acpi/sleep.c` | 202 | x86 sleep | ❌ | - ---- - -## 10. Execution Priority - -### Tier T0 — Kernel Substrate (CRITICAL — blocks all driver work) - -| Task | Files | Estimated | -|------|-------|-----------| -| MSI/MSI-X support | kernel apic + irq.rs | 4-6 weeks | -| TSC calibration | kernel time + tsc | 1-2 weeks | -| DMA API | kernel dma | 2-3 weeks | -| Virtio modern PCI | virtio-core transport | 2-3 weeks | -| cpufreqd (real impl) | local cpufreqd | 2-3 weeks | - -### Tier T1 — Storage + Network (HIGH) - -| Task | Files | Estimated | -|------|-------|-----------| -| AHCI NCQ runtime | ahci ncq.rs + main.rs | 2-3 weeks | -| AHCI PM + TRIM | ahci new module | 1-2 weeks | -| e1000 ITR runtime | e1000 itr.rs + device.rs | 1-2 weeks | -| r8169 PHY runtime | r8169 phy.rs + device.rs | 1-2 weeks | - -### Tier T2 — Audio + Input (MEDIUM) - -| Task | Files | Estimated | -|------|-------|-----------| -| HDA codec runtime | ihdad hda/codec.rs | 2-3 weeks | -| HDA stream playback | ihdad hda/stream.rs | 2-3 weeks | -| PS/2 controller reset | ps2d controller.rs | 3-5 days | -| Touchpad protocols | ps2d mouse.rs | 1-2 weeks | - -### Tier T3 — Completeness (LOW) - -| Task | Files | Estimated | -|------|-------|-----------| -| NVMe multi-queue | nvmed | 2-3 weeks | -| e1000 TSO | e1000 | 1-2 weeks | -| Jumbo frames | e1000 + r8169 | 3-5 days | -| AC97 multi-codec | ac97d | 1 week | - ---- - -## 11. Hardware Validation Matrix - -| Component | QEMU | Bare Metal | Status | -|-----------|------|------------|--------| -| AHCI SATA | ✅ | 🔲 | NCQ structure present | -| NVMe | 🔲 | 🔲 | Basic driver | -| virtio-blk | ✅ | N/A | QEMU only | -| e1000 | 🔲 | 🔲 | ITR structure present | -| rtl8168 | 🔲 | 🔲 | PHY config present | -| virtio-net | ✅ | N/A | QEMU only | -| Intel HDA | 🔲 | 🔲 | Codec+jack added | -| AC97 | 🔲 | 🔲 | Basic driver | -| PS/2 | ✅ | 🔲 | QEMU works | -| VESA | ✅ | 🔲 | QEMU FB works | -| virtio-gpu | ✅ | N/A | 2D only | -| cpufreqd | 🔲 | 🔲 | STUB (26 lines) | -| thermald | 🔲 | 🔲 | ACPI thermal | -| x2APIC/SMP | ✅ | ✅ | Multi-core works | - ---- - -## 12. File Inventory - -### Patches (durable) - -| Patch | Lines | Recipe | Status | -|-------|-------|--------|--------| -| `local/patches/relibc/P5-named-semaphores.patch` | 249 | relibc | ✅ Wired | -| `local/patches/base/P6-driver-main-fixes.patch` | 165 | base | ✅ Wired | -| `local/patches/base/P6-driver-new-modules.patch` | 185 | base | ✅ Wired | -| `local/patches/base/P6-cpufreqd-real-impl.patch` | 177 | — | 🔲 Not wired | - -### New Source Files - -| File | Lines | Phase | Status | -|------|-------|-------|--------| -| `ahcid/src/ahci/ncq.rs` | 12 | Phase 1 | ⚠️ Truncated | -| `e1000d/src/itr.rs` | 9 | Phase 2 | ⚠️ Truncated | -| `rtl8168d/src/phy.rs` | 5 | Phase 2 | ⚠️ Truncated | -| `ihdad/src/hda/codec.rs` | 4 | Phase 3 | ⚠️ Truncated | -| `ihdad/src/hda/jack.rs` | 5 | Phase 3 | ⚠️ Truncated | -| `cpufreqd/src/main.rs` | 26 | Kernel | ❌ STUB | - -### Scripts - -| Script | Phase | Status | -|--------|-------|--------| -| `local/scripts/test-storage-qemu.sh` | Phase 5 | ✅ | -| `local/scripts/test-network-qemu.sh` | Phase 5 | ✅ | -| `local/scripts/lint-config-paths.sh` | Phase 0 | ✅ | -| `local/scripts/validate-init-services.sh` | Phase 0 | ✅ | -| `local/scripts/validate-file-ownership.sh` | Phase 0 | ✅ | -| `local/scripts/generate-installs-manifest.sh` | Phase 0 | ✅ | - -### Documentation - -| Document | Lines | Status | -|----------|-------|--------| -| `IMPLEMENTATION-MASTER-PLAN.md` | — | This file | -| `CHANGELOG-DRIVER-IMPROVEMENT-PLAN.md` | 672 | Superseded | -| `COMPREHENSIVE-DRIVER-AUDIT-2026-05-04.md` | 316 | Superseded | -| `HARDWARE-VALIDATION-MATRIX.md` | 28 | Superseded | -| `BUILD-SYSTEM-HARDENING-PLAN.md` | 403 | Active | -| `BUILD-SYSTEM-INVARIANTS.md` | 436 | Active | -| `ACPI-IMPROVEMENT-PLAN.md` | 839 | Active | -| `IRQ-AND-LOWLEVEL-CONTROLLERS-ENHANCEMENT-PLAN.md` | 916 | Active | - ---- - -## 14. Scheduler & Threading Assessment (2026-05-04) - -### Architecture -- **Kernel**: DWRR scheduler (577 lines), 40 priority levels, per-CPU queues, futex (222 lines) -- **Userspace**: proc manager (2,638 lines), pthread (440 lines), signal delivery via proc scheme -- **IPC bridge**: 3 round-trips for thread creation vs Linux's single clone() syscall - -### Strengths -- DWRR with geometric weights, CPU affinity masks, soft-blocking with monotonic timeout -- Full POSIX process model (PID/PGID/SID, job control, orphan detection) -- Futex with physical-address keys for cross-process synchronization - -### Critical Gaps -1. **PIT-based tick (~148Hz)** — LAPIC timer exists but `setup_timer()` is commented out. Should use Periodic/TscDeadline mode at 1000Hz. -2. **Global CONTEXT_SWITCH_LOCK** — spinlock serializes all context switches across CPUs. Should be per-CPU. -3. **No load balancing** — idle CPUs don't steal work from busy CPUs -4. **No RT scheduling** — missing FIFO/RR/Deadline classes -5. **No cgroups** — no CPU bandwidth control or resource limits -6. **Thread creation latency** — 3 IPC hops vs single clone() - -| Tier | Duration | -|------|----------| -| T0 (kernel substrate) | 10-14 weeks | -| T1 (storage + network) | 6-10 weeks | -| T2 (audio + input) | 6-10 weeks | -| T3 (completeness) | 4-8 weeks | -| **Total (2 developers, parallel)** | **16-24 weeks** | -| **Total (1 developer, sequential)** | **26-42 weeks** | diff --git a/local/patches/base/P0-daemon-fix-init-notify-unwrap.patch b/local/patches/base/P0-daemon-fix-init-notify-unwrap.patch index 6ff51f14e3..c7db8b617f 100644 --- a/local/patches/base/P0-daemon-fix-init-notify-unwrap.patch +++ b/local/patches/base/P0-daemon-fix-init-notify-unwrap.patch @@ -2,7 +2,7 @@ diff --git a/daemon/src/lib.rs b/daemon/src/lib.rs index 9f507221..c69c2cfa 100644 --- a/daemon/src/lib.rs +++ b/daemon/src/lib.rs -@@ -10,15 +10,26 @@ use libredox::Fd; +@@ -10,15 +10,25 @@ use libredox::Fd; use redox_scheme::Socket; use redox_scheme::scheme::{SchemeAsync, SchemeSync}; @@ -10,7 +10,6 @@ index 9f507221..c69c2cfa 100644 - let fd: RawFd = std::env::var(var).unwrap().parse().unwrap(); +unsafe fn get_fd(var: &str) -> Option { + let fd: RawFd = match std::env::var(var) -+ .map_err(|e| eprintln!("daemon: env var {var} not set: {e}")) + .ok() + .and_then(|val| { + val.parse() @@ -33,7 +32,7 @@ index 9f507221..c69c2cfa 100644 } unsafe fn pass_fd(cmd: &mut Command, env: &str, fd: OwnedFd) { -@@ -38,20 +49,26 @@ unsafe fn pass_fd(cmd: &mut Command, env: &str, fd: OwnedFd) { +@@ -38,20 +48,26 @@ unsafe fn pass_fd(cmd: &mut Command, env: &str, fd: OwnedFd) { /// A long running background process that handles requests. #[must_use = "Daemon::ready must be called"] pub struct Daemon { @@ -63,7 +62,7 @@ index 9f507221..c69c2cfa 100644 } /// Executes `Command` as a child process. -@@ -83,25 +100,28 @@ impl Daemon { +@@ -83,25 +99,28 @@ impl Daemon { /// A long running background process that handles requests using schemes. #[must_use = "SchemeDaemon::ready must be called"] pub struct SchemeDaemon { diff --git a/local/patches/base/P57-fbbootlogd-graceful-init.patch b/local/patches/base/P57-fbbootlogd-graceful-init.patch new file mode 100644 index 0000000000..4f8eafb44b --- /dev/null +++ b/local/patches/base/P57-fbbootlogd-graceful-init.patch @@ -0,0 +1,184 @@ +diff --git a/drivers/graphics/fbbootlogd/src/main.rs b/drivers/graphics/fbbootlogd/src/main.rs +index 3e42d590..79c2119f 100644 +--- a/drivers/graphics/fbbootlogd/src/main.rs ++++ b/drivers/graphics/fbbootlogd/src/main.rs +@@ -46,13 +46,17 @@ fn daemon(daemon: daemon::SchemeDaemon) -> ! { + ) + .expect("fbbootlogd: failed to subscribe to scheme events"); + +- event_queue +- .subscribe( +- scheme.input_handle.event_handle().as_raw_fd() as usize, +- Source::Input, +- event::EventFlags::READ, +- ) +- .expect("fbbootlogd: failed to subscribe to scheme events"); ++ if let Some(ref input_handle) = scheme.input_handle { ++ event_queue ++ .subscribe( ++ input_handle.event_handle().as_raw_fd() as usize, ++ Source::Input, ++ event::EventFlags::READ, ++ ) ++ .expect("fbbootlogd: failed to subscribe to input events"); ++ } else { ++ eprintln!("fbbootlogd: running without input handle (log-only mode)"); ++ } + + { + let log_fd = socket +@@ -76,6 +80,11 @@ fn daemon(daemon: daemon::SchemeDaemon) -> ! { + // driver handoff. In the future inputd may directly pass a handle to the display instead. + //libredox::call::setrens(0, 0).expect("fbbootlogd: failed to enter null namespace"); + ++ enum Action { ++ Input(Event), ++ Handoff, ++ } ++ + for event in event_queue { + match event.expect("fbbootlogd: failed to get event").user_data { + Source::Scheme => loop { +@@ -88,20 +97,31 @@ fn daemon(daemon: daemon::SchemeDaemon) -> ! { + } + }, + Source::Input => { +- let mut events = [Event::new(); 16]; +- loop { +- match scheme +- .input_handle +- .read_events(&mut events) +- .expect("fbbootlogd: error while reading events") +- { +- ConsumerHandleEvent::Events(&[]) => break, +- ConsumerHandleEvent::Events(events) => { +- for event in events { +- scheme.handle_input(&event); ++ let mut actions: Vec = Vec::new(); ++ if let Some(ref mut input_handle) = scheme.input_handle { ++ let mut events = [Event::new(); 16]; ++ loop { ++ match input_handle ++ .read_events(&mut events) ++ .expect("fbbootlogd: error while reading events") ++ { ++ ConsumerHandleEvent::Events(&[]) => break, ++ ConsumerHandleEvent::Events(events) => { ++ for event in events { ++ actions.push(Action::Input(*event)); ++ } ++ } ++ ConsumerHandleEvent::Handoff => { ++ actions.push(Action::Handoff); ++ break; + } + } +- ConsumerHandleEvent::Handoff => { ++ } ++ } ++ for action in actions { ++ match action { ++ Action::Input(event) => scheme.handle_input(&event), ++ Action::Handoff => { + eprintln!("fbbootlogd: handoff requested"); + scheme.handle_handoff(); + } +diff --git a/drivers/graphics/fbbootlogd/src/scheme.rs b/drivers/graphics/fbbootlogd/src/scheme.rs +index 812c4a5b..53e4bc75 100644 +--- a/drivers/graphics/fbbootlogd/src/scheme.rs ++++ b/drivers/graphics/fbbootlogd/src/scheme.rs +@@ -14,7 +14,7 @@ use syscall::schemev2::NewFdFlags; + use syscall::{Error, Result, EACCES, EBADF, EINVAL, ENOENT}; + + pub struct FbbootlogScheme { +- pub input_handle: ConsumerHandle, ++ pub input_handle: Option, + display_map: Option, + text_screen: console_draw::TextScreen, + text_buffer: console_draw::TextBuffer, +@@ -25,8 +25,16 @@ pub struct FbbootlogScheme { + + impl FbbootlogScheme { + pub fn new() -> FbbootlogScheme { ++ let input_handle = match ConsumerHandle::bootlog_vt() { ++ Ok(handle) => Some(handle), ++ Err(err) => { ++ eprintln!("fbbootlogd: Failed to open vt (non-fatal): {err}"); ++ None ++ } ++ }; ++ + let mut scheme = FbbootlogScheme { +- input_handle: ConsumerHandle::bootlog_vt().expect("fbbootlogd: Failed to open vt"), ++ input_handle, + display_map: None, + text_screen: console_draw::TextScreen::new(), + text_buffer: console_draw::TextBuffer::new(1000), +@@ -41,8 +49,19 @@ impl FbbootlogScheme { + } + + pub fn handle_handoff(&mut self) { +- let new_display_handle = match self.input_handle.open_display_v2() { +- Ok(display) => V2GraphicsHandle::from_file(display).unwrap(), ++ let Some(ref input_handle) = self.input_handle else { ++ eprintln!("fbbootlogd: No input handle, skipping display handoff"); ++ return; ++ }; ++ ++ let new_display_handle = match input_handle.open_display_v2() { ++ Ok(display) => match V2GraphicsHandle::from_file(display) { ++ Ok(handle) => handle, ++ Err(err) => { ++ eprintln!("fbbootlogd: Display v2 protocol not supported: {err}"); ++ return; ++ } ++ }, + Err(err) => { + eprintln!("fbbootlogd: No display present yet: {err}"); + return; +diff --git a/drivers/graphics/fbcond/src/display.rs b/drivers/graphics/fbcond/src/display.rs +index eb09b97e..4e347475 100644 +--- a/drivers/graphics/fbcond/src/display.rs ++++ b/drivers/graphics/fbcond/src/display.rs +@@ -31,7 +31,13 @@ impl Display { + return; + } + }; +- let new_display_handle = V2GraphicsHandle::from_file(display_file).unwrap(); ++ let new_display_handle = match V2GraphicsHandle::from_file(display_file) { ++ Ok(handle) => handle, ++ Err(err) => { ++ log::error!("fbcond: Display v2 protocol not supported: {err}"); ++ return; ++ } ++ }; + + log::debug!("fbcond: Opened new display"); + +diff --git a/drivers/inputd/src/lib.rs b/drivers/inputd/src/lib.rs +index b68e8211..b3e8354c 100644 +--- a/drivers/inputd/src/lib.rs ++++ b/drivers/inputd/src/lib.rs +@@ -77,14 +77,14 @@ impl ConsumerHandle { + )); + let display_path = display_path.to_str().unwrap(); + +- let display_file = +- libredox::call::open(display_path, (O_CLOEXEC | O_NONBLOCK | O_RDWR) as _, 0) +- .map(|socket| unsafe { File::from_raw_fd(socket as RawFd) }) +- .unwrap_or_else(|err| { +- panic!("failed to open display {}: {}", display_path, err); +- }); +- +- Ok(display_file) ++ libredox::call::open(display_path, (O_CLOEXEC | O_NONBLOCK | O_RDWR) as _, 0) ++ .map(|socket| unsafe { File::from_raw_fd(socket as RawFd) }) ++ .map_err(|err| { ++ io::Error::new( ++ io::ErrorKind::Other, ++ format!("failed to open display {}: {}", display_path, err), ++ ) ++ }) + } + + pub fn read_events<'a>(&self, events: &'a mut [Event]) -> io::Result> { diff --git a/local/patches/base/P6-lived-block-size-512.patch b/local/patches/base/P6-lived-block-size-512.patch new file mode 100644 index 0000000000..a96a041445 --- /dev/null +++ b/local/patches/base/P6-lived-block-size-512.patch @@ -0,0 +1,65 @@ +diff --git a/drivers/storage/lived/src/main.rs b/drivers/storage/lived/src/main.rs +index 2ca1ff27..cd92fa85 100644 +--- a/drivers/storage/lived/src/main.rs ++++ b/drivers/storage/lived/src/main.rs +@@ -55,8 +55,10 @@ impl LiveDisk { + } + + impl Disk for LiveDisk { ++ // Must be 512 (redoxfs BLOCK_SIZE), not PAGE_SIZE: DiskWrapper::read rejects ++ // buffers not aligned to block_size, and redoxfs reads in 512-byte chunks. + fn block_size(&self) -> u32 { +- PAGE_SIZE as u32 ++ 512 + } + + fn size(&self) -> u64 { +@@ -64,11 +66,12 @@ impl Disk for LiveDisk { + } + + async fn read(&mut self, mut block: u64, buffer: &mut [u8]) -> syscall::Result { +- let mut offset = (block as usize) * PAGE_SIZE; ++ let bs = self.block_size() as usize; ++ let mut offset = (block as usize) * bs; + if offset + buffer.len() > self.original.len() { + return Err(syscall::Error::new(EINVAL)); + } +- for chunk in buffer.chunks_mut(PAGE_SIZE) { ++ for chunk in buffer.chunks_mut(bs) { + match self.overlay.get(&block) { + Some(overlay) => { + chunk.copy_from_slice(&overlay[..chunk.len()]); +@@ -78,26 +81,27 @@ impl Disk for LiveDisk { + } + } + block += 1; +- offset += PAGE_SIZE; ++ offset += bs; + } + Ok(buffer.len()) + } + + async fn write(&mut self, mut block: u64, buffer: &[u8]) -> syscall::Result { +- let mut offset = (block as usize) * PAGE_SIZE; ++ let bs = self.block_size() as usize; ++ let mut offset = (block as usize) * bs; + if offset + buffer.len() > self.original.len() { + return Err(syscall::Error::new(EINVAL)); + } +- for chunk in buffer.chunks(PAGE_SIZE) { ++ for chunk in buffer.chunks(bs) { + self.overlay.entry(block).or_insert_with(|| { +- let offset = (block as usize) * PAGE_SIZE; +- self.original[offset..offset + PAGE_SIZE] ++ let offset = (block as usize) * bs; ++ self.original[offset..offset + bs] + .to_vec() + .into_boxed_slice() + })[..chunk.len()] + .copy_from_slice(chunk); + block += 1; +- offset += PAGE_SIZE; ++ offset += bs; + } + Ok(buffer.len()) + } diff --git a/local/patches/relibc/P3-sys-types-stdint-include.patch b/local/patches/relibc/P3-sys-types-stdint-include.patch index 11bc36b48b..7291ca51ec 100644 --- a/local/patches/relibc/P3-sys-types-stdint-include.patch +++ b/local/patches/relibc/P3-sys-types-stdint-include.patch @@ -1,8 +1,8 @@ --- a/src/header/sys_types_internal/cbindgen.toml +++ b/src/header/sys_types_internal/cbindgen.toml @@ -1,4 +1,4 @@ --sys_includes = ["stddef.h"] -+sys_includes = ["stddef.h", "stdint.h"] +-sys_includes = ["stddef.h", "stdint.h"] ++sys_includes = ["stddef.h"] # TODO: figure out how to export void* type after_includes = """ diff --git a/local/recipes/drivers/redox-driver-core/source/src/manager.rs b/local/recipes/drivers/redox-driver-core/source/src/manager.rs index 70b017906a..4e5eee9a74 100644 --- a/local/recipes/drivers/redox-driver-core/source/src/manager.rs +++ b/local/recipes/drivers/redox-driver-core/source/src/manager.rs @@ -397,6 +397,7 @@ mod tests { description: "low-priority driver", priority: 10, matches: vec![DriverMatch { + bus: None, vendor: Some(0x1234), device: None, class: None, @@ -413,6 +414,7 @@ mod tests { description: "high-priority driver", priority: 100, matches: vec![DriverMatch { + bus: None, vendor: Some(0x1234), device: Some(0x5678), class: None, @@ -496,6 +498,7 @@ mod tests { description: "USB host controller", priority: 80, matches: vec![DriverMatch { + bus: None, vendor: Some(0x8086), device: None, class: Some(0x0c), diff --git a/local/recipes/drivers/redox-driver-core/source/src/match.rs b/local/recipes/drivers/redox-driver-core/source/src/match.rs index 8e795d1272..1ad3c21bed 100644 --- a/local/recipes/drivers/redox-driver-core/source/src/match.rs +++ b/local/recipes/drivers/redox-driver-core/source/src/match.rs @@ -8,6 +8,11 @@ pub type MatchPriority = i32; /// A single entry in a driver's match table. #[derive(Clone, Debug, PartialEq, Eq, Default)] pub struct DriverMatch { + /// Optional bus type match (e.g., "pci", "acpi"). + /// + /// When set, only devices on the specified bus will match. + /// When `None`, the match applies to any bus (backward compatible). + pub bus: Option, /// Optional vendor identifier match. pub vendor: Option, /// Optional device identifier match. @@ -27,7 +32,8 @@ pub struct DriverMatch { impl DriverMatch { /// Checks whether this match entry matches the provided device information. pub fn matches(&self, info: &DeviceInfo) -> bool { - self.vendor.map_or(true, |v| info.vendor == Some(v)) + self.bus.as_ref().map_or(true, |b| &info.id.bus == b) + && self.vendor.map_or(true, |v| info.vendor == Some(v)) && self.device.map_or(true, |d| info.device == Some(d)) && self.class.map_or(true, |c| info.class == Some(c)) && self.subclass.map_or(true, |s| info.subclass == Some(s)) @@ -100,6 +106,7 @@ mod tests { fn driver_match_accepts_exact_match() { let info = sample_device(); let driver_match = DriverMatch { + bus: None, vendor: Some(0x8086), device: Some(0x1234), class: Some(0x03), @@ -116,6 +123,7 @@ mod tests { fn driver_match_supports_wildcards() { let info = sample_device(); let driver_match = DriverMatch { + bus: None, vendor: Some(0x8086), device: None, class: Some(0x03), @@ -132,6 +140,7 @@ mod tests { fn driver_match_rejects_mismatch() { let info = sample_device(); let driver_match = DriverMatch { + bus: None, vendor: Some(0x10ec), device: None, class: None, @@ -143,4 +152,48 @@ mod tests { assert!(!driver_match.matches(&info)); } + + #[test] + fn driver_match_bus_filtering() { + let info = sample_device(); + + // Matching bus should pass + let pci_match = DriverMatch { + bus: Some(String::from("pci")), + vendor: Some(0x8086), + device: None, + class: None, + subclass: None, + prog_if: None, + subsystem_vendor: None, + subsystem_device: None, + }; + assert!(pci_match.matches(&info)); + + // Non-matching bus should fail + let acpi_match = DriverMatch { + bus: Some(String::from("acpi")), + vendor: Some(0x8086), + device: None, + class: None, + subclass: None, + prog_if: None, + subsystem_vendor: None, + subsystem_device: None, + }; + assert!(!acpi_match.matches(&info)); + + // None bus should match any device (backward compatible) + let any_bus = DriverMatch { + bus: None, + vendor: Some(0x8086), + device: None, + class: None, + subclass: None, + prog_if: None, + subsystem_vendor: None, + subsystem_device: None, + }; + assert!(any_bus.matches(&info)); + } } diff --git a/local/recipes/drivers/redox-driver-sys/source/src/irq.rs b/local/recipes/drivers/redox-driver-sys/source/src/irq.rs index 2ff9d2de27..18b72ffc86 100644 --- a/local/recipes/drivers/redox-driver-sys/source/src/irq.rs +++ b/local/recipes/drivers/redox-driver-sys/source/src/irq.rs @@ -291,12 +291,21 @@ fn read_cpu_count() -> Result { #[cfg(target_os = "redox")] fn alloc_cpu_id() -> u8 { match read_cpu_count() { - Ok(n) if n > 0 => { + Ok(0) => { + log::warn!("redox-driver-sys: read_cpu_count returned 0, defaulting to BSP (cpu 0)"); + 0 + } + Ok(n) => { use std::sync::atomic::{AtomicU8, Ordering}; static NEXT: AtomicU8 = AtomicU8::new(0); - NEXT.fetch_add(1, Ordering::Relaxed) % n + let cpu_id = NEXT.fetch_add(1, Ordering::Relaxed) % n; + log::debug!("redox-driver-sys: alloc_cpu_id selected cpu {} (of {})", cpu_id, n); + cpu_id + } + Err(err) => { + log::warn!("redox-driver-sys: read_cpu_count failed ({}), defaulting to BSP (cpu 0)", err); + 0 } - _ => 0, } } diff --git a/local/recipes/gpu/redox-drm/source/daemon/src/lib.rs b/local/recipes/gpu/redox-drm/source/daemon/src/lib.rs index aa54905a16..c1fe30e652 100644 --- a/local/recipes/gpu/redox-drm/source/daemon/src/lib.rs +++ b/local/recipes/gpu/redox-drm/source/daemon/src/lib.rs @@ -11,12 +11,16 @@ use redox_scheme::Socket; use redox_scheme::scheme::{SchemeAsync, SchemeSync}; unsafe fn get_fd(var: &str) -> Option { + // Env vars like INIT_NOTIFY are optional — daemons not spawned by init + // simply don't have them. Return None silently instead of spewing errors. let fd: RawFd = match std::env::var(var) - .map_err(|e| eprintln!("daemon: env var {var} not set: {e}")) .ok() .and_then(|val| { - val.parse() - .map_err(|e| eprintln!("daemon: failed to parse {var} as fd: {e}")) + val.parse::() + .map_err(|e| { + eprintln!("daemon: failed to parse {var} as fd: {e}"); + e + }) .ok() }) { Some(fd) => fd, diff --git a/local/recipes/gpu/redox-drm/source/src/kms/connector.rs b/local/recipes/gpu/redox-drm/source/src/kms/connector.rs index 39e6949544..65843dbdda 100644 --- a/local/recipes/gpu/redox-drm/source/src/kms/connector.rs +++ b/local/recipes/gpu/redox-drm/source/src/kms/connector.rs @@ -123,45 +123,3 @@ mod tests { assert_eq!(&edid[0..8], &header, "EDID header should be valid"); } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn synthetic_displayport_has_correct_fields() { - let conn = Connector::synthetic_displayport(5, 10); - assert_eq!(conn.info.id, 5); - assert_eq!(conn.info.encoder_id, 10); - assert_eq!(conn.info.connector_type, ConnectorType::DisplayPort); - assert_eq!(conn.info.connection, ConnectorStatus::Connected); - assert!( - !conn.info.modes.is_empty(), - "synthetic DisplayPort should have modes" - ); - } - - #[test] - fn synthetic_displayport_modes_have_valid_dimensions() { - let conn = Connector::synthetic_displayport(1, 1); - for mode in &conn.info.modes { - assert!(mode.hdisplay > 0, "mode hdisplay should be > 0"); - assert!(mode.vdisplay > 0, "mode vdisplay should be > 0"); - assert!(mode.vrefresh > 0, "mode vrefresh should be > 0"); - assert!(mode.clock > 0, "mode clock should be > 0"); - } - } - - #[test] - fn synthetic_edid_returns_exactly_112_bytes() { - let edid = synthetic_edid(); - assert_eq!(edid.len(), 112); - } - - #[test] - fn synthetic_edid_has_valid_header() { - let edid = synthetic_edid(); - let header: [u8; 8] = [0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00]; - assert_eq!(&edid[0..8], &header, "EDID header should be valid"); - } -} diff --git a/local/recipes/gpu/redox-drm/source/src/kms/mod.rs b/local/recipes/gpu/redox-drm/source/src/kms/mod.rs index bc0bc40679..7d559459a3 100644 --- a/local/recipes/gpu/redox-drm/source/src/kms/mod.rs +++ b/local/recipes/gpu/redox-drm/source/src/kms/mod.rs @@ -198,14 +198,15 @@ mod tests { } #[test] - fn from_edid_synthetic_edid_too_short_returns_empty() { + fn from_edid_synthetic_edid_parses_1080p_mode() { let edid = super::connector::synthetic_edid(); - assert!(edid.len() < 128, "synthetic EDID is shorter than 128 bytes"); + assert_eq!(edid.len(), 128, "synthetic EDID must be 128 bytes"); let modes = ModeInfo::from_edid(&edid); - assert!( - modes.is_empty(), - "EDID shorter than 128 bytes should produce no modes" - ); + assert!(!modes.is_empty(), "valid 128-byte EDID should produce at least one mode"); + let mode = &modes[0]; + assert_eq!(mode.hdisplay, 1920, "first mode should be 1920px wide"); + assert_eq!(mode.vdisplay, 1080, "first mode should be 1080px tall"); + assert_eq!(mode.vrefresh, 60, "first mode should be 60 Hz"); } #[test] diff --git a/local/recipes/libs/glib/source/LICENSES/LGPL-2.1-or-later.txt b/local/recipes/libs/glib/source/LICENSES/LGPL-2.1-or-later.txt index c9aa53018e..ddf687147f 100644 --- a/local/recipes/libs/glib/source/LICENSES/LGPL-2.1-or-later.txt +++ b/local/recipes/libs/glib/source/LICENSES/LGPL-2.1-or-later.txt @@ -1,175 +1 @@ -GNU LESSER GENERAL PUBLIC LICENSE - -Version 2.1, February 1999 - -Copyright (C) 1991, 1999 Free Software Foundation, Inc. -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. - -[This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] - -Preamble - -The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. - -This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. - -When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. - -To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. - -For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. - -We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. - -To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. - -Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. - -Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. - -When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. - -We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. - -For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. - -In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. - -Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. - -The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. - -TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - -0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". - -A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. - -The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) - -"Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. - -Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. - -1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. - -You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. - -2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: - - a) The modified work must itself be a software library. - - b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. - - c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. - - d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. - -(For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) - -These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. - -In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. - -3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. - -Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. - -This option is useful when you wish to copy part of the code of the Library into a program that is not a library. - -4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. - -If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. - -5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. - -However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. - -When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. - -If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) - -Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. - -6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. - -You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: - - a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) - - b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. - - c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. - - d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. - - e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. - -For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. - -It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. - -7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: - - a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. - - b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. - -8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. - -9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. - -10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. - -11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. - -If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. - -It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. - -This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. - -12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. - -13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. - -Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. - -14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. - -NO WARRANTY - -15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - -16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - -END OF TERMS AND CONDITIONS - -How to Apply These Terms to Your New Libraries - -If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). - -To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. - - one line to give the library's name and an idea of what it does. - Copyright (C) year name of author - - This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. - -You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: - -Yoyodyne, Inc., hereby disclaims all copyright interest in -the library `Frob' (a library for tweaking knobs) written -by James Random Hacker. - -signature of Ty Coon, 1 April 1990 -Ty Coon, President of Vice -That's all there is to it! +../LICENSES/LGPL-2.1-or-later.txt \ No newline at end of file diff --git a/local/recipes/system/cpufreqd/source/src/main.rs b/local/recipes/system/cpufreqd/source/src/main.rs index eeba7f14bb..94824ba937 100644 --- a/local/recipes/system/cpufreqd/source/src/main.rs +++ b/local/recipes/system/cpufreqd/source/src/main.rs @@ -58,8 +58,12 @@ fn read_acpi_pss(cpu: u32) -> Vec { } fn write_msr(cpu: u32, msr: u32, val: u64) -> bool { - fs::OpenOptions::new().write(true).open(format!("/dev/cpu/{}/msr", cpu)).ok() - .map(|mut f| f.write_all(&val.to_ne_bytes()).is_ok()).unwrap_or(false) + let path = format!("/scheme/sys/msr/{}/{:x}", cpu, msr); + fs::OpenOptions::new().write(true).open(&path).ok() + .and_then(|mut f| { + let hex_val = format!("{:016x}", val); + f.write_all(hex_val.as_bytes()).ok() + }).is_some() } fn measure_load(cpu: u32, prev: &mut (u64, u64)) -> f64 { diff --git a/local/recipes/system/driver-manager/Cargo.toml b/local/recipes/system/driver-manager/Cargo.toml index b3233faaff..bbd4ae5197 100644 --- a/local/recipes/system/driver-manager/Cargo.toml +++ b/local/recipes/system/driver-manager/Cargo.toml @@ -11,6 +11,7 @@ path = "src/main.rs" [dependencies] redox-driver-core = { path = "../../drivers/redox-driver-core" } redox-driver-pci = { path = "../../drivers/redox-driver-pci" } +redox-driver-acpi = { path = "../../drivers/redox-driver-acpi" } pcid_interface = { path = "../../../../recipes/core/base/source/drivers/pcid", package = "pcid" } redox_syscall = "0.7" log = "0.4" diff --git a/local/recipes/system/driver-manager/recipe.toml b/local/recipes/system/driver-manager/recipe.toml index 4e47e6bb51..33011bc243 100644 --- a/local/recipes/system/driver-manager/recipe.toml +++ b/local/recipes/system/driver-manager/recipe.toml @@ -2,4 +2,20 @@ path = "source" [build] -template = "cargo" +template = "custom" +script = """ +# driver-manager runs in both rootfs and initfs; initfs has no dynamic linker, +# so we must build a statically linked binary. +export RUSTFLAGS="${RUSTFLAGS} -Ctarget-feature=+crt-static -L native=${COOKBOOK_SYSROOT}/lib" +"${COOKBOOK_CARGO}" build \ + --manifest-path "${COOKBOOK_SOURCE}/Cargo.toml" \ + --target "${TARGET}" \ + ${build_flags} +mkdir -pv "${COOKBOOK_STAGE}/usr/bin" +cp -v "target/${TARGET}/${build_type}/driver-manager" "${COOKBOOK_STAGE}/usr/bin/driver-manager" +""" + +[dependencies] +redox-driver-core = {} +redox-driver-pci = {} +redox-driver-acpi = {} diff --git a/local/recipes/system/driver-manager/source/Cargo.toml b/local/recipes/system/driver-manager/source/Cargo.toml index 858fc6823a..3b63b895a5 100644 --- a/local/recipes/system/driver-manager/source/Cargo.toml +++ b/local/recipes/system/driver-manager/source/Cargo.toml @@ -11,9 +11,11 @@ path = "src/main.rs" [dependencies] redox-driver-core = { path = "../../../drivers/redox-driver-core/source" } redox-driver-pci = { path = "../../../drivers/redox-driver-pci/source" } +redox-driver-acpi = { path = "../../../drivers/redox-driver-acpi/source" } pcid_interface = { path = "../../../../../recipes/core/base/source/drivers/pcid", package = "pcid" } redox-scheme = "0.11" syscall = { package = "redox_syscall", version = "0.7" } log = "0.4" toml = "0.8" serde = { version = "1", features = ["derive"] } +libc = "0.2" diff --git a/local/recipes/system/driver-manager/source/src/config.rs b/local/recipes/system/driver-manager/source/src/config.rs index 37ddc2cef1..83bcb52a7b 100644 --- a/local/recipes/system/driver-manager/source/src/config.rs +++ b/local/recipes/system/driver-manager/source/src/config.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::collections::BTreeSet; use std::fs; use std::os::fd::{AsRawFd, FromRawFd, OwnedFd}; use std::path::Path; @@ -8,11 +9,18 @@ use std::sync::Mutex; use std::vec::Vec; use pcid_interface::PciFunctionHandle; +use redox_driver_acpi::AcpiBus; use redox_driver_core::device::DeviceInfo; use redox_driver_core::driver::{Driver, DriverError, ProbeResult}; use redox_driver_core::r#match::DriverMatch; use redox_driver_core::params::{DriverParams, ParamValue}; +// Device+driver pairs that should never be re-probed because the driver +// binary is absent (Fatal), the driver declined the device (NotSupported), +// or deferred retries were exhausted. Checked by probe() before any work. +pub(crate) static PERMANENTLY_SKIPPED: Mutex> = + Mutex::new(BTreeSet::new()); + use serde::Deserialize; #[derive(Debug)] @@ -48,6 +56,7 @@ impl Clone for DriverConfig { #[derive(Deserialize)] struct RawDriverMatch { + bus: Option, vendor: Option, device: Option, class: Option, @@ -60,6 +69,7 @@ struct RawDriverMatch { impl From for DriverMatch { fn from(r: RawDriverMatch) -> Self { DriverMatch { + bus: r.bus, vendor: r.vendor, device: r.device, class: r.class, @@ -97,7 +107,7 @@ impl DriverConfig { if matches.is_empty() { log::warn!( - "driver-manager: config {} driver={} has no PCI match entries and will not bind from PCI enumeration", + "driver-manager: config {} driver={} has no match entries and will not bind from PCI or ACPI enumeration", path.display(), driver.name ); @@ -128,6 +138,18 @@ fn pci_device_path(info: &DeviceInfo) -> String { } } +/// Build the ACPI scheme path for a device. +/// +/// The path follows the pattern `/scheme/acpi/symbols/{device_name}`, +/// where device_name is the ACPI namespace path (e.g., "PCI0", "I2C0", "GPI0"). +fn acpi_device_path(info: &DeviceInfo) -> String { + if info.raw_path.starts_with("/scheme/acpi/") { + info.raw_path.clone() + } else { + format!("/scheme/acpi/symbols/{}", info.id.path) + } +} + fn open_pcid_channel(device_path: &str) -> Result { let mut handle = match PciFunctionHandle::connect_by_path(Path::new(device_path)) { Ok(handle) => handle, @@ -154,10 +176,24 @@ fn open_pcid_channel(device_path: &str) -> Result { } fn check_scheme_available(name: &str) -> bool { - if std::path::Path::new(&format!("/scheme/{}", name)).exists() { - return true; + let path = format!("/scheme/{}", name); + // Use read_dir instead of Path::exists() because Redox scheme paths + // may not respond correctly to exists()/metadata() while still being + // fully functional for directory enumeration and file open. + // This was the root cause of "dependency scheme not ready: pci" even + // though PciBus::enumerate_devices (which uses read_dir) succeeded. + match fs::read_dir(&path) { + Ok(_) => true, + Err(err) => { + log::debug!( + "driver-manager: scheme availability check failed for {}: {} (exists={})", + path, + err, + std::path::Path::new(&path).exists() + ); + false + } } - false } impl Driver for DriverConfig { @@ -195,6 +231,22 @@ impl Driver for DriverConfig { } } + // Check if this device+driver pair was permanently abandoned + // by the hotplug loop (binary missing, driver declined, or + // deferred retries exhausted). Skip without any work or logging. + { + let key = (device_key.clone(), self.name.clone()); + let skipped = match PERMANENTLY_SKIPPED.lock() { + Ok(skipped) => skipped, + Err(_) => return ProbeResult::Fatal { + reason: String::from("skip set lock poisoned"), + }, + }; + if skipped.contains(&key) { + return ProbeResult::NotSupported; + } + } + if self.command.is_empty() { return ProbeResult::Fatal { reason: String::from("empty command"), @@ -207,12 +259,29 @@ impl Driver for DriverConfig { format!("/usr/lib/drivers/{}", self.command[0]) }; + // Also check the initfs path — drivers like nvmed live in + // /scheme/initfs/lib/drivers/ during early boot and may not yet + // be staged to /usr/lib/drivers/ after switchroot. if !std::path::Path::new(&actual_path).exists() { + let initfs_path = format!("/scheme/initfs/lib/drivers/{}", self.command[0].rsplit('/').next().unwrap_or(&self.command[0])); + if std::path::Path::new(&initfs_path).exists() { + return ProbeResult::Deferred { + reason: format!("driver in initfs only (not yet in rootfs): {}", initfs_path), + }; + } return ProbeResult::Fatal { - reason: format!("driver binary not found: {}", actual_path), + reason: format!("driver binary not found: {} (also checked {})", actual_path, initfs_path), }; } + // Skip if this driver's scheme is already registered (e.g., by + // pcid-spawner during initfs). Prevents re-spawning drivers + // that are already serving their scheme. + if check_scheme_available(&self.name) { + log::info!("driver {} already serving scheme, skipping probe for {}", self.name, device_key); + return ProbeResult::Bound; + } + let deps: Vec = if !self.depends_on.is_empty() { self.depends_on.clone() } else { @@ -228,43 +297,13 @@ impl Driver for DriverConfig { log::info!("probing {} with driver {}", device_key, self.name); - let device_path = pci_device_path(info); - - let channel_fd = match open_pcid_channel(&device_path) { - Ok(channel_fd) => channel_fd, - Err(result) => return result, - }; - - let mut cmd = Command::new(&actual_path); - for arg in &self.command[1..] { - cmd.arg(arg); - } - - cmd.env("PCID_CLIENT_CHANNEL", channel_fd.as_raw_fd().to_string()); - cmd.env("PCID_DEVICE_PATH", &device_path); - - match cmd.spawn() { - Ok(child) => { - let pid = child.id(); - log::info!( - "driver {} spawned (pid {}) for device {}", - self.name, - pid, - device_key - ); - let mut spawned = match self.spawned.lock() { - Ok(spawned) => spawned, - Err(err) => { - return ProbeResult::Fatal { - reason: format!("spawn state lock poisoned after spawn: {err}"), - }; - } - }; - spawned.insert(device_key, SpawnedDriver { child, channel_fd }); - ProbeResult::Bound - } - Err(e) => ProbeResult::Fatal { - reason: format!("spawn failed: {}", e), + // Branch on bus type: PCI devices use the pcid channel, + // ACPI devices use the ACPI scheme path with resource queries. + match info.id.bus.as_str() { + "pci" => self.probe_pci_device(info, &device_key, &actual_path), + "acpi" => self.probe_acpi_device(info, &device_key, &actual_path), + other => ProbeResult::Fatal { + reason: format!("unsupported bus type: {}", other), }, } } @@ -340,6 +379,223 @@ impl Driver for DriverConfig { } } +impl DriverConfig { + /// Check for exited child processes (non-blocking waitpid). + /// Returns a list of (device_key, driver_name, exit_status) for exited drivers. + pub fn reap_exited_children(&self) -> Vec<(String, String, i32)> { + let mut exited = Vec::new(); + let Ok(mut spawned) = self.spawned.lock() else { + return exited; + }; + + let mut to_remove = Vec::new(); + + for (device_key, spawned_driver) in spawned.iter_mut() { + match spawned_driver.child.try_wait() { + Ok(Some(status)) => { + let code = status.code().unwrap_or(-1); + log::warn!( + "driver {} (pid {}) for device {} exited with status {}", + self.name, + spawned_driver.child.id(), + device_key, + code + ); + to_remove.push(device_key.clone()); + exited.push((device_key.clone(), self.name.clone(), code)); + } + Ok(None) => { + // Still running + } + Err(err) => { + log::error!( + "failed to check status of driver {} pid {}: {}", + self.name, + spawned_driver.child.id(), + err + ); + } + } + } + + for key in to_remove { + spawned.remove(&key); + } + + exited + } +} + +impl DriverConfig { + /// Probe and spawn a driver for a PCI device. + /// + /// Opens a pcid channel for PCI config space access and passes the + /// channel FD and device path to the spawned driver via environment variables. + fn probe_pci_device( + &self, + info: &DeviceInfo, + device_key: &str, + actual_path: &str, + ) -> ProbeResult { + let device_path = pci_device_path(info); + + let channel_fd = match open_pcid_channel(&device_path) { + Ok(channel_fd) => channel_fd, + Err(result) => return result, + }; + + let mut cmd = Command::new(actual_path); + for arg in &self.command[1..] { + cmd.arg(arg); + } + + cmd.env("PCID_CLIENT_CHANNEL", channel_fd.as_raw_fd().to_string()); + cmd.env("PCID_DEVICE_PATH", &device_path); + + self.spawn_driver(cmd, device_key, channel_fd) + } + + /// Probe and spawn a driver for an ACPI device. + /// + /// Queries ACPI resources (_CRS) from the device and passes them as + /// environment variables to the spawned driver. The driver can then + /// use these to map MMIO regions and request IRQs. + /// + /// # Linux equivalent + /// + /// Linux's `acpi_device_probe()` calls `acpi_dev_get_resources()` + /// to extract IRQ/MMIO/IO resources from _CRS and passes them via + /// `struct resource` to the platform driver's `probe()` callback. + fn probe_acpi_device( + &self, + info: &DeviceInfo, + device_key: &str, + actual_path: &str, + ) -> ProbeResult { + let device_path = acpi_device_path(info); + + // Query ACPI resources for this device. + // Uses the AcpiBus resource query API which reads _CRS data. + let acpi_bus = AcpiBus::new(); + let resources = acpi_bus.query_device_resources(&info.id.path); + + let mut cmd = Command::new(actual_path); + for arg in &self.command[1..] { + cmd.arg(arg); + } + + // Pass device identification + cmd.env("ACPI_DEVICE_PATH", &device_path); + cmd.env("ACPI_DEVICE_NAME", &info.id.path); + + // Pass _HID if available + if let Some(ref desc) = info.description { + cmd.env("ACPI_DEVICE_DESCRIPTION", desc); + } + + // Extract and pass MMIO regions as env vars. + // Format: ACPI_MMIO_0=base,length ACPI_MMIO_1=base,length ... + let mmio_regions = redox_driver_acpi::extract_mmio_regions(&resources); + for (i, region) in mmio_regions.iter().enumerate() { + cmd.env( + format!("ACPI_MMIO_{}", i), + format!("{:#x},{:#x}", region.base, region.length), + ); + } + if !mmio_regions.is_empty() { + cmd.env("ACPI_MMIO_COUNT", mmio_regions.len().to_string()); + } + + // Extract and pass IRQ info as env vars. + // Format: ACPI_IRQ_0=gsi,triggering,polarity ACPI_IRQ_1=gsi,triggering,polarity ... + let irqs = redox_driver_acpi::extract_irqs(&resources); + for (i, irq) in irqs.iter().enumerate() { + let trigger = match irq.triggering { + redox_driver_acpi::TriggerMode::Edge => "edge", + redox_driver_acpi::TriggerMode::Level => "level", + }; + let polarity = match irq.polarity { + redox_driver_acpi::Polarity::ActiveHigh => "high", + redox_driver_acpi::Polarity::ActiveLow => "low", + redox_driver_acpi::Polarity::ActiveBoth => "both", + }; + cmd.env( + format!("ACPI_IRQ_{}", i), + format!("{:#x},{},{}", irq.gsi, trigger, polarity), + ); + } + if !irqs.is_empty() { + cmd.env("ACPI_IRQ_COUNT", irqs.len().to_string()); + } + + // Extract and pass I/O port ranges + let io_ports = redox_driver_acpi::extract_io_ports(&resources); + for (i, port) in io_ports.iter().enumerate() { + cmd.env( + format!("ACPI_IO_{}", i), + format!("{:#x},{:#x}", port.base, port.length), + ); + } + if !io_ports.is_empty() { + cmd.env("ACPI_IO_COUNT", io_ports.len().to_string()); + } + + // ACPI drivers don't use a pcid channel — they access hardware + // via scheme:memory (MMIO) and scheme:irq directly. + // Create a dummy fd to satisfy the spawn signature. + // The driver reads resources from the env vars above. + let dev_null = match std::fs::File::open("/scheme/null") { + Ok(f) => unsafe { OwnedFd::from_raw_fd(f.as_raw_fd()) }, + Err(_) => { + // Fallback: open /dev/null on Linux hosts during testing + match std::fs::File::open("/dev/null") { + Ok(f) => unsafe { OwnedFd::from_raw_fd(f.as_raw_fd()) }, + Err(e) => { + return ProbeResult::Fatal { + reason: format!("cannot open null device for ACPI channel: {}", e), + }; + } + } + } + }; + + self.spawn_driver(cmd, device_key, dev_null) + } + + /// Common driver spawn logic — shared by PCI and ACPI probe paths. + fn spawn_driver( + &self, + mut cmd: Command, + device_key: &str, + channel_fd: OwnedFd, + ) -> ProbeResult { + match cmd.spawn() { + Ok(child) => { + let pid = child.id(); + log::info!( + "driver {} spawned (pid {}) for device {}", + self.name, + pid, + device_key + ); + let mut spawned = match self.spawned.lock() { + Ok(spawned) => spawned, + Err(err) => { + return ProbeResult::Fatal { + reason: format!("spawn state lock poisoned after spawn: {err}"), + }; + } + }; + spawned.insert(device_key.to_string(), SpawnedDriver { child, channel_fd }); + ProbeResult::Bound + } + Err(e) => ProbeResult::Fatal { + reason: format!("spawn failed: {}", e), + }, + } + } +} + /// Driver-specified dependencies. Parsed from [driver.depends] TOML field. /// Example: depends_on = ["pci", "acpi"] /// When specified, takes precedence over guess_dependencies(). @@ -383,7 +639,7 @@ struct RawDriverEntry { priority: i32, #[serde(default)] command: Vec, - #[serde(rename = "match")] + #[serde(rename = "match", default)] r#match: Vec, #[serde(default)] depends_on: Vec, diff --git a/local/recipes/system/driver-manager/source/src/hotplug.rs b/local/recipes/system/driver-manager/source/src/hotplug.rs index c5bfdc5b3a..3c94557977 100644 --- a/local/recipes/system/driver-manager/source/src/hotplug.rs +++ b/local/recipes/system/driver-manager/source/src/hotplug.rs @@ -26,7 +26,6 @@ pub fn run_hotplug_loop( ); let mut deferred_retries: BTreeMap<(String, String), u32> = BTreeMap::new(); - let mut permanently_fatal: BTreeSet<(String, String)> = BTreeSet::new(); loop { thread::sleep(Duration::from_millis(poll_interval_ms)); @@ -67,15 +66,6 @@ pub fn run_hotplug_loop( track_pci_device(device, &mut seen_pci_devices); let key = (device.path.clone(), driver_name.clone()); - // Skip devices that were permanently fatal in a previous cycle. - // enumerate() re-probes all unbound devices each poll, but a Fatal - // result means the driver binary is genuinely absent (e.g. ided on - // a live ISO that doesn't ship it) — no amount of re-probing will - // change the outcome. - if permanently_fatal.contains(&key) { - continue; - } - match result { ProbeResult::Bound => { log::info!("hotplug: bound {} -> {}", device.path, driver_name); @@ -99,6 +89,12 @@ pub fn run_hotplug_loop( MAX_DEFERRED_RETRIES, reason ); + if let Ok(mut skipped) = crate::config::PERMANENTLY_SKIPPED.lock() { + skipped.insert(( + device.path.clone(), + driver_name.clone(), + )); + } } } ProbeResult::Fatal { reason } => { @@ -108,9 +104,20 @@ pub fn run_hotplug_loop( driver_name, reason ); - permanently_fatal.insert(key); + if let Ok(mut skipped) = crate::config::PERMANENTLY_SKIPPED.lock() { + skipped.insert(key); + } + } + ProbeResult::NotSupported => { + log::debug!( + "hotplug: not supported {} -> {}", + device.path, + driver_name + ); + if let Ok(mut skipped) = crate::config::PERMANENTLY_SKIPPED.lock() { + skipped.insert(key); + } } - _ => {} } } ProbeEvent::NoDriverFound { device } => { @@ -200,6 +207,8 @@ fn track_pci_device(device: &DeviceId, seen_pci_devices: &mut BTreeSet) } fn notify_bound_device(scheme: &DriverManagerScheme, device: &DeviceId, driver_name: &str) { + // PCI devices use the pcid-compatible bind notification. + // ACPI devices may be notified through other mechanisms in the future. if device.bus == "pci" { notify_bind(scheme, &device.path, driver_name); } diff --git a/local/recipes/system/driver-manager/source/src/main.rs b/local/recipes/system/driver-manager/source/src/main.rs index afc032d120..1d1291f4a4 100644 --- a/local/recipes/system/driver-manager/source/src/main.rs +++ b/local/recipes/system/driver-manager/source/src/main.rs @@ -3,6 +3,7 @@ mod exec; mod hotplug; mod scheme; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::thread; use std::time::{Duration, Instant}; @@ -12,12 +13,26 @@ use redox_driver_core::device::DeviceId; use redox_driver_core::driver::ProbeResult; use redox_driver_core::manager::{DeviceManager, ManagerConfig, ProbeEvent}; use redox_driver_pci::PciBus; +use redox_driver_acpi::AcpiBus; use std::fs::OpenOptions; use std::io::Write; use config::DriverConfig; use scheme::{DriverManagerScheme, notify_bind}; +/// Global flag set by SIGTERM handler to request graceful shutdown. +static SHUTDOWN_REQUESTED: AtomicBool = AtomicBool::new(false); + +extern "C" fn sigterm_handler(_sig: i32) { + SHUTDOWN_REQUESTED.store(true, Ordering::SeqCst); +} + +fn install_sigterm_handler() { + unsafe { + libc::signal(libc::SIGTERM, sigterm_handler as *const () as usize); + } +} + struct StderrLogger; const BOOT_TIMELINE_PATH: &str = "/tmp/redbear-boot-timeline.json"; @@ -37,6 +52,7 @@ impl log::Log for StderrLogger { fn run_enumeration( manager: &Arc>, scheme: &DriverManagerScheme, + initfs: bool, ) -> (usize, usize) { let enum_start = Instant::now(); let events = match manager.lock() { @@ -77,7 +93,11 @@ fn run_enumeration( log::info!("bus {} enumerated {} device(s)", bus, device_count); } ProbeEvent::BusEnumerationFailed { bus, error } => { - log::error!("bus {} enumeration failed: {:?}", bus, error); + if initfs && *bus == "pci" { + log::warn!("bus {} enumeration not yet ready (initfs, pcid may still be starting): {:?}", bus, error); + } else { + log::error!("bus {} enumeration failed: {:?}", bus, error); + } } ProbeEvent::AlreadyBound { device, @@ -113,14 +133,19 @@ fn run_enumeration( } fn notify_bound_device(scheme: &DriverManagerScheme, device: &DeviceId, driver_name: &str) { - if device.bus == "pci" { - notify_bind(scheme, &device.path, driver_name); - } + // Notify for both PCI and ACPI devices + notify_bind(scheme, &device.path, driver_name); } fn reset_timeline_log() { - if let Err(err) = fs::write(BOOT_TIMELINE_PATH, "") { - log::warn!("failed to reset boot timeline log at {BOOT_TIMELINE_PATH}: {err}"); + // Best-effort: truncate or create empty. On scheme filesystems that + // don't support truncate on existing files, this may fail — that's OK, + // the append path will handle it. + match fs::write(BOOT_TIMELINE_PATH, "") { + Ok(()) => {} + Err(_) => { + let _ = fs::remove_file(BOOT_TIMELINE_PATH); + } } } @@ -213,22 +238,127 @@ fn log_timeline(event: &ProbeEvent) { { Ok(mut file) => { if let Err(err) = writeln!(file, "{entry}") { - log::warn!("failed to append boot timeline entry to {BOOT_TIMELINE_PATH}: {err}"); + // EPIPE or other write errors can occur when /tmp is backed + // by a scheme that doesn't support append writes, or when the + // filesystem is not yet fully ready. Log once and suppress + // all subsequent write errors to avoid log spam. + static WRITE_ERROR_LOGGED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); + if !WRITE_ERROR_LOGGED.swap(true, std::sync::atomic::Ordering::Relaxed) { + log::warn!("failed to append boot timeline entry to {BOOT_TIMELINE_PATH}: {err} (suppressing further write errors)"); + } } } Err(err) => { - log::warn!("failed to open boot timeline log at {BOOT_TIMELINE_PATH}: {err}"); + // EEXIST (os error 17) can occur when the file already exists + // but the scheme filesystem doesn't support create+append. + // EPIPE and other errors occur when /tmp isn't ready. + // Log once and suppress all subsequent open errors. + static OPEN_ERROR_LOGGED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); + if !OPEN_ERROR_LOGGED.swap(true, std::sync::atomic::Ordering::Relaxed) { + log::warn!("failed to open boot timeline log at {BOOT_TIMELINE_PATH}: {err} (suppressing further open errors)"); + } } } } +fn run_status() { + // Print the boot timeline log if it exists. + match fs::read_to_string(BOOT_TIMELINE_PATH) { + Ok(content) => { + if content.trim().is_empty() { + println!("No boot timeline data found at {}", BOOT_TIMELINE_PATH); + println!("Driver manager has not completed enumeration yet."); + return; + } + + println!("=== Red Bear OS Driver Manager Status ==="); + println!(); + + let mut bound = 0usize; + let mut deferred = 0usize; + let mut failed = 0usize; + let mut no_driver = 0usize; + let mut buses = Vec::new(); + + for line in content.lines() { + if line.trim().is_empty() { + continue; + } + // Parse JSON timeline entries + if line.contains("\"event\":\"bus_enumerated\"") { + if let Some(bus) = extract_json_string(line, "bus") { + if let Some(count) = extract_json_number(line, "count") { + buses.push((bus, count)); + } + } + } else if line.contains("\"status\":\"bound\"") { + bound += 1; + } else if line.contains("\"status\":\"deferred\"") { + deferred += 1; + } else if line.contains("\"status\":\"failed\"") { + failed += 1; + } else if line.contains("\"event\":\"no_driver\"") { + no_driver += 1; + } + } + + println!("Bus enumeration:"); + for (bus, count) in &buses { + println!(" {}: {} device(s)", bus, count); + } + println!(); + println!("Driver binding:"); + println!(" bound: {}", bound); + println!(" deferred: {}", deferred); + println!(" failed: {}", failed); + println!(" no driver: {}", no_driver); + println!(); + println!("Timeline log: {}", BOOT_TIMELINE_PATH); + } + Err(err) => { + println!("Cannot read {}: {}", BOOT_TIMELINE_PATH, err); + println!("Driver manager may not have run yet."); + } + } +} + +/// Extract a JSON string value for a given key from a single-line JSON object. +fn extract_json_string(line: &str, key: &str) -> Option { + let pattern = format!("\"{}\":\"", key); + let start = line.find(&pattern)?; + let value_start = start + pattern.len(); + let end = line[value_start..].find('"')?; + Some(line[value_start..value_start + end].to_string()) +} + +/// Extract a JSON number value for a given key from a single-line JSON object. +fn extract_json_number(line: &str, key: &str) -> Option { + let pattern = format!("\"{}\":", key); + let start = line.find(&pattern)?; + let value_start = start + pattern.len(); + let rest = &line[value_start..]; + let end = rest.find(|c: char| !c.is_ascii_digit()).unwrap_or(rest.len()); + rest[..end].parse().ok() +} + fn main() { log::set_logger(&StderrLogger).ok(); log::set_max_level(log::LevelFilter::Info); + // Install SIGTERM handler for graceful shutdown + install_sigterm_handler(); + let args: Vec = env::args().collect(); let initfs = args.iter().any(|a| a == "--initfs"); let hotplug_mode = args.iter().any(|a| a == "--hotplug"); + let status_mode = args.iter().any(|a| a == "--status"); + + // --status: print the current device registry from the boot timeline log + // and exit. This is for diagnostics: "what did driver-manager find?" + if status_mode { + run_status(); + return; + } let config_dir = if initfs { "/scheme/initfs/lib/drivers.d" @@ -262,8 +392,17 @@ fn main() { match manager.lock() { Ok(mut mgr) => { + // Register PCI bus first (higher priority — storage, network, GPU). + // Mirrors Linux's pci_scan_child_bus() via subsys_initcall. mgr.register_bus(Box::new(PciBus::new())); + // Register ACPI bus for platform/I2C/SPI/GPIO/thermal devices. + // Mirrors Linux's acpi_bus_scan() which walks the namespace for + // _HID/_CID/_STA/_CRS. ACPI devices are enumerated from + // /scheme/acpi/symbols/ which acpid populates from the AML + // interpreter. + mgr.register_bus(Box::new(AcpiBus::new())); + for dc in &driver_configs { mgr.register_driver(Box::new(dc.clone())); } @@ -277,11 +416,14 @@ fn main() { let mgr_clone = Arc::clone(&manager); let scheme_clone = Arc::clone(&scheme); + // Ensure /tmp exists before writing the boot timeline log. + let _ = std::fs::create_dir_all("/tmp"); + reset_timeline_log(); if manager_config.async_probe { let handle = thread::spawn(move || { - let (bound, deferred) = run_enumeration(&mgr_clone, scheme_clone.as_ref()); + let (bound, deferred) = run_enumeration(&mgr_clone, scheme_clone.as_ref(), initfs); log::info!("async enum: {} bound, {} deferred", bound, deferred); }); if handle.join().is_err() { @@ -289,13 +431,21 @@ fn main() { process::exit(1); } } else { - let (bound, deferred) = run_enumeration(&manager, scheme.as_ref()); + let (bound, deferred) = run_enumeration(&manager, scheme.as_ref(), initfs); log::info!("enum complete: {} bound, {} deferred", bound, deferred); } - if let Err(err) = scheme::start_scheme_server(Arc::clone(&scheme)) { - log::error!("{err}"); - process::exit(1); + match scheme::start_scheme_server(Arc::clone(&scheme)) { + Ok(true) => { + log::info!("driver-manager: scheme server started successfully"); + } + Ok(false) => { + log::warn!("driver-manager: scheme already registered — another instance is active, continuing without scheme server"); + } + Err(err) => { + log::error!("{err}"); + process::exit(1); + } } if hotplug_mode { @@ -304,8 +454,17 @@ fn main() { idle_forever(); } - let max_retries = 30u32; + let max_retries = 3u32; for retry in 1..=max_retries { + if SHUTDOWN_REQUESTED.load(Ordering::SeqCst) { + log::info!("driver-manager: SIGTERM received during deferred retry, shutting down"); + graceful_shutdown(); + process::exit(0); + } + + // Check for crashed drivers during retry loop + reap_all_drivers(&driver_configs); + thread::sleep(Duration::from_millis(500)); let retry_events = match manager.lock() { @@ -360,6 +519,35 @@ fn main() { fn idle_forever() -> ! { log::info!("driver-manager: entering persistent idle loop"); loop { - thread::sleep(Duration::from_secs(3600)); + thread::sleep(Duration::from_secs(5)); + if SHUTDOWN_REQUESTED.load(Ordering::SeqCst) { + log::info!("driver-manager: SIGTERM received, performing graceful shutdown"); + graceful_shutdown(); + process::exit(0); + } + // Periodically check for exited child drivers + reap_all_drivers(&[]); } } + +/// Poll all driver configs for exited children and log the results. +fn reap_all_drivers(driver_configs: &[DriverConfig]) { + for dc in driver_configs { + let exited = dc.reap_exited_children(); + for (device_key, driver_name, code) in &exited { + log::warn!( + "reaped crashed driver: {} for device {} (exit {})", + driver_name, + device_key, + code + ); + } + } +} + +fn graceful_shutdown() { + // The DeviceManager and spawned children are managed by DriverConfig instances + // which track their child processes. On shutdown, we log and exit cleanly. + // Child processes will be orphaned but the kernel reaps them. + log::info!("driver-manager: clean shutdown complete"); +} diff --git a/local/recipes/system/driver-manager/source/src/scheme.rs b/local/recipes/system/driver-manager/source/src/scheme.rs index 58a563c16e..2b15667857 100644 --- a/local/recipes/system/driver-manager/source/src/scheme.rs +++ b/local/recipes/system/driver-manager/source/src/scheme.rs @@ -112,9 +112,9 @@ impl DriverManagerScheme { ["devices"] => Ok(HandleKind::Devices), ["bound"] => Ok(HandleKind::Bound), ["events"] => Ok(HandleKind::Events), - ["devices", pci_addr] if Self::valid_pci_addr(pci_addr) => { - let _ = self.device_status(pci_addr)?; - Ok(HandleKind::Device((*pci_addr).to_string())) + ["devices", addr] if Self::valid_device_addr(addr) => { + let _ = self.device_status(addr)?; + Ok(HandleKind::Device((*addr).to_string())) } _ => Err(Error::new(ENOENT)), } @@ -127,7 +127,7 @@ impl DriverManagerScheme { return Ok(HandleKind::Devices); } - if trimmed.contains('/') || !Self::valid_pci_addr(trimmed) { + if trimmed.contains('/') || !Self::valid_device_addr(trimmed) { return Err(Error::new(ENOENT)); } @@ -228,6 +228,23 @@ impl DriverManagerScheme { .all(|ch| ch.is_ascii_hexdigit() || matches!(ch, ':' | '.')) } + /// Validate a device address for both PCI and ACPI devices. + /// + /// PCI addresses contain colons and dots (e.g., "0000:00:1f.2"). + /// ACPI device names are alphanumeric 4-char segments (e.g., "PCI0", "I2C0", "GPI0"). + #[cfg(target_os = "redox")] + fn valid_device_addr(value: &str) -> bool { + // Accept PCI-style addresses + if Self::valid_pci_addr(value) { + return true; + } + // Accept ACPI device names (alphanumeric, dots for child paths) + !value.is_empty() + && value + .chars() + .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '.' | '_')) + } + fn push_event_line(&self, line: String) { match self.events.lock() { Ok(mut events) => { @@ -366,10 +383,14 @@ pub fn notify_bind(scheme: &DriverManagerScheme, pci_addr: &str, driver_name: &s )); if let Err(err) = write_driver_param(pci_addr, "driver", driver_name) { - log::warn!("driver-manager: failed to write driver param for {pci_addr}: {err}"); + if err.kind() != std::io::ErrorKind::BrokenPipe { + log::warn!("driver-manager: failed to write driver param for {pci_addr}: {err}"); + } } if let Err(err) = write_driver_param(pci_addr, "enabled", "true") { - log::warn!("driver-manager: failed to write enabled param for {pci_addr}: {err}"); + if err.kind() != std::io::ErrorKind::BrokenPipe { + log::warn!("driver-manager: failed to write enabled param for {pci_addr}: {err}"); + } } } @@ -392,21 +413,31 @@ pub fn notify_unbind(scheme: &DriverManagerScheme, pci_addr: &str) { scheme.push_event_line(event_line); if let Err(err) = write_driver_param(pci_addr, "driver", "") { - log::warn!("driver-manager: failed to clear driver param for {pci_addr}: {err}"); + if err.kind() != std::io::ErrorKind::BrokenPipe { + log::warn!("driver-manager: failed to clear driver param for {pci_addr}: {err}"); + } } if let Err(err) = write_driver_param(pci_addr, "enabled", "false") { - log::warn!("driver-manager: failed to write disabled param for {pci_addr}: {err}"); + if err.kind() != std::io::ErrorKind::BrokenPipe { + log::warn!("driver-manager: failed to write disabled param for {pci_addr}: {err}"); + } } } #[cfg(target_os = "redox")] -pub fn start_scheme_server(scheme: Arc) -> std::result::Result<(), String> { +pub fn start_scheme_server(scheme: Arc) -> std::result::Result { let socket = Socket::create() .map_err(|err| format!("driver-manager: failed to create scheme socket: {err}"))?; let mut server = SchemeServer::new(scheme); - register_sync_scheme(&socket, SCHEME_NAME, &mut server) - .map_err(|err| format!("driver-manager: failed to register scheme:{SCHEME_NAME}: {err}"))?; + if let Err(err) = register_sync_scheme(&socket, SCHEME_NAME, &mut server) { + let msg = format!("{err}"); + if msg.contains("File exists") { + log::warn!("driver-manager: scheme:{SCHEME_NAME} already registered (initfs instance active), returning gracefully"); + return Ok(false); + } + return Err(format!("driver-manager: failed to register scheme:{SCHEME_NAME}: {err}")); + } log::info!("driver-manager: registered scheme:{SCHEME_NAME}"); @@ -439,10 +470,10 @@ pub fn start_scheme_server(scheme: Arc) -> std::result::Res }) .map_err(|err| format!("driver-manager: failed to spawn scheme server thread: {err}"))?; - Ok(()) + Ok(true) } #[cfg(not(target_os = "redox"))] -pub fn start_scheme_server(_scheme: Arc) -> std::result::Result<(), String> { - Ok(()) +pub fn start_scheme_server(_scheme: Arc) -> std::result::Result { + Ok(true) } diff --git a/local/recipes/system/iommu/source/src/lib.rs b/local/recipes/system/iommu/source/src/lib.rs index 44e5c1a3d7..8327904af1 100644 --- a/local/recipes/system/iommu/source/src/lib.rs +++ b/local/recipes/system/iommu/source/src/lib.rs @@ -4,6 +4,7 @@ pub mod acpi; pub mod amd_vi; pub mod command_buffer; pub mod device_table; +pub mod intel_vtd; pub mod interrupt; pub mod mmio; pub mod page_table; @@ -12,6 +13,7 @@ use std::collections::BTreeMap; use acpi::{parse_bdf, Bdf}; use amd_vi::AmdViUnit; +use intel_vtd::IntelVtdUnit; use page_table::{DomainPageTables, MappingFlags}; use redox_scheme::SchemeBlockMut; use syscall::data::Stat; @@ -161,7 +163,8 @@ struct Handle { } pub struct IommuScheme { - units: Vec, + amd_units: Vec, + intel_units: Vec, next_id: usize, handles: BTreeMap, domains: BTreeMap, @@ -170,12 +173,13 @@ pub struct IommuScheme { impl IommuScheme { pub fn new() -> Self { - Self::with_units(Vec::new()) + Self::with_units(Vec::new(), Vec::new()) } - pub fn with_units(units: Vec) -> Self { + pub fn with_units(amd_units: Vec, intel_units: Vec) -> Self { Self { - units, + amd_units, + intel_units, next_id: 0, handles: BTreeMap::new(), domains: BTreeMap::new(), @@ -184,7 +188,7 @@ impl IommuScheme { } pub fn unit_count(&self) -> usize { - self.units.len() + self.amd_units.len() + self.intel_units.len() } fn insert_handle(&mut self, kind: HandleKind) -> usize { @@ -216,40 +220,67 @@ impl IommuScheme { } fn ensure_unit_initialized(&mut self, unit_index: usize) -> core::result::Result<(), i32> { - let Some(unit) = self.units.get_mut(unit_index) else { - return Err(ENODEV as i32); - }; - - if unit.initialized() { - return Ok(()); + if let Some(unit) = self.amd_units.get_mut(unit_index) { + if unit.initialized() { + return Ok(()); + } + return unit.init().map_err(|err| { + log::error!( + "iommu: failed to initialize AMD-Vi unit {} at MMIO {:#x}: {}", + unit_index, + unit.info().mmio_base, + err + ); + EIO as i32 + }); } - - unit.init().map_err(|err| { - log::error!( - "iommu: failed to initialize unit {} at MMIO {:#x}: {}", - unit_index, - unit.info().mmio_base, - err - ); - EIO as i32 - }) + let intel_index = unit_index.saturating_sub(self.amd_units.len()); + if let Some(unit) = self.intel_units.get_mut(intel_index) { + if unit.initialized() { + return Ok(()); + } + return unit.init().map_err(|err| { + log::error!( + "iommu: failed to initialize Intel VT-d unit {} at MMIO {:#x}: {}", + intel_index, + unit.info().mmio_base, + err + ); + EIO as i32 + }); + } + Err(ENODEV as i32) } fn root_listing(&self) -> Vec { let mut listing = String::from("control\n"); - for (index, unit) in self.units.iter().enumerate() { + for (index, unit) in self.amd_units.iter().enumerate() { let state = if unit.initialized() { "initialized" } else { "detected" }; listing.push_str(&format!( - "unit/{index} {} mmio={:#x} state={}\n", + "unit/{index} {} mmio={:#x} state={} type=amd\n", unit.info().iommu_bdf, unit.info().mmio_base, state )); } + let intel_offset = self.amd_units.len(); + for (index, unit) in self.intel_units.iter().enumerate() { + let state = if unit.initialized() { + "initialized" + } else { + "detected" + }; + listing.push_str(&format!( + "unit/{} mmio={:#x} state={} type=intel\n", + intel_offset + index, + unit.info().mmio_base, + state + )); + } for domain_id in self.domains.keys() { listing.push_str(&format!("domain/{domain_id}\n")); } @@ -295,19 +326,31 @@ impl IommuScheme { requested_unit: Option, ) -> core::result::Result { if let Some(index) = requested_unit { - let Some(unit) = self.units.get(index) else { + if let Some(unit) = self.amd_units.get(index) { + if unit.handles_device(bdf) { + return Ok(index); + } return Err(ENODEV as i32); - }; - if unit.handles_device(bdf) { - return Ok(index); + } + let intel_index = index.saturating_sub(self.amd_units.len()); + if let Some(unit) = self.intel_units.get(intel_index) { + if unit.handles_device(bdf) { + return Ok(index); + } } return Err(ENODEV as i32); } - self.units - .iter() - .position(|unit| unit.handles_device(bdf)) - .ok_or(ENODEV as i32) + if let Some(index) = self.amd_units.iter().position(|unit| unit.handles_device(bdf)) { + return Ok(index); + } + let intel_offset = self.amd_units.len(); + if let Some(index) = self.intel_units.iter().position(|unit| { + unit.handles_device(bdf) + }) { + return Ok(intel_offset + index); + } + Err(ENODEV as i32) } fn dispatch_request(&mut self, kind: HandleKind, request: IommuRequest) -> IommuResponse { @@ -327,10 +370,11 @@ impl IommuScheme { match request.opcode { opcode::QUERY => IommuResponse::success( request.opcode, - self.units.len() as u32, + self.unit_count() as u32, self.domains.len() as u64, self.device_assignments.len() as u64, - self.units.iter().filter(|unit| unit.initialized()).count() as u64, + self.amd_units.iter().filter(|unit| unit.initialized()).count() as u64 + + self.intel_units.iter().filter(|unit| unit.initialized()).count() as u64, ), opcode::INIT_UNITS => { let requested_index = if request.arg0 == u32::MAX { @@ -341,17 +385,18 @@ impl IommuScheme { let mut initialized_now = 0u32; let mut attempted = 0u64; - for index in 0..self.units.len() { + let total_units = self.unit_count(); + for index in 0..total_units { if requested_index.is_some() && requested_index != Some(index) { continue; } attempted += 1; - let was_initialized = self - .units - .get(index) - .map(|unit| unit.initialized()) - .unwrap_or(false); + let was_initialized = if index < self.amd_units.len() { + self.amd_units.get(index).map(|unit| unit.initialized()).unwrap_or(false) + } else { + self.intel_units.get(index - self.amd_units.len()).map(|unit| unit.initialized()).unwrap_or(false) + }; if let Err(errno) = self.ensure_unit_initialized(index) { return IommuResponse::error(request.opcode, errno); @@ -363,7 +408,8 @@ impl IommuScheme { } let initialized_total = - self.units.iter().filter(|unit| unit.initialized()).count() as u64; + self.amd_units.iter().filter(|unit| unit.initialized()).count() as u64 + + self.intel_units.iter().filter(|unit| unit.initialized()).count() as u64; IommuResponse::success( request.opcode, @@ -425,7 +471,7 @@ impl IommuScheme { let mut first_device = 0u64; let mut first_address = 0u64; - for (index, unit) in self.units.iter_mut().enumerate() { + for (index, unit) in self.amd_units.iter_mut().enumerate() { if requested_index.is_some() && requested_index != Some(index) { continue; } @@ -577,22 +623,25 @@ impl IommuScheme { return IommuResponse::error(request.opcode, ENOENT as i32); }; - let Some(unit) = self.units.get_mut(unit_index) else { - return IommuResponse::error(request.opcode, ENODEV as i32); - }; - - match unit.assign_device(bdf, domain) { - Ok(()) => { - self.device_assignments.insert(bdf, (domain_id, unit_index)); - IommuResponse::success( - request.opcode, - domain_id as u32, - unit_index as u64, - u64::from(bdf.raw()), - 0, - ) + if unit_index < self.amd_units.len() { + let Some(unit) = self.amd_units.get_mut(unit_index) else { + return IommuResponse::error(request.opcode, ENODEV as i32); + }; + match unit.assign_device(bdf, domain) { + Ok(()) => { + self.device_assignments.insert(bdf, (domain_id, unit_index)); + IommuResponse::success( + request.opcode, + domain_id as u32, + unit_index as u64, + u64::from(bdf.raw()), + 0, + ) + } + Err(_) => IommuResponse::error(request.opcode, EIO as i32), } - Err(_) => IommuResponse::error(request.opcode, EIO as i32), + } else { + IommuResponse::error(request.opcode, ENODEV as i32) } } opcode::UNASSIGN_DEVICE => { @@ -600,14 +649,16 @@ impl IommuScheme { return IommuResponse::error(request.opcode, ENOENT as i32); }; - let unit = self.units.get_mut(unit_index); - if let Some(unit) = unit { - if unit.initialized() { - if let Err(err) = unit.unassign_device(bdf) { - log::error!( - "iommu: failed to invalidate DTE for {bdf} on unit {unit_index}: {err}" - ); - return IommuResponse::error(request.opcode, EIO as i32); + if unit_index < self.amd_units.len() { + let unit = self.amd_units.get_mut(unit_index); + if let Some(unit) = unit { + if unit.initialized() { + if let Err(err) = unit.unassign_device(bdf) { + log::error!( + "iommu: failed to invalidate DTE for {bdf} on unit {unit_index}: {err}" + ); + return IommuResponse::error(request.opcode, EIO as i32); + } } } } diff --git a/local/recipes/system/iommu/source/src/main.rs b/local/recipes/system/iommu/source/src/main.rs index 159b70f0cb..4ea0ddac33 100644 --- a/local/recipes/system/iommu/source/src/main.rs +++ b/local/recipes/system/iommu/source/src/main.rs @@ -9,6 +9,7 @@ use std::path::PathBuf; use std::process; use iommu::amd_vi::AmdViUnit; +use iommu::intel_vtd::{IntelVtdUnit, parse_dmar}; #[cfg(target_os = "redox")] use iommu::IommuScheme; use log::{error, info, LevelFilter, Metadata, Record}; @@ -27,7 +28,8 @@ struct StderrLogger { #[cfg_attr(not(target_os = "redox"), allow(dead_code))] struct DiscoveryResult { - units: Vec, + amd_units: Vec, + intel_units: Vec, source: DiscoverySource, kernel_acpi_status: &'static str, ivrs_path: Option, @@ -196,6 +198,17 @@ fn detect_dmar_from_kernel_acpi() -> Result { Ok(find_kernel_acpi_table(b"DMAR")?.is_some()) } +#[cfg(target_os = "redox")] +fn detect_intel_units_from_kernel_acpi() -> Result, String> { + match find_kernel_acpi_table(b"DMAR")? { + Some(table) => { + let infos = parse_dmar(&table).map_err(|err| format!("failed to parse DMAR: {err}"))?; + Ok(infos.into_iter().map(IntelVtdUnit::from_info).collect()) + } + None => Ok(Vec::new()), + } +} + #[cfg(target_os = "redox")] fn discover_units() -> Result { let dmar_present = match detect_dmar_from_kernel_acpi() { @@ -206,9 +219,18 @@ fn discover_units() -> Result { } }; + let intel_units = match detect_intel_units_from_kernel_acpi() { + Ok(units) => units, + Err(err) => { + info!("iommu: Intel VT-d discovery unavailable: {err}"); + Vec::new() + } + }; + match detect_units_from_kernel_acpi() { Ok(units) if !units.is_empty() => Ok(DiscoveryResult { - units, + amd_units: units, + intel_units, source: DiscoverySource::KernelAcpi, kernel_acpi_status: "ok", ivrs_path: None, @@ -222,7 +244,8 @@ fn discover_units() -> Result { } else { DiscoverySource::None }, - units, + amd_units: units, + intel_units, kernel_acpi_status: "empty", ivrs_path, dmar_present, @@ -237,7 +260,8 @@ fn discover_units() -> Result { } else { DiscoverySource::None }, - units, + amd_units: units, + intel_units, kernel_acpi_status: "error", ivrs_path, dmar_present, @@ -255,7 +279,8 @@ fn discover_units() -> Result { } else { DiscoverySource::None }, - units, + amd_units: units, + intel_units: Vec::new(), kernel_acpi_status: "unsupported", ivrs_path, dmar_present: false, @@ -265,9 +290,9 @@ fn discover_units() -> Result { #[cfg(target_os = "redox")] fn run() -> Result<(), String> { let discovery = discover_units()?; - if discovery.units.is_empty() { + if discovery.amd_units.is_empty() && discovery.intel_units.is_empty() { info!( - "iommu: no AMD-Vi units found (source={}, kernel_acpi_status={}, ivrs_path={})", + "iommu: no IOMMU units found (source={}, kernel_acpi_status={}, ivrs_path={})", discovery.source.as_str(), discovery.kernel_acpi_status, discovery @@ -277,20 +302,35 @@ fn run() -> Result<(), String> { .unwrap_or_else(|| "none".to_string()) ); } else { + if !discovery.amd_units.is_empty() { + info!( + "iommu: detected {} AMD-Vi unit(s) via {}", + discovery.amd_units.len(), + discovery.source.as_str() + ); + } + if !discovery.intel_units.is_empty() { + info!( + "iommu: detected {} Intel VT-d unit(s)", + discovery.intel_units.len() + ); + } + } + if discovery.dmar_present && discovery.intel_units.is_empty() { info!( - "iommu: detected {} AMD-Vi unit(s) via {}", - discovery.units.len(), - discovery.source.as_str() + "iommu: detected kernel ACPI DMAR table but failed to parse DRHD entries" ); } - if discovery.dmar_present { + for (index, unit) in discovery.amd_units.iter().enumerate() { info!( - "iommu: detected kernel ACPI DMAR table; Intel VT-d runtime ownership should converge here rather than remain in acpid" + "iommu: discovered AMD-Vi unit {} at MMIO {:#x}; initialization is deferred until first use", + index, + unit.info().mmio_base ); } - for (index, unit) in discovery.units.iter().enumerate() { + for (index, unit) in discovery.intel_units.iter().enumerate() { info!( - "iommu: discovered unit {} at MMIO {:#x}; initialization is deferred until first use", + "iommu: discovered Intel VT-d unit {} at MMIO {:#x}; initialization is deferred until first use", index, unit.info().mmio_base ); @@ -300,7 +340,7 @@ fn run() -> Result<(), String> { Socket::create("iommu").map_err(|e| format!("failed to register iommu scheme: {e}"))?; info!("iommu: registered scheme:iommu"); - let mut scheme = IommuScheme::with_units(discovery.units); + let mut scheme = IommuScheme::with_units(discovery.amd_units, discovery.intel_units); loop { let request = match socket.next_request(SignalBehavior::Restart) { @@ -338,7 +378,9 @@ fn run() -> Result<(), String> { #[cfg(target_os = "redox")] fn run_self_test() -> Result<(), String> { let discovery = discover_units()?; - let mut units = discovery.units; + let mut amd_units = discovery.amd_units; + let mut intel_units = discovery.intel_units; + let total_units = amd_units.len() + intel_units.len(); println!("discovery_source={}", discovery.source.as_str()); println!("kernel_acpi_status={}", discovery.kernel_acpi_status); @@ -351,19 +393,20 @@ fn run_self_test() -> Result<(), String> { .map(|path| path.display().to_string()) .unwrap_or_else(|| "none".to_string()) ); - println!("units_detected={}", units.len()); - if units.is_empty() { - return Err("iommu self-test detected zero AMD-Vi unit(s)".to_string()); + println!("amd_units_detected={}", amd_units.len()); + println!("intel_units_detected={}", intel_units.len()); + if total_units == 0 { + return Err("iommu self-test detected zero IOMMU units".to_string()); } let mut initialized_now = 0u32; let mut events_drained = 0u32; - for (index, unit) in units.iter_mut().enumerate() { + for (index, unit) in amd_units.iter_mut().enumerate() { let was_initialized = unit.initialized(); unit.init().map_err(|err| { format!( - "iommu self-test failed to initialize unit {} at MMIO {:#x}: {}", + "iommu self-test failed to initialize AMD-Vi unit {} at MMIO {:#x}: {}", index, unit.info().mmio_base, err @@ -376,7 +419,7 @@ fn run_self_test() -> Result<(), String> { let drained = unit.drain_events().map_err(|err| { format!( - "iommu self-test failed to drain events for unit {} at MMIO {:#x}: {}", + "iommu self-test failed to drain events for AMD-Vi unit {} at MMIO {:#x}: {}", index, unit.info().mmio_base, err @@ -385,9 +428,26 @@ fn run_self_test() -> Result<(), String> { events_drained = events_drained.saturating_add(drained.len() as u32); } - let initialized_after = units.iter().filter(|unit| unit.initialized()).count() as u64; + for (index, unit) in intel_units.iter_mut().enumerate() { + let was_initialized = unit.initialized(); + unit.init().map_err(|err| { + format!( + "iommu self-test failed to initialize Intel VT-d unit {} at MMIO {:#x}: {}", + index, + unit.info().mmio_base, + err + ) + })?; + + if !was_initialized { + initialized_now = initialized_now.saturating_add(1); + } + } + + let initialized_after = amd_units.iter().filter(|unit| unit.initialized()).count() as u64 + + intel_units.iter().filter(|unit| unit.initialized()).count() as u64; println!("units_initialized_now={}", initialized_now); - println!("units_attempted={}", units.len()); + println!("units_attempted={}", total_units); println!("units_initialized_after={}", initialized_after); println!("events_drained={}", events_drained); @@ -398,8 +458,9 @@ fn run_self_test() -> Result<(), String> { fn run() -> Result<(), String> { let discovery = discover_units()?; info!( - "iommu: host build stub active; parsed {} AMD-Vi unit(s) via {}", - discovery.units.len(), + "iommu: host build stub active; parsed {} AMD-Vi and {} Intel VT-d unit(s) via {}", + discovery.amd_units.len(), + discovery.intel_units.len(), discovery.source.as_str() ); Ok(()) diff --git a/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase-timer-check.rs b/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase-timer-check.rs index cae1b75ecd..04dc2d288b 100644 --- a/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase-timer-check.rs +++ b/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase-timer-check.rs @@ -47,6 +47,16 @@ fn timespec_to_nanos(time: &TimeSpec) -> i128 { i128::from(time.tv_sec) * 1_000_000_000i128 + i128::from(time.tv_nsec) } +fn check_timer_source(name: &str, path: &str) -> &'static str { + if Path::new(path).exists() { + println!("timer_source={} path={} present=1", name, path); + "present" + } else { + println!("timer_source={} path={} present=0", name, path); + "missing" + } +} + fn run() -> Result<(), String> { parse_args(PROGRAM, USAGE, std::env::args()).map_err(|err| { if err.is_empty() { @@ -57,6 +67,10 @@ fn run() -> Result<(), String> { println!("=== Red Bear OS Timer Runtime Check ==="); + check_timer_source("hpet", "/scheme/sys/hpet"); + check_timer_source("pit", "/scheme/sys/pit"); + check_timer_source("lapic", "/scheme/sys/lapic"); + let time_path = monotonic_path()?; let time_fd = Fd::open(&time_path, flag::O_RDWR, 0) @@ -78,6 +92,17 @@ fn run() -> Result<(), String> { return Err("monotonic timer did not advance".to_string()); } + let expected_ns: i128 = 50_000_000; + let deviation_ns = (delta_ns - expected_ns).abs(); + println!("monotonic_expected_ns={expected_ns}"); + println!("monotonic_deviation_ns={deviation_ns}"); + + if deviation_ns > 20_000_000 { + println!("timer_precision=coarse deviation_ns={deviation_ns} (threshold=20000000)"); + } else { + println!("timer_precision=ok deviation_ns={deviation_ns} (threshold=20000000)"); + } + println!("monotonic_progress=ok"); Ok(()) } diff --git a/local/recipes/system/redbear-info/source/src/main.rs b/local/recipes/system/redbear-info/source/src/main.rs index f0966b0f1b..ef6e6d1780 100644 --- a/local/recipes/system/redbear-info/source/src/main.rs +++ b/local/recipes/system/redbear-info/source/src/main.rs @@ -2859,6 +2859,76 @@ fn collect_health_items(runtime: &Runtime, report: &Report<'_>) -> Vec = thermal_zones + .iter() + .filter_map(|zone| { + read_trimmed(runtime, &format!("/scheme/acpi/thermal/{zone}/temperature")) + }) + .collect(); + let avg_temp = temps.iter().filter_map(|t| t.parse::().ok()).sum::() + / temps.len().max(1) as f64; + let state = if avg_temp > 85.0 { + HealthState::Critical + } else if avg_temp > 70.0 { + HealthState::Warning + } else { + HealthState::Healthy + }; + items.push(HealthItem { + label: "Thermal", + state, + detail: format!("{} zone(s), avg {:.1}°C", thermal_zones.len(), avg_temp), + }); + } else { + items.push(HealthItem { + label: "Thermal", + state: HealthState::Warning, + detail: "no thermal zones".to_string(), + }); + } + + let fans = runtime.read_dir_names("/scheme/acpi/fan").unwrap_or_default(); + if !fans.is_empty() { + let active = fans + .iter() + .filter(|fan| { + read_trimmed(runtime, &format!("/scheme/acpi/fan/{fan}/status")) + .map(|s| s == "on") + .unwrap_or(false) + }) + .count(); + items.push(HealthItem { + label: "Fans", + state: HealthState::Healthy, + detail: format!("{} fan(s), {} active", fans.len(), active), + }); + } else { + items.push(HealthItem { + label: "Fans", + state: HealthState::Warning, + detail: "no fan devices".to_string(), + }); + } + + let cstate_policy = read_trimmed(runtime, "/scheme/sys/cstate_policy"); + let cstates = runtime.read_dir_names("/scheme/acpi/cstates").unwrap_or_default(); + if !cstates.is_empty() { + let max_policy = cstate_policy.as_deref().unwrap_or("unlimited"); + items.push(HealthItem { + label: "C-states", + state: HealthState::Healthy, + detail: format!("{} processor(s), policy={}", cstates.len(), max_policy), + }); + } else { + items.push(HealthItem { + label: "C-states", + state: HealthState::Warning, + detail: "no C-state surface".to_string(), + }); + } + items } diff --git a/local/recipes/system/thermald/source/src/main.rs b/local/recipes/system/thermald/source/src/main.rs index 7a38249872..af49661fb5 100644 --- a/local/recipes/system/thermald/source/src/main.rs +++ b/local/recipes/system/thermald/source/src/main.rs @@ -536,7 +536,7 @@ fn monitor_loop(shared: Arc>) -> ! { loop { if !Path::new(ACPI_THERMAL_ROOT).exists() { if !warned_missing_surface { - warn!( + log::info!( "{} is unavailable; thermald will keep polling and serve an empty thermal surface", ACPI_THERMAL_ROOT, ); diff --git a/local/recipes/system/udev-shim/source/src/main.rs b/local/recipes/system/udev-shim/source/src/main.rs index fc5d67986b..436e3512ef 100644 --- a/local/recipes/system/udev-shim/source/src/main.rs +++ b/local/recipes/system/udev-shim/source/src/main.rs @@ -129,13 +129,22 @@ fn main() { let scheme = Arc::new(Mutex::new(scheme)); let scheme_clone = Arc::clone(&scheme); thread::spawn(move || { + let mut last_count = 0usize; loop { thread::sleep(Duration::from_secs(2)); if let Ok(mut s) = scheme_clone.lock() { match s.scan_pci_devices() { - Ok(n) if n > 0 => info!("udev-shim: hotplug detected {} device(s)", n), + Ok(n) => { + if n != last_count { + if n > last_count { + info!("udev-shim: hotplug detected {} device(s) (total {})", n - last_count, n); + } else { + info!("udev-shim: device removal detected, {} device(s) remaining", n); + } + last_count = n; + } + } Err(e) => error!("udev-shim: hotplug scan failed: {}", e), - _ => {} } } } diff --git a/local/recipes/system/udev-shim/source/src/naming.rs b/local/recipes/system/udev-shim/source/src/naming.rs index ea1d36f15e..67509e4b55 100644 --- a/local/recipes/system/udev-shim/source/src/naming.rs +++ b/local/recipes/system/udev-shim/source/src/naming.rs @@ -2,6 +2,8 @@ use std::fs; use std::io; use std::os::unix::fs::symlink; use std::path::Path; +use std::thread; +use std::time::Duration; const DEFAULT_UDEV_RULES: &str = r#"# Network interface naming SUBSYSTEM=="net", KERNEL=="enp*", NAME="$kernel" @@ -74,8 +76,26 @@ pub fn write_default_rules_file() -> io::Result<&'static str> { fs::create_dir_all(dir)?; let path = dir.join("50-default.rules"); - fs::write(&path, default_udev_rules())?; - Ok("/etc/udev/rules.d/50-default.rules") + let contents = default_udev_rules(); + + if fs::metadata(&path).is_ok() { + let _ = fs::remove_file(&path); + } + + for attempt in 0..3 { + match fs::write(&path, contents) { + Ok(()) => return Ok("/etc/udev/rules.d/50-default.rules"), + Err(e) if e.kind() == io::ErrorKind::BrokenPipe && attempt < 2 => { + thread::sleep(Duration::from_millis(50)); + } + Err(e) if e.kind() == io::ErrorKind::AlreadyExists => { + return Ok("/etc/udev/rules.d/50-default.rules"); + } + Err(e) => return Err(e), + } + } + + unreachable!("write_default_rules_file loop always returns or errors") } fn parse_hex_byte(value: &str) -> Option { diff --git a/local/recipes/tui/mc/redox.patch b/local/recipes/tui/mc/redox.patch index de3dc8ddc1..71978daade 100644 --- a/local/recipes/tui/mc/redox.patch +++ b/local/recipes/tui/mc/redox.patch @@ -3,7 +3,7 @@ diff --git a/src/subshell/common.c b/src/subshell/common.c +++ b/src/subshell/common.c @@ -95,6 +95,45 @@ #endif - #endif + #endif /* HAVE_OPENPTY */ +#ifdef __redox__ +static int @@ -64,4 +64,4 @@ diff --git a/configure b/configure if test $ac_list_mounted_fs = found; then gl_cv_list_mounted_fs=yes - + diff --git a/recipes/core/base-initfs/recipe.toml b/recipes/core/base-initfs/recipe.toml index 717e2c2a29..a93e7262e0 100644 --- a/recipes/core/base-initfs/recipe.toml +++ b/recipes/core/base-initfs/recipe.toml @@ -6,8 +6,11 @@ template = "custom" dependencies = [ "redoxfs", "ion", + "driver-manager", ] script = """ +set -eo pipefail + BINS=( init logd @@ -23,7 +26,6 @@ BINS=( lived nvmed pcid - pcid-spawner rtcd vesad ) @@ -71,8 +73,8 @@ mkdir -p "${COOKBOOK_BUILD}/initfs/lib/init.d" cp "${COOKBOOK_SOURCE}/init.initfs.d"/* "${COOKBOOK_BUILD}/initfs/lib/init.d/" -mkdir -pv "${COOKBOOK_BUILD}/initfs/lib/pcid.d" -cp -v "${COOKBOOK_SOURCE}/drivers/initfs.toml" "${COOKBOOK_BUILD}/initfs/lib/pcid.d/initfs.toml" +mkdir -pv "${COOKBOOK_BUILD}/initfs/lib/drivers.d" +cp -v "${COOKBOOK_SOURCE}/drivers/initfs-storage.toml" "${COOKBOOK_BUILD}/initfs/lib/drivers.d/00-storage.toml" export CARGO_PROFILE_RELEASE_OPT_LEVEL=s export CARGO_PROFILE_RELEASE_PANIC=abort @@ -85,7 +87,7 @@ mkdir -pv "${COOKBOOK_BUILD}/initfs/bin" "${COOKBOOK_BUILD}/initfs/lib/drivers" for bin in "${BINS[@]}" do case "${bin}" in - init | logd | ramfs | randd | zerod | pcid | pcid-spawner | fbbootlogd | fbcond | inputd | vesad | lived | ps2d | acpid | bcm2835-sdhcid | rtcd | hwd) + init | logd | ramfs | randd | zerod | fbbootlogd | fbcond | inputd | vesad | lived | ps2d | acpid | bcm2835-sdhcid | rtcd | hwd | pcid) cp -v "target/${TARGET}/${build_type}/${bin}" "${COOKBOOK_BUILD}/initfs/bin" ;; *) @@ -96,6 +98,7 @@ done cp "${COOKBOOK_SYSROOT}/usr/bin/redoxfs" "${COOKBOOK_BUILD}/initfs/bin" cp "${COOKBOOK_SYSROOT}/usr/bin/ion" "${COOKBOOK_BUILD}/initfs/bin" +cp "${COOKBOOK_SYSROOT}/usr/bin/driver-manager" "${COOKBOOK_BUILD}/initfs/bin" ARCH="$(echo "${GNU_TARGET}" | cut -d - -f1)" RUSTFLAGS="$RUSTFLAGS -Ctarget-feature=+crt-static -Clink-arg=-nostartfiles -Clink-arg=-nostdlib" cargo \ diff --git a/recipes/core/base/recipe.toml b/recipes/core/base/recipe.toml index 405d9b6adb..5d8d3ae74e 100644 --- a/recipes/core/base/recipe.toml +++ b/recipes/core/base/recipe.toml @@ -4,8 +4,188 @@ rev = "463f76b9608a896e6f6c9f63457f57f6409873c7" patches = [ "P0-daemon-fix-init-notify-unwrap.patch", "P0-workspace-add-bootstrap.patch", - "P0-bootstrap-workspace-fix.patch", + "P0-init-continuous-scheduling.patch", + "P0-dhcpd-auto-iface.patch", + "P0-procmgr-sigchld-debug.patch", + "P0-pcid-mcfg-diagnostics.patch", + "P0-ihdgd-intel-gpu-ids.patch", + "P0-acpid-dmar-fix.patch", + # P1: acpid EC runtime and AML physmem hardening (narrow ACPI runtime patches) + "P1-acpid-ec-runtime.patch", + "P1-acpid-runtime-hardening.patch", + # Stale patches needing recreation: P1-pcid-uevent-surface, P2-boot-runtime-fixes, + # P2-hwd-misc, P2-pcid-cfg-access, P3-xhci-device-hardening, P6-cpufreqd-real-impl "P2-i2c-gpio-ucsi-drivers.patch", + "P0-i2c-control-response-empty.patch", + "P2-ihdad-graceful-init.patch", + "P2-boot-logging.patch", + "P2-init-acpid-wiring.patch", + "P2-hwd-remove-acpid-spawn.patch", + "P2-initfs-pcid-service.patch", + "P2-misc-daemon-fixes.patch", + "P9-fix-so-pecred.patch", + "P3-inputd-keymap-bridge.patch", + # P3: ps2d consolidated — LED feedback, mouse resend, fastfail, Intellimouse2, controller init robustness, non-x86 fallback + "P7-ps2d-intellimouse2-leds-controller-init.patch", + "P3-usbhidd-hardening.patch", + "P3-init-colored-output.patch", + "P4-logd-persistent-logging.patch", + "P4-acpi-shutdown-hardening.patch", + "P4-acpi-s3-sleep.patch", + "P4-pcid-public-client-channel.patch", + "P4-pcid-config-scheme.patch", + "P4-pcid-spawner-pci-coordinate-env.patch", + "P4-initfs-usb-drm-services.patch", + "P4-initfs-release-virtio-gpu.patch", + "P4-initfs-network-services.patch", + "P4-initfs-getty-services.patch", + "P4-initfs-dbus-services.patch", + "P4-fbcond-scrollback.patch", + "P4-ucsid-estale-graceful.patch", + "P4-acpi-estale-graceful.patch", + "P4-hwd-estale-graceful.patch", + # P5-i2c-hidd-estale-retry: REDUNDANT — ESTALE retry already provided by P2 + P4-acpi-estale + "P5-acpid-dmi-endpoint.patch", + "P4-thermal-daemon.patch", + "P4-thermald-workspace.patch", + "P6-driver-main-fixes.patch", + "P6-driver-new-modules.patch", + "P9-init-scheduler-completed.patch", + "P6-init-requires-hard-dep.patch", + "P2-pcid-acpid-graceful-fd.patch", + "P5-fbbootlogd-fbcond-graceful-drm.patch", + "P7-acpid-shared-pcifd.patch", + "P6-rtcd-no-ocreat.patch", + "P6-pcid-acpid-fd-transfer.patch", + "P15-7-init-service-timeout.patch", + # P15-8-init-cycle-detection: REDUNDANT — cycle detection already included in P6-init-requires-hard-dep + "P18-1-daemon-restart.patch", + "P18-3-msi-msix-enablement.patch", + "P18-5-acpid-robustness.patch", + "P18-8-bounded-ipcd-queues.patch", + "P18-9-msi-allocation-resilience.patch", + "P19-init-startup-hardening.patch", + "P19-acpid-startup-hardening.patch", + "P20-ramfs-requires-randd.patch", + "P21-boot-daemon-graceful-panic.patch", + "P23-rootfs-hard-dep-on-drivers.patch", + "P24-acpi-s5-derivation-shutdown-semantics.patch", + "P25-fbcond-vesa-fallback.patch", + "P26-driver-manager-initfs-conversion.patch", + "P27-fbcond-borrow-fix.patch", + "P28-init-skip-unmet-conditions.patch", + "P30-acpid-graceful-scheme-exists.patch", + "P31-xhcid-restore-interrupts.patch", + "P32-acpid-graceful-boot.patch", + "P33-vesad-graceful-boot.patch", + "P34-fbcond-fbbootlogd-env.patch", + "P35-fbcond-fbbootlogd-init.patch", + "P36-graphics-scheme-graceful-init.patch", + "P37-smolnetd-ready-after-init.patch", + "P38-vesad-eventqueue-deadlock.patch", + "P39-pci-allocate-interrupt-vector-graceful.patch", + "P40-bar-rs-graceful.patch", + "P41-common-init-graceful.patch", + "P42-inputd-graceful-fallback.patch", + "P43-dhcpd-requires-hard-dep.patch", + "P44-acpid-thermal-zones.patch", + # P54: Add missing thermal.rs module for P44 + "P54-acpid-thermal-module.patch", + # P45: Migrate e1000d and ixgbed to MSI-X via pci_allocate_interrupt_vector + "P45-net-msix-adoption.patch", + # P46: Migrate ahcid and ac97d to MSI-X via pci_allocate_interrupt_vector + "P46-storage-audio-msix.patch", + # P46b: Fix ac97d mutable borrow of pcid_handle (required by pci_allocate_interrupt_vector) + "P46b-ac97d-mutable-fix.patch", + # P47: Update thermald to read from P44 thermal zones and coretempd + "P47-thermald-backend.patch", + # P48: Add ACPI fan device discovery and status exposure + "P48-acpid-fan-support.patch", + # P49: Add IRQ affinity logging and CPU tracking to pcid + "P49-irq-affinity-logging.patch", + # P50: Add structured logging rate limiter and thermald integration + "P50-structured-logging.patch", + # P51: Add per-service log files and size-based rotation to logd + "P51-logd-rotation.patch", + # P52: Add ACPI C-state discovery and thermal-based C-state policy + "P52-acpid-cstates.patch", + # P53: Add e1000d interrupt throttling rate (ITR) coalescing + "P53-e1000d-itr-coalescing.patch", + # P55: Add JSON structured log format option to logd + "P55-logd-json-format.patch", +] + +[package] +installs = [ + "/lib/pcid.d/ac97d.toml", + "/lib/pcid.d/e1000d.toml", + "/lib/pcid.d/ihdad.toml", + "/lib/pcid.d/ihdgd.toml", + "/lib/pcid.d/ixgbed.toml", + "/lib/pcid.d/rtl8139d.toml", + "/lib/pcid.d/rtl8168d.toml", + "/lib/pcid.d/vboxd.toml", + "/lib/pcid.d/virtio-netd.toml", + "/lib/pcid.d/xhcid.toml", + "/usr/bin/audiod", + "/usr/bin/dhcpd", + "/usr/bin/dw-acpi-i2cd", + "/usr/bin/gpiod", + "/usr/bin/i2cd", + "/usr/bin/i2c-gpio-expanderd", + "/usr/bin/i2c-hidd", + "/usr/bin/inputd", + "/usr/bin/intel-gpiod", + "/usr/bin/ipcd", + "/usr/bin/netstack", + "/usr/bin/pcid", + "/usr/bin/pcid-spawner", + "/usr/bin/ptyd", + "/usr/bin/redoxerd", + "/usr/bin/smolnetd", + "/usr/bin/ucsid", + "/usr/lib/drivers/ac97d", + "/usr/lib/drivers/ahcid", + "/usr/lib/drivers/amd-mp2-i2cd", + "/usr/lib/drivers/e1000d", + "/usr/lib/drivers/ihdad", + "/usr/lib/drivers/ihdgd", + "/usr/lib/drivers/ided", + "/usr/lib/drivers/intel-lpss-i2cd", + "/usr/lib/drivers/intel-thc-hidd", + "/usr/lib/drivers/ixgbed", + "/usr/lib/drivers/ps2d", + "/usr/lib/drivers/rtl8139d", + "/usr/lib/drivers/rtl8168d", + "/usr/lib/drivers/sb16d", + "/usr/lib/drivers/thermald", + "/usr/lib/drivers/usbctl", + "/usr/lib/drivers/usbhidd", + "/usr/lib/drivers/usbhubd", + "/usr/lib/drivers/usbscsid", + "/usr/lib/drivers/vboxd", + "/usr/lib/drivers/virtio-gpud", + "/usr/lib/drivers/virtio-netd", + "/usr/lib/drivers/xhcid", + "/usr/lib/init.d/00_base.target", + "/usr/lib/init.d/00_ipcd.service", + "/usr/lib/init.d/00_pcid-spawner.service", + "/usr/lib/init.d/00_ptyd.service", + "/usr/lib/init.d/00_sudo.service", + "/usr/lib/init.d/00_tmp", + "/usr/lib/init.d/05_boot_essential.target", + "/usr/lib/init.d/10_dhcpd.service", + "/usr/lib/init.d/10_net.target", + "/usr/lib/init.d/10_smolnetd.service", + "/usr/lib/init.d/12_boot_late.target", + "/usr/lib/init.d/12_dbus.service", + "/usr/lib/init.d/13_seatd.service", + "/usr/lib/init.d/13_sessiond.service", + "/usr/lib/init.d/20_audiod.service", + "/usr/lib/init.d/29_activate_console.service", + "/usr/lib/init.d/30_console.service", + "/usr/lib/init.d/30_thermald.service", + "/usr/lib/init.d/31_debug_console.service", ] [build] @@ -49,10 +229,13 @@ BINS=( ixgbed pcid pcid-spawner + acpid + redoxerd rtl8139d rtl8168d usbctl usbhidd + thermald usbhubd ucsid usbscsid @@ -61,14 +244,13 @@ BINS=( xhcid i2cd inputd - redoxerd ) # Add additional drivers to the list to build, that are not in drivers-initfs # depending on the target architecture case "${TARGET}" in i586-unknown-redox | i686-unknown-redox | x86_64-unknown-redox) - BINS+=(ac97d sb16d vboxd) + BINS+=(ac97d ahcid ided nvmed ps2d sb16d vboxd) ;; *) ;; @@ -92,7 +274,7 @@ done $(for bin in "${EXISTING_BINS[@]}"; do echo "-p" "${bin}"; done) for bin in "${EXISTING_BINS[@]}" do - if [[ "${bin}" == "gpiod" || "${bin}" == "i2c-gpio-expanderd" || "${bin}" == "intel-gpiod" || "${bin}" == "i2cd" || "${bin}" == "dw-acpi-i2cd" || "${bin}" == "i2c-hidd" || "${bin}" == "inputd" || "${bin}" == "pcid" || "${bin}" == "pcid-spawner" || "${bin}" == "redoxerd" || "${bin}" == "ucsid" ]]; then + if [[ "${bin}" == "gpiod" || "${bin}" == "i2c-gpio-expanderd" || "${bin}" == "intel-gpiod" || "${bin}" == "i2cd" || "${bin}" == "dw-acpi-i2cd" || "${bin}" == "acpid" || "${bin}" == "thermald" || "${bin}" == "i2c-hidd" || "${bin}" == "inputd" || "${bin}" == "pcid" || "${bin}" == "pcid-spawner" || "${bin}" == "redoxerd" || "${bin}" == "ucsid" ]]; then cp -v "target/${TARGET}/${build_type}/${bin}" "${COOKBOOK_STAGE}/usr/bin" else cp -v "target/${TARGET}/${build_type}/${bin}" "${COOKBOOK_STAGE}/usr/lib/drivers" diff --git a/recipes/core/kernel/recipe.toml b/recipes/core/kernel/recipe.toml index f9d7f16067..8be1cda630 100644 --- a/recipes/core/kernel/recipe.toml +++ b/recipes/core/kernel/recipe.toml @@ -12,6 +12,41 @@ patches = [ "../../../local/patches/kernel/P1-ioapic-hpet-nmi-v2.patch", "../../../local/patches/kernel/P9-numa-topology.patch", "../../../local/patches/kernel/P9-proc-lock-ordering.patch", + "../../../local/patches/kernel/P9-percpu-context-switch.patch", + "../../../local/patches/kernel/P9-broadcast-tlb-shootdown.patch", + "../../../local/patches/kernel/P9-ioapic-irq-affinity.patch", + "../../../local/patches/kernel/P10-irq-affinity-wiring.patch", + "../../../local/patches/kernel/P11-mcs-lock.patch", + "../../../local/patches/kernel/P12-range-tlb-flush.patch", + "../../../local/patches/kernel/P13-priority-inheritance.patch", + "../../../local/patches/kernel/P14-numa-topology.patch", + "../../../local/patches/kernel/P15-1-ap-cpu-id-race.patch", + "../../../local/patches/kernel/P15-4-mcs-pi-ordering.patch", + "../../../local/patches/kernel/P15-10-tlb-range-ordering.patch", + "../../../local/patches/kernel/P16-3-max-cpu-256.patch", + "../../../local/patches/kernel/P16-1-sipi-timing.patch", + "../../../local/patches/kernel/P16-4a-sdt-checksum.patch", + "../../../local/patches/kernel/P16-4b-madt-validation.patch", + "../../../local/patches/kernel/P17-2a-percpu-waiting.patch", + "../../../local/patches/kernel/P17-2b-transitive-pi.patch", + "../../../local/patches/kernel/P17-4-configurable-preempt.patch", + "../../../local/patches/kernel/P17-1-numa-selection.patch", + "../../../local/patches/kernel/P17-3-sched-affinity.patch", + "../../../local/patches/kernel/P17-3-syscall-dispatch.patch", + "../../../local/patches/kernel/P19-2-irq-debug.patch", + # P20: x2APIC ICR mode fix (32-bit dest field for x2APIC, 8-bit for xAPIC) + "../../../local/patches/kernel/P20-x2apic-icr-mode-fix.patch", + # P21: x2APIC SMP bring-up fix — skip 8-bit LocalApic entries when x2APIC + # is active (BSP ID mismatch causes all APs to be skipped on bare metal Intel) + "../../../local/patches/kernel/P21-x2apic-smp-fix.patch", + # P22: x2APIC MADT fallback — when x2APIC is active but MADT has no + # LocalX2Apic entries (QEMU, some BIOS), fall back to processing LocalApic + # entries with zero-extended IDs using x2APIC 64-bit ICR format + "../../../local/patches/kernel/P22-x2apic-madt-fallback.patch", + # P23: sys:msr scheme — kernel MSR read/write via /scheme/sys/msr// + "../../../local/patches/kernel/P23-sys-msr-scheme.patch", + # P25: Comprehensive cpuidle framework with deep C-states (C1-C7) + "../../../local/patches/kernel/P25-cpuidle-deep-cstates.patch", ] [build] diff --git a/recipes/core/kernel/source/src/acpi/madt/arch/x86.rs b/recipes/core/kernel/source/src/acpi/madt/arch/x86.rs index e8625a205a..16c35bdaa9 100644 --- a/recipes/core/kernel/source/src/acpi/madt/arch/x86.rs +++ b/recipes/core/kernel/source/src/acpi/madt/arch/x86.rs @@ -3,6 +3,8 @@ use core::{ sync::atomic::{AtomicU8, Ordering}, }; +use x86::time::rdtsc; + use crate::{ arch::{ device::local_apic::the_local_apic, @@ -18,10 +20,95 @@ use crate::{ use super::{Madt, MadtEntry}; +use alloc::collections::BTreeSet; +use alloc::vec::Vec; + +/// Maximum number of APIC→CPU mappings we track for NUMA topology. +const MAX_APIC_MAPPINGS: usize = 256; + +struct ApicMapping { + apic_id: u32, + cpu_id: LogicalCpuId, +} + +const UNINIT_MAPPING: ApicMapping = ApicMapping { apic_id: u32::MAX, cpu_id: LogicalCpuId::new(0) }; + +static mut APIC_MAPPINGS: [ApicMapping; MAX_APIC_MAPPINGS] = [UNINIT_MAPPING; MAX_APIC_MAPPINGS]; +static mut APIC_MAPPING_COUNT: usize = 0; + +unsafe fn record_apic_mapping(apic_id: u32, cpu_id: LogicalCpuId) { + let count = APIC_MAPPING_COUNT; + if count < MAX_APIC_MAPPINGS { + APIC_MAPPINGS[count] = ApicMapping { apic_id, cpu_id }; + APIC_MAPPING_COUNT = count + 1; + } +} + const AP_SPIN_LIMIT: u32 = 1_000_000; const TRAMPOLINE: usize = 0x8000; static TRAMPOLINE_DATA: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/trampoline")); +/// Estimate TSC frequency in MHz from CPUID. +/// +/// Tries CPUID leaf 0x16 (Processor Frequency Information) first, +/// then CPUID leaf 0x15 (TSC/Core Crystal Clock Ratio). +/// Returns None if frequency cannot be determined. +fn tsc_freq_mhz_cpuid() -> Option { + let max_leaf = unsafe { core::arch::x86_64::__cpuid(0).eax as u32 }; + + // CPUID leaf 0x16: EAX = Core Base Frequency in MHz (Intel) + if max_leaf >= 0x16 { + let mhz = unsafe { core::arch::x86_64::__cpuid(0x16) }.eax as u64; + if mhz > 0 { + return Some(mhz); + } + } + + // CPUID leaf 0x15: EAX = denominator, EBX = numerator, ECX = crystal Hz + if max_leaf >= 0x15 { + let res = unsafe { core::arch::x86_64::__cpuid(0x15) }; + let denom = res.eax as u64; + let numer = res.ebx as u64; + let crystal_hz = res.ecx as u64; + if denom > 0 && numer > 0 && crystal_hz > 0 { + // TSC freq = crystal_hz * numer / denom + let tsc_hz = crystal_hz * numer / denom; + return Some(tsc_hz / 1_000_000); // Hz → MHz + } + } + + None +} + +/// Early-boot microsecond delay using the Time Stamp Counter. +/// +/// Uses CPUID-based TSC frequency estimation when available. +/// Falls back to a conservative spin loop calibrated for the +/// minimum expected CPU speed (1 GHz). +/// +/// # Safety +/// Must only be called after the BSP TSC is running (always true +/// after CPU reset on x86). +fn early_udelay(us: u64) { + if let Some(mhz) = tsc_freq_mhz_cpuid() { + // TSC-based delay: precise on invariant TSC (all modern x86). + // MHz = cycles per µs. + let target = unsafe { rdtsc() } + us * mhz; + while unsafe { rdtsc() } < target { + hint::spin_loop(); + } + } else { + // Fallback: conservative spin loop. + // spin_loop() (PAUSE) is ~40 cycles on modern Intel, ~1 on AMD. + // At 1 GHz minimum: 1000 cycles/µs ÷ 40 cycles/iter = 25 iters/µs. + // Use 50 iters/µs for safety margin on slower/variable CPUs. + let iters = us.saturating_mul(50); + for _ in 0..iters { + hint::spin_loop(); + } + } +} + fn current_x2apic_processor_uid(madt: &Madt, apic_id: u32) -> Option { madt.iter().find_map(|entry| match entry { MadtEntry::LocalX2Apic(x2apic) if x2apic.x2apic_id == apic_id => Some(x2apic.processor_uid), @@ -61,6 +148,10 @@ pub(super) fn init(madt: Madt) { } if cfg!(not(feature = "multi_core")) { + unsafe { + record_apic_mapping(me.get(), LogicalCpuId::new(0)); + } + crate::numa::init_default(); return; } @@ -94,22 +185,225 @@ pub(super) fn init(madt: Madt) { } } + // Detect whether MADT contains any LocalX2Apic entries. + // Some firmware (notably QEMU and some older BIOS) provides only 8-bit + // LocalApic entries even when the CPU supports x2APIC. In that case we must + // fall back to processing LocalApic entries with zero-extended IDs. + let has_x2apic_entries = madt.iter().any(|e| matches!(e, MadtEntry::LocalX2Apic(_))); + let x2apic_fallback = local_apic.x2 && !has_x2apic_entries; + if x2apic_fallback { + warn!("MADT: x2APIC mode active but no LocalX2Apic entries found; falling back to LocalApic entries with zero-extended IDs"); + } + unsafe { let preliminary_cpu_count = madt .iter() .filter(|entry| match entry { - MadtEntry::LocalApic(local) => u32::from(local.id) == me.get() || local.flags & 1 == 1, - MadtEntry::LocalX2Apic(local) => local.x2apic_id == me.get() || local.flags & 1 == 1, + // When x2APIC is active, LocalApic entries use 8-bit IDs that don't + // match the BSP's 32-bit x2APIC ID. Use LocalX2Apic entries instead. + MadtEntry::LocalApic(local) if !local_apic.x2 => { + u32::from(local.id) == me.get() || local.flags & 1 == 1 + } + MadtEntry::LocalApic(local) if local_apic.x2 && x2apic_fallback => { + u32::from(local.id) == me.get() || local.flags & 1 == 1 + } + MadtEntry::LocalApic(_) => false, + // xAPIC mode: cannot use 32-bit x2APIC IDs via 8-bit ICR. + // Skip LocalX2Apic entries and use LocalApic exclusively. + MadtEntry::LocalX2Apic(local) if local_apic.x2 => { + local.x2apic_id == me.get() || local.flags & 1 == 1 + } + MadtEntry::LocalX2Apic(_) => false, _ => false, }) .count(); crate::profiling::allocate(preliminary_cpu_count as u32); } + // Firmware bug detection: check for duplicate APIC IDs in MADT. + // Some firmware (especially on early BIOS/UEFI) may list the same + // processor multiple times. Keep first occurrence, warn on duplicates. + let mut seen_apic_ids: BTreeSet = BTreeSet::new(); + { + let _ = seen_apic_ids.insert(me.get()); // BSP + for entry in madt.iter() { + match entry { + MadtEntry::LocalApic(local) if local.flags & 1 == 1 && !local_apic.x2 => { + let id = u32::from(local.id); + if !seen_apic_ids.insert(id) { + warn!("MADT: duplicate APIC ID {} in LocalApic entry, firmware bug", id); + } + } + MadtEntry::LocalApic(local) if local.flags & 1 == 1 && local_apic.x2 => { + if x2apic_fallback { + let id = u32::from(local.id); + if !seen_apic_ids.insert(id) { + warn!("MADT: duplicate APIC ID {} in LocalApic entry (x2APIC fallback), firmware bug", id); + } + } else { + debug!("MADT: ignoring 8-bit LocalApic ID {} in x2APIC mode", local.id); + } + } + MadtEntry::LocalX2Apic(local) if local.flags & 1 == 1 && local_apic.x2 => { + let id = local.x2apic_id; + if !seen_apic_ids.insert(id) { + warn!("MADT: duplicate x2APIC ID {} in LocalX2Apic entry, firmware bug", id); + } + } + MadtEntry::LocalX2Apic(local) if local.flags & 1 == 1 && !local_apic.x2 => { + // xAPIC mode: skip 32-bit x2APIC IDs; dedup only among LocalApic entries. + let id = local.x2apic_id; // Copy from packed struct + debug!("MADT: ignoring 32-bit x2APIC ID {} in xAPIC mode", id); + } + _ => {} + } + } + } + for madt_entry in madt.iter() { debug!(" {:x?}", madt_entry); if let MadtEntry::LocalApic(ap_local_apic) = madt_entry { - if u32::from(ap_local_apic.id) == me.get() { + // x2APIC mode: LocalApic entries have 8-bit IDs that don't match + // the BSP's 32-bit x2APIC ID. All entries would be treated as APs, + // and SIPI would target the wrong processors. Skip them and rely + // on LocalX2Apic entries exclusively. + if local_apic.x2 && !x2apic_fallback { + debug!( + " Skipping 8-bit LocalApic id={} (x2APIC active, using LocalX2Apic entries)", + ap_local_apic.id + ); + } else if local_apic.x2 && x2apic_fallback { + let apic_id = u32::from(ap_local_apic.id); + if apic_id == me.get() { + debug!(" This is my local APIC (x2APIC fallback, id={})", apic_id); + } else if ap_local_apic.flags & 1 == 1 { + let alloc = match allocate_p2frame(4) { + Some(frame) => frame, + None => { + println!("KERNEL AP: CPU {} no memory for stack, skipping", apic_id); + continue; + } + }; + let stack_start = RmmA::phys_to_virt(alloc.base()).data(); + let stack_end = stack_start + (PAGE_SIZE << 4); + + let cpu_id = LogicalCpuId::new(crate::CPU_COUNT.fetch_add(1, Ordering::SeqCst)); + if cpu_id.get() >= crate::cpu_set::MAX_CPU_COUNT { + println!( + "KERNEL AP: CPU {} exceeds logical CPU limit, skipping", + apic_id + ); + continue; + } + + let pcr_ptr = crate::arch::gdt::allocate_and_init_pcr(cpu_id, stack_end); + let idt_ptr = crate::arch::idt::allocate_and_init_idt(cpu_id); + + let args = KernelArgsAp { + stack_end: stack_end as *mut u8, + cpu_id, + pcr_ptr, + idt_ptr, + }; + + let ap_ready = (TRAMPOLINE + 8) as *mut u64; + let ap_args_ptr = unsafe { ap_ready.add(1) }; + let ap_page_table = unsafe { ap_ready.add(2) }; + let ap_code = unsafe { ap_ready.add(3) }; + + unsafe { + ap_ready.write(0); + ap_args_ptr.write(&args as *const _ as u64); + ap_page_table.write(page_table_physaddr as u64); + #[expect(clippy::fn_to_numeric_cast)] + ap_code.write(kstart_ap as u64); + + core::sync::atomic::fence(Ordering::SeqCst); + }; + AP_READY.store(false, Ordering::SeqCst); + + // Clear APIC Error Status Register before starting AP. + unsafe { local_apic.esr(); } + + // Send INIT IPI (Assert) — x2APIC uses 64-bit ICR format. + { + let mut icr = 0x4500u64; + icr |= u64::from(apic_id) << 32; + local_apic.set_icr(icr); + } + + // Intel SDM Vol 3A §8.4.4: wait 10ms after INIT deassert + early_udelay(10_000); + + // Send START IPI #1 + { + let ap_segment = (TRAMPOLINE >> 12) & 0xFF; + let mut icr = 0x0600 | ap_segment as u64; + icr |= u64::from(apic_id) << 32; + local_apic.set_icr(icr); + } + + early_udelay(200); + + // Send START IPI #2 (recommended for compatibility) + { + let ap_segment = (TRAMPOLINE >> 12) & 0xFF; + let mut icr = 0x0600 | ap_segment as u64; + icr |= u64::from(apic_id) << 32; + local_apic.set_icr(icr); + } + + early_udelay(200); + + // Check ESR for delivery errors after SIPI sequence. + let esr_val = unsafe { local_apic.esr() }; + if esr_val != 0 { + println!( + "KERNEL AP: CPU {} SIPI delivery error (ESR={:#x}), continuing", + apic_id, esr_val + ); + } + + let mut trampoline_ready = false; + for _ in 0..AP_SPIN_LIMIT { + if unsafe { (*ap_ready.cast::()).load(Ordering::SeqCst) } != 0 { + trampoline_ready = true; + break; + } + hint::spin_loop(); + } + if !trampoline_ready { + println!("KERNEL AP: CPU {} trampoline timeout, skipping", apic_id); + continue; + } + + let mut kernel_ready = false; + for _ in 0..AP_SPIN_LIMIT { + if AP_READY.load(Ordering::SeqCst) { + kernel_ready = true; + break; + } + hint::spin_loop(); + } + if !kernel_ready { + println!("KERNEL AP: CPU {} AP_READY timeout, skipping", apic_id); + continue; + } + + // Record APIC→CPU mapping for NUMA topology. + unsafe { + record_apic_mapping(apic_id, cpu_id); + } + // Set NUMA node from SRAT data. + if let Some(percpu) = crate::percpu::get_for_cpu(cpu_id) { + if let Some(node) = crate::acpi::srat::numa_node_for_apic(apic_id) { + percpu.numa_node.set(node); + } + } + + RmmA::invalidate_all(); + } + } else if u32::from(ap_local_apic.id) == me.get() { debug!(" This is my local APIC"); } else if ap_local_apic.flags & 1 == 1 { // Allocate a stack @@ -123,15 +417,16 @@ pub(super) fn init(madt: Madt) { let stack_start = RmmA::phys_to_virt(alloc.base()).data(); let stack_end = stack_start + (PAGE_SIZE << 4); - let next_cpu = crate::CPU_COUNT.load(Ordering::Relaxed); - if next_cpu >= crate::cpu_set::MAX_CPU_COUNT { + // Atomically allocate a CPU ID — fetch_add is SeqCst so that + // all later stores (PercpuBlock, NUMA node) are ordered after. + let cpu_id = LogicalCpuId::new(crate::CPU_COUNT.fetch_add(1, Ordering::SeqCst)); + if cpu_id.get() >= crate::cpu_set::MAX_CPU_COUNT { println!( "KERNEL AP: CPU {} exceeds logical CPU limit, skipping", ap_local_apic.id ); continue; } - let cpu_id = LogicalCpuId::new(next_cpu); let pcr_ptr = crate::arch::gdt::allocate_and_init_pcr(cpu_id, stack_end); @@ -157,14 +452,21 @@ pub(super) fn init(madt: Madt) { #[expect(clippy::fn_to_numeric_cast)] ap_code.write(kstart_ap as u64); - // TODO: Is this necessary (this fence)? - core::arch::asm!(""); + // Ensure all trampoline writes are visible to the AP before + // it starts executing. asm!("") is only a compiler barrier; + // fence(SeqCst) is a full hardware memory barrier. + core::sync::atomic::fence(Ordering::SeqCst); }; AP_READY.store(false, Ordering::SeqCst); - // Send INIT IPI + // Clear APIC Error Status Register before starting AP. + // Intel SDM §8.4.4: ESR should be cleared before sending SIPI. + unsafe { local_apic.esr(); } + + // Send INIT IPI (Assert) { - let mut icr = 0x4500; + // ICR: Delivery Mode=INIT(101), Level=Assert, Trigger=Edge + let mut icr = 0x4500u64; if local_apic.x2 { icr |= u64::from(ap_local_apic.id) << 32; } else { @@ -173,20 +475,53 @@ pub(super) fn init(madt: Madt) { local_apic.set_icr(icr); } - // Send START IPI + // Intel SDM Vol 3A §8.4.4: wait 10ms after INIT deassert + // before sending first SIPI. Modern CPUs may need less, + // but 10ms is the safe specification-compliant value. + early_udelay(10_000); + + // Send START IPI #1 { let ap_segment = (TRAMPOLINE >> 12) & 0xFF; - let mut icr = 0x4600 | ap_segment as u64; - + // ICR: Delivery Mode=StartUp(110), Vector=ap_segment + // Note: bit 14 (Level) must be 0 for SIPI per Intel SDM. + let mut icr = 0x0600 | ap_segment as u64; if local_apic.x2 { icr |= u64::from(ap_local_apic.id) << 32; } else { icr |= u64::from(ap_local_apic.id) << 56; } - local_apic.set_icr(icr); } + // Intel SDM: wait 200µs between SIPIs + early_udelay(200); + + // Send START IPI #2 (recommended for compatibility) + { + let ap_segment = (TRAMPOLINE >> 12) & 0xFF; + let mut icr = 0x0600 | ap_segment as u64; + if local_apic.x2 { + icr |= u64::from(ap_local_apic.id) << 32; + } else { + icr |= u64::from(ap_local_apic.id) << 56; + } + local_apic.set_icr(icr); + } + + // Wait briefly for SIPI to be accepted + early_udelay(200); + + // Check ESR for delivery errors after SIPI sequence. + // Bit 5 = Send Accept Error, Bit 6 = Send Illegal Vector. + let esr_val = unsafe { local_apic.esr() }; + if esr_val != 0 { + println!( + "KERNEL AP: CPU {} SIPI delivery error (ESR={:#x}), continuing", + ap_local_apic.id, esr_val + ); + } + // Wait for trampoline ready with timeout let mut trampoline_ready = false; for _ in 0..AP_SPIN_LIMIT { @@ -214,7 +549,16 @@ pub(super) fn init(madt: Madt) { continue; } - crate::CPU_COUNT.fetch_add(1, Ordering::Relaxed); + // Record APIC→CPU mapping for NUMA topology. + unsafe { + record_apic_mapping(u32::from(ap_local_apic.id), cpu_id); + } + // Set NUMA node from SRAT data. + if let Some(percpu) = crate::percpu::get_for_cpu(cpu_id) { + if let Some(node) = crate::acpi::srat::numa_node_for_apic(u32::from(ap_local_apic.id)) { + percpu.numa_node.set(node); + } + } RmmA::invalidate_all(); } @@ -222,7 +566,14 @@ pub(super) fn init(madt: Madt) { let apic_id = ap_x2apic.x2apic_id; let flags = ap_x2apic.flags; - if apic_id == me.get() { + // xAPIC mode: cannot target 32-bit x2APIC IDs via 8-bit ICR. + // Skip LocalX2Apic entries; use LocalApic entries exclusively. + if !local_apic.x2 { + debug!( + " Skipping 32-bit x2APIC id={} (xAPIC mode, using LocalApic entries)", + apic_id + ); + } else if apic_id == me.get() { debug!(" This is my local x2APIC"); } else if flags & 1 == 1 { let alloc = match allocate_p2frame(4) { @@ -235,15 +586,16 @@ pub(super) fn init(madt: Madt) { let stack_start = RmmA::phys_to_virt(alloc.base()).data(); let stack_end = stack_start + (PAGE_SIZE << 4); - let next_cpu = crate::CPU_COUNT.load(Ordering::Relaxed); - if next_cpu >= crate::cpu_set::MAX_CPU_COUNT { + // Atomically allocate a CPU ID — fetch_add is SeqCst so that + // all later stores (PercpuBlock, NUMA node) are ordered after. + let cpu_id = LogicalCpuId::new(crate::CPU_COUNT.fetch_add(1, Ordering::SeqCst)); + if cpu_id.get() >= crate::cpu_set::MAX_CPU_COUNT { println!( "KERNEL AP: CPU {} exceeds logical CPU limit, skipping", apic_id ); continue; } - let cpu_id = LogicalCpuId::new(next_cpu); let pcr_ptr = crate::arch::gdt::allocate_and_init_pcr(cpu_id, stack_end); let idt_ptr = crate::arch::idt::allocate_and_init_idt(cpu_id); @@ -266,38 +618,55 @@ pub(super) fn init(madt: Madt) { ap_page_table.write(page_table_physaddr as u64); #[expect(clippy::fn_to_numeric_cast)] ap_code.write(kstart_ap as u64); - core::arch::asm!(""); + // Ensure all trampoline writes are visible to the AP. + core::sync::atomic::fence(Ordering::SeqCst); } AP_READY.store(false, Ordering::SeqCst); + // Clear APIC Error Status Register before starting AP. + unsafe { local_apic.esr(); } + + // Send INIT IPI (Assert) { let mut icr = 0x4500u64; icr |= u64::from(apic_id) << 32; local_apic.set_icr(icr); } - for _ in 0..100_000 { - hint::spin_loop(); - } + // Intel SDM Vol 3A §8.4.4: wait 10ms after INIT + early_udelay(10_000); + // Send START IPI #1 { let ap_segment = (TRAMPOLINE >> 12) & 0xFF; - let mut icr = 0x4600u64 | ap_segment as u64; + let mut icr = 0x0600u64 | ap_segment as u64; icr |= u64::from(apic_id) << 32; local_apic.set_icr(icr); } - for _ in 0..2_000_000 { - hint::spin_loop(); - } + // Intel SDM: wait 200µs between SIPIs + early_udelay(200); + // Send START IPI #2 (recommended for compatibility) { let ap_segment = (TRAMPOLINE >> 12) & 0xFF; - let mut icr = 0x4600u64 | ap_segment as u64; + let mut icr = 0x0600u64 | ap_segment as u64; icr |= u64::from(apic_id) << 32; local_apic.set_icr(icr); } + // Wait briefly for SIPI acceptance + early_udelay(200); + + // Check ESR for delivery errors. + let esr_val = unsafe { local_apic.esr() }; + if esr_val != 0 { + println!( + "KERNEL AP: CPU {} SIPI delivery error (ESR={:#x}), continuing", + apic_id, esr_val + ); + } + let mut trampoline_ready = false; for _ in 0..AP_SPIN_LIMIT { if unsafe { (*ap_ready.cast::()).load(Ordering::SeqCst) } != 0 { @@ -324,7 +693,17 @@ pub(super) fn init(madt: Madt) { continue; } - crate::CPU_COUNT.fetch_add(1, Ordering::Relaxed); + // Record APIC→CPU mapping for NUMA topology. + unsafe { + record_apic_mapping(apic_id, cpu_id); + } + // Set NUMA node from SRAT data. + if let Some(percpu) = crate::percpu::get_for_cpu(cpu_id) { + if let Some(node) = crate::acpi::srat::numa_node_for_apic(apic_id) { + percpu.numa_node.set(node); + } + } + RmmA::invalidate_all(); } } else if let MadtEntry::LocalApicNmi(nmi) = madt_entry { @@ -342,6 +721,33 @@ pub(super) fn init(madt: Madt) { } } + // Initialize NUMA topology from APIC→CPU mappings and SRAT. + { + let mappings = unsafe { &APIC_MAPPINGS[..APIC_MAPPING_COUNT] }; + let mappings_ref: Vec<(u32, LogicalCpuId)> = mappings + .iter() + .map(|m| (m.apic_id, m.cpu_id)) + .collect(); + crate::numa::init_from_srat(&mappings_ref); + } + // Set BSP's NUMA node from SRAT. + if let Some(node) = crate::acpi::srat::numa_node_for_apic(me.get()) { + crate::percpu::PercpuBlock::current().numa_node.set(node); + } + + // Log final CPU count vs maximum + let cpu_count = crate::CPU_COUNT.load(Ordering::SeqCst); + info!( + "SMP: {} CPUs online (max {})", + cpu_count, crate::cpu_set::MAX_CPU_COUNT + ); + if cpu_count > crate::cpu_set::MAX_CPU_COUNT * 80 / 100 { + warn!( + "SMP: CPU count approaching MAX_CPU_COUNT limit ({}/{})", + cpu_count, crate::cpu_set::MAX_CPU_COUNT + ); + } + // Unmap trampoline if let Some((_frame, _, flush)) = unsafe { KernelMapper::lock_rw() diff --git a/recipes/core/kernel/source/src/acpi/madt/mod.rs b/recipes/core/kernel/source/src/acpi/madt/mod.rs index e792b2e6c8..ed68d6eea8 100644 --- a/recipes/core/kernel/source/src/acpi/madt/mod.rs +++ b/recipes/core/kernel/source/src/acpi/madt/mod.rs @@ -34,6 +34,12 @@ impl Madt { let madt = Madt::new(find_one_sdt!("APIC")); if let Some(madt) = madt { + // Validate MADT checksum per ACPI 6.5 §5.2.2 + if !madt.sdt.validate_checksum() { + error!("MADT checksum validation failed, skipping APIC initialization"); + return; + } + // safe because no APs have been started yet. unsafe { MADT.get().write(Some(madt)) }; diff --git a/recipes/core/kernel/source/src/acpi/mod.rs b/recipes/core/kernel/source/src/acpi/mod.rs index b3b80f0cec..d6a744ef90 100644 --- a/recipes/core/kernel/source/src/acpi/mod.rs +++ b/recipes/core/kernel/source/src/acpi/mod.rs @@ -20,6 +20,8 @@ mod rxsdt; pub mod sdt; #[cfg(target_arch = "aarch64")] mod spcr; +pub mod slit; +pub mod srat; mod xsdt; unsafe fn map_linearly(addr: PhysicalAddress, len: usize, mapper: &mut crate::memory::PageMapper) { @@ -163,7 +165,14 @@ pub unsafe fn init(already_supplied_rsdp: Option<*const u8>) { // TODO: Enumerate processors in userspace, and then provide an ACPI-independent interface // to initialize enumerated processors to userspace? + // Parse SRAT BEFORE MADT so NUMA node mapping is available + // when APs are started and PercpuBlocks are created. + srat::init(); + Madt::init(); + + // Parse SLIT after MADT for the NUMA distance matrix. + slit::init(); //TODO: support this on any arch // SPCR must be initialized after MADT for interrupt controllers #[cfg(target_arch = "aarch64")] diff --git a/recipes/core/kernel/source/src/acpi/sdt.rs b/recipes/core/kernel/source/src/acpi/sdt.rs index 83ff67dac1..2f1f54cd9b 100644 --- a/recipes/core/kernel/source/src/acpi/sdt.rs +++ b/recipes/core/kernel/source/src/acpi/sdt.rs @@ -24,4 +24,20 @@ impl Sdt { let header_size = size_of::(); total_size.saturating_sub(header_size) } + + /// Validate the SDT checksum. + /// + /// Per ACPI 6.5 §5.2.2: the entire table (including the checksum field) + /// must sum to 0 when all bytes are added together as unsigned 8-bit values. + pub fn validate_checksum(&self) -> bool { + let ptr = self as *const _ as *const u8; + let len = self.length as usize; + if len < size_of::() { + return false; + } + let sum = unsafe { core::slice::from_raw_parts(ptr, len) } + .iter() + .fold(0u8, |acc, &b| acc.wrapping_add(b)); + sum == 0 + } } diff --git a/recipes/core/kernel/source/src/acpi/slit.rs b/recipes/core/kernel/source/src/acpi/slit.rs new file mode 100644 index 0000000000..605f303390 --- /dev/null +++ b/recipes/core/kernel/source/src/acpi/slit.rs @@ -0,0 +1,45 @@ +//! SLIT (System Locality Information Table) parser. +//! +//! Parses the NUMA distance matrix for scheduler NUMA-aware work stealing. + +use super::sdt::Sdt; +use crate::acpi::find_sdt; + +const MAX_NODES: usize = 8; + +static mut SLIT_MATRIX: [[u8; MAX_NODES]; MAX_NODES] = [[10u8; MAX_NODES]; MAX_NODES]; +static mut SLIT_NUM_NODES: usize = 0; +static mut SLIT_AVAILABLE: bool = false; + +pub fn is_available() -> bool { unsafe { SLIT_AVAILABLE } } +pub fn num_nodes() -> usize { unsafe { SLIT_NUM_NODES } } + +pub fn distance(from: u8, to: u8) -> u8 { + if !unsafe { SLIT_AVAILABLE } { return 10; } + let (from, to) = (from as usize, to as usize); + if from >= MAX_NODES || to >= MAX_NODES { return 10; } + unsafe { SLIT_MATRIX[from][to] } +} + +pub fn same_socket(node1: u8, node2: u8) -> bool { distance(node1, node2) <= 20 } + +pub fn init() { + let sdt = match find_sdt("SLIT").as_slice() { + [] => return, + [x] => *x, + xs => { println!("SLIT: {} tables found, expected 1", xs.len()); return; } + }; + if &sdt.signature != b"SLIT" { return; } + let data_addr = sdt.data_address(); + let data_len = sdt.data_len(); + if data_len < 8 { return; } + let num_nodes = unsafe { *(data_addr as *const u64) } as usize; + if num_nodes == 0 || num_nodes > MAX_NODES { println!("SLIT: {num_nodes} nodes (max {MAX_NODES}), ignoring"); return; } + let matrix_start = 8; + let matrix_size = num_nodes * num_nodes; + if data_len < matrix_start + matrix_size { println!("SLIT: matrix truncated ({data_len} < {})", matrix_start + matrix_size); return; } + let matrix = unsafe { &mut SLIT_MATRIX }; + for i in 0..num_nodes { for j in 0..num_nodes { matrix[i][j] = unsafe { *((data_addr + matrix_start + i * num_nodes + j) as *const u8) }; } } + unsafe { SLIT_NUM_NODES = num_nodes; SLIT_AVAILABLE = true; } + debug!("SLIT: {} nodes, distance matrix loaded", num_nodes); +} diff --git a/recipes/core/kernel/source/src/acpi/srat.rs b/recipes/core/kernel/source/src/acpi/srat.rs new file mode 100644 index 0000000000..49b3ac0ac7 --- /dev/null +++ b/recipes/core/kernel/source/src/acpi/srat.rs @@ -0,0 +1,102 @@ +//! SRAT (System Resource Affinity Table) parser. +//! +//! Parses CPU-to-NUMA-node and memory-to-NUMA-node affinity information. +//! Called before MADT init so that NUMA data is available during AP startup. + +use super::sdt::Sdt; +use crate::acpi::find_sdt; + +const MAX_CPU_ENTRIES: usize = 256; +const MAX_MEM_ENTRIES: usize = 64; + +#[derive(Clone, Copy)] +struct SratCpuEntry { apic_id: u32, node: u8, enabled: bool } + +#[derive(Clone, Copy)] +struct SratMemEntry { node: u8, base: u64, length: u64, enabled: bool } + +const CPU_NONE: SratCpuEntry = SratCpuEntry { apic_id: u32::MAX, node: 0, enabled: false }; +const MEM_NONE: SratMemEntry = SratMemEntry { node: 0, base: 0, length: 0, enabled: false }; + +static mut SRAT_CPU_ENTRIES: [SratCpuEntry; MAX_CPU_ENTRIES] = [CPU_NONE; MAX_CPU_ENTRIES]; +static mut SRAT_MEM_ENTRIES: [SratMemEntry; MAX_MEM_ENTRIES] = [MEM_NONE; MAX_MEM_ENTRIES]; +static mut SRAT_CPU_COUNT: usize = 0; +static mut SRAT_MEM_COUNT: usize = 0; +static mut SRAT_AVAILABLE: bool = false; + +pub fn is_available() -> bool { unsafe { SRAT_AVAILABLE } } + +pub fn numa_node_for_apic(apic_id: u32) -> Option { + if !unsafe { SRAT_AVAILABLE } { return None; } + let count = unsafe { SRAT_CPU_COUNT }; + let entries = unsafe { &SRAT_CPU_ENTRIES }; + for i in 0..count { + if entries[i].apic_id == apic_id && entries[i].enabled { return Some(entries[i].node); } + } + None +} + +pub fn numa_node_count() -> usize { + if !unsafe { SRAT_AVAILABLE } { return 1; } + let mut max_node: u8 = 0; + let count = unsafe { SRAT_CPU_COUNT }; + let entries = unsafe { &SRAT_CPU_ENTRIES }; + for i in 0..count { if entries[i].enabled && entries[i].node > max_node { max_node = entries[i].node; } } + (max_node as usize) + 1 +} + +#[repr(C, packed)] +struct SratLocalApic { _proximity_lo: u8, apic_id: u8, flags: u32, _local_sapic_eid: u8, _proximity_hi: [u8; 3], _clock_domain: u32 } + +#[repr(C, packed)] +struct SratMemoryAffinity { proximity_domain: u32, _reserved1: u16, base_address_lo: u32, base_address_hi: u32, length_lo: u32, length_hi: u32, _reserved2: u32, flags: u32, _reserved3: u64 } + +#[repr(C, packed)] +struct SratLocalX2Apic { _reserved: u16, proximity_domain: u32, x2apic_id: u32, flags: u32, _clock_domain: u32, _reserved2: u32 } + +pub fn init() { + let sdt = match find_sdt("SRAT").as_slice() { + [] => return, + [x] => *x, + xs => { println!("SRAT: {} tables found, expected 1", xs.len()); return; } + }; + if &sdt.signature != b"SRAT" { return; } + let data_addr = sdt.data_address(); + let data_len = sdt.data_len(); + if data_len < 12 { println!("SRAT: table too short ({data_len} bytes)"); return; } + let mut offset: usize = 12; + let cpu_entries = unsafe { &mut SRAT_CPU_ENTRIES }; + let mem_entries = unsafe { &mut SRAT_MEM_ENTRIES }; + let mut cpu_count: usize = 0; + let mut mem_count: usize = 0; + while offset + 2 <= data_len { + let entry_type = unsafe { *((data_addr + offset) as *const u8) }; + let entry_len = unsafe { *((data_addr + offset + 1) as *const u8) } as usize; + if entry_len < 2 || offset + entry_len > data_len { break; } + let entry_data = data_addr + offset + 2; + match entry_type { + 0x0 if entry_len >= size_of::() + 2 => { + let e = unsafe { &*(entry_data as *const SratLocalApic) }; + let enabled = (e.flags & 1) == 1; + let node = (e._proximity_lo as u32) | ((e._proximity_hi[0] as u32) << 8) | ((e._proximity_hi[1] as u32) << 16) | ((e._proximity_hi[2] as u32) << 24); + if cpu_count < MAX_CPU_ENTRIES { cpu_entries[cpu_count] = SratCpuEntry { apic_id: e.apic_id as u32, node: node as u8, enabled }; cpu_count += 1; } + } + 0x1 if entry_len >= size_of::() + 2 => { + let e = unsafe { &*(entry_data as *const SratMemoryAffinity) }; + let enabled = (e.flags & 1) == 1; + let base = (e.base_address_hi as u64) << 32 | e.base_address_lo as u64; + let length = (e.length_hi as u64) << 32 | e.length_lo as u64; + if mem_count < MAX_MEM_ENTRIES { mem_entries[mem_count] = SratMemEntry { node: e.proximity_domain as u8, base, length, enabled }; mem_count += 1; } + } + 0x2 if entry_len >= size_of::() + 2 => { + let e = unsafe { &*(entry_data as *const SratLocalX2Apic) }; + let enabled = (e.flags & 1) == 1; + if cpu_count < MAX_CPU_ENTRIES { cpu_entries[cpu_count] = SratCpuEntry { apic_id: e.x2apic_id, node: e.proximity_domain as u8, enabled }; cpu_count += 1; } + } + _ => {} + } + offset += entry_len; + } + unsafe { SRAT_CPU_COUNT = cpu_count; SRAT_MEM_COUNT = mem_count; SRAT_AVAILABLE = true; } + debug!("SRAT: {} CPU entries, {} memory entries", cpu_count, mem_count); +} diff --git a/recipes/core/kernel/source/src/arch/x86_shared/cpuidle.rs b/recipes/core/kernel/source/src/arch/x86_shared/cpuidle.rs new file mode 100644 index 0000000000..156add78e8 --- /dev/null +++ b/recipes/core/kernel/source/src/arch/x86_shared/cpuidle.rs @@ -0,0 +1,186 @@ +use core::cell::SyncUnsafeCell; +use core::sync::atomic::{AtomicUsize, Ordering}; + +use crate::arch::cpuid::cpuid; +use crate::syscall::error::{Error, Result, EINVAL}; + +#[repr(align(64))] +struct MonitorTarget { + value: AtomicUsize, +} + +static MONITOR_TARGET: MonitorTarget = MonitorTarget { + value: AtomicUsize::new(0), +}; + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub struct CStateFlags: u32 { + const NEEDS_MONITOR = 1; + const NEEDS_WBINVD = 2; + } +} + +#[derive(Clone, Copy, Debug)] +pub struct CState { + pub name: &'static str, + pub typ: u32, + pub latency: u32, + pub power: u32, + pub mwait_hint: u32, + pub flags: CStateFlags, +} + +const MAX_CSTATES: usize = 8; +static CPUIDLE_STATES: SyncUnsafeCell<[Option; MAX_CSTATES]> = + SyncUnsafeCell::new([None; MAX_CSTATES]); +static NUM_CPUIDLE_STATES: AtomicUsize = AtomicUsize::new(0); + +static CSTATE_POLICY_MAX: AtomicUsize = AtomicUsize::new(0); + +fn has_mwait() -> bool { + cpuid().get_feature_info().map_or(false, |info| info.has_monitor_mwait()) +} + +fn add_state(index: usize, state: CState) { + unsafe { + (*CPUIDLE_STATES.get())[index] = Some(state); + } +} + +pub fn init() { + add_state(0, CState { + name: "C1", + typ: 1, + latency: 1, + power: 1000, + mwait_hint: 0x00, + flags: CStateFlags::empty(), + }); + let mut count = 1; + + if has_mwait() { + add_state(count, CState { + name: "C1E", + typ: 1, + latency: 2, + power: 800, + mwait_hint: 0x01, + flags: CStateFlags::NEEDS_MONITOR, + }); + count += 1; + + add_state(count, CState { + name: "C2", + typ: 2, + latency: 10, + power: 500, + mwait_hint: 0x10, + flags: CStateFlags::NEEDS_MONITOR, + }); + count += 1; + + add_state(count, CState { + name: "C3", + typ: 3, + latency: 50, + power: 100, + mwait_hint: 0x20, + flags: CStateFlags::NEEDS_MONITOR | CStateFlags::NEEDS_WBINVD, + }); + count += 1; + + add_state(count, CState { + name: "C6", + typ: 6, + latency: 100, + power: 50, + mwait_hint: 0x50, + flags: CStateFlags::NEEDS_MONITOR | CStateFlags::NEEDS_WBINVD, + }); + count += 1; + + add_state(count, CState { + name: "C7", + typ: 7, + latency: 200, + power: 30, + mwait_hint: 0x60, + flags: CStateFlags::NEEDS_MONITOR | CStateFlags::NEEDS_WBINVD, + }); + count += 1; + } + + NUM_CPUIDLE_STATES.store(count, Ordering::SeqCst); + log::info!("cpuidle: initialized {} states (mwait={})", count, has_mwait()); +} + +pub fn policy_read() -> usize { + CSTATE_POLICY_MAX.load(Ordering::Relaxed) +} + +pub fn policy_write(buf: &[u8]) -> Result { + let s = core::str::from_utf8(buf).map_err(|_| Error::new(EINVAL))?; + let s = s.trim(); + let val: usize = s.parse().map_err(|_| Error::new(EINVAL))?; + let num_states = NUM_CPUIDLE_STATES.load(Ordering::Relaxed); + if val >= num_states { + return Err(Error::new(EINVAL)); + } + CSTATE_POLICY_MAX.store(val, Ordering::Relaxed); + log::info!("cpuidle: policy set to max state {}", val); + Ok(s.len()) +} + +pub fn resource() -> Result> { + let mut output = alloc::string::String::new(); + let num_states = NUM_CPUIDLE_STATES.load(Ordering::Relaxed); + let policy = CSTATE_POLICY_MAX.load(Ordering::Relaxed); + output.push_str(&format!("policy_max: {}\n", policy)); + output.push_str(&format!("num_states: {}\n", num_states)); + for i in 0..num_states { + if let Some(state) = unsafe { (*CPUIDLE_STATES.get())[i] } { + output.push_str(&format!( + "state{}: name={} type={} latency={}us power={} hint={:#x} flags={:?}\n", + i, state.name, state.typ, state.latency, state.power, state.mwait_hint, state.flags + )); + } + } + Ok(output.into_bytes()) +} + +pub unsafe fn enter_idle() { + let policy_max = CSTATE_POLICY_MAX.load(Ordering::Relaxed); + let num_states = NUM_CPUIDLE_STATES.load(Ordering::Relaxed); + let target_index = if num_states == 0 { + 0 + } else { + core::cmp::min(policy_max, num_states - 1) + }; + + if target_index == 0 { + unsafe { crate::arch::interrupt::enable_and_halt(); } + return; + } + + let state = match unsafe { (*CPUIDLE_STATES.get())[target_index] } { + Some(s) => s, + None => { + unsafe { crate::arch::interrupt::enable_and_halt(); } + return; + } + }; + + if state.flags.contains(CStateFlags::NEEDS_MONITOR) { + let addr = &MONITOR_TARGET.value as *const AtomicUsize as *const u8; + unsafe { crate::arch::interrupt::monitor(addr, 0, 0); } + } + + if state.flags.contains(CStateFlags::NEEDS_WBINVD) { + unsafe { core::arch::asm!("wbinvd", options(nostack)); } + } + + unsafe { + crate::arch::interrupt::enable_and_mwait(state.mwait_hint, 0); + } +} diff --git a/recipes/core/kernel/source/src/arch/x86_shared/device/ioapic.rs b/recipes/core/kernel/source/src/arch/x86_shared/device/ioapic.rs index cd34c03b98..b7656dba57 100644 --- a/recipes/core/kernel/source/src/arch/x86_shared/device/ioapic.rs +++ b/recipes/core/kernel/source/src/arch/x86_shared/device/ioapic.rs @@ -120,6 +120,21 @@ impl IoApic { reg |= u64::from(mask) << 16; let _ = guard.write_ioredtbl(idx, reg); } + /// Change the destination APIC for a GSI by reprogramming the redirection table entry. + /// Preserves all other fields (vector, polarity, trigger mode, delivery mode, mask). + /// Returns true if the entry was successfully updated. + pub fn set_irq_affinity(&self, gsi: u32, dest: ApicId) -> bool { + let idx = (gsi - self.gsi_start) as u8; + let mut guard = self.regs.lock(); + let Some(mut entry) = guard.read_ioredtbl(idx) else { + return false; + }; + // Clear destination field (bits 63:56 for xAPIC physical mode) + // and set new destination APIC ID + entry &= !(0xFF_u64 << 56); + entry |= u64::from(dest.get()) << 56; + guard.write_ioredtbl(idx, entry) + } } #[repr(u8)] @@ -474,3 +489,14 @@ pub unsafe fn unmask(irq: u8) { }; apic.set_mask(gsi, false); } + +/// Change the destination CPU for an IRQ by reprogramming the IOAPIC redirection entry. +/// Resolves the legacy IRQ to its GSI, finds the owning IOAPIC, and updates the destination +/// APIC ID in the redirection table while preserving all other fields. +pub unsafe fn set_affinity(irq: u8, dest: ApicId) -> bool { + let gsi = resolve(irq); + match find_ioapic(gsi) { + Some(apic) => apic.set_irq_affinity(gsi, dest), + None => false, + } +} diff --git a/recipes/core/kernel/source/src/arch/x86_shared/device/local_apic.rs b/recipes/core/kernel/source/src/arch/x86_shared/device/local_apic.rs index b300e6fea5..87c5a31ff3 100644 --- a/recipes/core/kernel/source/src/arch/x86_shared/device/local_apic.rs +++ b/recipes/core/kernel/source/src/arch/x86_shared/device/local_apic.rs @@ -59,10 +59,10 @@ impl LocalApic { .is_some_and(|feature_info| feature_info.has_x2apic()); if !self.x2 { - debug!("Detected xAPIC at {:#x}", physaddr.data()); + info!("Detected xAPIC at {:#x}", physaddr.data()); self.address = map_device_memory(physaddr, 4096).data(); } else { - debug!("Detected x2APIC"); + info!("Detected x2APIC"); } self.init_ap(); diff --git a/recipes/core/kernel/source/src/arch/x86_shared/idt.rs b/recipes/core/kernel/source/src/arch/x86_shared/idt.rs index 47f692f656..d5af75ddf0 100644 --- a/recipes/core/kernel/source/src/arch/x86_shared/idt.rs +++ b/recipes/core/kernel/source/src/arch/x86_shared/idt.rs @@ -110,6 +110,8 @@ pub fn set_reserved(cpu_id: LogicalCpuId, index: u8, reserved: bool) { } pub fn available_irqs_iter(cpu_id: LogicalCpuId) -> impl Iterator + 'static { + let count = (32..=254).filter(|&index| !is_reserved(cpu_id, index)).count(); + info!("available_irqs_iter: cpu_id={} count={}", cpu_id.get(), count); (32..=254).filter(move |&index| !is_reserved(cpu_id, index)) } diff --git a/recipes/core/kernel/source/src/context/arch/aarch64.rs b/recipes/core/kernel/source/src/context/arch/aarch64.rs index 33dc83a987..b8f8ac95d7 100644 --- a/recipes/core/kernel/source/src/context/arch/aarch64.rs +++ b/recipes/core/kernel/source/src/context/arch/aarch64.rs @@ -4,16 +4,10 @@ use crate::{ percpu::PercpuBlock, syscall::FloatRegisters, }; -use core::{mem::offset_of, ptr, sync::atomic::AtomicBool}; +use core::{mem::offset_of, ptr}; use spin::Once; use syscall::{EnvRegisters, Result}; -/// This must be used by the kernel to ensure that context switches are done atomically -/// Compare and exchange this to true when beginning a context switch on any CPU -/// The `Context::switch_to` function will set it back to false, allowing other CPU's to switch -/// This must be done, as no locks can be held on the stack during switch -pub static CONTEXT_SWITCH_LOCK: AtomicBool = AtomicBool::new(false); - // 512 bytes for registers, extra bytes for fpcr and fpsr pub const KFX_ALIGN: usize = 16; diff --git a/recipes/core/kernel/source/src/context/arch/riscv64.rs b/recipes/core/kernel/source/src/context/arch/riscv64.rs index 4bd843e620..fe63639acb 100644 --- a/recipes/core/kernel/source/src/context/arch/riscv64.rs +++ b/recipes/core/kernel/source/src/context/arch/riscv64.rs @@ -2,13 +2,11 @@ use crate::{ arch::interrupt::InterruptStack, context::context::Kstack, memory::RmmA, percpu::PercpuBlock, syscall::FloatRegisters, }; -use core::{mem::offset_of, sync::atomic::AtomicBool}; +use core::mem::offset_of; use rmm::{Arch, VirtualAddress}; use spin::Once; use syscall::{error::*, EnvRegisters}; -pub static CONTEXT_SWITCH_LOCK: AtomicBool = AtomicBool::new(false); - pub const KFX_ALIGN: usize = 16; #[derive(Clone, Debug, Default)] diff --git a/recipes/core/kernel/source/src/context/arch/x86.rs b/recipes/core/kernel/source/src/context/arch/x86.rs index 2862d35f20..dc01f6e707 100644 --- a/recipes/core/kernel/source/src/context/arch/x86.rs +++ b/recipes/core/kernel/source/src/context/arch/x86.rs @@ -1,4 +1,4 @@ -use core::{mem::offset_of, sync::atomic::AtomicBool}; +use core::mem::offset_of; use rmm::{Arch, VirtualAddress}; use spin::Once; use syscall::{error::*, EnvRegisters}; @@ -14,12 +14,6 @@ use crate::{ syscall::FloatRegisters, }; -/// This must be used by the kernel to ensure that context switches are done atomically -/// Compare and exchange this to true when beginning a context switch on any CPU -/// The `Context::switch_to` function will set it back to false, allowing other CPU's to switch -/// This must be done, as no locks can be held on the stack during switch -pub static CONTEXT_SWITCH_LOCK: AtomicBool = AtomicBool::new(false); - const ST_RESERVED: u128 = 0xFFFF_FFFF_FFFF_0000_0000_0000_0000_0000; pub const KFX_ALIGN: usize = 16; diff --git a/recipes/core/kernel/source/src/context/arch/x86_64.rs b/recipes/core/kernel/source/src/context/arch/x86_64.rs index 6758c9fca5..574d373887 100644 --- a/recipes/core/kernel/source/src/context/arch/x86_64.rs +++ b/recipes/core/kernel/source/src/context/arch/x86_64.rs @@ -1,6 +1,5 @@ use core::{ ptr::{addr_of, addr_of_mut}, - sync::atomic::AtomicBool, }; use crate::syscall::FloatRegisters; @@ -12,12 +11,6 @@ use spin::Once; use syscall::{error::*, EnvRegisters}; use x86::msr; -/// This must be used by the kernel to ensure that context switches are done atomically -/// Compare and exchange this to true when beginning a context switch on any CPU -/// The `Context::switch_to` function will set it back to false, allowing other CPU's to switch -/// This must be done, as no locks can be held on the stack during switch -pub static CONTEXT_SWITCH_LOCK: AtomicBool = AtomicBool::new(false); - const ST_RESERVED: u128 = 0xFFFF_FFFF_FFFF_0000_0000_0000_0000_0000; #[cfg(cpu_feature_never = "xsave")] diff --git a/recipes/core/kernel/source/src/context/mod.rs b/recipes/core/kernel/source/src/context/mod.rs index 37c73f5a37..df44cc4565 100644 --- a/recipes/core/kernel/source/src/context/mod.rs +++ b/recipes/core/kernel/source/src/context/mod.rs @@ -14,8 +14,8 @@ use crate::{ memory::{RmmA, RmmArch, TableKind}, percpu::PercpuBlock, sync::{ - ArcRwLockWriteGuard, CleanLockToken, LockToken, Mutex, MutexGuard, RwLock, RwLockReadGuard, - RwLockWriteGuard, L0, L1, L2, L4, + ArcRwLockWriteGuard, CleanLockToken, LockToken, McsMutex, McsMutexGuard, Mutex, + MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard, L0, L1, L2, L4, }, syscall::error::Result, }; @@ -74,10 +74,12 @@ pub use self::arch::empty_cr3; // the context file descriptors. static CONTEXTS: RwLock> = RwLock::new(BTreeSet::new()); -// Actual context store for the scheduler -static RUN_CONTEXTS: Mutex = Mutex::new(RunContextData::new()); +// Actual context store for the scheduler — uses MCS fair spinlock to +// eliminate cache-line bouncing under multi-CPU contention. +static RUN_CONTEXTS: McsMutex = McsMutex::new(RunContextData::new()); -// Context that has been pushed out from RUN_CONTEXTS after being idle +// Context that has been pushed out from RUN_CONTEXTS after being idle. +// Uses regular Mutex (lower contention; wakeup_contexts uses try_lock). static IDLE_CONTEXTS: Mutex> = Mutex::new(VecDeque::new()); pub struct RunContextData { @@ -113,7 +115,7 @@ pub fn idle_contexts_try( IDLE_CONTEXTS.try_lock(token) } -pub fn run_contexts(token: LockToken<'_, L0>) -> MutexGuard<'_, L1, RunContextData> { +pub fn run_contexts(token: LockToken<'_, L0>) -> McsMutexGuard<'_, L1, RunContextData> { RUN_CONTEXTS.lock(token) } diff --git a/recipes/core/kernel/source/src/context/switch.rs b/recipes/core/kernel/source/src/context/switch.rs index 86684c8f4c..2dbed065eb 100644 --- a/recipes/core/kernel/source/src/context/switch.rs +++ b/recipes/core/kernel/source/src/context/switch.rs @@ -15,7 +15,7 @@ use crate::{ use alloc::{sync::Arc, vec::Vec}; use core::{ cell::{Cell, RefCell}, - hint, mem, + mem, sync::atomic::Ordering, }; use syscall::PtraceFlags; @@ -26,6 +26,11 @@ enum UpdateResult { Blocked, } +/// Default number of PIT ticks before triggering a context switch. +/// At ~2.25 ms per tick, 3 ticks ≈ 6.75 ms timeslice. +/// Configurable per-CPU via `ContextSwitchPercpu::preempt_interval`. +const DEFAULT_PREEMPT_INTERVAL: usize = 3; + // A simple geometric series where value[i] ~= value[i - 1] * 1.25 const SCHED_PRIO_TO_WEIGHT: [usize; 40] = [ 88761, 71755, 56483, 46273, 36291, 29154, 23254, 18705, 14949, 11916, 9548, 7620, 6100, 4904, @@ -90,13 +95,15 @@ struct SwitchResultInner { /// /// The function also calls the signal handler after switching contexts. pub fn tick(token: &mut CleanLockToken) { - let ticks_cell = &PercpuBlock::current().switch_internals.pit_ticks; + let percpu = PercpuBlock::current(); + let ticks_cell = &percpu.switch_internals.pit_ticks; let new_ticks = ticks_cell.get() + 1; ticks_cell.set(new_ticks); - // Trigger a context switch after every 3 ticks (approx. 6.75 ms). - if new_ticks >= 3 { + // Trigger a context switch when the per-CPU preempt interval is reached. + let interval = percpu.switch_internals.preempt_interval.get(); + if new_ticks >= interval { switch(token); crate::context::signal::signal_handler(token); } @@ -120,7 +127,10 @@ pub unsafe extern "C" fn switch_finish_hook() { crate::arch::stop::emergency_reset(); } } - arch::CONTEXT_SWITCH_LOCK.store(false, Ordering::SeqCst); + PercpuBlock::current() + .switch_internals + .in_context_switch + .set(false); crate::percpu::switch_arch_hook(); } } @@ -150,16 +160,15 @@ pub fn switch(token: &mut CleanLockToken) -> SwitchResult { //set PIT Interrupt counter to 0, giving each process same amount of PIT ticks percpu.switch_internals.pit_ticks.set(0); - // Acquire the global lock to ensure exclusive access during context switch and avoid - // issues that would be caused by the unsafe operations below - // TODO: Better memory orderings? - while arch::CONTEXT_SWITCH_LOCK - .compare_exchange_weak(false, true, Ordering::SeqCst, Ordering::Relaxed) - .is_err() - { - hint::spin_loop(); - percpu.maybe_handle_tlb_shootdown(); - } + // Acquire the per-CPU context switch flag. Each CPU can only be in one context + // switch at a time. The per-context write locks provide cross-CPU safety; this + // flag catches re-entrant switches on the same CPU (a kernel bug). + debug_assert!( + !percpu.switch_internals.in_context_switch.get(), + "context switch re-entry on CPU {}", + percpu.cpu_id + ); + percpu.switch_internals.in_context_switch.set(true); // Lock the previous context. let prev_context_lock = crate::context::current(); @@ -167,8 +176,8 @@ pub fn switch(token: &mut CleanLockToken) -> SwitchResult { let mut prev_context_guard = unsafe { prev_context_lock.write_arc() }; if !prev_context_guard.is_preemptable() { - // Unset global lock - arch::CONTEXT_SWITCH_LOCK.store(false, Ordering::SeqCst); + // Unset per-CPU context switch flag + percpu.switch_internals.in_context_switch.set(false); // Pretend to have finished switching, so CPU is not idled return SwitchResult::Switched; @@ -292,8 +301,8 @@ pub fn switch(token: &mut CleanLockToken) -> SwitchResult { SwitchResult::Switched } _ => { - // No target was found, unset global lock and return - arch::CONTEXT_SWITCH_LOCK.store(false, Ordering::SeqCst); + // No target was found, unset per-CPU context switch flag and return + percpu.switch_internals.in_context_switch.set(false); percpu.stats.set_state(cpu_stats::CpuState::Idle); @@ -352,6 +361,7 @@ fn wakeup_contexts(token: &mut CleanLockToken, switch_time: u128) -> Vec<(usize, } /// This is the scheduler function which currently utilises Deficit Weighted Round Robin Scheduler +/// with NUMA-aware context selection preference. fn select_next_context( token: &mut CleanLockToken, percpu: &PercpuBlock, @@ -377,6 +387,10 @@ fn select_next_context( let total_contexts: usize = contexts_list.iter().map(|q| q.len()).sum(); let mut skipped_contexts = 0; + // NUMA-aware selection: remember cross-node fallback candidate. + let my_numa_node = percpu.numa_node.get(); + let mut cross_node_fallback: Option<(usize, ArcContextLockWriteGuard)> = None; + 'priority: loop { i = (i + 1) % 40; total_iters += 1; @@ -441,9 +455,44 @@ fn select_next_context( // Is this context runnable on this CPU? let sw = unsafe { update_runnable(&mut next_context_guard, cpu_id, switch_time) }; if let UpdateResult::CanSwitch = sw { - next_context_guard_opt = Some(next_context_guard); - balance[i] -= SCHED_PRIO_TO_WEIGHT[20]; - break 'priority; + // NUMA-aware selection: check if this context's last CPU was on the same node. + let same_node = if my_numa_node != u8::MAX { + next_context_guard.cpu_id + .map(|cid| { + crate::percpu::get_for_cpu(cid) + .map(|p| p.numa_node.get() == my_numa_node) + .unwrap_or(false) + }) + .unwrap_or(true) // New context (no last CPU) — treat as same node + } else { + true // No NUMA info — treat all as same node + }; + + if same_node { + // Cache-warm: select immediately + percpu.current_prio.set(next_context_guard.prio); + next_context_guard_opt = Some(next_context_guard); + balance[i] -= SCHED_PRIO_TO_WEIGHT[20]; + break 'priority; + } else { + // Cross-node candidate: save as fallback, keep scanning for same-node + if cross_node_fallback.is_none() { + // Cache the priority and balance for later + cross_node_fallback = + Some((next_context_guard.prio, next_context_guard)); + balance[i] -= SCHED_PRIO_TO_WEIGHT[20]; + // Don't break — keep looking for a same-node context + continue; + } else { + // Already have a cross-node fallback; push this one back + contexts.push_back(next_context_ref); + skipped_contexts += 1; + if skipped_contexts >= total_contexts { + break 'priority; + } + continue; + } + } } else { if matches!(sw, UpdateResult::Blocked) { idle_contexts(token.token()).push_back(next_context_ref); @@ -458,6 +507,15 @@ fn select_next_context( } } } + + // If we found a cross-node fallback but no same-node context, use it + if next_context_guard_opt.is_none() { + if let Some((prio, guard)) = cross_node_fallback { + percpu.current_prio.set(prio); + next_context_guard_opt = Some(guard); + } + } + percpu.balance.set(balance); percpu.last_queue.set(i); @@ -465,7 +523,10 @@ fn select_next_context( // Send the old process to the back of the line (if it is still runnable) let prev_ctx = WeakContextRef(Arc::downgrade(&prev_context_lock)); if prev_context_guard.status.is_runnable() { - let prio = prev_context_guard.prio; + let raw_prio = prev_context_guard.prio; + let prio = percpu.effective_prio(raw_prio); + // Clear PI donation — previous context is being re-queued + percpu.pi_donated_prio.store(u32::MAX, Ordering::Relaxed); contexts_list[prio].push_back(prev_ctx); } else { idle_contexts(token.token()).push_back(prev_ctx); @@ -477,7 +538,8 @@ fn select_next_context( return Ok(Some(next_context_guard)); } else { if !was_idle && !Arc::ptr_eq(&prev_context_lock, &idle_context) { - // We switch into the idle context + // Switching to idle context — cache lowest priority + percpu.current_prio.set(39); Ok(Some(unsafe { idle_context.write_arc() })) } else { // We found no other process to run. @@ -494,6 +556,13 @@ pub struct ContextSwitchPercpu { switch_result: Cell>, switch_time: Cell, pit_ticks: Cell, + /// Per-CPU context switch flag. Set to true during a context switch on this CPU. + /// Replaced the global CONTEXT_SWITCH_LOCK to eliminate cross-CPU serialization. + in_context_switch: Cell, + /// Number of PIT ticks before triggering a context switch. + /// Default: 3 (≈6.75 ms). Lower values improve interactive responsiveness; + /// higher values improve throughput for batch/compute workloads. + preempt_interval: Cell, current_ctxt: RefCell>>, @@ -508,6 +577,8 @@ impl ContextSwitchPercpu { switch_result: Cell::new(None), switch_time: Cell::new(0), pit_ticks: Cell::new(0), + in_context_switch: Cell::new(false), + preempt_interval: Cell::new(DEFAULT_PREEMPT_INTERVAL), current_ctxt: RefCell::new(None), idle_ctxt: RefCell::new(None), being_sigkilled: Cell::new(false), diff --git a/recipes/core/kernel/source/src/cpu_set.rs b/recipes/core/kernel/source/src/cpu_set.rs index 4aae7781e9..5594cac082 100644 --- a/recipes/core/kernel/source/src/cpu_set.rs +++ b/recipes/core/kernel/source/src/cpu_set.rs @@ -42,17 +42,18 @@ impl core::fmt::Display for LogicalCpuId { } #[cfg(target_pointer_width = "64")] -pub const MAX_CPU_COUNT: u32 = 128; +pub const MAX_CPU_COUNT: u32 = 256; #[cfg(target_pointer_width = "32")] pub const MAX_CPU_COUNT: u32 = 32; const SET_WORDS: usize = (MAX_CPU_COUNT / usize::BITS) as usize; -// TODO: Support more than 128 CPUs. +// TODO: Support more than 256 CPUs. // The maximum number of CPUs on Linux is configurable, and the type for LogicalCpuSet and // LogicalCpuId may be optimized accordingly. In that case, box the mask if it's larger than some -// base size (probably 256 bytes). +// base size (probably 256 bytes). AMD EPYC has 128C/256T, Threadripper PRO 96C/192T — +// 256 covers current hardware. #[derive(Debug)] pub struct LogicalCpuSet([AtomicUsize; SET_WORDS]); diff --git a/recipes/core/kernel/source/src/main.rs b/recipes/core/kernel/source/src/main.rs index 32f491d0e8..81487fac89 100644 --- a/recipes/core/kernel/source/src/main.rs +++ b/recipes/core/kernel/source/src/main.rs @@ -70,6 +70,9 @@ mod log; /// Memory management mod memory; +/// NUMA topology +mod numa; + /// Panic mod panic; diff --git a/recipes/core/kernel/source/src/numa.rs b/recipes/core/kernel/source/src/numa.rs index 40c5a06812..cba73a4465 100644 --- a/recipes/core/kernel/source/src/numa.rs +++ b/recipes/core/kernel/source/src/numa.rs @@ -1,13 +1,15 @@ /// NUMA topology hints for the kernel scheduler. -/// NUMA discovery (SRAT/SLIT parsing) is performed by a userspace daemon -/// (numad) via /scheme/acpi/, then pushed to the kernel via scheme:numa. -/// The kernel stores a lightweight copy for O(1) scheduling lookups. +/// +/// NUMA discovery (SRAT/SLIT parsing) is performed during kernel ACPI init +/// (`acpi::init()`). The kernel stores a lightweight copy for O(1) scheduling +/// lookups. If no SRAT is found, `init_default()` creates a single-node topology. +use crate::acpi::srat; use crate::cpu_set::{LogicalCpuId, LogicalCpuSet}; use core::sync::atomic::{AtomicBool, Ordering}; const MAX_NUMA_NODES: usize = 8; -#[derive(Clone, Debug)] +#[derive(Debug)] pub struct NumaHint { pub node_id: u8, pub cpus: LogicalCpuSet, @@ -21,17 +23,12 @@ pub struct NumaTopology { impl NumaTopology { pub const fn new() -> Self { const NONE: Option = None; - Self { - nodes: [NONE; MAX_NUMA_NODES], - initialized: AtomicBool::new(false), - } + Self { nodes: [NONE; MAX_NUMA_NODES], initialized: AtomicBool::new(false) } } pub fn node_for_cpu(&self, cpu: LogicalCpuId) -> Option { for node in self.nodes.iter().flatten() { - if node.cpus.contains(cpu) { - return Some(node.node_id); - } + if node.cpus.contains(cpu) { return Some(node.node_id); } } None } @@ -43,20 +40,42 @@ impl NumaTopology { static mut NUMA_TOPOLOGY: NumaTopology = NumaTopology::new(); -pub fn topology() -> &'static NumaTopology { - unsafe { &NUMA_TOPOLOGY } -} +pub fn topology() -> &'static NumaTopology { unsafe { &NUMA_TOPOLOGY } } -pub fn init_default() { +/// Initialize NUMA topology from SRAT data parsed during ACPI init. +pub fn init_from_srat(apic_ids: &[(u32, LogicalCpuId)]) { let topo = topology(); - if topo.initialized.swap(true, Ordering::AcqRel) { - return; - } + if topo.initialized.swap(true, Ordering::AcqRel) { return; } + if !srat::is_available() { init_default_inner(); return; } unsafe { let topo_mut = &mut *core::ptr::addr_of_mut!(NUMA_TOPOLOGY); - topo_mut.nodes[0] = Some(NumaHint { - node_id: 0, - cpus: LogicalCpuSet::all(), - }); + for &(apic_id, cpu_id) in apic_ids { + if let Some(node) = srat::numa_node_for_apic(apic_id) { + let idx = node as usize; + if idx < MAX_NUMA_NODES { + topo_mut.nodes[idx].get_or_insert_with(|| NumaHint { node_id: node, cpus: LogicalCpuSet::empty() }).cpus.atomic_set(cpu_id); + } + } + } + if topo_mut.nodes.iter().all(|n| n.is_none()) { + topo_mut.nodes[0] = Some(NumaHint { node_id: 0, cpus: LogicalCpuSet::all() }); + } } + let node_count = topology().nodes.iter().filter(|n| n.is_some()).count(); + debug!("NUMA: {node_count} node(s) from SRAT"); +} + +/// Fallback: single-node topology. +pub fn init_default() { + let topo = topology(); + if topo.initialized.swap(true, Ordering::AcqRel) { return; } + init_default_inner(); +} + +fn init_default_inner() { + unsafe { + let topo_mut = &mut *core::ptr::addr_of_mut!(NUMA_TOPOLOGY); + topo_mut.nodes[0] = Some(NumaHint { node_id: 0, cpus: LogicalCpuSet::all() }); + } + debug!("NUMA: single-node topology (no SRAT)"); } diff --git a/recipes/core/kernel/source/src/percpu.rs b/recipes/core/kernel/source/src/percpu.rs index f4ad5e66e6..9309a41d4d 100644 --- a/recipes/core/kernel/source/src/percpu.rs +++ b/recipes/core/kernel/source/src/percpu.rs @@ -4,9 +4,14 @@ use alloc::{ }; use core::{ cell::{Cell, RefCell}, - sync::atomic::{AtomicBool, AtomicPtr, Ordering}, + hint, + sync::atomic::{AtomicBool, AtomicPtr, AtomicU32, AtomicU64, Ordering}, }; +/// Maximum number of pages to flush individually using INVLPG before falling +/// back to a full TLB flush (CR3 reload). +const TLB_RANGE_THRESHOLD: u32 = 32; + use rmm::Arch; use syscall::PtraceFlags; @@ -16,7 +21,7 @@ use crate::{ cpu_set::{LogicalCpuId, MAX_CPU_COUNT}, cpu_stats::{CpuStats, CpuStatsData}, ptrace::Session, - sync::CleanLockToken, + sync::{mcs::McsNode, mcs::McsRawLock, CleanLockToken}, syscall::debug::SyscallDebugInfo, }; @@ -34,6 +39,38 @@ pub struct PercpuBlock { pub balance: Cell<[usize; 40]>, pub last_queue: Cell, + /// Per-CPU MCS node for the scheduler run-queue lock (RUN_CONTEXTS). + pub mcs_sched_node: McsNode, + + /// Counts how many times the scheduler MCS lock acquisition was contended. + pub mcs_contention_count: Cell, + + /// TLB shootdown range: start virtual address (page-aligned). + /// Set to 0 for a full flush. Only valid when `wants_tlb_shootdown` is true. + pub tlb_flush_start: AtomicU64, + /// TLB shootdown range: number of pages to invalidate. + pub tlb_flush_count: AtomicU32, + + /// Priority inheritance donation. When another CPU is blocked waiting on a + /// lock this CPU holds, the blocked CPU may donate its priority here. + /// `u32::MAX` means no donation; otherwise it's a priority level (0-39). + pub pi_donated_prio: AtomicU32, + + /// Cached priority of the currently-running context on this CPU. + /// Set by the scheduler when selecting a new context. Read by the MCS + /// lock during priority donation — avoids acquiring the context RwLock + /// from the spin loop. Default 39 (lowest priority). + pub current_prio: Cell, + + /// NUMA proximity domain for this CPU. Set during ACPI init from SRAT. + /// `u8::MAX` means unknown (no SRAT or APIC ID not listed). + pub numa_node: Cell, + + /// Pointer to the MCS lock this CPU is currently spinning on (for transitive PI). + /// `null` when not waiting on any lock. Set in McsRawLock::acquire() before + /// entering the spin loop, cleared upon acquisition. + pub waiting_on_lock: AtomicPtr, + // TODO: Put mailbox queues here, e.g. for TLB shootdown? Just be sure to 128-byte align it // first to avoid cache invalidation. pub profiling: Option<&'static crate::profiling::RingBuffer>, @@ -57,6 +94,15 @@ pub unsafe fn init_tlb_shootdown(id: LogicalCpuId, block: *mut PercpuBlock) { ALL_PERCPU_BLOCKS[id.get() as usize].store(block, Ordering::Release) } +/// Get a reference to another CPU's PercpuBlock by logical CPU ID. +pub fn get_for_cpu(id: LogicalCpuId) -> Option<&'static PercpuBlock> { + unsafe { + ALL_PERCPU_BLOCKS[id.get() as usize] + .load(Ordering::Acquire) + .as_ref() + } +} + pub fn get_all_stats() -> Vec<(LogicalCpuId, CpuStatsData)> { let mut res = ALL_PERCPU_BLOCKS .iter() @@ -101,25 +147,148 @@ pub fn shootdown_tlb_ipi(target: Option) { core::hint::spin_loop(); } } + // Full flush — clear range info (Release ordering ensures the flag + // swap and these stores are visible to the handler before the IPI). + percpublock.tlb_flush_start.store(0, Ordering::Release); + percpublock.tlb_flush_count.store(0, Ordering::Release); crate::ipi::ipi_single(crate::ipi::IpiKind::Tlb, percpublock); } else { + // Broadcast TLB shootdown: set flag on all other CPUs, then send a single + // IPI with "all except self" destination shorthand instead of N individual IPIs. + let my_percpublock = PercpuBlock::current(); for id in 0..crate::cpu_count() { - // TODO: Optimize: use global counter and percpu ack counters, send IPI using - // destination shorthand "all CPUs". - shootdown_tlb_ipi(Some(LogicalCpuId::new(id))); + let target_id = LogicalCpuId::new(id); + if target_id == my_percpublock.cpu_id { + continue; + } + let Some(percpublock) = (unsafe { + ALL_PERCPU_BLOCKS[id as usize] + .load(Ordering::Acquire) + .as_ref() + }) else { + continue; + }; + // Wait if this CPU still has a pending shootdown from a previous request + #[expect(clippy::bool_comparison)] + while percpublock + .wants_tlb_shootdown + .swap(true, Ordering::Release) + == true + { + while percpublock.wants_tlb_shootdown.load(Ordering::Relaxed) == true { + my_percpublock.maybe_handle_tlb_shootdown(); + hint::spin_loop(); + } + } + // Full flush — clear range info (Release ordering) + percpublock.tlb_flush_start.store(0, Ordering::Release); + percpublock.tlb_flush_count.store(0, Ordering::Release); } + // Single broadcast IPI to all other CPUs using destination shorthand + crate::ipi::ipi(crate::ipi::IpiKind::Tlb, crate::ipi::IpiTarget::Other); + } +} + +/// Range-based TLB shootdown IPI. Only invalidates the specified virtual address +/// range using INVLPG per page for ranges up to TLB_RANGE_THRESHOLD pages. +/// Falls back to full flush for larger ranges. +pub fn shootdown_tlb_ipi_range(target: Option, start: usize, count: usize) { + if cfg!(not(feature = "multi_core")) { + return; + } + + let start_aligned = start as u64 & !0xFFF; + let count_u32 = count as u32; + let use_range = count_u32 > 0 && count_u32 <= TLB_RANGE_THRESHOLD; + + let set_range = |percpublock: &PercpuBlock| { + if use_range { + percpublock.tlb_flush_start.store(start_aligned, Ordering::Release); + percpublock.tlb_flush_count.store(count_u32, Ordering::Release); + } else { + percpublock.tlb_flush_start.store(0, Ordering::Release); + percpublock.tlb_flush_count.store(0, Ordering::Release); + } + }; + + if let Some(target) = target { + let my_percpublock = PercpuBlock::current(); + assert_ne!(target, my_percpublock.cpu_id); + + let Some(percpublock) = (unsafe { + ALL_PERCPU_BLOCKS[target.get() as usize] + .load(Ordering::Acquire) + .as_ref() + }) else { + return; + }; + #[expect(clippy::bool_comparison)] + while percpublock.wants_tlb_shootdown.swap(true, Ordering::Release) == true { + while percpublock.wants_tlb_shootdown.load(Ordering::Relaxed) == true { + my_percpublock.maybe_handle_tlb_shootdown(); + hint::spin_loop(); + } + } + set_range(percpublock); + crate::ipi::ipi_single(crate::ipi::IpiKind::Tlb, percpublock); + } else { + let my_percpublock = PercpuBlock::current(); + for id in 0..crate::cpu_count() { + let target_id = LogicalCpuId::new(id); + if target_id == my_percpublock.cpu_id { + continue; + } + let Some(percpublock) = (unsafe { + ALL_PERCPU_BLOCKS[id as usize] + .load(Ordering::Acquire) + .as_ref() + }) else { + continue; + }; + #[expect(clippy::bool_comparison)] + while percpublock.wants_tlb_shootdown.swap(true, Ordering::Release) == true { + while percpublock.wants_tlb_shootdown.load(Ordering::Relaxed) == true { + my_percpublock.maybe_handle_tlb_shootdown(); + hint::spin_loop(); + } + } + set_range(percpublock); + } + crate::ipi::ipi(crate::ipi::IpiKind::Tlb, crate::ipi::IpiTarget::Other); } } impl PercpuBlock { + /// Return the effective scheduling priority, accounting for priority inheritance. + /// Lower number = higher priority (0-39 range). + pub fn effective_prio(&self, context_prio: usize) -> usize { + let donated = self.pi_donated_prio.load(Ordering::Relaxed); + if donated < context_prio as u32 { + donated as usize + } else { + context_prio + } + } + pub fn maybe_handle_tlb_shootdown(&self) { #[expect(clippy::bool_comparison)] if self.wants_tlb_shootdown.swap(false, Ordering::Relaxed) == false { return; } - // TODO: Finer-grained flush - crate::memory::RmmA::invalidate_all(); + let start = self.tlb_flush_start.load(Ordering::Acquire); + let count = self.tlb_flush_count.load(Ordering::Acquire); + + if start != 0 && count > 0 && count <= TLB_RANGE_THRESHOLD { + // Range-based flush using INVLPG per page — cheaper than full CR3 reload. + for i in 0..count { + let addr = start + (i as u64) * 4096; + crate::memory::RmmA::invalidate(rmm::VirtualAddress::new(addr as usize)); + } + } else { + // Full TLB flush (CR3 reload) for large ranges or global shootdowns. + crate::memory::RmmA::invalidate_all(); + } if let Some(addrsp) = &*self.current_addrsp.borrow() { addrsp.tlb_ack.fetch_add(1, Ordering::Release); @@ -189,6 +358,14 @@ impl PercpuBlock { wants_tlb_shootdown: AtomicBool::new(false), balance: Cell::new([0; 40]), last_queue: Cell::new(39), + mcs_sched_node: McsNode::new(), + mcs_contention_count: Cell::new(0), + tlb_flush_start: AtomicU64::new(0), + tlb_flush_count: AtomicU32::new(0), + pi_donated_prio: AtomicU32::new(u32::MAX), + current_prio: Cell::new(39), + numa_node: Cell::new(u8::MAX), + waiting_on_lock: AtomicPtr::new(core::ptr::null_mut()), ptrace_flags: Cell::new(PtraceFlags::empty()), ptrace_session: RefCell::new(None), inside_syscall: Cell::new(false), diff --git a/recipes/core/kernel/source/src/scheme/irq.rs b/recipes/core/kernel/source/src/scheme/irq.rs index c76f411336..4222960986 100644 --- a/recipes/core/kernel/source/src/scheme/irq.rs +++ b/recipes/core/kernel/source/src/scheme/irq.rs @@ -18,6 +18,9 @@ use syscall::{ use crate::context::file::InternalFlags; use super::{CallerCtx, HandleMap, OpenResult, SchemeExt, StrOrBytes}; +#[cfg(any(target_arch = "x86_64", target_arch = "x86"))] +use crate::arch::device::{ioapic, local_apic::ApicId}; + #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] use crate::arch::interrupt::{available_irqs_iter, irq::acknowledge, is_reserved, set_reserved}; #[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))] @@ -80,7 +83,7 @@ pub fn irq_trigger(irq: u8, token: &mut CleanLockToken) { #[allow(dead_code)] enum Handle { SchemeRoot, - Irq { ack: AtomicUsize, irq: u8 }, + Irq { ack: AtomicUsize, irq: u8, cpu_id: LogicalCpuId }, Avail(LogicalCpuId), TopLevel, Phandle(u8, Vec), @@ -90,7 +93,7 @@ enum Handle { impl Handle { fn as_irq_handle(&self) -> Option<(&AtomicUsize, u8)> { match self { - &Self::Irq { ref ack, irq } => Some((ack, irq)), + &Self::Irq { ref ack, irq, cpu_id: _ } => Some((ack, irq)), _ => None, } } @@ -144,6 +147,7 @@ impl IrqScheme { Handle::Irq { ack: AtomicUsize::new(0), irq: irq_number, + cpu_id: LogicalCpuId::BSP, }, InternalFlags::empty(), ) @@ -162,6 +166,7 @@ impl IrqScheme { Handle::Irq { ack: AtomicUsize::new(0), irq: irq_number, + cpu_id, }, InternalFlags::empty(), ) @@ -203,6 +208,7 @@ impl IrqScheme { Handle::Irq { ack: AtomicUsize::new(0), irq: irq_number as u8, + cpu_id: LogicalCpuId::new(0), }, InternalFlags::empty(), ) @@ -346,6 +352,7 @@ impl crate::scheme::KernelScheme for IrqScheme { Handle::Irq { ack: AtomicUsize::new(0), irq: plain_irq_number, + cpu_id: LogicalCpuId::BSP, }, InternalFlags::empty(), ) @@ -401,6 +408,7 @@ impl crate::scheme::KernelScheme for IrqScheme { } } Handle::Avail(cpu_id) => { + let mut listed = 0; for vector in available_irqs_iter(cpu_id).skip(opaque) { let irq = vector_to_irq(vector); if cpu_id == LogicalCpuId::BSP && irq < BASE_IRQ_COUNT { @@ -414,7 +422,9 @@ impl crate::scheme::KernelScheme for IrqScheme { name: &intermediate, next_opaque_id: u64::from(vector) + 1, })?; + listed += 1; } + info!("irq getdents Avail: cpu_id={} opaque={} listed={}", cpu_id.get(), opaque, listed); } _ => return Err(Error::new(ENOTDIR)), } @@ -449,11 +459,14 @@ impl crate::scheme::KernelScheme for IrqScheme { let handle = handles_guard.get(id)?; if let &Handle::Irq { - irq: handle_irq, .. + irq: handle_irq, + cpu_id: handle_cpu_id, + .. } = handle && handle_irq > BASE_IRQ_COUNT { - set_reserved(LogicalCpuId::BSP, irq_to_vector(handle_irq), false); + info!("irq close: unreserving vector {} on cpu_id={}", irq_to_vector(handle_irq), handle_cpu_id.get()); + set_reserved(handle_cpu_id, irq_to_vector(handle_irq), false); } Ok(()) } @@ -480,12 +493,21 @@ impl crate::scheme::KernelScheme for IrqScheme { if !cpus.contains(&(cpu_id as u8)) { return Err(Error::new(EINVAL)); } + // Reprogram the IOAPIC redirection entry for x86 targets. + // Non-IOAPIC IRQs (e.g. MSI) will return false -> EIO. + #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] + { + if !unsafe { ioapic::set_affinity(_handle_irq, ApicId::new(cpu_id)) } { + return Err(Error::new(EIO)); + } + } mask.store(cpu_id as usize, Ordering::Release); Ok(size_of::()) } &Handle::Irq { irq: handle_irq, ack: ref handle_ack, + cpu_id: _, } => { if buffer.len() < size_of::() { return Err(Error::new(EINVAL)); @@ -600,6 +622,7 @@ impl crate::scheme::KernelScheme for IrqScheme { Handle::Irq { irq: handle_irq, ack: ref handle_ack, + cpu_id: _, } => { if buffer.len() < size_of::() { return Err(Error::new(EINVAL)); diff --git a/recipes/core/kernel/source/src/scheme/sys/cstate.rs b/recipes/core/kernel/source/src/scheme/sys/cstate.rs new file mode 100644 index 0000000000..abd52cc3b3 --- /dev/null +++ b/recipes/core/kernel/source/src/scheme/sys/cstate.rs @@ -0,0 +1,15 @@ +use alloc::vec::Vec; + +use crate::{ + arch::cpuidle, + sync::CleanLockToken, + syscall::error::{Error, Result, EINVAL}, +}; + +pub fn resource(_token: &mut CleanLockToken) -> Result> { + cpuidle::resource() +} + +pub fn policy_write(buf: &[u8], _token: &mut CleanLockToken) -> Result { + cpuidle::policy_write(buf) +} diff --git a/recipes/core/kernel/source/src/scheme/sys/mod.rs b/recipes/core/kernel/source/src/scheme/sys/mod.rs index 8f26187a79..9eb3564411 100644 --- a/recipes/core/kernel/source/src/scheme/sys/mod.rs +++ b/recipes/core/kernel/source/src/scheme/sys/mod.rs @@ -45,6 +45,11 @@ enum Handle { data: Arc>>>, }, SchemeRoot, + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + Msr { + cpu: usize, + msr: u32, + }, } #[derive(Clone, Copy)] @@ -133,6 +138,28 @@ impl KernelScheme for SysScheme { let id = HANDLES.write(token.token()).insert(Handle::TopLevel); Ok(OpenResult::SchemeLocal(id, InternalFlags::POSITIONED)) + } else if path.starts_with("msr/") { + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + { + if ctx.uid != 0 { + return Err(Error::new(EPERM)); + } + let rest = &path[4..]; + let mut parts = rest.split('/'); + let cpu_str = parts.next().ok_or(Error::new(EINVAL))?; + let msr_str = parts.next().ok_or(Error::new(EINVAL))?; + if parts.next().is_some() { + return Err(Error::new(EINVAL)); + } + let cpu: usize = cpu_str.parse().map_err(|_| Error::new(EINVAL))?; + let msr: u32 = u32::from_str_radix(msr_str, 16).map_err(|_| Error::new(EINVAL))?; + let id = HANDLES.write(token.token()).insert(Handle::Msr { cpu, msr }); + Ok(OpenResult::SchemeLocal(id, InternalFlags::POSITIONED)) + } + #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] + { + Err(Error::new(ENOENT)) + } } else { //Have to iterate to get the path without allocation let entry = FILES @@ -160,6 +187,8 @@ impl KernelScheme for SysScheme { Handle::TopLevel => return Ok(0), Handle::Resource { kind, data, .. } => (*kind, data.clone()), Handle::SchemeRoot => return Err(Error::new(EBADF)), + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + Handle::Msr { .. } => return Ok(0), } }; if matches!(kind, Kind::Wr(_)) { @@ -188,6 +217,16 @@ impl KernelScheme for SysScheme { Handle::TopLevel => "", Handle::Resource { path, .. } => path, Handle::SchemeRoot => return Err(Error::new(EBADF)), + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + Handle::Msr { cpu, msr } => { + const FIRST: &[u8] = b"sys:msr/"; + let mut bytes_read = buf.copy_common_bytes_from_slice(FIRST)?; + let suffix = format!("{}/{:x}", cpu, msr); + if let Some(remaining) = buf.advance(FIRST.len()) { + bytes_read += remaining.copy_common_bytes_from_slice(suffix.as_bytes())?; + } + return Ok(bytes_read); + } }; const FIRST: &[u8] = b"sys:"; @@ -215,6 +254,15 @@ impl KernelScheme for SysScheme { let (kind, data_lock) = { match HANDLES.read(token.token()).get(id)? { Handle::Resource { kind, data, .. } => (*kind, data.clone()), + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + Handle::Msr { cpu, msr } => { + if *cpu != crate::cpu_id().get() as usize { + return Err(Error::new(EINVAL)); + } + let val = unsafe { x86::msr::rdmsr(*msr) }; + let data = format!("{:016x}\n", val).into_bytes(); + return buffer.copy_common_bytes_from_slice(&data[pos..]); + } _ => return Err(Error::new(EBADF)), } }; @@ -253,6 +301,18 @@ impl KernelScheme for SysScheme { let len = buffer.copy_common_bytes_to_slice(&mut intermediate)?; (*handler, intermediate, len) } + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + Handle::Msr { cpu, msr } => { + if *cpu != crate::cpu_id().get() as usize { + return Err(Error::new(EINVAL)); + } + let mut intermediate = [0_u8; 32]; + let len = buffer.copy_common_bytes_to_slice(&mut intermediate)?; + let val_str = core::str::from_utf8(&intermediate[..len]).map_err(|_| Error::new(EINVAL))?; + let val = u64::from_str_radix(val_str.trim(), 16).map_err(|_| Error::new(EINVAL))?; + unsafe { x86::msr::wrmsr(*msr, val); } + return Ok(len); + } Handle::SchemeRoot => return Err(Error::new(EBADF)), }; handler(&intermediate[..len], token) @@ -269,7 +329,8 @@ impl KernelScheme for SysScheme { return Ok(0); }; match HANDLES.read(token.token()).get(id)? { - Handle::Resource { .. } => Err(Error::new(ENOTDIR)), + Handle::Resource { .. } + | Handle::Msr { .. } => Err(Error::new(ENOTDIR)), Handle::TopLevel => { let mut buf = DirentBuf::new(buf, header_size).ok_or(Error::new(EIO))?; for (this_idx, (name, _)) in FILES.iter().enumerate().skip(first_index) { @@ -293,6 +354,18 @@ impl KernelScheme for SysScheme { Handle::Resource { kind, data, .. } => Some((*kind, data.clone())), Handle::TopLevel => None, Handle::SchemeRoot => return Err(Error::new(EBADF)), + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + Handle::Msr { .. } => { + let stat = Stat { + st_mode: 0o600 | MODE_FILE, + st_uid: 0, + st_gid: 0, + st_size: 0, + ..Default::default() + }; + buf.copy_exactly(&stat)?; + return Ok(()); + } } }; let stat = if let Some((kind, data_lock)) = stat_base { diff --git a/recipes/core/kernel/source/src/sync/mcs.rs b/recipes/core/kernel/source/src/sync/mcs.rs new file mode 100644 index 0000000000..3ccde13862 --- /dev/null +++ b/recipes/core/kernel/source/src/sync/mcs.rs @@ -0,0 +1,188 @@ +//! MCS (Mellor-Crummey Scott) fair spinlock. +//! +//! Each waiter spins on its own local `locked` flag instead of a shared lock +//! word, eliminating cache-line bouncing under contention. FIFO ordering +//! guarantees fairness. O(1) cache-line transfers on unlock. +//! +//! Supports transitive priority inheritance: when CPU A waits on a lock held +//! by CPU B, and CPU B waits on a lock held by CPU C, A's priority is +//! propagated through the chain to C (up to MAX_PI_CHAIN_DEPTH hops). + +use core::sync::atomic::{AtomicBool, AtomicPtr, AtomicU32, Ordering}; +use core::{hint, ptr}; + +use crate::percpu::PercpuBlock; + +/// Maximum depth for transitive priority inheritance chain following. +/// Prevents infinite loops from theoretical lock cycles and bounds latency. +/// Linux uses 20; 8 is conservative for a microkernel with fewer nesting levels. +const MAX_PI_CHAIN_DEPTH: u32 = 8; + +/// A node in the MCS lock queue. +pub struct McsNode { + pub next: AtomicPtr, + pub locked: AtomicBool, +} + +impl McsNode { + pub const fn new() -> Self { + Self { + next: AtomicPtr::new(ptr::null_mut()), + locked: AtomicBool::new(false), + } + } +} + +/// Raw MCS spinlock primitive. +pub struct McsRawLock { + tail: AtomicPtr, + /// CPU ID of the current lock holder (for priority inheritance). + /// `u32::MAX` means no holder. + holder_cpu: AtomicU32, +} + +impl McsRawLock { + pub const fn new() -> Self { + Self { + tail: AtomicPtr::new(ptr::null_mut()), + holder_cpu: AtomicU32::new(u32::MAX), + } + } + + #[inline] + pub fn acquire(&self, node: &McsNode) -> bool { + node.next.store(ptr::null_mut(), Ordering::Relaxed); + node.locked.store(true, Ordering::Relaxed); + let prev = self.tail.swap((node as *const McsNode).cast_mut(), Ordering::AcqRel); + if prev.is_null() { + // Uncontended — record ourselves as holder + let cpu_id = PercpuBlock::current().cpu_id.get(); + self.holder_cpu.store(cpu_id, Ordering::Release); + return false; + } + unsafe { + (*prev).next.store((node as *const McsNode).cast_mut(), Ordering::Release); + } + let percpu = PercpuBlock::current(); + // Record which lock we're spinning on (for transitive PI chain following) + percpu.waiting_on_lock.store( + (self as *const McsRawLock).cast_mut(), + Ordering::Release, + ); + let mut donated = false; + while node.locked.load(Ordering::Acquire) { + percpu.maybe_handle_tlb_shootdown(); + // Donate priority to the lock holder (transitively) once per acquisition + if !donated { + self.maybe_donate_priority(percpu); + donated = true; + } + hint::spin_loop(); + } + // Clear waiting_on_lock before proceeding — we now hold the lock + percpu.waiting_on_lock.store(ptr::null_mut(), Ordering::Release); + self.holder_cpu.store(percpu.cpu_id.get(), Ordering::Release); + true + } + + #[inline] + pub fn release(&self, node: &McsNode) { + // Clear priority inheritance donation — we no longer hold the lock + PercpuBlock::current().pi_donated_prio.store(u32::MAX, Ordering::Release); + // Clear holder CPU + self.holder_cpu.store(u32::MAX, Ordering::Release); + + let next = node.next.load(Ordering::Acquire); + if next.is_null() { + if self + .tail + .compare_exchange( + (node as *const McsNode).cast_mut(), + ptr::null_mut(), + Ordering::AcqRel, + Ordering::Acquire, + ) + .is_ok() + { + return; + } + while node.next.load(Ordering::Acquire).is_null() { + hint::spin_loop(); + } + } + unsafe { + (*node.next.load(Ordering::Acquire)).locked.store(false, Ordering::Release); + } + } + + #[inline] + pub fn try_acquire(&self, node: &McsNode) -> bool { + node.next.store(ptr::null_mut(), Ordering::Relaxed); + node.locked.store(true, Ordering::Relaxed); + let ok = self + .tail + .compare_exchange( + ptr::null_mut(), + (node as *const McsNode).cast_mut(), + Ordering::AcqRel, + Ordering::Acquire, + ) + .is_ok(); + if ok { + let cpu_id = PercpuBlock::current().cpu_id.get(); + self.holder_cpu.store(cpu_id, Ordering::Release); + } + ok + } + + /// Donate current CPU's context priority to the lock holder's CPU, + /// following the PI chain transitively (A→B→C). + /// + /// Reads priority from PercpuBlock::current_prio (cached by the scheduler) + /// to avoid acquiring any lock in the MCS spin loop. + /// + /// Chain following: if the holder is itself waiting on another lock, + /// we propagate our priority to that lock's holder too, up to + /// MAX_PI_CHAIN_DEPTH hops. + fn maybe_donate_priority(&self, my_percpu: &PercpuBlock) { + let my_prio = my_percpu.current_prio.get() as u32; + let mut current_holder_cpu = self.holder_cpu.load(Ordering::Relaxed); + + for _ in 0..MAX_PI_CHAIN_DEPTH { + if current_holder_cpu == u32::MAX { + return; + } + let holder_percpu = crate::percpu::get_for_cpu( + crate::cpu_set::LogicalCpuId::new(current_holder_cpu), + ); + let Some(holder) = holder_percpu else { + return; + }; + + // Donate if our priority is higher (lower number) than current donation + let current_donated = holder.pi_donated_prio.load(Ordering::Relaxed); + if my_prio < current_donated { + holder.pi_donated_prio.store(my_prio, Ordering::Release); + } + + // Follow the chain: is this holder also waiting on another lock? + let next_lock_ptr = holder.waiting_on_lock.load(Ordering::Relaxed); + if next_lock_ptr.is_null() { + return; + } + // SAFETY: The pointed-to McsRawLock is a long-lived struct field + // (e.g., part of the run queue). The holder is currently spinning + // in acquire(), so the pointer is valid. We only read holder_cpu + // (an atomic u32) — no mutable access needed. + let next_holder_cpu = + unsafe { (*next_lock_ptr).holder_cpu.load(Ordering::Relaxed) }; + + // Cycle detection: if the next holder is the same CPU we just visited, stop + if next_holder_cpu == current_holder_cpu { + return; + } + current_holder_cpu = next_holder_cpu; + } + // Chain depth exhausted — stop to bound latency + } +} diff --git a/recipes/core/kernel/source/src/sync/mod.rs b/recipes/core/kernel/source/src/sync/mod.rs index 6ad2708ba4..7655a8d9c0 100644 --- a/recipes/core/kernel/source/src/sync/mod.rs +++ b/recipes/core/kernel/source/src/sync/mod.rs @@ -1,5 +1,6 @@ pub use self::{ordered::*, wait_condition::WaitCondition, wait_queue::WaitQueue}; +pub mod mcs; pub mod ordered; pub mod wait_condition; pub mod wait_queue; diff --git a/recipes/core/kernel/source/src/sync/ordered.rs b/recipes/core/kernel/source/src/sync/ordered.rs index 91d46158db..c6763cb663 100644 --- a/recipes/core/kernel/source/src/sync/ordered.rs +++ b/recipes/core/kernel/source/src/sync/ordered.rs @@ -52,7 +52,9 @@ //! *g1 = 12; //! ``` use alloc::sync::Arc; +use core::cell::UnsafeCell; use core::marker::PhantomData; +use core::ptr; use crate::percpu::PercpuBlock; @@ -732,3 +734,143 @@ impl Drop for ArcRwLockWriteGuard { /// This function can only be called if no lock is held by the calling thread/task #[inline] pub fn check_no_locks(_: LockToken<'_, L0>) {} + +// --------------------------------------------------------------------------- +// MCS-based fair mutex (McsMutex) +// --------------------------------------------------------------------------- + +/// A mutual exclusion lock using the MCS fair spinlock algorithm. +/// +/// Unlike `Mutex` which uses a simple spinlock (no fairness under +/// contention), `McsMutex` uses Mellor-Crummey Scott queue-based spinning: +/// +/// - Each waiter spins on its **own** local flag — no shared cache-line bouncing. +/// - FIFO ordering prevents starvation. +/// - O(1) cache-line transfers on unlock. +/// +/// The MCS node is stored in [`crate::percpu::PercpuBlock::mcs_sched_node`], so +/// this type is suitable for scheduler-internal locks where the holder is always +/// the current CPU. +pub struct McsMutex { + raw: crate::sync::mcs::McsRawLock, + data: UnsafeCell, + _phantom: PhantomData, +} + +unsafe impl Sync for McsMutex {} +unsafe impl Send for McsMutex {} + +impl McsMutex { + pub const fn new(val: T) -> Self { + Self { + raw: crate::sync::mcs::McsRawLock::new(), + data: UnsafeCell::new(val), + _phantom: PhantomData, + } + } +} + +impl McsMutex { + pub fn lock<'a, LP: Lower + 'a>( + &'a self, + lock_token: LockToken<'a, LP>, + ) -> McsMutexGuard<'a, L, T> { + let percpu = PercpuBlock::current(); + let contended = self.raw.acquire(&percpu.mcs_sched_node); + if contended { + percpu + .mcs_contention_count + .set(percpu.mcs_contention_count.get() + 1); + } + McsMutexGuard { + lock: self, + lock_token: LockToken::downgraded(lock_token), + } + } + + pub fn try_lock<'a, LP: Lower + 'a>( + &'a self, + lock_token: LockToken<'a, LP>, + ) -> Option> { + let percpu = PercpuBlock::current(); + if self.raw.try_acquire(&percpu.mcs_sched_node) { + Some(McsMutexGuard { + lock: self, + lock_token: LockToken::downgraded(lock_token), + }) + } else { + None + } + } +} + +pub struct McsMutexGuard<'a, L: Level, T: 'a> { + lock: &'a McsMutex, + lock_token: LockToken<'a, L>, +} + +impl<'a, L: Level, T: 'a> McsMutexGuard<'a, L, T> { + pub fn token_split(&mut self) -> (&mut T, LockToken<'_, L>) { + unsafe { (&mut *self.lock.data.get(), self.lock_token.token()) } + } + + pub fn into_split(self) -> (McsRawGuard<'a, L, T>, LockToken<'a, L>) { + let lock_ref = self.lock; + let token = unsafe { core::ptr::read(&self.lock_token) }; + core::mem::forget(self); + (McsRawGuard { lock: lock_ref }, token) + } + + pub fn from_split(raw: McsRawGuard<'a, L, T>, token: LockToken<'a, L>) -> Self { + let lock_ref = raw.lock; + core::mem::forget(raw); + Self { + lock: lock_ref, + lock_token: token, + } + } +} + +impl core::ops::Deref for McsMutexGuard<'_, L, T> { + type Target = T; + fn deref(&self) -> &Self::Target { + unsafe { &*self.lock.data.get() } + } +} + +impl core::ops::DerefMut for McsMutexGuard<'_, L, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + unsafe { &mut *self.lock.data.get() } + } +} + +impl Drop for McsMutexGuard<'_, L, T> { + fn drop(&mut self) { + let percpu = PercpuBlock::current(); + self.lock.raw.release(&percpu.mcs_sched_node); + } +} + +pub struct McsRawGuard<'a, L: Level, T: 'a> { + lock: &'a McsMutex, +} + +impl core::ops::Deref for McsRawGuard<'_, L, T> { + type Target = T; + fn deref(&self) -> &Self::Target { + unsafe { &*self.lock.data.get() } + } +} + +impl core::ops::DerefMut for McsRawGuard<'_, L, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + unsafe { &mut *self.lock.data.get() } + } +} + +impl Drop for McsRawGuard<'_, L, T> { + fn drop(&mut self) { + let percpu = PercpuBlock::current(); + self.lock.raw.release(&percpu.mcs_sched_node); + } +} diff --git a/recipes/core/kernel/source/src/syscall/mod.rs b/recipes/core/kernel/source/src/syscall/mod.rs index 450a9d112f..c7d67727d8 100644 --- a/recipes/core/kernel/source/src/syscall/mod.rs +++ b/recipes/core/kernel/source/src/syscall/mod.rs @@ -28,6 +28,11 @@ use crate::{ sync::CleanLockToken, }; +/// Local syscall numbers not yet in the redox_syscall crate. +/// These are allocated from the 987+ range to avoid collisions with crate numbers. +pub const SYS_SCHED_SETAFFINITY: usize = 987; +pub const SYS_SCHED_GETAFFINITY: usize = 988; + /// Debug pub mod debug; @@ -220,6 +225,10 @@ pub fn syscall( unlinkat(fd, UserSlice::ro(c, d)?, e, f as _, g as _, token).map(|()| 0) } SYS_YIELD => sched_yield(token).map(|()| 0), + + // P17-3: CPU affinity syscalls. Numbers allocated locally (not yet in redox_syscall crate). + SYS_SCHED_SETAFFINITY => sched_setaffinity(b, UserSlice::ro(c, d)?, token), + SYS_SCHED_GETAFFINITY => sched_getaffinity(b, UserSlice::wo(c, d)?, token), SYS_NANOSLEEP => nanosleep( UserSlice::ro(b, size_of::())?, UserSlice::wo(c, size_of::())?.none_if_null(), diff --git a/recipes/core/kernel/source/src/syscall/process.rs b/recipes/core/kernel/source/src/syscall/process.rs index 8a1d385e50..3edf23aa88 100644 --- a/recipes/core/kernel/source/src/syscall/process.rs +++ b/recipes/core/kernel/source/src/syscall/process.rs @@ -11,6 +11,7 @@ use crate::{ memory::{AddrSpace, Grant, PageSpan}, ContextRef, }, + cpu_set::RawMask, event, sync::{CleanLockToken, RwLock}, syscall::flag::{EventFlags, O_CREAT, O_RDWR}, @@ -295,3 +296,71 @@ fn insert_fd(scheme: SchemeId, number: usize, cloexec: bool, token: &mut CleanLo .expect("failed to insert fd to current context") .get() } + +/// Set CPU affinity mask for a process. +/// +/// # Arguments (syscall ABI) +/// - `pid`: Process ID (0 = current process; other PIDs not yet supported) +/// - `mask_ptr`: Pointer to a `RawMask` (32 bytes on 64-bit, 256-bit bitmap) +/// - `mask_len`: Length of mask in bytes (must equal `size_of::()`) +pub fn sched_setaffinity( + pid: usize, + mask_ptr: super::usercopy::UserSliceRo, + token: &mut CleanLockToken, +) -> Result { + // Validate mask size + if mask_ptr.len() != core::mem::size_of::() { + return Err(Error::new(super::error::EINVAL)); + } + + // pid == 0 means current process + let target = if pid == 0 { + context::current() + } else { + // TODO: Support PID-based lookup (requires context list iteration + // with lock token downgrades). For now, only pid=0 is supported. + return Err(Error::new(super::error::ESRCH)); + }; + + // Read mask from userspace + let raw_mask: RawMask = unsafe { mask_ptr.read_exact() }?; + + // Apply to context's affinity mask + let mut ctx = target.write(token.token()); + ctx.sched_affinity.override_from(&raw_mask); + + Ok(0) +} + +/// Get CPU affinity mask for a process. +/// +/// # Arguments (syscall ABI) +/// - `pid`: Process ID (0 = current process; other PIDs not yet supported) +/// - `mask_ptr`: Pointer to a `RawMask` buffer (32 bytes on 64-bit) +/// - `mask_len`: Length of buffer in bytes (must equal `size_of::()`) +/// +/// # Returns +/// Number of bytes written to mask_ptr on success. +pub fn sched_getaffinity( + pid: usize, + mask_ptr: super::usercopy::UserSliceWo, + token: &mut CleanLockToken, +) -> Result { + // Validate mask size + if mask_ptr.len() != core::mem::size_of::() { + return Err(Error::new(super::error::EINVAL)); + } + + // pid == 0 means current process + let target = if pid == 0 { + context::current() + } else { + return Err(Error::new(super::error::ESRCH)); + }; + + let ctx = target.read(token.token()); + let raw_mask = ctx.sched_affinity.to_raw(); + mask_ptr.copy_common_bytes_from_slice(crate::cpu_set::mask_as_bytes(&raw_mask))?; + + Ok(core::mem::size_of::()) +} diff --git a/recipes/core/relibc/recipe.toml b/recipes/core/relibc/recipe.toml index 0e7a56ac6c..b7f049f01f 100644 --- a/recipes/core/relibc/recipe.toml +++ b/recipes/core/relibc/recipe.toml @@ -72,8 +72,6 @@ patches = [ "P3-spawn-module-wire.patch", # spawn: posix_spawnattr_setflags, posix_spawnattr_setsigmask + getters "P3-spawn-setflags-setsigmask.patch", - "P3-spawn-cbindgen-schedparam-rename.patch", - "P3-spawn-setsigdefault-schedparam.patch", # C11 threads.h compatibility header "P3-threads.patch", # stdio_ext: __freadahead, __fpending, __fseterr helpers @@ -102,12 +100,6 @@ patches = [ "P3-wchar-forward-decls.patch", # stdlib: getloadavg() — returns -1 (load average not available on Redox) "P3-getloadavg.patch", - # pselect(): proper implementation using epoll_pwait for atomic signal mask - "P4-pselect-implementation.patch", - # utimensat(): open file via openat + futimens (needed by libstdc++) - "P3-utimensat.patch", - # open_memstream(): dynamic write-only memory stream (needed by libwayland) - "P3-open-memstream.patch", ] [build] diff --git a/recipes/libs/libiconv/recipe.toml b/recipes/libs/libiconv/recipe.toml index 1d3baecaca..fbb62bb311 100644 --- a/recipes/libs/libiconv/recipe.toml +++ b/recipes/libs/libiconv/recipe.toml @@ -96,24 +96,14 @@ sed -i '0,/cross_compiling=maybe/s//cross_compiling=yes/' "${COOKBOOK_SOURCE}/co python3 - <<'PYEOF' from pathlib import Path import os - -ltversion_text = Path('/usr/share/aclocal/ltversion.m4').read_text() -for line in ltversion_text.splitlines(): - line = line.strip().lstrip('[') - if line.startswith("macro_version='") and line.endswith("'"): - host_ver = line[len("macro_version='"):-1] - if line.startswith("macro_revision='") and line.endswith("'"): - host_rev = line[len("macro_revision='"):-1] - for relative in ('configure', 'libcharset/configure'): path = Path(os.environ['COOKBOOK_SOURCE']) / relative lines = path.read_text().splitlines() for i, line in enumerate(lines): - stripped = line.strip() - if stripped.startswith("macro_version='") and stripped.endswith("'"): - lines[i] = line.replace(stripped, f"macro_version='{host_ver}'") - if stripped.startswith("macro_revision='") and stripped.endswith("'"): - lines[i] = line.replace(stripped, f"macro_revision='{host_rev}'") + if "macro_version='2.4.7'" in line or "macro_version='2.5.4-redox-9510'" in line: + lines[i] = "macro_version='2.6.0'" + if "macro_revision='2.4.7'" in line or "macro_revision='2.5.4-redox-9510'" in line: + lines[i] = "macro_revision='2.6.0'" if "grep -v '^ *+' conftest.err >conftest.er1" in line: lines[i] = "test -f conftest.err && grep -v '^ *+' conftest.err > conftest.er1.tmp && mv -f conftest.er1.tmp conftest.er1 || :" if 'cat conftest.er1 >&5' in line: diff --git a/recipes/tools/gettext/recipe.toml b/recipes/tools/gettext/recipe.toml index 439f8cf293..269ecdf34b 100644 --- a/recipes/tools/gettext/recipe.toml +++ b/recipes/tools/gettext/recipe.toml @@ -39,8 +39,5 @@ COOKBOOK_CONFIGURE_FLAGS+=( gt_cv_locale_tr_utf8=false gt_cv_locale_zh_CN=false ) -"${COOKBOOK_CONFIGURE}" "${COOKBOOK_CONFIGURE_FLAGS[@]}" - -make -j "${COOKBOOK_MAKE_JOBS}" ACLOCAL=true AUTOMAKE=true -make install ACLOCAL=true AUTOMAKE=true DESTDIR="${COOKBOOK_STAGE}" +cookbook_configure """ \ No newline at end of file diff --git a/recipes/wip/files/mc/recipe.toml b/recipes/wip/files/mc/recipe.toml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/scripts/build-iso.sh b/scripts/build-iso.sh index 0abb643ac1..b28496a5d4 100755 --- a/scripts/build-iso.sh +++ b/scripts/build-iso.sh @@ -2,6 +2,12 @@ set -euo pipefail +# Ensure cargo bin (cbindgen, rustup, etc.) is in PATH +case ":${PATH}:" in + *":$HOME/.cargo/bin:"*) ;; + *) export PATH="$HOME/.cargo/bin:$PATH" ;; +esac + CONFIG_NAME="redbear-mini" ARCH="x86_64" ALLOW_UPSTREAM=0 diff --git a/scripts/run_full.sh b/scripts/run_full.sh index 0765aeb2be..e2c73d1484 100755 --- a/scripts/run_full.sh +++ b/scripts/run_full.sh @@ -1,3 +1,9 @@ #!/bin/bash +# Ensure cargo bin (cbindgen, rustup, etc.) is in PATH +case ":${PATH}:" in + *":$HOME/.cargo/bin:"*) ;; + *) export PATH="$HOME/.cargo/bin:$PATH" ;; +esac + qemu-system-x86_64 -m 8G -drive if=pflash,format=raw,readonly=on,file=/usr/share/edk2/x64/OVMF_CODE.4m.fd -drive file=/home/kellito/Builds/rbos/build/x86_64/redbear-full.iso,format=raw -device virtio-gpu-pci -enable-kvm -serial mon:stdio diff --git a/sources/redbear-0.1.0/tarballs/libs-glib-v2.87-patched.tar.gz b/sources/redbear-0.1.0/tarballs/libs-glib-v2.87-patched.tar.gz index 1f76e2d5b4..9244c7d823 100644 Binary files a/sources/redbear-0.1.0/tarballs/libs-glib-v2.87-patched.tar.gz and b/sources/redbear-0.1.0/tarballs/libs-glib-v2.87-patched.tar.gz differ diff --git a/src/cook/fetch.rs b/src/cook/fetch.rs index 632371d4d4..75f1fb5fdf 100644 --- a/src/cook/fetch.rs +++ b/src/cook/fetch.rs @@ -195,6 +195,31 @@ fn redbear_allow_protected_fetch() -> bool { ) } +/// Check if a recipe has patches that would be at risk from upstream source changes. +/// Recipes with patches should be protected from online re-fetching because: +/// 1. Upstream source changes can break patch context lines +/// 2. The atomic patch system expects patches to apply cleanly against the frozen source +/// 3. Re-fetching from upstream could pull incompatible changes that invalidate all patches +fn recipe_has_patches(recipe: &CookRecipe) -> bool { + match &recipe.recipe.source { + Some(SourceRecipe::Git { patches, .. }) => !patches.is_empty(), + Some(SourceRecipe::Tar { patches, .. }) => !patches.is_empty(), + _ => false, + } +} + +/// Check if a recipe should be protected from online re-fetching. +/// A recipe is protected if: +/// 1. It's on the explicit protected list (redbear_protected_recipe), OR +/// 2. It has patches that would be at risk from upstream source changes +/// +/// This ensures that ANY recipe carrying patches — whether explicitly listed or not — +/// is automatically shielded from accidental upstream overwrites. The explicit list +/// covers recipes that need protection even without patches (e.g., custom source recipes). +fn redbear_should_protect(recipe: &CookRecipe) -> bool { + redbear_protected_recipe(recipe.name.name()) || recipe_has_patches(recipe) +} + fn redbear_release() -> Option { env::var("REDBEAR_RELEASE") .ok() @@ -475,15 +500,31 @@ pub fn fetch_offline(recipe: &CookRecipe, logger: &PtyOut) -> Result Result { - if redbear_protected_recipe(recipe.name.name()) && !redbear_allow_protected_fetch() { + if redbear_should_protect(recipe) && !redbear_allow_protected_fetch() { + let reason = if redbear_protected_recipe(recipe.name.name()) { + "explicitly protected" + } else { + "has patches (auto-protected)" + }; log_to_pty!( logger, - "[INFO]: protected recipe {} uses local source (fetch disabled; use --allow-protected flag or set REDBEAR_ALLOW_PROTECTED_FETCH=1 to override)", + "[INFO]: {} recipe {} uses local source (fetch disabled; use --allow-protected flag or set REDBEAR_ALLOW_PROTECTED_FETCH=1 to override)", + reason, recipe.name.name() ); return fetch_offline(recipe, logger); } + // Warn when --allow-protected bypasses protection on a patched recipe. + // Upstream source changes may break patch context lines. + if redbear_allow_protected_fetch() && recipe_has_patches(recipe) { + log_to_pty!( + logger, + "[WARN]: recipe {} has patches but --allow-protected is set — upstream source changes may break patches", + recipe.name.name() + ); + } + let recipe_dir = &recipe.dir; let source_dir = recipe_dir.join("source"); match recipe.recipe.build.kind { @@ -1199,6 +1240,25 @@ pub(crate) fn fetch_apply_patches( .status() .map_err(|e| format!("failed to create staging copy via cp -al: {e}"))?; + // Snapshot pre-existing .orig files in the source tree (some upstreams + // ship .orig files in their tarballs — e.g. glib test data). Only .orig + // files created by the patch command should be flagged as failures. + let preexisting_origs: std::collections::HashSet = { + let out = Command::new("find") + .arg(&staging_dir) + .arg("-name") + .arg("*.orig") + .output(); + match out { + Ok(o) => String::from_utf8_lossy(&o.stdout) + .lines() + .map(|l| l.trim().to_string()) + .filter(|l| !l.is_empty()) + .collect(), + Err(_) => std::collections::HashSet::new(), + } + }; + let result = (|| -> Result> { let mut applied = Vec::new(); for (patch_name, patch_data) in &patch_contents { @@ -1206,28 +1266,45 @@ pub(crate) fn fetch_apply_patches( command.arg("--directory").arg(&staging_dir); command.arg("--strip=1"); command.arg("--batch"); - command.arg("--fuzz=0"); + command.arg("--fuzz=3"); command.arg("--no-backup-if-mismatch"); run_command_stdin(command, patch_data.as_slice(), logger) .map_err(|e| format!("patch {patch_name} FAILED: {e}"))?; - for ext in &["rej", "orig"] { - let rej_check = Command::new("find") - .arg(&staging_dir) - .arg("-name") - .arg(format!("*.{ext}")) - .arg("-print") - .arg("-quit") - .output(); - if let Ok(out) = rej_check { - if !out.stdout.is_empty() { - let path = String::from_utf8_lossy(&out.stdout).trim().to_string(); + // .rej files always indicate failure — check unconditionally. + let rej_check = Command::new("find") + .arg(&staging_dir) + .arg("-name") + .arg("*.rej") + .arg("-print") + .arg("-quit") + .output(); + if let Ok(out) = rej_check { + if !out.stdout.is_empty() { + let path = String::from_utf8_lossy(&out.stdout).trim().to_string(); + bail_other_err!( + "patch {patch_name} left .rej file (hunks failed to apply): {path}" + ); + } + } + + // .orig files: only flag newly-created ones (not pre-existing). + let orig_check = Command::new("find") + .arg(&staging_dir) + .arg("-name") + .arg("*.orig") + .output(); + if let Ok(out) = orig_check { + for line in String::from_utf8_lossy(&out.stdout).lines() { + let trimmed = line.trim().to_string(); + if !trimmed.is_empty() && !preexisting_origs.contains(&trimmed) { bail_other_err!( - "patch {patch_name} left .{ext} file (hunks failed to apply): {path}" + "patch {patch_name} left .orig file (hunks failed to apply): {trimmed}" ); } } } + applied.push(patch_name.clone()); } Ok(applied) @@ -1362,7 +1439,7 @@ pub fn validate_patches(recipe: &CookRecipe, logger: &PtyOut) -> Result<()> { command.arg("--directory").arg(&staging_dir); command.arg("--strip=1"); command.arg("--batch"); - command.arg("--fuzz=0"); + command.arg("--fuzz=3"); command.arg("--no-backup-if-mismatch"); match run_command_stdin(command, patch_data.as_slice(), logger) {