diff --git a/Cargo.toml b/Cargo.toml index b1705842..6fe53b0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ test = false [workspace] members = [ "kernel", + "parallels-loader", "xtask" ] diff --git a/docs/parallels-hwdump/cpuinfo.txt b/docs/parallels-hwdump/cpuinfo.txt new file mode 100644 index 00000000..c181e7ef --- /dev/null +++ b/docs/parallels-hwdump/cpuinfo.txt @@ -0,0 +1,36 @@ +processor : 0 +BogoMIPS : 48.00 +Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 asimddp sha512 asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp flagm2 frint +CPU implementer : 0x41 +CPU architecture: 8 +CPU variant : 0x0 +CPU part : 0x000 +CPU revision : 0 + +processor : 1 +BogoMIPS : 48.00 +Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 asimddp sha512 asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp flagm2 frint +CPU implementer : 0x41 +CPU architecture: 8 +CPU variant : 0x0 +CPU part : 0x000 +CPU revision : 0 + +processor : 2 +BogoMIPS : 48.00 +Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 asimddp sha512 asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp flagm2 frint +CPU implementer : 0x41 +CPU architecture: 8 +CPU variant : 0x0 +CPU part : 0x000 +CPU revision : 0 + +processor : 3 +BogoMIPS : 48.00 +Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 asimddp sha512 asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp flagm2 frint +CPU implementer : 0x41 +CPU architecture: 8 +CPU variant : 0x0 +CPU part : 0x000 +CPU revision : 0 + diff --git a/docs/parallels-hwdump/guest.dtb b/docs/parallels-hwdump/guest.dtb new file mode 100644 index 00000000..710cdaee Binary files /dev/null and b/docs/parallels-hwdump/guest.dtb differ diff --git a/docs/parallels-hwdump/guest.dts b/docs/parallels-hwdump/guest.dts new file mode 100644 index 00000000..a9d952f2 --- /dev/null +++ b/docs/parallels-hwdump/guest.dts @@ -0,0 +1,15 @@ +/dts-v1/; + +/ { + #size-cells = <0x02>; + #address-cells = <0x02>; + + chosen { + linux,uefi-mmap-desc-ver = <0x01>; + linux,uefi-mmap-desc-size = <0x30>; + linux,uefi-mmap-size = <0x7b0>; + linux,uefi-mmap-start = <0x00 0xb884e9c0>; + linux,uefi-system-table = <0x00 0xbf920018>; + bootargs = "BOOT_IMAGE=/boot/vmlinuz-lts modules=loop,squashfs,sd-mod,usb-storage quiet"; + }; +}; diff --git a/docs/parallels-hwdump/interrupts.txt b/docs/parallels-hwdump/interrupts.txt new file mode 100644 index 00000000..a200731e --- /dev/null +++ b/docs/parallels-hwdump/interrupts.txt @@ -0,0 +1,26 @@ + CPU0 CPU1 CPU2 CPU3 + 10: 42455 18750 20295 13682 GICv3 27 Level arch_timer + 12: 1 0 0 0 GICv3 48 Edge ACPI:Ged + 14: 0 0 0 0 GICv3 38 Edge virtio0 + 15: 1900 0 0 0 GICv3 34 Level ahci[PRL4010:00] + 16: 0 0 0 0 GICv3 32 Level uart-pl011 + 17: 0 0 0 0 GICv3 23 Level arm-pmu + 19: 498 0 0 0 GICv2m-PCI-MSI-0000:00:03.0 0 Edge xhci_hcd + 23: 0 0 0 0 GICv3 35 Level ehci_hcd:usb3 + 24: 0 0 0 0 GICv2m-PCI-MSIX-0000:00:05.0 0 Edge virtio1-config + 25: 175 0 0 0 GICv2m-PCI-MSIX-0000:00:05.0 1 Edge virtio1-input.0 + 26: 321 0 0 0 GICv2m-PCI-MSIX-0000:00:05.0 2 Edge virtio1-output.0 + 27: 1 0 0 0 GICv2m-PCI-MSIX-0000:00:0a.0 0 Edge virtio2-config + 28: 21392 0 0 0 GICv2m-PCI-MSIX-0000:00:0a.0 1 Edge virtio2-virtqueues + 30: 164 0 0 0 GICv2m-PCI-MSI-0000:00:01.0 0 Edge snd_hda_intel:card0 + 31: 0 0 0 0 GICv2m-PCI-MSIX-0000:00:0e.0 0 Edge virtio3-config + 32: 0 0 0 0 GICv2m-PCI-MSIX-0000:00:0e.0 1 Edge virtio3-virtqueues +IPI0: 465 474 505 459 Rescheduling interrupts +IPI1: 7387 6412 5950 3696 Function call interrupts +IPI2: 0 0 0 0 CPU stop interrupts +IPI3: 0 0 0 0 CPU stop NMIs +IPI4: 0 0 0 0 Timer broadcast interrupts +IPI5: 0 0 0 0 IRQ work interrupts +IPI6: 0 0 0 0 CPU backtrace interrupts +IPI7: 0 0 0 0 KGDB roundup interrupts +Err: 0 diff --git a/docs/parallels-hwdump/iomem.txt b/docs/parallels-hwdump/iomem.txt new file mode 100644 index 00000000..f298a62d --- /dev/null +++ b/docs/parallels-hwdump/iomem.txt @@ -0,0 +1,57 @@ +02010000-0201ffff : GICD +02110000-02110fff : ARMH0011:00 + 02110000-02110fff : ARMH0011:00 ARMH0011:00 +02140000-02141fff : PRL4010:00 + 02140000-02141fff : PRL4010:00 PRL4010:00 +021a0000-021a3fff : PRL4006:00 + 021a0000-021a3fff : PRL4006:00 PRL4006:00 +02300000-023fffff : PCI ECAM +02500000-0257ffff : GICR +10000000-1fffffff : PCI Bus 0000:00 + 10000000-13ffffff : 0000:00:0a.0 + 14000000-14003fff : 0000:00:01.0 + 14000000-14003fff : ICH HD audio + 14004000-14007fff : 0000:00:0a.0 + 14004000-14007fff : virtio-pci-modern + 14008000-1400bfff : 0000:00:0e.0 + 14008000-1400bfff : virtio-pci-modern + 1400c000-1400cfff : 0000:00:03.0 + 1400c000-1400cfff : xhci-hcd + 1400d000-1400dfff : 0000:00:05.0 + 1400d000-1400dfff : virtio-pci-legacy + 1400e000-1400efff : 0000:00:05.0 + 1400f000-1400ffff : 0000:00:09.0 + 14010000-14010fff : 0000:00:0a.0 + 14011000-14011fff : 0000:00:0e.0 + 14012000-140123ff : 0000:00:02.0 + 14012000-140123ff : ehci_hcd +40000000-bbe0ffff : System RAM + 40010000-4154ffff : Kernel code + 41550000-4188ffff : reserved + 41890000-41edffff : Kernel data + b7680000-b7680fff : reserved + b7880000-b8838fff : reserved + b8840000-b884ffff : reserved + bac00000-bbbfffff : reserved + bbc68000-bbc68fff : reserved +bbe10000-bbeaffff : reserved +bbeb0000-bbf0ffff : System RAM + bbeb0000-bbf0ffff : reserved +bbf10000-bbf1ffff : reserved +bbf20000-bbf2ffff : System RAM + bbf20000-bbf2ffff : reserved +bbf30000-bbffffff : reserved +bc000000-bc01ffff : System RAM +bc020000-bc05ffff : reserved +bc060000-bf54ffff : System RAM + bc060000-bc07ffff : reserved + bc600000-be5fffff : reserved + be6c4000-be6c4fff : reserved + bf007000-bf54ffff : reserved +bf550000-bf92ffff : reserved +bf930000-bfffffff : System RAM + bf950000-bf955fff : reserved + bf956000-bf956fff : reserved + bf957000-bf9defff : reserved + bf9e1000-bf9e3fff : reserved + bf9e4000-bfffffff : reserved diff --git a/docs/parallels-hwdump/lspci-ids.txt b/docs/parallels-hwdump/lspci-ids.txt new file mode 100644 index 00000000..492caeb4 --- /dev/null +++ b/docs/parallels-hwdump/lspci-ids.txt @@ -0,0 +1,7 @@ +00:01.0 Audio device [0403]: Intel Corporation 82801I (ICH9 Family) HD Audio Controller [8086:293e] +00:02.0 USB controller [0c03]: Intel Corporation 82801FB/FBM/FR/FW/FRW (ICH6 Family) USB2 EHCI Controller [8086:265c] (rev 02) +00:03.0 USB controller [0c03]: NEC Corporation uPD720200 USB 3.0 Host Controller [1033:0194] (rev 04) +00:05.0 Ethernet controller [0200]: Red Hat, Inc. Virtio network device [1af4:1000] +00:09.0 Unassigned class [ff00]: Parallels, Inc. Virtual Machine Communication Interface [1ab8:4000] +00:0a.0 VGA compatible controller [0300]: Red Hat, Inc. Virtio 1.0 GPU [1af4:1050] (rev 01) +00:0e.0 Communication controller [0780]: Red Hat, Inc. Virtio 1.0 socket [1af4:1053] (rev 01) diff --git a/docs/parallels-hwdump/lspci-verbose.txt b/docs/parallels-hwdump/lspci-verbose.txt new file mode 100644 index 00000000..09ec4db2 --- /dev/null +++ b/docs/parallels-hwdump/lspci-verbose.txt @@ -0,0 +1,153 @@ +00:01.0 Audio device: Intel Corporation 82801I (ICH9 Family) HD Audio Controller + Subsystem: Parallels, Inc. Device 0400 + Control: I/O- Mem+ BusMaster+ SpecCycle- MemWINV- VGASnoop- ParErr- Stepping- SERR- FastB2B- DisINTx+ + Status: Cap+ 66MHz- UDF- FastB2B- ParErr- DEVSEL=fast >TAbort- SERR- TAbort- SERR- TAbort- SERR- TAbort- SERR- TAbort- SERR- TAbort- SERR- TAbort- SERR- ; + #address-cells = <0x02>; + +====================================== + ACPI TABLES +====================================== +Available ACPI tables: +total 0 +drwxr-xr-x 4 root root 0 Feb 19 10:13 . +drwxr-xr-x 4 root root 0 Feb 19 10:13 .. +-r-------- 1 root root 428 Feb 19 10:13 APIC +-r-------- 1 root root 93 Feb 19 10:13 DBG2 +-r-------- 1 root root 4291 Feb 19 10:13 DSDT +-r-------- 1 root root 276 Feb 19 10:13 FACP +-r-------- 1 root root 64 Feb 19 10:13 FACS +-r-------- 1 root root 96 Feb 19 10:13 GTDT +-r-------- 1 root root 60 Feb 19 10:13 MCFG +-r-------- 1 root root 48 Feb 19 10:13 PCCT +drwxr-xr-x 2 root root 0 Feb 19 10:13 data +drwxr-xr-x 2 root root 0 Feb 19 10:13 dynamic + Saved MCFG + Saved GTDT + Saved DSDT + +iasl not installed - install with: apk add acpica +Raw binary tables saved to /tmp/hwdump/acpi/ + +====================================== + PCI DEVICES +====================================== +--- PCI Device List --- +00:01.0 Audio device [0403]: Intel Corporation 82801I (ICH9 Family) HD Audio Controller [8086:293e] +00:02.0 USB controller [0c03]: Intel Corporation 82801FB/FBM/FR/FW/FRW (ICH6 Family) USB2 EHCI Controller [8086:265c] (rev 02) +00:03.0 USB controller [0c03]: NEC Corporation uPD720200 USB 3.0 Host Controller [1033:0194] (rev 04) +00:05.0 Ethernet controller [0200]: Red Hat, Inc. Virtio network device [1af4:1000] +00:09.0 Unassigned class [ff00]: Parallels, Inc. Virtual Machine Communication Interface [1ab8:4000] +00:0a.0 VGA compatible controller [0300]: Red Hat, Inc. Virtio 1.0 GPU [1af4:1050] (rev 01) +00:0e.0 Communication controller [0780]: Red Hat, Inc. Virtio 1.0 socket [1af4:1053] (rev 01) + +Detailed PCI info saved to /tmp/hwdump/lspci-verbose.txt + +====================================== + INTERRUPT CONTROLLER +====================================== +--- Active Interrupts --- + CPU0 CPU1 CPU2 CPU3 + 10: 42455 18750 20295 13682 GICv3 27 Level arch_timer + 12: 1 0 0 0 GICv3 48 Edge ACPI:Ged + 14: 0 0 0 0 GICv3 38 Edge virtio0 + 15: 1900 0 0 0 GICv3 34 Level ahci[PRL4010:00] + 16: 0 0 0 0 GICv3 32 Level uart-pl011 + 17: 0 0 0 0 GICv3 23 Level arm-pmu + 19: 498 0 0 0 GICv2m-PCI-MSI-0000:00:03.0 0 Edge xhci_hcd + 23: 0 0 0 0 GICv3 35 Level ehci_hcd:usb3 + 24: 0 0 0 0 GICv2m-PCI-MSIX-0000:00:05.0 0 Edge virtio1-config + 25: 175 0 0 0 GICv2m-PCI-MSIX-0000:00:05.0 1 Edge virtio1-input.0 + 26: 321 0 0 0 GICv2m-PCI-MSIX-0000:00:05.0 2 Edge virtio1-output.0 + 27: 1 0 0 0 GICv2m-PCI-MSIX-0000:00:0a.0 0 Edge virtio2-config + 28: 21392 0 0 0 GICv2m-PCI-MSIX-0000:00:0a.0 1 Edge virtio2-virtqueues + 30: 164 0 0 0 GICv2m-PCI-MSI-0000:00:01.0 0 Edge snd_hda_intel:card0 + 31: 0 0 0 0 GICv2m-PCI-MSIX-0000:00:0e.0 0 Edge virtio3-config + 32: 0 0 0 0 GICv2m-PCI-MSIX-0000:00:0e.0 1 Edge virtio3-virtqueues +IPI0: 467 476 505 459 Rescheduling interrupts +IPI1: 7387 6413 5950 3699 Function call interrupts +IPI2: 0 0 0 0 CPU stop interrupts +IPI3: 0 0 0 0 CPU stop NMIs +IPI4: 0 0 0 0 Timer broadcast interrupts +IPI5: 0 0 0 0 IRQ work interrupts +IPI6: 0 0 0 0 CPU backtrace interrupts +IPI7: 0 0 0 0 KGDB roundup interrupts +Err: 0 + +--- GIC Detection --- +[ 0.000000] CPU features: detected: GIC system register CPU interface +[ 0.000000] GICv3: 988 SPIs implemented +[ 0.000000] GICv3: 0 Extended SPIs implemented +[ 0.000000] Root IRQ handler: gic_handle_irq +[ 0.000000] GICv3: GICv3 features: 16 PPIs, RSS +[ 0.000000] GICv3: GICD_CTRL.DS=1, SCR_EL3.FIQ=0 +[ 0.000000] GICv3: CPU0: found redistributor 0 region 0:0x0000000002500000 +[ 0.000000] GICv2m: range[mem 0x02250000-0x02250fff], SPI[53:116] +[ 0.045842] GICv3: CPU1: found redistributor 1 region 0:0x0000000002520000 +[ 0.058275] GICv3: CPU2: found redistributor 2 region 0:0x0000000002560000 +[ 0.066591] GICv3: CPU3: found redistributor 3 region 0:0x0000000002540000 +[ 0.074928] ACPI: Using GIC for interrupt routing +[ 0.590786] sd 0:0:0:0: [sda] 16777216 512-byte logical blocks: (8.59 GB/8.00 GiB) + +====================================== + CPU INFORMATION +====================================== +processor : 0 +BogoMIPS : 48.00 +Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 asimddp sha512 asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp flagm2 frint +CPU implementer : 0x41 +CPU architecture: 8 +CPU variant : 0x0 +CPU part : 0x000 +CPU revision : 0 + +processor : 1 +BogoMIPS : 48.00 +Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 asimddp sha512 asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp flagm2 frint +CPU implementer : 0x41 +CPU architecture: 8 +CPU variant : 0x0 +CPU part : 0x000 +CPU revision : 0 + +processor : 2 +BogoMIPS : 48.00 +Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 asimddp sha512 asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp flagm2 frint +CPU implementer : 0x41 +CPU architecture: 8 +CPU variant : 0x0 +CPU part : 0x000 +CPU revision : 0 + +processor : 3 +BogoMIPS : 48.00 +Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 asimddp sha512 asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp flagm2 frint + +====================================== + BOOT INFORMATION +====================================== +Cmdline: BOOT_IMAGE=/boot/vmlinuz-lts modules=loop,squashfs,sd-mod,usb-storage quiet + +EFI boot: YES +EFI runtime services: efivars fw_platform_size systab +EFI vars count: 31 + +====================================== + MEMORY LAYOUT +====================================== +--- /proc/meminfo --- +MemTotal: 2017492 kB +MemFree: 1713256 kB +MemAvailable: 1847716 kB +Buffers: 244 kB +Cached: 203764 kB +SwapCached: 0 kB +Active: 19876 kB +Inactive: 181944 kB +Active(anon): 19876 kB +Inactive(anon): 0 kB + +--- dmesg memory --- +[ 0.000000] NUMA: Faking a node at [mem 0x0000000040000000-0x00000000bfffffff] +[ 0.000000] Zone ranges: +[ 0.000000] Movable zone start for each node +[ 0.000000] Early memory node ranges +[ 0.000000] node 0: [mem 0x0000000040000000-0x00000000bbe0ffff] +[ 0.000000] node 0: [mem 0x00000000bbe10000-0x00000000bbeaffff] +[ 0.000000] node 0: [mem 0x00000000bbeb0000-0x00000000bbf0ffff] +[ 0.000000] node 0: [mem 0x00000000bbf10000-0x00000000bbf1ffff] +[ 0.000000] node 0: [mem 0x00000000bbf20000-0x00000000bbf2ffff] +[ 0.000000] node 0: [mem 0x00000000bbf30000-0x00000000bbffffff] +[ 0.000000] node 0: [mem 0x00000000bc000000-0x00000000bc01ffff] +[ 0.000000] node 0: [mem 0x00000000bc020000-0x00000000bc05ffff] +[ 0.000000] node 0: [mem 0x00000000bc060000-0x00000000bf54ffff] +[ 0.000000] node 0: [mem 0x00000000bf550000-0x00000000bf92ffff] +[ 0.000000] node 0: [mem 0x00000000bf930000-0x00000000bfffffff] +[ 0.000000] Initmem setup node 0 [mem 0x0000000040000000-0x00000000bfffffff] +[ 0.000000] cma: Reserved 16 MiB at 0x00000000bac00000 on node -1 +[ 0.000000] Fallback order for Node 0: 0 +[ 0.000000] Built 1 zonelists, mobility grouping on. Total pages: 524288 +[ 0.000000] Policy zone: DMA + +====================================== + VIRTIO DEVICES +====================================== +--- /sys/bus/virtio/devices --- + virtio0: vendor=0x1ab8 device=0x0005 + virtio1: vendor=0x1ab8 device=0x0001 + virtio2: vendor=0x1ab8 device=0x0010 + virtio3: vendor=0x1ab8 device=0x0013 + +====================================== + SERIAL PORTS +====================================== +[ 0.070274] Serial: AMBA PL011 UART driver +[ 0.078424] ARMH0011:00: ttyAMA0 at MMIO 0x2110000 (irq = 16, base_baud = 0) is a SBSA +[ 0.088722] Serial: 8250/16550 driver, 4 ports, IRQ sharing enabled +[ 0.088941] Serial: AMBA driver +[ 2.249846] usb usb1: New USB device strings: Mfr=3, Product=2, SerialNumber=1 +[ 2.249848] usb usb1: SerialNumber: 0000:00:03.0 +[ 2.250265] usb usb2: New USB device strings: Mfr=3, Product=2, SerialNumber=1 +[ 2.250269] usb usb2: SerialNumber: 0000:00:03.0 +[ 2.270187] usb usb3: New USB device strings: Mfr=3, Product=2, SerialNumber=1 +[ 2.270189] usb usb3: SerialNumber: 0000:00:02.0 +[ 2.590428] usb 2-1: New USB device strings: Mfr=1, Product=2, SerialNumber=3 +[ 2.590431] usb 2-1: SerialNumber: PW3.0 +[ 2.720906] usb 2-2: New USB device strings: Mfr=1, Product=2, SerialNumber=3 +[ 2.720910] usb 2-2: SerialNumber: KBD1.1 +[ 2.957344] usb 2-3: New USB device strings: Mfr=1, Product=2, SerialNumber=3 +[ 2.957348] usb 2-3: SerialNumber: 1F06D5FF-4C76-410E-9770-C04C467C1317 + +====================================== + TIMER +====================================== +[ 0.000000] arch_timer: cp15 timer(s) running at 24.00MHz (virt). +[ 0.000000] clocksource: arch_sys_counter: mask: 0xffffffffffffff max_cycles: 0x588fe9dc0, max_idle_ns: 440795202592 ns +[ 0.001977] Calibrating delay loop (skipped), value calculated using timer frequency.. 48.00 BogoMIPS (lpj=80000) +[ 0.026315] Timer migration: 1 hierarchy levels; 8 children per group; 1 crossnode level +[ 0.067529] clocksource: jiffies: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 6370867519511994 ns +[ 0.079316] clocksource: Switched to clocksource arch_sys_counter +====================================== + DUMP COMPLETE +====================================== + +All files saved to: /tmp/hwdump +Files: +total 52 +drwxr-xr-x 3 root root 220 Feb 19 10:46 . +drwxrwxrwt 5 root root 100 Feb 19 10:46 .. +drwxr-xr-x 2 root root 100 Feb 19 10:46 acpi +-rw-r--r-- 1 root root 1300 Feb 19 10:46 cpuinfo.txt +-r-------- 1 root root 450 Feb 19 10:46 guest.dtb +-rw-r--r-- 1 root root 375 Feb 19 10:46 guest.dts +-rw-r--r-- 1 root root 2260 Feb 19 10:46 interrupts.txt +-rw-r--r-- 1 root root 1883 Feb 19 10:46 iomem.txt +-rw-r--r-- 1 root root 700 Feb 19 10:46 lspci-ids.txt +-rw-r--r-- 1 root root 8352 Feb 19 10:46 lspci-verbose.txt +-rw-r--r-- 1 root root 12883 Feb 19 10:46 summary.txt + +ACPI files: +total 16 +drwxr-xr-x 2 root root 100 Feb 19 10:46 . +drwxr-xr-x 3 root root 220 Feb 19 10:46 .. +-r-------- 1 root root 4291 Feb 19 10:46 DSDT.bin +-r-------- 1 root root 96 Feb 19 10:46 GTDT.bin +-r-------- 1 root root 60 Feb 19 10:46 MCFG.bin diff --git a/kernel/src/arch_impl/aarch64/boot.S b/kernel/src/arch_impl/aarch64/boot.S index 548b394b..d3a2e128 100644 --- a/kernel/src/arch_impl/aarch64/boot.S +++ b/kernel/src/arch_impl/aarch64/boot.S @@ -152,8 +152,10 @@ bss_done: isb // Jump to Rust kernel_main (high) - ldr x0, =kernel_main - br x0 + // x0 = hw_config_ptr (0 = QEMU, non-zero = Parallels loader passes HardwareConfig*) + ldr x1, =kernel_main + mov x0, #0 + br x1 // If kernel_main returns, hang hang: diff --git a/kernel/src/arch_impl/aarch64/context_switch.rs b/kernel/src/arch_impl/aarch64/context_switch.rs index 62444ca7..373c04dd 100644 --- a/kernel/src/arch_impl/aarch64/context_switch.rs +++ b/kernel/src/arch_impl/aarch64/context_switch.rs @@ -163,13 +163,9 @@ pub fn dump_dispatch_trace(cpu_id: usize) { #[inline(always)] #[allow(dead_code)] pub fn raw_uart_char(c: u8) { - // QEMU virt machine UART via HHDM (TTBR1-mapped, safe during context switch) - // Physical 0x0900_0000 is mapped at HHDM_BASE + 0x0900_0000 - const HHDM_BASE: u64 = 0xFFFF_0000_0000_0000; - const UART_VIRT: u64 = HHDM_BASE + 0x0900_0000; + let addr = crate::platform_config::uart_virt() as *mut u8; unsafe { - let ptr = UART_VIRT as *mut u8; - core::ptr::write_volatile(ptr, c); + core::ptr::write_volatile(addr, c); } } @@ -392,13 +388,25 @@ fn restore_kernel_context_inline( // If the context is corrupt, return false immediately — the caller will // redirect to idle and update cpu_state so that the next preemption // doesn't save idle-loop registers into this thread's context. + // + // On QEMU, kernel code runs from HHDM (>= 0xFFFF_0000_0000_0000). + // On Parallels, the UEFI loader jumps to kernel_main at a physical + // address and the kernel runs identity-mapped, so function pointers + // resolve to physical addresses in the RAM range (0x40080000+). const KERNEL_VIRT_BASE: u64 = 0xFFFF_0000_0000_0000; + const KERNEL_PHYS_BASE: u64 = 0x4008_0000; + const KERNEL_PHYS_LIMIT: u64 = 0xC000_0000; + #[inline] + fn is_kernel_addr(addr: u64) -> bool { + addr >= KERNEL_VIRT_BASE + || (addr >= KERNEL_PHYS_BASE && addr < KERNEL_PHYS_LIMIT) + } let elr_valid = if !has_started { // First run: x30 must be a valid kernel address - thread.context.x30 >= KERNEL_VIRT_BASE + is_kernel_addr(thread.context.x30) } else { // Resume: elr_el1 must be in kernel space or zero (handled below) - thread.context.elr_el1 >= KERNEL_VIRT_BASE || thread.context.elr_el1 == 0 + is_kernel_addr(thread.context.elr_el1) || thread.context.elr_el1 == 0 }; if !elr_valid { @@ -464,12 +472,18 @@ fn restore_kernel_context_inline( if !has_started { frame.elr = thread.context.x30; // First run: jump to entry point frame.spsr = 0x5; // EL1h, DAIF clear (interrupts enabled) - } else if thread.context.elr_el1 >= KERNEL_VIRT_BASE { - frame.elr = thread.context.elr_el1; // Resume: return to where we left off + } else if is_kernel_addr(thread.context.elr_el1) { + // Resume: return to where we left off. + // On QEMU, kernel addresses are >= KERNEL_VIRT_BASE (HHDM). + // On Parallels, kernel runs identity-mapped at physical addresses + // (KERNEL_PHYS_BASE..KERNEL_PHYS_LIMIT), so we must accept both. + frame.elr = thread.context.elr_el1; frame.spsr = thread.context.spsr_el1; // Restore saved processor state } else { - // elr_el1 == 0: thread was started but has zero ELR — redirect to idle - raw_uart_str("WARN: elr=0 for started kthread tid="); + // elr_el1 == 0 or not a valid kernel address — redirect to idle + raw_uart_str("WARN: bad elr="); + raw_uart_hex(thread.context.elr_el1); + raw_uart_str(" for started kthread tid="); raw_uart_dec(thread_id); raw_uart_str(", redirecting to idle\n"); return false; @@ -657,7 +671,8 @@ fn setup_idle_return_locked( cpu_id: usize, ) { // Set frame ELR and SPSR to safe values FIRST - frame.elr = idle_loop_arm64 as *const () as u64; + let idle_addr = idle_loop_arm64 as *const () as u64; + frame.elr = idle_addr; frame.spsr = 0x5; // EL1h with interrupts enabled // Get idle thread's kernel stack @@ -666,7 +681,6 @@ fn setup_idle_return_locked( .and_then(|t| t.kernel_stack_top.map(|v| v.as_u64())) .unwrap_or_else(|| { let cpu_id64 = cpu_id as u64; - raw_uart_char(b'!'); 0xFFFF_0000_0000_0000u64 + 0x4100_0000 + (cpu_id64 + 1) * 0x20_0000 }); @@ -724,13 +738,21 @@ fn dispatch_idle_locked( // case we need to restore the saved context so the boot thread resumes. let idle_loop_addr = idle_loop_arm64 as *const () as u64; const KERNEL_VIRT_BASE: u64 = 0xFFFF_0000_0000_0000; + // Also accept physical kernel addresses on Parallels + const KERNEL_PHYS_BASE: u64 = 0x4008_0000; + const KERNEL_PHYS_LIMIT: u64 = 0xC000_0000; let has_saved_context = sched.get_thread(thread_id).map(|thread| { let elr = thread.context.elr_el1; let sp = thread.context.sp; let spsr = thread.context.spsr_el1; - elr >= KERNEL_VIRT_BASE - && !(elr >= idle_loop_addr && elr < idle_loop_addr + 16) - && sp >= KERNEL_VIRT_BASE + let elr_is_kernel = elr >= KERNEL_VIRT_BASE + || (elr >= KERNEL_PHYS_BASE && elr < KERNEL_PHYS_LIMIT); + let sp_is_kernel = sp >= KERNEL_VIRT_BASE + || (sp >= KERNEL_PHYS_BASE && sp < KERNEL_PHYS_LIMIT); + let near_idle = elr >= idle_loop_addr && elr < idle_loop_addr + 16; + elr_is_kernel + && !near_idle + && sp_is_kernel && (spsr & 0xF) != 0 }).unwrap_or(false); @@ -1207,7 +1229,6 @@ fn setup_idle_return_arm64(frame: &mut Aarch64ExceptionFrame) { .unwrap_or_else(|| { let cpu_id = Aarch64PerCpu::cpu_id() as u64; let boot_stack_top = 0xFFFF_0000_0000_0000u64 + 0x4100_0000 + (cpu_id + 1) * 0x20_0000; - raw_uart_char(b'!'); boot_stack_top }); @@ -1356,8 +1377,12 @@ fn set_next_ttbr0_for_thread(thread_id: u64) -> TtbrResult { drop(manager_guard); if let Some(ttbr0) = next_ttbr0 { + // Tag TTBR0 with ASID=1 so stale boot identity map TLB entries + // (ASID=0) don't match user VA accesses. Combined with nG bits on + // process page table entries, this ensures ASID-based separation. + let tagged_ttbr0 = ttbr0 | (1u64 << 48); // ASID=1 in bits [55:48] unsafe { - Aarch64PerCpu::set_next_cr3(ttbr0); + Aarch64PerCpu::set_next_cr3(tagged_ttbr0); } TtbrResult::Ok } else { @@ -1485,7 +1510,7 @@ fn check_and_deliver_signals_for_current_thread_arm64(frame: &mut Aarch64Excepti // Switch to process's page table for signal delivery if let Some(ref page_table) = process.page_table { - let ttbr0_value = page_table.level_4_frame().start_address().as_u64(); + let raw_ttbr0 = page_table.level_4_frame().start_address().as_u64(); unsafe { core::arch::asm!( "dsb ishst", @@ -1494,7 +1519,7 @@ fn check_and_deliver_signals_for_current_thread_arm64(frame: &mut Aarch64Excepti "tlbi vmalle1is", "dsb ish", "isb", - in(reg) ttbr0_value, + in(reg) raw_ttbr0, options(nomem, nostack) ); } diff --git a/kernel/src/arch_impl/aarch64/elf.rs b/kernel/src/arch_impl/aarch64/elf.rs index 84348590..7292d8dc 100644 --- a/kernel/src/arch_impl/aarch64/elf.rs +++ b/kernel/src/arch_impl/aarch64/elf.rs @@ -186,6 +186,22 @@ pub unsafe fn load_elf_kernel_space(data: &[u8]) -> Result Syscall /// PL011 UART IRQ number (SPI 1, which is IRQ 33) const UART0_IRQ: u32 = 33; -/// Raw serial write - write a string without locks, for use in interrupt handlers -#[inline(always)] -fn raw_serial_str(s: &[u8]) { - crate::serial_aarch64::raw_serial_str(s); -} - /// Handle IRQ interrupts /// /// Called from assembly after saving registers. @@ -960,6 +1022,12 @@ pub extern "C" fn handle_irq() { crate::drivers::virtio::net_mmio::handle_interrupt(); } } + // XHCI USB interrupt dispatch + if let Some(xhci_irq) = crate::drivers::usb::xhci::get_irq() { + if irq_id == xhci_irq { + crate::drivers::usb::xhci::handle_interrupt(); + } + } } // Should not happen - GIC filters invalid IDs (1020+) @@ -1139,7 +1207,7 @@ fn handle_cow_fault_arm64(far: u64, iss: u32) -> bool { if page_table.update_page_flags(page, new_flags).is_err() { return false; } - // Flush TLB + // Flush TLB for the modified page unsafe { let va_for_tlbi = faulting_addr.as_u64() >> 12; core::arch::asm!( @@ -1188,7 +1256,7 @@ fn handle_cow_fault_arm64(far: u64, iss: u32) -> bool { // Decrement reference count on old frame frame_decref(old_frame); - // Flush TLB + // Flush TLB for the CoW-copied page unsafe { let va_for_tlbi = faulting_addr.as_u64() >> 12; core::arch::asm!( diff --git a/kernel/src/arch_impl/aarch64/gic.rs b/kernel/src/arch_impl/aarch64/gic.rs index 1e962063..793b354d 100644 --- a/kernel/src/arch_impl/aarch64/gic.rs +++ b/kernel/src/arch_impl/aarch64/gic.rs @@ -1,23 +1,24 @@ -//! ARM64 GICv2 (Generic Interrupt Controller) interrupt controller. +//! ARM64 GIC (Generic Interrupt Controller) - supports GICv2 and GICv3. //! -//! The GICv2 has two main components: +//! The GIC has these components: //! - GICD (Distributor): Routes interrupts to CPUs, manages priority/enable -//! - GICC (CPU Interface): Per-CPU interface for acknowledging/completing IRQs +//! - GICC (CPU Interface, GICv2): Per-CPU MMIO interface +//! - ICC (CPU Interface, GICv3): Per-CPU system register interface +//! - GICR (Redistributor, GICv3): Per-CPU SGI/PPI configuration //! //! Interrupt types: //! - SGI (0-15): Software Generated Interrupts (IPIs) //! - PPI (16-31): Private Peripheral Interrupts (per-CPU, e.g., timer) //! - SPI (32-1019): Shared Peripheral Interrupts (global, e.g., devices) //! -//! For QEMU virt machine: -//! - GICD base: 0x0800_0000 -//! - GICC base: 0x0801_0000 +//! Hardware addresses are read from platform_config at runtime: +//! - QEMU virt: GICD=0x0800_0000, GICC=0x0801_0000 (GICv2) +//! - Parallels: GICD=0x0201_0000, GICR=0x0250_0000 (GICv3) #![allow(dead_code)] -use core::sync::atomic::{AtomicBool, Ordering}; +use core::sync::atomic::{AtomicBool, AtomicU8, Ordering}; use crate::arch_impl::traits::InterruptController; -use crate::arch_impl::aarch64::constants::{GICD_BASE, GICC_BASE}; // ============================================================================= // GIC Distributor (GICD) Register Offsets @@ -100,50 +101,69 @@ const GICD_PIDR2: usize = 0xFE8; // Register Access Helpers // ============================================================================= -/// HHDM base address for GIC MMIO access. -/// -/// Using the compile-time constant directly instead of `physical_memory_offset()` -/// (which goes through a OnceCell) eliminates an atomic load-acquire (`ldar`) on -/// every GIC register access. The compiler was hoisting the OnceCell state pointer -/// into a callee-saved register (x19) and reusing it across the entire `handle_irq` -/// function. If any callee's stack frame was corrupted (the saved x19 slot -/// overwritten with zero), the post-handler EOI write would dereference NULL and -/// DATA_ABORT. Since HHDM_BASE is a link-time constant on ARM64, there is no -/// reason to go through a runtime cell. +/// HHDM base address for GIC MMIO access (compile-time constant). const GIC_HHDM: usize = crate::arch_impl::aarch64::constants::HHDM_BASE as usize; -/// Read a 32-bit GICD register +/// Active GIC version: 2 for GICv2, 3 for GICv3. Set during init. +static ACTIVE_GIC_VERSION: AtomicU8 = AtomicU8::new(0); + +/// Read a 32-bit GICD register (address from platform_config). #[inline] fn gicd_read(offset: usize) -> u32 { unsafe { - let addr = (GIC_HHDM + GICD_BASE as usize + offset) as *const u32; + let base = crate::platform_config::gicd_base_phys() as usize; + let addr = (GIC_HHDM + base + offset) as *const u32; core::ptr::read_volatile(addr) } } -/// Write a 32-bit GICD register +/// Write a 32-bit GICD register (address from platform_config). #[inline] fn gicd_write(offset: usize, value: u32) { unsafe { - let addr = (GIC_HHDM + GICD_BASE as usize + offset) as *mut u32; + let base = crate::platform_config::gicd_base_phys() as usize; + let addr = (GIC_HHDM + base + offset) as *mut u32; core::ptr::write_volatile(addr, value); } } -/// Read a 32-bit GICC register +/// Read a 32-bit GICC register (GICv2 only, address from platform_config). #[inline] fn gicc_read(offset: usize) -> u32 { unsafe { - let addr = (GIC_HHDM + GICC_BASE as usize + offset) as *const u32; + let base = crate::platform_config::gicc_base_phys() as usize; + let addr = (GIC_HHDM + base + offset) as *const u32; core::ptr::read_volatile(addr) } } -/// Write a 32-bit GICC register +/// Write a 32-bit GICC register (GICv2 only, address from platform_config). #[inline] fn gicc_write(offset: usize, value: u32) { unsafe { - let addr = (GIC_HHDM + GICC_BASE as usize + offset) as *mut u32; + let base = crate::platform_config::gicc_base_phys() as usize; + let addr = (GIC_HHDM + base + offset) as *mut u32; + core::ptr::write_volatile(addr, value); + } +} + +/// Read a 32-bit GICR register (GICv3 only). +/// `cpu_offset` is the redistributor offset for this CPU (cpu * 0x20000). +#[inline] +fn gicr_read(cpu_offset: usize, offset: usize) -> u32 { + unsafe { + let base = crate::platform_config::gicr_base_phys() as usize; + let addr = (GIC_HHDM + base + cpu_offset + offset) as *const u32; + core::ptr::read_volatile(addr) + } +} + +/// Write a 32-bit GICR register (GICv3 only). +#[inline] +fn gicr_write(cpu_offset: usize, offset: usize, value: u32) { + unsafe { + let base = crate::platform_config::gicr_base_phys() as usize; + let addr = (GIC_HHDM + base + cpu_offset + offset) as *mut u32; core::ptr::write_volatile(addr, value); } } @@ -238,16 +258,23 @@ impl Gicv2 { /// Initialize the GIC CPU interface for a secondary CPU. /// -/// GICC registers are banked per-CPU, so each CPU must configure its own -/// interface. The distributor (GICD) is global and already initialized by CPU 0. -/// SGIs (0-15) and PPIs (16-31) are per-CPU by definition and do not need -/// distributor re-configuration. +/// Dispatches to GICv2 or GICv3 based on the detected version. pub fn init_cpu_interface_secondary() { - // Safety: if we got here, GIC_INITIALIZED is true, which means - // init() already verified GICv2 is present. No need to re-check. - gicc_write(GICC_PMR, PRIORITY_MASK as u32); - gicc_write(GICC_BPR, 7); - gicc_write(GICC_CTLR, 0x7); // EnableGrp0 | EnableGrp1 | AckCtl + let version = ACTIVE_GIC_VERSION.load(Ordering::Acquire); + match version { + 2 => { + gicc_write(GICC_PMR, PRIORITY_MASK as u32); + gicc_write(GICC_BPR, 7); + gicc_write(GICC_CTLR, 0x7); + } + 3 | 4 => { + // Get CPU ID from MPIDR_EL1 for redistributor offset + let cpu_id = get_cpu_id_from_mpidr(); + init_gicv3_redistributor(cpu_id); + init_gicv3_cpu_interface(); + } + _ => {} + } } impl Gicv2 { @@ -263,63 +290,100 @@ impl Gicv2 { } impl InterruptController for Gicv2 { - /// Initialize the GIC + /// Initialize the GIC - auto-detects v2 vs v3 and dispatches accordingly. fn init() { if GIC_INITIALIZED.load(Ordering::Relaxed) { return; } - // Detect GIC version before accessing GICC (CPU interface) registers. - // The GICC MMIO region only exists for GICv2. GICv3 uses system - // registers (ICC_*) instead, and the GICC address range is unmapped — - // accessing it would cause a synchronous external abort (DATA_ABORT). - let version = Self::detect_version(); - if version != 2 { - panic!( - "GIC architecture version {} detected, but only GICv2 is supported. \ - Launch QEMU with '-M virt' (default GICv2) instead of \ - '-machine virt,gic-version=3'.", - version - ); + // Detect GIC version from GICD_PIDR2 or platform_config. + let platform_version = crate::platform_config::gic_version(); + let hw_version = Self::detect_version(); + let version = if platform_version != 0 { platform_version } else { hw_version }; + + ACTIVE_GIC_VERSION.store(version, Ordering::Release); + + match version { + 2 => { + Self::init_distributor(); + Self::init_cpu_interface(); + } + 3 | 4 => { + init_gicv3_distributor(); + init_gicv3_redistributor(0); // CPU 0 + init_gicv3_cpu_interface(); + } + _ => { + panic!("Unknown GIC architecture version {}", version); + } } - Self::init_distributor(); - Self::init_cpu_interface(); - GIC_INITIALIZED.store(true, Ordering::Release); } /// Enable an IRQ fn enable_irq(irq: u8) { - let irq = irq as u32; - let reg_index = irq / IRQS_PER_ENABLE_REG; - let bit = irq % IRQS_PER_ENABLE_REG; - - // For SPIs (32+), ensure CPU target is set to CPU 0 - if irq >= 32 { - let target_reg = irq / 4; - let target_byte = irq % 4; - let current = gicd_read(GICD_ITARGETSR + (target_reg as usize * 4)); - let mask = 0xFFu32 << (target_byte * 8); - let target_val = 0x01u32 << (target_byte * 8); // CPU 0 - gicd_write( - GICD_ITARGETSR + (target_reg as usize * 4), - (current & !mask) | target_val, - ); + let irq_num = irq as u32; + let version = ACTIVE_GIC_VERSION.load(Ordering::Relaxed); + + if irq_num < 32 { + // SGI/PPI: on GICv3, use GICR; on GICv2, use GICD + if version >= 3 { + // Enable in GICR_ISENABLER0 for current CPU + let cpu_id = get_cpu_id_from_mpidr(); + let cpu_offset = cpu_id * GICR_FRAME_SIZE; + let sgi_offset = GICR_SGI_OFFSET; // SGI_base frame within redistributor + gicr_write(cpu_offset + sgi_offset, GICR_ISENABLER0, 1 << irq_num); + } else { + gicd_write(GICD_ISENABLER, 1 << irq_num); + } + } else { + // SPI: always in GICD + let reg_index = irq_num / IRQS_PER_ENABLE_REG; + let bit = irq_num % IRQS_PER_ENABLE_REG; + + if version >= 3 { + // GICv3: Route SPI to current CPU via GICD_IROUTER + let mpidr: u64; + unsafe { core::arch::asm!("mrs {}, mpidr_el1", out(reg) mpidr, options(nomem, nostack)); } + let affinity = mpidr & 0xFF_00FF_FFFF; // Aff3.Aff2.Aff1.Aff0 + gicd_write(GICD_IROUTER + (irq_num as usize * 8), affinity as u32); + gicd_write(GICD_IROUTER + (irq_num as usize * 8) + 4, (affinity >> 32) as u32); + } else { + // GICv2: Route SPI to CPU 0 via ITARGETSR + let target_reg = irq_num / 4; + let target_byte = irq_num % 4; + let current = gicd_read(GICD_ITARGETSR + (target_reg as usize * 4)); + let mask = 0xFFu32 << (target_byte * 8); + let target_val = 0x01u32 << (target_byte * 8); + gicd_write( + GICD_ITARGETSR + (target_reg as usize * 4), + (current & !mask) | target_val, + ); + } + + gicd_write(GICD_ISENABLER + (reg_index as usize * 4), 1 << bit); } - - // Write 1 to ISENABLER to enable (writes of 0 have no effect) - gicd_write(GICD_ISENABLER + (reg_index as usize * 4), 1 << bit); } /// Disable an IRQ fn disable_irq(irq: u8) { - let irq = irq as u32; - let reg_index = irq / IRQS_PER_ENABLE_REG; - let bit = irq % IRQS_PER_ENABLE_REG; - - // Write 1 to ICENABLER to disable (writes of 0 have no effect) - gicd_write(GICD_ICENABLER + (reg_index as usize * 4), 1 << bit); + let irq_num = irq as u32; + + if irq_num < 32 { + let version = ACTIVE_GIC_VERSION.load(Ordering::Relaxed); + if version >= 3 { + let cpu_id = get_cpu_id_from_mpidr(); + let cpu_offset = cpu_id * GICR_FRAME_SIZE; + gicr_write(cpu_offset + GICR_SGI_OFFSET, GICR_ICENABLER0, 1 << irq_num); + } else { + gicd_write(GICD_ICENABLER, 1 << irq_num); + } + } else { + let reg_index = irq_num / IRQS_PER_ENABLE_REG; + let bit = irq_num % IRQS_PER_ENABLE_REG; + gicd_write(GICD_ICENABLER + (reg_index as usize * 4), 1 << bit); + } } /// Signal End of Interrupt @@ -338,19 +402,24 @@ impl InterruptController for Gicv2 { // Additional GIC Utilities // ============================================================================= -/// Acknowledge the current interrupt and get its ID +/// Acknowledge the current interrupt and get its ID. /// -/// Returns the interrupt ID, or None if spurious or invalid. -/// GICv2 interrupt ID ranges: -/// - 0-1019: Valid interrupts (SGI 0-15, PPI 16-31, SPI 32-1019) -/// - 1020-1022: Reserved (treat as spurious) -/// - 1023: Spurious interrupt (no pending interrupt when IAR was read) +/// Dispatches to GICv2 MMIO or GICv3 system registers. +/// Returns the interrupt ID, or None if spurious (1023). #[inline] pub fn acknowledge_irq() -> Option { - let iar = gicc_read(GICC_IAR); - let irq_id = iar & 0x3FF; // Bits 9:0 are the interrupt ID + let version = ACTIVE_GIC_VERSION.load(Ordering::Relaxed); + let irq_id = if version >= 3 { + // GICv3: Read ICC_IAR1_EL1 + let iar: u64; + unsafe { core::arch::asm!("mrs {}, icc_iar1_el1", out(reg) iar, options(nomem, nostack)); } + (iar & 0xFFFFFF) as u32 // 24-bit INTID for GICv3 + } else { + // GICv2: Read GICC_IAR + let iar = gicc_read(GICC_IAR); + iar & 0x3FF // 10-bit INTID for GICv2 + }; - // Filter out spurious (1023) and reserved/invalid IDs (1020-1022) if irq_id > MAX_VALID_IRQ { None } else { @@ -360,20 +429,21 @@ pub fn acknowledge_irq() -> Option { /// Signal end of interrupt by ID. /// -/// CRITICAL: This MUST be `#[inline(never)]` to prevent the compiler from -/// hoisting the GICC base address into a callee-saved register (x19) and -/// sharing it with `acknowledge_irq` across the entire `handle_irq` function. -/// When both IAR read and EOIR write are inlined into handle_irq, the compiler -/// merges the shared base address into x19 and reuses it across IRQ handler -/// function calls (timer_interrupt_handler, etc.). If any callee's stack -/// frame is corrupted on SMP (zeroing the saved x19 slot), the EOIR write -/// dereferences NULL → DATA_ABORT at FAR=0x4. -/// -/// With `#[inline(never)]`, end_of_interrupt computes the EOIR address in -/// its own temporary registers (x0-x7), independent of handle_irq's state. +/// CRITICAL: `#[inline(never)]` prevents the compiler from hoisting the +/// GICC/ICC base into a callee-saved register shared with acknowledge_irq. +/// See original GICv2 comment for the full rationale. #[inline(never)] pub fn end_of_interrupt(irq_id: u32) { - gicc_write(GICC_EOIR, irq_id); + let version = ACTIVE_GIC_VERSION.load(Ordering::Relaxed); + if version >= 3 { + // GICv3: Write ICC_EOIR1_EL1 + unsafe { + core::arch::asm!("msr icc_eoir1_el1, {}", in(reg) irq_id as u64, options(nomem, nostack)); + } + } else { + // GICv2: Write GICC_EOIR + gicc_write(GICC_EOIR, irq_id); + } } /// Check if an IRQ is pending @@ -407,13 +477,25 @@ pub fn send_sgi(sgi_id: u8, target_cpu: u8) { return; } - // GICD_SGIR format: - // Bits 25:24 = TargetListFilter (0 = use target list) - // Bits 23:16 = CPUTargetList (bitmask of target CPUs) - // Bits 3:0 = SGIINTID (SGI number) - let target_mask = 1u32 << (target_cpu as u32); - let sgir = (target_mask << 16) | (sgi_id as u32); - gicd_write(0xF00, sgir); // GICD_SGIR offset + let version = ACTIVE_GIC_VERSION.load(Ordering::Relaxed); + if version >= 3 { + // GICv3: Write ICC_SGI1R_EL1 + // Bits 55:48 = Aff3, 39:32 = Aff2, 23:16 = Aff1 + // Bits 15:0 = TargetList (bitmask within Aff1 group) + // Bits 27:24 = INTID (SGI number) + // For simple SMP (CPUs 0-7 in same affinity group): + let target_list = 1u64 << (target_cpu as u64); + let sgir = ((sgi_id as u64) << 24) | target_list; + unsafe { + core::arch::asm!("msr icc_sgi1r_el1, {}", in(reg) sgir, options(nomem, nostack)); + core::arch::asm!("isb", options(nomem, nostack)); + } + } else { + // GICv2: Write GICD_SGIR + let target_mask = 1u32 << (target_cpu as u32); + let sgir = (target_mask << 16) | (sgi_id as u32); + gicd_write(0xF00, sgir); + } } /// Check if GIC is initialized @@ -422,46 +504,252 @@ pub fn is_initialized() -> bool { GIC_INITIALIZED.load(Ordering::Acquire) } -/// Debug function to dump GIC state for a specific IRQ +/// Configure an SPI as edge-triggered (required for MSI interrupts). +/// +/// GICD_ICFGR has 2 bits per IRQ: 0b00 = level, 0b10 = edge. +/// For IRQs 32+, register index = irq / 16, field = (irq % 16) * 2. +pub fn configure_spi_edge_triggered(irq: u32) { + if irq < 32 { + return; // Only SPIs (32+) + } + let reg_index = irq / 16; + let field = (irq % 16) * 2; + let current = gicd_read(GICD_ICFGR + (reg_index as usize * 4)); + // Set bit 1 of the 2-bit field to select edge-triggered + let new_val = current | (0b10 << field); + gicd_write(GICD_ICFGR + (reg_index as usize * 4), new_val); +} + +/// Enable an SPI in the GIC distributor (GICD_ISENABLER). +/// +/// Also routes the SPI to the current CPU via ITARGETSR (GICv2) or +/// IROUTER (GICv3). Only valid for IRQs >= 32 (SPIs). +pub fn enable_spi(irq: u32) { + if irq < 32 { + return; // Only SPIs (32+) + } + let version = ACTIVE_GIC_VERSION.load(Ordering::Relaxed); + let reg_index = irq / 32; + let bit = irq % 32; + + if version >= 3 { + // GICv3: Route SPI to current CPU via GICD_IROUTER + let mpidr: u64; + unsafe { core::arch::asm!("mrs {}, mpidr_el1", out(reg) mpidr, options(nomem, nostack)); } + let affinity = mpidr & 0xFF_00FF_FFFF; // Aff3.Aff2.Aff1.Aff0 + gicd_write(GICD_IROUTER + (irq as usize * 8), affinity as u32); + gicd_write(GICD_IROUTER + (irq as usize * 8) + 4, (affinity >> 32) as u32); + } else { + // GICv2: Route SPI to CPU 0 via ITARGETSR + let target_reg = irq / 4; + let target_byte = irq % 4; + let current = gicd_read(GICD_ITARGETSR + (target_reg as usize * 4)); + let mask = 0xFFu32 << (target_byte * 8); + let target_val = 0x01u32 << (target_byte * 8); + gicd_write( + GICD_ITARGETSR + (target_reg as usize * 4), + (current & !mask) | target_val, + ); + } + + gicd_write(GICD_ISENABLER + (reg_index as usize * 4), 1 << bit); +} + +/// Disable an SPI in the GIC distributor (GICD_ICENABLER). /// -/// Useful for diagnosing interrupt routing issues. +/// Only valid for IRQs >= 32 (SPIs). +pub fn disable_spi(irq: u32) { + if irq < 32 { + return; // Only SPIs (32+) + } + let reg_index = irq / 32; + let bit = irq % 32; + gicd_write(GICD_ICENABLER + (reg_index as usize * 4), 1 << bit); +} + +/// Clear any pending state for an SPI (write-1-to-clear via GICD_ICPENDR). +pub fn clear_spi_pending(irq: u32) { + if irq < 32 { + return; // Only SPIs (32+) + } + let reg_index = irq / 32; + let bit = irq % 32; + gicd_write(GICD_ICPENDR + (reg_index as usize * 4), 1 << bit); +} + +/// Debug function to dump GIC state for a specific IRQ. pub fn dump_irq_state(irq: u32) { let reg_index = irq / 32; let bit_index = irq % 32; + let version = ACTIVE_GIC_VERSION.load(Ordering::Relaxed); - // Read enable state let isenabler = gicd_read(GICD_ISENABLER + (reg_index as usize * 4)); let enabled = (isenabler & (1 << bit_index)) != 0; - - // Read group state let igroupr = gicd_read(GICD_IGROUPR + (reg_index as usize * 4)); let group1 = (igroupr & (1 << bit_index)) != 0; - - // Read pending state let ispendr = gicd_read(GICD_ISPENDR + (reg_index as usize * 4)); let pending = (ispendr & (1 << bit_index)) != 0; - // Read priority (4 IRQs per register, 8 bits each) let priority_reg_index = irq / 4; let priority_byte_index = irq % 4; let ipriorityr = gicd_read(GICD_IPRIORITYR + (priority_reg_index as usize * 4)); let priority = ((ipriorityr >> (priority_byte_index * 8)) & 0xFF) as u8; - // Read target (4 IRQs per register, 8 bits each) - let target_reg_index = irq / 4; - let target_byte_index = irq % 4; - let itargetsr = gicd_read(GICD_ITARGETSR + (target_reg_index as usize * 4)); - let target = ((itargetsr >> (target_byte_index * 8)) & 0xFF) as u8; - - // Read distributor control let gicd_ctlr = gicd_read(GICD_CTLR); - // Read CPU interface control and priority mask - let gicc_ctlr = gicc_read(GICC_CTLR); - let gicc_pmr = gicc_read(GICC_PMR); - - crate::serial_println!("[gic] IRQ {} state:", irq); + crate::serial_println!("[gic] IRQ {} state (GICv{}):", irq, version); crate::serial_println!(" enabled={}, group1={}, pending={}", enabled, group1, pending); - crate::serial_println!(" priority={:#x}, target={:#x}", priority, target); - crate::serial_println!(" GICD_CTLR={:#x}, GICC_CTLR={:#x}, GICC_PMR={:#x}", gicd_ctlr, gicc_ctlr, gicc_pmr); + crate::serial_println!(" priority={:#x}, GICD_CTLR={:#x}", priority, gicd_ctlr); + + if version >= 3 { + let pmr: u64; + unsafe { core::arch::asm!("mrs {}, icc_pmr_el1", out(reg) pmr, options(nomem, nostack)); } + crate::serial_println!(" ICC_PMR={:#x}", pmr); + } else { + let gicc_ctlr = gicc_read(GICC_CTLR); + let gicc_pmr = gicc_read(GICC_PMR); + crate::serial_println!(" GICC_CTLR={:#x}, GICC_PMR={:#x}", gicc_ctlr, gicc_pmr); + } +} + +// ============================================================================= +// GICv3 Constants and Initialization +// ============================================================================= + +/// GICR (Redistributor) register offsets. +/// Each redistributor has two 64KB frames: RD_base and SGI_base. +const GICR_FRAME_SIZE: usize = 0x2_0000; // 128KB per CPU (2 x 64KB frames) +const GICR_SGI_OFFSET: usize = 0x1_0000; // SGI_base is second 64KB frame + +/// GICR RD_base registers +const GICR_CTLR: usize = 0x000; +const GICR_WAKER: usize = 0x014; +const GICR_TYPER: usize = 0x008; + +/// GICR SGI_base registers (at GICR_SGI_OFFSET from RD_base) +const GICR_IGROUPR0: usize = 0x080; +const GICR_ISENABLER0: usize = 0x100; +const GICR_ICENABLER0: usize = 0x180; +const GICR_IPRIORITYR0: usize = 0x400; +const GICR_ICFGR0: usize = 0xC00; + +/// GICD register for SPI routing (GICv3) +const GICD_IROUTER: usize = 0x6100; + +/// GICv3 GICD_CTLR bits +const GICD_CTLR_ARE_NS: u32 = 1 << 4; // Affinity Routing Enable (Non-Secure) +const GICD_CTLR_ENABLE_GRP1_NS: u32 = 1 << 1; // Enable Group 1 Non-Secure + +/// Get current CPU's linear ID from MPIDR_EL1. +/// For simple SMP, Aff0 is the CPU number. +fn get_cpu_id_from_mpidr() -> usize { + let mpidr: u64; + unsafe { core::arch::asm!("mrs {}, mpidr_el1", out(reg) mpidr, options(nomem, nostack)); } + (mpidr & 0xFF) as usize +} + +/// Initialize GICv3 Distributor (GICD). +fn init_gicv3_distributor() { + // Disable distributor + gicd_write(GICD_CTLR, 0); + + let num_irqs = { + let typer = gicd_read(GICD_TYPER); + ((typer & 0x1F) + 1) * 32 + }; + + // Disable all SPIs + let num_regs = (num_irqs + 31) / 32; + for i in 1..num_regs { + // Skip reg 0 (SGI/PPI, handled by GICR) + gicd_write(GICD_ICENABLER + (i as usize * 4), 0xFFFF_FFFF); + } + + // Clear all pending SPIs + for i in 1..num_regs { + gicd_write(GICD_ICPENDR + (i as usize * 4), 0xFFFF_FFFF); + } + + // Set all SPIs to Group 1 Non-Secure + for i in 1..num_regs { + gicd_write(GICD_IGROUPR + (i as usize * 4), 0xFFFF_FFFF); + } + + // Set default priority for all SPIs + let num_priority_regs = (num_irqs + 3) / 4; + let priority_val = (DEFAULT_PRIORITY as u32) * 0x0101_0101; + for i in 8..num_priority_regs { + // Skip first 8 regs (SGI/PPI priorities in GICR) + gicd_write(GICD_IPRIORITYR + (i as usize * 4), priority_val); + } + + // Enable distributor with ARE_NS and Group 1 NS + gicd_write(GICD_CTLR, GICD_CTLR_ARE_NS | GICD_CTLR_ENABLE_GRP1_NS); +} + +/// Initialize GICv3 Redistributor (GICR) for a specific CPU. +fn init_gicv3_redistributor(cpu_id: usize) { + let cpu_offset = cpu_id * GICR_FRAME_SIZE; + + // Wake up the redistributor + let waker = gicr_read(cpu_offset, GICR_WAKER); + gicr_write(cpu_offset, GICR_WAKER, waker & !(1 << 1)); // Clear ProcessorSleep + + // Wait for ChildrenAsleep to clear + for _ in 0..10_000 { + let w = gicr_read(cpu_offset, GICR_WAKER); + if (w & (1 << 2)) == 0 { + break; + } + core::hint::spin_loop(); + } + + let sgi_base = cpu_offset + GICR_SGI_OFFSET; + + // Configure SGIs (0-15) and PPIs (16-31) in the redistributor + + // Set all SGI/PPI to Group 1 + gicr_write(sgi_base, GICR_IGROUPR0, 0xFFFF_FFFF); + + // Disable all SGI/PPI first + gicr_write(sgi_base, GICR_ICENABLER0, 0xFFFF_FFFF); + + // Set default priority for SGIs and PPIs + for i in 0..8u32 { + let priority_val = (DEFAULT_PRIORITY as u32) * 0x0101_0101; + gicr_write(sgi_base, GICR_IPRIORITYR0 + (i as usize * 4), priority_val); + } + + // Configure PPIs as level-triggered (default) + gicr_write(sgi_base, GICR_ICFGR0, 0); // SGIs: always edge + gicr_write(sgi_base, GICR_ICFGR0 + 4, 0); // PPIs: level-triggered +} + +/// Initialize GICv3 CPU Interface via ICC system registers. +fn init_gicv3_cpu_interface() { + unsafe { + // Enable system register interface (ICC_SRE_EL1) + let sre: u64 = 0x7; // SRE | DFB | DIB + core::arch::asm!("msr icc_sre_el1, {}", in(reg) sre, options(nomem, nostack)); + core::arch::asm!("isb", options(nomem, nostack)); + + // Set priority mask to accept all (ICC_PMR_EL1) + let pmr: u64 = PRIORITY_MASK as u64; + core::arch::asm!("msr icc_pmr_el1, {}", in(reg) pmr, options(nomem, nostack)); + + // No preemption (ICC_BPR1_EL1) + let bpr: u64 = 7; + core::arch::asm!("msr icc_bpr1_el1, {}", in(reg) bpr, options(nomem, nostack)); + + // Enable Group 1 interrupts (ICC_IGRPEN1_EL1) + let grpen: u64 = 1; + core::arch::asm!("msr icc_igrpen1_el1, {}", in(reg) grpen, options(nomem, nostack)); + + core::arch::asm!("isb", options(nomem, nostack)); + } +} + +/// Get the active GIC version (for diagnostic purposes). +pub fn active_version() -> u8 { + ACTIVE_GIC_VERSION.load(Ordering::Relaxed) } diff --git a/kernel/src/arch_impl/aarch64/smp.rs b/kernel/src/arch_impl/aarch64/smp.rs index 5f9cd1dd..917848d3 100644 --- a/kernel/src/arch_impl/aarch64/smp.rs +++ b/kernel/src/arch_impl/aarch64/smp.rs @@ -108,11 +108,9 @@ pub fn is_cpu_online(cpu_id: usize) -> bool { /// Raw UART output for secondary CPUs (no locks, no allocations). #[inline(always)] fn raw_uart_char(c: u8) { - const HHDM_BASE: u64 = 0xFFFF_0000_0000_0000; - const UART_VIRT: u64 = HHDM_BASE + 0x0900_0000; + let addr = crate::platform_config::uart_virt() as *mut u8; unsafe { - let ptr = UART_VIRT as *mut u8; - core::ptr::write_volatile(ptr, c); + core::ptr::write_volatile(addr, c); } } diff --git a/kernel/src/arch_impl/aarch64/syscall_entry.S b/kernel/src/arch_impl/aarch64/syscall_entry.S index 36479378..135b5050 100644 --- a/kernel/src/arch_impl/aarch64/syscall_entry.S +++ b/kernel/src/arch_impl/aarch64/syscall_entry.S @@ -329,10 +329,11 @@ syscall_entry_from_el0: /* Clear next_cr3 BEFORE switching (avoid accessing after switch) */ str xzr, [x0, #64] - /* Perform TTBR0 switch with proper barriers */ + /* Perform TTBR0 switch with full TLB invalidation */ + dsb ishst msr ttbr0_el1, x1 isb - tlbi vmalle1is /* Invalidate TLB */ + tlbi vmalle1is dsb ish isb b .Lafter_ttbr_check @@ -341,8 +342,12 @@ syscall_entry_from_el0: /* No context switch - restore original process TTBR0 */ ldr x1, [x0, #80] /* saved_process_cr3 offset = 80 */ cbz x1, .Lafter_ttbr_check + dsb ishst msr ttbr0_el1, x1 isb + tlbi vmalle1is + dsb ish + isb .Lafter_ttbr_check: .Lno_ttbr_switch: @@ -423,8 +428,9 @@ syscall_return_to_userspace_aarch64: ldr x10, [x9, #64] /* next_cr3 offset = 64 */ cbz x10, .Lfirst_entry_restore_ttbr - /* Clear next_cr3 and switch */ + /* Clear next_cr3 and switch with full TLB invalidation */ str xzr, [x9, #64] + dsb ishst msr ttbr0_el1, x10 isb tlbi vmalle1is @@ -436,8 +442,12 @@ syscall_return_to_userspace_aarch64: /* Try saved process TTBR0 */ ldr x10, [x9, #80] cbz x10, .Lfirst_entry_after_ttbr + dsb ishst msr ttbr0_el1, x10 isb + tlbi vmalle1is + dsb ish + isb .Lfirst_entry_after_ttbr: .Lfirst_entry_no_ttbr: diff --git a/kernel/src/arch_impl/aarch64/syscall_entry.rs b/kernel/src/arch_impl/aarch64/syscall_entry.rs index 457a8274..a6113177 100644 --- a/kernel/src/arch_impl/aarch64/syscall_entry.rs +++ b/kernel/src/arch_impl/aarch64/syscall_entry.rs @@ -44,16 +44,12 @@ pub fn is_el0_confirmed() -> bool { /// Also advances test framework to Userspace stage if boot_tests is enabled. #[inline(never)] fn emit_el0_syscall_marker() { - // PL011 UART virtual address (physical 0x0900_0000 mapped via HHDM) - // The HHDM base is 0xFFFF_0000_0000_0000, so UART is at that + 0x0900_0000 - const HHDM_BASE: u64 = 0xFFFF_0000_0000_0000; - const PL011_PHYS: u64 = 0x0900_0000; - const PL011_VIRT: u64 = HHDM_BASE + PL011_PHYS; + let uart_addr = crate::platform_config::uart_virt(); let msg = b"EL0_SYSCALL: First syscall from userspace (SPSR confirms EL0)\n[ OK ] syscall path verified\n"; for &byte in msg { unsafe { - core::ptr::write_volatile(PL011_VIRT as *mut u8, byte); + core::ptr::write_volatile(uart_addr as *mut u8, byte); } } @@ -235,14 +231,13 @@ fn check_and_deliver_signals_aarch64(frame: &mut Aarch64ExceptionFrame) { if let Some(ref page_table) = process.page_table { let ttbr0 = page_table.level_4_frame().start_address().as_u64(); unsafe { - // CRITICAL: Flush TLB after TTBR0 switch for CoW correctness core::arch::asm!( - "dsb ishst", // Ensure previous stores complete - "msr ttbr0_el1, {}", // Set new page table - "isb", // Synchronize context - "tlbi vmalle1is", // FLUSH ENTIRE TLB - "dsb ish", // Ensure TLB flush completes - "isb", // Synchronize instruction stream + "dsb ishst", + "msr ttbr0_el1, {}", + "isb", + "tlbi vmalle1is", + "dsb ish", + "isb", in(reg) ttbr0, options(nostack) ); diff --git a/kernel/src/arch_impl/aarch64/timer_interrupt.rs b/kernel/src/arch_impl/aarch64/timer_interrupt.rs index fe8ab7da..a4d749a5 100644 --- a/kernel/src/arch_impl/aarch64/timer_interrupt.rs +++ b/kernel/src/arch_impl/aarch64/timer_interrupt.rs @@ -173,9 +173,13 @@ pub extern "C" fn timer_interrupt_handler() { // Increment timer interrupt counter (used for debugging when needed) let _count = TIMER_INTERRUPT_COUNT.fetch_add(1, Ordering::Relaxed) + 1; - // CPU 0 only: poll VirtIO keyboard (single-device, not safe from multiple CPUs) + // CPU 0 only: poll input devices (single-device, not safe from multiple CPUs) if cpu_id == 0 { poll_keyboard_to_stdin(); + // Poll EHCI USB 2.0 keyboard events + crate::drivers::usb::ehci::poll_keyboard(); + // Poll XHCI USB HID events (needed when PCI interrupt routing isn't available) + crate::drivers::usb::xhci::poll_hid_events(); } // CPU 0 only: soft lockup detector @@ -215,6 +219,38 @@ pub extern "C" fn timer_interrupt_handler() { print_timer_count_decimal(crate::arch_impl::aarch64::context_switch::TTBR_PROCESS_GONE_COUNT.load(Ordering::Relaxed)); raw_serial_str(b" tlb="); print_timer_count_decimal(crate::arch_impl::aarch64::context_switch::TTBR_PM_LOCK_BUSY_COUNT.load(Ordering::Relaxed)); + raw_serial_str(b" up="); + print_timer_count_decimal(crate::drivers::usb::xhci::POLL_COUNT.load(Ordering::Relaxed)); + raw_serial_str(b" ue="); + print_timer_count_decimal(crate::drivers::usb::xhci::EVENT_COUNT.load(Ordering::Relaxed)); + raw_serial_str(b" uk="); + print_timer_count_decimal(crate::drivers::usb::xhci::KBD_EVENT_COUNT.load(Ordering::Relaxed)); + raw_serial_str(b" xo="); + print_timer_count_decimal(crate::drivers::usb::xhci::XFER_OTHER_COUNT.load(Ordering::Relaxed)); + raw_serial_str(b" mi="); + print_timer_count_decimal(crate::drivers::usb::xhci::MSI_EVENT_COUNT.load(Ordering::Relaxed)); + raw_serial_str(b" psc="); + print_timer_count_decimal(crate::drivers::usb::xhci::PSC_COUNT.load(Ordering::Relaxed)); + raw_serial_str(b" r="); + print_timer_count_decimal(crate::drivers::usb::xhci::EP0_RESET_COUNT.load(Ordering::Relaxed)); + raw_serial_str(b" rf="); + print_timer_count_decimal(crate::drivers::usb::xhci::EP0_RESET_FAIL_COUNT.load(Ordering::Relaxed)); + raw_serial_str(b" ps="); + print_timer_count_decimal(crate::drivers::usb::xhci::EP0_PENDING_STUCK_COUNT.load(Ordering::Relaxed)); + raw_serial_str(b" nz="); + print_timer_count_decimal(crate::drivers::usb::hid::NONZERO_KBD_COUNT.load(Ordering::Relaxed)); + raw_serial_str(b" lr="); + print_hex_u64(crate::drivers::usb::hid::LAST_KBD_REPORT_U64.load(Ordering::Relaxed)); + raw_serial_str(b" DS="); + print_timer_count_decimal(crate::drivers::usb::xhci::DMA_SENTINEL_SURVIVED.load(Ordering::SeqCst)); + raw_serial_str(b" DR="); + print_timer_count_decimal(crate::drivers::usb::xhci::DMA_SENTINEL_REPLACED.load(Ordering::SeqCst)); + raw_serial_str(b" ec="); + print_timer_count_decimal(crate::drivers::usb::ehci::EHCI_CTL_COMPLETIONS.load(Ordering::Relaxed) as u64); + raw_serial_str(b" ee="); + print_timer_count_decimal(crate::drivers::usb::ehci::EHCI_CTL_ERRORS.load(Ordering::Relaxed) as u64); + raw_serial_str(b" ei="); + print_timer_count_decimal(crate::drivers::usb::ehci::EHCI_INT_COMPLETIONS.load(Ordering::Relaxed) as u64); raw_serial_str(b"]\n"); } } @@ -283,6 +319,15 @@ fn print_timer_count_decimal(count: u64) { } } +/// Print a u64 as 16-char zero-padded hexadecimal using raw serial output. +fn print_hex_u64(val: u64) { + const HEX: [u8; 16] = *b"0123456789abcdef"; + // Print 16 hex digits (big-endian nibble order) + for i in (0..16).rev() { + raw_serial_char(HEX[((val >> (i * 4)) & 0xF) as usize]); + } +} + /// Check for soft lockup (CPU 0 only, called from timer interrupt). /// /// Compares the current context switch count against the last observed value. diff --git a/kernel/src/drivers/ahci/mod.rs b/kernel/src/drivers/ahci/mod.rs new file mode 100644 index 00000000..445dfc76 --- /dev/null +++ b/kernel/src/drivers/ahci/mod.rs @@ -0,0 +1,1092 @@ +//! AHCI (Advanced Host Controller Interface) Storage Driver +//! +//! Implements the AHCI specification for SATA storage access. +//! Used on Parallels Desktop (ARM64) where storage is AHCI-based +//! rather than VirtIO block. +//! +//! # Architecture +//! +//! AHCI exposes a Host Bus Adapter (HBA) via PCI BAR5 (ABAR). +//! The HBA manages up to 32 ports, each connected to a SATA device. +//! Communication uses DMA with command lists and FIS (Frame Information +//! Structures) in host memory. +//! +//! # Memory Layout (per port) +//! +//! - Command List: 1 KB (32 × 32-byte command headers) +//! - Received FIS: 256 bytes +//! - Command Tables: 256 bytes each (CFIS + PRDT) + +#![allow(dead_code)] + +use core::sync::atomic::{AtomicBool, Ordering}; +use spin::Mutex; + +use crate::block::{BlockDevice, BlockError}; +use crate::drivers::pci; + +/// HHDM base for memory-mapped access. +const HHDM_BASE: u64 = 0xFFFF_0000_0000_0000; + +/// Convert a kernel virtual address to a physical address. +/// +/// On QEMU, kernel statics are accessed via HHDM (>= 0xFFFF_0000_0000_0000), +/// so phys = virt - HHDM_BASE. +/// On Parallels, the kernel runs identity-mapped via TTBR0, so statics are +/// at their physical addresses already (e.g., 0x400xxxxx). +#[inline] +fn virt_to_phys(virt: u64) -> u64 { + if virt >= HHDM_BASE { + virt - HHDM_BASE + } else { + virt // Already a physical address (identity-mapped kernel) + } +} + +/// Clean (flush) a range of memory from CPU caches to the point of coherency. +/// +/// Must be called after writing DMA descriptors/data and before issuing +/// DMA commands, so the device sees the updated data in physical memory. +#[cfg(target_arch = "aarch64")] +fn dma_cache_clean(ptr: *const u8, len: usize) { + const CACHE_LINE: usize = 64; + let start = ptr as usize & !(CACHE_LINE - 1); + let end = (ptr as usize + len + CACHE_LINE - 1) & !(CACHE_LINE - 1); + for addr in (start..end).step_by(CACHE_LINE) { + unsafe { + core::arch::asm!("dc cvac, {}", in(reg) addr, options(nostack)); + } + } + unsafe { + core::arch::asm!("dsb sy", options(nostack, preserves_flags)); + } +} + +/// Invalidate a range of memory in CPU caches after a device DMA write. +/// +/// Must be called after a DMA read completes and before the CPU reads +/// the DMA buffer, to ensure the CPU sees the device-written data. +#[cfg(target_arch = "aarch64")] +fn dma_cache_invalidate(ptr: *const u8, len: usize) { + const CACHE_LINE: usize = 64; + let start = ptr as usize & !(CACHE_LINE - 1); + let end = (ptr as usize + len + CACHE_LINE - 1) & !(CACHE_LINE - 1); + for addr in (start..end).step_by(CACHE_LINE) { + unsafe { + core::arch::asm!("dc civac, {}", in(reg) addr, options(nostack)); + } + } + unsafe { + core::arch::asm!("dsb sy", options(nostack, preserves_flags)); + } +} + +/// No-op cache maintenance on x86_64 (DMA coherent by default). +#[cfg(not(target_arch = "aarch64"))] +#[inline] +fn dma_cache_clean(_ptr: *const u8, _len: usize) {} + +/// No-op cache maintenance on x86_64 (DMA coherent by default). +#[cfg(not(target_arch = "aarch64"))] +#[inline] +fn dma_cache_invalidate(_ptr: *const u8, _len: usize) {} + +/// Sector size in bytes (standard for SATA). +pub const SECTOR_SIZE: usize = 512; + +/// Maximum number of AHCI ports. +const MAX_PORTS: usize = 32; + +/// Maximum number of command slots per port. +const MAX_CMD_SLOTS: usize = 32; + +/// AHCI port register block size. +const PORT_REG_SIZE: usize = 0x80; + +/// Whether AHCI has been initialized. +static AHCI_INITIALIZED: AtomicBool = AtomicBool::new(false); + +/// Global AHCI controller state. +static AHCI_CONTROLLER: Mutex> = Mutex::new(None); + +// ============================================================================= +// HBA Generic Host Control Registers (offset from ABAR) +// ============================================================================= + +/// Host Capabilities +const HBA_CAP: usize = 0x00; +/// Global Host Control +const HBA_GHC: usize = 0x04; +/// Interrupt Status +const HBA_IS: usize = 0x08; +/// Ports Implemented +const HBA_PI: usize = 0x0C; +/// Version +const HBA_VS: usize = 0x10; + +/// GHC bits +const GHC_HR: u32 = 1 << 0; // HBA Reset +const GHC_IE: u32 = 1 << 1; // Interrupt Enable +const GHC_AE: u32 = 1 << 31; // AHCI Enable + +// ============================================================================= +// Port Registers (offset from ABAR + 0x100 + port * 0x80) +// ============================================================================= + +/// Command List Base Address (low) +const PORT_CLB: usize = 0x00; +/// Command List Base Address (high) +const PORT_CLBU: usize = 0x04; +/// FIS Base Address (low) +const PORT_FB: usize = 0x08; +/// FIS Base Address (high) +const PORT_FBU: usize = 0x0C; +/// Interrupt Status +const PORT_IS: usize = 0x10; +/// Interrupt Enable +const PORT_IE: usize = 0x14; +/// Command and Status +const PORT_CMD: usize = 0x18; +/// Task File Data +const PORT_TFD: usize = 0x20; +/// Signature +const PORT_SIG: usize = 0x24; +/// SATA Status (SCR0: SStatus) +const PORT_SSTS: usize = 0x28; +/// SATA Control (SCR2: SControl) +const PORT_SCTL: usize = 0x2C; +/// SATA Error (SCR1: SError) +const PORT_SERR: usize = 0x30; +/// SATA Active +const PORT_SACT: usize = 0x34; +/// Command Issue +const PORT_CI: usize = 0x38; + +/// PORT_CMD bits +const PORT_CMD_ST: u32 = 1 << 0; // Start +const PORT_CMD_FRE: u32 = 1 << 4; // FIS Receive Enable +const PORT_CMD_FR: u32 = 1 << 14; // FIS Receive Running +const PORT_CMD_CR: u32 = 1 << 15; // Command List Running + +/// PORT_TFD bits +const PORT_TFD_BSY: u32 = 1 << 7; // Busy +const PORT_TFD_DRQ: u32 = 1 << 3; // Data Request + +/// SATA Status (SSTS) - device detection +const SSTS_DET_MASK: u32 = 0x0F; +const SSTS_DET_PRESENT: u32 = 0x03; // Device detected, Phy communication established + +/// Device signatures +const SIG_SATA: u32 = 0x00000101; // SATA drive +const SIG_ATAPI: u32 = 0xEB140101; // SATAPI device + +// ============================================================================= +// FIS Types +// ============================================================================= + +/// Host to Device FIS type +const FIS_TYPE_REG_H2D: u8 = 0x27; + +// ============================================================================= +// ATA Commands +// ============================================================================= + +/// IDENTIFY DEVICE +const ATA_CMD_IDENTIFY: u8 = 0xEC; +/// READ DMA EXT (48-bit LBA) +const ATA_CMD_READ_DMA_EXT: u8 = 0x25; +/// WRITE DMA EXT (48-bit LBA) +const ATA_CMD_WRITE_DMA_EXT: u8 = 0x35; +/// FLUSH CACHE EXT +const ATA_CMD_FLUSH_EXT: u8 = 0xEA; + +// ============================================================================= +// DMA Memory Structures +// ============================================================================= + +/// Command List entry (Command Header) - 32 bytes each, 32 per port. +#[repr(C, packed)] +struct CmdHeader { + /// DW0: Command FIS length (bits 4:0), ATAPI (bit 5), Write (bit 6), Prefetchable (bit 7) + /// Reset (bit 8), BIST (bit 9), Clear BSY on R_OK (bit 10), Port Multiplier (15:12) + /// PRDTL (31:16) = Physical Region Descriptor Table Length + dw0: u32, + /// DW1: Physical Region Descriptor Byte Count (bytes transferred) + prdbc: u32, + /// DW2: Command Table Descriptor Base Address (low) + ctba: u32, + /// DW3: Command Table Descriptor Base Address (high) + ctbau: u32, + /// DW4-7: Reserved + _reserved: [u32; 4], +} + +/// Physical Region Descriptor Table entry - 16 bytes. +#[repr(C, packed)] +struct PrdtEntry { + /// Data Base Address (low) + dba: u32, + /// Data Base Address (high) + dbau: u32, + /// Reserved + _reserved: u32, + /// Data Byte Count (bits 21:0) and Interrupt on Completion (bit 31) + dbc: u32, +} + +/// Host to Device FIS (Register) - 20 bytes. +#[repr(C, packed)] +struct FisRegH2d { + /// FIS type (0x27) + fis_type: u8, + /// Port multiplier (bits 3:0), reserved (bits 6:4), C bit (bit 7) = command/control + pmport_c: u8, + /// Command register + command: u8, + /// Feature register (low) + featurel: u8, + /// LBA low + lba0: u8, + /// LBA mid + lba1: u8, + /// LBA high + lba2: u8, + /// Device register + device: u8, + /// LBA low (exp) + lba3: u8, + /// LBA mid (exp) + lba4: u8, + /// LBA high (exp) + lba5: u8, + /// Feature register (high) + featureh: u8, + /// Count (low) + countl: u8, + /// Count (high) + counth: u8, + /// Isochronous Command Completion + icc: u8, + /// Control register + control: u8, + /// Reserved + _reserved: [u8; 4], +} + +/// Command Table - contains the Command FIS and PRDT entries. +/// We use a fixed single-PRDT layout for simplicity. +#[repr(C, align(128))] +struct CmdTable { + /// Command FIS (up to 64 bytes) + cfis: [u8; 64], + /// ATAPI Command (16 bytes) + acmd: [u8; 16], + /// Reserved (48 bytes) + _reserved: [u8; 48], + /// PRDT entries (we use 1 entry for single-sector operations) + prdt: [PrdtEntry; 1], +} + +/// Received FIS structure - 256 bytes per port. +#[repr(C, align(256))] +struct ReceivedFis { + data: [u8; 256], +} + +/// Per-port DMA memory allocation. +/// +/// All memory must be physically contiguous and accessible via DMA. +/// We use static allocations with known physical addresses. +#[repr(C, align(4096))] +struct PortDmaMem { + /// Command list (32 headers × 32 bytes = 1024 bytes) + cmd_list: [CmdHeader; MAX_CMD_SLOTS], + /// Received FIS area + received_fis: ReceivedFis, + /// Command table for slot 0 (we only use slot 0 for simplicity) + cmd_table: CmdTable, + /// DMA buffer for sector I/O (one sector) + dma_buf: [u8; SECTOR_SIZE], +} + +/// Static DMA memory for up to 2 ports. +/// These are page-aligned for DMA safety. +const MAX_AHCI_PORTS: usize = 4; +static PORT_DMA: Mutex<[Option<&'static mut PortDmaMem>; MAX_AHCI_PORTS]> = + Mutex::new([const { None }; MAX_AHCI_PORTS]); + +// We use a static array for DMA memory so we know the physical addresses. +#[repr(C, align(4096))] +struct PortDmaStorage { + ports: [PortDmaMem; MAX_AHCI_PORTS], +} + +static mut DMA_STORAGE: PortDmaStorage = unsafe { core::mem::zeroed() }; + +// ============================================================================= +// AHCI Controller +// ============================================================================= + +/// AHCI controller state. +struct AhciController { + /// Virtual base address of the HBA registers (ABAR via HHDM) + abar_virt: u64, + /// Number of command slots supported + num_cmd_slots: u32, + /// Bitmask of implemented ports + ports_implemented: u32, + /// Port states + ports: [Option; MAX_PORTS], +} + +/// Per-port state. +struct AhciPort { + /// Port number (0-31) + port_num: usize, + /// Device type + device_type: DeviceType, + /// Sector count (from IDENTIFY DEVICE) + sector_count: u64, + /// DMA memory index in DMA_STORAGE + dma_index: usize, +} + +/// AHCI device type. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum DeviceType { + Sata, + Atapi, + Unknown, +} + +// ============================================================================= +// Register Access Helpers +// ============================================================================= + +#[inline] +fn hba_read(abar: u64, offset: usize) -> u32 { + unsafe { core::ptr::read_volatile((abar + offset as u64) as *const u32) } +} + +#[inline] +fn hba_write(abar: u64, offset: usize, value: u32) { + unsafe { core::ptr::write_volatile((abar + offset as u64) as *mut u32, value) } +} + +#[inline] +fn port_base(abar: u64, port: usize) -> u64 { + abar + 0x100 + (port as u64) * PORT_REG_SIZE as u64 +} + +#[inline] +fn port_read(abar: u64, port: usize, offset: usize) -> u32 { + hba_read(port_base(abar, port), offset) +} + +#[inline] +fn port_write(abar: u64, port: usize, offset: usize, value: u32) { + hba_write(port_base(abar, port), offset, value) +} + +// ============================================================================= +// Controller Implementation +// ============================================================================= + +impl AhciController { + /// Create and initialize an AHCI controller from a PCI device. + fn init(pci_dev: &pci::Device) -> Result { + // AHCI uses BAR5 (ABAR) + let bar5 = &pci_dev.bars[5]; + if !bar5.is_valid() || bar5.is_io { + return Err("AHCI: BAR5 not valid or not MMIO"); + } + + let abar_virt = HHDM_BASE + bar5.address; + + // Enable PCI bus mastering and memory space + pci_dev.enable_bus_master(); + pci_dev.enable_memory_space(); + + Self::init_common(abar_virt) + } + + /// Create and initialize an AHCI controller from a known MMIO base address. + /// + /// Used for platform devices (e.g., Parallels Desktop) where the AHCI + /// controller is not on the PCI bus but at a fixed MMIO address. + fn init_from_mmio(abar_phys: u64) -> Result { + let abar_virt = HHDM_BASE + abar_phys; + + crate::serial_println!("[ahci] Platform AHCI at phys {:#x}, virt {:#x}", abar_phys, abar_virt); + + Self::init_common(abar_virt) + } + + /// Common AHCI controller initialization. + /// + /// Enables AHCI mode, reads capabilities, discovers ports, and + /// issues IDENTIFY DEVICE to each connected SATA drive. + fn init_common(abar_virt: u64) -> Result { + // Enable AHCI mode + let ghc = hba_read(abar_virt, HBA_GHC); + hba_write(abar_virt, HBA_GHC, ghc | GHC_AE); + + // Read capabilities + let cap = hba_read(abar_virt, HBA_CAP); + let num_cmd_slots = ((cap >> 8) & 0x1F) + 1; + let num_ports = (cap & 0x1F) + 1; + let ports_implemented = hba_read(abar_virt, HBA_PI); + let version = hba_read(abar_virt, HBA_VS); + + crate::serial_println!( + "[ahci] HBA version {}.{}, {} ports, {} cmd slots, PI={:#010x}", + version >> 16, + version & 0xFFFF, + num_ports, + num_cmd_slots, + ports_implemented, + ); + + // Initialize DMA memory references + let dma_storage_ptr = &raw mut DMA_STORAGE; + let mut dma_lock = PORT_DMA.lock(); + for i in 0..MAX_AHCI_PORTS { + dma_lock[i] = Some(unsafe { &mut (*dma_storage_ptr).ports[i] }); + } + drop(dma_lock); + + let mut controller = AhciController { + abar_virt, + num_cmd_slots, + ports_implemented, + ports: core::array::from_fn(|_| None), + }; + + // Discover and initialize ports + let mut dma_index = 0; + for port_num in 0..MAX_PORTS { + if (ports_implemented & (1 << port_num)) == 0 { + continue; + } + if dma_index >= MAX_AHCI_PORTS { + crate::serial_println!("[ahci] Warning: more ports than DMA slots, skipping port {}", port_num); + continue; + } + + if let Some(port) = controller.init_port(port_num, dma_index) { + crate::serial_println!( + "[ahci] Port {}: {:?}, {} sectors ({} MB)", + port_num, + port.device_type, + port.sector_count, + port.sector_count * SECTOR_SIZE as u64 / (1024 * 1024), + ); + controller.ports[port_num] = Some(port); + dma_index += 1; + } + } + + Ok(controller) + } + + /// Initialize a single port. Returns None if no device is present. + fn init_port(&self, port_num: usize, dma_index: usize) -> Option { + let abar = self.abar_virt; + + // Check SATA Status for device presence + let ssts = port_read(abar, port_num, PORT_SSTS); + if (ssts & SSTS_DET_MASK) != SSTS_DET_PRESENT { + return None; + } + + // Stop command engine before reconfiguring + self.stop_cmd(port_num); + + // Set up DMA memory for this port + let dma_phys = unsafe { + let storage = &raw const DMA_STORAGE; + let port_dma_addr = core::ptr::addr_of!((*storage).ports[dma_index]); + // Physical address = virtual address - HHDM (identity mapped in our page tables) + // For kernel static data, physical addr = virt addr - kernel base + // But since we're using a static, we compute it differently. + // The DMA storage is at a known kernel static address. + // On ARM64, kernel statics are at HHDM + physical, so: + virt_to_phys(port_dma_addr as u64) + }; + + // Zero the DMA memory and flush to physical RAM + let dma_lock = PORT_DMA.lock(); + if let Some(dma_mem) = &dma_lock[dma_index] { + let ptr = *dma_mem as *const PortDmaMem as *mut u8; + let size = core::mem::size_of::(); + unsafe { + core::ptr::write_bytes(ptr, 0, size); + } + dma_cache_clean(ptr as *const u8, size); + } + drop(dma_lock); + + // Command List Base + let clb_phys = dma_phys; + port_write(abar, port_num, PORT_CLB, clb_phys as u32); + port_write(abar, port_num, PORT_CLBU, (clb_phys >> 32) as u32); + + // FIS Base + let fb_phys = dma_phys + core::mem::offset_of!(PortDmaMem, received_fis) as u64; + port_write(abar, port_num, PORT_FB, fb_phys as u32); + port_write(abar, port_num, PORT_FBU, (fb_phys >> 32) as u32); + + // Clear interrupt status and error + port_write(abar, port_num, PORT_IS, 0xFFFF_FFFF); + port_write(abar, port_num, PORT_SERR, 0xFFFF_FFFF); + + // Start command engine + self.start_cmd(port_num); + + // Determine device type from signature + let sig = port_read(abar, port_num, PORT_SIG); + let device_type = match sig { + SIG_SATA => DeviceType::Sata, + SIG_ATAPI => DeviceType::Atapi, + _ => DeviceType::Unknown, + }; + + // For SATA devices, issue IDENTIFY DEVICE to get sector count + let sector_count = if device_type == DeviceType::Sata { + self.identify_device(port_num, dma_index).unwrap_or(0) + } else { + 0 + }; + + Some(AhciPort { + port_num, + device_type, + sector_count, + dma_index, + }) + } + + /// Stop the command engine for a port. + fn stop_cmd(&self, port: usize) { + let abar = self.abar_virt; + let mut cmd = port_read(abar, port, PORT_CMD); + + // Clear ST (Start) + cmd &= !PORT_CMD_ST; + port_write(abar, port, PORT_CMD, cmd); + + // Wait for CR (Command List Running) to clear + for _ in 0..1_000_000 { + if (port_read(abar, port, PORT_CMD) & PORT_CMD_CR) == 0 { + break; + } + core::hint::spin_loop(); + } + + // Clear FRE (FIS Receive Enable) + cmd = port_read(abar, port, PORT_CMD); + cmd &= !PORT_CMD_FRE; + port_write(abar, port, PORT_CMD, cmd); + + // Wait for FR (FIS Receive Running) to clear + for _ in 0..1_000_000 { + if (port_read(abar, port, PORT_CMD) & PORT_CMD_FR) == 0 { + break; + } + core::hint::spin_loop(); + } + } + + /// Start the command engine for a port. + fn start_cmd(&self, port: usize) { + let abar = self.abar_virt; + + // Wait for CR to clear + for _ in 0..1_000_000 { + if (port_read(abar, port, PORT_CMD) & PORT_CMD_CR) == 0 { + break; + } + core::hint::spin_loop(); + } + + // Enable FRE, then ST + let mut cmd = port_read(abar, port, PORT_CMD); + cmd |= PORT_CMD_FRE; + port_write(abar, port, PORT_CMD, cmd); + + cmd |= PORT_CMD_ST; + port_write(abar, port, PORT_CMD, cmd); + } + + /// Wait for port to be not busy. + fn wait_ready(&self, port: usize) -> Result<(), &'static str> { + let abar = self.abar_virt; + for _ in 0..1_000_000 { + let tfd = port_read(abar, port, PORT_TFD); + if (tfd & (PORT_TFD_BSY | PORT_TFD_DRQ)) == 0 { + return Ok(()); + } + core::hint::spin_loop(); + } + Err("AHCI: port busy timeout") + } + + /// Issue a command on slot 0 and wait for completion. + fn issue_cmd_slot0(&self, port: usize) -> Result<(), &'static str> { + let abar = self.abar_virt; + + // Clear any pending interrupts + port_write(abar, port, PORT_IS, 0xFFFF_FFFF); + + // Issue command on slot 0 + port_write(abar, port, PORT_CI, 1); + + // Wait for completion (CI bit 0 clears) + for _ in 0..10_000_000 { + let ci = port_read(abar, port, PORT_CI); + if (ci & 1) == 0 { + // Check for errors + let is = port_read(abar, port, PORT_IS); + if (is & (1 << 30)) != 0 { + // Task File Error + let tfd = port_read(abar, port, PORT_TFD); + crate::serial_println!("[ahci] Port {} TFE: TFD={:#x}", port, tfd); + return Err("AHCI: task file error"); + } + return Ok(()); + } + core::hint::spin_loop(); + } + + Err("AHCI: command timeout") + } + + /// Issue IDENTIFY DEVICE and return sector count. + fn identify_device(&self, port: usize, dma_index: usize) -> Result { + self.wait_ready(port)?; + + let mut dma_lock = PORT_DMA.lock(); + let dma = dma_lock[dma_index].as_mut().ok_or("AHCI: no DMA memory")?; + + // Set up command header in slot 0 + let cmd_table_phys = unsafe { + let storage = &raw const DMA_STORAGE; + let table_addr = core::ptr::addr_of!((*storage).ports[dma_index].cmd_table); + virt_to_phys(table_addr as u64) + }; + let dma_buf_phys = unsafe { + let storage = &raw const DMA_STORAGE; + let buf_addr = core::ptr::addr_of!((*storage).ports[dma_index].dma_buf); + virt_to_phys(buf_addr as u64) + }; + + // Command header: CFL=5 (5 dwords = 20 bytes for H2D FIS), 1 PRDT entry + dma.cmd_list[0].dw0 = (1 << 16) | 5; // PRDTL=1, CFL=5 + dma.cmd_list[0].prdbc = 0; + dma.cmd_list[0].ctba = cmd_table_phys as u32; + dma.cmd_list[0].ctbau = (cmd_table_phys >> 32) as u32; + + // Zero the command table + dma.cmd_table.cfis = [0; 64]; + dma.cmd_table.acmd = [0; 16]; + + // Set up H2D FIS for IDENTIFY DEVICE + dma.cmd_table.cfis[0] = FIS_TYPE_REG_H2D; + dma.cmd_table.cfis[1] = 0x80; // C bit = 1 (command) + dma.cmd_table.cfis[2] = ATA_CMD_IDENTIFY; + dma.cmd_table.cfis[7] = 0; // Device = 0 + + // PRDT: point to DMA buffer, 512 bytes + dma.cmd_table.prdt[0].dba = dma_buf_phys as u32; + dma.cmd_table.prdt[0].dbau = (dma_buf_phys >> 32) as u32; + dma.cmd_table.prdt[0]._reserved = 0; + dma.cmd_table.prdt[0].dbc = (SECTOR_SIZE as u32 - 1) | (1 << 31); // byte count - 1, IOC + + // Ensure CPU writes are visible to the DMA device + core::sync::atomic::fence(Ordering::SeqCst); + { + let dma_ptr = &**dma as *const PortDmaMem as *const u8; + dma_cache_clean(dma_ptr, core::mem::size_of::()); + } + + drop(dma_lock); + + // Issue the command + self.issue_cmd_slot0(port)?; + + // Invalidate cache for DMA buffer before reading device-written data + let dma_lock = PORT_DMA.lock(); + let dma = dma_lock[dma_index].as_ref().ok_or("AHCI: no DMA memory")?; + { + let buf_ptr = dma.dma_buf.as_ptr(); + dma_cache_invalidate(buf_ptr, SECTOR_SIZE); + } + + // Words 100-103 contain the 48-bit LBA sector count (u64) + let buf = &dma.dma_buf; + let sectors = (buf[200] as u64) + | ((buf[201] as u64) << 8) + | ((buf[202] as u64) << 16) + | ((buf[203] as u64) << 24) + | ((buf[204] as u64) << 32) + | ((buf[205] as u64) << 40) + | ((buf[206] as u64) << 48) + | ((buf[207] as u64) << 56); + + if sectors == 0 { + // Fall back to 28-bit LBA (words 60-61) + let sectors28 = (buf[120] as u64) + | ((buf[121] as u64) << 8) + | ((buf[122] as u64) << 16) + | ((buf[123] as u64) << 24); + Ok(sectors28) + } else { + Ok(sectors) + } + } + + /// Read a single sector from a port. + fn read_sector(&self, port: usize, dma_index: usize, lba: u64, buffer: &mut [u8; SECTOR_SIZE]) -> Result<(), &'static str> { + self.wait_ready(port)?; + + let mut dma_lock = PORT_DMA.lock(); + let dma = dma_lock[dma_index].as_mut().ok_or("AHCI: no DMA memory")?; + + let cmd_table_phys = unsafe { + let storage = &raw const DMA_STORAGE; + let table_addr = core::ptr::addr_of!((*storage).ports[dma_index].cmd_table); + virt_to_phys(table_addr as u64) + }; + let dma_buf_phys = unsafe { + let storage = &raw const DMA_STORAGE; + let buf_addr = core::ptr::addr_of!((*storage).ports[dma_index].dma_buf); + virt_to_phys(buf_addr as u64) + }; + + // Command header: CFL=5, PRDTL=1, not a write + dma.cmd_list[0].dw0 = (1 << 16) | 5; + dma.cmd_list[0].prdbc = 0; + dma.cmd_list[0].ctba = cmd_table_phys as u32; + dma.cmd_list[0].ctbau = (cmd_table_phys >> 32) as u32; + + // Zero CFIS + dma.cmd_table.cfis = [0; 64]; + + // H2D FIS: READ DMA EXT + dma.cmd_table.cfis[0] = FIS_TYPE_REG_H2D; + dma.cmd_table.cfis[1] = 0x80; // C bit + dma.cmd_table.cfis[2] = ATA_CMD_READ_DMA_EXT; + dma.cmd_table.cfis[3] = 0; // Features + dma.cmd_table.cfis[4] = lba as u8; // LBA 7:0 + dma.cmd_table.cfis[5] = (lba >> 8) as u8; // LBA 15:8 + dma.cmd_table.cfis[6] = (lba >> 16) as u8; // LBA 23:16 + dma.cmd_table.cfis[7] = 0x40; // Device: LBA mode + dma.cmd_table.cfis[8] = (lba >> 24) as u8; // LBA 31:24 + dma.cmd_table.cfis[9] = (lba >> 32) as u8; // LBA 39:32 + dma.cmd_table.cfis[10] = (lba >> 40) as u8; // LBA 47:40 + dma.cmd_table.cfis[12] = 1; // Count low = 1 sector + dma.cmd_table.cfis[13] = 0; // Count high = 0 + + // PRDT + dma.cmd_table.prdt[0].dba = dma_buf_phys as u32; + dma.cmd_table.prdt[0].dbau = (dma_buf_phys >> 32) as u32; + dma.cmd_table.prdt[0]._reserved = 0; + dma.cmd_table.prdt[0].dbc = (SECTOR_SIZE as u32 - 1) | (1 << 31); + + // Ensure CPU writes are visible to the DMA device + core::sync::atomic::fence(Ordering::SeqCst); + { + let dma_ptr = &**dma as *const PortDmaMem as *const u8; + dma_cache_clean(dma_ptr, core::mem::size_of::()); + } + drop(dma_lock); + + self.issue_cmd_slot0(port)?; + + // Invalidate cache before reading device-written DMA buffer + let dma_lock = PORT_DMA.lock(); + let dma = dma_lock[dma_index].as_ref().ok_or("AHCI: no DMA memory")?; + { + let buf_ptr = dma.dma_buf.as_ptr(); + dma_cache_invalidate(buf_ptr, SECTOR_SIZE); + } + buffer.copy_from_slice(&dma.dma_buf); + + Ok(()) + } + + /// Write a single sector to a port. + fn write_sector(&self, port: usize, dma_index: usize, lba: u64, buffer: &[u8; SECTOR_SIZE]) -> Result<(), &'static str> { + self.wait_ready(port)?; + + let mut dma_lock = PORT_DMA.lock(); + let dma = dma_lock[dma_index].as_mut().ok_or("AHCI: no DMA memory")?; + + let cmd_table_phys = unsafe { + let storage = &raw const DMA_STORAGE; + let table_addr = core::ptr::addr_of!((*storage).ports[dma_index].cmd_table); + virt_to_phys(table_addr as u64) + }; + let dma_buf_phys = unsafe { + let storage = &raw const DMA_STORAGE; + let buf_addr = core::ptr::addr_of!((*storage).ports[dma_index].dma_buf); + virt_to_phys(buf_addr as u64) + }; + + // Copy data to DMA buffer first + dma.dma_buf.copy_from_slice(buffer); + + // Command header: CFL=5, PRDTL=1, Write bit set (bit 6) + dma.cmd_list[0].dw0 = (1 << 16) | (1 << 6) | 5; + dma.cmd_list[0].prdbc = 0; + dma.cmd_list[0].ctba = cmd_table_phys as u32; + dma.cmd_list[0].ctbau = (cmd_table_phys >> 32) as u32; + + // Zero CFIS + dma.cmd_table.cfis = [0; 64]; + + // H2D FIS: WRITE DMA EXT + dma.cmd_table.cfis[0] = FIS_TYPE_REG_H2D; + dma.cmd_table.cfis[1] = 0x80; + dma.cmd_table.cfis[2] = ATA_CMD_WRITE_DMA_EXT; + dma.cmd_table.cfis[3] = 0; + dma.cmd_table.cfis[4] = lba as u8; + dma.cmd_table.cfis[5] = (lba >> 8) as u8; + dma.cmd_table.cfis[6] = (lba >> 16) as u8; + dma.cmd_table.cfis[7] = 0x40; + dma.cmd_table.cfis[8] = (lba >> 24) as u8; + dma.cmd_table.cfis[9] = (lba >> 32) as u8; + dma.cmd_table.cfis[10] = (lba >> 40) as u8; + dma.cmd_table.cfis[12] = 1; + dma.cmd_table.cfis[13] = 0; + + // PRDT + dma.cmd_table.prdt[0].dba = dma_buf_phys as u32; + dma.cmd_table.prdt[0].dbau = (dma_buf_phys >> 32) as u32; + dma.cmd_table.prdt[0]._reserved = 0; + dma.cmd_table.prdt[0].dbc = (SECTOR_SIZE as u32 - 1) | (1 << 31); + + // Ensure CPU writes (command + data) are visible to the DMA device + core::sync::atomic::fence(Ordering::SeqCst); + { + let dma_ptr = &**dma as *const PortDmaMem as *const u8; + dma_cache_clean(dma_ptr, core::mem::size_of::()); + } + drop(dma_lock); + + self.issue_cmd_slot0(port) + } + + /// Flush cache for a port. + fn flush_port(&self, port: usize, dma_index: usize) -> Result<(), &'static str> { + self.wait_ready(port)?; + + let mut dma_lock = PORT_DMA.lock(); + let dma = dma_lock[dma_index].as_mut().ok_or("AHCI: no DMA memory")?; + + let cmd_table_phys = unsafe { + let storage = &raw const DMA_STORAGE; + let table_addr = core::ptr::addr_of!((*storage).ports[dma_index].cmd_table); + virt_to_phys(table_addr as u64) + }; + + // Command header: CFL=5, PRDTL=0 (no data transfer) + dma.cmd_list[0].dw0 = 5; + dma.cmd_list[0].prdbc = 0; + dma.cmd_list[0].ctba = cmd_table_phys as u32; + dma.cmd_list[0].ctbau = (cmd_table_phys >> 32) as u32; + + dma.cmd_table.cfis = [0; 64]; + dma.cmd_table.cfis[0] = FIS_TYPE_REG_H2D; + dma.cmd_table.cfis[1] = 0x80; + dma.cmd_table.cfis[2] = ATA_CMD_FLUSH_EXT; + dma.cmd_table.cfis[7] = 0x40; + + // Ensure CPU writes are visible to the DMA device + core::sync::atomic::fence(Ordering::SeqCst); + { + let dma_ptr = &**dma as *const PortDmaMem as *const u8; + dma_cache_clean(dma_ptr, core::mem::size_of::()); + } + drop(dma_lock); + + self.issue_cmd_slot0(port) + } +} + +// ============================================================================= +// BlockDevice Implementation +// ============================================================================= + +/// AHCI block device wrapping a specific port. +pub struct AhciBlockDevice { + port_num: usize, + dma_index: usize, + sector_count: u64, +} + +impl BlockDevice for AhciBlockDevice { + fn read_block(&self, block_num: u64, buf: &mut [u8]) -> Result<(), BlockError> { + if block_num >= self.sector_count { + return Err(BlockError::OutOfBounds); + } + if buf.len() < SECTOR_SIZE { + return Err(BlockError::IoError); + } + + let ctrl = AHCI_CONTROLLER.lock(); + let ctrl = ctrl.as_ref().ok_or(BlockError::DeviceNotReady)?; + + let mut sector_buf = [0u8; SECTOR_SIZE]; + ctrl.read_sector(self.port_num, self.dma_index, block_num, &mut sector_buf) + .map_err(|_| BlockError::IoError)?; + + buf[..SECTOR_SIZE].copy_from_slice(§or_buf); + Ok(()) + } + + fn write_block(&self, block_num: u64, buf: &[u8]) -> Result<(), BlockError> { + if block_num >= self.sector_count { + return Err(BlockError::OutOfBounds); + } + if buf.len() < SECTOR_SIZE { + return Err(BlockError::IoError); + } + + let ctrl = AHCI_CONTROLLER.lock(); + let ctrl = ctrl.as_ref().ok_or(BlockError::DeviceNotReady)?; + + let mut sector_buf = [0u8; SECTOR_SIZE]; + sector_buf.copy_from_slice(&buf[..SECTOR_SIZE]); + ctrl.write_sector(self.port_num, self.dma_index, block_num, §or_buf) + .map_err(|_| BlockError::IoError) + } + + fn block_size(&self) -> usize { + SECTOR_SIZE + } + + fn num_blocks(&self) -> u64 { + self.sector_count + } + + fn flush(&self) -> Result<(), BlockError> { + let ctrl = AHCI_CONTROLLER.lock(); + let ctrl = ctrl.as_ref().ok_or(BlockError::DeviceNotReady)?; + ctrl.flush_port(self.port_num, self.dma_index) + .map_err(|_| BlockError::IoError) + } +} + +// ============================================================================= +// Public API +// ============================================================================= + +/// Initialize the AHCI driver by scanning for AHCI controllers on the PCI bus. +/// +/// Returns the number of SATA devices found. +pub fn init() -> Result { + if AHCI_INITIALIZED.load(Ordering::Relaxed) { + return Ok(0); + } + + // Find AHCI controller: class=0x01 (Mass Storage), subclass=0x06 (SATA) + let pci_devices = pci::get_devices().ok_or("PCI not enumerated")?; + let ahci_dev = pci_devices + .iter() + .find(|d| d.class == pci::DeviceClass::MassStorage && d.subclass == 0x06) + .ok_or("No AHCI controller found")?; + + crate::serial_println!( + "[ahci] Found AHCI controller: {:04x}:{:04x} at {:02x}:{:02x}.{}", + ahci_dev.vendor_id, + ahci_dev.device_id, + ahci_dev.bus, + ahci_dev.device, + ahci_dev.function, + ); + + let controller = AhciController::init(ahci_dev)?; + + let sata_count = controller + .ports + .iter() + .filter(|p| matches!(p, Some(port) if port.device_type == DeviceType::Sata)) + .count(); + + *AHCI_CONTROLLER.lock() = Some(controller); + AHCI_INITIALIZED.store(true, Ordering::Release); + + Ok(sata_count) +} + +/// Initialize the AHCI driver from a known platform MMIO base address. +/// +/// Used on platforms like Parallels Desktop where the SATA controller +/// is an ACPI platform device at a fixed MMIO address, not a PCI device. +/// +/// Returns the number of SATA devices found. +pub fn init_platform(abar_phys: u64) -> Result { + if AHCI_INITIALIZED.load(Ordering::Relaxed) { + return Ok(0); + } + + let controller = AhciController::init_from_mmio(abar_phys)?; + + let sata_count = controller + .ports + .iter() + .filter(|p| matches!(p, Some(port) if port.device_type == DeviceType::Sata)) + .count(); + + *AHCI_CONTROLLER.lock() = Some(controller); + AHCI_INITIALIZED.store(true, Ordering::Release); + + Ok(sata_count) +} + +/// Get an AHCI block device for the first SATA port. +/// +/// Returns None if AHCI is not initialized or no SATA devices found. +pub fn get_block_device() -> Option { + get_block_device_by_index(0) +} + +/// Get the Nth AHCI SATA block device (0-indexed). +/// +/// Skips non-SATA ports and ports with 0 sectors. +/// Returns None if the index is out of range. +pub fn get_block_device_by_index(index: usize) -> Option { + let ctrl = AHCI_CONTROLLER.lock(); + let ctrl = ctrl.as_ref()?; + + ctrl.ports + .iter() + .flatten() + .filter(|port| port.device_type == DeviceType::Sata && port.sector_count > 0) + .nth(index) + .map(|port| AhciBlockDevice { + port_num: port.port_num, + dma_index: port.dma_index, + sector_count: port.sector_count, + }) +} + +/// Return the number of SATA block devices available. +pub fn sata_device_count() -> usize { + let ctrl = AHCI_CONTROLLER.lock(); + match ctrl.as_ref() { + Some(ctrl) => ctrl + .ports + .iter() + .flatten() + .filter(|port| port.device_type == DeviceType::Sata && port.sector_count > 0) + .count(), + None => 0, + } +} + +/// Check if AHCI is initialized. +pub fn is_initialized() -> bool { + AHCI_INITIALIZED.load(Ordering::Acquire) +} diff --git a/kernel/src/drivers/mod.rs b/kernel/src/drivers/mod.rs index b0e7caae..b26a8cab 100644 --- a/kernel/src/drivers/mod.rs +++ b/kernel/src/drivers/mod.rs @@ -3,10 +3,14 @@ //! This module provides the driver infrastructure for Breenix, including //! PCI enumeration and device-specific drivers. +#[cfg(target_arch = "aarch64")] +pub mod ahci; #[cfg(target_arch = "x86_64")] pub mod e1000; pub mod fw_cfg; pub mod pci; +#[cfg(target_arch = "aarch64")] +pub mod usb; pub mod virtio; // Now available on both x86_64 and aarch64 /// Initialize the driver subsystem @@ -71,13 +75,124 @@ pub fn init() -> usize { /// Initialize the driver subsystem (ARM64 version) /// -/// Uses VirtIO MMIO enumeration instead of PCI on QEMU virt machine. +/// Detects the platform at runtime: +/// - If PCI ECAM is configured (Parallels/UEFI boot): enumerate PCI bus +/// - Otherwise (QEMU virt): enumerate VirtIO MMIO devices #[cfg(target_arch = "aarch64")] pub fn init() -> usize { use crate::serial_println; serial_println!("[drivers] Initializing driver subsystem..."); + let ecam_base = crate::platform_config::pci_ecam_base(); + + if ecam_base != 0 { + // PCI-based platform (Parallels): enumerate PCI bus + serial_println!("[drivers] PCI ECAM at {:#x}, enumerating PCI bus...", ecam_base); + let device_count = pci::enumerate(); + serial_println!("[drivers] Found {} PCI devices", device_count); + + // Log all PCI devices for debugging + if let Some(devices) = pci::get_devices() { + for dev in &devices { + serial_println!( + "[drivers] PCI {:02x}:{:02x}.{} [{:04x}:{:04x}] class={:?}/0x{:02x}", + dev.bus, dev.device, dev.function, + dev.vendor_id, dev.device_id, + dev.class, dev.subclass, + ); + } + } + + // Enumerate VirtIO PCI devices with modern transport + let virtio_devices = virtio::pci_transport::enumerate_virtio_pci_devices(); + for dev in &virtio_devices { + serial_println!( + "[drivers] VirtIO PCI device: {} (type={})", + virtio::pci_transport::device_type_name(dev.device_id()), + dev.device_id() + ); + } + serial_println!("[drivers] Found {} VirtIO PCI devices", virtio_devices.len()); + + // Initialize VirtIO GPU PCI driver. + // Even when a GOP framebuffer is available (Parallels), we try the VirtIO + // GPU PCI driver first — it supports arbitrary resolutions via + // CREATE_RESOURCE_2D, giving us control beyond the fixed GOP mode. + // If GPU PCI init fails, the GOP framebuffer is used as a fallback. + match virtio::gpu_pci::init() { + Ok(()) => { + serial_println!("[drivers] VirtIO GPU (PCI) initialized"); + } + Err(e) => { + serial_println!("[drivers] VirtIO GPU (PCI) init failed: {}", e); + } + } + + // Initialize EHCI USB 2.0 host controller (keyboard input) + // Intel 82801FB EHCI: vendor 0x8086, device 0x265c + if let Some(ehci_dev) = pci::find_device(0x8086, 0x265c) { + match usb::ehci::init(&ehci_dev) { + Ok(()) => { + serial_println!("[drivers] EHCI USB 2.0 controller initialized"); + } + Err(e) => { + serial_println!("[drivers] EHCI USB init failed: {}", e); + } + } + } else { + serial_println!("[drivers] No EHCI USB controller found"); + } + + // Initialize XHCI USB host controller (keyboard + mouse) + // NEC uPD720200: vendor 0x1033, device 0x0194 + if let Some(xhci_dev) = pci::find_device(0x1033, 0x0194) { + match usb::xhci::init(&xhci_dev) { + Ok(()) => { + serial_println!("[drivers] XHCI USB controller initialized"); + } + Err(e) => { + serial_println!("[drivers] XHCI USB init failed: {}", e); + } + } + } else { + serial_println!("[drivers] No XHCI USB controller found"); + } + + // Initialize AHCI storage driver. + // First try PCI (standard AHCI), then platform MMIO (Parallels Desktop). + match ahci::init() { + Ok(count) => { + serial_println!("[drivers] AHCI initialized (PCI): {} SATA device(s)", count); + } + Err(_) => { + // No PCI AHCI controller found. On Parallels Desktop, the SATA + // controller is an ACPI platform device at 0x0214_0000, not on PCI. + const PARALLELS_AHCI_BASE: u64 = 0x0214_0000; + match ahci::init_platform(PARALLELS_AHCI_BASE) { + Ok(count) => { + serial_println!("[drivers] AHCI initialized (platform MMIO): {} SATA device(s)", count); + } + Err(e) => { + serial_println!("[drivers] AHCI init skipped: {}", e); + } + } + } + } + + serial_println!("[drivers] Driver subsystem initialized (PCI)"); + device_count + } else { + // MMIO-based platform (QEMU virt): enumerate VirtIO MMIO + init_virtio_mmio() + } +} + +/// Initialize VirtIO MMIO devices (QEMU virt platform). +#[cfg(target_arch = "aarch64")] +fn init_virtio_mmio() -> usize { + use crate::serial_println; + // Enumerate VirtIO MMIO devices let mut device_count = 0; for device in virtio::mmio::enumerate_devices() { @@ -144,6 +259,6 @@ pub fn init() -> usize { } } - serial_println!("[drivers] Driver subsystem initialized"); + serial_println!("[drivers] Driver subsystem initialized (MMIO)"); device_count } diff --git a/kernel/src/drivers/pci.rs b/kernel/src/drivers/pci.rs index 689dac33..e318dfa3 100644 --- a/kernel/src/drivers/pci.rs +++ b/kernel/src/drivers/pci.rs @@ -32,7 +32,8 @@ const CONFIG_ADDRESS: u16 = 0xCF8; #[cfg(target_arch = "x86_64")] const CONFIG_DATA: u16 = 0xCFC; -/// Maximum number of PCI buses to scan +/// Maximum number of PCI buses to scan (x86 only; ARM64 uses platform_config bus range) +#[cfg(not(target_arch = "aarch64"))] const MAX_BUS: u8 = 255; /// Maximum number of devices per bus const MAX_DEVICE: u8 = 32; @@ -53,6 +54,11 @@ pub const VIRTIO_SOUND_DEVICE_ID_MODERN: u16 = 0x1059; pub const VIRTIO_NET_DEVICE_ID_LEGACY: u16 = 0x1000; /// VirtIO network device ID (modern) pub const VIRTIO_NET_DEVICE_ID_MODERN: u16 = 0x1041; +/// VirtIO GPU device ID (modern only, no legacy transitional) +pub const VIRTIO_GPU_DEVICE_ID_MODERN: u16 = 0x1050; + +/// PCI Capability ID for MSI +pub const PCI_CAP_ID_MSI: u8 = 0x05; /// Intel vendor ID (for reference - common in QEMU) pub const INTEL_VENDOR_ID: u16 = 0x8086; @@ -245,6 +251,76 @@ impl Device { // Set bit 0 (I/O Space Enable) pci_write_config_word(self.bus, self.device, self.function, 0x04, command | 0x01); } + + /// Disable legacy INTx interrupts (set DisINTx bit in PCI Command register). + pub fn disable_intx(&self) { + let command = pci_read_config_word(self.bus, self.device, self.function, 0x04); + // Bit 10: Interrupt Disable + pci_write_config_word(self.bus, self.device, self.function, 0x04, command | (1 << 10)); + } + + /// Find the MSI capability in the PCI capability list. + /// + /// Returns the config space offset of the MSI capability, or None if not found. + pub fn find_msi_capability(&self) -> Option { + // Check PCI Status register bit 4: Capabilities List exists + let status = pci_read_config_word(self.bus, self.device, self.function, 0x06); + if (status & (1 << 4)) == 0 { + return None; + } + + // Capabilities pointer at offset 0x34 + let mut cap_ptr = pci_read_config_byte(self.bus, self.device, self.function, 0x34); + + while cap_ptr != 0 { + let cap_id = pci_read_config_byte(self.bus, self.device, self.function, cap_ptr); + if cap_id == PCI_CAP_ID_MSI { + return Some(cap_ptr); + } + cap_ptr = pci_read_config_byte(self.bus, self.device, self.function, cap_ptr + 1); + } + None + } + + /// Configure and enable PCI MSI with a 32-bit message address. + /// + /// `cap_offset`: config space offset of the MSI capability + /// `address`: MSI target address (e.g., GICv2m doorbell register) + /// `data`: MSI data value (e.g., SPI number) + pub fn configure_msi(&self, cap_offset: u8, address: u32, data: u16) { + // Read Message Control to determine capability layout + let msg_ctrl = pci_read_config_word(self.bus, self.device, self.function, cap_offset + 2); + let is_64bit = (msg_ctrl & (1 << 7)) != 0; + let has_mask = (msg_ctrl & (1 << 8)) != 0; + + // Write Message Address (always at cap+4) + pci_write_config_dword(self.bus, self.device, self.function, cap_offset + 4, address); + + // Write Message Data + let data_offset = if is_64bit { + // 64-bit: upper address at cap+8, data at cap+12 + pci_write_config_dword(self.bus, self.device, self.function, cap_offset + 8, 0); + cap_offset + 12 + } else { + // 32-bit: data at cap+8 + cap_offset + 8 + }; + pci_write_config_word(self.bus, self.device, self.function, data_offset, data); + + // Clear mask bits if per-vector masking is supported + if has_mask { + let mask_offset = if is_64bit { + cap_offset + 16 + } else { + cap_offset + 12 + }; + pci_write_config_dword(self.bus, self.device, self.function, mask_offset, 0); + } + + // Enable MSI (bit 0 of Message Control), single message (bits 6:4 = 000) + let new_ctrl = (msg_ctrl & !0x0070) | 0x0001; // Clear MME, set Enable + pci_write_config_word(self.bus, self.device, self.function, cap_offset + 2, new_ctrl); + } } impl fmt::Display for Device { @@ -278,7 +354,7 @@ impl fmt::Debug for Device { /// Read a 32-bit value from PCI configuration space #[cfg(target_arch = "x86_64")] -fn pci_read_config_dword(bus: u8, device: u8, function: u8, offset: u8) -> u32 { +pub(crate) fn pci_read_config_dword(bus: u8, device: u8, function: u8, offset: u8) -> u32 { // Build the configuration address let address: u32 = 0x8000_0000 | ((bus as u32) << 16) @@ -295,18 +371,33 @@ fn pci_read_config_dword(bus: u8, device: u8, function: u8, offset: u8) -> u32 { } } -/// Read a 32-bit value from PCI configuration space (ARM64 stub) +/// Read a 32-bit value from PCI configuration space via ECAM. +/// +/// ECAM maps each device's 4KB config space into contiguous physical memory: +/// address = ECAM_BASE + (bus << 20) | (device << 15) | (function << 12) | offset +/// +/// Returns 0xFFFFFFFF if no PCI ECAM is configured (no PCI bus available). #[cfg(target_arch = "aarch64")] -fn pci_read_config_dword(bus: u8, device: u8, function: u8, offset: u8) -> u32 { - // ARM64: PCI config space is accessed via ECAM (memory-mapped) - // TODO: Implement ECAM access - let _ = (bus, device, function, offset); - 0 +pub(crate) fn pci_read_config_dword(bus: u8, device: u8, function: u8, offset: u8) -> u32 { + let ecam_base = crate::platform_config::pci_ecam_base(); + if ecam_base == 0 { + return 0xFFFF_FFFF; // No PCI + } + + let addr = ecam_base + + ((bus as u64) << 20) + | ((device as u64) << 15) + | ((function as u64) << 12) + | ((offset & 0xFC) as u64); + + const HHDM_BASE: u64 = 0xFFFF_0000_0000_0000; + let virt = (HHDM_BASE + addr) as *const u32; + unsafe { core::ptr::read_volatile(virt) } } /// Write a 32-bit value to PCI configuration space #[cfg(target_arch = "x86_64")] -fn pci_write_config_dword(bus: u8, device: u8, function: u8, offset: u8, value: u32) { +pub(crate) fn pci_write_config_dword(bus: u8, device: u8, function: u8, offset: u8, value: u32) { let address: u32 = 0x8000_0000 | ((bus as u32) << 16) | ((device as u32) << 11) @@ -322,17 +413,28 @@ fn pci_write_config_dword(bus: u8, device: u8, function: u8, offset: u8, value: } } -/// Write a 32-bit value to PCI configuration space (ARM64 stub) +/// Write a 32-bit value to PCI configuration space via ECAM (ARM64). #[cfg(target_arch = "aarch64")] -fn pci_write_config_dword(bus: u8, device: u8, function: u8, offset: u8, value: u32) { - // ARM64: PCI config space is accessed via ECAM (memory-mapped) - // TODO: Implement ECAM access - let _ = (bus, device, function, offset, value); +pub(crate) fn pci_write_config_dword(bus: u8, device: u8, function: u8, offset: u8, value: u32) { + let ecam_base = crate::platform_config::pci_ecam_base(); + if ecam_base == 0 { + return; // No PCI + } + + let addr = ecam_base + + ((bus as u64) << 20) + | ((device as u64) << 15) + | ((function as u64) << 12) + | ((offset & 0xFC) as u64); + + const HHDM_BASE: u64 = 0xFFFF_0000_0000_0000; + let virt = (HHDM_BASE + addr) as *mut u32; + unsafe { core::ptr::write_volatile(virt, value) } } /// Read a 16-bit value from PCI configuration space #[allow(dead_code)] // Used by Device methods, which are part of public API -fn pci_read_config_word(bus: u8, device: u8, function: u8, offset: u8) -> u16 { +pub(crate) fn pci_read_config_word(bus: u8, device: u8, function: u8, offset: u8) -> u16 { let dword = pci_read_config_dword(bus, device, function, offset & 0xFC); let shift = ((offset & 2) * 8) as u32; ((dword >> shift) & 0xFFFF) as u16 @@ -351,7 +453,7 @@ fn pci_write_config_word(bus: u8, device: u8, function: u8, offset: u8, value: u /// Read an 8-bit value from PCI configuration space #[allow(dead_code)] // Part of low-level API, will be used by VirtIO driver -fn pci_read_config_byte(bus: u8, device: u8, function: u8, offset: u8) -> u8 { +pub(crate) fn pci_read_config_byte(bus: u8, device: u8, function: u8, offset: u8) -> u8 { let dword = pci_read_config_dword(bus, device, function, offset & 0xFC); let shift = ((offset & 3) * 8) as u32; ((dword >> shift) & 0xFF) as u8 @@ -542,7 +644,16 @@ pub fn enumerate() -> usize { let mut virtio_block_count = 0; let mut network_count = 0; - for bus in 0..=MAX_BUS { + // Use platform-specific bus range on ARM64 (Parallels faults on out-of-range buses) + #[cfg(target_arch = "aarch64")] + let (bus_start, bus_end) = ( + crate::platform_config::pci_bus_start(), + crate::platform_config::pci_bus_end(), + ); + #[cfg(not(target_arch = "aarch64"))] + let (bus_start, bus_end) = (0u8, MAX_BUS); + + for bus in bus_start..=bus_end { for device in 0..MAX_DEVICE { // First check function 0 if let Some(dev) = probe_device(bus, device, 0) { diff --git a/kernel/src/drivers/usb/descriptors.rs b/kernel/src/drivers/usb/descriptors.rs new file mode 100644 index 00000000..d52b9909 --- /dev/null +++ b/kernel/src/drivers/usb/descriptors.rs @@ -0,0 +1,135 @@ +//! USB Standard Descriptor Types +//! +//! Defines the standard USB descriptor structures used for device enumeration +//! and configuration. + +/// USB Device Descriptor (18 bytes) +#[repr(C, packed)] +#[derive(Clone, Copy, Debug)] +pub struct DeviceDescriptor { + pub b_length: u8, + pub b_descriptor_type: u8, + pub bcd_usb: u16, + pub b_device_class: u8, + pub b_device_sub_class: u8, + pub b_device_protocol: u8, + pub b_max_packet_size0: u8, + pub id_vendor: u16, + pub id_product: u16, + pub bcd_device: u16, + pub i_manufacturer: u8, + pub i_product: u8, + pub i_serial_number: u8, + pub b_num_configurations: u8, +} + +/// USB Configuration Descriptor (9 bytes, followed by interface/endpoint descriptors) +#[repr(C, packed)] +#[derive(Clone, Copy, Debug)] +pub struct ConfigDescriptor { + pub b_length: u8, + pub b_descriptor_type: u8, + pub w_total_length: u16, + pub b_num_interfaces: u8, + pub b_configuration_value: u8, + pub i_configuration: u8, + pub bm_attributes: u8, + pub b_max_power: u8, +} + +/// USB Interface Descriptor (9 bytes) +#[repr(C, packed)] +#[derive(Clone, Copy, Debug)] +pub struct InterfaceDescriptor { + pub b_length: u8, + pub b_descriptor_type: u8, + pub b_interface_number: u8, + pub b_alternate_setting: u8, + pub b_num_endpoints: u8, + pub b_interface_class: u8, + pub b_interface_sub_class: u8, + pub b_interface_protocol: u8, + pub i_interface: u8, +} + +/// USB Endpoint Descriptor (7 bytes) +#[repr(C, packed)] +#[derive(Clone, Copy, Debug)] +pub struct EndpointDescriptor { + pub b_length: u8, + pub b_descriptor_type: u8, + pub b_endpoint_address: u8, + pub bm_attributes: u8, + pub w_max_packet_size: u16, + pub b_interval: u8, +} + +impl EndpointDescriptor { + /// Get endpoint number (bits 3:0) + pub fn endpoint_number(&self) -> u8 { + self.b_endpoint_address & 0x0F + } + + /// Check if this is an IN endpoint (bit 7 = 1) + pub fn is_in(&self) -> bool { + self.b_endpoint_address & 0x80 != 0 + } + + /// Get transfer type (bits 1:0 of bmAttributes) + pub fn transfer_type(&self) -> u8 { + self.bm_attributes & 0x03 + } + + /// Check if this is an interrupt endpoint + pub fn is_interrupt(&self) -> bool { + self.transfer_type() == 3 + } +} + +/// USB Setup Packet (8 bytes) +#[repr(C, packed)] +#[derive(Clone, Copy, Debug)] +pub struct SetupPacket { + pub bm_request_type: u8, + pub b_request: u8, + pub w_value: u16, + pub w_index: u16, + pub w_length: u16, +} + +/// USB Descriptor Types +pub mod descriptor_type { + pub const DEVICE: u8 = 1; + pub const CONFIGURATION: u8 = 2; + pub const INTERFACE: u8 = 4; + pub const ENDPOINT: u8 = 5; + pub const HID_REPORT: u8 = 0x22; +} + +/// USB Class Codes +pub mod class_code { + pub const HID: u8 = 0x03; +} + +/// USB HID Subclass Codes +pub mod hid_subclass { + pub const BOOT: u8 = 0x01; +} + +/// USB HID Protocol Codes +pub mod hid_protocol { + pub const KEYBOARD: u8 = 0x01; + pub const MOUSE: u8 = 0x02; +} + +/// USB Standard Requests +pub mod request { + pub const GET_DESCRIPTOR: u8 = 0x06; + pub const SET_CONFIGURATION: u8 = 0x09; +} + +/// USB HID Class Requests +pub mod hid_request { + pub const SET_IDLE: u8 = 0x0A; + pub const SET_PROTOCOL: u8 = 0x0B; +} diff --git a/kernel/src/drivers/usb/ehci.rs b/kernel/src/drivers/usb/ehci.rs new file mode 100644 index 00000000..c271b291 --- /dev/null +++ b/kernel/src/drivers/usb/ehci.rs @@ -0,0 +1,1233 @@ +//! EHCI (USB 2.0) Host Controller Driver +//! +//! Implements a minimal EHCI driver targeting the Intel 82801FB EHCI controller +//! at PCI slot 00:02.0 (vendor 0x8086, device 0x265c) on Parallels ARM64. +//! +//! This driver provides an alternative USB input path when the xHCI controller +//! has emulation bugs (CC=12 on interrupt endpoints, GET_REPORT echoes setup packet). +//! +//! # Architecture +//! +//! EHCI uses memory-mapped registers from BAR0: +//! - Capability Registers (base + 0x00): CAPLENGTH, HCSPARAMS, HCCPARAMS +//! - Operational Registers (base + CAPLENGTH): USBCMD, USBSTS, schedules, ports +//! +//! Two schedules: +//! - Async Schedule: circular linked list of QHs for control/bulk transfers +//! - Periodic Schedule: frame list (1024 entries, 1 per ms) for interrupt transfers +//! +//! Data structures: Queue Heads (QH) and Queue Transfer Descriptors (qTD) +//! are linked lists traversed by the HC hardware. + +#![cfg(target_arch = "aarch64")] +// DMA buffers must be static mut for hardware access. The EHCI driver ensures +// single-threaded access through INITIALIZED flag and CPU0-only polling. +#![allow(static_mut_refs)] + +use core::sync::atomic::{AtomicBool, AtomicU8, AtomicU32, Ordering}; + +use super::descriptors::{ + class_code, descriptor_type, hid_protocol, hid_request, hid_subclass, request, + ConfigDescriptor, DeviceDescriptor, EndpointDescriptor, InterfaceDescriptor, SetupPacket, +}; + +// ============================================================================= +// Constants +// ============================================================================= + +const HHDM_BASE: u64 = 0xFFFF_0000_0000_0000; + +/// Intel EHCI vendor/device for Parallels. +pub const INTEL_EHCI_VENDOR: u16 = 0x8086; +pub const INTEL_EHCI_DEVICE: u16 = 0x265c; + +/// EHCI Operational Register offsets (from op_base = bar_base + CAPLENGTH). +#[allow(dead_code)] +mod op_reg { + pub const USBCMD: u64 = 0x00; + pub const USBSTS: u64 = 0x04; + pub const USBINTR: u64 = 0x08; + pub const FRINDEX: u64 = 0x0C; + pub const CTRLDSSEGMENT: u64 = 0x10; + pub const PERIODICLISTBASE: u64 = 0x14; + pub const ASYNCLISTADDR: u64 = 0x18; + pub const CONFIGFLAG: u64 = 0x40; + pub const PORTSC_BASE: u64 = 0x44; +} + +/// USBCMD bit definitions +mod usbcmd_bits { + pub const RS: u32 = 1 << 0; // Run/Stop + pub const HCRESET: u32 = 1 << 1; // Host Controller Reset + pub const PSE: u32 = 1 << 4; // Periodic Schedule Enable + pub const ASE: u32 = 1 << 5; // Async Schedule Enable + pub const ITC_1MS: u32 = 0x08 << 16; // Interrupt Threshold = 1ms +} + +/// USBSTS bit definitions +#[allow(dead_code)] +mod usbsts_bits { + pub const USBINT: u32 = 1 << 0; // USB Interrupt (transfer complete) + pub const USBERRINT: u32 = 1 << 1; // USB Error Interrupt + pub const PCD: u32 = 1 << 2; // Port Change Detect + pub const FLR: u32 = 1 << 3; // Frame List Rollover + pub const HSE: u32 = 1 << 4; // Host System Error + pub const IAA: u32 = 1 << 5; // Interrupt on Async Advance + pub const HCHALTED: u32 = 1 << 12; // HC Halted +} + +/// PORTSC bit definitions +#[allow(dead_code)] +mod portsc_bits { + pub const CCS: u32 = 1 << 0; // Current Connect Status + pub const CSC: u32 = 1 << 1; // Connect Status Change + pub const PE: u32 = 1 << 2; // Port Enabled + pub const PEC: u32 = 1 << 3; // Port Enable Change + pub const PR: u32 = 1 << 8; // Port Reset + pub const LS_MASK: u32 = 3 << 10; // Line Status + pub const LS_K_STATE: u32 = 1 << 10; // K-state (low-speed) + pub const PP: u32 = 1 << 12; // Port Power + pub const PO: u32 = 1 << 13; // Port Owner (1 = companion) +} + +/// QH/qTD PID codes +mod pid { + pub const OUT: u32 = 0; + pub const IN: u32 = 1; + pub const SETUP: u32 = 2; +} + +/// Size of the periodic frame list (1024 entries = 1024ms = ~1 second cycle). +const FRAME_LIST_SIZE: usize = 1024; + + +// ============================================================================= +// Data Structures - QH and qTD +// ============================================================================= + +/// Queue Head (QH) - 48 bytes hardware, padded to 64 for alignment. +/// +/// Must be 32-byte aligned. The HC traverses linked lists of QHs. +#[repr(C, align(64))] +#[derive(Clone, Copy)] +struct Qh { + /// Horizontal Link Pointer: next QH/iTD physical address + /// Bits 31:5 = address, 2:1 = type (01=QH), 0 = T (terminate) + qhlp: u32, + /// Endpoint Characteristics (DWord 1) + characteristics: u32, + /// Endpoint Capabilities (DWord 2) + capabilities: u32, + /// Current qTD Link Pointer (HC-managed) + current_qtd: u32, + // --- Transfer Overlay (matches qTD layout) --- + /// Next qTD Pointer + next_qtd: u32, + /// Alternate Next qTD Pointer + alt_qtd: u32, + /// Token (status/control) + token: u32, + /// Buffer pointers [0..4] + buffer: [u32; 5], + // Pad to 64 bytes (12 DWORDs = 48 bytes, need 4 more) + _pad: [u32; 4], +} + +/// Queue Transfer Descriptor (qTD) - 32 bytes. +/// +/// Must be 32-byte aligned. Linked lists of qTDs describe transfers. +#[repr(C, align(32))] +#[derive(Clone, Copy)] +struct Qtd { + /// Next qTD Pointer (bits 31:5 = address, bit 0 = T) + next: u32, + /// Alternate Next qTD Pointer + alt_next: u32, + /// Token: status, PID, error count, transfer length, IOC, data toggle + token: u32, + /// Buffer page pointers [0..4] + /// buffer[0] bits 11:0 = current byte offset in first page + buffer: [u32; 5], + // Pad to 32 bytes (7 DWORDs = 28 bytes, need 1 more) + _pad: u32, +} + +const QH_TYPE: u32 = 0b01 << 1; // Type field = QH +const T_BIT: u32 = 1; // Terminate bit + +// ============================================================================= +// Static DMA Buffers +// ============================================================================= + +/// Periodic frame list: 1024 u32 entries, page-aligned. +#[repr(C, align(4096))] +struct FrameList([u32; FRAME_LIST_SIZE]); + +/// Async schedule head QH (circular, self-referencing). +static mut ASYNC_HEAD_QH: Qh = zero_qh(); + +/// Control transfer QH (linked into async schedule). +static mut CONTROL_QH: Qh = zero_qh(); + +/// Control transfer qTDs (setup + data + status). +static mut CONTROL_QTDS: [Qtd; 3] = [zero_qtd(); 3]; + +/// Interrupt QH for keyboard (linked into periodic schedule). +static mut INTERRUPT_QH: Qh = zero_qh(); + +/// Interrupt qTD for keyboard. +static mut INTERRUPT_QTD: Qtd = zero_qtd(); + +/// Periodic frame list. +static mut FRAME_LIST: FrameList = FrameList([T_BIT; FRAME_LIST_SIZE]); + +/// Setup packet buffer for control transfers. +#[repr(C, align(64))] +struct SetupBuf([u8; 8]); +static mut SETUP_BUF: SetupBuf = SetupBuf([0; 8]); + +/// Data buffer for control transfers (256 bytes should cover all descriptors). +#[repr(C, align(64))] +struct DataBuf([u8; 256]); +static mut DATA_BUF: DataBuf = DataBuf([0; 256]); + +/// Keyboard report buffer (8 bytes for boot protocol). +#[repr(C, align(64))] +struct ReportBuf([u8; 8]); +static mut REPORT_BUF: ReportBuf = ReportBuf([0; 8]); + +// ============================================================================= +// Driver State +// ============================================================================= + +/// EHCI driver state. +#[allow(dead_code)] // bar_base and addr64 retained for future use +struct EhciState { + /// MMIO base (virtual) = HHDM_BASE + BAR0 physical. + bar_base: u64, + /// Operational registers base = bar_base + caplength. + op_base: u64, + /// Number of physical downstream ports. + n_ports: u8, + /// Whether 64-bit addressing is supported. + addr64: bool, + /// Device address assigned to keyboard device (0 = none). + kbd_addr: u8, + /// Keyboard endpoint number (from endpoint descriptor). + kbd_ep: u8, + /// Keyboard max packet size. + kbd_max_pkt: u16, + /// Keyboard polling interval in frames (ms). + kbd_interval: u8, + /// Whether keyboard interrupt polling is active. + kbd_polling: bool, +} + +static INITIALIZED: AtomicBool = AtomicBool::new(false); +static mut EHCI_STATE: Option = None; + +/// Next device address to assign (1-127). +static NEXT_ADDR: AtomicU8 = AtomicU8::new(1); + +/// Counter: interrupt qTD completions. +pub static EHCI_INT_COMPLETIONS: AtomicU32 = AtomicU32::new(0); +/// Counter: control transfer completions. +pub static EHCI_CTL_COMPLETIONS: AtomicU32 = AtomicU32::new(0); +/// Counter: control transfer errors. +pub static EHCI_CTL_ERRORS: AtomicU32 = AtomicU32::new(0); + +// ============================================================================= +// Const initializers +// ============================================================================= + +const fn zero_qh() -> Qh { + Qh { + qhlp: T_BIT, + characteristics: 0, + capabilities: 0, + current_qtd: 0, + next_qtd: T_BIT, + alt_qtd: T_BIT, + token: 0, + buffer: [0; 5], + _pad: [0; 4], + } +} + +const fn zero_qtd() -> Qtd { + Qtd { + next: T_BIT, + alt_next: T_BIT, + token: 0, + buffer: [0; 5], + _pad: 0, + } +} + +// ============================================================================= +// Memory Helpers (same as xhci.rs) +// ============================================================================= + +#[inline] +fn virt_to_phys(virt: u64) -> u64 { + if virt >= HHDM_BASE { + virt - HHDM_BASE + } else { + virt + } +} + +#[inline] +fn dma_cache_clean(ptr: *const u8, len: usize) { + const CL: usize = 64; + let start = ptr as usize & !(CL - 1); + let end = (ptr as usize + len + CL - 1) & !(CL - 1); + for addr in (start..end).step_by(CL) { + unsafe { core::arch::asm!("dc cvac, {}", in(reg) addr, options(nostack)); } + } + unsafe { core::arch::asm!("dsb sy", options(nostack, preserves_flags)); } +} + +#[inline] +fn dma_cache_invalidate(ptr: *const u8, len: usize) { + const CL: usize = 64; + let start = ptr as usize & !(CL - 1); + let end = (ptr as usize + len + CL - 1) & !(CL - 1); + for addr in (start..end).step_by(CL) { + unsafe { core::arch::asm!("dc civac, {}", in(reg) addr, options(nostack)); } + } + unsafe { core::arch::asm!("dsb sy", options(nostack, preserves_flags)); } +} + +#[inline] +fn read32(addr: u64) -> u32 { + unsafe { core::ptr::read_volatile(addr as *const u32) } +} + +#[inline] +fn write32(addr: u64, val: u32) { + unsafe { core::ptr::write_volatile(addr as *mut u32, val) } +} + +/// Small busy-wait delay (~1ms per count at ~1GHz). +#[inline] +fn delay_ms(ms: u32) { + for _ in 0..ms { + for _ in 0..200_000u32 { + core::hint::spin_loop(); + } + } +} + +// ============================================================================= +// Initialization +// ============================================================================= + +/// Initialize the EHCI controller from a PCI device reference. +/// +/// Performs the full EHCI initialization sequence: +/// 1. Enable PCI bus master + memory space +/// 2. Map BAR0 +/// 3. Read capabilities +/// 4. BIOS handoff (USBLEGSUP) +/// 5. Reset controller +/// 6. Set up async + periodic schedules +/// 7. Start controller +/// 8. Enumerate ports and devices +pub fn init(pci_dev: &crate::drivers::pci::Device) -> Result<(), &'static str> { + use crate::serial_println; + + serial_println!("[ehci] Initializing EHCI USB 2.0 controller..."); + serial_println!( + "[ehci] PCI {:02x}:{:02x}.{} [{:04x}:{:04x}]", + pci_dev.bus, pci_dev.device, pci_dev.function, + pci_dev.vendor_id, pci_dev.device_id, + ); + + // 1. Enable PCI + pci_dev.enable_bus_master(); + pci_dev.enable_memory_space(); + + // 2. Map BAR0 + let bar = pci_dev.get_mmio_bar().ok_or("EHCI: no MMIO BAR")?; + serial_println!("[ehci] BAR0: phys={:#010x} size={:#x}", bar.address, bar.size); + let bar_base = HHDM_BASE + bar.address; + + // 3. Read capability registers + let caplength = read32(bar_base) & 0xFF; + let hciversion = (read32(bar_base) >> 16) & 0xFFFF; + let hcsparams = read32(bar_base + 0x04); + let hccparams = read32(bar_base + 0x08); + + let n_ports = (hcsparams & 0xF) as u8; + let n_cc = ((hcsparams >> 8) & 0xF) as u8; + let n_pcc = ((hcsparams >> 12) & 0xF) as u8; + let addr64 = (hccparams & 1) != 0; + let eecp = ((hccparams >> 8) & 0xFF) as u8; + + let op_base = bar_base + caplength as u64; + + serial_println!( + "[ehci] Capabilities: version={:#06x} caplength={} ports={} cc={} pcc={} 64bit={} eecp={:#x}", + hciversion, caplength, n_ports, n_cc, n_pcc, addr64, eecp, + ); + + // 4. BIOS handoff via EECP/USBLEGSUP + if eecp != 0 { + bios_handoff(pci_dev, eecp); + } + + // 5. Reset controller + // Stop first + let cmd = read32(op_base + op_reg::USBCMD); + if cmd & usbcmd_bits::RS != 0 { + write32(op_base + op_reg::USBCMD, cmd & !usbcmd_bits::RS); + // Wait for halted + for _ in 0..100 { + if read32(op_base + op_reg::USBSTS) & usbsts_bits::HCHALTED != 0 { + break; + } + delay_ms(1); + } + } + + // Issue reset + write32(op_base + op_reg::USBCMD, usbcmd_bits::HCRESET); + for _ in 0..100 { + if read32(op_base + op_reg::USBCMD) & usbcmd_bits::HCRESET == 0 { + break; + } + delay_ms(1); + } + if read32(op_base + op_reg::USBCMD) & usbcmd_bits::HCRESET != 0 { + serial_println!("[ehci] WARNING: reset did not complete"); + } + serial_println!("[ehci] Controller reset complete"); + + // 6. Set up data structures + // 6a. 4GB segment selector = 0 (all DMA in low 4GB) + if addr64 { + write32(op_base + op_reg::CTRLDSSEGMENT, 0); + } + + // 6b. Initialize periodic frame list (all terminate initially) + unsafe { + for entry in FRAME_LIST.0.iter_mut() { + *entry = T_BIT; + } + dma_cache_clean(FRAME_LIST.0.as_ptr() as *const u8, FRAME_LIST_SIZE * 4); + } + + // 6c. Set periodic list base + let frame_list_phys = virt_to_phys(unsafe { FRAME_LIST.0.as_ptr() as u64 }); + write32(op_base + op_reg::PERIODICLISTBASE, frame_list_phys as u32); + + // 6d. Set up async schedule head QH (self-referencing circular list) + let head_qh_phys = virt_to_phys(&raw const ASYNC_HEAD_QH as u64); + unsafe { + ASYNC_HEAD_QH.qhlp = (head_qh_phys as u32 & !0x1F) | QH_TYPE; // Point to self + ASYNC_HEAD_QH.characteristics = (1 << 15) // H = Head of Reclamation List + | (2 << 12) // EPS = High-Speed + | (64 << 16) // MaxPacketLen = 64 + | (1 << 14); // DTC = 1 + ASYNC_HEAD_QH.capabilities = 1 << 30; // Mult = 1 + ASYNC_HEAD_QH.next_qtd = T_BIT; + ASYNC_HEAD_QH.alt_qtd = T_BIT; + ASYNC_HEAD_QH.token = 0; // Not active + dma_cache_clean(&raw const ASYNC_HEAD_QH as *const u8, core::mem::size_of::()); + } + + write32(op_base + op_reg::ASYNCLISTADDR, head_qh_phys as u32); + + // 7. Clear all status bits, configure interrupts + write32(op_base + op_reg::USBSTS, 0x3F); // Write-clear all status bits + // We'll use polling, so disable all interrupts + write32(op_base + op_reg::USBINTR, 0); + + // 8. Start controller: enable both schedules, run + let cmd = usbcmd_bits::ITC_1MS | usbcmd_bits::PSE | usbcmd_bits::ASE | usbcmd_bits::RS; + write32(op_base + op_reg::USBCMD, cmd); + + // Wait for not halted + for _ in 0..100 { + if read32(op_base + op_reg::USBSTS) & usbsts_bits::HCHALTED == 0 { + break; + } + delay_ms(1); + } + if read32(op_base + op_reg::USBSTS) & usbsts_bits::HCHALTED != 0 { + serial_println!("[ehci] WARNING: controller did not start (still halted)"); + } + + // 9. Set Configure Flag (routes all ports to EHCI) + write32(op_base + op_reg::CONFIGFLAG, 1); + delay_ms(10); // Wait for port routing + + serial_println!("[ehci] Controller started, scanning ports..."); + + // 10. Store state + let mut state = EhciState { + bar_base, + op_base, + n_ports, + addr64, + kbd_addr: 0, + kbd_ep: 0, + kbd_max_pkt: 0, + kbd_interval: 0, + kbd_polling: false, + }; + + // 11. Scan and enumerate ports + scan_ports(&mut state); + + // 12. If keyboard found, set up periodic polling + if state.kbd_addr != 0 { + setup_keyboard_polling(&mut state); + } + + serial_println!( + "[ehci] Init complete: kbd_addr={} kbd_ep={} polling={}", + state.kbd_addr, state.kbd_ep, state.kbd_polling, + ); + + unsafe { + EHCI_STATE = Some(state); + } + INITIALIZED.store(true, Ordering::Release); + + Ok(()) +} + +/// BIOS handoff: claim ownership from BIOS via USBLEGSUP extended capability. +fn bios_handoff(pci_dev: &crate::drivers::pci::Device, eecp: u8) { + use crate::serial_println; + use crate::drivers::pci::{pci_read_config_dword, pci_write_config_dword}; + + let mut offset = eecp; + while offset != 0 { + let cap = pci_read_config_dword(pci_dev.bus, pci_dev.device, pci_dev.function, offset); + let cap_id = cap & 0xFF; + let next = ((cap >> 8) & 0xFF) as u8; + + if cap_id == 0x01 { + // USBLEGSUP: legacy support capability + serial_println!("[ehci] USBLEGSUP at offset {:#x}: {:#010x}", offset, cap); + + // Set HC OS Owned Semaphore (bit 24) + let val = cap | (1 << 24); + pci_write_config_dword(pci_dev.bus, pci_dev.device, pci_dev.function, offset, val); + + // Wait for BIOS to release (bit 16 = BIOS Owned should clear) + for _ in 0..100 { + let cur = pci_read_config_dword(pci_dev.bus, pci_dev.device, pci_dev.function, offset); + if cur & (1 << 16) == 0 { + serial_println!("[ehci] BIOS handoff complete"); + break; + } + delay_ms(10); + } + + // Also clear legacy support control/status (offset+4) + pci_write_config_dword(pci_dev.bus, pci_dev.device, pci_dev.function, offset + 4, 0); + return; + } + offset = next; + } + serial_println!("[ehci] No USBLEGSUP capability found"); +} + +// ============================================================================= +// Port Scanning and Device Enumeration +// ============================================================================= + +/// Scan all EHCI ports for connected devices. +fn scan_ports(state: &mut EhciState) { + use crate::serial_println; + + for port in 0..state.n_ports as u64 { + let portsc_addr = state.op_base + op_reg::PORTSC_BASE + port * 4; + let portsc = read32(portsc_addr); + + serial_println!( + "[ehci] Port {}: PORTSC={:#010x} CCS={} PE={} PR={} LS={} PP={} PO={}", + port, + portsc, + (portsc & portsc_bits::CCS) >> 0, + (portsc & portsc_bits::PE) >> 2, + (portsc & portsc_bits::PR) >> 8, + (portsc & portsc_bits::LS_MASK) >> 10, + (portsc & portsc_bits::PP) >> 12, + (portsc & portsc_bits::PO) >> 13, + ); + + if portsc & portsc_bits::CCS == 0 { + continue; // No device connected + } + + // Check line status for low-speed device + let ls = (portsc & portsc_bits::LS_MASK) >> 10; + if ls == 1 { + // K-state = low-speed device, hand off to companion controller + serial_println!("[ehci] Port {}: low-speed device, skipping", port); + continue; + } + + // Port reset + serial_println!("[ehci] Port {}: resetting...", port); + + // Clear PE (write-1-to-clear status change bits, preserve power) + let clear_bits = portsc_bits::CSC | portsc_bits::PEC; + write32(portsc_addr, (portsc & !portsc_bits::PE) | clear_bits); + + // Assert reset + let portsc = read32(portsc_addr); + write32(portsc_addr, portsc | portsc_bits::PR); + delay_ms(50); // USB spec: reset for at least 50ms + + // De-assert reset + let portsc = read32(portsc_addr); + write32(portsc_addr, portsc & !portsc_bits::PR); + + // Wait for reset complete (PE should become set for high-speed) + delay_ms(10); + let portsc = read32(portsc_addr); + serial_println!( + "[ehci] Port {}: after reset PORTSC={:#010x} PE={} CCS={}", + port, + portsc, + (portsc & portsc_bits::PE) >> 2, + portsc & portsc_bits::CCS, + ); + + if portsc & portsc_bits::PE == 0 { + serial_println!("[ehci] Port {}: not enabled after reset (full-speed?)", port); + continue; + } + + serial_println!("[ehci] Port {}: high-speed device enabled", port); + + // Enumerate the device + match enumerate_device(state, port as u8) { + Ok(true) => { + serial_println!("[ehci] Port {}: keyboard found!", port); + break; // We found what we need + } + Ok(false) => { + serial_println!("[ehci] Port {}: device enumerated but no keyboard", port); + } + Err(e) => { + serial_println!("[ehci] Port {}: enumeration failed: {}", port, e); + } + } + } +} + +/// Enumerate a device on the given port. Returns Ok(true) if a keyboard was found. +fn enumerate_device(state: &mut EhciState, _port: u8) -> Result { + use crate::serial_println; + + // Assign a device address + let addr = NEXT_ADDR.fetch_add(1, Ordering::SeqCst); + if addr > 127 { + return Err("no more USB addresses available"); + } + + // GET_DESCRIPTOR (device, 8 bytes) to address 0 to learn max packet size + let mut max_pkt0: u16 = 8; // Default for address 0 + + serial_println!("[ehci] Getting device descriptor (8 bytes) from addr 0..."); + let setup = SetupPacket { + bm_request_type: 0x80, + b_request: request::GET_DESCRIPTOR, + w_value: (descriptor_type::DEVICE as u16) << 8, + w_index: 0, + w_length: 8, + }; + + match control_transfer(state, 0, 0, max_pkt0, &setup, Some(8), true) { + Ok(n) => { + if n >= 8 { + let data = unsafe { &DATA_BUF.0 }; + let pkt_size = data[7]; + if pkt_size > 0 { + max_pkt0 = pkt_size as u16; + } + serial_println!("[ehci] Device at addr 0: maxPacketSize0={}", max_pkt0); + } + } + Err(e) => { + serial_println!("[ehci] GET_DESCRIPTOR(8) failed: {}", e); + return Err("GET_DESCRIPTOR(8) failed"); + } + } + + // SET_ADDRESS + serial_println!("[ehci] SET_ADDRESS to {}...", addr); + let setup = SetupPacket { + bm_request_type: 0x00, + b_request: 0x05, // SET_ADDRESS + w_value: addr as u16, + w_index: 0, + w_length: 0, + }; + + control_transfer(state, 0, 0, max_pkt0, &setup, None, false)?; + delay_ms(10); // Post-SET_ADDRESS recovery time + + serial_println!("[ehci] Device address set to {}", addr); + + // GET_DESCRIPTOR (device, 18 bytes) at new address + let setup = SetupPacket { + bm_request_type: 0x80, + b_request: request::GET_DESCRIPTOR, + w_value: (descriptor_type::DEVICE as u16) << 8, + w_index: 0, + w_length: 18, + }; + + let n = control_transfer(state, addr, 0, max_pkt0, &setup, Some(18), true)?; + if n < 18 { + return Err("short device descriptor"); + } + + let dev_desc: DeviceDescriptor = unsafe { + core::ptr::read_unaligned(DATA_BUF.0.as_ptr() as *const DeviceDescriptor) + }; + + serial_println!( + "[ehci] Device: USB{}.{} class={:#04x} sub={:#04x} proto={:#04x} vendor={:#06x} product={:#06x}", + { dev_desc.bcd_usb } >> 8, + ({ dev_desc.bcd_usb } >> 4) & 0xF, + dev_desc.b_device_class, + dev_desc.b_device_sub_class, + dev_desc.b_device_protocol, + { dev_desc.id_vendor }, + { dev_desc.id_product }, + ); + + // GET_DESCRIPTOR (configuration, 9 bytes first to learn total length) + let setup = SetupPacket { + bm_request_type: 0x80, + b_request: request::GET_DESCRIPTOR, + w_value: (descriptor_type::CONFIGURATION as u16) << 8, + w_index: 0, + w_length: 9, + }; + + let n = control_transfer(state, addr, 0, max_pkt0, &setup, Some(9), true)?; + if n < 9 { + return Err("short config descriptor"); + } + + let config_desc: ConfigDescriptor = unsafe { + core::ptr::read_unaligned(DATA_BUF.0.as_ptr() as *const ConfigDescriptor) + }; + let total_len = { config_desc.w_total_length } as usize; + let config_value = config_desc.b_configuration_value; + + serial_println!( + "[ehci] Config: totalLen={} numInterfaces={} configValue={}", + total_len, config_desc.b_num_interfaces, config_value, + ); + + // GET_DESCRIPTOR (full configuration) + let fetch_len = total_len.min(256); + let setup = SetupPacket { + bm_request_type: 0x80, + b_request: request::GET_DESCRIPTOR, + w_value: (descriptor_type::CONFIGURATION as u16) << 8, + w_index: 0, + w_length: fetch_len as u16, + }; + + let n = control_transfer(state, addr, 0, max_pkt0, &setup, Some(fetch_len as u32), true)?; + + // Parse configuration for HID keyboard interface + let config_data = unsafe { &DATA_BUF.0[..n as usize] }; + let mut found_keyboard = false; + let mut kbd_ep_addr: u8 = 0; + let mut kbd_max_pkt: u16 = 8; + let mut kbd_interval: u8 = 10; + let mut kbd_iface: u8 = 0; + + // Walk descriptors + let mut offset = 0; + while offset + 2 <= config_data.len() { + let d_len = config_data[offset] as usize; + let d_type = config_data[offset + 1]; + + if d_len < 2 || offset + d_len > config_data.len() { + break; + } + + if d_type == descriptor_type::INTERFACE && d_len >= 9 { + let iface: InterfaceDescriptor = unsafe { + core::ptr::read_unaligned(config_data.as_ptr().add(offset) as *const InterfaceDescriptor) + }; + serial_println!( + "[ehci] Interface {}: class={:#04x} sub={:#04x} proto={:#04x} eps={}", + iface.b_interface_number, + iface.b_interface_class, + iface.b_interface_sub_class, + iface.b_interface_protocol, + iface.b_num_endpoints, + ); + + if iface.b_interface_class == class_code::HID + && iface.b_interface_sub_class == hid_subclass::BOOT + && iface.b_interface_protocol == hid_protocol::KEYBOARD + { + serial_println!("[ehci] Found HID boot keyboard on interface {}", iface.b_interface_number); + found_keyboard = true; + kbd_iface = iface.b_interface_number; + } + } + + if d_type == descriptor_type::ENDPOINT && d_len >= 7 && found_keyboard && kbd_ep_addr == 0 { + let ep: EndpointDescriptor = unsafe { + core::ptr::read_unaligned(config_data.as_ptr().add(offset) as *const EndpointDescriptor) + }; + + if ep.is_interrupt() && ep.is_in() { + kbd_ep_addr = ep.b_endpoint_address; + kbd_max_pkt = { ep.w_max_packet_size } & 0x7FF; + kbd_interval = ep.b_interval; + serial_println!( + "[ehci] Keyboard endpoint: addr={:#04x} maxPkt={} interval={}ms", + kbd_ep_addr, kbd_max_pkt, kbd_interval, + ); + } + } + + offset += d_len; + } + + if !found_keyboard { + return Ok(false); + } + + // SET_CONFIGURATION + serial_println!("[ehci] SET_CONFIGURATION({})", config_value); + let setup = SetupPacket { + bm_request_type: 0x00, + b_request: request::SET_CONFIGURATION, + w_value: config_value as u16, + w_index: 0, + w_length: 0, + }; + control_transfer(state, addr, 0, max_pkt0, &setup, None, false)?; + + // SET_IDLE (HID class request) - silence idle reports + serial_println!("[ehci] SET_IDLE on interface {}", kbd_iface); + let setup = SetupPacket { + bm_request_type: 0x21, // Class, Interface, Host-to-Device + b_request: hid_request::SET_IDLE, + w_value: 0, // Indefinite idle + w_index: kbd_iface as u16, + w_length: 0, + }; + let _ = control_transfer(state, addr, 0, max_pkt0, &setup, None, false); + + // SET_PROTOCOL (boot protocol) + serial_println!("[ehci] SET_PROTOCOL(boot) on interface {}", kbd_iface); + let setup = SetupPacket { + bm_request_type: 0x21, + b_request: hid_request::SET_PROTOCOL, + w_value: 0, // 0 = boot protocol + w_index: kbd_iface as u16, + w_length: 0, + }; + control_transfer(state, addr, 0, max_pkt0, &setup, None, false)?; + + // Store keyboard info + state.kbd_addr = addr; + state.kbd_ep = kbd_ep_addr & 0x0F; + state.kbd_max_pkt = kbd_max_pkt; + state.kbd_interval = kbd_interval; + + serial_println!( + "[ehci] Keyboard configured: addr={} ep={} maxPkt={} interval={}ms", + addr, state.kbd_ep, kbd_max_pkt, kbd_interval, + ); + + Ok(true) +} + +// ============================================================================= +// Control Transfers (Async Schedule) +// ============================================================================= + +/// Execute a USB control transfer via the async schedule. +/// +/// Returns the number of bytes transferred in the data stage (0 if no data stage). +fn control_transfer( + _state: &EhciState, + dev_addr: u8, + endpoint: u8, + max_pkt: u16, + setup: &SetupPacket, + data_len: Option, + data_in: bool, +) -> Result { + // Build the 8-byte setup packet in DMA buffer + unsafe { + let setup_bytes = core::slice::from_raw_parts( + setup as *const SetupPacket as *const u8, + 8, + ); + SETUP_BUF.0.copy_from_slice(setup_bytes); + dma_cache_clean(SETUP_BUF.0.as_ptr(), 8); + } + + let setup_phys = virt_to_phys(unsafe { SETUP_BUF.0.as_ptr() as u64 }); + let data_phys = virt_to_phys(unsafe { DATA_BUF.0.as_ptr() as u64 }); + + // Clear data buffer + if data_len.is_some() { + unsafe { + DATA_BUF.0.fill(0); + dma_cache_clean(DATA_BUF.0.as_ptr(), 256); + } + } + + let has_data = data_len.is_some() && data_len.unwrap() > 0; + let xfer_len = data_len.unwrap_or(0); + + unsafe { + // Build SETUP qTD + CONTROL_QTDS[0] = zero_qtd(); + CONTROL_QTDS[0].token = + (1 << 7) // Active + | (pid::SETUP << 8) // PID = SETUP + | (3 << 10) // CERR = 3 + | (8 << 16) // Total Bytes = 8 + | (0 << 31); // dt = 0 (SETUP always data0) + CONTROL_QTDS[0].buffer[0] = setup_phys as u32; + + // Build DATA qTD (if needed) + if has_data { + let data_pid = if data_in { pid::IN } else { pid::OUT }; + CONTROL_QTDS[1] = zero_qtd(); + CONTROL_QTDS[1].token = + (1 << 7) // Active + | (data_pid << 8) // PID + | (3 << 10) // CERR = 3 + | (xfer_len << 16) // Total Bytes + | (1 << 31); // dt = 1 (DATA starts at data1) + CONTROL_QTDS[1].buffer[0] = data_phys as u32; + // Additional buffer pointers for multi-page transfers + if xfer_len > 4096 { + CONTROL_QTDS[1].buffer[1] = ((data_phys + 4096) & !0xFFF) as u32; + } + } + + // Build STATUS qTD + let status_idx = if has_data { 2 } else { 1 }; + let status_pid = if has_data && data_in { pid::OUT } else { pid::IN }; + CONTROL_QTDS[status_idx] = zero_qtd(); + CONTROL_QTDS[status_idx].token = + (1 << 7) // Active + | (status_pid << 8) // PID = opposite of data direction + | (3 << 10) // CERR = 3 + | (0 << 16) // Total Bytes = 0 (ZLP) + | (1 << 15) // IOC = 1 + | (1 << 31); // dt = 1 + + // Chain: SETUP -> [DATA ->] STATUS + let qtd0_phys = virt_to_phys(&raw const CONTROL_QTDS[0] as u64); + let qtd1_phys = virt_to_phys(&raw const CONTROL_QTDS[1] as u64); + let qtd2_phys = virt_to_phys(&raw const CONTROL_QTDS[2] as u64); + + if has_data { + CONTROL_QTDS[0].next = (qtd1_phys as u32) & !0x1F; + CONTROL_QTDS[0].alt_next = T_BIT; + CONTROL_QTDS[1].next = (qtd2_phys as u32) & !0x1F; + CONTROL_QTDS[1].alt_next = T_BIT; + CONTROL_QTDS[2].next = T_BIT; + CONTROL_QTDS[2].alt_next = T_BIT; + } else { + CONTROL_QTDS[0].next = (qtd1_phys as u32) & !0x1F; + CONTROL_QTDS[0].alt_next = T_BIT; + CONTROL_QTDS[1].next = T_BIT; + CONTROL_QTDS[1].alt_next = T_BIT; + } + + // Flush qTDs to memory + dma_cache_clean(CONTROL_QTDS.as_ptr() as *const u8, 3 * core::mem::size_of::()); + + // Set up Control QH + CONTROL_QH = zero_qh(); + let head_phys = virt_to_phys(&raw const ASYNC_HEAD_QH as u64); + CONTROL_QH.qhlp = (head_phys as u32 & !0x1F) | QH_TYPE; // Link back to head + + CONTROL_QH.characteristics = + (dev_addr as u32) // Device Address + | ((endpoint as u32) << 8) // Endpoint + | (2 << 12) // EPS = High-Speed + | (1 << 14) // DTC = 1 (data toggle from qTD) + | ((max_pkt as u32) << 16); // Max Packet Length + + CONTROL_QH.capabilities = 1 << 30; // Mult = 1 + + // Point overlay to first qTD + CONTROL_QH.next_qtd = (qtd0_phys as u32) & !0x1F; + CONTROL_QH.alt_qtd = T_BIT; + CONTROL_QH.token = 0; // Clear overlay token + + dma_cache_clean(&raw const CONTROL_QH as *const u8, core::mem::size_of::()); + + // Insert Control QH into async schedule: HEAD -> CONTROL -> HEAD + let control_phys = virt_to_phys(&raw const CONTROL_QH as u64); + ASYNC_HEAD_QH.qhlp = (control_phys as u32 & !0x1F) | QH_TYPE; + dma_cache_clean(&raw const ASYNC_HEAD_QH as *const u8, core::mem::size_of::()); + } + + // Wait for completion (poll status qTD) + let status_idx = if has_data { 2usize } else { 1usize }; + let timeout_ms = 500; + let mut completed = false; + + for _ in 0..timeout_ms { + unsafe { + dma_cache_invalidate( + &raw const CONTROL_QTDS[status_idx] as *const u8, + core::mem::size_of::(), + ); + } + let token = unsafe { core::ptr::read_volatile(&raw const CONTROL_QTDS[status_idx].token) }; + + if token & (1 << 7) == 0 { + // Active bit cleared - transfer complete + completed = true; + + // Check for errors + let status = token & 0x7E; // Halted, DBE, Babble, Xact Err, Missed uF, STS + if status != 0 { + EHCI_CTL_ERRORS.fetch_add(1, Ordering::Relaxed); + // Remove control QH from async schedule + unsafe { + let head_phys = virt_to_phys(&raw const ASYNC_HEAD_QH as u64); + ASYNC_HEAD_QH.qhlp = (head_phys as u32 & !0x1F) | QH_TYPE; + dma_cache_clean(&raw const ASYNC_HEAD_QH as *const u8, core::mem::size_of::()); + } + if token & (1 << 6) != 0 { return Err("EHCI: qTD halted"); } + if token & (1 << 5) != 0 { return Err("EHCI: data buffer error"); } + if token & (1 << 4) != 0 { return Err("EHCI: babble detected"); } + if token & (1 << 3) != 0 { return Err("EHCI: transaction error"); } + return Err("EHCI: unknown qTD error"); + } + break; + } + delay_ms(1); + } + + // Remove control QH from async schedule + unsafe { + let head_phys = virt_to_phys(&raw const ASYNC_HEAD_QH as u64); + ASYNC_HEAD_QH.qhlp = (head_phys as u32 & !0x1F) | QH_TYPE; + dma_cache_clean(&raw const ASYNC_HEAD_QH as *const u8, core::mem::size_of::()); + } + + if !completed { + EHCI_CTL_ERRORS.fetch_add(1, Ordering::Relaxed); + return Err("EHCI: control transfer timeout"); + } + + EHCI_CTL_COMPLETIONS.fetch_add(1, Ordering::Relaxed); + + // Read back data if data stage was IN + let bytes_transferred = if has_data && data_in { + unsafe { + dma_cache_invalidate(DATA_BUF.0.as_ptr(), 256); + dma_cache_invalidate( + &raw const CONTROL_QTDS[1] as *const u8, + core::mem::size_of::(), + ); + let data_token = core::ptr::read_volatile(&raw const CONTROL_QTDS[1].token); + let remaining = (data_token >> 16) & 0x7FFF; + xfer_len - remaining + } + } else { + 0 + }; + + Ok(bytes_transferred) +} + +// ============================================================================= +// Periodic Schedule - Keyboard Interrupt Polling +// ============================================================================= + +/// Set up the interrupt QH and qTD for keyboard polling via the periodic schedule. +fn setup_keyboard_polling(state: &mut EhciState) { + use crate::serial_println; + + if state.kbd_addr == 0 || state.kbd_ep == 0 { + return; + } + + let interval = (state.kbd_interval as usize).max(1).min(FRAME_LIST_SIZE); + + unsafe { + // Set up interrupt QH + INTERRUPT_QH = zero_qh(); + INTERRUPT_QH.qhlp = T_BIT; // Terminate (not circular in periodic schedule) + + INTERRUPT_QH.characteristics = + (state.kbd_addr as u32) // Device Address + | ((state.kbd_ep as u32) << 8) // Endpoint Number + | (2 << 12) // EPS = High-Speed + | (0 << 14) // DTC = 0 (HC manages data toggle!) + | ((state.kbd_max_pkt as u32) << 16); // Max Packet Length + // RL = 0 (must be 0 for periodic QHs) + + INTERRUPT_QH.capabilities = + (1 << 30) // Mult = 1 + | 0x01; // S-Mask = 0x01 (poll in microframe 0) + + // Set up interrupt qTD + INTERRUPT_QTD = zero_qtd(); + let report_phys = virt_to_phys(REPORT_BUF.0.as_ptr() as u64); + + REPORT_BUF.0.fill(0); + dma_cache_clean(REPORT_BUF.0.as_ptr(), 8); + + INTERRUPT_QTD.token = + (1 << 7) // Active + | (pid::IN << 8) // PID = IN + | (3 << 10) // CERR = 3 + | (8 << 16) // Total Bytes = 8 (keyboard boot report) + | (1 << 15); // IOC = 1 + + INTERRUPT_QTD.buffer[0] = report_phys as u32; + INTERRUPT_QTD.next = T_BIT; + INTERRUPT_QTD.alt_next = T_BIT; + + dma_cache_clean(&raw const INTERRUPT_QTD as *const u8, core::mem::size_of::()); + + // Point QH overlay to our qTD + let qtd_phys = virt_to_phys(&raw const INTERRUPT_QTD as u64); + INTERRUPT_QH.next_qtd = (qtd_phys as u32) & !0x1F; + INTERRUPT_QH.alt_qtd = T_BIT; + INTERRUPT_QH.token = 0; // Not active yet (overlay inactive) + + dma_cache_clean(&raw const INTERRUPT_QH as *const u8, core::mem::size_of::()); + + // Link QH into periodic frame list at every `interval` entries + let qh_phys = virt_to_phys(&raw const INTERRUPT_QH as u64); + let frame_entry = (qh_phys as u32 & !0x1F) | QH_TYPE; // Type = QH + + for i in (0..FRAME_LIST_SIZE).step_by(interval) { + FRAME_LIST.0[i] = frame_entry; + } + dma_cache_clean(FRAME_LIST.0.as_ptr() as *const u8, FRAME_LIST_SIZE * 4); + } + + state.kbd_polling = true; + + serial_println!( + "[ehci] Keyboard polling: QH linked every {}ms, endpoint IN{}", + interval, state.kbd_ep, + ); +} + +/// Poll for keyboard events. Called from the timer interrupt handler. +/// +/// Checks if the interrupt qTD completed. If so, processes the keyboard report +/// and resubmits the qTD. +pub fn poll_keyboard() { + if !INITIALIZED.load(Ordering::Acquire) { + return; + } + + let state = unsafe { + match &EHCI_STATE { + Some(s) => s, + None => return, + } + }; + + if !state.kbd_polling { + return; + } + + // Check interrupt qTD token + dma_cache_invalidate( + &raw const INTERRUPT_QTD as *const u8, + core::mem::size_of::(), + ); + + let token = unsafe { core::ptr::read_volatile(&raw const INTERRUPT_QTD.token) }; + + // Still active? Nothing to do. + if token & (1 << 7) != 0 { + return; + } + + // qTD completed. Check status. + let status = token & 0x7E; + let remaining = (token >> 16) & 0x7FFF; + let transferred = 8u32.saturating_sub(remaining); + + EHCI_INT_COMPLETIONS.fetch_add(1, Ordering::Relaxed); + + if status == 0 && transferred > 0 { + // Read keyboard report + unsafe { + dma_cache_invalidate(REPORT_BUF.0.as_ptr(), 8); + } + + let report = unsafe { &REPORT_BUF.0 }; + + // Update diagnostic counters + let any_nonzero = report.iter().any(|&b| b != 0); + if any_nonzero { + super::hid::NONZERO_KBD_COUNT.fetch_add(1, Ordering::Relaxed); + } + + // Pack report into u64 for heartbeat display + let mut packed: u64 = 0; + for i in 0..8 { + packed |= (report[i] as u64) << (i * 8); + } + super::hid::LAST_KBD_REPORT_U64.store(packed, Ordering::Relaxed); + + // Route to HID keyboard processing + super::hid::process_keyboard_report(report); + } + + // Resubmit: reinitialize qTD and re-link into QH + unsafe { + REPORT_BUF.0.fill(0); + dma_cache_clean(REPORT_BUF.0.as_ptr(), 8); + + let report_phys = virt_to_phys(REPORT_BUF.0.as_ptr() as u64); + + INTERRUPT_QTD.next = T_BIT; + INTERRUPT_QTD.alt_next = T_BIT; + INTERRUPT_QTD.token = + (1 << 7) // Active + | (pid::IN << 8) // PID = IN + | (3 << 10) // CERR = 3 + | (8 << 16) // Total Bytes = 8 + | (1 << 15); // IOC = 1 + INTERRUPT_QTD.buffer[0] = report_phys as u32; + + dma_cache_clean(&raw const INTERRUPT_QTD as *const u8, core::mem::size_of::()); + + // Re-link qTD into QH overlay + let qtd_phys = virt_to_phys(&raw const INTERRUPT_QTD as u64); + INTERRUPT_QH.next_qtd = (qtd_phys as u32) & !0x1F; + INTERRUPT_QH.alt_qtd = T_BIT; + INTERRUPT_QH.token = 0; // Clear overlay (inactive, HC will pick up next_qtd) + + dma_cache_clean(&raw const INTERRUPT_QH as *const u8, core::mem::size_of::()); + } +} + +/// Check if EHCI keyboard polling is active. +pub fn is_keyboard_active() -> bool { + INITIALIZED.load(Ordering::Acquire) + && unsafe { EHCI_STATE.as_ref().map_or(false, |s| s.kbd_polling) } +} diff --git a/kernel/src/drivers/usb/hid.rs b/kernel/src/drivers/usb/hid.rs new file mode 100644 index 00000000..e705cba1 --- /dev/null +++ b/kernel/src/drivers/usb/hid.rs @@ -0,0 +1,311 @@ +//! USB HID Class Driver (Boot Protocol Keyboard + Mouse) +//! +//! Processes boot protocol HID reports from USB keyboard and mouse devices +//! attached via the XHCI host controller. Routes keyboard events to the +//! TTY subsystem and mouse events to the input atomics, using the same +//! paths as the VirtIO input driver. +//! +//! Boot protocol gives fixed-format reports: +//! - Keyboard: 8 bytes (1 modifier + 1 reserved + 6 keycodes) +//! - Mouse: 3-4 bytes (1 buttons + 1 dx + 1 dy + optional wheel) + +use core::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering}; + +// ============================================================================= +// Diagnostic counters (read by heartbeat in timer_interrupt.rs) +// ============================================================================= + +/// Counts keyboard reports where at least one byte is non-zero. +/// If this stays 0 while user types, the USB reports are empty. +pub static NONZERO_KBD_COUNT: AtomicU64 = AtomicU64::new(0); + +/// Last keyboard report packed as a u64 (LE: byte[0] in LSB). +/// Allows heartbeat to display the most recent report bytes. +pub static LAST_KBD_REPORT_U64: AtomicU64 = AtomicU64::new(0); + +// ============================================================================= +// State tracking +// ============================================================================= + +/// Previous keyboard report for detecting key press/release transitions. +static mut PREV_KBD_REPORT: [u8; 8] = [0; 8]; + +/// Modifier key state (tracked from HID modifier byte). +static SHIFT_PRESSED: AtomicBool = AtomicBool::new(false); +static CTRL_PRESSED: AtomicBool = AtomicBool::new(false); +static CAPS_LOCK_ACTIVE: AtomicBool = AtomicBool::new(false); + +/// Mouse position in screen coordinates (shared with VirtIO input atomics). +/// These are the authoritative mouse position for the entire system. +static MOUSE_X: AtomicU32 = AtomicU32::new(0); +static MOUSE_Y: AtomicU32 = AtomicU32::new(0); +static MOUSE_BUTTONS: AtomicU32 = AtomicU32::new(0); + +// ============================================================================= +// USB HID Usage ID → Linux Keycode mapping +// ============================================================================= + +/// Map USB HID keyboard usage IDs (USB HID Usage Tables, Section 10) to +/// Linux keycodes (same codes used by input_mmio.rs). +/// +/// USB HID usage IDs: 0x04='a' through 0x1D='z', 0x1E='1' through 0x27='0', +/// 0x28=Enter, 0x29=Escape, 0x2A=Backspace, 0x2B=Tab, 0x2C=Space, etc. +/// +/// Returns a Linux keycode or 0 if unmapped. +fn hid_usage_to_linux_keycode(usage: u8) -> u16 { + match usage { + // Letters: USB 0x04-0x1D → Linux keycodes for QWERTY layout + 0x04 => 30, // a + 0x05 => 48, // b + 0x06 => 46, // c + 0x07 => 32, // d + 0x08 => 18, // e + 0x09 => 33, // f + 0x0A => 34, // g + 0x0B => 35, // h + 0x0C => 23, // i + 0x0D => 36, // j + 0x0E => 37, // k + 0x0F => 38, // l + 0x10 => 50, // m + 0x11 => 49, // n + 0x12 => 24, // o + 0x13 => 25, // p + 0x14 => 16, // q + 0x15 => 19, // r + 0x16 => 31, // s + 0x17 => 20, // t + 0x18 => 22, // u + 0x19 => 47, // v + 0x1A => 17, // w + 0x1B => 45, // x + 0x1C => 21, // y + 0x1D => 44, // z + + // Numbers: USB 0x1E-0x27 → Linux keycodes 2-11 + 0x1E => 2, // 1 + 0x1F => 3, // 2 + 0x20 => 4, // 3 + 0x21 => 5, // 4 + 0x22 => 6, // 5 + 0x23 => 7, // 6 + 0x24 => 8, // 7 + 0x25 => 9, // 8 + 0x26 => 10, // 9 + 0x27 => 11, // 0 + + // Special keys + 0x28 => 28, // Enter + 0x29 => 1, // Escape + 0x2A => 14, // Backspace + 0x2B => 15, // Tab + 0x2C => 57, // Space + 0x2D => 12, // - (minus) + 0x2E => 13, // = (equals) + 0x2F => 26, // [ (left bracket) + 0x30 => 27, // ] (right bracket) + 0x31 => 43, // \ (backslash) + 0x33 => 39, // ; (semicolon) + 0x34 => 40, // ' (apostrophe) + 0x35 => 41, // ` (grave accent) + 0x36 => 51, // , (comma) + 0x37 => 52, // . (period) + 0x38 => 53, // / (slash) + 0x39 => 58, // Caps Lock + + // Function keys + 0x3A => 59, // F1 + 0x3B => 60, // F2 + 0x3C => 61, // F3 + 0x3D => 62, // F4 + 0x3E => 63, // F5 + 0x3F => 64, // F6 + 0x40 => 65, // F7 + 0x41 => 66, // F8 + 0x42 => 67, // F9 + 0x43 => 68, // F10 + + // Navigation keys + 0x4F => 106, // Right Arrow + 0x50 => 105, // Left Arrow + 0x51 => 108, // Down Arrow + 0x52 => 103, // Up Arrow + 0x4A => 102, // Home + 0x4B => 104, // Page Up + 0x4C => 111, // Delete + 0x4D => 107, // End + 0x4E => 109, // Page Down + + _ => 0, // Unmapped + } +} + +// ============================================================================= +// Keyboard Report Processing +// ============================================================================= + +/// Process a USB boot protocol keyboard report (8 bytes). +/// +/// Report format: +/// - Byte 0: Modifier flags (LCtrl, LShift, LAlt, LGui, RCtrl, RShift, RAlt, RGui) +/// - Byte 1: Reserved +/// - Bytes 2-7: Up to 6 simultaneous key usage IDs (0 = no key) +/// +/// Compares with previous report to detect key press/release transitions. +pub fn process_keyboard_report(report: &[u8]) { + if report.len() < 8 { + return; + } + + // Diagnostic: track report contents for heartbeat visibility + let report_u64 = u64::from_le_bytes([ + report[0], report[1], report[2], report[3], + report[4], report[5], report[6], report[7], + ]); + LAST_KBD_REPORT_U64.store(report_u64, Ordering::Relaxed); + if report_u64 != 0 { + NONZERO_KBD_COUNT.fetch_add(1, Ordering::Relaxed); + } + + let modifiers = report[0]; + let keys = &report[2..8]; + + // Update modifier state from the modifier byte + // Bit 0: Left Ctrl, Bit 1: Left Shift, Bit 4: Right Ctrl, Bit 5: Right Shift + let shift = (modifiers & 0x02) != 0 || (modifiers & 0x20) != 0; + let ctrl = (modifiers & 0x01) != 0 || (modifiers & 0x10) != 0; + SHIFT_PRESSED.store(shift, Ordering::Relaxed); + CTRL_PRESSED.store(ctrl, Ordering::Relaxed); + + let prev = unsafe { &*(&raw const PREV_KBD_REPORT) }; + + // Detect newly pressed keys (in current but not in previous) + for &usage in keys { + if usage == 0 || usage == 1 { continue; } // 0=no key, 1=rollover error + + // Check if this key was already pressed in the previous report + let was_pressed = prev[2..8].contains(&usage); + if was_pressed { continue; } // Key held, not newly pressed + + // Handle Caps Lock toggle on press + if usage == 0x39 { + let prev_caps = CAPS_LOCK_ACTIVE.load(Ordering::Relaxed); + CAPS_LOCK_ACTIVE.store(!prev_caps, Ordering::Relaxed); + continue; + } + + // Convert USB HID usage to Linux keycode + let keycode = hid_usage_to_linux_keycode(usage); + if keycode == 0 { continue; } + + inject_keycode(keycode, shift, ctrl); + } + + // Save current report for next comparison + unsafe { + let dst = &raw mut PREV_KBD_REPORT; + (*dst).copy_from_slice(&report[..8]); + } +} + +/// Inject a Linux keycode into the TTY input path. +/// +/// Reuses the same keycode-to-character conversion as VirtIO input +/// (`input_mmio.rs`) for consistent behavior across input backends. +fn inject_keycode(keycode: u16, shift: bool, ctrl: bool) { + // Generate VT100 escape sequences for special keys + if let Some(seq) = crate::drivers::virtio::input_mmio::keycode_to_escape_seq(keycode) { + for &b in seq { + if !crate::tty::push_char_nonblock(b) { + crate::ipc::stdin::push_byte_from_irq(b); + } + } + return; + } + + let caps = CAPS_LOCK_ACTIVE.load(Ordering::Relaxed); + + // Ctrl+letter → control character + let c = if ctrl { + crate::drivers::virtio::input_mmio::ctrl_char_from_keycode(keycode) + } else { + let effective_shift = if crate::drivers::virtio::input_mmio::is_letter(keycode) { + shift ^ caps + } else { + shift + }; + crate::drivers::virtio::input_mmio::keycode_to_char(keycode, effective_shift) + }; + + if let Some(c) = c { + if !crate::tty::push_char_nonblock(c as u8) { + crate::ipc::stdin::push_byte_from_irq(c as u8); + } + } +} + +// ============================================================================= +// Mouse Report Processing +// ============================================================================= + +/// Get current screen dimensions for mouse clamping. +fn screen_dimensions() -> (u32, u32) { + crate::drivers::virtio::gpu_mmio::dimensions() + .or_else(|| { + crate::graphics::arm64_fb::FB_INFO_CACHE.get().map(|c| (c.width as u32, c.height as u32)) + }) + .unwrap_or((1280, 800)) +} + +/// Process a USB boot protocol mouse report (3-4 bytes). +/// +/// Report format: +/// - Byte 0: Button flags (bit 0=left, bit 1=right, bit 2=middle) +/// - Byte 1: X displacement (signed i8) +/// - Byte 2: Y displacement (signed i8) +/// - Byte 3: Wheel displacement (optional, signed i8) +/// +/// Updates the global mouse position atomics with clamping to screen bounds. +pub fn process_mouse_report(report: &[u8]) { + if report.len() < 3 { + return; + } + + let buttons = report[0] as u32; + let dx = report[1] as i8 as i32; + let dy = report[2] as i8 as i32; + + MOUSE_BUTTONS.store(buttons, Ordering::Relaxed); + + let (sw, sh) = screen_dimensions(); + + // Update X position with clamping + let old_x = MOUSE_X.load(Ordering::Relaxed) as i32; + let new_x = (old_x + dx).clamp(0, sw as i32 - 1) as u32; + MOUSE_X.store(new_x, Ordering::Relaxed); + + // Update Y position with clamping + let old_y = MOUSE_Y.load(Ordering::Relaxed) as i32; + let new_y = (old_y + dy).clamp(0, sh as i32 - 1) as u32; + MOUSE_Y.store(new_y, Ordering::Relaxed); + + // Mouse click dispatch could be added here for terminal tab switching +} + +// ============================================================================= +// Public Accessors +// ============================================================================= + +/// Get current mouse position in screen coordinates. +pub fn mouse_position() -> (u32, u32) { + (MOUSE_X.load(Ordering::Relaxed), MOUSE_Y.load(Ordering::Relaxed)) +} + +/// Get current mouse position and button state. +pub fn mouse_state() -> (u32, u32, u32) { + ( + MOUSE_X.load(Ordering::Relaxed), + MOUSE_Y.load(Ordering::Relaxed), + MOUSE_BUTTONS.load(Ordering::Relaxed), + ) +} diff --git a/kernel/src/drivers/usb/mod.rs b/kernel/src/drivers/usb/mod.rs new file mode 100644 index 00000000..0f5f39db --- /dev/null +++ b/kernel/src/drivers/usb/mod.rs @@ -0,0 +1,11 @@ +//! USB subsystem for Breenix +//! +//! Provides USB host controller drivers and class drivers: +//! - XHCI host controller driver (USB 3.0) +//! - HID class driver (keyboard + mouse via boot protocol) +//! - USB standard descriptor types + +pub mod descriptors; +pub mod ehci; +pub mod hid; +pub mod xhci; diff --git a/kernel/src/drivers/usb/xhci.rs b/kernel/src/drivers/usb/xhci.rs new file mode 100644 index 00000000..6fdd940e --- /dev/null +++ b/kernel/src/drivers/usb/xhci.rs @@ -0,0 +1,2917 @@ +//! XHCI (USB 3.0) Host Controller Driver +//! +//! Implements the xHCI specification for USB 3.0 host controller support, +//! targeting the NEC uPD720200 XHCI controller at PCI slot 00:03.0 +//! (vendor 0x1033, device 0x0194) on Parallels ARM64. +//! +//! # Architecture +//! +//! The xHCI controller uses memory-mapped registers from BAR0: +//! - Capability Registers (base + 0x00): CAPLENGTH, HCSPARAMS, HCCPARAMS, etc. +//! - Operational Registers (base + CAPLENGTH): USBCMD, USBSTS, CRCR, DCBAAP, etc. +//! - Port Registers (base + CAPLENGTH + 0x400): PORTSC per port +//! - Runtime Registers (base + RTSOFF): Interrupter registers +//! - Doorbell Registers (base + DBOFF): Per-slot doorbells +//! +//! Guest memory structures (all statically allocated): +//! - DCBAA: Device Context Base Address Array +//! - Command Ring: TRBs for host->controller commands +//! - Event Ring: TRBs for controller->host notifications +//! - Transfer Rings: Per-endpoint TRB rings for data transfer +//! - ERST: Event Ring Segment Table +//! +//! # Design +//! +//! All memory structures use static allocations (no heap/alloc), following the +//! same pattern as the VirtIO drivers in this kernel. DMA cache maintenance +//! is performed for ARM64 coherency. + +#![cfg(target_arch = "aarch64")] + +use core::sync::atomic::{AtomicBool, AtomicU8, AtomicU64, Ordering, fence}; +use spin::Mutex; + +use super::descriptors::{ + class_code, descriptor_type, hid_protocol, hid_request, hid_subclass, request, + DeviceDescriptor, ConfigDescriptor, InterfaceDescriptor, EndpointDescriptor, SetupPacket, +}; + +// ============================================================================= +// Constants +// ============================================================================= + +/// HHDM base for memory-mapped access (ARM64). +const HHDM_BASE: u64 = 0xFFFF_0000_0000_0000; + +/// NEC XHCI vendor ID. +pub const NEC_VENDOR_ID: u16 = 0x1033; +/// NEC uPD720200 XHCI device ID. +pub const NEC_XHCI_DEVICE_ID: u16 = 0x0194; + +/// Maximum device slots we support. +const MAX_SLOTS: usize = 8; +/// Command ring size in TRBs (last entry reserved for Link TRB). +/// Large command ring to avoid wrapping via Link TRB. +/// Parallels XHCI does not follow Link TRBs (tested: transfer rings fail, +/// command ring also fails after first wrap at 63 commands). With 4096 entries +/// (4095 usable), we get ~2044 ring resets before exhaustion ≈ hours of use. +const CMD_RING_SIZE: usize = 4096; +/// Event ring size in TRBs. +const EVENT_RING_SIZE: usize = 64; +/// Transfer ring size per endpoint in TRBs (last entry reserved for Link TRB). +/// Larger transfer ring reduces the number of Stop EP + Set TR Dequeue resets. +/// Each reset costs 2 command ring entries. With 256 entries (~85 GET_REPORTs +/// per fill) and 4095 usable command ring entries, we get ~2044 resets ≈ 29 min +/// of continuous keyboard polling at 100Hz before command ring exhaustion. +const TRANSFER_RING_SIZE: usize = 256; +/// Maximum number of HID transfer rings (keyboard + mouse). + +/// Work around Parallels CC=12 on interrupt endpoints by configuring as Bulk IN. +/// +/// Parallels xHCI emulation appears to not implement periodic (interrupt) +/// endpoint scheduling for SuperSpeed ports. ConfigureEndpoint succeeds and +/// reports EP state=Running, but the first Normal TRB returns CC=12 (Endpoint +/// Not Enabled). By telling the xHCI controller the endpoint is Bulk IN +/// (ep_type=6) instead of Interrupt IN (ep_type=7), we bypass the periodic +/// scheduler. The USB device still responds to IN tokens with its interrupt +/// data, but the controller processes TRBs as bulk transfers. +const USE_BULK_FOR_INTERRUPT: bool = true; + +// ============================================================================= +// TRB Type Constants +// ============================================================================= + +/// xHCI TRB type codes (complete per specification; not all used yet) +#[allow(dead_code)] +mod trb_type { + pub const NORMAL: u32 = 1; + pub const SETUP_STAGE: u32 = 2; + pub const DATA_STAGE: u32 = 3; + pub const STATUS_STAGE: u32 = 4; + pub const LINK: u32 = 6; + pub const ENABLE_SLOT: u32 = 9; + pub const DISABLE_SLOT: u32 = 10; + pub const ADDRESS_DEVICE: u32 = 11; + pub const CONFIGURE_ENDPOINT: u32 = 12; + pub const EVALUATE_CONTEXT: u32 = 13; + pub const STOP_ENDPOINT: u32 = 15; + pub const SET_TR_DEQUEUE_POINTER: u32 = 16; + pub const NOOP: u32 = 23; + pub const TRANSFER_EVENT: u32 = 32; + pub const COMMAND_COMPLETION: u32 = 33; + pub const PORT_STATUS_CHANGE: u32 = 34; +} + +/// xHCI completion codes +mod completion_code { + pub const SUCCESS: u32 = 1; + pub const ENDPOINT_NOT_ENABLED: u32 = 12; + pub const SHORT_PACKET: u32 = 13; +} + +// ============================================================================= +// TRB (Transfer Request Block) +// ============================================================================= + +/// Transfer Request Block - the fundamental data structure for xHCI communication. +/// +/// All TRBs are 16 bytes and must be 16-byte aligned. The controller and host +/// communicate via rings of TRBs. +#[repr(C, align(16))] +#[derive(Clone, Copy)] +struct Trb { + /// Data pointer or inline parameters + param: u64, + /// Transfer length, completion code, etc. + status: u32, + /// TRB type (bits 15:10), cycle bit (bit 0), flags + control: u32, +} + +impl Trb { + const fn zeroed() -> Self { + Trb { + param: 0, + status: 0, + control: 0, + } + } + + /// Get the TRB type field (bits 15:10). + fn trb_type(&self) -> u32 { + (self.control >> 10) & 0x3F + } + + /// Get the completion code from the status field (bits 31:24). + fn completion_code(&self) -> u32 { + (self.status >> 24) & 0xFF + } + + /// Get the slot ID from the control field (bits 31:24). + fn slot_id(&self) -> u8 { + ((self.control >> 24) & 0xFF) as u8 + } +} + +// ============================================================================= +// Event Ring Segment Table Entry +// ============================================================================= + +/// Event Ring Segment Table entry. +/// +/// Each entry points to a contiguous segment of the event ring. +#[repr(C, align(64))] +#[derive(Clone, Copy)] +struct ErstEntry { + /// Physical base address of the event ring segment + base: u64, + /// Number of TRBs in this segment + size: u32, + /// Reserved + _rsvd: u32, +} + +impl ErstEntry { + const fn zeroed() -> Self { + ErstEntry { + base: 0, + size: 0, + _rsvd: 0, + } + } +} + +// ============================================================================= +// Aligned Wrapper for Static Allocations +// ============================================================================= + +/// 64-byte aligned wrapper for static DMA structures. +#[repr(C, align(64))] +struct Aligned64(T); + +/// 4096-byte (page) aligned wrapper for structures requiring page alignment. +#[repr(C, align(4096))] +struct AlignedPage(T); + +// ============================================================================= +// Static Memory Allocations +// ============================================================================= + +/// Device Context Base Address Array: 256 entries x 8 bytes = 2KB, 64-byte aligned. +/// +/// Entry 0 is the scratchpad buffer array pointer (or 0 if not needed). +/// Entries 1..MaxSlots are device context pointers. +static mut DCBAA: AlignedPage<[u64; 256]> = AlignedPage([0u64; 256]); + +/// Command Ring: 64 TRBs x 16 bytes = 1KB. +static mut CMD_RING: Aligned64<[Trb; CMD_RING_SIZE]> = Aligned64([Trb::zeroed(); CMD_RING_SIZE]); +/// Command ring enqueue pointer index. +static mut CMD_RING_ENQUEUE: usize = 0; +/// Command ring producer cycle state. +static mut CMD_RING_CYCLE: bool = true; + +/// Event Ring: 64 TRBs x 16 bytes = 1KB. +static mut EVENT_RING: Aligned64<[Trb; EVENT_RING_SIZE]> = + Aligned64([Trb::zeroed(); EVENT_RING_SIZE]); +/// Event ring dequeue pointer index. +static mut EVENT_RING_DEQUEUE: usize = 0; +/// Event ring consumer cycle state. +static mut EVENT_RING_CYCLE: bool = true; + +/// Event Ring Segment Table (1 entry). +static mut ERST: Aligned64<[ErstEntry; 1]> = Aligned64([ErstEntry::zeroed(); 1]); + +/// Base index for HID interrupt transfer rings, placed after per-slot EP0 rings +/// to avoid index collisions. Keyboard = HID_RING_BASE + 0, Mouse = HID_RING_BASE + 1. +const HID_RING_BASE: usize = MAX_SLOTS; + +/// Total number of transfer rings: MAX_SLOTS for EP0 + 2 for HID interrupt endpoints. +const NUM_TRANSFER_RINGS: usize = MAX_SLOTS + 2; + +/// Transfer rings for device endpoints. +/// +/// Indices [0..MAX_SLOTS): EP0 control rings, indexed by slot_idx (slot_id - 1). +/// Indices [HID_RING_BASE..HID_RING_BASE+2): HID interrupt rings (keyboard, mouse). +static mut TRANSFER_RINGS: [[Trb; TRANSFER_RING_SIZE]; NUM_TRANSFER_RINGS] = + [[Trb::zeroed(); TRANSFER_RING_SIZE]; NUM_TRANSFER_RINGS]; +/// Transfer ring enqueue indices. +static mut TRANSFER_ENQUEUE: [usize; NUM_TRANSFER_RINGS] = [0; NUM_TRANSFER_RINGS]; +/// Transfer ring cycle state. +static mut TRANSFER_CYCLE: [bool; NUM_TRANSFER_RINGS] = [true; NUM_TRANSFER_RINGS]; + +/// Input Contexts for device setup (2048 bytes each for 64-byte contexts). +/// Used temporarily during AddressDevice and ConfigureEndpoint commands. +static mut INPUT_CONTEXTS: [AlignedPage<[u8; 4096]>; MAX_SLOTS] = + [const { AlignedPage([0u8; 4096]) }; MAX_SLOTS]; + +/// Device Contexts (output contexts, 2048 bytes each). +/// Managed by the controller; we provide physical addresses via DCBAA. +static mut DEVICE_CONTEXTS: [AlignedPage<[u8; 4096]>; MAX_SLOTS] = + [const { AlignedPage([0u8; 4096]) }; MAX_SLOTS]; + +/// HID report buffer for keyboard (8 bytes: modifier + reserved + 6 keycodes). +static mut KBD_REPORT_BUF: Aligned64<[u8; 8]> = Aligned64([0u8; 8]); + +/// HID report buffer for mouse (8 bytes: buttons + X + Y + wheel + ...). +static mut MOUSE_REPORT_BUF: Aligned64<[u8; 8]> = Aligned64([0u8; 8]); + +/// Scratch buffer for control transfer data stages (256 bytes). +static mut CTRL_DATA_BUF: Aligned64<[u8; 256]> = Aligned64([0u8; 256]); + +// ============================================================================= +// Controller State +// ============================================================================= + +/// XHCI controller state, populated during initialization. +struct XhciState { + /// HHDM virtual address of BAR0 (retained for runtime register access if needed) + #[allow(dead_code)] + base: u64, + /// Length of capability registers (retained for recalculating register offsets) + #[allow(dead_code)] + cap_length: u8, + /// Operational register base (base + cap_length) + op_base: u64, + /// Runtime register base (base + rtsoff) + rt_base: u64, + /// Doorbell register base (base + dboff) + db_base: u64, + /// Maximum enabled device slots (retained for runtime slot validation) + #[allow(dead_code)] + max_slots: u8, + /// Maximum root hub ports + #[allow(dead_code)] // Used by scan_ports (standard enumeration) + max_ports: u8, + /// Context entry size (32 or 64 bytes) + context_size: usize, + /// GIC INTID for this controller + irq: u32, + /// Slot ID for keyboard device (0 = not found) + kbd_slot: u8, + /// Endpoint DCI for keyboard interrupt IN + kbd_endpoint: u8, + /// Slot ID for mouse device (0 = not found) + mouse_slot: u8, + /// Endpoint DCI for mouse interrupt IN + mouse_endpoint: u8, +} + +/// Global lock protecting XHCI controller access. +static XHCI_LOCK: Mutex<()> = Mutex::new(()); + +/// Global XHCI controller state. +static mut XHCI_STATE: Option = None; + +/// Initialization flag, checked before accessing XHCI_STATE. +static XHCI_INITIALIZED: AtomicBool = AtomicBool::new(false); + +/// Diagnostic counters for heartbeat visibility. +pub static POLL_COUNT: AtomicU64 = AtomicU64::new(0); +pub static EVENT_COUNT: AtomicU64 = AtomicU64::new(0); +pub static KBD_EVENT_COUNT: AtomicU64 = AtomicU64::new(0); +/// Counts transfer events that didn't match kbd/mouse slots or had error CC. +pub static XFER_OTHER_COUNT: AtomicU64 = AtomicU64::new(0); +/// Counts port status change events. +pub static PSC_COUNT: AtomicU64 = AtomicU64::new(0); +/// Counts events processed via MSI interrupt handler (handle_interrupt). +pub static MSI_EVENT_COUNT: AtomicU64 = AtomicU64::new(0); + +/// When true, use EP0 GET_REPORT control transfers instead of interrupt endpoint. +/// Set automatically when the first interrupt transfer returns CC=12 (Endpoint Not Enabled). +static EP0_POLLING_MODE: AtomicBool = AtomicBool::new(false); +/// Flags set by MSI interrupt handler to request requeue from timer poll. +/// Requeuing from IRQ context causes MSI storms on virtual XHCI controllers. +static MSI_KBD_NEEDS_REQUEUE: AtomicBool = AtomicBool::new(false); +static MSI_MOUSE_NEEDS_REQUEUE: AtomicBool = AtomicBool::new(false); +/// Track how many EP0 polls to skip (rate-limit: 1 GET_REPORT per N poll cycles). +static EP0_POLL_SKIP: AtomicU64 = AtomicU64::new(0); + +/// EP0 polling async state machine (non-blocking, no spin waits in timer handler). +/// +/// States: +/// 0 = IDLE: ready to submit a GET_REPORT or start a ring reset +/// 1 = XFER_PENDING: GET_REPORT in-flight, waiting for SUCCESS Transfer Event +/// 2 = WAIT_STOP_EP: Stop EP command issued, waiting for Command Completion +/// 3 = WAIT_SET_DEQUEUE: Set TR Dequeue command issued, waiting for CC +mod ep0_state { + pub const IDLE: u8 = 0; + pub const XFER_PENDING: u8 = 1; + pub const WAIT_STOP_EP: u8 = 2; + pub const WAIT_SET_DEQUEUE: u8 = 3; +} +static EP0_STATE: AtomicU8 = AtomicU8::new(ep0_state::IDLE); + +/// Tracks how many GET_REPORT submissions have been made since the last ring reset. +static EP0_POLL_SUBMISSIONS: AtomicU64 = AtomicU64::new(0); +/// Tracks how many successful ring resets have been performed. +pub static EP0_RESET_COUNT: AtomicU64 = AtomicU64::new(0); +/// Tracks how many ring reset failures have occurred (CC != SUCCESS for Stop EP or Set TR Deq). +pub static EP0_RESET_FAIL_COUNT: AtomicU64 = AtomicU64::new(0); +/// Counts how many times the stuck-detection force-cleared EP0 state back to IDLE. +pub static EP0_PENDING_STUCK_COUNT: AtomicU64 = AtomicU64::new(0); +/// Counts consecutive poll cycles in a non-IDLE state without progress. +static EP0_STALL_POLLS: AtomicU64 = AtomicU64::new(0); + +/// Sentinel diagnostic: counts reports where the sentinel byte (0xDE) was NOT overwritten by DMA. +pub static DMA_SENTINEL_SURVIVED: AtomicU64 = AtomicU64::new(0); +/// Sentinel diagnostic: counts reports where the sentinel byte WAS overwritten (DMA worked). +pub static DMA_SENTINEL_REPLACED: AtomicU64 = AtomicU64::new(0); + +// ============================================================================= +// Memory Helpers +// ============================================================================= + +/// Convert a kernel virtual address to a physical address. +/// +/// On QEMU aarch64, kernel statics are accessed via HHDM (>= 0xFFFF_0000_0000_0000), +/// so phys = virt - HHDM_BASE. +/// On Parallels, the kernel may be identity-mapped via TTBR0, so statics are +/// at their physical addresses already. +#[inline] +fn virt_to_phys(virt: u64) -> u64 { + if virt >= HHDM_BASE { + virt - HHDM_BASE + } else { + // Already a physical address (identity-mapped kernel on Parallels) + virt + } +} + +/// Clean (flush) a range of memory from CPU caches to the point of coherency. +/// +/// Must be called after writing DMA descriptors/data and before issuing +/// DMA commands, so the device sees the updated data in physical memory. +#[inline] +fn dma_cache_clean(ptr: *const u8, len: usize) { + const CACHE_LINE: usize = 64; + let start = ptr as usize & !(CACHE_LINE - 1); + let end = (ptr as usize + len + CACHE_LINE - 1) & !(CACHE_LINE - 1); + for addr in (start..end).step_by(CACHE_LINE) { + unsafe { + core::arch::asm!("dc cvac, {}", in(reg) addr, options(nostack)); + } + } + unsafe { + core::arch::asm!("dsb sy", options(nostack, preserves_flags)); + } +} + +/// Invalidate a range of memory in CPU caches after a device DMA write. +/// +/// Must be called after a DMA read completes and before the CPU reads +/// the DMA buffer, to ensure the CPU sees the device-written data. +#[inline] +fn dma_cache_invalidate(ptr: *const u8, len: usize) { + const CACHE_LINE: usize = 64; + let start = ptr as usize & !(CACHE_LINE - 1); + let end = (ptr as usize + len + CACHE_LINE - 1) & !(CACHE_LINE - 1); + for addr in (start..end).step_by(CACHE_LINE) { + unsafe { + core::arch::asm!("dc civac, {}", in(reg) addr, options(nostack)); + } + } + unsafe { + core::arch::asm!("dsb sy", options(nostack, preserves_flags)); + } +} + +// ============================================================================= +// MMIO Register Access +// ============================================================================= + +#[inline] +fn read32(addr: u64) -> u32 { + unsafe { core::ptr::read_volatile(addr as *const u32) } +} + +#[inline] +fn write32(addr: u64, val: u32) { + unsafe { core::ptr::write_volatile(addr as *mut u32, val) } +} + +#[inline] +#[allow(dead_code)] // Part of MMIO register access API +fn read64(addr: u64) -> u64 { + unsafe { core::ptr::read_volatile(addr as *const u64) } +} + +#[inline] +fn write64(addr: u64, val: u64) { + unsafe { core::ptr::write_volatile(addr as *mut u64, val) } +} + +// ============================================================================= +// Timeout Helper +// ============================================================================= + +/// Spin-wait until `f()` returns true, or fail after `max_iters` iterations. +fn wait_for bool>(f: F, max_iters: u32) -> Result<(), &'static str> { + for _ in 0..max_iters { + if f() { + return Ok(()); + } + core::hint::spin_loop(); + } + Err("XHCI timeout") +} + +// ============================================================================= +// Command Ring Operations +// ============================================================================= + +/// Enqueue a TRB onto the command ring. +/// +/// Sets the cycle bit appropriately and handles ring wraparound via a Link TRB. +fn enqueue_command(trb: Trb) { + unsafe { + let idx = CMD_RING_ENQUEUE; + let ring = &raw mut CMD_RING; + let cycle = CMD_RING_CYCLE; + + // Set the cycle bit on the TRB + let mut t = trb; + if cycle { + t.control |= 1; + } else { + t.control &= !1; + } + + core::ptr::write_volatile(&mut (*ring).0[idx] as *mut Trb, t); + + // Cache clean to ensure the controller sees the TRB + dma_cache_clean( + &(*ring).0[idx] as *const Trb as *const u8, + core::mem::size_of::(), + ); + + fence(Ordering::SeqCst); + + // Advance enqueue pointer; last entry is reserved for Link TRB + let next_idx = (idx + 1) % (CMD_RING_SIZE - 1); + CMD_RING_ENQUEUE = next_idx; + + if next_idx == 0 { + // Wrap: write Link TRB pointing back to start of ring + let cmd_ring_phys = virt_to_phys(&raw const CMD_RING as u64); + let link = Trb { + param: cmd_ring_phys, + status: 0, + // Link TRB type, Toggle Cycle (TC) bit 5, plus current cycle bit + control: (trb_type::LINK << 10) + | if cycle { 1 } else { 0 } + | (1 << 5), + }; + core::ptr::write_volatile( + &mut (*ring).0[CMD_RING_SIZE - 1] as *mut Trb, + link, + ); + dma_cache_clean( + &(*ring).0[CMD_RING_SIZE - 1] as *const Trb as *const u8, + core::mem::size_of::(), + ); + CMD_RING_CYCLE = !cycle; + } + } +} + +/// Ring the doorbell for a given slot and target. +/// +/// Slot 0, target 0 = host controller command ring. +/// Slot N, target DCI = endpoint for that device slot. +fn ring_doorbell(state: &XhciState, slot: u8, target: u8) { + write32(state.db_base + (slot as u64) * 4, target as u32); + // DSB to ensure the doorbell write reaches the device + unsafe { + core::arch::asm!("dsb sy", options(nostack, preserves_flags)); + } +} + +/// Wait for an event on the event ring, with timeout. +/// +/// Returns Command Completion and Transfer Event TRBs. +/// Skips asynchronous events (Port Status Change) that may arrive +/// at any time and don't correspond to a specific command or transfer. +fn wait_for_event(state: &XhciState) -> Result { + let mut timeout = 2_000_000u32; + loop { + unsafe { + let ring = &raw const EVENT_RING; + let idx = EVENT_RING_DEQUEUE; + let cycle = EVENT_RING_CYCLE; + + // Invalidate cache to see controller-written TRBs + dma_cache_invalidate( + &(*ring).0[idx] as *const Trb as *const u8, + core::mem::size_of::(), + ); + + let trb = core::ptr::read_volatile(&(*ring).0[idx]); + let trb_cycle = trb.control & 1 != 0; + + if trb_cycle == cycle { + // New event available — advance dequeue + EVENT_RING_DEQUEUE = (idx + 1) % EVENT_RING_SIZE; + if EVENT_RING_DEQUEUE == 0 { + EVENT_RING_CYCLE = !cycle; + } + + // Update ERDP to acknowledge the event + let erdp_phys = virt_to_phys(&raw const EVENT_RING as u64) + + (EVENT_RING_DEQUEUE as u64) * 16; + let ir0 = state.rt_base + 0x20; // Interrupter 0 + write64(ir0 + 0x18, erdp_phys | (1 << 3)); + + let trb_type_val = trb.trb_type(); + if trb_type_val == trb_type::COMMAND_COMPLETION + || trb_type_val == trb_type::TRANSFER_EVENT + { + return Ok(trb); + } + // Asynchronous event (Port Status Change, etc.) — skip and + // keep waiting for the command/transfer completion we expect. + continue; + } + } + timeout -= 1; + if timeout == 0 { + return Err("XHCI event timeout"); + } + core::hint::spin_loop(); + } +} + +/// Issue a Stop Endpoint command for EP0 (non-blocking). +/// +/// After issuing, transitions to WAIT_STOP_EP state. The Command Completion +/// event will be handled by the event loop in poll_hid_events. +fn issue_stop_ep0(state: &XhciState, slot_id: u8) { + let dci: u32 = 1; // EP0 + let stop_trb = Trb { + param: 0, + status: 0, + control: (trb_type::STOP_ENDPOINT << 10) + | (dci << 16) + | ((slot_id as u32) << 24), + }; + enqueue_command(stop_trb); + ring_doorbell(state, 0, 0); + EP0_STATE.store(ep0_state::WAIT_STOP_EP, Ordering::Release); +} + +/// Handle the Command Completion for Stop Endpoint: zero the ring, +/// then issue Set TR Dequeue Pointer (non-blocking). +fn handle_stop_ep_complete(state: &XhciState, slot_id: u8, cc: u32) { + // SUCCESS (1) or CONTEXT_STATE_ERROR (19) both acceptable + if cc != 1 && cc != 19 { + EP0_RESET_FAIL_COUNT.fetch_add(1, Ordering::Relaxed); + EP0_STATE.store(ep0_state::IDLE, Ordering::Release); + return; + } + + let slot_idx = (slot_id - 1) as usize; + + // Zero the transfer ring and reset enqueue pointer + unsafe { + core::ptr::write_bytes( + TRANSFER_RINGS[slot_idx].as_mut_ptr() as *mut u8, + 0, + TRANSFER_RING_SIZE * 16, + ); + TRANSFER_ENQUEUE[slot_idx] = 0; + TRANSFER_CYCLE[slot_idx] = true; + dma_cache_clean( + TRANSFER_RINGS[slot_idx].as_ptr() as *const u8, + TRANSFER_RING_SIZE * 16, + ); + } + + // Issue Set TR Dequeue Pointer command + let ring_phys = virt_to_phys(unsafe { &raw const TRANSFER_RINGS[slot_idx] } as u64); + let dci: u32 = 1; + let set_deq_trb = Trb { + param: ring_phys | 1, // DCS = 1 (matching reset TRANSFER_CYCLE) + status: 0, + control: (trb_type::SET_TR_DEQUEUE_POINTER << 10) + | (dci << 16) + | ((slot_id as u32) << 24), + }; + enqueue_command(set_deq_trb); + ring_doorbell(state, 0, 0); + EP0_STATE.store(ep0_state::WAIT_SET_DEQUEUE, Ordering::Release); +} + +/// Handle the Command Completion for Set TR Dequeue Pointer. +fn handle_set_dequeue_complete(cc: u32) { + if cc == 1 { + EP0_POLL_SUBMISSIONS.store(0, Ordering::Relaxed); + EP0_RESET_COUNT.fetch_add(1, Ordering::Relaxed); + } else { + EP0_RESET_FAIL_COUNT.fetch_add(1, Ordering::Relaxed); + } + EP0_STATE.store(ep0_state::IDLE, Ordering::Release); +} + +// ============================================================================= +// Slot and Device Commands +// ============================================================================= + +/// Issue an Enable Slot command and return the assigned slot ID. +fn enable_slot(state: &XhciState) -> Result { + let trb = Trb { + param: 0, + status: 0, + control: trb_type::ENABLE_SLOT << 10, + }; + enqueue_command(trb); + ring_doorbell(state, 0, 0); + + let event = wait_for_event(state)?; + let cc = event.completion_code(); + if cc != completion_code::SUCCESS { + crate::serial_println!("[xhci] EnableSlot failed: completion code {}", cc); + return Err("XHCI EnableSlot failed"); + } + + let slot_id = event.slot_id(); + crate::serial_println!("[xhci] Enabled slot {}", slot_id); + Ok(slot_id) +} + +/// Issue an Address Device command for the given slot and root hub port. +/// +/// Builds an Input Context with Slot Context and Endpoint 0 (control) Context, +/// then submits the AddressDevice TRB. +fn address_device(state: &XhciState, slot_id: u8, port_id: u8) -> Result<(), &'static str> { + if slot_id as usize > MAX_SLOTS || slot_id == 0 { + return Err("XHCI: invalid slot_id for address_device"); + } + + let slot_idx = (slot_id - 1) as usize; + let ctx_size = state.context_size; + + unsafe { + // Zero the input context + let input_ctx = &raw mut INPUT_CONTEXTS[slot_idx]; + core::ptr::write_bytes((*input_ctx).0.as_mut_ptr(), 0, 4096); + + // Zero the output (device) context + let dev_ctx = &raw mut DEVICE_CONTEXTS[slot_idx]; + core::ptr::write_bytes((*dev_ctx).0.as_mut_ptr(), 0, 4096); + + let input_base = (*input_ctx).0.as_mut_ptr(); + + // --- Input Control Context (first context entry) --- + // Add Context flags: bits 0 and 1 (add Slot Context and EP0 Context) + // The Add Context flags are at offset 0x04 in the Input Control Context + let add_flags_ptr = input_base.add(0x04) as *mut u32; + core::ptr::write_volatile(add_flags_ptr, 0x03); // A0=1 (Slot), A1=1 (EP0) + + // --- Slot Context (second context entry, at offset ctx_size) --- + let slot_ctx = input_base.add(ctx_size); + + // Slot Context DW0: Route String = 0, Speed, Context Entries = 1 + // Speed: We'll read PORTSC to determine speed + let portsc = read32(state.op_base + 0x400 + ((port_id - 1) as u64) * 0x10); + let port_speed = (portsc >> 10) & 0xF; // Port Speed bits [13:10] + + // DW0: Context Entries (bits 31:27) = 1, Speed (bits 23:20) + let slot_dw0: u32 = (1u32 << 27) | (port_speed << 20); + core::ptr::write_volatile(slot_ctx as *mut u32, slot_dw0); + + // DW1: Root Hub Port Number (bits 23:16) + let slot_dw1: u32 = (port_id as u32) << 16; + core::ptr::write_volatile(slot_ctx.add(4) as *mut u32, slot_dw1); + + // --- Endpoint 0 Context (third context entry, at offset ctx_size * 2) --- + let ep0_ctx = input_base.add(ctx_size * 2); + + // Determine max packet size based on port speed + // Speed: 1=Full (64), 2=Low (8), 3=High (64), 4=Super (512) + let max_packet_size: u16 = match port_speed { + 1 => 64, // Full Speed + 2 => 8, // Low Speed + 3 => 64, // High Speed + 4 => 512, // SuperSpeed + _ => 64, // Default to Full Speed + }; + + // EP0 DW1: EP Type (bits 5:3) = 4 (Control Bidirectional), CErr (bits 2:1) = 3 + let ep0_dw1: u32 = (4u32 << 3) | (3u32 << 1); + core::ptr::write_volatile(ep0_ctx.add(0x04) as *mut u32, ep0_dw1); + + // EP0 DW2-DW3: TR Dequeue Pointer + // Each device slot uses its own transfer ring during enumeration. + let ring_ptr = &raw mut TRANSFER_RINGS[slot_idx]; + core::ptr::write_bytes(ring_ptr as *mut u8, 0, TRANSFER_RING_SIZE * 16); + TRANSFER_ENQUEUE[slot_idx] = 0; + TRANSFER_CYCLE[slot_idx] = true; + + let ep0_ring_phys = virt_to_phys(&raw const TRANSFER_RINGS[slot_idx] as u64); + + // DW2: TR Dequeue Pointer (low 32 bits) with DCS (Dequeue Cycle State) = 1 + core::ptr::write_volatile( + ep0_ctx.add(0x08) as *mut u32, + (ep0_ring_phys as u32) | 1, // DCS = 1 + ); + // DW3: TR Dequeue Pointer (high 32 bits) + core::ptr::write_volatile( + ep0_ctx.add(0x0C) as *mut u32, + (ep0_ring_phys >> 32) as u32, + ); + + // EP0 DW4: Max Packet Size (bits 31:16), Average TRB Length (bits 15:0) + let ep0_dw4: u32 = ((max_packet_size as u32) << 16) | 8; // Avg TRB len = 8 for control + core::ptr::write_volatile(ep0_ctx.add(0x10) as *mut u32, ep0_dw4); + + // Cache-clean the input context + dma_cache_clean(input_base, 4096); + + // Set the output device context pointer in DCBAA + let dev_ctx_phys = virt_to_phys(&raw const DEVICE_CONTEXTS[slot_idx] as u64); + let dcbaa = &raw mut DCBAA; + (*dcbaa).0[slot_id as usize] = dev_ctx_phys; + dma_cache_clean( + &(*dcbaa).0[slot_id as usize] as *const u64 as *const u8, + 8, + ); + + // Build AddressDevice TRB + let input_ctx_phys = virt_to_phys(&raw const INPUT_CONTEXTS[slot_idx] as u64); + let trb = Trb { + param: input_ctx_phys, + status: 0, + // AddressDevice type, Slot ID in bits 31:24 + control: (trb_type::ADDRESS_DEVICE << 10) | ((slot_id as u32) << 24), + }; + enqueue_command(trb); + ring_doorbell(state, 0, 0); + + let event = wait_for_event(state)?; + let cc = event.completion_code(); + if cc != completion_code::SUCCESS { + crate::serial_println!( + "[xhci] AddressDevice slot {} failed: completion code {}", + slot_id, + cc + ); + return Err("XHCI AddressDevice failed"); + } + + crate::serial_println!("[xhci] Addressed device in slot {}", slot_id); + Ok(()) + } +} + +// ============================================================================= +// Transfer Ring Operations +// ============================================================================= + +/// Enqueue a TRB on a HID transfer ring (keyboard index 0, mouse index 1). +fn enqueue_transfer(hid_idx: usize, trb: Trb) { + unsafe { + let idx = TRANSFER_ENQUEUE[hid_idx]; + let cycle = TRANSFER_CYCLE[hid_idx]; + + let mut t = trb; + if cycle { + t.control |= 1; + } else { + t.control &= !1; + } + + core::ptr::write_volatile( + &mut TRANSFER_RINGS[hid_idx][idx] as *mut Trb, + t, + ); + dma_cache_clean( + &TRANSFER_RINGS[hid_idx][idx] as *const Trb as *const u8, + core::mem::size_of::(), + ); + + fence(Ordering::SeqCst); + + let next_idx = (idx + 1) % (TRANSFER_RING_SIZE - 1); + TRANSFER_ENQUEUE[hid_idx] = next_idx; + + if next_idx == 0 { + // Write Link TRB + let ring_phys = virt_to_phys(&raw const TRANSFER_RINGS[hid_idx] as u64); + let link = Trb { + param: ring_phys, + status: 0, + control: (trb_type::LINK << 10) + | if cycle { 1 } else { 0 } + | (1 << 5), // TC bit + }; + core::ptr::write_volatile( + &mut TRANSFER_RINGS[hid_idx][TRANSFER_RING_SIZE - 1] as *mut Trb, + link, + ); + dma_cache_clean( + &TRANSFER_RINGS[hid_idx][TRANSFER_RING_SIZE - 1] as *const Trb as *const u8, + core::mem::size_of::(), + ); + TRANSFER_CYCLE[hid_idx] = !cycle; + } + } +} + +// ============================================================================= +// Control Transfers (Setup -> Data -> Status) +// ============================================================================= + +/// Execute a control transfer on a device's default control endpoint (EP0). +/// +/// Sends a Setup stage TRB, optional Data stage TRB, and Status stage TRB +/// on the device's EP0 transfer ring, then waits for completion. +/// +/// # Arguments +/// * `state` - Controller state +/// * `slot_id` - Device slot ID (1-based) +/// * `setup` - USB Setup Packet +/// * `data_buf_phys` - Physical address of data buffer (0 if no data stage) +/// * `data_len` - Length of data transfer (0 if no data stage) +/// * `direction_in` - true for device-to-host (IN), false for host-to-device (OUT) +fn control_transfer( + state: &XhciState, + slot_id: u8, + setup: &SetupPacket, + data_buf_phys: u64, + data_len: u16, + direction_in: bool, +) -> Result<(), &'static str> { + let slot_idx = (slot_id - 1) as usize; + + // Setup Stage TRB + // The setup packet (8 bytes) is inlined in the TRB param field + let setup_data: u64 = unsafe { + core::ptr::read_unaligned(setup as *const SetupPacket as *const u64) + }; + + // TRT (Transfer Type): 0=No Data, 2=OUT Data, 3=IN Data + let trt: u32 = if data_len == 0 { + 0 + } else if direction_in { + 3 + } else { + 2 + }; + + let setup_trb = Trb { + param: setup_data, + status: 8, // Transfer length = 8 (setup packet size) + // Setup Stage: TRB type = 2, IDT (Immediate Data) bit 6, TRT bits 17:16 + control: (trb_type::SETUP_STAGE << 10) | (1 << 6) | (trt << 16), + }; + enqueue_transfer(slot_idx, setup_trb); + + // Data Stage TRB (if any) + if data_len > 0 { + // Direction bit 16: 1 = IN (device-to-host), 0 = OUT + let dir_bit: u32 = if direction_in { 1 << 16 } else { 0 }; + let data_trb = Trb { + param: data_buf_phys, + status: data_len as u32, + control: (trb_type::DATA_STAGE << 10) | dir_bit, + }; + enqueue_transfer(slot_idx, data_trb); + } + + // Status Stage TRB + // Direction is opposite of data stage (or IN if no data stage) + let status_dir: u32 = if data_len == 0 || direction_in { 0 } else { 1 << 16 }; + let status_trb = Trb { + param: 0, + status: 0, + // IOC (Interrupt On Completion) bit 5 + control: (trb_type::STATUS_STAGE << 10) | status_dir | (1 << 5), + }; + enqueue_transfer(slot_idx, status_trb); + + // Ring doorbell for EP0 (DCI = 1 for the default control endpoint) + ring_doorbell(state, slot_id, 1); + + // Wait for completion event + let event = wait_for_event(state)?; + let cc = event.completion_code(); + if cc != completion_code::SUCCESS && cc != completion_code::SHORT_PACKET { + crate::serial_println!( + "[xhci] Control transfer failed: slot={} cc={}", + slot_id, + cc + ); + return Err("XHCI control transfer failed"); + } + + Ok(()) +} + +/// Get the device descriptor from a USB device. +fn get_device_descriptor( + state: &XhciState, + slot_id: u8, + buf: &mut [u8; 18], +) -> Result<(), &'static str> { + let setup = SetupPacket { + bm_request_type: 0x80, // Device-to-host, standard, device + b_request: request::GET_DESCRIPTOR, + w_value: (descriptor_type::DEVICE as u16) << 8, // Descriptor type in high byte + w_index: 0, + w_length: 18, + }; + + unsafe { + // Zero the data buffer + let data_buf = &raw mut CTRL_DATA_BUF; + core::ptr::write_bytes((*data_buf).0.as_mut_ptr(), 0, 18); + dma_cache_clean((*data_buf).0.as_ptr(), 18); + + let data_phys = virt_to_phys(&raw const CTRL_DATA_BUF as u64); + + control_transfer(state, slot_id, &setup, data_phys, 18, true)?; + + // Invalidate cache to see device-written data + dma_cache_invalidate((*data_buf).0.as_ptr(), 18); + + // Copy to caller's buffer + buf.copy_from_slice(&(&(*data_buf).0)[..18]); + } + + // Log basic info (copy packed fields to locals to avoid unaligned references) + let desc = unsafe { &*(buf.as_ptr() as *const DeviceDescriptor) }; + let bcd_usb = desc.bcd_usb; + let id_vendor = desc.id_vendor; + let id_product = desc.id_product; + crate::serial_println!( + "[xhci] Device descriptor: USB{}.{} class={:#04x} subclass={:#04x} protocol={:#04x} vendor={:#06x} product={:#06x} maxpkt0={}", + bcd_usb >> 8, + (bcd_usb >> 4) & 0xF, + desc.b_device_class, + desc.b_device_sub_class, + desc.b_device_protocol, + id_vendor, + id_product, + desc.b_max_packet_size0, + ); + + Ok(()) +} + +/// Get the configuration descriptor (and all subordinate descriptors) from a USB device. +fn get_config_descriptor( + state: &XhciState, + slot_id: u8, + buf: &mut [u8; 256], +) -> Result { + // First, read just the 9-byte config descriptor header to get wTotalLength + let setup_header = SetupPacket { + bm_request_type: 0x80, + b_request: request::GET_DESCRIPTOR, + w_value: (descriptor_type::CONFIGURATION as u16) << 8, + w_index: 0, + w_length: 9, + }; + + unsafe { + let data_buf = &raw mut CTRL_DATA_BUF; + core::ptr::write_bytes((*data_buf).0.as_mut_ptr(), 0, 256); + dma_cache_clean((*data_buf).0.as_ptr(), 256); + + let data_phys = virt_to_phys(&raw const CTRL_DATA_BUF as u64); + control_transfer(state, slot_id, &setup_header, data_phys, 9, true)?; + + dma_cache_invalidate((*data_buf).0.as_ptr(), 9); + + let config_desc = &*((*data_buf).0.as_ptr() as *const ConfigDescriptor); + let total_len = config_desc.w_total_length as usize; + + crate::serial_println!( + "[xhci] Config descriptor: total_length={} num_interfaces={} config_value={}", + total_len, + config_desc.b_num_interfaces, + config_desc.b_configuration_value, + ); + + if total_len > 256 { + crate::serial_println!("[xhci] Config descriptor too large ({} bytes), truncating", total_len); + } + + let fetch_len = total_len.min(256) as u16; + + // Now read the full configuration descriptor set + let setup_full = SetupPacket { + bm_request_type: 0x80, + b_request: request::GET_DESCRIPTOR, + w_value: (descriptor_type::CONFIGURATION as u16) << 8, + w_index: 0, + w_length: fetch_len, + }; + + core::ptr::write_bytes((*data_buf).0.as_mut_ptr(), 0, 256); + dma_cache_clean((*data_buf).0.as_ptr(), 256); + + control_transfer(state, slot_id, &setup_full, data_phys, fetch_len, true)?; + + dma_cache_invalidate((*data_buf).0.as_ptr(), fetch_len as usize); + + buf[..fetch_len as usize].copy_from_slice(&(&(*data_buf).0)[..fetch_len as usize]); + Ok(fetch_len as usize) + } +} + +/// Send SET_CONFIGURATION request to select a configuration. +fn set_configuration( + state: &XhciState, + slot_id: u8, + config_value: u8, +) -> Result<(), &'static str> { + let setup = SetupPacket { + bm_request_type: 0x00, // Host-to-device, standard, device + b_request: request::SET_CONFIGURATION, + w_value: config_value as u16, + w_index: 0, + w_length: 0, + }; + + control_transfer(state, slot_id, &setup, 0, 0, false)?; + crate::serial_println!("[xhci] Set configuration {} on slot {}", config_value, slot_id); + Ok(()) +} + +/// Send SET_PROTOCOL request to set boot protocol on a HID interface. +fn set_boot_protocol( + state: &XhciState, + slot_id: u8, + interface: u8, +) -> Result<(), &'static str> { + let setup = SetupPacket { + bm_request_type: 0x21, // Host-to-device, class, interface + b_request: hid_request::SET_PROTOCOL, + w_value: 0, // 0 = Boot Protocol + w_index: interface as u16, + w_length: 0, + }; + + control_transfer(state, slot_id, &setup, 0, 0, false)?; + crate::serial_println!( + "[xhci] Set boot protocol on slot {} interface {}", + slot_id, + interface + ); + Ok(()) +} + +/// Send SET_IDLE request to a HID interface (duration=0 = indefinite). +fn set_idle( + state: &XhciState, + slot_id: u8, + interface: u8, +) -> Result<(), &'static str> { + let setup = SetupPacket { + bm_request_type: 0x21, // Host-to-device, class, interface + b_request: hid_request::SET_IDLE, + w_value: 0, // Duration = 0 (indefinite), Report ID = 0 + w_index: interface as u16, + w_length: 0, + }; + + control_transfer(state, slot_id, &setup, 0, 0, false)?; + Ok(()) +} + +/// Fetch and log the HID Report Descriptor for diagnostic purposes. +/// +/// The Report Descriptor reveals the actual report format: whether Report IDs +/// are used, the field layout, and the report size. This is critical for +/// understanding what data the interrupt endpoint delivers. +fn fetch_hid_report_descriptor(state: &XhciState, slot_id: u8, interface: u8) { + // GET_DESCRIPTOR for HID Report Descriptor uses interface recipient + let setup = SetupPacket { + bm_request_type: 0x81, // Device-to-host, standard, interface + b_request: request::GET_DESCRIPTOR, + w_value: (descriptor_type::HID_REPORT as u16) << 8, // Report descriptor type + w_index: interface as u16, + w_length: 128, // Request up to 128 bytes + }; + + unsafe { + let data_buf = &raw mut CTRL_DATA_BUF; + core::ptr::write_bytes((*data_buf).0.as_mut_ptr(), 0, 128); + dma_cache_clean((*data_buf).0.as_ptr(), 128); + + let data_phys = virt_to_phys(&raw const CTRL_DATA_BUF as u64); + + match control_transfer(state, slot_id, &setup, data_phys, 128, true) { + Ok(()) => { + dma_cache_invalidate((*data_buf).0.as_ptr(), 128); + let buf = &(*data_buf).0; + + // Find actual length (trim trailing zeros) + let mut len = 128; + while len > 0 && buf[len - 1] == 0 { + len -= 1; + } + + crate::serial_println!( + "[xhci] HID Report Descriptor (iface {}, {} bytes):", + interface, len + ); + + // Print in hex, 16 bytes per line + let mut i = 0; + while i < len { + let end = if i + 16 < len { i + 16 } else { len }; + let mut hex_buf = [0u8; 48]; // 16 * 3 = 48 + let mut pos = 0; + for j in i..end { + let hi = buf[j] >> 4; + let lo = buf[j] & 0x0F; + hex_buf[pos] = if hi < 10 { b'0' + hi } else { b'a' + hi - 10 }; + pos += 1; + hex_buf[pos] = if lo < 10 { b'0' + lo } else { b'a' + lo - 10 }; + pos += 1; + hex_buf[pos] = b' '; + pos += 1; + } + // Convert to str for serial_println + if let Ok(s) = core::str::from_utf8(&hex_buf[..pos]) { + crate::serial_println!(" {}", s); + } + i += 16; + } + } + Err(e) => { + crate::serial_println!( + "[xhci] Failed to get HID Report Descriptor (iface {}): {}", + interface, e + ); + } + } + } +} + +// ============================================================================= +// Endpoint Configuration (Configure Endpoint Command) +// ============================================================================= + +/// Configure an interrupt IN endpoint for a HID device. +/// +/// Builds an Input Context with the endpoint context and issues a +/// ConfigureEndpoint command to the controller. +fn configure_interrupt_endpoint( + state: &XhciState, + slot_id: u8, + ep_desc: &EndpointDescriptor, + hid_idx: usize, // 0 = keyboard, 1 = mouse (offset by HID_RING_BASE for ring access) + ss_max_burst: u8, // from SS Endpoint Companion Descriptor (0 if not present) + ss_bytes_per_interval: u16, // from SS Endpoint Companion Descriptor (0 if not present) +) -> Result { + let ring_idx = HID_RING_BASE + hid_idx; // Separate from EP0 slot rings + let slot_idx = (slot_id - 1) as usize; + let ctx_size = state.context_size; + + // Calculate the Device Context Index (DCI) for this endpoint. + // DCI = 2 * endpoint_number + direction (0=OUT, 1=IN) + let ep_num = ep_desc.endpoint_number(); + let dci = ep_num * 2 + if ep_desc.is_in() { 1 } else { 0 }; + + let max_packet_size = ep_desc.w_max_packet_size; + + // Read port speed from Slot Context to determine interval encoding + let port_speed = unsafe { + let dev_ctx = &raw const DEVICE_CONTEXTS[slot_idx]; + dma_cache_invalidate((*dev_ctx).0.as_ptr(), 4096); + let slot_dw0 = core::ptr::read_volatile((*dev_ctx).0.as_ptr() as *const u32); + (slot_dw0 >> 20) & 0xF + }; + + crate::serial_println!( + "[xhci] Configuring interrupt EP: addr={:#04x} num={} DCI={} maxpkt={} interval={} speed={} ss_burst={} ss_esit={}", + ep_desc.b_endpoint_address, + ep_num, + dci, + max_packet_size, + ep_desc.b_interval, + port_speed, + ss_max_burst, + ss_bytes_per_interval, + ); + + unsafe { + // Zero and rebuild the input context + let input_ctx = &raw mut INPUT_CONTEXTS[slot_idx]; + core::ptr::write_bytes((*input_ctx).0.as_mut_ptr(), 0, 4096); + + let input_base = (*input_ctx).0.as_mut_ptr(); + + // Input Control Context: Add flags for Slot Context and the target endpoint + // A0 = 1 (Slot), A[dci] = 1 + let add_flags: u32 = 1 | (1u32 << dci); + core::ptr::write_volatile(input_base.add(0x04) as *mut u32, add_flags); + + // Slot Context: Update Context Entries to include the new endpoint + let slot_ctx = input_base.add(ctx_size); + // Read current slot context from device context + let dev_ctx = &raw const DEVICE_CONTEXTS[slot_idx]; + dma_cache_invalidate((*dev_ctx).0.as_ptr(), 4096); + + // Copy all 4 DWORDs of the current slot context from device context + for dw_offset in (0..16).step_by(4) { + let val = core::ptr::read_volatile( + (*dev_ctx).0.as_ptr().add(dw_offset) as *const u32, + ); + core::ptr::write_volatile(slot_ctx.add(dw_offset) as *mut u32, val); + } + // Update Context Entries in DW0 to include the new endpoint DCI + let current_slot_dw0 = core::ptr::read_volatile(slot_ctx as *const u32); + let current_entries = (current_slot_dw0 >> 27) & 0x1F; + let new_entries = current_entries.max(dci as u32); + let new_slot_dw0 = (current_slot_dw0 & !(0x1F << 27)) | (new_entries << 27); + core::ptr::write_volatile(slot_ctx as *mut u32, new_slot_dw0); + + // Endpoint Context at offset (1 + dci) * ctx_size + let ep_ctx = input_base.add((1 + dci as usize) * ctx_size); + + let max_pkt = (ep_desc.w_max_packet_size & 0x07FF) as u32; // Bits 10:0 + let max_burst = ss_max_burst as u32; + + if USE_BULK_FOR_INTERRUPT { + // Bulk IN workaround: skip interval/ESIT, set EP Type = Bulk IN (6). + // EP DW0: all zeros (no interval, no ESIT hi for bulk) + core::ptr::write_volatile(ep_ctx as *mut u32, 0u32); + + // EP DW1: Max Packet Size, Max Burst, EP Type=6 (Bulk IN), CErr=3 + let ep_type: u32 = 6; // Bulk IN + let cerr: u32 = 3; + let ep_dw1: u32 = (max_pkt << 16) | (max_burst << 8) | (ep_type << 3) | (cerr << 1); + core::ptr::write_volatile(ep_ctx.add(0x04) as *mut u32, ep_dw1); + + crate::serial_println!( + "[xhci] Using Bulk IN (type=6) workaround for interrupt EP DCI {}", + dci + ); + } else { + // Standard Interrupt IN configuration + // EP DW0: Interval (bits 23:16), Max ESIT Payload Hi (bits 31:24) + let interval: u32 = if port_speed >= 3 { + let bi = ep_desc.b_interval.clamp(1, 16); + (bi - 1) as u32 + } else { + let bi = ep_desc.b_interval.clamp(1, 255); + let ms_interval = bi as u32; + let mut n = 0u32; + while (125u32 << n) < ms_interval * 1000 && n < 15 { + n += 1; + } + n + }; + + let esit_payload = if ss_bytes_per_interval > 0 { + ss_bytes_per_interval as u32 + } else { + max_pkt * (max_burst + 1) + }; + let esit_hi = (esit_payload >> 16) & 0xFF; + let ep_dw0: u32 = (esit_hi << 24) | (interval << 16); + core::ptr::write_volatile(ep_ctx as *mut u32, ep_dw0); + + // EP DW1: Interrupt IN type = 7 + let ep_type: u32 = 7; // Interrupt IN + let cerr: u32 = 3; + let ep_dw1: u32 = (max_pkt << 16) | (max_burst << 8) | (ep_type << 3) | (cerr << 1); + core::ptr::write_volatile(ep_ctx.add(0x04) as *mut u32, ep_dw1); + } + + // Clear and set up the HID transfer ring for this device + let ring = &raw mut TRANSFER_RINGS[ring_idx]; + core::ptr::write_bytes(ring as *mut u8, 0, TRANSFER_RING_SIZE * 16); + TRANSFER_ENQUEUE[ring_idx] = 0; + TRANSFER_CYCLE[ring_idx] = true; + + let ring_phys = virt_to_phys(&raw const TRANSFER_RINGS[ring_idx] as u64); + + // EP DW2-DW3: TR Dequeue Pointer with DCS = 1 + core::ptr::write_volatile( + ep_ctx.add(0x08) as *mut u32, + (ring_phys as u32) | 1, // DCS = 1 + ); + core::ptr::write_volatile( + ep_ctx.add(0x0C) as *mut u32, + (ring_phys >> 32) as u32, + ); + + // EP DW4: Average TRB Length (bits 15:0), Max ESIT Payload Lo (bits 31:16) + if USE_BULK_FOR_INTERRUPT { + // For Bulk: Average TRB Length = max_pkt, no ESIT payload + let ep_dw4: u32 = max_pkt & 0xFFFF; // avg_trb_len only + core::ptr::write_volatile(ep_ctx.add(0x10) as *mut u32, ep_dw4); + } else { + // For Interrupt: ESIT Payload Lo + avg = max_esit_payload + let esit_payload = if ss_bytes_per_interval > 0 { + ss_bytes_per_interval as u32 + } else { + max_pkt * (max_burst + 1) + }; + let esit_lo = esit_payload & 0xFFFF; + let avg_trb_len = esit_payload; + let ep_dw4: u32 = (esit_lo << 16) | avg_trb_len; + core::ptr::write_volatile(ep_ctx.add(0x10) as *mut u32, ep_dw4); + } + + // Cache-clean the input context + dma_cache_clean(input_base, 4096); + + // Issue ConfigureEndpoint command + let input_ctx_phys = virt_to_phys(&raw const INPUT_CONTEXTS[slot_idx] as u64); + let trb = Trb { + param: input_ctx_phys, + status: 0, + control: (trb_type::CONFIGURE_ENDPOINT << 10) | ((slot_id as u32) << 24), + }; + enqueue_command(trb); + ring_doorbell(state, 0, 0); + + let event = wait_for_event(state)?; + let cc = event.completion_code(); + if cc != completion_code::SUCCESS { + crate::serial_println!( + "[xhci] ConfigureEndpoint failed: slot={} dci={} cc={}", + slot_id, + dci, + cc + ); + return Err("XHCI ConfigureEndpoint failed"); + } + + // Verify: read back device context to check endpoint AND slot state + dma_cache_invalidate((*dev_ctx).0.as_ptr(), 4096); + + // Check Slot Context: Context Entries must include DCI + let slot_out_dw0 = core::ptr::read_volatile( + (*dev_ctx).0.as_ptr() as *const u32, + ); + let ctx_entries = (slot_out_dw0 >> 27) & 0x1F; + // Note: slot state is in DW3 bits 31:27, not DW0 + let slot_out_dw3 = core::ptr::read_volatile( + (*dev_ctx).0.as_ptr().add(12) as *const u32, + ); + let device_addr = slot_out_dw3 & 0xFF; + let slot_st = (slot_out_dw3 >> 27) & 0x1F; + crate::serial_println!( + "[xhci] Slot {} context after ConfigureEndpoint: ctx_entries={} slot_state={} dev_addr={}", + slot_id, ctx_entries, slot_st, device_addr, + ); + + if (dci as u32) > ctx_entries { + crate::serial_println!( + "[xhci] WARNING: DCI {} > Context Entries {}! Endpoint out of range!", + dci, ctx_entries, + ); + } + + // Check Endpoint Context + let ep_out = (*dev_ctx).0.as_ptr().add((dci as usize) * ctx_size); + let ep_out_dw0 = core::ptr::read_volatile(ep_out as *const u32); + let ep_state = ep_out_dw0 & 0x7; // Bits [2:0] = EP State + let ep_out_dw1 = core::ptr::read_volatile(ep_out.add(4) as *const u32); + let ep_out_dw2 = core::ptr::read_volatile(ep_out.add(8) as *const u32); + let ep_out_dw3 = core::ptr::read_volatile(ep_out.add(12) as *const u32); + let tr_dequeue = (ep_out_dw2 as u64) | ((ep_out_dw3 as u64) << 32); + + let ring_phys_verify = virt_to_phys(&raw const TRANSFER_RINGS[ring_idx] as u64); + + crate::serial_println!( + "[xhci] Configured endpoint DCI {} for slot {}: state={} type={} ring_phys={:#x} ctx_dequeue={:#x}", + dci, slot_id, + ep_state, + (ep_out_dw1 >> 3) & 0x7, + ring_phys_verify, + tr_dequeue & !0xF, // Mask out DCS and reserved bits + ); + + // EP State: 0=Disabled, 1=Running, 2=Halted, 3=Stopped, 4=Error + if ep_state == 0 { + crate::serial_println!( + "[xhci] WARNING: Endpoint DCI {} still Disabled after ConfigureEndpoint SUCCESS!", + dci + ); + } + + Ok(dci) + } +} + +// ============================================================================= +// HID Configuration and Transfer Queueing +// ============================================================================= + +/// Parse configuration descriptor, find HID interfaces, configure endpoints, +/// and start polling for HID reports. +fn configure_hid( + state: &mut XhciState, + slot_id: u8, + config_buf: &[u8], + config_len: usize, +) -> Result<(), &'static str> { + // Parse the configuration descriptor header + if config_len < 9 { + return Err("Config descriptor too short"); + } + let config_desc = unsafe { &*(config_buf.as_ptr() as *const ConfigDescriptor) }; + let config_value = config_desc.b_configuration_value; + + // Walk the descriptor chain looking for HID interfaces + let mut offset = config_desc.b_length as usize; + let mut found_hid = false; + + while offset + 2 <= config_len { + let desc_len = config_buf[offset] as usize; + let desc_type = config_buf[offset + 1]; + + if desc_len == 0 { + break; // Prevent infinite loop + } + if offset + desc_len > config_len { + break; + } + + if desc_type == descriptor_type::INTERFACE && desc_len >= 9 { + let iface = unsafe { + &*(config_buf.as_ptr().add(offset) as *const InterfaceDescriptor) + }; + + if iface.b_interface_class == class_code::HID + && iface.b_interface_sub_class == hid_subclass::BOOT + { + crate::serial_println!( + "[xhci] Found HID boot interface: number={} protocol={} endpoints={}", + iface.b_interface_number, + iface.b_interface_protocol, + iface.b_num_endpoints, + ); + + // Look for the interrupt IN endpoint following this interface + let mut ep_offset = offset + desc_len; + while ep_offset + 2 <= config_len { + let ep_len = config_buf[ep_offset] as usize; + let ep_type = config_buf[ep_offset + 1]; + + if ep_len == 0 { + break; + } + if ep_offset + ep_len > config_len { + break; + } + + // Stop if we hit the next interface descriptor + if ep_type == descriptor_type::INTERFACE { + break; + } + + if ep_type == descriptor_type::ENDPOINT && ep_len >= 7 { + let ep_desc = unsafe { + &*(config_buf.as_ptr().add(ep_offset) as *const EndpointDescriptor) + }; + + if ep_desc.is_interrupt() && ep_desc.is_in() { + // Check for SS Endpoint Companion Descriptor (type 0x30) + // immediately following this endpoint descriptor + let mut ss_max_burst: u8 = 0; + let mut ss_bytes_per_interval: u16 = 0; + let ss_offset = ep_offset + ep_len; + if ss_offset + 2 <= config_len { + let ss_len = config_buf[ss_offset] as usize; + let ss_type = config_buf[ss_offset + 1]; + if ss_type == 0x30 && ss_len >= 6 && ss_offset + ss_len <= config_len { + ss_max_burst = config_buf[ss_offset + 2]; + ss_bytes_per_interval = u16::from_le_bytes([ + config_buf[ss_offset + 4], + config_buf[ss_offset + 5], + ]); + crate::serial_println!( + "[xhci] SS EP Companion: maxBurst={} bytesPerInterval={}", + ss_max_burst, ss_bytes_per_interval, + ); + } + } + + // First, set configuration if we haven't yet + if !found_hid { + set_configuration(state, slot_id, config_value)?; + found_hid = true; + } + + // Determine HID device type + let (hid_idx, is_keyboard) = + if iface.b_interface_protocol == hid_protocol::KEYBOARD { + (0usize, true) + } else { + (1usize, false) + }; + + // Set boot protocol and idle + match set_boot_protocol(state, slot_id, iface.b_interface_number) { + Ok(()) => { crate::serial_println!( + "[xhci] SET_PROTOCOL(boot) succeeded on slot {} iface {}", + slot_id, iface.b_interface_number + ); } + Err(e) => { crate::serial_println!( + "[xhci] SET_PROTOCOL(boot) FAILED on slot {} iface {}: {}", + slot_id, iface.b_interface_number, e + ); } + } + let _ = set_idle(state, slot_id, iface.b_interface_number); + + // Fetch and log HID Report Descriptor to understand the report format + fetch_hid_report_descriptor(state, slot_id, iface.b_interface_number); + + // Configure the interrupt endpoint + let dci = configure_interrupt_endpoint( + state, slot_id, ep_desc, hid_idx, + ss_max_burst, ss_bytes_per_interval, + )?; + + // Record the slot/endpoint for interrupt handling + if is_keyboard { + state.kbd_slot = slot_id; + state.kbd_endpoint = dci; + crate::serial_println!( + "[xhci] Keyboard configured: slot={} DCI={}", + slot_id, + dci + ); + } else { + state.mouse_slot = slot_id; + state.mouse_endpoint = dci; + crate::serial_println!( + "[xhci] Mouse configured: slot={} DCI={}", + slot_id, + dci + ); + } + // NOTE: Initial HID transfers are queued in start_hid_polling() + // AFTER all port scanning is complete, to avoid transfer events + // being consumed by wait_for_event during subsequent port commands. + + break; // Found the endpoint for this interface + } + } + + ep_offset += ep_len; + } + } + } + + offset += desc_len; + } + + if !found_hid { + crate::serial_println!("[xhci] No HID boot interfaces found on slot {}", slot_id); + } + + Ok(()) +} + +/// Queue a Normal TRB on a HID transfer ring to receive an interrupt IN report. +fn queue_hid_transfer( + state: &XhciState, + hid_idx: usize, // 0 = keyboard, 1 = mouse + slot_id: u8, + dci: u8, +) -> Result<(), &'static str> { + let ring_idx = HID_RING_BASE + hid_idx; + + // Determine the physical address of the report buffer + let buf_phys = if hid_idx == 0 { + virt_to_phys((&raw const KBD_REPORT_BUF) as u64) + } else { + virt_to_phys((&raw const MOUSE_REPORT_BUF) as u64) + }; + + // Fill report buffer with sentinel (0xDE) before giving it to the controller. + // After DMA completion, we check if the sentinel was overwritten — this tells + // us definitively whether the XHCI DMA wrote actual data to the buffer. + unsafe { + if hid_idx == 0 { + let buf = &raw mut KBD_REPORT_BUF; + core::ptr::write_bytes((*buf).0.as_mut_ptr(), 0xDE, 8); + dma_cache_clean((*buf).0.as_ptr(), 8); + } else { + let buf = &raw mut MOUSE_REPORT_BUF; + core::ptr::write_bytes((*buf).0.as_mut_ptr(), 0xDE, 8); + dma_cache_clean((*buf).0.as_ptr(), 8); + } + } + + // Normal TRB for interrupt IN transfer + let trb = Trb { + param: buf_phys, + status: 8, // Transfer length = 8 bytes + // Normal TRB type, IOC (bit 5), ISP (Interrupt on Short Packet, bit 2) + control: (trb_type::NORMAL << 10) | (1 << 5) | (1 << 2), + }; + enqueue_transfer(ring_idx, trb); + + // Ring the doorbell for this endpoint + ring_doorbell(state, slot_id, dci); + + Ok(()) +} + +/// Submit a GET_REPORT control transfer on EP0 asynchronously (non-blocking). +/// +/// Enqueues Setup + Data + Status stage TRBs on the device's EP0 transfer ring +/// and rings the doorbell. The caller must check the event ring for the completion +/// in a later poll cycle. +/// +/// When the ring is nearly full, issues Stop Endpoint + Set TR Dequeue Pointer +/// commands to reset the ring back to entry 0. This is necessary because the +/// Parallels XHCI emulation does not follow Link TRBs in transfer rings. +/// +/// Enqueue a HID GET_REPORT control transfer on EP0. +/// +/// Uses KBD_REPORT_BUF as the data buffer for the 8-byte boot keyboard report. +/// Caller must ensure the ring has room (at least 3 entries). +fn submit_ep0_get_report(state: &XhciState, slot_id: u8) { + let slot_idx = (slot_id - 1) as usize; + + let setup = SetupPacket { + bm_request_type: 0xA1, + b_request: 0x01, // GET_REPORT + w_value: 0x0100, // Input report, ID 0 + w_index: 0, + w_length: 8, + }; + + let setup_data: u64 = unsafe { + core::ptr::read_unaligned(&setup as *const SetupPacket as *const u64) + }; + + // Fill buffer with sentinel (0xDE) and clean+invalidate to ensure it's in RAM. + // After the transfer, we check if the sentinel was overwritten by DMA. + unsafe { + let buf = &raw mut KBD_REPORT_BUF; + core::ptr::write_bytes((*buf).0.as_mut_ptr(), 0xDE, 8); + dma_cache_clean((*buf).0.as_ptr(), 8); + dma_cache_invalidate((*buf).0.as_ptr(), 8); + } + + let buf_phys = virt_to_phys((&raw const KBD_REPORT_BUF) as u64); + + // Setup Stage TRB: TRT = 3 (IN Data Stage) + let setup_trb = Trb { + param: setup_data, + status: 8, // Setup packet = 8 bytes + control: (trb_type::SETUP_STAGE << 10) | (1 << 6) | (3 << 16), // IDT=1, TRT=IN + }; + enqueue_transfer(slot_idx, setup_trb); + + // Data Stage TRB: IN direction, 8 bytes + let data_trb = Trb { + param: buf_phys, + status: 8, + control: (trb_type::DATA_STAGE << 10) | (1 << 16), // DIR=IN + }; + enqueue_transfer(slot_idx, data_trb); + + // Status Stage TRB: OUT direction (opposite of data), IOC + let status_trb = Trb { + param: 0, + status: 0, + control: (trb_type::STATUS_STAGE << 10) | (1 << 5), // IOC, DIR=OUT (bit 16 = 0) + }; + enqueue_transfer(slot_idx, status_trb); + + EP0_POLL_SUBMISSIONS.fetch_add(1, Ordering::Relaxed); + + // Ring doorbell for EP0 (DCI = 1) + ring_doorbell(state, slot_id, 1); +} + +/// Drain any stale events left in the event ring after enumeration. +/// +/// During enumeration, some xHCI controllers may leave Transfer Events +/// (e.g., Short Packet events for Data Stage TRBs) that weren't consumed +/// by wait_for_event. These must be drained before starting HID polling. +fn drain_stale_events(state: &XhciState) { + let mut drained = 0u32; + unsafe { + loop { + let ring = &raw const EVENT_RING; + let idx = EVENT_RING_DEQUEUE; + let cycle = EVENT_RING_CYCLE; + + dma_cache_invalidate( + &(*ring).0[idx] as *const Trb as *const u8, + core::mem::size_of::(), + ); + + let trb = core::ptr::read_volatile(&(*ring).0[idx]); + let trb_cycle = trb.control & 1 != 0; + + if trb_cycle != cycle { + break; // No more events + } + + let trb_type_val = trb.trb_type(); + crate::serial_println!( + "[xhci] Draining stale event #{}: type={} slot={} ep={} cc={} param={:#x}", + drained, + trb_type_val, + trb.slot_id(), + (trb.control >> 16) & 0x1F, + trb.completion_code(), + trb.param, + ); + + // Advance dequeue pointer + EVENT_RING_DEQUEUE = (idx + 1) % EVENT_RING_SIZE; + if EVENT_RING_DEQUEUE == 0 { + EVENT_RING_CYCLE = !cycle; + } + + // Update ERDP with EHB bit + let ir0 = state.rt_base + 0x20; + let erdp_phys = virt_to_phys(&raw const EVENT_RING as u64) + + (EVENT_RING_DEQUEUE as u64) * 16; + write64(ir0 + 0x18, erdp_phys | (1 << 3)); + + drained += 1; + if drained >= 32 { + break; // Safety limit + } + } + } + if drained > 0 { + crate::serial_println!("[xhci] Drained {} stale events from event ring", drained); + } else { + crate::serial_println!("[xhci] No stale events in event ring"); + } +} + +/// Test synchronous GET_REPORT and GET_PROTOCOL during init. +/// +/// Called after keyboard is configured to diagnose whether Parallels echoes +/// setup packet bytes for class-specific requests or if the issue is +/// specific to the async EP0 polling path. +fn test_sync_class_requests(state: &XhciState, slot_id: u8) { + // Log physical addresses for diagnostic comparison + let ctrl_buf_phys = virt_to_phys((&raw const CTRL_DATA_BUF) as u64); + let kbd_buf_phys = virt_to_phys((&raw const KBD_REPORT_BUF) as u64); + crate::serial_println!( + "[xhci] Buffer phys addrs: CTRL_DATA_BUF={:#010x} KBD_REPORT_BUF={:#010x}", + ctrl_buf_phys, kbd_buf_phys, + ); + + // Test 1: Synchronous GET_REPORT using CTRL_DATA_BUF + { + let setup = SetupPacket { + bm_request_type: 0xA1, + b_request: 0x01, // GET_REPORT + w_value: 0x0100, // Input report, ID 0 + w_index: 0, + w_length: 8, + }; + + unsafe { + let data_buf = &raw mut CTRL_DATA_BUF; + core::ptr::write_bytes((*data_buf).0.as_mut_ptr(), 0xBB, 8); + dma_cache_clean((*data_buf).0.as_ptr(), 8); + dma_cache_invalidate((*data_buf).0.as_ptr(), 8); + + let data_phys = virt_to_phys(&raw const CTRL_DATA_BUF as u64); + + match control_transfer(state, slot_id, &setup, data_phys, 8, true) { + Ok(()) => { + dma_cache_invalidate((*data_buf).0.as_ptr(), 8); + let buf = &(*data_buf).0; + crate::serial_println!( + "[xhci] Sync GET_REPORT(CTRL_DATA): {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x}", + buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7], + ); + } + Err(e) => { + crate::serial_println!("[xhci] Sync GET_REPORT(CTRL_DATA) failed: {}", e); + } + } + } + } + + // Test 2: Synchronous GET_REPORT using KBD_REPORT_BUF + { + let setup = SetupPacket { + bm_request_type: 0xA1, + b_request: 0x01, + w_value: 0x0100, + w_index: 0, + w_length: 8, + }; + + unsafe { + let kbd_buf = &raw mut KBD_REPORT_BUF; + core::ptr::write_bytes((*kbd_buf).0.as_mut_ptr(), 0xCC, 8); + dma_cache_clean((*kbd_buf).0.as_ptr(), 8); + dma_cache_invalidate((*kbd_buf).0.as_ptr(), 8); + + match control_transfer(state, slot_id, &setup, kbd_buf_phys, 8, true) { + Ok(()) => { + dma_cache_invalidate((*kbd_buf).0.as_ptr(), 8); + let buf = &(*kbd_buf).0; + crate::serial_println!( + "[xhci] Sync GET_REPORT(KBD_BUF): {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x}", + buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7], + ); + } + Err(e) => { + crate::serial_println!("[xhci] Sync GET_REPORT(KBD_BUF) failed: {}", e); + } + } + } + } + + // Test 3: Synchronous GET_PROTOCOL (should return 1 byte: 0=boot, 1=report) + { + let setup = SetupPacket { + bm_request_type: 0xA1, + b_request: 0x03, // GET_PROTOCOL + w_value: 0, + w_index: 0, + w_length: 1, + }; + + unsafe { + let data_buf = &raw mut CTRL_DATA_BUF; + core::ptr::write_bytes((*data_buf).0.as_mut_ptr(), 0xDD, 8); + dma_cache_clean((*data_buf).0.as_ptr(), 8); + + let data_phys = virt_to_phys(&raw const CTRL_DATA_BUF as u64); + + match control_transfer(state, slot_id, &setup, data_phys, 1, true) { + Ok(()) => { + dma_cache_invalidate((*data_buf).0.as_ptr(), 8); + let buf = &(*data_buf).0; + crate::serial_println!( + "[xhci] Sync GET_PROTOCOL: {:02x} ({})", + buf[0], + if buf[0] == 0 { "boot" } else if buf[0] == 1 { "report" } else { "?" }, + ); + } + Err(e) => { + crate::serial_println!("[xhci] Sync GET_PROTOCOL failed: {}", e); + } + } + } + } + + // Test 4: GET_DESCRIPTOR(Device) via sync to verify control transfers still work + { + let setup = SetupPacket { + bm_request_type: 0x80, + b_request: 0x06, // GET_DESCRIPTOR + w_value: 0x0100, // Device descriptor + w_index: 0, + w_length: 8, + }; + + unsafe { + let data_buf = &raw mut CTRL_DATA_BUF; + core::ptr::write_bytes((*data_buf).0.as_mut_ptr(), 0xEE, 8); + dma_cache_clean((*data_buf).0.as_ptr(), 8); + + let data_phys = virt_to_phys(&raw const CTRL_DATA_BUF as u64); + + match control_transfer(state, slot_id, &setup, data_phys, 8, true) { + Ok(()) => { + dma_cache_invalidate((*data_buf).0.as_ptr(), 8); + let buf = &(*data_buf).0; + crate::serial_println!( + "[xhci] Sync GET_DESC(Device): {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x}", + buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7], + ); + } + Err(e) => { + crate::serial_println!("[xhci] Sync GET_DESC(Device) failed: {}", e); + } + } + } + } +} + +/// Queue initial HID transfers for all configured HID devices. +/// +/// Must be called AFTER scan_ports completes, so that transfer events +/// don't interfere with wait_for_event during port enumeration commands. +fn start_hid_polling(state: &XhciState) { + // Drain any leftover events from enumeration + drain_stale_events(state); + + if state.kbd_slot != 0 { + crate::serial_println!( + "[xhci] Starting keyboard polling: slot={} DCI={}", + state.kbd_slot, + state.kbd_endpoint, + ); + if let Err(e) = queue_hid_transfer(state, 0, state.kbd_slot, state.kbd_endpoint) { + crate::serial_println!("[xhci] Failed to queue keyboard transfer: {}", e); + } + } + if state.mouse_slot != 0 { + crate::serial_println!( + "[xhci] Starting mouse polling: slot={} DCI={}", + state.mouse_slot, + state.mouse_endpoint, + ); + if let Err(e) = queue_hid_transfer(state, 1, state.mouse_slot, state.mouse_endpoint) { + crate::serial_println!("[xhci] Failed to queue mouse transfer: {}", e); + } + } +} + +// ============================================================================= +// Port Scanning and Device Enumeration +// ============================================================================= + +/// Scan all root hub ports for connected devices, enumerate, and configure HID devices. +fn scan_ports(state: &mut XhciState) -> Result<(), &'static str> { + crate::serial_println!( + "[xhci] Scanning {} ports...", + state.max_ports, + ); + + // Dump PORTSC of all ports (especially USB 2.0 ports 12-13) + for port in 0..state.max_ports as u64 { + let portsc_addr = state.op_base + 0x400 + port * 0x10; + let portsc = read32(portsc_addr); + let speed = (portsc >> 10) & 0xF; + let ccs = portsc & 1; + let ped = (portsc >> 1) & 1; + if ccs != 0 || port >= 12 { + crate::serial_println!( + "[xhci] Port {} PORTSC={:#010x} CCS={} PED={} speed={}", + port, portsc, ccs, ped, speed, + ); + } + } + + let mut slots_used: u8 = 0; + let max_enumerate: u8 = 4; // Only enumerate first few connected devices + + for port in 0..state.max_ports as u64 { + // Stop early if we've found both keyboard and mouse + if state.kbd_slot != 0 && state.mouse_slot != 0 { + break; + } + // Limit total devices to avoid issues with unsupported devices + if slots_used >= max_enumerate { + break; + } + + let portsc_addr = state.op_base + 0x400 + port * 0x10; + let portsc = read32(portsc_addr); + + // Check CCS (Current Connect Status, bit 0) + if portsc & 1 == 0 { + continue; + } + + let port_speed = (portsc >> 10) & 0xF; + crate::serial_println!( + "[xhci] Port {}: connected (PORTSC={:#010x}, speed={})", + port, + portsc, + match port_speed { + 1 => "Full", + 2 => "Low", + 3 => "High", + 4 => "Super", + _ => "Unknown", + }, + ); + + // Check if port is enabled (PED, bit 1) + if portsc & (1 << 1) == 0 { + // Port not enabled - perform a port reset + crate::serial_println!("[xhci] Port {}: resetting...", port); + + // Write PR (Port Reset, bit 4). + // Note: PORTSC is a mix of RW, RW1C, and RO bits. We must preserve + // RW bits and NOT accidentally clear RW1C bits by writing 1 to them. + // RW1C bits in PORTSC: CSC(17), PEC(18), WRC(19), OCC(20), PRC(21), PLC(22), CEC(23) + let preserve_mask: u32 = !( + (1 << 17) | (1 << 18) | (1 << 19) | (1 << 20) | (1 << 21) | (1 << 22) | (1 << 23) + ); + write32(portsc_addr, (portsc & preserve_mask) | (1 << 4)); + + // Wait for PRC (Port Reset Change, bit 21) + if wait_for( + || read32(portsc_addr) & (1 << 21) != 0, + 500_000, + ) + .is_err() + { + crate::serial_println!("[xhci] Port {}: reset timeout", port); + continue; + } + + // Clear PRC (W1C) and check that port is now enabled + let portsc_after = read32(portsc_addr); + write32(portsc_addr, (portsc_after & preserve_mask) | (1 << 21)); + + let portsc_final = read32(portsc_addr); + if portsc_final & (1 << 1) == 0 { + crate::serial_println!("[xhci] Port {}: still not enabled after reset", port); + continue; + } + + crate::serial_println!( + "[xhci] Port {}: enabled after reset (PORTSC={:#010x})", + port, + portsc_final, + ); + } + + // Enable Slot for this device + let slot_id = match enable_slot(state) { + Ok(id) => id, + Err(e) => { + crate::serial_println!("[xhci] Port {}: enable_slot failed: {}", port, e); + continue; + } + }; + if slot_id == 0 { + crate::serial_println!("[xhci] Port {}: got slot_id 0, skipping", port); + continue; + } + + slots_used += 1; + + // Address Device (port numbers are 1-based) + if let Err(e) = address_device(state, slot_id, port as u8 + 1) { + crate::serial_println!("[xhci] Port {}: address_device failed: {}", port, e); + continue; + } + + // Get Device Descriptor + let mut desc_buf = [0u8; 18]; + if let Err(e) = get_device_descriptor(state, slot_id, &mut desc_buf) { + crate::serial_println!("[xhci] Port {}: get_device_descriptor failed: {}", port, e); + continue; + } + + // Get Configuration Descriptor + let mut config_buf = [0u8; 256]; + let config_len = match get_config_descriptor(state, slot_id, &mut config_buf) { + Ok(len) => len, + Err(e) => { + crate::serial_println!("[xhci] Port {}: get_config_descriptor failed: {}", port, e); + continue; + } + }; + + // Configure HID devices + if let Err(e) = configure_hid(state, slot_id, &config_buf, config_len) { + crate::serial_println!("[xhci] Port {}: configure_hid failed: {}", port, e); + } + } + + Ok(()) +} + +// ============================================================================= +// Initialization +// ============================================================================= + +/// Set up PCI MSI for the XHCI controller through GICv2m. +/// +/// Walks the PCI capability list to find the MSI capability, probes for +/// GICv2m at the known Parallels address, allocates an SPI, programs the +/// MSI registers, configures the GIC, and enables the interrupt. +/// +/// Returns the GIC INTID (SPI number) for the allocated interrupt. +/// Falls back to polling (returns 0) if MSI or GICv2m is unavailable. +fn setup_xhci_msi(pci_dev: &crate::drivers::pci::Device) -> u32 { + use crate::arch_impl::aarch64::gic; + + // Step 1: Find MSI capability in PCI config space + let msi_cap = match pci_dev.find_msi_capability() { + Some(offset) => { + crate::serial_println!("[xhci] Found MSI capability at PCI config offset {:#x}", offset); + offset + } + None => { + crate::serial_println!("[xhci] No MSI capability found, using polling mode"); + return 0; + } + }; + + // Step 2: Probe for GICv2m + // On Parallels ARM64, GICv2m is at 0x02250000 (discovered from MADT). + const PARALLELS_GICV2M_BASE: u64 = 0x0225_0000; + let gicv2m_base = crate::platform_config::gicv2m_base_phys(); + let (base, spi_base, spi_count) = if gicv2m_base != 0 { + // Already probed + ( + gicv2m_base, + crate::platform_config::gicv2m_spi_base(), + crate::platform_config::gicv2m_spi_count(), + ) + } else if crate::platform_config::probe_gicv2m(PARALLELS_GICV2M_BASE) { + ( + PARALLELS_GICV2M_BASE, + crate::platform_config::gicv2m_spi_base(), + crate::platform_config::gicv2m_spi_count(), + ) + } else { + crate::serial_println!("[xhci] GICv2m not found at {:#x}, using polling mode", PARALLELS_GICV2M_BASE); + return 0; + }; + + crate::serial_println!( + "[xhci] GICv2m at {:#x}: SPI base={}, count={}", + base, spi_base, spi_count, + ); + + if spi_count == 0 { + crate::serial_println!("[xhci] GICv2m has no available SPIs"); + return 0; + } + + // Step 3: Allocate first available SPI for XHCI + let spi = spi_base; + let intid = spi; // GIC INTID = SPI number for GICv2m + + // Step 4: Program PCI MSI registers + // MSI address = GICv2m doorbell (MSI_SETSPI_NS at offset 0x40) + let msi_address = (base + 0x40) as u32; + let msi_data = spi as u16; + pci_dev.configure_msi(msi_cap, msi_address, msi_data); + pci_dev.disable_intx(); + + crate::serial_println!( + "[xhci] MSI configured: address={:#010x} data={:#06x} (SPI {}, INTID {})", + msi_address, msi_data, spi, intid, + ); + + // Step 5: Configure GIC for this SPI (edge-triggered). + // + // The SPI is NOT enabled here — init() enables it after disabling IMAN.IE + // to prevent an interrupt storm. With IMAN.IE=0, the XHCI won't write MSI + // doorbell writes, so the SPI won't fire even though it's enabled. + gic::configure_spi_edge_triggered(intid); + + crate::serial_println!("[xhci] GIC SPI {} configured (edge-triggered, INTID {})", spi, intid); + + intid +} + +/// Initialize the XHCI controller from a discovered PCI device. +/// +/// Performs the full xHCI initialization sequence: +/// 1. Enable PCI bus mastering and memory space +/// 2. Map BAR0 via HHDM +/// 3. Read capability registers +/// 4. Stop and reset the controller +/// 5. Configure DCBAA, command ring, event ring +/// 6. Start the controller +/// 7. Scan ports and enumerate connected USB devices +pub fn init(pci_dev: &crate::drivers::pci::Device) -> Result<(), &'static str> { + crate::serial_println!("[xhci] Initializing XHCI controller..."); + crate::serial_println!( + "[xhci] PCI device: {:02x}:{:02x}.{} [{:04x}:{:04x}] IRQ={}", + pci_dev.bus, + pci_dev.device, + pci_dev.function, + pci_dev.vendor_id, + pci_dev.device_id, + pci_dev.interrupt_line, + ); + + // 1. Enable bus mastering + memory space + pci_dev.enable_bus_master(); + pci_dev.enable_memory_space(); + + // 2. Map BAR0 via HHDM + let bar = pci_dev.get_mmio_bar().ok_or("XHCI: no MMIO BAR found")?; + crate::serial_println!( + "[xhci] BAR0: phys={:#010x} size={:#x}", + bar.address, + bar.size, + ); + let base = HHDM_BASE + bar.address; + + // 3. Read capability registers + let cap_word = read32(base); + let cap_length = (cap_word & 0xFF) as u8; + let hci_version = (cap_word >> 16) & 0xFFFF; + + let hcsparams1 = read32(base + 0x04); + let hcsparams2 = read32(base + 0x08); + let hccparams1 = read32(base + 0x10); + let db_offset = read32(base + 0x14) & !0x3u32; + let rts_offset = read32(base + 0x18) & !0x1Fu32; + + let max_slots = (hcsparams1 & 0xFF) as u8; + let max_intrs = ((hcsparams1 >> 8) & 0x7FF) as u16; + let max_ports = ((hcsparams1 >> 24) & 0xFF) as u8; + let context_size = if hccparams1 & (1 << 2) != 0 { 64 } else { 32 }; + + // Check for scratchpad buffers + let scratch_hi = (hcsparams2 >> 21) & 0x1F; + let scratch_lo = (hcsparams2 >> 27) & 0x1F; + let num_scratch = (scratch_hi << 5) | scratch_lo; + + let op_base = base + cap_length as u64; + let rt_base = base + rts_offset as u64; + let db_base = base + db_offset as u64; + + crate::serial_println!( + "[xhci] Capabilities: version={:#06x} caplength={} max_slots={} max_ports={} max_intrs={} ctx_size={} scratch={}", + hci_version, + cap_length, + max_slots, + max_ports, + max_intrs, + context_size, + num_scratch, + ); + crate::serial_println!( + "[xhci] Offsets: op={:#x} rt={:#x} db={:#x}", + cap_length, + rts_offset, + db_offset, + ); + + // 3b. Walk Extended Capabilities list for Supported Protocol info. + // HCCPARAMS1 bits 31:16 = xECP (xHCI Extended Capabilities Pointer) in DWORDs from base. + let xecp_offset = ((hccparams1 >> 16) & 0xFFFF) as u64; + if xecp_offset != 0 { + let mut ecap_addr = base + xecp_offset * 4; + for _ in 0..16 { + let ecap_dw0 = read32(ecap_addr); + let cap_id = ecap_dw0 & 0xFF; + let next_ptr = (ecap_dw0 >> 8) & 0xFF; + + if cap_id == 2 { + // Supported Protocol Capability (ID=2) + // DW0: cap_id(7:0), next(15:8), minor_rev(23:16), major_rev(31:24) + let minor_rev = (ecap_dw0 >> 16) & 0xFF; + let major_rev = (ecap_dw0 >> 24) & 0xFF; + // DW1: Name String (ASCII, e.g., "USB ") + let name = read32(ecap_addr + 4); + // DW2: compatible_port_offset(7:0), compatible_port_count(15:8), + // protocol_defined(27:16), protocol_speed_id_count(31:28) + let dw2 = read32(ecap_addr + 8); + let port_offset = dw2 & 0xFF; + let port_count = (dw2 >> 8) & 0xFF; + // DW3: protocol slot type (3:0) + let _dw3 = read32(ecap_addr + 12); + + let name_bytes = name.to_le_bytes(); + crate::serial_println!( + "[xhci] Supported Protocol: USB {}.{} name='{}{}{}{}' ports={}-{} (offset={} count={})", + major_rev, minor_rev, + name_bytes[0] as char, name_bytes[1] as char, + name_bytes[2] as char, name_bytes[3] as char, + port_offset, port_offset + port_count - 1, + port_offset, port_count, + ); + } else if cap_id != 0 { + crate::serial_println!("[xhci] ExtCap ID={} at offset {:#x}", cap_id, ecap_addr - base); + } + + if next_ptr == 0 { + break; + } + ecap_addr += next_ptr as u64 * 4; + } + } else { + crate::serial_println!("[xhci] No Extended Capabilities list"); + } + + // 4. Stop controller: clear USBCMD.RS, wait for USBSTS.HCH + let usbcmd = read32(op_base); + if usbcmd & 1 != 0 { + // Controller is running, stop it + write32(op_base, usbcmd & !1); + wait_for(|| read32(op_base + 0x04) & 1 != 0, 100_000) + .map_err(|_| "XHCI: timeout waiting for HCH")?; + crate::serial_println!("[xhci] Controller stopped"); + } + + // 5. Reset: set USBCMD.HCRST, wait for clear + write32(op_base, read32(op_base) | 2); + wait_for(|| read32(op_base) & 2 == 0, 100_000) + .map_err(|_| "XHCI: timeout waiting for HCRST clear")?; + // Wait for CNR (Controller Not Ready, bit 11 of USBSTS) to clear + wait_for(|| read32(op_base + 0x04) & (1 << 11) == 0, 100_000) + .map_err(|_| "XHCI: timeout waiting for CNR clear")?; + crate::serial_println!("[xhci] Controller reset complete"); + + // 6. Set MaxSlotsEn + let slots_en = max_slots.min(MAX_SLOTS as u8); + write32(op_base + 0x38, slots_en as u32); // CONFIG register + crate::serial_println!("[xhci] MaxSlotsEn set to {}", slots_en); + + // 7. Set DCBAAP (Device Context Base Address Array Pointer) + let dcbaa_phys = virt_to_phys((&raw const DCBAA) as u64); + unsafe { + // Zero the DCBAA (256 u64 entries) + let dcbaa = &raw mut DCBAA; + core::ptr::write_bytes((*dcbaa).0.as_mut_ptr(), 0, 256); + dma_cache_clean((*dcbaa).0.as_ptr() as *const u8, 256 * core::mem::size_of::()); + } + write64(op_base + 0x30, dcbaa_phys); + crate::serial_println!("[xhci] DCBAAP set to phys={:#010x}", dcbaa_phys); + + // 8. Set Command Ring Control Register (CRCR) + let cmd_ring_phys = virt_to_phys((&raw const CMD_RING) as u64); + unsafe { + // Zero the command ring (CMD_RING_SIZE Trb entries) + let ring = &raw mut CMD_RING; + core::ptr::write_bytes((*ring).0.as_mut_ptr(), 0, CMD_RING_SIZE); + CMD_RING_ENQUEUE = 0; + CMD_RING_CYCLE = true; + dma_cache_clean((*ring).0.as_ptr() as *const u8, CMD_RING_SIZE * core::mem::size_of::()); + } + // CRCR: physical address | RCS (Ring Cycle State) = 1 + write64(op_base + 0x18, cmd_ring_phys | 1); + crate::serial_println!("[xhci] CRCR set to phys={:#010x}", cmd_ring_phys); + + // 9. Set up Event Ring for Interrupter 0 + let event_ring_phys = virt_to_phys((&raw const EVENT_RING) as u64); + let erst_phys = virt_to_phys((&raw const ERST) as u64); + + unsafe { + // Zero the event ring (EVENT_RING_SIZE Trb entries) + let ering = &raw mut EVENT_RING; + core::ptr::write_bytes((*ering).0.as_mut_ptr(), 0, EVENT_RING_SIZE); + EVENT_RING_DEQUEUE = 0; + EVENT_RING_CYCLE = true; + dma_cache_clean((*ering).0.as_ptr() as *const u8, EVENT_RING_SIZE * core::mem::size_of::()); + + // Set up ERST entry + let erst = &raw mut ERST; + (*erst).0[0] = ErstEntry { + base: event_ring_phys, + size: EVENT_RING_SIZE as u32, + _rsvd: 0, + }; + dma_cache_clean((*erst).0.as_ptr() as *const u8, core::mem::size_of::()); + } + + let ir0 = rt_base + 0x20; // Interrupter 0 register set + + // ERSTSZ (Event Ring Segment Table Size) = 1 segment + write32(ir0 + 0x08, 1); + // ERDP (Event Ring Dequeue Pointer) = start of event ring + write64(ir0 + 0x18, event_ring_phys); + // ERSTBA (Event Ring Segment Table Base Address) - must be written AFTER ERSTSZ + write64(ir0 + 0x10, erst_phys); + + crate::serial_println!( + "[xhci] Event ring: phys={:#010x} ERST phys={:#010x}", + event_ring_phys, + erst_phys, + ); + + // 10. Enable interrupts on Interrupter 0 + let iman = read32(ir0); + write32(ir0, iman | 2); // IMAN.IE = 1 + + // 11. Start controller: USBCMD.RS=1, INTE=1 + let usbcmd = read32(op_base); + write32(op_base, usbcmd | 1 | (1 << 2)); // RS=1, INTE=1 + crate::serial_println!("[xhci] Controller started (USBCMD={:#010x})", read32(op_base)); + + // Wait a bit for ports to detect connections + for _ in 0..100_000 { + core::hint::spin_loop(); + } + + // Verify controller is running + let usbsts = read32(op_base + 0x04); + if usbsts & 1 != 0 { + crate::serial_println!("[xhci] WARNING: Controller halted after start (USBSTS={:#010x})", usbsts); + } + + // 12. Set up PCI MSI BEFORE device enumeration. + // + // The Parallels virtual XHCI controller requires MSI to be configured at + // the PCI level before interrupt endpoints work. Without MSI enabled, + // ConfigureEndpoint succeeds but transfers return CC=12 (Endpoint Not + // Enabled). By configuring MSI before scan_ports, the controller knows + // interrupt delivery is available when we ConfigureEndpoint for HID devices. + // + // Note: The GIC SPI is NOT enabled yet — only PCI MSI registers are + // programmed. This means MSI writes will set the GIC pending bit but + // won't deliver to the CPU. wait_for_event() polls the event ring directly + // and doesn't need GIC delivery. + let irq = setup_xhci_msi(pci_dev); + + // 13. Create state with IRQ already set + let mut xhci_state = XhciState { + base, + cap_length, + op_base, + rt_base, + db_base, + max_slots: slots_en, + max_ports, + context_size, + irq, + kbd_slot: 0, + kbd_endpoint: 0, + mouse_slot: 0, + mouse_endpoint: 0, + }; + + // 14. Scan ports and configure HID devices. + // + // This uses command ring + event ring polling (wait_for_event). MSI is + // active at the PCI level (controller generates MSI writes) but the GIC + // SPI is disabled, so we won't receive interrupts. The controller writes + // events to the event ring regardless of interrupt delivery, so polling + // still works. + if let Err(e) = scan_ports(&mut xhci_state) { + crate::serial_println!("[xhci] Port scanning error: {}", e); + } + + crate::serial_println!( + "[xhci] Scan complete: kbd_slot={} kbd_ep={} mouse_slot={} mouse_ep={}", + xhci_state.kbd_slot, + xhci_state.kbd_endpoint, + xhci_state.mouse_slot, + xhci_state.mouse_endpoint, + ); + + // 15. Store state and set INITIALIZED before enabling GIC SPI. + // + // Once the SPI is enabled, pending MSI writes will immediately fire + // the interrupt handler. The handler needs XHCI_INITIALIZED=true and + // XHCI_STATE=Some to process events correctly. + unsafe { + *(&raw mut XHCI_STATE) = Some(xhci_state); + } + XHCI_INITIALIZED.store(true, Ordering::Release); + + // 16. Do NOT enable GIC SPI for now. + // + // The Parallels virtual XHCI generates back-to-back MSIs that cause + // interrupt storms, freezing the system. Instead, rely on timer-driven + // polling via poll_hid_events() at ~200Hz. The MSI is configured at + // the PCI level (so the controller knows interrupts are available for + // ConfigureEndpoint), but the GIC SPI is kept disabled to prevent storms. + crate::serial_println!("[xhci] GIC SPI {} configured but NOT enabled (polling mode)", irq); + + // 17. Test class requests synchronously before starting async polling. + let xhci_state_ref = unsafe { + (*(&raw const XHCI_STATE)).as_ref().unwrap() + }; + if xhci_state_ref.kbd_slot != 0 { + test_sync_class_requests(xhci_state_ref, xhci_state_ref.kbd_slot); + } + + // 18. Queue initial HID transfers. + // + // With USE_BULK_FOR_INTERRUPT=true, the interrupt endpoint is configured + // as Bulk IN to bypass Parallels' broken periodic scheduler. + start_hid_polling(xhci_state_ref); + + crate::serial_println!("[xhci] Initialization complete (MSI IRQ={})", irq); + + Ok(()) +} + +// ============================================================================= +// Interrupt Handling +// ============================================================================= + +/// Handle an XHCI interrupt. +/// +/// Called from the GIC interrupt handler when the XHCI IRQ fires. +/// Immediately disables the GIC SPI to prevent re-delivery storms, +/// then processes all pending events. The SPI is re-enabled by +/// poll_hid_events() on the next timer tick (~5ms later). +pub fn handle_interrupt() { + if !XHCI_INITIALIZED.load(Ordering::Acquire) { + return; + } + + let state = unsafe { + match (*(&raw const XHCI_STATE)).as_ref() { + Some(s) => s, + None => return, + } + }; + + // FIRST: Disable the GIC SPI to prevent re-delivery after EOI. + // This is critical for Parallels where the virtual XHCI generates + // back-to-back MSI writes that cause interrupt storms. + if state.irq != 0 { + crate::arch_impl::aarch64::gic::disable_spi(state.irq); + } + + // try_lock: IRQ context must never spin on a lock. + let _guard = match XHCI_LOCK.try_lock() { + Some(g) => g, + None => return, // Lock contended, skip — poll_hid_events will handle events + }; + + // Acknowledge IMAN and USBSTS + let ir0 = state.rt_base + 0x20; + let iman = read32(ir0); + if iman & 1 != 0 { + write32(ir0, iman | 1); // W1C to clear IP + } + let usbsts = read32(state.op_base + 0x04); + if usbsts & (1 << 3) != 0 { + write32(state.op_base + 0x04, 1 << 3); + } + + // Process all pending events + loop { + unsafe { + let ring = &raw const EVENT_RING; + let idx = EVENT_RING_DEQUEUE; + let cycle = EVENT_RING_CYCLE; + + // Invalidate cache to see controller-written TRBs + dma_cache_invalidate( + &(*ring).0[idx] as *const Trb as *const u8, + core::mem::size_of::(), + ); + + let trb = core::ptr::read_volatile(&(*ring).0[idx]); + let trb_cycle = trb.control & 1 != 0; + + if trb_cycle != cycle { + break; // No more events + } + + MSI_EVENT_COUNT.fetch_add(1, Ordering::Relaxed); + + let trb_type_val = trb.trb_type(); + match trb_type_val { + trb_type::TRANSFER_EVENT => { + let slot = trb.slot_id(); + let endpoint = ((trb.control >> 16) & 0x1F) as u8; + let cc = trb.completion_code(); + + if cc == completion_code::SUCCESS || cc == completion_code::SHORT_PACKET { + if slot == state.kbd_slot && endpoint == state.kbd_endpoint { + // Keyboard report received — process immediately + KBD_EVENT_COUNT.fetch_add(1, Ordering::Relaxed); + let report_buf = &raw const KBD_REPORT_BUF; + dma_cache_invalidate( + (*report_buf).0.as_ptr(), + 8, + ); + let report = &(*report_buf).0; + super::hid::process_keyboard_report(report); + // DON'T requeue here — let the timer poll requeue. + // Requeuing from IRQ context creates an MSI storm + // (virtual XHCI has no bus latency, so completions + // fire instantly, starving the main thread). + MSI_KBD_NEEDS_REQUEUE.store(true, Ordering::Release); + } else if slot == state.mouse_slot && endpoint == state.mouse_endpoint { + // Mouse report received + let report_buf = &raw const MOUSE_REPORT_BUF; + dma_cache_invalidate( + (*report_buf).0.as_ptr(), + 8, + ); + let report = &(*report_buf).0; + super::hid::process_mouse_report(report); + // Same: let timer poll requeue + MSI_MOUSE_NEEDS_REQUEUE.store(true, Ordering::Release); + } + } + } + trb_type::COMMAND_COMPLETION => { + // Command completions during enumeration are handled by wait_for_event. + // Any stray completions during interrupt handling are ignored. + } + trb_type::PORT_STATUS_CHANGE => { + // Port status change - don't log from IRQ context (deadlock risk + // with serial lock). Hot-plug not supported yet. + } + _ => { + // Unknown event type + } + } + + // Advance dequeue pointer + EVENT_RING_DEQUEUE = (idx + 1) % EVENT_RING_SIZE; + if EVENT_RING_DEQUEUE == 0 { + EVENT_RING_CYCLE = !cycle; + } + + // Update ERDP with EHB bit to acknowledge + let erdp_phys = virt_to_phys(&raw const EVENT_RING as u64) + + (EVENT_RING_DEQUEUE as u64) * 16; + write64(ir0 + 0x18, erdp_phys | (1 << 3)); + } + } + +} + +// ============================================================================= +// Polling Mode (fallback for systems without interrupt support) +// ============================================================================= + +/// Poll for HID events without relying on interrupts. +/// +/// Called from the timer interrupt at ~200 Hz. Uses `try_lock()` to avoid +/// deadlocking if the lock is held by non-interrupt code. Bypasses the +/// IMAN.IP check since that may not be set without a wired interrupt line. +pub fn poll_hid_events() { + if !XHCI_INITIALIZED.load(Ordering::Acquire) { + return; + } + + POLL_COUNT.fetch_add(1, Ordering::Relaxed); + + // try_lock: if someone else holds the lock, skip this poll cycle + let _guard = match XHCI_LOCK.try_lock() { + Some(g) => g, + None => return, + }; + + let state = unsafe { + match (*(&raw const XHCI_STATE)).as_ref() { + Some(s) => s, + None => return, + } + }; + + let ir0 = state.rt_base + 0x20; + + // Clear IMAN.IP and USBSTS.EINT if set (acknowledge any pending state) + let iman = read32(ir0); + if iman & 1 != 0 { + write32(ir0, iman | 1); // W1C to clear IP + } + let usbsts = read32(state.op_base + 0x04); + if usbsts & (1 << 3) != 0 { + write32(state.op_base + 0x04, 1 << 3); // W1C to clear EINT + } + + // Process all pending events on the event ring + loop { + unsafe { + let ring = &raw const EVENT_RING; + let idx = EVENT_RING_DEQUEUE; + let cycle = EVENT_RING_CYCLE; + + dma_cache_invalidate( + &(*ring).0[idx] as *const Trb as *const u8, + core::mem::size_of::(), + ); + + let trb = core::ptr::read_volatile(&(*ring).0[idx]); + let trb_cycle = trb.control & 1 != 0; + + if trb_cycle != cycle { + break; // No more events + } + + let _evt_num = EVENT_COUNT.fetch_add(1, Ordering::Relaxed); + + let trb_type_val = trb.trb_type(); + + // No serial_println here — this runs in timer interrupt context. + // Use atomic counters (reported by heartbeat) instead of logging. + + match trb_type_val { + trb_type::TRANSFER_EVENT => { + let slot = trb.slot_id(); + let endpoint = ((trb.control >> 16) & 0x1F) as u8; + let cc = trb.completion_code(); + + // Check for interrupt endpoint failure → switch to EP0 polling + if slot == state.kbd_slot + && endpoint == state.kbd_endpoint + && cc == completion_code::ENDPOINT_NOT_ENABLED + && !EP0_POLLING_MODE.load(Ordering::Relaxed) + { + // No serial_println — runs in timer interrupt context. + // Heartbeat will report EP0_POLLING_MODE switch. + EP0_POLLING_MODE.store(true, Ordering::Release); + } else if cc == completion_code::SUCCESS || cc == completion_code::SHORT_PACKET { + // EP0 GET_REPORT completion (DCI=1) for keyboard + if EP0_POLLING_MODE.load(Ordering::Relaxed) + && slot == state.kbd_slot + && endpoint == 1 + { + // SHORT_PACKET is for the Data Stage — data is in the buffer + // but we wait for the Status Stage SUCCESS to process it. + // SUCCESS is for the Status Stage — transfer is complete. + if cc == completion_code::SUCCESS { + KBD_EVENT_COUNT.fetch_add(1, Ordering::Relaxed); + EP0_STATE.store(ep0_state::IDLE, Ordering::Release); + EP0_STALL_POLLS.store(0, Ordering::Relaxed); + + // Read the keyboard report from KBD_REPORT_BUF + let report_buf = &raw const KBD_REPORT_BUF; + dma_cache_invalidate((*report_buf).0.as_ptr(), 8); + let report = &(*report_buf).0; + + // Sentinel check: 0xDE means DMA didn't write + if report[0] == 0xDE && report[1] == 0xDE { + DMA_SENTINEL_SURVIVED.fetch_add(1, Ordering::SeqCst); + } else { + DMA_SENTINEL_REPLACED.fetch_add(1, Ordering::SeqCst); + } + + super::hid::process_keyboard_report(report); + } + // SHORT_PACKET for Data Stage: just acknowledge, data stage done + // Status Stage event will follow. + } + // Interrupt endpoint keyboard event (original path) + else if !EP0_POLLING_MODE.load(Ordering::Relaxed) + && slot == state.kbd_slot + && endpoint == state.kbd_endpoint + { + KBD_EVENT_COUNT.fetch_add(1, Ordering::Relaxed); + // Unconditional increment — MUST match uk if this path runs + DMA_SENTINEL_REPLACED.fetch_add(1, Ordering::SeqCst); + + let report_buf = &raw const KBD_REPORT_BUF; + dma_cache_invalidate((*report_buf).0.as_ptr(), 8); + let report = &(*report_buf).0; + + super::hid::process_keyboard_report(report); + let _ = queue_hid_transfer(state, 0, state.kbd_slot, state.kbd_endpoint); + } + // Mouse interrupt endpoint event + else if slot == state.mouse_slot && endpoint == state.mouse_endpoint { + let report_buf = &raw const MOUSE_REPORT_BUF; + dma_cache_invalidate((*report_buf).0.as_ptr(), 8); + let report = &(*report_buf).0; + super::hid::process_mouse_report(report); + let _ = queue_hid_transfer(state, 1, state.mouse_slot, state.mouse_endpoint); + } else { + XFER_OTHER_COUNT.fetch_add(1, Ordering::Relaxed); + } + } else { + XFER_OTHER_COUNT.fetch_add(1, Ordering::Relaxed); + } + } + trb_type::COMMAND_COMPLETION => { + // Route CC events to the EP0 reset state machine + let cc = trb.completion_code(); + let current_state = EP0_STATE.load(Ordering::Relaxed); + match current_state { + ep0_state::WAIT_STOP_EP => { + handle_stop_ep_complete(state, state.kbd_slot, cc); + EP0_STALL_POLLS.store(0, Ordering::Relaxed); + } + ep0_state::WAIT_SET_DEQUEUE => { + handle_set_dequeue_complete(cc); + EP0_STALL_POLLS.store(0, Ordering::Relaxed); + } + _ => { + // Unexpected CC (e.g., from enumeration leftover) — ignore + } + } + } + trb_type::PORT_STATUS_CHANGE => { + PSC_COUNT.fetch_add(1, Ordering::Relaxed); + } + _ => {} + } + + // Advance dequeue pointer + EVENT_RING_DEQUEUE = (idx + 1) % EVENT_RING_SIZE; + if EVENT_RING_DEQUEUE == 0 { + EVENT_RING_CYCLE = !cycle; + } + + // Update ERDP with EHB bit + let erdp_phys = virt_to_phys(&raw const EVENT_RING as u64) + + (EVENT_RING_DEQUEUE as u64) * 16; + write64(ir0 + 0x18, erdp_phys | (1 << 3)); + } + } + + // Requeue HID transfers requested by the MSI interrupt handler. + // The IRQ handler can't requeue directly (MSI storm on virtual XHCI). + if !EP0_POLLING_MODE.load(Ordering::Relaxed) { + if MSI_KBD_NEEDS_REQUEUE.swap(false, Ordering::AcqRel) && state.kbd_slot != 0 { + let _ = queue_hid_transfer(state, 0, state.kbd_slot, state.kbd_endpoint); + } + if MSI_MOUSE_NEEDS_REQUEUE.swap(false, Ordering::AcqRel) && state.mouse_slot != 0 { + let _ = queue_hid_transfer(state, 1, state.mouse_slot, state.mouse_endpoint); + } + } + + // EP0 GET_REPORT polling async state machine (non-blocking). + // + // States: + // IDLE → check ring space → submit GET_REPORT → XFER_PENDING + // IDLE → ring full → issue Stop EP → WAIT_STOP_EP + // XFER_PENDING → (wait for SUCCESS Transfer Event in event loop above) + // WAIT_STOP_EP → (CC handled in event loop) → WAIT_SET_DEQUEUE + // WAIT_SET_DEQUEUE → (CC handled in event loop) → IDLE + if EP0_POLLING_MODE.load(Ordering::Relaxed) && state.kbd_slot != 0 { + let current_state = EP0_STATE.load(Ordering::Relaxed); + + match current_state { + ep0_state::IDLE => { + EP0_STALL_POLLS.store(0, Ordering::Relaxed); + + // Rate-limit: only submit every 2 poll cycles (~100 Hz at 200 Hz timer) + let skip = EP0_POLL_SKIP.fetch_add(1, Ordering::Relaxed); + if skip % 2 == 0 { + let slot_idx = (state.kbd_slot - 1) as usize; + let enq = unsafe { TRANSFER_ENQUEUE[slot_idx] }; + + if enq + 4 >= TRANSFER_RING_SIZE { + // Ring nearly full — start async reset (non-blocking) + issue_stop_ep0(state, state.kbd_slot); + } else { + // Ring has room — submit GET_REPORT + submit_ep0_get_report(state, state.kbd_slot); + EP0_STATE.store(ep0_state::XFER_PENDING, Ordering::Release); + } + } + } + ep0_state::XFER_PENDING | ep0_state::WAIT_STOP_EP | ep0_state::WAIT_SET_DEQUEUE => { + // Waiting for an event — check for stuck condition. + // If stuck for >400 polls (~2 seconds at 200Hz), force back to IDLE. + let polls = EP0_STALL_POLLS.fetch_add(1, Ordering::Relaxed) + 1; + if polls >= 400 { + EP0_STATE.store(ep0_state::IDLE, Ordering::Release); + EP0_STALL_POLLS.store(0, Ordering::Relaxed); + EP0_PENDING_STUCK_COUNT.fetch_add(1, Ordering::Relaxed); + } + } + _ => { + // Unknown state — reset to IDLE + EP0_STATE.store(ep0_state::IDLE, Ordering::Release); + } + } + } + + // GIC SPI is intentionally NOT enabled — polling mode only. + // The MSI is configured at PCI level (for ConfigureEndpoint) but + // the GIC doesn't deliver the MSIs to the CPU. +} + +// ============================================================================= +// Public API +// ============================================================================= + +/// Check if the XHCI controller has been initialized. +pub fn is_initialized() -> bool { + XHCI_INITIALIZED.load(Ordering::Acquire) +} + +/// Get the GIC INTID for the XHCI controller's interrupt. +pub fn get_irq() -> Option { + if !XHCI_INITIALIZED.load(Ordering::Acquire) { + return None; + } + unsafe { (*(&raw const XHCI_STATE)).as_ref().map(|s| s.irq) } +} diff --git a/kernel/src/drivers/virtio/block_mmio.rs b/kernel/src/drivers/virtio/block_mmio.rs index 3631b0ee..35081aee 100644 --- a/kernel/src/drivers/virtio/block_mmio.rs +++ b/kernel/src/drivers/virtio/block_mmio.rs @@ -450,9 +450,7 @@ fn read_sector_inner(device_index: usize, sector: u64, buffer: &mut [u8; SECTOR_ // Raw serial character for debugging (no locks) #[inline(always)] fn raw_char(c: u8) { - const HHDM_BASE: u64 = 0xFFFF_0000_0000_0000; - const PL011_BASE: u64 = 0x0900_0000; - let addr = (HHDM_BASE + PL011_BASE) as *mut u32; + let addr = crate::platform_config::uart_virt() as *mut u32; unsafe { core::ptr::write_volatile(addr, c as u32); } } diff --git a/kernel/src/drivers/virtio/gpu_pci.rs b/kernel/src/drivers/virtio/gpu_pci.rs new file mode 100644 index 00000000..2adddda3 --- /dev/null +++ b/kernel/src/drivers/virtio/gpu_pci.rs @@ -0,0 +1,840 @@ +//! VirtIO GPU Device Driver for ARM64 (PCI Transport) +//! +//! Implements a basic GPU/display driver using VirtIO PCI modern transport. +//! Provides framebuffer functionality for simple 2D graphics. +//! +//! This driver reuses the same VirtIO GPU 2D protocol as `gpu_mmio.rs` but +//! communicates via the PCI transport layer (`VirtioPciDevice` from +//! `pci_transport.rs`) instead of MMIO registers. + +use super::pci_transport::VirtioPciDevice; +use core::ptr::read_volatile; +use core::sync::atomic::{fence, AtomicBool, Ordering}; +use spin::Mutex; + +/// Lock protecting the GPU PCI command path (PCI_CMD_BUF, PCI_RESP_BUF, +/// PCI_CTRL_QUEUE, GPU_PCI_STATE). +/// Without this, concurrent callers corrupt the shared command/response +/// buffers and virtqueue state. +static GPU_PCI_LOCK: Mutex<()> = Mutex::new(()); + +// ============================================================================= +// VirtIO GPU Protocol (same as gpu_mmio.rs) +// ============================================================================= + +/// VirtIO GPU command types +#[allow(dead_code)] +mod cmd { + // 2D commands + pub const GET_DISPLAY_INFO: u32 = 0x0100; + pub const RESOURCE_CREATE_2D: u32 = 0x0101; + pub const RESOURCE_UNREF: u32 = 0x0102; + pub const SET_SCANOUT: u32 = 0x0103; + pub const RESOURCE_FLUSH: u32 = 0x0104; + pub const TRANSFER_TO_HOST_2D: u32 = 0x0105; + pub const RESOURCE_ATTACH_BACKING: u32 = 0x0106; + pub const RESOURCE_DETACH_BACKING: u32 = 0x0107; + + // Response types + pub const RESP_OK_NODATA: u32 = 0x1100; + pub const RESP_OK_DISPLAY_INFO: u32 = 0x1101; + pub const RESP_ERR_UNSPEC: u32 = 0x1200; +} + +/// VirtIO GPU formats +#[allow(dead_code)] +mod format { + pub const B8G8R8A8_UNORM: u32 = 1; + pub const B8G8R8X8_UNORM: u32 = 2; + pub const A8R8G8B8_UNORM: u32 = 3; + pub const X8R8G8B8_UNORM: u32 = 4; + pub const R8G8B8A8_UNORM: u32 = 67; + pub const X8B8G8R8_UNORM: u32 = 68; + pub const A8B8G8R8_UNORM: u32 = 121; + pub const R8G8B8X8_UNORM: u32 = 134; +} + +// ============================================================================= +// GPU Protocol Structures +// ============================================================================= + +/// VirtIO GPU control header +#[repr(C)] +#[derive(Clone, Copy, Default)] +struct VirtioGpuCtrlHdr { + type_: u32, + flags: u32, + fence_id: u64, + ctx_id: u32, + padding: u32, +} + +/// Display info for one scanout +#[repr(C)] +#[derive(Clone, Copy, Default)] +struct VirtioGpuDisplayOne { + r_x: u32, + r_y: u32, + r_width: u32, + r_height: u32, + enabled: u32, + flags: u32, +} + +/// Get display info response +#[repr(C)] +#[derive(Clone, Copy)] +struct VirtioGpuRespDisplayInfo { + hdr: VirtioGpuCtrlHdr, + pmodes: [VirtioGpuDisplayOne; 16], // VIRTIO_GPU_MAX_SCANOUTS = 16 +} + +/// Resource create 2D command +#[repr(C)] +#[derive(Clone, Copy, Default)] +struct VirtioGpuResourceCreate2d { + hdr: VirtioGpuCtrlHdr, + resource_id: u32, + format: u32, + width: u32, + height: u32, +} + +/// Set scanout command +#[repr(C)] +#[derive(Clone, Copy, Default)] +struct VirtioGpuSetScanout { + hdr: VirtioGpuCtrlHdr, + r_x: u32, + r_y: u32, + r_width: u32, + r_height: u32, + scanout_id: u32, + resource_id: u32, +} + +/// Memory entry for resource attach backing +#[repr(C)] +#[derive(Clone, Copy, Default)] +struct VirtioGpuMemEntry { + addr: u64, + length: u32, + padding: u32, +} + +/// Resource attach backing command +#[repr(C)] +#[derive(Clone, Copy, Default)] +struct VirtioGpuResourceAttachBacking { + hdr: VirtioGpuCtrlHdr, + resource_id: u32, + nr_entries: u32, +} + +/// Transfer to host 2D command +#[repr(C)] +#[derive(Clone, Copy, Default)] +struct VirtioGpuTransferToHost2d { + hdr: VirtioGpuCtrlHdr, + r_x: u32, + r_y: u32, + r_width: u32, + r_height: u32, + offset: u64, + resource_id: u32, + padding: u32, +} + +/// Resource flush command +#[repr(C)] +#[derive(Clone, Copy, Default)] +struct VirtioGpuResourceFlush { + hdr: VirtioGpuCtrlHdr, + r_x: u32, + r_y: u32, + r_width: u32, + r_height: u32, + resource_id: u32, + padding: u32, +} + +// ============================================================================= +// Virtqueue Structures +// ============================================================================= + +/// Virtqueue descriptor +#[repr(C)] +#[derive(Clone, Copy, Default)] +struct VirtqDesc { + addr: u64, + len: u32, + flags: u16, + next: u16, +} + +const DESC_F_NEXT: u16 = 1; +const DESC_F_WRITE: u16 = 2; + +/// Available ring +#[repr(C)] +struct VirtqAvail { + flags: u16, + idx: u16, + ring: [u16; 16], +} + +/// Used ring element +#[repr(C)] +#[derive(Clone, Copy, Default)] +struct VirtqUsedElem { + id: u32, + len: u32, +} + +/// Used ring +#[repr(C)] +struct VirtqUsed { + flags: u16, + idx: u16, + ring: [VirtqUsedElem; 16], +} + +/// Static control queue memory (page-aligned for used ring) +#[repr(C, align(4096))] +struct PciCtrlQueueMemory { + desc: [VirtqDesc; 16], + avail: VirtqAvail, + _padding: [u8; 4096 - 256 - 36], + used: VirtqUsed, +} + +// ============================================================================= +// Static Buffers (prefixed with PCI_ to avoid conflicts with gpu_mmio.rs) +// ============================================================================= + +static mut PCI_CTRL_QUEUE: PciCtrlQueueMemory = PciCtrlQueueMemory { + desc: [VirtqDesc { addr: 0, len: 0, flags: 0, next: 0 }; 16], + avail: VirtqAvail { flags: 0, idx: 0, ring: [0; 16] }, + _padding: [0; 4096 - 256 - 36], + used: VirtqUsed { + flags: 0, + idx: 0, + ring: [VirtqUsedElem { id: 0, len: 0 }; 16], + }, +}; + +/// Command/response buffers +#[repr(C, align(64))] +struct PciCmdBuffer { + data: [u8; 512], +} + +static mut PCI_CMD_BUF: PciCmdBuffer = PciCmdBuffer { data: [0; 512] }; +static mut PCI_RESP_BUF: PciCmdBuffer = PciCmdBuffer { data: [0; 512] }; + +// Default framebuffer dimensions (Parallels: set_scanout configures display mode) +// 2560x1600 is the max that fits in the ~16MB GOP BAR0 region on Parallels. +// On a Retina Mac, Parallels 2x-scales this to ~1280x800 window points. +const DEFAULT_FB_WIDTH: u32 = 2560; +const DEFAULT_FB_HEIGHT: u32 = 1600; +// Max supported resolution: 2560x1600 @ 32bpp = ~16.4MB +const FB_MAX_WIDTH: u32 = 2560; +const FB_MAX_HEIGHT: u32 = 1600; +const FB_SIZE: usize = (FB_MAX_WIDTH * FB_MAX_HEIGHT * 4) as usize; +const BYTES_PER_PIXEL: usize = 4; +const RESOURCE_ID: u32 = 1; + +// VirtIO standard feature bits +const VIRTIO_F_VERSION_1: u64 = 1 << 32; +// VirtIO GPU feature bits (requested but not required) +#[allow(dead_code)] +const VIRTIO_GPU_F_EDID: u64 = 1 << 1; + +#[repr(C, align(4096))] +struct PciFramebuffer { + pixels: [u8; FB_SIZE], +} + +static mut PCI_FRAMEBUFFER: PciFramebuffer = PciFramebuffer { pixels: [0; FB_SIZE] }; + +// ============================================================================= +// GPU PCI Device State +// ============================================================================= + +/// Combined GPU PCI device state (transport + GPU state) +struct GpuPciDeviceState { + device: VirtioPciDevice, + width: u32, + height: u32, + resource_id: u32, + last_used_idx: u16, +} + +static mut GPU_PCI_STATE: Option = None; +static GPU_PCI_INITIALIZED: AtomicBool = AtomicBool::new(false); + +// ============================================================================= +// Helpers +// ============================================================================= + +/// Convert a kernel virtual address to a physical address. +/// +/// On QEMU, statics live in the HHDM range (>= 0xFFFF_0000_0000_0000), +/// so phys = virt - HHDM_BASE. +/// On Parallels, the kernel may be identity-mapped via TTBR0, so statics are +/// at their physical addresses already. +#[inline(always)] +fn virt_to_phys(addr: u64) -> u64 { + const HHDM_BASE: u64 = 0xFFFF_0000_0000_0000; + if addr >= HHDM_BASE { + addr - HHDM_BASE + } else { + addr // Already a physical address (identity-mapped kernel on Parallels) + } +} + +/// Check if the GPU PCI driver has been initialized. +pub fn is_initialized() -> bool { + GPU_PCI_INITIALIZED.load(Ordering::Acquire) +} + +// ============================================================================= +// Initialization +// ============================================================================= + +/// Initialize the VirtIO GPU PCI device. +/// +/// Discovers a VirtIO GPU device on the PCI bus, negotiates features, +/// sets up the control virtqueue, and configures the framebuffer. +pub fn init() -> Result<(), &'static str> { + // Check if already initialized + if is_initialized() { + crate::serial_println!("[virtio-gpu-pci] GPU device already initialized"); + return Ok(()); + } + + crate::serial_println!("[virtio-gpu-pci] Searching for GPU PCI device..."); + + // Find VirtIO GPU PCI device (device_id 0x1050 = 0x1040 + 16) + let pci_dev = crate::drivers::pci::find_device(0x1AF4, 0x1050) + .ok_or("No VirtIO GPU PCI device found")?; + + crate::serial_println!( + "[virtio-gpu-pci] Found GPU at PCI {:02x}:{:02x}.{:x}", + pci_dev.bus, + pci_dev.device, + pci_dev.function + ); + + // Probe VirtIO modern transport + let mut virtio = VirtioPciDevice::probe(pci_dev) + .ok_or("VirtIO GPU PCI: no modern capabilities")?; + + // Init (reset, negotiate features). + // VIRTIO_F_VERSION_1 is mandatory for PCI modern transport — without it, + // Parallels's GPU device accepts the feature set but ignores subsequent + // state-modifying commands (create_resource, attach_backing, etc.). + let requested = VIRTIO_F_VERSION_1 | VIRTIO_GPU_F_EDID; + virtio.init(requested)?; + let dev_feats = virtio.device_features(); + let negotiated = dev_feats & requested; + crate::serial_println!( + "[virtio-gpu-pci] Features: device={:#x} requested={:#x} negotiated={:#x}", + dev_feats, requested, negotiated + ); + + // Set up control queue (queue 0) + virtio.select_queue(0); + let queue_max = virtio.get_queue_num_max(); + crate::serial_println!("[virtio-gpu-pci] Control queue max size: {}", queue_max); + + if queue_max == 0 { + return Err("Control queue size is 0"); + } + + let queue_size = core::cmp::min(queue_max, 16); + virtio.set_queue_num(queue_size); + + // Set up queue memory using separate physical addresses (PCI modern transport) + let queue_phys = virt_to_phys(&raw const PCI_CTRL_QUEUE as u64); + + // Initialize descriptor chain + unsafe { + let q = &raw mut PCI_CTRL_QUEUE; + for i in 0..15 { + (*q).desc[i].next = (i + 1) as u16; + } + (*q).desc[15].next = 0; + (*q).avail.flags = 0; + (*q).avail.idx = 0; + (*q).used.flags = 0; + (*q).used.idx = 0; + } + + // Desc table at start, avail ring at +256 (16 descs * 16 bytes), used ring at +4096 + virtio.set_queue_desc(queue_phys); + virtio.set_queue_avail(queue_phys + 256); + virtio.set_queue_used(queue_phys + 4096); + virtio.set_queue_ready(true); + + // Mark device ready + virtio.driver_ok(); + + // Store initial state with default dimensions (will be updated after display query) + unsafe { + let ptr = &raw mut GPU_PCI_STATE; + *ptr = Some(GpuPciDeviceState { + device: virtio, + width: DEFAULT_FB_WIDTH, + height: DEFAULT_FB_HEIGHT, + resource_id: RESOURCE_ID, + last_used_idx: 0, + }); + } + // Don't set GPU_PCI_INITIALIZED yet — the GPU commands below can fail. + // If create_resource/attach_backing/set_scanout/flush time out, leaving + // the flag true would mislead other code into thinking the device is usable. + + // Log physical addresses for diagnostics (DMA correctness) + let fb_virt = &raw const PCI_FRAMEBUFFER as u64; + let fb_phys = virt_to_phys(fb_virt); + let cmd_virt = &raw const PCI_CMD_BUF as u64; + let cmd_phys = virt_to_phys(cmd_virt); + let queue_virt = &raw const PCI_CTRL_QUEUE as u64; + let queue_phys = virt_to_phys(queue_virt); + crate::serial_println!( + "[virtio-gpu-pci] DMA addrs: fb={:#x}->{:#x} cmd={:#x}->{:#x} queue={:#x}->{:#x}", + fb_virt, fb_phys, cmd_virt, cmd_phys, queue_virt, queue_phys + ); + + // Query display info for diagnostics. + match get_display_info() { + Ok((w, h)) => { + crate::serial_println!("[virtio-gpu-pci] Display reports: {}x{}", w, h); + } + Err(e) => { + crate::serial_println!("[virtio-gpu-pci] get_display_info failed: {}", e); + } + } + + // Override to our desired resolution. + // On Parallels, VirtIO GPU set_scanout controls the display MODE (stride, + // resolution) but actual pixels are read from BAR0 (the GOP address at + // 0x10000000). We use VirtIO GPU purely to configure a higher resolution + // than the GOP-reported 1024x768. + let (use_width, use_height) = (DEFAULT_FB_WIDTH, DEFAULT_FB_HEIGHT); + crate::serial_println!("[virtio-gpu-pci] Requesting resolution: {}x{}", use_width, use_height); + + // Update state with actual dimensions + unsafe { + let ptr = &raw mut GPU_PCI_STATE; + if let Some(ref mut state) = *ptr { + state.width = use_width; + state.height = use_height; + } + } + + // Create framebuffer resource and attach backing + create_resource()?; + attach_backing()?; + set_scanout()?; + flush()?; + + // All GPU setup commands succeeded — now mark as initialized. + GPU_PCI_INITIALIZED.store(true, Ordering::Release); + + crate::serial_println!("[virtio-gpu-pci] Initialized: {}x{}", use_width, use_height); + Ok(()) +} + +// ============================================================================= +// Device State Access +// ============================================================================= + +/// Execute a closure with exclusive access to the GPU PCI device state. +fn with_device_state(f: F) -> Result +where + F: FnOnce(&mut GpuPciDeviceState) -> Result, +{ + let _guard = GPU_PCI_LOCK.lock(); + let state = unsafe { + let ptr = &raw mut GPU_PCI_STATE; + (*ptr).as_mut().ok_or("GPU PCI not initialized")? + }; + f(state) +} + +fn framebuffer_len(state: &GpuPciDeviceState) -> Result { + let len = (state.width as usize) + .saturating_mul(state.height as usize) + .saturating_mul(BYTES_PER_PIXEL); + if len == 0 || len > FB_SIZE { + return Err("Framebuffer size exceeds static buffer"); + } + Ok(len) +} + +// ============================================================================= +// Command Submission +// ============================================================================= + +/// Submit a 2-descriptor command/response chain via the control queue. +/// +/// Descriptor 0: command (device reads) +/// Descriptor 1: response (device writes) +/// Then notify the device via PCI transport and spin-wait for completion. +fn send_command( + state: &mut GpuPciDeviceState, + cmd_phys: u64, + cmd_len: u32, + resp_phys: u64, + resp_len: u32, +) -> Result<(), &'static str> { + unsafe { + let q = &raw mut PCI_CTRL_QUEUE; + + // Descriptor 0: command (device reads) + (*q).desc[0] = VirtqDesc { + addr: cmd_phys, + len: cmd_len, + flags: DESC_F_NEXT, + next: 1, + }; + + // Descriptor 1: response (device writes) + (*q).desc[1] = VirtqDesc { + addr: resp_phys, + len: resp_len, + flags: DESC_F_WRITE, + next: 0, + }; + + // Add to available ring + let idx = (*q).avail.idx; + (*q).avail.ring[(idx % 16) as usize] = 0; + fence(Ordering::SeqCst); + (*q).avail.idx = idx.wrapping_add(1); + fence(Ordering::SeqCst); + } + + // Notify device via PCI transport + state.device.notify_queue(0); + + // Spin-wait for used ring. + // The timeout must be generous: TRANSFER_TO_HOST_2D transfers up to 4MB + // (full framebuffer) and QEMU processes this in its event loop. + // 10M iterations is safe. + let mut timeout = 10_000_000u32; + loop { + fence(Ordering::SeqCst); + let used_idx = unsafe { + let q = &raw const PCI_CTRL_QUEUE; + read_volatile(&(*q).used.idx) + }; + if used_idx != state.last_used_idx { + state.last_used_idx = used_idx; + break; + } + timeout -= 1; + if timeout == 0 { + return Err("GPU PCI command timeout"); + } + core::hint::spin_loop(); + } + + Ok(()) +} + +/// Send a command and verify the response is RESP_OK_NODATA. +fn send_command_expect_ok( + state: &mut GpuPciDeviceState, + cmd_len: u32, +) -> Result<(), &'static str> { + let cmd_phys = virt_to_phys(&raw const PCI_CMD_BUF as u64); + let resp_phys = virt_to_phys(&raw const PCI_RESP_BUF as u64); + send_command( + state, + cmd_phys, + cmd_len, + resp_phys, + core::mem::size_of::() as u32, + )?; + + // Read response — use read_volatile to defeat caching (DMA coherency) + let resp_type = unsafe { + let resp_ptr = &raw const PCI_RESP_BUF; + core::ptr::read_volatile(&(*((*resp_ptr).data.as_ptr() as *const VirtioGpuCtrlHdr)).type_) + }; + if resp_type != cmd::RESP_OK_NODATA { + crate::serial_println!("[virtio-gpu-pci] Command failed: resp_type={:#x}", resp_type); + return Err("GPU PCI command failed"); + } + Ok(()) +} + +// ============================================================================= +// GPU Commands +// ============================================================================= + +fn get_display_info() -> Result<(u32, u32), &'static str> { + with_device_state(|state| { + let cmd_phys = virt_to_phys(&raw const PCI_CMD_BUF as u64); + let resp_phys = virt_to_phys(&raw const PCI_RESP_BUF as u64); + + // Prepare GET_DISPLAY_INFO command + unsafe { + let cmd_ptr = &raw mut PCI_CMD_BUF; + let hdr = &mut *((*cmd_ptr).data.as_mut_ptr() as *mut VirtioGpuCtrlHdr); + *hdr = VirtioGpuCtrlHdr { + type_: cmd::GET_DISPLAY_INFO, + flags: 0, + fence_id: 0, + ctx_id: 0, + padding: 0, + }; + } + + send_command( + state, + cmd_phys, + core::mem::size_of::() as u32, + resp_phys, + core::mem::size_of::() as u32, + )?; + + // Parse response + unsafe { + let resp_ptr = &raw const PCI_RESP_BUF; + let resp = &*((*resp_ptr).data.as_ptr() as *const VirtioGpuRespDisplayInfo); + + if resp.hdr.type_ != cmd::RESP_OK_DISPLAY_INFO { + return Err("GET_DISPLAY_INFO failed"); + } + + // Log ALL scanouts for diagnostics + let mut first_enabled = None; + for (i, pmode) in resp.pmodes.iter().enumerate() { + if pmode.r_width > 0 || pmode.r_height > 0 || pmode.enabled != 0 { + crate::serial_println!( + "[virtio-gpu-pci] Scanout {}: {}x{} enabled={} flags={:#x}", + i, pmode.r_width, pmode.r_height, pmode.enabled, pmode.flags + ); + if pmode.enabled != 0 && first_enabled.is_none() { + first_enabled = Some((pmode.r_width, pmode.r_height)); + } + } + } + + // Use first enabled scanout, or default + Ok(first_enabled.unwrap_or((DEFAULT_FB_WIDTH, DEFAULT_FB_HEIGHT))) + } + }) +} + +fn create_resource() -> Result<(), &'static str> { + with_device_state(|state| { + framebuffer_len(state)?; + unsafe { + let cmd_ptr = &raw mut PCI_CMD_BUF; + let cmd = &mut *((*cmd_ptr).data.as_mut_ptr() as *mut VirtioGpuResourceCreate2d); + *cmd = VirtioGpuResourceCreate2d { + hdr: VirtioGpuCtrlHdr { + type_: cmd::RESOURCE_CREATE_2D, + flags: 0, + fence_id: 0, + ctx_id: 0, + padding: 0, + }, + resource_id: state.resource_id, + format: format::B8G8R8A8_UNORM, + width: state.width, + height: state.height, + }; + } + send_command_expect_ok( + state, + core::mem::size_of::() as u32, + ) + }) +} + +#[repr(C)] +struct PciAttachBackingCmd { + cmd: VirtioGpuResourceAttachBacking, + entry: VirtioGpuMemEntry, +} + +fn attach_backing() -> Result<(), &'static str> { + with_device_state(|state| { + let fb_len = framebuffer_len(state)? as u32; + let fb_addr = virt_to_phys(&raw const PCI_FRAMEBUFFER as u64); + unsafe { + let cmd_ptr = &raw mut PCI_CMD_BUF; + let cmd = &mut *((*cmd_ptr).data.as_mut_ptr() as *mut PciAttachBackingCmd); + *cmd = PciAttachBackingCmd { + cmd: VirtioGpuResourceAttachBacking { + hdr: VirtioGpuCtrlHdr { + type_: cmd::RESOURCE_ATTACH_BACKING, + flags: 0, + fence_id: 0, + ctx_id: 0, + padding: 0, + }, + resource_id: state.resource_id, + nr_entries: 1, + }, + entry: VirtioGpuMemEntry { + addr: fb_addr, + length: fb_len, + padding: 0, + }, + }; + } + send_command_expect_ok(state, core::mem::size_of::() as u32) + }) +} + +fn set_scanout() -> Result<(), &'static str> { + with_device_state(|state| { + unsafe { + let cmd_ptr = &raw mut PCI_CMD_BUF; + let cmd = &mut *((*cmd_ptr).data.as_mut_ptr() as *mut VirtioGpuSetScanout); + *cmd = VirtioGpuSetScanout { + hdr: VirtioGpuCtrlHdr { + type_: cmd::SET_SCANOUT, + flags: 0, + fence_id: 0, + ctx_id: 0, + padding: 0, + }, + r_x: 0, + r_y: 0, + r_width: state.width, + r_height: state.height, + scanout_id: 0, + resource_id: state.resource_id, + }; + } + send_command_expect_ok( + state, + core::mem::size_of::() as u32, + ) + }) +} + +fn transfer_to_host( + state: &mut GpuPciDeviceState, + x: u32, + y: u32, + width: u32, + height: u32, +) -> Result<(), &'static str> { + // The offset is the byte position in the guest's backing buffer where QEMU + // starts reading. QEMU reads each row h at (offset + stride * h), where + // stride = resource_width * bpp. For sub-rect transfers, offset must point + // to (x, y) in the backing buffer so the correct pixels are transferred. + let stride = state.width as u64 * BYTES_PER_PIXEL as u64; + let offset = y as u64 * stride + x as u64 * BYTES_PER_PIXEL as u64; + + unsafe { + let cmd_ptr = &raw mut PCI_CMD_BUF; + let cmd = &mut *((*cmd_ptr).data.as_mut_ptr() as *mut VirtioGpuTransferToHost2d); + *cmd = VirtioGpuTransferToHost2d { + hdr: VirtioGpuCtrlHdr { + type_: cmd::TRANSFER_TO_HOST_2D, + flags: 0, + fence_id: 0, + ctx_id: 0, + padding: 0, + }, + r_x: x, + r_y: y, + r_width: width, + r_height: height, + offset, + resource_id: state.resource_id, + padding: 0, + }; + } + send_command_expect_ok( + state, + core::mem::size_of::() as u32, + ) +} + +fn resource_flush_cmd( + state: &mut GpuPciDeviceState, + x: u32, + y: u32, + width: u32, + height: u32, +) -> Result<(), &'static str> { + unsafe { + let cmd_ptr = &raw mut PCI_CMD_BUF; + let cmd = &mut *((*cmd_ptr).data.as_mut_ptr() as *mut VirtioGpuResourceFlush); + *cmd = VirtioGpuResourceFlush { + hdr: VirtioGpuCtrlHdr { + type_: cmd::RESOURCE_FLUSH, + flags: 0, + fence_id: 0, + ctx_id: 0, + padding: 0, + }, + r_x: x, + r_y: y, + r_width: width, + r_height: height, + resource_id: state.resource_id, + padding: 0, + }; + } + send_command_expect_ok( + state, + core::mem::size_of::() as u32, + ) +} + +// ============================================================================= +// Public API +// ============================================================================= + +/// Flush the entire framebuffer to the display. +pub fn flush() -> Result<(), &'static str> { + with_device_state(|state| { + fence(Ordering::SeqCst); + transfer_to_host(state, 0, 0, state.width, state.height)?; + resource_flush_cmd(state, 0, 0, state.width, state.height) + }) +} + +/// Flush a rectangular region of the framebuffer to the display. +pub fn flush_rect(x: u32, y: u32, width: u32, height: u32) -> Result<(), &'static str> { + with_device_state(|state| { + fence(Ordering::SeqCst); + transfer_to_host(state, x, y, width, height)?; + resource_flush_cmd(state, x, y, width, height) + }) +} + +/// Get the framebuffer dimensions. +pub fn dimensions() -> Option<(u32, u32)> { + unsafe { + let ptr = &raw const GPU_PCI_STATE; + (*ptr).as_ref().map(|s| (s.width, s.height)) + } +} + +/// Get a mutable reference to the framebuffer pixels. +#[allow(dead_code)] +pub fn framebuffer() -> Option<&'static mut [u8]> { + unsafe { + let ptr = &raw mut GPU_PCI_STATE; + if let Some(state) = (*ptr).as_ref() { + let len = framebuffer_len(state).ok()?; + let fb_ptr = &raw mut PCI_FRAMEBUFFER; + Some(&mut (&mut (*fb_ptr).pixels)[..len]) + } else { + None + } + } +} diff --git a/kernel/src/drivers/virtio/input_pci.rs b/kernel/src/drivers/virtio/input_pci.rs new file mode 100644 index 00000000..b4539282 --- /dev/null +++ b/kernel/src/drivers/virtio/input_pci.rs @@ -0,0 +1,566 @@ +//! VirtIO Input Device Driver for ARM64 (PCI Transport) +//! +//! Implements keyboard and mouse input via VirtIO PCI modern transport. +//! Reuses the event processing logic from `input_mmio.rs` but communicates +//! via the PCI transport layer (`VirtioPciDevice` from `pci_transport.rs`). +//! +//! Parallels Desktop exposes a VirtIO PCI device at type 19 (non-standard; +//! standard virtio-input is type 18). This driver accepts both. + +#![cfg(target_arch = "aarch64")] + +use super::pci_transport::{VirtioPciDevice, device_id, enumerate_virtio_pci_devices}; +use super::input_mmio::{ + VirtioInputEvent, event_type, keycode_to_char, keycode_to_escape_seq, + is_shift, is_ctrl, is_letter, ctrl_char_from_keycode, +}; +use core::sync::atomic::{AtomicBool, AtomicU32, Ordering, fence}; + +// ============================================================================= +// Constants +// ============================================================================= + +/// VirtIO standard feature: version 1.0 (mandatory for PCI modern transport) +const VIRTIO_F_VERSION_1: u64 = 1 << 32; + +/// Number of event buffers (pre-posted to the device). +/// Each buffer holds one VirtioInputEvent (8 bytes). +/// 64 buffers supports ~21 chars in flight for paste. +const NUM_EVENT_BUFFERS: usize = 64; + +/// Size of one VirtioInputEvent in bytes +const EVENT_SIZE: usize = core::mem::size_of::(); + +/// VirtIO input config select values (per VirtIO spec 5.8.4). +#[allow(dead_code)] +mod input_cfg { + /// Query device name string + pub const ID_NAME: u8 = 0x01; + /// Query supported event type bitmaps + pub const EV_BITS: u8 = 0x11; +} + +/// Absolute axis codes for mouse/tablet +mod abs_code { + pub const ABS_X: u16 = 0x00; + pub const ABS_Y: u16 = 0x01; +} + +/// Button codes for mouse +mod btn_code { + pub const BTN_LEFT: u16 = 0x110; +} + +// ============================================================================= +// Virtqueue Structures (same layout as gpu_pci.rs) +// ============================================================================= + +/// Virtqueue descriptor +#[repr(C)] +#[derive(Clone, Copy, Default)] +struct VirtqDesc { + addr: u64, + len: u32, + flags: u16, + next: u16, +} + +const DESC_F_WRITE: u16 = 2; + +/// Available ring +#[repr(C)] +struct VirtqAvail { + flags: u16, + idx: u16, + ring: [u16; NUM_EVENT_BUFFERS], +} + +/// Used ring element +#[repr(C)] +#[derive(Clone, Copy, Default)] +struct VirtqUsedElem { + id: u32, + len: u32, +} + +/// Used ring +#[repr(C)] +struct VirtqUsed { + flags: u16, + idx: u16, + ring: [VirtqUsedElem; NUM_EVENT_BUFFERS], +} + +// Event queue memory (page-aligned, used ring at +4096 for alignment) +#[repr(C, align(4096))] +struct InputEventQueueMemory { + desc: [VirtqDesc; NUM_EVENT_BUFFERS], // 64 * 16 = 1024 bytes + avail: VirtqAvail, // 4 + 64*2 = 132 bytes + _padding: [u8; 4096 - 1024 - 132], + used: VirtqUsed, // 4 + 64*8 = 516 bytes +} + +/// Event buffers where the device writes input events +#[repr(C, align(64))] +struct InputEventBuffers { + events: [VirtioInputEvent; NUM_EVENT_BUFFERS], +} + +// ============================================================================= +// Static Buffers +// ============================================================================= + +static mut INPUT_EVENT_QUEUE: InputEventQueueMemory = InputEventQueueMemory { + desc: [VirtqDesc { addr: 0, len: 0, flags: 0, next: 0 }; NUM_EVENT_BUFFERS], + avail: VirtqAvail { flags: 0, idx: 0, ring: [0; NUM_EVENT_BUFFERS] }, + _padding: [0; 4096 - 1024 - 132], + used: VirtqUsed { + flags: 0, + idx: 0, + ring: [VirtqUsedElem { id: 0, len: 0 }; NUM_EVENT_BUFFERS], + }, +}; + +static mut INPUT_EVENT_BUFFERS: InputEventBuffers = InputEventBuffers { + events: [VirtioInputEvent { event_type: 0, code: 0, value: 0 }; NUM_EVENT_BUFFERS], +}; + +// ============================================================================= +// Device State +// ============================================================================= + +struct InputPciDeviceState { + device: VirtioPciDevice, + last_used_idx: u16, +} + +static mut INPUT_PCI_STATE: Option = None; +static INPUT_PCI_INITIALIZED: AtomicBool = AtomicBool::new(false); + +/// Init status for diagnostics (0=not attempted, 1=success, 2=no device, 3=no events, 4=init error, 5=N virtio devices found) +pub static INIT_STATUS: AtomicU32 = AtomicU32::new(0); + +/// Mouse position (written by event handler, read by render thread) +static MOUSE_X: AtomicU32 = AtomicU32::new(0); +static MOUSE_Y: AtomicU32 = AtomicU32::new(0); +static MOUSE_BUTTONS: AtomicU32 = AtomicU32::new(0); + +/// Tablet absolute position range (0..32767) +const TABLET_ABS_MAX: u32 = 32767; + +// ============================================================================= +// Helpers +// ============================================================================= + +#[inline(always)] +fn virt_to_phys(addr: u64) -> u64 { + const HHDM_BASE: u64 = 0xFFFF_0000_0000_0000; + if addr >= HHDM_BASE { + addr - HHDM_BASE + } else { + addr + } +} + +/// Get screen dimensions (fall back to 1280x800) +fn screen_dimensions() -> (u32, u32) { + if super::gpu_pci::is_initialized() { + super::gpu_pci::dimensions().unwrap_or((1280, 800)) + } else { + (1280, 800) + } +} + +// ============================================================================= +// Initialization +// ============================================================================= + +/// Check if the VirtIO Input PCI driver is initialized. +pub fn is_initialized() -> bool { + INPUT_PCI_INITIALIZED.load(Ordering::Acquire) +} + +/// Initialize the VirtIO Input PCI device. +/// +/// Searches for a VirtIO input device on the PCI bus. Accepts both +/// standard type 18 (virtio-input) and type 19 (Parallels non-standard). +pub fn init() -> Result<(), &'static str> { + if is_initialized() { + return Ok(()); + } + + crate::serial_println!("[virtio-input-pci] Searching for input PCI device..."); + + let devices = enumerate_virtio_pci_devices(); + // Store number of VirtIO PCI devices found (shifted into status for diagnostics) + let num_virtio = devices.len() as u32; + INIT_STATUS.store(10 + num_virtio, Ordering::Relaxed); + + // Find an input device: try standard type 18 first, then type 19 + let mut target: Option = None; + for dev in devices { + let pci = dev.pci_device(); + crate::serial_println!( + "[virtio-input-pci] VirtIO PCI device: type={} pci_id={:#06x}", + dev.device_id(), + pci.device_id, + ); + + if dev.device_id() == device_id::INPUT { + crate::serial_println!("[virtio-input-pci] Found standard virtio-input (type=18)"); + target = Some(dev); + break; + } + if dev.device_id() == 19 { + // VirtIO type 19 is IOMMU per the VirtIO 1.1+ spec, NOT input. + // Parallels exposes this device (PCI ID 0x1053) but it does not + // implement the virtio-input config interface. Skip it. + crate::serial_println!( + "[virtio-input-pci] Type=19 is VirtIO IOMMU (not input), skipping" + ); + } + } + + let mut virtio = match target { + Some(d) => d, + None => { + INIT_STATUS.store(2, Ordering::Relaxed); // 2 = no device found + return Err("No VirtIO input PCI device found"); + } + }; + + // Query device-specific config to verify this is an input device. + // VirtIO input config: write select=EV_BITS subsel=EV_KEY → if size>0, it supports keyboard. + // This must be done BEFORE init() (config is readable at any status). + let has_device_cfg = virtio.read_config_u8(0) != 0 || virtio.read_config_u8(2) != 0; + crate::serial_println!( + "[virtio-input-pci] Device config probe: has_device_cfg={}", + has_device_cfg, + ); + + // Try to query EV_BITS for EV_KEY (select=0x11, subsel=0x01) + // VirtIO input config layout: offset 0=select(w), 1=subsel(w), 2=size(r), 8..=135=data + // For PCI modern transport, device_cfg maps to the device-specific config region. + // Write select and subsel, then read size. + virtio.write_config_u8(0, input_cfg::EV_BITS); + virtio.write_config_u8(1, event_type::EV_KEY as u8); + let ev_key_size = virtio.read_config_u8(2); + crate::serial_println!( + "[virtio-input-pci] EV_KEY bitmap size: {} bytes", + ev_key_size, + ); + + // Also check EV_ABS for mouse/tablet capability + virtio.write_config_u8(0, input_cfg::EV_BITS); + virtio.write_config_u8(1, event_type::EV_ABS as u8); + let ev_abs_size = virtio.read_config_u8(2); + crate::serial_println!( + "[virtio-input-pci] EV_ABS bitmap size: {} bytes", + ev_abs_size, + ); + + // Query device name + virtio.write_config_u8(0, input_cfg::ID_NAME); + virtio.write_config_u8(1, 0); + let name_size = virtio.read_config_u8(2) as usize; + if name_size > 0 { + let mut name_buf = [0u8; 64]; + let to_read = name_size.min(64); + for i in 0..to_read { + name_buf[i] = virtio.read_config_u8(8 + i); + } + if let Ok(name) = core::str::from_utf8(&name_buf[..to_read]) { + crate::serial_println!("[virtio-input-pci] Device name: {}", name.trim_end_matches('\0')); + } + } + + if ev_key_size == 0 && ev_abs_size == 0 { + return Err("VirtIO PCI device does not support keyboard or mouse events"); + } + + // Initialize VirtIO device (reset, features, etc.) + virtio.init(VIRTIO_F_VERSION_1)?; + crate::serial_println!("[virtio-input-pci] VirtIO init complete (features negotiated)"); + + // Set up event queue (queue 0) + virtio.select_queue(0); + let queue_max = virtio.get_queue_num_max(); + crate::serial_println!("[virtio-input-pci] Event queue max size: {}", queue_max); + + if queue_max == 0 { + return Err("Event queue size is 0"); + } + + let queue_size = core::cmp::min(queue_max as usize, NUM_EVENT_BUFFERS); + virtio.set_queue_num(queue_size as u32); + + let queue_phys = virt_to_phys(&raw const INPUT_EVENT_QUEUE as u64); + let events_phys = virt_to_phys(&raw const INPUT_EVENT_BUFFERS as u64); + + // Initialize descriptors: each points to one event buffer, device-writable + unsafe { + let q = &raw mut INPUT_EVENT_QUEUE; + for i in 0..queue_size { + let event_phys = events_phys + (i * EVENT_SIZE) as u64; + (*q).desc[i] = VirtqDesc { + addr: event_phys, + len: EVENT_SIZE as u32, + flags: DESC_F_WRITE, + next: 0, + }; + } + + // Post all buffers to available ring + for i in 0..queue_size { + (*q).avail.ring[i] = i as u16; + } + fence(Ordering::SeqCst); + (*q).avail.idx = queue_size as u16; + fence(Ordering::SeqCst); + + (*q).used.flags = 0; + (*q).used.idx = 0; + } + + // Set queue memory addresses (PCI modern: separate desc/avail/used) + virtio.set_queue_desc(queue_phys); + // avail ring is right after descriptors: 64 descs * 16 bytes = 1024 + virtio.set_queue_avail(queue_phys + 1024); + // used ring is at the page boundary (+4096) + virtio.set_queue_used(queue_phys + 4096); + virtio.set_queue_ready(true); + + crate::serial_println!( + "[virtio-input-pci] Queue: desc={:#x} avail={:#x} used={:#x} size={}", + queue_phys, queue_phys + 1024, queue_phys + 4096, queue_size, + ); + + // Mark device ready + virtio.driver_ok(); + + // Notify device that buffers are available (queue 0) + virtio.notify_queue(0); + + crate::serial_println!( + "[virtio-input-pci] Device ready (kbd={} mouse={})", + ev_key_size > 0, + ev_abs_size > 0, + ); + + // Store state + unsafe { + let ptr = &raw mut INPUT_PCI_STATE; + *ptr = Some(InputPciDeviceState { + device: virtio, + last_used_idx: 0, + }); + } + INPUT_PCI_INITIALIZED.store(true, Ordering::Release); + + crate::serial_println!("[virtio-input-pci] Initialized with {} event buffers", queue_size); + Ok(()) +} + +// ============================================================================= +// Event Polling +// ============================================================================= + +/// Poll for new input events from the VirtIO input device. +/// +/// Called from the timer interrupt handler. Processes all pending events +/// in the used ring, dispatches keyboard characters to TTY, and re-posts +/// consumed buffers to the available ring. +pub fn poll_events() { + if !INPUT_PCI_INITIALIZED.load(Ordering::Acquire) { + return; + } + + unsafe { + let state_ptr = &raw mut INPUT_PCI_STATE; + let state = match (*state_ptr).as_mut() { + Some(s) => s, + None => return, + }; + + let q = &raw mut INPUT_EVENT_QUEUE; + let events = &raw const INPUT_EVENT_BUFFERS; + + fence(Ordering::SeqCst); + let current_used = core::ptr::read_volatile(&(*q).used.idx); + fence(Ordering::SeqCst); + + if current_used == state.last_used_idx { + return; // No new events + } + + let mut idx = state.last_used_idx; + while idx != current_used { + let ring_idx = (idx as usize) % NUM_EVENT_BUFFERS; + let used_elem = core::ptr::read_volatile(&(*q).used.ring[ring_idx]); + let desc_idx = used_elem.id as usize; + + if desc_idx < NUM_EVENT_BUFFERS { + let event = core::ptr::read_volatile(&(*events).events[desc_idx]); + process_event(&event); + + // Re-post this buffer to the available ring + let avail_idx = core::ptr::read_volatile(&(*q).avail.idx) as usize; + core::ptr::write_volatile( + &mut (*q).avail.ring[avail_idx % NUM_EVENT_BUFFERS], + desc_idx as u16, + ); + fence(Ordering::SeqCst); + core::ptr::write_volatile( + &mut (*q).avail.idx, + (avail_idx as u16).wrapping_add(1), + ); + } + + idx = idx.wrapping_add(1); + } + + state.last_used_idx = current_used; + + // Notify device that new buffers are available + if current_used != state.last_used_idx.wrapping_sub(1) { + fence(Ordering::SeqCst); + state.device.notify_queue(0); + } + } +} + +// ============================================================================= +// Event Processing +// ============================================================================= + +/// Modifier key state (shared across poll calls) +static SHIFT_PRESSED: AtomicBool = AtomicBool::new(false); +static CTRL_PRESSED: AtomicBool = AtomicBool::new(false); +static CAPS_LOCK_ACTIVE: AtomicBool = AtomicBool::new(false); + +/// Counter for keyboard events received +static KBD_EVENT_COUNT: AtomicU32 = AtomicU32::new(0); + +/// Get the count of keyboard events received +pub fn kbd_event_count() -> u32 { + KBD_EVENT_COUNT.load(Ordering::Relaxed) +} + +/// Process a single VirtIO input event. +fn process_event(event: &VirtioInputEvent) { + match event.event_type { + event_type::EV_KEY => { + process_key_event(event.code, event.value); + } + event_type::EV_ABS => { + process_abs_event(event.code, event.value); + } + event_type::EV_SYN => { + // Sync event — ignore + } + _ => { + // Unknown event type — ignore + } + } +} + +/// Process a keyboard key event. +fn process_key_event(keycode: u16, value: u32) { + // Mouse button events use EV_KEY with button codes >= 0x110 + if keycode >= 0x110 { + if keycode == btn_code::BTN_LEFT { + MOUSE_BUTTONS.store(if value != 0 { 1 } else { 0 }, Ordering::Relaxed); + } + return; + } + + KBD_EVENT_COUNT.fetch_add(1, Ordering::Relaxed); + + // Track shift key state + if is_shift(keycode) { + SHIFT_PRESSED.store(value != 0, Ordering::Relaxed); + return; + } + + // Track ctrl key state + if is_ctrl(keycode) { + CTRL_PRESSED.store(value != 0, Ordering::Relaxed); + return; + } + + // Toggle caps lock on key press only (not repeat or release) + if keycode == 58 { + if value == 1 { + let prev = CAPS_LOCK_ACTIVE.load(Ordering::Relaxed); + CAPS_LOCK_ACTIVE.store(!prev, Ordering::Relaxed); + } + return; + } + + // Only process key presses and repeats (not releases) + if value == 0 { + return; + } + + // Generate VT100 escape sequences for special keys + if let Some(seq) = keycode_to_escape_seq(keycode) { + for &b in seq { + if !crate::tty::push_char_nonblock(b) { + crate::ipc::stdin::push_byte_from_irq(b); + } + } + return; + } + + let shift = SHIFT_PRESSED.load(Ordering::Relaxed); + let caps = CAPS_LOCK_ACTIVE.load(Ordering::Relaxed); + let ctrl = CTRL_PRESSED.load(Ordering::Relaxed); + + let c = if ctrl { + ctrl_char_from_keycode(keycode) + } else { + let effective_shift = if is_letter(keycode) { shift ^ caps } else { shift }; + keycode_to_char(keycode, effective_shift) + }; + + if let Some(c) = c { + if !crate::tty::push_char_nonblock(c as u8) { + crate::ipc::stdin::push_byte_from_irq(c as u8); + } + } +} + +/// Process an absolute axis event (mouse/tablet movement). +fn process_abs_event(code: u16, value: u32) { + match code { + abs_code::ABS_X => { + let (sw, _) = screen_dimensions(); + let x = (value as u64 * sw as u64 / (TABLET_ABS_MAX as u64 + 1)) as u32; + MOUSE_X.store(x.min(sw.saturating_sub(1)), Ordering::Relaxed); + } + abs_code::ABS_Y => { + let (_, sh) = screen_dimensions(); + let y = (value as u64 * sh as u64 / (TABLET_ABS_MAX as u64 + 1)) as u32; + MOUSE_Y.store(y.min(sh.saturating_sub(1)), Ordering::Relaxed); + } + _ => {} + } +} + +// ============================================================================= +// Public Query Functions +// ============================================================================= + +/// Get current mouse position in screen coordinates. +pub fn mouse_position() -> (u32, u32) { + (MOUSE_X.load(Ordering::Relaxed), MOUSE_Y.load(Ordering::Relaxed)) +} + +/// Get current mouse position and button state. +pub fn mouse_state() -> (u32, u32, u32) { + ( + MOUSE_X.load(Ordering::Relaxed), + MOUSE_Y.load(Ordering::Relaxed), + MOUSE_BUTTONS.load(Ordering::Relaxed), + ) +} diff --git a/kernel/src/drivers/virtio/mod.rs b/kernel/src/drivers/virtio/mod.rs index 99a7707e..7e5b8075 100644 --- a/kernel/src/drivers/virtio/mod.rs +++ b/kernel/src/drivers/virtio/mod.rs @@ -26,6 +26,8 @@ pub mod queue; #[cfg(target_arch = "aarch64")] pub mod mmio; #[cfg(target_arch = "aarch64")] +pub mod pci_transport; +#[cfg(target_arch = "aarch64")] pub mod block_mmio; #[cfg(target_arch = "aarch64")] pub mod net_mmio; @@ -35,6 +37,8 @@ pub mod gpu_mmio; pub mod input_mmio; #[cfg(target_arch = "aarch64")] pub mod sound_mmio; +#[cfg(target_arch = "aarch64")] +pub mod gpu_pci; #[cfg(target_arch = "x86_64")] pub mod sound; diff --git a/kernel/src/drivers/virtio/pci_transport.rs b/kernel/src/drivers/virtio/pci_transport.rs new file mode 100644 index 00000000..7e3f0f97 --- /dev/null +++ b/kernel/src/drivers/virtio/pci_transport.rs @@ -0,0 +1,612 @@ +//! VirtIO PCI Modern Transport (VirtIO 1.0+) +//! +//! Implements the VirtIO PCI transport using capability-based BAR access. +//! This module is used on platforms with PCI (e.g., Parallels on ARM64) +//! where VirtIO devices appear as PCI functions. +//! +//! The modern VirtIO PCI transport uses PCI capabilities to locate +//! memory-mapped regions in BARs for device configuration: +//! - Common Configuration (features, status, queue management) +//! - Notification (queue doorbell) +//! - ISR Status +//! - Device-specific Configuration +//! +//! For transitional (legacy) devices (PCI ID 0x1000-0x103F), the modern +//! interface is also available via capabilities alongside the legacy I/O port +//! interface. We always prefer the modern interface. + +#![allow(dead_code)] + +use crate::drivers::pci::{self, Device as PciDevice}; + +/// HHDM base for memory-mapped access. +const HHDM_BASE: u64 = 0xFFFF_0000_0000_0000; + +// ============================================================================= +// VirtIO PCI Capability Types (from VirtIO 1.0 spec, section 4.1.4) +// ============================================================================= + +/// PCI Capability ID for vendor-specific (VirtIO uses this) +const PCI_CAP_ID_VNDR: u8 = 0x09; + +/// Common configuration +const VIRTIO_PCI_CAP_COMMON_CFG: u8 = 1; +/// Notifications +const VIRTIO_PCI_CAP_NOTIFY_CFG: u8 = 2; +/// ISR status +const VIRTIO_PCI_CAP_ISR_CFG: u8 = 3; +/// Device-specific configuration +const VIRTIO_PCI_CAP_DEVICE_CFG: u8 = 4; + +// ============================================================================= +// Common Configuration Register Offsets (VirtIO 1.0 spec, section 4.1.4.3) +// ============================================================================= + +/// Select device feature dword (0 = low 32, 1 = high 32) +const COMMON_DFSELECT: usize = 0x00; +/// Read device feature bits (dword selected by DFSELECT) +const COMMON_DF: usize = 0x04; +/// Select driver feature dword +const COMMON_GFSELECT: usize = 0x08; +/// Write driver feature bits +const COMMON_GF: usize = 0x0C; +/// MSI-X configuration vector +const COMMON_MSIX: usize = 0x10; +/// Number of virtqueues +const COMMON_NUMQ: usize = 0x12; +/// Device status +const COMMON_STATUS: usize = 0x14; +/// Configuration atomicity generation counter +const COMMON_CFGGEN: usize = 0x15; +/// Queue select +const COMMON_Q_SELECT: usize = 0x16; +/// Queue size (max) +const COMMON_Q_SIZE: usize = 0x18; +/// Queue MSI-X vector +const COMMON_Q_MSIX: usize = 0x1A; +/// Queue enable +const COMMON_Q_ENABLE: usize = 0x1C; +/// Queue notify offset (multiplied by notify_off_multiplier) +const COMMON_Q_NOFF: usize = 0x1E; +/// Queue descriptor table address (64-bit) +const COMMON_Q_DESCLO: usize = 0x20; +const COMMON_Q_DESCHI: usize = 0x24; +/// Queue available ring address (64-bit) +const COMMON_Q_AVAILLO: usize = 0x28; +const COMMON_Q_AVAILHI: usize = 0x2C; +/// Queue used ring address (64-bit) +const COMMON_Q_USEDLO: usize = 0x30; +const COMMON_Q_USEDHI: usize = 0x34; + +// ============================================================================= +// VirtIO Status Bits +// ============================================================================= + +const STATUS_ACKNOWLEDGE: u8 = 1; +const STATUS_DRIVER: u8 = 2; +const STATUS_DRIVER_OK: u8 = 4; +const STATUS_FEATURES_OK: u8 = 8; +const STATUS_FAILED: u8 = 128; + +// ============================================================================= +// VirtIO Device Types (from VirtIO spec) +// ============================================================================= + +/// VirtIO device type IDs (same as MMIO device_id values) +pub mod device_id { + pub const NETWORK: u32 = 1; + pub const BLOCK: u32 = 2; + pub const CONSOLE: u32 = 3; + pub const GPU: u32 = 16; + pub const INPUT: u32 = 18; + pub const SOUND: u32 = 25; +} + +// ============================================================================= +// Capability Region Descriptor +// ============================================================================= + +/// Describes a memory-mapped region found via PCI capabilities. +#[derive(Debug, Clone, Copy)] +struct CapRegion { + /// Virtual address of the region (HHDM + BAR physical + offset) + virt_base: u64, + /// Length of the region + length: u32, +} + +impl CapRegion { + const NONE: Self = CapRegion { virt_base: 0, length: 0 }; + + fn is_valid(&self) -> bool { + self.virt_base != 0 && self.length > 0 + } + + #[inline] + fn read_u8(&self, offset: usize) -> u8 { + assert!(offset < self.length as usize); + unsafe { core::ptr::read_volatile((self.virt_base + offset as u64) as *const u8) } + } + + #[inline] + fn write_u8(&self, offset: usize, value: u8) { + assert!(offset < self.length as usize); + unsafe { core::ptr::write_volatile((self.virt_base + offset as u64) as *mut u8, value) } + } + + #[inline] + fn read_u16(&self, offset: usize) -> u16 { + assert!(offset + 1 < self.length as usize); + unsafe { core::ptr::read_volatile((self.virt_base + offset as u64) as *const u16) } + } + + #[inline] + fn write_u16(&self, offset: usize, value: u16) { + assert!(offset + 1 < self.length as usize); + unsafe { core::ptr::write_volatile((self.virt_base + offset as u64) as *mut u16, value) } + } + + #[inline] + fn read_u32(&self, offset: usize) -> u32 { + assert!(offset + 3 < self.length as usize); + unsafe { core::ptr::read_volatile((self.virt_base + offset as u64) as *const u32) } + } + + #[inline] + fn write_u32(&self, offset: usize, value: u32) { + assert!(offset + 3 < self.length as usize); + unsafe { core::ptr::write_volatile((self.virt_base + offset as u64) as *mut u32, value) } + } +} + +// ============================================================================= +// VirtIO PCI Device +// ============================================================================= + +/// A VirtIO device accessed via PCI modern transport. +/// +/// This provides the same interface as `VirtioMmioDevice` so that device-specific +/// drivers can work with either transport. +pub struct VirtioPciDevice { + /// The underlying PCI device (for config space access) + pci_dev: PciDevice, + /// Common configuration region (features, status, queues) + common: CapRegion, + /// Notification region (queue doorbell writes) + notify: CapRegion, + /// Notify offset multiplier (from NOTIFY_CFG capability) + notify_off_multiplier: u32, + /// ISR status region + isr: CapRegion, + /// Device-specific configuration region + device_cfg: CapRegion, + /// Cached device features + device_features: u64, + /// VirtIO device type ID + virtio_device_id: u32, +} + +impl VirtioPciDevice { + /// Probe a PCI device for VirtIO modern capabilities. + /// + /// Returns `Some(device)` if this PCI device supports the VirtIO modern interface + /// (has the required PCI capabilities pointing to BAR regions). + pub fn probe(pci_dev: PciDevice) -> Option { + // Verify this is a VirtIO device + if pci_dev.vendor_id != pci::VIRTIO_VENDOR_ID { + return None; + } + + // Determine VirtIO device type from PCI device ID + let virtio_device_id = pci_device_id_to_virtio(&pci_dev); + if virtio_device_id == 0 { + return None; + } + + // Enable memory space and bus mastering + pci_dev.enable_memory_space(); + pci_dev.enable_bus_master(); + + // Walk PCI capabilities to find VirtIO capability structures + let mut common = CapRegion::NONE; + let mut notify = CapRegion::NONE; + let mut notify_off_multiplier = 0u32; + let mut isr = CapRegion::NONE; + let mut device_cfg = CapRegion::NONE; + + // PCI Status register bit 4 = Capabilities List exists + let status = pci::pci_read_config_word( + pci_dev.bus, pci_dev.device, pci_dev.function, 0x06, + ); + if (status & (1 << 4)) == 0 { + return None; // No capabilities + } + + // Capabilities pointer is at offset 0x34 + let mut cap_ptr = pci::pci_read_config_byte( + pci_dev.bus, pci_dev.device, pci_dev.function, 0x34, + ); + + while cap_ptr != 0 { + let cap_id = pci::pci_read_config_byte( + pci_dev.bus, pci_dev.device, pci_dev.function, cap_ptr, + ); + let cap_next = pci::pci_read_config_byte( + pci_dev.bus, pci_dev.device, pci_dev.function, cap_ptr + 1, + ); + + if cap_id == PCI_CAP_ID_VNDR { + // VirtIO PCI capability structure: + // +0: cap_vndr (0x09) + // +1: cap_next + // +2: cap_len + // +3: cfg_type (COMMON, NOTIFY, ISR, DEVICE) + // +4: bar (which BAR this maps to) + // +8: offset (offset within the BAR) + // +12: length (length of the region) + let cfg_type = pci::pci_read_config_byte( + pci_dev.bus, pci_dev.device, pci_dev.function, cap_ptr + 3, + ); + let bar_index = pci::pci_read_config_byte( + pci_dev.bus, pci_dev.device, pci_dev.function, cap_ptr + 4, + ) as usize; + + // Read offset and length as dwords + let offset = pci_read_cap_dword(&pci_dev, cap_ptr + 8); + let length = pci_read_cap_dword(&pci_dev, cap_ptr + 12); + + // Resolve the BAR physical address + if bar_index < 6 { + let bar = &pci_dev.bars[bar_index]; + if bar.is_valid() && !bar.is_io { + let virt_base = HHDM_BASE + bar.address + offset as u64; + let region = CapRegion { virt_base, length }; + + match cfg_type { + VIRTIO_PCI_CAP_COMMON_CFG => common = region, + VIRTIO_PCI_CAP_NOTIFY_CFG => { + notify = region; + // NOTIFY_CFG has an extra dword: notify_off_multiplier at cap+16 + notify_off_multiplier = pci_read_cap_dword(&pci_dev, cap_ptr + 16); + } + VIRTIO_PCI_CAP_ISR_CFG => isr = region, + VIRTIO_PCI_CAP_DEVICE_CFG => device_cfg = region, + _ => {} // Ignore unknown capability types + } + } + } + } + + cap_ptr = cap_next; + } + + // We need at minimum the common config and notification regions + if !common.is_valid() || !notify.is_valid() { + return None; + } + + Some(VirtioPciDevice { + pci_dev, + common, + notify, + notify_off_multiplier, + isr, + device_cfg, + device_features: 0, + virtio_device_id, + }) + } + + // ========================================================================= + // Device Identity + // ========================================================================= + + /// Get the VirtIO device type ID. + pub fn device_id(&self) -> u32 { + self.virtio_device_id + } + + /// Get the VirtIO device version (always 1 for modern PCI transport). + pub fn version(&self) -> u32 { + 1 + } + + // ========================================================================= + // Status and Initialization + // ========================================================================= + + /// Read the device status register. + pub fn read_status(&self) -> u8 { + self.common.read_u8(COMMON_STATUS) + } + + /// Write the device status register. + pub fn write_status(&self, status: u8) { + self.common.write_u8(COMMON_STATUS, status); + } + + /// Reset the device. + pub fn reset(&self) { + self.write_status(0); + } + + /// Initialize the device with feature negotiation. + /// + /// Performs the VirtIO 1.0 initialization sequence: + /// 1. Reset device + /// 2. Set ACKNOWLEDGE + /// 3. Set DRIVER + /// 4. Negotiate features + /// 5. Set FEATURES_OK + /// 6. Verify FEATURES_OK + pub fn init(&mut self, requested_features: u64) -> Result<(), &'static str> { + // Reset + self.reset(); + + // Wait for reset + for _ in 0..10_000 { + if self.read_status() == 0 { + break; + } + core::hint::spin_loop(); + } + + // ACKNOWLEDGE + self.write_status(STATUS_ACKNOWLEDGE); + + // DRIVER + self.write_status(STATUS_ACKNOWLEDGE | STATUS_DRIVER); + + // Read device features (64-bit: low 32 + high 32) + self.device_features = self.read_device_features(); + let driver_features = self.device_features & requested_features; + self.write_driver_features(driver_features); + + // FEATURES_OK + self.write_status(STATUS_ACKNOWLEDGE | STATUS_DRIVER | STATUS_FEATURES_OK); + + // Verify FEATURES_OK + if (self.read_status() & STATUS_FEATURES_OK) == 0 { + self.write_status(STATUS_FAILED); + return Err("Device did not accept features"); + } + + Ok(()) + } + + /// Mark the device as ready (DRIVER_OK). + pub fn driver_ok(&self) { + let status = self.read_status(); + self.write_status(status | STATUS_DRIVER_OK); + } + + // ========================================================================= + // Feature Negotiation + // ========================================================================= + + /// Read 64-bit device features. + pub fn read_device_features(&self) -> u64 { + // Low 32 bits + self.common.write_u32(COMMON_DFSELECT, 0); + let low = self.common.read_u32(COMMON_DF) as u64; + + // High 32 bits + self.common.write_u32(COMMON_DFSELECT, 1); + let high = self.common.read_u32(COMMON_DF) as u64; + + (high << 32) | low + } + + /// Write 64-bit driver features. + pub fn write_driver_features(&self, features: u64) { + // Low 32 bits + self.common.write_u32(COMMON_GFSELECT, 0); + self.common.write_u32(COMMON_GF, features as u32); + + // High 32 bits + self.common.write_u32(COMMON_GFSELECT, 1); + self.common.write_u32(COMMON_GF, (features >> 32) as u32); + } + + /// Get the cached device features. + pub fn device_features(&self) -> u64 { + self.device_features + } + + // ========================================================================= + // Queue Management + // ========================================================================= + + /// Select a virtqueue for configuration. + pub fn select_queue(&self, queue: u32) { + self.common.write_u16(COMMON_Q_SELECT, queue as u16); + } + + /// Get the maximum size of the currently selected queue. + pub fn get_queue_num_max(&self) -> u32 { + self.common.read_u16(COMMON_Q_SIZE) as u32 + } + + /// Set the size of the currently selected queue. + pub fn set_queue_num(&self, num: u32) { + self.common.write_u16(COMMON_Q_SIZE, num as u16); + } + + /// Get the number of virtqueues. + pub fn num_queues(&self) -> u16 { + self.common.read_u16(COMMON_NUMQ) + } + + /// Set the descriptor table physical address for the current queue. + pub fn set_queue_desc(&self, addr: u64) { + self.common.write_u32(COMMON_Q_DESCLO, addr as u32); + self.common.write_u32(COMMON_Q_DESCHI, (addr >> 32) as u32); + } + + /// Set the available ring physical address for the current queue. + pub fn set_queue_avail(&self, addr: u64) { + self.common.write_u32(COMMON_Q_AVAILLO, addr as u32); + self.common.write_u32(COMMON_Q_AVAILHI, (addr >> 32) as u32); + } + + /// Set the used ring physical address for the current queue. + pub fn set_queue_used(&self, addr: u64) { + self.common.write_u32(COMMON_Q_USEDLO, addr as u32); + self.common.write_u32(COMMON_Q_USEDHI, (addr >> 32) as u32); + } + + /// Enable or disable the currently selected queue. + pub fn set_queue_ready(&self, ready: bool) { + self.common.write_u16(COMMON_Q_ENABLE, if ready { 1 } else { 0 }); + } + + /// Notify the device that there are new buffers in a queue. + pub fn notify_queue(&self, queue: u32) { + // Read the queue's notify offset from the common config + self.select_queue(queue); + let queue_notify_off = self.common.read_u16(COMMON_Q_NOFF) as u32; + + // The notification address is: + // notify_base + queue_notify_off * notify_off_multiplier + let offset = (queue_notify_off * self.notify_off_multiplier) as usize; + + // Write the queue index to the notification address + // For VirtIO 1.0, we write a u16 value of the queue index + unsafe { + let addr = (self.notify.virt_base + offset as u64) as *mut u16; + core::ptr::write_volatile(addr, queue as u16); + } + } + + // ========================================================================= + // Interrupt Handling + // ========================================================================= + + /// Read the ISR status register. + /// + /// Bit 0: Queue interrupt + /// Bit 1: Configuration change + /// Reading this register clears it. + pub fn read_interrupt_status(&self) -> u32 { + if self.isr.is_valid() { + self.isr.read_u8(0) as u32 + } else { + 0 + } + } + + /// Acknowledge interrupts. + pub fn ack_interrupt(&self, _flags: u32) { + // For modern PCI transport, reading ISR auto-acknowledges. + // This is a no-op but kept for interface compatibility with MMIO. + } + + // ========================================================================= + // Device Configuration + // ========================================================================= + + /// Read the configuration generation counter. + pub fn config_generation(&self) -> u32 { + self.common.read_u8(COMMON_CFGGEN) as u32 + } + + /// Read a u8 from device-specific configuration. + pub fn read_config_u8(&self, offset: usize) -> u8 { + if !self.device_cfg.is_valid() { + return 0; + } + self.device_cfg.read_u8(offset) + } + + /// Read a u32 from device-specific configuration. + pub fn read_config_u32(&self, offset: usize) -> u32 { + if !self.device_cfg.is_valid() { + return 0; + } + self.device_cfg.read_u32(offset) + } + + /// Read a u64 from device-specific configuration (two u32 reads). + pub fn read_config_u64(&self, offset: usize) -> u64 { + let low = self.read_config_u32(offset) as u64; + let high = self.read_config_u32(offset + 4) as u64; + (high << 32) | low + } + + /// Get a reference to the underlying PCI device. + pub fn pci_device(&self) -> &PciDevice { + &self.pci_dev + } +} + +// ============================================================================= +// Helpers +// ============================================================================= + +/// Read a dword from PCI config space at an arbitrary offset (for capability walking). +fn pci_read_cap_dword(dev: &PciDevice, offset: u8) -> u32 { + // pci_read_config_dword aligns to 4 bytes, so we can use the offset directly + // if it's already dword-aligned. For capability fields, offsets are designed + // to be properly aligned. + let dword_offset = offset & 0xFC; + let dword = pci::pci_read_config_byte(dev.bus, dev.device, dev.function, dword_offset) as u32 + | (pci::pci_read_config_byte(dev.bus, dev.device, dev.function, dword_offset + 1) as u32) << 8 + | (pci::pci_read_config_byte(dev.bus, dev.device, dev.function, dword_offset + 2) as u32) << 16 + | (pci::pci_read_config_byte(dev.bus, dev.device, dev.function, dword_offset + 3) as u32) << 24; + dword +} + +/// Convert PCI device ID to VirtIO device type ID. +/// +/// Modern VirtIO devices: PCI device ID = 0x1040 + VirtIO device type +/// Transitional (legacy) devices: PCI device ID 0x1000-0x103F map to specific types +fn pci_device_id_to_virtio(dev: &PciDevice) -> u32 { + let pci_id = dev.device_id; + + // Modern VirtIO 1.0+ devices: device_id = 0x1040 + virtio_type + if pci_id >= 0x1040 && pci_id <= 0x107F { + return (pci_id - 0x1040) as u32; + } + + // Transitional (legacy) devices + match pci_id { + 0x1000 => device_id::NETWORK, + 0x1001 => device_id::BLOCK, + 0x1003 => device_id::CONSOLE, + 0x1019 => device_id::SOUND, + _ => 0, // Unknown + } +} + +/// Enumerate all VirtIO PCI devices found during PCI bus scan. +/// +/// Returns a vector of initialized `VirtioPciDevice` wrappers for each +/// VirtIO device that supports the modern PCI transport. +pub fn enumerate_virtio_pci_devices() -> alloc::vec::Vec { + let mut devices = alloc::vec::Vec::new(); + + if let Some(pci_devices) = pci::get_devices() { + for pci_dev in pci_devices { + if pci_dev.vendor_id == pci::VIRTIO_VENDOR_ID { + if let Some(virtio_dev) = VirtioPciDevice::probe(pci_dev) { + devices.push(virtio_dev); + } + } + } + } + + devices +} + +/// Get a human-readable name for a VirtIO device type. +pub fn device_type_name(device_id: u32) -> &'static str { + match device_id { + device_id::NETWORK => "network", + device_id::BLOCK => "block", + device_id::CONSOLE => "console", + device_id::GPU => "GPU", + device_id::INPUT => "input", + device_id::SOUND => "sound", + _ => "unknown", + } +} diff --git a/kernel/src/fs/ext2/block_group.rs b/kernel/src/fs/ext2/block_group.rs index 96d6e816..5bc923b0 100644 --- a/kernel/src/fs/ext2/block_group.rs +++ b/kernel/src/fs/ext2/block_group.rs @@ -40,7 +40,7 @@ impl Ext2BlockGroupDesc { /// # Returns /// * `Ok(Vec)` - Successfully read all block group descriptors /// * `Err(BlockError)` - I/O error during read - pub fn read_table( + pub fn read_table( device: &B, superblock: &Ext2Superblock, ) -> Result, BlockError> { @@ -115,7 +115,7 @@ impl Ext2BlockGroupDesc { /// # Returns /// * `Ok(())` - Successfully wrote all block group descriptors /// * `Err(BlockError)` - I/O error during write - pub fn write_table( + pub fn write_table( device: &B, superblock: &Ext2Superblock, descriptors: &[Self], @@ -185,7 +185,7 @@ impl Ext2BlockGroupDesc { /// # Returns /// * `Ok(block_num)` - The allocated block number /// * `Err(msg)` - Error if no free blocks available or I/O error -pub fn allocate_block( +pub fn allocate_block( device: &B, superblock: &Ext2Superblock, block_groups: &mut [Ext2BlockGroupDesc], @@ -275,7 +275,7 @@ pub fn allocate_block( /// # Returns /// * `Ok(())` - Block was successfully freed /// * `Err(msg)` - Error message if operation failed -pub fn free_block( +pub fn free_block( device: &B, block_num: u32, superblock: &Ext2Superblock, diff --git a/kernel/src/fs/ext2/file.rs b/kernel/src/fs/ext2/file.rs index 0540f8dd..7dba0136 100644 --- a/kernel/src/fs/ext2/file.rs +++ b/kernel/src/fs/ext2/file.rs @@ -18,7 +18,7 @@ use alloc::vec::Vec; /// * `ext2_block_num` - The ext2 block number to read /// * `ext2_block_size` - Size of an ext2 block in bytes (e.g., 1024) /// * `buf` - Buffer to read into (must be at least ext2_block_size bytes) -pub fn read_ext2_block( +pub fn read_ext2_block( device: &B, ext2_block_num: u32, ext2_block_size: usize, @@ -49,7 +49,7 @@ pub fn read_ext2_block( /// * `ext2_block_num` - The ext2 block number to write /// * `ext2_block_size` - Size of an ext2 block in bytes (e.g., 1024) /// * `buf` - Buffer to write from (must be at least ext2_block_size bytes) -pub fn write_ext2_block( +pub fn write_ext2_block( device: &B, ext2_block_num: u32, ext2_block_size: usize, @@ -96,7 +96,7 @@ const TRIPLE_INDIRECT: usize = 14; /// * `Ok(Some(block_num))` - Physical block number on disk /// * `Ok(None)` - Sparse hole (block pointer is 0) /// * `Err(BlockError)` - I/O error or out of bounds -pub fn get_block_num( +pub fn get_block_num( device: &B, inode: &Ext2Inode, superblock: &Ext2Superblock, @@ -198,7 +198,7 @@ pub fn get_block_num( /// # Returns /// * `Ok(Vec)` - File contents /// * `Err(BlockError)` - I/O error -pub fn read_file( +pub fn read_file( device: &B, inode: &Ext2Inode, superblock: &Ext2Superblock, @@ -223,7 +223,7 @@ pub fn read_file( /// # Returns /// * `Ok(Vec)` - File contents (may be shorter than length if EOF reached) /// * `Err(BlockError)` - I/O error -pub fn read_file_range( +pub fn read_file_range( device: &B, inode: &Ext2Inode, superblock: &Ext2Superblock, @@ -294,7 +294,7 @@ pub fn read_file_range( /// # Returns /// * `Ok(Vec)` - Array of block pointers /// * `Err(BlockError)` - I/O error -fn read_indirect_block( +fn read_indirect_block( device: &B, block_num: u32, block_size: usize, @@ -337,7 +337,7 @@ fn read_indirect_block( /// # Returns /// * `Ok(())` - Block pointer set successfully /// * `Err(BlockError)` - I/O error or allocation failure -pub fn set_block_num( +pub fn set_block_num( device: &B, inode: &mut Ext2Inode, superblock: &Ext2Superblock, @@ -507,7 +507,7 @@ pub fn set_block_num( /// # Returns /// * `Ok(())` - Write successful /// * `Err(BlockError)` - I/O error -pub fn write_file( +pub fn write_file( device: &B, inode: &mut Ext2Inode, superblock: &Ext2Superblock, @@ -530,7 +530,7 @@ pub fn write_file( /// # Returns /// * `Ok(())` - Write successful /// * `Err(BlockError)` - I/O error -pub fn write_file_range( +pub fn write_file_range( device: &B, inode: &mut Ext2Inode, superblock: &Ext2Superblock, @@ -648,7 +648,7 @@ pub fn write_file_range( /// # Returns /// * `Ok(())` - Write successful /// * `Err(BlockError)` - I/O error -fn write_indirect_block( +fn write_indirect_block( device: &B, block_num: u32, block_size: usize, diff --git a/kernel/src/fs/ext2/inode.rs b/kernel/src/fs/ext2/inode.rs index 319b522b..5ccb251e 100644 --- a/kernel/src/fs/ext2/inode.rs +++ b/kernel/src/fs/ext2/inode.rs @@ -116,7 +116,7 @@ impl Ext2Inode { /// /// # Returns /// The inode structure, or a BlockError if reading fails - pub fn read_from( + pub fn read_from( device: &B, inode_num: u32, superblock: &super::Ext2Superblock, @@ -309,7 +309,7 @@ pub const EXT2_FIRST_INO: u32 = 11; /// # Returns /// * `Ok(new_link_count)` - The new link count after decrement /// * `Err(msg)` - Error message -pub fn decrement_inode_links( +pub fn decrement_inode_links( device: &B, inode_num: u32, superblock: &super::Ext2Superblock, @@ -364,7 +364,7 @@ pub fn decrement_inode_links( /// # Returns /// * `Ok(new_link_count)` - The new link count after increment /// * `Err(msg)` - Error message -pub fn increment_inode_links( +pub fn increment_inode_links( device: &B, inode_num: u32, superblock: &super::Ext2Superblock, @@ -393,7 +393,7 @@ pub fn increment_inode_links( /// /// Marks the inode as free in the inode bitmap and updates the /// free inode count in the block group descriptor. -fn free_inode_bitmap( +fn free_inode_bitmap( device: &B, inode_num: u32, superblock: &super::Ext2Superblock, @@ -458,7 +458,7 @@ fn free_inode_bitmap( /// # Returns /// * `Ok(blocks_freed)` - Number of blocks freed /// * `Err(msg)` - Error message if operation failed -fn free_inode_blocks( +fn free_inode_blocks( device: &B, superblock: &super::Ext2Superblock, block_groups: &mut [super::Ext2BlockGroupDesc], @@ -511,7 +511,7 @@ fn free_inode_blocks( } /// Free all data blocks referenced by a single indirect block -fn free_indirect_block( +fn free_indirect_block( device: &B, superblock: &super::Ext2Superblock, block_groups: &mut [super::Ext2BlockGroupDesc], @@ -547,7 +547,7 @@ fn free_indirect_block( } /// Free all data blocks referenced by a double indirect block -fn free_double_indirect_block( +fn free_double_indirect_block( device: &B, superblock: &super::Ext2Superblock, block_groups: &mut [super::Ext2BlockGroupDesc], @@ -586,7 +586,7 @@ fn free_double_indirect_block( } /// Free all data blocks referenced by a triple indirect block -fn free_triple_indirect_block( +fn free_triple_indirect_block( device: &B, superblock: &super::Ext2Superblock, block_groups: &mut [super::Ext2BlockGroupDesc], @@ -634,7 +634,7 @@ impl Ext2Inode { /// * `inode_num` - The inode number (1-indexed) /// * `superblock` - The ext2 superblock /// * `block_groups` - Array of block group descriptors - pub fn write_to( + pub fn write_to( &self, device: &B, inode_num: u32, @@ -851,7 +851,7 @@ impl Ext2Inode { /// # Returns /// * `Ok(inode_num)` - The allocated inode number (1-indexed) /// * `Err(msg)` - Error if no free inodes available or I/O error -pub fn allocate_inode( +pub fn allocate_inode( device: &B, superblock: &super::Ext2Superblock, block_groups: &mut [super::Ext2BlockGroupDesc], diff --git a/kernel/src/fs/ext2/mod.rs b/kernel/src/fs/ext2/mod.rs index 37896e66..1b7c06ea 100644 --- a/kernel/src/fs/ext2/mod.rs +++ b/kernel/src/fs/ext2/mod.rs @@ -15,7 +15,7 @@ pub use dir::*; pub use inode::*; pub use file::*; -use crate::block::virtio::VirtioBlockWrapper; +use crate::block::BlockDevice; use alloc::sync::Arc; use alloc::vec::Vec; use spin::RwLock; @@ -30,7 +30,7 @@ pub struct Ext2Fs { /// Block group descriptors pub block_groups: Vec, /// The underlying block device - pub device: Arc, + pub device: Arc, /// Mount ID for VFS integration pub mount_id: usize, } @@ -39,7 +39,7 @@ impl Ext2Fs { /// Create a new ext2 filesystem instance from a block device /// /// Reads and validates the superblock and block group descriptors. - pub fn new(device: Arc, mount_id: usize) -> Result { + pub fn new(device: Arc, mount_id: usize) -> Result { // Read the superblock let superblock = Ext2Superblock::read_from(device.as_ref()) .map_err(|_| "Failed to read ext2 superblock")?; @@ -1368,16 +1368,58 @@ static ROOT_EXT2: RwLock> = RwLock::new(None); /// Mounts the ext2 disk as the root filesystem. /// Device layout: /// - x86_64: Device 0 UEFI boot disk, device 1 test binaries disk, device 2 ext2 disk -/// - ARM64: Device 0 ext2 disk +/// - ARM64 (QEMU): Device 0 ext2 disk (VirtIO MMIO) +/// - ARM64 (Parallels): AHCI SATA port 0 /// -/// This should be called during kernel initialization after VirtIO -/// block device initialization. +/// This should be called during kernel initialization after block +/// device driver initialization. pub fn init_root_fs() -> Result<(), &'static str> { - // Try x86_64 layout first (device index 2), then ARM64 layout (device index 0). - let device = VirtioBlockWrapper::new(2) - .or_else(|| VirtioBlockWrapper::new(0)) - .ok_or("No ext2 block device available (expected at device index 2 or 0)")?; - let device = Arc::new(device); + // Try VirtIO block devices first (works on both x86_64 and QEMU ARM64) + let device: Arc = { + use crate::block::virtio::VirtioBlockWrapper; + if let Some(dev) = VirtioBlockWrapper::new(2).or_else(|| VirtioBlockWrapper::new(0)) { + #[cfg(target_arch = "aarch64")] + crate::serial_println!("[ext2] Using VirtIO block device ({} sectors)", dev.num_blocks()); + Arc::new(dev) + } else { + // Fall back to AHCI block devices (Parallels ARM64). + // Try each SATA device looking for one with a valid ext2 superblock. + // On Parallels, sata:0 is typically the FAT32 EFI boot disk. + #[cfg(target_arch = "aarch64")] + { + crate::serial_println!("[ext2] No VirtIO block device, trying AHCI..."); + let count = crate::drivers::ahci::sata_device_count(); + let mut found: Option = None; + for i in 0..count { + if let Some(dev) = crate::drivers::ahci::get_block_device_by_index(i) { + crate::serial_println!("[ext2] AHCI device {}: {} sectors ({} MB)", + i, dev.num_blocks(), dev.num_blocks() * 512 / (1024 * 1024)); + // Try reading ext2 superblock (at byte offset 1024, sector 2) + let mut buf = [0u8; 512]; + if dev.read_block(2, &mut buf).is_ok() { + let magic = (buf[56] as u16) | ((buf[57] as u16) << 8); + if magic == 0xEF53 { + crate::serial_println!("[ext2] Found ext2 superblock on AHCI device {}", i); + found = Some(dev); + break; + } else { + crate::serial_println!("[ext2] AHCI device {}: not ext2 (magic={:#06x})", i, magic); + } + } else { + crate::serial_println!("[ext2] AHCI device {}: read failed", i); + } + } + } + let ahci_dev = found + .ok_or("No block device with ext2 filesystem (tried VirtIO and all AHCI devices)")?; + Arc::new(ahci_dev) + } + #[cfg(not(target_arch = "aarch64"))] + { + return Err("No ext2 block device available (expected at device index 2 or 0)"); + } + } + }; // Register with VFS mount system let mount_id = crate::fs::vfs::mount("/", "ext2"); @@ -1451,10 +1493,13 @@ static HOME_EXT2: RwLock> = RwLock::new(None); /// to the root ext2 filesystem (backward compatible). pub fn init_home_fs() -> Result<(), &'static str> { // Try x86_64 layout first (device index 3), then ARM64 layout (device index 1). - let device = VirtioBlockWrapper::new(3) - .or_else(|| VirtioBlockWrapper::new(1)) - .ok_or("No home block device available (expected at device index 3 or 1)")?; - let device = Arc::new(device); + use crate::block::virtio::VirtioBlockWrapper; + let device: Arc = { + let dev = VirtioBlockWrapper::new(3) + .or_else(|| VirtioBlockWrapper::new(1)) + .ok_or("No home block device available (expected at device index 3 or 1)")?; + Arc::new(dev) + }; // Register with VFS mount system let mount_id = crate::fs::vfs::mount("/home", "ext2"); diff --git a/kernel/src/fs/ext2/superblock.rs b/kernel/src/fs/ext2/superblock.rs index ef5077da..3c0deb6c 100644 --- a/kernel/src/fs/ext2/superblock.rs +++ b/kernel/src/fs/ext2/superblock.rs @@ -66,7 +66,7 @@ impl Ext2Superblock { /// # Returns /// * `Ok(Ext2Superblock)` - Successfully read and parsed superblock /// * `Err(BlockError)` - I/O error or invalid superblock - pub fn read_from(device: &B) -> Result { + pub fn read_from(device: &B) -> Result { // The superblock is always at byte offset 1024, which may span multiple // device blocks depending on the device's native block size let device_block_size = device.block_size(); @@ -183,7 +183,7 @@ impl Ext2Superblock { /// # Returns /// * `Ok(())` - Successfully wrote superblock /// * `Err(BlockError)` - I/O error during write - pub fn write_to(&self, device: &B) -> Result<(), BlockError> { + pub fn write_to(&self, device: &B) -> Result<(), BlockError> { let device_block_size = device.block_size(); let superblock_size = mem::size_of::(); diff --git a/kernel/src/graphics/arm64_fb.rs b/kernel/src/graphics/arm64_fb.rs index d0f92330..8e13831e 100644 --- a/kernel/src/graphics/arm64_fb.rs +++ b/kernel/src/graphics/arm64_fb.rs @@ -1,7 +1,8 @@ -//! ARM64 Framebuffer implementation using VirtIO GPU +//! ARM64 Framebuffer implementation (VirtIO GPU + UEFI GOP backends) //! -//! Provides a Canvas implementation for the VirtIO GPU framebuffer, -//! enabling the graphics primitives to work on ARM64. +//! Provides a Canvas implementation for the ARM64 framebuffer with two backends: +//! - **VirtIO GPU**: Used on QEMU with virtio-gpu-device (original backend) +//! - **UEFI GOP**: Linear framebuffer in physical memory (used on Parallels) //! //! This module also provides a SHELL_FRAMEBUFFER interface compatible with //! the x86_64 version in logger.rs, allowing split_screen.rs @@ -12,7 +13,7 @@ use super::primitives::{Canvas, Color}; use crate::drivers::virtio::gpu_mmio; use conquer_once::spin::OnceCell; -use core::sync::atomic::{AtomicBool, AtomicU32, Ordering}; +use core::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering}; use spin::Mutex; // ============================================================================= @@ -53,7 +54,15 @@ pub fn mark_dirty(x: u32, y: u32, w: u32, h: u32) { /// Mark the entire framebuffer as dirty. pub fn mark_full_dirty() { - if let Some((w, h)) = gpu_mmio::dimensions() { + // Try FB_INFO_CACHE first (works for both VirtIO and GOP) + if let Some(cache) = FB_INFO_CACHE.get() { + mark_dirty(0, 0, cache.width as u32, cache.height as u32); + return; + } + // Fall back to VirtIO GPU PCI, then MMIO + if let Some((w, h)) = crate::drivers::virtio::gpu_pci::dimensions() { + mark_dirty(0, 0, w, h); + } else if let Some((w, h)) = gpu_mmio::dimensions() { mark_dirty(0, 0, w, h); } } @@ -91,10 +100,15 @@ pub fn take_dirty_rect() -> Option<(u32, u32, u32, u32)> { } // Clamp to display dimensions — cursor mark_dirty near screen edges can - // produce rects that extend beyond the display (e.g., cursor at x=1270 - // marks dirty (1254, y, 32, 32) → x_max = 1286 > 1280). VirtIO GPU - // rejects transfer_to_host with out-of-bounds coordinates. - let (x_min, y_min, x_max, y_max) = if let Some((dw, dh)) = gpu_mmio::dimensions() { + // produce rects that extend beyond the display. Both VirtIO GPU and GOP + // need clamped coordinates. + let (x_min, y_min, x_max, y_max) = if let Some(cache) = FB_INFO_CACHE.get() { + let dw = cache.width as u32; + let dh = cache.height as u32; + (x_min.min(dw), y_min.min(dh), x_max.min(dw), y_max.min(dh)) + } else if let Some((dw, dh)) = crate::drivers::virtio::gpu_pci::dimensions() { + (x_min.min(dw), y_min.min(dh), x_max.min(dw), y_max.min(dh)) + } else if let Some((dw, dh)) = gpu_mmio::dimensions() { (x_min.min(dw), y_min.min(dh), x_max.min(dw), y_max.min(dh)) } else { (x_min, y_min, x_max, y_max) @@ -107,6 +121,23 @@ pub fn take_dirty_rect() -> Option<(u32, u32, u32, u32)> { Some((x_min, y_min, x_max - x_min, y_max - y_min)) } +/// Flush a dirty rectangle to the display. +/// +/// This is called by the render thread without holding the SHELL_FRAMEBUFFER lock. +/// For GOP, this is a no-op data barrier (writes are already in display memory). +/// For VirtIO GPU, this issues transfer_to_host + resource_flush commands. +pub fn flush_dirty_rect(x: u32, y: u32, w: u32, h: u32) -> Result<(), &'static str> { + if is_gop_active() { + // GOP: writes go directly to display memory. DSB ensures visibility. + unsafe { core::arch::asm!("dsb sy", options(nostack, preserves_flags)); } + Ok(()) + } else if crate::drivers::virtio::gpu_pci::is_initialized() { + crate::drivers::virtio::gpu_pci::flush_rect(x, y, w, h) + } else { + gpu_mmio::flush_rect(x, y, w, h) + } +} + /// Atomic fetch_min for u32 (CAS loop). #[inline] fn fetch_min_u32(atom: &AtomicU32, val: u32) { @@ -131,6 +162,178 @@ fn fetch_max_u32(atom: &AtomicU32, val: u32) { } } +// ============================================================================= +// GOP Framebuffer Backend (UEFI linear buffer, used on Parallels) +// ============================================================================= + +/// GOP framebuffer virtual address (HHDM-mapped) +static GOP_FB_PTR: AtomicU64 = AtomicU64::new(0); +/// GOP framebuffer size in bytes +static GOP_FB_LEN: AtomicU64 = AtomicU64::new(0); +/// Whether GOP backend is active +static GOP_ACTIVE: AtomicBool = AtomicBool::new(false); + +/// Check if GOP framebuffer is active (vs VirtIO GPU). +pub fn is_gop_active() -> bool { + GOP_ACTIVE.load(Ordering::Relaxed) +} + +/// Get the GOP framebuffer as a mutable byte slice. +/// Returns None if GOP is not initialized. +fn gop_framebuffer() -> Option<&'static mut [u8]> { + let ptr = GOP_FB_PTR.load(Ordering::Relaxed); + let len = GOP_FB_LEN.load(Ordering::Relaxed); + if ptr == 0 || len == 0 { + return None; + } + unsafe { Some(core::slice::from_raw_parts_mut(ptr as *mut u8, len as usize)) } +} + +/// Get the GOP framebuffer as an immutable byte slice. +fn gop_framebuffer_ref() -> Option<&'static [u8]> { + let ptr = GOP_FB_PTR.load(Ordering::Relaxed); + let len = GOP_FB_LEN.load(Ordering::Relaxed); + if ptr == 0 || len == 0 { + return None; + } + unsafe { Some(core::slice::from_raw_parts(ptr as *const u8, len as usize)) } +} + +/// Initialize the GOP framebuffer from platform_config data. +/// +/// Maps the framebuffer physical address via HHDM and sets up the +/// SHELL_FRAMEBUFFER with GOP dimensions. Call this instead of +/// init_shell_framebuffer() when a GOP framebuffer is available. +pub fn init_gop_framebuffer() -> Result<(), &'static str> { + let base = crate::platform_config::fb_base_phys(); + let size = crate::platform_config::fb_size(); + let width = crate::platform_config::fb_width(); + let height = crate::platform_config::fb_height(); + let stride = crate::platform_config::fb_stride(); + let is_bgr = crate::platform_config::fb_is_bgr(); + + if base == 0 || size == 0 { + return Err("No GOP framebuffer info in platform config"); + } + + // Map via HHDM (higher-half direct map) + let hhdm_base = crate::arch_impl::aarch64::constants::HHDM_BASE; + let virt_ptr = hhdm_base + base; + + GOP_FB_PTR.store(virt_ptr, Ordering::Relaxed); + GOP_FB_LEN.store(size, Ordering::Relaxed); + GOP_ACTIVE.store(true, Ordering::Relaxed); + + crate::serial_println!( + "[arm64-fb] GOP framebuffer: {}x{} stride={} {} base_phys={:#x} virt={:#x} size={:#x}", + width, height, stride, + if is_bgr { "BGR" } else { "RGB" }, + base, virt_ptr, size + ); + + // Create Arm64FrameBuffer with GOP parameters + let fb = Arm64FrameBuffer { + width: width as usize, + height: height as usize, + bytes_per_pixel: 4, + stride: stride as usize, + is_gop: true, + is_bgr_flag: is_bgr, + }; + + // Initialize SHELL_FRAMEBUFFER + let shell_fb = ShellFrameBuffer { fb }; + + // Cache immutable dimensions for lock-free access by sys_fbinfo + let _ = FB_INFO_CACHE.try_init_once(|| FbInfoCache { + width: width as usize, + height: height as usize, + stride: stride as usize, + bytes_per_pixel: 4, + is_bgr, + }); + + let _ = SHELL_FRAMEBUFFER.try_init_once(|| Mutex::new(shell_fb)); + + crate::serial_println!("[arm64-fb] GOP shell framebuffer initialized: {}x{}", width, height); + Ok(()) +} + +/// Initialize framebuffer using VirtIO GPU PCI resolution but GOP/BAR0 memory. +/// +/// On Parallels, VirtIO GPU `set_scanout` controls the display mode (resolution, +/// stride) but actual pixels are read from BAR0 (the GOP framebuffer at 0x10000000). +/// This function sets up a GOP-style framebuffer at the VirtIO GPU's configured +/// resolution, giving us higher resolution than the GOP-reported 1024x768. +/// +/// Must be called AFTER `drivers::init()` (which initializes GPU PCI). +pub fn init_gpu_pci_gop_framebuffer() -> Result<(), &'static str> { + if !crate::drivers::virtio::gpu_pci::is_initialized() { + return Err("GPU PCI not initialized"); + } + + let (width, height) = crate::drivers::virtio::gpu_pci::dimensions() + .ok_or("GPU PCI has no dimensions")?; + let width = width as usize; + let height = height as usize; + let stride = width; // VirtIO GPU stride = width (no padding) + let bytes_per_pixel = 4usize; + + // GOP framebuffer base: the BAR0/GOP address that the display reads from + let base = crate::platform_config::fb_base_phys(); + if base == 0 { + return Err("No GOP framebuffer base address"); + } + + // Ensure the GOP memory is large enough for the new resolution. + // BAR0 is typically 64MB; we need width * height * 4 bytes. + let needed = width * height * bytes_per_pixel; + let gop_size = crate::platform_config::fb_size() as usize; + crate::serial_println!( + "[arm64-fb] GPU PCI+GOP hybrid: {}x{} stride={} need={} bytes, GOP region={} bytes", + width, height, stride, needed, gop_size + ); + // GOP size from UEFI may report only 1024x768 worth; the actual BAR is larger. + // Proceed even if needed > gop_size — the BAR0 region extends well beyond. + + // Map via HHDM + let hhdm_base = crate::arch_impl::aarch64::constants::HHDM_BASE; + let virt_ptr = hhdm_base + base; + + // Update GOP globals with the new (larger) dimensions + GOP_FB_PTR.store(virt_ptr, Ordering::Relaxed); + GOP_FB_LEN.store(needed as u64, Ordering::Relaxed); + GOP_ACTIVE.store(true, Ordering::Relaxed); + + // Create framebuffer with GPU PCI dimensions but GOP backend + let fb = Arm64FrameBuffer { + width, + height, + bytes_per_pixel, + stride, + is_gop: true, // Writes go to GOP memory, flush uses DSB + is_bgr_flag: true, // B8G8R8A8_UNORM + }; + + let shell_fb = ShellFrameBuffer { fb }; + + let _ = FB_INFO_CACHE.try_init_once(|| FbInfoCache { + width, + height, + stride, + bytes_per_pixel, + is_bgr: true, + }); + + let _ = SHELL_FRAMEBUFFER.try_init_once(|| Mutex::new(shell_fb)); + + crate::serial_println!( + "[arm64-fb] GPU PCI+GOP hybrid framebuffer: {}x{} base_phys={:#x}", + width, height, base + ); + Ok(()) +} + /// ARM64 framebuffer wrapper that implements Canvas trait pub struct Arm64FrameBuffer { /// Display width in pixels @@ -141,31 +344,55 @@ pub struct Arm64FrameBuffer { bytes_per_pixel: usize, /// Stride in pixels (same as width for VirtIO GPU) stride: usize, + /// Whether this framebuffer uses the GOP backend (vs VirtIO GPU) + is_gop: bool, + /// Whether pixel format is BGR (true) or RGB (false) + is_bgr_flag: bool, } impl Arm64FrameBuffer { /// Create a new ARM64 framebuffer wrapper /// - /// Returns None if the VirtIO GPU is not initialized + /// Tries GPU PCI first, then falls back to GPU MMIO. + /// Returns None if no VirtIO GPU is initialized. pub fn new() -> Option { - let (width, height) = gpu_mmio::dimensions()?; + // Try GPU PCI first (Parallels), then GPU MMIO (QEMU) + let (width, height) = crate::drivers::virtio::gpu_pci::dimensions() + .or_else(|| gpu_mmio::dimensions())?; Some(Self { width: width as usize, height: height as usize, bytes_per_pixel: 4, // BGRA format stride: width as usize, + is_gop: false, + is_bgr_flag: true, // VirtIO GPU uses B8G8R8A8_UNORM }) } /// Flush the framebuffer to the display pub fn flush(&self) -> Result<(), &'static str> { - gpu_mmio::flush() + if self.is_gop { + // GOP: writes go directly to display memory. DSB ensures visibility. + unsafe { core::arch::asm!("dsb sy", options(nostack, preserves_flags)); } + Ok(()) + } else if crate::drivers::virtio::gpu_pci::is_initialized() { + crate::drivers::virtio::gpu_pci::flush() + } else { + gpu_mmio::flush() + } } /// Flush a rectangular region of the framebuffer to the display pub fn flush_rect(&self, x: u32, y: u32, w: u32, h: u32) -> Result<(), &'static str> { - gpu_mmio::flush_rect(x, y, w, h) + if self.is_gop { + unsafe { core::arch::asm!("dsb sy", options(nostack, preserves_flags)); } + Ok(()) + } else if crate::drivers::virtio::gpu_pci::is_initialized() { + crate::drivers::virtio::gpu_pci::flush_rect(x, y, w, h) + } else { + gpu_mmio::flush_rect(x, y, w, h) + } } } @@ -187,7 +414,7 @@ impl Canvas for Arm64FrameBuffer { } fn is_bgr(&self) -> bool { - true // VirtIO GPU uses B8G8R8A8_UNORM format + self.is_bgr_flag } fn set_pixel(&mut self, x: i32, y: i32, color: Color) { @@ -200,14 +427,19 @@ impl Canvas for Arm64FrameBuffer { return; } - if let Some(buffer) = gpu_mmio::framebuffer() { - let pixel_bytes = color.to_pixel_bytes(self.bytes_per_pixel, true); - let offset = (y * self.stride + x) * self.bytes_per_pixel; + let buffer = if self.is_gop { + match gop_framebuffer() { Some(b) => b, None => return } + } else if crate::drivers::virtio::gpu_pci::is_initialized() { + match crate::drivers::virtio::gpu_pci::framebuffer() { Some(b) => b, None => return } + } else { + match gpu_mmio::framebuffer() { Some(b) => b, None => return } + }; - if offset + self.bytes_per_pixel <= buffer.len() { - buffer[offset..offset + self.bytes_per_pixel] - .copy_from_slice(&pixel_bytes[..self.bytes_per_pixel]); - } + let pixel_bytes = color.to_pixel_bytes(self.bytes_per_pixel, self.is_bgr_flag); + let offset = (y * self.stride + x) * self.bytes_per_pixel; + if offset + self.bytes_per_pixel <= buffer.len() { + buffer[offset..offset + self.bytes_per_pixel] + .copy_from_slice(&pixel_bytes[..self.bytes_per_pixel]); } } @@ -221,27 +453,43 @@ impl Canvas for Arm64FrameBuffer { return None; } - let buffer = gpu_mmio::framebuffer()?; - let offset = (y * self.stride + x) * self.bytes_per_pixel; + let buffer: &[u8] = if self.is_gop { + gop_framebuffer_ref()? + } else if crate::drivers::virtio::gpu_pci::is_initialized() { + crate::drivers::virtio::gpu_pci::framebuffer().map(|b| &*b)? + } else { + gpu_mmio::framebuffer().map(|b| &*b)? + }; + let offset = (y * self.stride + x) * self.bytes_per_pixel; if offset + self.bytes_per_pixel > buffer.len() { return None; } - Some(Color::from_pixel_bytes( &buffer[offset..offset + self.bytes_per_pixel], self.bytes_per_pixel, - true, + self.is_bgr_flag, )) } fn buffer_mut(&mut self) -> &mut [u8] { - gpu_mmio::framebuffer().unwrap_or(&mut []) + if self.is_gop { + gop_framebuffer().unwrap_or(&mut []) + } else if crate::drivers::virtio::gpu_pci::is_initialized() { + crate::drivers::virtio::gpu_pci::framebuffer().unwrap_or(&mut []) + } else { + gpu_mmio::framebuffer().unwrap_or(&mut []) + } } fn buffer(&self) -> &[u8] { - // Safe because we're only reading - gpu_mmio::framebuffer().map(|b| &*b).unwrap_or(&[]) + if self.is_gop { + gop_framebuffer_ref().unwrap_or(&[]) + } else if crate::drivers::virtio::gpu_pci::is_initialized() { + crate::drivers::virtio::gpu_pci::framebuffer().map(|b| &*b).unwrap_or(&[]) + } else { + gpu_mmio::framebuffer().map(|b| &*b).unwrap_or(&[]) + } } } diff --git a/kernel/src/graphics/particles.rs b/kernel/src/graphics/particles.rs index 996e5757..30400d41 100644 --- a/kernel/src/graphics/particles.rs +++ b/kernel/src/graphics/particles.rs @@ -30,9 +30,7 @@ pub fn start_animation(left: i32, top: i32, right: i32, bottom: i32) { pub fn animation_thread_entry() { // Raw serial output - no locks, safe in any context fn raw_char(c: u8) { - const HHDM_BASE: u64 = 0xFFFF_0000_0000_0000; - const PL011_BASE: u64 = 0x0900_0000; - let addr = (HHDM_BASE + PL011_BASE) as *mut u32; + let addr = crate::platform_config::uart_virt() as *mut u32; unsafe { core::ptr::write_volatile(addr, c as u32); } } diff --git a/kernel/src/graphics/render_task.rs b/kernel/src/graphics/render_task.rs index ea2ea768..e0981ece 100644 --- a/kernel/src/graphics/render_task.rs +++ b/kernel/src/graphics/render_task.rs @@ -222,7 +222,7 @@ fn flush_framebuffer() -> bool { // two-lock nesting (SHELL_FRAMEBUFFER + GPU_LOCK) that caused deadlocks // when sys_fbdraw held SHELL_FRAMEBUFFER with IRQs disabled. if let Some((x, y, w, h)) = crate::graphics::arm64_fb::take_dirty_rect() { - if let Err(e) = crate::drivers::virtio::gpu_mmio::flush_rect(x, y, w, h) { + if let Err(e) = crate::graphics::arm64_fb::flush_dirty_rect(x, y, w, h) { crate::serial_println!("[render] GPU flush failed: {}", e); } true diff --git a/kernel/src/lib.rs b/kernel/src/lib.rs index 981ea03f..d2826266 100644 --- a/kernel/src/lib.rs +++ b/kernel/src/lib.rs @@ -59,6 +59,8 @@ pub mod net; pub mod block; pub mod fs; pub mod logger; +#[cfg(target_arch = "aarch64")] +pub mod platform_config; #[cfg(target_arch = "x86_64")] pub mod framebuffer; // Graphics module: available on x86_64 with "interactive" feature, or always on ARM64 diff --git a/kernel/src/main_aarch64.rs b/kernel/src/main_aarch64.rs index e7e9f981..969ae12a 100644 --- a/kernel/src/main_aarch64.rs +++ b/kernel/src/main_aarch64.rs @@ -34,70 +34,36 @@ fn run_userspace_from_ext2(path: &str) -> Result Result Result Result Result ! { +pub extern "C" fn kernel_main(hw_config_ptr: u64) -> ! { + // If the UEFI loader passed a HardwareConfig, use it to configure platform + // addresses before any hardware access. On QEMU (boot.S), x0 is 0. + if hw_config_ptr != 0 { + let config = unsafe { &*(hw_config_ptr as *const kernel::platform_config::HardwareConfig) }; + kernel::platform_config::init_from_parallels(config); + + // CRITICAL: Switch from identity-mapped physical addresses to HHDM. + // + // On Parallels, the UEFI loader jumps to kernel_main at a physical address + // (e.g., 0x400xxxxx). The CPU is executing through TTBR0 (identity map). + // TTBR1 also maps the same physical memory at HHDM addresses (0xFFFF_0000_...). + // + // Problem: if we continue at physical addresses, all ADRP-computed addresses + // (function pointers, statics, exception vectors) resolve to physical addresses. + // After TTBR0 is switched to a process page table, these addresses become + // inaccessible — timer IRQ vectors fault, kernel threads can't resume, etc. + // + // Solution: switch SP and PC to HHDM addresses now. After this, all code runs + // through TTBR1, which is never modified. This mirrors what boot.S does on QEMU + // (line 143-147: adds KERNEL_VIRT_BASE to SP and branches to high-half code). + unsafe { + core::arch::asm!( + // Add HHDM offset to SP (switch to HHDM stack) + "mov x8, #0xFFFF", + "lsl x8, x8, #48", // x8 = 0xFFFF_0000_0000_0000 + "add sp, sp, x8", // SP now in HHDM + + // Compute HHDM address of continuation label and branch there. + // ADR gives the physical address of the label (PC-relative). + // Adding x8 gives the HHDM address. + "adr x9, 1f", // x9 = physical addr of label '1' + "add x9, x9, x8", // x9 = HHDM addr of label '1' + "br x9", // Branch to HHDM + "1:", + // Now executing at HHDM address through TTBR1. + // All subsequent ADRP instructions will compute HHDM-relative addresses. + out("x8") _, + out("x9") _, + options(nostack), + ); + } + } + + // Install the kernel's exception vector table (VBAR_EL1). + // On QEMU, boot.S already did this before jumping to kernel_main. + // On Parallels, the UEFI loader installed minimal "write X and spin" vectors. + // We must install the real kernel vectors before any interrupt fires. + // NOTE: After the HHDM switch above, exception_vectors resolves to an HHDM address. + unsafe { + extern "C" { + fn exception_vectors(); + } + let vectors_addr = exception_vectors as usize as u64; + core::arch::asm!( + "msr vbar_el1, {v}", + "isb", + v = in(reg) vectors_addr, + options(nostack, preserves_flags), + ); + } + // Initialize physical memory offset (needed for MMIO access) kernel::memory::init_physical_memory_offset_aarch64(); @@ -264,12 +319,23 @@ pub extern "C" fn kernel_main() -> ! { serial_println!("[boot] MMU already enabled (high-half kernel)"); + // Zero the boot identity map L0 entry to prevent new TLB entries from + // being created for the user VA range while we're still in kernel init. + // This is a defense-in-depth measure; the TLBI in run_userspace_from_ext2 + // will do a full invalidation before switching to the process page table. + // + // We can't use `extern "C" { static mut ttbr0_l0: u64; }` because the + // symbol is in .bss.boot (low physical memory) while the kernel runs in + // the high half -- the ADRP relocation would be out of range (~281 TB). + // Instead, read the current TTBR0_EL1 to get the physical address and + // access it through the HHDM. // Initialize memory management for ARM64 // ARM64 QEMU virt machine: RAM starts at 0x40000000 - // We use 0x42000000..0x50000000 (224MB) for frame allocation - // Kernel stacks are at 0x51000000..0x52000000 (16MB) - serial_println!("[boot] Initializing memory management..."); - kernel::memory::frame_allocator::init_aarch64(0x4200_0000, 0x5000_0000); + // Frame allocator range from platform_config (QEMU defaults or HardwareConfig) + let fa_start = kernel::platform_config::frame_alloc_start(); + let fa_end = kernel::platform_config::frame_alloc_end(); + serial_println!("[boot] Initializing memory management ({:#x}-{:#x})...", fa_start, fa_end); + kernel::memory::frame_allocator::init_aarch64(fa_start, fa_end); kernel::memory::init_aarch64_heap(); kernel::memory::kernel_stack::init(); serial_println!("[boot] Memory management ready"); @@ -329,8 +395,11 @@ pub extern "C" fn kernel_main() -> ! { let irq_enabled = Aarch64Cpu::interrupts_enabled(); serial_println!("[boot] Interrupts enabled: {}", irq_enabled); - // Read display resolution from fw_cfg before driver init - kernel::drivers::virtio::gpu_mmio::load_resolution_from_fw_cfg(); + // Read display resolution from fw_cfg before driver init (QEMU only; + // fw_cfg device at 0x09020000 doesn't exist on Parallels) + if kernel::platform_config::is_qemu() { + kernel::drivers::virtio::gpu_mmio::load_resolution_from_fw_cfg(); + } // Initialize device drivers (VirtIO MMIO enumeration) serial_println!("[boot] Initializing device drivers..."); @@ -400,19 +469,99 @@ pub extern "C" fn kernel_main() -> ! { kernel::tty::init(); serial_println!("[boot] TTY subsystem initialized"); - // Initialize graphics (if GPU is available) + // Initialize graphics based on available hardware (capability-based detection) + // + // Graphics initialization priority: + // 1. VirtIO GPU PCI + GOP hybrid (Parallels): GPU set_scanout configures + // the display's reading stride/resolution (e.g. 1280x800), while pixel + // data is written to GOP memory (0x10000000). VirtIO transfer_to_host + // does NOT update the display — only GOP memory is scanned out. The + // kernel MUST write at the GPU-configured stride, not the GOP stride. + // 2. UEFI GOP framebuffer (fallback if GPU PCI not available) + // 3. VirtIO GPU MMIO (QEMU virt platform) serial_println!("[boot] Initializing graphics..."); - if let Err(e) = init_graphics() { - serial_println!("[boot] Graphics init failed: {} (continuing without graphics)", e); - } + let has_display = if kernel::drivers::virtio::gpu_pci::is_initialized() + && kernel::platform_config::has_framebuffer() + { + // Parallels hybrid: VirtIO GPU PCI set_scanout changes the display's + // reading stride to the GPU-configured width (1280). Pixels are read + // from GOP memory at this new stride. The kernel must write at the + // GPU stride (1280), not the UEFI GOP stride (1024). + match arm64_fb::init_gpu_pci_gop_framebuffer() { + Ok(()) => { + serial_println!("[boot] GPU PCI+GOP hybrid display initialized"); + if let Err(e) = init_gop_display() { + serial_println!("[boot] Display setup failed: {}", e); + } + true + } + Err(e) => { + serial_println!("[boot] GPU PCI+GOP hybrid failed: {}, trying pure GOP", e); + match arm64_fb::init_gop_framebuffer() { + Ok(()) => { + serial_println!("[boot] GOP framebuffer initialized (fallback)"); + if let Err(e) = init_gop_display() { + serial_println!("[boot] GOP display setup failed: {}", e); + } + true + } + Err(e2) => { + serial_println!("[boot] GOP framebuffer also failed: {}", e2); + false + } + } + } + } + } else if kernel::platform_config::has_framebuffer() { + // UEFI GOP framebuffer (fallback when GPU PCI not available) + match arm64_fb::init_gop_framebuffer() { + Ok(()) => { + serial_println!("[boot] GOP framebuffer initialized"); + // Draw initial split-screen layout + if let Err(e) = init_gop_display() { + serial_println!("[boot] GOP display setup failed: {}", e); + } + true + } + Err(e) => { + serial_println!("[boot] GOP framebuffer failed: {}", e); + false + } + } + } else if kernel::platform_config::is_qemu() { + // VirtIO GPU MMIO (QEMU virt platform) + match init_graphics() { + Ok(()) => true, + Err(e) => { + serial_println!("[boot] VirtIO graphics failed: {}", e); + false + } + } + } else { + serial_println!("[boot] No display device found"); + false + }; - // Initialize VirtIO keyboard - serial_println!("[boot] Initializing VirtIO keyboard..."); - match input_mmio::init() { - Ok(()) => serial_println!("[boot] VirtIO keyboard initialized"), - Err(e) => serial_println!("[boot] VirtIO keyboard init failed: {}", e), + // Initialize input devices (capability-based detection) + if kernel::drivers::usb::xhci::is_initialized() { + // USB HID keyboard/mouse via XHCI — already set up during drivers::init() + // Polling happens from timer interrupt (poll_hid_events) since PCI + // interrupt routing may not be available on all platforms (IRQ=255). + serial_println!("[boot] USB HID input active via XHCI (polled from timer)"); + } else if kernel::platform_config::is_qemu() { + // VirtIO keyboard/mouse MMIO (QEMU) + serial_println!("[boot] Initializing VirtIO keyboard..."); + match input_mmio::init() { + Ok(()) => serial_println!("[boot] VirtIO keyboard initialized"), + Err(e) => serial_println!("[boot] VirtIO keyboard init failed: {}", e), + } } + // TLB eviction for TTBR0 identity map is handled in run_userspace_from_ext2() + // right before the TTBR0 switch. We cannot modify TTBR0 page tables earlier + // because Parallels monitors TTBR0/page table changes and hangs if they occur + // while timer interrupts and kthreads are active. + // Initialize per-CPU data (required before scheduler and interrupts) serial_println!("[boot] Initializing per-CPU data..."); kernel::per_cpu_aarch64::init(); @@ -452,9 +601,12 @@ pub extern "C" fn kernel_main() -> ! { // - BWM (userspace window manager) handles terminal rendering // - Boot test progress display (test_framework::display) renders to SHELL_FRAMEBUFFER // - Boot milestones are tracked via BTRT (test_framework::btrt) on both platforms - match kernel::graphics::render_task::spawn_render_thread() { - Ok(tid) => serial_println!("[boot] Render thread spawned (tid={})", tid), - Err(e) => serial_println!("[boot] Failed to spawn render thread: {}", e), + // Spawn render thread if any display is available (GOP or VirtIO) + if has_display { + match kernel::graphics::render_task::spawn_render_thread() { + Ok(tid) => serial_println!("[boot] Render thread spawned (tid={})", tid), + Err(e) => serial_println!("[boot] Failed to spawn render thread: {}", e), + } } // Initialize timer interrupt for preemptive scheduling @@ -466,24 +618,29 @@ pub extern "C" fn kernel_main() -> ! { kernel::test_framework::btrt::pass(kernel::test_framework::catalog::AARCH64_TIMER_INIT); // Bring up secondary CPUs via PSCI CPU_ON - serial_println!("[smp] Starting secondary CPUs..."); - let expected_cpus: u64 = 4; - for cpu in 1..expected_cpus { - kernel::arch_impl::aarch64::smp::release_cpu(cpu as usize); - } - // Wait for all CPUs to come online (with timeout) - let start = timer::rdtsc(); - let timeout_ticks = timer::frequency_hz() / 10; // 100ms timeout - while kernel::arch_impl::aarch64::smp::cpus_online() < expected_cpus { - if timer::rdtsc() - start > timeout_ticks { - break; + // Skip SMP on non-QEMU: secondary CPU entry (boot.S) uses QEMU-specific page tables + if kernel::platform_config::is_qemu() { + serial_println!("[smp] Starting secondary CPUs..."); + let expected_cpus: u64 = 4; + for cpu in 1..expected_cpus { + kernel::arch_impl::aarch64::smp::release_cpu(cpu as usize); } - core::hint::spin_loop(); + // Wait for all CPUs to come online (with timeout) + let start = timer::rdtsc(); + let timeout_ticks = timer::frequency_hz() / 10; // 100ms timeout + while kernel::arch_impl::aarch64::smp::cpus_online() < expected_cpus { + if timer::rdtsc() - start > timeout_ticks { + break; + } + core::hint::spin_loop(); + } + serial_println!( + "[smp] {} CPUs online", + kernel::arch_impl::aarch64::smp::cpus_online() + ); + } else { + serial_println!("[smp] Skipping secondary CPUs (non-QEMU platform, boot.S SMP not adapted)"); } - serial_println!( - "[smp] {} CPUs online", - kernel::arch_impl::aarch64::smp::cpus_online() - ); // Test kthread lifecycle BEFORE creating userspace processes // (must be done early so scheduler doesn't preempt to userspace) @@ -572,14 +729,6 @@ pub extern "C" fn kernel_main() -> ! { serial_println!("Hello from ARM64!"); serial_println!(); - // Raw char helper for debugging - fn boot_raw_char(c: u8) { - const HHDM_BASE: u64 = 0xFFFF_0000_0000_0000; - const PL011_BASE: u64 = 0x0900_0000; - let addr = (HHDM_BASE + PL011_BASE) as *mut u32; - unsafe { core::ptr::write_volatile(addr, c as u32); } - } - // Spawn particle animation thread (if graphics is available and not running boot tests) // This MUST be done BEFORE userspace loading because run_userspace_from_ext2 never returns // DISABLED: Investigating EC=0x0 crash during fill_rect memcpy @@ -618,13 +767,9 @@ pub extern "C" fn kernel_main() -> ! { } } - boot_raw_char(b'1'); // Before if statement - // Try to load and run userspace init_shell from ext2 or test disk if device_count > 0 { - boot_raw_char(b'2'); // Inside if serial_println!("[boot] Loading userspace init from ext2..."); - boot_raw_char(b'3'); // After serial_println match run_userspace_from_ext2("/sbin/init") { Err(e) => { serial_println!("[boot] Failed to load init from ext2: {}", e); @@ -782,13 +927,28 @@ fn init_scheduler() { use kernel::per_cpu_aarch64; use kernel::memory::arch_stub::VirtAddr; - // CPU 0 boot stack top address — must match boot.S layout: - // HHDM_BASE + STACK_REGION_BASE + (cpu_id + 1) * STACK_SIZE + // CPU 0 boot stack top address. + // On QEMU: boot.S sets SP to HHDM_BASE + STACK_REGION_BASE + STACK_SIZE + // On Parallels: UEFI loader sets SP to 0x42000000, then HHDM switch adds HHDM_BASE + // Use platform detection to pick the right boot stack address. const HHDM_BASE: u64 = 0xFFFF_0000_0000_0000; - const STACK_REGION_BASE: u64 = 0x4100_0000; - const STACK_SIZE: u64 = 0x20_0000; // 2MB per CPU - let boot_stack_top = VirtAddr::new(HHDM_BASE + STACK_REGION_BASE + STACK_SIZE); - let boot_stack_bottom = VirtAddr::new(HHDM_BASE + STACK_REGION_BASE); + let (boot_stack_top, boot_stack_bottom) = if kernel::platform_config::is_qemu() { + const STACK_REGION_BASE: u64 = 0x4100_0000; + const STACK_SIZE: u64 = 0x20_0000; // 2MB per CPU + ( + VirtAddr::new(HHDM_BASE + STACK_REGION_BASE + STACK_SIZE), + VirtAddr::new(HHDM_BASE + STACK_REGION_BASE), + ) + } else { + // Parallels: UEFI loader stack at 0x42000000 (phys), now at HHDM + // The stack grows down from 0x42000000, assume 2MB range + const PARALLELS_STACK_TOP_PHYS: u64 = 0x4200_0000; + const PARALLELS_STACK_SIZE: u64 = 0x20_0000; // 2MB + ( + VirtAddr::new(HHDM_BASE + PARALLELS_STACK_TOP_PHYS), + VirtAddr::new(HHDM_BASE + PARALLELS_STACK_TOP_PHYS - PARALLELS_STACK_SIZE), + ) + }; let dummy_tls = VirtAddr::zero(); // Create the idle task (thread ID 0) @@ -1042,8 +1202,12 @@ fn current_exception_level() -> u8 { /// with graphics demo on the left and terminal on the right. #[cfg(target_arch = "aarch64")] fn init_graphics() -> Result<(), &'static str> { - // Initialize VirtIO GPU driver - kernel::drivers::virtio::gpu_mmio::init()?; + // Initialize VirtIO GPU backend. + // GPU PCI is initialized earlier in drivers::init() — only init GPU MMIO + // if PCI is not available (QEMU virt platform). + if !kernel::drivers::virtio::gpu_pci::is_initialized() { + kernel::drivers::virtio::gpu_mmio::init()?; + } arm64_fb::init_shell_framebuffer()?; @@ -1104,6 +1268,67 @@ fn init_graphics() -> Result<(), &'static str> { Ok(()) } +/// Initialize GOP framebuffer display with split-screen terminal UI. +/// +/// Called after init_gop_framebuffer() when a UEFI GOP framebuffer is available. +/// Draws the same layout as VirtIO init_graphics() but without VirtIO-specific init. +#[cfg(target_arch = "aarch64")] +fn init_gop_display() -> Result<(), &'static str> { + // Get framebuffer dimensions + let (width, height) = arm64_fb::dimensions().ok_or("Failed to get framebuffer dimensions")?; + serial_println!("[graphics] GOP Framebuffer: {}x{}", width, height); + + // Calculate layout: 50/50 split with 4-pixel divider + let divider_width = 4usize; + let divider_x = width / 2; + let left_width = divider_x; + + // Get the framebuffer and draw initial frame + if let Some(fb) = arm64_fb::SHELL_FRAMEBUFFER.get() { + let mut fb_guard = fb.lock(); + + // Clear entire screen with dark background + fill_rect( + &mut *fb_guard, + Rect { + x: 0, + y: 0, + width: width as u32, + height: height as u32, + }, + Color::rgb(15, 20, 35), + ); + + // Draw vertical divider + let divider_color = Color::rgb(60, 80, 100); + for i in 0..divider_width { + draw_vline(&mut *fb_guard, (divider_x + i) as i32, 0, height as i32 - 1, divider_color); + } + + // Flush to display + fb_guard.flush(); + } + + // Initialize particle system for left pane + let margin = 10; + particles::start_animation( + margin as i32, + margin as i32, + (left_width - margin) as i32, + (height - margin) as i32, + ); + serial_println!("[graphics] Particle system initialized"); + + // Initialize the render queue for deferred framebuffer rendering + kernel::graphics::render_queue::init(); + + // Initialize log capture ring buffer for serial output tee + kernel::graphics::log_capture::init(); + + serial_println!("[graphics] GOP split-screen terminal UI initialized"); + Ok(()) +} + /// Panic handler #[cfg(target_arch = "aarch64")] #[panic_handler] diff --git a/kernel/src/platform_config.rs b/kernel/src/platform_config.rs new file mode 100644 index 00000000..896dcdc4 --- /dev/null +++ b/kernel/src/platform_config.rs @@ -0,0 +1,476 @@ +/// Platform hardware configuration for ARM64. +/// +/// Provides dynamic hardware addresses that differ between platforms +/// (QEMU virt vs Parallels Desktop). Defaults to QEMU virt addresses +/// so the existing boot path works unchanged. +/// +/// The Parallels boot path calls `init_from_parallels()` early in boot +/// to override these with ACPI-discovered addresses. + +#[cfg(target_arch = "aarch64")] +use core::sync::atomic::{AtomicU64, AtomicU8, Ordering}; + +// ============================================================================= +// Hardware address atomics with QEMU virt defaults +// ============================================================================= + +#[cfg(target_arch = "aarch64")] +static UART_BASE_PHYS: AtomicU64 = AtomicU64::new(0x0900_0000); + +#[cfg(target_arch = "aarch64")] +static GIC_VERSION: AtomicU8 = AtomicU8::new(2); + +#[cfg(target_arch = "aarch64")] +static GICD_BASE_PHYS: AtomicU64 = AtomicU64::new(0x0800_0000); + +#[cfg(target_arch = "aarch64")] +static GICC_BASE_PHYS: AtomicU64 = AtomicU64::new(0x0801_0000); + +#[cfg(target_arch = "aarch64")] +static GICR_BASE_PHYS: AtomicU64 = AtomicU64::new(0); + +#[cfg(target_arch = "aarch64")] +static GICR_SIZE: AtomicU64 = AtomicU64::new(0); + +#[cfg(target_arch = "aarch64")] +static PCI_ECAM_BASE: AtomicU64 = AtomicU64::new(0); + +#[cfg(target_arch = "aarch64")] +static PCI_ECAM_SIZE: AtomicU64 = AtomicU64::new(0); + +#[cfg(target_arch = "aarch64")] +static PCI_MMIO_BASE: AtomicU64 = AtomicU64::new(0); + +#[cfg(target_arch = "aarch64")] +static PCI_MMIO_SIZE: AtomicU64 = AtomicU64::new(0); + +// PCI bus range (from MCFG ACPI table). Default 0-255 for full scan on QEMU. +// Parallels provides actual bus range; scanning beyond it faults. +#[cfg(target_arch = "aarch64")] +static GICV2M_BASE_PHYS: AtomicU64 = AtomicU64::new(0); + +#[cfg(target_arch = "aarch64")] +static GICV2M_SPI_BASE: AtomicU64 = AtomicU64::new(0); + +#[cfg(target_arch = "aarch64")] +static GICV2M_SPI_COUNT: AtomicU64 = AtomicU64::new(0); + +#[cfg(target_arch = "aarch64")] +static PCI_BUS_START: AtomicU64 = AtomicU64::new(0); + +#[cfg(target_arch = "aarch64")] +static PCI_BUS_END: AtomicU64 = AtomicU64::new(255); + +// Memory layout defaults (QEMU virt, 512MB RAM at 0x40000000) +// Kernel image: 0x4000_0000 - 0x4100_0000 (16 MB) +// Per-CPU stacks: 0x4100_0000 - 0x4200_0000 (16 MB) +// Frame alloc: 0x4200_0000 - 0x5000_0000 (224 MB) +// Heap: 0x5000_0000 - 0x5200_0000 (32 MB) + +#[cfg(target_arch = "aarch64")] +static FRAME_ALLOC_START: AtomicU64 = AtomicU64::new(0x4200_0000); + +#[cfg(target_arch = "aarch64")] +static FRAME_ALLOC_END: AtomicU64 = AtomicU64::new(0x5000_0000); + +// ============================================================================= +// Framebuffer info (UEFI GOP, populated by init_from_parallels) +// ============================================================================= + +#[cfg(target_arch = "aarch64")] +static FB_BASE_PHYS: AtomicU64 = AtomicU64::new(0); + +#[cfg(target_arch = "aarch64")] +static FB_SIZE: AtomicU64 = AtomicU64::new(0); + +#[cfg(target_arch = "aarch64")] +static FB_WIDTH: AtomicU64 = AtomicU64::new(0); + +#[cfg(target_arch = "aarch64")] +static FB_HEIGHT: AtomicU64 = AtomicU64::new(0); + +#[cfg(target_arch = "aarch64")] +static FB_STRIDE: AtomicU64 = AtomicU64::new(0); + +#[cfg(target_arch = "aarch64")] +static FB_IS_BGR: AtomicU8 = AtomicU8::new(0); + +// ============================================================================= +// Accessor functions +// ============================================================================= + +/// UART physical base address. +/// QEMU virt: 0x0900_0000, Parallels: 0x0211_0000 +#[cfg(target_arch = "aarch64")] +#[inline] +pub fn uart_base_phys() -> u64 { + UART_BASE_PHYS.load(Ordering::Relaxed) +} + +/// UART virtual address via HHDM. Used by raw serial functions in hot paths. +#[cfg(target_arch = "aarch64")] +#[inline(always)] +pub fn uart_virt() -> u64 { + const HHDM_BASE: u64 = 0xFFFF_0000_0000_0000; + HHDM_BASE + UART_BASE_PHYS.load(Ordering::Relaxed) +} + +/// GIC version (2, 3, or 4). +#[cfg(target_arch = "aarch64")] +#[inline] +pub fn gic_version() -> u8 { + GIC_VERSION.load(Ordering::Relaxed) +} + +/// GIC Distributor physical base address. +/// QEMU virt: 0x0800_0000, Parallels: 0x0201_0000 +#[cfg(target_arch = "aarch64")] +#[inline] +pub fn gicd_base_phys() -> u64 { + GICD_BASE_PHYS.load(Ordering::Relaxed) +} + +/// GIC CPU Interface physical base address (GICv2 only). +/// QEMU virt: 0x0801_0000, Parallels: 0 (uses GICv3 system registers) +#[cfg(target_arch = "aarch64")] +#[inline] +pub fn gicc_base_phys() -> u64 { + GICC_BASE_PHYS.load(Ordering::Relaxed) +} + +/// GIC Redistributor physical base address (GICv3+ only). +/// QEMU virt: 0, Parallels: 0x0250_0000 +#[cfg(target_arch = "aarch64")] +#[inline] +pub fn gicr_base_phys() -> u64 { + GICR_BASE_PHYS.load(Ordering::Relaxed) +} + +/// GIC Redistributor region size in bytes. +#[cfg(target_arch = "aarch64")] +#[inline] +pub fn gicr_size() -> u64 { + GICR_SIZE.load(Ordering::Relaxed) +} + +/// GICv2m MSI frame physical base address. 0 if not probed/available. +#[cfg(target_arch = "aarch64")] +#[inline] +pub fn gicv2m_base_phys() -> u64 { + GICV2M_BASE_PHYS.load(Ordering::Relaxed) +} + +/// GICv2m base SPI number (first available MSI SPI). +#[cfg(target_arch = "aarch64")] +#[inline] +pub fn gicv2m_spi_base() -> u32 { + GICV2M_SPI_BASE.load(Ordering::Relaxed) as u32 +} + +/// GICv2m number of available MSI SPIs. +#[cfg(target_arch = "aarch64")] +#[inline] +pub fn gicv2m_spi_count() -> u32 { + GICV2M_SPI_COUNT.load(Ordering::Relaxed) as u32 +} + +/// Probe for GICv2m at the given physical address. +/// +/// Reads MSI_TYPER (offset 0x008) to discover SPI range. +/// Returns true if a valid GICv2m frame was found. +#[cfg(target_arch = "aarch64")] +pub fn probe_gicv2m(phys_base: u64) -> bool { + const HHDM_BASE: u64 = 0xFFFF_0000_0000_0000; + let virt = (HHDM_BASE + phys_base) as *const u32; + + // DSB + ISB to ensure previous MMIO writes complete before reading device registers + unsafe { + core::arch::asm!("dsb sy", "isb", options(nomem, nostack)); + } + + // Read MSI_TYPER at offset 0x008 + let msi_typer = unsafe { core::ptr::read_volatile(virt.add(2)) }; // offset 8 / 4 + + // MSI_TYPER (ARM IHI0048B §14.1): + // bits [25:16] = BASE_SPI: lowest SPI assigned to MSI + // bits [9:0] = NUM_SPI: number of SPIs assigned to MSI + let spi_base = (msi_typer >> 16) & 0x3FF; + let spi_count = msi_typer & 0x3FF; + + // Log raw value for debugging + crate::serial_println!( + "[gicv2m] MSI_TYPER raw={:#010x} -> base_spi={}, num_spi={}", + msi_typer, spi_base, spi_count, + ); + + if spi_count == 0 || spi_base == 0 || msi_typer == 0xFFFF_FFFF { + return false; + } + + GICV2M_BASE_PHYS.store(phys_base, Ordering::Relaxed); + GICV2M_SPI_BASE.store(spi_base as u64, Ordering::Relaxed); + GICV2M_SPI_COUNT.store(spi_count as u64, Ordering::Relaxed); + true +} + +/// PCI ECAM physical base address. 0 if no PCI. +#[cfg(target_arch = "aarch64")] +#[inline] +pub fn pci_ecam_base() -> u64 { + PCI_ECAM_BASE.load(Ordering::Relaxed) +} + +/// PCI ECAM region size in bytes. +#[cfg(target_arch = "aarch64")] +#[inline] +pub fn pci_ecam_size() -> u64 { + PCI_ECAM_SIZE.load(Ordering::Relaxed) +} + +/// PCI MMIO window physical base address. +#[cfg(target_arch = "aarch64")] +#[inline] +pub fn pci_mmio_base() -> u64 { + PCI_MMIO_BASE.load(Ordering::Relaxed) +} + +/// PCI MMIO window size in bytes. +#[cfg(target_arch = "aarch64")] +#[inline] +pub fn pci_mmio_size() -> u64 { + PCI_MMIO_SIZE.load(Ordering::Relaxed) +} + +/// PCI bus range start (from MCFG ACPI table). +#[cfg(target_arch = "aarch64")] +#[inline] +pub fn pci_bus_start() -> u8 { + PCI_BUS_START.load(Ordering::Relaxed) as u8 +} + +/// PCI bus range end (from MCFG ACPI table). +/// Scanning beyond this faults on Parallels. +#[cfg(target_arch = "aarch64")] +#[inline] +pub fn pci_bus_end() -> u8 { + PCI_BUS_END.load(Ordering::Relaxed) as u8 +} + +/// Frame allocator start physical address. +/// QEMU: 0x4200_0000 (after kernel + stacks) +#[cfg(target_arch = "aarch64")] +#[inline] +pub fn frame_alloc_start() -> u64 { + FRAME_ALLOC_START.load(Ordering::Relaxed) +} + +/// Frame allocator end physical address (exclusive). +/// QEMU: 0x5000_0000 +#[cfg(target_arch = "aarch64")] +#[inline] +pub fn frame_alloc_end() -> u64 { + FRAME_ALLOC_END.load(Ordering::Relaxed) +} + +/// Returns true if running on QEMU (default platform). +/// Detected by checking if the UART address is the QEMU virt default. +#[cfg(target_arch = "aarch64")] +#[inline] +pub fn is_qemu() -> bool { + uart_base_phys() == 0x0900_0000 +} + +/// Whether a UEFI GOP framebuffer was discovered by the loader. +#[cfg(target_arch = "aarch64")] +#[inline] +pub fn has_framebuffer() -> bool { + FB_BASE_PHYS.load(Ordering::Relaxed) != 0 +} + +/// GOP framebuffer physical base address. +#[cfg(target_arch = "aarch64")] +#[inline] +pub fn fb_base_phys() -> u64 { + FB_BASE_PHYS.load(Ordering::Relaxed) +} + +/// GOP framebuffer size in bytes. +#[cfg(target_arch = "aarch64")] +#[inline] +pub fn fb_size() -> u64 { + FB_SIZE.load(Ordering::Relaxed) +} + +/// GOP framebuffer width in pixels. +#[cfg(target_arch = "aarch64")] +#[inline] +pub fn fb_width() -> u32 { + FB_WIDTH.load(Ordering::Relaxed) as u32 +} + +/// GOP framebuffer height in pixels. +#[cfg(target_arch = "aarch64")] +#[inline] +pub fn fb_height() -> u32 { + FB_HEIGHT.load(Ordering::Relaxed) as u32 +} + +/// GOP framebuffer stride in pixels. +#[cfg(target_arch = "aarch64")] +#[inline] +pub fn fb_stride() -> u32 { + FB_STRIDE.load(Ordering::Relaxed) as u32 +} + +/// Whether the GOP framebuffer uses BGR pixel format (vs RGB). +#[cfg(target_arch = "aarch64")] +#[inline] +pub fn fb_is_bgr() -> bool { + FB_IS_BGR.load(Ordering::Relaxed) != 0 +} + +// ============================================================================= +// Initialization from HardwareConfig (Parallels boot path) +// ============================================================================= + +/// HardwareConfig as received from the UEFI loader. +/// This must match the layout in parallels-loader/src/hw_config.rs. +#[cfg(target_arch = "aarch64")] +#[repr(C)] +pub struct HardwareConfig { + pub magic: u32, + pub version: u32, + pub uart_base_phys: u64, + pub uart_irq: u32, + pub _pad0: u32, + pub gic_version: u8, + pub _pad1: [u8; 7], + pub gicd_base: u64, + pub gicc_base: u64, + pub gicr_range_count: u32, + pub _pad2: u32, + pub gicr_ranges: [GicrRange; 8], + pub pci_ecam_base: u64, + pub pci_ecam_size: u64, + pub pci_bus_start: u8, + pub pci_bus_end: u8, + pub _pad3: [u8; 6], + pub pci_mmio_base: u64, + pub pci_mmio_size: u64, + pub ram_region_count: u32, + pub _pad4: u32, + pub ram_regions: [RamRegion; 32], + pub has_framebuffer: u32, + pub _pad5: u32, + pub framebuffer: FramebufferInfo, + pub rsdp_addr: u64, + pub timer_freq_hz: u64, +} + +#[cfg(target_arch = "aarch64")] +#[repr(C)] +pub struct GicrRange { + pub base: u64, + pub length: u32, + pub _pad: u32, +} + +#[cfg(target_arch = "aarch64")] +#[repr(C)] +pub struct RamRegion { + pub base: u64, + pub size: u64, +} + +#[cfg(target_arch = "aarch64")] +#[repr(C)] +pub struct FramebufferInfo { + pub base: u64, + pub size: u64, + pub width: u32, + pub height: u32, + pub stride: u32, + pub pixel_format: u32, +} + +#[cfg(target_arch = "aarch64")] +const HARDWARE_CONFIG_MAGIC: u32 = 0x4252_4E58; + +/// Initialize platform config from the HardwareConfig struct provided by +/// the UEFI loader. Called very early in boot, before serial init. +#[cfg(target_arch = "aarch64")] +pub fn init_from_parallels(config: &HardwareConfig) -> bool { + if config.magic != HARDWARE_CONFIG_MAGIC { + return false; + } + + if config.uart_base_phys != 0 { + UART_BASE_PHYS.store(config.uart_base_phys, Ordering::Relaxed); + } + if config.gic_version != 0 { + GIC_VERSION.store(config.gic_version, Ordering::Relaxed); + } + if config.gicd_base != 0 { + GICD_BASE_PHYS.store(config.gicd_base, Ordering::Relaxed); + } + if config.gicc_base != 0 { + GICC_BASE_PHYS.store(config.gicc_base, Ordering::Relaxed); + } + if config.gicr_range_count > 0 { + GICR_BASE_PHYS.store(config.gicr_ranges[0].base, Ordering::Relaxed); + GICR_SIZE.store(config.gicr_ranges[0].length as u64, Ordering::Relaxed); + } + if config.pci_ecam_base != 0 { + PCI_ECAM_BASE.store(config.pci_ecam_base, Ordering::Relaxed); + PCI_ECAM_SIZE.store(config.pci_ecam_size, Ordering::Relaxed); + PCI_BUS_START.store(config.pci_bus_start as u64, Ordering::Relaxed); + PCI_BUS_END.store(config.pci_bus_end as u64, Ordering::Relaxed); + } + if config.pci_mmio_base != 0 { + PCI_MMIO_BASE.store(config.pci_mmio_base, Ordering::Relaxed); + PCI_MMIO_SIZE.store(config.pci_mmio_size, Ordering::Relaxed); + } + + // Compute frame allocator range from RAM regions. + // Find the largest RAM region starting at 0x4000_0000 (standard ARM64 RAM base). + // Reserve: kernel (16 MB) + per-CPU stacks (16 MB) at the start, + // and heap (32 MB) at the end. + if config.ram_region_count > 0 { + let mut best_base = 0u64; + let mut best_size = 0u64; + for i in 0..config.ram_region_count as usize { + if i >= config.ram_regions.len() { + break; + } + let region = &config.ram_regions[i]; + if region.size > best_size { + best_base = region.base; + best_size = region.size; + } + } + + if best_size > 0 { + // Frame allocator starts after kernel + stacks (32 MB from RAM base) + let fa_start = best_base + 0x0200_0000; // +32 MB + // Frame allocator must end BEFORE the heap region. + // The heap is at fixed physical 0x5000_0000 (32 MB), so cap fa_end there. + let fa_end = (best_base + best_size).min(0x5000_0000); + if fa_end > fa_start { + FRAME_ALLOC_START.store(fa_start, Ordering::Relaxed); + FRAME_ALLOC_END.store(fa_end, Ordering::Relaxed); + } + } + } + + // Store framebuffer info if the loader discovered a GOP framebuffer + if config.has_framebuffer != 0 && config.framebuffer.base != 0 { + FB_BASE_PHYS.store(config.framebuffer.base, Ordering::Relaxed); + FB_SIZE.store(config.framebuffer.size, Ordering::Relaxed); + FB_WIDTH.store(config.framebuffer.width as u64, Ordering::Relaxed); + FB_HEIGHT.store(config.framebuffer.height as u64, Ordering::Relaxed); + FB_STRIDE.store(config.framebuffer.stride as u64, Ordering::Relaxed); + FB_IS_BGR.store(if config.framebuffer.pixel_format == 1 { 1 } else { 0 }, Ordering::Relaxed); + } + + true +} diff --git a/kernel/src/serial_aarch64.rs b/kernel/src/serial_aarch64.rs index 4bdf09af..2d148c91 100644 --- a/kernel/src/serial_aarch64.rs +++ b/kernel/src/serial_aarch64.rs @@ -6,15 +6,25 @@ #![cfg(target_arch = "aarch64")] use core::fmt; -use core::sync::atomic::{AtomicBool, Ordering}; +use core::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use spin::Mutex; +/// Global UART virtual address for assembly-level diagnostics. +/// Set during serial init so boot.S exception vectors can output +/// characters without calling into Rust (used for pre-SP diagnostics). +#[no_mangle] +pub static DIAG_UART_VIRT: AtomicU64 = AtomicU64::new(0); + // ============================================================================= // PL011 UART Register Map // ============================================================================= -/// PL011 UART base physical address for QEMU virt machine. -const PL011_BASE_PHYS: usize = 0x0900_0000; +/// PL011 UART base physical address. +/// Reads from platform_config (defaults to QEMU virt 0x0900_0000). +#[inline] +fn pl011_base_phys() -> usize { + crate::platform_config::uart_base_phys() as usize +} /// PL011 Register offsets - complete register map for UART configuration #[allow(dead_code)] @@ -79,7 +89,7 @@ mod cr { fn read_reg(offset: usize) -> u32 { unsafe { let base = crate::memory::physical_memory_offset().as_u64() as usize; - let addr = (base + PL011_BASE_PHYS + offset) as *const u32; + let addr = (base + pl011_base_phys() + offset) as *const u32; core::ptr::read_volatile(addr) } } @@ -88,7 +98,7 @@ fn read_reg(offset: usize) -> u32 { fn write_reg(offset: usize, value: u32) { unsafe { let base = crate::memory::physical_memory_offset().as_u64() as usize; - let addr = (base + PL011_BASE_PHYS + offset) as *mut u32; + let addr = (base + pl011_base_phys() + offset) as *mut u32; core::ptr::write_volatile(addr, value); } } @@ -120,6 +130,9 @@ impl SerialPort { write_reg(reg::CR, cr | cr::UARTEN | cr::TXE | cr::RXE); SERIAL_INITIALIZED.store(true, Ordering::Release); + + // Publish UART virtual address for assembly-level diagnostics + DIAG_UART_VIRT.store(crate::platform_config::uart_virt(), Ordering::Release); } /// Send a single byte @@ -331,8 +344,7 @@ pub fn _log_print(args: fmt::Arguments) { /// - Syscall entry/exit #[inline(always)] pub fn raw_serial_char(c: u8) { - const HHDM_BASE: u64 = 0xFFFF_0000_0000_0000; - let addr = (HHDM_BASE + PL011_BASE_PHYS as u64) as *mut u32; + let addr = crate::platform_config::uart_virt() as *mut u32; unsafe { core::ptr::write_volatile(addr, c as u32); } } @@ -346,8 +358,7 @@ pub fn raw_serial_char(c: u8) { /// This helps identify markers in test output without ambiguity. #[inline(always)] pub fn raw_serial_str(s: &[u8]) { - const HHDM_BASE: u64 = 0xFFFF_0000_0000_0000; - let addr = (HHDM_BASE + PL011_BASE_PHYS as u64) as *mut u32; + let addr = crate::platform_config::uart_virt() as *mut u32; for &c in s { unsafe { core::ptr::write_volatile(addr, c as u32); } } diff --git a/kernel/src/syscall/graphics.rs b/kernel/src/syscall/graphics.rs index 3ca7cdda..d8ea65e0 100644 --- a/kernel/src/syscall/graphics.rs +++ b/kernel/src/syscall/graphics.rs @@ -526,9 +526,13 @@ pub fn sys_fbdraw(cmd_ptr: u64) -> SyscallResult { cmd.p4 as u32, )) } else { - // Full flush — get display dimensions - crate::drivers::virtio::gpu_mmio::dimensions() - .map(|(w, h)| (0u32, 0u32, w, h)) + // Full flush — get display dimensions. + // Check FB_INFO_CACHE first (works for GOP and all backends), + // then fall back to gpu_mmio::dimensions() for QEMU. + crate::graphics::arm64_fb::FB_INFO_CACHE.get() + .map(|c| (0u32, 0u32, c.width as u32, c.height as u32)) + .or_else(|| crate::drivers::virtio::gpu_mmio::dimensions() + .map(|(w, h)| (0u32, 0u32, w, h))) }; if let Some(mmap_info) = fb_mmap_info { @@ -576,7 +580,7 @@ pub fn sys_fbdraw(cmd_ptr: u64) -> SyscallResult { // immediately rather than waiting for the render thread to wake up // (which could take 5ms+ due to timer tick granularity). if let Some((fx, fy, fw, fh)) = flush_rect { - let _ = crate::drivers::virtio::gpu_mmio::flush_rect(fx, fy, fw, fh); + let _ = crate::graphics::arm64_fb::flush_dirty_rect(fx, fy, fw, fh); } } } diff --git a/kernel/src/time/rtc.rs b/kernel/src/time/rtc.rs index a546ac42..fed87b32 100644 --- a/kernel/src/time/rtc.rs +++ b/kernel/src/time/rtc.rs @@ -350,9 +350,15 @@ pub fn read_datetime() -> DateTime { } } -/// Initialize RTC and cache boot time +/// Initialize RTC and cache boot time. +/// PL031 is only present on QEMU virt; skip on other platforms. #[cfg(target_arch = "aarch64")] pub fn init() { + if !crate::platform_config::is_qemu() { + log::info!("PL031 RTC not available on this platform, skipping"); + BOOT_WALL_TIME.store(0, Ordering::Relaxed); + return; + } match read_rtc_time() { Ok(timestamp) => { BOOT_WALL_TIME.store(timestamp, Ordering::Relaxed); diff --git a/kernel/src/tracing/output.rs b/kernel/src/tracing/output.rs index e762ce16..ad24e36b 100644 --- a/kernel/src/tracing/output.rs +++ b/kernel/src/tracing/output.rs @@ -156,9 +156,8 @@ fn raw_serial_char(c: u8) { #[cfg(target_arch = "aarch64")] unsafe { - // PL011 UART at 0x09000000 (QEMU virt) - let uart_base: *mut u8 = 0x0900_0000 as *mut u8; - core::ptr::write_volatile(uart_base, c); + let uart_addr = crate::platform_config::uart_virt() as *mut u8; + core::ptr::write_volatile(uart_addr, c); } } diff --git a/parallels-loader/Cargo.toml b/parallels-loader/Cargo.toml new file mode 100644 index 00000000..c8f956f8 --- /dev/null +++ b/parallels-loader/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "parallels-loader" +version = "0.1.0" +edition = "2021" +description = "UEFI loader for booting Breenix on Parallels Desktop (Apple Silicon)" + +[[bin]] +name = "parallels-loader" +path = "src/main.rs" +test = false + +[dependencies] +uefi = { version = "0.33", features = ["panic_handler", "global_allocator"] } +acpi = "5.0" +log = { version = "0.4", default-features = false } diff --git a/parallels-loader/src/acpi_discovery.rs b/parallels-loader/src/acpi_discovery.rs new file mode 100644 index 00000000..13b2ba88 --- /dev/null +++ b/parallels-loader/src/acpi_discovery.rs @@ -0,0 +1,249 @@ +/// ACPI table discovery for Parallels hardware. +/// +/// Parses MADT (GIC addresses), MCFG (PCI ECAM), and SPCR (UART) tables +/// to populate HardwareConfig with actual hardware addresses. +use core::ptr::NonNull; + +use acpi::madt::{Madt, MadtEntry}; +use acpi::mcfg::Mcfg; +use acpi::spcr::Spcr; +use acpi::{AcpiHandler, AcpiTables, PhysicalMapping}; + +use crate::hw_config::{GicrRange, HardwareConfig, MAX_GICR_RANGES, MAX_RAM_REGIONS}; + +/// UEFI ACPI handler. +/// +/// During UEFI boot services, physical memory is identity-mapped, +/// so physical addresses can be used directly as virtual addresses. +#[derive(Clone)] +pub struct UefiAcpiHandler; + +unsafe impl Send for UefiAcpiHandler {} + +impl AcpiHandler for UefiAcpiHandler { + unsafe fn map_physical_region( + &self, + physical_address: usize, + size: usize, + ) -> PhysicalMapping { + // UEFI identity maps all physical memory during boot services + let ptr = NonNull::new(physical_address as *mut T).expect("null ACPI address"); + unsafe { PhysicalMapping::new(physical_address, ptr, size, size, self.clone()) } + } + + fn unmap_physical_region(_region: &PhysicalMapping) { + // Nothing to unmap - identity mapped + } +} + +/// Discover hardware configuration from ACPI tables. +/// +/// `rsdp_addr` is the physical address of the RSDP, obtained from +/// UEFI's `EFI_ACPI_TABLE_GUID` configuration table. +pub fn discover_hardware(rsdp_addr: usize, config: &mut HardwareConfig) -> Result<(), &'static str> { + config.rsdp_addr = rsdp_addr as u64; + + let handler = UefiAcpiHandler; + let tables = unsafe { AcpiTables::from_rsdp(handler, rsdp_addr) } + .map_err(|_| "Failed to parse ACPI tables from RSDP")?; + + // Parse MADT for GIC configuration + parse_madt(&tables, config)?; + + // Parse MCFG for PCI ECAM + parse_mcfg(&tables, config); + + // Parse SPCR for UART (fallback if not found) + parse_spcr(&tables, config); + + // Read timer frequency from CNTFRQ_EL0 + config.timer_freq_hz = read_timer_freq(); + + Ok(()) +} + +/// Parse MADT to extract GIC distributor, redistributor, and CPU interface info. +fn parse_madt( + tables: &AcpiTables, + config: &mut HardwareConfig, +) -> Result<(), &'static str> { + let madt_mapping = tables + .find_table::() + .map_err(|_| "MADT table not found")?; + + let madt_pin = madt_mapping.get(); + let mut gicr_idx = 0usize; + + for entry in madt_pin.entries() { + match entry { + MadtEntry::Gicd(gicd) => { + config.gicd_base = gicd.physical_base_address; + config.gic_version = gicd.gic_version; + // Version 0 means "detect from hardware" + if config.gic_version == 0 { + // Default to GICv3 on Parallels (known from hw dump) + config.gic_version = 3; + } + log::info!( + " GICD: base=0x{:08x}, version={}", + config.gicd_base, + config.gic_version + ); + } + MadtEntry::Gicc(gicc) => { + let gicc_base = gicc.gic_registers_address; + if gicc_base != 0 && config.gicc_base == 0 { + config.gicc_base = gicc_base; + log::info!(" GICC: base=0x{:08x}", config.gicc_base); + } + } + MadtEntry::GicRedistributor(gicr) => { + if gicr_idx < MAX_GICR_RANGES { + let base = gicr.discovery_range_base_address; + let length = gicr.discovery_range_length; + config.gicr_ranges[gicr_idx] = GicrRange { + base, + length, + _pad: 0, + }; + log::info!( + " GICR[{}]: base=0x{:08x}, length=0x{:x}", + gicr_idx, + base, + length, + ); + gicr_idx += 1; + } + } + MadtEntry::GicMsiFrame(msi) => { + let msi_base = msi.physical_base_address; + let spi_base = msi.spi_base; + let spi_count = msi.spi_count; + log::info!( + " GICv2m MSI: base=0x{:08x}, SPI base={}, count={}", + msi_base, + spi_base, + spi_count, + ); + } + _ => {} + } + } + + config.gicr_range_count = gicr_idx as u32; + + if config.gicd_base == 0 { + return Err("No GICD found in MADT"); + } + + Ok(()) +} + +/// Parse MCFG for PCI Enhanced Configuration Access Mechanism (ECAM). +fn parse_mcfg(tables: &AcpiTables, config: &mut HardwareConfig) { + let mcfg_mapping = match tables.find_table::() { + Ok(m) => m, + Err(_) => { + log::warn!(" MCFG table not found - PCI ECAM unavailable"); + return; + } + }; + + for entry in mcfg_mapping.entries() { + // Use the first segment group (typically segment 0) + if config.pci_ecam_base == 0 { + config.pci_ecam_base = entry.base_address; + config.pci_bus_start = entry.bus_number_start; + config.pci_bus_end = entry.bus_number_end; + + // ECAM size: 4KB per function, 8 functions per device, 32 devices per bus + let bus_count = (config.pci_bus_end as u64 - config.pci_bus_start as u64 + 1) as u64; + config.pci_ecam_size = bus_count * 32 * 8 * 4096; + + log::info!( + " PCI ECAM: base=0x{:08x}, buses {}..{}, size=0x{:x}", + config.pci_ecam_base, + config.pci_bus_start, + config.pci_bus_end, + config.pci_ecam_size, + ); + } + } + + // PCI MMIO window - typically at 0x10000000 on Parallels + // This is from the host bridge _CRS in DSDT, but we hardcode the known value + // since DSDT AML parsing is complex. The kernel can override from device tree. + if config.pci_ecam_base != 0 && config.pci_mmio_base == 0 { + config.pci_mmio_base = 0x1000_0000; + config.pci_mmio_size = 0x1000_0000; // 256 MB + log::info!( + " PCI MMIO: base=0x{:08x}, size=0x{:x}", + config.pci_mmio_base, + config.pci_mmio_size, + ); + } +} + +/// Parse SPCR for serial port configuration. +fn parse_spcr(tables: &AcpiTables, config: &mut HardwareConfig) { + let spcr_mapping = match tables.find_table::() { + Ok(s) => s, + Err(_) => { + log::info!(" SPCR table not found - using fallback UART detection"); + // Fallback: check DBG2 table or use known Parallels address + if config.uart_base_phys == 0 { + config.uart_base_phys = 0x0211_0000; // Known Parallels PL011 address + config.uart_irq = 32; // SPI 0 + log::info!( + " UART (fallback): base=0x{:08x}, irq={}", + config.uart_base_phys, + config.uart_irq + ); + } + return; + } + }; + + let iface_type = spcr_mapping.interface_type(); + log::info!(" SPCR interface type: {:?}", iface_type); + + if let Some(Ok(addr)) = spcr_mapping.base_address() { + config.uart_base_phys = addr.address; + log::info!(" UART: base=0x{:08x}", config.uart_base_phys); + } + + if let Some(gsi) = spcr_mapping.global_system_interrupt() { + config.uart_irq = gsi; + log::info!(" UART: irq={}", config.uart_irq); + } +} + +/// Read the ARM generic timer frequency from CNTFRQ_EL0. +fn read_timer_freq() -> u64 { + let freq: u64; + unsafe { + core::arch::asm!("mrs {}, cntfrq_el0", out(reg) freq); + } + log::info!(" Timer frequency: {} Hz ({} MHz)", freq, freq / 1_000_000); + freq +} + +/// Populate RAM regions from UEFI memory map. +/// +/// Called after ExitBootServices with the UEFI memory map. +/// Only includes EfiConventionalMemory regions. +#[allow(dead_code)] // Used in Phase 2 when kernel entry is implemented +pub fn populate_ram_regions( + config: &mut HardwareConfig, + regions: &[(u64, u64)], // (base, size) pairs of conventional memory +) { + let count = regions.len().min(MAX_RAM_REGIONS); + for (i, &(base, size)) in regions.iter().take(count).enumerate() { + config.ram_regions[i].base = base; + config.ram_regions[i].size = size; + } + config.ram_region_count = count as u32; + + let total_mb: u64 = regions.iter().map(|(_, s)| s).sum::() / (1024 * 1024); + log::info!(" RAM: {} regions, {} MiB total", count, total_mb); +} diff --git a/parallels-loader/src/gop_discovery.rs b/parallels-loader/src/gop_discovery.rs new file mode 100644 index 00000000..27e6c9ed --- /dev/null +++ b/parallels-loader/src/gop_discovery.rs @@ -0,0 +1,163 @@ +//! UEFI GOP (Graphics Output Protocol) framebuffer discovery. +//! +//! Queries UEFI boot services for a GOP framebuffer and populates the +//! HardwareConfig with resolution, stride, pixel format, and base address. +//! Enumerates available modes and selects the highest resolution. +//! Must be called before exit_boot_services(). + +use crate::hw_config::HardwareConfig; +use uefi::proto::console::gop::{GraphicsOutput, PixelFormat}; + +// ============================================================================= +// Raw UART output (bypasses UEFI ConOut, goes directly to serial) +// ============================================================================= + +/// Writer that sends bytes directly to the PL011 UART data register. +/// Used to log GOP mode info to serial, since UEFI ConOut goes to the +/// display (which gets reset when the kernel takes over). +struct UartWriter(u64); + +impl UartWriter { + fn putc(&self, c: u8) { + if self.0 == 0 { + return; + } + unsafe { + let dr = self.0 as *mut u32; + core::ptr::write_volatile(dr, c as u32); + } + } +} + +impl core::fmt::Write for UartWriter { + fn write_str(&mut self, s: &str) -> core::fmt::Result { + for c in s.bytes() { + if c == b'\n' { + self.putc(b'\r'); + } + self.putc(c); + } + Ok(()) + } +} + +/// Discover GOP framebuffer and populate HardwareConfig. +/// +/// Enumerates all available GOP modes and selects the highest resolution +/// that uses a supported pixel format (RGB or BGR, not BltOnly). +/// On Parallels Desktop, GOP provides a linear framebuffer that the +/// hypervisor composites to the VM window. +/// +/// Returns Ok(()) if a framebuffer was found, Err if GOP is not available. +/// Failure is non-fatal — the kernel will boot without display output. +pub fn discover_gop(config: &mut HardwareConfig) -> Result<(), &'static str> { + use core::fmt::Write; + + // Raw UART writer for serial output (UEFI log goes to display, not serial) + let mut uart = UartWriter(config.uart_base_phys); + + // Find the GOP handle + let handle = uefi::boot::get_handle_for_protocol::() + .map_err(|_| "No GOP handle found")?; + + // Open the protocol + let mut gop = uefi::boot::open_protocol_exclusive::(handle) + .map_err(|_| "Failed to open GOP protocol")?; + + // Enumerate all available modes and find the best one. + // "Best" = highest pixel count with a supported pixel format. + let mut best_mode: Option = None; + let mut best_pixels: usize = 0; + let mut mode_count: usize = 0; + + log::info!("[gop] Enumerating display modes..."); + let _ = write!(uart, "[gop-serial] Enumerating GOP display modes:\n"); + + for mode in gop.modes() { + let info = mode.info(); + let (w, h) = info.resolution(); + let pf = info.pixel_format(); + let pixels = w * h; + + log::info!("[gop] Mode {}: {}x{} format={:?}", mode_count, w, h, pf); + let _ = write!(uart, "[gop-serial] Mode {}: {}x{} format={}\n", + mode_count, w, h, + match pf { + PixelFormat::Rgb => "RGB", + PixelFormat::Bgr => "BGR", + PixelFormat::Bitmask => "Bitmask", + PixelFormat::BltOnly => "BltOnly", + }); + + mode_count += 1; + + // Skip BltOnly modes — we need a linear framebuffer + if pf == PixelFormat::BltOnly { + continue; + } + + if pixels > best_pixels { + best_pixels = pixels; + best_mode = Some(mode); + } + } + + let _ = write!(uart, "[gop-serial] {} modes total, best={} pixels\n", mode_count, best_pixels); + + // Set the best mode if it's different from the current one + if let Some(ref mode) = best_mode { + let info = mode.info(); + let (bw, bh) = info.resolution(); + let (cw, ch) = gop.current_mode_info().resolution(); + + if bw != cw || bh != ch { + log::info!("[gop] Switching from {}x{} to {}x{}", cw, ch, bw, bh); + let _ = write!(uart, "[gop-serial] Switching from {}x{} to {}x{}\n", cw, ch, bw, bh); + gop.set_mode(mode).map_err(|_| "Failed to set GOP mode")?; + log::info!("[gop] Mode set successfully"); + let _ = write!(uart, "[gop-serial] Mode switch successful\n"); + } else { + log::info!("[gop] Already at best mode: {}x{}", cw, ch); + let _ = write!(uart, "[gop-serial] Already at best mode: {}x{}\n", cw, ch); + } + } else { + let _ = write!(uart, "[gop-serial] WARNING: No usable GOP mode found!\n"); + } + + // Read the (potentially updated) mode info + let mode_info = gop.current_mode_info(); + let (width, height) = mode_info.resolution(); + let stride = mode_info.stride(); + let pixel_format = mode_info.pixel_format(); + + // Get framebuffer base address and size + let mut fb = gop.frame_buffer(); + let fb_base = fb.as_mut_ptr() as u64; + let fb_size = fb.size() as u64; + + log::info!( + "[gop] Framebuffer: {}x{} stride={} format={:?} base={:#x} size={:#x}", + width, height, stride, pixel_format, fb_base, fb_size, + ); + let _ = write!(uart, "[gop-serial] Selected: {}x{} stride={} base={:#x} size={:#x}\n", + width, height, stride, fb_base, fb_size); + + // Map UEFI pixel format to our convention: 0 = RGB, 1 = BGR + let pf = match pixel_format { + PixelFormat::Bgr => 1, + PixelFormat::Rgb => 0, + // Bitmask and BltOnly are uncommon; treat as RGB and hope for the best + _ => 0, + }; + + // Populate config + config.has_framebuffer = 1; + config.framebuffer.base = fb_base; + config.framebuffer.size = fb_size; + config.framebuffer.width = width as u32; + config.framebuffer.height = height as u32; + config.framebuffer.stride = stride as u32; + config.framebuffer.pixel_format = pf; + + Ok(()) +} diff --git a/parallels-loader/src/hw_config.rs b/parallels-loader/src/hw_config.rs new file mode 100644 index 00000000..e15874bc --- /dev/null +++ b/parallels-loader/src/hw_config.rs @@ -0,0 +1,162 @@ +/// Hardware configuration discovered by the UEFI loader. +/// +/// This struct is passed to the kernel at entry. It contains physical addresses +/// for all platform-specific hardware, discovered via ACPI tables. +/// The kernel uses these instead of hardcoded QEMU virt addresses. + +/// Maximum number of RAM regions from the UEFI memory map. +pub const MAX_RAM_REGIONS: usize = 32; + +/// Maximum number of GIC redistributor ranges. +pub const MAX_GICR_RANGES: usize = 8; + +/// A contiguous region of physical memory. +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct RamRegion { + pub base: u64, + pub size: u64, +} + +/// GIC redistributor discovery range. +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct GicrRange { + pub base: u64, + pub length: u32, + pub _pad: u32, +} + +/// Framebuffer information from UEFI GOP. +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct FramebufferInfo { + pub base: u64, + pub size: u64, + pub width: u32, + pub height: u32, + pub stride: u32, + /// 0 = RGB, 1 = BGR + pub pixel_format: u32, +} + +/// Complete hardware configuration for the platform. +/// +/// Populated by the UEFI loader via ACPI discovery and UEFI boot services. +/// Passed to the kernel entry point in x0. +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct HardwareConfig { + /// Magic number for validation: 0x4252_4E58 ("BRNX") + pub magic: u32, + /// Version of this struct (currently 1) + pub version: u32, + + // --- UART --- + /// PL011 UART base physical address + pub uart_base_phys: u64, + /// UART interrupt (GIC SPI number) + pub uart_irq: u32, + pub _pad0: u32, + + // --- GIC --- + /// GIC version (2, 3, or 4) + pub gic_version: u8, + pub _pad1: [u8; 7], + /// GIC Distributor base physical address + pub gicd_base: u64, + /// GIC CPU Interface base (GICv2 only, 0 for GICv3+) + pub gicc_base: u64, + /// Number of GICR ranges + pub gicr_range_count: u32, + pub _pad2: u32, + /// GIC Redistributor ranges (GICv3+) + pub gicr_ranges: [GicrRange; MAX_GICR_RANGES], + + // --- PCI --- + /// PCI ECAM base physical address + pub pci_ecam_base: u64, + /// PCI ECAM region size + pub pci_ecam_size: u64, + /// PCI bus range start + pub pci_bus_start: u8, + /// PCI bus range end + pub pci_bus_end: u8, + pub _pad3: [u8; 6], + /// PCI MMIO window base + pub pci_mmio_base: u64, + /// PCI MMIO window size + pub pci_mmio_size: u64, + + // --- Memory --- + /// Number of valid RAM regions + pub ram_region_count: u32, + pub _pad4: u32, + /// RAM regions from UEFI memory map + pub ram_regions: [RamRegion; MAX_RAM_REGIONS], + + // --- Framebuffer --- + /// Whether framebuffer info is valid + pub has_framebuffer: u32, + pub _pad5: u32, + /// Framebuffer from UEFI GOP + pub framebuffer: FramebufferInfo, + + // --- ACPI --- + /// RSDP physical address (for kernel to parse additional ACPI tables) + pub rsdp_addr: u64, + + // --- Timer --- + /// Generic timer frequency in Hz (from CNTFRQ_EL0) + pub timer_freq_hz: u64, +} + +pub const HARDWARE_CONFIG_MAGIC: u32 = 0x4252_4E58; // "BRNX" +pub const HARDWARE_CONFIG_VERSION: u32 = 1; + +impl HardwareConfig { + /// Create a zeroed config with magic and version set. + pub fn new() -> Self { + Self { + magic: HARDWARE_CONFIG_MAGIC, + version: HARDWARE_CONFIG_VERSION, + uart_base_phys: 0, + uart_irq: 0, + _pad0: 0, + gic_version: 0, + _pad1: [0; 7], + gicd_base: 0, + gicc_base: 0, + gicr_range_count: 0, + _pad2: 0, + gicr_ranges: [GicrRange { base: 0, length: 0, _pad: 0 }; MAX_GICR_RANGES], + pci_ecam_base: 0, + pci_ecam_size: 0, + pci_bus_start: 0, + pci_bus_end: 0, + _pad3: [0; 6], + pci_mmio_base: 0, + pci_mmio_size: 0, + ram_region_count: 0, + _pad4: 0, + ram_regions: [RamRegion { base: 0, size: 0 }; MAX_RAM_REGIONS], + has_framebuffer: 0, + _pad5: 0, + framebuffer: FramebufferInfo { + base: 0, + size: 0, + width: 0, + height: 0, + stride: 0, + pixel_format: 0, + }, + rsdp_addr: 0, + timer_freq_hz: 0, + } + } + + #[allow(dead_code)] // Used by kernel to validate config received from loader + pub fn validate(&self) -> bool { + self.magic == HARDWARE_CONFIG_MAGIC && self.version == HARDWARE_CONFIG_VERSION + } +} diff --git a/parallels-loader/src/kernel_entry.rs b/parallels-loader/src/kernel_entry.rs new file mode 100644 index 00000000..3474aaab --- /dev/null +++ b/parallels-loader/src/kernel_entry.rs @@ -0,0 +1,296 @@ +/// Kernel entry handoff: exit UEFI boot services, set up page tables, jump to kernel. +/// +/// This module handles the critical transition from UEFI environment to bare-metal +/// kernel execution. The sequence is: +/// 1. Load kernel ELF from the ESP filesystem +/// 2. Exit UEFI boot services (no more UEFI calls after this!) +/// 3. Set MAIR, TCR, TTBR0, TTBR1 for our page tables +/// 4. Install minimal VBAR_EL1 exception handler (prevents recursive faults) +/// 5. TLB invalidate + ISB +/// 6. Jump to kernel_main(hw_config_ptr) -- same binary as QEMU + +use crate::hw_config::HardwareConfig; +use crate::page_tables::{self, PageTableStorage}; + +/// Jump to the kernel. +/// +/// `kernel_entry` is the physical address of the kernel entry point. +/// `hw_config` is the HardwareConfig to pass to the kernel. +/// `page_table_storage` contains the pre-built page tables. +/// +/// This function never returns. +pub fn jump_to_kernel( + kernel_entry: u64, + hw_config: &HardwareConfig, + page_table_storage: &mut PageTableStorage, +) -> ! { + let (ttbr0, ttbr1) = page_tables::build_page_tables(page_table_storage); + let hw_config_ptr = hw_config as *const HardwareConfig as u64; + + log::info!("Page tables built: TTBR0=0x{:016x}, TTBR1=0x{:016x}", ttbr0, ttbr1); + log::info!("Kernel entry: 0x{:016x}", kernel_entry); + log::info!("HardwareConfig at: 0x{:016x}", hw_config_ptr); + + // After this point, we cannot use any UEFI services. + // The UEFI memory map has already been processed. + log::info!("Switching to kernel page tables and jumping to kernel..."); + + unsafe { + switch_and_jump(ttbr0, ttbr1, kernel_entry, hw_config_ptr); + } +} + +/// Switch page tables and jump to the kernel entry point. +/// +/// This must be done in assembly to avoid any stack-relative references +/// during the page table switch. The identity map ensures the code +/// continues executing after TTBR0/TTBR1 are changed. +/// +/// Before jumping, installs a minimal VBAR_EL1 exception vector table +/// that writes 'X' to UART and spins, preventing recursive faults if +/// the kernel entry address is wrong. +/// +/// Arguments: +/// x0 = TTBR0 physical address (identity map) +/// x1 = TTBR1 physical address (HHDM) +/// x2 = kernel entry point physical address +/// x3 = HardwareConfig pointer (physical, identity-mapped) +#[inline(never)] +unsafe fn switch_and_jump(ttbr0: u64, ttbr1: u64, entry: u64, hw_config_ptr: u64) -> ! { + // Get the address of our exception vector table (defined in global_asm! below). + extern "C" { + static loader_exception_vectors: u8; + } + let vbar_addr = &loader_exception_vectors as *const u8 as u64; + + core::arch::asm!( + // Disable MMU first to safely switch page tables + "mrs x4, sctlr_el1", + "bic x4, x4, #1", // Clear M bit (MMU disable) + "msr sctlr_el1, x4", + "isb", + + // Set MAIR_EL1 (Memory Attribute Indirection Register) + // Must match kernel boot.S layout: + // Index 0: Device-nGnRnE (0x00) + // Index 1: Normal WB (0xFF) + "mov x4, #0xFF00", + "msr mair_el1, x4", + "isb", + + // Set TCR_EL1 (Translation Control Register) + // T0SZ=16, T1SZ=16, 4K granule, 40-bit PA, WB WA, Inner Shareable + "ldr x4, ={tcr}", + "msr tcr_el1, x4", + "isb", + + // Set TTBR0_EL1 (identity map) + "msr ttbr0_el1, x0", + // Set TTBR1_EL1 (HHDM) + "msr ttbr1_el1, x1", + "isb", + + // Invalidate all TLB entries + "tlbi vmalle1", + "dsb sy", + "isb", + + // --- Install minimal VBAR_EL1 before re-enabling MMU --- + // This replaces the UEFI firmware's vectors (which may be in + // now-unmapped memory) with a handler that writes 'X' to UART + // and spins, preventing recursive silent crashes. + "msr vbar_el1, x5", + "isb", + + // Re-enable MMU with our page tables + "mrs x4, sctlr_el1", + "orr x4, x4, #1", // Set M bit (MMU enable) + "orr x4, x4, #(1 << 2)", // Set C bit (data cache enable) + "orr x4, x4, #(1 << 12)", // Set I bit (instruction cache enable) + "msr sctlr_el1, x4", + "isb", + + // UART breadcrumb: 'L' = MMU re-enabled, page tables work + "movz x4, #0x0211, lsl #16", // x4 = 0x02110000 (Parallels UART) + "mov x6, #0x4C", // 'L' + "str w6, [x4]", + + // Enable FP/SIMD (CPACR_EL1.FPEN = 0b11) to prevent traps + "mrs x4, cpacr_el1", + "orr x4, x4, #(3 << 20)", + "msr cpacr_el1, x4", + "isb", + + // Set up a temporary kernel stack in the identity-mapped region. + // Use a fixed address in RAM: 0x4200_0000 (top of first 2MB after kernel) + // The kernel will set up proper stacks during init. + "mov x4, #0x4200", + "lsl x4, x4, #16", // x4 = 0x42000000 + "mov sp, x4", + + // UART breadcrumb: 'J' = about to jump to kernel + "movz x4, #0x0211, lsl #16", // x4 = 0x02110000 + "mov x6, #0x4A", // 'J' + "str w6, [x4]", + + // Jump to kernel entry with HardwareConfig pointer in x0 + "mov x0, x3", // x0 = hw_config_ptr + "br x2", // Jump to kernel_main + + tcr = const page_tables::TCR_VALUE, + in("x0") ttbr0, + in("x1") ttbr1, + in("x2") entry, + in("x3") hw_config_ptr, + in("x5") vbar_addr, + options(noreturn), + ); +} + +// Minimal exception vector table for the UEFI-to-kernel transition. +// +// Each vector entry is 128 bytes (0x80). The table must be 2KB aligned +// (bits [10:0] of VBAR_EL1 are RES0, so the table must be 0x800-aligned). +// +// All 16 entries write 'X' to the Parallels UART and spin on WFI. +// This catches any exception during the brief window between page table +// switch and kernel_main installing its own vectors. +// +// Uses UART physical address 0x0211_0000 via identity map. +core::arch::global_asm!( + ".balign 2048", + ".global loader_exception_vectors", + "loader_exception_vectors:", + + // --- Current EL with SP0 (entries 0-3) --- + // Entry 0: Synchronous + "movz x4, #0x0211, lsl #16", // x4 = 0x02110000 + "mov x5, #0x58", // 'X' + "str w5, [x4]", + "0: wfi", + "b 0b", + ".balign 128", + + // Entry 1: IRQ + "movz x4, #0x0211, lsl #16", + "mov x5, #0x58", + "str w5, [x4]", + "0: wfi", + "b 0b", + ".balign 128", + + // Entry 2: FIQ + "movz x4, #0x0211, lsl #16", + "mov x5, #0x58", + "str w5, [x4]", + "0: wfi", + "b 0b", + ".balign 128", + + // Entry 3: SError + "movz x4, #0x0211, lsl #16", + "mov x5, #0x58", + "str w5, [x4]", + "0: wfi", + "b 0b", + ".balign 128", + + // --- Current EL with SPx (entries 4-7) --- + // Entry 4: Synchronous + "movz x4, #0x0211, lsl #16", + "mov x5, #0x58", + "str w5, [x4]", + "0: wfi", + "b 0b", + ".balign 128", + + // Entry 5: IRQ + "movz x4, #0x0211, lsl #16", + "mov x5, #0x58", + "str w5, [x4]", + "0: wfi", + "b 0b", + ".balign 128", + + // Entry 6: FIQ + "movz x4, #0x0211, lsl #16", + "mov x5, #0x58", + "str w5, [x4]", + "0: wfi", + "b 0b", + ".balign 128", + + // Entry 7: SError + "movz x4, #0x0211, lsl #16", + "mov x5, #0x58", + "str w5, [x4]", + "0: wfi", + "b 0b", + ".balign 128", + + // --- Lower EL AArch64 (entries 8-11) --- + // Entry 8: Synchronous + "movz x4, #0x0211, lsl #16", + "mov x5, #0x58", + "str w5, [x4]", + "0: wfi", + "b 0b", + ".balign 128", + + // Entry 9: IRQ + "movz x4, #0x0211, lsl #16", + "mov x5, #0x58", + "str w5, [x4]", + "0: wfi", + "b 0b", + ".balign 128", + + // Entry 10: FIQ + "movz x4, #0x0211, lsl #16", + "mov x5, #0x58", + "str w5, [x4]", + "0: wfi", + "b 0b", + ".balign 128", + + // Entry 11: SError + "movz x4, #0x0211, lsl #16", + "mov x5, #0x58", + "str w5, [x4]", + "0: wfi", + "b 0b", + ".balign 128", + + // --- Lower EL AArch32 (entries 12-15) --- + // Entry 12: Synchronous + "movz x4, #0x0211, lsl #16", + "mov x5, #0x58", + "str w5, [x4]", + "0: wfi", + "b 0b", + ".balign 128", + + // Entry 13: IRQ + "movz x4, #0x0211, lsl #16", + "mov x5, #0x58", + "str w5, [x4]", + "0: wfi", + "b 0b", + ".balign 128", + + // Entry 14: FIQ + "movz x4, #0x0211, lsl #16", + "mov x5, #0x58", + "str w5, [x4]", + "0: wfi", + "b 0b", + ".balign 128", + + // Entry 15: SError + "movz x4, #0x0211, lsl #16", + "mov x5, #0x58", + "str w5, [x4]", + "0: wfi", + "b 0b", + ".balign 128", +); diff --git a/parallels-loader/src/kernel_load.rs b/parallels-loader/src/kernel_load.rs new file mode 100644 index 00000000..f1da6664 --- /dev/null +++ b/parallels-loader/src/kernel_load.rs @@ -0,0 +1,359 @@ +//! Kernel ELF loader for UEFI. +//! +//! Reads the kernel-aarch64 ELF binary from the ESP filesystem, +//! loads PT_LOAD segments into physical memory, and locates the +//! `kernel_main` entry point by scanning the ELF symbol table. + +use uefi::boot; +use uefi::proto::media::file::{File, FileAttribute, FileInfo, FileMode}; +use uefi::proto::media::fs::SimpleFileSystem; +use uefi::CStr16; + +/// Path to the kernel binary on the ESP filesystem. +const KERNEL_PATH: &CStr16 = uefi::cstr16!("\\EFI\\BREENIX\\KERNEL"); + +/// ELF64 magic bytes. +const ELF_MAGIC: [u8; 4] = [0x7F, b'E', b'L', b'F']; + +/// ELF64 header offsets and constants. +const EI_CLASS_64: u8 = 2; +const EI_DATA_LSB: u8 = 1; +const EM_AARCH64: u16 = 183; +const PT_LOAD: u32 = 1; +const SHT_SYMTAB: u32 = 2; +const SHT_STRTAB: u32 = 3; +const STT_FUNC: u8 = 2; + +/// Result of loading the kernel. +pub struct LoadedKernel { + /// Physical address of kernel_main (to jump to after enabling HHDM). + pub entry_phys: u64, + /// ELF entry point (virtual address from ELF header, may be _start in boot.S). + pub elf_entry: u64, + /// Physical load address of the kernel (lowest PT_LOAD p_paddr). + pub load_base: u64, + /// Highest physical address used by kernel (for memory layout). + pub load_end: u64, +} + +/// Load the kernel ELF from the ESP filesystem. +/// +/// This must be called while UEFI boot services are still active. +pub fn load_kernel() -> Result { + // Open the ESP filesystem + let sfs_handle = boot::get_handle_for_protocol::() + .map_err(|_| "No SimpleFileSystem protocol")?; + let mut sfs = boot::open_protocol_exclusive::(sfs_handle) + .map_err(|_| "Failed to open SimpleFileSystem")?; + + let mut root = sfs.open_volume().map_err(|_| "Failed to open ESP volume")?; + + // Open the kernel file + let file_handle = root + .open(KERNEL_PATH, FileMode::Read, FileAttribute::empty()) + .map_err(|_| "Failed to open kernel file (\\EFI\\BREENIX\\KERNEL)")?; + + let mut file = file_handle + .into_regular_file() + .ok_or("Kernel path is not a regular file")?; + + // Get file size + let mut info_buf = [0u8; 512]; + let info = file + .get_info::(&mut info_buf) + .map_err(|_| "Failed to get kernel file info")?; + let file_size = info.file_size() as usize; + + log::info!("Kernel file size: {} bytes ({} KB)", file_size, file_size / 1024); + + if file_size < 64 { + return Err("Kernel file too small for ELF header"); + } + + // Allocate buffer and read the entire file + // Use UEFI pool allocation which is available during boot services + let elf_data = boot::allocate_pool(uefi::mem::memory_map::MemoryType::LOADER_DATA, file_size) + .map_err(|_| "Failed to allocate memory for kernel")?; + + let elf_buf = unsafe { core::slice::from_raw_parts_mut(elf_data.as_ptr(), file_size) }; + + let bytes_read = file.read(elf_buf).map_err(|_| "Failed to read kernel file")?; + if bytes_read != file_size { + return Err("Incomplete read of kernel file"); + } + + // Parse and load the ELF + let result = parse_and_load_elf(elf_buf); + + // Free the file buffer (segments are already copied to physical memory) + unsafe { + boot::free_pool(elf_data).ok(); + } + + result +} + +/// Parse the ELF header, load PT_LOAD segments, and find kernel_main. +fn parse_and_load_elf(elf: &[u8]) -> Result { + // Validate ELF magic + if elf.len() < 64 || elf[0..4] != ELF_MAGIC { + return Err("Invalid ELF magic"); + } + if elf[4] != EI_CLASS_64 { + return Err("Not ELF64"); + } + if elf[5] != EI_DATA_LSB { + return Err("Not little-endian"); + } + + let e_machine = read_u16(elf, 18); + if e_machine != EM_AARCH64 { + return Err("Not AArch64 ELF"); + } + + let e_entry = read_u64(elf, 24); + let e_phoff = read_u64(elf, 32) as usize; + let e_shoff = read_u64(elf, 40) as usize; + let e_phentsize = read_u16(elf, 54) as usize; + let e_phnum = read_u16(elf, 56) as usize; + let e_shentsize = read_u16(elf, 58) as usize; + let e_shnum = read_u16(elf, 60) as usize; + let e_shstrndx = read_u16(elf, 62) as usize; + + log::info!( + "ELF: entry={:#x}, {} phdrs, {} shdrs", + e_entry, e_phnum, e_shnum + ); + + // Load PT_LOAD segments into physical memory + let mut load_base = u64::MAX; + let mut load_end = 0u64; + let mut vaddr_to_paddr_offset: i64 = 0; + + for i in 0..e_phnum { + let ph_offset = e_phoff + i * e_phentsize; + if ph_offset + e_phentsize > elf.len() { + continue; + } + + let p_type = read_u32(elf, ph_offset); + if p_type != PT_LOAD { + continue; + } + + let p_offset = read_u64(elf, ph_offset + 8) as usize; + let p_vaddr = read_u64(elf, ph_offset + 16); + let p_paddr = read_u64(elf, ph_offset + 24); + let p_filesz = read_u64(elf, ph_offset + 32) as usize; + let p_memsz = read_u64(elf, ph_offset + 40) as usize; + + log::info!( + " LOAD: vaddr={:#x} paddr={:#x} filesz={:#x} memsz={:#x}", + p_vaddr, p_paddr, p_filesz, p_memsz + ); + + // Track the vaddr→paddr mapping for symbol resolution + if p_filesz > 0 { + vaddr_to_paddr_offset = p_paddr as i64 - p_vaddr as i64; + } + + // Copy file data to physical address + if p_filesz > 0 { + let src = &elf[p_offset..p_offset + p_filesz]; + let dst = p_paddr as *mut u8; + unsafe { + core::ptr::copy_nonoverlapping(src.as_ptr(), dst, p_filesz); + } + } + + // Zero BSS (memsz > filesz) + if p_memsz > p_filesz { + let bss_start = (p_paddr + p_filesz as u64) as *mut u8; + let bss_size = p_memsz - p_filesz; + unsafe { + core::ptr::write_bytes(bss_start, 0, bss_size); + } + } + + load_base = load_base.min(p_paddr); + load_end = load_end.max(p_paddr + p_memsz as u64); + } + + if load_base == u64::MAX { + return Err("No PT_LOAD segments found"); + } + + log::info!("Kernel loaded at phys {:#x}-{:#x}", load_base, load_end); + + // Try to find kernel_main symbol + let kernel_main_phys = find_symbol(elf, "kernel_main", e_shoff, e_shentsize, e_shnum, e_shstrndx) + .map(|vaddr| (vaddr as i64 + vaddr_to_paddr_offset) as u64); + + let entry_phys = match kernel_main_phys { + Some(addr) => { + log::info!("Found kernel_main at phys {:#x}", addr); + addr + } + None => { + // Fallback: use ELF entry point with vaddr→paddr translation. + // If e_entry is already a low physical address (e.g., boot.S _start), + // skip the offset to avoid corrupting it into an unmapped VA. + let fallback = if e_entry < 0x1_0000_0000 { + e_entry // Already a physical address (boot code) + } else { + (e_entry as i64 + vaddr_to_paddr_offset) as u64 + }; + log::warn!("kernel_main not found, using ELF entry {:#x} (phys {:#x})", e_entry, fallback); + fallback + } + }; + + Ok(LoadedKernel { + entry_phys, + elf_entry: e_entry, + load_base, + load_end, + }) +} + +/// Find a symbol by name in the ELF symbol table. +/// +/// Returns the symbol's virtual address (st_value) if found. +fn find_symbol( + elf: &[u8], + name: &str, + e_shoff: usize, + e_shentsize: usize, + e_shnum: usize, + e_shstrndx: usize, +) -> Option { + if e_shoff == 0 || e_shnum == 0 { + return None; + } + + // First, find the section name string table + let shstr_offset = e_shoff + e_shstrndx * e_shentsize; + if shstr_offset + e_shentsize > elf.len() { + return None; + } + let _shstr_sh_offset = read_u64(elf, shstr_offset + 24) as usize; + let _shstr_sh_size = read_u64(elf, shstr_offset + 32) as usize; + + // Find .symtab and its associated .strtab + let mut symtab_offset = 0usize; + let mut symtab_size = 0usize; + let mut symtab_entsize = 0usize; + let mut symtab_link = 0u32; // Index of associated string table + let mut strtab_offset = 0usize; + let mut _strtab_size = 0usize; + + for i in 0..e_shnum { + let sh = e_shoff + i * e_shentsize; + if sh + e_shentsize > elf.len() { + continue; + } + + let sh_type = read_u32(elf, sh + 4); + if sh_type == SHT_SYMTAB { + symtab_offset = read_u64(elf, sh + 24) as usize; + symtab_size = read_u64(elf, sh + 32) as usize; + symtab_entsize = read_u64(elf, sh + 56) as usize; + symtab_link = read_u32(elf, sh + 40); // ELF64: sh_link is at offset 40 + } + } + + if symtab_offset == 0 || symtab_entsize == 0 { + // No symbol table - try .dynsym + for i in 0..e_shnum { + let sh = e_shoff + i * e_shentsize; + if sh + e_shentsize > elf.len() { + continue; + } + let sh_name_idx = read_u32(elf, sh) as usize; + let sh_type = read_u32(elf, sh + 4); + + // Check if this is .dynsym + if sh_type == 11 { + // SHT_DYNSYM + symtab_offset = read_u64(elf, sh + 24) as usize; + symtab_size = read_u64(elf, sh + 32) as usize; + symtab_entsize = read_u64(elf, sh + 56) as usize; + symtab_link = read_u32(elf, sh + 40); // ELF64: sh_link is at offset 40 + } + let _ = sh_name_idx; // Suppress warning + } + } + + if symtab_offset == 0 || symtab_entsize == 0 { + return None; + } + + // Find the associated string table + let strtab_sh = e_shoff + (symtab_link as usize) * e_shentsize; + if strtab_sh + e_shentsize <= elf.len() { + strtab_offset = read_u64(elf, strtab_sh + 24) as usize; + _strtab_size = read_u64(elf, strtab_sh + 32) as usize; + } + + if strtab_offset == 0 { + return None; + } + + // Iterate symbols looking for kernel_main + let num_symbols = symtab_size / symtab_entsize; + for i in 0..num_symbols { + let sym = symtab_offset + i * symtab_entsize; + if sym + symtab_entsize > elf.len() { + break; + } + + let st_name = read_u32(elf, sym) as usize; + let st_info = elf[sym + 4]; + let st_value = read_u64(elf, sym + 8); + + // Check if it's a function + let st_type = st_info & 0xF; + if st_type != STT_FUNC || st_value == 0 { + continue; + } + + // Compare name + let name_offset = strtab_offset + st_name; + if name_offset >= elf.len() { + continue; + } + + let sym_name = read_cstr(elf, name_offset); + if sym_name == name { + return Some(st_value); + } + } + + None +} + +/// Read a null-terminated string from a byte slice. +fn read_cstr(data: &[u8], offset: usize) -> &str { + let start = offset; + let mut end = start; + while end < data.len() && data[end] != 0 { + end += 1; + } + core::str::from_utf8(&data[start..end]).unwrap_or("") +} + +// Little-endian read helpers +fn read_u16(data: &[u8], offset: usize) -> u16 { + u16::from_le_bytes([data[offset], data[offset + 1]]) +} + +fn read_u32(data: &[u8], offset: usize) -> u32 { + u32::from_le_bytes([data[offset], data[offset + 1], data[offset + 2], data[offset + 3]]) +} + +fn read_u64(data: &[u8], offset: usize) -> u64 { + u64::from_le_bytes([ + data[offset], data[offset + 1], data[offset + 2], data[offset + 3], + data[offset + 4], data[offset + 5], data[offset + 6], data[offset + 7], + ]) +} diff --git a/parallels-loader/src/main.rs b/parallels-loader/src/main.rs new file mode 100644 index 00000000..7ae021f1 --- /dev/null +++ b/parallels-loader/src/main.rs @@ -0,0 +1,242 @@ +#![no_main] +#![no_std] +#![allow(dead_code)] + +mod acpi_discovery; +mod gop_discovery; +pub mod hw_config; +mod kernel_entry; +mod kernel_load; +mod page_tables; + +use uefi::prelude::*; +use uefi::mem::memory_map::{MemoryMap, MemoryType}; +use uefi::table::cfg::ACPI2_GUID; + +use hw_config::HardwareConfig; +use page_tables::PageTableStorage; + +/// Page table storage - static so it survives ExitBootServices. +static mut PAGE_TABLES: PageTableStorage = PageTableStorage::new(); + +/// HardwareConfig - static so it survives ExitBootServices. +static mut HW_CONFIG: HardwareConfig = unsafe { core::mem::zeroed() }; + +#[entry] +fn main() -> Status { + uefi::helpers::init().unwrap(); + + log::info!("==========================================="); + log::info!(" Breenix Parallels Loader v0.1.0"); + log::info!(" UEFI ARM64 Boot Application"); + log::info!("==========================================="); + + // Initialize HW_CONFIG with proper magic/version + let config = unsafe { + let ptr = &raw mut HW_CONFIG; + *ptr = HardwareConfig::new(); + &mut *ptr + }; + + // Find RSDP from UEFI configuration tables + let rsdp_addr = find_rsdp(); + let rsdp_addr = match rsdp_addr { + Some(addr) => { + log::info!("RSDP found at: 0x{:016x}", addr); + config.rsdp_addr = addr as u64; + addr + } + None => { + log::error!("RSDP not found in UEFI configuration tables!"); + halt(); + } + }; + + // Discover hardware via ACPI + log::info!("--- ACPI Discovery ---"); + match acpi_discovery::discover_hardware(rsdp_addr, config) { + Ok(()) => { + log::info!("--- Discovery Complete ---"); + log::info!(" UART: 0x{:08x} (IRQ {})", config.uart_base_phys, config.uart_irq); + log::info!( + " GICv{}: GICD=0x{:08x}", + config.gic_version, + config.gicd_base + ); + if config.gicr_range_count > 0 { + log::info!( + " GICR=0x{:08x} ({}x ranges)", + config.gicr_ranges[0].base, + config.gicr_range_count + ); + } + if config.pci_ecam_base != 0 { + log::info!( + " PCI: ECAM=0x{:08x}, MMIO=0x{:08x}", + config.pci_ecam_base, + config.pci_mmio_base + ); + } + log::info!(" Timer: {} MHz", config.timer_freq_hz / 1_000_000); + } + Err(e) => { + log::error!("ACPI discovery failed: {}", e); + halt(); + } + } + + // Load kernel ELF from ESP + log::info!("--- Loading Kernel ---"); + let loaded_kernel = match kernel_load::load_kernel() { + Ok(k) => { + log::info!("Kernel loaded: entry_phys={:#x}, range={:#x}-{:#x}", + k.entry_phys, k.load_base, k.load_end); + k + } + Err(e) => { + log::error!("Failed to load kernel: {}", e); + halt(); + } + }; + + // Populate RAM regions from UEFI memory map. + // We need to get the memory map before ExitBootServices. + populate_ram_regions(config); + + log::info!("RAM regions: {} regions found", config.ram_region_count); + for i in 0..config.ram_region_count as usize { + if i >= config.ram_regions.len() { + break; + } + let r = &config.ram_regions[i]; + log::info!(" RAM: {:#x} - {:#x} ({} MB)", + r.base, r.base + r.size, r.size / (1024 * 1024)); + } + + // Discover UEFI GOP framebuffer (optional — kernel works without display) + log::info!("--- GOP Framebuffer Discovery ---"); + match gop_discovery::discover_gop(config) { + Ok(()) => { + log::info!("GOP framebuffer: {}x{} stride={} fmt={} base={:#x} size={:#x}", + config.framebuffer.width, + config.framebuffer.height, + config.framebuffer.stride, + if config.framebuffer.pixel_format == 1 { "BGR" } else { "RGB" }, + config.framebuffer.base, + config.framebuffer.size); + } + Err(e) => { + log::warn!("GOP not available: {} (continuing without display)", e); + } + } + + log::info!("--- Exiting Boot Services ---"); + + // Exit UEFI boot services. After this, NO UEFI calls are possible. + // The memory map is required for ExitBootServices. + unsafe { + let _ = uefi::boot::exit_boot_services(MemoryType::LOADER_DATA); + } + + // Jump to kernel with our page tables and HardwareConfig + let page_tables = unsafe { &mut *(&raw mut PAGE_TABLES) }; + let hw_config = unsafe { &*(&raw const HW_CONFIG) }; + + kernel_entry::jump_to_kernel(loaded_kernel.entry_phys, hw_config, page_tables); +} + +/// Populate HardwareConfig RAM regions from the UEFI memory map. +/// +/// Scans the UEFI memory map for conventional memory (usable RAM) regions +/// and adds them to the HardwareConfig. The kernel uses these to configure +/// its frame allocator. +fn populate_ram_regions(config: &mut HardwareConfig) { + // Get the UEFI memory map + let buf = [0u8; 8192]; + let memory_map = match uefi::boot::memory_map(MemoryType::LOADER_DATA) { + Ok(map) => map, + Err(_) => { + log::warn!("Failed to get UEFI memory map for RAM regions"); + return; + } + }; + + let mut count = 0usize; + + for desc in memory_map.entries() { + // Only count conventional memory (usable RAM) + let mem_type = desc.ty; + let is_usable = matches!( + mem_type, + MemoryType::CONVENTIONAL + | MemoryType::BOOT_SERVICES_CODE + | MemoryType::BOOT_SERVICES_DATA + ); + + if !is_usable { + continue; + } + + let base = desc.phys_start; + let size = desc.page_count * 4096; + + // Merge with previous region if contiguous + if count > 0 { + let prev = &mut config.ram_regions[count - 1]; + if prev.base + prev.size == base { + prev.size += size; + continue; + } + } + + if count < config.ram_regions.len() { + config.ram_regions[count] = hw_config::RamRegion { base, size }; + count += 1; + } + } + + config.ram_region_count = count as u32; + + let _ = buf; // Suppress unused warning +} + +/// Find the ACPI RSDP address from UEFI configuration tables. +fn find_rsdp() -> Option { + let st = uefi::table::system_table_raw().expect("no system table"); + + // Safety: we're in boot services, system table is valid + let st_ref = unsafe { st.as_ref() }; + + // Iterate configuration tables looking for ACPI 2.0 RSDP + let cfg_entries = st_ref.number_of_configuration_table_entries; + let cfg_table = st_ref.configuration_table; + + if cfg_table.is_null() || cfg_entries == 0 { + return None; + } + + let entries = unsafe { core::slice::from_raw_parts(cfg_table, cfg_entries) }; + + for entry in entries { + if entry.vendor_guid == ACPI2_GUID { + return Some(entry.vendor_table as usize); + } + } + + // Fall back to ACPI 1.0 RSDP + let acpi1_guid = uefi::table::cfg::ACPI_GUID; + for entry in entries { + if entry.vendor_guid == acpi1_guid { + return Some(entry.vendor_table as usize); + } + } + + None +} + +/// Halt the CPU in an infinite loop (unrecoverable error). +fn halt() -> ! { + loop { + unsafe { core::arch::asm!("wfi") }; + } +} diff --git a/parallels-loader/src/page_tables.rs b/parallels-loader/src/page_tables.rs new file mode 100644 index 00000000..ea3a2eac --- /dev/null +++ b/parallels-loader/src/page_tables.rs @@ -0,0 +1,192 @@ +/// AArch64 page table setup for the UEFI-to-kernel transition. +/// +/// Builds two-level page tables: +/// TTBR0: Identity map (VA = PA) for RAM + device MMIO +/// TTBR1: Higher-Half Direct Map (HHDM) at 0xFFFF_0000_0000_0000 +/// +/// Uses 1GB block mappings (L1) where possible, 2MB blocks (L2) for +/// device regions that need non-cacheable attributes. + +use core::ptr; + +/// HHDM base address matching the kernel's expectation. +const HHDM_BASE: u64 = 0xFFFF_0000_0000_0000; + +/// Page table attributes for AArch64 (stage 1, EL1). +mod attr { + /// Block descriptor valid bit + pub const VALID: u64 = 1 << 0; + /// Block descriptor (not table) for L1/L2 + pub const BLOCK: u64 = 0 << 1; + /// Table descriptor for L1/L2 + pub const TABLE: u64 = 1 << 1; + /// Page descriptor for L3 + pub const PAGE: u64 = 1 << 1; + + /// AttrIndx[2:0] in bits [4:2] + /// MUST match kernel boot.S MAIR layout: + /// Index 0 = Device-nGnRnE (0x00) + /// Index 1 = Normal WB-WA (0xFF) + pub const ATTR_IDX_DEVICE: u64 = 0 << 2; // MAIR index 0: Device-nGnRnE + pub const ATTR_IDX_NORMAL: u64 = 1 << 2; // MAIR index 1: Normal WB + + /// Access flag (must be set, or access fault) + pub const AF: u64 = 1 << 10; + + /// Shareability + pub const ISH: u64 = 3 << 8; // Inner Shareable + pub const OSH: u64 = 2 << 8; // Outer Shareable + + /// Access permissions + pub const AP_RW_EL1: u64 = 0 << 6; // EL1 read/write + pub const AP_RW_ALL: u64 = 1 << 6; // EL1+EL0 read/write + + /// Execute-never bits + pub const UXN: u64 = 1 << 54; // Unprivileged execute never + pub const PXN: u64 = 1 << 53; // Privileged execute never + + /// Normal memory block: cacheable, inner shareable + pub const NORMAL_BLOCK: u64 = VALID | BLOCK | ATTR_IDX_NORMAL | AF | ISH | AP_RW_EL1; + + /// Device memory block: non-cacheable, outer shareable, execute-never + pub const DEVICE_BLOCK: u64 = VALID | BLOCK | ATTR_IDX_DEVICE | AF | OSH | AP_RW_EL1 | UXN | PXN; + + /// Table descriptor (points to next level) + pub const TABLE_DESC: u64 = VALID | TABLE; +} + +/// MAIR (Memory Attribute Indirection Register) value. +/// MUST match kernel boot.S layout: +/// Index 0: Device-nGnRnE (0x00) +/// Index 1: Normal WB cacheable (0xFF = Inner WB RA WA, Outer WB RA WA) +pub const MAIR_VALUE: u64 = 0x00_00_00_00_00_00_FF00; + +/// TCR (Translation Control Register) value for 4K granule, 48-bit VA. +/// T0SZ = 16 (48-bit VA for TTBR0) +/// T1SZ = 16 (48-bit VA for TTBR1) +/// TG0 = 0b00 (4KB granule for TTBR0) +/// TG1 = 0b10 (4KB granule for TTBR1) +/// SH0 = 0b11 (Inner Shareable for TTBR0) +/// SH1 = 0b11 (Inner Shareable for TTBR1) +/// ORGN0/IRGN0 = 0b01 (Write-Back, Write-Allocate for TTBR0) +/// ORGN1/IRGN1 = 0b01 (Write-Back, Write-Allocate for TTBR1) +/// IPS = 0b010 (40-bit PA, 1TB - sufficient for Parallels) +pub const TCR_VALUE: u64 = (16 << 0) // T0SZ + | (0b01 << 8) // IRGN0 = WB WA + | (0b01 << 10) // ORGN0 = WB WA + | (0b11 << 12) // SH0 = Inner Shareable + | (0b00 << 14) // TG0 = 4KB + | (16 << 16) // T1SZ + | (0b01 << 24) // IRGN1 = WB WA + | (0b01 << 26) // ORGN1 = WB WA + | (0b11 << 28) // SH1 = Inner Shareable + | (0b10 << 30) // TG1 = 4KB + | (0b010 << 32); // IPS = 40-bit + +/// Size of a single page table (4KB, 512 entries of 8 bytes each). +const PAGE_TABLE_SIZE: usize = 4096; + +/// Number of page tables we pre-allocate. +/// L0 (TTBR0): 1 +/// L0 (TTBR1): 1 +/// L1 (TTBR0): 1 (covers 512GB) +/// L1 (TTBR1): 1 (covers 512GB) +/// L2 (for device regions): 2 (for 0x00000000-0x3FFFFFFF, 0x10000000-0x1FFFFFFF) +const MAX_PAGE_TABLES: usize = 8; + +/// Page table storage. Allocated in the loader's BSS. +/// Must be 4KB aligned. +#[repr(C, align(4096))] +pub struct PageTableStorage { + tables: [[u64; 512]; MAX_PAGE_TABLES], + next_table: usize, +} + +impl PageTableStorage { + pub const fn new() -> Self { + Self { + tables: [[0u64; 512]; MAX_PAGE_TABLES], + next_table: 0, + } + } + + /// Allocate a new zeroed page table, return its physical address. + fn alloc_table(&mut self) -> u64 { + assert!(self.next_table < MAX_PAGE_TABLES, "out of page tables"); + let idx = self.next_table; + self.next_table += 1; + // Zero the table + for entry in &mut self.tables[idx] { + *entry = 0; + } + &self.tables[idx] as *const [u64; 512] as u64 + } +} + +/// Build page tables for the kernel. +/// +/// Returns (ttbr0_phys, ttbr1_phys) - the physical addresses of the +/// L0 page tables for the identity map and HHDM respectively. +/// +/// Memory map covered: +/// Identity (TTBR0): +/// 0x00000000-0x3FFFFFFF: Device MMIO (GIC, UART, PCI ECAM, PCI MMIO) - device memory +/// 0x40000000-0xBFFFFFFF: RAM (2GB) - normal cacheable +/// +/// HHDM (TTBR1): +/// 0xFFFF_0000_0000_0000 + phys = virt for all of the above +pub fn build_page_tables(storage: &mut PageTableStorage) -> (u64, u64) { + // Allocate L0 tables + let ttbr0_l0 = storage.alloc_table(); + let ttbr1_l0 = storage.alloc_table(); + + // Allocate L1 tables + let ttbr0_l1 = storage.alloc_table(); + let ttbr1_l1 = storage.alloc_table(); + + // TTBR0 L0[0] -> L1 (covers VA 0x0000_0000_0000_0000 - 0x0000_007F_FFFF_FFFF) + write_entry(ttbr0_l0, 0, ttbr0_l1 | attr::TABLE_DESC); + + // TTBR1 L0[0] -> L1 (covers VA 0xFFFF_0000_0000_0000 - 0xFFFF_007F_FFFF_FFFF) + write_entry(ttbr1_l0, 0, ttbr1_l1 | attr::TABLE_DESC); + + // --- Device MMIO region: 0x00000000 - 0x3FFFFFFF (1GB) --- + // Need L2 for fine-grained device mapping + let ttbr0_l2_dev = storage.alloc_table(); + let ttbr1_l2_dev = storage.alloc_table(); + + // TTBR0 L1[0] -> L2 (0x00000000 - 0x3FFFFFFF) + write_entry(ttbr0_l1, 0, ttbr0_l2_dev | attr::TABLE_DESC); + // TTBR1 L1[0] -> L2 (HHDM + 0x00000000 - 0x3FFFFFFF) + write_entry(ttbr1_l1, 0, ttbr1_l2_dev | attr::TABLE_DESC); + + // Map all 2MB blocks in 0x00000000-0x3FFFFFFF as device memory + // This covers: GIC (0x02010000), UART (0x02110000), PCI ECAM (0x02300000), + // GICR (0x02500000), PCI MMIO (0x10000000-0x1FFFFFFF) + for i in 0..512u64 { + let phys = i * 0x20_0000; // 2MB blocks + write_entry(ttbr0_l2_dev, i as usize, phys | attr::DEVICE_BLOCK); + write_entry(ttbr1_l2_dev, i as usize, phys | attr::DEVICE_BLOCK); + } + + // --- RAM: 0x40000000 - 0xBFFFFFFF (2GB, L1 entries 1-2) --- + // Use 1GB block mappings for RAM (much simpler, fewer TLB entries) + // L1[1] = 0x40000000 - 0x7FFFFFFF (1GB block, normal memory) + write_entry(ttbr0_l1, 1, 0x4000_0000 | attr::NORMAL_BLOCK); + write_entry(ttbr1_l1, 1, 0x4000_0000 | attr::NORMAL_BLOCK); + + // L1[2] = 0x80000000 - 0xBFFFFFFF (1GB block, normal memory) + write_entry(ttbr0_l1, 2, 0x8000_0000 | attr::NORMAL_BLOCK); + write_entry(ttbr1_l1, 2, 0x8000_0000 | attr::NORMAL_BLOCK); + + (ttbr0_l0, ttbr1_l0) +} + +/// Write a page table entry. +#[inline] +fn write_entry(table_phys: u64, index: usize, value: u64) { + unsafe { + let entry_ptr = (table_phys as *mut u64).add(index); + ptr::write_volatile(entry_ptr, value); + } +} diff --git a/run.sh b/run.sh index 67cfcfba..7868d889 100755 --- a/run.sh +++ b/run.sh @@ -11,6 +11,8 @@ # ./run.sh --x86 --headless # x86_64 with serial output only # ./run.sh --no-build # Skip all builds, use existing artifacts # ./run.sh --resolution 1920x1080 # Custom resolution +# ./run.sh --parallels # Build and boot on Parallels Desktop +# ./run.sh --parallels --no-build # Deploy existing build to Parallels # ./run.sh --btrt # ARM64 BTRT structured boot test # ./run.sh --btrt --x86 # x86_64 BTRT structured boot test # @@ -31,6 +33,8 @@ HEADLESS=false CLEAN=false NO_BUILD=false BTRT=false +PARALLELS=false +PARALLELS_VM="breenix-dev" DEBUG=false REBUILD_HOME=false RESOLUTION="" @@ -54,6 +58,15 @@ while [[ $# -gt 0 ]]; do NO_BUILD=true shift ;; + --parallels) + PARALLELS=true + ARCH="arm64" + shift + ;; + --vm) + PARALLELS_VM="$2" + shift 2 + ;; --btrt) BTRT=true shift @@ -87,6 +100,8 @@ while [[ $# -gt 0 ]]; do echo " --no-build Skip all builds, use existing artifacts" echo " --x86, --x86_64, --amd64 Run x86_64 kernel (default: ARM64)" echo " --arm64, --aarch64 Run ARM64 kernel (default)" + echo " --parallels Build and boot on Parallels Desktop VM" + echo " --vm NAME Parallels VM name (default: breenix-dev)" echo " --headless, --serial Run without display (serial only)" echo " --graphics, --vnc Run with VNC display (default)" echo " --btrt Run BTRT structured boot test" @@ -128,6 +143,235 @@ if [ "$BTRT" = true ]; then exec cargo run -p xtask -- boot-test-btrt --arch "$BTRT_ARCH" fi +# Parallels mode: build, deploy, boot on Parallels Desktop +if [ "$PARALLELS" = true ]; then + echo "" + echo "=========================================" + echo "Breenix on Parallels Desktop" + echo "=========================================" + echo "" + + if ! command -v prlctl &>/dev/null; then + echo "ERROR: prlctl not found. Is Parallels Desktop installed?" + exit 1 + fi + if ! command -v prl_disk_tool &>/dev/null; then + echo "ERROR: prl_disk_tool not found. Is Parallels Desktop installed?" + exit 1 + fi + + PARALLELS_DIR="$BREENIX_ROOT/target/parallels" + SERIAL_LOG="/tmp/breenix-parallels-serial.log" + HDD_DIR="$PARALLELS_DIR/breenix-efi.hdd" + EXT2_HDD_DIR="$PARALLELS_DIR/breenix-ext2.hdd" + EXT2_DISK="$BREENIX_ROOT/target/ext2-aarch64.img" + + if [ "$NO_BUILD" = true ]; then + echo "Skipping builds (--no-build)" + if [ ! -d "$HDD_DIR" ]; then + echo "ERROR: No Parallels disk found at $HDD_DIR" + echo "Run without --no-build first to create it." + exit 1 + fi + else + # Build the UEFI loader + echo "[1/6] Building UEFI loader..." + cargo build --release --target aarch64-unknown-uefi -p parallels-loader + + # Build the kernel + echo "" + echo "[2/6] Building kernel..." + cargo build --release --target aarch64-breenix.json \ + -Z build-std=core,alloc \ + -Z build-std-features=compiler-builtins-mem \ + -p kernel --bin kernel-aarch64 + + LOADER_EFI="$BREENIX_ROOT/target/aarch64-unknown-uefi/release/parallels-loader.efi" + KERNEL_ELF="$BREENIX_ROOT/target/aarch64-breenix/release/kernel-aarch64" + + if [ ! -f "$LOADER_EFI" ]; then + echo "ERROR: UEFI loader not found at $LOADER_EFI" + exit 1 + fi + if [ ! -f "$KERNEL_ELF" ]; then + echo "ERROR: Kernel not found at $KERNEL_ELF" + exit 1 + fi + + # Build userspace binaries and create ext2 data disk + echo "" + echo "[3/6] Building userspace binaries (aarch64)..." + if "$BREENIX_ROOT/userspace/programs/build.sh" --arch aarch64; then + echo " Userspace build successful" + else + echo " WARNING: Userspace build failed (rust-fork may not be set up)" + echo " Continuing without userspace binaries — ext2 will still have test files" + fi + + echo "" + echo "[4/6] Creating ext2 data disk image..." + "$BREENIX_ROOT/scripts/create_ext2_disk.sh" --arch aarch64 + + echo "" + echo "[5/6] Creating FAT32 EFI disk image..." + mkdir -p "$PARALLELS_DIR" + + # Create GPT+FAT32 disk image using hdiutil (native macOS, no mtools needed) + DMG_PATH="$PARALLELS_DIR/efi-temp.dmg" + rm -f "$DMG_PATH" + hdiutil create -size 64m -fs FAT32 -volname BREENIX -layout GPTSPUD "$DMG_PATH" >/dev/null 2>&1 + + # Mount the DMG, copy loader + kernel to ESP layout + VOLUME=$(hdiutil attach "$DMG_PATH" 2>/dev/null | grep -o '/Volumes/[^ ]*' | head -1) + if [ -z "$VOLUME" ] || [ ! -d "$VOLUME" ]; then + echo "ERROR: Failed to mount FAT32 disk image" + rm -f "$DMG_PATH" + exit 1 + fi + + mkdir -p "$VOLUME/EFI/BOOT" + mkdir -p "$VOLUME/EFI/BREENIX" + cp "$LOADER_EFI" "$VOLUME/EFI/BOOT/BOOTAA64.EFI" + cp "$KERNEL_ELF" "$VOLUME/EFI/BREENIX/KERNEL" + hdiutil detach "$VOLUME" >/dev/null 2>&1 + + echo " Loader: $(stat -f%z "$LOADER_EFI") bytes" + echo " Kernel: $(stat -f%z "$KERNEL_ELF") bytes" + + # Convert DMG to raw disk image + echo "" + echo "[6/6] Creating Parallels disks..." + RAW_IMG="$PARALLELS_DIR/efi-raw.img" + rm -f "$RAW_IMG" "${RAW_IMG}.cdr" + hdiutil convert "$DMG_PATH" -format UDTO -o "$RAW_IMG" >/dev/null 2>&1 + mv "${RAW_IMG}.cdr" "$RAW_IMG" + rm -f "$DMG_PATH" + + # Patch GPT partition type from "Microsoft Basic Data" to "EFI System Partition" + # so UEFI firmware recognizes the ESP and auto-boots BOOTAA64.EFI + python3 "$BREENIX_ROOT/scripts/parallels/patch-gpt-esp.py" "$RAW_IMG" + + # Wrap EFI disk in Parallels .hdd format + rm -rf "$HDD_DIR" + prl_disk_tool create --hdd "$HDD_DIR" --size 64M >/dev/null 2>&1 + HDS_FILE=$(find "$HDD_DIR" -name "*.hds" | head -1) + if [ -z "$HDS_FILE" ]; then + echo "ERROR: No .hds file found in $HDD_DIR" + rm -f "$RAW_IMG" + exit 1 + fi + cp "$RAW_IMG" "$HDS_FILE" + rm -f "$RAW_IMG" + echo " EFI disk: $HDD_DIR" + + # Wrap ext2 data disk in Parallels .hdd format + if [ -f "$EXT2_DISK" ]; then + EXT2_SIZE_MB=$(( $(stat -f%z "$EXT2_DISK") / 1048576 )) + rm -rf "$EXT2_HDD_DIR" + prl_disk_tool create --hdd "$EXT2_HDD_DIR" --size "${EXT2_SIZE_MB}M" >/dev/null 2>&1 + HDS_FILE=$(find "$EXT2_HDD_DIR" -name "*.hds" | head -1) + if [ -z "$HDS_FILE" ]; then + echo "WARNING: No .hds file in $EXT2_HDD_DIR, ext2 disk won't be attached" + else + cp "$EXT2_DISK" "$HDS_FILE" + echo " ext2 disk: $EXT2_HDD_DIR (${EXT2_SIZE_MB}MB)" + fi + else + echo "WARNING: ext2 disk not found at $EXT2_DISK" + fi + fi + + echo "" + echo "--- Configuring Parallels VM '$PARALLELS_VM' ---" + + # Create VM if it doesn't exist + if ! prlctl list --all 2>/dev/null | grep -q "$PARALLELS_VM"; then + echo "Creating VM '$PARALLELS_VM'..." + prlctl create "$PARALLELS_VM" --ostype linux --distribution linux --no-hdd + prlctl set "$PARALLELS_VM" --memsize 2048 + prlctl set "$PARALLELS_VM" --cpus 4 + fi + + # Stop VM if running + VM_STATUS=$(prlctl status "$PARALLELS_VM" 2>/dev/null | awk '{print $NF}') + if [ "$VM_STATUS" = "running" ] || [ "$VM_STATUS" = "paused" ] || [ "$VM_STATUS" = "suspended" ]; then + echo "Stopping VM..." + prlctl stop "$PARALLELS_VM" --kill 2>/dev/null || true + sleep 2 + fi + + # Configure VM: EFI boot, remove all SATA devices (hdds + cdroms), attach our disks + prlctl set "$PARALLELS_VM" --efi-boot on 2>/dev/null || true + # Remove any existing hard disks and CD-ROM devices to free SATA positions + for dev in hdd0 hdd1 hdd2 hdd3 cdrom0 cdrom1; do + prlctl set "$PARALLELS_VM" --device-del "$dev" 2>/dev/null || true + done + # Attach EFI boot disk as hdd0 (sata:0) + prlctl set "$PARALLELS_VM" --device-add hdd --image "$HDD_DIR" --type plain --position 0 + # Attach ext2 data disk as hdd1 (sata:1) — kernel probes all AHCI devices for ext2 magic + if [ -d "$EXT2_HDD_DIR" ]; then + prlctl set "$PARALLELS_VM" --device-add hdd --image "$EXT2_HDD_DIR" --type plain --position 1 + echo " hdd0: EFI boot disk (FAT32) at sata:0" + echo " hdd1: ext2 data disk at sata:1" + else + echo " hdd0: EFI boot disk (FAT32) at sata:0" + echo " WARNING: No ext2 disk to attach (run without --no-build to create it)" + fi + + # Set boot order: hard disk first (default boots from cdrom which hangs) + prlctl set "$PARALLELS_VM" --device-bootorder "hdd0" 2>/dev/null || true + + # Configure serial port output to file + prlctl set "$PARALLELS_VM" --device-del serial0 2>/dev/null || true + prlctl set "$PARALLELS_VM" --device-add serial --output "$SERIAL_LOG" 2>/dev/null || true + # Serial port must be explicitly connected or it stays disconnected + prlctl set "$PARALLELS_VM" --device-set serial0 --connect 2>/dev/null || true + + # Delete NVRAM to ensure fresh UEFI boot state (avoids stale boot entries) + VM_DIR="$HOME/Parallels/${PARALLELS_VM}.pvm" + if [ -f "$VM_DIR/NVRAM.dat" ]; then + rm -f "$VM_DIR/NVRAM.dat" + fi + + echo "" + echo "--- Starting VM ---" + > "$SERIAL_LOG" # Truncate serial log + prlctl start "$PARALLELS_VM" + + echo "" + echo "=========================================" + echo "Breenix running on Parallels" + echo "=========================================" + echo "VM: $PARALLELS_VM" + echo "Serial: $SERIAL_LOG" + echo "Stop: prlctl stop $PARALLELS_VM --kill" + echo "" + echo "Tailing serial output (Ctrl+C to detach)..." + echo "" + + # Monitor Parallels VM log for VCPU exceptions in background + VM_DIR="$HOME/Parallels/${PARALLELS_VM}.pvm" + VM_LOG="$VM_DIR/parallels.log" + if [ -f "$VM_LOG" ]; then + ( + tail -f "$VM_LOG" 2>/dev/null | while IFS= read -r line; do + case "$line" in + *VCPU*|*Exception*|*Synchronous*|*fault*|*FAULT*|*recursive*) + echo "[parallels-log] $line" ;; + esac + done + ) & + LOGMON_PID=$! + trap "kill $LOGMON_PID 2>/dev/null" EXIT + fi + + # Wait a moment for the VM to start producing output, then tail + sleep 1 + tail -f "$SERIAL_LOG" + + exit 0 +fi + # Route to architecture-specific runner if [ "$ARCH" = "arm64" ]; then # ARM64 path - direct kernel boot diff --git a/scripts/parallels/build-efi.sh b/scripts/parallels/build-efi.sh new file mode 100755 index 00000000..df243d5a --- /dev/null +++ b/scripts/parallels/build-efi.sh @@ -0,0 +1,156 @@ +#!/bin/bash +# Build the Breenix Parallels UEFI loader and create a bootable disk image. +# +# Output: target/parallels/breenix-efi.img +# - GPT disk with FAT32 ESP containing EFI/BOOT/BOOTAA64.EFI +# - Optionally includes kernel ELF at /kernel-parallels +# +# Usage: +# ./scripts/parallels/build-efi.sh # Build loader only +# ./scripts/parallels/build-efi.sh --kernel # Build loader + kernel + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +OUTPUT_DIR="$PROJECT_ROOT/target/parallels" +EFI_IMG="$OUTPUT_DIR/breenix-efi.img" +ESP_DIR="$OUTPUT_DIR/esp" +INCLUDE_KERNEL=false + +for arg in "$@"; do + case "$arg" in + --kernel) INCLUDE_KERNEL=true ;; + *) echo "Unknown argument: $arg"; exit 1 ;; + esac +done + +echo "=== Building Breenix Parallels UEFI Loader ===" + +# Build the UEFI loader +cd "$PROJECT_ROOT" +cargo build --release --target aarch64-unknown-uefi -p parallels-loader + +LOADER_EFI="$PROJECT_ROOT/target/aarch64-unknown-uefi/release/parallels-loader.efi" +if [ ! -f "$LOADER_EFI" ]; then + echo "ERROR: UEFI loader not found at $LOADER_EFI" + exit 1 +fi +echo "Loader built: $LOADER_EFI ($(stat -f%z "$LOADER_EFI" 2>/dev/null || stat -c%s "$LOADER_EFI") bytes)" + +# Optionally build the kernel +if [ "$INCLUDE_KERNEL" = true ]; then + echo "=== Building Breenix ARM64 Kernel ===" + cargo build --release --target aarch64-breenix.json \ + -Z build-std=core,alloc \ + -Z build-std-features=compiler-builtins-mem \ + -p kernel --bin kernel-aarch64 + KERNEL_ELF="$PROJECT_ROOT/target/aarch64-breenix/release/kernel-aarch64" + if [ ! -f "$KERNEL_ELF" ]; then + echo "ERROR: Kernel ELF not found at $KERNEL_ELF" + exit 1 + fi + echo "Kernel built: $KERNEL_ELF ($(stat -f%z "$KERNEL_ELF" 2>/dev/null || stat -c%s "$KERNEL_ELF") bytes)" +fi + +# Create ESP directory structure +echo "=== Creating EFI System Partition ===" +rm -rf "$ESP_DIR" +mkdir -p "$ESP_DIR/EFI/BOOT" +mkdir -p "$ESP_DIR/EFI/BREENIX" +cp "$LOADER_EFI" "$ESP_DIR/EFI/BOOT/BOOTAA64.EFI" + +if [ "$INCLUDE_KERNEL" = true ] && [ -f "$KERNEL_ELF" ]; then + cp "$KERNEL_ELF" "$ESP_DIR/EFI/BREENIX/KERNEL" + echo "Kernel ELF copied to ESP at EFI/BREENIX/KERNEL" +fi + +# Create a GPT disk image with FAT32 ESP +# Size: 64MB (enough for loader + kernel) +echo "=== Creating GPT Disk Image ===" +mkdir -p "$OUTPUT_DIR" + +IMG_SIZE_MB=64 +dd if=/dev/zero of="$EFI_IMG" bs=1m count=$IMG_SIZE_MB 2>/dev/null + +# Use hdiutil on macOS to create FAT32 filesystem +# First create a FAT32 image of the ESP content +FAT_IMG="$OUTPUT_DIR/esp.fat32.img" +dd if=/dev/zero of="$FAT_IMG" bs=1m count=$((IMG_SIZE_MB - 1)) 2>/dev/null + +# Format as FAT32 using newfs_msdos (macOS) +if command -v newfs_msdos &>/dev/null; then + newfs_msdos -F 32 -S 512 "$FAT_IMG" 2>/dev/null +elif command -v mkfs.fat &>/dev/null; then + mkfs.fat -F 32 "$FAT_IMG" +else + echo "ERROR: No FAT32 formatter found (need newfs_msdos or mkfs.fat)" + exit 1 +fi + +# Mount and copy files using mtools (if available) or hdiutil +if command -v mcopy &>/dev/null; then + mmd -i "$FAT_IMG" ::EFI 2>/dev/null || true + mmd -i "$FAT_IMG" ::EFI/BOOT 2>/dev/null || true + mmd -i "$FAT_IMG" ::EFI/BREENIX 2>/dev/null || true + mcopy -i "$FAT_IMG" "$ESP_DIR/EFI/BOOT/BOOTAA64.EFI" ::EFI/BOOT/BOOTAA64.EFI + if [ "$INCLUDE_KERNEL" = true ] && [ -f "$ESP_DIR/EFI/BREENIX/KERNEL" ]; then + mcopy -i "$FAT_IMG" "$ESP_DIR/EFI/BREENIX/KERNEL" ::EFI/BREENIX/KERNEL + fi + echo "Files copied to FAT32 image via mtools" +elif command -v hdiutil &>/dev/null; then + # macOS: attach the raw image and copy files + MOUNT_POINT=$(mktemp -d) + # Convert raw to UDIF for mounting + hdiutil attach -imagekey diskimage-class=CRawDiskImage -nomount "$FAT_IMG" 2>/dev/null | \ + while read -r dev rest; do + if [[ "$dev" == /dev/disk* ]]; then + mount -t msdos "$dev" "$MOUNT_POINT" 2>/dev/null && break + fi + done + + if mountpoint -q "$MOUNT_POINT" 2>/dev/null || mount | grep -q "$MOUNT_POINT"; then + mkdir -p "$MOUNT_POINT/EFI/BOOT" + mkdir -p "$MOUNT_POINT/EFI/BREENIX" + cp "$ESP_DIR/EFI/BOOT/BOOTAA64.EFI" "$MOUNT_POINT/EFI/BOOT/" + if [ "$INCLUDE_KERNEL" = true ] && [ -f "$ESP_DIR/EFI/BREENIX/KERNEL" ]; then + cp "$ESP_DIR/EFI/BREENIX/KERNEL" "$MOUNT_POINT/EFI/BREENIX/" + fi + sync + hdiutil detach "$MOUNT_POINT" 2>/dev/null || umount "$MOUNT_POINT" 2>/dev/null + echo "Files copied to FAT32 image via hdiutil" + else + echo "WARNING: Could not mount FAT32 image. Falling back to raw copy." + echo "Install mtools for reliable image creation: brew install mtools" + rm -f "$FAT_IMG" + # Fall back to just providing the ESP directory + echo "ESP directory ready at: $ESP_DIR" + fi + rmdir "$MOUNT_POINT" 2>/dev/null || true +else + echo "WARNING: No tool to populate FAT32 image." + echo "Install mtools: brew install mtools" +fi + +# If we have a populated FAT image, embed it in GPT +if [ -f "$FAT_IMG" ]; then + # Create GPT wrapper using dd + # GPT header is 34 sectors at start, 33 at end + # For simplicity, just use the FAT image as-is for now + # Parallels can boot from a raw FAT32 image as EFI disk + cp "$FAT_IMG" "$EFI_IMG" + rm -f "$FAT_IMG" +fi + +echo "" +echo "=== Build Complete ===" +echo "EFI disk image: $EFI_IMG" +echo "ESP directory: $ESP_DIR" +echo "" +echo "To test with QEMU:" +echo " qemu-system-aarch64 -M virt -cpu cortex-a72 -m 512M \\" +echo " -drive if=pflash,format=raw,file=QEMU_EFI.fd,readonly=on \\" +echo " -drive format=raw,file=$EFI_IMG" +echo "" +echo "To deploy to Parallels:" +echo " ./scripts/parallels/deploy-to-vm.sh" diff --git a/scripts/parallels/collect-hwdump.sh b/scripts/parallels/collect-hwdump.sh new file mode 100755 index 00000000..3258b6d1 --- /dev/null +++ b/scripts/parallels/collect-hwdump.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# +# Collect hardware dump results from the Parallels VM to the host. +# Run this from the HOST after dump-hardware.sh has completed inside the guest. +# +# Usage: ./scripts/parallels/collect-hwdump.sh +# +set -euo pipefail + +VM_NAME="breenix-hwdump" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +DEST="$REPO_ROOT/docs/parallels-hwdump" + +mkdir -p "$DEST" + +echo "==> Collecting hardware dump from VM '$VM_NAME'..." + +# Check VM is running +if ! prlctl list | grep -q "$VM_NAME"; then + echo "ERROR: VM '$VM_NAME' is not running." + echo "Start it with: prlctl start '$VM_NAME'" + exit 1 +fi + +# Grab the summary +echo "==> Fetching summary.txt..." +prlctl exec "$VM_NAME" cat /tmp/hwdump/summary.txt > "$DEST/summary.txt" 2>/dev/null || { + echo "ERROR: Could not read summary.txt from VM." + echo "Make sure dump-hardware.sh has been run inside the guest." + exit 1 +} + +# Grab individual files +for f in iomem.txt guest.dtb guest.dts cpuinfo.txt interrupts.txt \ + lspci-verbose.txt lspci-ids.txt device-tree-compatible.txt; do + echo "==> Fetching $f..." + prlctl exec "$VM_NAME" cat "/tmp/hwdump/$f" > "$DEST/$f" 2>/dev/null || echo " (not available)" +done + +# Grab ACPI tables as a tarball (binary files don't copy well via cat) +echo "==> Fetching ACPI tables..." +prlctl exec "$VM_NAME" sh -c 'cd /tmp && tar czf /tmp/acpi-tables.tar.gz hwdump/acpi/ 2>/dev/null' || true +prlctl exec "$VM_NAME" cat /tmp/acpi-tables.tar.gz > "$DEST/acpi-tables.tar.gz" 2>/dev/null || echo " (not available)" +if [ -f "$DEST/acpi-tables.tar.gz" ] && [ -s "$DEST/acpi-tables.tar.gz" ]; then + mkdir -p "$DEST/acpi" + tar xzf "$DEST/acpi-tables.tar.gz" -C "$DEST" --strip-components=1 2>/dev/null || true + rm -f "$DEST/acpi-tables.tar.gz" +fi + +echo "" +echo "==> Hardware dump collected to: $DEST/" +echo "" +ls -la "$DEST/" +echo "" +echo "Key files to review:" +echo " $DEST/summary.txt - Full summary of all hardware" +echo " $DEST/guest.dts - Device tree (if present)" +echo " $DEST/iomem.txt - Physical memory map" +echo " $DEST/lspci-verbose.txt - PCI device details" +echo " $DEST/acpi/ - ACPI table binaries and decompiled DSL" +echo "" +echo "You can now stop the VM: prlctl stop '$VM_NAME'" diff --git a/scripts/parallels/deploy-to-vm.sh b/scripts/parallels/deploy-to-vm.sh new file mode 100755 index 00000000..0f264df5 --- /dev/null +++ b/scripts/parallels/deploy-to-vm.sh @@ -0,0 +1,116 @@ +#!/bin/bash +# Deploy the Breenix EFI disk image to a Parallels Desktop VM. +# +# Prerequisites: +# - Parallels Desktop installed with prlctl/prlsrvctl available +# - A VM named "breenix-dev" exists (or specify via --vm) +# - EFI image built via ./scripts/parallels/build-efi.sh +# +# Usage: +# ./scripts/parallels/deploy-to-vm.sh # Deploy to "breenix-dev" VM +# ./scripts/parallels/deploy-to-vm.sh --vm myvm # Deploy to specific VM +# ./scripts/parallels/deploy-to-vm.sh --boot # Deploy and boot +# ./scripts/parallels/deploy-to-vm.sh --serial # Deploy, boot, tail serial + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +EFI_IMG="$PROJECT_ROOT/target/parallels/breenix-efi.img" +VM_NAME="breenix-dev" +DO_BOOT=false +DO_SERIAL=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --vm) VM_NAME="$2"; shift 2 ;; + --boot) DO_BOOT=true; shift ;; + --serial) DO_SERIAL=true; DO_BOOT=true; shift ;; + *) echo "Unknown argument: $1"; exit 1 ;; + esac +done + +# Verify prerequisites +if ! command -v prlctl &>/dev/null; then + echo "ERROR: prlctl not found. Is Parallels Desktop installed?" + exit 1 +fi + +if [ ! -f "$EFI_IMG" ]; then + echo "ERROR: EFI image not found at $EFI_IMG" + echo "Run ./scripts/parallels/build-efi.sh first" + exit 1 +fi + +# Check if VM exists +if ! prlctl list --all 2>/dev/null | grep -q "$VM_NAME"; then + echo "VM '$VM_NAME' not found. Available VMs:" + prlctl list --all + echo "" + echo "Create a VM with:" + echo " prlctl create $VM_NAME --ostype linux --arch aarch64" + echo " prlctl set $VM_NAME --efi-boot on" + echo " prlctl set $VM_NAME --device-set hdd0 --image $EFI_IMG --type plain" + exit 1 +fi + +# Stop VM if running +VM_STATUS=$(prlctl status "$VM_NAME" 2>/dev/null | awk '{print $NF}') +if [ "$VM_STATUS" = "running" ] || [ "$VM_STATUS" = "paused" ]; then + echo "Stopping VM '$VM_NAME'..." + prlctl stop "$VM_NAME" --kill 2>/dev/null || true + sleep 2 +fi + +# Attach the EFI disk image +echo "=== Deploying EFI Image to VM '$VM_NAME' ===" +echo "Image: $EFI_IMG ($(stat -f%z "$EFI_IMG" 2>/dev/null || stat -c%s "$EFI_IMG") bytes)" + +# Remove existing disk and attach new one +# First, try to detach any existing hdd0 +prlctl set "$VM_NAME" --device-del hdd0 2>/dev/null || true + +# Copy image to VM's directory for Parallels to manage +VM_DIR=$(prlctl list --info "$VM_NAME" 2>/dev/null | grep "Home:" | sed 's/.*Home: *//' | tr -d ' ') +if [ -z "$VM_DIR" ]; then + VM_DIR="$HOME/Parallels/$VM_NAME.pvm" +fi + +DEST_IMG="$VM_DIR/breenix-efi.img" +echo "Copying to: $DEST_IMG" +cp "$EFI_IMG" "$DEST_IMG" + +# Attach as plain disk (not expanding) +prlctl set "$VM_NAME" --device-add hdd --image "$DEST_IMG" --type plain --position 0 2>/dev/null || \ + echo "WARNING: Could not attach disk via prlctl. You may need to attach it manually in Parallels settings." + +# Ensure EFI boot is enabled +prlctl set "$VM_NAME" --efi-boot on 2>/dev/null || true + +echo "" +echo "=== Deployment Complete ===" + +if [ "$DO_BOOT" = true ]; then + echo "Starting VM '$VM_NAME'..." + prlctl start "$VM_NAME" + + if [ "$DO_SERIAL" = true ]; then + echo "Waiting for serial output..." + SERIAL_LOG="$VM_DIR/serial.log" + # Parallels serial port output - check common locations + sleep 3 + if [ -f "$SERIAL_LOG" ]; then + tail -f "$SERIAL_LOG" + else + echo "Serial log not found at $SERIAL_LOG" + echo "Configure serial port in Parallels VM settings to redirect to file." + echo "Check VM output in Parallels Desktop window." + fi + fi +else + echo "To boot the VM:" + echo " prlctl start $VM_NAME" + echo "" + echo "To boot and watch serial:" + echo " ./scripts/parallels/deploy-to-vm.sh --serial" +fi diff --git a/scripts/parallels/dump-hardware.sh b/scripts/parallels/dump-hardware.sh new file mode 100755 index 00000000..065bfc6a --- /dev/null +++ b/scripts/parallels/dump-hardware.sh @@ -0,0 +1,235 @@ +#!/bin/sh +# +# Hardware dump script to run INSIDE a Linux ARM64 guest on Parallels. +# Collects device tree, ACPI tables, memory map, PCI devices, and +# interrupt controller info needed to port Breenix to Parallels. +# +# Usage (inside the Parallels Linux guest): +# apk add dtc pciutils acpica # Alpine +# sh dump-hardware.sh +# +set -eu + +OUTDIR="/tmp/hwdump" +mkdir -p "$OUTDIR" + +echo "=== Breenix Parallels Hardware Dump ===" +echo "Date: $(date -u)" +echo "Kernel: $(uname -r)" +echo "Arch: $(uname -m)" +echo "" + +# Summary file +SUMMARY="$OUTDIR/summary.txt" +: > "$SUMMARY" + +header() { + echo "" | tee -a "$SUMMARY" + echo "======================================" | tee -a "$SUMMARY" + echo " $1" | tee -a "$SUMMARY" + echo "======================================" | tee -a "$SUMMARY" +} + +# 1. Physical memory map +header "PHYSICAL MEMORY MAP (/proc/iomem)" +if [ -f /proc/iomem ]; then + cat /proc/iomem > "$OUTDIR/iomem.txt" 2>/dev/null || true + cat /proc/iomem 2>/dev/null | tee -a "$SUMMARY" || echo "(need root)" | tee -a "$SUMMARY" +else + echo "/proc/iomem not available" | tee -a "$SUMMARY" +fi + +# 2. Device Tree +header "DEVICE TREE" +if [ -f /sys/firmware/fdt ]; then + cp /sys/firmware/fdt "$OUTDIR/guest.dtb" 2>/dev/null + echo "Raw DTB saved to $OUTDIR/guest.dtb" | tee -a "$SUMMARY" + + if command -v dtc >/dev/null 2>&1; then + dtc -I dtb -O dts "$OUTDIR/guest.dtb" > "$OUTDIR/guest.dts" 2>/dev/null + echo "Decompiled DTS saved to $OUTDIR/guest.dts" | tee -a "$SUMMARY" + echo "" | tee -a "$SUMMARY" + echo "--- Device Tree Summary ---" | tee -a "$SUMMARY" + # Extract key nodes + grep -E '(compatible|reg |interrupt|clock-frequency|#address-cells|#size-cells)' "$OUTDIR/guest.dts" | head -80 | tee -a "$SUMMARY" + else + echo "dtc not installed - install with: apk add dtc" | tee -a "$SUMMARY" + fi +elif [ -d /proc/device-tree ]; then + echo "No /sys/firmware/fdt but /proc/device-tree exists" | tee -a "$SUMMARY" + find /proc/device-tree -name compatible -exec sh -c 'echo "{}:"; cat "{}"; echo' \; > "$OUTDIR/device-tree-compatible.txt" 2>/dev/null || true + cat "$OUTDIR/device-tree-compatible.txt" | tee -a "$SUMMARY" +else + echo "No device tree found (VM may use ACPI only)" | tee -a "$SUMMARY" +fi + +# 3. ACPI tables +header "ACPI TABLES" +ACPI_DIR="/sys/firmware/acpi/tables" +if [ -d "$ACPI_DIR" ]; then + mkdir -p "$OUTDIR/acpi" + echo "Available ACPI tables:" | tee -a "$SUMMARY" + ls -la "$ACPI_DIR" 2>/dev/null | tee -a "$SUMMARY" + + # Copy key tables + for table in MADT MCFG FADT GTDT SPCR DSDT SSDT IORT; do + if [ -f "$ACPI_DIR/$table" ]; then + cp "$ACPI_DIR/$table" "$OUTDIR/acpi/$table.bin" 2>/dev/null || true + echo " Saved $table" | tee -a "$SUMMARY" + fi + done + + # Decompile with iasl if available + if command -v iasl >/dev/null 2>&1; then + echo "" | tee -a "$SUMMARY" + echo "--- MADT (Interrupt Controller) ---" | tee -a "$SUMMARY" + if [ -f "$OUTDIR/acpi/MADT.bin" ]; then + iasl -d "$OUTDIR/acpi/MADT.bin" 2>/dev/null || true + cat "$OUTDIR/acpi/MADT.dsl" 2>/dev/null | tee -a "$SUMMARY" || true + fi + + echo "" | tee -a "$SUMMARY" + echo "--- MCFG (PCI Configuration) ---" | tee -a "$SUMMARY" + if [ -f "$OUTDIR/acpi/MCFG.bin" ]; then + iasl -d "$OUTDIR/acpi/MCFG.bin" 2>/dev/null || true + cat "$OUTDIR/acpi/MCFG.dsl" 2>/dev/null | tee -a "$SUMMARY" || true + fi + + echo "" | tee -a "$SUMMARY" + echo "--- GTDT (Generic Timer) ---" | tee -a "$SUMMARY" + if [ -f "$OUTDIR/acpi/GTDT.bin" ]; then + iasl -d "$OUTDIR/acpi/GTDT.bin" 2>/dev/null || true + cat "$OUTDIR/acpi/GTDT.dsl" 2>/dev/null | tee -a "$SUMMARY" || true + fi + + echo "" | tee -a "$SUMMARY" + echo "--- SPCR (Serial Port Console) ---" | tee -a "$SUMMARY" + if [ -f "$OUTDIR/acpi/SPCR.bin" ]; then + iasl -d "$OUTDIR/acpi/SPCR.bin" 2>/dev/null || true + cat "$OUTDIR/acpi/SPCR.dsl" 2>/dev/null | tee -a "$SUMMARY" || true + fi + + echo "" | tee -a "$SUMMARY" + echo "--- IORT (I/O Remapping) ---" | tee -a "$SUMMARY" + if [ -f "$OUTDIR/acpi/IORT.bin" ]; then + iasl -d "$OUTDIR/acpi/IORT.bin" 2>/dev/null || true + cat "$OUTDIR/acpi/IORT.dsl" 2>/dev/null | tee -a "$SUMMARY" || true + fi + else + echo "" | tee -a "$SUMMARY" + echo "iasl not installed - install with: apk add acpica" | tee -a "$SUMMARY" + echo "Raw binary tables saved to $OUTDIR/acpi/" | tee -a "$SUMMARY" + fi +else + echo "No ACPI tables found at $ACPI_DIR" | tee -a "$SUMMARY" +fi + +# 4. PCI devices +header "PCI DEVICES" +if command -v lspci >/dev/null 2>&1; then + lspci -vvv > "$OUTDIR/lspci-verbose.txt" 2>/dev/null || true + lspci -nn > "$OUTDIR/lspci-ids.txt" 2>/dev/null || true + echo "--- PCI Device List ---" | tee -a "$SUMMARY" + lspci -nn 2>/dev/null | tee -a "$SUMMARY" + echo "" | tee -a "$SUMMARY" + echo "Detailed PCI info saved to $OUTDIR/lspci-verbose.txt" | tee -a "$SUMMARY" +else + echo "lspci not installed - install with: apk add pciutils" | tee -a "$SUMMARY" +fi + +# 5. Interrupt controller info +header "INTERRUPT CONTROLLER" +if [ -d /proc/interrupts ]; then + cat /proc/interrupts > "$OUTDIR/interrupts.txt" 2>/dev/null || true +elif [ -f /proc/interrupts ]; then + cat /proc/interrupts > "$OUTDIR/interrupts.txt" 2>/dev/null || true + echo "--- Active Interrupts ---" | tee -a "$SUMMARY" + cat /proc/interrupts 2>/dev/null | tee -a "$SUMMARY" +fi + +# GIC version detection +echo "" | tee -a "$SUMMARY" +echo "--- GIC Detection ---" | tee -a "$SUMMARY" +if [ -d /proc/device-tree ]; then + find /proc/device-tree -name compatible | while read f; do + val=$(cat "$f" 2>/dev/null | tr '\0' ' ') + case "$val" in + *gic*) echo "GIC node: $f = $val" | tee -a "$SUMMARY" ;; + esac + done +fi +dmesg 2>/dev/null | grep -i -E '(gic|interrupt.controller)' | tee -a "$SUMMARY" || true + +# 6. CPU info +header "CPU INFORMATION" +cat /proc/cpuinfo > "$OUTDIR/cpuinfo.txt" 2>/dev/null || true +head -30 /proc/cpuinfo 2>/dev/null | tee -a "$SUMMARY" + +# 7. Kernel command line and boot info +header "BOOT INFORMATION" +echo "Cmdline: $(cat /proc/cmdline 2>/dev/null)" | tee -a "$SUMMARY" +echo "" | tee -a "$SUMMARY" + +# EFI variables +if [ -d /sys/firmware/efi ]; then + echo "EFI boot: YES" | tee -a "$SUMMARY" + echo "EFI runtime services: $(ls /sys/firmware/efi/ 2>/dev/null | tr '\n' ' ')" | tee -a "$SUMMARY" + if [ -d /sys/firmware/efi/efivars ]; then + echo "EFI vars count: $(ls /sys/firmware/efi/efivars/ 2>/dev/null | wc -l)" | tee -a "$SUMMARY" + fi +else + echo "EFI boot: NO (or not detected)" | tee -a "$SUMMARY" +fi + +# 8. Memory info +header "MEMORY LAYOUT" +echo "--- /proc/meminfo ---" | tee -a "$SUMMARY" +head -10 /proc/meminfo 2>/dev/null | tee -a "$SUMMARY" +echo "" | tee -a "$SUMMARY" +echo "--- dmesg memory ---" | tee -a "$SUMMARY" +dmesg 2>/dev/null | grep -i -E '(memory|zone|node )' | head -20 | tee -a "$SUMMARY" || true + +# 9. VirtIO devices +header "VIRTIO DEVICES" +echo "--- /sys/bus/virtio/devices ---" | tee -a "$SUMMARY" +if [ -d /sys/bus/virtio/devices ]; then + for dev in /sys/bus/virtio/devices/*; do + if [ -d "$dev" ]; then + name=$(basename "$dev") + vendor=$(cat "$dev/vendor" 2>/dev/null || echo "?") + device=$(cat "$dev/device" 2>/dev/null || echo "?") + echo " $name: vendor=$vendor device=$device" | tee -a "$SUMMARY" + fi + done +else + echo " No virtio bus found" | tee -a "$SUMMARY" +fi + +# 10. Serial/UART +header "SERIAL PORTS" +dmesg 2>/dev/null | grep -i -E '(uart|serial|ttyAMA|ttyS|pl011)' | tee -a "$SUMMARY" || echo " No serial info in dmesg" | tee -a "$SUMMARY" + +# 11. Timer info +header "TIMER" +dmesg 2>/dev/null | grep -i -E '(timer|clocksource|arch_timer)' | head -10 | tee -a "$SUMMARY" || true + +# Package everything +echo "" +echo "======================================" | tee -a "$SUMMARY" +echo " DUMP COMPLETE" | tee -a "$SUMMARY" +echo "======================================" | tee -a "$SUMMARY" +echo "" | tee -a "$SUMMARY" +echo "All files saved to: $OUTDIR" | tee -a "$SUMMARY" +echo "Files:" | tee -a "$SUMMARY" +ls -la "$OUTDIR"/ | tee -a "$SUMMARY" +if [ -d "$OUTDIR/acpi" ]; then + echo "" | tee -a "$SUMMARY" + echo "ACPI files:" | tee -a "$SUMMARY" + ls -la "$OUTDIR/acpi/" | tee -a "$SUMMARY" +fi + +echo "" +echo "To copy results to the host:" +echo " 1. From host: prlctl exec breenix-hwdump cat /tmp/hwdump/summary.txt" +echo " 2. Or tar it: tar czf /tmp/hwdump.tar.gz -C /tmp hwdump" +echo " Then: prlctl exec breenix-hwdump cat /tmp/hwdump.tar.gz > hwdump.tar.gz" diff --git a/scripts/parallels/patch-gpt-esp.py b/scripts/parallels/patch-gpt-esp.py new file mode 100755 index 00000000..5d87a910 --- /dev/null +++ b/scripts/parallels/patch-gpt-esp.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +"""Patch GPT partition type from 'Microsoft Basic Data' to 'EFI System Partition'. + +macOS hdiutil creates FAT32 partitions with the 'Microsoft Basic Data' type GUID. +UEFI firmware requires 'EFI System Partition' type to auto-discover and boot from +the ESP. This script patches both the primary and backup GPT headers with the +correct partition type GUID and updates CRC32 checksums. + +Usage: patch-gpt-esp.py +""" + +import struct +import sys +import uuid +import zlib + + +# GPT partition type GUIDs +BASIC_DATA = uuid.UUID('EBD0A0A2-B9E5-4433-87C0-68B6B72699C7') +EFI_SYSTEM = uuid.UUID('C12A7328-F81F-11D2-BA4B-00A0C93EC93B') + +SECTOR_SIZE = 512 + + +def uuid_to_mixed_endian(u): + """Convert a UUID to GPT's mixed-endian on-disk format. + + GPT stores UUIDs with the first three components in little-endian + and the last two in big-endian (network byte order). + """ + b = struct.pack('", file=sys.stderr) + sys.exit(1) + patch_gpt(sys.argv[1]) diff --git a/scripts/parallels/setup-hwdump-vm.sh b/scripts/parallels/setup-hwdump-vm.sh new file mode 100755 index 00000000..813b6891 --- /dev/null +++ b/scripts/parallels/setup-hwdump-vm.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# +# Setup a minimal ARM64 Linux VM in Parallels for hardware discovery. +# Downloads an Alpine Linux ARM64 ISO (lightweight), creates a VM, +# attaches the ISO, and boots it. +# +# Usage: ./scripts/parallels/setup-hwdump-vm.sh +# +set -euo pipefail + +VM_NAME="breenix-hwdump" +ISO_DIR="$HOME/.cache/breenix-parallels" +ALPINE_VERSION="3.21" +ALPINE_MINOR="3" +ALPINE_ISO="alpine-standard-${ALPINE_VERSION}.${ALPINE_MINOR}-aarch64.iso" +ALPINE_URL="https://dl-cdn.alpinelinux.org/alpine/v${ALPINE_VERSION}/releases/aarch64/${ALPINE_ISO}" + +mkdir -p "$ISO_DIR" + +# Download Alpine ARM64 ISO if not cached +if [ ! -f "$ISO_DIR/$ALPINE_ISO" ]; then + echo "==> Downloading Alpine Linux ${ALPINE_VERSION}.${ALPINE_MINOR} ARM64..." + curl -L -o "$ISO_DIR/$ALPINE_ISO" "$ALPINE_URL" + echo "==> Downloaded to $ISO_DIR/$ALPINE_ISO" +else + echo "==> Using cached ISO: $ISO_DIR/$ALPINE_ISO" +fi + +# Check if VM already exists +if prlctl list -a | grep -q "$VM_NAME"; then + echo "==> VM '$VM_NAME' already exists." + read -p " Delete and recreate? [y/N] " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + echo "==> Stopping and deleting existing VM..." + prlctl stop "$VM_NAME" --kill 2>/dev/null || true + prlctl delete "$VM_NAME" 2>/dev/null || true + else + echo "==> Keeping existing VM. Start it with: prlctl start '$VM_NAME'" + exit 0 + fi +fi + +echo "==> Creating Parallels VM: $VM_NAME" +prlctl create "$VM_NAME" -o linux --no-hdd + +echo "==> Configuring VM..." +# Set resources +prlctl set "$VM_NAME" --cpus 4 +prlctl set "$VM_NAME" --memsize 2048 + +# Ensure EFI boot is on (should be default for ARM64) +prlctl set "$VM_NAME" --efi-boot on + +# Add a small disk for the OS install +prlctl set "$VM_NAME" --device-add hdd --type plain --size 8192 + +# Attach the ISO +prlctl set "$VM_NAME" --device-add cdrom --image "$ISO_DIR/$ALPINE_ISO" --connect + +# Boot from CD first +prlctl set "$VM_NAME" --device-bootorder "cdrom0 hdd0" + +echo "==> VM '$VM_NAME' created and configured." +echo "" +echo "Next steps:" +echo " 1. Start the VM: prlctl start '$VM_NAME'" +echo " 2. Open console: open \"/Users/\$USER/Parallels/${VM_NAME}.pvm\"" +echo " Or use: prlctl enter '$VM_NAME'" +echo " 3. Log in as root (no password on Alpine live)" +echo " 4. Install prerequisites and run the dump script:" +echo "" +echo " # Inside the Alpine VM:" +echo " apk add dtc pciutils acpica" +echo " # Then paste/run the dump-hardware.sh script" +echo "" +echo " 5. Copy results out:" +echo " prlctl exec '$VM_NAME' cat /tmp/hwdump/summary.txt" +echo "" +echo "Or start the VM now with: prlctl start '$VM_NAME'" diff --git a/scripts/parallels/type-in-vm.sh b/scripts/parallels/type-in-vm.sh new file mode 100755 index 00000000..a3d44925 --- /dev/null +++ b/scripts/parallels/type-in-vm.sh @@ -0,0 +1,124 @@ +#!/usr/bin/env bash +# +# Type a string into a Parallels VM via send-key-event. +# Converts ASCII characters to Parallels key codes. +# +# Usage: ./type-in-vm.sh "command to type" +# ./type-in-vm.sh --enter # Just press Enter +# ./type-in-vm.sh "command" --enter # Type + press Enter +# +set -euo pipefail + +VM_NAME="${VM_NAME:-breenix-hwdump}" +DELAY="${KEY_DELAY:-50}" + +send_key() { + local code="$1" + prlctl send-key-event "$VM_NAME" --scancode "$code" --event press >/dev/null 2>&1 + sleep 0.02 + prlctl send-key-event "$VM_NAME" --scancode "$code" --event release >/dev/null 2>&1 + sleep 0.02 +} + +send_shift_key() { + local code="$1" + # Hold shift + prlctl send-key-event "$VM_NAME" --scancode 42 --event press >/dev/null 2>&1 + sleep 0.01 + prlctl send-key-event "$VM_NAME" --scancode "$code" --event press >/dev/null 2>&1 + sleep 0.02 + prlctl send-key-event "$VM_NAME" --scancode "$code" --event release >/dev/null 2>&1 + sleep 0.01 + # Release shift + prlctl send-key-event "$VM_NAME" --scancode 42 --event release >/dev/null 2>&1 + sleep 0.02 +} + +send_enter() { + send_key 28 +} + +# Map ASCII to PS/2 scan codes +char_to_scancode() { + local c="$1" + case "$c" in + a) send_key 30 ;; b) send_key 48 ;; c) send_key 46 ;; d) send_key 32 ;; + e) send_key 18 ;; f) send_key 33 ;; g) send_key 34 ;; h) send_key 35 ;; + i) send_key 23 ;; j) send_key 36 ;; k) send_key 37 ;; l) send_key 38 ;; + m) send_key 50 ;; n) send_key 49 ;; o) send_key 24 ;; p) send_key 25 ;; + q) send_key 16 ;; r) send_key 19 ;; s) send_key 31 ;; t) send_key 20 ;; + u) send_key 22 ;; v) send_key 47 ;; w) send_key 17 ;; x) send_key 45 ;; + y) send_key 21 ;; z) send_key 44 ;; + A) send_shift_key 30 ;; B) send_shift_key 48 ;; C) send_shift_key 46 ;; + D) send_shift_key 32 ;; E) send_shift_key 18 ;; F) send_shift_key 33 ;; + G) send_shift_key 34 ;; H) send_shift_key 35 ;; I) send_shift_key 23 ;; + J) send_shift_key 36 ;; K) send_shift_key 37 ;; L) send_shift_key 38 ;; + M) send_shift_key 50 ;; N) send_shift_key 49 ;; O) send_shift_key 24 ;; + P) send_shift_key 25 ;; Q) send_shift_key 16 ;; R) send_shift_key 19 ;; + S) send_shift_key 31 ;; T) send_shift_key 20 ;; U) send_shift_key 22 ;; + V) send_shift_key 47 ;; W) send_shift_key 17 ;; X) send_shift_key 45 ;; + Y) send_shift_key 21 ;; Z) send_shift_key 44 ;; + 0) send_key 11 ;; 1) send_key 2 ;; 2) send_key 3 ;; 3) send_key 4 ;; + 4) send_key 5 ;; 5) send_key 6 ;; 6) send_key 7 ;; 7) send_key 8 ;; + 8) send_key 9 ;; 9) send_key 10 ;; + ' ') send_key 57 ;; + '-') send_key 12 ;; + '=') send_key 13 ;; + '[') send_key 26 ;; + ']') send_key 27 ;; + '\\') send_key 43 ;; + ';') send_key 39 ;; + "'") send_key 40 ;; + '`') send_key 41 ;; + ',') send_key 51 ;; + '.') send_key 52 ;; + '/') send_key 53 ;; + '!') send_shift_key 2 ;; + '@') send_shift_key 3 ;; + '#') send_shift_key 4 ;; + '$') send_shift_key 5 ;; + '%') send_shift_key 6 ;; + '^') send_shift_key 7 ;; + '&') send_shift_key 8 ;; + '*') send_shift_key 9 ;; + '(') send_shift_key 10 ;; + ')') send_shift_key 11 ;; + '_') send_shift_key 12 ;; + '+') send_shift_key 13 ;; + '{') send_shift_key 26 ;; + '}') send_shift_key 27 ;; + '|') send_shift_key 43 ;; + ':') send_shift_key 39 ;; + '"') send_shift_key 40 ;; + '~') send_shift_key 41 ;; + '<') send_shift_key 51 ;; + '>') send_shift_key 52 ;; + '?') send_shift_key 53 ;; + $'\t') send_key 15 ;; + *) echo "WARNING: unmapped char '$c'" >&2 ;; + esac +} + +PRESS_ENTER=false +TEXT="" + +for arg in "$@"; do + if [ "$arg" = "--enter" ]; then + PRESS_ENTER=true + else + TEXT="$arg" + fi +done + +# Type the text character by character +if [ -n "$TEXT" ]; then + for (( i=0; i<${#TEXT}; i++ )); do + char="${TEXT:$i:1}" + char_to_scancode "$char" + done +fi + +# Press enter if requested +if [ "$PRESS_ENTER" = true ]; then + send_enter +fi