diff --git a/kernel/src/ACPI/AML/AmlParser.cpp b/kernel/src/ACPI/AML/AmlParser.cpp new file mode 100644 index 0000000..b48fd5e --- /dev/null +++ b/kernel/src/ACPI/AML/AmlParser.cpp @@ -0,0 +1,169 @@ +/* + * AmlParser.cpp + * Primitive AML bytecode parser for extracting ACPI sleep state values + * Copyright (c) 2026 Daniel Hammer +*/ + +#include "AmlParser.hpp" +#include +#include +#include + +using namespace Kt; + +namespace Hal { + namespace AML { + + // Decode a PkgLength field and return its value. + // Advances *pos past the PkgLength bytes. + static uint32_t DecodePkgLength(const uint8_t* aml, uint32_t* pos) { + uint8_t lead = aml[*pos]; + uint32_t byteCount = (lead >> 6) & 0x03; + + if (byteCount == 0) { + // Single byte encoding: bits 0-5 are the length + (*pos)++; + return lead & 0x3F; + } + + // Multi-byte: lead bits 0-3 are low nibble, followed by byteCount bytes + uint32_t length = lead & 0x0F; + (*pos)++; + + for (uint32_t i = 0; i < byteCount; i++) { + length |= (uint32_t)aml[*pos] << (4 + 8 * i); + (*pos)++; + } + + return length; + } + + // Decode an AML integer at position *pos. + // Handles ZeroOp, OneOp, OnesOp, BytePrefix, WordPrefix, DWordPrefix. + // Returns the decoded value and advances *pos. + static uint32_t DecodeInteger(const uint8_t* aml, uint32_t* pos) { + uint8_t op = aml[*pos]; + + switch (op) { + case ZeroOp: + (*pos)++; + return 0; + case OneOp: + (*pos)++; + return 1; + case OnesOp: + (*pos)++; + return 0xFFFFFFFF; + case BytePrefix: { + (*pos)++; + uint8_t val = aml[*pos]; + (*pos)++; + return val; + } + case WordPrefix: { + (*pos)++; + uint16_t val = aml[*pos] | ((uint16_t)aml[*pos + 1] << 8); + *pos += 2; + return val; + } + case DWordPrefix: { + (*pos)++; + uint32_t val = aml[*pos] + | ((uint32_t)aml[*pos + 1] << 8) + | ((uint32_t)aml[*pos + 2] << 16) + | ((uint32_t)aml[*pos + 3] << 24); + *pos += 4; + return val; + } + default: + // Unknown encoding — treat as zero and skip + (*pos)++; + return 0; + } + } + + S5Object FindS5(void* dsdtData) { + S5Object result{}; + result.Valid = false; + + auto* header = (ACPI::CommonSDTHeader*)dsdtData; + + if (!ACPI::TestChecksum(header)) { + KernelLogStream(ERROR, "AML") << "DSDT checksum failed"; + return result; + } + + // The AML bytecode starts right after the CommonSDTHeader + const uint8_t* aml = (const uint8_t*)dsdtData; + uint32_t amlLength = header->Length; + uint32_t dataStart = sizeof(ACPI::CommonSDTHeader); + + // Scan for the \_S5_ name in the AML stream. + // We look for the 4-byte sequence '_S5_' preceded by a NameOp (0x08) + // or preceded by a scope path like '\' (0x5C). + for (uint32_t i = dataStart; i + 4 < amlLength; i++) { + if (aml[i] == '_' && aml[i+1] == 'S' && aml[i+2] == '5' && aml[i+3] == '_') { + // Verify a valid AML context: either NameOp before it, + // or '\' + NameOp pattern, or just the name in a scope + bool validContext = false; + + if (i >= 1 && aml[i-1] == NameOp) { + validContext = true; + } else if (i >= 2 && aml[i-2] == NameOp && aml[i-1] == '\\') { + validContext = true; + } + + if (!validContext) + continue; + + KernelLogStream(OK, "AML") << "Found \\_S5_ object at offset " << base::hex << (uint64_t)i; + + // Move past the name + uint32_t pos = i + 4; + + // Expect PackageOp + if (pos >= amlLength || aml[pos] != PackageOp) { + KernelLogStream(ERROR, "AML") << "Expected PackageOp after \\_S5_, got " << base::hex << (uint64_t)aml[pos]; + continue; + } + pos++; + + // Decode package length (we don't actually need the value, + // but must advance past it) + DecodePkgLength(aml, &pos); + + // Number of elements in the package + if (pos >= amlLength) continue; + uint8_t numElements = aml[pos]; + pos++; + + if (numElements < 1) { + KernelLogStream(ERROR, "AML") << "\\_S5_ package has no elements"; + continue; + } + + // First element: SLP_TYPa + result.SLP_TYPa = (uint16_t)DecodeInteger(aml, &pos); + + // Second element: SLP_TYPb (if present) + if (numElements >= 2 && pos < amlLength) { + result.SLP_TYPb = (uint16_t)DecodeInteger(aml, &pos); + } else { + result.SLP_TYPb = 0; + } + + result.Valid = true; + + KernelLogStream(OK, "AML") << "SLP_TYPa=" << base::hex << (uint64_t)result.SLP_TYPa + << " SLP_TYPb=" << base::hex << (uint64_t)result.SLP_TYPb; + + return result; + } + } + + KernelLogStream(ERROR, "AML") << "\\_S5_ object not found in DSDT"; + return result; + } + + }; +}; diff --git a/kernel/src/ACPI/AML/AmlParser.hpp b/kernel/src/ACPI/AML/AmlParser.hpp new file mode 100644 index 0000000..f468fa4 --- /dev/null +++ b/kernel/src/ACPI/AML/AmlParser.hpp @@ -0,0 +1,35 @@ +/* + * AmlParser.hpp + * Primitive AML bytecode parser for extracting ACPI sleep state values + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once +#include + +namespace Hal { + namespace AML { + + // AML opcodes used during \_S5_ parsing + static constexpr uint8_t NameOp = 0x08; + static constexpr uint8_t PackageOp = 0x12; + static constexpr uint8_t ZeroOp = 0x00; + static constexpr uint8_t OneOp = 0x01; + static constexpr uint8_t OnesOp = 0xFF; + static constexpr uint8_t BytePrefix = 0x0A; + static constexpr uint8_t WordPrefix = 0x0B; + static constexpr uint8_t DWordPrefix = 0x0C; + + struct S5Object { + uint16_t SLP_TYPa; + uint16_t SLP_TYPb; + bool Valid; + }; + + // Parse a DSDT (or SSDT) AML block to find the \_S5_ object. + // dsdtData points to the CommonSDTHeader of the DSDT (HHDM-mapped). + // Returns the parsed S5 values on success. + S5Object FindS5(void* dsdtData); + + }; +}; diff --git a/kernel/src/ACPI/AcpiShutdown.cpp b/kernel/src/ACPI/AcpiShutdown.cpp new file mode 100644 index 0000000..138b78c --- /dev/null +++ b/kernel/src/ACPI/AcpiShutdown.cpp @@ -0,0 +1,78 @@ +/* + * AcpiShutdown.cpp + * ACPI S5 (soft-off) shutdown via PM1 control registers + * Copyright (c) 2026 Daniel Hammer +*/ + +#include "AcpiShutdown.hpp" +#include +#include +#include +#include +#include +#include + +using namespace Kt; + +namespace Hal { + namespace AcpiShutdown { + + static constexpr uint16_t SLP_EN = 1 << 13; + + static bool g_available = false; + static uint32_t g_pm1aControlBlock = 0; + static uint32_t g_pm1bControlBlock = 0; + static uint16_t g_slpTypA = 0; + static uint16_t g_slpTypB = 0; + + void Initialize(ACPI::CommonSDTHeader* xsdt) { + FADT::ParsedFADT fadt{}; + if (!FADT::Parse(xsdt, fadt) || !fadt.Valid) { + KernelLogStream(ERROR, "ACPI") << "Failed to parse FADT — ACPI shutdown unavailable"; + return; + } + + // Map and parse the DSDT to find \_S5_ + auto* dsdt = (void*)Memory::HHDM(fadt.DsdtAddress); + AML::S5Object s5 = AML::FindS5(dsdt); + + if (!s5.Valid) { + KernelLogStream(ERROR, "ACPI") << "Could not find \\_S5_ in DSDT — ACPI shutdown unavailable"; + return; + } + + g_pm1aControlBlock = fadt.PM1aControlBlock; + g_pm1bControlBlock = fadt.PM1bControlBlock; + g_slpTypA = s5.SLP_TYPa; + g_slpTypB = s5.SLP_TYPb; + g_available = true; + + KernelLogStream(OK, "ACPI") << "ACPI shutdown initialized (PM1a=" << base::hex + << (uint64_t)g_pm1aControlBlock << " SLP_TYPa=" << (uint64_t)g_slpTypA << ")"; + } + + bool IsAvailable() { + return g_available; + } + + void Shutdown() { + if (!g_available) return; + + KernelLogStream(INFO, "ACPI") << "Performing ACPI S5 shutdown..."; + + // Write SLP_TYPa | SLP_EN to PM1a control register + Io::Out16((g_slpTypA << 10) | SLP_EN, (uint16_t)g_pm1aControlBlock); + + // If PM1b exists, write to it as well + if (g_pm1bControlBlock != 0) { + Io::Out16((g_slpTypB << 10) | SLP_EN, (uint16_t)g_pm1bControlBlock); + } + + // If we're still here, the shutdown didn't work immediately. + // Halt and wait for the hardware to power off. + for (;;) { + asm volatile("hlt"); + } + } + }; +}; diff --git a/kernel/src/ACPI/AcpiShutdown.hpp b/kernel/src/ACPI/AcpiShutdown.hpp new file mode 100644 index 0000000..4e8373c --- /dev/null +++ b/kernel/src/ACPI/AcpiShutdown.hpp @@ -0,0 +1,24 @@ +/* + * AcpiShutdown.hpp + * ACPI S5 (soft-off) shutdown via PM1 control registers + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once +#include + +namespace Hal { + namespace AcpiShutdown { + + // Parse the FADT and DSDT from the XSDT to prepare shutdown values. + // Must be called during boot before any shutdown attempt. + void Initialize(ACPI::CommonSDTHeader* xsdt); + + // Returns true if ACPI shutdown is available (Initialize succeeded). + bool IsAvailable(); + + // Perform an ACPI S5 shutdown by writing to PM1a/PM1b control registers. + // Does not return on success. + void Shutdown(); + }; +}; diff --git a/kernel/src/ACPI/FADT.cpp b/kernel/src/ACPI/FADT.cpp new file mode 100644 index 0000000..cf2d7f4 --- /dev/null +++ b/kernel/src/ACPI/FADT.cpp @@ -0,0 +1,78 @@ +/* + * FADT.cpp + * Fixed ACPI Description Table parsing + * Copyright (c) 2026 Daniel Hammer +*/ + +#include "FADT.hpp" +#include +#include +#include + +using namespace Kt; + +namespace Hal { + namespace FADT { + + static bool SignatureMatch(const char* sig, const char* target, int len) { + for (int i = 0; i < len; i++) { + if (sig[i] != target[i]) return false; + } + return true; + } + + static Table* FindFADTInXSDT(ACPI::CommonSDTHeader* xsdt) { + uint32_t entryCount = (xsdt->Length - sizeof(ACPI::CommonSDTHeader)) / 8; + uint64_t* entries = (uint64_t*)((uint64_t)xsdt + sizeof(ACPI::CommonSDTHeader)); + + for (uint32_t i = 0; i < entryCount; i++) { + auto* header = (ACPI::CommonSDTHeader*)Memory::HHDM(entries[i]); + if (SignatureMatch(header->Signature, "FACP", 4)) { + return (Table*)header; + } + } + + return nullptr; + } + + bool Parse(ACPI::CommonSDTHeader* xsdt, ParsedFADT& result) { + result.Valid = false; + + auto* fadt = FindFADTInXSDT(xsdt); + if (fadt == nullptr) { + KernelLogStream(ERROR, "FADT") << "FADT table not found in XSDT"; + return false; + } + + if (!ACPI::TestChecksum(&fadt->Header)) { + KernelLogStream(ERROR, "FADT") << "FADT checksum failed"; + return false; + } + + KernelLogStream(OK, "FADT") << "Found FADT table"; + + // Prefer X_Dsdt (64-bit) if available, otherwise fall back to 32-bit Dsdt + if (fadt->Header.Length >= offsetof(Table, X_Dsdt) + 8 && fadt->X_Dsdt != 0) { + result.DsdtAddress = fadt->X_Dsdt; + } else { + result.DsdtAddress = fadt->Dsdt; + } + + result.PM1aControlBlock = fadt->PM1aControlBlock; + result.PM1bControlBlock = fadt->PM1bControlBlock; + result.SMI_CommandPort = fadt->SMI_CommandPort; + result.AcpiEnable = fadt->AcpiEnable; + result.AcpiDisable = fadt->AcpiDisable; + result.Valid = true; + + KernelLogStream(INFO, "FADT") << "DSDT at " << base::hex << result.DsdtAddress; + KernelLogStream(INFO, "FADT") << "PM1a_CNT_BLK: " << base::hex << (uint64_t)result.PM1aControlBlock; + + if (result.PM1bControlBlock != 0) { + KernelLogStream(INFO, "FADT") << "PM1b_CNT_BLK: " << base::hex << (uint64_t)result.PM1bControlBlock; + } + + return true; + } + }; +}; diff --git a/kernel/src/ACPI/FADT.hpp b/kernel/src/ACPI/FADT.hpp new file mode 100644 index 0000000..414a9ab --- /dev/null +++ b/kernel/src/ACPI/FADT.hpp @@ -0,0 +1,84 @@ +/* + * FADT.hpp + * Fixed ACPI Description Table parsing + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once +#include +#include + +namespace Hal { + namespace FADT { + + struct Table { + ACPI::CommonSDTHeader Header; + uint32_t FirmwareCtrl; + uint32_t Dsdt; + uint8_t Reserved0; + uint8_t PreferredPMProfile; + uint16_t SCI_Interrupt; + uint32_t SMI_CommandPort; + uint8_t AcpiEnable; + uint8_t AcpiDisable; + uint8_t S4BIOS_REQ; + uint8_t PSTATE_Control; + uint32_t PM1aEventBlock; + uint32_t PM1bEventBlock; + uint32_t PM1aControlBlock; + uint32_t PM1bControlBlock; + uint32_t PM2ControlBlock; + uint32_t PMTimerBlock; + uint32_t GPE0Block; + uint32_t GPE1Block; + uint8_t PM1EventLength; + uint8_t PM1ControlLength; + uint8_t PM2ControlLength; + uint8_t PMTimerLength; + uint8_t GPE0Length; + uint8_t GPE1Length; + uint8_t GPE1Base; + uint8_t CStateControl; + uint16_t WorstC2Latency; + uint16_t WorstC3Latency; + uint16_t FlushSize; + uint16_t FlushStride; + uint8_t DutyOffset; + uint8_t DutyWidth; + uint8_t DayAlarm; + uint8_t MonthAlarm; + uint8_t Century; + uint16_t BootArchitectureFlags; + uint8_t Reserved1; + uint32_t Flags; + // Generic Address Structure for RESET_REG (12 bytes) + uint8_t ResetReg[12]; + uint8_t ResetValue; + uint16_t ArmBootArchFlags; + uint8_t FADTMinorVersion; + // Extended fields (ACPI 2.0+) + uint64_t X_FirmwareControl; + uint64_t X_Dsdt; + uint8_t X_PM1aEventBlock[12]; + uint8_t X_PM1bEventBlock[12]; + uint8_t X_PM1aControlBlock[12]; + uint8_t X_PM1bControlBlock[12]; + uint8_t X_PM2ControlBlock[12]; + uint8_t X_PMTimerBlock[12]; + uint8_t X_GPE0Block[12]; + uint8_t X_GPE1Block[12]; + } __attribute__((packed)); + + struct ParsedFADT { + uint64_t DsdtAddress; + uint32_t PM1aControlBlock; + uint32_t PM1bControlBlock; + uint32_t SMI_CommandPort; + uint8_t AcpiEnable; + uint8_t AcpiDisable; + bool Valid; + }; + + bool Parse(ACPI::CommonSDTHeader* xsdt, ParsedFADT& result); + }; +}; diff --git a/kernel/src/Api/Device.hpp b/kernel/src/Api/Device.hpp index 1648203..0d5bfa8 100644 --- a/kernel/src/Api/Device.hpp +++ b/kernel/src/Api/Device.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include "Syscall.hpp" @@ -128,7 +129,30 @@ namespace Montauk { } } - // PCI devices (category 7) + // Storage (category 7) + if (Drivers::Storage::Ahci::IsInitialized()) { + for (int port = 0; port < 32 && count < maxCount; port++) { + auto* info = Drivers::Storage::Ahci::GetPortInfo(port); + if (!info) continue; + uint64_t sectors = info->SectorCount; + uint64_t sizeMB = (sectors * 512) / (1024 * 1024); + uint64_t sizeGB = sizeMB / 1024; + char detail[48]; + int p = 0; + if (sizeGB > 0) { + p = dl_append_dec(detail, p, (int)sizeGB, 48); + p = dl_append(detail, p, " GiB, SATA port ", 48); + } else { + p = dl_append_dec(detail, p, (int)sizeMB, 48); + p = dl_append(detail, p, " MiB, SATA port ", 48); + } + p = dl_append_dec(detail, p, port, 48); + add(7, info->Model, detail); + buf[count - 1]._pad[0] = (uint8_t)port; // stash port index + } + } + + // PCI devices (category 8) auto& pciDevs = Pci::GetDevices(); for (int i = 0; i < (int)pciDevs.size() && count < maxCount; i++) { auto& d = pciDevs[i]; @@ -144,9 +168,37 @@ namespace Montauk { p = dl_append_hex(detail, p, d.VendorId, 4, 48); p = dl_append(detail, p, ":", 48); p = dl_append_hex(detail, p, d.DeviceId, 4, 48); - add(7, className, detail); + add(8, className, detail); } return count; } + + static int Sys_DiskInfo(DiskInfo* buf, int port) { + if (buf == nullptr) return -1; + if (!Drivers::Storage::Ahci::IsInitialized()) return -1; + + auto* info = Drivers::Storage::Ahci::GetPortInfo(port); + if (!info) return -1; + + buf->port = info->PortIndex; + buf->type = (uint8_t)info->Type; + buf->sataGen = (uint8_t)info->SataGen; + buf->sectorCount = info->SectorCount; + buf->sectorSizeLog = info->SectorSizeLog; + buf->sectorSizePhys = info->SectorSizePhys; + buf->rpm = info->Rpm; + buf->ncqDepth = info->NcqDepth; + buf->supportsLba48 = info->SupportsLba48 ? 1 : 0; + buf->supportsNcq = info->SupportsNcq ? 1 : 0; + buf->supportsTrim = info->SupportsTrim ? 1 : 0; + buf->supportsSmart = info->SupportsSmart ? 1 : 0; + buf->supportsWriteCache = info->SupportsWriteCache ? 1 : 0; + buf->supportsReadAhead = info->SupportsReadAhead ? 1 : 0; + dl_strcpy(buf->model, info->Model, 41); + dl_strcpy(buf->serial, info->Serial, 21); + dl_strcpy(buf->firmware, info->Firmware, 9); + + return 0; + } }; diff --git a/kernel/src/Api/Power.hpp b/kernel/src/Api/Power.hpp index 07b4f93..22f74d6 100644 --- a/kernel/src/Api/Power.hpp +++ b/kernel/src/Api/Power.hpp @@ -8,6 +8,7 @@ #include #include #include +#include namespace Montauk { @@ -25,13 +26,18 @@ namespace Montauk { } static void Sys_Shutdown() { + /* Primary: ACPI S5 shutdown via PM1 control registers */ + if (Hal::AcpiShutdown::IsAvailable()) { + Hal::AcpiShutdown::Shutdown(); + } + + /* Fallback: EFI ResetSystem */ if (Efi::g_ResetSystem) { - /* Switch to kernel PML4 which has identity-mapped UEFI runtime regions */ Memory::VMM::LoadCR3(Memory::VMM::g_paging->PML4); Efi::g_ResetSystem(Efi::EfiResetShutdown, 0, 0, nullptr); } - /* No fallback for shutdown; halt the CPU */ + /* Last resort: halt the CPU */ asm volatile("cli; hlt"); __builtin_unreachable(); } diff --git a/kernel/src/Api/Syscall.cpp b/kernel/src/Api/Syscall.cpp index c459016..5e5c015 100644 --- a/kernel/src/Api/Syscall.cpp +++ b/kernel/src/Api/Syscall.cpp @@ -209,6 +209,8 @@ namespace Montauk { } case SYS_DEVLIST: return (int64_t)Sys_DevList((DevInfo*)frame->arg1, (int)frame->arg2); + case SYS_DISKINFO: + return (int64_t)Sys_DiskInfo((DiskInfo*)frame->arg1, (int)frame->arg2); case SYS_WINSETSCALE: return (int64_t)Sys_WinSetScale((int)frame->arg1); case SYS_WINGETSCALE: diff --git a/kernel/src/Api/Syscall.hpp b/kernel/src/Api/Syscall.hpp index 87f5ad9..2687ec5 100644 --- a/kernel/src/Api/Syscall.hpp +++ b/kernel/src/Api/Syscall.hpp @@ -132,6 +132,7 @@ namespace Montauk { /* Device.hpp */ static constexpr uint64_t SYS_DEVLIST = 63; + static constexpr uint64_t SYS_DISKINFO = 69; /* MemInfo.hpp */ static constexpr uint64_t SYS_MEMSTATS = 67; @@ -224,12 +225,35 @@ namespace Montauk { }; struct DevInfo { - uint8_t category; // 0=CPU, 1=Interrupt, 2=Timer, 3=Input, 4=USB, 5=Network, 6=Display, 7=PCI + uint8_t category; // 0=CPU, 1=Interrupt, 2=Timer, 3=Input, 4=USB, 5=Network, 6=Display, 7=Storage, 8=PCI uint8_t _pad[3]; char name[48]; char detail[48]; }; + struct DiskInfo { + uint8_t port; // AHCI port index + uint8_t type; // 0=none, 1=SATA, 2=SATAPI + uint8_t sataGen; // SATA gen (1/2/3) + uint8_t _pad0; + uint64_t sectorCount; // Total user-addressable sectors + uint16_t sectorSizeLog; // Logical sector size (bytes) + uint16_t sectorSizePhys; // Physical sector size (bytes) + uint16_t rpm; // 0=unknown, 1=SSD, else RPM + uint16_t ncqDepth; // 0 if no NCQ + uint8_t supportsLba48; + uint8_t supportsNcq; + uint8_t supportsTrim; + uint8_t supportsSmart; + uint8_t supportsWriteCache; + uint8_t supportsReadAhead; + uint8_t _pad1[2]; + char model[41]; + char serial[21]; + char firmware[9]; + char _pad2[1]; + }; + struct ProcInfo { int32_t pid; int32_t parentPid; diff --git a/kernel/src/Drivers/Init.cpp b/kernel/src/Drivers/Init.cpp index 98159dc..1c170b9 100644 --- a/kernel/src/Drivers/Init.cpp +++ b/kernel/src/Drivers/Init.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -59,6 +60,10 @@ namespace Drivers { return Net::E1000E::Probe(dev); } + static bool ProbeAhci(const Pci::PciDevice& dev) { + return Storage::Ahci::Probe(dev); + } + // ------------------------------------------------------------------------- // Driver table // ------------------------------------------------------------------------- @@ -108,6 +113,18 @@ namespace Drivers { Pci::ProbePhase::Normal, ProbeE1000E, }, + // Order 5: AHCI — Normal phase, match class=0x01/0x06/0x01 (SATA AHCI) + { + "AHCI", + 0, // VendorId (any) + 0x01, // ClassCode (Mass Storage) + 0x06, // SubClass (SATA) + 0x01, // ProgIf (AHCI 1.0) + nullptr, + 0, + Pci::ProbePhase::Normal, + ProbeAhci, + }, }; static constexpr uint16_t g_driverTableCount = sizeof(g_driverTable) / sizeof(g_driverTable[0]); @@ -139,4 +156,10 @@ namespace Drivers { ::Net::Initialize(); } + void InitializeStorage() { + // AHCI driver registered SATA devices during ProbeNormal(). + // Nothing else to do here for now — the VFS registration + // is handled in Main.cpp after this call. + } + } diff --git a/kernel/src/Drivers/Init.hpp b/kernel/src/Drivers/Init.hpp index 3313eca..3ac080c 100644 --- a/kernel/src/Drivers/Init.hpp +++ b/kernel/src/Drivers/Init.hpp @@ -14,10 +14,13 @@ namespace Drivers { // Post-probe: wire up GPU framebuffer to cursor subsystem. void InitializeGraphics(); - // Probe PCI devices for Normal-phase drivers (xHCI, E1000, E1000E). + // Probe PCI devices for Normal-phase drivers (xHCI, E1000, E1000E, AHCI). void ProbeNormal(); // Post-probe: initialize network stack. void InitializeNetwork(); + // Post-probe: register SATA drives with VFS. + void InitializeStorage(); + } diff --git a/kernel/src/Drivers/Storage/Ahci.cpp b/kernel/src/Drivers/Storage/Ahci.cpp new file mode 100644 index 0000000..126c583 --- /dev/null +++ b/kernel/src/Drivers/Storage/Ahci.cpp @@ -0,0 +1,833 @@ +/* + * Ahci.cpp + * AHCI (Advanced Host Controller Interface) SATA driver + * Copyright (c) 2025 Daniel Hammer +*/ + +#include "Ahci.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Kt; + +namespace Drivers::Storage::Ahci { + + // ------------------------------------------------------------------------- + // Driver state + // ------------------------------------------------------------------------- + + static bool g_initialized = false; + static volatile uint8_t* g_mmioBase = nullptr; + static uint32_t g_portsImplemented = 0; + static int g_activePortCount = 0; + static PortInfo g_ports[MAX_PORTS] = {}; + + // ------------------------------------------------------------------------- + // Register access + // ------------------------------------------------------------------------- + + static void WriteReg(uint32_t reg, uint32_t value) { + *(volatile uint32_t*)(g_mmioBase + reg) = value; + } + + static uint32_t ReadReg(uint32_t reg) { + return *(volatile uint32_t*)(g_mmioBase + reg); + } + + static void WritePortReg(int port, uint32_t reg, uint32_t value) { + WriteReg(PORT_BASE + port * PORT_SIZE + reg, value); + } + + static uint32_t ReadPortReg(int port, uint32_t reg) { + return ReadReg(PORT_BASE + port * PORT_SIZE + reg); + } + + // ------------------------------------------------------------------------- + // DMA buffer allocation + // ------------------------------------------------------------------------- + + static void* AllocateDmaBuffer(uint64_t& outPhys, int pages = 1) { + void* virt; + if (pages == 1) { + virt = Memory::g_pfa->AllocateZeroed(); + } else { + virt = Memory::g_pfa->ReallocConsecutive(nullptr, pages); + memset(virt, 0, pages * 0x1000); + } + outPhys = Memory::SubHHDM(virt); + return virt; + } + + // ------------------------------------------------------------------------- + // BIOS/OS Handoff + // ------------------------------------------------------------------------- + + static void PerformBiosHandoff() { + uint32_t cap2 = ReadReg(REG_CAP2); + if (!(cap2 & (1u << 0))) { + return; // BOH not supported + } + + uint32_t bohc = ReadReg(REG_BOHC); + if (!(bohc & BOHC_BOS)) { + return; // BIOS doesn't own it + } + + KernelLogStream(INFO, "AHCI") << "Performing BIOS/OS handoff..."; + + // Set OS Owned Semaphore + WriteReg(REG_BOHC, bohc | BOHC_OOS); + + // Wait for BIOS to release (up to 25ms per spec) + for (int i = 0; i < 250000; i++) { + bohc = ReadReg(REG_BOHC); + if (!(bohc & BOHC_BOS)) { + KernelLogStream(OK, "AHCI") << "BIOS handoff complete"; + return; + } + asm volatile("" ::: "memory"); + } + + // If BIOS Busy, wait another 2 seconds + if (ReadReg(REG_BOHC) & BOHC_BB) { + for (int i = 0; i < 2000000; i++) { + if (!(ReadReg(REG_BOHC) & BOHC_BB)) break; + asm volatile("" ::: "memory"); + } + } + + KernelLogStream(WARNING, "AHCI") << "BIOS handoff timed out, proceeding anyway"; + } + + // ------------------------------------------------------------------------- + // Port engine control + // ------------------------------------------------------------------------- + + static void StopPort(int port) { + uint32_t cmd = ReadPortReg(port, PORT_CMD); + + // Clear ST (Stop command engine) + if (cmd & PORT_CMD_ST) { + cmd &= ~PORT_CMD_ST; + WritePortReg(port, PORT_CMD, cmd); + } + + // Clear FRE (Stop FIS receive) + if (cmd & PORT_CMD_FRE) { + cmd &= ~PORT_CMD_FRE; + WritePortReg(port, PORT_CMD, cmd); + } + + // Wait for CR and FR to clear + for (int i = 0; i < 500000; i++) { + cmd = ReadPortReg(port, PORT_CMD); + if (!(cmd & PORT_CMD_CR) && !(cmd & PORT_CMD_FR)) { + return; + } + asm volatile("" ::: "memory"); + } + + KernelLogStream(WARNING, "AHCI") << "Port " << port << " stop timed out"; + } + + static void StartPort(int port) { + // Wait for CR to clear before starting + for (int i = 0; i < 500000; i++) { + if (!(ReadPortReg(port, PORT_CMD) & PORT_CMD_CR)) break; + asm volatile("" ::: "memory"); + } + + uint32_t cmd = ReadPortReg(port, PORT_CMD); + cmd |= PORT_CMD_FRE; + WritePortReg(port, PORT_CMD, cmd); + + cmd |= PORT_CMD_ST; + WritePortReg(port, PORT_CMD, cmd); + } + + // ------------------------------------------------------------------------- + // Port detection and classification + // ------------------------------------------------------------------------- + + static PortType ClassifyPort(int port) { + uint32_t ssts = ReadPortReg(port, PORT_SSTS); + uint32_t det = ssts & SSTS_DET_MASK; + + if (det != SSTS_DET_PRESENT) { + return PortType::None; + } + + uint32_t sig = ReadPortReg(port, PORT_SIG); + switch (sig) { + case SIG_ATA: return PortType::Sata; + case SIG_ATAPI: return PortType::Satapi; + case SIG_SEMB: return PortType::Semb; + case SIG_PM: return PortType::PortMultiplier; + default: return PortType::Sata; // Default to SATA for unknown signatures + } + } + + // ------------------------------------------------------------------------- + // Port initialization (allocate command list, FIS area, command tables) + // ------------------------------------------------------------------------- + + static void InitPort(int port) { + StopPort(port); + + // Allocate Command List (1 KiB, 1024-byte aligned) + // and FIS area (256 bytes, 256-byte aligned) + // Both fit in one 4 KiB page + uint64_t clPhys; + void* clVirt = AllocateDmaBuffer(clPhys); + g_ports[port].CmdList = (CommandHeader*)clVirt; + g_ports[port].CmdListPhys = clPhys; + + // FIS area goes in the second half of the same page + uint64_t fbPhys = clPhys + 1024; + void* fbVirt = (void*)((uint8_t*)clVirt + 1024); + g_ports[port].FisArea = fbVirt; + g_ports[port].FisAreaPhys = fbPhys; + + // Set CLB and FB in port registers + WritePortReg(port, PORT_CLB, (uint32_t)(clPhys & 0xFFFFFFFF)); + WritePortReg(port, PORT_CLBU, (uint32_t)(clPhys >> 32)); + WritePortReg(port, PORT_FB, (uint32_t)(fbPhys & 0xFFFFFFFF)); + WritePortReg(port, PORT_FBU, (uint32_t)(fbPhys >> 32)); + + // Allocate command tables (one page each, 128-byte aligned) + CommandHeader* headers = g_ports[port].CmdList; + for (int i = 0; i < CMD_HEADER_COUNT; i++) { + uint64_t ctPhys; + void* ctVirt = AllocateDmaBuffer(ctPhys); + g_ports[port].CmdTables[i] = (CommandTable*)ctVirt; + g_ports[port].CmdTablesPhys[i] = ctPhys; + + headers[i].CtbaLow = (uint32_t)(ctPhys & 0xFFFFFFFF); + headers[i].CtbaHigh = (uint32_t)(ctPhys >> 32); + headers[i].PrdtLength = 0; + headers[i].PrdByteCount = 0; + } + + // Clear interrupt status and error + WritePortReg(port, PORT_SERR, 0xFFFFFFFF); + WritePortReg(port, PORT_IS, 0xFFFFFFFF); + + // Enable interrupts for this port + WritePortReg(port, PORT_IE, + PORT_IS_DHRS | PORT_IS_PSS | PORT_IS_DSS | + PORT_IS_SDBS | PORT_IS_TFES); + + // Power on and spin up if needed + uint32_t cmd = ReadPortReg(port, PORT_CMD); + cmd |= PORT_CMD_POD | PORT_CMD_SUD; + WritePortReg(port, PORT_CMD, cmd); + + StartPort(port); + } + + // ------------------------------------------------------------------------- + // Issue a command and wait for completion + // ------------------------------------------------------------------------- + + static bool IssueCommand(int port, int slot) { + // Wait for the slot to be free + for (int i = 0; i < 500000; i++) { + if (!(ReadPortReg(port, PORT_CI) & (1u << slot))) break; + asm volatile("" ::: "memory"); + } + + // Check that the port isn't in error + uint32_t tfd = ReadPortReg(port, PORT_TFD); + if (tfd & (PORT_TFD_BSY | PORT_TFD_DRQ)) { + // Device is busy, wait + for (int i = 0; i < 1000000; i++) { + tfd = ReadPortReg(port, PORT_TFD); + if (!(tfd & (PORT_TFD_BSY | PORT_TFD_DRQ))) break; + asm volatile("" ::: "memory"); + } + if (tfd & (PORT_TFD_BSY | PORT_TFD_DRQ)) { + KernelLogStream(ERROR, "AHCI") << "Port " << port << " device busy before command issue"; + return false; + } + } + + // Issue the command + WritePortReg(port, PORT_CI, 1u << slot); + + // Wait for completion + for (int i = 0; i < 5000000; i++) { + uint32_t ci = ReadPortReg(port, PORT_CI); + if (!(ci & (1u << slot))) { + // Check for errors + uint32_t is = ReadPortReg(port, PORT_IS); + if (is & PORT_IS_TFES) { + KernelLogStream(ERROR, "AHCI") << "Port " << port << " task file error"; + WritePortReg(port, PORT_IS, PORT_IS_TFES); + return false; + } + // Clear interrupt status + WritePortReg(port, PORT_IS, is); + return true; + } + + // Check for fatal error during wait + uint32_t is = ReadPortReg(port, PORT_IS); + if (is & PORT_IS_TFES) { + KernelLogStream(ERROR, "AHCI") << "Port " << port + << " task file error during command, TFD=" << base::hex << (uint64_t)ReadPortReg(port, PORT_TFD); + WritePortReg(port, PORT_IS, PORT_IS_TFES); + return false; + } + + asm volatile("" ::: "memory"); + } + + KernelLogStream(ERROR, "AHCI") << "Port " << port << " command timeout"; + return false; + } + + // ------------------------------------------------------------------------- + // Build a command FIS for a DMA read/write + // ------------------------------------------------------------------------- + + static int FindFreeSlot(int port) { + uint32_t slots = ReadPortReg(port, PORT_SACT) | ReadPortReg(port, PORT_CI); + for (int i = 0; i < CMD_HEADER_COUNT; i++) { + if (!(slots & (1u << i))) { + return i; + } + } + return -1; + } + + static void BuildReadWriteCommand(int port, int slot, uint64_t lba, uint32_t count, + uint64_t bufferPhys, bool write) { + CommandHeader* hdr = &g_ports[port].CmdList[slot]; + CommandTable* tbl = g_ports[port].CmdTables[slot]; + + // Clear the command table + memset(tbl, 0, sizeof(CommandTable) + sizeof(PrdtEntry) * MAX_PRDT_ENTRIES); + + // Build FIS + FisRegH2D* fis = (FisRegH2D*)tbl->CommandFis; + fis->FisType = (uint8_t)FisType::RegH2D; + fis->CmdCtl = 1; // Command + fis->Command = write ? ATA_CMD_WRITE_DMA_EX : ATA_CMD_READ_DMA_EX; + fis->Device = (1 << 6); // LBA mode + + fis->Lba0 = (uint8_t)(lba); + fis->Lba1 = (uint8_t)(lba >> 8); + fis->Lba2 = (uint8_t)(lba >> 16); + fis->Lba3 = (uint8_t)(lba >> 24); + fis->Lba4 = (uint8_t)(lba >> 32); + fis->Lba5 = (uint8_t)(lba >> 40); + + fis->Count = (uint16_t)count; + + // Build PRDT entries + uint32_t bytesRemaining = count * SECTOR_SIZE; + int prdtIdx = 0; + uint64_t currentPhys = bufferPhys; + + while (bytesRemaining > 0 && prdtIdx < MAX_PRDT_ENTRIES) { + uint32_t chunkSize = bytesRemaining; + // Each PRDT entry can transfer up to 4 MiB (aligned to word boundary) + if (chunkSize > 0x400000) { + chunkSize = 0x400000; + } + + tbl->PrdtEntries[prdtIdx].DataBaseLow = (uint32_t)(currentPhys & 0xFFFFFFFF); + tbl->PrdtEntries[prdtIdx].DataBaseHigh = (uint32_t)(currentPhys >> 32); + tbl->PrdtEntries[prdtIdx].Reserved = 0; + // Byte count is (actual bytes - 1), bit 31 = interrupt on completion for last entry + tbl->PrdtEntries[prdtIdx].ByteCount = (chunkSize - 1); + if (bytesRemaining <= chunkSize) { + tbl->PrdtEntries[prdtIdx].ByteCount |= (1u << 31); // IOC + } + + currentPhys += chunkSize; + bytesRemaining -= chunkSize; + prdtIdx++; + } + + // Set up command header + // CFL = FIS length in dwords (5 for FisRegH2D = 20 bytes / 4) + hdr->CflPmpA = 5; // CFL bits [4:0] + hdr->Flags = write ? CMDHDR_WRITE : 0; + hdr->PrdtLength = (uint16_t)prdtIdx; + hdr->PrdByteCount = 0; + } + + // ------------------------------------------------------------------------- + // IDENTIFY DEVICE + // ------------------------------------------------------------------------- + + static void SwapStringBytes(char* str, int len) { + for (int i = 0; i < len; i += 2) { + char tmp = str[i]; + str[i] = str[i + 1]; + str[i + 1] = tmp; + } + // Trim trailing spaces + for (int i = len - 1; i >= 0; i--) { + if (str[i] == ' ' || str[i] == '\0') { + str[i] = '\0'; + } else { + break; + } + } + } + + static bool IdentifyDevice(int port) { + int slot = FindFreeSlot(port); + if (slot < 0) { + KernelLogStream(ERROR, "AHCI") << "Port " << port << " no free command slot for IDENTIFY"; + return false; + } + + // Allocate a page for IDENTIFY data (512 bytes) + uint64_t identPhys; + uint16_t* identData = (uint16_t*)AllocateDmaBuffer(identPhys); + + CommandHeader* hdr = &g_ports[port].CmdList[slot]; + CommandTable* tbl = g_ports[port].CmdTables[slot]; + + memset(tbl, 0, sizeof(CommandTable) + sizeof(PrdtEntry)); + + FisRegH2D* fis = (FisRegH2D*)tbl->CommandFis; + fis->FisType = (uint8_t)FisType::RegH2D; + fis->CmdCtl = 1; + fis->Command = ATA_CMD_IDENTIFY; + fis->Device = 0; + + tbl->PrdtEntries[0].DataBaseLow = (uint32_t)(identPhys & 0xFFFFFFFF); + tbl->PrdtEntries[0].DataBaseHigh = (uint32_t)(identPhys >> 32); + tbl->PrdtEntries[0].ByteCount = 511 | (1u << 31); // 512 bytes, IOC + + hdr->CflPmpA = 5; + hdr->Flags = 0; + hdr->PrdtLength = 1; + hdr->PrdByteCount = 0; + + if (!IssueCommand(port, slot)) { + KernelLogStream(ERROR, "AHCI") << "Port " << port << " IDENTIFY failed"; + Memory::g_pfa->Free(identData); + return false; + } + + // Parse IDENTIFY data + // Word 100-103: Total number of user addressable sectors (48-bit LBA) + uint64_t sectors = (uint64_t)identData[100] + | ((uint64_t)identData[101] << 16) + | ((uint64_t)identData[102] << 32) + | ((uint64_t)identData[103] << 48); + + if (sectors == 0) { + // Fallback to 28-bit LBA sector count (words 60-61) + sectors = (uint64_t)identData[60] | ((uint64_t)identData[61] << 16); + } + + g_ports[port].SectorCount = sectors; + g_ports[port].PortIndex = (uint8_t)port; + + // Model string: words 27-46 (40 bytes) + memcpy(g_ports[port].Model, &identData[27], 40); + g_ports[port].Model[40] = '\0'; + SwapStringBytes(g_ports[port].Model, 40); + + // Serial number: words 10-19 (20 bytes) + memcpy(g_ports[port].Serial, &identData[10], 20); + g_ports[port].Serial[20] = '\0'; + SwapStringBytes(g_ports[port].Serial, 20); + + // Firmware revision: words 23-26 (8 bytes) + memcpy(g_ports[port].Firmware, &identData[23], 8); + g_ports[port].Firmware[8] = '\0'; + SwapStringBytes(g_ports[port].Firmware, 8); + + // Word 49: capabilities — bit 9 = LBA supported + // Word 83: command set supported — bit 10 = 48-bit LBA + uint16_t cmdSet83 = identData[83]; + g_ports[port].SupportsLba48 = (cmdSet83 & (1 << 10)) != 0; + + // Word 82: command set supported + uint16_t cmdSet82 = identData[82]; + g_ports[port].SupportsSmart = (cmdSet82 & (1 << 0)) != 0; + g_ports[port].SupportsWriteCache = (cmdSet82 & (1 << 5)) != 0; + g_ports[port].SupportsReadAhead = (cmdSet82 & (1 << 6)) != 0; + + // Word 84: command/feature set supported ext — bit 1 = SMART self-test + uint16_t cmdSet84 = identData[84]; + g_ports[port].SupportsSmartSelfTest = (cmdSet84 & (1 << 1)) != 0; + + // Word 75: NCQ queue depth (bits 4:0 = max depth - 1) + uint16_t word75 = identData[75]; + uint16_t sataCapabilities = identData[76]; // Word 76: SATA capabilities + g_ports[port].SupportsNcq = (sataCapabilities & (1 << 8)) != 0; + g_ports[port].NcqDepth = g_ports[port].SupportsNcq ? (uint16_t)((word75 & 0x1F) + 1) : 0; + + // Word 76: SATA capabilities — bits 3:1 = supported SATA generations + g_ports[port].SataGen = 0; + if (sataCapabilities & (1 << 3)) g_ports[port].SataGen = 3; // 6 Gbps + else if (sataCapabilities & (1 << 2)) g_ports[port].SataGen = 2; // 3 Gbps + else if (sataCapabilities & (1 << 1)) g_ports[port].SataGen = 1; // 1.5 Gbps + + // Word 169: TRIM support (Data Set Management) + uint16_t word169 = identData[169]; + g_ports[port].SupportsTrim = (word169 & (1 << 0)) != 0; + + // Word 106: Physical/logical sector size + uint16_t word106 = identData[106]; + if (word106 & (1 << 14)) { + // Valid field + g_ports[port].SectorSizePhys = (word106 & (1 << 12)) + ? (uint16_t)(512 * (1 << (word106 & 0x0F))) : 512; + g_ports[port].SectorSizeLog = (word106 & (1 << 13)) ? 0 : 512; + // If logical sector size specified in words 117-118 + if (word106 & (1 << 13)) { + g_ports[port].SectorSizeLog = (uint16_t)( + ((uint32_t)identData[117] | ((uint32_t)identData[118] << 16)) * 2); + } + } else { + g_ports[port].SectorSizeLog = 512; + g_ports[port].SectorSizePhys = 512; + } + if (g_ports[port].SectorSizeLog == 0) g_ports[port].SectorSizeLog = 512; + + // Word 217: Nominal Media Rotation Rate + // 0000h = not reported, 0001h = non-rotating (SSD), else RPM + g_ports[port].Rpm = identData[217]; + + Memory::g_pfa->Free(identData); + + uint64_t sizeBytes = sectors * SECTOR_SIZE; + uint64_t sizeMB = sizeBytes / (1024 * 1024); + uint64_t sizeGB = sizeMB / 1024; + + if (sizeGB > 0) { + KernelLogStream(OK, "AHCI") << "Port " << port << ": " << g_ports[port].Model + << " (" << sizeGB << " GiB)"; + } else { + KernelLogStream(OK, "AHCI") << "Port " << port << ": " << g_ports[port].Model + << " (" << sizeMB << " MiB)"; + } + + return true; + } + + // ------------------------------------------------------------------------- + // Interrupt handler + // ------------------------------------------------------------------------- + + static void HandleInterrupt(uint8_t irq) { + (void)irq; + + uint32_t is = ReadReg(REG_IS); + if (is == 0) return; + + // Acknowledge each port's interrupt + for (int i = 0; i < MAX_PORTS; i++) { + if (is & (1u << i)) { + uint32_t portIs = ReadPortReg(i, PORT_IS); + WritePortReg(i, PORT_IS, portIs); + } + } + + // Clear global interrupt status + WriteReg(REG_IS, is); + } + + // ------------------------------------------------------------------------- + // MSI setup + // ------------------------------------------------------------------------- + + static bool SetupMsi(uint8_t bus, uint8_t dev, uint8_t func) { + uint8_t cap = Pci::FindCapability(bus, dev, func, Pci::PCI_CAP_MSI); + if (cap == 0) { + KernelLogStream(INFO, "AHCI") << "MSI capability not found"; + return false; + } + + uint16_t msgCtrl = Pci::LegacyRead16(bus, dev, func, cap + 2); + bool is64bit = (msgCtrl & (1 << 7)) != 0; + + Pci::LegacyWrite32(bus, dev, func, cap + 4, MSI_ADDR_BASE); + + if (is64bit) { + Pci::LegacyWrite32(bus, dev, func, cap + 8, 0); + Pci::LegacyWrite16(bus, dev, func, cap + 12, MSI_VECTOR); + } else { + Pci::LegacyWrite16(bus, dev, func, cap + 8, MSI_VECTOR); + } + + msgCtrl &= ~(0x70); // Single message + msgCtrl |= (1 << 0); // MSI Enable + Pci::LegacyWrite16(bus, dev, func, cap + 2, msgCtrl); + + uint16_t pciCmd = Pci::LegacyRead16(bus, dev, func, (uint8_t)Pci::PCI_REG_COMMAND); + pciCmd |= Pci::PCI_CMD_INTX_DISABLE; + Pci::LegacyWrite16(bus, dev, func, (uint8_t)Pci::PCI_REG_COMMAND, pciCmd); + + Hal::RegisterIrqHandler(MSI_IRQ, HandleInterrupt); + + KernelLogStream(OK, "AHCI") << "MSI enabled: vector " << base::dec << (uint64_t)MSI_VECTOR; + return true; + } + + // ------------------------------------------------------------------------- + // HBA reset + // ------------------------------------------------------------------------- + + static bool ResetHba() { + // Set AHCI Enable + uint32_t ghc = ReadReg(REG_GHC); + ghc |= GHC_AE; + WriteReg(REG_GHC, ghc); + + // Perform HBA reset + ghc = ReadReg(REG_GHC); + ghc |= GHC_HR; + WriteReg(REG_GHC, ghc); + + // Wait for reset to complete (HR should self-clear within 1 second) + for (int i = 0; i < 1000000; i++) { + if (!(ReadReg(REG_GHC) & GHC_HR)) { + // Re-enable AHCI after reset + ghc = ReadReg(REG_GHC); + ghc |= GHC_AE; + WriteReg(REG_GHC, ghc); + return true; + } + asm volatile("" ::: "memory"); + } + + KernelLogStream(ERROR, "AHCI") << "HBA reset timed out"; + return false; + } + + // ------------------------------------------------------------------------- + // Probe (PCI driver entry point) + // ------------------------------------------------------------------------- + + bool Probe(const Pci::PciDevice& dev) { + if (g_initialized) return false; + + KernelLogStream(OK, "AHCI") << "Found AHCI controller at PCI " + << base::hex << (uint64_t)dev.Bus << ":" + << (uint64_t)dev.Device << "." << (uint64_t)dev.Function + << " (" << (uint64_t)dev.VendorId << ":" << (uint64_t)dev.DeviceId << ")"; + + // AHCI uses BAR5 (ABAR) for its MMIO registers. + // BAR5 is at PCI config offset 0x24. + uint32_t abarLow = Pci::LegacyRead32(dev.Bus, dev.Device, dev.Function, 0x24); + uint64_t mmioPhys = abarLow & 0xFFFFFFF0u; + + // Check for 64-bit BAR + if ((abarLow & 0x06) == 0x04) { + uint32_t abarHigh = Pci::LegacyRead32(dev.Bus, dev.Device, dev.Function, 0x28); + mmioPhys |= ((uint64_t)abarHigh << 32); + } + + KernelLogStream(INFO, "AHCI") << "ABAR (BAR5) physical: " << base::hex << mmioPhys; + + // Map MMIO region (AHCI spec says up to 8 KiB for GHC + 32 ports) + // Map 16 KiB to be safe + constexpr uint64_t MmioSize = 0x4000; + for (uint64_t offset = 0; offset < MmioSize; offset += 0x1000) { + Memory::VMM::g_paging->MapMMIO(mmioPhys + offset, Memory::HHDM(mmioPhys + offset)); + } + + g_mmioBase = (volatile uint8_t*)Memory::HHDM(mmioPhys); + + // Enable bus mastering and memory space + Pci::EnableBusMaster(dev.Bus, dev.Device, dev.Function); + + // BIOS/OS handoff + PerformBiosHandoff(); + + // Reset HBA + if (!ResetHba()) { + KernelLogStream(ERROR, "AHCI") << "HBA reset failed, aborting"; + return false; + } + + // Read version + uint32_t vs = ReadReg(REG_VS); + uint32_t vsMajor = (vs >> 16) & 0xFFFF; + uint32_t vsMinor = vs & 0xFFFF; + KernelLogStream(INFO, "AHCI") << "Version: " << base::dec + << (uint64_t)vsMajor << "." << (uint64_t)vsMinor; + + // Read capabilities + uint32_t cap = ReadReg(REG_CAP); + uint32_t numPorts = (cap & 0x1F) + 1; + uint32_t numSlots = ((cap >> 8) & 0x1F) + 1; + bool supports64bit = (cap & CAP_S64A) != 0; + + KernelLogStream(INFO, "AHCI") << "Ports: " << (uint64_t)numPorts + << ", Command slots: " << (uint64_t)numSlots + << ", 64-bit: " << (supports64bit ? "yes" : "no"); + + // Read which ports are implemented + g_portsImplemented = ReadReg(REG_PI); + KernelLogStream(INFO, "AHCI") << "Ports implemented: " << base::hex << (uint64_t)g_portsImplemented; + + // Set up MSI (or fall back to legacy IRQ) + bool hasMsi = SetupMsi(dev.Bus, dev.Device, dev.Function); + + if (!hasMsi) { + uint8_t irqLine = Pci::LegacyRead8(dev.Bus, dev.Device, dev.Function, + (uint8_t)Pci::PCI_REG_INTERRUPT); + if (irqLine != 0xFF) { + KernelLogStream(INFO, "AHCI") << "Using legacy IRQ " << base::dec << (uint64_t)irqLine; + Hal::RegisterIrqHandler(irqLine, HandleInterrupt); + Hal::IoApic::UnmaskIrq(Hal::IoApic::GetGsiForIrq(irqLine)); + } + } + + // Enable global interrupts + uint32_t ghc = ReadReg(REG_GHC); + ghc |= GHC_IE; + WriteReg(REG_GHC, ghc); + + // Initialize each implemented port + g_activePortCount = 0; + for (int i = 0; i < MAX_PORTS; i++) { + if (!(g_portsImplemented & (1u << i))) continue; + + PortType type = ClassifyPort(i); + g_ports[i].Type = type; + + if (type == PortType::None) continue; + + const char* typeStr = "Unknown"; + switch (type) { + case PortType::Sata: typeStr = "SATA"; break; + case PortType::Satapi: typeStr = "SATAPI"; break; + case PortType::Semb: typeStr = "SEMB"; break; + case PortType::PortMultiplier: typeStr = "PM"; break; + default: break; + } + + KernelLogStream(INFO, "AHCI") << "Port " << base::dec << (uint64_t)i + << ": " << typeStr << " device detected"; + + InitPort(i); + + if (type == PortType::Sata) { + if (IdentifyDevice(i)) { + g_ports[i].Active = true; + g_activePortCount++; + } + } + } + + g_initialized = true; + + KernelLogStream(OK, "AHCI") << "Initialization complete: " + << base::dec << (uint64_t)g_activePortCount << " SATA device(s) ready"; + + return true; + } + + // ------------------------------------------------------------------------- + // Public API + // ------------------------------------------------------------------------- + + bool IsInitialized() { + return g_initialized; + } + + int GetPortCount() { + return g_activePortCount; + } + + const PortInfo* GetPortInfo(int port) { + if (port < 0 || port >= MAX_PORTS || !g_ports[port].Active) { + return nullptr; + } + return &g_ports[port]; + } + + uint64_t GetSectorCount(int port) { + if (port < 0 || port >= MAX_PORTS || !g_ports[port].Active) { + return 0; + } + return g_ports[port].SectorCount; + } + + bool ReadSectors(int port, uint64_t lba, uint32_t count, void* buffer) { + if (!g_initialized || port < 0 || port >= MAX_PORTS || !g_ports[port].Active) { + return false; + } + if (count == 0 || buffer == nullptr) return false; + + // Limit to 128 sectors per command (64 KiB) + if (count > 128) { + KernelLogStream(ERROR, "AHCI") << "ReadSectors: count " << count << " exceeds max 128"; + return false; + } + + int slot = FindFreeSlot(port); + if (slot < 0) { + KernelLogStream(ERROR, "AHCI") << "ReadSectors: no free command slot"; + return false; + } + + // Allocate DMA buffer (we need physically contiguous memory for DMA) + uint32_t totalBytes = count * SECTOR_SIZE; + int pagesNeeded = (totalBytes + 0xFFF) / 0x1000; + uint64_t dmaPhys; + void* dmaVirt = AllocateDmaBuffer(dmaPhys, pagesNeeded); + + BuildReadWriteCommand(port, slot, lba, count, dmaPhys, false); + + bool ok = IssueCommand(port, slot); + if (ok) { + memcpy(buffer, dmaVirt, totalBytes); + } + + Memory::g_pfa->Free(dmaVirt, pagesNeeded); + return ok; + } + + bool WriteSectors(int port, uint64_t lba, uint32_t count, const void* buffer) { + if (!g_initialized || port < 0 || port >= MAX_PORTS || !g_ports[port].Active) { + return false; + } + if (count == 0 || buffer == nullptr) return false; + + if (count > 128) { + KernelLogStream(ERROR, "AHCI") << "WriteSectors: count " << count << " exceeds max 128"; + return false; + } + + int slot = FindFreeSlot(port); + if (slot < 0) { + KernelLogStream(ERROR, "AHCI") << "WriteSectors: no free command slot"; + return false; + } + + uint32_t totalBytes = count * SECTOR_SIZE; + int pagesNeeded = (totalBytes + 0xFFF) / 0x1000; + uint64_t dmaPhys; + void* dmaVirt = AllocateDmaBuffer(dmaPhys, pagesNeeded); + + memcpy(dmaVirt, buffer, totalBytes); + + BuildReadWriteCommand(port, slot, lba, count, dmaPhys, true); + + bool ok = IssueCommand(port, slot); + + Memory::g_pfa->Free(dmaVirt, pagesNeeded); + return ok; + } + +}; diff --git a/kernel/src/Drivers/Storage/Ahci.hpp b/kernel/src/Drivers/Storage/Ahci.hpp new file mode 100644 index 0000000..3859fc8 --- /dev/null +++ b/kernel/src/Drivers/Storage/Ahci.hpp @@ -0,0 +1,261 @@ +/* + * Ahci.hpp + * AHCI (Advanced Host Controller Interface) SATA driver + * Copyright (c) 2025 Daniel Hammer +*/ + +#pragma once +#include +#include + +namespace Drivers::Storage::Ahci { + + // ========================================================================= + // AHCI Generic Host Control registers (HBA memory, offset from BAR5/ABAR) + // ========================================================================= + + constexpr uint32_t REG_CAP = 0x00; // Host Capabilities + constexpr uint32_t REG_GHC = 0x04; // Global Host Control + constexpr uint32_t REG_IS = 0x08; // Interrupt Status + constexpr uint32_t REG_PI = 0x0C; // Ports Implemented + constexpr uint32_t REG_VS = 0x10; // Version + constexpr uint32_t REG_CAP2 = 0x24; // Host Capabilities Extended + constexpr uint32_t REG_BOHC = 0x28; // BIOS/OS Handoff Control + + // GHC register bits + constexpr uint32_t GHC_HR = (1u << 0); // HBA Reset + constexpr uint32_t GHC_IE = (1u << 1); // Interrupt Enable + constexpr uint32_t GHC_AE = (1u << 31); // AHCI Enable + + // CAP register bits + constexpr uint32_t CAP_S64A = (1u << 31); // Supports 64-bit Addressing + constexpr uint32_t CAP_SSS = (1u << 27); // Supports Staggered Spin-up + + // BOHC register bits + constexpr uint32_t BOHC_BOS = (1u << 0); // BIOS Owned Semaphore + constexpr uint32_t BOHC_OOS = (1u << 1); // OS Owned Semaphore + constexpr uint32_t BOHC_BB = (1u << 4); // BIOS Busy + + // ========================================================================= + // Per-port registers (base = 0x100 + port * 0x80) + // ========================================================================= + + constexpr uint32_t PORT_BASE = 0x100; + constexpr uint32_t PORT_SIZE = 0x80; + + // Port register offsets (relative to port base) + constexpr uint32_t PORT_CLB = 0x00; // Command List Base Address (low) + constexpr uint32_t PORT_CLBU = 0x04; // Command List Base Address (high) + constexpr uint32_t PORT_FB = 0x08; // FIS Base Address (low) + constexpr uint32_t PORT_FBU = 0x0C; // FIS Base Address (high) + constexpr uint32_t PORT_IS = 0x10; // Interrupt Status + constexpr uint32_t PORT_IE = 0x14; // Interrupt Enable + constexpr uint32_t PORT_CMD = 0x18; // Command and Status + constexpr uint32_t PORT_TFD = 0x20; // Task File Data + constexpr uint32_t PORT_SIG = 0x24; // Signature + constexpr uint32_t PORT_SSTS = 0x28; // SATA Status (SCR0: SStatus) + constexpr uint32_t PORT_SCTL = 0x2C; // SATA Control (SCR2: SControl) + constexpr uint32_t PORT_SERR = 0x30; // SATA Error (SCR1: SError) + constexpr uint32_t PORT_SACT = 0x34; // SATA Active + constexpr uint32_t PORT_CI = 0x38; // Command Issue + + // PORT_CMD bits + constexpr uint32_t PORT_CMD_ST = (1u << 0); // Start + constexpr uint32_t PORT_CMD_SUD = (1u << 1); // Spin-Up Device + constexpr uint32_t PORT_CMD_POD = (1u << 2); // Power On Device + constexpr uint32_t PORT_CMD_FRE = (1u << 4); // FIS Receive Enable + constexpr uint32_t PORT_CMD_FR = (1u << 14); // FIS Receive Running + constexpr uint32_t PORT_CMD_CR = (1u << 15); // Command List Running + constexpr uint32_t PORT_CMD_ICC_ACTIVE = (1u << 28); // Interface Comm Control: Active + + // PORT_TFD bits + constexpr uint32_t PORT_TFD_BSY = (1u << 7); // Busy + constexpr uint32_t PORT_TFD_DRQ = (1u << 3); // Data Request + constexpr uint32_t PORT_TFD_ERR = (1u << 0); // Error + + // PORT_IS bits (interrupt status) + constexpr uint32_t PORT_IS_DHRS = (1u << 0); // Device to Host Register FIS + constexpr uint32_t PORT_IS_PSS = (1u << 1); // PIO Setup FIS + constexpr uint32_t PORT_IS_DSS = (1u << 2); // DMA Setup FIS + constexpr uint32_t PORT_IS_SDBS = (1u << 3); // Set Device Bits + constexpr uint32_t PORT_IS_TFES = (1u << 30); // Task File Error Status + + // PORT_SSTS (SStatus) fields + constexpr uint32_t SSTS_DET_MASK = 0x0F; // Device Detection + constexpr uint32_t SSTS_DET_PRESENT = 0x03; // Phy communication established + + // Device signatures + constexpr uint32_t SIG_ATA = 0x00000101; // SATA drive + constexpr uint32_t SIG_ATAPI = 0xEB140101; // SATAPI drive + constexpr uint32_t SIG_SEMB = 0xC33C0101; // Enclosure management bridge + constexpr uint32_t SIG_PM = 0x96690101; // Port multiplier + + // ========================================================================= + // FIS (Frame Information Structure) types + // ========================================================================= + + enum class FisType : uint8_t { + RegH2D = 0x27, // Register FIS - Host to Device + RegD2H = 0x34, // Register FIS - Device to Host + DmaActivate = 0x39, + DmaSetup = 0x41, + Data = 0x46, + BistActivate = 0x58, + PioSetup = 0x5F, + DevBits = 0xA1, + }; + + // Register FIS - Host to Device (used for issuing ATA commands) + struct FisRegH2D { + uint8_t FisType; // FisType::RegH2D (0x27) + uint8_t PmPort : 4; // Port multiplier + uint8_t Reserved0 : 3; + uint8_t CmdCtl : 1; // 1 = Command, 0 = Control + uint8_t Command; // ATA command + uint8_t FeatureLow; // Feature register (7:0) + uint8_t Lba0; // LBA (7:0) + uint8_t Lba1; // LBA (15:8) + uint8_t Lba2; // LBA (23:16) + uint8_t Device; // Device register + uint8_t Lba3; // LBA (31:24) + uint8_t Lba4; // LBA (39:32) + uint8_t Lba5; // LBA (47:40) + uint8_t FeatureHigh; // Feature register (15:8) + uint16_t Count; // Count + uint8_t Icc; // Isochronous command completion + uint8_t Control; // Control register + uint32_t Reserved1; + } __attribute__((packed)); + + // ========================================================================= + // Command structures + // ========================================================================= + + // Physical Region Descriptor Table entry (16 bytes) + struct PrdtEntry { + uint32_t DataBaseLow; // Data base address (low) + uint32_t DataBaseHigh; // Data base address (high) + uint32_t Reserved; + uint32_t ByteCount; // Byte count (bit 31 = Interrupt on Completion) + } __attribute__((packed)); + + // Command Table (pointed to by command header, variable size) + struct CommandTable { + uint8_t CommandFis[64]; // Command FIS (up to 64 bytes) + uint8_t AtapiCommand[16]; // ATAPI command (12 or 16 bytes) + uint8_t Reserved[48]; // Reserved + PrdtEntry PrdtEntries[]; // PRDT entries (variable length) + } __attribute__((packed)); + + // Command Header (32 bytes each, 32 entries in Command List) + struct CommandHeader { + uint8_t CflPmpA; // Command FIS length (bits 4:0), PMP (bits 11:8), etc. + uint8_t Flags; // Flags (Write bit 6, Prefetchable bit 7, etc.) + uint16_t PrdtLength; // PRDT entry count + uint32_t PrdByteCount; // PRD byte count transferred + uint32_t CtbaLow; // Command table base address (low) + uint32_t CtbaHigh; // Command table base address (high) + uint32_t Reserved[4]; + } __attribute__((packed)); + + // Command header flag bits + constexpr uint8_t CMDHDR_WRITE = (1u << 6); // Write direction (in Flags byte) + constexpr uint8_t CMDHDR_PREFETCH = (1u << 1); // Prefetchable (in Flags byte) + constexpr uint8_t CMDHDR_CLR_BSY = (1u << 2); // Clear Busy upon R_OK (in Flags byte) + + // ========================================================================= + // ATA commands + // ========================================================================= + + constexpr uint8_t ATA_CMD_IDENTIFY = 0xEC; + constexpr uint8_t ATA_CMD_READ_DMA_EX = 0x25; // READ DMA EXT (48-bit LBA) + constexpr uint8_t ATA_CMD_WRITE_DMA_EX = 0x35; // WRITE DMA EXT (48-bit LBA) + + // ========================================================================= + // Constants + // ========================================================================= + + constexpr int MAX_PORTS = 32; + constexpr int CMD_HEADER_COUNT = 32; // 32 command slots per port + constexpr int MAX_PRDT_ENTRIES = 8; // Max PRDT entries per command + constexpr int SECTOR_SIZE = 512; + + // MSI configuration + constexpr uint8_t MSI_IRQ = 25; // IRQ slot 25 = vector 57 + constexpr uint32_t MSI_VECTOR = 57; + constexpr uint32_t MSI_ADDR_BASE = 0xFEE00000; + + // ========================================================================= + // Port info + // ========================================================================= + + enum class PortType : uint8_t { + None, + Sata, + Satapi, + Semb, + PortMultiplier, + }; + + struct PortInfo { + bool Active; + PortType Type; + uint64_t SectorCount; // Total sectors (from IDENTIFY) + char Model[41]; // Model string (from IDENTIFY) + char Serial[21]; // Serial number (from IDENTIFY) + char Firmware[9]; // Firmware revision (from IDENTIFY) + uint8_t PortIndex; // AHCI port number + + // Feature flags (from IDENTIFY) + bool SupportsLba48; + bool SupportsNcq; + bool SupportsTrim; + bool SupportsSmartSelfTest; + bool SupportsSmart; + bool SupportsWriteCache; + bool SupportsReadAhead; + + uint16_t SataGen; // SATA generation (1/2/3) + uint16_t NcqDepth; // NCQ queue depth (0 if no NCQ) + uint16_t SectorSizeLog; // Logical sector size (bytes, usually 512) + uint16_t SectorSizePhys; // Physical sector size (bytes, 512 or 4096) + uint16_t Rpm; // Nominal RPM (0 = unknown, 1 = SSD/non-rotating) + + // DMA structures (physical + virtual) + CommandHeader* CmdList; // Command list (1 KiB, 32 headers) + uint64_t CmdListPhys; + void* FisArea; // Received FIS area (256 bytes) + uint64_t FisAreaPhys; + CommandTable* CmdTables[CMD_HEADER_COUNT]; // Command tables + uint64_t CmdTablesPhys[CMD_HEADER_COUNT]; + }; + + // ========================================================================= + // Public API + // ========================================================================= + + // Probe a PCI device (called by driver matching framework) + bool Probe(const Pci::PciDevice& dev); + + // Check if the driver was initialized + bool IsInitialized(); + + // Get number of active ports with SATA devices + int GetPortCount(); + + // Read sectors from a SATA device + // port: port index (0-31), lba: starting LBA, count: sector count (max 128) + // buffer: destination buffer (must be large enough for count * 512 bytes) + // Returns true on success + bool ReadSectors(int port, uint64_t lba, uint32_t count, void* buffer); + + // Write sectors to a SATA device + bool WriteSectors(int port, uint64_t lba, uint32_t count, const void* buffer); + + // Get info about a specific port + const PortInfo* GetPortInfo(int port); + + // Get the total sector count for a port + uint64_t GetSectorCount(int port); + +}; diff --git a/kernel/src/Main.cpp b/kernel/src/Main.cpp index fcf8d02..c7a677e 100644 --- a/kernel/src/Main.cpp +++ b/kernel/src/Main.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -136,6 +137,8 @@ extern "C" void kmain() { #if defined (__x86_64__) if (g_acpi.GetXSDT() != nullptr) { + Hal::AcpiShutdown::Initialize(g_acpi.GetXSDT()); + Hal::ApicInitialize(g_acpi.GetXSDT()); Pci::Initialize(g_acpi.GetXSDT()); @@ -151,6 +154,7 @@ extern "C" void kmain() { Drivers::ProbeNormal(); Drivers::InitializeNetwork(); + Drivers::InitializeStorage(); } #endif diff --git a/programs/include/Api/Syscall.hpp b/programs/include/Api/Syscall.hpp index 6675088..62539aa 100644 --- a/programs/include/Api/Syscall.hpp +++ b/programs/include/Api/Syscall.hpp @@ -83,6 +83,7 @@ namespace Montauk { static constexpr uint64_t SYS_PROCLIST = 61; static constexpr uint64_t SYS_KILL = 62; static constexpr uint64_t SYS_DEVLIST = 63; + static constexpr uint64_t SYS_DISKINFO = 69; // Kernel introspection syscalls static constexpr uint64_t SYS_MEMSTATS = 67; @@ -168,12 +169,35 @@ namespace Montauk { }; struct DevInfo { - uint8_t category; // 0=CPU, 1=Interrupt, 2=Timer, 3=Input, 4=USB, 5=Network, 6=Display, 7=PCI + uint8_t category; // 0=CPU, 1=Interrupt, 2=Timer, 3=Input, 4=USB, 5=Network, 6=Display, 7=Storage, 8=PCI uint8_t _pad[3]; char name[48]; char detail[48]; }; + struct DiskInfo { + uint8_t port; // AHCI port index + uint8_t type; // 0=none, 1=SATA, 2=SATAPI + uint8_t sataGen; // SATA gen (1/2/3) + uint8_t _pad0; + uint64_t sectorCount; // Total user-addressable sectors + uint16_t sectorSizeLog; // Logical sector size (bytes) + uint16_t sectorSizePhys; // Physical sector size (bytes) + uint16_t rpm; // 0=unknown, 1=SSD, else RPM + uint16_t ncqDepth; // 0 if no NCQ + uint8_t supportsLba48; + uint8_t supportsNcq; + uint8_t supportsTrim; + uint8_t supportsSmart; + uint8_t supportsWriteCache; + uint8_t supportsReadAhead; + uint8_t _pad1[2]; + char model[41]; + char serial[21]; + char firmware[9]; + char _pad2[1]; + }; + struct ProcInfo { int32_t pid; int32_t parentPid; diff --git a/programs/include/montauk/syscall.h b/programs/include/montauk/syscall.h index 4a823e7..97b19ac 100644 --- a/programs/include/montauk/syscall.h +++ b/programs/include/montauk/syscall.h @@ -294,6 +294,9 @@ namespace montauk { inline int devlist(Montauk::DevInfo* buf, int max) { return (int)syscall2(Montauk::SYS_DEVLIST, (uint64_t)buf, (uint64_t)max); } + inline int diskinfo(Montauk::DiskInfo* buf, int port) { + return (int)syscall2(Montauk::SYS_DISKINFO, (uint64_t)buf, (uint64_t)port); + } // Kernel introspection inline void memstats(Montauk::MemStats* out) { syscall1(Montauk::SYS_MEMSTATS, (uint64_t)out); } diff --git a/programs/src/desktop/apps/app_devexplorer.cpp b/programs/src/desktop/apps/app_devexplorer.cpp index c2c4357..d6b8f56 100644 --- a/programs/src/desktop/apps/app_devexplorer.cpp +++ b/programs/src/desktop/apps/app_devexplorer.cpp @@ -1,6 +1,6 @@ /* * app_devexplorer.cpp - * MontaukOS Desktop - Device Explorer (lists hardware detected by the kernel) + * MontaukOS Desktop - Device Explorer * Copyright (c) 2026 Daniel Hammer */ @@ -26,9 +26,10 @@ static const char* category_names[] = { "USB", // 4 "Network", // 5 "Display", // 6 - "PCI", // 7 + "Storage", // 7 + "PCI", // 8 }; -static constexpr int NUM_CATEGORIES = 8; +static constexpr int NUM_CATEGORIES = 9; static Color category_colors[] = { Color::from_rgb(0x33, 0x66, 0xCC), // CPU - blue @@ -38,6 +39,7 @@ static Color category_colors[] = { Color::from_rgb(0x00, 0x88, 0x88), // USB - teal Color::from_rgb(0xCC, 0x55, 0x22), // Network - orange Color::from_rgb(0x44, 0x66, 0xCC), // Display - indigo + Color::from_rgb(0x99, 0x55, 0x00), // Storage - brown Color::from_rgb(0x66, 0x66, 0x66), // PCI - gray }; @@ -49,6 +51,10 @@ struct DevExplorerState { int selected_row; // index into visible display rows (-1 = none) int scroll_y; // scroll offset in display rows uint64_t last_poll_ms; + + // Double-click tracking + int last_click_row; + uint64_t last_click_ms; }; // ============================================================================ @@ -305,6 +311,313 @@ static void devexplorer_on_draw(Window* win, Framebuffer& fb) { } } +// ============================================================================ +// Disk Detail Window +// ============================================================================ + +static constexpr int DD_TAB_BAR_H = 32; +static constexpr int DD_TAB_COUNT = 2; +static const char* dd_tab_labels[DD_TAB_COUNT] = { "General", "Features" }; + +struct DiskDetailState { + DesktopState* desktop; + Montauk::DiskInfo info; + int active_tab; +}; + +// Helper: format uint64 to string (snprintf %d can't handle 64-bit) +static void fmt_u64(char* buf, int bufsize, uint64_t v) { + if (v == 0) { buf[0] = '0'; buf[1] = '\0'; return; } + char tmp[24]; int i = 0; + while (v > 0) { tmp[i++] = '0' + (int)(v % 10); v /= 10; } + int j = 0; + while (i > 0 && j < bufsize - 1) buf[j++] = tmp[--i]; + buf[j] = '\0'; +} + +// Draw a two-column table row +static void dd_table_row(Canvas& c, int x, int y, int col1_w, int row_h, + const char* key, const char* value, Color key_col, Color val_col) { + int fh = system_font_height(); + int ty = y + (row_h - fh) / 2; + c.text(x + 8, ty, key, key_col); + c.text(x + col1_w + 8, ty, value, val_col); +} + +// Draw a feature row with colored status indicator +static void dd_feature_row(Canvas& c, int x, int y, int w, int row_h, + const char* label, bool supported, const char* extra) { + int fh = system_font_height(); + int ty = y + (row_h - fh) / 2; + + // Status dot + int dot_y = y + (row_h - 8) / 2; + Color dot_col = supported ? Color::from_rgb(0x22, 0x88, 0x22) + : Color::from_rgb(0xBB, 0xBB, 0xBB); + c.fill_rounded_rect(x + 10, dot_y, 8, 8, 4, dot_col); + + // Label + Color text_col = supported ? colors::TEXT_COLOR : Color::from_rgb(0xAA, 0xAA, 0xAA); + c.text(x + 26, ty, label, text_col); + + // Extra info on right + if (extra && extra[0]) { + int ew = text_width(extra); + c.text(x + w - ew - 12, ty, extra, Color::from_rgb(0x66, 0x66, 0x66)); + } +} + +static void diskdetail_draw_general(Canvas& c, DiskDetailState* dd) { + char line[128]; + int fh = system_font_height(); + int x = 0; + int y = 12; + int w = c.w; + int col1_w = 110; + int row_h = fh + 8; + Color key_col = Color::from_rgb(0x66, 0x66, 0x66); + Color val_col = colors::TEXT_COLOR; + Color hdr_col = colors::ACCENT; + Color border = Color::from_rgb(0xE0, 0xE0, 0xE0); + + // --- Identification section --- + c.text(x + 8, y, "Identification", hdr_col); + y += fh + 8; + c.hline(x + 8, y, w - 16, border); + y += 1; + + // Table rows with alternating backgrounds + auto row_bg = [&](int ry) { + static int rowIdx = 0; + if (rowIdx++ % 2 == 0) + c.fill_rect(x + 8, ry, w - 16, row_h, Color::from_rgb(0xF7, 0xF7, 0xF7)); + }; + + int rowIdx = 0; + auto table_row = [&](const char* key, const char* val) { + if (rowIdx++ % 2 == 0) + c.fill_rect(x + 8, y, w - 16, row_h, Color::from_rgb(0xF7, 0xF7, 0xF7)); + dd_table_row(c, x, y, col1_w, row_h, key, val, key_col, val_col); + y += row_h; + }; + + table_row("Model", dd->info.model); + table_row("Serial", dd->info.serial); + table_row("Firmware", dd->info.firmware); + + const char* typeStr = "Unknown"; + if (dd->info.type == 1) typeStr = "SATA"; + else if (dd->info.type == 2) typeStr = "SATAPI"; + table_row("Type", typeStr); + + snprintf(line, sizeof(line), "%d", (int)dd->info.port); + table_row("AHCI Port", line); + + c.hline(x + 8, y, w - 16, border); + y += 12; + + // --- Capacity section --- + c.text(x + 8, y, "Capacity", hdr_col); + y += fh + 8; + c.hline(x + 8, y, w - 16, border); + y += 1; + + rowIdx = 0; + uint64_t totalBytes = dd->info.sectorCount * (uint64_t)dd->info.sectorSizeLog; + uint64_t totalMB = totalBytes / (1024 * 1024); + uint64_t totalGB = totalMB / 1024; + if (totalGB > 0) { + int fracGB = (int)((totalMB % 1024) * 10 / 1024); + snprintf(line, sizeof(line), "%d.%d GiB", (int)totalGB, fracGB); + } else { + snprintf(line, sizeof(line), "%d MiB", (int)totalMB); + } + table_row("Size", line); + + char secbuf[24]; + fmt_u64(secbuf, sizeof(secbuf), dd->info.sectorCount); + table_row("Sectors", secbuf); + + snprintf(line, sizeof(line), "%d bytes", (int)dd->info.sectorSizeLog); + table_row("Logical", line); + + snprintf(line, sizeof(line), "%d bytes", (int)dd->info.sectorSizePhys); + table_row("Physical", line); + + c.hline(x + 8, y, w - 16, border); + y += 12; + + // --- Interface section --- + c.text(x + 8, y, "Interface", hdr_col); + y += fh + 8; + c.hline(x + 8, y, w - 16, border); + y += 1; + + rowIdx = 0; + const char* sataSpeed = "Unknown"; + if (dd->info.sataGen == 1) sataSpeed = "SATA I (1.5 Gb/s)"; + else if (dd->info.sataGen == 2) sataSpeed = "SATA II (3.0 Gb/s)"; + else if (dd->info.sataGen == 3) sataSpeed = "SATA III (6.0 Gb/s)"; + table_row("Link Speed", sataSpeed); + + if (dd->info.rpm == 0) + table_row("Media", "Not reported"); + else if (dd->info.rpm == 1) + table_row("Media", "Solid State (SSD)"); + else { + snprintf(line, sizeof(line), "%d RPM", (int)dd->info.rpm); + table_row("Media", line); + } + + c.hline(x + 8, y, w - 16, border); +} + +static void diskdetail_draw_features(Canvas& c, DiskDetailState* dd) { + char line[64]; + int fh = system_font_height(); + int x = 0; + int y = 12; + int w = c.w; + int row_h = fh + 10; + Color hdr_col = colors::ACCENT; + Color border = Color::from_rgb(0xE0, 0xE0, 0xE0); + + c.text(x + 8, y, "Supported Features", hdr_col); + y += fh + 8; + c.hline(x + 8, y, w - 16, border); + y += 4; + + dd_feature_row(c, x, y, w, row_h, "48-bit LBA", dd->info.supportsLba48, nullptr); + y += row_h; + + if (dd->info.supportsNcq) { + snprintf(line, sizeof(line), "Depth: %d", (int)dd->info.ncqDepth); + dd_feature_row(c, x, y, w, row_h, "Native Command Queuing (NCQ)", true, line); + } else { + dd_feature_row(c, x, y, w, row_h, "Native Command Queuing (NCQ)", false, nullptr); + } + y += row_h; + + dd_feature_row(c, x, y, w, row_h, "TRIM (Data Set Management)", dd->info.supportsTrim, nullptr); + y += row_h; + + dd_feature_row(c, x, y, w, row_h, "S.M.A.R.T.", dd->info.supportsSmart, nullptr); + y += row_h; + + dd_feature_row(c, x, y, w, row_h, "Write Cache", dd->info.supportsWriteCache, nullptr); + y += row_h; + + dd_feature_row(c, x, y, w, row_h, "Read Look-Ahead", dd->info.supportsReadAhead, nullptr); + y += row_h; + + y += 4; + c.hline(x + 8, y, w - 16, border); + y += 12; + + // Legend + Color legend_col = Color::from_rgb(0x88, 0x88, 0x88); + int ly = y + (fh - 8) / 2; + c.fill_rounded_rect(x + 10, ly, 8, 8, 4, Color::from_rgb(0x22, 0x88, 0x22)); + c.text(x + 26, y, "Supported", legend_col); + + c.fill_rounded_rect(x + 120, ly, 8, 8, 4, Color::from_rgb(0xBB, 0xBB, 0xBB)); + c.text(x + 136, y, "Not supported", legend_col); +} + +static void diskdetail_on_draw(Window* win, Framebuffer& fb) { + DiskDetailState* dd = (DiskDetailState*)win->app_data; + if (!dd) return; + + Canvas c(win); + c.fill(colors::WINDOW_BG); + + int sfh = system_font_height(); + Color accent = colors::ACCENT; + + // --- Tab bar --- + c.fill_rect(0, 0, c.w, DD_TAB_BAR_H, Color::from_rgb(0xF5, 0xF5, 0xF5)); + c.hline(0, DD_TAB_BAR_H - 1, c.w, colors::BORDER); + + int tab_w = c.w / DD_TAB_COUNT; + for (int i = 0; i < DD_TAB_COUNT; i++) { + int tx = i * tab_w; + bool active = (i == dd->active_tab); + + if (active) { + c.fill_rect(tx, 0, tab_w, DD_TAB_BAR_H, colors::WINDOW_BG); + c.fill_rect(tx + 4, DD_TAB_BAR_H - 3, tab_w - 8, 3, accent); + } + + int tw = text_width(dd_tab_labels[i]); + Color tc = active ? accent : Color::from_rgb(0x66, 0x66, 0x66); + c.text(tx + (tab_w - tw) / 2, (DD_TAB_BAR_H - sfh) / 2, dd_tab_labels[i], tc); + } + + // --- Tab content --- + Canvas content(win->content + DD_TAB_BAR_H * win->content_w, + win->content_w, win->content_h - DD_TAB_BAR_H); + + switch (dd->active_tab) { + case 0: diskdetail_draw_general(content, dd); break; + case 1: diskdetail_draw_features(content, dd); break; + } +} + +static void diskdetail_on_mouse(Window* win, MouseEvent& ev) { + DiskDetailState* dd = (DiskDetailState*)win->app_data; + if (!dd || !ev.left_pressed()) return; + + Rect cr = win->content_rect(); + int mx = ev.x - cr.x; + int my = ev.y - cr.y; + + // Tab bar click + if (my >= 0 && my < DD_TAB_BAR_H) { + int tab_w = win->content_w / DD_TAB_COUNT; + int tab = mx / tab_w; + if (tab >= 0 && tab < DD_TAB_COUNT) { + dd->active_tab = tab; + } + } +} + +static void diskdetail_on_close(Window* win) { + if (win->app_data) { + montauk::mfree(win->app_data); + win->app_data = nullptr; + } +} + +static void open_disk_detail(DesktopState* ds, int port, const char* model) { + char title[64]; + int i = 0; + const char* prefix = "Disk: "; + while (prefix[i]) { title[i] = prefix[i]; i++; } + int j = 0; + while (model[j] && i < 62) { title[i++] = model[j++]; } + title[i] = '\0'; + + int idx = desktop_create_window(ds, title, 180, 90, 440, 420); + if (idx < 0) return; + + Window* win = &ds->windows[idx]; + DiskDetailState* dd = (DiskDetailState*)montauk::malloc(sizeof(DiskDetailState)); + montauk::memset(dd, 0, sizeof(DiskDetailState)); + + dd->desktop = ds; + dd->active_tab = 0; + montauk::diskinfo(&dd->info, port); + + win->app_data = dd; + win->on_draw = diskdetail_on_draw; + win->on_mouse = diskdetail_on_mouse; + win->on_close = diskdetail_on_close; +} + +// ============================================================================ +// Callbacks +// ============================================================================ + static void devexplorer_on_mouse(Window* win, MouseEvent& ev) { DevExplorerState* de = (DevExplorerState*)win->app_data; if (!de) return; @@ -340,6 +653,7 @@ static void devexplorer_on_mouse(Window* win, MouseEvent& ev) { int row_count = build_display_rows(de, rows); // Walk rows from scroll offset, accumulating y + uint64_t now = montauk::get_milliseconds(); int cur_y = list_y; for (int i = de->scroll_y; i < row_count; i++) { int row_h = (rows[i].type == ROW_CATEGORY) ? DE_CAT_H : DE_ITEM_H; @@ -350,8 +664,23 @@ static void devexplorer_on_mouse(Window* win, MouseEvent& ev) { int cat = rows[i].category; de->collapsed[cat] = !de->collapsed[cat]; de->selected_row = -1; + de->last_click_row = -1; } else { - de->selected_row = i; + // Check for double-click on storage device + int di = rows[i].dev_index; + bool is_double = (de->last_click_row == i) + && (now - de->last_click_ms < 400); + + if (is_double && de->devs[di].category == 7) { + // Storage device — open detail window + int port = (int)de->devs[di]._pad[0]; + open_disk_detail(de->desktop, port, de->devs[di].name); + de->last_click_row = -1; + } else { + de->selected_row = i; + de->last_click_row = i; + de->last_click_ms = now; + } } return; } @@ -360,6 +689,7 @@ static void devexplorer_on_mouse(Window* win, MouseEvent& ev) { } // Clicked below all rows de->selected_row = -1; + de->last_click_row = -1; } } } @@ -458,6 +788,8 @@ void open_devexplorer(DesktopState* ds) { de->selected_row = -1; de->scroll_y = 0; de->last_poll_ms = 0; + de->last_click_row = -1; + de->last_click_ms = 0; // All categories start expanded for (int i = 0; i < NUM_CATEGORIES; i++)