/**
 * PROJECT:         ExectOS
 * COPYRIGHT:       See COPYING.md in the top level directory
 * FILE:            xtldr/amd64/memory.cc
 * DESCRIPTION:     EFI memory management for AMD64 target
 * DEVELOPERS:      Rafal Kupiec <belliash@codingworkshop.eu.org>
 *                  Aiken Harris <harraiken91@gmail.com>
 */

#include <xtos.hh>


XTCDECL
EFI_STATUS
Xtos::BuildPageMap(IN PXTBL_PAGE_MAPPING PageMap)
{
    EFI_STATUS Status;

    /* Build page map */
    Status = XtLdrProtocol->Memory.BuildPageMap(PageMap, (PageMap->PageMapLevel > 4) ? MM_P5E_LA57_BASE : MM_PXE_BASE);
    if(Status != STATUS_EFI_SUCCESS)
    {
        /* Failed to build page map */
        XtLdrProtocol->Debug.Print(L"Failed to build page map (Status code: %zX)\n", Status);
        return Status;
    }

    /* Map memory for hardware layer */
    Status = MapHardwareMemoryPool(PageMap);
    if(Status != STATUS_EFI_SUCCESS)
    {
        /* Failed to map memory for hardware layer */
        XtLdrProtocol->Debug.Print(L"Failed to map memory for hardware leyer (Status code: %zX)\n", Status);
        return Status;
    }

    /* Return success */
    return STATUS_EFI_SUCCESS;
}

/**
 * Determines the appropriate EFI memory mapping strategy for the AMD64 architecture.
 *
 * @return This routine returns TRUE, what results in an identity mapping.
 *
 * @since XT 1.0
 */
XTCDECL
BOOLEAN
Xtos::DetermineMappingStrategy()
{
    /* Use an identity mapping strategy */
    return TRUE;
}

/**
 * Determines the appropriate paging level (PML) for the AMD64 architecture.
 *
 * @param Parameters
 *        A pointer to the wide character string containing the kernel boot parameters.
 *
 * @return This routine returns the appropriate page map level (5 if LA57 is enabled, 4 otherwise).
 *
 * @since XT 1.0
 */
XTCDECL
ULONG
Xtos::DeterminePagingLevel(IN CONST PWCHAR Parameters)
{
    CPUID_REGISTERS CpuRegisters;

    /* Prepare CPUID registers to query for STD7 features */
    XtLdrProtocol->Memory.ZeroMemory(&CpuRegisters, sizeof(CPUID_REGISTERS));
    CpuRegisters.Leaf = CPUID_GET_VENDOR_STRING;

    /* Query CPUID */
    XtLdrProtocol->Cpu.CpuId(&CpuRegisters);

    /* Verify if the CPU supports the STD7 feature leaf (0x00000007) */
    if(CpuRegisters.Eax >= CPUID_GET_STANDARD7_FEATURES)
    {
        /* Prepare CPUID registers to query for LA57 support */
        XtLdrProtocol->Memory.ZeroMemory(&CpuRegisters, sizeof(CPUID_REGISTERS));
        CpuRegisters.Leaf = CPUID_GET_STANDARD7_FEATURES;

        /* Query CPUID */
        XtLdrProtocol->Cpu.CpuId(&CpuRegisters);

        /* Check if eXtended Physical Addressing (XPA) is enabled and if LA57 is supported by the CPU */
        if((CpuRegisters.Ecx & CPUID_FEATURES_ECX_LA57) &&
           !(XtLdrProtocol->BootUtils.GetBooleanParameter(Parameters, L"NOXPA")))
        {
            /* Enable LA57 (PML5) */
            return 5;
        }
    }

    /* Disable LA57 and use PML4 by default */
    return 4;
}

/**
 * Builds the actual memory mapping page table and enables paging. This routine exits EFI boot services as well.
 *
 * @param PageMap
 *        Supplies a pointer to the page mapping structure.
 *
 * @return This routine returns a status code.
 *
 * @since XT 1.0
 */
