diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b9fbeac..a055d87 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -82,33 +82,66 @@ jobs: - name: Build bootable ISO (x86_64) run: cargo xtask iso - - name: Build kernel (aarch64) + - name: Build kernel (aarch64, render-capable) # aarch64 boots via QEMU `-kernel` (no Limine/ISO on -M virt), so the - # release artifact is the raw kernel ELF. This is platform-scope parity - # with the x86 ISO above: the automated build embeds whatever is committed - # under initramfs/ (the full render-capable Flutter runtime — engine .so, - # AOT shell snapshot, ICU, assets — is gitignored and staged separately by - # scripts/build-aarch64-shell.sh, exactly like the x86 full-shell ISO). + # release artifact is the raw kernel ELF. Unlike the x86 ISO (still + # platform-scope), the aarch64 kernel is RENDER-CAPABLE: we fetch the + # prebuilt Flutter shell runtime (engine .so + AOT shell snapshot + ICU + + # fonts/assets + embedder /init + libc + seed) from the `oscortex-engine-1` + # release and stage it into initramfs/ before building, so kernel/build.rs + # embeds it. The runtime is gitignored (large, prebuilt), hence the fetch. + # NOTE: refresh `oscortex-arm64-shell-runtime.tar.gz` on that release when + # the shell Dart/app or engine changes — it is a prebuilt snapshot. + env: + GH_TOKEN: ${{ github.token }} run: | set -euo pipefail + # Fetch + verify the render runtime bundle. + gh release download oscortex-engine-1 --repo "$GITHUB_REPOSITORY" \ + -p 'oscortex-arm64-shell-runtime.tar.gz' \ + -p 'oscortex-arm64-shell-runtime.tar.gz.sha256' -O /tmp/rt.tar.gz \ + --clobber || gh release download oscortex-engine-1 --repo "$GITHUB_REPOSITORY" \ + -p 'oscortex-arm64-shell-runtime.tar.gz' -O /tmp/rt.tar.gz --clobber + if [ -f oscortex-arm64-shell-runtime.tar.gz.sha256 ]; then + exp=$(awk '{print $1}' oscortex-arm64-shell-runtime.tar.gz.sha256) + got=$(sha256sum /tmp/rt.tar.gz | awk '{print $1}') + [ "$exp" = "$got" ] || { echo "runtime sha256 mismatch"; exit 1; } + fi + # Stage into initramfs/ (kernel/build.rs embeds it). + mkdir -p initramfs + tar -xzf /tmp/rt.tar.gz -C initramfs + ls -lh initramfs/init initramfs/system/lib/libflutter_engine.so \ + initramfs/system/flutter/libapp.so + # Build the render-capable kernel (embeds the staged runtime). + touch kernel/src/fs/initramfs.rs cargo build --release --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 + # Also build the UEFI/Limine bootable ISO (the same render runtime is + # already staged in initramfs/). This is what boots in UTM / VMs / bare + # metal — a real ISO, not just the raw -kernel ELF. build-iso-aarch64.sh + # builds a higher-half limine-boot kernel and packages it with + # BOOTAA64.EFI; LIMINE_DIR was exported by the limine install step. + LIMINE_DIR="$HOME/limine" bash scripts/build-iso-aarch64.sh + ls -lh oscortex-aarch64.iso - name: Stage release assets run: | set -euo pipefail mkdir -p dist - # x86_64 bootable ISO - cp oscortex.iso "dist/oscortex-${{ steps.ver.outputs.tag }}.iso" - sha256sum "dist/oscortex-${{ steps.ver.outputs.tag }}.iso" \ - > "dist/oscortex-${{ steps.ver.outputs.tag }}.iso.sha256" - # aarch64 kernel ELF - cp target/aarch64-unknown-none/release/kernel \ - "dist/oscortex-aarch64-${{ steps.ver.outputs.tag }}.kernel" - sha256sum "dist/oscortex-aarch64-${{ steps.ver.outputs.tag }}.kernel" \ - > "dist/oscortex-aarch64-${{ steps.ver.outputs.tag }}.kernel.sha256" + tag="${{ steps.ver.outputs.tag }}" + # Arch-labelled names. Each arch ships a bootable ISO (UEFI) AND, for + # aarch64, the raw -kernel ELF for QEMU `-M virt -kernel` users. + cp oscortex.iso "dist/oscortex-x86_64-${tag}.iso" + cp oscortex-aarch64.iso "dist/oscortex-aarch64-${tag}.iso" + cp target/aarch64-unknown-none/release/kernel "dist/oscortex-aarch64-${tag}.kernel" + # Checksums computed from inside dist/ so each .sha256 lists a bare + # filename and `sha256sum -c` works after downloading just the pair. + ( cd dist + sha256sum "oscortex-x86_64-${tag}.iso" > "oscortex-x86_64-${tag}.iso.sha256" + sha256sum "oscortex-aarch64-${tag}.iso" > "oscortex-aarch64-${tag}.iso.sha256" + sha256sum "oscortex-aarch64-${tag}.kernel" > "oscortex-aarch64-${tag}.kernel.sha256" ) ls -lh dist - name: Publish GitHub Release @@ -122,22 +155,33 @@ 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. + **Artifacts:** - - `oscortex-.iso` — x86_64 bootable kernel ISO (Limine BIOS + UEFI), - built via `cargo xtask iso`. - - `oscortex-aarch64-.kernel` — aarch64 kernel ELF; boot with - `qemu-system-aarch64 -M virt -cpu cortex-a72 -kernel -device ramfb`. - - Both are **platform-scope** builds. The full render-capable Flutter shell - (engine `.so` + AOT app snapshot + ICU + assets) is gitignored and staged - separately — x86 via `scripts/build-iso.sh`, aarch64 via - `scripts/build-aarch64-shell.sh` (which pulls the engine + `gen_snapshot` - from the `oscortex-engine-1` release). Wiring that runtime into the - automated release is a tracked follow-up. + - `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: + ``` + qemu-system-aarch64 -M virt -cpu cortex-a72 -smp 1 -m 2G \ + -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. Verify with the attached `.sha256` files. files: | - dist/oscortex-${{ steps.ver.outputs.tag }}.iso - dist/oscortex-${{ steps.ver.outputs.tag }}.iso.sha256 + dist/oscortex-x86_64-${{ steps.ver.outputs.tag }}.iso + dist/oscortex-x86_64-${{ steps.ver.outputs.tag }}.iso.sha256 + dist/oscortex-aarch64-${{ steps.ver.outputs.tag }}.iso + dist/oscortex-aarch64-${{ steps.ver.outputs.tag }}.iso.sha256 dist/oscortex-aarch64-${{ steps.ver.outputs.tag }}.kernel dist/oscortex-aarch64-${{ steps.ver.outputs.tag }}.kernel.sha256 diff --git a/iso_root_aarch64/EFI/BOOT/BOOTAA64.EFI b/iso_root_aarch64/EFI/BOOT/BOOTAA64.EFI new file mode 100644 index 0000000..26dad09 Binary files /dev/null and b/iso_root_aarch64/EFI/BOOT/BOOTAA64.EFI differ diff --git a/iso_root_aarch64/EFI/BOOT/limine.conf b/iso_root_aarch64/EFI/BOOT/limine.conf new file mode 100644 index 0000000..c2edc69 --- /dev/null +++ b/iso_root_aarch64/EFI/BOOT/limine.conf @@ -0,0 +1,9 @@ +# 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 new file mode 100644 index 0000000..3ab0c48 Binary files /dev/null and b/iso_root_aarch64/boot/kernel differ diff --git a/iso_root_aarch64/boot/limine/limine-uefi-cd.bin b/iso_root_aarch64/boot/limine/limine-uefi-cd.bin new file mode 100644 index 0000000..eb6d61e Binary files /dev/null and b/iso_root_aarch64/boot/limine/limine-uefi-cd.bin differ diff --git a/iso_root_aarch64/boot/limine/limine.conf b/iso_root_aarch64/boot/limine/limine.conf new file mode 100644 index 0000000..c2edc69 --- /dev/null +++ b/iso_root_aarch64/boot/limine/limine.conf @@ -0,0 +1,9 @@ +# 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 new file mode 100644 index 0000000..c2edc69 --- /dev/null +++ b/iso_root_aarch64/limine.conf @@ -0,0 +1,9 @@ +# 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 7c55429..bc8b1a3 100644 --- a/kernel/Cargo.toml +++ b/kernel/Cargo.toml @@ -39,6 +39,10 @@ default = ["arch-x86_64"] arch-x86_64 = [] arch-aarch64 = [] arch-riscv64 = [] +# Boot the aarch64 kernel via the Limine UEFI protocol (higher-half, MMU/HHDM/ +# framebuffer set up by the bootloader) instead of the bare `-kernel`/DTB path. +# Selects the higher-half linker script + the Limine entry trampoline. +limine-boot = [] ai-cortex = [] driver-wasm = [] self-healing = [] diff --git a/kernel/build.rs b/kernel/build.rs index 7448939..dc70b9b 100644 --- a/kernel/build.rs +++ b/kernel/build.rs @@ -3,7 +3,15 @@ fn main() { let arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default(); let out = std::env::var("OUT_DIR").unwrap(); + // The aarch64 port has TWO mutually-exclusive boot layouts: + // * `-kernel`/DTB boot → physical link @ 0x40080000, entry `_start`, MMU off + // (aarch64.ld, the default). + // * Limine UEFI boot → higher-half link @ -2 GiB, entry the Limine + // trampoline, MMU/HHDM/framebuffer already set up by the bootloader + // (aarch64-limine.ld), selected by the `limine-boot` feature. + let limine_boot = std::env::var("CARGO_FEATURE_LIMINE_BOOT").is_ok(); let ld = match arch.as_str() { + "aarch64" if limine_boot => "aarch64-limine.ld", "aarch64" => "aarch64.ld", "riscv64" => "riscv64.ld", _ => "x86_64.ld", // default / x86_64 @@ -15,6 +23,7 @@ fn main() { println!("cargo:rerun-if-changed=linker/{ld}"); println!("cargo:rerun-if-changed=linker/x86_64.ld"); println!("cargo:rerun-if-changed=linker/aarch64.ld"); + println!("cargo:rerun-if-changed=linker/aarch64-limine.ld"); println!("cargo:rerun-if-changed=linker/riscv64.ld"); println!("cargo:rerun-if-changed=build.rs"); diff --git a/kernel/linker/aarch64-limine.ld b/kernel/linker/aarch64-limine.ld new file mode 100644 index 0000000..ff102c8 --- /dev/null +++ b/kernel/linker/aarch64-limine.ld @@ -0,0 +1,72 @@ +/* OSCortex aarch64 linker script — Limine higher-half kernel (UEFI boot). + * + * Unlike the bare `-kernel` path (aarch64.ld, linked to physical 0x40080000 with + * the MMU off and entry `_start`), the Limine boot protocol hands the kernel a + * machine that already has the MMU on, a higher-half mapping of the kernel, an + * HHDM direct-map of physical RAM, and a framebuffer. Limine loads each PT_LOAD + * segment at its higher-half virtual link address and jumps to the ELF entry + * point at EL1. + * + * We therefore link this variant to the conventional Limine higher-half base + * (-2 GiB) and set the entry to the aarch64 Limine entry trampoline, mirroring + * the x86_64 Limine kernel layout. The Limine request statics are discovered by + * the bootloader by scanning the loaded image for their magic numbers. + */ +OUTPUT_FORMAT("elf64-littleaarch64") +OUTPUT_ARCH(aarch64) +ENTRY(_limine_aarch64_entry) + +PHDRS { + text PT_LOAD FLAGS(5); /* R-X */ + rodata PT_LOAD FLAGS(4); /* R-- */ + data PT_LOAD FLAGS(6); /* RW- */ +} + +SECTIONS { + . = 0xFFFFFFFF80000000; + + .text : ALIGN(4096) { + __text_start = .; + KEEP(*(.text.limine_entry)) /* entry trampoline first */ + *(.text .text.*) + __text_end = .; + } :text + + . = ALIGN(4096); + + .rodata : ALIGN(4096) { + __rodata_start = .; + *(.rodata .rodata.*) + __rodata_end = .; + } :rodata + + /* Limine requests (scanned by the bootloader for their magic numbers). */ + .limine_reqs : { + KEEP(*(.limine_requests*)) + KEEP(*(.limine_requests_start*)) + KEEP(*(.limine_requests_end*)) + } :rodata + + . = ALIGN(4096); + + .data : ALIGN(4096) { + __data_start = .; + *(.data .data.*) + *(.got .got.*) + __data_end = .; + } :data + + . = ALIGN(4096); + .bss (NOLOAD) : ALIGN(4096) { + __bss_start = .; + *(.bss .bss.*) + *(COMMON) + . = ALIGN(16); + __bss_end = .; + } :data + + . = ALIGN(4096); + __kernel_end = .; + + /DISCARD/ : { *(.eh_frame) *(.eh_frame_hdr) *(.note .note.*) *(.comment) } +} diff --git a/kernel/src/arch/aarch64/boot_limine.rs b/kernel/src/arch/aarch64/boot_limine.rs new file mode 100644 index 0000000..fbf1ce2 --- /dev/null +++ b/kernel/src/arch/aarch64/boot_limine.rs @@ -0,0 +1,248 @@ +//! aarch64 Limine boot path — UEFI / UTM boot via the Limine protocol. +//! +//! Unlike the bare `-kernel`/DTB path (boot.rs + boot_prod.rs), here the Limine +//! bootloader has already: +//! * loaded the kernel at its higher-half link address (TTBR1, MMU on), +//! * set up an HHDM direct map of physical RAM, +//! * probed a framebuffer, +//! * answered our request statics (memory map, kernel address, framebuffer). +//! +//! Limine does NOT bring up the interrupt controller, the generic timer, or the +//! EL1 exception vectors — that is still our job, and we reuse the exact same +//! aarch64 code the `-kernel` path uses (gic / timer / vectors). +//! +//! MMU strategy (see `mmu::limine_setup_ttbr0`): we keep Limine's TTBR1 (the +//! running kernel high-half) untouched and install our OWN low-half (TTBR0) +//! identity map — device MMIO + RAM, T0SZ = 25, L1 start. That makes the whole +//! rest of the port (raw-PA MMIO, the per-process TTBR0 paging, enter_user) +//! behave byte-for-byte like the `-kernel` path with HHDM offset 0. +//! +//! This module is compiled only with the `limine-boot` feature (which also +//! selects the higher-half linker script). The `-kernel` path is then absent. + +use crate::arch::aarch64::uart; +use crate::arch::aarch64::uart::a64println; + +// ── Limine request statics (aarch64) ───────────────────────────────────────── +// +// The bootloader scans the loaded image for these magic numbers and fills in the +// responses before jumping to our entry. We declare the same set x86 uses (minus +// the x2APIC-specific MP flag): base revision, HHDM, memory map, framebuffer, +// kernel address, modules, and an explicit 4-level paging request. +use limine::request::{ + FramebufferRequest, HhdmRequest, ExecutableAddressRequest, MemmapRequest, ModulesRequest, + PagingModeRequest, +}; +use limine::paging::PagingMode; +use limine::BaseRevision; + +#[used] +static BASE_REVISION: BaseRevision = BaseRevision::new(); + +#[used] +static FB_REQUEST: FramebufferRequest = FramebufferRequest::new(); +#[used] +static HHDM_REQUEST: HhdmRequest = HhdmRequest::new(); +#[used] +static MMAP_REQUEST: MemmapRequest = MemmapRequest::new(); +#[used] +static KADDR_REQUEST: ExecutableAddressRequest = ExecutableAddressRequest::new(); +#[used] +static MODULES_REQUEST: ModulesRequest = ModulesRequest::new(); +// Pin a 4-level (48-bit VA) translation regime so the higher-half kernel mapping +// is deterministic (request exactly AARCH64_4LVL: min == mode == max). +#[used] +static PAGING_REQUEST: PagingModeRequest = PagingModeRequest::new( + PagingMode::AARCH64_4LVL, + PagingMode::AARCH64_4LVL, + PagingMode::AARCH64_4LVL, +); + +// ── Boot stack (BSS-resident, 64 KiB) ──────────────────────────────────────── +core::arch::global_asm!( + r#" +.section .bss.limine_boot_stack, "aw", @nobits +.balign 16 +LIMINE_BOOT_STACK_BOTTOM: + .skip 65536 +.globl LIMINE_BOOT_STACK_TOP +LIMINE_BOOT_STACK_TOP: +"# +); + +extern "C" { + static LIMINE_BOOT_STACK_TOP: u8; +} + +// ── Entry trampoline ───────────────────────────────────────────────────────── +// +// Limine jumps to the ELF entry point (`_limine_aarch64_entry`, first in .text) +// at EL1 (or EL2, which it normally drops for us), MMU on, on a bootloader stack. +// We enable FP/SIMD (Rust codegen emits SIMD even in early bring-up), drop EL2→EL1 +// if needed, switch to our own BSS boot stack, then branch into Rust. The kernel +// runs from its higher-half link address, which Limine has mapped via TTBR1. +core::arch::global_asm!( + r#" +.section .text.limine_entry, "ax" +.globl _limine_aarch64_entry +_limine_aarch64_entry: + // If we somehow entered at EL2, drop to EL1h with a sane SPSR. + mrs x0, CurrentEL + lsr x0, x0, #2 + cmp x0, #2 + b.ne 1f + mov x0, #(1 << 31) // HCR_EL2.RW = 1 → EL1 is AArch64 + msr hcr_el2, x0 + mov x0, #0x3c5 // D,A,I,F masked + EL1h + msr spsr_el2, x0 + adr x0, 1f + msr elr_el2, x0 + eret +1: + // Mask interrupts during bring-up. + msr daifset, #0xf + // Enable FP/AdvSIMD at EL1/EL0 (CPACR_EL1.FPEN = 0b11). + mrs x0, cpacr_el1 + orr x0, x0, #(3 << 20) + msr cpacr_el1, x0 + isb + // Force EL1h (SPSel=1): the kernel must run on SP_EL1. Limine enters us at + // EL1t (SPSel=0), where `sp` aliases SP_EL0 — every kernel stack write then + // goes through SP_EL0, and enter_user's kernel-stack reset (`mov sp, ...`) + // clobbers the user SP it just delivered: pid1 entered EL0 with SP = the + // kernel syscall-stack top and its first push faulted into kernel RAM. + msr spsel, #1 + // Switch to our own boot stack (Limine's stack is reclaimable). + adrp x0, {boot_stack_top} + add x0, x0, :lo12:{boot_stack_top} + mov sp, x0 + bl {entry} +2: wfe + b 2b +"#, + boot_stack_top = sym LIMINE_BOOT_STACK_TOP, + entry = sym limine_kernel_entry, +); + +/// First Rust function on the Limine aarch64 path. Runs at EL1 with Limine's MMU +/// still active (kernel higher-half via TTBR1). Brings the ARM platform up onto +/// our own low-half identity map, then joins the shared kernel. +#[no_mangle] +pub extern "C" fn limine_kernel_entry() -> ! { + // ── 0. Kernel virt→phys (from Limine) so we can program TTBR0 ──────────── + // The page-table walker addresses tables by PHYSICAL address; our L1 table is + // a kernel static at a higher-half VA. Limine's executable-address response + // gives us the (physical_base, virtual_base) pair to translate. + let kaddr = KADDR_REQUEST + .response() + .expect("limine: no executable-address response"); + let phys_base = kaddr.physical_base; + let virt_base = kaddr.virtual_base; + let virt_to_phys = move |va: u64| -> u64 { phys_base + (va - virt_base) }; + + // ── 1. Install our low-half (TTBR0) identity map: devices + 2 GiB RAM ──── + // Keeps Limine's TTBR1 (the running higher-half kernel) intact. After this, + // raw-PA device MMIO (UART/GIC) and the per-process TTBR0 paging work exactly + // as on the `-kernel` path, with HHDM offset 0 in the low half. + unsafe { crate::arch::aarch64::mmu::limine_setup_ttbr0(2, virt_to_phys) }; + + // ── 2. Serial: the debug lifeline (PL011 @ 0x0900_0000, now in our map) ── + uart::init(); + a64println!(); + a64println!("========================================"); + a64println!(" OSCortex aarch64 — LIMINE boot"); + a64println!("========================================"); + uart::puts("[limine-boot] kernel phys_base = "); + uart::puthex_full(phys_base); + uart::puts(" virt_base = "); + uart::puthex_full(virt_base); + uart::puts("\n"); + a64println!("[limine-boot] low-half TTBR0 identity map installed (T0SZ=25)"); + + // ── 3. EL1 exception vectors (VBAR_EL1): SVC syscalls + faults ─────────── + a64println!("[limine-boot] installing EL1 exception vectors..."); + crate::arch::aarch64::vectors::install(); + + // ── 4. GIC: distributor + this core's CPU interface ────────────────────── + a64println!("[limine-boot] bringing up GIC (distributor + CPU interface)..."); + crate::arch::aarch64::gic::init_distributor(); + crate::arch::aarch64::gic::init_cpu_interface(); + + // ── 5. Memory map from Limine → neutral BootMemMap ─────────────────────── + use limine::memmap::MEMMAP_USABLE; + let mmap = MMAP_REQUEST + .response() + .expect("limine: no memory-map response"); + let mut map = crate::mm::BootMemMap::new(); + let mut total: u64 = 0; + for entry in mmap.entries() { + if entry.type_ == MEMMAP_USABLE { + map.push(entry.base, entry.length); + total += entry.length; + } + } + a64println!( + "[limine-boot] Limine memory map: {} MiB usable RAM", + total / (1024 * 1024) + ); + + // HHDM offset is 0 for our LOW-half identity map (VA == PA there). The kernel + // code/heap live in TTBR1 (higher-half) but every PA the frame allocator / + // paging layer dereferences (page tables, user frames) is in the low-half + // identity map, so a zero phys→virt offset is correct for that machinery. + let hhdm_offset: u64 = 0; + + // ── 6. Hand off to the shared kernel (heap, scheduler, VFS, process, …) ── + crate::kernel_main_arch_limine(map, hhdm_offset) +} + +/// Scan Limine modules for the Flutter engine .so and stash it (mirrors x86). +pub fn scan_modules() { + if let Some(mods) = MODULES_REQUEST.response() { + for module in mods.modules() { + if module.cmdline().contains("libflutter_engine") + || module.path().contains("libflutter_engine") + { + let data = module.data(); + *crate::FLUTTER_ENGINE_BYTES.lock() = Some(data); + log::info!( + "[limine-boot] module: libflutter_engine.so ({} bytes)", + data.len() + ); + } + } + } else { + log::warn!("[limine-boot] no Limine modules response"); + } +} + +/// Bring up the framebuffer from Limine's framebuffer response and publish it to +/// the shared fb console (same hook ramfb uses on the `-kernel` path). +pub fn init_framebuffer() { + let Some(resp) = FB_REQUEST.response() else { + log::warn!("[limine-boot] no framebuffer response — serial-only"); + return; + }; + let fbs = resp.framebuffers(); + let Some(fb) = fbs.first() else { + log::warn!("[limine-boot] framebuffer response but 0 framebuffers"); + return; + }; + let addr = fb.address() as u64; + let w = fb.width as u32; + let h = fb.height as u32; + let pitch = fb.pitch as u32; + log::info!( + "[limine-boot] framebuffer {}x{} bpp={} pitch={} addr={:#x}", + w, h, fb.bpp, pitch, addr + ); + // The framebuffer physical address is covered by our low-half identity map + // (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(); + log::info!("[limine-boot] framebuffer online: {}x{} via Limine", w, h); +} diff --git a/kernel/src/arch/aarch64/enter_user.rs b/kernel/src/arch/aarch64/enter_user.rs index 638d803..96c149b 100644 --- a/kernel/src/arch/aarch64/enter_user.rs +++ b/kernel/src/arch/aarch64/enter_user.rs @@ -118,6 +118,14 @@ fn build_image(regs: &EnterUserRegs) -> [u64; 34] { unsafe fn eret_to_el0(img: &[u64; 34]) -> ! { let reset_sp = crate::arch::aarch64::syscall::active_el1_sp(); core::arch::asm!( + // Mask ALL interrupts at EL1 for the duration of the resume. The SP reset + // below abandons the frame that still holds `img`; if a wait loop yielded + // here with IRQs unmasked, a timer IRQ would push a TrapFrame onto the + // reset stack — overlapping `img` — and we would read a corrupted resume + // image (observed: x18/SPSR load faulting in eret_to_el0_fp, ~25% of long + // runs). `eret` restores SPSR_EL0T (I/F clear) so EL0 runs with interrupts + // enabled — this masking has no lasting effect. + "msr daifset, #0xf", // Reclaim the EL1 kernel stack. This is a cooperative-yield resume: we // eret straight to EL0 WITHOUT RESTORE_FRAME, so the SVC's EL1 exception // frame is abandoned. Reset SP_EL1 to the thread's syscall-stack top so it @@ -180,6 +188,10 @@ unsafe fn eret_to_el0(img: &[u64; 34]) -> ! { unsafe fn eret_to_el0_fp(img: &[u64; 34], fp: &[u64; 66]) -> ! { let reset_sp = crate::arch::aarch64::syscall::active_el1_sp(); core::arch::asm!( + // Mask ALL interrupts at EL1 (see eret_to_el0): the SP reset abandons the + // frame holding `img`/`fp`, so a timer IRQ here would corrupt the resume + // image. `eret` restores SPSR_EL0T (interrupts enabled) for EL0. + "msr daifset, #0xf", // Reclaim the abandoned EL1 frame (cooperative-yield resume) — see // eret_to_el0. img/fp live on the old stack; their memory stays valid // until the next EL1 entry pushes. diff --git a/kernel/src/arch/aarch64/mmu.rs b/kernel/src/arch/aarch64/mmu.rs index 9b6d234..930e6a6 100644 --- a/kernel/src/arch/aarch64/mmu.rs +++ b/kernel/src/arch/aarch64/mmu.rs @@ -172,6 +172,63 @@ pub fn is_on() -> bool { MMU_ON.load(Ordering::Acquire) } +// ── Limine boot path: install our own low-half (TTBR0) identity map ────────── +// +// Under Limine the MMU is ALREADY on: the bootloader maps the kernel higher-half +// (TTBR1), an HHDM direct map, and a framebuffer. We do NOT touch TTBR1 (it holds +// the running kernel's code/data at its higher-half link address). Instead we +// install our OWN low-half identity map into TTBR0 — device MMIO @ 0-1 GiB + +// `ram_gibs` GiB of RAM from 0x4000_0000 — and set TCR_EL1.T0SZ = 25 so the +// TTBR0 walk starts at L1 (39-bit VA). +// +// This makes the ENTIRE rest of the aarch64 port (uart/gic raw-PA MMIO, the +// per-process L1-start TTBR0 paging in mm::paging, the `alloc_user_pml4` kernel- +// clone, enter_user) behave EXACTLY as on the bare `-kernel` path — VA == PA in +// the low half, HHDM offset 0 — with zero changes to that code. The only +// difference from `-kernel` is that the kernel's own code lives in TTBR1 (mapped +// by Limine) rather than in this TTBR0 identity map. +// +// `table_phys` translates a kernel static's higher-half VA to the physical +// address we must program into TTBR0 (the MMU walks tables by physical address). +#[cfg(feature = "limine-boot")] +pub unsafe fn limine_setup_ttbr0(ram_gibs: u64, virt_to_phys: impl Fn(u64) -> u64) { + let l1_low = &mut *core::ptr::addr_of_mut!(L1_LOW); + fill_identity(l1_low, ram_gibs); + + // The L1 table lives in the kernel image (BSS, higher-half). The MMU needs its + // PHYSICAL address. Likewise every descriptor's output address is already + // physical (fill_identity uses raw PAs). Flush the table out of the dcache so + // the page-table walker (which may bypass the cache) sees it. + let l1_va = core::ptr::addr_of!(L1_LOW) as u64; + let l1_pa = virt_to_phys(l1_va); + + // Adjust ONLY TTBR0's T0SZ to 25 (39-bit VA, L1 start) — TTBR1 (T1SZ, the + // kernel high half) is left as Limine programmed it. Read-modify-write so we + // preserve Limine's granule/cacheability/IPS bits for both halves. + let mut tcr: u64; + core::arch::asm!("mrs {}, tcr_el1", out(reg) tcr, options(nomem, nostack)); + tcr &= !0x3F; // clear T0SZ[5:0] + tcr |= TCR_T0SZ; // 25 + // Force a 4 KiB TTBR0 granule (TG0 = 0b00) so the L1-start math matches. + tcr &= !(0b11 << 14); + tcr |= TCR_TG0_4K; + + core::arch::asm!( + "dsb ish", + "msr tcr_el1, {tcr}", + "msr ttbr0_el1, {ttbr0}", + "isb", + "tlbi vmalle1is", + "dsb ish", + "isb", + tcr = in(reg) tcr, + ttbr0 = in(reg) l1_pa, + options(nostack, preserves_flags), + ); + + MMU_ON.store(true, Ordering::Release); +} + // ── EL0 user mapping (bring-up) ────────────────────────────────────────────── /// Table descriptor: valid + table type (points at the next-level table). diff --git a/kernel/src/arch/aarch64/mod.rs b/kernel/src/arch/aarch64/mod.rs index 051d81a..77d0110 100644 --- a/kernel/src/arch/aarch64/mod.rs +++ b/kernel/src/arch/aarch64/mod.rs @@ -29,8 +29,15 @@ pub mod enter_user; pub mod uart; pub mod fdt; pub mod ramfb; +// The bare `-kernel`/DTB entry path (its own `_start` trampoline + DTB-driven +// boot) and the Limine entry path are mutually exclusive: only one provides the +// ELF entry point + the boot prologue. `limine-boot` selects the Limine path. +#[cfg(not(feature = "limine-boot"))] pub mod boot; +#[cfg(not(feature = "limine-boot"))] pub mod boot_prod; +#[cfg(feature = "limine-boot")] +pub mod boot_limine; pub mod mmu; pub mod vectors; pub mod gic; diff --git a/kernel/src/arch/aarch64/timer.rs b/kernel/src/arch/aarch64/timer.rs index a8144d9..2d9b1a7 100644 --- a/kernel/src/arch/aarch64/timer.rs +++ b/kernel/src/arch/aarch64/timer.rs @@ -1,13 +1,22 @@ -//! ARM generic timer — periodic EL1 physical-timer tick (CNTP_*). +//! ARM generic timer — periodic EL1 VIRTUAL-timer tick (CNTV_*). //! //! Drives the same scheduler tick the x86 APIC timer drives. We use the EL1 -//! physical timer (CNTP_CTL_EL0 / CNTP_TVAL_EL0); on QEMU `-M virt` its -//! interrupt is PPI 30. CNTFRQ_EL0 gives the tick frequency (62.5 MHz on virt). +//! VIRTUAL timer (CNTV_CTL_EL0 / CNTV_TVAL_EL0 / CNTVCT_EL0), PPI 27. +//! +//! Why virtual, not physical: OSCortex runs the kernel at EL1. Under a +//! hypervisor (Apple's HVF — what UTM uses — KVM, Xen, …) EL2 owns the PHYSICAL +//! timer (CNTP, PPI 30): EL1 accesses are trapped and its interrupt is NOT +//! delivered to the guest, so a CNTP-based tick simply never fires under HVF → +//! no vsync baton → frames schedule but never present (the engine reaches the +//! event loop and stalls). The VIRTUAL timer is the architected EL1-guest timer +//! and is delivered on HVF, on bare metal (CNTVOFF=0 → virtual time == physical +//! time), AND on QEMU `-M virt` TCG — so it is strictly more portable. CNTFRQ_EL0 +//! gives the tick frequency (62.5 MHz on TCG virt, ~24 MHz on Apple Silicon). use core::sync::atomic::{AtomicU64, Ordering}; -/// EL1 physical-timer PPI on QEMU `-M virt`. -pub const TIMER_PPI: u32 = 30; +/// EL1 VIRTUAL-timer PPI (architected; delivered under HVF and on bare metal). +pub const TIMER_PPI: u32 = 27; /// Timer frequency in Hz (read from CNTFRQ_EL0 at init). static FREQ_HZ: AtomicU64 = AtomicU64::new(0); @@ -23,11 +32,12 @@ fn cntfrq() -> u64 { v } -/// Current physical count. +/// Current virtual count (CNTVCT_EL0 — matches the virtual timer we tick on, +/// and the same counter vsync cadence is measured against). #[inline] pub fn count() -> u64 { let v: u64; - unsafe { core::arch::asm!("mrs {}, cntpct_el0", out(reg) v, options(nomem, nostack)) }; + unsafe { core::arch::asm!("mrs {}, cntvct_el0", out(reg) v, options(nomem, nostack)) }; v } @@ -46,8 +56,8 @@ pub fn init_periodic(hz: u64) { arm_next(interval); unsafe { - // CNTP_CTL_EL0: ENABLE=1, IMASK=0 (bit0=enable, bit1=imask). - core::arch::asm!("msr cntp_ctl_el0, {}", in(reg) 1u64, options(nomem, nostack)); + // CNTV_CTL_EL0: ENABLE=1, IMASK=0 (bit0=enable, bit1=imask). + core::arch::asm!("msr cntv_ctl_el0, {}", in(reg) 1u64, options(nomem, nostack)); core::arch::asm!("isb", options(nomem, nostack)); } } @@ -56,7 +66,7 @@ pub fn init_periodic(hz: u64) { fn arm_next(interval: u64) { unsafe { // TVAL counts down from `interval`; the interrupt fires at zero. - core::arch::asm!("msr cntp_tval_el0, {}", in(reg) interval, options(nomem, nostack)); + core::arch::asm!("msr cntv_tval_el0, {}", in(reg) interval, options(nomem, nostack)); } } diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 4e39f7b..0c7ec21 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -276,6 +276,46 @@ pub fn kernel_main_arch(map: mm::BootMemMap, hhdm_offset: u64) -> ! { shared_init_and_run() } +/// aarch64 Limine kernel entry (UEFI / UTM boot). +/// +/// Called from `arch::aarch64::boot_limine::limine_kernel_entry` after the Limine +/// path has installed our low-half TTBR0 identity map, brought up serial, the EL1 +/// vectors, and the GIC. Mirrors `kernel_main_arch` but takes its memory map from +/// the Limine memory map (already translated into the neutral `BootMemMap` by the +/// caller) and its framebuffer + engine module from Limine instead of ramfb. +#[cfg(all(target_arch = "aarch64", feature = "limine-boot"))] +pub fn kernel_main_arch_limine(map: mm::BootMemMap, hhdm_offset: u64) -> ! { + logger::early_print("[OSCORTEX] kernel_main_arch_limine reached (aarch64/limine)\r\n"); + + mm::frame_allocator::set_hhdm_offset(hhdm_offset); + + // ── 1. Architecture early init (FPU/SIMD, SVC readiness) ───────────────── + // Vectors + GIC are already live (set up in boot_limine). + arch::early_init(); + logger::early_print("[OSCORTEX] arch::early_init done (aarch64/limine)\r\n"); + + // ── 2. Memory management from the Limine memory map ────────────────────── + mm::init_from_regions(&map, hhdm_offset); + + // ── 3. Logger (serial only here; the Limine framebuffer comes up next) ── + logger::init(None); + + log::warn!( + "[MM::FrameAlloc] capacity at boot: total_usable_frames={} ({} MiB), used_at_boot={} ({} MiB)", + mm::frame_allocator::frames_total(), + (mm::frame_allocator::frames_total() * 4096) / (1024 * 1024), + mm::frame_allocator::frames_used(), + (mm::frame_allocator::frames_used() * 4096) / (1024 * 1024) + ); + log::info!("OSCortex kernel {} booting (aarch64/limine)...", env!("CARGO_PKG_VERSION")); + + // ── 3b. Framebuffer + engine module via Limine ─────────────────────────── + arch::aarch64::boot_limine::init_framebuffer(); + arch::aarch64::boot_limine::scan_modules(); + + shared_init_and_run() +} + /// Architecture-neutral subsystem init, init-process spawn, and idle loop. /// /// Runs after each arch's boot prologue has brought up serial, the memory diff --git a/kernel/src/mm/mod.rs b/kernel/src/mm/mod.rs index 9c0ef18..d80813b 100644 --- a/kernel/src/mm/mod.rs +++ b/kernel/src/mm/mod.rs @@ -107,7 +107,13 @@ pub fn init_from_regions(map: &BootMemMap, hhdm_offset: u64) { // latent corruption that surfaces as a hang on the first large heap alloc. // Reserve [RAM base .. __kernel_end] before the heap is carved out. // (x86 gets this for free from the Limine memory-map types.) - #[cfg(target_arch = "aarch64")] + // Only the bare `-kernel`/DTB path needs this manual reserve: there the DTB + // reports the whole RAM span (incl. the loaded kernel image) as usable, so we + // must carve the image out by hand. On the Limine path the bootloader's memory + // map already marks the kernel + bootloader regions non-usable (they are never + // pushed into the BootMemMap), and `__kernel_end` is a higher-half VA — using + // it here would reserve the wrong physical range. So gate it out under Limine. + #[cfg(all(target_arch = "aarch64", not(feature = "limine-boot")))] { extern "C" { static __kernel_end: u8; diff --git a/kernel/src/process/mod.rs b/kernel/src/process/mod.rs index 33c2fde..5e6e83a 100644 --- a/kernel/src/process/mod.rs +++ b/kernel/src/process/mod.rs @@ -762,7 +762,13 @@ pub fn spawn_with_bootstrap( let idx = idx_of(pid); let mut regs = UserRegs::default(); regs.rip = entry; - regs.rsp = USER_STACK_TOP - 8; // leave one guard word + // AArch64 requires a 16-byte-aligned SP (enforced by SCTLR_EL1.SA on real + // hardware); x86 just wants a guard word. USER_STACK_TOP is page-aligned, so + // use it directly on aarch64 and keep the -8 guard word on x86. + #[cfg(target_arch = "aarch64")] + { regs.rsp = USER_STACK_TOP & !0xF; } + #[cfg(not(target_arch = "aarch64"))] + { regs.rsp = USER_STACK_TOP - 8; } // leave one guard word regs.rdi = bootstrap.rdi; regs.rsi = bootstrap.rsi; regs.rdx = bootstrap.rdx; @@ -1404,8 +1410,20 @@ pub fn spawn_thread( let mut regs = UserRegs::default(); regs.rip = entry_fn; regs.rdi = arg; // first argument via SysV ABI - regs.rsp = stack_top - 8; // RSP%16==8 at entry, per SysV ABI; the word - // at [RSP] is the thread-return trampoline. + // Thread-entry stack pointer. The two arches have OPPOSITE alignment rules: + // x86 SysV: at a function's first instruction RSP%16==8 (the CALL pushed an + // 8-byte return address), and the word at [RSP] is the return target — so + // we seed `stack_top - 8` with the thread-return trampoline (above). + // AArch64: SP must be 16-BYTE ALIGNED at all times (SCTLR_EL1.SA enforces it + // on real hardware — HVF / bare metal / a Raspberry Pi), and the return + // address lives in x30/LR, not on the stack (seeded via p.user_lr below). + // `stack_top - 8` gave an 8-aligned SP that TCG tolerated but real silicon + // faults on (EC=0x26 SP-alignment) the instant the new thread touches its + // stack — which is why nothing rendered under HVF/UTM. Align down to 16. + #[cfg(target_arch = "aarch64")] + { regs.rsp = stack_top & !0xF; } + #[cfg(not(target_arch = "aarch64"))] + { regs.rsp = stack_top - 8; } regs.rflags = 0x0202; // IF=1, reserved=1 // ── Thread TLS block ────────────────────────────────────────────────────── @@ -1733,7 +1751,9 @@ pub fn enter_user_by_pid_noreturn(pid: u32) -> ! { // kernel's leftover FP garbage — otherwise the new thread's AOT code reads an // uninitialized v-register → garbage value → bad Dart string ptr → "Invalid UTF8" // → crash (nondeterministic, early). Always restore an FP image; zeroed if none. + #[cfg(target_arch = "aarch64")] let fp_buf = aarch64_take_fp(pid).unwrap_or([0u64; 66]); + #[cfg(target_arch = "aarch64")] let fp_img: Option<&[u64; 66]> = Some(&fp_buf); if preempted_by_timer { @@ -1887,7 +1907,9 @@ pub fn enter_user_by_pid_noreturn_try(pid: u32) -> bool { // kernel's leftover FP garbage — otherwise the new thread's AOT code reads an // uninitialized v-register → garbage value → bad Dart string ptr → "Invalid UTF8" // → crash (nondeterministic, early). Always restore an FP image; zeroed if none. + #[cfg(target_arch = "aarch64")] let fp_buf = aarch64_take_fp(pid).unwrap_or([0u64; 66]); + #[cfg(target_arch = "aarch64")] let fp_img: Option<&[u64; 66]> = Some(&fp_buf); if preempted_by_timer { diff --git a/kernel/src/syscall/poll.rs b/kernel/src/syscall/poll.rs index b343988..62d95d5 100644 --- a/kernel/src/syscall/poll.rs +++ b/kernel/src/syscall/poll.rs @@ -469,12 +469,26 @@ fn finish_cond_timedout_return(pid: u32, mutex: u64) { let rip = p.regs.rip; let rsp = p.regs.rsp; let fs = crate::arch::cpu::get_fs_base(); - p.regs.rip = rip.wrapping_add(2); + // `rip` points AT the syscall instruction (re-exec mode set by + // save_return_context_reexec when the waiter parked). To make it RETURN + // ETIMEDOUT instead of re-waiting, advance past the syscall instruction: + // 2 bytes on x86 (`syscall`), 4 bytes on aarch64 (`svc #0`). The old + // hardcoded `+2` was an x86-ism that, on aarch64, landed in the middle of + // the 4-byte `svc` → a misaligned resume PC (svc+2) → EC=0x22 PC-alignment + // fault (the deterministic ~280-frame crash on long aarch64 runs). + p.regs.rip = rip.wrapping_add(crate::process::SYSCALL_INSN_LEN); p.regs.rsp = rsp; p.regs.rflags = 0x202; p.preempted_by_timer = false; p.fs_base = fs; p.regs.rax = 110; // ETIMEDOUT + // aarch64: a syscall returns its result in x0, but build_image only maps + // rax→x0 when aarch64_ret_in_x0 is set. The re-exec park cleared it; set it + // so the waiter actually sees ETIMEDOUT (not a stale arg0) on resume. + #[cfg(target_arch = "aarch64")] + { + p.aarch64_ret_in_x0 = true; + } } drop(g_ptable); diff --git a/scripts/build-iso-aarch64.sh b/scripts/build-iso-aarch64.sh new file mode 100755 index 0000000..db76734 --- /dev/null +++ b/scripts/build-iso-aarch64.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +# build-iso-aarch64.sh — build a bootable aarch64 OSCortex ISO that boots via the +# Limine UEFI protocol (so it runs in UTM / on UEFI ARM machines), then optionally +# boot it under qemu-system-aarch64 with the edk2 AAVMF ARM UEFI firmware. +# +# This mirrors scripts/build-iso.sh's Limine ISO assembly, but for aarch64: +# * kernel built with the `limine-boot` feature (higher-half link, Limine entry), +# * 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. +# +# Usage: +# scripts/build-iso-aarch64.sh # build the ISO +# scripts/build-iso-aarch64.sh --run # build + boot under QEMU UEFI (headless) +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +# Limine asset dir (limine-uefi-cd.bin, BOOTAA64.EFI). Defaults to the Homebrew +# location for local macOS builds; override with LIMINE_DIR=... on CI (the GitHub +# runner clones limine to $HOME/limine). +LIMINE_DIR="${LIMINE_DIR:-/opt/homebrew/share/limine}" +ISO_ROOT="$ROOT/iso_root_aarch64" +OUTPUT="$ROOT/oscortex-aarch64.iso" +TARGET="aarch64-unknown-none" +# edk2 AArch64 UEFI firmware shipped with Homebrew QEMU. +QEMU_SHARE="$(dirname "$(command -v qemu-system-aarch64)")/../share/qemu" +AAVMF_CODE="$QEMU_SHARE/edk2-aarch64-code.fd" + +echo "[1/4] Building aarch64 kernel ELF (Limine higher-half)..." +cd "$ROOT" +cargo build \ + --release \ + --package oscortex-kernel \ + --target "$TARGET" \ + --no-default-features \ + --features arch-aarch64,limine-boot \ + -Z build-std=core,compiler_builtins,alloc \ + -Z build-std-features=compiler-builtins-mem + +KERNEL_ELF="$ROOT/target/$TARGET/release/kernel" +if [ ! -f "$KERNEL_ELF" ]; then + echo "ERROR: kernel ELF not found: $KERNEL_ELF" >&2 + exit 1 +fi + +echo "[2/4] Staging ISO root..." +rm -rf "$ISO_ROOT" +mkdir -p "$ISO_ROOT/boot/limine" +mkdir -p "$ISO_ROOT/EFI/BOOT" + +cat "$KERNEL_ELF" > "$ISO_ROOT/boot/kernel" + +# Limine UEFI artifacts for aarch64: the CD EFI image + the AArch64 EFI app. +cp "$LIMINE_DIR/limine-uefi-cd.bin" "$ISO_ROOT/boot/limine/" +cp "$LIMINE_DIR/BOOTAA64.EFI" "$ISO_ROOT/EFI/BOOT/" + +cat > "$ISO_ROOT/boot/limine/limine.conf" << 'EOF' +# OSCortex aarch64 boot configuration (Limine UEFI) +timeout: 0 +serial: yes +verbose: yes + +/OSCortex AI-First Kernel + protocol: limine + path: boot():/boot/kernel + kaslr: no +EOF + +cp "$ISO_ROOT/boot/limine/limine.conf" "$ISO_ROOT/limine.conf" +cp "$ISO_ROOT/boot/limine/limine.conf" "$ISO_ROOT/EFI/BOOT/limine.conf" + +echo "[3/4] Building ISO with xorriso (UEFI-only, no BIOS on ARM)..." +xorriso -as mkisofs \ + --efi-boot boot/limine/limine-uefi-cd.bin \ + -efi-boot-part --efi-boot-image --protective-msdos-label \ + "$ISO_ROOT" -o "$OUTPUT" 2>/dev/null + +echo "[4/4] ISO built: $OUTPUT ($(du -sh "$OUTPUT" | cut -f1))" +echo "" +echo "Boot under QEMU UEFI (AArch64):" +echo " qemu-system-aarch64 -M virt -cpu cortex-a72 -m 2G -smp 1 \\" +echo " -bios $AAVMF_CODE \\" +echo " -cdrom $OUTPUT -device ramfb \\" +echo " -serial stdio -display none" +echo "" +echo "In UTM: create a new ARM64 VM, attach $OUTPUT as a CD/DVD, use the" +echo "edk2 AArch64 UEFI firmware (UTM default for ARM), boot." + +if [[ "${1:-}" == "--run" ]]; then + if [ ! -f "$AAVMF_CODE" ]; then + echo "ERROR: AArch64 UEFI firmware not found: $AAVMF_CODE" >&2 + exit 1 + fi + SERIAL_LOG="${SERIAL_LOG:-/tmp/limine-serial.log}" + echo "" + echo "Launching QEMU UEFI (headless, serial → $SERIAL_LOG)..." + qemu-system-aarch64 \ + -M virt -cpu cortex-a72 -m 2G -smp 1 \ + -bios "$AAVMF_CODE" \ + -cdrom "$OUTPUT" \ + -device ramfb \ + -serial "file:$SERIAL_LOG" \ + -display none \ + -no-reboot \ + 2>&1 & + QEMU_PID=$! + echo "QEMU PID $QEMU_PID — serial log: $SERIAL_LOG" + wait "$QEMU_PID" || true +fi diff --git a/tools/flutter-embedder/src/main.rs b/tools/flutter-embedder/src/main.rs index 4d828e7..7ed6a14 100644 --- a/tools/flutter-embedder/src/main.rs +++ b/tools/flutter-embedder/src/main.rs @@ -2531,6 +2531,7 @@ extern "C" fn main_embedder(host_mode: u64, app_id: u64, aot_va: u64) { ARG5.as_ptr(), ARG6.as_ptr(), ARG7.as_ptr(), + ARG8.as_ptr(), ]); let mut project_args = FlutterProjectArgsRaw {