Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 34 additions & 26 deletions kernel/src/drivers/usb/xhci.rs
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,9 @@ pub static DIAG_KBD_PORTSC: AtomicU32 = AtomicU32::new(0);
pub static DIAG_KBD_EP_STATE: AtomicU32 = AtomicU32::new(0);
/// Periodic diagnostic: SPI enable count (how many times SPI was re-enabled).
pub static DIAG_SPI_ENABLE_COUNT: AtomicU64 = AtomicU64::new(0);
/// Set once after the first deferred SPI activation; handle_interrupt
/// keeps SPI alive after that, so poll_hid_events doesn't re-enable.
static SPI_ACTIVATED: AtomicBool = AtomicBool::new(false);
/// Diagnostic counter for doorbell/transfer events (shown as `db=` in heartbeat).
pub static DIAG_DOORBELL_EP_STATE: AtomicU32 = AtomicU32::new(0);
/// Diagnostic: last CC received for any GET_REPORT Transfer Event (0xFF = none seen yet).
Expand Down Expand Up @@ -4777,9 +4780,9 @@ pub fn init(pci_dev: &crate::drivers::pci::Device) -> Result<(), &'static str> {
/// 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).
/// Disables the GIC SPI while processing to prevent re-delivery during
/// IMAN/ERDP acknowledgment, then re-enables it before returning so the
/// next event gets a real interrupt with no polling delay.
pub fn handle_interrupt() {
if !XHCI_INITIALIZED.load(Ordering::Acquire) {
// SPI should not be enabled during init (it's deferred until
Expand Down Expand Up @@ -5012,6 +5015,17 @@ pub fn handle_interrupt() {
}
}

// Re-enable the GIC SPI now that we've drained the event ring.
// Any MSI generated by the IMAN/ERDP writes above will fire as a
// new interrupt after we return. That second invocation will find
// an empty ring (IP=0, no cycle-bit match) and return quickly —
// no storm, because we only write IMAN/USBSTS when their bits are
// actually set.
if state.irq != 0 {
crate::arch_impl::aarch64::gic::clear_spi_pending(state.irq);
crate::arch_impl::aarch64::gic::enable_spi(state.irq);
}

}

// =============================================================================
Expand Down Expand Up @@ -5186,24 +5200,21 @@ fn deferred_queue_trbs(state: &XhciState) {
}
}

