diff --git a/Cargo.lock b/Cargo.lock index f433f252..63a1956e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -266,6 +266,7 @@ dependencies = [ "linked_list_allocator", "morpheus-core", "morpheus-display", + "morpheus-hwinit", "smoltcp", "spin", ] diff --git a/bootloader/src/baremetal.rs b/bootloader/src/baremetal.rs index 5bc20eb8..54b51cb6 100644 --- a/bootloader/src/baremetal.rs +++ b/bootloader/src/baremetal.rs @@ -50,6 +50,51 @@ static BAREMETAL_MODE: AtomicBool = AtomicBool::new(false); // After that every puts() in hwinit mirrors to the screen in real-time. static mut LIVE_CONSOLE: Option = None; +static mut PRE_EBS_HELIX_BASE: u64 = 0; +static mut PRE_EBS_HELIX_SIZE: usize = 0; +static mut PRE_EBS_HELIX_SECTOR_SIZE: u32 = 512; + +const LOADED_IMAGE_PROTOCOL_GUID: [u8; 16] = [ + 0xA1, 0x31, 0x1B, 0x5B, 0x62, 0x95, 0xD2, 0x11, 0x8E, 0x3F, 0x00, 0xA0, 0xC9, 0x69, + 0x72, 0x3B, +]; +const SIMPLE_FILE_SYSTEM_PROTOCOL_GUID: [u8; 16] = [ + 0x22, 0x5B, 0x4E, 0x96, 0x59, 0x64, 0xD2, 0x11, 0x8E, 0x39, 0x00, 0xA0, 0xC9, 0x69, + 0x72, 0x3B, +]; +const ACPI_20_TABLE_GUID: [u8; 16] = [ + 0x71, 0xE8, 0x68, 0x88, 0xF1, 0xE4, 0xD3, 0x11, 0xBC, 0x22, 0x00, 0x80, 0xC7, 0x3C, + 0x88, 0x81, +]; +const ACPI_10_TABLE_GUID: [u8; 16] = [ + 0x30, 0x2D, 0x9D, 0xEB, 0x88, 0x2D, 0xD3, 0x11, 0x9A, 0x16, 0x00, 0x90, 0x27, 0x3F, + 0xC1, 0x4D, +]; +const PRE_EBS_STAGE_MAX_BYTES: u64 = 512 * 1024 * 1024; +const HELIX_IMG_SECTOR_SIZE: u32 = 512; +const EFI_FILE_MODE_READ: u64 = 0x0000_0000_0000_0001; +const HELIX_IMG_PATH: [u16; 20] = [ + b'\\' as u16, + b'm' as u16, + b'o' as u16, + b'r' as u16, + b'p' as u16, + b'h' as u16, + b'e' as u16, + b'u' as u16, + b's' as u16, + b'\\' as u16, + b'h' as u16, + b'e' as u16, + b'l' as u16, + b'i' as u16, + b'x' as u16, + b'.' as u16, + b'i' as u16, + b'm' as u16, + b'g' as u16, + 0, +]; /// Called by the hwinit serial hook for every byte emitted via `puts()`. /// Writes directly to the TextConsole backed by the GOP framebuffer. @@ -97,6 +142,20 @@ pub fn get_framebuffer_info() -> Option { } } +pub unsafe fn take_pre_ebs_helix_image() -> Option<(u64, usize, u32)> { + if PRE_EBS_HELIX_BASE == 0 || PRE_EBS_HELIX_SIZE == 0 { + return None; + } + let out = ( + PRE_EBS_HELIX_BASE, + PRE_EBS_HELIX_SIZE, + PRE_EBS_HELIX_SECTOR_SIZE, + ); + PRE_EBS_HELIX_BASE = 0; + PRE_EBS_HELIX_SIZE = 0; + Some(out) +} + // BSoD CRASH HOOK — called by exception handlers in hwinit /// Callback invoked by hwinit exception handlers to display the crash screen. @@ -107,10 +166,59 @@ unsafe fn bsod_crash_hook(info: &morpheus_hwinit::CrashInfo) { crate::bsod::show_crash_screen(info); } +#[inline(always)] +unsafe fn pe_image_size(image_base: u64) -> u64 { + let pe_off_ptr = (image_base + 0x3C) as *const u32; + let pe_off = core::ptr::read_unaligned(pe_off_ptr) as u64; + // SizeOfImage is at offset 56 in the PE32+ optional header. + // PE signature (4) + COFF header (20) + offset 56 into optional header = +80. + let size_of_image_ptr = (image_base + pe_off + 4 + 20 + 56) as *const u32; + core::ptr::read_unaligned(size_of_image_ptr) as u64 +} + +#[repr(C)] +struct EfiLoadedImage { + _revision: u32, + _parent_handle: *mut (), + _system_table: *mut (), + device_handle: *mut (), +} + +#[repr(C)] +struct EfiSimpleFileSystem { + _revision: u64, + open_volume: + extern "efiapi" fn(*mut EfiSimpleFileSystem, *mut *mut EfiFileProtocol) -> usize, +} + +#[repr(C)] +struct EfiConfigurationTable { + vendor_guid: [u8; 16], + vendor_table: *const (), +} + +#[repr(C)] +struct EfiFileProtocol { + _revision: u64, + open: extern "efiapi" fn( + *mut EfiFileProtocol, + *mut *mut EfiFileProtocol, + *const u16, + u64, + u64, + ) -> usize, + close: extern "efiapi" fn(*mut EfiFileProtocol) -> usize, + _delete: usize, + read: extern "efiapi" fn(*mut EfiFileProtocol, *mut usize, *mut u8) -> usize, + _write: usize, + get_position: extern "efiapi" fn(*mut EfiFileProtocol, *mut u64) -> usize, + set_position: extern "efiapi" fn(*mut EfiFileProtocol, u64) -> usize, +} + // THE ENTRY POINT — NEVER RETURNS pub unsafe fn enter_baremetal(config: BaremetalEntryConfig) -> ! { - use morpheus_hwinit::serial::{log_error, log_info, log_ok, puts}; + use morpheus_hwinit::serial::{log_error, log_info, log_ok, log_warn, puts}; // First serial output — if you see this, our binary loaded and ran. log_info("BOOT", 901, "enter baremetal"); @@ -138,6 +246,8 @@ pub unsafe fn enter_baremetal(config: BaremetalEntryConfig) -> ! { _stderr: *const (), _runtime: *const (), boot_services: *const MinBootServices, + number_of_table_entries: usize, + configuration_table: *const EfiConfigurationTable, } #[repr(C)] @@ -151,15 +261,62 @@ pub unsafe fn enter_baremetal(config: BaremetalEntryConfig) -> ! { extern "efiapi" fn(*mut usize, *mut u8, *mut usize, *mut usize, *mut u32) -> usize, _alloc_pool: usize, _free_pool: usize, - // slots 8..26 (create_event → unload_image) = 19 × 8 = 152 bytes - // puts exit_boot_services at offset 232, matching UEFI spec exactly. - _padding: [usize; 19], + _create_event: usize, + _set_timer: usize, + _wait_for_event: usize, + _signal_event: usize, + _close_event: usize, + _check_event: usize, + _install_protocol_interface: usize, + _reinstall_protocol_interface: usize, + _uninstall_protocol_interface: usize, + handle_protocol: + extern "efiapi" fn(*mut (), *const [u8; 16], *mut *mut ()) -> usize, + _reserved: usize, + _register_protocol_notify: usize, + _locate_handle: usize, + _locate_device_path: usize, + _install_configuration_table: usize, + _load_image: usize, + _start_image: usize, + _exit: usize, + _unload_image: usize, exit_boot_services: extern "efiapi" fn(*mut (), usize) -> usize, } let st = &*(config.system_table as *const MinSystemTable); let bs = &*st.boot_services; + let mut acpi_rsdp_phys = 0u64; + if !st.configuration_table.is_null() { + let tables = core::slice::from_raw_parts(st.configuration_table, st.number_of_table_entries); + for t in tables { + if t.vendor_guid == ACPI_20_TABLE_GUID { + acpi_rsdp_phys = t.vendor_table as u64; + break; + } + } + if acpi_rsdp_phys == 0 { + for t in tables { + if t.vendor_guid == ACPI_10_TABLE_GUID { + acpi_rsdp_phys = t.vendor_table as u64; + break; + } + } + } + } + if acpi_rsdp_phys != 0 { + log_ok("ACPI", 901, "found RSDP via UEFI config table"); + } else { + log_warn("ACPI", 901, "RSDP not present in UEFI config table"); + } + + // We still need this for hwinit image reservation post-EBS. + extern "C" { + static __ImageBase: u8; + } + let image_base = &__ImageBase as *const u8 as u64; + log_info("EBS", 902, "allocating kernel stack"); let mut stack_base: u64 = 0; let status = (bs.allocate_pages)(0, 2, STACK_PAGES, &mut stack_base); @@ -174,8 +331,105 @@ pub unsafe fn enter_baremetal(config: BaremetalEntryConfig) -> ! { let stack_top = stack_base + STACK_SIZE as u64; log_info("EBS", 903, "stack ready; fetching memory map"); + // Pre-EBS media stage: load /morpheus/helix.img from ESP into RAM. + let mut loaded_image_ptr: *mut () = core::ptr::null_mut(); + let li_status = (bs.handle_protocol)( + config.image_handle, + &LOADED_IMAGE_PROTOCOL_GUID, + &mut loaded_image_ptr, + ); + if li_status == 0 && !loaded_image_ptr.is_null() { + let li = &*(loaded_image_ptr as *const EfiLoadedImage); + let mut sfs_ptr: *mut () = core::ptr::null_mut(); + let sfs_status = (bs.handle_protocol)( + li.device_handle, + &SIMPLE_FILE_SYSTEM_PROTOCOL_GUID, + &mut sfs_ptr, + ); + if sfs_status == 0 && !sfs_ptr.is_null() { + let sfs = sfs_ptr as *mut EfiSimpleFileSystem; + let mut root: *mut EfiFileProtocol = core::ptr::null_mut(); + if ((*sfs).open_volume)(sfs, &mut root) == 0 && !root.is_null() { + let mut img: *mut EfiFileProtocol = core::ptr::null_mut(); + let open_rc = ((*root).open)( + root, + &mut img, + HELIX_IMG_PATH.as_ptr(), + EFI_FILE_MODE_READ, + 0, + ); + if open_rc == 0 && !img.is_null() { + // Position at EOF to query file size, then rewind. + let _ = ((*img).set_position)(img, u64::MAX); + let mut file_size = 0u64; + let _ = ((*img).get_position)(img, &mut file_size); + let _ = ((*img).set_position)(img, 0); + + if file_size > 0 && file_size <= PRE_EBS_STAGE_MAX_BYTES { + let mut alloc_bytes = file_size as usize; + let rem = alloc_bytes % HELIX_IMG_SECTOR_SIZE as usize; + if rem != 0 { + alloc_bytes += (HELIX_IMG_SECTOR_SIZE as usize) - rem; + } + let mut stage_base: u64 = 0; + let stage_pages = alloc_bytes.div_ceil(4096); + let alloc_rc = (bs.allocate_pages)(0, 2, stage_pages, &mut stage_base); + if alloc_rc == 0 && stage_base != 0 { + let mut off = 0usize; + let mut ok = true; + while off < file_size as usize { + let remaining = (file_size as usize) - off; + let mut want = core::cmp::min(1024 * 1024, remaining); + let rc = ((*img).read)( + img, + &mut want, + (stage_base as *mut u8).add(off), + ); + if rc != 0 { + ok = false; + break; + } + // want == 0 means EOF — nothing more to read. + if want == 0 { + break; + } + off += want; + // don't break on short reads — some UEFI firmware caps per-call + // transfer size. just keep looping with what we got. + } + + if ok && off > 0 { + let usable = off - (off % HELIX_IMG_SECTOR_SIZE as usize); + if usable > 0 { + PRE_EBS_HELIX_BASE = stage_base; + PRE_EBS_HELIX_SIZE = usable; + PRE_EBS_HELIX_SECTOR_SIZE = HELIX_IMG_SECTOR_SIZE; + log_ok("EBS", 901, "pre-EBS loaded /morpheus/helix.img to RAM"); + } else { + log_warn("EBS", 901, "helix.img size not sector-aligned"); + } + } else { + log_warn("EBS", 901, "failed reading /morpheus/helix.img"); + } + } else { + log_warn("EBS", 901, "pre-EBS stage alloc failed"); + } + } else { + log_warn("EBS", 901, "helix.img missing/empty/too-large"); + } + let _ = ((*img).close)(img); + } else { + log_warn("EBS", 901, "/morpheus/helix.img not found on ESP"); + } + let _ = ((*root).close)(root); + } + } + } + + log_info("EBS", 903, "fetching memory map"); + // memory map + exitbootservices - static mut MMAP_BUF: [u8; 32768] = [0u8; 32768]; + static mut MMAP_BUF: [u8; 65536] = [0u8; 65536]; static mut MMAP_SIZE: usize = 0; static mut DESC_SIZE: usize = 0; static mut DESC_VER: u32 = 0; @@ -233,28 +487,13 @@ pub unsafe fn enter_baremetal(config: BaremetalEntryConfig) -> ! { // Switch allocator to our own heap (UEFI pool is gone) crate::uefi_allocator::switch_to_post_ebs(); - // switch stack + // Switch once, after EBS, onto our own loaderdata stack. core::arch::asm!("mov rsp, {0}", in(reg) stack_top, options(nostack)); log_ok("BOOT", 906, "machine ownership transferred"); // compute pe image bounds for buddy-allocator reservation - // __ImageBase is defined by LLD for every PE/COFF binary. From it we - // read the PE optional header's SizeOfImage to determine the full - // virtual extent (including BSS) that must be kept out of the free pool. - extern "C" { - static __ImageBase: u8; - } - let image_base = &__ImageBase as *const u8 as u64; - let image_pages = { - let pe_off_ptr = (image_base + 0x3C) as *const u32; - let pe_off = core::ptr::read_unaligned(pe_off_ptr) as u64; - // SizeOfImage is at offset 56 in the PE32+ optional header. - // PE signature (4) + COFF header (20) + offset 56 into optional header = +80. - let size_of_image_ptr = (image_base + pe_off + 4 + 20 + 56) as *const u32; - let size_of_image = core::ptr::read_unaligned(size_of_image_ptr) as u64; - size_of_image.div_ceil(4096) - }; + let image_pages = pe_image_size(image_base).div_ceil(4096); // hwinit — gdt, idt, pic, heap, tsc, dma, bus mastering let hwinit_cfg = morpheus_hwinit::SelfContainedConfig { @@ -266,6 +505,7 @@ pub unsafe fn enter_baremetal(config: BaremetalEntryConfig) -> ! { image_pages, stack_base, stack_pages: STACK_PAGES as u64, + acpi_rsdp_phys, }; let platform = match morpheus_hwinit::platform_init_selfcontained(hwinit_cfg) { diff --git a/bootloader/src/storage.rs b/bootloader/src/storage.rs index ab217b83..7bb7278c 100644 --- a/bootloader/src/storage.rs +++ b/bootloader/src/storage.rs @@ -24,15 +24,20 @@ //! Total ≈ 128 KB (well within 2 MB) //! ``` -use morpheus_helix::device::RawBlockDevice; +use morpheus_helix::device::{MemBlockDevice, RawBlockDevice}; +/// In-RAM copy of the selected Helix partition (if RAM staging succeeds). +static mut RAM_HELIX_DEVICE: Option = None; use morpheus_hwinit::dma::DmaRegion; +use morpheus_hwinit::memory::{global_registry_mut, AllocateType, MemoryType}; use morpheus_hwinit::paging::is_paging_initialized; use morpheus_hwinit::serial::{log_error, log_info, log_ok, log_warn, puts}; use morpheus_hwinit::{kmap_mmio, pci_cfg_read16, pci_cfg_read32, PciAddr}; use morpheus_network::{ + AhciInitError, SdhciInitError, UsbMsdInitError, VirtioBlkInitError, create_unified_from_detected, scan_all_block_devices, BlockDmaConfig, BlockDriver, DetectedBlockDevice, UnifiedBlockDevice, UnifiedBlockIo, }; +use morpheus_network::device::UnifiedBlockError; // DMA LAYOUT CONSTANTS @@ -54,6 +59,7 @@ const OFF_AHCI_IDENTIFY: usize = 0x0_4800; // I/O transfer buffer — used by UnifiedBlockIo for synchronous read/write const OFF_IO_BUFFER: usize = 0x1_0000; const IO_BUFFER_SIZE: usize = 64 * 1024; // 64 KB = UnifiedBlockIo::MAX_TRANSFER_SIZE +const UNKNOWN_TOTAL_SECTORS: u64 = u32::MAX as u64; // PCI BUS DUMP (diagnostic) @@ -207,10 +213,366 @@ static mut STORAGE_DMA: Option = None; /// TSC frequency for timeout computation. static mut STORAGE_TSC_FREQ: u64 = 0; +/// Start LBA of selected persistent region (0 = whole disk). +static mut STORAGE_LBA_BASE: u64 = 0; +/// Sector count of selected persistent region. +static mut STORAGE_REGION_SECTORS: u64 = 0; /// Whether persistent storage was successfully initialized. static mut PERSISTENT_READY: bool = false; +const RAM_STAGE_MAX_BYTES: u64 = 512 * 1024 * 1024; +static mut RAM_STAGE_LAST_REASON: &'static str = "none"; + +const GPT_SIG: &[u8; 8] = b"EFI PART"; +// EFI System Partition type GUID on disk (little-endian fields). +const GPT_TYPE_ESP: [u8; 16] = [ + 0x28, 0x73, 0x2A, 0xC1, 0x1F, 0xF8, 0xD2, 0x11, 0xBA, 0x4B, 0x00, 0xA0, 0xC9, 0x3E, 0xC9, + 0x3B, +]; + +#[derive(Clone, Copy)] +struct DataRegion { + lba_start: u64, + sectors: u64, +} + +#[derive(Clone, Copy)] +struct GptPartition { + type_guid: [u8; 16], + first_lba: u64, + last_lba: u64, +} + +#[inline(always)] +fn le_u32(buf: &[u8], off: usize) -> u32 { + u32::from_le_bytes([buf[off], buf[off + 1], buf[off + 2], buf[off + 3]]) +} + +#[inline(always)] +fn le_u64(buf: &[u8], off: usize) -> u64 { + u64::from_le_bytes([ + buf[off], + buf[off + 1], + buf[off + 2], + buf[off + 3], + buf[off + 4], + buf[off + 5], + buf[off + 6], + buf[off + 7], + ]) +} + +#[inline(always)] +fn select_largest_gpt_free_region( + first_usable: u64, + last_usable: u64, + used_ranges: &mut alloc::vec::Vec<(u64, u64)>, + sector_size: u32, +) -> Option { + if first_usable == 0 || last_usable < first_usable { + return None; + } + + used_ranges.sort_unstable_by_key(|(start, _)| *start); + + let mut cursor = first_usable; + let mut best_start = 0u64; + let mut best_sectors = 0u64; + + for (raw_start, raw_end) in used_ranges.iter().copied() { + if raw_end < first_usable || raw_start > last_usable { + continue; + } + + let start = raw_start.max(first_usable); + let end = raw_end.min(last_usable); + + if start > cursor { + let gap = start - cursor; + if gap > best_sectors { + best_start = cursor; + best_sectors = gap; + } + } + + let next = end.saturating_add(1); + if next > cursor { + cursor = next; + } + + if cursor > last_usable { + break; + } + } + + if cursor <= last_usable { + let tail = (last_usable - cursor) + 1; + if tail > best_sectors { + best_start = cursor; + best_sectors = tail; + } + } + + let min_free_sectors = ((64u64 * 1024 * 1024) + (sector_size as u64 - 1)) / sector_size as u64; + if best_sectors >= min_free_sectors { + Some(DataRegion { + lba_start: best_start, + sectors: best_sectors, + }) + } else { + None + } +} + +/// Select a writable data region from the currently active block device. +/// +/// Policy: +/// - GPT disk: prefer partition immediately after ESP on same disk +/// - MBR-only disk: prefer partition immediately after EFI partition entry +/// - Fallback: largest free GPT region, then first non-ESP partition, then largest MBR primary +/// - No partition table: whole disk is data region +unsafe fn select_data_region(sector_size: u32, total_sectors: u64) -> Option { + let dev = BLOCK_DEVICE.as_mut()?; + let dma = STORAGE_DMA.as_ref()?; + + let io_cpu = dma.cpu_base().add(OFF_IO_BUFFER); + let io_phys = dma.bus_at(OFF_IO_BUFFER); + let io_buf = core::slice::from_raw_parts_mut(io_cpu, IO_BUFFER_SIZE); + let timeout = STORAGE_TSC_FREQ * 5; + + let mut bio = UnifiedBlockIo::new(dev, io_buf, io_phys, timeout).ok()?; + + use morpheus_network::GptBlockIo; + use morpheus_network::GptLba; + + let mut first_two = alloc::vec![0u8; (sector_size as usize) * 2]; + if bio.read_blocks(GptLba(0), &mut first_two).is_err() { + return None; + } + + let has_mbr = first_two.len() >= 512 && first_two[510] == 0x55 && first_two[511] == 0xAA; + let gpt_off = sector_size as usize; + let has_gpt = first_two.len() >= gpt_off + 8 && &first_two[gpt_off..gpt_off + 8] == GPT_SIG; + + if has_gpt { + let hdr = &first_two[gpt_off..gpt_off + sector_size as usize]; + let first_usable = le_u64(hdr, 40); + let last_usable = le_u64(hdr, 48); + let entries_lba = le_u64(hdr, 72); + let num_entries = le_u32(hdr, 80) as usize; + let entry_size = le_u32(hdr, 84) as usize; + + if entry_size < 56 || num_entries == 0 { + return None; + } + + let entries_per_sector = (sector_size as usize) / entry_size; + if entries_per_sector == 0 { + return None; + } + + let mut sec = alloc::vec![0u8; sector_size as usize]; + let mut used_ranges = alloc::vec::Vec::<(u64, u64)>::new(); + let mut partitions = alloc::vec::Vec::::new(); + let mut first_non_esp: Option = None; + + for idx in 0..num_entries { + let sector_delta = idx / entries_per_sector; + let idx_in_sector = idx % entries_per_sector; + let lba = entries_lba + sector_delta as u64; + + if bio.read_blocks(GptLba(lba), &mut sec).is_err() { + return None; + } + + let off = idx_in_sector * entry_size; + let ent = &sec[off..off + entry_size]; + + // Empty partition entry if type GUID is all zero. + if ent[..16].iter().all(|b| *b == 0) { + continue; + } + + let first_lba = le_u64(ent, 32); + let last_lba = le_u64(ent, 40); + if first_lba == 0 || last_lba < first_lba { + continue; + } + + used_ranges.push((first_lba, last_lba)); + + partitions.push(GptPartition { + type_guid: [ + ent[0], ent[1], ent[2], ent[3], ent[4], ent[5], ent[6], ent[7], ent[8], + ent[9], ent[10], ent[11], ent[12], ent[13], ent[14], ent[15], + ], + first_lba, + last_lba, + }); + + if ent[..16] == GPT_TYPE_ESP { + continue; + } + + let sectors = last_lba - first_lba + 1; + if sectors == 0 { + continue; + } + + if first_non_esp.is_none() { + first_non_esp = Some(DataRegion { + lba_start: first_lba, + sectors, + }); + } + } + + // Pair Helix with the same media as the boot ESP: pick next partition after ESP. + partitions.sort_unstable_by_key(|p| p.first_lba); + if let Some((boot_idx, boot_part)) = partitions + .iter() + .enumerate() + .find(|(_, p)| p.type_guid == GPT_TYPE_ESP) + { + let mut next_after_boot: Option = None; + for part in partitions.iter().skip(boot_idx + 1) { + if part.type_guid == GPT_TYPE_ESP { + continue; + } + if part.first_lba <= boot_part.last_lba { + continue; + } + + let sectors = part.last_lba - part.first_lba + 1; + if sectors == 0 { + continue; + } + + if part.first_lba == boot_part.last_lba.saturating_add(1) { + return Some(DataRegion { + lba_start: part.first_lba, + sectors, + }); + } + + if next_after_boot.is_none() { + next_after_boot = Some(DataRegion { + lba_start: part.first_lba, + sectors, + }); + } + } + + if let Some(region) = next_after_boot { + return Some(region); + } + } + + // Legacy fallback when explicit ESP pairing is unavailable. + if let Some(region) = + select_largest_gpt_free_region(first_usable, last_usable, &mut used_ranges, sector_size) + { + return Some(region); + } + + if let Some(region) = first_non_esp { + return Some(region); + } + + return None; + } + + if has_mbr { + // Prefer the partition entry after EFI partition entry in MBR layout. + const MBR_PART_OFF: usize = 446; + const MBR_PART_SIZE: usize = 16; + const MBR_PARTS: usize = 4; + let mut mbr_parts: [Option<(u8, u64, u64)>; MBR_PARTS] = [None, None, None, None]; + + let mut best_start = 0u64; + let mut best_sectors = 0u64; + + for i in 0..MBR_PARTS { + let off = MBR_PART_OFF + (i * MBR_PART_SIZE); + let ptype = first_two[off + 4]; + + // 0x00 empty, 0xEE GPT protective, 0xEF EFI system partition. + if ptype == 0x00 || ptype == 0xEE || ptype == 0xEF { + continue; + } + + // Extended partition containers are not directly writable data regions. + if ptype == 0x05 || ptype == 0x0F || ptype == 0x85 { + continue; + } + + let start = le_u32(&first_two, off + 8) as u64; + let sectors = le_u32(&first_two, off + 12) as u64; + + if start == 0 || sectors == 0 { + continue; + } + + if total_sectors != UNKNOWN_TOTAL_SECTORS + && start.saturating_add(sectors) > total_sectors + { + continue; + } + + mbr_parts[i] = Some((ptype, start, sectors)); + + if sectors > best_sectors { + best_start = start; + best_sectors = sectors; + } + } + + if let Some((boot_idx, _)) = mbr_parts + .iter() + .enumerate() + .find(|(_, p)| matches!(p, Some((0xEF, _, _)))) + { + for part in mbr_parts.iter().skip(boot_idx + 1) { + if let Some((ptype, start, sectors)) = *part { + if ptype == 0x00 || ptype == 0xEE || ptype == 0xEF { + continue; + } + if ptype == 0x05 || ptype == 0x0F || ptype == 0x85 { + continue; + } + if start == 0 || sectors == 0 { + continue; + } + + return Some(DataRegion { + lba_start: start, + sectors, + }); + } + } + } + + if best_sectors != 0 { + return Some(DataRegion { + lba_start: best_start, + sectors: best_sectors, + }); + } + + return None; + } + + if total_sectors == UNKNOWN_TOTAL_SECTORS { + return None; + } + + Some(DataRegion { + lba_start: 0, + sectors: total_sectors, + }) +} + // spinner static mut SPIN_ACTIVE: bool = false; @@ -285,6 +647,37 @@ fn spinner_done() { pub unsafe fn init_persistent_storage(dma: &DmaRegion, tsc_freq: u64) { log_info("STORAGE", 822, "probing block devices"); + if let Some((base, size, sector_size)) = crate::baremetal::take_pre_ebs_helix_image() { + log_info("STORAGE", 822, "using pre-EBS staged Helix image"); + RAM_HELIX_DEVICE = Some(MemBlockDevice::new(base as *mut u8, size, sector_size)); + if let Some(mem_dev) = RAM_HELIX_DEVICE.as_mut() { + match morpheus_helix::vfs::global::replace_root_device( + MemBlockDevice::into_raw(mem_dev), + false, + ) { + Ok(()) => { + if root_path_exists("/bin/init") { + PERSISTENT_READY = true; + log_ok("STORAGE", 827, "mounted pre-EBS staged Helix root"); + return; + } + log_warn( + "STORAGE", + 827, + "pre-EBS staged root missing /bin/init; falling back to probe", + ); + } + Err(_) => { + log_warn( + "STORAGE", + 827, + "pre-EBS staged root mount failed; falling back to probe", + ); + } + } + } + } + // Dump PCI bus to serial for device identification diagnostics // (Commenting out PCI dump — enable if debugging PCI device discovery) // dump_pci_devices(); @@ -340,22 +733,240 @@ pub unsafe fn init_persistent_storage(dma: &DmaRegion, tsc_freq: u64) { // Try each device: skip boot disks (GPT/MBR), use the first blank or HelixFS one. let mut found_data_disk = false; - #[allow(clippy::needless_range_loop)] - for i in 0..dev_count { + let mut saw_unimplemented_backend = false; + 'device_scan: for i in 0..dev_count { let detected = match &devices[i] { Some(d) => d, None => continue, }; + match detected { + DetectedBlockDevice::Ahci(_) => { + log_info("STORAGE", 824, "probing AHCI candidate"); + } + DetectedBlockDevice::Sdhci(_) => { + log_info("STORAGE", 824, "probing SDHCI candidate"); + } + DetectedBlockDevice::UsbMsd(_) => { + log_info("STORAGE", 824, "probing USB xHCI/MSD candidate"); + } + DetectedBlockDevice::VirtIO { .. } => { + log_info("STORAGE", 824, "probing VirtIO-blk candidate"); + } + } + // Map high-address MMIO BARs before driver init touches them if let DetectedBlockDevice::VirtIO { pci_addr, .. } = detected { map_virtio_bars(pci_addr.bus, pci_addr.device, pci_addr.function); + } else if let DetectedBlockDevice::Ahci(info) = detected { + if is_paging_initialized() { + // ABAR covers generic HBA regs + up to 32 ports × 0x80 = 0x1100. + // 0x2000 rounds to 2 pages; UC flags set by kmap_mmio. + let _ = kmap_mmio(info.abar, 0x2000); + } + } else if let DetectedBlockDevice::Sdhci(info) = detected { + if is_paging_initialized() { + // SDHCI register space is typically < 4KB, map one page. + let _ = kmap_mmio(info.mmio_base, 0x1000); + } + } else if let DetectedBlockDevice::UsbMsd(info) = detected { + if is_paging_initialized() { + // xHCI operational + runtime windows vary by controller; map a conservative range. + let _ = kmap_mmio(info.mmio_base, 0x4000); + } } + let is_ahci = matches!(detected, DetectedBlockDevice::Ahci(_)); let device = match create_unified_from_detected(detected, &config) { Ok(dev) => dev, - Err(_) => { - log_warn("STORAGE", 825, "driver init failed for one candidate; skipping"); + Err(err) => { + match err { + UnifiedBlockError::AhciError(e) => { + match e { + AhciInitError::InvalidConfig => { + log_warn("STORAGE", 825, "AHCI init failed: invalid config"); + } + AhciInitError::ResetFailed => { + log_warn("STORAGE", 825, "AHCI init failed: HBA reset timeout"); + } + AhciInitError::NoDeviceFound => { + log_warn("STORAGE", 825, "AHCI init failed: no SATA device found"); + } + AhciInitError::PortStopTimeout => { + log_warn("STORAGE", 825, "AHCI init failed: port stop timeout"); + } + AhciInitError::PortStartFailed => { + log_warn("STORAGE", 825, "AHCI init failed: port start failed"); + } + AhciInitError::IdentifyFailed => { + log_warn("STORAGE", 825, "AHCI init failed: IDENTIFY failed"); + } + AhciInitError::No64BitSupport => { + log_warn("STORAGE", 825, "AHCI init failed: no 64-bit DMA support"); + } + AhciInitError::DeviceNotResponding => { + log_warn("STORAGE", 825, "AHCI init failed: device not responding"); + } + AhciInitError::DmaSetupFailed => { + log_warn("STORAGE", 825, "AHCI init failed: DMA setup failed"); + } + } + log_warn("STORAGE", 825, "AHCI candidate skipped"); + } + UnifiedBlockError::VirtioError(e) => { + match e { + VirtioBlkInitError::ResetFailed => { + log_warn("STORAGE", 825, "VirtIO init failed: reset failed"); + } + VirtioBlkInitError::FeatureNegotiationFailed => { + log_warn("STORAGE", 825, "VirtIO init failed: feature negotiation failed"); + } + VirtioBlkInitError::QueueSetupFailed => { + log_warn("STORAGE", 825, "VirtIO init failed: queue setup failed"); + } + VirtioBlkInitError::DeviceFailed => { + log_warn("STORAGE", 825, "VirtIO init failed: device failed status"); + } + VirtioBlkInitError::InvalidConfig => { + log_warn("STORAGE", 825, "VirtIO init failed: invalid config"); + } + VirtioBlkInitError::TransportError => { + log_warn("STORAGE", 825, "VirtIO init failed: transport error"); + } + } + log_warn("STORAGE", 825, "VirtIO candidate skipped"); + } + UnifiedBlockError::NoDevice => { + if is_ahci { + log_warn("STORAGE", 825, "AHCI controller init failed; skipping candidate"); + } else { + log_warn("STORAGE", 825, "driver init failed for one candidate; skipping"); + } + } + UnifiedBlockError::SdhciError(e) => { + match e { + SdhciInitError::InvalidConfig => { + log_warn("STORAGE", 825, "SDHCI init failed: invalid config"); + } + SdhciInitError::ControllerResetFailed => { + log_warn("STORAGE", 825, "SDHCI init failed: controller reset failed"); + } + SdhciInitError::NoCardPresent => { + log_warn("STORAGE", 825, "SDHCI init failed: no card present"); + } + SdhciInitError::VoltageSwitchFailed => { + log_warn("STORAGE", 825, "SDHCI init failed: voltage switch failed"); + } + SdhciInitError::ClockSetupFailed => { + log_warn("STORAGE", 825, "SDHCI init failed: clock setup failed"); + } + SdhciInitError::CommandTimeout => { + log_warn("STORAGE", 825, "SDHCI init failed: command timeout"); + } + SdhciInitError::DataTimeout => { + log_warn("STORAGE", 825, "SDHCI init failed: data timeout"); + } + SdhciInitError::IoError => { + log_warn("STORAGE", 825, "SDHCI init failed: I/O error"); + } + SdhciInitError::NotImplemented => { + saw_unimplemented_backend = true; + log_warn("STORAGE", 825, "SDHCI init failed: not implemented"); + } + } + log_warn("STORAGE", 825, "SDHCI candidate skipped"); + } + UnifiedBlockError::UsbMsdError(e) => { + match e { + UsbMsdInitError::InvalidConfig => { + log_warn("STORAGE", 825, "USB-MSD init failed: invalid config"); + } + UsbMsdInitError::ControllerInitFailed => { + log_warn("STORAGE", 825, "USB-MSD init failed: controller init failed"); + } + UsbMsdInitError::ControllerProbeFailed => { + log_warn("STORAGE", 825, "USB-MSD init failed: controller probe failed"); + } + UsbMsdInitError::ControllerResetFailed => { + log_warn("STORAGE", 825, "USB-MSD init failed: controller reset failed"); + } + UsbMsdInitError::ControllerScratchpadUnsupported => { + log_warn("STORAGE", 825, "USB-MSD init failed: scratchpad requirement unsupported"); + } + UsbMsdInitError::ControllerStartFailed => { + log_warn("STORAGE", 825, "USB-MSD init failed: controller start failed (HCH stuck)"); + } + UsbMsdInitError::HubUnsupported => { + log_warn("STORAGE", 825, "USB-MSD init failed: USB hub detected; downstream hub traversal not implemented"); + } + UsbMsdInitError::PortResetFailed => { + log_warn("STORAGE", 825, "USB-MSD init failed: port reset failed"); + } + UsbMsdInitError::PortResetTimeout => { + log_warn("STORAGE", 825, "USB-MSD init failed: port reset timeout"); + } + UsbMsdInitError::PortResetHotCmdTimeout => { + log_warn("STORAGE", 825, "USB-MSD init failed: hot reset command timeout"); + } + UsbMsdInitError::PortResetHotSettleTimeout => { + log_warn("STORAGE", 825, "USB-MSD init failed: hot reset settle timeout"); + } + UsbMsdInitError::PortResetWarmTimeout => { + log_warn("STORAGE", 825, "USB-MSD init failed: warm reset timeout"); + } + UsbMsdInitError::PortResetNoLink => { + log_warn("STORAGE", 825, "USB-MSD init failed: USB link not usable"); + } + UsbMsdInitError::EnableSlotFailed => { + log_warn("STORAGE", 825, "USB-MSD init failed: enable-slot command failed"); + } + UsbMsdInitError::AddressDeviceFailed => { + log_warn("STORAGE", 825, "USB-MSD init failed: address-device command failed"); + } + UsbMsdInitError::DeviceDescriptorFailed => { + log_warn("STORAGE", 825, "USB-MSD init failed: GET_DESCRIPTOR(device) failed"); + } + UsbMsdInitError::ConfigDescriptorFailed => { + log_warn("STORAGE", 825, "USB-MSD init failed: GET_DESCRIPTOR(config) failed"); + } + UsbMsdInitError::MassStorageProtocolUnsupported => { + log_warn("STORAGE", 825, "USB-MSD init failed: mass-storage protocol unsupported (need BOT)"); + } + UsbMsdInitError::NoBotMassStorageInterface => { + log_warn("STORAGE", 825, "USB-MSD init failed: no BOT mass-storage interface found"); + } + UsbMsdInitError::ActivePortsNoConnectedDevice => { + log_warn("STORAGE", 825, "USB-MSD init failed: root ports active but no connected device detected"); + } + UsbMsdInitError::SetConfigurationFailed => { + log_warn("STORAGE", 825, "USB-MSD init failed: SET_CONFIGURATION failed"); + } + UsbMsdInitError::ConfigureEndpointsFailed => { + log_warn("STORAGE", 825, "USB-MSD init failed: configure endpoint command failed"); + } + UsbMsdInitError::DeviceEnumerationFailed => { + log_warn("STORAGE", 825, "USB-MSD init failed: device enumeration failed"); + } + UsbMsdInitError::TransportInitFailed => { + log_warn("STORAGE", 825, "USB-MSD init failed: transport init failed"); + } + UsbMsdInitError::NoMedia => { + log_warn("STORAGE", 825, "USB-MSD init failed: no media"); + } + UsbMsdInitError::CommandTimeout => { + log_warn("STORAGE", 825, "USB-MSD init failed: command timeout"); + } + UsbMsdInitError::IoError => { + log_warn("STORAGE", 825, "USB-MSD init failed: I/O error"); + } + UsbMsdInitError::NotImplemented => { + saw_unimplemented_backend = true; + log_warn("STORAGE", 825, "USB-MSD init failed: not implemented"); + } + } + log_warn("STORAGE", 825, "USB-MSD candidate skipped"); + } + } continue; } }; @@ -365,15 +976,29 @@ pub unsafe fn init_persistent_storage(dma: &DmaRegion, tsc_freq: u64) { // Store temporarily to check if it's a boot disk. BLOCK_DEVICE = Some(device); - if is_boot_disk(info.sector_size) { - log_info("STORAGE", 826, "boot/system disk detected; skipping"); + let region = match select_data_region(info.sector_size, info.total_sectors) { + Some(r) => r, + None => { + log_info("STORAGE", 826, "boot/system disk detected; skipping"); + BLOCK_DEVICE = None; + continue; + } + }; + + STORAGE_LBA_BASE = region.lba_start; + STORAGE_REGION_SECTORS = region.sectors; + + if region.lba_start != 0 { + log_info("STORAGE", 837, "selected non-zero LBA data region"); + } + + if region.sectors == 0 { BLOCK_DEVICE = None; continue; } - // This device is NOT a boot disk — use it for HelixFS. - log_ok("STORAGE", 827, "selected data disk"); - found_data_disk = true; + // Candidate selected. We only commit once it proves to hold HelixFS. + log_ok("STORAGE", 827, "selected data-disk candidate"); // try to recover or format helixfs let mut raw_dev = make_raw_block_device(); @@ -399,75 +1024,137 @@ pub unsafe fn init_persistent_storage(dma: &DmaRegion, tsc_freq: u64) { }; if needs_format { - log_info("STORAGE", 829, "no valid helixfs; formatting disk"); - - let uuid = [ - 0x4Du8, 0x58, 0x52, 0x4F, 0x4F, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x01, - ]; - spinner_start(); - match morpheus_helix::format::format_helix( - &mut raw_dev, - 0, - info.total_sectors, - info.sector_size, - "root", - uuid, - ) { - Ok(_sb) => { - spinner_done(); - log_ok("STORAGE", 830, "format completed"); - } - Err(e) => { - spinner_done(); - let _ = e; - log_error("STORAGE", 831, "format failed"); - BLOCK_DEVICE = None; - break; - } - } - - // Verify format succeeded - match morpheus_helix::log::recovery::recover_superblock( - &mut raw_dev, - 0, - info.sector_size, - ) { - Ok(_sb) => {} - Err(e) => { - let _ = e; - log_warn("STORAGE", 832, "superblock readback failed after format"); - } - } + // Bring-up safety: never auto-format random host disks at boot. + // Keep scanning until we find an existing HelixFS root. + log_warn("STORAGE", 829, "no valid helixfs on candidate; skipping disk"); + BLOCK_DEVICE = None; + continue; } else { log_info("STORAGE", 833, "valid helixfs found; mounting"); } - // Now do the actual replace_root_device - // If we already formatted above, pass do_format=false to avoid double-format - let mount_dev = make_raw_block_device(); + let mut mounted_from_ram = false; + let mount_dev = match stage_selected_region_to_ram(info.sector_size) { + Some(mem_dev) => { + log_ok("STORAGE", 838, "helix partition staged into RAM"); + mounted_from_ram = true; + mem_dev + } + None => { + let reason = RAM_STAGE_LAST_REASON; + if reason == "none" { + log_warn("STORAGE", 838, "RAM staging failed; mounting directly from media"); + } else { + log_warn("STORAGE", 838, reason); + } + make_raw_block_device() + } + }; + spinner_start(); match morpheus_helix::vfs::global::replace_root_device(mount_dev, false) { Ok(()) => { spinner_done(); + let mut root_has_init = root_path_exists("/bin/init"); + if mounted_from_ram && !root_path_exists("/bin/init") { + log_warn( + "STORAGE", + 844, + "RAM-staged root missing /bin/init; remounting directly from media", + ); + spinner_start(); + match morpheus_helix::vfs::global::replace_root_device( + make_raw_block_device(), + false, + ) { + Ok(()) => { + spinner_done(); + if root_path_exists("/bin/init") { + root_has_init = true; + log_ok("STORAGE", 845, "direct-media root remount restored /bin/init"); + } else { + root_has_init = false; + log_warn( + "STORAGE", + 846, + "direct-media root still missing /bin/init", + ); + } + } + Err(_) => { + spinner_done(); + root_has_init = false; + log_warn("STORAGE", 847, "direct-media root remount failed"); + } + } + } + + if !root_has_init { + log_warn( + "STORAGE", + 851, + "candidate root rejected: /bin/init missing; scanning next disk", + ); + BLOCK_DEVICE = None; + continue; + } + PERSISTENT_READY = true; + found_data_disk = true; + if root_path_exists("/bin/init") { + log_ok("STORAGE", 848, "root check: /bin/init present"); + } else { + log_warn("STORAGE", 848, "root check: /bin/init missing"); + } + if root_path_exists("/bin/compd") { + log_ok("STORAGE", 849, "root check: /bin/compd present"); + } else { + log_warn("STORAGE", 849, "root check: /bin/compd missing"); + } + if root_path_exists("/bin/shelld") { + log_ok("STORAGE", 850, "root check: /bin/shelld present"); + } else { + log_warn("STORAGE", 850, "root check: /bin/shelld missing"); + } log_ok("STORAGE", 834, "persistent root filesystem mounted at /"); + break 'device_scan; } Err(e) => { spinner_done(); let _ = e; log_error("STORAGE", 835, "failed to mount persistent filesystem"); BLOCK_DEVICE = None; + continue; } } - break; } if !found_data_disk { log_warn("STORAGE", 836, "no suitable data disk; using RAM-disk fallback"); + log_warn( + "STORAGE", + 839, + "runtime persistent backends currently support AHCI/VirtIO/SDHCI (USB/NVMe pending)", + ); + if saw_unimplemented_backend { + log_error( + "STORAGE", + 852, + "boot medium backend is scaffold-only (SDHCI/USB-MSD not implemented); /bin/init will be unavailable", + ); + } } } +fn root_path_exists(path: &str) -> bool { + let fs = match unsafe { morpheus_helix::vfs::global::fs_global_mut() } { + Some(f) => f, + None => return false, + }; + + morpheus_helix::vfs::vfs_stat(&fs.mount_table, path).is_ok() +} + /// Whether persistent storage is active (vs RAM-disk fallback). pub fn is_persistent() -> bool { unsafe { PERSISTENT_READY } @@ -529,6 +1216,142 @@ unsafe fn is_boot_disk(sector_size: u32) -> bool { false } +/// Copy selected Helix partition content into RAM and expose it as RawBlockDevice. +unsafe fn stage_selected_region_to_ram(sector_size: u32) -> Option { + RAM_STAGE_LAST_REASON = "none"; + log_info("STORAGE", 843, "RAM stage attempt begin"); + + let mut probe = make_raw_block_device(); + let sb = match morpheus_helix::log::recovery::recover_superblock(&mut probe, 0, sector_size) { + Ok(sb) => sb, + Err(_) => { + RAM_STAGE_LAST_REASON = "RAM stage: superblock probe failed; mounting directly from media"; + return None; + } + }; + + // Stage only live filesystem footprint, not full partition capacity. + let mut stage_blocks = 2u64; + let log_hi = sb.log_end_block.saturating_add(1); + if log_hi > stage_blocks { + stage_blocks = log_hi; + } + + let data_hi = sb.data_start_block.saturating_add(sb.blocks_used); + if data_hi > stage_blocks { + stage_blocks = data_hi; + } + + if stage_blocks > sb.total_blocks { + stage_blocks = sb.total_blocks; + } + + if stage_blocks == 0 { + RAM_STAGE_LAST_REASON = "RAM stage: empty footprint; mounting directly from media"; + return None; + } + + let fs_bytes = match stage_blocks.checked_mul(sb.block_size as u64) { + Some(v) => v, + None => { + RAM_STAGE_LAST_REASON = "RAM stage: byte size overflow; mounting directly from media"; + return None; + } + }; + if fs_bytes == 0 { + return None; + } + + if fs_bytes > RAM_STAGE_MAX_BYTES { + RAM_STAGE_LAST_REASON = "RAM stage: footprint exceeds RAM cap; mounting directly from media"; + return None; + } + + if fs_bytes > usize::MAX as u64 { + RAM_STAGE_LAST_REASON = "RAM stage: size exceeds usize; mounting directly from media"; + return None; + } + + let sector_bytes = sector_size as usize; + let mut copy_bytes = fs_bytes as usize; + let rem = copy_bytes % sector_bytes; + if rem != 0 { + copy_bytes = match copy_bytes.checked_add(sector_bytes - rem) { + Some(v) => v, + None => { + RAM_STAGE_LAST_REASON = "RAM stage: alignment overflow; mounting directly from media"; + return None; + } + }; + } + + let region_bytes = match (STORAGE_REGION_SECTORS as usize).checked_mul(sector_bytes) { + Some(v) => v, + None => { + RAM_STAGE_LAST_REASON = "RAM stage: region size overflow; mounting directly from media"; + return None; + } + }; + if copy_bytes > region_bytes { + RAM_STAGE_LAST_REASON = + "RAM stage: footprint exceeds selected region; mounting directly from media"; + return None; + } + + let stage_pages = copy_bytes.div_ceil(4096); + log_info("STORAGE", 843, "RAM stage: allocating page-backed image"); + let stage_base = { + let mut registry = global_registry_mut(); + match registry.allocate_pages( + AllocateType::AnyPages, + MemoryType::LoaderData, + stage_pages as u64, + ) { + Ok(p) => p, + Err(_) => { + RAM_STAGE_LAST_REASON = + "RAM stage: page allocation failed; mounting directly from media"; + return None; + } + } + }; + + if stage_base == 0 { + RAM_STAGE_LAST_REASON = "RAM stage: zero allocation base; mounting directly from media"; + return None; + } + + let image = core::slice::from_raw_parts_mut(stage_base as *mut u8, copy_bytes); + + const CHUNK_SECTORS: usize = 256; + let chunk_bytes = CHUNK_SECTORS * sector_bytes; + let mut lba = 0u64; + let mut off = 0usize; + + while off < copy_bytes { + let this_chunk = core::cmp::min(chunk_bytes, copy_bytes - off); + if !raw_read( + core::ptr::null_mut(), + lba, + image.as_mut_ptr().add(off), + this_chunk, + ) { + RAM_STAGE_LAST_REASON = "RAM stage: media read failed; mounting directly from media"; + return None; + } + + off += this_chunk; + lba = lba.saturating_add((this_chunk / sector_bytes) as u64); + } + + let base = image.as_mut_ptr(); + let size = image.len(); + + RAM_HELIX_DEVICE = Some(MemBlockDevice::new(base, size, sector_size)); + let mem_dev = RAM_HELIX_DEVICE.as_mut()?; + Some(MemBlockDevice::into_raw(mem_dev)) +} + /// Create the standard initFS directory structure. /// /// Idempotent — silently ignores directories that already exist. @@ -576,10 +1399,15 @@ pub fn create_init_directories() { unsafe fn make_raw_block_device() -> RawBlockDevice { let dev = BLOCK_DEVICE.as_ref().unwrap(); let info = dev.info(); + let total = if STORAGE_REGION_SECTORS != 0 { + STORAGE_REGION_SECTORS + } else { + info.total_sectors + }; RawBlockDevice::new( core::ptr::null_mut(), // ctx unused — we access statics directly - info.total_sectors, + total, info.sector_size, raw_read, raw_write, @@ -616,7 +1444,8 @@ unsafe fn raw_read(_ctx: *mut u8, lba: u64, dst: *mut u8, len: usize) -> bool { use morpheus_network::GptBlockIo; use morpheus_network::GptLba; - bio.read_blocks(GptLba(lba), dst_slice).is_ok() + bio.read_blocks(GptLba(lba + STORAGE_LBA_BASE), dst_slice) + .is_ok() } /// Write callback for `RawBlockDevice`. @@ -645,7 +1474,8 @@ unsafe fn raw_write(_ctx: *mut u8, lba: u64, src: *const u8, len: usize) -> bool use morpheus_network::GptBlockIo; use morpheus_network::GptLba; - bio.write_blocks(GptLba(lba), src_slice).is_ok() + bio.write_blocks(GptLba(lba + STORAGE_LBA_BASE), src_slice) + .is_ok() } /// Flush callback for `RawBlockDevice`. diff --git a/bootloader/src/tui/desktop.rs b/bootloader/src/tui/desktop.rs index 557d5d42..a861fed7 100644 --- a/bootloader/src/tui/desktop.rs +++ b/bootloader/src/tui/desktop.rs @@ -95,8 +95,8 @@ pub fn run_desktop(_display_info: &FramebufferInfo) -> ! { log_info("KERNEL", 926, "preparing to launch /bin/init"); let mut keyboard = Keyboard::new(); - let mut mouse = super::mouse::Mouse::new(); show_boot_log_screen(&mut keyboard); + let mut mouse = super::mouse::Mouse::new(); // Load and spawn the userland init supervisor let elf_data = match load_elf_from_fs("init") { @@ -157,6 +157,10 @@ pub fn run_desktop(_display_info: &FramebufferInfo) -> ! { continue; } + if device != 0x01 { + continue; + } + // Keyboard byte — feed through the decoder if let Some(input) = keyboard.feed_raw(byte) { // Accept any non-zero unicode_char that fits in a u8. diff --git a/bootloader/src/tui/input.rs b/bootloader/src/tui/input.rs index bcc1b2fe..7464b69e 100644 --- a/bootloader/src/tui/input.rs +++ b/bootloader/src/tui/input.rs @@ -301,6 +301,8 @@ pub struct Keyboard { initialized: bool, /// Active keymap layout: KeyLayout, + /// Some controllers mis-tag keyboard bytes as AUX; fallback mode accepts both tags. + aux_as_kbd: bool, } impl Keyboard { @@ -328,6 +330,7 @@ impl Keyboard { extended: false, initialized: false, layout: KeyLayout::Us, + aux_as_kbd: false, }; unsafe { kb.init_controller() }; @@ -354,49 +357,143 @@ impl Keyboard { self.alt } - unsafe fn init_controller(&mut self) { - // OVMF already configured the 8042 — don't send 0xAA/0xFF as they clear - // Translation (bit 6), causing set-2 bytes to arrive instead of set-1. - // RMW the config byte: keep translation on, disable IRQs (we poll). + pub fn aux_as_kbd(&self) -> bool { + self.aux_as_kbd + } - asm_ps2_flush(); + unsafe fn init_controller(&mut self) { + // Full i8042+keyboard reinit. No partial-trust path. + morpheus_hwinit::serial::log_info("INPUT", 935, "keyboard controller init begin"); + self.aux_as_kbd = false; + self.initialized = false; - asm_ps2_write_cmd(0xAD); // disable port 1 scanning + // 1) Hard-stop both channels and drain everything. + asm_ps2_write_cmd(0xAD); // disable keyboard port + Self::io_delay(); + asm_ps2_write_cmd(0xA7); // disable aux port + Self::io_delay(); + Self::drain_all(512); + + // 2) Put controller in known config: IRQs off, clocks on, translation off. + asm_ps2_write_cmd(0x20); + let mut cfg = self.wait_kbd_byte(100_000).unwrap_or(0x00); + cfg &= !0x43; // clear IRQ1, IRQ12, translation + cfg &= !0x30; // enable both clocks (bits are disable flags) + asm_ps2_write_cmd(0x60); + asm_ps2_write_data(cfg); Self::io_delay(); - asm_ps2_flush(); + Self::drain_all(128); - asm_ps2_write_cmd(0x20); // read config byte - let config = Self::wait_response(50_000).unwrap_or(0x45); - let new_config = (config | 0x40) & !0x03; // set translation, clear IRQs + // 3) Controller and interface tests. + asm_ps2_write_cmd(0xAA); + let ctl_ok = self.wait_kbd_byte(200_000) == Some(0x55); + if !ctl_ok { + morpheus_hwinit::serial::log_warn("INPUT", 936, "8042 self-test failed"); + } - asm_ps2_write_cmd(0x60); // write config byte - asm_ps2_write_data(new_config); + // Self-test may rewrite config on some controllers. Re-assert config. + asm_ps2_write_cmd(0x60); + asm_ps2_write_data(cfg); Self::io_delay(); - asm_ps2_write_cmd(0xAE); // re-enable port 1 + asm_ps2_write_cmd(0xAB); + let port1_ok = self.wait_kbd_byte(100_000) == Some(0x00); + if !port1_ok { + morpheus_hwinit::serial::log_warn("INPUT", 937, "8042 port1 test failed"); + } + + // 4) Enable keyboard port only for deterministic bring-up. + asm_ps2_write_cmd(0xAE); + Self::io_delay(); + asm_ps2_write_cmd(0xA7); Self::io_delay(); + Self::drain_all(128); + + // 5) Reset keyboard with retries. + let mut reset_ok = false; + for _ in 0..3 { + asm_ps2_write_data(0xFF); + let ack = self.wait_kbd_byte(150_000); + let bat = self.wait_kbd_byte(300_000); + if ack == Some(0xFA) && bat == Some(0xAA) { + reset_ok = true; + break; + } + Self::drain_all(128); + asm_ps2_write_cmd(0xAD); + Self::io_delay(); + asm_ps2_write_cmd(0xAE); + Self::io_delay(); + } + if !reset_ok { + morpheus_hwinit::serial::log_warn("INPUT", 938, "keyboard reset/BAT failed after retries"); + } + + // 6) Program known scan behavior (set 1) and verify. + let mut scan_ok = true; + + asm_ps2_write_data(0xF5); // disable scanning + scan_ok &= self.wait_kbd_byte(100_000) == Some(0xFA); + + asm_ps2_write_data(0xF6); // defaults + scan_ok &= self.wait_kbd_byte(100_000) == Some(0xFA); + + asm_ps2_write_data(0xF0); // set scancode cmd + scan_ok &= self.wait_kbd_byte(100_000) == Some(0xFA); + asm_ps2_write_data(0x01); // set 1 + scan_ok &= self.wait_kbd_byte(100_000) == Some(0xFA); + + // Query to verify set-1 actually latched. + asm_ps2_write_data(0xF0); + scan_ok &= self.wait_kbd_byte(100_000) == Some(0xFA); + asm_ps2_write_data(0x00); + scan_ok &= self.wait_kbd_byte(100_000) == Some(0xFA); + let set_id = self.wait_kbd_byte(100_000); + scan_ok &= set_id == Some(0x01); asm_ps2_write_data(0xF4); // enable scanning - let _ = Self::wait_response(50_000); + let f4_ack = self.wait_kbd_byte(100_000); + scan_ok &= f4_ack == Some(0xFA); - asm_ps2_flush(); + if !scan_ok { + morpheus_hwinit::serial::log_warn("INPUT", 931, "keyboard scan-set programming failed"); + } + + Self::drain_all(128); - self.initialized = true; - morpheus_hwinit::serial::log_ok("INPUT", 930, "PS/2 keyboard ready"); + self.initialized = ctl_ok && port1_ok && reset_ok && scan_ok; + if self.initialized { + morpheus_hwinit::serial::log_ok("INPUT", 930, "PS/2 keyboard ready (full reset path)"); + } else { + morpheus_hwinit::serial::log_warn("INPUT", 941, "PS/2 keyboard init failed (full reset path)"); + } } - /// Spin-wait for a response byte from port 0x60, with bounded timeout. - unsafe fn wait_response(max_spins: u32) -> Option { + unsafe fn wait_kbd_byte(&mut self, max_spins: u32) -> Option { for _ in 0..max_spins { - let r = asm_ps2_poll(); - if r & 0x100 != 0 { + let r = asm_ps2_poll_any(); + let tag = r & 0x300; + if tag == 0x100 { return Some((r & 0xFF) as u8); } + if tag == 0x300 { + // mouse/aux byte while doing keyboard bring-up; drop it. + continue; + } core::hint::spin_loop(); } None } + unsafe fn drain_all(max_reads: u32) { + for _ in 0..max_reads { + if asm_ps2_poll_any() == 0 { + break; + } + core::hint::spin_loop(); + } + } + /// Tiny delay between commands (write to unused port 0x80, like PIC does). #[inline(always)] unsafe fn io_delay() { @@ -414,8 +511,9 @@ impl Keyboard { /// key-press event was decoded, `None` if the buffer was empty or /// only a modifier/break code was processed. pub fn read_key(&mut self) -> Option { - let raw = unsafe { asm_ps2_poll() }; - if raw & 0x100 == 0 { + let raw = unsafe { asm_ps2_poll_any() }; + let tag = raw & 0x300; + if tag != 0x100 && !(self.aux_as_kbd && tag == 0x300) { return None; } let byte = (raw & 0xFF) as u8; @@ -435,14 +533,28 @@ impl Keyboard { /// near-zero power draw while waiting and immediate wakeup on keypress. pub fn wait_for_key(&mut self) -> InputKey { loop { - if let Some(key) = self.read_key() { - return key; + let raw = unsafe { asm_ps2_poll_any() }; + let tag = raw & 0x300; + if tag == 0x100 || (self.aux_as_kbd && tag == 0x300) { + let byte = (raw & 0xFF) as u8; + + if let Some(key) = self.decode(byte) { + return key; + } + + // Boot gate should continue on any non-break keyboard make byte, + // even if it's a modifier or currently unmapped key. + if byte != EXTENDED_PREFIX && (byte & BREAK_FLAG) == 0 { + return InputKey { + scan_code: SCAN_NULL, + unicode_char: 0, + }; + } } - // Halt the CPU until the next interrupt (IRQ1 = PS/2 keyboard). - // Eliminates busy-waiting with no latency cost on actual keypresses. - // SAFETY: sti/hlt/cli is safe in bare-metal bootloader context. - unsafe { - core::arch::asm!("sti", "hlt", "cli", options(nostack, nomem)); + + // Do not rely on interrupt wakeups at this stage; poll with short backoff. + for _ in 0..4096 { + core::hint::spin_loop(); } } } diff --git a/bootloader/src/tui/mouse.rs b/bootloader/src/tui/mouse.rs index 5dd2d829..635f5056 100644 --- a/bootloader/src/tui/mouse.rs +++ b/bootloader/src/tui/mouse.rs @@ -53,8 +53,9 @@ impl Mouse { // RMW config: enable aux IRQ (bit 1), clear aux clock-disable (bit 5) asm_ps2_write_cmd(0x20); - let config = wait_data(50_000); - let new_config = (config | 0x02) & !0x20; + let config = wait_kbd_data(50_000).unwrap_or(0x45); + // Keep translation on and keyboard IRQ enabled; just bring mouse path up. + let new_config = (config | 0x43) & !0x30; asm_ps2_write_cmd(0x60); asm_ps2_write_data(new_config); io_delay(); @@ -195,6 +196,18 @@ unsafe fn wait_data(max_spins: u32) -> u8 { 0 } +/// Spin-read one keyboard-tagged byte (0x1xx) from port 0x60. +unsafe fn wait_kbd_data(max_spins: u32) -> Option { + for _ in 0..max_spins { + let r = asm_ps2_poll_any(); + if (r & 0x300) == 0x100 { + return Some((r & 0xFF) as u8); + } + core::hint::spin_loop(); + } + None +} + /// Spin-read one aux-port byte (mouse channel, tagged as 0x3xx). unsafe fn wait_aux_data(max_spins: u32) -> u8 { for _ in 0..max_spins { diff --git a/cli/src/main.rs b/cli/src/main.rs index 4b7cb94b..fc3f7b6b 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -51,8 +51,12 @@ struct FileBlockDevice { impl FileBlockDevice { fn open(path: &str) -> io::Result { - let file = OpenOptions::new().read(true).write(true).open(path)?; - let len = file.metadata()?.len(); + let mut file = OpenOptions::new().read(true).write(true).open(path)?; + let mut len = file.metadata()?.len(); + if len == 0 { + // Block devices may report 0 via metadata; use seek-to-end as fallback. + len = file.seek(SeekFrom::End(0))?; + } let total_sectors = len / SECTOR_SIZE as u64; Ok(Self { file, @@ -111,6 +115,7 @@ fn usage() { eprintln!(); eprintln!("USAGE:"); eprintln!(" morpheus-cli inject [--dest /bin/name]"); + eprintln!(" morpheus-cli pack [--max-mb N]"); eprintln!(" morpheus-cli ls [path]"); eprintln!(" morpheus-cli mkbin "); eprintln!(); @@ -119,9 +124,96 @@ fn usage() { " morpheus-cli inject testing/helix-data.img \\\n target/x86_64-morpheus/release/syscall-e2e" ); eprintln!(" morpheus-cli inject testing/helix-data.img my-app --dest /bin/app"); + eprintln!(" morpheus-cli pack /dev/sdb2 testing/helix.img --max-mb 384"); eprintln!(" morpheus-cli ls testing/helix-data.img /bin"); } +fn cmd_pack(disk: &str, output: &str, max_mb: u64) -> Result<(), String> { + if max_mb == 0 { + return Err("--max-mb must be > 0".to_string()); + } + + let mut dev = + FileBlockDevice::open(disk).map_err(|e| format!("cannot open '{}': {}", disk, e))?; + + let sb = recover_superblock(&mut dev, 0, SECTOR_SIZE) + .map_err(|e| format!("recover_superblock: {:?}", e))?; + + let mut stage_blocks = 2u64; + let log_hi = sb.log_end_block.saturating_add(1); + if log_hi > stage_blocks { + stage_blocks = log_hi; + } + + let data_hi = sb.data_start_block.saturating_add(sb.blocks_used); + if data_hi > stage_blocks { + stage_blocks = data_hi; + } + + if stage_blocks > sb.total_blocks { + stage_blocks = sb.total_blocks; + } + + if stage_blocks == 0 { + return Err("empty Helix footprint".to_string()); + } + + let mut bytes = stage_blocks + .checked_mul(sb.block_size as u64) + .ok_or_else(|| "footprint byte overflow".to_string())?; + + let max_bytes = max_mb + .checked_mul(1024) + .and_then(|v| v.checked_mul(1024)) + .ok_or_else(|| "max size overflow".to_string())?; + + if bytes > max_bytes { + bytes = max_bytes; + } + + let rem = bytes % SECTOR_SIZE as u64; + if rem != 0 { + bytes = bytes.saturating_sub(rem); + } + + if bytes == 0 { + return Err("packed image size resolved to 0 bytes".to_string()); + } + + let mut out = OpenOptions::new() + .create(true) + .truncate(true) + .write(true) + .open(output) + .map_err(|e| format!("cannot create '{}': {}", output, e))?; + + const CHUNK: usize = 1024 * 1024; + let mut buf = vec![0u8; CHUNK]; + let mut copied = 0u64; + + while copied < bytes { + let remaining = (bytes - copied) as usize; + let n = remaining.min(CHUNK); + dev.file + .seek(SeekFrom::Start(copied)) + .map_err(|_| "seek failed".to_string())?; + dev.file + .read_exact(&mut buf[..n]) + .map_err(|_| "read failed".to_string())?; + out.write_all(&buf[..n]) + .map_err(|_| "write failed".to_string())?; + copied += n as u64; + } + + out.flush().map_err(|e| format!("flush failed: {}", e))?; + + println!("[pack] source : {}", disk); + println!("[pack] output : {}", output); + println!("[pack] bytes : {}", bytes); + println!("[pack] done"); + Ok(()) +} + // Mount an existing or fresh HelixFS from a FileBlockDevice. // Returns (device, mount_table). fn mount(disk: &str) -> Result<(FileBlockDevice, MountTable), String> { @@ -307,6 +399,19 @@ fn cmd_ls(disk: &str, path: &str) -> Result<(), String> { // mkbin command — pre-create /bin directory // ────────────────────────────────────────────────────────────────────────────── +// ────────────────────────────────────────────────────────────────────────────── +// format command — wipe and reformat HelixFS partition +// ────────────────────────────────────────────────────────────────────────────── + +fn cmd_format(disk: &str) -> Result<(), String> { + let mut dev = + FileBlockDevice::open(disk).map_err(|e| format!("cannot open '{}': {}", disk, e))?; + println!("[format] wiping and reformatting {}", disk); + do_format(&mut dev)?; + println!("[format] done — clean HelixFS ready"); + Ok(()) +} + fn cmd_mkbin(disk: &str) -> Result<(), String> { let (_dev, mut mt) = mount(disk)?; match vfs::vfs_mkdir(&mut mt, "/bin", 0) { @@ -339,7 +444,6 @@ fn main() { let disk = &args[2]; let binary = &args[3]; - // Default dest = /bin/ let default_dest = format!( "/bin/{}", Path::new(binary) @@ -352,7 +456,6 @@ fn main() { .find(|w| w[0] == "--dest") .map(|w| w[1].as_str()) .unwrap_or(&default_dest); - cmd_inject(disk, binary, dest) } "ls" => { @@ -371,6 +474,27 @@ fn main() { } cmd_mkbin(&args[2]) } + "pack" => { + if args.len() < 4 { + eprintln!("Usage: morpheus-cli pack [--max-mb N]"); + std::process::exit(1); + } + let disk = &args[2]; + let output = &args[3]; + let max_mb = args + .windows(2) + .find(|w| w[0] == "--max-mb") + .and_then(|w| w[1].parse::().ok()) + .unwrap_or(512); + cmd_pack(disk, output, max_mb) + } + "format" => { + if args.len() < 3 { + eprintln!("Usage: morpheus-cli format "); + std::process::exit(1); + } + cmd_format(&args[2]) + } _ => { usage(); std::process::exit(1); diff --git a/compd/Cargo.toml b/compd/Cargo.toml index d44f9a1a..7d179cbb 100644 --- a/compd/Cargo.toml +++ b/compd/Cargo.toml @@ -12,7 +12,3 @@ libmorpheus = { path = "../libmorpheus" } morpheus-display = { path = "../display", features = ["framebuffer-backend"] } morpheus-gfx3d = { path = "../gfx3d" } channel = { path = "../channel" } - -[profile.release] -opt-level = 3 -lto = true diff --git a/compd/src/islands/input.rs b/compd/src/islands/input.rs index c4967f4e..5f06d79b 100644 --- a/compd/src/islands/input.rs +++ b/compd/src/islands/input.rs @@ -115,7 +115,7 @@ fn route_mouse_spatial(state: &mut CompState, msg: MouseSpatialMsg) { match region { HitRegion::Close => { if let Some(ref win) = state.windows[idx] { - let _ = process::kill(win.pid, process::signal::SIGKILL); + let _ = process::kill(win.pid, process::signal::SIGTERM); } state.capture = None; route_to_child = false; diff --git a/hwinit/asm/cpu/ap_trampoline.s b/hwinit/asm/cpu/ap_trampoline.s index 6fdd9546..5d290667 100644 --- a/hwinit/asm/cpu/ap_trampoline.s +++ b/hwinit/asm/cpu/ap_trampoline.s @@ -11,13 +11,17 @@ ; DATA AREA at offset 0xF00 within this page is filled by ap_boot.rs ; before each AP is woken. Layout must match the TD_* constants there. ; -; DEBUG markers are intentionally disabled in normal builds to keep -; early-boot logs readable on SMP. + ; DEBUG markers are ENABLED for real-hardware bring-up diagnosis. +; Each emits a single char to COM1 showing where the AP reached. +; ; ; ═══════════════════════════════════════════════════════════════════════════ -; ── COM1 debug macro ───────────────────────────────────────────────────── +; ── COM1 debug macros ──────────────────────────────────────────────────── ; direct port I/O, no LSR check — QEMU's virtual UART is always ready. +; real UART is fast enough for single-byte markers. + +; 16-bit real mode version %macro SERIAL_MARKER 1 push ax push dx @@ -28,6 +32,28 @@ pop ax %endmacro +; 32-bit protected mode version (uses eax/edx) +%macro SERIAL_MARKER32 1 + push eax + push edx + mov dx, 0x3F8 + mov al, %1 + out dx, al + pop edx + pop eax +%endmacro + +; 64-bit long mode version (uses rax/rdx) +%macro SERIAL_MARKER64 1 + push rax + push rdx + mov dx, 0x3F8 + mov al, %1 + out dx, al + pop rdx + pop rax +%endmacro + bits 16 org 0x8000 ; physical load address @@ -45,7 +71,7 @@ ap_start: mov ss, ax xor sp, sp ; stack at top of segment (wraps to 0xFFFF) - ; SERIAL_MARKER '1' ; marker 1: real mode entry alive + ; SERIAL_MARKER '1' ; marker 1: real mode entry alive ; ── load a TEMPORARY GDT that lives inside this trampoline page ────── ; The BSP's GDT is above 4 GB (PE BSS at 0x140xxxxxx). In 16-bit @@ -68,7 +94,7 @@ ap_start: ; ─────────────────────────────────────────────────────────────────────────── bits 32 ap_pm32: - ; marker disabled + ; SERIAL_MARKER32 '2' ; marker 2: protected mode reached ; load data segments with kernel data selector (0x10) mov ax, 0x10 @@ -78,16 +104,20 @@ ap_pm32: mov gs, ax mov ss, ax - ; ── enable PAE (required for long mode) ─────────────────────────────── + ; ── enable PAE + SSE support (required before Rust code) ───────────── + ; PAE: long mode prerequisite. + ; OSFXSR: OS supports FXSAVE/FXRSTOR — without this, SSE = #UD. + ; OSXMMEXCPT: OS handles SIMD FP exceptions. + ; UEFI sets these on the BSP. INIT resets CR4 to 0. we fix it here. mov eax, cr4 - or eax, (1 << 5) ; CR4.PAE + or eax, (1 << 5) | (1 << 9) | (1 << 10) ; PAE + OSFXSR + OSXMMEXCPT mov cr4, eax ; ── load kernel CR3 from data area ──────────────────────────────────── mov eax, dword [0x8F00] ; TD_CR3 low 32 bits (phys address < 4GB) mov cr3, eax - ; marker disabled + ; SERIAL_MARKER32 '3' ; marker 3: CR3 loaded, about to enable LM ; ── enable long mode via IA32_EFER.LME + NXE ────────────────────────── ; NXE (bit 11) is MANDATORY: the kernel page tables have NX bits (bit 63) @@ -98,12 +128,17 @@ ap_pm32: or eax, (1 << 8) | (1 << 11) ; LME + NXE wrmsr - ; ── enable paging → activates long mode ─────────────────────────────── + ; ── enable paging + caching → activates long mode ───────────────────── + ; INIT leaves CR0 = 0x60000011 (CD=1, NW=1). if we just OR in PG the AP + ; runs with caches OFF — every fetch hits DRAM. clear CD+NW here. + ; also set MP (monitor coprocessor) and NE (numeric exceptions) so the + ; FPU/SSE doesn't trip over legacy x87 emulation traps. mov eax, cr0 - or eax, (1 << 31) ; CR0.PG + and eax, 0x9FFFFFF3 ; clear CD(30), NW(29), TS(3), EM(2) + or eax, (1 << 31) | (1 << 1) | (1 << 5) ; PG + MP + NE mov cr0, eax - ; marker disabled + ; SERIAL_MARKER32 '4' ; marker 4: paging enabled, about to jump LM64 ; far jump to 64-bit long mode code. ; selector 0x18 = temp GDT's 64-bit code descriptor (L=1, D=0). @@ -115,7 +150,9 @@ ap_pm32: ; ─────────────────────────────────────────────────────────────────────────── bits 64 ap_lm64: - ; marker disabled + ; load the stack FIRST so pushes work! + mov rsp, qword [0x8F10] + ; SERIAL_MARKER64 '5' ; marker 5: 64-bit long mode reached ; reload data segments for 64-bit mode mov ax, 0x10 @@ -141,7 +178,6 @@ ap_lm64: ; RSP is 0 from the real-mode `xor sp, sp`. push/retfq would write ; to 0xFFFFFFFF_FFFFFFF8 which is unmapped → #PF → triple fault. ; load the real stack first so the retfq has somewhere to push. - mov rsp, qword [0x8F10] ; TD_STACK ; reload CS via a far return trick — push selector:offset, retfq lea rax, [rel .cs_reloaded] @@ -156,7 +192,7 @@ ap_lm64: mov esi, dword [0x8F1C] ; TD_LAPIC_ID → RSI (arg2, SysV x64) ; ── jump to Rust entry point ────────────────────────────────────────── - ; marker disabled + ; SERIAL_MARKER64 '6' ; marker 6: about to jump to Rust entry mov rax, qword [0x8F08] ; TD_ENTRY64 jmp rax ; ap_rust_entry(core_idx, lapic_id) — never returns diff --git a/hwinit/asm/cpu/context_switch.s b/hwinit/asm/cpu/context_switch.s index f06d6080..420568ca 100644 --- a/hwinit/asm/cpu/context_switch.s +++ b/hwinit/asm/cpu/context_switch.s @@ -61,8 +61,8 @@ bits 64 default rel -; LAPIC EOI register (identity-mapped) -%define LAPIC_EOI_ADDR 0xFEE000B0 +; LAPIC EOI register offset from base +%define LAPIC_EOI_OFF 0x0B0 section .text @@ -121,11 +121,12 @@ irq_timer_isr: .skip_fxsave: ; ── ACK LAPIC (write 0 to EOI register) ────────────────────────────── - ; 0xFEE000B0 has bit 31 set. [imm32] in 64-bit mode sign-extends to - ; 0xFFFFFFFFFEE000B0. load into r11d: zero-extends, gives correct ptr. - ; r11 is already saved at [rsp+0x50] so clobbering it here is safe. - mov r11d, LAPIC_EOI_ADDR ; r11 = 0x00000000FEE000B0 - mov dword [r11], 0 ; EOI + ; Read the actual LAPIC base from per-CPU data (gs:[0x38]) instead of + ; hardcoding 0xFEE00000. Firmware can relocate the LAPIC base via + ; IA32_APIC_BASE MSR; the probe runs during BSP/AP init and stores + ; the result in PerCpu.lapic_base. + mov r11, [gs:0x38] ; lapic_base from PerCpu + mov dword [r11 + LAPIC_EOI_OFF], 0 ; EOI ; ── Call scheduler_tick(current_ctx: *const CpuContext) ────────────── ; MS x64 ABI: first arg in RCX. Need 32-byte shadow space on stack. diff --git a/hwinit/src/cpu/acpi.rs b/hwinit/src/cpu/acpi.rs index 3f946e50..7ed1c199 100644 --- a/hwinit/src/cpu/acpi.rs +++ b/hwinit/src/cpu/acpi.rs @@ -8,6 +8,7 @@ use crate::cpu::per_cpu::MAX_CPUS; use crate::serial::{put_hex32, puts}; +use core::sync::atomic::{AtomicU64, Ordering}; // ── RSDP ───────────────────────────────────────────────────────────────── @@ -72,6 +73,17 @@ struct MadtLocalApic { flags: u32, } +/// MADT entry type 9: Processor Local x2APIC. +#[repr(C, packed)] +struct MadtLocalX2Apic { + entry_type: u8, // 9 + length: u8, // 16 + _reserved: u16, + x2apic_id: u32, + flags: u32, + _acpi_processor_uid: u32, +} + // MADT Local APIC flags const LAPIC_ENABLED: u32 = 1 << 0; const LAPIC_ONLINE_CAPABLE: u32 = 1 << 1; @@ -93,7 +105,29 @@ impl ApLapicIds { } } -// ── RSDP scanning ──────────────────────────────────────────────────────── +#[inline(always)] +fn push_apic_id(result: &mut ApLapicIds, apic_id: u32, bsp_lapic_id: u32) { + if apic_id == bsp_lapic_id || result.count >= MAX_CPUS { + return; + } + for i in 0..result.count { + if result.ids[i] == apic_id { + return; + } + } + result.ids[result.count] = apic_id; + result.count += 1; +} + +// ── RSDP source ────────────────────────────────────────────────────────── + +static RSDP_PHYS_OVERRIDE: AtomicU64 = AtomicU64::new(0); + +pub fn set_rsdp_phys(rsdp_phys: u64) { + RSDP_PHYS_OVERRIDE.store(rsdp_phys, Ordering::Release); +} + +// ── Legacy RSDP scan helpers (kept for diagnostics only) ──────────────── /// Scan a physical memory range for the RSDP signature. /// Returns the physical address of the RSDP, or 0 on failure. @@ -140,7 +174,21 @@ unsafe fn find_rsdp() -> u64 { } // ── SDT traversal ──────────────────────────────────────────────────────── - +/// Validate an ACPI SDT by checksumming its entire reported length. +/// Returns true if all bytes sum to 0 (valid per ACPI spec). +unsafe fn validate_sdt_checksum(table_phys: u64) -> bool { + let header = table_phys as *const SdtHeader; + let length = core::ptr::read_unaligned(core::ptr::addr_of!((*header).length)) as usize; + // Sanity: reject implausible lengths (0, or > 1 MiB). + if length < SDT_HEADER_SIZE || length > 0x10_0000 { + return false; + } + let mut sum: u8 = 0; + for i in 0..length { + sum = sum.wrapping_add(*((table_phys + i as u64) as *const u8)); + } + sum == 0 +} /// Find a table with the given 4-byte signature in the RSDT (32-bit pointers). unsafe fn find_table_rsdt(rsdt_phys: u64, sig: &[u8; 4]) -> u64 { let header = rsdt_phys as *const SdtHeader; @@ -211,6 +259,17 @@ unsafe fn parse_madt(madt_phys: u64, bsp_lapic_id: u32) -> ApLapicIds { put_hex32(reported_lapic_addr); puts("\n"); + // Cross-check MADT LAPIC address against the probed IA32_APIC_BASE. + // A mismatch indicates firmware is lying about one of them. + let probed_base = crate::cpu::apic::lapic_base() as u32; + if reported_lapic_addr != 0 && reported_lapic_addr != probed_base { + puts("[ACPI] WARN: MADT LAPIC addr "); + put_hex32(reported_lapic_addr); + puts(" != probed base "); + put_hex32(probed_base); + puts("\n"); + } + // walk variable-length entries after the fixed header let mut offset = fixed_offset; while offset + 2 <= total_len { @@ -222,7 +281,7 @@ unsafe fn parse_madt(madt_phys: u64, bsp_lapic_id: u32) -> ApLapicIds { break; // malformed — bail } - // type 0 = Processor Local APIC + // type 0 = Processor Local APIC (xAPIC) if entry_type == 0 && entry_len >= 8 { let lapic_entry = entry_ptr as *const MadtLocalApic; let apic_id = (*lapic_entry).apic_id as u32; @@ -231,9 +290,20 @@ unsafe fn parse_madt(madt_phys: u64, bsp_lapic_id: u32) -> ApLapicIds { // enabled or online-capable let usable = (flags & LAPIC_ENABLED) != 0 || (flags & LAPIC_ONLINE_CAPABLE) != 0; - if usable && apic_id != bsp_lapic_id && result.count < MAX_CPUS { - result.ids[result.count] = apic_id; - result.count += 1; + if usable { + push_apic_id(&mut result, apic_id, bsp_lapic_id); + } + } + + // type 9 = Processor Local x2APIC + if entry_type == 9 && entry_len >= 16 { + let x2_entry = entry_ptr as *const MadtLocalX2Apic; + let apic_id = (*x2_entry).x2apic_id; + let flags = (*x2_entry).flags; + + let usable = (flags & LAPIC_ENABLED) != 0 || (flags & LAPIC_ONLINE_CAPABLE) != 0; + if usable { + push_apic_id(&mut result, apic_id, bsp_lapic_id); } } @@ -254,9 +324,9 @@ unsafe fn parse_madt(madt_phys: u64, bsp_lapic_id: u32) -> ApLapicIds { /// Memory must be identity-mapped (true after UEFI ExitBootServices + our paging). /// The ACPI tables must not have been overwritten. Call before reclaiming AcpiReclaim. pub unsafe fn discover_ap_lapic_ids(bsp_lapic_id: u32) -> ApLapicIds { - let rsdp_phys = find_rsdp(); + let rsdp_phys = RSDP_PHYS_OVERRIDE.load(Ordering::Acquire); if rsdp_phys == 0 { - crate::serial::log_warn("ACPI", 760, "RSDP not found; MADT discovery unavailable"); + crate::serial::log_warn("ACPI", 760, "UEFI RSDP pointer unavailable; MADT discovery unavailable"); return ApLapicIds::empty(); } @@ -270,30 +340,57 @@ pub unsafe fn discover_ap_lapic_ids(bsp_lapic_id: u32) -> ApLapicIds { let rsdp2 = rsdp_phys as *const Rsdp2; let xsdt = (*rsdp2).xsdt_address; if xsdt != 0 { - let _ = xsdt; - let found = find_table_xsdt(xsdt, &MADT_SIG); - if found != 0 { - found + if !validate_sdt_checksum(xsdt) { + crate::serial::log_warn("ACPI", 764, "XSDT checksum invalid; trying RSDT"); + let rsdt = (*rsdp).rsdt_address as u64; + if rsdt != 0 && validate_sdt_checksum(rsdt) { + find_table_rsdt(rsdt, &MADT_SIG) + } else { + 0 + } } else { - // XSDT didn't have MADT, try RSDT - find_table_rsdt((*rsdp).rsdt_address as u64, &MADT_SIG) + let found = find_table_xsdt(xsdt, &MADT_SIG); + if found != 0 { + found + } else { + // XSDT didn't have MADT, try RSDT + find_table_rsdt((*rsdp).rsdt_address as u64, &MADT_SIG) + } } } else { - find_table_rsdt((*rsdp).rsdt_address as u64, &MADT_SIG) + let rsdt = (*rsdp).rsdt_address as u64; + if rsdt != 0 && validate_sdt_checksum(rsdt) { + find_table_rsdt(rsdt, &MADT_SIG) + } else { + 0 + } } } else { - find_table_rsdt((*rsdp).rsdt_address as u64, &MADT_SIG) + let rsdt = (*rsdp).rsdt_address as u64; + if rsdt != 0 && validate_sdt_checksum(rsdt) { + find_table_rsdt(rsdt, &MADT_SIG) + } else { + 0 + } }; if madt_phys == 0 { crate::serial::log_warn("ACPI", 761, "MADT not found in RSDT/XSDT"); return ApLapicIds::empty(); } - let _ = madt_phys; + + // Validate MADT checksum before trusting its contents. + if !validate_sdt_checksum(madt_phys) { + crate::serial::log_warn("ACPI", 765, "MADT checksum invalid; skipping AP discovery"); + return ApLapicIds::empty(); + } let result = parse_madt(madt_phys, bsp_lapic_id); - let _ = result.count; + if result.count == 0 { + crate::serial::log_warn("ACPI", 763, "MADT parsed but no usable AP LAPIC entries"); + } + crate::serial::log_ok("ACPI", 762, "MADT parsed"); result diff --git a/hwinit/src/cpu/ap_boot.rs b/hwinit/src/cpu/ap_boot.rs index 4cb8d313..ec948323 100644 --- a/hwinit/src/cpu/ap_boot.rs +++ b/hwinit/src/cpu/ap_boot.rs @@ -16,7 +16,6 @@ use crate::cpu::per_cpu::{self, MAX_CPUS}; use crate::memory::{global_registry_mut, AllocateType, MemoryType, PAGE_SIZE}; use crate::serial::{log_error, log_info, log_ok, log_warn}; use core::sync::atomic::Ordering; - /// Physical address where the AP trampoline is copied. /// Must be page-aligned, below 1 MiB, and not in use by anything else. /// 0x8000 is the traditional choice (page 8). @@ -28,8 +27,14 @@ const AP_TRAMPOLINE_PAGE: u8 = (AP_TRAMPOLINE_PHYS / 0x1000) as u8; /// AP kernel stack size (64 KiB per core). const AP_STACK_SIZE: u64 = 64 * 1024; -// ── Trampoline data area offsets ───────────────────────────────────────── -// These must match the .data section layout at the end of ap_trampoline.s. +// Brute-force LAPIC probing is a fallback path. Keep it bounded so one bad +// topology guess doesn't make boot look dead on real hardware. +const AP_WAIT_STEP_US: u64 = 50; +const AP_WAIT_TIMEOUT_US: u64 = 20_000; +const AP_BRUTE_SCAN_BUDGET_US: u64 = 3_000_000; +const AP_MADT_SCAN_BUDGET_US: u64 = 1_000_000; + +// These must match the .data section layout at the end of ap_trampoline.s.lapic_id // The trampoline data block starts at AP_TRAMPOLINE_PHYS + TRAMPOLINE_DATA_OFFSET. const TRAMPOLINE_DATA_OFFSET: u64 = 0xF00; // within the 4K page const TD_CR3: u64 = TRAMPOLINE_DATA_OFFSET + 0x00; @@ -85,6 +90,14 @@ unsafe fn setup_trampoline() -> bool { core::arch::asm!("mov {}, cr3", out(reg) cr3, options(nostack, nomem)); let kernel_cr3 = cr3 & 0x000F_FFFF_FFFF_F000; + // The 32-bit protected mode trampoline loads CR3 via `mov eax, [0x8F00]` + // which only reads the low 32 bits. If the kernel page tables are above + // 4 GB, the AP would load a truncated CR3 and triple-fault immediately. + if kernel_cr3 > 0xFFFF_FFFF { + log_error("AP", 514, "kernel CR3 above 4GB; AP trampoline cannot load it in 32-bit mode"); + return false; + } + // read current GDT for APs to load (temporary, until per-core GDT) let mut gdt_buf = [0u8; 10]; core::arch::asm!("sgdt [{}]", in(reg) gdt_buf.as_mut_ptr(), options(nostack)); @@ -128,23 +141,23 @@ unsafe fn boot_single_ap(core_idx: u32, lapic_id: u32) -> bool { let before = per_cpu::AP_ONLINE_COUNT.load(Ordering::SeqCst); // INIT IPI - apic::send_init_ipi(lapic_id); + apic::send_init_assert(lapic_id); apic::delay_us(10_000); // 10ms // SIPI #1 apic::send_sipi(lapic_id, AP_TRAMPOLINE_PAGE); - apic::delay_us(200); // 200µs + apic::delay_us(10_000); // 10ms // SIPI #2 (per Intel spec, send twice for reliability) apic::send_sipi(lapic_id, AP_TRAMPOLINE_PAGE); - apic::delay_us(200); + apic::delay_us(10_000); // 10ms - // wait for AP to come online (up to 100ms) - let mut waited = 0u32; + // wait for AP to come online (bounded; fallback path must stay snappy) + let mut waited_us = 0u64; while per_cpu::AP_ONLINE_COUNT.load(Ordering::SeqCst) <= before { - apic::delay_us(100); - waited += 1; - if waited > 1000 { + apic::delay_us(AP_WAIT_STEP_US); + waited_us += AP_WAIT_STEP_US; + if waited_us >= AP_WAIT_TIMEOUT_US { break; } } @@ -152,7 +165,6 @@ unsafe fn boot_single_ap(core_idx: u32, lapic_id: u32) -> bool { if per_cpu::AP_ONLINE_COUNT.load(Ordering::SeqCst) > before { true } else { - log_warn("AP", 502, "ap did not respond to INIT+SIPI; freeing stack"); // don't leak 64KB per ghost core let _ = global_registry_mut().free_pages(stack_base, stack_pages); false @@ -185,13 +197,30 @@ pub unsafe fn start_aps_from_list(ap_lapic_ids: &[u32]) { } let mut core_idx: u32 = 1; // 0 = BSP + let mut budget_used_us: u64 = 0; + let x2_mode = apic::is_x2apic_mode(); for &lapic_id in ap_lapic_ids { if core_idx as usize >= MAX_CPUS { break; } + + if !x2_mode && lapic_id > 0xFF { + // xAPIC destination field is 8-bit. without x2APIC mode this target is unreachable. + log_warn("AP", 513, "skipping MADT x2APIC ID in xAPIC mode"); + continue; + } + + if budget_used_us >= AP_MADT_SCAN_BUDGET_US { + log_warn("AP", 512, "MADT AP bring-up budget exhausted; continuing with discovered cores"); + break; + } + if boot_single_ap(core_idx, lapic_id) { core_idx += 1; } + + // same per-attempt upper bound used by brute-force path. + budget_used_us += 10_400 + AP_WAIT_TIMEOUT_US; } log_ok("AP", 505, "MADT AP bring-up pass complete"); @@ -225,6 +254,7 @@ pub unsafe fn start_aps() { let mut core_idx: u32 = 1; // 0 = BSP let mut online: u32 = 0; + let mut scan_budget_used_us: u64 = 0; for lapic_id in 0u32..256 { if lapic_id == bsp_lapic_id { continue; @@ -237,11 +267,23 @@ pub unsafe fn start_aps() { break; } + if scan_budget_used_us >= AP_BRUTE_SCAN_BUDGET_US { + log_warn("AP", 510, "brute-force AP scan budget exhausted; continuing with discovered cores"); + break; + } + + if (lapic_id & 0x1F) == 0 { + log_info("AP", 511, "AP scan progress checkpoint"); + } + if boot_single_ap(core_idx, lapic_id) { // only consume a core slot on success core_idx += 1; online += 1; } + + // upper bound for one probe: INIT delay + SIPI delays + wait timeout + scan_budget_used_us += 10_400 + AP_WAIT_TIMEOUT_US; } log_ok("AP", 509, "brute-force AP bring-up pass complete"); @@ -266,10 +308,10 @@ pub unsafe fn start_aps() { #[no_mangle] pub unsafe extern "sysv64" fn ap_rust_entry(core_idx: u32, lapic_id: u32) -> ! { // 1. Set up per-core GDT + TSS - // get the current stack we're running on (allocated by BSP) - let rsp: u64; - core::arch::asm!("mov {}, rsp", out(reg) rsp, options(nostack, nomem)); - let stack_top = (rsp + 0x1000) & !0xFFF; // round up to page boundary + // Read the exact stack_top from the trampoline data area. + // The BSP wrote the precise value to TD_STACK; rounding RSP up + // can overshoot if RSP is near a page boundary. + let stack_top = *((AP_TRAMPOLINE_PHYS + TD_STACK) as *const u64); gdt::init_gdt_for_ap(stack_top, core_idx); // 2. Load the shared IDT @@ -289,7 +331,8 @@ pub unsafe extern "sysv64" fn ap_rust_entry(core_idx: u32, lapic_id: u32) -> ! { // 6. Initialize LAPIC + timer on this core apic::init_ap(); - apic::setup_timer(100); // 100 Hz, same as BSP + apic::setup_timer(100); + crate::cpu::per_cpu::AP_ONLINE_COUNT.fetch_add(1, core::sync::atomic::Ordering::SeqCst); // 7. Signal we're online (done in init_ap via per_cpu::init_ap) diff --git a/hwinit/src/cpu/apic.rs b/hwinit/src/cpu/apic.rs index fac57b21..69dbed6b 100644 --- a/hwinit/src/cpu/apic.rs +++ b/hwinit/src/cpu/apic.rs @@ -9,6 +9,15 @@ use crate::cpu::per_cpu; use crate::cpu::pio::outb; use crate::serial::{log_info, log_ok, log_warn}; +use core::sync::atomic::{AtomicBool, AtomicU32, Ordering}; + +/// LAPIC timer init count calibrated by BSP — shared with all APs. +/// APs read this and skip the PIT calibration race. +/// 0 = not yet calibrated (do full calibration). +static LAPIC_TIMER_INIT_COUNT: AtomicU32 = AtomicU32::new(0); + + + // ── LAPIC register offsets ─────────────────────────────────────────────── @@ -48,9 +57,60 @@ pub const TIMER_VECTOR: u8 = 0x20; /// IA32_APIC_BASE MSR — contains the actual LAPIC physical base address. const IA32_APIC_BASE_MSR: u32 = 0x1B; +const IA32_X2APIC_ICR_MSR: u32 = 0x830; +const IA32_X2APIC_ID_MSR: u32 = 0x802; /// Actual LAPIC base, probed from MSR 0x1B. written once during BSP init. static mut LAPIC_BASE_ACTUAL: u64 = DEFAULT_LAPIC_BASE; +static X2APIC_ENABLED: AtomicBool = AtomicBool::new(false); + +#[inline(always)] +unsafe fn rdmsr(msr: u32) -> u64 { + let lo: u32; + let hi: u32; + core::arch::asm!( + "rdmsr", + in("ecx") msr, + out("eax") lo, + out("edx") hi, + options(nostack, nomem), + ); + ((hi as u64) << 32) | lo as u64 +} + +#[inline(always)] +unsafe fn wrmsr(msr: u32, val: u64) { + let lo = val as u32; + let hi = (val >> 32) as u32; + core::arch::asm!( + "wrmsr", + in("ecx") msr, + in("eax") lo, + in("edx") hi, + options(nostack, nomem), + ); +} + +#[inline(always)] +unsafe fn wrmsr_parts(msr: u32, lo: u32, hi: u32) { + core::arch::asm!( + "wrmsr", + in("ecx") msr, + in("eax") lo, + in("edx") hi, + options(nostack, nomem), + ); +} + +#[inline(always)] +fn x2apic_enabled() -> bool { + X2APIC_ENABLED.load(Ordering::Relaxed) +} + +#[inline(always)] +pub fn is_x2apic_mode() -> bool { + x2apic_enabled() +} /// Probe the LAPIC base address from IA32_APIC_BASE MSR. /// Stores the result for all subsequent LAPIC access. @@ -69,6 +129,8 @@ pub unsafe fn probe_lapic_base() -> u64 { options(nostack, nomem), ); let raw = (hi as u64) << 32 | lo as u64; + let x2 = (raw & (1 << 10)) != 0; + X2APIC_ENABLED.store(x2, Ordering::Relaxed); // bits [12:N] = base physical address, bits [0:11] = flags let base = raw & 0xFFFF_FFFF_FFFF_F000; LAPIC_BASE_ACTUAL = base; @@ -105,7 +167,11 @@ unsafe fn lapic_write(base: u64, reg: u32, val: u32) { /// Read the current CPU's LAPIC ID from hardware. pub unsafe fn read_lapic_id() -> u32 { - lapic_read(lapic_base(), LAPIC_ID) >> 24 + if x2apic_enabled() { + rdmsr(IA32_X2APIC_ID_MSR) as u32 + } else { + lapic_read(lapic_base(), LAPIC_ID) >> 24 + } } /// Check if APIC is available via CPUID. @@ -190,6 +256,18 @@ pub unsafe fn send_eoi() { pub unsafe fn setup_timer(target_hz: u32) { let base = lapic_base(); + // APs skip PIT calibration — they use the BSP's result directly. + // multiple APs racing on PIT channel 2 simultaneously will reset each + // other's countdown → all get stuck in the spin loop forever. + // BSP stores init_count after successful calibration. APs load it. + let cached = LAPIC_TIMER_INIT_COUNT.load(Ordering::Acquire); + if cached != 0 { + lapic_write(base, LAPIC_LVT_TIMER, TIMER_PERIODIC | TIMER_VECTOR as u32); + lapic_write(base, LAPIC_TIMER_DIV, 0x03); + lapic_write(base, LAPIC_TIMER_INIT, cached); + return; + } + // divide configuration: divide by 16 lapic_write(base, LAPIC_TIMER_DIV, 0x03); // 0b0011 = divide by 16 @@ -205,14 +283,18 @@ pub unsafe fn setup_timer(target_hz: u32) { outb(0x42, (PIT_TICKS & 0xFF) as u8); outb(0x42, ((PIT_TICKS >> 8) & 0xFF) as u8); - // start LAPIC timer with max initial count - lapic_write(base, LAPIC_TIMER_INIT, 0xFFFF_FFFF); - - // wait for PIT channel 2 to expire (bit 5 of port 0x61 goes high) - // re-arm the gate + // Re-arm the PIT gate BEFORE starting the LAPIC timer. + // On real hardware each I/O port access takes ~1µs. If the LAPIC + // timer starts first, the gate re-arm sequence inflates the measured + // LAPIC frequency by several microseconds → timer runs 5-15% fast + // → delay_us finishes early → AP SIPI delays too short. let v = crate::cpu::pio::inb(0x61) & 0xFE; - outb(0x61, v); - outb(0x61, v | 1); + outb(0x61, v); // gate LOW + outb(0x61, v | 1); // gate HIGH — PIT countdown restarts NOW + + // Start LAPIC timer immediately after PIT gate goes high so both + // timers run from the same instant. + lapic_write(base, LAPIC_TIMER_INIT, 0xFFFF_FFFF); // spin until PIT output goes high. if this hangs: PIT gate not armed, or // hardware has no PIT channel 2. the checkpoint tells us we entered the spin. @@ -240,6 +322,10 @@ pub unsafe fn setup_timer(target_hz: u32) { let init_count = (ticks_per_second / target_hz as u64) as u32; let _ = (elapsed, init_count, target_hz); + + // store for APs before arming so they never touch the PIT + LAPIC_TIMER_INIT_COUNT.store(init_count, Ordering::Release); + if crate::cpu::per_cpu::current_core_index() == 0 { log_ok("LAPIC", 724, "timer configured"); } @@ -262,23 +348,42 @@ pub unsafe fn disable_pic8259() { // ── IPI ────────────────────────────────────────────────────────────────── -/// Send an INIT IPI to target APIC ID. -pub unsafe fn send_init_ipi(target_apic_id: u32) { +/// Assert INIT IPI to target APIC ID. +/// Caller MUST wait >= 200µs then call `send_init_deassert`. +pub unsafe fn send_init_assert(target_apic_id: u32) { let base = lapic_base(); - // INIT assert + if x2apic_enabled() { + // level-triggered + assert + INIT = 0xC500 + let icr = (target_apic_id as u64) << 32 + | (ICR_INIT | ICR_TRIGGER_LEVEL | ICR_LEVEL_ASSERT) as u64; + wrmsr(IA32_X2APIC_ICR_MSR, icr); + wait_icr_idle(base); + return; + } + + // level-triggered assert. 0xC500 — matches linux/xv6/every OS ever shipped. lapic_write(base, LAPIC_ICR_HI, target_apic_id << 24); - lapic_write(base, LAPIC_ICR_LO, ICR_INIT | ICR_LEVEL_ASSERT); + lapic_write(base, LAPIC_ICR_LO, ICR_INIT | ICR_TRIGGER_LEVEL | ICR_LEVEL_ASSERT); wait_icr_idle(base); +} - // INIT deassert — trigger mode MUST be level (bit 15) with level=0. - // edge-triggered deassert is treated as another INIT assertion by KVM. +/// Deassert INIT IPI to target APIC ID. +/// Call after holding INIT assert for >= 200µs. +pub unsafe fn send_init_deassert(target_apic_id: u32) { + let base = lapic_base(); + + if x2apic_enabled() { + let icr = (target_apic_id as u64) << 32 + | (ICR_INIT | ICR_TRIGGER_LEVEL | ICR_LEVEL_DEASSERT) as u64; + wrmsr(IA32_X2APIC_ICR_MSR, icr); + wait_icr_idle(base); + return; + } + + // level-triggered deassert. trigger MUST be level or KVM treats it as assert. lapic_write(base, LAPIC_ICR_HI, target_apic_id << 24); - lapic_write( - base, - LAPIC_ICR_LO, - ICR_INIT | ICR_TRIGGER_LEVEL | ICR_LEVEL_DEASSERT, - ); + lapic_write(base, LAPIC_ICR_LO, ICR_INIT | ICR_TRIGGER_LEVEL | ICR_LEVEL_DEASSERT); wait_icr_idle(base); } @@ -288,6 +393,13 @@ pub unsafe fn send_init_ipi(target_apic_id: u32) { /// Must be below 1 MiB and page-aligned (e.g., 0x8000 → start_page = 8). pub unsafe fn send_sipi(target_apic_id: u32, start_page: u8) { let base = lapic_base(); + if x2apic_enabled() { + let icr_lo = ICR_STARTUP | start_page as u32; + let icr_hi = target_apic_id; + wrmsr_parts(IA32_X2APIC_ICR_MSR, icr_lo, icr_hi); + wait_icr_idle(base); + return; + } lapic_write(base, LAPIC_ICR_HI, target_apic_id << 24); lapic_write(base, LAPIC_ICR_LO, ICR_STARTUP | start_page as u32); wait_icr_idle(base); @@ -296,8 +408,16 @@ pub unsafe fn send_sipi(target_apic_id: u32, start_page: u8) { /// Wait for the ICR delivery status bit to clear. #[inline] unsafe fn wait_icr_idle(base: u64) { - let mut timeout = 100_000u32; - while lapic_read(base, LAPIC_ICR_LO) & ICR_DELIVERY_STATUS != 0 { + // x2APIC: ICR writes via MSR 0x830 are performed synchronously by + // the CPU microcode. There is no delivery-status bit to poll, and + // reading the MSR back can #GP on some implementations. + if x2apic_enabled() { + return; + } + + // xAPIC: poll MMIO ICR_LO for delivery-status bit to clear. + let mut timeout = 10_000u32; + while (lapic_read(base, LAPIC_ICR_LO) & ICR_DELIVERY_STATUS) != 0 { core::hint::spin_loop(); timeout -= 1; if timeout == 0 { @@ -313,11 +433,37 @@ unsafe fn wait_icr_idle(base: u64) { /// Falls back to a dumb loop if TSC freq is unknown. pub unsafe fn delay_us(us: u64) { let freq = crate::process::scheduler::tsc_frequency(); - if freq > 0 { + // Sanity-window the TSC frequency. Bad calibration here turns a 10ms AP + // bring-up delay into a geological epoch on some firmware. + if (1_000_000..=10_000_000_000).contains(&freq) { + let cycles_per_us = freq / 1_000_000; + if cycles_per_us == 0 { + for _ in 0..(us * 1000) { + core::hint::spin_loop(); + } + return; + } + let start = crate::cpu::tsc::read_tsc(); - let target = start + (freq / 1_000_000) * us; + let delta = cycles_per_us.saturating_mul(us); + let target = start.saturating_add(delta); + + // Scale watchdog with TSC frequency. Each loop iteration takes + // roughly 10–30 cycles (RDTSC + compare + PAUSE). Use a + // conservative estimate of 10 cycles/iteration so the watchdog + // never fires before the TSC target is actually reached. + let iters_per_us = (cycles_per_us / 10).max(1); + let max_spins_u64 = us.saturating_mul(iters_per_us) + .clamp(10_000, 1_000_000_000); + let max_spins = max_spins_u64 as u32; + let mut spins = 0u32; while crate::cpu::tsc::read_tsc() < target { core::hint::spin_loop(); + spins = spins.wrapping_add(1); + if spins >= max_spins { + log_warn("LAPIC", 727, "delay_us watchdog tripped; falling back"); + break; + } } } else { // dumb fallback: ~1µs per iteration at ~1GHz diff --git a/hwinit/src/cpu/gdt.rs b/hwinit/src/cpu/gdt.rs index 22cadda9..e68e4122 100644 --- a/hwinit/src/cpu/gdt.rs +++ b/hwinit/src/cpu/gdt.rs @@ -212,14 +212,24 @@ const IST1_STATIC_STACK_SIZE: usize = 32 * 1024; #[repr(C, align(16))] struct StaticStack([u8; IST1_STATIC_STACK_SIZE]); -/// The actual stack bytes. Zeroed by the BSS loader; no runtime init needed. -static mut IST1_STATIC_STACK: StaticStack = StaticStack([0; IST1_STATIC_STACK_SIZE]); +/// Per-core IST1 stacks. Each CPU needs its own IST1 so concurrent +/// double-faults on different cores don't corrupt each other's exception +/// frames. BSP uses index 0, APs use 1..MAX_CPUS-1. +/// Zeroed by the BSS loader; no runtime init needed. +static mut IST1_STACKS: [StaticStack; MAX_CPUS] = + [const { StaticStack([0; IST1_STATIC_STACK_SIZE]) }; MAX_CPUS]; + +/// Returns a pointer to the TOP of the per-core IST1 stack +/// (stacks grow downward on x86-64). +pub fn ist1_stack_top_for_core(core_idx: usize) -> u64 { + let idx = if core_idx < MAX_CPUS { core_idx } else { 0 }; + // SAFETY: read-only pointer arithmetic on a static array. + unsafe { IST1_STACKS[idx].0.as_ptr().add(IST1_STATIC_STACK_SIZE) as u64 } +} -/// Public accessor: returns a pointer to the TOP of the static IST1 stack -/// (stacks grow downward on x86-64). Written into TSS.IST[0] once at boot. +/// Backwards-compatible accessor for BSP (core 0). pub fn ist1_static_stack_top() -> u64 { - // SAFETY: read-only pointer arithmetic on a static array. - unsafe { IST1_STATIC_STACK.0.as_ptr().add(IST1_STATIC_STACK_SIZE) as u64 } + ist1_stack_top_for_core(0) } /// Our GDT (static, page-aligned for safety) @@ -388,7 +398,7 @@ pub unsafe fn init_gdt_for_ap(stack_top: u64, core_idx: u32) { // set up this AP's TSS AP_TSS[idx].rsp0 = stack_top; - AP_TSS[idx].ist[0] = ist1_static_stack_top(); // share the IST1 stack (double-fault only) + AP_TSS[idx].ist[0] = ist1_stack_top_for_core(idx); // per-core IST1 stack // point the GDT's TSS descriptor at this AP's TSS let tss_addr = &AP_TSS[idx] as *const Tss as u64; diff --git a/hwinit/src/cpu/per_cpu.rs b/hwinit/src/cpu/per_cpu.rs index 5050ce8b..a659d592 100644 --- a/hwinit/src/cpu/per_cpu.rs +++ b/hwinit/src/cpu/per_cpu.rs @@ -16,6 +16,7 @@ use core::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering}; /// Maximum cores we support. 16 is generous for a desktop OS on QEMU. /// Increase if you're running on a Threadripper and hate yourself. pub const MAX_CPUS: usize = 16; +const LAPIC_ID_MAP_SIZE: usize = 1024; // ── ABI field offsets (must match struct layout below) ──────────────────── // These are used by context_switch.s and syscall.s via `gs:[OFFSET]`. @@ -74,6 +75,9 @@ pub struct PerCpu { pub park_state: u8, /// Last observed active TSC on this core. pub last_active_tsc: u64, + /// Sequential core index (0 = BSP). Cached here so + /// `current_core_index()` avoids the LAPIC-ID table lookup. + pub core_index: u32, } impl PerCpu { @@ -95,6 +99,7 @@ impl PerCpu { load_hint: 0, park_state: 0, last_active_tsc: 0, + core_index: 0, } } } @@ -106,7 +111,37 @@ impl PerCpu { static mut PER_CPU_ARRAY: [PerCpu; MAX_CPUS] = [const { PerCpu::zeroed() }; MAX_CPUS]; /// Maps LAPIC ID → sequential core index. Sparse (most entries 0xFF = unused). -static mut LAPIC_TO_IDX: [u8; 256] = [0xFF; 256]; +static mut LAPIC_TO_IDX: [u8; LAPIC_ID_MAP_SIZE] = [0xFF; LAPIC_ID_MAP_SIZE]; + +#[inline(always)] +unsafe fn map_lapic_id(lapic_id: u32, idx: u8) { + if (lapic_id as usize) < LAPIC_ID_MAP_SIZE { + LAPIC_TO_IDX[lapic_id as usize] = idx; + } else { + puts("[PERCPU] LAPIC ID out of fast-map range: "); + put_hex32(lapic_id); + puts("\n"); + } +} + +#[inline(always)] +unsafe fn lapic_id_to_index(lapic_id: u32) -> Option { + if (lapic_id as usize) < LAPIC_ID_MAP_SIZE { + let idx = LAPIC_TO_IDX[lapic_id as usize]; + if idx != 0xFF { + return Some(idx as u32); + } + } + + // fallback scan for sparse/high IDs. MAX_CPUS is tiny, so this is cheap. + for i in 0..MAX_CPUS { + let pcpu = &PER_CPU_ARRAY[i]; + if pcpu.online && pcpu.cpu_id == lapic_id { + return Some(i as u32); + } + } + None +} /// Number of cores that have completed init. pub static AP_ONLINE_COUNT: AtomicU32 = AtomicU32::new(0); @@ -177,10 +212,11 @@ pub unsafe fn init_bsp(lapic_id: u32, lapic_base: u64) { pcpu.cpu_id = lapic_id; pcpu.current_pid = 0; // kernel pcpu.lapic_base = lapic_base; + pcpu.core_index = idx as u32; pcpu.online = true; // map LAPIC ID → index - LAPIC_TO_IDX[lapic_id as usize] = idx as u8; + map_lapic_id(lapic_id, idx as u8); // load GS-base to point at this PerCpu let addr = pcpu as *const PerCpu as u64; @@ -210,6 +246,7 @@ pub unsafe fn init_ap(core_idx: u32, lapic_id: u32, lapic_base: u64) { pcpu.cpu_id = lapic_id; pcpu.current_pid = 0; // starts idle on kernel pcpu.lapic_base = lapic_base; + pcpu.core_index = core_idx; pcpu.online = true; // save the AP's kernel stack top so the scheduler can restore it @@ -218,13 +255,13 @@ pub unsafe fn init_ap(core_idx: u32, lapic_id: u32, lapic_base: u64) { core::arch::asm!("mov {}, rsp", out(reg) rsp, options(nostack, nomem)); pcpu.boot_kernel_rsp = (rsp + 0x1000) & !0xFFF; - LAPIC_TO_IDX[lapic_id as usize] = idx as u8; + map_lapic_id(lapic_id, idx as u8); let addr = pcpu as *const PerCpu as u64; wrmsr(IA32_GS_BASE, addr); wrmsr(IA32_KERNEL_GS_BASE, 0); - AP_ONLINE_COUNT.fetch_add(1, Ordering::SeqCst); + // AP bring-up is noisy on many-core systems; summary is logged by SMP phase. } @@ -253,25 +290,13 @@ pub unsafe fn by_index(idx: u32) -> &'static mut PerCpu { /// Get PerCpu by LAPIC ID. #[inline(always)] pub unsafe fn by_lapic_id(lapic_id: u32) -> Option<&'static mut PerCpu> { - let idx = LAPIC_TO_IDX[lapic_id as usize]; - if idx == 0xFF { - None - } else { - Some(&mut PER_CPU_ARRAY[idx as usize]) - } + lapic_id_to_index(lapic_id).map(|idx| &mut PER_CPU_ARRAY[idx as usize]) } /// Current core's sequential index (0 = BSP). #[inline(always)] pub unsafe fn current_core_index() -> u32 { - let lapic_id: u32; - core::arch::asm!( - "mov {0:e}, gs:[0x08]", // cpu_id is u32 at offset 0x08. read 32-bit to avoid - // bleeding into current_pid at 0x0C. - out(reg) lapic_id, - options(nostack, readonly, preserves_flags), - ); - LAPIC_TO_IDX[lapic_id as usize] as u32 + current().core_index } /// Current core's LAPIC ID. diff --git a/hwinit/src/memory.rs b/hwinit/src/memory.rs index 6442a77d..354f9b35 100644 --- a/hwinit/src/memory.rs +++ b/hwinit/src/memory.rs @@ -11,7 +11,7 @@ pub const PAGE_SHIFT: u32 = 12; /// one constant to rule them all. 26 → 256 GiB, 28 → 1 TiB, 30 → 4 TiB. const MAX_ORDER: usize = 26; -const MAX_MAP: usize = 384; // UEFI snapshot slots +const MAX_MAP: usize = 768; // UEFI snapshot slots — real hardware can have 400+ // memory types (UEFI + our own tags) diff --git a/hwinit/src/paging/mod.rs b/hwinit/src/paging/mod.rs index f417119d..7d75ad8d 100644 --- a/hwinit/src/paging/mod.rs +++ b/hwinit/src/paging/mod.rs @@ -7,7 +7,7 @@ pub mod table; pub use entry::{PageFlags, PageTable, PageTableEntry}; pub use table::{MappedPageSize, PageTableManager, VirtAddr}; -use crate::serial::{log_info, log_ok}; +use crate::serial::{log_info, log_ok, log_warn}; use crate::sync::SpinLock; // kernel page table singleton — SMP-safe via SpinLock @@ -44,6 +44,13 @@ pub unsafe fn init_kernel_page_table() { options(nomem, nostack), ); log_info("PAGING", 731, "cleared CR0.WP"); + + // Flush TLB so stale entries that cached R/W=0 under WP=1 are + // discarded. Without this, writes to UEFI-owned PT pages can + // still fault on real hardware that cached the old permission. + let cr3_val: u64; + core::arch::asm!("mov {}, cr3", out(reg) cr3_val, options(nomem, nostack)); + core::arch::asm!("mov cr3, {}", in(reg) cr3_val, options(nostack)); } *KERNEL_PT.lock() = Some(mgr); @@ -174,8 +181,10 @@ pub unsafe fn kmark_uncacheable(virt: u64) -> Result<(), &'static str> { /// - up to ~24 PD pages (one per 1 GiB) /// - PT pages only if 4 KiB region exists (firmware code, MMIO, etc.) /// -/// 512 entries is vastly more than needed but fits comfortably on the stack. -const MAX_PT_PAGES: usize = 512; +/// On some firmware builds, low memory and MMIO windows are mapped with many +/// 4 KiB leaves, which can produce far more than a few hundred PT pages. +/// Keep this comfortably large so reclaim never misses live paging structures. +const MAX_PT_PAGES: usize = 8192; /// Walk the active CR3 page table hierarchy and collect the physical /// addresses of **every** page that is itself a page table page (PML4, @@ -197,6 +206,7 @@ pub unsafe fn collect_page_table_pages() -> ([u64; MAX_PT_PAGES], usize) { let mut pages = [0u64; MAX_PT_PAGES]; let mut count = 0usize; + let mut truncated = false; // Helper closure (inlined) to add a page if not already seen. macro_rules! add_page { @@ -209,9 +219,13 @@ pub unsafe fn collect_page_table_pages() -> ([u64; MAX_PT_PAGES], usize) { break; } } - if !seen && count < MAX_PT_PAGES { - pages[count] = p; - count += 1; + if !seen { + if count < MAX_PT_PAGES { + pages[count] = p; + count += 1; + } else { + truncated = true; + } } }; } @@ -255,6 +269,10 @@ pub unsafe fn collect_page_table_pages() -> ([u64; MAX_PT_PAGES], usize) { } } + if truncated { + log_warn("PAGING", 733, "page-table page collection truncated"); + } + (pages, count) } diff --git a/hwinit/src/platform.rs b/hwinit/src/platform.rs index 090cf70d..ff4ade10 100644 --- a/hwinit/src/platform.rs +++ b/hwinit/src/platform.rs @@ -18,7 +18,7 @@ use crate::memory::{ PhysicalAllocator, }; use crate::paging::init_kernel_page_table; -use crate::pci::{offset, pci_cfg_read16, pci_cfg_write16, PciAddr}; +use crate::pci::{offset, pci_cfg_read8, pci_cfg_read16, pci_cfg_write16, PciAddr}; use crate::process::scheduler::init_scheduler; use crate::serial::{checkpoint, log_error, log_info, log_ok, log_warn}; use crate::syscall::init_syscall; @@ -61,6 +61,9 @@ pub struct SelfContainedConfig { pub stack_base: u64, /// Number of 4 KiB pages in the boot stack allocation. pub stack_pages: u64, + /// Physical address of the ACPI RSDP from UEFI configuration table. + /// 0 means unavailable. + pub acpi_rsdp_phys: u64, } /// Platform configuration input (legacy - externally allocated). @@ -119,6 +122,11 @@ pub unsafe fn platform_init_selfcontained( in(reg) cr0 & !(1u64 << 16), options(nomem, nostack), ); + // Flush TLB so no stale entries carry the old WP=1 permission + // model. Some real CPUs cache the WP bit in TLB entries. + let cr3: u64; + core::arch::asm!("mov {}, cr3", out(reg) cr3, options(nomem, nostack)); + core::arch::asm!("mov cr3, {}", in(reg) cr3, options(nostack)); } } @@ -333,6 +341,41 @@ pub unsafe fn platform_init_selfcontained( return Err(InitError::TscCalibrationFailed); } + // Check for invariant TSC (CPUID.80000007H:EDX[8]). + // Without it, CPU frequency scaling (P-states, C-states) causes TSC + // drift — delay_us and scheduler timing will be inaccurate on real HW. + { + let max_ext: u32; + core::arch::asm!( + "push rbx", + "mov eax, 0x80000000", + "cpuid", + "pop rbx", + out("eax") max_ext, + out("ecx") _, + out("edx") _, + options(nostack), + ); + if max_ext >= 0x80000007 { + let edx: u32; + core::arch::asm!( + "push rbx", + "mov eax, 0x80000007", + "cpuid", + "pop rbx", + out("edx") edx, + out("eax") _, + out("ecx") _, + options(nostack), + ); + if edx & (1 << 8) == 0 { + log_warn("TSC", 551, "CPU lacks invariant TSC; timing may drift with P-state changes"); + } + } else { + log_warn("TSC", 552, "CPUID extended leaf 0x80000007 unavailable; cannot verify invariant TSC"); + } + } + checkpoint("phase5-done"); crate::process::scheduler::set_tsc_frequency(tsc_freq); @@ -344,7 +387,7 @@ pub unsafe fn platform_init_selfcontained( let dma_phys = { let mut dma_reg = global_registry_mut(); dma_reg - .allocate_pages(AllocateType::AnyPages, MemoryType::AllocatedDma, dma_pages) + .allocate_pages(AllocateType::MaxAddress(0xFFFF_FFFF), MemoryType::AllocatedDma, dma_pages) .map_err(|_| InitError::NoFreeMemory)? }; @@ -423,6 +466,9 @@ pub unsafe fn platform_init_selfcontained( // Collect them first, sort, and pass as an exclusion set. log_info("BOOT", 111, "phase 10.5/12: reclaim boot services ram"); checkpoint("phase10.5-reclaim-begin"); + // Keep this whole critical section non-preemptible. The timer IRQ is live + // after phase 10; reclaim/reserve mutates allocator + paging metadata. + crate::cpu::idt::disable_interrupts(); { let (mut pt_pages, pt_count) = crate::paging::collect_page_table_pages(); @@ -448,6 +494,7 @@ pub unsafe fn platform_init_selfcontained( let reg = global_registry_mut(); reg.validate_free_lists(); } + crate::cpu::idt::enable_interrupts(); checkpoint("phase10.5-done"); // phase 11: filesystem @@ -470,7 +517,7 @@ pub unsafe fn platform_init_selfcontained( core::ptr::write_bytes(root_fs_base as *mut u8, 0, ROOT_FS_SIZE); checkpoint("phase11-fs-mount"); match morpheus_helix::vfs::global::init_root_fs(root_fs_base as *mut u8, ROOT_FS_SIZE) { - Ok(()) => log_ok("FS", 112, "helixfs mounted at /"), + Ok(()) => log_ok("FS", 112, "bootstrap RAM helixfs mounted at /"), Err(_) => { log_warn("FS", 412, "root fs init failed; continuing without fs"); // Non-fatal — system continues without FS. @@ -493,6 +540,9 @@ pub unsafe fn platform_init_selfcontained( { use crate::cpu::{acpi, ap_boot, apic, per_cpu}; + // UEFI gave us the authoritative ACPI root. no BIOS scavenger hunt. + acpi::set_rsdp_phys(config.acpi_rsdp_phys); + let bsp_lapic_id = apic::read_lapic_id(); checkpoint("phase12-madt-scan"); @@ -504,7 +554,9 @@ pub unsafe fn platform_init_selfcontained( if madt_result.count > 0 { per_cpu::set_cpu_count(madt_result.count as u32 + 1); // +1 for BSP checkpoint("phase12-start-aps-madt"); + crate::cpu::idt::disable_interrupts(); ap_boot::start_aps_from_list(&madt_result.ids[..madt_result.count]); + crate::cpu::idt::enable_interrupts(); } else { // fallback: CPUID-based count + brute-force LAPIC ID enumeration. // works but slow on sparse topologies. @@ -515,7 +567,9 @@ pub unsafe fn platform_init_selfcontained( if cpu_count > 1 { checkpoint("phase12-start-aps-cpuid"); + crate::cpu::idt::disable_interrupts(); ap_boot::start_aps(); + crate::cpu::idt::enable_interrupts(); } else { log_info("SMP", 114, "single-core detected; no AP startup"); } @@ -589,8 +643,8 @@ unsafe fn enable_all_pci_devices() -> usize { continue; } - // Enable this device - enable_bus_mastering(addr); + // Enable this device (if it's not a bridge) + maybe_enable_bus_mastering(addr); count += 1; // Check for multi-function @@ -600,7 +654,7 @@ unsafe fn enable_all_pci_devices() -> usize { let faddr = PciAddr::new(bus, device, function); let v = pci_cfg_read16(faddr, offset::VENDOR_ID); if v != 0xFFFF && v != 0x0000 { - enable_bus_mastering(faddr); + maybe_enable_bus_mastering(faddr); count += 1; } } @@ -611,6 +665,19 @@ unsafe fn enable_all_pci_devices() -> usize { count } +/// Check class code and skip bridge devices. Blindly enabling bus mastering +/// on host bridges, PCI-PCI bridges, and ISA bridges can trigger IOMMU +/// faults or stray DMA from shadow BARs on real hardware. +fn maybe_enable_bus_mastering(addr: PciAddr) { + let class = pci_cfg_read8(addr, 0x0B); // base class at offset 0x0B + if class == 0x06 { + // 0x06 = Bridge device (host, ISA, PCI-PCI, CardBus, etc.) + // Don't toggle bus mastering — the firmware configured these. + return; + } + enable_bus_mastering(addr); +} + /// Enable memory space and bus mastering on a device. fn enable_bus_mastering(addr: PciAddr) { let cmd = pci_cfg_read16(addr, offset::COMMAND); diff --git a/init/Cargo.toml b/init/Cargo.toml index 89d8ef99..9677ca91 100644 --- a/init/Cargo.toml +++ b/init/Cargo.toml @@ -10,6 +10,3 @@ path = "src/main.rs" [dependencies] libmorpheus = { path = "../libmorpheus" } -[profile.release] -opt-level = 3 -lto = true diff --git a/network/Cargo.toml b/network/Cargo.toml index f12351e6..0a58c115 100644 --- a/network/Cargo.toml +++ b/network/Cargo.toml @@ -12,6 +12,7 @@ display = ["morpheus-display"] # Enable framebuffer display for post-EBS debug [dependencies] morpheus-core = { workspace = true } +morpheus-hwinit = { workspace = true } morpheus-display = { path = "../display", features = ["framebuffer-backend"], optional = true } # GPT disk I/O trait for filesystem compatibility diff --git a/network/asm/drivers/sdhci/init.s b/network/asm/drivers/sdhci/init.s new file mode 100644 index 00000000..911907f9 --- /dev/null +++ b/network/asm/drivers/sdhci/init.s @@ -0,0 +1,290 @@ +; ═══════════════════════════════════════════════════════════════════════════ +; SDHCI initialization primitives +; ABI: Microsoft x64 (RCX, RDX, R8, R9, stack) +; ═══════════════════════════════════════════════════════════════════════════ + +section .data + %include "asm/drivers/sdhci/regs.s" + +section .text + +extern asm_mmio_read32 +extern asm_mmio_write32 +extern asm_mmio_write16 +extern asm_mmio_read8 +extern asm_mmio_write8 +extern asm_tsc_read +extern asm_bar_mfence + +global asm_sdhci_read_caps +global asm_sdhci_card_present +global asm_sdhci_controller_reset +global asm_sdhci_basic_power_clock +global asm_sdhci_read_block_pio + +; RCX = mmio_base +; EAX = capabilities +asm_sdhci_read_caps: + sub rsp, 32 + lea rcx, [rcx + SDHCI_REG_CAPABILITIES] + call asm_mmio_read32 + add rsp, 32 + ret + +; RCX = mmio_base +; EAX = 1 if card inserted, else 0 +asm_sdhci_card_present: + sub rsp, 32 + lea rcx, [rcx + SDHCI_REG_PRESENT_STATE] + call asm_mmio_read32 + and eax, SDHCI_PRESENT_CARD_INSERTED + xor edx, edx + test eax, eax + setnz dl + mov eax, edx + add rsp, 32 + ret + +; RCX = mmio_base, RDX = tsc_freq +; EAX = 0 success, 1 timeout +asm_sdhci_controller_reset: + push rbx + push r12 + push r13 + sub rsp, 32 + + mov r12, rcx ; base + mov r13, rdx ; tsc_freq + + ; Write software reset all. + lea rcx, [r12 + SDHCI_REG_SOFTWARE_RESET] + mov dl, SDHCI_RESET_ALL + call asm_mmio_write8 + call asm_bar_mfence + + ; Timeout ~100ms. + call asm_tsc_read + mov rbx, rax + mov rax, r13 + xor rdx, rdx + mov rcx, 10 + div rcx ; tsc_freq / 10 + mov r13, rax + +.reset_wait: + lea rcx, [r12 + SDHCI_REG_SOFTWARE_RESET] + call asm_mmio_read8 + test al, SDHCI_RESET_ALL + jz .ok + + call asm_tsc_read + sub rax, rbx + cmp rax, r13 + jb .reset_wait + + mov eax, 1 + jmp .out + +.ok: + xor eax, eax + +.out: + add rsp, 32 + pop r13 + pop r12 + pop rbx + ret + +; RCX = mmio_base +; EAX = 0 on success (minimal setup) +asm_sdhci_basic_power_clock: + sub rsp, 32 + + ; Best-effort minimal power enable for bring-up path. + lea rcx, [rcx + SDHCI_REG_POWER_CTRL] + mov dl, 0x0F ; bus power + nominal voltage bits + call asm_mmio_write8 + + ; Enable internal clock + SD clock (controller-specific, minimal). + lea rcx, [rcx + SDHCI_REG_CLOCK_CTRL] + mov dx, 0x0005 + call asm_mmio_write16 + + xor eax, eax + add rsp, 32 + ret + +; RCX = mmio_base, RDX = lba, R8 = dst_ptr, R9 = tsc_freq +; EAX = 0 success +; 1 timeout waiting inhibit clear +; 2 command phase error/timeout +; 3 buffer-read phase error/timeout +; 4 transfer-complete phase error/timeout +asm_sdhci_read_block_pio: + push rbx + push r12 + push r13 + push r14 + push r15 + sub rsp, 32 + + mov r12, rcx ; base + mov r13, rdx ; lba + mov r14, r8 ; dst + + ; 100ms timeout ticks + mov rax, r9 + xor rdx, rdx + mov rcx, 10 + div rcx + mov r15, rax + + ; wait until command/data lines are idle + call asm_tsc_read + mov rbx, rax +.wait_idle: + lea rcx, [r12 + SDHCI_REG_PRESENT_STATE] + call asm_mmio_read32 + mov edx, SDHCI_PRESENT_CMD_INHIBIT | SDHCI_PRESENT_DAT_INHIBIT + test eax, edx + jz .idle_ok + call asm_tsc_read + sub rax, rbx + cmp rax, r15 + jb .wait_idle + mov eax, 1 + jmp .out + +.idle_ok: + ; clear stale interrupts + lea rcx, [r12 + SDHCI_REG_INT_STATUS] + mov edx, 0xFFFF_FFFF + call asm_mmio_write32 + + ; block size = 512, block count = 1 + lea rcx, [r12 + SDHCI_REG_BLOCK_SIZE] + mov dx, 512 + call asm_mmio_write16 + + lea rcx, [r12 + SDHCI_REG_BLOCK_COUNT] + mov dx, 1 + call asm_mmio_write16 + + ; timeout control: max + lea rcx, [r12 + SDHCI_REG_TIMEOUT_CTRL] + mov dl, 0x0E + call asm_mmio_write8 + + ; argument = LBA (SDHC/SDXC block addressing) + lea rcx, [r12 + SDHCI_REG_ARGUMENT] + mov edx, r13d + call asm_mmio_write32 + + ; transfer mode: read + block count enable + lea rcx, [r12 + SDHCI_REG_TRANSFER_MODE] + mov dx, SDHCI_TRNS_READ | SDHCI_TRNS_BLK_CNT_EN + call asm_mmio_write16 + + ; CMD17 READ_SINGLE_BLOCK, short resp + crc + index + data + lea rcx, [r12 + SDHCI_REG_COMMAND] + mov dx, (17 << 8) | SDHCI_CMD_RESP_SHORT | SDHCI_CMD_CRC | SDHCI_CMD_INDEX | SDHCI_CMD_DATA + call asm_mmio_write16 + + ; wait command complete + call asm_tsc_read + mov rbx, rax +.wait_cmd: + lea rcx, [r12 + SDHCI_REG_INT_STATUS] + call asm_mmio_read32 + test eax, SDHCI_INT_ERROR + jnz .cmd_err + test eax, SDHCI_INT_CMD_COMPLETE + jnz .cmd_ok + call asm_tsc_read + sub rax, rbx + cmp rax, r15 + jb .wait_cmd +.cmd_err: + lea rcx, [r12 + SDHCI_REG_INT_STATUS] + mov edx, 0xFFFF_FFFF + call asm_mmio_write32 + mov eax, 2 + jmp .out + +.cmd_ok: + lea rcx, [r12 + SDHCI_REG_INT_STATUS] + mov edx, SDHCI_INT_CMD_COMPLETE + call asm_mmio_write32 + + ; wait buffer read ready + call asm_tsc_read + mov rbx, rax +.wait_buf: + lea rcx, [r12 + SDHCI_REG_INT_STATUS] + call asm_mmio_read32 + test eax, SDHCI_INT_ERROR + jnz .buf_err + test eax, SDHCI_INT_BUF_READ_READY + jnz .buf_ok + call asm_tsc_read + sub rax, rbx + cmp rax, r15 + jb .wait_buf +.buf_err: + lea rcx, [r12 + SDHCI_REG_INT_STATUS] + mov edx, 0xFFFF_FFFF + call asm_mmio_write32 + mov eax, 3 + jmp .out + +.buf_ok: + lea rcx, [r12 + SDHCI_REG_INT_STATUS] + mov edx, SDHCI_INT_BUF_READ_READY + call asm_mmio_write32 + + ; read 512 bytes (128 dwords) + xor r11d, r11d +.copy_loop: + lea rcx, [r12 + SDHCI_REG_BUFFER_DATA] + call asm_mmio_read32 + mov [r14 + r11*4], eax + inc r11d + cmp r11d, 128 + jb .copy_loop + + ; wait transfer complete + call asm_tsc_read + mov rbx, rax +.wait_xfer: + lea rcx, [r12 + SDHCI_REG_INT_STATUS] + call asm_mmio_read32 + test eax, SDHCI_INT_ERROR + jnz .xfer_err + test eax, SDHCI_INT_XFER_COMPLETE + jnz .xfer_ok + call asm_tsc_read + sub rax, rbx + cmp rax, r15 + jb .wait_xfer +.xfer_err: + lea rcx, [r12 + SDHCI_REG_INT_STATUS] + mov edx, 0xFFFF_FFFF + call asm_mmio_write32 + mov eax, 4 + jmp .out + +.xfer_ok: + lea rcx, [r12 + SDHCI_REG_INT_STATUS] + mov edx, SDHCI_INT_XFER_COMPLETE + call asm_mmio_write32 + + xor eax, eax + +.out: + add rsp, 32 + pop r15 + pop r14 + pop r13 + pop r12 + pop rbx + ret diff --git a/network/asm/drivers/sdhci/regs.s b/network/asm/drivers/sdhci/regs.s new file mode 100644 index 00000000..44f55240 --- /dev/null +++ b/network/asm/drivers/sdhci/regs.s @@ -0,0 +1,39 @@ +; SDHCI register definitions (SD Host Controller Spec) + +%ifndef SDHCI_REGS_INCLUDED +%define SDHCI_REGS_INCLUDED + +SDHCI_REG_CAPABILITIES equ 0x40 +SDHCI_REG_BLOCK_SIZE equ 0x04 +SDHCI_REG_BLOCK_COUNT equ 0x06 +SDHCI_REG_ARGUMENT equ 0x08 +SDHCI_REG_TRANSFER_MODE equ 0x0C +SDHCI_REG_COMMAND equ 0x0E +SDHCI_REG_BUFFER_DATA equ 0x20 +SDHCI_REG_PRESENT_STATE equ 0x24 +SDHCI_REG_POWER_CTRL equ 0x29 +SDHCI_REG_CLOCK_CTRL equ 0x2C +SDHCI_REG_TIMEOUT_CTRL equ 0x2E +SDHCI_REG_SOFTWARE_RESET equ 0x2F +SDHCI_REG_INT_STATUS equ 0x30 + +SDHCI_PRESENT_CARD_INSERTED equ (1 << 16) +SDHCI_PRESENT_CMD_INHIBIT equ (1 << 0) +SDHCI_PRESENT_DAT_INHIBIT equ (1 << 1) + +SDHCI_RESET_ALL equ 0x01 + +SDHCI_INT_CMD_COMPLETE equ (1 << 0) +SDHCI_INT_XFER_COMPLETE equ (1 << 1) +SDHCI_INT_BUF_READ_READY equ (1 << 5) +SDHCI_INT_ERROR equ (1 << 15) + +SDHCI_TRNS_BLK_CNT_EN equ (1 << 1) +SDHCI_TRNS_READ equ (1 << 4) + +SDHCI_CMD_RESP_SHORT equ 0x02 +SDHCI_CMD_CRC equ 0x08 +SDHCI_CMD_INDEX equ 0x10 +SDHCI_CMD_DATA equ 0x20 + +%endif diff --git a/network/asm/drivers/usb/init.s b/network/asm/drivers/usb/init.s new file mode 100644 index 00000000..73496e82 --- /dev/null +++ b/network/asm/drivers/usb/init.s @@ -0,0 +1,219 @@ +; ═══════════════════════════════════════════════════════════════════════════ +; USB xHCI host controller primitives — brutal init edition +; ABI: Microsoft x64 (RCX, RDX, R8, R9, stack) +; ═══════════════════════════════════════════════════════════════════════════ + +section .data + %include "asm/drivers/usb/regs.s" + +section .text + +extern asm_mmio_read8 +extern asm_mmio_read16 +extern asm_mmio_read32 +extern asm_mmio_write32 +extern asm_tsc_read +extern asm_bar_mfence + +global asm_usb_host_probe +global asm_xhci_controller_soft_restart +global asm_xhci_bios_handoff + +; ─────────────────────────────────────────────────────────────────────────── +; asm_usb_host_probe +; ─────────────────────────────────────────────────────────────────────────── +; RCX = mmio_base (BAR0) +; Returns: +; EAX bits 7:0 = CAPLENGTH +; EAX bits 31:16 = HCIVERSION (should be >= 0x0100) +; EAX = 0 if bar reads back all-F (unmapped/dead) +asm_usb_host_probe: + push rbx + sub rsp, 32 ; 1 push + ret = 16; +32 = 48; 48%16=0. aligned. + + mov rbx, rcx + + ; read dword at mmio_base+0: [7:0]=CAPLENGTH, [31:16]=HCIVERSION + call asm_mmio_read32 + cmp eax, 0xFFFFFFFF + je .probe_dead + ; sanity: CAPLENGTH must be ≥ 0x10 (relaxed — some controllers are small) + movzx edx, al + cmp edx, 0x10 + jb .probe_dead + ; HCIVERSION must be ≥ 0x0100 + shr eax, 16 + cmp ax, 0x0100 + jb .probe_dead + + ; reconstruct: low byte = CAPLENGTH, high half = HCIVERSION + shl eax, 16 + or eax, edx + jmp .probe_out + +.probe_dead: + xor eax, eax +.probe_out: + add rsp, 32 + pop rbx + ret + +; ─────────────────────────────────────────────────────────────────────────── +; asm_xhci_bios_handoff +; ─────────────────────────────────────────────────────────────────────────── +; Claim xHCI from BIOS/SMM via USBLEGSUP extended capability. +; RCX = mmio_base +; RDX = hccparams1 (from mmio_base + 0x10) +; R8 = tsc_freq +; Returns: EAX = 0 success (or no legacy cap), 1 timeout waiting BIOS release +asm_xhci_bios_handoff: + push rbx + push r12 + push r13 + push r14 + sub rsp, 40 + + mov r12, rcx ; mmio_base + mov r14, r8 ; tsc_freq + + ; extended capability pointer from HCCPARAMS1 bits 31:16 (dword offset) + mov eax, edx + shr eax, 16 + and eax, 0xFFFF + shl eax, 2 ; *4 = byte offset from mmio_base + test eax, eax + jz .bios_none ; no extended caps + + ; walk extended capability list looking for ID=1 (legacy support) + lea r13, [r12 + rax] ; ext_cap_ptr +.bios_walk: + mov rcx, r13 + call asm_mmio_read32 + movzx edx, al ; cap ID + cmp dl, XHCI_EXT_CAP_LEGACY + je .bios_found + + ; next pointer = bits 15:8, shift left 2 + mov ecx, eax + shr ecx, 8 + and ecx, 0xFF + test ecx, ecx + jz .bios_none ; end of list + shl ecx, 2 + add r13, rcx + jmp .bios_walk + +.bios_found: + ; USBLEGSUP at r13. Set OS_OWNED bit (byte at offset +3) + mov rcx, r13 + call asm_mmio_read32 + or eax, XHCI_LEGSUP_OS_OWNED + mov edx, eax + mov rcx, r13 + call asm_mmio_write32 + call asm_bar_mfence + + ; wait for BIOS_OWNED to clear (1 second timeout because firmware is slow) + call asm_tsc_read + mov rbx, rax +.bios_wait: + mov rcx, r13 + call asm_mmio_read32 + test eax, XHCI_LEGSUP_BIOS_OWNED + jz .bios_claimed + call asm_tsc_read + sub rax, rbx + cmp rax, r14 ; 1 second = tsc_freq ticks + jb .bios_wait + mov eax, 1 ; timeout waiting owner release + jmp .bios_out + +.bios_claimed: +.bios_none: + xor eax, eax +.bios_out: + add rsp, 40 + pop r14 + pop r13 + pop r12 + pop rbx + ret + +; ─────────────────────────────────────────────────────────────────────────── +; asm_xhci_controller_soft_restart +; ─────────────────────────────────────────────────────────────────────────── +; Restart xHCI without hard reset (preserves port state from UEFI handoff). +; Assumes UEFI left controller in a valid state. We just stop and restart. +; RCX = op_base (mmio_base + CAPLENGTH) +; RDX = tsc_freq +; Returns: EAX = 0 success, 1 halt timeout, 2 start timeout +asm_xhci_controller_soft_restart: + push rbx + push r12 + push r13 + push r14 + sub rsp, 40 + + mov r12, rcx ; op_base + mov r13, rdx ; tsc_freq + mov r14, r13 ; 1 second timeout = tsc_freq ticks + + ; ── step 1: stop controller (RS = 0) ── + lea rcx, [r12 + XHCI_OP_USBCMD] + xor edx, edx + call asm_mmio_write32 + call asm_bar_mfence + + ; ── wait for HCH (halted) ── + call asm_tsc_read + mov rbx, rax +.wait_halt_soft: + lea rcx, [r12 + XHCI_OP_USBSTS] + call asm_mmio_read32 + test eax, XHCI_STS_HCH + jnz .halted_soft + call asm_tsc_read + sub rax, rbx + cmp rax, r14 + jb .wait_halt_soft + mov eax, 1 ; halt timeout + jmp .out_soft + +.halted_soft: + ; ── clear status bits ── + lea rcx, [r12 + XHCI_OP_USBSTS] + mov edx, 0xFFFFFFFF + call asm_mmio_write32 + call asm_bar_mfence + + ; ── start controller (RS = 1, INTE = 1) — NO HCRST ── + lea rcx, [r12 + XHCI_OP_USBCMD] + mov edx, XHCI_CMD_RS | XHCI_CMD_INTE + call asm_mmio_write32 + call asm_bar_mfence + + ; ── wait for HCH to clear (running) ── + call asm_tsc_read + mov rbx, rax +.wait_running: + lea rcx, [r12 + XHCI_OP_USBSTS] + call asm_mmio_read32 + test eax, XHCI_STS_HCH + jz .running_soft + call asm_tsc_read + sub rax, rbx + cmp rax, r14 + jb .wait_running + mov eax, 2 ; start timeout + jmp .out_soft + +.running_soft: + xor eax, eax + +.out_soft: + add rsp, 40 + pop r14 + pop r13 + pop r12 + pop rbx + ret diff --git a/network/asm/drivers/usb/regs.s b/network/asm/drivers/usb/regs.s new file mode 100644 index 00000000..30f90f32 --- /dev/null +++ b/network/asm/drivers/usb/regs.s @@ -0,0 +1,49 @@ +; xHCI register definitions (eXtensible Host Controller Interface Spec 1.2) + +%ifndef XHCI_REGS_INCLUDED +%define XHCI_REGS_INCLUDED + +; ─── Capability Registers (offset from BAR0) ───────────────────────────── +XHCI_CAP_CAPLENGTH equ 0x00 +XHCI_CAP_HCIVERSION equ 0x02 +XHCI_CAP_HCSPARAMS1 equ 0x04 +XHCI_CAP_HCSPARAMS2 equ 0x08 +XHCI_CAP_HCCPARAMS1 equ 0x10 +XHCI_CAP_DBOFF equ 0x14 +XHCI_CAP_RTSOFF equ 0x18 + +; ─── Operational Registers (offset from op_base = BAR0 + CAPLENGTH) ────── +XHCI_OP_USBCMD equ 0x00 +XHCI_OP_USBSTS equ 0x04 +XHCI_OP_PAGESIZE equ 0x08 +XHCI_OP_CRCR_LO equ 0x18 +XHCI_OP_CRCR_HI equ 0x1C +XHCI_OP_DCBAAP_LO equ 0x30 +XHCI_OP_DCBAAP_HI equ 0x34 +XHCI_OP_CONFIG equ 0x38 + +; ─── USBCMD bits ───────────────────────────────────────────────────────── +XHCI_CMD_RS equ (1 << 0) +XHCI_CMD_HCRST equ (1 << 1) +XHCI_CMD_INTE equ (1 << 2) + +; ─── USBSTS bits ───────────────────────────────────────────────────────── +XHCI_STS_HCH equ (1 << 0) +XHCI_STS_CNR equ (1 << 11) + +; ─── PORTSC (at op_base + 0x400 + port*0x10) ───────────────────────────── +XHCI_PORTSC_CCS equ (1 << 0) +XHCI_PORTSC_PED equ (1 << 1) +XHCI_PORTSC_PR equ (1 << 4) +XHCI_PORTSC_PP equ (1 << 9) +XHCI_PORTSC_PRC equ (1 << 21) + +; ─── xHCI Extended Capability IDs ──────────────────────────────────────── +XHCI_EXT_CAP_LEGACY equ 1 +XHCI_EXT_CAP_PROTOCOL equ 2 + +; ─── USBLEGSUP register (legacy support handoff) ───────────────────────── +XHCI_LEGSUP_BIOS_OWNED equ (1 << 16) +XHCI_LEGSUP_OS_OWNED equ (1 << 24) + +%endif diff --git a/network/build.rs b/network/build.rs index ae88fd1b..7660e13a 100644 --- a/network/build.rs +++ b/network/build.rs @@ -52,6 +52,10 @@ const ASM_AHCI: &[&str] = &[ "asm/drivers/ahci/io.s", ]; +const ASM_SDHCI: &[&str] = &["asm/drivers/sdhci/init.s"]; + +const ASM_USB: &[&str] = &["asm/drivers/usb/init.s"]; + const ASM_PHY: &[&str] = &["asm/phy/mdio.s", "asm/phy/mii.s", "asm/phy/link.s"]; fn main() { @@ -114,6 +118,8 @@ fn main() { ("virtio", ASM_VIRTIO), ("intel", ASM_INTEL), ("ahci", ASM_AHCI), + ("sdhci", ASM_SDHCI), + ("usb", ASM_USB), ("phy", ASM_PHY), ]; diff --git a/network/src/boot/block_probe.rs b/network/src/boot/block_probe.rs index 696fb8eb..cbf696f5 100644 --- a/network/src/boot/block_probe.rs +++ b/network/src/boot/block_probe.rs @@ -20,9 +20,9 @@ //! ``` use crate::device::{UnifiedBlockDevice, UnifiedBlockError}; -use crate::driver::ahci::{ - AhciConfig, AhciDriver, AhciInitError, AHCI_DEVICE_IDS, INTEL_VENDOR_ID, -}; +use crate::driver::ahci::{AhciConfig, AhciDriver, AhciInitError, INTEL_VENDOR_ID}; +use crate::driver::sdhci::{SdhciConfig, SdhciDriver, SdhciInitError}; +use crate::driver::usb_msd::{UsbMsdConfig, UsbMsdDriver, UsbMsdInitError}; use crate::driver::virtio::transport::{PciModernConfig, VirtioTransport}; use crate::driver::virtio_blk::{VirtioBlkConfig, VirtioBlkDriver, VirtioBlkInitError}; use crate::pci::capability::probe_virtio_caps; @@ -69,8 +69,17 @@ const VIRTIO_BLK_DEVICE_LEGACY: u16 = 0x1001; /// VirtIO-blk device ID (modern) const VIRTIO_BLK_DEVICE_MODERN: u16 = 0x1042; -/// PCI Class code for SATA AHCI controller -const PCI_CLASS_SATA_AHCI: u32 = 0x0106; +/// PCI subclass/prog-if for SATA AHCI controller (0x06/0x01). +/// +/// We compare against `(class_code >> 8) & 0xFFFF`, which yields +/// subclass:prog_if (not class:subclass). +const PCI_CLASS_SATA_AHCI: u32 = 0x0601; +/// PCI class/subclass for SD Host Controller: 0x08/0x05. +/// +/// Prog-if differs across controller revisions, so do not pin it to one value. +const PCI_CLASS_SUBCLASS_SDHCI: u32 = 0x0805; +/// PCI subclass/prog-if for USB xHCI: 0x03/0x30. +const PCI_CLASS_USB_XHCI: u32 = 0x0330; // ═══════════════════════════════════════════════════════════════════════════ // PROBE ERRORS @@ -85,6 +94,10 @@ pub enum BlockProbeError { VirtioInitFailed, /// AHCI initialization failed AhciInitFailed, + /// SDHCI initialization failed + SdhciInitFailed, + /// USB mass-storage initialization failed + UsbMsdInitFailed, /// BAR mapping failed BarMappingFailed, /// Device not responding @@ -103,6 +116,18 @@ impl From for BlockProbeError { } } +impl From for BlockProbeError { + fn from(_: SdhciInitError) -> Self { + BlockProbeError::SdhciInitFailed + } +} + +impl From for BlockProbeError { + fn from(_: UsbMsdInitError) -> Self { + BlockProbeError::UsbMsdInitFailed + } +} + // ═══════════════════════════════════════════════════════════════════════════ // DETECTED DEVICE INFO // ═══════════════════════════════════════════════════════════════════════════ @@ -114,6 +139,10 @@ pub enum DetectedBlockDevice { VirtIO { pci_addr: PciAddr, mmio_base: u64 }, /// AHCI SATA controller Ahci(AhciInfo), + /// SDHCI controller + Sdhci(SdhciInfo), + /// USB xHCI controller candidate for USB MSD path + UsbMsd(UsbMsdInfo), } /// Information about detected AHCI controller. @@ -127,12 +156,38 @@ pub struct AhciInfo { pub device_id: u16, } +/// Information about detected SDHCI controller. +#[derive(Debug, Clone, Copy)] +pub struct SdhciInfo { + /// PCI address + pub pci_addr: PciAddr, + /// MMIO base from BAR0 + pub mmio_base: u64, + /// Device ID + pub device_id: u16, +} + +/// Information about detected USB xHCI controller. +#[derive(Debug, Clone, Copy)] +pub struct UsbMsdInfo { + /// PCI address + pub pci_addr: PciAddr, + /// MMIO base from BAR0 + pub mmio_base: u64, + /// Device ID + pub device_id: u16, +} + /// Result of successful probe and initialization. pub enum BlockProbeResult { /// VirtIO-blk driver VirtIO(VirtioBlkDriver), /// AHCI SATA driver Ahci(AhciDriver), + /// SDHCI block driver + Sdhci(SdhciDriver), + /// USB mass-storage block driver + UsbMsd(UsbMsdDriver), } // ═══════════════════════════════════════════════════════════════════════════ @@ -140,7 +195,7 @@ pub enum BlockProbeResult { // ═══════════════════════════════════════════════════════════════════════════ /// Maximum block devices we can discover in a single scan. -const MAX_BLOCK_DEVICES: usize = 8; +const MAX_BLOCK_DEVICES: usize = 32; /// Scan PCI bus for supported block devices. /// @@ -152,6 +207,16 @@ pub fn scan_for_block_device() -> Option { return Some(DetectedBlockDevice::Ahci(info)); } + // Then try SDHCI (SD card host) + if let Some(info) = find_sdhci_controller() { + return Some(DetectedBlockDevice::Sdhci(info)); + } + + // Then try USB xHCI for USB mass-storage path + if let Some(info) = find_usb_xhci_controller() { + return Some(DetectedBlockDevice::UsbMsd(info)); + } + // Fall back to VirtIO-blk (QEMU, VMs) if let Some((pci_addr, mmio_base)) = find_virtio_blk() { return Some(DetectedBlockDevice::VirtIO { @@ -165,7 +230,7 @@ pub fn scan_for_block_device() -> Option { /// Scan PCI bus for ALL supported block devices. /// -/// Returns all detected AHCI and VirtIO-blk devices (up to 8). +/// Returns all detected AHCI/SDHCI/USB/VirtIO devices (up to MAX_BLOCK_DEVICES). /// AHCI devices are listed first, then VirtIO-blk. pub fn scan_all_block_devices() -> ([Option; MAX_BLOCK_DEVICES], usize) { let mut result: [Option; MAX_BLOCK_DEVICES] = [None; MAX_BLOCK_DEVICES]; @@ -192,24 +257,12 @@ pub fn scan_all_block_devices() -> ([Option; MAX_BLOCK_DEVI } continue; } - if vendor_id != INTEL_VENDOR_ID { - if function == 0 { - let header = pci_cfg_read16(addr, offset::HEADER_TYPE) & 0x80; - if header == 0 { - break; - } - } - continue; - } let class_code = pci_cfg_read32(addr, offset::CLASS_CODE); let class = (class_code >> 8) & 0xFFFF; if class != PCI_CLASS_SATA_AHCI { continue; } let device_id = pci_cfg_read16(addr, offset::DEVICE_ID); - if !AHCI_DEVICE_IDS.contains(&device_id) { - continue; - } let bar5 = pci_cfg_read32(addr, offset::BAR5); if bar5 == 0 || (bar5 & 0x01) != 0 { continue; @@ -231,6 +284,103 @@ pub fn scan_all_block_devices() -> ([Option; MAX_BLOCK_DEVI } } + // Collect all VirtIO-blk devices. + // Collect all SDHCI controllers. + for bus in 0..=255u8 { + if count >= MAX_BLOCK_DEVICES { + break; + } + for device in 0..32u8 { + if count >= MAX_BLOCK_DEVICES { + break; + } + for function in 0..8u8 { + if count >= MAX_BLOCK_DEVICES { + break; + } + let addr = PciAddr::new(bus, device, function); + let vendor_id = pci_cfg_read16(addr, offset::VENDOR_ID); + if vendor_id == 0xFFFF { + if function == 0 { + break; + } + continue; + } + let class_code = pci_cfg_read32(addr, offset::CLASS_CODE); + let class_subclass = (class_code >> 16) & 0xFFFF; + if class_subclass != PCI_CLASS_SUBCLASS_SDHCI { + continue; + } + let device_id = pci_cfg_read16(addr, offset::DEVICE_ID); + let bar0 = pci_cfg_read32(addr, offset::BAR0); + if bar0 == 0 || (bar0 & 0x01) != 0 { + continue; + } + let is_64bit = (bar0 & 0x06) == 0x04; + let mmio_base = if is_64bit { + let bar1 = pci_cfg_read32(addr, offset::BAR1); + ((bar1 as u64) << 32) | ((bar0 & 0xFFFFFFF0) as u64) + } else { + (bar0 & 0xFFFFFFF0) as u64 + }; + result[count] = Some(DetectedBlockDevice::Sdhci(SdhciInfo { + pci_addr: addr, + mmio_base, + device_id, + })); + count += 1; + } + } + } + + // Collect all USB xHCI controllers. + for bus in 0..=255u8 { + if count >= MAX_BLOCK_DEVICES { + break; + } + for device in 0..32u8 { + if count >= MAX_BLOCK_DEVICES { + break; + } + for function in 0..8u8 { + if count >= MAX_BLOCK_DEVICES { + break; + } + let addr = PciAddr::new(bus, device, function); + let vendor_id = pci_cfg_read16(addr, offset::VENDOR_ID); + if vendor_id == 0xFFFF { + if function == 0 { + break; + } + continue; + } + let class_code = pci_cfg_read32(addr, offset::CLASS_CODE); + let class = (class_code >> 8) & 0xFFFF; + if class != PCI_CLASS_USB_XHCI { + continue; + } + let device_id = pci_cfg_read16(addr, offset::DEVICE_ID); + let bar0 = pci_cfg_read32(addr, offset::BAR0); + if bar0 == 0 || (bar0 & 0x01) != 0 { + continue; + } + let is_64bit = (bar0 & 0x06) == 0x04; + let mmio_base = if is_64bit { + let bar1 = pci_cfg_read32(addr, offset::BAR1); + ((bar1 as u64) << 32) | ((bar0 & 0xFFFFFFF0) as u64) + } else { + (bar0 & 0xFFFFFFF0) as u64 + }; + result[count] = Some(DetectedBlockDevice::UsbMsd(UsbMsdInfo { + pci_addr: addr, + mmio_base, + device_id, + })); + count += 1; + } + } + } + // Collect all VirtIO-blk devices. for bus in 0..=255u8 { if count >= MAX_BLOCK_DEVICES { @@ -303,17 +453,6 @@ pub fn find_ahci_controller() -> Option { continue; } - // Check for Intel - if vendor_id != INTEL_VENDOR_ID { - if function == 0 { - let header = pci_cfg_read16(addr, offset::HEADER_TYPE) & 0x80; - if header == 0 { - break; - } - } - continue; - } - // Check class code for SATA AHCI let class_code = pci_cfg_read32(addr, offset::CLASS_CODE); let class = (class_code >> 8) & 0xFFFF; @@ -323,11 +462,6 @@ pub fn find_ahci_controller() -> Option { let device_id = pci_cfg_read16(addr, offset::DEVICE_ID); - // Verify it's a known AHCI device - if !AHCI_DEVICE_IDS.contains(&device_id) { - continue; - } - // Read BAR5 (ABAR - AHCI Base Address Register) // AHCI uses BAR5 for MMIO let bar5 = pci_cfg_read32(addr, offset::BAR5); @@ -356,6 +490,100 @@ pub fn find_ahci_controller() -> Option { None } +/// Scan for SDHCI controller. +pub fn find_sdhci_controller() -> Option { + for bus in 0..=255u8 { + for device in 0..32u8 { + for function in 0..8u8 { + let addr = PciAddr::new(bus, device, function); + + let vendor_id = pci_cfg_read16(addr, offset::VENDOR_ID); + if vendor_id == 0xFFFF { + if function == 0 { + break; + } + continue; + } + + let class_code = pci_cfg_read32(addr, offset::CLASS_CODE); + let class_subclass = (class_code >> 16) & 0xFFFF; + if class_subclass != PCI_CLASS_SUBCLASS_SDHCI { + continue; + } + + let device_id = pci_cfg_read16(addr, offset::DEVICE_ID); + let bar0 = pci_cfg_read32(addr, offset::BAR0); + if bar0 == 0 || (bar0 & 0x01) != 0 { + continue; + } + + let is_64bit = (bar0 & 0x06) == 0x04; + let mmio_base = if is_64bit { + let bar1 = pci_cfg_read32(addr, offset::BAR1); + ((bar1 as u64) << 32) | ((bar0 & 0xFFFFFFF0) as u64) + } else { + (bar0 & 0xFFFFFFF0) as u64 + }; + + return Some(SdhciInfo { + pci_addr: addr, + mmio_base, + device_id, + }); + } + } + } + + None +} + +/// Scan for USB xHCI controller (candidate for USB mass-storage backend). +pub fn find_usb_xhci_controller() -> Option { + for bus in 0..=255u8 { + for device in 0..32u8 { + for function in 0..8u8 { + let addr = PciAddr::new(bus, device, function); + + let vendor_id = pci_cfg_read16(addr, offset::VENDOR_ID); + if vendor_id == 0xFFFF { + if function == 0 { + break; + } + continue; + } + + let class_code = pci_cfg_read32(addr, offset::CLASS_CODE); + let class = (class_code >> 8) & 0xFFFF; + if class != PCI_CLASS_USB_XHCI { + continue; + } + + let device_id = pci_cfg_read16(addr, offset::DEVICE_ID); + let bar0 = pci_cfg_read32(addr, offset::BAR0); + if bar0 == 0 || (bar0 & 0x01) != 0 { + continue; + } + + let is_64bit = (bar0 & 0x06) == 0x04; + let mmio_base = if is_64bit { + let bar1 = pci_cfg_read32(addr, offset::BAR1); + ((bar1 as u64) << 32) | ((bar0 & 0xFFFFFFF0) as u64) + } else { + (bar0 & 0xFFFFFFF0) as u64 + }; + + return Some(UsbMsdInfo { + pci_addr: addr, + mmio_base, + device_id, + }); + } + } + } + + None +} + /// Scan for VirtIO-blk device. fn find_virtio_blk() -> Option<(PciAddr, u64)> { for bus in 0..=255u8 { @@ -474,6 +702,50 @@ fn enable_pci_device(addr: PciAddr) { pci_cfg_write16(addr, offset::COMMAND, cmd | 0x06); } +/// Intel AHCI quirk: firmware sometimes leaves PCS port-enable bits unset. +/// Mirror PORTS_IMPL into PCS_6 so AHCI ports are actually visible to software. +fn intel_ahci_pcs_quirk(addr: PciAddr, abar: u64) { + const PCS_6: u8 = 0x92; + const PCS_7: u8 = 0x94; + const AHCI_CAP_OFF: u64 = 0x00; + const AHCI_PI_OFF: u64 = 0x0C; + + if pci_cfg_read16(addr, offset::VENDOR_ID) != INTEL_VENDOR_ID { + return; + } + + let mut port_map = unsafe { core::ptr::read_volatile((abar + AHCI_PI_OFF) as *const u32) }; + if port_map == 0 { + let cap = unsafe { core::ptr::read_volatile((abar + AHCI_CAP_OFF) as *const u32) }; + let n_ports = ((cap & 0x1F) + 1).min(32); + port_map = if n_ports >= 32 { + u32::MAX + } else { + (1u32 << n_ports) - 1 + }; + } + + if port_map == 0 { + return; + } + + let lo = (port_map & 0xFFFF) as u16; + if lo != 0 { + let pcs6 = pci_cfg_read16(addr, PCS_6); + if (pcs6 & lo) != lo { + pci_cfg_write16(addr, PCS_6, pcs6 | lo); + } + } + + let hi = ((port_map >> 16) & 0xFFFF) as u16; + if hi != 0 { + let pcs7 = pci_cfg_read16(addr, PCS_7); + if (pcs7 & hi) != hi { + pci_cfg_write16(addr, PCS_7, pcs7 | hi); + } + } +} + /// Probe for block device and create appropriate driver. /// /// # Safety @@ -488,6 +760,7 @@ pub unsafe fn probe_and_create_block_driver( DetectedBlockDevice::Ahci(info) => { // Enable device enable_pci_device(info.pci_addr); + intel_ahci_pcs_quirk(info.pci_addr, info.abar); // Create AHCI config let ahci_config = AhciConfig { @@ -507,6 +780,45 @@ pub unsafe fn probe_and_create_block_driver( Ok(BlockProbeResult::Ahci(driver)) } + DetectedBlockDevice::Sdhci(info) => { + enable_pci_device(info.pci_addr); + + let sdhci_config = SdhciConfig { + tsc_freq: config.tsc_freq, + dma_phys: 0, + dma_size: 0, + }; + + let driver = SdhciDriver::new(info.mmio_base, sdhci_config)?; + Ok(BlockProbeResult::Sdhci(driver)) + } + + DetectedBlockDevice::UsbMsd(info) => { + crate::serial_str("[BLK-PROBE] USB xHCI pci="); + dbg_hex8(info.pci_addr.bus); + crate::serial_str(":"); + dbg_hex8(info.pci_addr.device); + crate::serial_str("."); + dbg_hex8(info.pci_addr.function); + crate::serial_str(" bar0="); + dbg_hex64(info.mmio_base); + crate::serial_str("\n"); + + enable_pci_device(info.pci_addr); + // also disable INTx so BIOS SMI handlers can't interfere + let cmd = pci_cfg_read16(info.pci_addr, offset::COMMAND); + pci_cfg_write16(info.pci_addr, offset::COMMAND, cmd | (1 << 10)); + + let usb_config = UsbMsdConfig { + tsc_freq: config.tsc_freq, + dma_phys: 0, + dma_size: 0, + }; + + let driver = UsbMsdDriver::new(info.mmio_base, usb_config)?; + Ok(BlockProbeResult::UsbMsd(driver)) + } + DetectedBlockDevice::VirtIO { pci_addr, mmio_base, @@ -566,9 +878,13 @@ pub unsafe fn probe_unified_block_device( match probe_and_create_block_driver(config) { Ok(BlockProbeResult::VirtIO(driver)) => Ok(UnifiedBlockDevice::VirtIO(driver)), Ok(BlockProbeResult::Ahci(driver)) => Ok(UnifiedBlockDevice::Ahci(driver)), + Ok(BlockProbeResult::Sdhci(driver)) => Ok(UnifiedBlockDevice::Sdhci(driver)), + Ok(BlockProbeResult::UsbMsd(driver)) => Ok(UnifiedBlockDevice::UsbMsd(driver)), Err(BlockProbeError::NoDevice) => Err(UnifiedBlockError::NoDevice), Err(BlockProbeError::VirtioInitFailed) => Err(UnifiedBlockError::NoDevice), Err(BlockProbeError::AhciInitFailed) => Err(UnifiedBlockError::NoDevice), + Err(BlockProbeError::SdhciInitFailed) => Err(UnifiedBlockError::NoDevice), + Err(BlockProbeError::UsbMsdInitFailed) => Err(UnifiedBlockError::NoDevice), Err(_) => Err(UnifiedBlockError::NoDevice), } } @@ -587,6 +903,7 @@ pub unsafe fn create_unified_from_detected( match *detected { DetectedBlockDevice::Ahci(info) => { enable_pci_device(info.pci_addr); + intel_ahci_pcs_quirk(info.pci_addr, info.abar); let ahci_config = AhciConfig { tsc_freq: config.tsc_freq, cmd_list_cpu: config.ahci_cmd_list_cpu, @@ -598,10 +915,44 @@ pub unsafe fn create_unified_from_detected( identify_cpu: config.ahci_identify_cpu, identify_phys: config.ahci_identify_phys, }; - let driver = - AhciDriver::new(info.abar, ahci_config).map_err(|_| UnifiedBlockError::NoDevice)?; + let driver = AhciDriver::new(info.abar, ahci_config).map_err(UnifiedBlockError::AhciError)?; Ok(UnifiedBlockDevice::Ahci(driver)) } + DetectedBlockDevice::Sdhci(info) => { + enable_pci_device(info.pci_addr); + let sdhci_config = SdhciConfig { + tsc_freq: config.tsc_freq, + dma_phys: 0, + dma_size: 0, + }; + let driver = SdhciDriver::new(info.mmio_base, sdhci_config) + .map_err(UnifiedBlockError::SdhciError)?; + Ok(UnifiedBlockDevice::Sdhci(driver)) + } + DetectedBlockDevice::UsbMsd(info) => { + crate::serial_str("[BLK-PROBE] USB xHCI pci="); + dbg_hex8(info.pci_addr.bus); + crate::serial_str(":"); + dbg_hex8(info.pci_addr.device); + crate::serial_str("."); + dbg_hex8(info.pci_addr.function); + crate::serial_str(" bar0="); + dbg_hex64(info.mmio_base); + crate::serial_str("\n"); + + enable_pci_device(info.pci_addr); + let cmd = pci_cfg_read16(info.pci_addr, offset::COMMAND); + pci_cfg_write16(info.pci_addr, offset::COMMAND, cmd | (1 << 10)); + + let usb_config = UsbMsdConfig { + tsc_freq: config.tsc_freq, + dma_phys: 0, + dma_size: 0, + }; + let driver = UsbMsdDriver::new(info.mmio_base, usb_config) + .map_err(UnifiedBlockError::UsbMsdError)?; + Ok(UnifiedBlockDevice::UsbMsd(driver)) + } DetectedBlockDevice::VirtIO { pci_addr, mmio_base, @@ -807,7 +1158,7 @@ pub unsafe fn create_unified_from_detected( VirtioBlkInitError::TransportError => dbg_str("TransportError"), } dbg_str("\n"); - Err(UnifiedBlockError::NoDevice) + Err(UnifiedBlockError::VirtioError(e)) } } } else { @@ -817,13 +1168,47 @@ pub unsafe fn create_unified_from_detected( legacy_config.notify_addr = config.virtio_notify_addr; legacy_config.transport_type = 0; let driver = VirtioBlkDriver::new(mmio_base, legacy_config) - .map_err(|_| UnifiedBlockError::NoDevice)?; + .map_err(UnifiedBlockError::VirtioError)?; Ok(UnifiedBlockDevice::VirtIO(driver)) } } } } +/// Create a unified block device from a specific AHCI port on a detected controller. +/// +/// Returns `NoDevice` for non-AHCI devices. +/// +/// # Safety +/// Same as `probe_unified_block_device`. +pub unsafe fn create_unified_from_detected_ahci_port( + detected: &DetectedBlockDevice, + config: &BlockDmaConfig, + port_num: u32, +) -> Result { + match *detected { + DetectedBlockDevice::Ahci(info) => { + enable_pci_device(info.pci_addr); + intel_ahci_pcs_quirk(info.pci_addr, info.abar); + let ahci_config = AhciConfig { + tsc_freq: config.tsc_freq, + cmd_list_cpu: config.ahci_cmd_list_cpu, + cmd_list_phys: config.ahci_cmd_list_phys, + fis_cpu: config.ahci_fis_cpu, + fis_phys: config.ahci_fis_phys, + cmd_tables_cpu: config.ahci_cmd_tables_cpu, + cmd_tables_phys: config.ahci_cmd_tables_phys, + identify_cpu: config.ahci_identify_cpu, + identify_phys: config.ahci_identify_phys, + }; + let driver = AhciDriver::new_on_port(info.abar, ahci_config, port_num) + .map_err(UnifiedBlockError::AhciError)?; + Ok(UnifiedBlockDevice::Ahci(driver)) + } + _ => Err(UnifiedBlockError::NoDevice), + } +} + // ═══════════════════════════════════════════════════════════════════════════ // DEVICE TYPE DETECTION // ═══════════════════════════════════════════════════════════════════════════ diff --git a/network/src/device/mod.rs b/network/src/device/mod.rs index a3d515cd..ce7248a5 100644 --- a/network/src/device/mod.rs +++ b/network/src/device/mod.rs @@ -235,6 +235,8 @@ impl NetworkDevice for UnifiedNetDevice { use crate::driver::ahci::{AhciDriver, AhciInitError}; use crate::driver::block_traits::{BlockCompletion, BlockDeviceInfo, BlockDriver, BlockError}; +use crate::driver::sdhci::{SdhciDriver, SdhciInitError}; +use crate::driver::usb_msd::{UsbMsdDriver, UsbMsdInitError}; use crate::driver::virtio_blk::{VirtioBlkDriver, VirtioBlkInitError}; /// Unified block device that works with both VirtIO-blk and AHCI. @@ -264,6 +266,10 @@ pub enum UnifiedBlockDevice { VirtIO(VirtioBlkDriver), /// AHCI SATA driver (real hardware - ThinkPad T450s) Ahci(AhciDriver), + /// SDHCI block driver (SD/MMC host path) + Sdhci(SdhciDriver), + /// USB mass-storage block driver + UsbMsd(UsbMsdDriver), } /// Errors from unified block device operations. @@ -275,6 +281,10 @@ pub enum UnifiedBlockError { VirtioError(VirtioBlkInitError), /// AHCI initialization failed AhciError(AhciInitError), + /// SDHCI initialization failed + SdhciError(SdhciInitError), + /// USB MSD initialization failed + UsbMsdError(UsbMsdInitError), } impl From for UnifiedBlockError { @@ -289,12 +299,26 @@ impl From for UnifiedBlockError { } } +impl From for UnifiedBlockError { + fn from(e: SdhciInitError) -> Self { + UnifiedBlockError::SdhciError(e) + } +} + +impl From for UnifiedBlockError { + fn from(e: UsbMsdInitError) -> Self { + UnifiedBlockError::UsbMsdError(e) + } +} + impl UnifiedBlockDevice { /// Get which driver type is being used. pub fn driver_type(&self) -> &'static str { match self { UnifiedBlockDevice::VirtIO(_) => "VirtIO-blk", UnifiedBlockDevice::Ahci(_) => "AHCI SATA", + UnifiedBlockDevice::Sdhci(_) => "SDHCI", + UnifiedBlockDevice::UsbMsd(_) => "USB-MSD", } } @@ -303,6 +327,8 @@ impl UnifiedBlockDevice { match self { UnifiedBlockDevice::VirtIO(_) => true, // VirtIO always ready after init UnifiedBlockDevice::Ahci(d) => d.link_up(), + UnifiedBlockDevice::Sdhci(_) => true, + UnifiedBlockDevice::UsbMsd(_) => true, } } } @@ -312,6 +338,8 @@ impl BlockDriver for UnifiedBlockDevice { match self { UnifiedBlockDevice::VirtIO(d) => d.info(), UnifiedBlockDevice::Ahci(d) => d.info(), + UnifiedBlockDevice::Sdhci(d) => d.info(), + UnifiedBlockDevice::UsbMsd(d) => d.info(), } } @@ -319,6 +347,8 @@ impl BlockDriver for UnifiedBlockDevice { match self { UnifiedBlockDevice::VirtIO(d) => d.can_submit(), UnifiedBlockDevice::Ahci(d) => d.can_submit(), + UnifiedBlockDevice::Sdhci(d) => d.can_submit(), + UnifiedBlockDevice::UsbMsd(d) => d.can_submit(), } } @@ -336,6 +366,12 @@ impl BlockDriver for UnifiedBlockDevice { UnifiedBlockDevice::Ahci(d) => { d.submit_read(sector, buffer_phys, num_sectors, request_id) } + UnifiedBlockDevice::Sdhci(d) => { + d.submit_read(sector, buffer_phys, num_sectors, request_id) + } + UnifiedBlockDevice::UsbMsd(d) => { + d.submit_read(sector, buffer_phys, num_sectors, request_id) + } } } @@ -353,6 +389,12 @@ impl BlockDriver for UnifiedBlockDevice { UnifiedBlockDevice::Ahci(d) => { d.submit_write(sector, buffer_phys, num_sectors, request_id) } + UnifiedBlockDevice::Sdhci(d) => { + d.submit_write(sector, buffer_phys, num_sectors, request_id) + } + UnifiedBlockDevice::UsbMsd(d) => { + d.submit_write(sector, buffer_phys, num_sectors, request_id) + } } } @@ -360,6 +402,8 @@ impl BlockDriver for UnifiedBlockDevice { match self { UnifiedBlockDevice::VirtIO(d) => d.poll_completion(), UnifiedBlockDevice::Ahci(d) => d.poll_completion(), + UnifiedBlockDevice::Sdhci(d) => d.poll_completion(), + UnifiedBlockDevice::UsbMsd(d) => d.poll_completion(), } } @@ -367,6 +411,8 @@ impl BlockDriver for UnifiedBlockDevice { match self { UnifiedBlockDevice::VirtIO(d) => d.notify(), UnifiedBlockDevice::Ahci(d) => d.notify(), + UnifiedBlockDevice::Sdhci(d) => d.notify(), + UnifiedBlockDevice::UsbMsd(d) => d.notify(), } } @@ -374,6 +420,8 @@ impl BlockDriver for UnifiedBlockDevice { match self { UnifiedBlockDevice::VirtIO(d) => d.flush(), UnifiedBlockDevice::Ahci(d) => d.flush(), + UnifiedBlockDevice::Sdhci(d) => d.flush(), + UnifiedBlockDevice::UsbMsd(d) => d.flush(), } } } diff --git a/network/src/driver/ahci/mod.rs b/network/src/driver/ahci/mod.rs index 70becfed..e5b8003a 100644 --- a/network/src/driver/ahci/mod.rs +++ b/network/src/driver/ahci/mod.rs @@ -36,6 +36,7 @@ pub mod regs; use crate::driver::block_traits::{ BlockCompletion, BlockDeviceInfo, BlockDriver, BlockDriverInit, BlockError, }; +use crate::boot::read_tsc_raw; use core::ptr; // Re-exports @@ -143,6 +144,7 @@ pub const AHCI_DEVICE_WPT_LP: u16 = 0x9C83; /// Other supported AHCI device IDs pub const AHCI_DEVICE_IDS: &[u16] = &[ + 0x2922, // ICH9 AHCI (QEMU ich9-ahci) 0x9C83, // Wildcat Point-LP (T450s) 0x9C03, // Lynx Point-LP 0x8C02, // 8 Series/C220 @@ -237,6 +239,28 @@ impl AhciDriver { /// - `abar` must be valid AHCI MMIO address (from BAR5) /// - `config` must contain valid DMA memory pointers/addresses pub unsafe fn new(abar: u64, config: AhciConfig) -> Result { + Self::new_inner(abar, config, None) + } + + /// Create and initialize AHCI driver on a specific port. + /// + /// # Safety + /// - `abar` must be valid AHCI MMIO address (from BAR5) + /// - `config` must contain valid DMA memory pointers/addresses + /// - `port_num` must be an AHCI port index (0..31) + pub unsafe fn new_on_port( + abar: u64, + config: AhciConfig, + port_num: u32, + ) -> Result { + Self::new_inner(abar, config, Some(port_num)) + } + + unsafe fn new_inner( + abar: u64, + config: AhciConfig, + forced_port: Option, + ) -> Result { // Validate config if config.cmd_list_cpu.is_null() || config.fis_cpu.is_null() { return Err(AhciInitError::InvalidConfig); @@ -245,8 +269,11 @@ impl AhciDriver { let tsc_freq = config.tsc_freq; // ═══════════════════════════════════════════════════════════════════ - // STEP 1: Enable AHCI mode + // STEP 1: Reset HBA to known state, then enable AHCI mode // ═══════════════════════════════════════════════════════════════════ + if asm_ahci_hba_reset(abar, tsc_freq) != 0 { + return Err(AhciInitError::ResetFailed); + } asm_ahci_enable(abar); // ═══════════════════════════════════════════════════════════════════ @@ -268,95 +295,174 @@ impl AhciDriver { asm_ahci_disable_interrupts(abar); // ═══════════════════════════════════════════════════════════════════ - // STEP 4: Find first port with an attached ATA device + // STEP 4: Build candidate port list (strict ATA first, then fallback) // ═══════════════════════════════════════════════════════════════════ - let mut active_port: Option = None; - - for port in 0..32u32 { - if (ports_impl & (1 << port)) == 0 { - continue; // Port not implemented + let mut strict_ports = [u32::MAX; 32]; + let mut strict_count = 0usize; + let mut fallback_ports = [u32::MAX; 32]; + let mut fallback_count = 0usize; + + if let Some(port) = forced_port { + if port >= 32 { + return Err(AhciInitError::NoDeviceFound); + } + let mut impl_mask = ports_impl; + if impl_mask == 0 { + let n_ports = asm_ahci_get_num_ports(abar).min(32); + impl_mask = if n_ports >= 32 { + u32::MAX + } else { + (1u32 << n_ports) - 1 + }; + } + if (impl_mask & (1 << port)) == 0 { + return Err(AhciInitError::NoDeviceFound); + } + strict_ports[0] = port; + strict_count = 1; + } else { + let mut impl_mask = ports_impl; + if impl_mask == 0 { + let n_ports = asm_ahci_get_num_ports(abar).min(32); + impl_mask = if n_ports >= 32 { + u32::MAX + } else { + (1u32 << n_ports) - 1 + }; } - let det = asm_ahci_port_detect(abar, port); - if det != DET_PHY_COMM { - continue; // No device or not ready + let settle_ticks = (tsc_freq / 1000).saturating_mul(200); + for port in 0..32u32 { + if (impl_mask & (1 << port)) == 0 { + continue; + } + + let start = read_tsc_raw(); + let mut strict = false; + let mut fallback = false; + + while read_tsc_raw().wrapping_sub(start) < settle_ticks { + let det = asm_ahci_port_detect(abar, port); + let sig = asm_ahci_port_read_sig(abar, port); + let ssts = asm_ahci_port_read_ssts(abar, port); + let ipm = (ssts >> 8) & 0x0F; + + if (det == DET_PHY_COMM || det == DET_PRESENT) && sig == SIG_ATA { + strict = true; + break; + } + + if det != DET_NONE || sig != 0 || ipm != 0 { + fallback = true; + } + + core::hint::spin_loop(); + } + + if strict && strict_count < strict_ports.len() { + strict_ports[strict_count] = port; + strict_count += 1; + } else if fallback && fallback_count < fallback_ports.len() { + fallback_ports[fallback_count] = port; + fallback_count += 1; + } } + } + + if strict_count == 0 && fallback_count == 0 { + return Err(AhciInitError::NoDeviceFound); + } - // Check signature - let sig = asm_ahci_port_read_sig(abar, port); - if sig == SIG_ATA { - active_port = Some(port); + let mut candidate_ports = [u32::MAX; 32]; + let mut candidate_count = 0usize; + for i in 0..strict_count { + candidate_ports[candidate_count] = strict_ports[i]; + candidate_count += 1; + } + for i in 0..fallback_count { + if candidate_count >= candidate_ports.len() { break; } + candidate_ports[candidate_count] = fallback_ports[i]; + candidate_count += 1; } - let port_num = active_port.ok_or(AhciInitError::NoDeviceFound)?; - // ═══════════════════════════════════════════════════════════════════ - // STEP 5: Stop port and configure DMA structures + // STEP 5-7: Try candidates until one IDENTIFY succeeds // ═══════════════════════════════════════════════════════════════════ - let stop_result = asm_ahci_port_stop(abar, port_num, tsc_freq); - if stop_result != 0 { - return Err(AhciInitError::PortStopTimeout); - } + let mut last_err = AhciInitError::NoDeviceFound; - // Setup command list and FIS receive buffer - asm_ahci_port_setup(abar, port_num, config.cmd_list_phys, config.fis_phys); + for i in 0..candidate_count { + let port_num = candidate_ports[i]; - // Clear any pending errors - asm_ahci_port_clear_errors(abar, port_num); - asm_ahci_port_disable_interrupts(abar, port_num); + let stop_result = asm_ahci_port_stop(abar, port_num, tsc_freq); + if stop_result != 0 { + last_err = AhciInitError::PortStopTimeout; + continue; + } - // ═══════════════════════════════════════════════════════════════════ - // STEP 6: Start port - // ═══════════════════════════════════════════════════════════════════ - asm_ahci_port_start(abar, port_num); + asm_ahci_port_setup(abar, port_num, config.cmd_list_phys, config.fis_phys); + asm_ahci_port_clear_errors(abar, port_num); + asm_ahci_port_disable_interrupts(abar, port_num); + asm_ahci_port_start(abar, port_num); + + // Give link/signature a brief settle window after engine start. + let link_ticks = (tsc_freq / 1000).saturating_mul(50); + let link_start = read_tsc_raw(); + while read_tsc_raw().wrapping_sub(link_start) < link_ticks { + let det = asm_ahci_port_detect(abar, port_num); + let sig = asm_ahci_port_read_sig(abar, port_num); + if det != DET_NONE && (sig == SIG_ATA || sig == 0) { + break; + } + core::hint::spin_loop(); + } - // ═══════════════════════════════════════════════════════════════════ - // STEP 7: Issue IDENTIFY DEVICE to get capacity - // ═══════════════════════════════════════════════════════════════════ - let identify_result = asm_ahci_identify_device( - abar, - port_num, - config.identify_phys, - config.cmd_list_cpu as u64, - config.cmd_tables_cpu as u64, - config.cmd_tables_phys, - tsc_freq, - ); - - if identify_result != 0 { - return Err(AhciInitError::IdentifyFailed); - } + let identify_result = asm_ahci_identify_device( + abar, + port_num, + config.identify_phys, + config.cmd_list_cpu as u64, + config.cmd_tables_cpu as u64, + config.cmd_tables_phys, + tsc_freq, + ); - // Parse IDENTIFY data - let total_sectors = asm_ahci_get_identify_capacity(config.identify_cpu as u64); - let sector_size = asm_ahci_get_identify_sector_size(config.identify_cpu as u64); + if identify_result != 0 { + last_err = AhciInitError::IdentifyFailed; + continue; + } - let info = BlockDeviceInfo { - total_sectors, - sector_size, - max_sectors_per_request: 256, // Conservative for DMA - read_only: false, - }; + let total_sectors = asm_ahci_get_identify_capacity(config.identify_cpu as u64); + let sector_size = asm_ahci_get_identify_sector_size(config.identify_cpu as u64); + + let info = BlockDeviceInfo { + total_sectors, + sector_size, + max_sectors_per_request: 256, + read_only: false, + }; + + return Ok(Self { + abar, + port_num, + tsc_freq, + info, + num_slots, + in_flight: [InFlightRequest::default(); MAX_CMD_SLOTS], + next_slot: 0, + cmd_list_cpu: config.cmd_list_cpu, + cmd_list_phys: config.cmd_list_phys, + fis_cpu: config.fis_cpu, + fis_phys: config.fis_phys, + cmd_tables_cpu: config.cmd_tables_cpu, + cmd_tables_phys: config.cmd_tables_phys, + identify_cpu: config.identify_cpu, + identify_phys: config.identify_phys, + }); + } - Ok(Self { - abar, - port_num, - tsc_freq, - info, - num_slots, - in_flight: [InFlightRequest::default(); MAX_CMD_SLOTS], - next_slot: 0, - cmd_list_cpu: config.cmd_list_cpu, - cmd_list_phys: config.cmd_list_phys, - fis_cpu: config.fis_cpu, - fis_phys: config.fis_phys, - cmd_tables_cpu: config.cmd_tables_cpu, - cmd_tables_phys: config.cmd_tables_phys, - identify_cpu: config.identify_cpu, - identify_phys: config.identify_phys, - }) + Err(last_err) } /// Get command header pointer for a slot diff --git a/network/src/driver/intel/init.rs b/network/src/driver/intel/init.rs index 99f5b222..0b785c9a 100644 --- a/network/src/driver/intel/init.rs +++ b/network/src/driver/intel/init.rs @@ -287,9 +287,8 @@ pub unsafe fn init_e1000e( write32(mmio_base + regs::TDT as u64, 0); let _ = read32(mmio_base + regs::STATUS as u64); // flush - // Clear RAR[0] - don't trust firmware MAC, will reprogram later - write32(mmio_base + regs::RAL0 as u64, 0); - write32(mmio_base + regs::RAH0 as u64, 0); + // Keep RAR[0] intact until Phase 8 MAC read. + // Some parts expose MAC via RAL/RAH + AV and EEPROM path can be fragile. // Clear loopback mode explicitly let rctl = read32(mmio_base + regs::RCTL as u64); diff --git a/network/src/driver/intel/mod.rs b/network/src/driver/intel/mod.rs index e259f002..c522224c 100644 --- a/network/src/driver/intel/mod.rs +++ b/network/src/driver/intel/mod.rs @@ -30,8 +30,10 @@ pub const INTEL_VENDOR_ID: u16 = 0x8086; /// This list covers the most common Intel GbE controllers found in /// ThinkPads and QEMU emulation. pub const E1000E_DEVICE_IDS: &[u16] = &[ + 0x100E, // 82540EM (QEMU e1000) 0x1502, // I218-LM (ThinkPad T450s, T440s, X240, etc.) 0x1503, // I218-V (Consumer variant) + 0x155A, // I218-LM (ThinkPad T450s variant) 0x10D3, // 82574L (QEMU e1000e emulation) 0x10EA, // 82577LM 0x10EB, // 82577LC @@ -55,11 +57,11 @@ pub fn is_supported_device(vendor_id: u16, device_id: u16) -> bool { vendor_id == INTEL_VENDOR_ID && E1000E_DEVICE_IDS.contains(&device_id) } -/// PCI class code for Ethernet controller. -pub const PCI_CLASS_NETWORK_ETHERNET: u32 = 0x020000; +/// PCI class code for Ethernet controller (class:subclass at bits 31:16). +pub const PCI_CLASS_NETWORK_ETHERNET: u32 = 0x0200_0000; -/// Mask for PCI class code (ignore revision). -pub const PCI_CLASS_MASK: u32 = 0xFFFF00; +/// Mask for PCI class/subclass (ignore prog-if and revision). +pub const PCI_CLASS_MASK: u32 = 0xFFFF_0000; use crate::pci::config::{offset, pci_cfg_read16, pci_cfg_read32, PciAddr}; diff --git a/network/src/driver/mod.rs b/network/src/driver/mod.rs index 04929ba5..7efbcbdc 100644 --- a/network/src/driver/mod.rs +++ b/network/src/driver/mod.rs @@ -24,9 +24,11 @@ pub mod ahci; pub mod block_io_adapter; pub mod block_traits; pub mod intel; +pub mod sdhci; pub mod traits; pub mod unified; pub mod unified_block_io; +pub mod usb_msd; pub mod virtio; pub mod virtio_blk; // Future: @@ -51,6 +53,8 @@ pub use virtio_blk::{VirtioBlkConfig, VirtioBlkDriver, VirtioBlkInitError}; // Re-exports - Block (AHCI/SATA for real hardware) pub use ahci::{AhciConfig, AhciDriver, AhciInitError}; +pub use sdhci::{SdhciConfig, SdhciDriver, SdhciInitError}; +pub use usb_msd::{UsbMsdConfig, UsbMsdDriver, UsbMsdInitError}; // Re-exports - BlockIo adapters (for filesystem compatibility) pub use block_io_adapter::{BlockIoError, VirtioBlkBlockIo}; diff --git a/network/src/driver/sdhci/mod.rs b/network/src/driver/sdhci/mod.rs new file mode 100644 index 00000000..32bcf797 --- /dev/null +++ b/network/src/driver/sdhci/mod.rs @@ -0,0 +1,354 @@ +//! SDHCI block device driver scaffold. +//! +//! This module defines the BlockDriver-facing contract for the upcoming +//! ASM-backed SDHCI implementation. Hardware-touching logic will be added in +//! assembly primitives; Rust keeps orchestration/state/error surfaces stable. + +use crate::driver::block_traits::{ + BlockCompletion, BlockDeviceInfo, BlockDriver, BlockDriverInit, BlockError, +}; +use crate::asm::core::mmio; +use crate::asm::core::tsc; + +extern "win64" { + fn asm_sdhci_read_caps(mmio_base: u64) -> u32; + fn asm_sdhci_card_present(mmio_base: u64) -> u32; + fn asm_sdhci_controller_reset(mmio_base: u64, tsc_freq: u64) -> u32; + fn asm_sdhci_basic_power_clock(mmio_base: u64) -> u32; + fn asm_sdhci_read_block_pio(mmio_base: u64, lba: u64, dst: u64, tsc_freq: u64) -> u32; +} + +/// PCI base class / subclass / prog-if for SD Host Controller. +pub const PCI_CLASS_SDHCI: u32 = 0x080501; + +/// SDHCI configuration. +#[derive(Debug, Clone)] +pub struct SdhciConfig { + /// TSC frequency for timeout calculations. + pub tsc_freq: u64, + /// Optional DMA bounce buffer base (physical). + pub dma_phys: u64, + /// Optional DMA bounce buffer size. + pub dma_size: usize, +} + +/// SDHCI initialization errors. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SdhciInitError { + InvalidConfig, + ControllerResetFailed, + NoCardPresent, + VoltageSwitchFailed, + ClockSetupFailed, + CommandTimeout, + DataTimeout, + IoError, + NotImplemented, +} + +impl core::fmt::Display for SdhciInitError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::InvalidConfig => write!(f, "Invalid SDHCI configuration"), + Self::ControllerResetFailed => write!(f, "SDHCI controller reset failed"), + Self::NoCardPresent => write!(f, "No SD card present"), + Self::VoltageSwitchFailed => write!(f, "SDHCI voltage switch failed"), + Self::ClockSetupFailed => write!(f, "SDHCI clock setup failed"), + Self::CommandTimeout => write!(f, "SDHCI command timeout"), + Self::DataTimeout => write!(f, "SDHCI data timeout"), + Self::IoError => write!(f, "SDHCI I/O error"), + Self::NotImplemented => write!(f, "SDHCI driver not implemented yet"), + } + } +} + +/// SDHCI driver state (scaffold). +pub struct SdhciDriver { + mmio_base: u64, + tsc_freq: u64, + _caps: u32, + info: BlockDeviceInfo, + high_capacity: bool, + rca: u16, + last_completion: Option, +} + +const REG_ARGUMENT: u64 = 0x08; +const REG_COMMAND: u64 = 0x0E; +const REG_RESPONSE0: u64 = 0x10; +const REG_PRESENT_STATE: u64 = 0x24; +const REG_INT_STATUS: u64 = 0x30; + +const PRESENT_CMD_INHIBIT: u32 = 1 << 0; +const PRESENT_DAT_INHIBIT: u32 = 1 << 1; + +const INT_CMD_COMPLETE: u32 = 1 << 0; +const INT_ERROR: u32 = 1 << 15; + +const CMD_RESP_NONE: u16 = 0x00; +const CMD_RESP_LONG: u16 = 0x01; +const CMD_RESP_SHORT: u16 = 0x02; +const CMD_RESP_SHORT_BUSY: u16 = 0x03; +const CMD_CRC: u16 = 0x08; +const CMD_INDEX: u16 = 0x10; + +const ACMD41_OCR: u32 = 0x40FF_8000; + +impl SdhciDriver { + #[inline(always)] + unsafe fn reg32(&self, off: u64) -> u64 { + self.mmio_base + off + } + + fn timeout_ticks(&self, ms: u64) -> u64 { + self.tsc_freq.saturating_mul(ms) / 1000 + } + + unsafe fn wait_not_inhibit(&self, timeout_ms: u64) -> Result<(), SdhciInitError> { + let start = tsc::read_tsc(); + let timeout = self.timeout_ticks(timeout_ms); + loop { + let ps = mmio::read32(self.reg32(REG_PRESENT_STATE)); + if (ps & (PRESENT_CMD_INHIBIT | PRESENT_DAT_INHIBIT)) == 0 { + return Ok(()); + } + if tsc::read_tsc().wrapping_sub(start) > timeout { + return Err(SdhciInitError::CommandTimeout); + } + core::hint::spin_loop(); + } + } + + unsafe fn clear_ints(&self) { + mmio::write32(self.reg32(REG_INT_STATUS), 0xFFFF_FFFF); + } + + unsafe fn send_cmd( + &self, + index: u8, + arg: u32, + flags: u16, + timeout_ms: u64, + ) -> Result { + self.wait_not_inhibit(timeout_ms)?; + self.clear_ints(); + + mmio::write32(self.reg32(REG_ARGUMENT), arg); + let cmd = ((index as u16) << 8) | flags; + mmio::write16(self.reg32(REG_COMMAND), cmd); + + let start = tsc::read_tsc(); + let timeout = self.timeout_ticks(timeout_ms); + loop { + let st = mmio::read32(self.reg32(REG_INT_STATUS)); + if (st & INT_ERROR) != 0 { + self.clear_ints(); + return Err(SdhciInitError::IoError); + } + if (st & INT_CMD_COMPLETE) != 0 { + mmio::write32(self.reg32(REG_INT_STATUS), INT_CMD_COMPLETE); + return Ok(mmio::read32(self.reg32(REG_RESPONSE0))); + } + if tsc::read_tsc().wrapping_sub(start) > timeout { + self.clear_ints(); + return Err(SdhciInitError::CommandTimeout); + } + core::hint::spin_loop(); + } + } + + unsafe fn init_card(&mut self) -> Result<(), SdhciInitError> { + // CMD0: idle + let _ = self.send_cmd(0, 0, CMD_RESP_NONE, 100)?; + + // CMD8: interface condition; if it fails, assume legacy SDSC. + let mut supports_v2 = false; + if let Ok(r7) = self.send_cmd(8, 0x0000_01AA, CMD_RESP_SHORT | CMD_CRC | CMD_INDEX, 100) + { + supports_v2 = (r7 & 0xFFF) == 0x1AA; + } + + // ACMD41 loop until card powers up. + let mut ocr: u32; + let hcs = if supports_v2 { 1u32 << 30 } else { 0 }; + let start = tsc::read_tsc(); + let timeout = self.timeout_ticks(2000); + loop { + let _ = self.send_cmd(55, 0, CMD_RESP_SHORT | CMD_CRC | CMD_INDEX, 100)?; + ocr = self.send_cmd(41, ACMD41_OCR | hcs, CMD_RESP_SHORT, 100)?; + if (ocr & (1u32 << 31)) != 0 { + break; + } + if tsc::read_tsc().wrapping_sub(start) > timeout { + return Err(SdhciInitError::CommandTimeout); + } + } + self.high_capacity = (ocr & (1u32 << 30)) != 0; + + // CMD2: CID + let _ = self.send_cmd(2, 0, CMD_RESP_LONG | CMD_CRC, 200)?; + + // CMD3: RCA assign + let rca_resp = self.send_cmd(3, 0, CMD_RESP_SHORT | CMD_CRC | CMD_INDEX, 200)?; + self.rca = (rca_resp >> 16) as u16; + if self.rca == 0 { + return Err(SdhciInitError::IoError); + } + + // CMD7: select card + let _ = self.send_cmd( + 7, + (self.rca as u32) << 16, + CMD_RESP_SHORT_BUSY | CMD_CRC | CMD_INDEX, + 200, + )?; + + // CMD16: set block length for SDSC cards. + if !self.high_capacity { + let _ = self.send_cmd(16, 512, CMD_RESP_SHORT | CMD_CRC | CMD_INDEX, 100)?; + } + + Ok(()) + } + + /// Create a new SDHCI driver instance. + /// + /// Phase-1 scaffold returns NotImplemented until ASM primitives land. + pub unsafe fn new(mmio_base: u64, config: SdhciConfig) -> Result { + if mmio_base == 0 || config.tsc_freq == 0 { + return Err(SdhciInitError::InvalidConfig); + } + + if asm_sdhci_controller_reset(mmio_base, config.tsc_freq) != 0 { + return Err(SdhciInitError::ControllerResetFailed); + } + + if asm_sdhci_basic_power_clock(mmio_base) != 0 { + return Err(SdhciInitError::ClockSetupFailed); + } + + if asm_sdhci_card_present(mmio_base) == 0 { + return Err(SdhciInitError::NoCardPresent); + } + + let caps = asm_sdhci_read_caps(mmio_base); + + let mut this = Self { + mmio_base, + tsc_freq: config.tsc_freq, + _caps: caps, + info: BlockDeviceInfo { + total_sectors: u32::MAX as u64, + sector_size: 512, + max_sectors_per_request: 128, + read_only: true, + }, + high_capacity: true, + rca: 0, + last_completion: None, + }; + + this.init_card()?; + let _ = config; + Ok(this) + } +} + +impl BlockDriverInit for SdhciDriver { + type Error = SdhciInitError; + type Config = SdhciConfig; + + fn supported_vendors() -> &'static [u16] { + &[] + } + + fn supported_devices() -> &'static [u16] { + &[] + } + + unsafe fn create(mmio_base: u64, config: Self::Config) -> Result { + Self::new(mmio_base, config) + } +} + +impl BlockDriver for SdhciDriver { + fn info(&self) -> BlockDeviceInfo { + self.info + } + + fn can_submit(&self) -> bool { + self.last_completion.is_none() + } + + fn submit_read( + &mut self, + sector: u64, + buffer_phys: u64, + num_sectors: u32, + request_id: u32, + ) -> Result<(), BlockError> { + if self.last_completion.is_some() { + return Err(BlockError::QueueFull); + } + if num_sectors == 0 || num_sectors > self.info.max_sectors_per_request { + return Err(BlockError::RequestTooLarge); + } + if buffer_phys == 0 { + return Err(BlockError::InvalidSector); + } + + let end_sector = sector + .checked_add(num_sectors as u64) + .ok_or(BlockError::InvalidSector)?; + if end_sector > self.info.total_sectors { + return Err(BlockError::InvalidSector); + } + + let mut curr_sector = sector; + let mut curr_dst = buffer_phys; + for _ in 0..num_sectors { + let arg = if self.high_capacity { + curr_sector + } else { + curr_sector + .checked_mul(self.info.sector_size as u64) + .ok_or(BlockError::InvalidSector)? + }; + let rc = unsafe { + asm_sdhci_read_block_pio(self.mmio_base, arg, curr_dst, self.tsc_freq) + }; + if rc != 0 { + return Err(match rc { + 1 | 2 | 3 | 4 => BlockError::Timeout, + _ => BlockError::IoError, + }); + } + + curr_sector = curr_sector.wrapping_add(1); + curr_dst = curr_dst.wrapping_add(self.info.sector_size as u64); + } + + self.last_completion = Some(BlockCompletion { + request_id, + status: 0, + bytes_transferred: num_sectors * self.info.sector_size, + }); + Ok(()) + } + + fn submit_write( + &mut self, + _sector: u64, + _buffer_phys: u64, + _num_sectors: u32, + _request_id: u32, + ) -> Result<(), BlockError> { + Err(BlockError::Unsupported) + } + + fn poll_completion(&mut self) -> Option { + self.last_completion.take() + } + + fn notify(&mut self) {} +} diff --git a/network/src/driver/usb_msd/mod.rs b/network/src/driver/usb_msd/mod.rs new file mode 100644 index 00000000..cc97b902 --- /dev/null +++ b/network/src/driver/usb_msd/mod.rs @@ -0,0 +1,1810 @@ +//! USB mass-storage block driver — xHCI host + BOT transport + SCSI read. +//! +//! Finds the first USB mass storage device on the xHCI bus, initialises it +//! via Bulk-Only Transport, and exposes SCSI READ(10) through BlockDriver. +//! Read-only; write returns Unsupported. + +use crate::asm::core::mmio; +use crate::asm::core::tsc; +use crate::driver::block_traits::{ + BlockCompletion, BlockDeviceInfo, BlockDriver, BlockDriverInit, BlockError, +}; + +// ═══════════════════════════════════════════════════════════════════════════ +// ASM externs +// ═══════════════════════════════════════════════════════════════════════════ + +extern "win64" { + /// Reads CAPLENGTH + HCIVERSION. 0 = dead controller. + /// Low byte = CAPLENGTH, bits 31:16 = HCIVERSION. + fn asm_usb_host_probe(mmio_base: u64) -> u32; + /// Soft restart: stop + start (NO HCRST). Preserves UEFI port state. 0 = ok, 1/2 = timeout. + fn asm_xhci_controller_soft_restart(op_base: u64, tsc_freq: u64) -> u32; + /// Walk extended caps, claim ownership from BIOS/SMM. 0 = ok. + fn asm_xhci_bios_handoff(mmio_base: u64, hccparams1: u64, tsc_freq: u64) -> u32; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// xHCI register offsets (operational, from op_base) +// ═══════════════════════════════════════════════════════════════════════════ + +const OP_USBCMD: u64 = 0x00; +const OP_USBSTS: u64 = 0x04; +const OP_CRCR: u64 = 0x18; +const OP_DCBAAP: u64 = 0x30; +const OP_CONFIG: u64 = 0x38; + +const PORT_REG_BASE: u64 = 0x400; +const PORT_REG_STRIDE: u64 = 0x10; + +// interrupter 0 offsets from rt_base +const IR0_IMAN: u64 = 0x20; +const IR0_IMOD: u64 = 0x24; +const IR0_ERSTSZ: u64 = 0x28; +const IR0_ERSTBA: u64 = 0x30; +const IR0_ERDP: u64 = 0x38; + +// USBCMD / USBSTS bits +const CMD_RS: u32 = 1 << 0; +const CMD_INTE: u32 = 1 << 2; +const STS_HCH: u32 = 1 << 0; + +// PORTSC bits +const PORTSC_CCS: u32 = 1 << 0; +const PORTSC_PED: u32 = 1 << 1; +const PORTSC_PR: u32 = 1 << 4; +const PORTSC_PLS_MASK: u32 = 0xF << 5; +const PORTSC_PP: u32 = 1 << 9; +const PORTSC_LWS: u32 = 1 << 16; +const PORTSC_WRC: u32 = 1 << 19; +const PORTSC_PRC: u32 = 1 << 21; +const PORTSC_CAS: u32 = 1 << 24; +const PORTSC_WPR: u32 = 1 << 31; +// RW1C mask: bits 17-23 must be written 0 to preserve, 1 to clear +const PORTSC_RW1C: u32 = 0x00FE_0000; +const PORTSC_SPEED_SHIFT: u32 = 10; +const PLS_U0: u32 = 0x0 << 5; +const PLS_U3: u32 = 0x3 << 5; +const PLS_RECOVERY: u32 = 0x8 << 5; +const PLS_RESUME: u32 = 0xF << 5; +const PLS_INACTIVE: u32 = 0x6 << 5; +const PLS_POLLING: u32 = 0x7 << 5; +const PLS_COMPLIANCE: u32 = 0xA << 5; + +// ═══════════════════════════════════════════════════════════════════════════ +// TRB types (pre-shifted to bits 15:10) +// ═══════════════════════════════════════════════════════════════════════════ + +const TRB_NORMAL: u32 = 1 << 10; +const TRB_SETUP: u32 = 2 << 10; +const TRB_DATA: u32 = 3 << 10; +const TRB_STATUS: u32 = 4 << 10; +const TRB_LINK: u32 = 6 << 10; +const TRB_ENABLE_SLOT: u32 = 9 << 10; +const TRB_DISABLE_SLOT: u32 = 10 << 10; +const TRB_ADDRESS_DEV: u32 = 11 << 10; +const TRB_CONFIGURE_EP: u32 = 12 << 10; +const TRB_XFER_EVENT: u32 = 32 << 10; +const TRB_CMD_COMPLETE: u32 = 33 << 10; + +// TRB control bits +const TRB_TC: u32 = 1 << 1; +const TRB_ISP: u32 = 1 << 2; +const TRB_IOC: u32 = 1 << 5; +const TRB_IDT: u32 = 1 << 6; +const TRB_DIR_IN: u32 = 1 << 16; +const TRB_TRT_IN: u32 = 3 << 16; + +const TRB_TYPE_MASK: u32 = 0x3F << 10; + +// ═══════════════════════════════════════════════════════════════════════════ +// USB / SCSI / BOT constants +// ═══════════════════════════════════════════════════════════════════════════ + +const USB_CLASS_MASS_STORAGE: u8 = 0x08; +const USB_CLASS_HUB: u8 = 0x09; +const USB_SUBCLASS_SCSI: u8 = 0x06; +const USB_PROTOCOL_BOT: u8 = 0x50; + +const CBW_SIG: u32 = 0x4342_5355; +const CSW_SIG: u32 = 0x5342_5355; +const SCSI_TEST_UNIT_READY: u8 = 0x00; +const SCSI_READ_CAPACITY_10: u8 = 0x25; +const SCSI_READ_10: u8 = 0x28; + +// ═══════════════════════════════════════════════════════════════════════════ +// DMA region layout — all offsets 64-byte aligned inside a 64KB-aligned buf +// ═══════════════════════════════════════════════════════════════════════════ + +const DMA_SIZE: usize = 0x48000; +const CMD_RING_LEN: u8 = 32; +const EVT_RING_LEN: u8 = 32; +const XFER_RING_LEN: u8 = 16; + +const OFF_DCBAA: usize = 0x0000; // 2KB +const OFF_CMD_RING: usize = 0x1000; // 512B +const OFF_EVT_RING: usize = 0x1200; // 512B +const OFF_ERST: usize = 0x1400; // 16B +const OFF_OUT_CTX: usize = 0x2000; // 2KB (supports CSZ=1) +const OFF_IN_CTX: usize = 0x3000; // 2.5KB +const OFF_XFER_EP0: usize = 0x4000; // 256B +const OFF_XFER_BOUT: usize = 0x4100; // 256B +const OFF_XFER_BIN: usize = 0x4200; // 256B +const OFF_CBW: usize = 0x4400; // 64B +const OFF_CSW: usize = 0x4440; // 64B +const OFF_DESC: usize = 0x4480; // 256B +const OFF_DATA: usize = 0x5000; // 4KB sector bounce buffer (one page, no 64KB boundary crossing) +const DATA_BUF_SIZE: usize = 4096; +const OFF_SCRATCH_ARR: usize = 0x7000; // 64B +const OFF_SCRATCH_PG: usize = 0x8000; // scratchpad pages begin here +const MAX_SCRATCH: usize = 64; + +#[repr(C, align(4096))] +struct XhciDma([u8; DMA_SIZE]); + +static mut XHCI_DMA: XhciDma = XhciDma([0u8; DMA_SIZE]); + +// ═══════════════════════════════════════════════════════════════════════════ +// Public types (kept wire-compatible with scaffold) +// ═══════════════════════════════════════════════════════════════════════════ + +/// USB mass-storage configuration. +#[derive(Debug, Clone)] +pub struct UsbMsdConfig { + /// TSC frequency for timeout calculations. + pub tsc_freq: u64, + /// Optional DMA bounce buffer base (physical). + pub dma_phys: u64, + /// Optional DMA bounce buffer size. + pub dma_size: usize, +} + +/// USB mass-storage init errors. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum UsbMsdInitError { + InvalidConfig, + ControllerInitFailed, + ControllerProbeFailed, + ControllerResetFailed, + ControllerScratchpadUnsupported, + ControllerStartFailed, + HubUnsupported, + PortResetFailed, + PortResetTimeout, + PortResetHotCmdTimeout, + PortResetHotSettleTimeout, + PortResetWarmTimeout, + PortResetNoLink, + EnableSlotFailed, + AddressDeviceFailed, + DeviceDescriptorFailed, + ConfigDescriptorFailed, + MassStorageProtocolUnsupported, + NoBotMassStorageInterface, + ActivePortsNoConnectedDevice, + SetConfigurationFailed, + ConfigureEndpointsFailed, + DeviceEnumerationFailed, + TransportInitFailed, + NoMedia, + CommandTimeout, + IoError, + NotImplemented, +} + +impl core::fmt::Display for UsbMsdInitError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::InvalidConfig => write!(f, "Invalid USB MSD configuration"), + Self::ControllerInitFailed => write!(f, "USB xHCI controller init failed"), + Self::ControllerProbeFailed => write!(f, "USB xHCI probe failed (dead/invalid BAR)"), + Self::ControllerResetFailed => write!(f, "USB xHCI reset failed (halt/reset/cnr timeout)"), + Self::ControllerScratchpadUnsupported => { + write!(f, "USB xHCI requires too many scratchpad buffers") + } + Self::ControllerStartFailed => write!(f, "USB xHCI start failed (HCH never cleared)"), + Self::HubUnsupported => write!(f, "USB hub detected but hub traversal is not implemented"), + Self::PortResetFailed => write!(f, "USB port reset failed"), + Self::PortResetTimeout => write!(f, "USB port reset timed out"), + Self::PortResetHotCmdTimeout => write!(f, "USB hot reset command did not complete"), + Self::PortResetHotSettleTimeout => write!(f, "USB hot reset completed but link did not settle"), + Self::PortResetWarmTimeout => write!(f, "USB warm reset did not complete"), + Self::PortResetNoLink => write!(f, "USB reset completed but link never became usable"), + Self::EnableSlotFailed => write!(f, "USB enable-slot command failed"), + Self::AddressDeviceFailed => write!(f, "USB address-device command failed"), + Self::DeviceDescriptorFailed => write!(f, "USB device descriptor fetch failed"), + Self::ConfigDescriptorFailed => write!(f, "USB config descriptor fetch failed"), + Self::MassStorageProtocolUnsupported => { + write!(f, "USB mass-storage present but protocol is not BOT") + } + Self::NoBotMassStorageInterface => { + write!(f, "USB config has no BOT mass-storage interface") + } + Self::ActivePortsNoConnectedDevice => { + write!(f, "USB root ports active but no connected device detected") + } + Self::SetConfigurationFailed => write!(f, "USB SET_CONFIGURATION failed"), + Self::ConfigureEndpointsFailed => write!(f, "USB Configure Endpoint command failed"), + Self::DeviceEnumerationFailed => write!(f, "USB device enumeration failed"), + Self::TransportInitFailed => write!(f, "USB BOT transport init failed"), + Self::NoMedia => write!(f, "No USB mass-storage device found"), + Self::CommandTimeout => write!(f, "USB command timeout"), + Self::IoError => write!(f, "USB I/O error"), + Self::NotImplemented => write!(f, "USB mass-storage driver not implemented yet"), + } + } +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Transfer ring identifier +// ═══════════════════════════════════════════════════════════════════════════ + +#[derive(Clone, Copy)] +enum Ring { + Ep0, + BulkOut, + BulkIn, +} + +enum ConfigParse { + MassStorage { + cfg_val: u8, + ep_in: u8, + ep_out: u8, + mp_in: u16, + mp_out: u16, + }, + Hub, + MassStorageUnsupported, + None, +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Driver state +// ═══════════════════════════════════════════════════════════════════════════ + +pub struct UsbMsdDriver { + // xHCI register bases + op_base: u64, + rt_base: u64, + db_base: u64, + tsc_freq: u64, + max_ports: u8, + ctx_size: u8, // 32 or 64 + + dma_base: u64, + + // ring producer/consumer state + cmd_enq: u8, + cmd_cycle: u8, + evt_deq: u8, + evt_cycle: u8, + ep0_enq: u8, + ep0_cycle: u8, + bout_enq: u8, + bout_cycle: u8, + bin_enq: u8, + bin_cycle: u8, + + // USB device + slot_id: u8, + dci_bulk_in: u8, + dci_bulk_out: u8, + + info: BlockDeviceInfo, + last_completion: Option, + bot_tag: u32, +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Volatile helpers (DMA RAM, NOT mmio) +// ═══════════════════════════════════════════════════════════════════════════ + +#[inline(always)] +unsafe fn vr32(a: u64) -> u32 { + core::ptr::read_volatile(a as *const u32) +} +#[inline(always)] +unsafe fn vw32(a: u64, v: u32) { + core::ptr::write_volatile(a as *mut u32, v); +} +#[inline(always)] +unsafe fn vw64(a: u64, v: u64) { + vw32(a, v as u32); + vw32(a + 4, (v >> 32) as u32); +} + +/// Write a TRB at `base + idx*16`. Control word (with cycle bit) written last. +#[inline(always)] +unsafe fn write_trb(base: u64, idx: usize, param: u64, status: u32, ctrl: u32) { + let a = base + (idx as u64) * 16; + vw32(a, param as u32); + vw32(a + 4, (param >> 32) as u32); + vw32(a + 8, status); + vw32(a + 12, ctrl); +} + +// busy-wait delay using TSC. ms=0 is a no-op. +#[inline(always)] +unsafe fn tsc_delay(tsc_freq: u64, ms: u64) { + if ms == 0 { + return; + } + let ticks = tsc_freq / 1000 * ms; + let start = tsc::read_tsc(); + while tsc::read_tsc().wrapping_sub(start) < ticks { + core::hint::spin_loop(); + } +} + +// Preserve only RO+RWS bits on PORTSC writes (Linux xhci_port_state_to_neutral style). +#[inline(always)] +fn portsc_neutral(state: u32) -> u32 { + const PORT_RO: u32 = (1 << 0) | (1 << 3) | (0xF << 10) | (1 << 30); + const PORT_RWS: u32 = (0xF << 5) | (1 << 9) | (0x3 << 14) | (0x7 << 25); + state & (PORT_RO | PORT_RWS) +} + +#[inline(always)] +unsafe fn portsc_write(addr: u64, current: u32, set_bits: u32, clear_bits: u32) { + let mut v = portsc_neutral(current); + v &= !clear_bits; + v |= set_bits; + mmio::write32(addr, v); +} + +#[inline(always)] +fn warm_reset_needed(ps: u32) -> bool { + let pls = ps & PORTSC_PLS_MASK; + let speed = (ps >> PORTSC_SPEED_SHIFT) & 0xF; + (ps & PORTSC_CAS) != 0 + || pls == PLS_POLLING + || pls == PLS_COMPLIANCE + || pls == PLS_INACTIVE + || speed >= 4 +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Implementation +// ═══════════════════════════════════════════════════════════════════════════ + +// serial and framebuffer diagnostics via console hook +fn dbg(s: &str) { + morpheus_hwinit::serial::puts(s); +} +fn dbg_u32(v: u32) { + let mut digits = [0u8; 10]; + let mut n = v; + let mut len = 0usize; + if n == 0 { + digits[0] = b'0'; + len = 1; + } else { + while n > 0 { + digits[len] = b'0' + (n % 10) as u8; + len += 1; + n /= 10; + } + digits[..len].reverse(); + } + if let Ok(s) = core::str::from_utf8(&digits[..len]) { + morpheus_hwinit::serial::puts(s); + } +} +fn dbg_hex32(v: u32) { + const HEX: &[u8; 16] = b"0123456789abcdef"; + morpheus_hwinit::serial::puts("0x"); + for i in (0..8).rev() { + let byte = HEX[((v >> (i * 4)) & 0xF) as usize]; + morpheus_hwinit::serial::putc(byte); + } +} +fn dbg_hex64(v: u64) { + const HEX: &[u8; 16] = b"0123456789abcdef"; + crate::serial_str("0x"); + for i in (0..16).rev() { + crate::serial_byte(HEX[((v >> (i * 4)) & 0xF) as usize]); + } +} + +impl UsbMsdDriver { + #[inline(always)] + fn err_label(e: UsbMsdInitError) -> &'static str { + match e { + UsbMsdInitError::InvalidConfig => "invalid-config", + UsbMsdInitError::ControllerInitFailed => "controller-init", + UsbMsdInitError::ControllerProbeFailed => "controller-probe", + UsbMsdInitError::ControllerResetFailed => "controller-restart", + UsbMsdInitError::ControllerScratchpadUnsupported => "scratchpad-unsupported", + UsbMsdInitError::ControllerStartFailed => "controller-start", + UsbMsdInitError::HubUnsupported => "hub-unsupported", + UsbMsdInitError::PortResetFailed => "port-reset", + UsbMsdInitError::PortResetTimeout => "port-timeout", + UsbMsdInitError::PortResetHotCmdTimeout => "port-hotcmd-timeout", + UsbMsdInitError::PortResetHotSettleTimeout => "port-settle-timeout", + UsbMsdInitError::PortResetWarmTimeout => "port-warm-timeout", + UsbMsdInitError::PortResetNoLink => "no-link", + UsbMsdInitError::EnableSlotFailed => "enable-slot", + UsbMsdInitError::AddressDeviceFailed => "address-device", + UsbMsdInitError::DeviceDescriptorFailed => "device-desc", + UsbMsdInitError::ConfigDescriptorFailed => "config-desc", + UsbMsdInitError::MassStorageProtocolUnsupported => "msd-protocol", + UsbMsdInitError::NoBotMassStorageInterface => "no-bot-intf", + UsbMsdInitError::ActivePortsNoConnectedDevice => "active-no-device", + UsbMsdInitError::SetConfigurationFailed => "set-config", + UsbMsdInitError::ConfigureEndpointsFailed => "config-eps", + UsbMsdInitError::DeviceEnumerationFailed => "enumeration", + UsbMsdInitError::TransportInitFailed => "transport-init", + UsbMsdInitError::NoMedia => "no-media", + UsbMsdInitError::CommandTimeout => "cmd-timeout", + UsbMsdInitError::IoError => "io", + UsbMsdInitError::NotImplemented => "not-implemented", + } + } + + #[inline(always)] + fn log_port_attempt(&self, port: u8, portsc: u32, err: UsbMsdInitError) { + dbg("[USB] p"); + dbg_u32(port as u32); + dbg(" ccs="); + dbg_u32(if (portsc & PORTSC_CCS) != 0 { 1 } else { 0 }); + dbg(" ped="); + dbg_u32(if (portsc & PORTSC_PED) != 0 { 1 } else { 0 }); + dbg(" speed="); + dbg_u32((portsc >> PORTSC_SPEED_SHIFT) & 0xF); + dbg(" -> "); + dbg(Self::err_label(err)); + dbg("\n"); + } + + #[inline(always)] + fn log_port_success(&self, port: u8, portsc: u32) { + dbg("[USB] p"); + dbg_u32(port as u32); + dbg(" ccs="); + dbg_u32(if (portsc & PORTSC_CCS) != 0 { 1 } else { 0 }); + dbg(" ped="); + dbg_u32(if (portsc & PORTSC_PED) != 0 { 1 } else { 0 }); + dbg(" speed="); + dbg_u32((portsc >> PORTSC_SPEED_SHIFT) & 0xF); + dbg(" -> ok\n"); + } + + // ─── public entry point ────────────────────────────────────────────── + + /// Initialise xHCI controller, enumerate first USB mass-storage device, + /// run SCSI READ CAPACITY. Returns a ready-to-read driver or an error. + pub unsafe fn new(mmio_base: u64, config: UsbMsdConfig) -> Result { + dbg("[USB] new() mmio="); + dbg_hex64(mmio_base); + dbg(" tsc="); + dbg_hex64(config.tsc_freq); + dbg("\n"); + + if mmio_base == 0 || config.tsc_freq == 0 { + dbg("[USB] FAIL: invalid config\n"); + return Err(UsbMsdInitError::InvalidConfig); + } + + let mut drv = Self::init_controller(mmio_base, config.tsc_freq)?; + dbg("[USB] controller init OK, enumerating...\n"); + drv.enumerate_and_configure()?; + dbg("[USB] enumeration OK, SCSI init...\n"); + drv.scsi_init()?; + dbg("[USB] SCSI init OK, driver ready\n"); + Ok(drv) + } + + // ─── phase 1: controller bring-up (brutal edition) ───────────────── + + unsafe fn init_controller(mmio_base: u64, tsc_freq: u64) -> Result { + // probe: low byte = CAPLENGTH, high half = HCIVERSION. 0 = dead. + let probe = asm_usb_host_probe(mmio_base); + dbg("[USB] probe="); + dbg_hex32(probe); + dbg("\n"); + if probe == 0 { + dbg("[USB] FAIL: probe returned 0 (dead BAR or bad CAPLENGTH/HCIVERSION)\n"); + // read raw dword for diagnostics + let raw = mmio::read32(mmio_base); + dbg("[USB] raw mmio_base+0 = "); + dbg_hex32(raw); + dbg("\n"); + return Err(UsbMsdInitError::ControllerProbeFailed); + } + let cap_len = (probe & 0xFF) as u64; + let hci_ver = probe >> 16; + dbg("[USB] CAPLENGTH="); + dbg_hex32(cap_len as u32); + dbg(" HCIVERSION="); + dbg_hex32(hci_ver); + dbg("\n"); + let op_base = mmio_base + cap_len; + + let hcsparams1 = mmio::read32(mmio_base + 0x04); + let hcsparams2 = mmio::read32(mmio_base + 0x08); + let hccparams1 = mmio::read32(mmio_base + 0x10); + let db_off = mmio::read32(mmio_base + 0x14) & !0x03; + let rts_off = mmio::read32(mmio_base + 0x18) & !0x1F; + + let max_slots = (hcsparams1 & 0xFF) as u8; + let max_ports = ((hcsparams1 >> 24) & 0xFF) as u8; + let ctx_size: u8 = if hccparams1 & (1 << 2) != 0 { 64 } else { 32 }; + let scratch_hi = ((hcsparams2 >> 21) & 0x1F) as u16; + let scratch_lo = ((hcsparams2 >> 27) & 0x1F) as u16; + let n_scratch = ((scratch_hi << 5) | scratch_lo) as usize; + + let rt_base = mmio_base + rts_off as u64; + let db_base = mmio_base + db_off as u64; + + dbg("[USB] slots="); + crate::serial_u32(max_slots as u32); + dbg(" ports="); + crate::serial_u32(max_ports as u32); + dbg(" ctxsz="); + crate::serial_u32(ctx_size as u32); + dbg(" scratch="); + crate::serial_u32(n_scratch as u32); + dbg(" hccparams1="); + dbg_hex32(hccparams1); + dbg("\n"); + dbg("[USB] op_base="); + dbg_hex64(op_base); + dbg(" rt_base="); + dbg_hex64(rt_base); + dbg(" db_base="); + dbg_hex64(db_base); + dbg("\n"); + + // read USBSTS before we touch anything + let sts_before = mmio::read32(op_base + OP_USBSTS); + dbg("[USB] USBSTS before reset="); + dbg_hex32(sts_before); + dbg("\n"); + + // static DMA buffer, identity-mapped in UEFI + let dma_base = core::ptr::addr_of_mut!(XHCI_DMA) as u64; + core::ptr::write_bytes(dma_base as *mut u8, 0, DMA_SIZE); + dbg("[USB] DMA base="); + dbg_hex64(dma_base); + dbg("\n"); + + // ── legacy owner handoff (SMM/firmware) ── + dbg("[USB] xHCI legacy-owner handoff...\n"); + let handoff_rc = asm_xhci_bios_handoff(mmio_base, hccparams1 as u64, tsc_freq); + tsc_delay(tsc_freq, 10); + if handoff_rc == 0 { + dbg("[USB] xHCI legacy-owner handoff done\n"); + } else { + dbg("[USB] xHCI legacy-owner handoff timed out; continuing without force takeover\n"); + } + + // If firmware already trained links, don't touch controller run state. + let mut linked_ports = 0u32; + let mut active_ports = 0u32; + for p in 0..max_ports { + let ps = mmio::read32(op_base + PORT_REG_BASE + (p as u64) * PORT_REG_STRIDE); + let speed = (ps >> PORTSC_SPEED_SHIFT) & 0xF; + if ps != 0 && ps != u32::MAX { + active_ports += 1; + } + if (ps & (PORTSC_CCS | PORTSC_PED)) != 0 || speed != 0 { + linked_ports += 1; + } + } + dbg_u32(linked_ports); + dbg(" active="); + dbg_u32(active_ports); + dbg(" total="); + dbg_u32(max_ports as u32); + dbg("\n"); + + if linked_ports == 0 { + // All dead after handoff: one soft restart may wake stale HC run state. + let rc_soft = asm_xhci_controller_soft_restart(op_base, tsc_freq); + if rc_soft != 0 { + dbg("[USB] FAIL: soft restart failed rc="); + crate::serial_u32(rc_soft); + dbg("\n"); + return Err(UsbMsdInitError::ControllerResetFailed); + } + } + + // post-reset settle + tsc_delay(tsc_freq, 50); + + // MaxSlotsEn + mmio::write32(op_base + OP_CONFIG, max_slots.min(16) as u32); + + // scratchpad buffers (controller refuses to start without them) + if n_scratch > MAX_SCRATCH { + dbg("[USB] FAIL: too many scratchpad bufs ("); + crate::serial_u32(n_scratch as u32); + dbg(" > "); + crate::serial_u32(MAX_SCRATCH as u32); + dbg(")\n"); + return Err(UsbMsdInitError::ControllerScratchpadUnsupported); + } + if n_scratch > 0 { + let arr = dma_base + OFF_SCRATCH_ARR as u64; + for i in 0..n_scratch { + let pg = dma_base + (OFF_SCRATCH_PG + i * 4096) as u64; + vw64(arr + (i as u64) * 8, pg); + } + // DCBAA[0] = scratchpad buffer array + vw64(dma_base + OFF_DCBAA as u64, arr); + } + + // DCBAAP + let dcbaa = dma_base + OFF_DCBAA as u64; + mmio::write32(op_base + OP_DCBAAP, dcbaa as u32); + mmio::write32(op_base + OP_DCBAAP + 4, (dcbaa >> 32) as u32); + + // command ring → CRCR (RCS = 1) + let cr = dma_base + OFF_CMD_RING as u64; + mmio::write32(op_base + OP_CRCR, (cr as u32 & !0x3F) | 1); + mmio::write32(op_base + OP_CRCR + 4, (cr >> 32) as u32); + + // event ring: ERST entry, then registers + let er = dma_base + OFF_EVT_RING as u64; + let erst = dma_base + OFF_ERST as u64; + vw32(erst, er as u32); + vw32(erst + 4, (er >> 32) as u32); + vw32(erst + 8, EVT_RING_LEN as u32); + vw32(erst + 12, 0); + + mmio::write32(rt_base + IR0_ERSTSZ, 1); + // ERDP must be written before ERSTBA per spec + mmio::write32(rt_base + IR0_ERDP, (er as u32 & !0xF) | 0x08); + mmio::write32(rt_base + IR0_ERDP + 4, (er >> 32) as u32); + mmio::write32(rt_base + IR0_ERSTBA, erst as u32); + mmio::write32(rt_base + IR0_ERSTBA + 4, (erst >> 32) as u32); + + // IMAN.IE — some controllers gate event generation on this + let iman = mmio::read32(rt_base + IR0_IMAN); + mmio::write32(rt_base + IR0_IMAN, iman | 0x02); + + // start controller: RS=1, INTE=1 + dbg("[USB] starting controller (RS=1)...\n"); + mmio::write32(op_base + OP_USBCMD, CMD_RS | CMD_INTE); + + // wait HCH to clear (controller running) — 1 second + let start = tsc::read_tsc(); + let timeout = tsc_freq; + loop { + let sts = mmio::read32(op_base + OP_USBSTS); + if sts & STS_HCH == 0 { + dbg("[USB] controller running (HCH cleared)\n"); + break; + } + if tsc::read_tsc().wrapping_sub(start) > timeout { + dbg("[USB] FAIL: HCH never cleared. USBSTS="); + dbg_hex32(sts); + dbg(" USBCMD="); + dbg_hex32(mmio::read32(op_base + OP_USBCMD)); + dbg("\n"); + return Err(UsbMsdInitError::ControllerStartFailed); + } + core::hint::spin_loop(); + } + + // ensure PP is set and clear sticky change bits so we see fresh state. + for p in 0..max_ports { + let addr = op_base + PORT_REG_BASE + (p as u64) * PORT_REG_STRIDE; + let ps = mmio::read32(addr); + if ps & PORTSC_PP == 0 { + portsc_write(addr, ps, PORTSC_PP, 0); + } + // clear any stale change bits from before HCRST + let clr = ps & PORTSC_RW1C; + if clr != 0 { + portsc_write(addr, ps, clr, 0); + } + } + // give ports 200ms to surface CCS after controller restart + tsc_delay(tsc_freq, 200); + + // dump port status for diagnostics + dbg("[USB] port status after init:\n"); + for p in 0..max_ports { + let addr = op_base + PORT_REG_BASE + (p as u64) * PORT_REG_STRIDE; + let ps = mmio::read32(addr); + if ps & PORTSC_CCS != 0 { + dbg("[USB] port "); + crate::serial_u32(p as u32); + dbg(" PORTSC="); + dbg_hex32(ps); + dbg(" CCS=1"); + if ps & PORTSC_PED != 0 { dbg(" PED"); } + if ps & PORTSC_PP != 0 { dbg(" PP"); } + dbg(" speed="); + crate::serial_u32((ps >> PORTSC_SPEED_SHIFT) & 0xF); + dbg("\n"); + } + } + + Ok(Self { + op_base, + rt_base, + db_base, + tsc_freq, + max_ports, + ctx_size, + dma_base, + cmd_enq: 0, + cmd_cycle: 1, + evt_deq: 0, + evt_cycle: 1, + ep0_enq: 0, + ep0_cycle: 1, + bout_enq: 0, + bout_cycle: 1, + bin_enq: 0, + bin_cycle: 1, + slot_id: 0, + dci_bulk_in: 0, + dci_bulk_out: 0, + info: BlockDeviceInfo { + total_sectors: 0, + sector_size: 512, + max_sectors_per_request: 8, + read_only: true, + }, + last_completion: None, + bot_tag: 1, + }) + } + + // ─── phase 2: USB device enumeration ───────────────────────────────── + + unsafe fn enumerate_and_configure(&mut self) -> Result<(), UsbMsdInitError> { + // Fast-fail signature seen on some firmware handoffs: MMIO active but no link state. + let mut pre_active = 0u32; + let mut pre_linked = 0u32; + for port in 0..self.max_ports { + let ps = mmio::read32(self.portsc(port)); + if ps != 0 && ps != u32::MAX { + pre_active += 1; + } + let speed = (ps >> PORTSC_SPEED_SHIFT) & 0xF; + if (ps & (PORTSC_CCS | PORTSC_PED)) != 0 || speed != 0 { + pre_linked += 1; + } + } + if pre_active >= 8 && pre_linked == 0 { + dbg("[USB] active ports with zero link state; skip xHCI enum\n"); + return Err(UsbMsdInitError::ActivePortsNoConnectedDevice); + } + + let mut saw_connected = false; + let mut saw_activity = false; + let mut last_err: Option = None; + let mut kicked_mask: u64 = 0; + let mut speculative_mask: u64 = 0; + let scan_start = tsc::read_tsc(); + // Don't let one flaky port starve the rest; rounds stay quick, scan window is longer. + let scan_timeout = self.tsc_freq * 8; + + while tsc::read_tsc().wrapping_sub(scan_start) < scan_timeout { + let mut round_connected = false; + + for port in 0..self.max_ports { + let mut ps = mmio::read32(self.portsc(port)); + if ps != 0 && ps != u32::MAX { + saw_activity = true; + } + + // Some controllers/hubs don't present CCS until after a short settle. + if ps & PORTSC_CCS == 0 && ps != 0 && ps != u32::MAX { + let bit = if port < 64 { 1u64 << port } else { 0 }; + if bit == 0 || (kicked_mask & bit) == 0 { + self.kick_port_detect(port); + if bit != 0 { + kicked_mask |= bit; + } + ps = mmio::read32(self.portsc(port)); + } + } + + let speed_hint = (ps >> PORTSC_SPEED_SHIFT) & 0xF; + let candidate = (ps & PORTSC_CCS != 0) + || (ps & PORTSC_PED != 0) + || speed_hint != 0; + if !candidate { + // Some controllers don't expose CCS/speed promptly; try once anyway. + if ps != 0 && ps != u32::MAX { + let bit = if port < 64 { 1u64 << port } else { 0 }; + if bit == 0 || (speculative_mask & bit) == 0 { + if bit != 0 { + speculative_mask |= bit; + } + tsc_delay(self.tsc_freq, 20); + self.reset_transfer_state(); + match self.try_port_with_mode(port, false) { + Ok(()) => { + self.log_port_success(port, ps); + return Ok(()); + } + Err(e) => { + self.log_port_attempt(port, ps, e); + last_err = Some(e); + } + } + } + } + continue; + } + + if ps & PORTSC_CCS != 0 { + saw_connected = true; + } + round_connected = true; + + // hubs and card readers can appear late; let them settle. + tsc_delay(self.tsc_freq, 80); + self.reset_transfer_state(); + + match self.try_port(port) { + Ok(()) => { + self.log_port_success(port, ps); + return Ok(()); + } + Err(e) => { + self.log_port_attempt(port, ps, e); + last_err = Some(e); + } + } + } + + if round_connected { + tsc_delay(self.tsc_freq, 80); + } else { + tsc_delay(self.tsc_freq, 100); + } + } + + if saw_connected { + Err(last_err.unwrap_or(UsbMsdInitError::NoBotMassStorageInterface)) + } else if saw_activity { + Err(last_err.unwrap_or(UsbMsdInitError::ActivePortsNoConnectedDevice)) + } else { + Err(UsbMsdInitError::NoMedia) + } + } + + // Detection nudge only; do not reset here because firmware may own link state. + unsafe fn kick_port_detect(&self, port: u8) { + let _ = port; + // Give hardware a breath to update CCS/speed without us poking PR. + tsc_delay(self.tsc_freq, 8); + } + + unsafe fn try_port(&mut self, port: u8) -> Result<(), UsbMsdInitError> { + self.try_port_with_mode(port, false) + } + + unsafe fn try_port_with_mode(&mut self, port: u8, force_reset: bool) -> Result<(), UsbMsdInitError> { + let ps0 = mmio::read32(self.portsc(port)); + let mut speed = ((ps0 >> PORTSC_SPEED_SHIFT) & 0xF) as u8; + let connected = (ps0 & PORTSC_CCS) != 0; + let linked = connected || (ps0 & PORTSC_PED) != 0 || speed != 0; + + if !linked && !force_reset { + return Err(UsbMsdInitError::PortResetNoLink); + } + + // CCS=1 but PED=0 means USB 2.0 connect without bus reset. + // Controller reset wiped UEFI state so port reset is mandatory, not destructive. + let enabled = (ps0 & PORTSC_PED) != 0; + let needs_reset = connected && !enabled; + + if needs_reset || force_reset { + match self.port_reset(port, true) { + Ok(s) => speed = s, + Err(e) => { + // fallback: if port still claims a connected+typed link, keep going + let ps1 = mmio::read32(self.portsc(port)); + let speed1 = ((ps1 >> PORTSC_SPEED_SHIFT) & 0xF) as u8; + if (ps1 & PORTSC_CCS) != 0 && speed1 != 0 { + speed = speed1; + } else { + return Err(match e { + UsbMsdInitError::PortResetTimeout => UsbMsdInitError::PortResetTimeout, + UsbMsdInitError::PortResetHotCmdTimeout => { + UsbMsdInitError::PortResetHotCmdTimeout + } + UsbMsdInitError::PortResetHotSettleTimeout => { + UsbMsdInitError::PortResetHotSettleTimeout + } + UsbMsdInitError::PortResetWarmTimeout => { + UsbMsdInitError::PortResetWarmTimeout + } + UsbMsdInitError::PortResetNoLink => UsbMsdInitError::PortResetNoLink, + _ => UsbMsdInitError::PortResetFailed, + }); + } + } + } + } else if connected && speed == 0 { + // PED=1 but speed missing (USB 3.0 quirk) — poll briefly. + let start = tsc::read_tsc(); + let timeout = self.tsc_freq / 10; + loop { + let ps = mmio::read32(self.portsc(port)); + let s = ((ps >> PORTSC_SPEED_SHIFT) & 0xF) as u8; + if s != 0 { speed = s; break; } + if tsc::read_tsc().wrapping_sub(start) > timeout { break; } + core::hint::spin_loop(); + } + if speed == 0 { speed = 1; } + } + + let slot = self + .cmd_enable_slot() + .map_err(|_| UsbMsdInitError::EnableSlotFailed)?; + self.slot_id = slot; + + // wire output context into DCBAA + let out_ctx = self.dma_base + OFF_OUT_CTX as u64; + vw64( + self.dma_base + OFF_DCBAA as u64 + (slot as u64) * 8, + out_ctx, + ); + + self.cmd_address_device(port, speed) + .map_err(|_| UsbMsdInitError::AddressDeviceFailed)?; + + // GET_DESCRIPTOR device (18 bytes) + let dev_desc = self + .control_in(0x80, 0x06, 0x0100, 0, 18) + .map_err(|_| UsbMsdInitError::DeviceDescriptorFailed)?; + + // If root-port device is a hub, we'd need hub class traversal for downstream SD. + if dev_desc.len() >= 6 && dev_desc[4] == USB_CLASS_HUB { + return Err(UsbMsdInitError::HubUnsupported); + } + + // GET_DESCRIPTOR configuration (up to 255 bytes) + let _cfg = self + .control_in(0x80, 0x06, 0x0200, 0, 255) + .map_err(|_| UsbMsdInitError::ConfigDescriptorFailed)?; + + // parse for mass-storage interface + bulk endpoints + let (cfg_val, ep_in, ep_out, mpkt_in, mpkt_out) = match self.parse_config_desc() { + ConfigParse::MassStorage { + cfg_val, + ep_in, + ep_out, + mp_in, + mp_out, + } => (cfg_val, ep_in, ep_out, mp_in, mp_out), + ConfigParse::Hub => return Err(UsbMsdInitError::HubUnsupported), + ConfigParse::MassStorageUnsupported => { + return Err(UsbMsdInitError::MassStorageProtocolUnsupported) + } + ConfigParse::None => return Err(UsbMsdInitError::NoBotMassStorageInterface), + }; + + // SET_CONFIGURATION + self.control_nodata(0x00, 0x09, cfg_val as u16, 0) + .map_err(|_| UsbMsdInitError::SetConfigurationFailed)?; + + // compute DCIs + let dci_in = ((ep_in & 0x0F) * 2) + ((ep_in >> 7) & 1); + let dci_out = ((ep_out & 0x0F) * 2) + ((ep_out >> 7) & 1); + self.dci_bulk_in = dci_in; + self.dci_bulk_out = dci_out; + + // configure endpoint command + self.cmd_configure_eps(dci_in, dci_out, mpkt_in, mpkt_out) + .map_err(|_| UsbMsdInitError::ConfigureEndpointsFailed)?; + + Ok(()) + } + + // ─── phase 3: SCSI bring-up ────────────────────────────────────────── + + unsafe fn scsi_init(&mut self) -> Result<(), UsbMsdInitError> { + // TEST UNIT READY — absorb unit-attention condition, ignore errors + let _ = self.bot_command(&[SCSI_TEST_UNIT_READY, 0, 0, 0, 0, 0], 0, false); + + // READ CAPACITY(10) → 8 bytes response + let cap_cmd = [SCSI_READ_CAPACITY_10, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + self.bot_command(&cap_cmd, 8, true)?; + + let data = self.dma_base + OFF_DATA as u64; + let last_lba = u32::from_be(vr32(data)) as u64; + let blk_size = u32::from_be(vr32(data + 4)); + + self.info.total_sectors = last_lba + 1; + self.info.sector_size = if blk_size == 0 { 512 } else { blk_size }; + // max request limited by bounce buffer (4KB page, no 64KB boundary crossing) + let max_req = DATA_BUF_SIZE as u32 / self.info.sector_size; + self.info.max_sectors_per_request = max_req.max(1); + + Ok(()) + } + + // ─── port management ───────────────────────────────────────────────── + + #[inline(always)] + fn portsc(&self, port: u8) -> u64 { + self.op_base + PORT_REG_BASE + (port as u64) * PORT_REG_STRIDE + } + + unsafe fn port_reset(&self, port: u8, force: bool) -> Result { + let addr = self.portsc(port); + let mut stage_timeout: Option = None; + + // ensure port power before any reset attempt + let ps = mmio::read32(addr); + + if ps & PORTSC_PP == 0 { + portsc_write(addr, ps, PORTSC_PP, 0); + tsc_delay(self.tsc_freq, 10); + } + + // No link indicators at all: don't burn reset time on a probably empty port. + let pre = mmio::read32(addr); + let pre_speed = ((pre >> PORTSC_SPEED_SHIFT) & 0xF) as u8; + if !force && (pre & PORTSC_CCS) == 0 && (pre & PORTSC_PED) == 0 && pre_speed == 0 { + dbg("[USB] no link indicators, returning PortResetNoLink\n"); + return Err(UsbMsdInitError::PortResetNoLink); + } + + // Linux-style nudge: if SS link is in U3/Recovery/Resume, strobe link state to U0 first. + let pls = pre & PORTSC_PLS_MASK; + if pre_speed >= 4 && (pls == PLS_U3 || pls == PLS_RECOVERY || pls == PLS_RESUME) { + portsc_write(addr, pre, PORTSC_LWS | PLS_U0, PORTSC_PLS_MASK); + let start_u0 = tsc::read_tsc(); + let timeout_u0 = self.tsc_freq / 10; + loop { + let p = mmio::read32(addr); + if (p & PORTSC_PLS_MASK) == PLS_U0 { + break; + } + if tsc::read_tsc().wrapping_sub(start_u0) > timeout_u0 { + break; + } + core::hint::spin_loop(); + } + } + + // one quick hot reset per scan pass; later passes retry other ports fairly. + for _ in 0..1 { + let psh = mmio::read32(addr); + portsc_write(addr, psh, PORTSC_PR, 0); + + // stage 1: controller accepts/completes reset command + let start_cmd = tsc::read_tsc(); + let timeout_cmd = self.tsc_freq / 8; + let mut reset_done = false; + loop { + let psn = mmio::read32(addr); + if (psn & PORTSC_PR) == 0 || (psn & PORTSC_PRC) != 0 { + reset_done = true; + break; + } + if tsc::read_tsc().wrapping_sub(start_cmd) > timeout_cmd { + break; + } + core::hint::spin_loop(); + } + + // Clear sticky change bits only after reset command phase settles. + let ps_post_cmd = mmio::read32(addr); + let clr_post_cmd = ps_post_cmd & PORTSC_RW1C; + if clr_post_cmd != 0 { + portsc_write(addr, ps_post_cmd, clr_post_cmd, 0); + } + + if !reset_done { + let pst = mmio::read32(addr); + let speed_t = ((pst >> PORTSC_SPEED_SHIFT) & 0xF) as u8; + if (pst & PORTSC_CCS) != 0 && speed_t != 0 { + return Ok(speed_t); + } + stage_timeout = Some(UsbMsdInitError::PortResetHotCmdTimeout); + continue; + } + + // stage 2: give link training time to become usable + let start_settle = tsc::read_tsc(); + let timeout_settle = self.tsc_freq / 5; + loop { + let psn = mmio::read32(addr); + let clr = psn & PORTSC_RW1C; + if clr != 0 { + portsc_write(addr, psn, clr, 0); + } + + if psn & PORTSC_PED != 0 { + let speed = ((psn >> PORTSC_SPEED_SHIFT) & 0xF) as u8; + return Ok(speed); + } + + let speed = ((psn >> PORTSC_SPEED_SHIFT) & 0xF) as u8; + if (psn & PORTSC_CCS != 0) && speed != 0 { + return Ok(speed); + } + + // SS links can get stuck in non-U0 states right after reset. + let pls = psn & PORTSC_PLS_MASK; + if speed >= 4 && (pls == PLS_U3 || pls == PLS_RECOVERY || pls == PLS_RESUME) { + portsc_write(addr, psn, PORTSC_LWS | PLS_U0, PORTSC_PLS_MASK); + } + + if tsc::read_tsc().wrapping_sub(start_settle) > timeout_settle { + stage_timeout = Some(UsbMsdInitError::PortResetHotSettleTimeout); + break; + } + core::hint::spin_loop(); + } + } + + // warm reset only for superspeed/CAS/stuck-link cases + let psw = mmio::read32(addr); + let hot_cmd_timed_out = matches!( + stage_timeout, + Some(UsbMsdInitError::PortResetHotCmdTimeout) + ); + let speedw = ((psw >> PORTSC_SPEED_SHIFT) & 0xF) as u8; + let warm_fallback = hot_cmd_timed_out + && ((psw & (PORTSC_CCS | PORTSC_PED)) != 0 || speedw != 0); + if warm_reset_needed(psw) || warm_fallback { + portsc_write(addr, psw, PORTSC_WPR, 0); + let start_w = tsc::read_tsc(); + let timeout_w = self.tsc_freq / 5; + + loop { + let psn = mmio::read32(addr); + let clr = psn & PORTSC_RW1C; + if clr != 0 { + portsc_write(addr, psn, clr, 0); + } + + if psn & PORTSC_PED != 0 { + let speed = ((psn >> PORTSC_SPEED_SHIFT) & 0xF) as u8; + return Ok(speed); + } + let speed = ((psn >> PORTSC_SPEED_SHIFT) & 0xF) as u8; + if (psn & PORTSC_CCS != 0) && speed != 0 { + return Ok(speed); + } + + if tsc::read_tsc().wrapping_sub(start_w) > timeout_w { + stage_timeout = Some(UsbMsdInitError::PortResetWarmTimeout); + break; + } + core::hint::spin_loop(); + } + } + + let psf = mmio::read32(addr); + let speedf = ((psf >> PORTSC_SPEED_SHIFT) & 0xF) as u8; + if (psf & PORTSC_CCS) != 0 && speedf != 0 { + // Link can be valid even when PED lags behind on some controllers. + tsc_delay(self.tsc_freq, 20); + return Ok(speedf); + } + let looks_connected = (psf & PORTSC_CCS) != 0 || speedf != 0 || (psf & PORTSC_PED) != 0; + if looks_connected { + // Some controllers surface connect/enable first and speed later. + let start_late = tsc::read_tsc(); + let late_timeout = self.tsc_freq / 10; + loop { + let psl = mmio::read32(addr); + let speedl = ((psl >> PORTSC_SPEED_SHIFT) & 0xF) as u8; + if speedl != 0 { + return Ok(speedl); + } + if tsc::read_tsc().wrapping_sub(start_late) > late_timeout { + break; + } + core::hint::spin_loop(); + } + + // Connected but speed never surfaced; assume full-speed to keep enumeration moving. + if (psf & (PORTSC_CCS | PORTSC_PED)) != 0 { + return Ok(1); + } + } + if looks_connected { + Err(UsbMsdInitError::PortResetNoLink) + } else { + Err(stage_timeout.unwrap_or(UsbMsdInitError::PortResetTimeout)) + } + } + + // ─── command ring ──────────────────────────────────────────────────── + + unsafe fn cmd_enqueue(&mut self, param: u64, status: u32, ctrl: u32) { + let base = self.dma_base + OFF_CMD_RING as u64; + let c = (ctrl & !1) | (self.cmd_cycle as u32); + write_trb(base, self.cmd_enq as usize, param, status, c); + self.cmd_enq += 1; + if self.cmd_enq >= CMD_RING_LEN - 1 { + // link TRB wrapping back to start + let link_ctrl = TRB_LINK | TRB_TC | (self.cmd_cycle as u32); + write_trb(base, self.cmd_enq as usize, base, 0, link_ctrl); + self.cmd_enq = 0; + self.cmd_cycle ^= 1; + } + } + + #[inline(always)] + unsafe fn ring_cmd_doorbell(&self) { + mmio::write32(self.db_base, 0); + } + + /// Wait for a Command Completion Event. Returns (completion_code, slot_id). + unsafe fn wait_cmd(&mut self, timeout_ms: u64) -> Result<(u8, u8), UsbMsdInitError> { + let (_, status, ctrl) = self.wait_event(TRB_CMD_COMPLETE, timeout_ms)?; + let cc = (status >> 24) as u8; + let sid = (ctrl >> 24) as u8; + if cc != 1 { + return Err(UsbMsdInitError::IoError); + } + Ok((cc, sid)) + } + + unsafe fn cmd_enable_slot(&mut self) -> Result { + self.cmd_enqueue(0, 0, TRB_ENABLE_SLOT); + self.ring_cmd_doorbell(); + let (_, slot) = self.wait_cmd(2000)?; + if slot == 0 { + return Err(UsbMsdInitError::DeviceEnumerationFailed); + } + Ok(slot) + } + + unsafe fn cmd_address_device( + &mut self, + port: u8, + speed: u8, + ) -> Result<(), UsbMsdInitError> { + let cs = self.ctx_size as u64; + let in_ctx = self.dma_base + OFF_IN_CTX as u64; + + // zero input context + core::ptr::write_bytes(in_ctx as *mut u8, 0, (33 * cs) as usize); + + // input control context: add slot (A0) + EP0 (A1) + vw32(in_ctx + 4, 0x03); + + // slot context at index 1 + let slot_ctx = in_ctx + cs; + let max_pkt_ep0 = Self::ep0_max_packet(speed); + // dword 0: speed | context_entries=1 + vw32(slot_ctx, ((speed as u32) << 20) | (1u32 << 26)); + // dword 1: root hub port (1-based) + vw32(slot_ctx + 4, (port as u32 + 1) << 16); + + // EP0 context at index 2 + let ep0 = in_ctx + 2 * cs; + // dword 1: CErr=3, EP type=4 (Control), max packet size + vw32(ep0 + 4, (3u32 << 1) | (4u32 << 3) | ((max_pkt_ep0 as u32) << 16)); + // dword 2-3: TR dequeue pointer | DCS=1 + let ring_phys = self.dma_base + OFF_XFER_EP0 as u64; + vw32(ep0 + 8, (ring_phys as u32 & !0xF) | 1); + vw32(ep0 + 12, (ring_phys >> 32) as u32); + // dword 4: average TRB length + vw32(ep0 + 16, 8); + + // address device command + let ctrl = TRB_ADDRESS_DEV | ((self.slot_id as u32) << 24); + self.cmd_enqueue(in_ctx, 0, ctrl); + self.ring_cmd_doorbell(); + self.wait_cmd(2000)?; + Ok(()) + } + + unsafe fn cmd_configure_eps( + &mut self, + dci_in: u8, + dci_out: u8, + mpkt_in: u16, + mpkt_out: u16, + ) -> Result<(), UsbMsdInitError> { + let cs = self.ctx_size as u64; + let in_ctx = self.dma_base + OFF_IN_CTX as u64; + let max_dci = dci_in.max(dci_out); + + // zero input context + core::ptr::write_bytes(in_ctx as *mut u8, 0, ((max_dci as u64 + 2) * cs) as usize); + + // input control context: A0 (slot) | A(dci_in) | A(dci_out) + let add_flags = (1u32 << 0) | (1u32 << dci_in) | (1u32 << dci_out); + vw32(in_ctx + 4, add_flags); + + // slot context: update context entries + let slot_ctx = in_ctx + cs; + // read the current speed from output context + let out_slot = self.dma_base + OFF_OUT_CTX as u64; + let d0 = vr32(out_slot); + let speed_bits = d0 & (0xF << 20); + vw32(slot_ctx, speed_bits | ((max_dci as u32) << 26)); + // root hub port from output context + vw32(slot_ctx + 4, vr32(out_slot + 4)); + + // bulk-in endpoint context + let ep_in = in_ctx + ((dci_in as u64) + 1) * cs; + // EP type 6 = Bulk IN, CErr=3 + vw32(ep_in + 4, (3u32 << 1) | (6u32 << 3) | ((mpkt_in as u32) << 16)); + let ring_in = self.dma_base + OFF_XFER_BIN as u64; + vw32(ep_in + 8, (ring_in as u32 & !0xF) | 1); + vw32(ep_in + 12, (ring_in >> 32) as u32); + vw32(ep_in + 16, 1024); // average TRB length + + // bulk-out endpoint context + let ep_out = in_ctx + ((dci_out as u64) + 1) * cs; + // EP type 2 = Bulk OUT, CErr=3 + vw32(ep_out + 4, (3u32 << 1) | (2u32 << 3) | ((mpkt_out as u32) << 16)); + let ring_out = self.dma_base + OFF_XFER_BOUT as u64; + vw32(ep_out + 8, (ring_out as u32 & !0xF) | 1); + vw32(ep_out + 12, (ring_out >> 32) as u32); + vw32(ep_out + 16, 1024); + + let ctrl = TRB_CONFIGURE_EP | ((self.slot_id as u32) << 24); + self.cmd_enqueue(in_ctx, 0, ctrl); + self.ring_cmd_doorbell(); + self.wait_cmd(2000)?; + Ok(()) + } + + // ─── control transfers (EP0) ───────────────────────────────────────── + + /// IN control transfer: GET_DESCRIPTOR etc. + /// Returns slice into the descriptor buffer. + unsafe fn control_in( + &mut self, + req_type: u8, + request: u8, + value: u16, + index: u16, + len: u16, + ) -> Result<&[u8], UsbMsdInitError> { + let setup = (req_type as u64) + | ((request as u64) << 8) + | ((value as u64) << 16) + | ((index as u64) << 32) + | ((len as u64) << 48); + + let desc_buf = self.dma_base + OFF_DESC as u64; + + // setup stage: IDT=1, TRT=IN + self.xfer_enqueue(Ring::Ep0, setup, 8, TRB_SETUP | TRB_IDT | TRB_TRT_IN); + // data stage: DIR=IN + self.xfer_enqueue( + Ring::Ep0, + desc_buf, + len as u32, + TRB_DATA | TRB_DIR_IN | TRB_ISP, + ); + // status stage: DIR=OUT (no DIR_IN), IOC + self.xfer_enqueue(Ring::Ep0, 0, 0, TRB_STATUS | TRB_IOC); + + // ring EP0 doorbell (DCI=1) + mmio::write32(self.db_base + (self.slot_id as u64) * 4, 1); + self.wait_xfer(5000)?; + + Ok(core::slice::from_raw_parts( + desc_buf as *const u8, + len as usize, + )) + } + + /// No-data control transfer: SET_CONFIGURATION etc. + unsafe fn control_nodata( + &mut self, + req_type: u8, + request: u8, + value: u16, + index: u16, + ) -> Result<(), UsbMsdInitError> { + let setup = (req_type as u64) + | ((request as u64) << 8) + | ((value as u64) << 16) + | ((index as u64) << 32); + + // setup stage: no data + self.xfer_enqueue(Ring::Ep0, setup, 8, TRB_SETUP | TRB_IDT); + // status stage: DIR=IN, IOC + self.xfer_enqueue(Ring::Ep0, 0, 0, TRB_STATUS | TRB_IOC | TRB_DIR_IN); + + mmio::write32(self.db_base + (self.slot_id as u64) * 4, 1); + self.wait_xfer(5000)?; + Ok(()) + } + + // ─── transfer ring enqueue ─────────────────────────────────────────── + + unsafe fn xfer_enqueue(&mut self, ring: Ring, param: u64, status: u32, ctrl: u32) { + let (off, enq, cycle) = match ring { + Ring::Ep0 => (OFF_XFER_EP0, &mut self.ep0_enq, &mut self.ep0_cycle), + Ring::BulkOut => (OFF_XFER_BOUT, &mut self.bout_enq, &mut self.bout_cycle), + Ring::BulkIn => (OFF_XFER_BIN, &mut self.bin_enq, &mut self.bin_cycle), + }; + let base = self.dma_base + off as u64; + let c = (ctrl & !1) | (*cycle as u32); + write_trb(base, *enq as usize, param, status, c); + *enq += 1; + if *enq >= XFER_RING_LEN - 1 { + let link = TRB_LINK | TRB_TC | (*cycle as u32); + write_trb(base, *enq as usize, base, 0, link); + *enq = 0; + *cycle ^= 1; + } + } + + /// Wait for a Transfer Event. Returns remaining byte count. + unsafe fn wait_xfer(&mut self, timeout_ms: u64) -> Result { + let (_, status, _) = self.wait_event(TRB_XFER_EVENT, timeout_ms)?; + let cc = (status >> 24) as u8; + // 1 = success, 13 = short packet (ok for mass storage / descriptors) + if cc != 1 && cc != 13 { + return Err(UsbMsdInitError::IoError); + } + // remaining bytes in bits 23:0 + Ok(status & 0x00FF_FFFF) + } + + // ─── event ring ────────────────────────────────────────────────────── + + unsafe fn wait_event( + &mut self, + expected: u32, + timeout_ms: u64, + ) -> Result<(u64, u32, u32), UsbMsdInitError> { + let start = tsc::read_tsc(); + let timeout = self.tsc_freq.saturating_mul(timeout_ms) / 1000; + let base = self.dma_base + OFF_EVT_RING as u64; + + loop { + let a = base + (self.evt_deq as u64) * 16; + let ctrl = vr32(a + 12); + if (ctrl & 1) == self.evt_cycle as u32 { + let p_lo = vr32(a) as u64; + let p_hi = vr32(a + 4) as u64; + let param = p_lo | (p_hi << 32); + let status = vr32(a + 8); + + self.evt_deq += 1; + if self.evt_deq >= EVT_RING_LEN { + self.evt_deq = 0; + self.evt_cycle ^= 1; + } + // update ERDP, clear EHB + let new_erdp = base + (self.evt_deq as u64) * 16; + mmio::write32( + self.rt_base + IR0_ERDP, + (new_erdp as u32 & !0xF) | 0x08, + ); + mmio::write32(self.rt_base + IR0_ERDP + 4, (new_erdp >> 32) as u32); + + if (ctrl & TRB_TYPE_MASK) == expected { + return Ok((param, status, ctrl)); + } + // skip unexpected events (port status change, etc.) + continue; + } + if tsc::read_tsc().wrapping_sub(start) > timeout { + return Err(UsbMsdInitError::CommandTimeout); + } + core::hint::spin_loop(); + } + } + + // ─── BOT (Bulk-Only Transport) ─────────────────────────────────────── + + /// Send a SCSI command via BOT. + /// `scsi_cb` = command block (6-16 bytes). + /// `data_len` = expected data transfer length (0 = no data phase). + /// `data_in` = true for device→host data. + /// Data lands at OFF_DATA. + unsafe fn bot_command( + &mut self, + scsi_cb: &[u8], + data_len: u32, + data_in: bool, + ) -> Result { + let tag = self.bot_tag; + self.bot_tag = self.bot_tag.wrapping_add(1); + + // ── build CBW at OFF_CBW ── + let cbw = self.dma_base + OFF_CBW as u64; + core::ptr::write_bytes(cbw as *mut u8, 0, 31); + vw32(cbw, CBW_SIG); + vw32(cbw + 4, tag); + vw32(cbw + 8, data_len); + // flags: 0x80 = data-in, 0x00 = data-out/no-data + let flags: u8 = if data_in && data_len > 0 { 0x80 } else { 0x00 }; + core::ptr::write_volatile((cbw + 12) as *mut u8, flags); + // LUN = 0, CB length + core::ptr::write_volatile((cbw + 14) as *mut u8, scsi_cb.len().min(16) as u8); + // copy SCSI CDB + for (i, &b) in scsi_cb.iter().take(16).enumerate() { + core::ptr::write_volatile((cbw + 15 + i as u64) as *mut u8, b); + } + + // ── send CBW via bulk-out ── + self.xfer_enqueue(Ring::BulkOut, cbw, 31, TRB_NORMAL | TRB_IOC); + mmio::write32( + self.db_base + (self.slot_id as u64) * 4, + self.dci_bulk_out as u32, + ); + self.wait_xfer(5000)?; + + // ── data phase (if any) ── + let mut transferred = 0u32; + if data_len > 0 { + let data_buf = self.dma_base + OFF_DATA as u64; + if data_in { + self.xfer_enqueue(Ring::BulkIn, data_buf, data_len, TRB_NORMAL | TRB_IOC | TRB_ISP); + mmio::write32( + self.db_base + (self.slot_id as u64) * 4, + self.dci_bulk_in as u32, + ); + } else { + self.xfer_enqueue(Ring::BulkOut, data_buf, data_len, TRB_NORMAL | TRB_IOC); + mmio::write32( + self.db_base + (self.slot_id as u64) * 4, + self.dci_bulk_out as u32, + ); + } + let residue = self.wait_xfer(10000)?; + transferred = data_len.saturating_sub(residue); + } + + // ── receive CSW via bulk-in ── + let csw = self.dma_base + OFF_CSW as u64; + core::ptr::write_bytes(csw as *mut u8, 0, 13); + self.xfer_enqueue(Ring::BulkIn, csw, 13, TRB_NORMAL | TRB_IOC); + mmio::write32( + self.db_base + (self.slot_id as u64) * 4, + self.dci_bulk_in as u32, + ); + self.wait_xfer(5000)?; + + // verify CSW + let csw_sig = vr32(csw); + let csw_tag = vr32(csw + 4); + let csw_status = core::ptr::read_volatile((csw + 12) as *const u8); + if csw_sig != CSW_SIG || csw_tag != tag || csw_status != 0 { + return Err(UsbMsdInitError::IoError); + } + + Ok(transferred) + } + + /// SCSI READ(10) via BOT — reads `count` sectors at `lba` into OFF_DATA. + unsafe fn scsi_read_sectors( + &mut self, + lba: u64, + count: u32, + ) -> Result<(), UsbMsdInitError> { + let byte_count = count * self.info.sector_size; + let mut cmd = [0u8; 10]; + cmd[0] = SCSI_READ_10; + // LBA big-endian at offset 2 + cmd[2] = (lba >> 24) as u8; + cmd[3] = (lba >> 16) as u8; + cmd[4] = (lba >> 8) as u8; + cmd[5] = lba as u8; + // transfer length (blocks) big-endian at offset 7 + cmd[7] = (count >> 8) as u8; + cmd[8] = count as u8; + + self.bot_command(&cmd, byte_count, true)?; + Ok(()) + } + + // ─── descriptor parsing ────────────────────────────────────────────── + + /// Parse configuration descriptor and classify the device function. + unsafe fn parse_config_desc(&self) -> ConfigParse { + let d = self.dma_base + OFF_DESC as u64; + let total = u16::from_le_bytes([ + core::ptr::read_volatile((d + 2) as *const u8), + core::ptr::read_volatile((d + 3) as *const u8), + ]) as usize; + let cfg_val = core::ptr::read_volatile((d + 5) as *const u8); + let limit = total.min(255); + + let mut off = 0usize; + let mut in_msc = false; + let mut ep_in: u8 = 0; + let mut ep_out: u8 = 0; + let mut mp_in: u16 = 0; + let mut mp_out: u16 = 0; + let mut saw_hub_iface = false; + let mut saw_mass_storage_non_bot = false; + + while off + 2 <= limit { + let blen = core::ptr::read_volatile((d + off as u64) as *const u8) as usize; + let btype = core::ptr::read_volatile((d + off as u64 + 1) as *const u8); + if blen < 2 { + break; + } + if off + blen > limit { + break; + } + if btype == 4 && blen >= 9 { + // interface descriptor + let cls = core::ptr::read_volatile((d + off as u64 + 5) as *const u8); + let sub = core::ptr::read_volatile((d + off as u64 + 6) as *const u8); + let proto = core::ptr::read_volatile((d + off as u64 + 7) as *const u8); + if cls == USB_CLASS_HUB { + saw_hub_iface = true; + } + if cls == USB_CLASS_MASS_STORAGE + && (sub != USB_SUBCLASS_SCSI || proto != USB_PROTOCOL_BOT) + { + saw_mass_storage_non_bot = true; + } + in_msc = + cls == USB_CLASS_MASS_STORAGE && sub == USB_SUBCLASS_SCSI && proto == USB_PROTOCOL_BOT; + } + if btype == 5 && blen >= 7 && in_msc { + // endpoint descriptor + let addr = core::ptr::read_volatile((d + off as u64 + 2) as *const u8); + let attr = core::ptr::read_volatile((d + off as u64 + 3) as *const u8); + let mpkt = u16::from_le_bytes([ + core::ptr::read_volatile((d + off as u64 + 4) as *const u8), + core::ptr::read_volatile((d + off as u64 + 5) as *const u8), + ]); + if attr & 0x03 == 0x02 { + // bulk + if addr & 0x80 != 0 { + ep_in = addr; + mp_in = mpkt; + } else { + ep_out = addr; + mp_out = mpkt; + } + } + } + off += blen; + } + + if ep_in != 0 && ep_out != 0 { + ConfigParse::MassStorage { + cfg_val, + ep_in, + ep_out, + mp_in, + mp_out, + } + } else if saw_hub_iface { + ConfigParse::Hub + } else if saw_mass_storage_non_bot { + ConfigParse::MassStorageUnsupported + } else { + ConfigParse::None + } + } + + // ─── helpers ───────────────────────────────────────────────────────── + + fn ep0_max_packet(speed: u8) -> u16 { + match speed { + 4 => 512, // SS + 3 => 64, // HS + 2 => 8, // LS + _ => 64, // FS / default + } + } + + /// Zero transfer ring buffers and reset ring indices for a fresh enumeration attempt. + unsafe fn reset_transfer_state(&mut self) { + // zero ring memory + core::ptr::write_bytes((self.dma_base + OFF_XFER_EP0 as u64) as *mut u8, 0, 256); + core::ptr::write_bytes((self.dma_base + OFF_XFER_BOUT as u64) as *mut u8, 0, 256); + core::ptr::write_bytes((self.dma_base + OFF_XFER_BIN as u64) as *mut u8, 0, 256); + // zero contexts + core::ptr::write_bytes((self.dma_base + OFF_OUT_CTX as u64) as *mut u8, 0, 2048); + core::ptr::write_bytes((self.dma_base + OFF_IN_CTX as u64) as *mut u8, 0, 2560); + // reset indices + self.ep0_enq = 0; + self.ep0_cycle = 1; + self.bout_enq = 0; + self.bout_cycle = 1; + self.bin_enq = 0; + self.bin_cycle = 1; + } +} + +// ═══════════════════════════════════════════════════════════════════════════ +// BlockDriverInit +// ═══════════════════════════════════════════════════════════════════════════ + +impl BlockDriverInit for UsbMsdDriver { + type Error = UsbMsdInitError; + type Config = UsbMsdConfig; + + fn supported_vendors() -> &'static [u16] { + &[] + } + + fn supported_devices() -> &'static [u16] { + &[] + } + + unsafe fn create(mmio_base: u64, config: Self::Config) -> Result { + Self::new(mmio_base, config) + } +} + +// ═══════════════════════════════════════════════════════════════════════════ +// BlockDriver +// ═══════════════════════════════════════════════════════════════════════════ + +impl BlockDriver for UsbMsdDriver { + fn info(&self) -> BlockDeviceInfo { + self.info + } + + fn can_submit(&self) -> bool { + self.last_completion.is_none() + } + + fn submit_read( + &mut self, + sector: u64, + buffer_phys: u64, + num_sectors: u32, + request_id: u32, + ) -> Result<(), BlockError> { + if self.last_completion.is_some() { + return Err(BlockError::QueueFull); + } + if num_sectors == 0 || num_sectors > self.info.max_sectors_per_request { + return Err(BlockError::RequestTooLarge); + } + if buffer_phys == 0 { + return Err(BlockError::InvalidSector); + } + let end = sector + .checked_add(num_sectors as u64) + .ok_or(BlockError::InvalidSector)?; + if end > self.info.total_sectors { + return Err(BlockError::InvalidSector); + } + + let byte_count = num_sectors as u64 * self.info.sector_size as u64; + unsafe { + self.scsi_read_sectors(sector, num_sectors) + .map_err(|_| BlockError::IoError)?; + + // copy bounce buffer → caller's buffer + core::ptr::copy_nonoverlapping( + (self.dma_base + OFF_DATA as u64) as *const u8, + buffer_phys as *mut u8, + byte_count as usize, + ); + } + + self.last_completion = Some(BlockCompletion { + request_id, + status: 0, + bytes_transferred: (byte_count as u32), + }); + Ok(()) + } + + fn submit_write( + &mut self, + _sector: u64, + _buffer_phys: u64, + _num_sectors: u32, + _request_id: u32, + ) -> Result<(), BlockError> { + Err(BlockError::Unsupported) + } + + fn poll_completion(&mut self) -> Option { + self.last_completion.take() + } + + fn notify(&mut self) {} +} diff --git a/network/src/lib.rs b/network/src/lib.rs index 66039c5f..0bef8df1 100644 --- a/network/src/lib.rs +++ b/network/src/lib.rs @@ -120,6 +120,8 @@ pub use driver::block_traits::{BlockCompletion, BlockDeviceInfo, BlockDriver, Bl // Block drivers pub use driver::ahci::{AhciConfig, AhciDriver, AhciInitError}; +pub use driver::sdhci::{SdhciConfig, SdhciDriver, SdhciInitError}; +pub use driver::usb_msd::{UsbMsdConfig, UsbMsdDriver, UsbMsdInitError}; pub use driver::virtio_blk::{VirtioBlkConfig, VirtioBlkDriver, VirtioBlkInitError}; // BlockIo adapters (for filesystem compatibility) @@ -132,9 +134,10 @@ pub use gpt_disk_types::{BlockSize as GptBlockSize, Lba as GptLba}; // Block probe pub use boot::block_probe::{ - create_unified_from_detected, detect_block_device_type, probe_and_create_block_driver, - probe_unified_block_device, scan_all_block_devices, BlockDeviceType, BlockDmaConfig, - BlockProbeError, BlockProbeResult, DetectedBlockDevice, + create_unified_from_detected, create_unified_from_detected_ahci_port, + detect_block_device_type, probe_and_create_block_driver, probe_unified_block_device, + scan_all_block_devices, BlockDeviceType, BlockDmaConfig, BlockProbeError, BlockProbeResult, + DetectedBlockDevice, }; // Client diff --git a/persistent/src/pe/embedded_reloc_data.rs b/persistent/src/pe/embedded_reloc_data.rs index a185047e..0bf34a9b 100644 --- a/persistent/src/pe/embedded_reloc_data.rs +++ b/persistent/src/pe/embedded_reloc_data.rs @@ -8,111 +8,120 @@ //! Run: ./tools/extract-reloc-data.sh after each build /// Original .reloc section RVA -pub const RELOC_RVA: u32 = 0x00633000; +pub const RELOC_RVA: u32 = 0x0070d000; /// Original .reloc section size -pub const RELOC_SIZE: u32 = 0x00000464; +pub const RELOC_SIZE: u32 = 0x000004d4; /// Original ImageBase from linker script pub const ORIGINAL_IMAGE_BASE: u64 = 0x0000004001000000; -/// Hardcoded .reloc section data (1124 bytes) -/// Extracted from morpheus-bootloader.efi at file offset 0x001f4a00 +/// Hardcoded .reloc section data (1236 bytes) +/// Extracted from morpheus-bootloader.efi at file offset 0x00202400 #[allow(dead_code)] -pub const RELOC_DATA: [u8; 1124] = [ - 0x00, 0xb0, 0x06, 0x00, 0x64, 0x00, 0x00, 0x00, 0x50, 0xa4, 0xe0, 0xa5, - 0x38, 0xa6, 0x50, 0xa6, 0x68, 0xa6, 0x80, 0xa6, 0xd8, 0xa6, 0xf0, 0xa6, - 0x08, 0xa7, 0x20, 0xa7, 0x38, 0xa7, 0x50, 0xa7, 0x68, 0xa7, 0xe8, 0xa7, - 0x00, 0xa8, 0x18, 0xa8, 0x30, 0xa8, 0x48, 0xa8, 0x60, 0xa8, 0x78, 0xa8, - 0x90, 0xa8, 0xa8, 0xa8, 0x28, 0xa9, 0x40, 0xa9, 0x58, 0xa9, 0x70, 0xa9, - 0x88, 0xa9, 0xa0, 0xa9, 0xb8, 0xa9, 0xd0, 0xa9, 0xe8, 0xa9, 0x00, 0xaa, - 0x18, 0xaa, 0x30, 0xaa, 0x48, 0xaa, 0x60, 0xaa, 0x78, 0xaa, 0x90, 0xaa, - 0xa8, 0xaa, 0x28, 0xab, 0x40, 0xab, 0x58, 0xab, 0x70, 0xab, 0x88, 0xab, - 0x08, 0xac, 0x78, 0xaf, 0x00, 0xc0, 0x06, 0x00, 0x38, 0x00, 0x00, 0x00, - 0xe8, 0xa3, 0x80, 0xa6, 0x98, 0xa6, 0xb0, 0xa6, 0xf8, 0xa8, 0x00, 0xaa, - 0xe8, 0xab, 0x00, 0xac, 0x18, 0xac, 0x70, 0xac, 0x88, 0xac, 0xa0, 0xac, - 0x20, 0xad, 0x38, 0xad, 0x50, 0xad, 0x50, 0xae, 0x68, 0xae, 0x80, 0xae, - 0x98, 0xae, 0xb0, 0xae, 0xc8, 0xae, 0x48, 0xaf, 0x60, 0xaf, 0xe8, 0xaf, - 0x00, 0xd0, 0x06, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x48, 0xa0, 0x60, 0xa0, - 0x78, 0xa0, 0xd8, 0xa0, 0x58, 0xa1, 0x70, 0xa1, 0x88, 0xa1, 0xc8, 0xa1, - 0xd8, 0xa1, 0x20, 0xa2, 0x38, 0xa2, 0x50, 0xa2, 0x68, 0xa2, 0x08, 0xa3, - 0xd8, 0xa5, 0xf0, 0xa5, 0x30, 0xa6, 0x48, 0xa6, 0x60, 0xa6, 0x78, 0xa6, - 0x00, 0xa7, 0x18, 0xa7, 0xd8, 0xa7, 0x08, 0xa8, 0x68, 0xa8, 0x80, 0xa8, - 0xe0, 0xa8, 0xf8, 0xa8, 0x10, 0xa9, 0x28, 0xa9, 0x40, 0xa9, 0x58, 0xa9, - 0x90, 0xa9, 0xc0, 0xaa, 0xf8, 0xab, 0x10, 0xac, 0x60, 0xac, 0xd8, 0xac, - 0xf0, 0xac, 0x08, 0xad, 0x20, 0xad, 0x38, 0xad, 0x50, 0xad, 0x68, 0xad, - 0x80, 0xad, 0x98, 0xad, 0xb0, 0xad, 0xc8, 0xad, 0xe0, 0xad, 0xf8, 0xad, - 0x10, 0xae, 0x28, 0xae, 0x90, 0xae, 0xa8, 0xae, 0xc0, 0xae, 0xd8, 0xae, - 0xf0, 0xae, 0x08, 0xaf, 0x20, 0xaf, 0x38, 0xaf, 0x50, 0xaf, 0x68, 0xaf, - 0x80, 0xaf, 0xd0, 0xaf, 0xe0, 0xaf, 0x00, 0x00, 0x00, 0xe0, 0x06, 0x00, - 0x8c, 0x00, 0x00, 0x00, 0x38, 0xa0, 0x48, 0xa0, 0x28, 0xa2, 0x40, 0xa2, - 0x58, 0xa2, 0x70, 0xa2, 0xf0, 0xa2, 0x08, 0xa3, 0x20, 0xa3, 0x38, 0xa3, - 0x50, 0xa3, 0x68, 0xa3, 0x80, 0xa3, 0x98, 0xa3, 0xb0, 0xa3, 0xe0, 0xa3, - 0x80, 0xa4, 0x98, 0xa4, 0x18, 0xa5, 0x98, 0xa5, 0xb0, 0xa5, 0xc8, 0xa5, - 0xe0, 0xa5, 0xf8, 0xa5, 0x10, 0xa6, 0x98, 0xa6, 0xc0, 0xa7, 0xd0, 0xa7, - 0xe0, 0xa7, 0x58, 0xa8, 0x70, 0xa8, 0x88, 0xa8, 0xa0, 0xa8, 0xb8, 0xa8, +pub const RELOC_DATA: [u8; 1236] = [ + 0x00, 0x60, 0x07, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x50, 0xa4, 0xe0, 0xa5, + 0x38, 0xa6, 0x50, 0xa6, 0x68, 0xa6, 0x80, 0xa6, 0x10, 0xa7, 0x28, 0xa7, + 0x48, 0xa7, 0x08, 0xa9, 0x00, 0x70, 0x07, 0x00, 0x3c, 0x00, 0x00, 0x00, + 0x58, 0xa7, 0x98, 0xa9, 0xb0, 0xa9, 0xc8, 0xa9, 0xe0, 0xa9, 0xf8, 0xa9, + 0x10, 0xaa, 0x28, 0xaa, 0x40, 0xaa, 0x58, 0xaa, 0x70, 0xaa, 0x88, 0xaa, + 0xa0, 0xaa, 0xb8, 0xaa, 0xc0, 0xaa, 0xc8, 0xaa, 0xd0, 0xaa, 0xd8, 0xaa, + 0xe0, 0xaa, 0xe8, 0xaa, 0xf0, 0xaa, 0x38, 0xac, 0x90, 0xac, 0xa8, 0xac, + 0xc0, 0xac, 0xf8, 0xac, 0x00, 0x80, 0x07, 0x00, 0x54, 0x00, 0x00, 0x00, + 0xc8, 0xa3, 0xe0, 0xa3, 0xf8, 0xa3, 0x10, 0xa4, 0x28, 0xa4, 0xe8, 0xa4, + 0x18, 0xa5, 0x98, 0xa5, 0xb0, 0xa5, 0xc8, 0xa5, 0x78, 0xa8, 0x90, 0xa8, + 0xd0, 0xa8, 0xe8, 0xa8, 0x00, 0xa9, 0x18, 0xa9, 0xa0, 0xa9, 0xb8, 0xa9, + 0x08, 0xaa, 0x88, 0xaa, 0xa0, 0xaa, 0xb8, 0xaa, 0xf8, 0xaa, 0x08, 0xab, + 0x50, 0xab, 0x68, 0xab, 0x80, 0xab, 0x98, 0xab, 0xf0, 0xab, 0x08, 0xac, + 0x20, 0xac, 0x38, 0xac, 0x50, 0xac, 0x68, 0xac, 0x80, 0xac, 0xd8, 0xac, + 0xf0, 0xac, 0x08, 0xad, 0x00, 0x90, 0x07, 0x00, 0x58, 0x00, 0x00, 0x00, + 0xb8, 0xa0, 0xd0, 0xa0, 0xe8, 0xa0, 0x00, 0xa1, 0x18, 0xa1, 0x30, 0xa1, + 0x48, 0xa1, 0x60, 0xa1, 0x78, 0xa1, 0xf8, 0xa1, 0x78, 0xa2, 0x48, 0xa3, + 0x60, 0xa3, 0x78, 0xa3, 0x90, 0xa3, 0xa8, 0xa3, 0xc0, 0xa3, 0x40, 0xa4, + 0x58, 0xa4, 0x70, 0xa4, 0x88, 0xa4, 0xa0, 0xa4, 0xb8, 0xa4, 0xd0, 0xa4, + 0xe8, 0xa4, 0x00, 0xa5, 0x80, 0xa5, 0x98, 0xa5, 0xf8, 0xa5, 0x10, 0xa6, + 0x28, 0xa6, 0x88, 0xac, 0xa0, 0xac, 0x88, 0xad, 0xa0, 0xad, 0xb8, 0xad, + 0xd0, 0xad, 0xe8, 0xad, 0x00, 0xae, 0xa8, 0xaf, 0x00, 0xa0, 0x07, 0x00, + 0xa0, 0x00, 0x00, 0x00, 0xf0, 0xa0, 0x08, 0xa1, 0x58, 0xa1, 0xd0, 0xa1, + 0xe8, 0xa1, 0x00, 0xa2, 0x18, 0xa2, 0x30, 0xa2, 0x48, 0xa2, 0x60, 0xa2, + 0x78, 0xa2, 0x90, 0xa2, 0xa8, 0xa2, 0xc0, 0xa2, 0xd8, 0xa2, 0xf0, 0xa2, + 0x08, 0xa3, 0x20, 0xa3, 0x88, 0xa3, 0xa0, 0xa3, 0xb8, 0xa3, 0xd0, 0xa3, + 0xe8, 0xa3, 0x00, 0xa4, 0x18, 0xa4, 0x30, 0xa4, 0x48, 0xa4, 0x60, 0xa4, + 0x78, 0xa4, 0xc8, 0xa4, 0xd8, 0xa4, 0x30, 0xa5, 0x40, 0xa5, 0x20, 0xa7, + 0x38, 0xa7, 0x50, 0xa7, 0x68, 0xa7, 0xe8, 0xa7, 0x00, 0xa8, 0x18, 0xa8, + 0x30, 0xa8, 0x48, 0xa8, 0x60, 0xa8, 0x78, 0xa8, 0x90, 0xa8, 0xa8, 0xa8, + 0xd8, 0xa8, 0x78, 0xa9, 0x90, 0xa9, 0x10, 0xaa, 0x90, 0xaa, 0xa8, 0xaa, + 0xc0, 0xaa, 0xd8, 0xaa, 0xf0, 0xaa, 0x08, 0xab, 0x90, 0xab, 0xb8, 0xac, + 0xc8, 0xac, 0xd8, 0xac, 0x50, 0xad, 0x68, 0xad, 0x80, 0xad, 0x98, 0xad, + 0xb0, 0xad, 0xc8, 0xad, 0xe0, 0xad, 0xf8, 0xad, 0x10, 0xae, 0x28, 0xae, + 0x40, 0xae, 0x58, 0xae, 0x70, 0xae, 0x88, 0xae, 0xa0, 0xae, 0xb8, 0xae, + 0x00, 0xb0, 0x07, 0x00, 0x6c, 0x00, 0x00, 0x00, 0x60, 0xa0, 0x78, 0xa0, + 0x90, 0xa0, 0xa8, 0xa0, 0xc0, 0xa0, 0xd8, 0xa0, 0xf0, 0xa0, 0x08, 0xa1, + 0x20, 0xa1, 0xa8, 0xa1, 0xe8, 0xa1, 0xf8, 0xa1, 0xb8, 0xa2, 0x48, 0xa3, + 0x88, 0xa3, 0xd0, 0xa3, 0x68, 0xa4, 0xb0, 0xa4, 0xc0, 0xa4, 0xd0, 0xa4, + 0xe0, 0xa4, 0x60, 0xa5, 0x78, 0xa5, 0xf8, 0xa5, 0x98, 0xa6, 0xa8, 0xa6, + 0xb8, 0xa6, 0x50, 0xa7, 0x88, 0xa7, 0xa0, 0xa7, 0xb8, 0xa7, 0xd0, 0xa7, + 0x50, 0xa8, 0x90, 0xa8, 0xa8, 0xa8, 0xe8, 0xa9, 0x10, 0xab, 0x28, 0xab, + 0x58, 0xab, 0x90, 0xab, 0x18, 0xac, 0x30, 0xac, 0xc8, 0xac, 0xe0, 0xac, + 0xf8, 0xac, 0x10, 0xad, 0x28, 0xad, 0x38, 0xae, 0x98, 0xae, 0x10, 0xaf, + 0x00, 0xc0, 0x07, 0x00, 0x58, 0x00, 0x00, 0x00, 0x30, 0xa0, 0x88, 0xa0, + 0xa0, 0xa0, 0x10, 0xa1, 0x28, 0xa1, 0x40, 0xa1, 0x78, 0xa1, 0xd0, 0xa1, + 0xe8, 0xa1, 0x00, 0xa2, 0x18, 0xa2, 0x30, 0xa2, 0x60, 0xa2, 0x80, 0xa2, + 0x18, 0xa3, 0x30, 0xa3, 0x48, 0xa3, 0x50, 0xa5, 0x68, 0xa5, 0x80, 0xa5, + 0x98, 0xa5, 0xb0, 0xa5, 0xc8, 0xa5, 0xe0, 0xa5, 0xa0, 0xa6, 0x08, 0xa7, + 0x28, 0xa7, 0x40, 0xa7, 0x58, 0xa7, 0x70, 0xa7, 0x88, 0xa7, 0x90, 0xa8, + 0xe8, 0xa8, 0x00, 0xa9, 0x18, 0xa9, 0x30, 0xa9, 0x48, 0xa9, 0x88, 0xa9, + 0x18, 0xaa, 0x00, 0x00, 0x00, 0xd0, 0x07, 0x00, 0x5c, 0x00, 0x00, 0x00, + 0xb8, 0xa4, 0xd0, 0xa4, 0x58, 0xaa, 0x70, 0xaa, 0x88, 0xaa, 0xa0, 0xaa, + 0xb8, 0xaa, 0xd0, 0xaa, 0xe8, 0xab, 0xa8, 0xac, 0x00, 0xae, 0x10, 0xae, + 0x20, 0xae, 0x30, 0xae, 0x40, 0xae, 0x50, 0xae, 0x60, 0xae, 0x70, 0xae, + 0x80, 0xae, 0x90, 0xae, 0xa0, 0xae, 0xb0, 0xae, 0xc0, 0xae, 0xd0, 0xae, + 0xe0, 0xae, 0xf0, 0xae, 0x00, 0xaf, 0x10, 0xaf, 0x20, 0xaf, 0x30, 0xaf, + 0x40, 0xaf, 0x50, 0xaf, 0x60, 0xaf, 0x70, 0xaf, 0x80, 0xaf, 0x90, 0xaf, + 0xa0, 0xaf, 0xb0, 0xaf, 0xc0, 0xaf, 0xd0, 0xaf, 0xe0, 0xaf, 0xf0, 0xaf, + 0x00, 0xe0, 0x07, 0x00, 0x0c, 0x00, 0x00, 0x00, 0xd0, 0xa1, 0x00, 0x00, + 0x00, 0xf0, 0x07, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x90, 0xa4, 0x20, 0xa7, + 0x30, 0xa8, 0xc8, 0xaa, 0xe0, 0xaa, 0x30, 0xab, 0x40, 0xab, 0x50, 0xab, + 0x60, 0xab, 0x70, 0xab, 0xa0, 0xab, 0xd8, 0xab, 0x20, 0xac, 0x70, 0xac, + 0xa8, 0xac, 0xc0, 0xac, 0x08, 0xad, 0x90, 0xad, 0x00, 0x00, 0x08, 0x00, + 0x44, 0x00, 0x00, 0x00, 0xd0, 0xa9, 0x60, 0xaa, 0x78, 0xaa, 0xb0, 0xaa, + 0xe0, 0xaa, 0xf8, 0xaa, 0x10, 0xab, 0x28, 0xab, 0x40, 0xab, 0x58, 0xab, + 0x70, 0xab, 0x88, 0xab, 0xc8, 0xab, 0xe0, 0xab, 0xf8, 0xab, 0x10, 0xac, + 0x28, 0xac, 0x60, 0xac, 0x90, 0xac, 0xa8, 0xac, 0xc0, 0xac, 0xd8, 0xac, + 0x10, 0xad, 0x28, 0xad, 0x40, 0xad, 0x58, 0xad, 0x70, 0xad, 0x88, 0xad, + 0x18, 0xae, 0x00, 0x00, 0x00, 0x10, 0x08, 0x00, 0x50, 0x00, 0x00, 0x00, + 0x60, 0xa4, 0x70, 0xa4, 0xa0, 0xa4, 0xb8, 0xa4, 0x00, 0xa5, 0x10, 0xa5, + 0x20, 0xa5, 0x48, 0xa5, 0x80, 0xa5, 0xc0, 0xaa, 0xd0, 0xaa, 0xe0, 0xaa, + 0xf0, 0xaa, 0x40, 0xab, 0x50, 0xab, 0x60, 0xab, 0x70, 0xab, 0x80, 0xab, + 0xa8, 0xab, 0xb8, 0xab, 0xc8, 0xab, 0x30, 0xac, 0x40, 0xac, 0x50, 0xac, + 0xb0, 0xac, 0xf0, 0xac, 0x40, 0xad, 0x50, 0xad, 0x88, 0xad, 0x98, 0xad, + 0xc0, 0xad, 0xd0, 0xad, 0x08, 0xae, 0x20, 0xae, 0x78, 0xae, 0x00, 0x00, + 0x00, 0x20, 0x08, 0x00, 0x60, 0x00, 0x00, 0x00, 0xe8, 0xa4, 0x00, 0xa5, + 0x38, 0xa5, 0x88, 0xa5, 0xd0, 0xa5, 0xe0, 0xa5, 0x68, 0xa7, 0xa0, 0xa7, + 0xb8, 0xa7, 0xf0, 0xa7, 0x30, 0xa8, 0x48, 0xa8, 0x80, 0xa8, 0xb8, 0xa8, 0xd0, 0xa8, 0xe8, 0xa8, 0x00, 0xa9, 0x18, 0xa9, 0x30, 0xa9, 0x48, 0xa9, - 0x60, 0xa9, 0x78, 0xa9, 0x90, 0xa9, 0xa8, 0xa9, 0xc0, 0xa9, 0x68, 0xab, - 0x80, 0xab, 0x98, 0xab, 0xb0, 0xab, 0xc8, 0xab, 0xe0, 0xab, 0xf8, 0xab, - 0x10, 0xac, 0x28, 0xac, 0xb0, 0xac, 0xf0, 0xac, 0x00, 0xad, 0xc0, 0xad, - 0x50, 0xae, 0x90, 0xae, 0xd8, 0xae, 0x70, 0xaf, 0xb8, 0xaf, 0xc8, 0xaf, - 0xd8, 0xaf, 0xe8, 0xaf, 0x00, 0xf0, 0x06, 0x00, 0x5c, 0x00, 0x00, 0x00, - 0x68, 0xa0, 0x80, 0xa0, 0x00, 0xa1, 0xa0, 0xa1, 0xb0, 0xa1, 0xc0, 0xa1, - 0x58, 0xa2, 0x90, 0xa2, 0xa8, 0xa2, 0xc0, 0xa2, 0xd8, 0xa2, 0x58, 0xa3, - 0x98, 0xa3, 0xb0, 0xa3, 0xf0, 0xa4, 0x20, 0xa5, 0x38, 0xa5, 0x90, 0xa5, - 0xa8, 0xa5, 0xf8, 0xa5, 0x10, 0xa6, 0x28, 0xa6, 0x60, 0xa6, 0x90, 0xa6, - 0xc8, 0xa6, 0xc0, 0xa8, 0xd8, 0xa8, 0x70, 0xa9, 0x88, 0xa9, 0xa0, 0xa9, - 0xb8, 0xa9, 0xd0, 0xa9, 0xe0, 0xaa, 0x40, 0xab, 0xb8, 0xab, 0xb0, 0xac, - 0xc8, 0xac, 0xe0, 0xac, 0xf8, 0xac, 0x10, 0xad, 0x40, 0xad, 0x60, 0xad, - 0x00, 0x00, 0x07, 0x00, 0x50, 0x00, 0x00, 0x00, 0xa8, 0xa2, 0xc0, 0xa2, - 0xd8, 0xa2, 0xf0, 0xa2, 0x08, 0xa3, 0x20, 0xa3, 0x40, 0xa4, 0x58, 0xa4, - 0x70, 0xa4, 0xe8, 0xa4, 0x20, 0xa5, 0x90, 0xa7, 0xa8, 0xa7, 0xc0, 0xa7, - 0xd8, 0xa7, 0xf0, 0xa7, 0x08, 0xa8, 0x20, 0xa8, 0x58, 0xa8, 0x68, 0xa9, - 0xa8, 0xaa, 0x10, 0xab, 0x30, 0xab, 0x48, 0xab, 0x60, 0xab, 0x78, 0xab, - 0x90, 0xab, 0x98, 0xac, 0x08, 0xad, 0x20, 0xad, 0x38, 0xad, 0x50, 0xad, - 0x68, 0xad, 0x80, 0xad, 0xc0, 0xad, 0x50, 0xae, 0x00, 0x10, 0x07, 0x00, - 0x50, 0x00, 0x00, 0x00, 0xe8, 0xa8, 0x18, 0xaa, 0x70, 0xab, 0x80, 0xab, - 0x90, 0xab, 0xa0, 0xab, 0xb0, 0xab, 0xc0, 0xab, 0xd0, 0xab, 0xe0, 0xab, - 0xf0, 0xab, 0x00, 0xac, 0x10, 0xac, 0x20, 0xac, 0x30, 0xac, 0x40, 0xac, - 0x50, 0xac, 0x60, 0xac, 0x70, 0xac, 0x80, 0xac, 0x90, 0xac, 0xa0, 0xac, - 0xb0, 0xac, 0xc0, 0xac, 0xd0, 0xac, 0xe0, 0xac, 0xf0, 0xac, 0x00, 0xad, - 0x10, 0xad, 0x20, 0xad, 0x30, 0xad, 0x40, 0xad, 0x50, 0xad, 0x60, 0xad, - 0x40, 0xaf, 0x00, 0x00, 0x00, 0x30, 0x07, 0x00, 0x2c, 0x00, 0x00, 0x00, - 0x20, 0xa1, 0x78, 0xa1, 0xe8, 0xa2, 0x00, 0xa3, 0x50, 0xa3, 0x60, 0xa3, - 0x70, 0xa3, 0x80, 0xa3, 0x90, 0xa3, 0xc0, 0xa3, 0xf8, 0xa3, 0x40, 0xa4, - 0x90, 0xa4, 0xc8, 0xa4, 0xe0, 0xa4, 0x28, 0xa5, 0xb0, 0xa5, 0x00, 0x00, - 0x00, 0x40, 0x07, 0x00, 0x54, 0x00, 0x00, 0x00, 0x68, 0xa2, 0x80, 0xa2, - 0xb8, 0xa2, 0xe8, 0xa2, 0x00, 0xa3, 0x18, 0xa3, 0x30, 0xa3, 0x48, 0xa3, - 0x60, 0xa3, 0x78, 0xa3, 0x90, 0xa3, 0xd0, 0xa3, 0xe8, 0xa3, 0x00, 0xa4, - 0x18, 0xa4, 0x30, 0xa4, 0x68, 0xa4, 0x98, 0xa4, 0xb0, 0xa4, 0xc8, 0xa4, - 0xe0, 0xa4, 0x18, 0xa5, 0x30, 0xa5, 0x48, 0xa5, 0x60, 0xa5, 0x78, 0xa5, - 0x90, 0xa5, 0x20, 0xa6, 0x68, 0xac, 0x78, 0xac, 0xa8, 0xac, 0xc0, 0xac, - 0x08, 0xad, 0x18, 0xad, 0x28, 0xad, 0x50, 0xad, 0x88, 0xad, 0x00, 0x00, - 0x00, 0x50, 0x07, 0x00, 0x50, 0x00, 0x00, 0x00, 0xc8, 0xa2, 0xd8, 0xa2, - 0xe8, 0xa2, 0xf8, 0xa2, 0x48, 0xa3, 0x58, 0xa3, 0x68, 0xa3, 0x78, 0xa3, - 0x88, 0xa3, 0xb0, 0xa3, 0xc0, 0xa3, 0xd0, 0xa3, 0x38, 0xa4, 0x48, 0xa4, - 0x58, 0xa4, 0xa0, 0xa4, 0xb0, 0xa4, 0xe8, 0xa4, 0xf8, 0xa4, 0x20, 0xa5, - 0x30, 0xa5, 0x68, 0xa5, 0x80, 0xa5, 0xd8, 0xa5, 0x48, 0xac, 0x60, 0xac, - 0x98, 0xac, 0xe8, 0xac, 0x30, 0xad, 0x40, 0xad, 0xc8, 0xae, 0x00, 0xaf, - 0x18, 0xaf, 0xe0, 0xaf, 0xf8, 0xaf, 0x00, 0x00, 0x00, 0x60, 0x07, 0x00, - 0x90, 0x00, 0x00, 0x00, 0xa8, 0xa0, 0xc0, 0xa0, 0x10, 0xa1, 0x20, 0xa1, - 0x50, 0xa1, 0x88, 0xa1, 0x98, 0xa1, 0xe8, 0xa1, 0xf8, 0xa1, 0x40, 0xa2, - 0x50, 0xa2, 0xe0, 0xa2, 0xf0, 0xa2, 0x28, 0xa3, 0x38, 0xa3, 0x68, 0xa3, - 0x78, 0xa3, 0x90, 0xa3, 0xd0, 0xa3, 0xe0, 0xa3, 0xf8, 0xa3, 0x50, 0xa4, - 0x60, 0xa4, 0x78, 0xa4, 0x90, 0xa4, 0xa8, 0xa4, 0xe0, 0xa4, 0xf8, 0xa4, - 0x10, 0xa5, 0x28, 0xa5, 0xc0, 0xa5, 0xd8, 0xa5, 0xf0, 0xa5, 0x08, 0xa6, - 0x20, 0xa6, 0x38, 0xa6, 0x50, 0xa6, 0x20, 0xa8, 0x38, 0xa8, 0x78, 0xa8, - 0x90, 0xa8, 0xa8, 0xa8, 0xc0, 0xa8, 0x00, 0xa9, 0x80, 0xa9, 0x98, 0xa9, - 0xb0, 0xa9, 0xc8, 0xa9, 0xe0, 0xa9, 0xf8, 0xa9, 0x10, 0xaa, 0x28, 0xaa, - 0x40, 0xaa, 0x58, 0xaa, 0x70, 0xaa, 0x88, 0xaa, 0xa0, 0xaa, 0xf0, 0xac, - 0x08, 0xad, 0x00, 0xae, 0xa8, 0xae, 0x58, 0xaf, 0x70, 0xaf, 0x88, 0xaf, - 0xa0, 0xaf, 0xb8, 0xaf, 0xd0, 0xaf, 0xe8, 0xaf, 0x00, 0x70, 0x07, 0x00, - 0x14, 0x00, 0x00, 0x00, 0x50, 0xa0, 0x68, 0xa0, 0x80, 0xa0, 0xf0, 0xa0, - 0x20, 0xa1, 0x00, 0x00, 0x00, 0x90, 0x07, 0x00, 0x14, 0x00, 0x00, 0x00, - 0xf0, 0xa6, 0xf8, 0xa6, 0x00, 0xa7, 0x08, 0xa7, 0x10, 0xa7, 0x18, 0xa7, - 0x00, 0x30, 0x1e, 0x00, 0x14, 0x00, 0x00, 0x00, 0x80, 0xa7, 0x90, 0xa8, - 0x98, 0xa8, 0xa0, 0xa8, 0xa8, 0xa8, 0x00, 0x00, 0x00, 0x50, 0x1f, 0x00, - 0x0c, 0x00, 0x00, 0x00, 0xe0, 0xae, 0x00, 0x00, 0x00, 0x20, 0x63, 0x00, - 0x0c, 0x00, 0x00, 0x00, 0x20, 0xa0, 0x00, 0x00, + 0x60, 0xa9, 0x08, 0xaf, 0x10, 0xaf, 0x18, 0xaf, 0x20, 0xaf, 0x28, 0xaf, + 0x30, 0xaf, 0x38, 0xaf, 0x40, 0xaf, 0x48, 0xaf, 0x50, 0xaf, 0x58, 0xaf, + 0x60, 0xaf, 0x68, 0xaf, 0x70, 0xaf, 0x78, 0xaf, 0x80, 0xaf, 0x88, 0xaf, + 0x90, 0xaf, 0x98, 0xaf, 0xa0, 0xaf, 0xa8, 0xaf, 0xb0, 0xaf, 0x00, 0x00, + 0x00, 0x30, 0x08, 0x00, 0x74, 0x00, 0x00, 0x00, 0x00, 0xa0, 0x10, 0xa0, + 0x40, 0xa0, 0x78, 0xa0, 0x88, 0xa0, 0xd8, 0xa0, 0xe8, 0xa0, 0x30, 0xa1, + 0x40, 0xa1, 0xd0, 0xa5, 0xe0, 0xa5, 0x18, 0xa6, 0x28, 0xa6, 0x58, 0xa6, + 0x68, 0xa6, 0x80, 0xa6, 0xc0, 0xa6, 0xd0, 0xa6, 0xe8, 0xa6, 0x40, 0xa7, + 0x50, 0xa7, 0x68, 0xa7, 0x80, 0xa7, 0x98, 0xa7, 0x18, 0xa8, 0x30, 0xa8, + 0x48, 0xa8, 0x60, 0xa8, 0x78, 0xa8, 0x90, 0xa8, 0xa8, 0xa8, 0x78, 0xaa, + 0x90, 0xaa, 0xd0, 0xaa, 0xe8, 0xaa, 0x00, 0xab, 0x18, 0xab, 0x58, 0xab, + 0xd8, 0xab, 0xf0, 0xab, 0x08, 0xac, 0x20, 0xac, 0x38, 0xac, 0x50, 0xac, + 0x68, 0xac, 0x80, 0xac, 0x98, 0xac, 0xb0, 0xac, 0xc8, 0xac, 0xe0, 0xac, + 0xf8, 0xac, 0x78, 0xaf, 0x90, 0xaf, 0xa8, 0xaf, 0x00, 0x40, 0x08, 0x00, + 0x1c, 0x00, 0x00, 0x00, 0x18, 0xa0, 0x40, 0xa1, 0xa0, 0xa1, 0xf8, 0xa1, + 0x10, 0xa2, 0x28, 0xa2, 0x40, 0xa2, 0x58, 0xa2, 0x70, 0xa2, 0x88, 0xa2, + 0x00, 0x50, 0x08, 0x00, 0x0c, 0x00, 0x00, 0x00, 0xe0, 0xa1, 0x00, 0x00, + 0x00, 0x60, 0x08, 0x00, 0x14, 0x00, 0x00, 0x00, 0x08, 0xa7, 0x10, 0xa7, + 0x18, 0xa7, 0x20, 0xa7, 0x28, 0xa7, 0x30, 0xa7, 0x00, 0x00, 0x1f, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x80, 0xa7, 0x88, 0xa7, 0x90, 0xa7, 0x98, 0xa7, + 0x00, 0x10, 0x1f, 0x00, 0x0c, 0x00, 0x00, 0x00, 0xe0, 0xa2, 0x00, 0x00, + 0x00, 0x30, 0x20, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x28, 0xad, 0x00, 0x00, + 0x00, 0xc0, 0x70, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x20, 0xa0, 0x00, 0x00, ]; diff --git a/scripts/qemu-e2e.sh b/scripts/qemu-e2e.sh index b123d1b4..ebf3abda 100755 --- a/scripts/qemu-e2e.sh +++ b/scripts/qemu-e2e.sh @@ -155,7 +155,7 @@ run_qemu() { local verbose="$6" local serial_log="$7" - local qemu_args=( + local qemu_args=(-smp 4 -m 512M -bios "$ovmf" -drive "format=raw,file=$esp_img" diff --git a/setup-dev.sh b/setup-dev.sh index 22b63afa..c26e10fe 100755 --- a/setup-dev.sh +++ b/setup-dev.sh @@ -11,6 +11,7 @@ readonly VERSION="2.0.4" # Must match the nightly in rust-toolchain.toml comments. # To update: change both here and in rust-toolchain.toml, then rebuild. readonly PINNED_NIGHTLY="nightly-2026-02-22" +readonly HELIX_PAYLOAD_MB="512" readonly C_RED='\033[0;31m' readonly C_GREEN='\033[0;32m' @@ -447,6 +448,24 @@ do_inject_apps() { log_info "Building morpheus-cli (host)..." cargo build --release -p morpheus-cli 2>&1 || { log_error "morpheus-cli build failed"; return 1; } + # Unified image layout: p1=ESP, p2=HelixFS data region. + local loopdev="" + local data_part="" + loopdev=$(sudo losetup --find --show --partscan "$disk_img") || { + log_error "Failed to attach loop device for unified image" + return 1 + } + data_part="${loopdev}p2" + for _ in {1..50}; do + [[ -b "$data_part" ]] && break + sleep 0.1 + done + if [[ ! -b "$data_part" ]]; then + sudo losetup -d "$loopdev" 2>/dev/null || true + log_error "Unified image data partition not found (${data_part})" + return 1 + fi + local injected=0 for entry in "${USER_APPS[@]}"; do local pkg="${entry%%,*}" @@ -458,17 +477,88 @@ do_inject_apps() { continue fi - log_info "Injecting ${pkg} → ${dest} (main disk HelixFS)..." - cargo run --release -p morpheus-cli -- inject "$disk_img" "$elf" --dest "$dest" 2>&1 \ - || { log_error "Inject failed for ${pkg}"; return 1; } + log_info "Injecting ${pkg} → ${dest} (HelixFS partition)..." + sudo "${PROJECT_ROOT}/target/release/morpheus-cli" inject "$data_part" "$elf" --dest "$dest" 2>&1 \ + || { + sudo losetup -d "$loopdev" 2>/dev/null || true + log_error "Inject failed for ${pkg}" + return 1 + } injected=$((injected + 1)) done + sudo losetup -d "$loopdev" 2>/dev/null || true + [[ $injected -gt 0 ]] \ && log_success "${injected} app(s) deployed to HelixFS — boot MorpheusX and run 'exec '" \ || log_warn "No apps injected" } +sync_unified_esp() { + local disk_img="${TESTING_DIR}/test-disk-50g.img" + [[ -f "$disk_img" ]] || return 1 + + set +e + local rc=0 + local loopdev="" + local esp_part="" + local mnt="" + + loopdev=$(sudo losetup --find --show --partscan "$disk_img") + if [[ -z "$loopdev" ]]; then + rc=1 + fi + + if [[ $rc -eq 0 ]]; then + esp_part="${loopdev}p1" + for _ in {1..50}; do + [[ -b "$esp_part" ]] && break + sleep 0.1 + done + [[ -b "$esp_part" ]] || rc=1 + fi + + if [[ $rc -eq 0 ]]; then + mnt=$(mktemp -d) + if ! sudo mount -t vfat "$esp_part" "$mnt"; then + log_warn "ESP mount failed; reformatting partition 1 as FAT32" + sudo partprobe "$loopdev" >/dev/null 2>&1 || true + has_cmd udevadm && sudo udevadm settle >/dev/null 2>&1 || true + + if ! sudo mkfs.vfat -F 32 -n MORPHEUS "$esp_part" >/dev/null 2>&1; then + rc=1 + else + # Retry mount a few times in case kernel uevents lag after mkfs. + local mounted=0 + for _ in {1..10}; do + if sudo mount -t vfat "$esp_part" "$mnt"; then + mounted=1 + break + fi + sleep 0.1 + done + [[ $mounted -eq 1 ]] || rc=1 + fi + fi + fi + + if [[ $rc -eq 0 ]]; then + sudo mkdir -p "$mnt/EFI/BOOT" || rc=1 + fi + + if [[ $rc -eq 0 ]]; then + sudo rsync -rtD --delete "${ESP_DIR}/" "$mnt/" || rc=1 + sync + fi + + [[ -n "$mnt" ]] && sudo umount "$mnt" >/dev/null 2>&1 + [[ -n "$mnt" ]] && rmdir "$mnt" >/dev/null 2>&1 + [[ -n "$loopdev" ]] && sudo losetup -d "$loopdev" >/dev/null 2>&1 + + set -e + return $rc +} + do_create_disk() { log_step "Creating Test Disk" @@ -479,15 +569,38 @@ do_create_disk() { local disk_img="${TESTING_DIR}/test-disk-50g.img" - # Create a raw sparse image — no partition table. - # The kernel's storage layer skips GPT/MBR disks as "boot disks", so the - # HelixFS data disk must be partition-table-free. morpheus-cli inject will - # format HelixFS on first write; no pre-formatting needed. - log_info "Creating 50GB sparse HelixFS data disk..." + # Unified image layout: + # p1: EFI System Partition (FAT32, BOOTX64.EFI) + # p2: HelixFS data partition (raw, formatted by morpheus-cli inject) + log_info "Creating 50GB unified GPT disk (ESP + HelixFS)..." rm -f "$disk_img" truncate -s 50G "$disk_img" - log_success "Test disk ready (50GB sparse, will be formatted by inject)" + parted "$disk_img" --script mklabel gpt + parted "$disk_img" --script mkpart ESP fat32 1MiB 513MiB + parted "$disk_img" --script set 1 esp on + parted "$disk_img" --script set 1 boot on + parted "$disk_img" --script mkpart HELIX 513MiB 100% + + local loopdev + loopdev=$(sudo losetup --find --show --partscan "$disk_img") || die "Failed to attach unified disk loop device" + local esp_part="${loopdev}p1" + for _ in {1..50}; do + [[ -b "$esp_part" ]] && break + sleep 0.1 + done + [[ -b "$esp_part" ]] || { sudo losetup -d "$loopdev" 2>/dev/null || true; die "ESP partition node not found"; } + + sudo mkfs.vfat -F 32 -n MORPHEUS "$esp_part" >/dev/null 2>&1 || { + sudo losetup -d "$loopdev" 2>/dev/null || true + die "Failed to format ESP partition" + } + + sudo losetup -d "$loopdev" 2>/dev/null || true + + sync_unified_esp || die "Failed to populate ESP partition from testing/esp" + + log_success "Unified disk ready (GPT: p1 ESP, p2 HelixFS)" } populate_esp_image_with_mtools() { @@ -613,10 +726,8 @@ do_launch_qemu() { -cpu host \ -bios "$ovmf_path" \ -object iothread,id=iothread0 \ - -drive file="$esp_img",format=raw,if=none,id=disk0,cache=writeback \ + -drive file="${TESTING_DIR}/test-disk-50g.img",format=raw,if=none,id=disk0,cache=writeback \ -device virtio-blk-pci,drive=disk0,disable-legacy=on,bootindex=1 \ - -drive file="${TESTING_DIR}/test-disk-50g.img",format=raw,if=none,id=disk1,cache=writeback \ - -device virtio-blk-pci,drive=disk1,disable-legacy=on,iothread=iothread0 \ "${net_args[@]}" \ -smp 8 \ -m 12G \ @@ -857,6 +968,9 @@ do_launch_thinkpad() { fi if [[ -f "${TESTING_DIR}/test-disk-50g.img" ]]; then + sync_unified_esp || die "Failed to refresh unified ESP partition" + + # Disk 0 (AHCI): unified GPT image (p1 ESP + p2 HelixFS) if ! run_qemu_command qemu-system-x86_64 \ -enable-kvm \ -machine q35,accel=kvm,i8042=on,usb=off \ @@ -944,6 +1058,175 @@ cmd_thinkpad() { do_launch_thinkpad } +# ══════════════════════════════════════════════════════════════════════════════ +# Flash to Physical Media +# Provisions a real block device (USB stick, SD card) with the two-partition +# layout used by the unified image — but only copies what's actually needed. +# No 50 GB dd. Just ESP contents + app injection. +# ══════════════════════════════════════════════════════════════════════════════ + +cmd_flash() { + local target="${1:-}" + print_banner + log_step "Flash to Physical Media" + + [[ -n "$target" ]] || die "Usage: $0 flash (e.g. $0 flash /dev/sdd)" + + log_step "Refreshing build artifacts" + FORCE_MODE=true + do_build + do_build_user_apps + + # Refuse to flash the device that contains the current root fs. + local root_dev + root_dev=$(findmnt -n -o SOURCE / 2>/dev/null | sed 's/[0-9]*$//' || true) + [[ -z "$root_dev" ]] && root_dev=$(df / 2>/dev/null | awk 'NR==2{print $1}' | sed 's/[0-9]*$//' || true) + if [[ -n "$root_dev" && "$(realpath "$target")" == "$(realpath "$root_dev")" ]]; then + die "Refusing to flash to the system root device (${root_dev})" + fi + + # Only write to block devices. + [[ -b "$target" ]] || die "${target} is not a block device" + + # Removable-only guard (RM flag in sysfs). If sysfs says 0 it's internal. + local sysfs_name + sysfs_name=$(basename "$target") + local rm_flag=1 + [[ -r "/sys/block/${sysfs_name}/removable" ]] && rm_flag=$(cat "/sys/block/${sysfs_name}/removable") + if [[ "$rm_flag" != "1" ]]; then + log_warn "${target} does not report as removable (sysfs RM=0)" + printf "${C_RED}${C_BOLD}This looks like an internal disk. Type CONFIRM to proceed anyway: ${C_RESET}" + read -r answer + [[ "$answer" == "CONFIRM" ]] || die "Aborted" + fi + + # Show what we're about to do and ask for confirmation. + local dev_size + dev_size=$(lsblk -dno SIZE "$target" 2>/dev/null || echo "?") + local dev_model + dev_model=$(lsblk -dno MODEL "$target" 2>/dev/null | xargs || echo "unknown") + printf "\n" + printf " ${C_BOLD}Target :${C_RESET} ${C_CYAN}%s${C_RESET}\n" "$target" + printf " ${C_BOLD}Model :${C_RESET} %s\n" "$dev_model" + printf " ${C_BOLD}Size :${C_RESET} %s\n" "$dev_size" + printf " ${C_BOLD}Action :${C_RESET} repartition → p1 ESP (512 MiB FAT32) + p2 HELIX (rest)\n" + printf " copy ESP files only + inject apps. No full dd.\n" + printf "\n" + printf "${C_YELLOW}${C_BOLD}ALL DATA ON ${target} WILL BE LOST. Type YES to continue: ${C_RESET}" + read -r answer + [[ "$answer" == "YES" ]] || die "Aborted" + + log_step "Unmounting partitions on ${target}" + for part in $(lsblk -lno NAME "$target" | tail -n +2); do + sudo umount "/dev/${part}" 2>/dev/null && log_info "Unmounted /dev/${part}" || true + done + + log_step "Partitioning ${target}" + sudo parted "$target" --script mklabel gpt + sudo parted "$target" --script mkpart ESP fat32 1MiB 513MiB + sudo parted "$target" --script set 1 esp on + sudo parted "$target" --script set 1 boot on + sudo parted "$target" --script mkpart HELIX 513MiB 100% + sudo partprobe "$target" 2>/dev/null || true + has_cmd udevadm && sudo udevadm settle 2>/dev/null || sleep 1 + + # Resolve partition nodes (handles both /dev/sddN and /dev/mmcblkXpN). + local esp_part helix_part + if [[ -b "${target}p1" ]]; then + esp_part="${target}p1" + helix_part="${target}p2" + else + esp_part="${target}1" + helix_part="${target}2" + fi + + # Wait for nodes to appear. + for _ in {1..50}; do [[ -b "$esp_part" ]] && break; sleep 0.1; done + [[ -b "$esp_part" ]] || die "ESP partition node did not appear (${esp_part})" + for _ in {1..50}; do [[ -b "$helix_part" ]] && break; sleep 0.1; done + [[ -b "$helix_part" ]] || die "HELIX partition node did not appear (${helix_part})" + + log_step "Formatting ESP (${esp_part})" + sudo mkfs.vfat -F 32 -n MORPHEUS "$esp_part" >/dev/null \ + || die "Failed to format ESP partition" + + log_step "Copying ESP contents" + check_bootloader || die "Bootloader not built — run: $0 build" + local mnt + mnt=$(mktemp -d) + sudo mount -t vfat "$esp_part" "$mnt" || die "Failed to mount ESP on ${esp_part}" + sudo mkdir -p "$mnt/EFI/BOOT" + sudo rsync -rtD --delete "${ESP_DIR}/" "$mnt/" \ + || { sudo umount "$mnt"; rmdir "$mnt"; die "rsync to ESP failed"; } + sudo umount "$mnt" + rmdir "$mnt" + log_success "ESP written to ${esp_part}" + + log_step "Formatting HELIX (${helix_part})" + # Build morpheus-cli first. + cargo build --release -p morpheus-cli 2>&1 \ + || die "morpheus-cli build failed" + sudo "${PROJECT_ROOT}/target/release/morpheus-cli" format "$helix_part" \ + || die "Failed to format HELIX partition" + log_success "HELIX formatted — clean HelixFS ready" + + log_step "Injecting apps into HELIX (${helix_part})" + local injected=0 + for entry in "${USER_APPS[@]}"; do + local pkg="${entry%%,*}" + local dest="${entry##*,}" + local elf="target/x86_64-morpheus/release/${pkg}" + if [[ ! -f "$elf" ]]; then + log_warn "${pkg}: ELF not found (build first), skipping" + continue + fi + log_info "Injecting ${pkg} → ${dest}" + sudo "${PROJECT_ROOT}/target/release/morpheus-cli" inject "$helix_part" "$elf" --dest "$dest" 2>&1 \ + || { log_error "Inject failed for ${pkg}"; continue; } + injected=$((injected + 1)) + done + + log_step "Writing helix.img to ESP (${esp_part})" + mnt=$(mktemp -d) + sudo mount -t vfat "$esp_part" "$mnt" || { + die "Failed to remount ESP on ${esp_part}" + } + sudo mkdir -p "$mnt/morpheus" + + # Auto-fit payload to available ESP space with safety headroom for FAT metadata. + local esp_free_bytes + esp_free_bytes=$(df -B1 --output=avail "$mnt" | tail -n 1 | tr -d '[:space:]') + local reserve_bytes=$((24 * 1024 * 1024)) + local max_payload_bytes=$((HELIX_PAYLOAD_MB * 1024 * 1024)) + local payload_bytes=0 + if [[ "$esp_free_bytes" -gt "$reserve_bytes" ]]; then + payload_bytes=$((esp_free_bytes - reserve_bytes)) + if [[ "$payload_bytes" -gt "$max_payload_bytes" ]]; then + payload_bytes=$max_payload_bytes + fi + fi + + local payload_mb=$((payload_bytes / 1024 / 1024)) + if [[ "$payload_mb" -lt 64 ]]; then + sudo umount "$mnt" || true + rmdir "$mnt" || true + die "ESP free space too small for helix.img payload (need >=64 MiB usable)" + fi + + log_info "helix.img payload size: ${payload_mb} MiB (ESP free: $((esp_free_bytes / 1024 / 1024)) MiB)" + if ! sudo "${PROJECT_ROOT}/target/release/morpheus-cli" pack "$helix_part" "$mnt/morpheus/helix.img" --max-mb "$payload_mb"; then + sudo umount "$mnt" || true + rmdir "$mnt" || true + die "Failed to write helix.img to ESP" + fi + sudo umount "$mnt" + rmdir "$mnt" + + sudo sync + log_success "Flash complete — ${injected} app(s) on ${helix_part}, helix.img on ${esp_part}:/morpheus/helix.img" + printf "\n Eject the card and boot the ThinkPad from it.\n\n" +} + cmd_clean() { print_banner log_step "Cleaning" @@ -1003,6 +1286,7 @@ usage() { printf " ${C_CYAN}disk${C_RESET} [target] Create disk image (50g|info)\n" printf " ${C_CYAN}run${C_RESET} Launch QEMU (VirtIO devices)\n" printf " ${C_CYAN}thinkpad${C_RESET} Launch QEMU with ThinkPad T450s hardware\n" + printf " ${C_CYAN}flash${C_RESET} Write to physical media (SD/USB) — no full dd\n" printf " ${C_CYAN}deploy${C_RESET} Build & inject user apps into HelixFS\n" printf " ${C_CYAN}status${C_RESET} Show environment status\n" printf " ${C_CYAN}clean${C_RESET} Remove artifacts\n" @@ -1022,6 +1306,8 @@ usage() { printf " %s build\n\n" "$(basename "$0")" printf " ${C_DIM}# Test with ThinkPad T450s hardware (AHCI + Intel e1000)${C_RESET}\n" printf " %s thinkpad\n\n" "$(basename "$0")" + printf " ${C_DIM}# Flash to real SD card / USB stick (fast — ESP files + apps only)${C_RESET}\n" + printf " %s flash /dev/sdd\n\n" "$(basename "$0")" } main() { @@ -1062,6 +1348,7 @@ main() { disk|image) cmd_disk "${args[@]:-}" ;; run|start|qemu) cmd_run ;; thinkpad|t450s) cmd_thinkpad ;; + flash) cmd_flash "${args[0]:-}" ;; status|info) cmd_status ;; clean|purge) cmd_clean ;; *) die "Unknown command: $cmd" ;; diff --git a/shelld/Cargo.toml b/shelld/Cargo.toml index 92c3ad82..9cb032eb 100644 --- a/shelld/Cargo.toml +++ b/shelld/Cargo.toml @@ -12,6 +12,3 @@ libmorpheus = { path = "../libmorpheus" } channel = { path = "../channel" } display = { path = "../display", features = ["framebuffer-backend"], package = "morpheus-display" } -[profile.release] -opt-level = 3 -lto = true diff --git a/shelld/src/islands/launcher.rs b/shelld/src/islands/launcher.rs index 7a4cfaf8..14030fdf 100644 --- a/shelld/src/islands/launcher.rs +++ b/shelld/src/islands/launcher.rs @@ -125,8 +125,8 @@ pub fn handle_click(state: &mut ShellState, mx: i32, my: i32) -> bool { Ok(_pid) => { io::println("shelld: spawned msh"); } - Err(_) => { - io::println("shelld: failed to spawn msh"); + Err(e) => { + libmorpheus::println!("shelld: failed to spawn msh err=0x{:x}", e); } } // close launcher after spawning diff --git a/wait_test.rs b/wait_test.rs new file mode 100644 index 00000000..d43c694e --- /dev/null +++ b/wait_test.rs @@ -0,0 +1,3 @@ +pub fn main() { + let freq = 0; // or something +}