XTCDECL
EFI_STATUS
Xtos::EnablePaging(IN PXTBL_PAGE_MAPPING PageMap)
{
    EFI_STATUS Status;
    EFI_PHYSICAL_ADDRESS TrampolineAddress;
    PXT_TRAMPOLINE_ENTRY TrampolineEntry;
    ULONG_PTR TrampolineSize;
    PVOID TrampolineCode;

    /* Check the configured page map level to set the LA57 state accordingly */
    if(PageMap->PageMapLevel == 5)
    {
        /* Get the trampoline code information */
        XtLdrProtocol->BootUtils.GetTrampolineInformation(TrampolineEnableXpa, &TrampolineCode, &TrampolineSize);
        if(TrampolineCode == NULLPTR || TrampolineSize == 0)
        {
            /* Failed to get trampoline information */
            XtLdrProtocol->Debug.Print(L"Failed to get trampoline information\n");
            return STATUS_EFI_INVALID_PARAMETER;
        }

        /* Set the address of the trampoline code below 1MB */
        TrampolineAddress = MM_TRAMPOLINE_ADDRESS;

        /* Allocate pages for the trampoline */
        Status = XtLdrProtocol->Memory.AllocatePages(AllocateAddress, EFI_SIZE_TO_PAGES(TrampolineSize), &TrampolineAddress);
        if(Status != STATUS_EFI_SUCCESS)
        {
            /* Failed to allocate memory for trampoline code */
            XtLdrProtocol->Debug.Print(L"Failed to allocate memory for trampoline code (Status code: %zX)\n", Status);
            return Status;
        }

        /* Set the trampoline entry point and copy its code into the allocated buffer */
        TrampolineEntry = (PXT_TRAMPOLINE_ENTRY)(UINT_PTR)TrampolineAddress;
        XtLdrProtocol->Memory.CopyMemory((PVOID)TrampolineEntry, TrampolineCode, TrampolineSize);
    }

    /* Exit EFI Boot Services */
    XtLdrProtocol->Debug.Print(L"Exiting EFI boot services\n");
    Status = XtLdrProtocol->Utils.ExitBootServices();
    if(Status != STATUS_EFI_SUCCESS)
    {
        /* Failed to exit boot services */
        XtLdrProtocol->Debug.Print(L"Failed to exit boot services (Status code: %zX)\n", Status);
        return STATUS_EFI_ABORTED;
    }

    /* Check the configured page map level to set the LA57 state accordingly */
    if(PageMap->PageMapLevel == 5)
    {
        /* Enable Linear Address 57-bit (LA57) extension */
        XtLdrProtocol->Debug.Print(L"Enabling Linear Address 57-bit (LA57)\n");

        /* Execute the trampoline to enable LA57 and write PML5 to CR3 */
        TrampolineEntry((UINT64)PageMap->PtePointer);
    }
    else
    {
        /* Disable Linear Address 57-bit (LA57) extension */
        XtLdrProtocol->Debug.Print(L"Disabling Linear Address 57-bit (LA57)\n");

        /* Write PML4 to CR3 and enable paging */
        XtLdrProtocol->Cpu.WriteControlRegister(3, (UINT_PTR)PageMap->PtePointer);
        XtLdrProtocol->Cpu.WriteControlRegister(0, XtLdrProtocol->Cpu.ReadControlRegister(0) | CR0_PG);
    }

    /* Return success */
    return STATUS_EFI_SUCCESS;
}

/**
 * Maps the page table for hardware layer addess space.
 *
 * @param PageMap
 *        Supplies a pointer to the page mapping structure.
 *
 * @return This routine returns a status code.
 *
 * @since XT 1.0
 */