/// Timer-tick housekeeping for xHCI.
///
/// 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.
/// Called from the timer interrupt at 200 Hz (every 5ms). Handles:
/// - One-time deferred SPI activation (first 250ms after init)
/// - Endpoint reset recovery for CC=12 errors
/// - Doorbell re-ring after SPI activation
/// - Draining any events the MSI handler missed (safety net only;
/// the primary event path is handle_interrupt)
pub fn poll_hid_events() {
if !XHCI_INITIALIZED.load(Ordering::Acquire) {
return;
}

POLL_COUNT.fetch_add(1, Ordering::Relaxed);

// Rate-limit: poll every 4th tick (~50 Hz at 200 Hz timer).
// Balances responsiveness (20ms latency) with hypervisor overhead.
let poll = POLL_COUNT.load(Ordering::Relaxed);
if poll % 4 != 0 {
return;
}

// try_lock: if someone else holds the lock, skip this poll cycle
let _guard = match XHCI_LOCK.try_lock() {
Some(g) => g,
Expand Down Expand Up @@ -5558,29 +5569,26 @@ pub fn poll_hid_events() {
}

// Deferred MSI activation.
// SPI is enabled after a stabilization period (200 polls = 1 second)
// to avoid interfering with init.
if state.irq != 0 && poll >= 200 {
// Enable SPI for MSI delivery (handle_interrupt disables on each fire)
// Enable SPI after a short stabilization period (50 polls = 250ms)
// so xHCI init completes before interrupts start firing.
// Once enabled, handle_interrupt() re-enables SPI after each invocation,
// so this only matters for the very first activation.
if state.irq != 0 && poll >= 50 && !SPI_ACTIVATED.load(Ordering::Relaxed) {
SPI_ACTIVATED.store(true, Ordering::Release);
crate::arch_impl::aarch64::gic::clear_spi_pending(state.irq);
crate::arch_impl::aarch64::gic::enable_spi(state.irq);
DIAG_SPI_ENABLE_COUNT.fetch_add(1, Ordering::Relaxed);
}

// Ensure HID_TRBS_QUEUED is set after initialization completes.
if poll >= 250 && !HID_TRBS_QUEUED.load(Ordering::Acquire) {
if poll >= 100 && !HID_TRBS_QUEUED.load(Ordering::Acquire) {
HID_TRBS_QUEUED.store(true, Ordering::Release);
}

// Re-ring doorbells after SPI activation (poll=300, ~1.5s after timer starts).
//
// The Parallels vxHC may not process interrupt endpoint TRBs until the MSI/SPI
// interrupt path is active. TRBs were queued and doorbells rung during init
// (before the timer started), but the SPI wasn't enabled until poll=200.
// Re-ringing doorbells after SPI activation tells the xHC to re-check the
// transfer rings now that the interrupt delivery path is ready.
// Re-ring doorbells shortly after SPI activation (poll=75, ~375ms).
// Tells the xHC to re-check transfer rings now that the interrupt path is live.
static DOORBELLS_RE_RUNG: AtomicBool = AtomicBool::new(false);
if poll == 300 && !DOORBELLS_RE_RUNG.load(Ordering::Acquire) {
if poll == 75 && !DOORBELLS_RE_RUNG.load(Ordering::Acquire) {
DOORBELLS_RE_RUNG.store(true, Ordering::Release);
// Mouse EP3
if state.mouse_slot != 0 && state.mouse_endpoint != 0 {
Expand Down
41 changes: 29 additions & 12 deletions kernel/src/drivers/virtio/gpu_pci.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,10 +233,9 @@ 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;
// 1728x1080 matches the QEMU resolution for consistent performance comparison.
const DEFAULT_FB_WIDTH: u32 = 1728;
const DEFAULT_FB_HEIGHT: u32 = 1080;
// Max supported resolution: 2560x1600 @ 32bpp = ~16.4MB
const FB_MAX_WIDTH: u32 = 2560;
const FB_MAX_HEIGHT: u32 = 1600;
Expand Down Expand Up @@ -380,15 +379,20 @@ pub fn init() -> Result<(), &'static str> {
// If create_resource/attach_backing/set_scanout/flush time out, leaving
// the flag true would mislead other code into thinking the device is usable.

// Query display info (ignore result — we override to our desired resolution).
let _ = get_display_info();
// Query display info to see what Parallels reports as native resolution.
let display_dims = get_display_info();
match display_dims {
Ok((dw, dh)) => crate::serial_println!("[virtio-gpu-pci] Display reports: {}x{}", dw, dh),
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);
// Use the display-reported resolution if it's reasonable, otherwise
// fall back to our default. This respects the actual Parallels display
// mode instead of forcing a resolution that may be ignored.
let (use_width, use_height) = match display_dims {
Ok((w, h)) if w >= 640 && h >= 480 && w <= FB_MAX_WIDTH && h <= FB_MAX_HEIGHT => (w, h),
_ => (DEFAULT_FB_WIDTH, DEFAULT_FB_HEIGHT),
};

// Update state with actual dimensions
unsafe {
Expand Down Expand Up @@ -774,6 +778,19 @@ pub fn flush_rect(x: u32, y: u32, width: u32, height: u32) -> Result<(), &'stati
})
}

/// Send only a RESOURCE_FLUSH command without TRANSFER_TO_HOST_2D.
///
/// Used in GOP hybrid mode where pixels are already in BAR0 (the display
/// scanout memory). The RESOURCE_FLUSH tells Parallels which region changed
/// so it can update the host window, without the overhead of a DMA transfer
/// from PCI_FRAMEBUFFER (which isn't used in hybrid mode).
pub fn resource_flush_only(x: u32, y: u32, width: u32, height: u32) -> Result<(), &'static str> {
with_device_state(|state| {
fence(Ordering::SeqCst);
resource_flush_cmd(state, x, y, width, height)
})
}

/// Get the framebuffer dimensions.
pub fn dimensions() -> Option<(u32, u32)> {
unsafe {
Expand Down
4 changes: 4 additions & 0 deletions kernel/src/fs/procfs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ use alloc::vec::Vec;
use spin::Mutex;

mod trace;
#[cfg(target_arch = "aarch64")]
mod xhci;

/// Procfs entry types
Expand Down Expand Up @@ -451,7 +452,10 @@ pub fn read_entry(entry_type: ProcEntryType) -> Result<String, i32> {
let entries = list_xhci_entries();
Ok(entries.join("\n") + "\n")
}
#[cfg(target_arch = "aarch64")]
ProcEntryType::XhciTrace => Ok(xhci::generate_xhci_trace()),
#[cfg(not(target_arch = "aarch64"))]
ProcEntryType::XhciTrace => Ok(String::from("")),
ProcEntryType::BreenixDir => {
// Directory listing
Ok(String::from("testing\n"))
Expand Down
Loading
Loading