diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a055d87..476c44d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -155,27 +155,36 @@ jobs: body: | Automated production release built from `main`. - Both architectures ship a **bootable UEFI ISO** — flash it or attach it - in UTM / a VM / bare metal. aarch64 additionally ships the raw `-kernel` - ELF for `qemu-system-aarch64 -M virt -kernel` users. + Both architectures render the full Flutter shell. For **UTM on Apple + Silicon**, boot the aarch64 **`.kernel`** via *Linux → Boot from kernel + image* (see below) — UTM's bundled UEFI firmware does not boot the Limine + ISO, so the ISO is for QEMU/other UEFI VMs and bare metal. **Artifacts:** - - `oscortex-x86_64-.iso` — x86_64 bootable kernel ISO (Limine BIOS + - UEFI), built via `cargo xtask iso`. - - `oscortex-aarch64-.iso` — **render-capable** aarch64 UEFI ISO - (Limine → higher-half kernel), embedding the full Flutter shell runtime - (engine + AOT shell snapshot + ICU + fonts/assets). This is the image to - boot in **UTM / VMs / bare metal**. In UTM use a single CPU core - (OSCortex runs a cooperative single-core scheduler). - - `oscortex-aarch64-.kernel` — the same render-capable kernel as a raw - ELF for direct QEMU boot: + - `oscortex-x86_64-.iso` — x86_64 render-capable kernel ISO (Limine + BIOS + UEFI) embedding the Flutter shell. + - `oscortex-aarch64-.iso` — render-capable aarch64 UEFI ISO (Limine → + higher-half kernel) embedding the full Flutter shell runtime (engine + + shell snapshot + ICU + fonts/assets). Boots under QEMU with the edk2 + AArch64 firmware and on UEFI ARM bare metal. **Not for UTM** (see below). + - `oscortex-aarch64-.kernel` — the render-capable kernel as a raw ELF. + **This is the UTM-bootable artifact** and also boots directly under QEMU: ``` - qemu-system-aarch64 -M virt -cpu cortex-a72 -smp 1 -m 2G \ + qemu-system-aarch64 -M virt,accel=hvf -cpu host -smp 1 -m 4G \ -kernel oscortex-aarch64-.kernel -device ramfb \ -device virtio-tablet-device -device virtio-keyboard-device ``` - Wait ~60–90s (TCG warm-up) for the shell to paint; the virtio-tablet - gives mouse hover/click. + + **Booting the `.kernel` in UTM (Apple Silicon):** + 1. New VM → **Virtualize** → **Linux**; leave *Use Apple Virtualization* + unchecked (we need the QEMU backend for ramfb). + 2. *Boot from kernel image* → select `oscortex-aarch64-.kernel` + (no initrd / root FS). + 3. Finish, then **Edit** the VM: **System → CPU Cores = 1** (OSCortex is a + cooperative single-core scheduler); **Display → Emulated Display Card = + ram framebuffer standalone device (ramfb)**. + 4. Run. Under HVF the shell paints in a few seconds (under TCG, allow + ~60–90s warm-up). Verify with the attached `.sha256` files. files: | diff --git a/.gitignore b/.gitignore index 8b785a6..17aa0aa 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,9 @@ docs/goal-general-purpose.md # Fetched engine tool binaries (from fetch-engine.sh) /tools/flutter-engine/bin/ initramfs/system/lib/liboscortex_libc.so + +# build artifacts / debug traces (untracked 2026-06-12) +*.so.trace +iso_root_aarch64/ +oscortex-aarch64.iso +tools/flutter-engine/linux-arm64/ diff --git a/Cargo.lock b/Cargo.lock index a68087d..495237e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + [[package]] name = "bitflags" version = "1.3.2" @@ -23,6 +29,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + [[package]] name = "byteorder" version = "1.5.0" @@ -54,6 +66,12 @@ dependencies = [ "typenum", ] +[[package]] +name = "ct-codecs" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49fb0c6640b4507ebd99ff67677009e381ba5eee1d14df78de4a3d16eb123c39" + [[package]] name = "defmt" version = "0.3.100" @@ -105,6 +123,28 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "ed25519-compact" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5c0284a5d4b1a2fae017a9fe55fd7d01699711f1b572493f16593e173ea2801" +dependencies = [ + "ct-codecs", + "getrandom", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "generic-array" version = "0.14.7" @@ -115,6 +155,21 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasip3", + "wasm-bindgen", +] + [[package]] name = "hash32" version = "0.3.1" @@ -124,6 +179,21 @@ dependencies = [ "byteorder", ] +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + [[package]] name = "heapless" version = "0.8.0" @@ -134,6 +204,52 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2025f20d7a4fa7785846e7b63d10a76d3f1cee98ee5cb79ea59703f95e42162" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.186" @@ -182,12 +298,25 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + [[package]] name = "oscortex-kernel" version = "0.1.0" dependencies = [ "bitflags 2.11.1", "cfg-if", + "ed25519-compact", "libm", "limine", "linked_list_allocator", @@ -204,9 +333,20 @@ version = "0.1.0" name = "pkg-server" version = "0.1.0" dependencies = [ + "ed25519-compact", "sha2", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -247,12 +387,72 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "sha2" version = "0.10.9" @@ -346,12 +546,215 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a254a4b10c19a76f09a27640e7ffbf9bc30bf67e16a3bf28aaefa4920fe81563" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a40fc75b0ec6f3746ceb10d36f53a93dcd68a93b11b6445983945d79eba0dc" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "908f34bd9b9ce3d4caf07b72dfab63d61504d156856c6bd3cd87fa350cf3985b" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7acbf7616c27b194bbb550bf77ed0c2c3e5b7fd1260a93082b95fb7f47959b92" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "xtask" version = "0.1.0" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/docs/branch-integration-plan.md b/docs/branch-integration-plan.md new file mode 100644 index 0000000..030934c --- /dev/null +++ b/docs/branch-integration-plan.md @@ -0,0 +1,21 @@ +# ARM/x86 branch integration plan (2026-06-12 recon) + +Goal: "one source tree, both arches" true on ONE branch. Currently split: +- origin/main: v0.0.6 aarch64 real-HW render (5 fixes: SPSel a27a61d-area/7c1d751, daifset bd96e7e, crash#2 454bd63, SP-align 7e1a019, CNTV timer e9fd481) + release pipeline (a2d6750/3191527/5d0e750). 64 ahead of feat. +- feat/native-engine-port: pkg pipeline (b4dcdb5/463e689/30c3684/ff933f0), Ed25519 signing (0cf5aa8), caps (c769520), crash-recovery (62e2cd3), hardening (e870f28). 99 ahead of main. + +Merge-base 320e92d. A real merge = 37 conflicted files, MODERATE severity. + +STRATEGY (recommended): merge origin/main INTO feat/native-engine-port in a worktree. +Resolution heuristics: +- aarch64/{timer,enter_user,boot,vectors,mmu,apic,cpu,syscall,mod}.rs → TAKE MAIN (--theirs): it has the 5 render fixes; feat's is pre-fix. timer.rs MUST become CNTV/PPI27. +- boot_limine.rs → auto (add-only on main). +- .github/workflows/release.yml → take main (arch-labeled artifacts + aarch64 UEFI ISO). +- kernel/src/pkg/*, embedder/abi.rs (pkg syscalls 0x4C0-0x4C3), syscall/handlers/* → TAKE FEAT (--ours): main lacks the pipeline. +- MANUAL 3-way union (the real work): process/mod.rs (626 lines — crash-recovery/caps/kill_group/note_thread_exit + caps field/helpers vs ARM teardown/SP/arch_frame), syscall/dispatch.rs (pkg syscalls + cap gate vs ARM), syscall/mod.rs, main.rs, mm/mod.rs, cortex/pid0.rs (CAP_CORTEX check). + +VERIFICATION GATE (all must pass before landing): (1) x86 + aarch64 kernel+embedder build, (2) x86 ISO renders shell smp=1, (3) ARM renders under `-M virt,accel=hvf -cpu host` (SP-align+CNTV are TCG-invisible — HVF mandatory), (4) pkg pipeline cold-boot 5.4MB fetch→SHA256→cache→install, (5) Ed25519+caps+crash-recovery smoke. + +FALLBACK if process/mod.rs union is intractable: cherry-pick the 9 ARM/release commits onto feat (lower blast radius, aarch64/*+release.yml only): a27a61d 7c1d751 bd96e7e 454bd63 7e1a019 e9fd481 5d0e750 3191527 a2d6750. + +Do it in a worktree; only fast-forward + push once the full gate passes. feat's ARM = real port but pre-render-fixes (runs in TCG, won't render on HVF). diff --git a/docs/utm-bringup.md b/docs/utm-bringup.md new file mode 100644 index 0000000..6922244 --- /dev/null +++ b/docs/utm-bringup.md @@ -0,0 +1,78 @@ +# OSCortex UTM Bring-up Guide + +This guide details how to configure, deploy, and boot **OSCortex** on **UTM** (the GUI frontend for QEMU on macOS) for both emulated `x86_64` and native virtualised `aarch64` architectures. Testing under UTM simulates physical hardware characteristics more closely than standard command-line QEMU. + +--- + +## 1. x86_64 Emulation on M-series/Intel Macs + +To test the full graphical shell and persistence on UTM: + +1. **Build the ISO**: + Ensure you have built the latest bootable ISO: + ```bash + bash scripts/build-iso.sh + ``` + This generates `oscortex.iso` in the project root. + +2. **Create the VM in UTM**: + * Open UTM, click **+** (Create a New Virtual Machine). + * Select **Emulate** (performs instruction translation on M-series Macs). + * Choose **Other** as the platform. + * Under **Boot ISO Image**, browse and select `oscortex.iso`. + +3. **Configure VM Settings**: + * **System**: + * Hardware: Allocate **2048 MB** of RAM and **2 CPU Cores**. + * Target: **Standard PC (Q35 + ICH9, 2009) (q35)**. + * CPU: **qemu64** or **host** (Intel). Ensure x2apic is enabled. + * **QEMU**: + * Add the following arguments under QEMU settings if you want custom serial redirection: + `-serial file:/tmp/osc_serial.log` + * **Drives**: + * The CD/DVD drive is automatically mounted with `oscortex.iso`. + * Click **New Drive** to add a data drive for package persistence: + * Size: **8 MB** (or import `vdisk.img`). + * Interface: **VirtIO** (matches the `virtio-blk-pci` driver). + * Click **New Drive** to add an NVMe test drive: + * Size: **16 MB** (or import `nvme.img`). + * Interface: **NVMe** (matches the NVMe controller). + +4. **Launch the VM**: + * Save settings and click the **Play** button. + * The Limine bootloader will hand off execution to the kernel, bringing up the Flutter graphics shell. + +--- + +## 2. AArch64 (ARM64) Native Virtualisation on M-series Macs + +To run the ARM64 kernel natively with hypervisor acceleration (hypervisor.framework): + +1. **Build the ARM64 Kernel**: + ```bash + cargo build --target aarch64-unknown-none -p oscortex-kernel \ + --no-default-features --features arch-aarch64 \ + -Z build-std=core,compiler_builtins,alloc \ + -Z build-std-features=compiler-builtins-mem + ``` + This generates the ELF kernel binary at `target/aarch64-unknown-none/debug/kernel`. + +2. **Create the VM in UTM**: + * Click **+** (Create a New Virtual Machine). + * Select **Virtualize** (uses native Apple Silicon virtualization). + * Choose **Other** as the platform. + * Check **Skip ISO boot**. + +3. **Configure VM Settings**: + * **System**: + * Target: **QEMU Virtual Machine (virt)**. + * Boot: Select **Direct Kernel Boot**. + * Kernel Image: Select the built ELF kernel `target/aarch64-unknown-none/debug/kernel`. + * RAM: Allocate **2048 MB**. + * **Display & Devices**: + * Add a **Virtual Display** backed by QEMU's **RAMFB** (corresponds to the guest `-device ramfb` driver). + * Add a **Serial Console** interface (handles the `PL011` serial output). + +4. **Launch the VM**: + * Save settings and click the **Play** button. + * The kernel will boot natively via the hypervisor, initializing the MMU, GIC, and showing the boot frame buffer. diff --git a/iso_root_aarch64/EFI/BOOT/BOOTAA64.EFI b/iso_root_aarch64/EFI/BOOT/BOOTAA64.EFI deleted file mode 100644 index 26dad09..0000000 Binary files a/iso_root_aarch64/EFI/BOOT/BOOTAA64.EFI and /dev/null differ diff --git a/iso_root_aarch64/EFI/BOOT/limine.conf b/iso_root_aarch64/EFI/BOOT/limine.conf deleted file mode 100644 index c2edc69..0000000 --- a/iso_root_aarch64/EFI/BOOT/limine.conf +++ /dev/null @@ -1,9 +0,0 @@ -# OSCortex aarch64 boot configuration (Limine UEFI) -timeout: 0 -serial: yes -verbose: yes - -/OSCortex AI-First Kernel - protocol: limine - path: boot():/boot/kernel - kaslr: no diff --git a/iso_root_aarch64/boot/kernel b/iso_root_aarch64/boot/kernel deleted file mode 100644 index 3ab0c48..0000000 Binary files a/iso_root_aarch64/boot/kernel and /dev/null differ diff --git a/iso_root_aarch64/boot/limine/limine-uefi-cd.bin b/iso_root_aarch64/boot/limine/limine-uefi-cd.bin deleted file mode 100644 index eb6d61e..0000000 Binary files a/iso_root_aarch64/boot/limine/limine-uefi-cd.bin and /dev/null differ diff --git a/iso_root_aarch64/boot/limine/limine.conf b/iso_root_aarch64/boot/limine/limine.conf deleted file mode 100644 index c2edc69..0000000 --- a/iso_root_aarch64/boot/limine/limine.conf +++ /dev/null @@ -1,9 +0,0 @@ -# OSCortex aarch64 boot configuration (Limine UEFI) -timeout: 0 -serial: yes -verbose: yes - -/OSCortex AI-First Kernel - protocol: limine - path: boot():/boot/kernel - kaslr: no diff --git a/iso_root_aarch64/limine.conf b/iso_root_aarch64/limine.conf deleted file mode 100644 index c2edc69..0000000 --- a/iso_root_aarch64/limine.conf +++ /dev/null @@ -1,9 +0,0 @@ -# OSCortex aarch64 boot configuration (Limine UEFI) -timeout: 0 -serial: yes -verbose: yes - -/OSCortex AI-First Kernel - protocol: limine - path: boot():/boot/kernel - kaslr: no diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml index bc8b1a3..965341b 100644 --- a/kernel/Cargo.toml +++ b/kernel/Cargo.toml @@ -10,6 +10,7 @@ name = "kernel" path = "src/main.rs" [dependencies] +ed25519-compact = { version = "2", default-features = false } spin = { version = "0.9", features = ["mutex", "rwlock", "once"] } bitflags = "2" libm = { version = "0.2", default-features = false } diff --git a/kernel/assets/logo_mask.bin b/kernel/assets/logo_mask.bin new file mode 100644 index 0000000..dfd04e8 Binary files /dev/null and b/kernel/assets/logo_mask.bin differ diff --git a/kernel/src/app_registry/mod.rs b/kernel/src/app_registry/mod.rs index f788d3e..bde2985 100644 --- a/kernel/src/app_registry/mod.rs +++ b/kernel/src/app_registry/mod.rs @@ -485,6 +485,9 @@ pub fn launch(app_id: u32, _flags: u32) -> i64 { // aot_va=0: the embedder ignores it in APP mode when is_aot=false. crate::process::set_bootstrap_regs(child_pid, HOST_MODE_APP, app_id as u64, 0); + // Track the running instance for crash auto-recovery (see note_thread_exit). + note_running(child_pid, app_id); + crate::wm::push_app_event(child_pid, crate::embedder::abi::APP_LAUNCH, 0); // Switch focus to the freshly launched app. This does double duty: @@ -526,3 +529,128 @@ pub fn get_info(app_id: u32, buf: &mut [u8; 88]) -> bool { buf[84..88].copy_from_slice(&(record.aot_data.len() as u32).to_le_bytes()); true } + +// ── Crash auto-recovery ─────────────────────────────────────────────────────── +// +// "One crashes, the kernel recovers it in milliseconds." A launched app whose +// thread group dies abnormally is torn down and relaunched automatically, with +// a retry cap so a crash-looping app degrades to the shell instead of flapping. +// +// Flow: process::exit() → note_thread_exit (try-lock only — runs in whatever +// context the death happened in, possibly an ISR) records the dead app. The +// shell's event pump (sys_wm_next_event, pid 1, called continuously) drains via +// drain_relaunches() in a normal syscall context: tear down group survivors, +// refocus the shell, and relaunch the app. + +const RECOVERY_SLOTS: usize = 8; +/// Max automatic relaunches of one app within the window before giving up. +const MAX_RELAUNCHES: u32 = 3; +/// Backoff window (30 s). +const RELAUNCH_WINDOW_NS: u64 = 30_000_000_000; + +/// (root_pid, app_id) of currently-running launched apps. +static RUNNING: Mutex<[(u32, u32); RECOVERY_SLOTS]> = Mutex::new([(0, 0); RECOVERY_SLOTS]); +/// Abnormally-dead apps awaiting relaunch: (app_id, dead_leader_pid). +static PENDING: Mutex<[(u32, u32); RECOVERY_SLOTS]> = Mutex::new([(0, 0); RECOVERY_SLOTS]); +/// Relaunch backoff: (app_id, attempts, window_start_ns). +static HISTORY: Mutex<[(u32, u32, u64); RECOVERY_SLOTS]> = Mutex::new([(0, 0, 0); RECOVERY_SLOTS]); + +/// Record a freshly-launched app (called from `launch`). +fn note_running(root_pid: u32, app_id: u32) { + let mut t = RUNNING.lock(); + if let Some(slot) = t.iter_mut().find(|s| s.0 == 0) { + *slot = (root_pid, app_id); + } +} + +/// Process-exit notification from `process::exit()`. May run in ISR context — +/// try-lock only; a missed notification under contention just means no +/// auto-relaunch for that particular death (the scheduler liveness fallback +/// still returns control to the shell). +pub fn note_thread_exit(leader: u32, pid: u32, code: i32) { + // Only thread-group LEADER deaths matter for bookkeeping of graceful exits; + // for crashes, any member's abnormal death condemns the group. + let Some(mut running) = RUNNING.try_lock() else { return }; + let Some(slot) = running.iter_mut().find(|s| s.0 == leader && s.0 != 0) else { + return; + }; + let app_id = slot.1; + if code == 0 { + // Graceful exit of the leader: just forget it. (A worker thread + // exiting cleanly is normal and doesn't end the app.) + if pid == leader { + *slot = (0, 0); + } + return; + } + // Abnormal death (fault / kill): condemn the group, queue a relaunch. + *slot = (0, 0); + drop(running); + if let Some(mut pending) = PENDING.try_lock() { + if let Some(p) = pending.iter_mut().find(|s| s.0 == 0) { + *p = (app_id, leader); + log::error!( + "[recover] app_id={} (group {}) died abnormally (pid={} code={}) — relaunch queued", + app_id, leader, pid, code + ); + } + } +} + +/// Drain pending relaunches. Called from the shell's event pump +/// (sys_wm_next_event, pid 1) — a normal syscall context where spawning is +/// safe. Cheap when idle: one try-lock + a scan of 8 slots. +pub fn drain_relaunches() { + let (app_id, dead_leader) = { + let Some(mut pending) = PENDING.try_lock() else { return }; + let Some(p) = pending.iter_mut().find(|s| s.0 != 0) else { return }; + let v = *p; + *p = (0, 0); + v + }; + + // 1. Tear down any surviving threads of the dead instance — a relaunch + // while old engine threads still run is the two-VM corruption case. + let killed = crate::process::kill_group(dead_leader); + + // 2. Make sure the shell is the foreground again (the crash may have left + // focus pointing at the dead group). + if crate::wm::focus_pid() != 1 { + crate::wm::set_focus_pid(1); + } + + // 3. Backoff: give up after MAX_RELAUNCHES within the window. + let now = crate::syscall::poll::monotonic_ns(); + let mut history = HISTORY.lock(); + let slot = match history.iter_mut().find(|s| s.0 == app_id) { + Some(s) => s, + None => match history.iter_mut().find(|s| s.0 == 0) { + Some(s) => { + *s = (app_id, 0, now); + s + } + None => return, // table full — drop silently (8 distinct crashing apps) + }, + }; + if now.saturating_sub(slot.2) > RELAUNCH_WINDOW_NS { + // Window expired — reset the counter. + *slot = (app_id, 0, now); + } + if slot.1 >= MAX_RELAUNCHES { + log::error!( + "[recover] app_id={} crashed {} times in 30s — giving up (staying at shell)", + app_id, slot.1 + ); + return; + } + slot.1 += 1; + let attempt = slot.1; + drop(history); + + // 4. Relaunch. + let pid = launch(app_id, 0); + log::error!( + "[recover] relaunched app_id={} -> pid={} (attempt {}/{}, reaped {} survivors)", + app_id, pid, attempt, MAX_RELAUNCHES, killed + ); +} diff --git a/kernel/src/arch/aarch64/apic.rs b/kernel/src/arch/aarch64/apic.rs index 75322f5..ece7eb8 100644 --- a/kernel/src/arch/aarch64/apic.rs +++ b/kernel/src/arch/aarch64/apic.rs @@ -134,6 +134,10 @@ fn production_irq_handler(f: &mut super::vectors::TrapFrame) { // present syscall path (present_surface → render_frame) in normal context. crate::compositor::vsync_baton_tick(); reset_vsync_last_tsc(); + // Poll the USB xHCI runtime for HID input (mouse/keyboard) on the vsync + // boundary — mirrors the x86 APIC ISR (idt.rs). Drains transfer events + // (→ wm::push_pointer/push_key) and re-arms the interrupt transfer. + crate::drivers::usb::poll(); } // Only the EL0-preempt switch below requires an interrupted EL0 frame. SPSR_EL1 @@ -159,7 +163,7 @@ fn production_irq_handler(f: &mut super::vectors::TrapFrame) { if target != 0 && (cur == 0 || crate::process::is_blocked_try(cur)) && (crate::wm::input_pending_for(target) > 0 - || (target == 1 && crate::wm::embedder_baton_due())) + || crate::wm::embedder_baton_due(target)) && cur != target { // BISECT: just wake the target; let normal cooperative scheduling enter @@ -193,7 +197,7 @@ fn production_irq_handler(f: &mut super::vectors::TrapFrame) { && target != 0 && cur != target && (crate::wm::input_pending_for(target) > 0 - || (target == 1 && crate::wm::embedder_baton_due())))); + || crate::wm::embedder_baton_due(target)))); { static PREEMPT_TICK_LOG: AtomicU32 = AtomicU32::new(0); let n = PREEMPT_TICK_LOG.fetch_add(1, Ordering::Relaxed); diff --git a/kernel/src/arch/aarch64/boot_limine.rs b/kernel/src/arch/aarch64/boot_limine.rs index fbf1ce2..a9e0f30 100644 --- a/kernel/src/arch/aarch64/boot_limine.rs +++ b/kernel/src/arch/aarch64/boot_limine.rs @@ -240,9 +240,8 @@ pub fn init_framebuffer() { // (device + RAM, 0..2 GiB) on QEMU/UTM virt, so the raw address is directly // writable. Publish it to the shared fb console. crate::drivers::fb::init_raw(addr, w, h, pitch); - crate::drivers::fb::fill_rect(0, 0, w, 48, 0x0014_B8A6); - crate::drivers::fb::fill_rect(0, 48, w, h.saturating_sub(48), 0x001A_1A2E); - crate::drivers::fb::write_str("\n OSCortex aarch64 — Limine framebuffer up\n"); - crate::drivers::fb::disable_fb_logging(); + // Clean boot screen from the first frame (silences on-screen log spam). + crate::drivers::bootscreen::init(); + crate::drivers::bootscreen::render(); log::info!("[limine-boot] framebuffer online: {}x{} via Limine", w, h); } diff --git a/kernel/src/arch/aarch64/mmu.rs b/kernel/src/arch/aarch64/mmu.rs index 930e6a6..1a8a437 100644 --- a/kernel/src/arch/aarch64/mmu.rs +++ b/kernel/src/arch/aarch64/mmu.rs @@ -113,6 +113,50 @@ unsafe fn fill_identity(table: &mut Table, ram_gibs: u64) { } } +/// Identity-map the 1 GiB Device block containing the physical address `pa` +/// into both translation halves, if it is not already mapped. +/// +/// The boot identity map only covers the low 1 GiB of device MMIO (entry 0) plus +/// a few GiB of RAM. Windows above that — notably the QEMU `virt` PCIe ECAM at +/// 0x40_1000_0000 (= 257 GiB) — fall outside it and must be mapped on demand +/// before the kernel touches them. This installs a single Device-nGnRE 1 GiB L1 +/// block at the aligned index and flushes the TLB so the new mapping takes +/// effect. Idempotent. +/// +/// # Safety +/// Must run at EL1 with the MMU on. `pa` must lie within the 39-bit VA range. +pub unsafe fn map_device_1gib(pa: u64) { + if !MMU_ON.load(Ordering::Acquire) { + return; + } + let idx = (pa >> 30) as usize; + if idx >= 512 { + return; + } + let base = (pa >> 30) << 30; // 1 GiB-aligned + let desc = block_desc(base, ATTR_IDX_DEVICE, DESC_AP_RW_EL1); + let l1_low = &mut *core::ptr::addr_of_mut!(L1_LOW); + let l1_high = &mut *core::ptr::addr_of_mut!(L1_HIGH); + let mut changed = false; + if l1_low.0[idx] != desc { + l1_low.0[idx] = desc; + changed = true; + } + if l1_high.0[idx] != desc { + l1_high.0[idx] = desc; + changed = true; + } + if changed { + core::arch::asm!( + "dsb ish", + "tlbi vmalle1is", + "dsb ish", + "isb", + options(nostack, preserves_flags), + ); + } +} + /// Returns the physical address of the active TTBR0 L1 table (for the shared /// `arch::memory::read_cr3` view). Until per-process tables exist, all address /// spaces share this identity map. diff --git a/kernel/src/arch/aarch64/mod.rs b/kernel/src/arch/aarch64/mod.rs index 77d0110..d47802c 100644 --- a/kernel/src/arch/aarch64/mod.rs +++ b/kernel/src/arch/aarch64/mod.rs @@ -23,6 +23,7 @@ pub mod cpu; pub mod syscall; pub mod acpi; pub mod smp; +pub mod pci; pub mod enter_user; // ── Bare-metal bring-up (direct `-kernel` boot) ───────────────────────────── diff --git a/kernel/src/arch/aarch64/pci.rs b/kernel/src/arch/aarch64/pci.rs new file mode 100644 index 0000000..9cdd223 --- /dev/null +++ b/kernel/src/arch/aarch64/pci.rs @@ -0,0 +1,256 @@ +//! PCI configuration space — aarch64 PCIe ECAM (memory-mapped config). +//! +//! aarch64 has no legacy CF8/CFC config ports. On the QEMU `virt` machine PCIe +//! config space is exposed as a memory-mapped ECAM region. The DTB `pcie@…` +//! node gives (verified against `qemu-system-aarch64 -M virt`, QEMU 11): +//! * ECAM `reg` = <0x40 0x1000_0000 0x00 0x1000_0000> +//! → base 0x40_1000_0000 (257 GiB), size 256 MiB. +//! * 32-bit MMIO = bus 0x1000_0000 → cpu 0x1000_0000, size 0x2eff_0000 +//! → window 0x1000_0000–0x3eff_0000 (BARs land here). +//! +//! The 32-bit MMIO/BAR window (0x1000_0000–0x3eff_0000) is below 0x4000_0000 and +//! is already covered by the boot identity map's Device entry 0 (mmu.rs), so a +//! decoded BAR0 is directly accessible at its physical address (hhdm = 0 on +//! aarch64). The ECAM region itself sits at 257 GiB, outside the boot map, so we +//! map its 1 GiB block on first use via `mmu::map_device_1gib`. +//! +//! ECAM address = ECAM_BASE + ((bus<<20)|(dev<<15)|(func<<12)) + (off & 0xFFC). + +use core::sync::atomic::{AtomicBool, AtomicU64, Ordering}; + +/// Bump allocator for the QEMU `virt` 32-bit PCIe MMIO window +/// (0x1000_0000–0x3eff_0000). On `-kernel` boot there is no firmware to program +/// PCIe BARs, so the kernel assigns them itself out of this window. +static MMIO_ALLOC_NEXT: AtomicU64 = AtomicU64::new(0x1000_0000); +const MMIO_WINDOW_END: u64 = 0x3eff_0000; + +use crate::arch::mmio; +use crate::drivers::common::pci_bar; + +/// QEMU `virt` PCIe ECAM physical base (see module docs). +const ECAM_BASE: u64 = 0x40_1000_0000; + +/// True when this architecture exposes legacy PCI config I/O ports (CF8/CFC). +/// aarch64 has none — config space is the ECAM MMIO window instead. +pub const LEGACY_IO_AVAILABLE: bool = false; + +/// True when PCI config space is reachable at all on this arch (via ECAM here). +pub const PCI_AVAILABLE: bool = true; + +static ECAM_MAPPED: AtomicBool = AtomicBool::new(false); + +/// Ensure the ECAM 1 GiB block is identity-mapped before any config access. +fn ensure_ecam_mapped() { + if ECAM_MAPPED.swap(true, Ordering::AcqRel) { + return; + } + unsafe { crate::arch::aarch64::mmu::map_device_1gib(ECAM_BASE) }; +} + +#[inline] +fn cfg_addr(bus: u8, dev: u8, func: u8, offset: u8) -> u64 { + ECAM_BASE + + (((bus as u64) << 20) | ((dev as u64) << 15) | ((func as u64) << 12)) + + ((offset as u64) & 0xFFC) +} + +/// Read a 32-bit PCI config DWORD. `offset` is the byte offset (0, 4, 8, …). +pub fn config_read32(bus: u8, dev: u8, func: u8, offset: u8) -> u32 { + ensure_ecam_mapped(); + unsafe { mmio::read32(cfg_addr(bus, dev, func, offset), 0) } +} + +/// Write a 32-bit PCI config DWORD. +pub fn config_write32(bus: u8, dev: u8, func: u8, offset: u8, val: u32) { + ensure_ecam_mapped(); + unsafe { mmio::write32(cfg_addr(bus, dev, func, offset), 0, val) } +} + +/// Enable memory space + bus mastering on a PCI function (bits 1 and 2 of the +/// command register at config offset 0x04). aarch64 PCIe BARs are memory-mapped, +/// so we set memory-space rather than the x86 legacy I/O-space bit. +pub fn enable_io_and_busmaster(bus: u8, dev: u8, func: u8) { + let cmd = config_read32(bus, dev, func, 0x04); + // Enable memory-space decode (bit 1), then bus-master (bit 2), with a settle + // gap after each. Enabling memory-space remaps the BAR's MMIO region; under + // HVF the host rebuild is async and a racing access hangs the vCPU. + config_write32(bus, dev, func, 0x04, cmd | (1 << 1)); + pci_settle(); + config_write32(bus, dev, func, 0x04, cmd | (1 << 1) | (1 << 2)); + pci_settle(); +} + +/// Barrier + wall-clock gap to let QEMU's HVF-side memory-map rebuild complete +/// before the next config/MMIO access (see bar0_mmio_phys). +fn pci_settle() { + unsafe { + core::arch::asm!("dsb sy", "isb", options(nomem, nostack)); + } + for _ in 0..5_000_000u64 { + core::hint::spin_loop(); + } +} + +/// BAR0 as a legacy I/O port base. Meaningless on aarch64 (no port I/O) — 0. +pub fn bar0_io_base(_bus: u8, _dev: u8, _func: u8) -> u16 { + 0 +} + +/// BAR0 as a memory-mapped physical base (I/O-space BARs return None). +/// +/// On the QEMU `virt` machine booted via `-kernel` there is no firmware to +/// program PCIe BARs, so BAR0 reads back with its address field zeroed (only the +/// type bits are set, e.g. `0x4` = 64-bit memory). When that happens we size the +/// BAR (write all-ones, read the mask back) and assign it an aligned address out +/// of the 32-bit MMIO window before returning it. +/// +/// UEFI/edk2 boot is different: the firmware DOES assign BARs, and for a 64-bit +/// BAR it places them in the high PCIe MMIO window at 512 GiB +/// (e.g. 0x80_0000_8000). That address is outside our 39-bit TTBR0 identity map +/// (and `map_device_1gib` can't reach it), so touching it faults. We only accept +/// a firmware assignment that lands in the low 32-bit window we can identity-map; +/// anything higher (or unassigned) is (re)assigned into that window. +pub fn bar0_mmio_phys(bus: u8, dev: u8, func: u8) -> Option { + let bar0_lo = config_read32(bus, dev, func, 0x10); + if bar0_lo & 1 != 0 { + return None; // I/O-space BAR — no MMIO base + } + let is64 = (bar0_lo >> 1) & 0x3 == 0x2; + let bar0_hi = if is64 { config_read32(bus, dev, func, 0x14) } else { 0 }; + let current = ((bar0_hi as u64) << 32) | ((bar0_lo & !0xF) as u64); + // Accept an existing assignment only if it's inside the identity-mapped low + // 32-bit MMIO window; otherwise fall through and re-assign it there. + if current != 0 && current < MMIO_WINDOW_END { + return Some(current); + } + + // Unassigned (or assigned too high) — size the BAR (write all-ones, read the + // mask back) and re-assign it into the low 32-bit window below. + config_write32(bus, dev, func, 0x10, 0xFFFF_FFFF); + let size_lo = config_read32(bus, dev, func, 0x10); + let size_hi = if is64 { + config_write32(bus, dev, func, 0x14, 0xFFFF_FFFF); + config_read32(bus, dev, func, 0x14) + } else { + 0xFFFF_FFFF + }; + let mask = ((size_hi as u64) << 32) | ((size_lo & !0xF) as u64); + if mask == 0 { + // No address bits implemented — restore and bail. + config_write32(bus, dev, func, 0x10, bar0_lo); + if is64 { + config_write32(bus, dev, func, 0x14, bar0_hi); + } + return None; + } + let size = (!mask).wrapping_add(1); + let align = if size < 0x1000 { 0x1000 } else { size }; + + // Bump-allocate an aligned address out of the 32-bit MMIO window. + let base = loop { + let next = MMIO_ALLOC_NEXT.load(Ordering::Acquire); + let aligned = (next + align - 1) & !(align - 1); + if aligned + size > MMIO_WINDOW_END { + config_write32(bus, dev, func, 0x10, bar0_lo); + if is64 { + config_write32(bus, dev, func, 0x14, bar0_hi); + } + return None; // window exhausted + } + if MMIO_ALLOC_NEXT + .compare_exchange(next, aligned + size, Ordering::AcqRel, Ordering::Acquire) + .is_ok() + { + break aligned; + } + }; + + // Programming the BAR triggers QEMU to (asynchronously, under HVF) rebuild the + // guest memory map for the new MMIO region. If anything (notably the timer + // ISR's framebuffer/GIC MMIO) accesses device memory while that rebuild is in + // flight, the vCPU hangs hard. Mask interrupts across the write + a settle gap + // (full completion barrier + wall-clock spin, no MMIO) so the remap finishes + // uncontended before the next config access (e.g. enabling memory decode). + let irq = crate::arch::interrupts_save_and_disable(); + config_write32(bus, dev, func, 0x10, (base as u32 & 0xFFFF_FFF0) | (bar0_lo & 0xF)); + if is64 { + config_write32(bus, dev, func, 0x14, (base >> 32) as u32); + } + unsafe { + core::arch::asm!("dsb sy", "isb", options(nomem, nostack)); + } + for _ in 0..5_000_000u64 { + core::hint::spin_loop(); + } + crate::arch::interrupts_restore(irq); + Some(base) +} + +/// Locate a PCI function by class code; returns BDF + MMIO BAR0 physical base. +pub fn find_device_pci(class: u8, subclass: u8, prog_if: u8) -> Option<(u8, u8, u8, u64)> { + // QEMU `virt` exposes a single PCIe bus; scan 0..=1 to stay cheap on MMIO. + for bus in 0u8..=1 { + for dev in 0u8..32 { + for func in 0u8..8 { + let id = config_read32(bus, dev, func, 0x00); + if id == 0xFFFF_FFFF { + continue; + } + let cc = config_read32(bus, dev, func, 0x08); + let dev_class = ((cc >> 24) & 0xFF) as u8; + let dev_subclass = ((cc >> 16) & 0xFF) as u8; + let dev_progif = ((cc >> 8) & 0xFF) as u8; + if dev_class == class && dev_subclass == subclass && dev_progif == prog_if { + let bar = bar0_mmio_phys(bus, dev, func)?; + return Some((bus, dev, func, bar)); + } + } + } + } + None +} + +/// Scan a bus for a VirtIO device with the given vendor/device ID. +pub fn find_virtio_legacy(bus: u8, vendor: u16, device: u16) -> Option<(u8, u8)> { + for dev in 0u8..32 { + let id = config_read32(bus, dev, 0, 0x00); + if id == 0xFFFF_FFFF { + continue; + } + if (id as u16) == vendor && ((id >> 16) as u16) == device { + return Some((bus, dev)); + } + } + None +} + +/// Scan for a device matching PCI class / subclass / prog-if; return BAR0 phys. +pub fn find_device_bar0(class: u8, subclass: u8, prog_if: u8) -> Option { + find_device_pci(class, subclass, prog_if).map(|(_, _, _, bar)| bar) +} + +/// Count devices matching class / subclass / prog-if (up to `max` results). +pub fn count_class(bus_limit: u8, class: u8, subclass: u8, prog_if: u8, max: u32) -> u32 { + let mut found = 0u32; + 'outer: for bus in 0u8..=bus_limit { + for dev in 0u8..32 { + for func in 0u8..8 { + let id = config_read32(bus, dev, func, 0x00); + if id == 0xFFFF_FFFF { + continue; + } + let class_reg = config_read32(bus, dev, func, 0x08); + let c = (class_reg >> 24) as u8; + let sc = (class_reg >> 16) as u8; + let pi = (class_reg >> 8) as u8; + if c == class && sc == subclass && pi == prog_if { + found += 1; + if found >= max { + break 'outer; + } + } + } + } + } + found +} diff --git a/kernel/src/arch/pci.rs b/kernel/src/arch/pci.rs index c876e98..84761a1 100644 --- a/kernel/src/arch/pci.rs +++ b/kernel/src/arch/pci.rs @@ -3,8 +3,11 @@ cfg_if::cfg_if! { if #[cfg(target_arch = "x86_64")] { pub use crate::arch::x86_64::pci::*; + } else if #[cfg(target_arch = "aarch64")] { + pub use crate::arch::aarch64::pci::*; } else { pub const LEGACY_IO_AVAILABLE: bool = false; + pub const PCI_AVAILABLE: bool = false; pub fn config_read32(_bus: u8, _dev: u8, _func: u8, _offset: u8) -> u32 { 0xFFFF_FFFF diff --git a/kernel/src/arch/x86_64/idt.rs b/kernel/src/arch/x86_64/idt.rs index f4ae273..ef175f6 100644 --- a/kernel/src/arch/x86_64/idt.rs +++ b/kernel/src/arch/x86_64/idt.rs @@ -626,7 +626,7 @@ extern "C" fn apic_timer_handler(frame_ptr: *mut TimerTrapFrame) { && target != 0 && cur != target && !crate::wm::flutter_bootstrap_spin_active() - && (crate::wm::input_pending_for(target) > 0 || (target == 1 && crate::wm::embedder_baton_due())) + && (crate::wm::input_pending_for(target) > 0 || crate::wm::embedder_baton_due(target)) ); if should_preempt { @@ -701,7 +701,7 @@ extern "C" fn apic_timer_handler(frame_ptr: *mut TimerTrapFrame) { && crate::wm::flutter_init_ready() && !crate::wm::flutter_bootstrap_spin_active() && (cur == 0 || crate::process::is_blocked_try(cur)) - && (crate::wm::input_pending_for(target) > 0 || (target == 1 && crate::wm::embedder_baton_due())) + && (crate::wm::input_pending_for(target) > 0 || crate::wm::embedder_baton_due(target)) { if crate::process::set_state_try(target, crate::process::ProcState::Running) { diff --git a/kernel/src/arch/x86_64/pci.rs b/kernel/src/arch/x86_64/pci.rs index 79c6d08..0a18935 100644 --- a/kernel/src/arch/x86_64/pci.rs +++ b/kernel/src/arch/x86_64/pci.rs @@ -11,6 +11,11 @@ const CONFIG_DATA: u16 = 0xCFC; /// True when this architecture exposes legacy PCI config I/O ports. pub const LEGACY_IO_AVAILABLE: bool = true; +/// True when PCI config space is reachable on this arch. On x86 the legacy +/// CF8/CFC ports are always present, so this mirrors `LEGACY_IO_AVAILABLE` and +/// the x86 path is unchanged. +pub const PCI_AVAILABLE: bool = LEGACY_IO_AVAILABLE; + /// Read a 32-bit PCI config DWORD. `offset` is the byte offset (0, 4, 8, …). pub fn config_read32(bus: u8, dev: u8, func: u8, offset: u8) -> u32 { let addr: u32 = (1u32 << 31) diff --git a/kernel/src/compositor/mod.rs b/kernel/src/compositor/mod.rs index 30077ca..47c276a 100644 --- a/kernel/src/compositor/mod.rs +++ b/kernel/src/compositor/mod.rs @@ -716,15 +716,17 @@ fn cursor_line(x: i32, y: i32, mut x0: i32, mut y0: i32, x1: i32, y1: i32, color /// field hover → I-beam), so hovering interactive widgets actually changes the /// cursor — a real platform capability, not a stub. fn draw_software_cursor() { - use core::sync::atomic::Ordering; - if !crate::drivers::ps2::PS2_READY.load(Ordering::Relaxed) { + // Cursor state is unified in `wm` and fed by every pointer source (x86 PS/2 + // AND aarch64 virtio-input), so this works on both arches. Previously it was + // gated on the x86-only PS2_READY flag and never drew on ARM. + if !crate::wm::cursor_seen() { return; } - // Auto-disappear cursor after 3 seconds of inactivity (6,000,000,000 TSC cycles @ 2GHz) - let last_act = crate::drivers::ps2::last_activity_tsc(); - let now = crate::arch::rdtsc(); - if now.wrapping_sub(last_act) > 6_000_000_000 { + // Auto-disappear cursor after 3 seconds of inactivity (arch-neutral ns clock). + let last_act = crate::wm::cursor_last_act_ns(); + let now = crate::arch::rdtsc_ns(); + if now.wrapping_sub(last_act) > 3_000_000_000 { return; } @@ -734,8 +736,8 @@ fn draw_software_cursor() { return; } - let (mut x, mut y) = crate::drivers::ps2::cursor_pos(); - let buttons = crate::drivers::ps2::cursor_buttons(); + let (mut x, mut y) = crate::wm::cursor_pos(); + let buttons = crate::wm::cursor_buttons(); // Keep the sprite fully on-screen (max sprite extent is ~10px from hotspot). if let Some((w, h)) = crate::drivers::fb::size_px() { @@ -947,17 +949,17 @@ pub fn render_frame() { let frame = c.frame_counter.wrapping_add(1); c.frame_counter = frame; drop(c); - if let Some((w, h)) = crate::drivers::fb::size_px() { - crate::drivers::fb::fill_rect(0, 0, w, h, 0x000c1c26); - // No app has presented a frame yet → we're in the engine warm-up. - // Draw the animated loading splash so the screen isn't a dead blank. - crate::drivers::fb::draw_boot_splash(frame); - } + // No app has presented a frame yet → we're in the engine warm-up. Draw the + // kernel boot screen (dot-matrix OSCORTEX + progress + status, or the F2 + // verbose log overlay) instead of raw logs. It fills its own black bg. + crate::drivers::bootscreen::render(); draw_software_cursor(); crate::drivers::fb::swap_buffers(); crate::wm::push_vsync(frame); return; } + // First real surface presented → boot is done; snap the progress to 100%. + crate::drivers::bootscreen::mark_done(); let bypass = is_fb_bypass(); drop(c); diff --git a/kernel/src/cortex/pid0.rs b/kernel/src/cortex/pid0.rs index 9fb5fae..3e5bb00 100644 --- a/kernel/src/cortex/pid0.rs +++ b/kernel/src/cortex/pid0.rs @@ -35,10 +35,11 @@ pub fn init() { } fn require_cortex_caller() -> bool { - let pid = crate::process::current_pid(); - // Until capabilities are stored on each PCB, only init (pid 1) and the - // kernel idle task (pid 0) may invoke the Cortex API. - pid == 0 || pid == 1 + // The kernel idle task (pid 0) is implicitly trusted; every other caller + // must hold CAP_CORTEX (the system shell does, launched apps do not). A + // real per-PCB capability check now, not a PID whitelist. + crate::process::current_pid() == 0 + || crate::process::current_has_caps(crate::security::Capabilities::CORTEX) } fn node_kind_from_u64(kind: u64) -> Option { diff --git a/kernel/src/drivers/bootscreen.rs b/kernel/src/drivers/bootscreen.rs new file mode 100644 index 0000000..08a1dc5 --- /dev/null +++ b/kernel/src/drivers/bootscreen.rs @@ -0,0 +1,268 @@ +//! Boot screen — a clean, product-grade boot UI drawn by the kernel while the +//! Flutter engine warms up, instead of scrolling raw kernel logs. +//! +//! Layout (mirrors the DotCorr "Doto" design language): +//! * a dot-matrix "OSCORTEX" wordmark, centered; +//! * a small "s cold boot ●" caption to its right; +//! * a thin progress bar (dark track, blue fill) that ramps as boot proceeds; +//! * a green status line "cortex:: · s". +//! +//! Kernel logs are silenced on the framebuffer by default (they still go to the +//! serial console for developers). Press **F2** to toggle a verbose overlay that +//! renders the recent kernel log ring on demand. + +use core::sync::atomic::{AtomicBool, AtomicU64, Ordering}; + +use crate::drivers::fb; +use crate::logger; + +// ── palette (0x00RRGGBB) ───────────────────────────────────────────────────── +const BG: u32 = 0x0000_0000; // black — power-friendly +const DOT: u32 = 0x00C2_C6CE; // light grey wordmark dots +const CAPTION: u32 = 0x006A_6E78; // dim grey caption +const ACCENT: u32 = 0x003B_9EFF; // blue dot + progress fill +const TRACK: u32 = 0x001E_2129; // progress track +const STATUS: u32 = 0x0052_C88A; // green status line +const LOG_FG: u32 = 0x008A_9098; // verbose log text +const LOG_ERR: u32 = 0x00E0_5A4A; // verbose log error/warn text + +static START_NS: AtomicU64 = AtomicU64::new(0); +static VERBOSE: AtomicBool = AtomicBool::new(false); +static DONE: AtomicBool = AtomicBool::new(false); + +fn now_ns() -> u64 { + crate::syscall::poll::monotonic_ns() +} + +/// Record the boot start instant and silence on-screen log spam. Idempotent. +pub fn init() { + START_NS + .compare_exchange(0, now_ns().max(1), Ordering::AcqRel, Ordering::Relaxed) + .ok(); + fb::disable_fb_logging(); +} + +/// Mark boot complete — the progress bar snaps to 100% and the status reads +/// `boot_completed`. +pub fn mark_done() { + DONE.store(true, Ordering::Release); +} + +pub fn is_verbose() -> bool { + VERBOSE.load(Ordering::Acquire) +} + +/// F2 handler: flip between the boot screen and the verbose log overlay. Verbose +/// mode raises the live log level to Info so the ring keeps filling; the default +/// stays Error so a normal boot is fast. +pub fn toggle_verbose() { + let v = !VERBOSE.load(Ordering::Acquire); + VERBOSE.store(v, Ordering::Release); + log::set_max_level(if v { + log::LevelFilter::Info + } else { + log::LevelFilter::Error + }); +} + +fn elapsed_ns() -> u64 { + let s = START_NS.load(Ordering::Relaxed); + if s == 0 { + return 0; + } + now_ns().saturating_sub(s) +} + +/// Asymptotic ramp toward 92% (so it always looks like it's making progress +/// without knowing the total warm-up time), snapping to 100% once done. +fn percent() -> u32 { + if DONE.load(Ordering::Acquire) { + return 100; + } + let ms = elapsed_ns() / 1_000_000; + ((92 * ms) / (ms + 2500)) as u32 +} + +fn stage() -> &'static [u8] { + if DONE.load(Ordering::Acquire) { + return b"boot_completed"; + } + match percent() { + 0..=24 => b"starting", + 25..=54 => b"init_runtime", + _ => b"warming_engine", + } +} + +/// Format `ns` as `".s"` into `buf`; returns the written slice. +fn fmt_secs(ns: u64, buf: &mut [u8]) -> usize { + let tenths = ns / 100_000_000; + let whole = tenths / 10; + let frac = tenths % 10; + let mut n = write_u64(whole, buf); + if n < buf.len() { + buf[n] = b'.'; + n += 1; + } + if n < buf.len() { + buf[n] = b'0' + frac as u8; + n += 1; + } + if n < buf.len() { + buf[n] = b's'; + n += 1; + } + n +} + +fn write_u64(mut v: u64, buf: &mut [u8]) -> usize { + let mut tmp = [0u8; 20]; + let mut i = 0; + if v == 0 { + tmp[0] = b'0'; + i = 1; + } else { + while v > 0 && i < tmp.len() { + tmp[i] = b'0' + (v % 10) as u8; + v /= 10; + i += 1; + } + } + let mut n = 0; + while n < i && n < buf.len() { + buf[n] = tmp[i - 1 - n]; + n += 1; + } + n +} + +fn append<'a>(buf: &'a mut [u8], at: usize, src: &[u8]) -> usize { + let mut n = at; + for &b in src { + if n >= buf.len() { + break; + } + buf[n] = b; + n += 1; + } + n +} + +/// Render one frame of the boot UI (or the verbose log overlay). Called each +/// vsync by the compositor while no app surface has presented yet, and once +/// early in boot so the screen is never blank. +pub fn render() { + let (w, h) = match fb::size_px() { + Some(d) => d, + None => return, + }; + fb::fill_rect(0, 0, w, h, BG); + + if VERBOSE.load(Ordering::Acquire) { + render_logs(w, h); + } else { + render_boot(w, h); + } +} + +fn render_boot(w: u32, h: u32) { + let wi = w as i32; + let hi = h as i32; + + // Dot-matrix wordmark, ~45% of screen width. + const MARK: &[u8] = b"OSCORTEX"; + let cell = ((w * 45 / 100) / (MARK.len() as u32 * 8)).clamp(6, 16); + let dot = (cell * 3 / 5).max(3); + let mark_w = fb::dotted_text_width(MARK.len(), cell); + let mark_h = (8 * cell) as i32; + let mark_x = (wi - mark_w) / 2; + let mark_y = hi * 30 / 100; + fb::draw_dotted_text(MARK, mark_x, mark_y, cell, dot, DOT); + + // "s cold boot ●" caption, to the right of the wordmark. + let mut cap = [0u8; 32]; + let mut n = fmt_secs(elapsed_ns(), &mut cap); + n = append(&mut cap, n, b" cold boot"); + let cap_scale = (cell / 5).max(1); + let cap_x = mark_x + mark_w + (cell * 2) as i32; + let cap_y = mark_y + (cell as i32); // near the top of the wordmark + if cap_x + fb::text_width(n, cap_scale) + (8 * cap_scale) as i32 <= wi { + fb::draw_text(&cap[..n], cap_x, cap_y, cap_scale, CAPTION); + // accent dot ● + let dot_px = (3 * cap_scale).max(3); + let dx = cap_x + fb::text_width(n, cap_scale) + (cap_scale * 4) as i32; + fb::fill_rect(dx, cap_y + (cap_scale) as i32, dot_px, dot_px, ACCENT); + } + + // Progress bar, same width as the wordmark. + let bar_h = (cell).max(4); + let bar_x = mark_x; + let bar_y = mark_y + mark_h + (cell * 4) as i32; + fb::fill_rect(bar_x, bar_y, mark_w as u32, bar_h, TRACK); + let fill = (mark_w as u32) * percent() / 100; + if fill > 0 { + fb::fill_rect(bar_x, bar_y, fill, bar_h, ACCENT); + } + + // "cortex:: · s" status line, centered below the bar. The + // mid-dot separator isn't in the ASCII font, so it's drawn as a small square. + let mut left = [0u8; 48]; + let mut ln = append(&mut left, 0, b"cortex::"); + ln = append(&mut left, ln, stage()); + let mut right = [0u8; 16]; + let rn = fmt_secs(elapsed_ns(), &mut right); + + let st_scale = (cell / 4).max(1); + let gap = (6 * st_scale) as i32; + let dot_sz = (2 * st_scale).max(2); + let lw = fb::text_width(ln, st_scale); + let rw = fb::text_width(rn, st_scale); + let total = lw + gap + dot_sz as i32 + gap + rw; + let mut x = (wi - total) / 2; + let y = bar_y + bar_h as i32 + (cell * 3) as i32; + fb::draw_text(&left[..ln], x, y, st_scale, STATUS); + x += lw + gap; + fb::fill_rect(x, y + (3 * st_scale) as i32, dot_sz, dot_sz, STATUS); + x += dot_sz as i32 + gap; + fb::draw_text(&right[..rn], x, y, st_scale, STATUS); +} + +fn render_logs(w: u32, h: u32) { + let mut lines = [[0u8; logger::RING_COLS]; logger::RING_LINES]; + let mut lens = [0u8; logger::RING_LINES]; + let count = logger::snapshot_logs(&mut lines, &mut lens); + + let scale = if w >= 1100 { 2 } else { 1 }; + let line_h = (8 * scale + 4) as i32; + let pad = 16i32; + + fb::draw_text(b"VERBOSE (F2 to hide) - full log: serial COM1", pad, pad, scale, STATUS); + if count == 0 { + fb::draw_text( + b"(no kernel log lines captured yet)", + pad, + pad + line_h + 6, + scale, + LOG_FG, + ); + } + + // Draw the most recent lines that fit below the header. + let avail = ((h as i32 - pad * 2 - line_h) / line_h).max(0) as usize; + let first = count.saturating_sub(avail); + let mut y = pad + line_h + 6; + for i in first..count { + let line = &lines[i][..lens[i] as usize]; + let is_err = contains(line, b"[ERROR]") || contains(line, b"[WARN]"); + let color = if is_err { LOG_ERR } else { LOG_FG }; + fb::draw_text(line, pad, y, scale, color); + y += line_h; + } +} + +fn contains(hay: &[u8], needle: &[u8]) -> bool { + if needle.is_empty() || hay.len() < needle.len() { + return false; + } + hay.windows(needle.len()).any(|w| w == needle) +} diff --git a/kernel/src/drivers/fb.rs b/kernel/src/drivers/fb.rs index c20f7fd..9483004 100644 --- a/kernel/src/drivers/fb.rs +++ b/kernel/src/drivers/fb.rs @@ -131,6 +131,32 @@ static FONT: [[u8; 8]; 96] = [ static FB_ADDR: AtomicU64 = AtomicU64::new(0); /// Pitch in 32-bit words (= pitch_bytes / 4 for 32bpp). static FB_PITCH_PX: AtomicU32 = AtomicU32::new(0); + +// ── Pixel format (firmware channel order) ───────────────────────────────────── +// Colors flow through this module as 0x00RRGGBB. Real UEFI GOP framebuffers are +// not always XRGB (e.g. many Intel Macs report RGB / red_shift=0), so we read the +// firmware's channel shifts and repack at the final framebuffer write. The +// XRGB fast-path keeps the common case (ARM ramfb, x86 std-vga) byte-identical. +static FB_R_SHIFT: AtomicU32 = AtomicU32::new(16); +static FB_G_SHIFT: AtomicU32 = AtomicU32::new(8); +static FB_B_SHIFT: AtomicU32 = AtomicU32::new(0); +/// True when channel order is exactly XRGB (16/8/0) → write u32 colors verbatim. +static FB_XRGB_FAST: AtomicBool = AtomicBool::new(true); + +/// Repack a 0x00RRGGBB color into the firmware's channel order. Identity on the +/// XRGB fast-path (no cost on the common path). +#[inline(always)] +fn fb_pack(color: u32) -> u32 { + if FB_XRGB_FAST.load(Ordering::Relaxed) { + return color; + } + let r = (color >> 16) & 0xFF; + let g = (color >> 8) & 0xFF; + let b = color & 0xFF; + (r << FB_R_SHIFT.load(Ordering::Relaxed)) + | (g << FB_G_SHIFT.load(Ordering::Relaxed)) + | (b << FB_B_SHIFT.load(Ordering::Relaxed)) +} /// Display width in pixels. static FB_WIDTH: AtomicU32 = AtomicU32::new(0); /// Display height in pixels. @@ -186,8 +212,22 @@ pub fn init(fb_resp: &limine::request::FramebufferResponse) { let addr = fb.address() as u64; if addr == 0 { return; } - // Only 32 bpp is supported by this console. - if fb.bpp != 32 { return; } + // Read the firmware's channel order. Log it unconditionally so a real + // machine's GOP format is visible on the serial console for diagnosis. + let (rs, gs, bs) = (fb.red_mask_shift as u32, fb.green_mask_shift as u32, fb.blue_mask_shift as u32); + log::info!( + "[fb] GOP {}x{} bpp={} pitch={} model={} shifts r={} g={} b={}", + fb.width, fb.height, fb.bpp, fb.pitch, fb.memory_model, rs, gs, bs + ); + + // Only 32 bpp is supported by this console (24bpp byte-packing is a TODO). + if fb.bpp != 32 { log::warn!("[fb] unsupported bpp {} — no UI", fb.bpp); return; } + + FB_R_SHIFT.store(rs, Ordering::Release); + FB_G_SHIFT.store(gs, Ordering::Release); + FB_B_SHIFT.store(bs, Ordering::Release); + // Fast-path only when the firmware is exactly XRGB. Otherwise repack at write. + FB_XRGB_FAST.store(rs == 16 && gs == 8 && bs == 0, Ordering::Release); let pitch_px = (fb.pitch / 4) as u32; let width = fb.width as u32; @@ -249,6 +289,12 @@ pub fn disable_fb_logging() { FB_SILENT.store(true, Ordering::Release); } +/// Re-enable framebuffer console text logging (used by the verbose-log toggle +/// and the panic handler so failures are always visible on screen). +pub fn enable_fb_logging() { + FB_SILENT.store(false, Ordering::Release); +} + /// Return whether the framebuffer console is initialised. pub fn is_ready() -> bool { FB_READY.load(Ordering::Acquire) @@ -297,7 +343,7 @@ pub fn set_pixel(x: u32, y: u32, color: u32) { } } let addr = FB_ADDR.load(Ordering::Relaxed) as *mut u32; - unsafe { addr.add(y as usize * pitch_px + x as usize).write_volatile(color); } + unsafe { addr.add(y as usize * pitch_px + x as usize).write_volatile(fb_pack(color)); } } /// Read a single pixel at `(x, y)` from the framebuffer or double buffer. @@ -350,11 +396,12 @@ pub fn fill_rect(x: i32, y: i32, w: u32, h: u32, color: u32) { } let addr = FB_ADDR.load(Ordering::Relaxed) as *mut u32; + let packed = fb_pack(color); unsafe { for py in y0 as usize..y1 as usize { let row = addr.add(py * pitch_px); for px in x0 as usize..x1 as usize { - row.add(px).write_volatile(color); + row.add(px).write_volatile(packed); } } } @@ -401,47 +448,98 @@ fn draw_text_centered(s: &[u8], py: i32, scale: u32, color: u32) { } } -/// Minimal animated loading spinner, drawn by the compositor while no app -/// surface has presented yet (the Flutter engine's JIT warm-up). Just a ring of -/// dots with a rotating bright head — enough to show the machine is alive, no -/// text. A Flutter spinner can't appear until the engine is ready, so this has -/// to be drawn by the kernel. -pub fn draw_boot_splash(frame: u64) { - let width = FB_WIDTH.load(Ordering::Relaxed) as i32; - let height = FB_HEIGHT.load(Ordering::Relaxed) as i32; - if width == 0 || height == 0 { +/// Pixel width of `len` glyphs at `scale` (8 px per glyph cell). +pub fn text_width(len: usize, scale: u32) -> i32 { + (len as u32 * CHAR_W * scale) as i32 +} + +/// Draw an ASCII string left-aligned at `(x, py)`, magnified by `scale`. +pub fn draw_text(s: &[u8], x: i32, py: i32, scale: u32, color: u32) { + let cw = (CHAR_W * scale) as i32; + let mut cx = x; + for &ch in s { + draw_glyph_px(ch, cx, py, scale, color); + cx += cw; + } +} + +/// Pixel width of `len` glyphs rendered dot-matrix style with grid `cell`. +pub fn dotted_text_width(len: usize, cell: u32) -> i32 { + (len as u32 * CHAR_W * cell) as i32 +} + +/// Render ASCII as a dot matrix (the "Doto" look): each lit pixel of the 8×8 +/// glyph becomes a `dot`-sized square centered in a `cell`-sized grid cell. +/// `cell > dot` leaves the gaps between dots. Left-aligned at `(x, py)`. +pub fn draw_dotted_text(s: &[u8], x: i32, py: i32, cell: u32, dot: u32, color: u32) { + let off = (cell.saturating_sub(dot) / 2) as i32; + let cell_i = cell as i32; + let char_adv = (CHAR_W * cell) as i32; + let mut cx = x; + for &ch in s { + if (0x20..0x80).contains(&ch) { + let glyph = &FONT[(ch - 0x20) as usize]; + let mut gy = 0u32; + while gy < CHAR_H { + let bits = glyph[gy as usize]; + let mut gx = 0u32; + while gx < CHAR_W { + if (bits >> (7 - gx)) & 1 != 0 { + fill_rect( + cx + (gx as i32) * cell_i + off, + py + (gy as i32) * cell_i + off, + dot, + dot, + color, + ); + } + gx += 1; + } + gy += 1; + } + } + cx += char_adv; + } +} + +/// 256×256 8-bit alpha mask of the white OSCortex/Dotcorr logo mark, rasterised +/// from landing/public/dotcorr-logo-mark-white.svg at build-prep time. +static LOGO_MASK: &[u8; 256 * 256] = include_bytes!("../../assets/logo_mask.bin"); +const LOGO_SRC: u32 = 256; + +/// Boot splash, drawn by the compositor while no app surface has presented yet +/// (the Flutter engine's JIT warm-up). The white OSCortex logo, centered, scaled +/// to ~1/4 of the screen's shorter dimension — a consistent, Apple-boot-mark size +/// on any display. The caller paints the (black) background first; this blits the +/// mark over it. Static (white on black: black is power-friendly, the mark is +/// already white). A Flutter splash can't appear until the engine is ready, so +/// this has to be drawn by the kernel. +pub fn draw_boot_splash(_frame: u64) { + let w = FB_WIDTH.load(Ordering::Relaxed); + let h = FB_HEIGHT.load(Ordering::Relaxed); + if w == 0 || h == 0 { return; } - let cx = width / 2; - let cy = height / 2; - - // 8 dot positions evenly around a circle (radius ~28px), clockwise from right. - const OFFS: [(i32, i32); 8] = [ - (28, 0), - (20, 20), - (0, 28), - (-20, 20), - (-28, 0), - (-20, -20), - (0, -28), - (20, -20), - ]; - let head = ((frame / 4) % 8) as i32; - let mut i = 0i32; - while i < 8 { - // Trailing distance behind the rotating head (0 = brightest). - let d = ((head - i) & 7) as u32; - // Fade teal (0x2D,0xD4,0xBF) by (8-d)/8 down the tail. - let f = (8 - d) as u32; - let r = (0x2D * f) / 8; - let g = (0xD4 * f) / 8; - let b = (0xBF * f) / 8; - let color = (r << 16) | (g << 8) | b; - let size: u32 = if d == 0 { 9 } else { 7 }; - let off = (size as i32) / 2; - let (dx, dy) = OFFS[i as usize]; - fill_rect(cx + dx - off, cy + dy - off, size, size, color); - i += 1; + // Target box = 1/5 of the shorter screen side (clamped), centered — the same + // proportion as the Apple boot mark. Nearest-neighbour scale of the 256² alpha + // mask (which is cropped tight to the logo), so it's crisp and fully + // resolution-independent. + let target = (w.min(h) / 5).max(64); + let ox = w.saturating_sub(target) / 2; + let oy = h.saturating_sub(target) / 2; + let mut ty = 0u32; + while ty < target { + let sy = (ty * LOGO_SRC) / target; + let mut tx = 0u32; + while tx < target { + let sx = (tx * LOGO_SRC) / target; + // The mark is white-on-transparent: alpha doubles as coverage. + if LOGO_MASK[(sy * LOGO_SRC + sx) as usize] >= 110 { + set_pixel(ox + tx, oy + ty, 0x00FF_FFFF); // white (XRGB8888) + } + tx += 1; + } + ty += 1; } } @@ -513,7 +611,7 @@ pub fn blit_rgba32(x: i32, y: i32, src_w: u32, src_h: u32, src: &[u32]) { let g = (px & 0x0000_FF00) >> 8; let b = (px & 0x00FF_0000) >> 16; let xrgb = (r << 16) | (g << 8) | b; - dst_base.add(dst_row + col).write_volatile(xrgb); + dst_base.add(dst_row + col).write_volatile(fb_pack(xrgb)); } } } @@ -545,7 +643,14 @@ pub fn swap_buffers() { let pitch_px = FB_PITCH_PX.load(Ordering::Relaxed) as usize; let total = height * pitch_px; unsafe { - core::ptr::copy_nonoverlapping(buf.as_ptr(), addr, total); + if FB_XRGB_FAST.load(Ordering::Relaxed) { + core::ptr::copy_nonoverlapping(buf.as_ptr(), addr, total); + } else { + // Non-XRGB firmware: repack each pixel into the panel's channel order. + for i in 0..total { + addr.add(i).write_volatile(fb_pack(buf[i])); + } + } } } } @@ -635,7 +740,7 @@ fn blit_char(ch: u8, col: u32, row: u32) { // offset is guaranteed within bounds by px < width && py < height. unsafe { let ptr = addr as *mut u32; - ptr.add(offset).write_volatile(color); + ptr.add(offset).write_volatile(fb_pack(color)); } } gx += 1; @@ -674,7 +779,7 @@ fn scroll_up() { // Clear the last character row. let clear_start = base.add((rows - 1) as usize * CHAR_H as usize * words_per_row); for i in 0..(CHAR_H as usize * words_per_row) { - clear_start.add(i).write_volatile(BG); + clear_start.add(i).write_volatile(fb_pack(BG)); } } } @@ -689,7 +794,7 @@ fn clear() { unsafe { let base = addr as *mut u32; for i in 0..total { - base.add(i).write_volatile(BG); + base.add(i).write_volatile(fb_pack(BG)); } } } diff --git a/kernel/src/drivers/mod.rs b/kernel/src/drivers/mod.rs index 460be09..85801dd 100644 --- a/kernel/src/drivers/mod.rs +++ b/kernel/src/drivers/mod.rs @@ -2,6 +2,7 @@ #[cfg(target_arch = "x86_64")] pub mod beep; +pub mod bootscreen; pub mod common; pub mod fb; pub mod nvme; diff --git a/kernel/src/drivers/platform.rs b/kernel/src/drivers/platform.rs index bb54e02..6cec09b 100644 --- a/kernel/src/drivers/platform.rs +++ b/kernel/src/drivers/platform.rs @@ -19,8 +19,17 @@ pub fn init_early(qemu_like: bool) { log::warn!("[Input] PS/2 skipped (bare-metal safe mode)"); } - if pci::LEGACY_IO_AVAILABLE { - super::usb::probe_and_init(); + // USB xHCI HID probe. Skipped on the aarch64 UEFI/Limine ISO build: probing + // the firmware-configured xHCI's PCIe config space under QEMU+HVF trips a + // QEMU host-side `assert(isv)` (hvf.c) that kills the VM — a QEMU+HVF host bug + // on the edk2 boot path, not present on real hardware, under TCG, or on the + // bare `-kernel` boot (which self-assigns BARs and works fully). The `-kernel` + // artifact is the supported UTM arm64 path and keeps USB HID; x86 keeps it too. + #[cfg(not(all(target_arch = "aarch64", feature = "limine-boot")))] + { + if pci::PCI_AVAILABLE { + super::usb::probe_and_init(); + } } register_input_natives(qemu_like); diff --git a/kernel/src/drivers/usb.rs b/kernel/src/drivers/usb.rs index b7c17b0..2248243 100644 --- a/kernel/src/drivers/usb.rs +++ b/kernel/src/drivers/usb.rs @@ -64,7 +64,7 @@ pub fn probe_and_init() { if USB_XHCI_COUNT.load(Ordering::Relaxed) != 0 { return; } - if !pci::LEGACY_IO_AVAILABLE { + if !pci::PCI_AVAILABLE { log::info!("[USB] XHCI skipped — PCI unavailable on this arch"); return; } @@ -72,7 +72,11 @@ pub fn probe_and_init() { let hhdm = crate::mm::frame_allocator::hhdm_offset(); let mut found = 0u32; - for bus in 0u8..=255u8 { + // QEMU `virt` (aarch64) exposes a single PCIe bus; scanning all 256 buses + // over ECAM MMIO is slow and pointless. x86 keeps the full legacy scan. + let bus_limit: u8 = if cfg!(target_arch = "aarch64") { 1 } else { 255 }; + + for bus in 0u8..=bus_limit { for dev in 0u8..32u8 { for func in 0u8..8u8 { let id = pci::config_read32(bus, dev, func, 0x00); @@ -97,14 +101,32 @@ pub fn probe_and_init() { let vendor = (id & 0xFFFF) as u16; let device = (id >> 16) as u16; - pci::enable_io_and_busmaster(bus, dev, func); - let bar_virt = bar_phys + hhdm; - unsafe { - crate::mm::paging::map_mmio(bar_phys, bar_virt, XHCI_MMIO_SIZE); - } + // On aarch64 the QEMU virt 32-bit MMIO window (where the assigned + // BAR lands) is already covered by the boot identity map's Device + // entry 0, so the BAR is reachable at bar_phys directly (hhdm = 0). + // Do NOT re-map it — double-mapping the 1GiB block faults. On x86 + // the BAR is reached via the HHDM after an explicit map_mmio. + #[cfg(target_arch = "aarch64")] + let bar_virt = bar_phys; + #[cfg(not(target_arch = "aarch64"))] + let bar_virt = { + let v = bar_phys + hhdm; + unsafe { crate::mm::paging::map_mmio(bar_phys, v, XHCI_MMIO_SIZE); } + v + }; + + // Enabling memory-space decode makes QEMU rebuild the guest memory + // map for the BAR; under HVF a timer-ISR MMIO access racing that + // rebuild hangs the vCPU. Mask interrupts across the enable AND the + // first controller MMIO (init_controller) so nothing else touches + // device memory mid-remap. + let irq = crate::arch::interrupts_save_and_disable(); + pci::enable_io_and_busmaster(bus, dev, func); + let init = init_controller(bus, dev, func, bar_phys, bar_virt, vendor, device); + crate::arch::interrupts_restore(irq); - match init_controller(bus, dev, func, bar_phys, bar_virt, vendor, device) { + match init { Ok(ctrl) => { if (found as usize) < MAX_CONTROLLERS { if let Err(e) = super::xhci_runtime::start(&ctrl) { @@ -158,7 +180,9 @@ fn init_controller( ) -> Result { unsafe { let cap = mmio::read32(bar_virt, 0); - let hcsparams1 = mmio::read32(bar_virt, (cap & 0xFF) as usize); + // HCSPARAMS1 is at the fixed capability-register offset 0x04, NOT at + // CAPLENGTH (that's where the operational registers begin). + let hcsparams1 = mmio::read32(bar_virt, 0x04); let caps = parse_capability(cap, hcsparams1)?; let usbcmd_off = caps.usbcmd_off; diff --git a/kernel/src/drivers/virtio_net.rs b/kernel/src/drivers/virtio_net.rs index ce0a3a0..767857f 100644 --- a/kernel/src/drivers/virtio_net.rs +++ b/kernel/src/drivers/virtio_net.rs @@ -12,7 +12,12 @@ use crate::drivers::common::virtio_net_frame::{ }; use crate::drivers::common::vring::{VirtqDesc, VringLayout, VIRTQ_DESC_F_WRITE}; -const RX_SLOTS: usize = 8; +// Posted RX descriptors. 8 slots (~12 KB of frames in flight) overflowed the +// moment a TCP sender bursts more than 8 frames between guest polls — slirp +// then drops and falls into retransmit-timeout pacing (~28 KB/s observed on a +// 5.4 MB package fetch, regardless of the TCP window). 64 slots absorbs the +// bursts; the virtqueue is 256 entries and each slot is one 4 KiB frame. +const RX_SLOTS: usize = 64; const VIRTIO_DEVICE_FEATURES: u16 = 0x00; const VIRTIO_GUEST_FEATURES: u16 = 0x04; diff --git a/kernel/src/drivers/xhci_runtime.rs b/kernel/src/drivers/xhci_runtime.rs index 971e0b0..0e3de2f 100644 --- a/kernel/src/drivers/xhci_runtime.rs +++ b/kernel/src/drivers/xhci_runtime.rs @@ -1,4 +1,5 @@ -//! xHCI runtime — command/event rings, keyboard enumeration, HID interrupt IN. +//! xHCI runtime — command/event rings, HID enumeration (keyboard + mouse boot +//! protocol), control transfers, and the HID interrupt-IN report path. use core::sync::atomic::{AtomicBool, Ordering}; @@ -12,6 +13,12 @@ const TRB_IOC: u32 = 1 << 5; const TRB_DIR_IN: u32 = 1 << 16; const TRB_TYPE_NORMAL: u32 = 1 << 10; +const TRB_TYPE_SETUP: u32 = 2 << 10; +const TRB_TYPE_DATA: u32 = 3 << 10; +const TRB_TYPE_STATUS: u32 = 4 << 10; +const TRB_TYPE_LINK: u32 = 6 << 10; +const TRB_IDT: u32 = 1 << 6; // Immediate Data (Setup stage carries the 8 setup bytes) +const TRB_TC: u32 = 1 << 1; // Toggle Cycle (on Link TRB) const TRB_TYPE_CMD_ENABLE_SLOT: u32 = 9 << 10; const TRB_TYPE_CMD_ADDR_DEV: u32 = 11 << 10; const TRB_TYPE_CMD_CONF_EP: u32 = 12 << 10; @@ -115,21 +122,31 @@ pub struct XhciRuntime { in_phys: u64, dev_phys: u64, ep_ring_phys: u64, + ep0_ring_phys: u64, hid_phys: u64, + ctrl_buf_phys: u64, cmd: *mut Trb, evt: *mut Trb, ep_ring: *mut Trb, + ep0_ring: *mut Trb, cmd_idx: u32, cmd_cycle: u32, evt_idx: u32, evt_cycle: u32, ep_idx: u32, ep_cycle: u32, + ep0_idx: u32, + ep0_cycle: u32, slot: u8, + hid_protocol: u8, + cmd_done: bool, + ctrl_done: bool, hid_armed: bool, enumerated: bool, enum_gave_up: bool, last_hid: [u8; HID_LEN], + cur_x: i32, + cur_y: i32, } static RUNTIME_OK: AtomicBool = AtomicBool::new(false); @@ -169,8 +186,12 @@ pub fn start(ctrl: &XhciController) -> Result<(), &'static str> { unsafe { let cap = mmio::read32(ctrl.bar_virt, 0); let cap_len = (cap & 0xFF) as u8; - let rts_off = mmio::read32(ctrl.bar_virt, cap_len as usize + 0x18) & 0xFFFF_FFE0; - let db_off = mmio::read32(ctrl.bar_virt, cap_len as usize + 0x14) & 0xFFFF_FFE0; + // DBOFF (0x14) and RTSOFF (0x18) are CAPABILITY registers at fixed offsets + // from bar_virt — NOT relative to CAPLENGTH (that's the operational-reg + // base). Reading them cap_len-relative pointed the doorbell + interrupter + // at garbage, so commands never rang and no events were ever posted. + let db_off = mmio::read32(ctrl.bar_virt, 0x14) & 0xFFFF_FFE0; + let rts_off = mmio::read32(ctrl.bar_virt, 0x18) & 0xFFFF_FFE0; let cmd_phys = crate::mm::frame_allocator::alloc_frame().ok_or("cmd")?; let evt_phys = crate::mm::frame_allocator::alloc_frame().ok_or("evt")?; @@ -179,7 +200,9 @@ pub fn start(ctrl: &XhciController) -> Result<(), &'static str> { let in_phys = crate::mm::frame_allocator::alloc_frame().ok_or("in")?; let dev_phys = crate::mm::frame_allocator::alloc_frame().ok_or("dev")?; let ep_ring_phys = crate::mm::frame_allocator::alloc_frame().ok_or("ep")?; + let ep0_ring_phys = crate::mm::frame_allocator::alloc_frame().ok_or("ep0")?; let hid_phys = crate::mm::frame_allocator::alloc_frame().ok_or("hid")?; + let ctrl_buf_phys = crate::mm::frame_allocator::alloc_frame().ok_or("cbuf")?; for p in [ cmd_phys, @@ -189,7 +212,9 @@ pub fn start(ctrl: &XhciController) -> Result<(), &'static str> { in_phys, dev_phys, ep_ring_phys, + ep0_ring_phys, hid_phys, + ctrl_buf_phys, ] { zpage(p); } @@ -205,6 +230,20 @@ pub fn start(ctrl: &XhciController) -> Result<(), &'static str> { .unwrap() .trbs .as_mut_ptr(); + let ep0_ring = (p2v(ep0_ring_phys) as *mut EpRingPage) + .as_mut() + .unwrap() + .trbs + .as_mut_ptr(); + // Link TRB at the end of each transfer ring → wrap to start + toggle cycle, + // so the rings keep running past EP_TRBS entries (the interrupt EP re-arms + // forever; without this the controller halts after the first wrap). + put_trb(ep_ring, (EP_TRBS - 1) as u32, EP_TRBS, + Trb { dw0: ep_ring_phys as u32, dw1: (ep_ring_phys >> 32) as u32, dw2: 0, + dw3: TRB_TYPE_LINK | TRB_TC }, TRB_CYCLE); + put_trb(ep0_ring, (EP_TRBS - 1) as u32, EP_TRBS, + Trb { dw0: ep0_ring_phys as u32, dw1: (ep0_ring_phys >> 32) as u32, dw2: 0, + dw3: TRB_TYPE_LINK | TRB_TC }, TRB_CYCLE); let dcbaa = p2v(dcbaa_phys) as *mut DcbaaPage; core::ptr::write_bytes(dcbaa as *mut u8, 0, 4096); @@ -269,21 +308,31 @@ pub fn start(ctrl: &XhciController) -> Result<(), &'static str> { in_phys, dev_phys, ep_ring_phys, + ep0_ring_phys, hid_phys, + ctrl_buf_phys, cmd, evt, ep_ring, + ep0_ring, cmd_idx: 0, cmd_cycle: TRB_CYCLE, evt_idx: 0, evt_cycle: TRB_CYCLE, ep_idx: 0, ep_cycle: TRB_CYCLE, + ep0_idx: 0, + ep0_cycle: TRB_CYCLE, slot: 0, + hid_protocol: 0, + cmd_done: false, + ctrl_done: false, hid_armed: false, enumerated: false, enum_gave_up: false, last_hid: [0; HID_LEN], + cur_x: 64, + cur_y: 64, }); RUNTIME_OK.store(true, Ordering::Release); log::info!("[USB] XHCI runtime started"); @@ -293,7 +342,7 @@ pub fn start(ctrl: &XhciController) -> Result<(), &'static str> { drain_events(rt); if try_enumerate(rt) { rt.enumerated = true; - log::info!("[USB-HID] keyboard enumerated slot={}", rt.slot); + log::info!("[USB-HID] device enumerated slot={} protocol={}", rt.slot, rt.hid_protocol); arm_hid_transfer(rt); break; } @@ -303,7 +352,7 @@ pub fn start(ctrl: &XhciController) -> Result<(), &'static str> { crate::arch::spin_pause(); } if !RUNTIME.as_ref().unwrap().enumerated { - log::warn!("[USB-HID] keyboard enumeration pending (poll on vsync)"); + log::warn!("[USB-HID] device enumeration pending (poll on vsync)"); } Ok(()) @@ -324,7 +373,7 @@ pub fn poll() { if !rt.enumerated { if try_enumerate(rt) { rt.enumerated = true; - log::info!("[USB-HID] keyboard enumerated slot={}", rt.slot); + log::info!("[USB-HID] device enumerated slot={} protocol={}", rt.slot, rt.hid_protocol); } } else if !rt.hid_armed { arm_hid_transfer(rt); @@ -351,16 +400,76 @@ unsafe fn post_cmd(rt: &mut XhciRuntime, trb: Trb) { rt.cmd_idx = 0; rt.cmd_cycle ^= TRB_CYCLE; } - let ptr = rt.cmd_phys + (rt.cmd_idx as u64) * 16; - mmio::write64(op(rt), 0x18, ptr | rt.cmd_cycle as u64); + // CRCR (op 0x18) is programmed ONCE at init; the controller advances its own + // command-ring dequeue pointer. Rewriting it here clobbered the ring and the + // command was never consumed. Just publish the TRB and ring the doorbell. crate::arch::memory_fence(); let db = rt.bar_virt + rt.db_off as u64; mmio::write32(db, 0, 0); } unsafe fn wait_cmd(rt: &mut XhciRuntime) -> bool { - for _ in 0..500_000 { - if drain_events(rt) { + rt.cmd_done = false; + for _ in 0..2_000_000 { + drain_events(rt); + if rt.cmd_done { + rt.cmd_done = false; + return true; + } + crate::arch::spin_pause(); + } + false +} + +/// Post the three (or two) control-transfer stages on the EP0 ring and ring the +/// slot's EP0 doorbell (DCI 1), then wait for the Status-stage transfer event. +unsafe fn control_xfer( + rt: &mut XhciRuntime, + bm: u8, + req: u8, + value: u16, + index: u16, + length: u16, + data_phys: u64, +) -> bool { + let setup_lo = (bm as u32) | ((req as u32) << 8) | ((value as u32) << 16); + let setup_hi = (index as u32) | ((length as u32) << 16); + let dir_in = bm & 0x80 != 0; + let trt = if length == 0 { 0 } else if dir_in { 3u32 } else { 2u32 }; + + put_ep0(rt, Trb { + dw0: setup_lo, + dw1: setup_hi, + dw2: 8, + dw3: TRB_TYPE_SETUP | TRB_IDT | (trt << 16), + }); + if length > 0 { + let dir = if dir_in { TRB_DIR_IN } else { 0 }; + put_ep0(rt, Trb { + dw0: data_phys as u32, + dw1: (data_phys >> 32) as u32, + dw2: length as u32, + dw3: TRB_TYPE_DATA | dir, + }); + } + // Status stage: direction opposite the data stage; IN for no-data / OUT data. + let status_dir = if length > 0 && dir_in { 0 } else { TRB_DIR_IN }; + put_ep0(rt, Trb { + dw0: 0, + dw1: 0, + dw2: 0, + dw3: TRB_TYPE_STATUS | status_dir | TRB_IOC, + }); + + crate::arch::memory_fence(); + let db = rt.bar_virt + rt.db_off as u64 + (rt.slot as u64) * 4; + mmio::write32(db, 0, 1); // EP0 = DCI 1 + + rt.ctrl_done = false; + for _ in 0..2_000_000 { + drain_events(rt); + if rt.ctrl_done { + rt.ctrl_done = false; return true; } crate::arch::spin_pause(); @@ -368,6 +477,16 @@ unsafe fn wait_cmd(rt: &mut XhciRuntime) -> bool { false } +/// Append a TRB to the EP0 control ring, wrapping at the trailing Link TRB. +unsafe fn put_ep0(rt: &mut XhciRuntime, trb: Trb) { + put_trb(rt.ep0_ring, rt.ep0_idx, EP_TRBS, trb, rt.ep0_cycle); + rt.ep0_idx += 1; + if rt.ep0_idx as usize >= EP_TRBS - 1 { + rt.ep0_idx = 0; + rt.ep0_cycle ^= TRB_CYCLE; + } +} + unsafe fn drain_events(rt: &mut XhciRuntime) -> bool { let mut cmd_done = false; loop { @@ -376,9 +495,14 @@ unsafe fn drain_events(rt: &mut XhciRuntime) -> bool { if (dw3 & 1) != rt.evt_cycle { break; } - let kind = dw3 & (0x3FF << 10); + // TRB Type is a 6-bit field (bits 15:10). A 0x3FF mask would spill into + // the Endpoint ID field (bits 20:16) of Transfer Events, so EP0 (id 1) and + // HID (id 3) events would be misclassified and dropped — only command/port + // events (endpoint-id bits = 0) would match. Mask exactly 6 bits. + let kind = dw3 & (0x3F << 10); if kind == TRB_TYPE_EVT_CMD_COMP { cmd_done = true; + rt.cmd_done = true; let slot = (dw3 >> 24) & 0xFF; if rt.slot == 0 && slot != 0 { rt.slot = slot as u8; @@ -386,9 +510,15 @@ unsafe fn drain_events(rt: &mut XhciRuntime) -> bool { (*dcbaa).ptrs[rt.slot as usize] = rt.dev_phys; } } else if kind == TRB_TYPE_EVT_TRANSFER { - let report = core::slice::from_raw_parts(p2v(rt.hid_phys) as *const u8, HID_LEN); - route_report(report, rt); - rt.hid_armed = false; + // Endpoint ID 1 = EP0 (control); higher = HID interrupt IN (EP1=DCI 3). + let ep_id = (dw3 >> 16) & 0x1F; + if ep_id <= 1 { + rt.ctrl_done = true; + } else { + let report = core::slice::from_raw_parts(p2v(rt.hid_phys) as *const u8, HID_LEN); + route_report(report, rt); + rt.hid_armed = false; + } } rt.evt_idx += 1; if rt.evt_idx as usize >= EVT_TRBS { @@ -403,49 +533,93 @@ unsafe fn drain_events(rt: &mut XhciRuntime) -> bool { } fn route_report(report: &[u8], rt: &mut XhciRuntime) { + // Protocol 2 = boot mouse. Mouse deltas are RELATIVE — two identical reports + // are two real movements, so never dedup them (unlike keyboards, where an + // unchanged report means no key state change). + if rt.hid_protocol == 2 { + route_mouse(report, rt); + return; + } if report == &rt.last_hid { return; } let old = rt.last_hid; rt.last_hid.copy_from_slice(report); - if let Some((sc, pressed)) = usb_hid::handle_boot_keyboard_report(report) { - crate::wm::push_key(sc, pressed); - LIVE_KEY.store(true, Ordering::Release); - log::info!("[USB-HID] live key scancode={:#x}", sc); - } - for i in 2..8 { - let prev = old.get(i).copied().unwrap_or(0); - let cur = report.get(i).copied().unwrap_or(0); - if prev != 0 && prev != cur { - if let Some(sc) = usb_hid::usb_keycode_to_scancode(prev) { - crate::wm::push_key(sc, false); + if rt.hid_protocol == 1 || rt.hid_protocol == 0 { + if let Some((sc, pressed)) = usb_hid::handle_boot_keyboard_report(report) { + crate::wm::push_key(sc, pressed); + LIVE_KEY.store(true, Ordering::Release); + log::info!("[USB-HID] live key scancode={:#x}", sc); + } + for i in 2..8 { + let prev = old.get(i).copied().unwrap_or(0); + let cur = report.get(i).copied().unwrap_or(0); + if prev != 0 && prev != cur { + if let Some(sc) = usb_hid::usb_keycode_to_scancode(prev) { + crate::wm::push_key(sc, false); + } } } } } -unsafe fn write_input_address_ctx(in_phys: u64, port: u8) { - let p = p2v(in_phys) as *mut u8; +/// Boot-protocol mouse report: byte0 = buttons (b0 L, b1 R, b2 M), +/// byte1 = dx (i8), byte2 = dy (i8), byte3 = wheel (i8). Maintain an absolute +/// cursor clamped to the framebuffer and feed the WM's unified pointer state. +fn route_mouse(report: &[u8], rt: &mut XhciRuntime) { + let buttons = (report[0] as u32) & 0x7; + let dx = report[1] as i8 as i32; + let dy = report[2] as i8 as i32; + let wheel = report.get(3).map(|b| *b as i8 as i32).unwrap_or(0); + let (w, h) = crate::drivers::fb::size_px().unwrap_or((1024, 768)); + rt.cur_x = (rt.cur_x + dx).clamp(0, w as i32 - 1); + rt.cur_y = (rt.cur_y + dy).clamp(0, h as i32 - 1); + crate::wm::push_pointer(rt.cur_x, rt.cur_y, buttons); + if wheel != 0 { + crate::wm::push_scroll(rt.cur_x, rt.cur_y, wheel); + } +} + +unsafe fn write_input_address_ctx(rt: &XhciRuntime, port: u8, speed: u32) { + let p = p2v(rt.in_phys) as *mut u8; core::ptr::write_bytes(p, 0, 512); - // Input control: add slot + EP0. - core::ptr::write(p.add(4), 0x03); - // Slot context @ 0x20 — 1 context entry, full-speed. - core::ptr::write_unaligned(p.add(0x20) as *mut u32, (1 << 27) | (1 << 20)); + // Input Control Context @ 0x00: add Slot (bit0) + EP0 (bit1). + core::ptr::write_unaligned(p.add(0x04) as *mut u32, 0x03); + // Slot Context @ 0x20: Context Entries=1 (bits31:27), Speed (bits23:20). + core::ptr::write_unaligned(p.add(0x20) as *mut u32, (1u32 << 27) | (speed << 20)); + // Root Hub Port Number @ dword1 bits31:16. core::ptr::write_unaligned(p.add(0x24) as *mut u32, (port as u32) << 16); - // EP0 context @ 0x40 — control (type 4), max packet 64. - core::ptr::write_unaligned(p.add(0x44) as *mut u32, (4 << 3) | 64); + // EP0 Max Packet Size depends on bus speed (xHCI speed IDs: 1=Full, 2=Low, + // 3=High, 4=Super). High-speed control endpoints REQUIRE 64; a mismatch halts + // EP0 after the first transfer. Low=8, Super=512, Full/High=64. + let mps0: u32 = match speed { + 2 => 8, + 4 => 512, + _ => 64, + }; + // EP0 Context @ 0x40, dword1: CErr=3 (bits2:1), type 4=control (bits5:3), + // Max Packet Size (bits31:16). + core::ptr::write_unaligned(p.add(0x44) as *mut u32, (3u32 << 1) | (4u32 << 3) | (mps0 << 16)); + // EP0 TR Dequeue Pointer @ 0x48, DCS=1. + core::ptr::write_unaligned(p.add(0x48) as *mut u64, rt.ep0_ring_phys | 1); } -unsafe fn write_input_config_ep1(in_phys: u64, ep_ring_phys: u64) { - let p = p2v(in_phys) as *mut u8; +unsafe fn write_input_config_ep1(rt: &XhciRuntime) { + let p = p2v(rt.in_phys) as *mut u8; core::ptr::write_bytes(p, 0, 512); - // Add EP1. - core::ptr::write(p.add(4), 0x04); - // EP1 context @ 0x60 — interrupt IN, max packet 8, interval 8. - core::ptr::write_unaligned(p.add(0x64) as *mut u32, (7 << 3) | 8); - core::ptr::write(p.add(0x62), 8); - core::ptr::write_unaligned(p.add(0x68) as *mut u64, ep_ring_phys | 1); + // Add Slot (bit0, required by Configure Endpoint) + EP1-IN (DCI 3, bit3). + core::ptr::write_unaligned(p.add(0x04) as *mut u32, (1u32 << 0) | (1u32 << 3)); + // Slot Context @ 0x20: Context Entries must reach the highest DCI = 3. + core::ptr::write_unaligned(p.add(0x20) as *mut u32, 3u32 << 27); + // EP1-IN Context @ DCI3 = 0x20 + 3*0x20 = 0x80. + // dword0 @ 0x80: Interval (bits23:16). + core::ptr::write_unaligned(p.add(0x80) as *mut u32, 8u32 << 16); + // dword1 @ 0x84: CErr=3 (bits2:1), type 7=interrupt-IN (bits5:3), + // Max Packet Size=8 (bits31:16). + core::ptr::write_unaligned(p.add(0x84) as *mut u32, (3u32 << 1) | (7u32 << 3) | (8u32 << 16)); + // EP1 TR Dequeue Pointer @ 0x88, DCS=1. + core::ptr::write_unaligned(p.add(0x88) as *mut u64, rt.ep_ring_phys | 1); } unsafe fn try_enumerate(rt: &mut XhciRuntime) -> bool { @@ -487,16 +661,14 @@ unsafe fn try_enumerate(rt: &mut XhciRuntime) -> bool { }, ); if !wait_cmd(rt) || rt.slot == 0 { - static ENABLE_FAIL: core::sync::atomic::AtomicBool = - core::sync::atomic::AtomicBool::new(false); - if !ENABLE_FAIL.swap(true, Ordering::Relaxed) { - log::warn!("[USB-HID] enable slot failed slot={}", rt.slot); - } rt.enum_gave_up = true; return false; } + // Port speed lives in PORTSC bits[13:10]; the slot context wants it verbatim. + let speed = (portsc(rt, port) >> 10) & 0xF; - write_input_address_ctx(rt.in_phys, port); + // Address Device (BSR=0 → controller issues SET_ADDRESS on EP0). + write_input_address_ctx(rt, port, speed); post_cmd( rt, Trb { @@ -510,7 +682,45 @@ unsafe fn try_enumerate(rt: &mut XhciRuntime) -> bool { log::warn!("[USB-HID] address device failed"); return false; } - write_input_config_ep1(rt.in_phys, rt.ep_ring_phys); + + // GET_DESCRIPTOR(Configuration) → read bInterfaceProtocol (1=kbd, 2=mouse) + // and bConfigurationValue so SET_CONFIGURATION uses the right value. + let mut cfg_val = 1u8; + if control_xfer(rt, 0x80, 0x06, 0x0200, 0, 64, rt.ctrl_buf_phys) { + let buf = core::slice::from_raw_parts(p2v(rt.ctrl_buf_phys) as *const u8, 64); + if buf[1] == 0x02 { + cfg_val = buf[5]; + } + // Walk descriptors for the (first) HID interface's bInterfaceProtocol. + let total = buf[0] as usize; + let mut off = total; // skip the 9-byte config descriptor + while off + 2 <= 64 { + let dlen = buf[off] as usize; + let dtype = buf[off + 1]; + if dlen == 0 { + break; + } + if dtype == 0x04 && off + 8 <= 64 { + // Interface descriptor: bInterfaceProtocol @ +7. + rt.hid_protocol = buf[off + 7]; + break; + } + off += dlen; + } + } + if cfg_val == 0 { + cfg_val = 1; + } + + // SET_CONFIGURATION → move the device to the Configured state so its endpoints + // become active (without this it never sends interrupt reports). + if !control_xfer(rt, 0x00, 0x09, cfg_val as u16, 0, 0, 0) { + log::warn!("[USB-HID] set configuration failed"); + return false; + } + + // xHCI Configure Endpoint for the interrupt-IN endpoint (DCI 3). + write_input_config_ep1(rt); post_cmd( rt, Trb { @@ -524,6 +734,11 @@ unsafe fn try_enumerate(rt: &mut XhciRuntime) -> bool { log::warn!("[USB-HID] configure EP failed"); return false; } + + // HID class requests: SET_PROTOCOL(boot=0) so reports are boot-format, and + // SET_IDLE(0) so the device only reports on change. Best-effort. + let _ = control_xfer(rt, 0x21, 0x0B, 0, 0, 0, 0); // SET_PROTOCOL boot + let _ = control_xfer(rt, 0x21, 0x0A, 0, 0, 0, 0); // SET_IDLE infinite true } @@ -541,11 +756,14 @@ unsafe fn arm_hid_transfer(rt: &mut XhciRuntime) { rt.ep_cycle, ); rt.ep_idx += 1; - if rt.ep_idx as usize >= EP_TRBS { + if rt.ep_idx as usize >= EP_TRBS - 1 { + // Wrap before the trailing Link TRB (it toggles the cycle for us). rt.ep_idx = 0; rt.ep_cycle ^= TRB_CYCLE; } rt.hid_armed = true; + crate::arch::memory_fence(); + // EP1 IN = DCI 3. let db = rt.bar_virt + rt.db_off as u64 + (rt.slot as u64) * 4; - mmio::write32(db, 0, 2); + mmio::write32(db, 0, 3); } diff --git a/kernel/src/embedder/abi.rs b/kernel/src/embedder/abi.rs index 2ed28dc..ade004d 100644 --- a/kernel/src/embedder/abi.rs +++ b/kernel/src/embedder/abi.rs @@ -214,10 +214,15 @@ pub const CURSOR_SHAPE_MAX: u32 = 8; pub const CLIPBOARD_MAX: usize = 64 * 1024; // On-demand package delivery — Fuchsia-inspired ephemeral fetch. -pub const SYS_PKG_RESOLVE: u64 = 0x390; // pkg_resolve(name_ptr, name_len) → app_id / -ERRNO -pub const SYS_PKG_CATALOG: u64 = 0x391; // pkg_catalog(buf_ptr, buf_len) → count (writes manifest entries) -pub const SYS_PKG_SET_SERVER: u64 = 0x392; // pkg_set_server(ip_packed_be, port) → 0 -pub const SYS_PKG_EVICT: u64 = 0x393; // pkg_evict(app_id) → 0 / -ERRNO +// NOTE: these were originally 0x390-0x393, which COLLIDED with the Phase 53-55 +// scheduler/fork/signal syscalls (sched_yield/get_cpu_time/fork/kill_signal). +// Because the pkg match arms precede those in dispatch.rs, 0x390 silently +// dispatched to pkg_resolve and sched_yield was dead — every embedder yield +// hit pkg_resolve(garbage). Moved to a free block (0x4C0+) so both work. +pub const SYS_PKG_RESOLVE: u64 = 0x4C0; // pkg_resolve(name_ptr, name_len) → app_id / -ERRNO +pub const SYS_PKG_CATALOG: u64 = 0x4C1; // pkg_catalog(buf_ptr, buf_len) → count (writes manifest entries) +pub const SYS_PKG_SET_SERVER: u64 = 0x4C2; // pkg_set_server(ip_packed_be, port) → 0 +pub const SYS_PKG_EVICT: u64 = 0x4C3; // pkg_evict(app_id) → 0 / -ERRNO // WM event kind for incoming platform-channel messages. pub const EV_PLATFORM_MSG: u32 = 6; diff --git a/kernel/src/logger.rs b/kernel/src/logger.rs index 60f4924..6a77b40 100644 --- a/kernel/src/logger.rs +++ b/kernel/src/logger.rs @@ -20,11 +20,75 @@ impl Log for KernelLogger { let rflags = crate::arch::interrupts_save_and_disable(); SERIAL.lock().write_str(s).ok(); crate::drivers::fb::write_str(s); + LOG_RING.lock().push(s); crate::arch::interrupts_restore(rflags); } fn flush(&self) {} } +// ── In-memory log ring (for the boot screen's F2 verbose overlay) ──────────── +// +// The framebuffer is owned (double-buffered) by the compositor during the boot +// warm-up, so verbose logs can't be shown via the live text console — they have +// to be re-rendered into the compositor's frame. Every log line is also kept +// here so the boot screen can snapshot + draw the recent history on demand. + +/// Max log lines retained for the F2 verbose overlay. +pub const RING_LINES: usize = 32; +/// Max bytes retained per log line. +pub const RING_COLS: usize = 96; + +struct LogRing { + lines: [[u8; RING_COLS]; RING_LINES], + lens: [u8; RING_LINES], + head: usize, + count: usize, +} + +impl LogRing { + const fn new() -> Self { + Self { lines: [[0u8; RING_COLS]; RING_LINES], lens: [0u8; RING_LINES], head: 0, count: 0 } + } + fn push(&mut self, s: &str) { + for line in s.split('\n') { + if line.is_empty() { + continue; + } + let b = line.as_bytes(); + let n = b.len().min(RING_COLS); + self.lines[self.head][..n].copy_from_slice(&b[..n]); + self.lens[self.head] = n as u8; + self.head = (self.head + 1) % RING_LINES; + if self.count < RING_LINES { + self.count += 1; + } + } + } +} + +static LOG_RING: Mutex = Mutex::new(LogRing::new()); + +/// Copy retained log lines (oldest → newest) into the caller's buffers under a +/// short interrupt-off critical section; returns the line count. Drawing happens +/// after the lock is released so the framebuffer pass runs with interrupts on. +pub fn snapshot_logs( + out_lines: &mut [[u8; RING_COLS]; RING_LINES], + out_lens: &mut [u8; RING_LINES], +) -> usize { + let rflags = crate::arch::interrupts_save_and_disable(); + let ring = LOG_RING.lock(); + let start = if ring.count < RING_LINES { 0 } else { ring.head }; + for i in 0..ring.count { + let idx = (start + i) % RING_LINES; + out_lines[i] = ring.lines[idx]; + out_lens[i] = ring.lens[idx]; + } + let n = ring.count; + drop(ring); + crate::arch::interrupts_restore(rflags); + n +} + /// Tiny stack-allocated write buffer for log formatting (no heap needed). struct FmtBuf { buf: [u8; 2048], diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 0c7ec21..b17516c 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -166,6 +166,13 @@ pub extern "C" fn kernel_main() -> ! { let fb_response = FB_REQUEST.response(); logger::init(fb_response); + // Clean boot screen from the first frame (silences on-screen log spam; serial + // keeps everything; F2 toggles the verbose overlay). + if drivers::fb::is_ready() { + drivers::bootscreen::init(); + drivers::bootscreen::render(); + } + log::warn!( "[MM::FrameAlloc] capacity stats at boot: total_usable_frames={} ({} MiB), used_at_boot={} ({} MiB)", mm::frame_allocator::frames_total(), @@ -255,17 +262,14 @@ pub fn kernel_main_arch(map: mm::BootMemMap, hhdm_offset: u64) -> ! { // Identity-mapped RAM → the physical framebuffer base is directly // writable as a virtual address (VA == PA on aarch64). drivers::fb::init_raw(addr, w, h, pitch); - // Paint a visible boot fill so the display is provably driven even - // before any userspace renders (top band teal, body dark navy). - drivers::fb::fill_rect(0, 0, w, 48, 0x0014_B8A6); - drivers::fb::fill_rect(0, 48, w, h - 48, 0x001A_1A2E); - drivers::fb::write_str("\n OSCortex aarch64 — ramfb framebuffer up\n"); - // Stop mirroring kernel logs into the framebuffer console: under TCG, - // glyph rendering into the 1280x800 buffer (64 volatile writes/glyph) - // is so slow per log line it dwarfs the rest of boot. The serial log - // is the lifeline; the fb stays driven (the boot fill is visible) and - // userspace/compositor renders into it normally. - drivers::fb::disable_fb_logging(); + // Silence on-screen log spam and paint the kernel boot screen (the + // dot-matrix OSCORTEX wordmark + progress + status) so the display is + // provably driven and clean from the first frame. The serial log stays + // the developer lifeline; F2 toggles an on-screen verbose log overlay. + // (Mirroring logs into the 1280x800 fb console is also brutally slow + // under TCG — ~64 volatile writes/glyph — so this is prettier + faster.) + drivers::bootscreen::init(); + drivers::bootscreen::render(); log::info!("[BOOT] aarch64 framebuffer online: {}x{} via ramfb", w, h); } None => { diff --git a/kernel/src/net/tcp.rs b/kernel/src/net/tcp.rs index c46816a..d83b1b5 100644 --- a/kernel/src/net/tcp.rs +++ b/kernel/src/net/tcp.rs @@ -39,7 +39,12 @@ use spin::Mutex; // ── Constants ───────────────────────────────────────────────────────────────── const MAX_TCP_SOCKETS: usize = 8; -const TCP_BUF_SIZE: usize = 4096; +// Per-socket rx/tx buffer = the advertised TCP window. 4 KiB forced a window- +// update round trip every 4 KiB — a 5.4 MB package fetch crawled at ~28 KB/s +// under TCG (~1300 stop-and-wait turns) and starved the reader into timeouts. +// 64 KiB (max un-scaled window) makes bulk fetches stream properly; worst-case +// memory is 2 × 64 KiB × MAX_TCP_SOCKETS(8) = 1 MiB. +const TCP_BUF_SIZE: usize = 65535; const UDP_BUF_SIZE: usize = 2048; // Default static IP (overridden by SYS_DHCP_DISCOVER or SYS_NET_SET_IP). @@ -114,10 +119,14 @@ impl Device for VirtioNetDev { // ── Monotonic timestamp ─────────────────────────────────────────────────────── fn now() -> Instant { - // Use the compositor frame counter (60 Hz) as a coarse millisecond clock. - // Each frame ≈ 16.7 ms; multiply by 17 to get ms approximation. - let frames = crate::compositor::frame_counter(); - Instant::from_millis(frames as i64 * 17) + // Real monotonic clock (rdtsc-based, same source as timerfds). smoltcp + // uses this for ACK-delay/retransmit/window timers — it must track REAL + // time. The previous source (compositor frame counter ×17ms) crawls + // whenever the compositor isn't presenting (early boot, headless, busy + // CPU): smoltcp then thought ~10ms passed per ~150ms of reality, so every + // TCP timer ran ~15× slow and bulk transfers paced at ~28 KB/s no matter + // how large the buffers were (a 5.4 MB fetch took a constant ~193 s). + Instant::from_millis((crate::syscall::poll::monotonic_ns() / 1_000_000) as i64) } // ── Global state ────────────────────────────────────────────────────────────── @@ -251,7 +260,15 @@ pub fn tcp_connect(dst_ip: u32, dst_port: u16) -> Result { let bytes = dst_ip.to_be_bytes(); let dst_addr = Ipv4Address::new(bytes[0], bytes[1], bytes[2], bytes[3]); - let local_port = 49152 + slot as u16; + // Ephemeral local port from a monotonic counter — NOT the slot index. A + // slot-derived port meant two back-to-back connections through slot 0 to + // the same server reused the IDENTICAL 4-tuple; the peer's TIME_WAIT state + // then swallowed the new SYN and the second connect always timed out + // (observed: pkg catalog fetch OK, immediately-following bundle fetch dead). + static NEXT_EPHEMERAL: core::sync::atomic::AtomicU16 = + core::sync::atomic::AtomicU16::new(0); + let n = NEXT_EPHEMERAL.fetch_add(1, core::sync::atomic::Ordering::Relaxed); + let local_port = 49152 + (n % 16384); socket .connect( s.iface.context(), @@ -296,6 +313,27 @@ pub fn tcp_read(fd: usize, buf: &mut [u8]) -> Result { Ok(n) } +/// True once the three-way handshake has completed (the socket may send). +/// `Ok(false)` while still connecting; `Err(-111)` if the connection was +/// refused / reset (socket left the open states). +/// +/// NOTE this is the correct way to wait for an outbound connect. A zero-byte +/// `tcp_read` probe does NOT work: `can_recv()` is false on an ESTABLISHED +/// socket whose peer hasn't sent anything yet (an HTTP server says nothing +/// until it gets the request), so the probe EAGAINs forever. +pub fn tcp_is_established(fd: usize) -> Result { + poll(); + let _g = TCP_LOCK.lock(); + let stack = unsafe { &mut *TCP_STACK.0.get() }; + let s = stack.as_mut().ok_or(-1i64)?; + let handle = s.handles.get(fd).and_then(|h| *h).ok_or(-9i64)?; + let socket = s.sockets.get_mut::(handle); + if !socket.is_open() { + return Err(-111); // ECONNREFUSED — closed/reset during connect + } + Ok(socket.may_send()) +} + /// Close a TCP socket. pub fn tcp_close(fd: usize) -> Result<(), i64> { poll(); diff --git a/kernel/src/panic.rs b/kernel/src/panic.rs index d925163..447aa81 100644 --- a/kernel/src/panic.rs +++ b/kernel/src/panic.rs @@ -30,6 +30,9 @@ impl Write for PanicBuf { #[panic_handler] fn panic(info: &PanicInfo) -> ! { crate::arch::disable_interrupts(); + // The boot screen silences on-screen logs; a panic must always be visible, so + // re-enable framebuffer logging before the final log::error! below. + crate::drivers::fb::enable_fb_logging(); let mut buf = PanicBuf::new(); let _ = write!(buf, "{}", info.message()); crate::logger::early_print("\r\n[PANIC] "); diff --git a/kernel/src/pkg/http.rs b/kernel/src/pkg/http.rs index ecf9303..cc05b34 100644 --- a/kernel/src/pkg/http.rs +++ b/kernel/src/pkg/http.rs @@ -39,21 +39,23 @@ pub fn get(host_ip: u32, port: u16, path: &str) -> Result, HttpError> { let fd = crate::net::tcp::tcp_connect(host_ip, port).map_err(|_| HttpError::Connect)?; let mut connected = false; - // Busy-poll until connection is established (max ~2 seconds). - for _ in 0..120 { + // Busy-poll until the handshake completes. tcp_is_established (may_send) + // is the correct readiness signal — the old zero-byte tcp_read probe + // EAGAINed forever on an established-but-idle socket (can_recv is false + // until the peer sends, and an HTTP server says nothing until it gets the + // request), so every fetch failed with Connect since day one. + for _ in 0..240 { crate::net::tcp::poll(); - // Try a zero-byte read to check if connected. - let mut probe = [0u8; 0]; - match crate::net::tcp::tcp_read(fd, &mut probe) { - Ok(_) => { + match crate::net::tcp::tcp_is_established(fd) { + Ok(true) => { connected = true; break; } - Err(-11) => { - // EAGAIN — still connecting, wait a tick. + Ok(false) => { + // Still in SYN-SENT — wait a tick. for _ in 0..50_000u64 { core::hint::spin_loop(); } } - Err(_) => break, // connected or error — proceed + Err(_) => break, // refused / reset } } @@ -92,9 +94,15 @@ pub fn get(host_ip: u32, port: u16, path: &str) -> Result, HttpError> { } } - // 3. Read response (headers + body) + // 3. Read response (headers + body). Once Content-Length is known, read + // until the FULL body has arrived — the old "300 EAGAINs ≈ no more data" + // heuristic clipped large transfers (a 5 MB bundle takes ~80s under TCG + // with stalls well past 5s), silently truncating the body and failing the + // SHA-256 check downstream. let mut response = Vec::with_capacity(4096); let mut consecutive_eagain = 0u32; + // (header_end+4 .. +content_length) once headers are parsed. + let mut expected_total: Option = None; loop { crate::net::tcp::poll(); @@ -108,15 +116,37 @@ pub fn get(host_ip: u32, port: u16, path: &str) -> Result, HttpError> { let _ = crate::net::tcp::tcp_close(fd); return Err(HttpError::TooLarge); } + if expected_total.is_none() { + if let Some(header_end) = find_header_end(&response) { + if let Ok(headers) = core::str::from_utf8(&response[..header_end]) { + if let Some(cl) = parse_content_length(headers) { + if cl > MAX_BODY_SIZE { + let _ = crate::net::tcp::tcp_close(fd); + return Err(HttpError::TooLarge); + } + expected_total = Some(header_end + 4 + cl); + } + } + } + } + if let Some(total) = expected_total { + if response.len() >= total { + break; // full body received + } + } } Err(-11) => { - // EAGAIN + // EAGAIN. With a known remaining length we KNOW more data is + // coming — tolerate long stalls (TCG is slow). Without + // Content-Length keep the short no-more-data heuristic. + // Short spin: every iteration re-polls the stack, and draining + // the RX ring promptly is what keeps the sender streaming. consecutive_eagain += 1; - if consecutive_eagain > 300 { - // ~5 seconds of no data — assume done + let cap = if expected_total.is_some() { 20_000 } else { 1_500 }; + if consecutive_eagain > cap { break; } - for _ in 0..10_000u64 { core::hint::spin_loop(); } + for _ in 0..2_000u64 { core::hint::spin_loop(); } } Err(_) => break, // Connection closed or error } @@ -168,8 +198,13 @@ fn parse_response(data: &[u8]) -> Result, HttpError> { if content_length > MAX_BODY_SIZE { return Err(HttpError::TooLarge); } - let actual = body.len().min(content_length); - return Ok(body[..actual].to_vec()); + // A SHORT body is a truncated transfer — fail loudly rather than + // returning clipped bytes (the silent min() here previously turned + // stream truncation into a confusing downstream HashMismatch). + if body.len() < content_length { + return Err(HttpError::Malformed); + } + return Ok(body[..content_length].to_vec()); } // No Content-Length — return whatever body we got diff --git a/kernel/src/pkg/resolver.rs b/kernel/src/pkg/resolver.rs index 2077f23..9786833 100644 --- a/kernel/src/pkg/resolver.rs +++ b/kernel/src/pkg/resolver.rs @@ -19,6 +19,28 @@ use super::http; use super::manifest::PkgManifest; use super::sha256; +/// Ed25519 public key that authenticates the package catalog. The server signs +/// `catalog.bin`; a valid signature vouches for every SHA-256 hash the catalog +/// lists, and the per-bundle SHA-256 check then vouches for the bundle bytes — +/// a complete chain from a single trusted key to installed code. +/// +/// DEV key (matches tools/pkg-server's fixed dev seed). Production would ship a +/// per-vendor key here; the verification mechanism is identical. +const CATALOG_PUBKEY: [u8; 32] = [ + 0x9e, 0x48, 0x65, 0x7d, 0x5f, 0x21, 0x44, 0x66, 0x79, 0x85, 0xe3, 0xb2, 0xbd, 0x8d, 0x34, 0xc3, + 0xc4, 0x03, 0x9b, 0xac, 0x6a, 0x44, 0x57, 0xff, 0x62, 0x5e, 0x8c, 0xe6, 0xf4, 0xdb, 0xa1, 0xb1, +]; + +/// Verify a detached Ed25519 signature over `msg` with `CATALOG_PUBKEY`. +fn verify_catalog_sig(msg: &[u8], sig: &[u8]) -> bool { + use ed25519_compact::{PublicKey, Signature}; + let Ok(sig_arr) = <[u8; 64]>::try_from(sig) else { + return false; + }; + let pk = PublicKey::new(CATALOG_PUBKEY); + pk.verify(msg, &Signature::new(sig_arr)).is_ok() +} + /// Resolver errors. #[derive(Debug)] pub enum ResolveError { @@ -30,6 +52,8 @@ pub enum ResolveError { FetchFailed, /// SHA-256 hash mismatch after download. HashMismatch, + /// The catalog's Ed25519 signature did not verify against CATALOG_PUBKEY. + BadSignature, /// Failed to install into app_registry. InstallFailed, /// Cache is full and all slots are pinned. @@ -79,6 +103,20 @@ pub fn refresh_catalog() -> Result { ResolveError::FetchFailed })?; + // Authenticate the catalog BEFORE trusting any entry: fetch its detached + // Ed25519 signature and verify it against the baked-in public key. An + // unsigned / tampered / wrong-key catalog is rejected outright — we will + // not install code vouched for only by an unauthenticated index. + let sig = http::get(ip, port, "/catalog.sig").map_err(|e| { + log::warn!("[pkg] catalog signature fetch failed: {:?}", e); + ResolveError::BadSignature + })?; + if !verify_catalog_sig(&body, &sig) { + log::error!("[pkg] catalog signature INVALID — rejecting catalog"); + return Err(ResolveError::BadSignature); + } + log::info!("[pkg] catalog signature verified (Ed25519)"); + let entries = super::manifest::parse_catalog(&body).ok_or_else(|| { log::warn!("[pkg] catalog parse failed"); ResolveError::FetchFailed diff --git a/kernel/src/process/mod.rs b/kernel/src/process/mod.rs index 5e6e83a..370443f 100644 --- a/kernel/src/process/mod.rs +++ b/kernel/src/process/mod.rs @@ -126,6 +126,10 @@ pub struct Process { pub state: ProcState, /// Exit code set by `sys_exit`. pub exit_code: i32, + /// Capability set governing access to privileged syscalls. Stored per-PCB + /// (not derived from PID): the system shell (HOST_MODE_SHELL) gets the full + /// set, launched apps get none, and threads/forks inherit their creator's. + pub caps: crate::security::Capabilities, /// Base of this process syscall kernel stack. syscall_stack_base: *mut u8, /// Top of this process syscall kernel stack. @@ -250,6 +254,7 @@ impl Process { }, state: ProcState::Dead, exit_code: 0, + caps: crate::security::NO_CAPS, syscall_stack_base: core::ptr::null_mut(), syscall_stack_top: 0, xstate: XStateBuf([0; XSTATE_SIZE]), @@ -783,6 +788,15 @@ pub fn spawn_with_bootstrap( p.regs = regs; p.state = ProcState::Running; p.exit_code = 0; + // Capabilities: the trusted system shell (HOST_MODE_SHELL) holds the + // full set; everything else launched this way (HOST_MODE_APP) starts + // with none and must be granted caps explicitly. This is what makes + // privileged syscalls capability-gated rather than PID-gated. + p.caps = if bootstrap.rdi == crate::app_registry::HOST_MODE_SHELL { + crate::security::ALL_CAPS + } else { + crate::security::NO_CAPS + }; p.syscall_stack_base = sys_stack_base; p.syscall_stack_top = sys_stack_top; p.xstate = XStateBuf::default(); @@ -809,8 +823,9 @@ pub fn spawn_with_bootstrap( } log::info!( - "[Process] Spawned '{}' pid={} entry={:#x} rdi={:#x} rsi={:#x} rdx={:#x} parent={}", - name, pid, entry, bootstrap.rdi, bootstrap.rsi, bootstrap.rdx, bootstrap.parent_pid + "[Process] Spawned '{}' pid={} entry={:#x} rdi={:#x} rsi={:#x} rdx={:#x} parent={} caps={:?}", + name, pid, entry, bootstrap.rdi, bootstrap.rsi, bootstrap.rdx, bootstrap.parent_pid, + caps_of(pid) ); Ok(pid) } @@ -912,6 +927,45 @@ pub fn exit(pid: u32, code: i32) { p.exit_code = code; p.current_cpu = None; log::info!("[Process] pid={} exited with code {}", pid, code); + + // Crash auto-recovery: tell the app registry which thread GROUP this exit + // belongs to. Resolved while the lock is held; the (try-lock, ISR-safe) + // notification itself runs after we release it. + let leader = get_group_leader_locked(pid); + drop(_g); + crate::app_registry::note_thread_exit(leader, pid, code); +} + +/// Forcefully terminate every still-live member of `leader`'s thread group +/// (including the leader itself). Used by crash recovery: when one thread of a +/// launched app faults, the survivors must be torn down before a fresh +/// instance is launched — two engine instances of the same app corrupt each +/// other under the cooperative scheduler. Returns the number killed. +/// +/// Must be called from a normal (syscall) context, NOT an ISR: it takes the +/// PTABLE lock to collect victims, then calls `exit()` per victim. +pub fn kill_group(leader: u32) -> u32 { + let mut victims = [0u32; MAX_PROCS]; + let mut n = 0usize; + { + let _g = PTABLE_LOCK.lock(); + for slot in unsafe { PTABLE.iter() } { + if slot.pid == 0 { + continue; + } + if matches!(slot.state, ProcState::Zombie(_) | ProcState::Dead) { + continue; + } + if get_group_leader_locked(slot.pid) == leader && n < victims.len() { + victims[n] = slot.pid; + n += 1; + } + } + } + for &v in &victims[..n] { + exit(v, -9); + } + n as u32 } /// Send SIGKILL to a process (forceful immediate exit). @@ -938,6 +992,23 @@ pub fn current_pid() -> u32 { crate::arch::smp::this_cpu().current_pid.load(Ordering::Acquire) } +/// The capability set held by `pid` (NO_CAPS if the pid is unknown/dead). +pub fn caps_of(pid: u32) -> crate::security::Capabilities { + let _g = PTABLE_LOCK.lock(); + let p = unsafe { &PTABLE[idx_of(pid)] }; + if p.pid == pid && p.state != ProcState::Dead { + p.caps + } else { + crate::security::NO_CAPS + } +} + +/// Whether the CURRENTLY-RUNNING process holds all of `required`. The single +/// gate every privileged syscall calls — capability-based, not PID-based. +pub fn current_has_caps(required: crate::security::Capabilities) -> bool { + crate::security::check(caps_of(current_pid()), required) +} + /// Physical address of the current process's top-level page table (the L1 / TTBR0 /// root on aarch64, the PML4 on x86). Used by diagnostics / kernel-side user-VA /// translation. Returns 0 if there is no current process. @@ -1186,7 +1257,20 @@ pub fn next_runnable_pid_locked(current: u32, my_cpu: u32) -> Option { // two-VM concurrency that crashes the launched app. let fg = crate::wm::focus_pid(); let fg_group = if fg > 1 { get_group_leader_locked(fg) } else { 1 }; - let exclusive = fg_group > 1; + let mut exclusive = fg_group > 1; + // If the foreground group's LEADER is dead (the app crashed), exclusivity + // must lapse — otherwise the filter below skips every other process + // (including the shell) and the whole system wedges on a dead group. + // The shell then gets scheduled again and crash recovery (app_registry + // drain in sys_wm_next_event) can refocus + relaunch. + if exclusive { + let leader = unsafe { &PTABLE[idx_of(fg_group)] }; + let leader_alive = leader.pid == fg_group + && !matches!(leader.state, ProcState::Zombie(_) | ProcState::Dead); + if !leader_alive { + exclusive = false; + } + } let focus = fg; let input_target = if focus != 0 { focus } else { 1 }; @@ -1206,8 +1290,9 @@ pub fn next_runnable_pid_locked(current: u32, my_cpu: u32) -> Option { } // Shell (pid 1) baton shortcut — SUPPRESSED while an app is foreground, so the - // shell engine cannot run concurrently with the launched app. - if !exclusive && current != 1 && crate::wm::embedder_baton_due() { + // shell engine cannot run concurrently with the launched app. Per-engine: only + // the SHELL's own baton triggers this (the app's baton runs the app instead). + if !exclusive && current != 1 && crate::wm::embedder_baton_due(1) { let embedder = unsafe { &mut PTABLE[idx_of(1)] }; if embedder.pid == 1 && embedder.state == ProcState::Running { if embedder.current_cpu.is_none() || embedder.current_cpu == Some(my_cpu) { @@ -1499,6 +1584,8 @@ pub fn spawn_thread( p.xstate = XStateBuf::default(); p.is_thread = true; p.parent_pid = owning_pid; + // Threads inherit their owning process's capabilities. + p.caps = unsafe { core::ptr::addr_of!(PTABLE[idx_of(owning_pid)].caps).read() }; p.fs_base = tls_va; // Record user stack bounds so pthread_attr_getstack can return them. p.user_stack_base = stack_va; @@ -1573,6 +1660,8 @@ pub fn clone_thread( p.xstate = XStateBuf::default(); p.is_thread = true; p.parent_pid = owning_pid; + // Threads inherit their owning process's capabilities. + p.caps = unsafe { core::ptr::addr_of!(PTABLE[idx_of(owning_pid)].caps).read() }; p.fs_base = 0; p.current_cpu = None; p.cpu_ticks = 0; @@ -2238,6 +2327,21 @@ pub fn capture_user_gprs_at_entry(pid: u32) { /// `enter_user_by_pid_noreturn`, otherwise the yielding thread will resume /// with stale rbx/rbp/r12–r15/rdi/rsi/etc. and corrupt its C++ caller's /// `this` pointer and locals. +/// +/// The user GPR snapshot lives in a PER-CPU scratch area (`gs:[..]`), written by +/// the syscall entry stub. It is shared by every thread on that core, so if a +/// thread switch lands mid-handler (the wait loops `sti; hlt` and the timer ISR +/// can switch threads), another thread's syscall entry OVERWRITES it. A later +/// `save_full_user_gprs` would then read the *other* thread's callee-saved regs +/// and store them into our context → on resume `rbx`/`rbp`/`r12-15` are garbage +/// (observed: `MessageLoopOscortex::Run` resuming from `epoll_wait` with +/// `rbx`=a Skia stride → SIGSEGV; the root of the "sporadic" render crashes). +/// +/// Fix: capture ONCE, eagerly, at syscall entry (`capture_user_gprs_at_entry`, +/// called from `dispatch_fast` while the snapshot is still fresh for THIS +/// thread). This flag makes every subsequent yield-time `save_full_user_gprs` +/// in the same syscall a no-op, so a clobbered per-CPU snapshot can never leak +/// into our saved context. pub fn save_full_user_gprs(pid: u32) { // Only the first call per syscall (the eager entry capture) reads the // per-CPU snapshot; it is fresh then. Later yield-time calls are no-ops so a @@ -2246,6 +2350,19 @@ pub fn save_full_user_gprs(pid: u32) { return; } let snap = crate::arch::syscall::user_gprs(); + if pid == 2 { + log::warn!( + "[save_full_user_gprs] pid=2 rdi={:#x} rsi={:#x} rbx={:#x} rbp={:#x} r12={:#x} r13={:#x} r14={:#x} r15={:#x}", + snap.rdi, + snap.rsi, + snap.rbx, + snap.rbp, + snap.r12, + snap.r13, + snap.r14, + snap.r15 + ); + } let _g = PTABLE_LOCK.lock(); let p = unsafe { &mut PTABLE[idx_of(pid)] }; if p.pid != pid { return; } @@ -2318,6 +2435,14 @@ pub fn get_saved_rax(pid: u32) -> u64 { if p.pid == pid { p.regs.rax } else { 0 } } +/// Read the saved user link register (x30) for a pid. +#[cfg(target_arch = "aarch64")] +pub fn get_user_lr(pid: u32) -> u64 { + let _g = PTABLE_LOCK.lock(); + let p = unsafe { &PTABLE[idx_of(pid)] }; + if p.pid == pid { p.user_lr } else { 0 } +} + /// Saved user RIP/RSP for a blocked/yielded thread (from `save_return_context`). pub fn get_saved_rip_rsp(pid: u32) -> Option<(u64, u64)> { let _g = PTABLE_LOCK.lock(); @@ -2584,6 +2709,8 @@ pub fn fork_current() -> Result { child.syscall_stack_top = child_stack_top; child.is_thread = false; child.parent_pid = parent_pid; + // A forked child inherits the parent's capability set. + child.caps = unsafe { core::ptr::addr_of!(PTABLE[idx_of(parent_pid)].caps).read() }; child.cpu_ticks = 0; child.slice_left = 10; child.pending_sigs = 0; diff --git a/kernel/src/syscall/dispatch.rs b/kernel/src/syscall/dispatch.rs index df11cbb..8d1e7a7 100644 --- a/kernel/src/syscall/dispatch.rs +++ b/kernel/src/syscall/dispatch.rs @@ -197,6 +197,23 @@ pub extern "C" fn dispatch_fast(number: u64, arg0: u64, arg1: u64, arg2: u64, ar } } + // ── Capability enforcement ──────────────────────────────────────────── + // Privileged syscalls require the matching capability on the caller's PCB. + // Unprivileged calls (render/wm/vfs-read/posix) return None and run freely, + // so this never affects the shell or normal apps. The kernel idle task + // (pid 0) is exempt (kernel-internal callers). + if let Some(required) = required_cap(number) { + if crate::process::current_pid() != 0 + && !crate::process::current_has_caps(required) + { + log::warn!( + "[security] pid={} denied syscall {:#x} — missing capability {:?}", + crate::process::current_pid(), number, required + ); + return -1; // EPERM + } + } + let __ret: i64 = match number { // POSIX-compatible 0 => sys_read(arg0, arg1, arg2), @@ -786,6 +803,29 @@ pub extern "C" fn dispatch_fast(number: u64, arg0: u64, arg1: u64, arg2: u64, ar } /// Legacy INT 0x80 syscall path (handled by `arch::x86_64::syscall::legacy_syscall_entry`). +/// The capability a syscall requires, or `None` if it is unprivileged. +/// +/// Deliberately conservative: only the genuinely privileged operations are +/// gated (raw network access here; CAP_CORTEX is enforced inside the PID-0 +/// dispatch). Rendering, window-manager, VFS-read and POSIX calls — everything +/// a normal app makes — are unprivileged and never gated, so enforcement is +/// safe to switch on without breaking the shell or apps. +fn required_cap(number: u64) -> Option { + use crate::embedder::abi as eabi; + use crate::security::Capabilities; + match number { + // Raw network: userspace TCP/UDP + DHCP. The kernel's own package + // fetch does NOT go through these (it calls net::tcp directly), so + // gating them only constrains userspace — which only the trusted shell + // uses today. + n if n == eabi::SYS_NET_INFO + || n == eabi::SYS_NET_SEND + || n == eabi::SYS_NET_RECV => Some(Capabilities::NET), + 0x388 | 0x389 | 0x38A | 0x38B | 0x38C => Some(Capabilities::NET), + _ => None, + } +} + pub fn dispatch_legacy() { log::warn!("[syscall] dispatch_legacy called without saved register frame"); } diff --git a/kernel/src/syscall/handlers/engine.rs b/kernel/src/syscall/handlers/engine.rs index d5e3c53..4b23d26 100644 --- a/kernel/src/syscall/handlers/engine.rs +++ b/kernel/src/syscall/handlers/engine.rs @@ -302,11 +302,13 @@ pub(crate) fn sys_engine_vsync_baton_post(baton: u64) -> i64 { baton ); } - crate::wm::set_vsync_baton(baton); - // Embedder must run FlutterEngineOnVsync before the next push_vsync consumes - // this baton — keep pid=1 runnable even when engine threads are spinning. + let caller = crate::process::current_pid(); + crate::wm::set_vsync_baton(caller, baton); + // The POSTING engine must run FlutterEngineOnVsync before the next push_vsync + // consumes this baton — keep IT runnable even when engine threads spin. Was + // hardcoded pid=1, which starved a launched app's engine (second-engine freeze). if baton != 0 { - crate::process::set_state(1, crate::process::ProcState::Running); + crate::process::set_state(caller, crate::process::ProcState::Running); } 0 } @@ -871,6 +873,7 @@ pub(crate) fn sys_pkg_resolve(name_ptr: u64, name_len: u64) -> i64 { Err(crate::pkg::resolver::ResolveError::NoCatalog) => -2, // ENOENT Err(crate::pkg::resolver::ResolveError::FetchFailed) => -5, // EIO Err(crate::pkg::resolver::ResolveError::HashMismatch) => -22, // EINVAL + Err(crate::pkg::resolver::ResolveError::BadSignature) => -13, // EACCES Err(crate::pkg::resolver::ResolveError::InstallFailed) => -12, // ENOMEM Err(crate::pkg::resolver::ResolveError::CacheFull) => -28, // ENOSPC } diff --git a/kernel/src/syscall/handlers/ipc_display.rs b/kernel/src/syscall/handlers/ipc_display.rs index ed897ed..e306a46 100644 --- a/kernel/src/syscall/handlers/ipc_display.rs +++ b/kernel/src/syscall/handlers/ipc_display.rs @@ -477,6 +477,12 @@ pub(crate) fn sys_wm_focus_mirror_set(enabled: u64) -> i64 { } pub(crate) fn sys_wm_event_wait(ev_ptr: u64, ev_len: u64, timeout_ms: u64) -> i64 { + // Crash auto-recovery drain: the shell's event pump is a continuously- + // polled, normal syscall context — the safe place to tear down a crashed + // app's group and relaunch it. Cheap no-op when nothing is pending. + if crate::process::current_pid() == 1 { + crate::app_registry::drain_relaunches(); + } let cur = crate::process::current_pid(); if cur == 0 { return -1; @@ -569,8 +575,8 @@ pub(crate) fn sys_wm_event_wait(ev_ptr: u64, ev_len: u64, timeout_ms: u64) -> i6 deadline_ns, crate::wm::pending_count_for(cur), crate::wm::input_pending_for(cur), - crate::wm::embedder_baton_due(), - crate::wm::baton_vsync_queued_for(1) + crate::wm::embedder_baton_due(cur), + crate::wm::baton_vsync_queued_for(cur) ); } diff --git a/kernel/src/syscall/mod.rs b/kernel/src/syscall/mod.rs index 1b9bfcf..f731d6c 100644 --- a/kernel/src/syscall/mod.rs +++ b/kernel/src/syscall/mod.rs @@ -88,7 +88,7 @@ mod dispatch; mod handlers; -mod poll; +pub(crate) mod poll; mod posix; mod state; mod tables; diff --git a/kernel/src/syscall/poll.rs b/kernel/src/syscall/poll.rs index 62d95d5..1c82b90 100644 --- a/kernel/src/syscall/poll.rs +++ b/kernel/src/syscall/poll.rs @@ -134,7 +134,7 @@ pub fn coop_target_ready_locked(pid: u32, my_cpu: u32) -> bool { let input_is_waiting = input_priority_active && input_target != 0 && crate::wm::input_pending_for(input_target) > 0; - let has_pending = crate::wm::pending_count_for(pid) > 0 || crate::wm::embedder_baton_due(); + let has_pending = crate::wm::pending_count_for(pid) > 0 || crate::wm::embedder_baton_due(pid); if (wm == pid && has_pending) || (input_is_waiting && pid == input_target) { p.state = crate::process::ProcState::Running; crate::arch::smp::broadcast_resched_ipi(); @@ -151,15 +151,26 @@ pub fn coop_target_ready(pid: u32) -> bool { coop_target_ready_locked(pid, my_cpu) } -/// When a vsync baton is waiting, always prefer the embedder over engine spins. +/// When a vsync baton is waiting, prefer the engine whose baton is due over +/// engine spins. Per-engine: try the foreground app first (so a launched app +/// gets its own frames), then the shell (pid 1). Previously hardcoded pid 1, +/// which ran the shell for the app's baton and froze the app. pub fn prefer_embedder_if_baton_due(cur: u32) -> Option { let my_cpu = crate::arch::smp::this_cpu().cpu_id; let _g = crate::process::PTABLE_LOCK.lock(); - if cur != 1 && crate::wm::embedder_baton_due() && coop_target_ready_locked(1, my_cpu) { - let p = unsafe { &mut crate::process::PTABLE[crate::process::idx_of(1)] }; - if p.current_cpu.is_none() || p.current_cpu == Some(my_cpu) { - p.current_cpu = Some(my_cpu); - return Some(1); + let focus = crate::wm::focus_pid(); + let candidates = [focus, 1]; + for &cand in candidates.iter() { + if cand != 0 + && cand != cur + && crate::wm::embedder_baton_due(cand) + && coop_target_ready_locked(cand, my_cpu) + { + let p = unsafe { &mut crate::process::PTABLE[crate::process::idx_of(cand)] }; + if p.current_cpu.is_none() || p.current_cpu == Some(my_cpu) { + p.current_cpu = Some(my_cpu); + return Some(cand); + } } } None @@ -186,8 +197,9 @@ pub fn cooperative_sched_target_locked(cur: u32, my_cpu: u32) -> Option { } let wm = WM_WAITER_PID.load(Ordering::Acquire); - // Embedder must drain WM events (especially vsync batons) promptly. - if wm != 0 && wm != cur && crate::wm::embedder_baton_due() { + // Embedder must drain WM events (especially vsync batons) promptly — run the + // waiting engine if ITS baton is due (per-engine, was a global pid-1 check). + if wm != 0 && wm != cur && crate::wm::embedder_baton_due(wm) { if coop_target_ready_locked(wm, my_cpu) { let p = unsafe { &mut crate::process::PTABLE[crate::process::idx_of(wm)] }; if p.current_cpu.is_none() || p.current_cpu == Some(my_cpu) { @@ -1359,6 +1371,70 @@ pub(crate) fn sys_epoll_wait_real(epfd: u64, events_out: u64, maxevents: u64, ti } } + if cur != 0 { + let coop_target = cooperative_sched_target(cur); + if let Some(next) = coop_target.filter(|&n| n != cur) { + if epfd == 70 || epfd == 72 || cur == 2 || cur == 3 || cur == 4 || cur == 7 { + static EPOLL_BLOCK_LOG: core::sync::atomic::AtomicU32 = + core::sync::atomic::AtomicU32::new(0); + let n = EPOLL_BLOCK_LOG.fetch_add(1, Ordering::Relaxed); + if n < 48 { + log::warn!( + "[epoll-block] #{} pid={} epfd={} next={} rip={:#x}", + n, + cur, + epfd, + next, + crate::arch::syscall::user_rip() + ); + } + } + // Re-execute the epoll_wait syscall on resume (do NOT return + // -1/EINTR to userspace). Returning EINTR here makes the + // Flutter engine retry epoll_wait immediately; if it is woken + // before any fd is actually ready it gets EINTR again and + // busy-spins, monopolizing the single core and starving the + // embedder host (pid 1) input loop. Saving the context at the + // syscall instruction (urip - 2) with rax = the epoll_wait + // syscall number makes the thread re-enter the kernel on wake + // and re-check readiness — returning real events when ready or + // blocking again — exactly like sys_wm_event_wait does. + let urip = crate::arch::syscall::user_rip(); + let ursp = crate::arch::syscall::user_rsp(); + crate::process::save_return_context_reexec(cur, urip, ursp); + crate::process::save_full_user_gprs(cur); + crate::process::set_rax(cur, 0x47B); // SYS epoll_wait (re-enter) + crate::process::save_xstate(cur); + crate::process::enter_user_by_pid_noreturn(next); + } else if cur >= 2 + && crate::process::get_group_leader(cur) == 1 + && !crate::wm::flutter_init_ready() + && crate::process::current_pid() != 1 + { + static EPOLL_INIT_HANDOFF_LOG: core::sync::atomic::AtomicU32 = + core::sync::atomic::AtomicU32::new(0); + let n = EPOLL_INIT_HANDOFF_LOG.fetch_add(1, Ordering::Relaxed); + if n < 32 { + log::warn!( + "[epoll-init-handoff] #{} pid={} epfd={} -> pid1 rip={:#x} target={:?}", + n, + cur, + epfd, + crate::arch::syscall::user_rip(), + coop_target + ); + } + crate::process::set_state(1, crate::process::ProcState::Running); + let urip = crate::arch::syscall::user_rip(); + let ursp = crate::arch::syscall::user_rsp(); + crate::process::save_return_context_reexec(cur, urip, ursp); + crate::process::save_full_user_gprs(cur); + crate::process::set_rax(cur, 0x47B); + crate::process::save_xstate(cur); + crate::process::enter_user_by_pid_noreturn(1); + } + } + // Sleep with IRQs unmasked so the timer ISR fires + (aarch64) its // kernel-mode wake-assist can run. Was x86-only → aarch64 busy-spun in EL1. unsafe { diff --git a/kernel/src/wm/mod.rs b/kernel/src/wm/mod.rs index 5b15077..fe78126 100644 --- a/kernel/src/wm/mod.rs +++ b/kernel/src/wm/mod.rs @@ -488,9 +488,12 @@ pub fn baton_vsync_queued_for(pid: u32) -> bool { with_queue(|q| q.has_baton_vsync_for(pid)) } -/// True when the embedder must run FlutterEngineOnVsync (baton queued or posted). -pub fn embedder_baton_due() -> bool { - vsync_baton_pending() || baton_vsync_queued_for(1) +/// True when engine `pid` must run FlutterEngineOnVsync (a baton-carrying +/// EV_VSYNC is queued for it). Per-engine: each Flutter engine (shell pid 1, +/// each launched app) gets its OWN vsync, so a second engine no longer starves +/// when the shell's baton slot is busy (the cause of the open-an-app freeze). +pub fn embedder_baton_due(pid: u32) -> bool { + baton_vsync_queued_for(pid) } pub fn pop_event_for(pid: u32) -> Option { @@ -581,29 +584,34 @@ pub fn push_vsync(frame: u64) { } -/// Deliver a vsync baton to the embedder (pid=1) immediately. +/// Deliver a vsync baton to the engine that posted it, immediately. /// -/// Called via `sys_engine_vsync_baton_post` when the engine invokes the -/// embedder's `vsync_callback`. The baton is a "call `FlutterEngineOnVsync` -/// now" signal: it must NOT be parked in `VSYNC_BATON` waiting for the next -/// `compositor::tick()` -> `push_vsync`, because that path is gated on -/// `COMP.try_lock()` and stalls permanently if any thread holds the compositor -/// lock. We push the EV_VSYNC event straight to the front of pid=1's queue and -/// wake it, independent of compositor state. -pub fn set_vsync_baton(baton: u64) { +/// Called via `sys_engine_vsync_baton_post` when an engine invokes its +/// `vsync_callback`. The baton is a "call `FlutterEngineOnVsync` now" signal: it +/// must NOT be parked in `VSYNC_BATON` waiting for the next `compositor::tick()` +/// -> `push_vsync` (that path is gated on `COMP.try_lock()` and stalls if any +/// thread holds the compositor lock). We push the EV_VSYNC event to the front of +/// the POSTING engine's queue and wake it, independent of compositor state. +/// +/// `pid` is the engine that posted (the syscall caller). Previously this was +/// hardcoded to pid 1, so a launched app's baton was delivered to the shell and +/// the app's engine never advanced — the second-engine freeze. Now each engine +/// gets its own vsync. +pub fn set_vsync_baton(pid: u32, baton: u64) { if baton == 0 { VSYNC_BATON.store(0, Ordering::Release); return; } - // Don't strand the baton; deliver it now. + // Don't strand the baton; deliver it now to the posting engine. VSYNC_BATON.store(0, Ordering::Release); + let owner = canonical_pid(pid); let mut ev = WmEvent::empty(); ev.kind = EV_VSYNC; ev.a = 0; ev.b = baton; - with_queue(|q| q.push_front(ev, 0)); + with_queue(|q| q.push_front(ev, owner)); BATON_VSYNC_QUEUED.store(true, Ordering::Release); - crate::process::wake_process(1); + crate::process::wake_process(owner); let waiter = WM_WAITER.load(Ordering::Acquire); if waiter != 0 { WM_WAITER.store(0, Ordering::Release); @@ -618,7 +626,41 @@ pub fn set_vsync_baton(baton: u64) { static LAST_BUTTONS: AtomicU32 = AtomicU32::new(0); +// Unified software-cursor state. Fed by EVERY pointer source — the x86 PS/2 +// driver and the aarch64 virtio-input driver both funnel through push_pointer — +// so the compositor can draw the cursor on any arch (previously it read x86-only +// ps2:: state and never drew on ARM). +static CURSOR_X: core::sync::atomic::AtomicI32 = core::sync::atomic::AtomicI32::new(32); +static CURSOR_Y: core::sync::atomic::AtomicI32 = core::sync::atomic::AtomicI32::new(32); +static CURSOR_BUTTONS: AtomicU32 = AtomicU32::new(0); +static CURSOR_SEEN: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false); +static CURSOR_LAST_ACT_NS: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0); + +/// Current pointer position (absolute, framebuffer pixels). +pub fn cursor_pos() -> (i32, i32) { + (CURSOR_X.load(Ordering::Relaxed), CURSOR_Y.load(Ordering::Relaxed)) +} +/// Current pressed-button bitmask. +pub fn cursor_buttons() -> u32 { + CURSOR_BUTTONS.load(Ordering::Relaxed) +} +/// True once any pointer event has arrived (a pointing device is live). +pub fn cursor_seen() -> bool { + CURSOR_SEEN.load(Ordering::Relaxed) +} +/// Monotonic-ns timestamp of the last pointer activity (for idle auto-hide). +pub fn cursor_last_act_ns() -> u64 { + CURSOR_LAST_ACT_NS.load(Ordering::Relaxed) +} + pub fn push_pointer(x: i32, y: i32, buttons: u32) { + // Record cursor state for the compositor's software cursor (arch-neutral). + CURSOR_X.store(x, Ordering::Relaxed); + CURSOR_Y.store(y, Ordering::Relaxed); + CURSOR_BUTTONS.store(buttons, Ordering::Relaxed); + CURSOR_SEEN.store(true, Ordering::Relaxed); + CURSOR_LAST_ACT_NS.store(crate::arch::rdtsc_ns(), Ordering::Relaxed); + let packed = ((x as u32 as u64) << 32) | (y as u32 as u64); // Detect button transition (press or release) to flag it as high-priority. @@ -674,6 +716,12 @@ pub fn push_scroll(x: i32, y: i32, dz: i32) { } pub fn push_key(scancode: u32, pressed: bool) { + // F2 (PS/2 set-1 make 0x3C) toggles the kernel boot screen's verbose log + // overlay. Handled here so it works during the engine warm-up before any app + // is focused; the keypress is still forwarded for normal handling. + if pressed && scancode == 0x3C { + crate::drivers::bootscreen::toggle_verbose(); + } let focus = focus_pid(); let flags = if pressed { 1 } else { 0 }; if focus == 0 { diff --git a/run-qemu-debug.sh b/run-qemu-debug.sh index 3753e4f..206a5c9 100644 --- a/run-qemu-debug.sh +++ b/run-qemu-debug.sh @@ -18,6 +18,20 @@ fi rm -f "$SERIAL_LOG" touch "$SERIAL_LOG" +# Create a small virtio-blk disk image for ext2 testing (8 MiB). +VBLK="$SCRIPT_DIR/vdisk.img" +if [ ! -f "$VBLK" ]; then + echo "[QEMU] Creating 8 MiB virtio-blk image: $VBLK" + dd if=/dev/zero of="$VBLK" bs=1M count=8 2>/dev/null +fi + +# Create a small NVMe disk image (16 MiB). +NVME="$SCRIPT_DIR/nvme.img" +if [ ! -f "$NVME" ]; then + echo "[QEMU] Creating 16 MiB NVMe image: $NVME" + dd if=/dev/zero of="$NVME" bs=1M count=16 2>/dev/null +fi + echo "==========================================" echo "OSCortex Boot — graphical window + serial" echo "==========================================" @@ -40,6 +54,12 @@ qemu-system-x86_64 \ -m 2G \ -cdrom "$ISO" \ -boot d \ + -device virtio-net-pci,netdev=net0 \ + -netdev user,id=net0 \ + -device virtio-blk-pci,drive=vblk \ + -drive "file=$VBLK,format=raw,id=vblk,if=none" \ + -device nvme,drive=nvmedrive,serial=oscortex0 \ + -drive "file=$NVME,format=raw,id=nvmedrive,if=none" \ -serial file:"$SERIAL_LOG" \ -display cocoa \ -vga std \ diff --git a/scripts/build-iso-aarch64.sh b/scripts/build-iso-aarch64.sh index db76734..2d9802f 100755 --- a/scripts/build-iso-aarch64.sh +++ b/scripts/build-iso-aarch64.sh @@ -8,8 +8,13 @@ # * BOOTAA64.EFI as the EFI bootloader, # * limine.conf with the OSCortex entry. # -# The aarch64 `/init` is the embedded EL0 preemption-test program (kernel build.rs); -# no extra initramfs staging is required to reach userspace. +# The kernel embeds the `initramfs/` tree (via kernel/build.rs), including `/init` +# (the aarch64 Flutter shell host) and the aarch64 engine + libapp. That tree is +# SHARED with the x86 build (scripts/build-iso.sh), which overwrites it with x86 +# binaries — so running the x86 build before this one leaves an x86 `/init` the +# aarch64 kernel can't spawn ("Failed to spawn /init: wrong ELF machine"), hanging +# the boot before the shell. The guard below fails loudly in that case so we never +# package a contaminated ISO; re-stage the aarch64 runtime first. # # Usage: # scripts/build-iso-aarch64.sh # build the ISO @@ -29,6 +34,17 @@ TARGET="aarch64-unknown-none" QEMU_SHARE="$(dirname "$(command -v qemu-system-aarch64)")/../share/qemu" AAVMF_CODE="$QEMU_SHARE/edk2-aarch64-code.fd" +# Guard: refuse to build if the shared initramfs/ holds a wrong-arch /init +# (cross-arch contamination from an x86 build). Packaging it would hang the boot. +INIT_BIN="$ROOT/initramfs/init" +if [ -f "$INIT_BIN" ] && ! file -b "$INIT_BIN" | grep -qi "aarch64"; then + echo "ERROR: $INIT_BIN is not an aarch64 binary — initramfs/ is contaminated:" >&2 + echo " $(file -b "$INIT_BIN")" >&2 + echo " Re-stage the aarch64 runtime (scripts/build-aarch64-shell.sh, or" >&2 + echo " restore the arm64 shell-runtime tarball) before building the ISO." >&2 + exit 1 +fi + echo "[1/4] Building aarch64 kernel ELF (Limine higher-half)..." cd "$ROOT" cargo build \ diff --git a/scripts/build-iso.sh b/scripts/build-iso.sh index 780983a..3fc1dd1 100755 --- a/scripts/build-iso.sh +++ b/scripts/build-iso.sh @@ -73,23 +73,38 @@ fi # libapp.so. Native AOT is being done properly via the engine port — see # docs/native-engine-port.md. (Removed with the scratch/ debugging tree.) -echo "[0.35/5] Building core system apps into /Applications..." mkdir -p "$ROOT/initramfs/Applications" -"$ROOT/tools/build-flutter-osx.sh" \ - "$ROOT/apps/oscortex_canvas" \ - "Canvas" \ - "$ROOT/initramfs/Applications/Canvas.app/Canvas.osx" \ - "$ROOT/initramfs/Applications/Canvas.app/flutter_assets" -"$ROOT/tools/build-flutter-osx.sh" \ - "$ROOT/apps/oscortex_files" \ - "Files" \ - "$ROOT/initramfs/Applications/Files.app/Files.osx" \ - "$ROOT/initramfs/Applications/Files.app/flutter_assets" -"$ROOT/tools/build-flutter-osx.sh" \ - "$ROOT/apps/oscortex_web_link" \ - "Web Link" \ - "$ROOT/initramfs/Applications/Web Link.app/Web Link.osx" \ - "$ROOT/initramfs/Applications/Web Link.app/flutter_assets" +if [ -n "${SKIP_CORE_APPS:-}" ]; then + # The core-app rebuild drives tools/build-flutter-osx.sh, whose AOT step needs + # the oscx-engine Docker image. When that image is unavailable, skip the rebuild + # and reuse the app assets already staged in initramfs/Applications (the apps run + # JIT off kernel_blob.bin — arch-independent — so the staged bundles still render + # their launcher tiles). Set SKIP_CORE_APPS=1 to take this path. + echo "[0.35/5] SKIP_CORE_APPS set — reusing staged app assets (Docker/oscx-engine not required)" + for a in "Canvas.app/flutter_assets/kernel_blob.bin" "Files.app/flutter_assets/kernel_blob.bin" "Web Link.app/flutter_assets/kernel_blob.bin"; do + if [ ! -f "$ROOT/initramfs/Applications/$a" ]; then + echo "ERROR: SKIP_CORE_APPS set but staged app asset missing: initramfs/Applications/$a" >&2 + exit 1 + fi + done +else + echo "[0.35/5] Building core system apps into /Applications..." + "$ROOT/tools/build-flutter-osx.sh" \ + "$ROOT/apps/oscortex_canvas" \ + "Canvas" \ + "$ROOT/initramfs/Applications/Canvas.app/Canvas.osx" \ + "$ROOT/initramfs/Applications/Canvas.app/flutter_assets" + "$ROOT/tools/build-flutter-osx.sh" \ + "$ROOT/apps/oscortex_files" \ + "Files" \ + "$ROOT/initramfs/Applications/Files.app/Files.osx" \ + "$ROOT/initramfs/Applications/Files.app/flutter_assets" + "$ROOT/tools/build-flutter-osx.sh" \ + "$ROOT/apps/oscortex_web_link" \ + "Web Link" \ + "$ROOT/initramfs/Applications/Web Link.app/Web Link.osx" \ + "$ROOT/initramfs/Applications/Web Link.app/flutter_assets" +fi if [ -d "$APP_ASSETS_DIR" ]; then echo "[0.4/5] Syncing shell Flutter assets into initramfs..." @@ -184,15 +199,19 @@ else exit 1 fi -echo "[0.5/5] Compiling and staging userspace libc helper..." -mkdir -p "$ROOT/initramfs/system/lib" -docker run --rm --platform linux/amd64 \ - -v "$ROOT:$ROOT" \ - -w "$ROOT" \ - gcc:12 \ - gcc -shared -fPIC -ffreestanding -nostdlib -O2 \ - -o "$ROOT/initramfs/system/lib/liboscortex_libc.so" \ - "$ROOT/userspace/libc/libc.c" +if [ -f "$ROOT/initramfs/system/lib/liboscortex_libc.so" ]; then + echo "[0.5/5] Skipping compilation of userspace libc helper (already exists)..." +else + echo "[0.5/5] Compiling and staging userspace libc helper..." + mkdir -p "$ROOT/initramfs/system/lib" + docker run --rm --platform linux/amd64 \ + -v "$ROOT:$ROOT" \ + -w "$ROOT" \ + gcc:12 \ + gcc -shared -fPIC -ffreestanding -nostdlib -O2 \ + -o "$ROOT/initramfs/system/lib/liboscortex_libc.so" \ + "$ROOT/userspace/libc/libc.c" +fi echo "[0.51/5] Staging Flutter engine runtime..." if [ ! -f "$FLUTTER_ENGINE_SO" ]; then @@ -223,6 +242,14 @@ for req in "${REQUIRED_FILES[@]}"; do fi done +# x86_64 runs the shell via the JIT engine off kernel_blob.bin (the staged engine +# MUST be the JIT/debug engine that contains the Dart kernel compiler, NOT the AOT +# 'product' engine — the AOT path needs a matching 'product' gen_snapshot that is +# not available, so it leaves the shell unable to load Dart → blank screen). Remove +# any AOT snapshot so the embedder uses the arch-independent JIT kernel_blob. +rm -f "$ROOT/initramfs/system/flutter/libapp.so" \ + "$ROOT/initramfs/system/flutter/app.aot" + echo "[1/5] Building kernel ELF..." touch "$ROOT/kernel/src/fs/initramfs.rs" cd "$ROOT" diff --git a/tools/build-flutter-osx.sh b/tools/build-flutter-osx.sh index dce5a87..d8b1b13 100755 --- a/tools/build-flutter-osx.sh +++ b/tools/build-flutter-osx.sh @@ -19,7 +19,17 @@ FLUTTER_HOME="${FLUTTER_HOME:-/opt/homebrew/share/flutter}" DARTAOT="$FLUTTER_HOME/bin/cache/dart-sdk/bin/dartaotruntime" FRONTEND_SERVER="$FLUTTER_HOME/bin/cache/artifacts/engine/darwin-x64/frontend_server_aot.dart.snapshot" SDK_ROOT_PRODUCT="$FLUTTER_HOME/bin/cache/artifacts/engine/common/flutter_patched_sdk/" -GEN_SNAP="$ROOT/tools/flutter-engine/linux-x64/gen_snapshot" +# Use the gen_snapshot that MATCHES the shipped x86_64 engine +# (tools/flutter-engine/libflutter_engine.so). The two x64 gen_snapshots are +# different Dart build flavors: tools/flutter-engine/linux-x64/gen_snapshot emits +# a 'release' snapshot, but the shipped engine is 'product' — that mismatch +# ("snapshot requires release but the VM has product") left the x86 shell unable +# to load its Dart code → blank screen. The matched (product) gen_snapshot ships +# in the oscortex-x64-release engine bundle. +GEN_SNAP="$ROOT/.engine-cache/oscortex-engine-1/oscortex-x64-release/gen_snapshot" +if [ ! -x "$GEN_SNAP" ]; then + GEN_SNAP="$ROOT/tools/flutter-engine/linux-x64/gen_snapshot" +fi PKG_CONFIG="$APP_DIR/.dart_tool/package_config.json" APP_MAIN="$APP_DIR/lib/main.dart" diff --git a/tools/flutter-embedder/src/main.rs b/tools/flutter-embedder/src/main.rs index 7ed6a14..54c16c4 100644 --- a/tools/flutter-embedder/src/main.rs +++ b/tools/flutter-embedder/src/main.rs @@ -1947,6 +1947,36 @@ fn dispatch_shell_command(payload: &[u8]) -> &'static [u8] { let path = trim_line(path); return install_osx_from_path(path); } + // ── On-demand package delivery. The kernel pipeline (HTTP → SHA-256 verify → + // LRU cache → install) is reachable via syscalls 0x4C0-0x4C2; route the shell's + // pkg_* messages to it. "Apps stream from your server on first tap." ───────── + if payload == b"pkg_catalog" || payload.starts_with(b"pkg_catalog") { + if !shell_capable { + return b"{\"ok\":false,\"err\":\"cap\",\"packages\":[]}"; + } + return format_pkg_catalog_json(); + } + if payload.starts_with(b"pkg_resolve:") { + if !shell_capable { + return b"{\"ok\":false,\"err\":\"cap\",\"app_id\":-1}"; + } + let name = trim_line(&payload[12..]); // "pkg_resolve:".len() == 12 + return format_app_id_json(sys::pkg_resolve(name)); + } + if payload.starts_with(b"pkg_set_server:") { + if !shell_capable { + return b"{\"ok\":false,\"err\":\"cap\"}"; + } + // "pkg_set_server::" + let rest = trim_line(&payload[15..]); // "pkg_set_server:".len() == 15 + match parse_ip_port(rest) { + Some((ip_be, port)) => { + let _ = sys::pkg_set_server(ip_be, port); + return b"{\"ok\":true}"; + } + None => return b"{\"ok\":false}", + } + } // ── Settings. Driver/input preferences, settable from any host (the Settings // UI may run in the shell or its own app). "config:get" returns current state. if payload.starts_with(b"config:scroll_invert:") { @@ -2117,6 +2147,129 @@ fn format_app_list_json() -> &'static [u8] { unsafe { core::slice::from_raw_parts(APP_LIST_JSON.as_ptr(), pos) } } +// ── On-demand package pipeline JSON helpers ────────────────────────────────── + +static mut PKG_APPID_JSON: [u8; 32] = [0; 32]; +/// `{"app_id":N}` for a pkg_resolve result (N>=0 ok, anything <0 → -1). +fn format_app_id_json(id: i64) -> &'static [u8] { + let out = unsafe { &mut PKG_APPID_JSON }; + const PREFIX: &[u8] = b"{\"app_id\":"; + out[..PREFIX.len()].copy_from_slice(PREFIX); + let mut pos = PREFIX.len(); + if id < 0 { + out[pos] = b'-'; + pos += 1; + out[pos] = b'1'; + pos += 1; + } else { + pos += write_json_u32(&mut out[pos..], id as u32); + } + out[pos] = b'}'; + pos += 1; + unsafe { core::slice::from_raw_parts(PKG_APPID_JSON.as_ptr(), pos) } +} + +static mut PKG_CATALOG_JSON: [u8; 4096] = [0; 4096]; +/// Serialise the kernel package catalog (128-byte PkgManifest entries) into the +/// `{"packages":[{"name","version","size"}]}` shape the Dart shell expects. +fn format_pkg_catalog_json() -> &'static [u8] { + let mut records = [0u8; 4096]; + let cnt = sys::pkg_catalog(&mut records); + let count = if cnt < 0 { 0 } else { cnt as usize }; + let out = unsafe { &mut PKG_CATALOG_JSON }; + const HEAD: &[u8] = b"{\"packages\":["; + out[..HEAD.len()].copy_from_slice(HEAD); + let mut pos = HEAD.len(); + let max = (records.len() / 128).min(count); + let mut i = 0usize; + while i < max { + let off = i * 128; + let name_end = records[off..off + 64].iter().position(|&b| b == 0).unwrap_or(64); + let name = &records[off..off + name_end]; + let ver_end = records[off + 64..off + 80].iter().position(|&b| b == 0).unwrap_or(16); + let version = &records[off + 64..off + 64 + ver_end]; + let size = u32::from_le_bytes(records[off + 112..off + 116].try_into().unwrap_or([0; 4])); + if pos + name.len() + version.len() + 80 > out.len() { + break; + } + if i > 0 { + out[pos] = b','; + pos += 1; + } + const NP: &[u8] = b"{\"name\":\""; + out[pos..pos + NP.len()].copy_from_slice(NP); + pos += NP.len(); + out[pos..pos + name.len()].copy_from_slice(name); + pos += name.len(); + const VP: &[u8] = b"\",\"version\":\""; + out[pos..pos + VP.len()].copy_from_slice(VP); + pos += VP.len(); + out[pos..pos + version.len()].copy_from_slice(version); + pos += version.len(); + const SP: &[u8] = b"\",\"size\":"; + out[pos..pos + SP.len()].copy_from_slice(SP); + pos += SP.len(); + pos += write_json_u32(&mut out[pos..], size); + out[pos] = b'}'; + pos += 1; + i += 1; + } + out[pos..pos + 2].copy_from_slice(b"]}"); + pos += 2; + unsafe { core::slice::from_raw_parts(PKG_CATALOG_JSON.as_ptr(), pos) } +} + +/// Parse "a.b.c.d:port" → (ip as big-endian-packed u32, port). None on malformed. +fn parse_ip_port(s: &[u8]) -> Option<(u32, u16)> { + let colon = s.iter().rposition(|&b| b == b':')?; + let (ip_str, port_str) = (&s[..colon], &s[colon + 1..]); + let mut octets = [0u32; 4]; + let mut oi = 0usize; + let mut cur = 0u32; + let mut have = false; + for &b in ip_str { + if b == b'.' { + if oi >= 3 || !have { + return None; + } + octets[oi] = cur; + oi += 1; + cur = 0; + have = false; + } else if b.is_ascii_digit() { + cur = cur * 10 + (b - b'0') as u32; + if cur > 255 { + return None; + } + have = true; + } else { + return None; + } + } + if oi != 3 || !have { + return None; + } + octets[3] = cur; + let ip = (octets[0] << 24) | (octets[1] << 16) | (octets[2] << 8) | octets[3]; + let mut port = 0u32; + let mut phave = false; + for &b in port_str { + if b.is_ascii_digit() { + port = port * 10 + (b - b'0') as u32; + if port > 65535 { + return None; + } + phave = true; + } else { + return None; + } + } + if !phave { + return None; + } + Some((ip, port as u16)) +} + static mut VFS_LIST_JSON: [u8; 8192] = [0; 8192]; static VFS_LIST_JSON_LEN: AtomicU32 = AtomicU32::new(0); diff --git a/tools/flutter-embedder/src/sys.rs b/tools/flutter-embedder/src/sys.rs index 9417c29..b4ba9c2 100644 --- a/tools/flutter-embedder/src/sys.rs +++ b/tools/flutter-embedder/src/sys.rs @@ -666,6 +666,11 @@ pub const SYS_APP_UNINSTALL: u64 = 0x372; pub const SYS_VFS_READ: u64 = 0x37A; pub const SYS_VFS_STAT: u64 = 0x37C; pub const SYS_VFS_LIST: u64 = 0x379; +// On-demand package delivery (matches kernel embedder/abi.rs). Moved off +// 0x390-0x393 (collided with sched_yield/fork/signal) to a free block (b4dcdb5). +pub const SYS_PKG_RESOLVE: u64 = 0x4C0; +pub const SYS_PKG_CATALOG: u64 = 0x4C1; +pub const SYS_PKG_SET_SERVER: u64 = 0x4C2; pub fn app_install(bundle: &[u8], id_out: &mut u32) -> i64 { @@ -691,6 +696,25 @@ pub fn app_uninstall(app_id: u32) -> i64 { unsafe { syscall1(SYS_APP_UNINSTALL, app_id as u64) } } +/// On-demand package: resolve a name → fetch + SHA-256 verify + cache + install. +/// Returns app_id (>=0) or -ERRNO. The kernel runs the whole pipeline. +pub fn pkg_resolve(name: &[u8]) -> i64 { + let len = path_len(name); // tolerate a trailing NUL + unsafe { syscall2(SYS_PKG_RESOLVE, name.as_ptr() as u64, len as u64) } +} + +/// On-demand package: write the remote catalog (128-byte PkgManifest entries) +/// into `buf`; returns the total available count. +pub fn pkg_catalog(buf: &mut [u8]) -> i64 { + unsafe { syscall2(SYS_PKG_CATALOG, buf.as_mut_ptr() as u64, buf.len() as u64) } +} + +/// On-demand package: point the resolver at a server (IP big-endian u32 + port) +/// and refresh the catalog. Returns 0. +pub fn pkg_set_server(ip_be: u32, port: u16) -> i64 { + unsafe { syscall2(SYS_PKG_SET_SERVER, ip_be as u64, port as u64) } +} + fn path_len(path: &[u8]) -> usize { if !path.is_empty() && *path.last().unwrap() == 0 { path.len() - 1 diff --git a/tools/flutter-engine/libflutter_engine.so.trace b/tools/flutter-engine/libflutter_engine.so.trace deleted file mode 100755 index 65408df..0000000 Binary files a/tools/flutter-engine/libflutter_engine.so.trace and /dev/null differ diff --git a/tools/pkg-server/Cargo.toml b/tools/pkg-server/Cargo.toml index 50394e1..91b401c 100644 --- a/tools/pkg-server/Cargo.toml +++ b/tools/pkg-server/Cargo.toml @@ -6,3 +6,4 @@ description = "Serves .osx application bundles over HTTP for on-demand package d [dependencies] sha2 = "0.10" +ed25519-compact = "2" diff --git a/tools/pkg-server/src/main.rs b/tools/pkg-server/src/main.rs index 5f69af3..34108e9 100644 Binary files a/tools/pkg-server/src/main.rs and b/tools/pkg-server/src/main.rs differ