XTCDECL
EFI_STATUS
Xtos::MapHardwareMemoryPool(IN PXTBL_PAGE_MAPPING PageMap)
{
    PHARDWARE_PTE P5eBase, PdeBase, PpeBase, PxeBase;
    EFI_PHYSICAL_ADDRESS Address;
    EFI_STATUS Status;
    ULONG Index;

    if(PageMap->PageMapLevel == 5)
    {
        /* Get P5E (PML5) base address */
        P5eBase = (PHARDWARE_PTE)PageMap->PtePointer;

        /* Check if P5E entry already exists */
        if(!P5eBase[(MM_HARDWARE_VA_START >> MM_P5I_SHIFT) & 0x1FF].Valid)
        {
            /* No valid P5E, allocate memory */
            Status = XtLdrProtocol->Memory.AllocatePages(AllocateAnyPages, 1, &Address);
            if(Status != STATUS_EFI_SUCCESS)
            {
                /* Memory allocation failure, return error */
                return Status;
            }

            /* Map hardware memory */
            XtLdrProtocol->Memory.MapVirtualMemory(PageMap, (ULONGLONG)NULLPTR, Address, 1, LoaderMemoryData);

            /* Zero fill memory used by P5E */
            XtLdrProtocol->Memory.ZeroMemory((PVOID)Address, EFI_PAGE_SIZE);

            /* Make P5E valid */
            P5eBase[(MM_HARDWARE_VA_START >> MM_P5I_SHIFT) & 0x1FF].Valid = 1;
            P5eBase[(MM_HARDWARE_VA_START >> MM_P5I_SHIFT) & 0x1FF].PageFrameNumber = Address / EFI_PAGE_SIZE;
            P5eBase[(MM_HARDWARE_VA_START >> MM_P5I_SHIFT) & 0x1FF].Writable = 1;

            /* Set PXE base address */
            PxeBase = (PHARDWARE_PTE)(UINT_PTR)Address;
        }
        else
        {
            /* Set PXE base address based on existing P5E */
            PxeBase = (PHARDWARE_PTE)((P5eBase[(MM_HARDWARE_VA_START >> MM_P5I_SHIFT) & 0x1FF].PageFrameNumber) << EFI_PAGE_SHIFT);
        }
    }
    else
    {
        /* Get PXE (PML4) base address */
        PxeBase = (PHARDWARE_PTE)PageMap->PtePointer;
    }

    /* Check if PXE entry already exists */
    if(!PxeBase[(MM_HARDWARE_VA_START >> MM_PXI_SHIFT) & 0x1FF].Valid)
    {
        /* No valid PXE, allocate memory */
        Status = XtLdrProtocol->Memory.AllocatePages(AllocateAnyPages, 1, &Address);
        if(Status != STATUS_EFI_SUCCESS)
        {
            /* Memory allocation failure, return error */
            return Status;
        }

        /* Map hardware memory */
        XtLdrProtocol->Memory.MapVirtualMemory(PageMap, (ULONGLONG)NULLPTR, Address, 1, LoaderMemoryData);

        /* Zero fill memory used by PXE */
        XtLdrProtocol->Memory.ZeroMemory((PVOID)Address, EFI_PAGE_SIZE);

        /* Make PXE valid */
        PxeBase[(MM_HARDWARE_VA_START >> MM_PXI_SHIFT) & 0x1FF].Valid = 1;
        PxeBase[(MM_HARDWARE_VA_START >> MM_PXI_SHIFT) & 0x1FF].PageFrameNumber = Address / EFI_PAGE_SIZE;
        PxeBase[(MM_HARDWARE_VA_START >> MM_PXI_SHIFT) & 0x1FF].Writable = 1;

        /* Set PPE base address */
        PpeBase = (PHARDWARE_PTE)(UINT_PTR)Address;
    }
    else
    {
        /* Set PPE base address based on existing PXE */
        PpeBase = (PHARDWARE_PTE)((PxeBase[(MM_HARDWARE_VA_START >> MM_PXI_SHIFT) & 0x1FF].PageFrameNumber) << EFI_PAGE_SHIFT);
    }

    /* Check if PPE entry already exists */
    if(!PpeBase[(MM_HARDWARE_VA_START >> MM_PPI_SHIFT) & 0x1FF].Valid)
    {
        /* No valid PPE, allocate memory */
        Status = XtLdrProtocol->Memory.AllocatePages(AllocateAnyPages, 1, &Address);
        if(Status != STATUS_EFI_SUCCESS)
        {
            /* Memory allocation failure, return error */
            return Status;
        }

        /* Map hardware memory */
        XtLdrProtocol->Memory.MapVirtualMemory(PageMap, (ULONGLONG)NULLPTR, Address, 1, LoaderMemoryData);

        /* Zero fill memory used by PPE */
        XtLdrProtocol->Memory.ZeroMemory((PVOID)Address, EFI_PAGE_SIZE);

        /* Make PPE valid */
        PpeBase[(MM_HARDWARE_VA_START >> MM_PPI_SHIFT) & 0x1FF].Valid = 1;
        PpeBase[(MM_HARDWARE_VA_START >> MM_PPI_SHIFT) & 0x1FF].PageFrameNumber = Address / EFI_PAGE_SIZE;
        PpeBase[(MM_HARDWARE_VA_START >> MM_PPI_SHIFT) & 0x1FF].Writable = 1;

        /* Set PDE base address */
        PdeBase = (PHARDWARE_PTE)Address;
    }
    else
    {
        /* Set PDE base address, based on existing PPE */
        PdeBase = (PHARDWARE_PTE)((PpeBase[(MM_HARDWARE_VA_START >> MM_PPI_SHIFT) & 0x1FF].PageFrameNumber) << EFI_PAGE_SHIFT);
    }

    /* Loop through 2 PDE entries */
    for(Index = 0 ; Index < 2 ; Index++)
    {
        /* Check if PDE entry already exists */
        if(!PdeBase[((MM_HARDWARE_VA_START >> MM_PDI_SHIFT) & 0x1FF) + Index].Valid)
        {
            /* No valid PDE, allocate memory */
            Status = XtLdrProtocol->Memory.AllocatePages(AllocateAnyPages, 1, &Address);
            if(Status != STATUS_EFI_SUCCESS)
            {
                /* Memory allocation failure, return error */
                return Status;
            }

            /* Map hardware memory */
            XtLdrProtocol->Memory.MapVirtualMemory(PageMap, (ULONGLONG)NULLPTR, Address, 1, LoaderMemoryData);

            /* Zero fill memory used by PDE */
            XtLdrProtocol->Memory.ZeroMemory((PVOID)Address, EFI_PAGE_SIZE);

            /* Make PDE valid */
            PdeBase[((MM_HARDWARE_VA_START >> MM_PDI_SHIFT) & 0x1FF) + Index].Valid = 1;
            PdeBase[((MM_HARDWARE_VA_START >> MM_PDI_SHIFT) & 0x1FF) + Index].PageFrameNumber = Address / EFI_PAGE_SIZE;
            PdeBase[((MM_HARDWARE_VA_START >> MM_PDI_SHIFT) & 0x1FF) + Index].Writable = 1;
        }
    }

    /* Return success */
    return STATUS_EFI_SUCCESS;
}
