Skip to content

Commit ac1591d

Browse files
authored
Merge pull request #248 from ryanbreen/feat/vmware-support
feat: VMware Fusion ARM64 support — full boot, XHCI USB keyboard + mouse, cursor
2 parents 79634c0 + d75a5cb commit ac1591d

37 files changed

Lines changed: 2557 additions & 316 deletions

VMWARE_HANDOFF.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# VMware Fusion Support — Handoff Document
2+
3+
**Branch:** `feat/vmware-support` (branched from `feat/full-gpu-compositing` at `c3744ea`)
4+
**Worktree:** `/Users/wrb/fun/code/breenix-vmware`
5+
**Date:** 2026-03-06
6+
7+
## What's Done
8+
9+
### 1. `run.sh --vmware` flag (COMPLETE)
10+
11+
Full VMware Fusion integration added to `run.sh`, mirroring the `--parallels` pattern:
12+
13+
- Builds UEFI loader + ARM64 kernel + userspace + ext2 disk (same pipeline as Parallels)
14+
- Creates FAT32 ESP image with GPT ESP patch
15+
- Converts raw images to VMDK via `qemu-img convert -f raw -O vmdk`
16+
- Generates `.vmwarevm` bundle with unique timestamped name
17+
- Cleans up old `breenix-*` VMs automatically
18+
- Launches via `vmrun start`, tails serial log
19+
20+
Usage:
21+
```bash
22+
./run.sh --vmware # Full build + boot
23+
./run.sh --vmware --no-build # Reuse last build
24+
./run.sh --vmware --clean # Clean rebuild
25+
```
26+
27+
### 2. VMX Generator (COMPLETE)
28+
29+
`scripts/vmware/generate-vmx.sh` — generates a `.vmx` config file with:
30+
- ARM64 EFI guest (`guestOS = "arm-other-64"`, `firmware = "efi"`)
31+
- NVMe boot disk (boot.vmdk with FAT32 ESP)
32+
- SATA ext2 data disk (optional)
33+
- Serial port output to `/tmp/breenix-vmware-serial.log`
34+
- NAT networking (e1000e)
35+
- XHCI USB
36+
- 4 vCPUs, 2GB RAM, 256MB VRAM
37+
38+
### 3. First Boot Test (DONE — partial success)
39+
40+
The kernel DOES boot on VMware Fusion. Evidence from `vmware.log`:
41+
- `Guest: Firmware has transitioned to runtime` — UEFI loader executed
42+
- vcpu-0 active with valid state: `PC=fbce8774`, `VBAR_EL1=ff3dd000`, `TTBR0_EL1=fffdf000`
43+
- MMU enabled (`SCTLR_EL1=3050198d`), page tables set up
44+
- Screen resized: `Screen 1 Defined: xywh(0, 0, 2048, 1536)`
45+
46+
## What's Broken
47+
48+
### Serial Output — GARBLED (not GPU-related)
49+
50+
The serial log (`/tmp/breenix-vmware-serial.log`) contains non-ASCII binary garbage (bytes in 0x00-0x1b range). The kernel is running but we can't see any output.
51+
52+
**Root cause:** The UEFI loader discovers the UART via ACPI SPCR table. When SPCR isn't found or doesn't match, it falls back to the Parallels PL011 address `0x0211_0000`. VMware Fusion almost certainly has its UART at a different base address.
53+
54+
**Key code path:**
55+
1. `parallels-loader/src/acpi_discovery.rs:187-219` — SPCR parsing, falls back to `0x0211_0000`
56+
2. `kernel/src/platform_config.rs:22` — QEMU default is `0x0900_0000`
57+
3. `kernel/src/serial_aarch64.rs` — PL011 driver (may need 16550 for VMware)
58+
59+
## What Needs To Happen Next
60+
61+
### Priority 1: Fix Serial Output
62+
63+
This is the blocker — without serial we're flying blind.
64+
65+
**Option A: Debug from UEFI loader side**
66+
- The UEFI loader has access to UEFI SimpleTextOutput (screen console) before handing off
67+
- Add SPCR table dump logging in `parallels-loader/src/acpi_discovery.rs` using UEFI output services
68+
- This will reveal what ACPI tables VMware provides and what UART address is in SPCR
69+
70+
**Option B: Dump ACPI tables from VMware**
71+
- Boot a Linux ISO on the same VMware VM config
72+
- Run `cat /sys/firmware/acpi/tables/SPCR | xxd` to see the UART address
73+
- Or `dmesg | grep -i uart` to see what Linux discovers
74+
75+
**Option C: Try known VMware UART addresses**
76+
- VMware ARM64 may use PL011 at `0x0900_0000` (same as QEMU virt)
77+
- Or it might use 16550-compatible UART at a different address
78+
- Could try hardcoding common addresses as a quick test
79+
80+
### Priority 2: Verify UART Type
81+
82+
VMware might not use PL011 at all on ARM64. It could use:
83+
- **16550 UART** (like x86) — different register layout, different init sequence
84+
- **PL011** at a different address — just need the right base
85+
- Check SPCR `interface_type` field: 0x03 = PL011, 0x12 = 16550
86+
87+
### Priority 3: Platform Abstraction
88+
89+
Once serial works, consider:
90+
- Adding a `VMware` variant to platform detection (currently just QEMU vs Parallels)
91+
- VMware's virtio/GPU/network devices may differ from Parallels
92+
- The existing Breenix VM at `~/Virtual Machines.localized/Breenix.vmwarevm/Breenix.vmx` has a known-working VMware config for reference
93+
94+
## File Inventory
95+
96+
### New files
97+
- `scripts/vmware/generate-vmx.sh` — VMX config generator
98+
- `VMWARE_HANDOFF.md` — this document
99+
100+
### Modified files
101+
- `run.sh` — added `--vmware` flag, VMWARE block (lines ~410-610)
102+
103+
### Build artifacts
104+
- `target/vmware/boot.vmdk` — EFI boot disk
105+
- `target/vmware/ext2-data.vmdk` — ext2 data disk
106+
- VM bundles land in `~/Virtual Machines.localized/breenix-*.vmwarevm/`
107+
108+
## VMware Fusion Tooling Reference
109+
110+
| Tool | Path | Purpose |
111+
|------|------|---------|
112+
| vmrun | `/Applications/VMware Fusion.app/Contents/Public/vmrun` | VM lifecycle (start/stop) |
113+
| vmware-vdiskmanager | `/Applications/VMware Fusion.app/Contents/Library/vmware-vdiskmanager` | VMDK creation/conversion |
114+
| vmcli | `/Applications/VMware Fusion.app/Contents/Public/vmcli` | VM configuration |
115+
| EFI ROM | `.../Library/roms/arm64/EFIAARCH64.ROM` | ARM64 EFI firmware |
116+
117+
Disk conversion uses `qemu-img` (already installed at `/opt/homebrew/bin/qemu-img`) rather than vmware-vdiskmanager, since qemu-img handles raw-to-vmdk directly.
118+
119+
## Existing VMware VM (Manual Setup)
120+
121+
There's an existing manually-created Breenix VM at:
122+
`~/Virtual Machines.localized/Breenix.vmwarevm/Breenix.vmx`
123+
124+
This was set up before the automated `--vmware` flow. It has the ext2 image attached as `sata0:1` (cdrom-image pointing at `target/ext2-aarch64.img`). The `.vmx` from that VM was used as reference for `generate-vmx.sh`. The VM is currently suspended with a checkpoint.

kernel/src/arch_impl/aarch64/boot.S

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ curr_el_spx_irq:
223223
b irq_handler
224224
.balign 0x80
225225
curr_el_spx_fiq:
226-
b unhandled_exception
226+
b irq_handler
227227
.balign 0x80
228228
curr_el_spx_serror:
229229
b unhandled_exception
@@ -237,7 +237,7 @@ lower_el_aarch64_irq:
237237
b irq_handler
238238
.balign 0x80
239239
lower_el_aarch64_fiq:
240-
b unhandled_exception
240+
b irq_handler
241241
.balign 0x80
242242
lower_el_aarch64_serror:
243243
b unhandled_exception

kernel/src/arch_impl/aarch64/cpu.rs

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,28 +31,32 @@ const DAIF_ALL_IMM: u32 = 0xF; // D, A, I, F
3131
pub struct Aarch64Cpu;
3232

3333
impl CpuOps for Aarch64Cpu {
34-
/// Enable IRQ interrupts by clearing the I bit in DAIF
34+
/// Enable IRQ and FIQ interrupts by clearing the I and F bits in DAIF.
35+
///
36+
/// Both IRQ and FIQ must be unmasked because on VMware Fusion, all GIC
37+
/// interrupts are Group 0 (FIQ) due to GICR_IGROUPR0 being RAZ/WI.
3538
#[inline]
3639
unsafe fn enable_interrupts() {
37-
// daifclr with #2 clears the I bit (enables IRQs)
38-
core::arch::asm!("msr daifclr, #2", options(nomem, nostack));
40+
// daifclr with #3 clears bits I and F (enables both IRQs and FIQs)
41+
core::arch::asm!("msr daifclr, #3", options(nomem, nostack));
3942
}
4043

41-
/// Disable IRQ interrupts by setting the I bit in DAIF
44+
/// Disable IRQ and FIQ interrupts by setting the I and F bits in DAIF
4245
#[inline]
4346
unsafe fn disable_interrupts() {
44-
// daifset with #2 sets the I bit (disables IRQs)
45-
core::arch::asm!("msr daifset, #2", options(nomem, nostack));
47+
// daifset with #3 sets bits I and F (disables both IRQs and FIQs)
48+
core::arch::asm!("msr daifset, #3", options(nomem, nostack));
4649
}
4750

48-
/// Check if IRQ interrupts are enabled (I bit is clear)
51+
/// Check if interrupts are enabled (both I and F bits are clear)
4952
#[inline]
5053
fn interrupts_enabled() -> bool {
5154
let daif: u64;
5255
unsafe {
5356
core::arch::asm!("mrs {}, daif", out(reg) daif, options(nomem, nostack));
5457
}
55-
// IRQs are enabled when the I bit (bit 7) is clear
58+
// Interrupts are enabled when the I bit (bit 7) is clear
59+
// (FIQ is also unmasked but we check I as the primary indicator)
5660
(daif & DAIF_IRQ_BIT) == 0
5761
}
5862

@@ -76,10 +80,10 @@ impl CpuOps for Aarch64Cpu {
7680
#[inline]
7781
fn halt_with_interrupts() {
7882
unsafe {
79-
// Enable IRQs and immediately wait
83+
// Enable IRQs/FIQs and immediately wait
8084
// Any pending interrupt will be taken before WFI completes
8185
core::arch::asm!(
82-
"msr daifclr, #2", // Enable IRQs
86+
"msr daifclr, #3", // Enable IRQs and FIQs
8387
"wfi", // Wait for interrupt
8488
options(nomem, nostack)
8589
);
@@ -101,22 +105,18 @@ impl CpuOps for Aarch64Cpu {
101105
core::arch::asm!("mrs {}, daif", out(reg) daif, options(nomem, nostack));
102106
}
103107

104-
// Disable IRQs
108+
// Disable IRQs and FIQs
105109
unsafe {
106-
core::arch::asm!("msr daifset, #2", options(nomem, nostack));
110+
core::arch::asm!("msr daifset, #3", options(nomem, nostack));
107111
}
108112

109113
// Execute the closure
110114
let result = f();
111115

112-
// Restore previous DAIF state (only restore IRQ bit to avoid affecting other flags)
113-
if (daif & DAIF_IRQ_BIT) == 0 {
114-
// IRQs were enabled before, re-enable them
115-
unsafe {
116-
core::arch::asm!("msr daifclr, #2", options(nomem, nostack));
117-
}
116+
// Restore previous DAIF state exactly
117+
unsafe {
118+
core::arch::asm!("msr daif, {}", in(reg) daif, options(nomem, nostack));
118119
}
119-
// If IRQs were disabled, leave them disabled (don't change anything)
120120

121121
result
122122
}

kernel/src/arch_impl/aarch64/exception.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -974,9 +974,9 @@ pub extern "C" fn handle_irq() {
974974
if let Some(irq_id) = gic::acknowledge_irq() {
975975
// Handle the interrupt based on ID
976976
match irq_id {
977-
// Virtual timer interrupt (PPI 27)
977+
// Timer interrupt — virtual (PPI 27) or physical (PPI 30)
978978
// This is the scheduling timer - calls into scheduler
979-
crate::arch_impl::aarch64::timer_interrupt::TIMER_IRQ => {
979+
irq if irq == crate::arch_impl::aarch64::timer_interrupt::timer_irq() => {
980980
// Call the timer interrupt handler which handles:
981981
// - Re-arming the timer
982982
// - Updating global time
@@ -999,7 +999,24 @@ pub extern "C" fn handle_irq() {
999999
}
10001000

10011001
// PPIs (16-31) - Private peripheral interrupts (excluding timer)
1002-
16..=31 => {}
1002+
// On VMware, we enable both virtual (27) and physical (30) timers
1003+
// to discover which fires. Handle either as a timer interrupt.
1004+
irq @ 16..=31 => {
1005+
if irq == crate::arch_impl::aarch64::timer_interrupt::VIRT_TIMER_IRQ
1006+
|| irq == crate::arch_impl::aarch64::timer_interrupt::PHYS_TIMER_IRQ
1007+
{
1008+
crate::arch_impl::aarch64::timer_interrupt::timer_interrupt_handler();
1009+
}
1010+
// Diagnostic: emit raw serial for any unexpected PPI
1011+
if irq != crate::arch_impl::aarch64::timer_interrupt::timer_irq()
1012+
&& irq != crate::arch_impl::aarch64::timer_interrupt::VIRT_TIMER_IRQ
1013+
&& irq != crate::arch_impl::aarch64::timer_interrupt::PHYS_TIMER_IRQ
1014+
{
1015+
crate::serial_aarch64::raw_serial_char(b'P');
1016+
crate::serial_aarch64::raw_serial_char(b'0' + (irq / 10) as u8);
1017+
crate::serial_aarch64::raw_serial_char(b'0' + (irq % 10) as u8);
1018+
}
1019+
}
10031020

10041021
// SPIs (32-1019) - Shared peripheral interrupts
10051022
// Note: No logging here - interrupt handlers must be < 1000 cycles

0 commit comments

Comments
 (0)