KVM support

Co-authored-by: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com>
Co-authored-by: hazelwiss <hazelwiss.rs@gmail.com>
This commit is contained in:
offtkp 2023-11-18 15:18:04 +02:00
parent 91f8f81418
commit bbbc7fb1f2
6 changed files with 517 additions and 4 deletions

View file

@ -43,6 +43,7 @@ option(ENABLE_HTTP_SERVER "Enable HTTP server. Used for Discord bot support" OFF
option(ENABLE_DISCORD_RPC "Compile with Discord RPC support (disabled by default)" ON)
option(ENABLE_LUAJIT "Enable scripting with the Lua programming language" ON)
option(ENABLE_QT_GUI "Enable the Qt GUI. If not selected then the emulator uses a minimal SDL-based UI instead" OFF)
option(USE_KVM "Use KVM instead of Dynarmic" OFF)
option(BUILD_HYDRA_CORE "Build a Hydra core" OFF)
option(BUILD_LIBRETRO_CORE "Build a Libretro core" OFF)
@ -155,12 +156,14 @@ else()
set(HOST_ARM64 FALSE)
endif()
if(HOST_X64 OR HOST_ARM64)
if(NOT USE_KVM AND (HOST_X64 OR HOST_ARM64))
set(DYNARMIC_TESTS OFF)
#set(DYNARMIC_NO_BUNDLED_FMT ON)
set(DYNARMIC_FRONTENDS "A32" CACHE STRING "")
add_subdirectory(third_party/dynarmic)
add_compile_definitions(CPU_DYNARMIC)
elseif(USE_KVM AND HOST_ARM64)
add_compile_definitions(CPU_KVM)
else()
message(FATAL_ERROR "Currently unsupported CPU architecture")
endif()

View file

@ -3,7 +3,7 @@
#ifdef CPU_DYNARMIC
#include "cpu_dynarmic.hpp"
#elif defined(CPU_KVM)
#error KVM CPU is not implemented yet
#include "cpu_kvm.hpp"
#else
#error No CPU core implemented :(
#endif

235
include/cpu_kvm.hpp Normal file
View file

@ -0,0 +1,235 @@
#pragma once
#include <fcntl.h>
#include <linux/kvm.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/mman.h>
#include "helpers.hpp"
#include "kernel.hpp"
#include "memory.hpp"
#define AARCH64_CORE_REG(x) (KVM_REG_ARM64 | KVM_REG_SIZE_U64 | KVM_REG_ARM_CORE | KVM_REG_ARM_CORE_REG(x))
struct MmuTables {
u32 level1[4096];
u32 level2SectionTables[256];
};
constexpr u32 hypervisorCodeAddress = 0xD0000000;
constexpr u32 hypervisorDataAddress = 0xE0000000;
constexpr u32 hypervisorCodeSize = hypervisorDataAddress - hypervisorCodeAddress;
constexpr u32 hypervisorDataSize = hypervisorCodeSize;
constexpr u32 mmuTableOffset = hypervisorDataSize - sizeof(MmuTables);
constexpr u32 mmuTableAddress = hypervisorDataAddress + mmuTableOffset;
constexpr u32 exitCodeOffset = 0; // at start of hypervisor data segment
constexpr u32 customEntryOffset = 0x100000; // arbitrary, far enough that the exit code won't ever overlap with this
constexpr u32 guestStateOffset = 0x200000; // also arbitrary, store the guest state here upon exit
struct GuestState
{
std::array<u32, 16> regs;
std::array<u32, 32> fprs;
u32 cpsr;
u32 fpscr;
// u32 tlsBase?
// u64 ticks?
};
struct Environment {
Environment(Memory& mem, Kernel& kernel) : mem(mem), kernel(kernel) {
u32 currentMemorySlot = 0;
kvmDescriptor = open("/dev/kvm", O_RDWR);
if (kvmDescriptor < 0) {
Helpers::panic("Failed to open /dev/kvm");
}
vmDescriptor = ioctl(kvmDescriptor, KVM_CREATE_VM, 0);
if (vmDescriptor < 0) {
Helpers::panic("Failed to create KVM VM");
}
if (ioctl(vmDescriptor, KVM_CHECK_EXTENSION, KVM_CAP_ARM_EL1_32BIT) <= 0) {
Helpers::panic("CPU doesn't support EL1 32-bit mode, KVM won't work on this CPU");
}
// TODO: allocate these with mmap instead of malloc
kvm_userspace_memory_region vramRegionDesc = {
.slot = currentMemorySlot++,
.flags = 0,
.guest_phys_addr = PhysicalAddrs::VRAM,
.memory_size = PhysicalAddrs::VRAMSize,
.userspace_addr = (uint64_t)mem.getVRAM()};
if (ioctl(vmDescriptor, KVM_SET_USER_MEMORY_REGION, &vramRegionDesc) < 0) {
Helpers::panic("Failed to set VRAM memory region");
}
kvm_userspace_memory_region dspRegionDesc = {
.slot = currentMemorySlot++,
.flags = 0,
.guest_phys_addr = PhysicalAddrs::DSPMem,
.memory_size = PhysicalAddrs::DSPMemSize,
.userspace_addr = (uint64_t)mem.getDSPMem()};
if (ioctl(vmDescriptor, KVM_SET_USER_MEMORY_REGION, &dspRegionDesc) < 0) {
Helpers::panic("Failed to set DSP memory region");
}
kvm_userspace_memory_region fcramRegionDesc = {
.slot = currentMemorySlot++,
.flags = 0,
.guest_phys_addr = PhysicalAddrs::FCRAM,
.memory_size = PhysicalAddrs::FCRAMSize * 2,
.userspace_addr = (uint64_t)mem.getFCRAM()};
if (ioctl(vmDescriptor, KVM_SET_USER_MEMORY_REGION, &fcramRegionDesc) < 0) {
Helpers::panic("Failed to set FCRAM memory region");
}
hypervisorCodeRegion = mmap(NULL, hypervisorCodeSize, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
if (hypervisorCodeRegion == MAP_FAILED) {
Helpers::panic("Failed to allocate memory for hypervisor I/O");
}
kvm_userspace_memory_region hypervisorCodeRegionDesc = {
.slot = currentMemorySlot++,
.flags = KVM_MEM_READONLY, // We want writes here to cause VM exits
.guest_phys_addr = hypervisorCodeAddress,
.memory_size = hypervisorCodeSize,
.userspace_addr = (uint64_t)hypervisorCodeRegion
};
if (ioctl(vmDescriptor, KVM_SET_USER_MEMORY_REGION, &hypervisorCodeRegionDesc) < 0) {
Helpers::panic("Failed to set up hypervisor IO memory region\n");
return;
}
hypervisorDataRegion = mmap(NULL, hypervisorDataSize, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (hypervisorDataRegion == MAP_FAILED) {
Helpers::panic("Failed to allocate memory for hypervisor code");
}
kvm_userspace_memory_region hypervisorDataRegionDesc = {
.slot = currentMemorySlot++,
.flags = 0,
.guest_phys_addr = hypervisorDataAddress,
.memory_size = hypervisorDataSize,
.userspace_addr = (uint64_t)hypervisorDataRegion
};
if (ioctl(vmDescriptor, KVM_SET_USER_MEMORY_REGION, &hypervisorDataRegionDesc) < 0) {
Helpers::panic("Failed to set up hypervisor code memory region\n");
return;
}
cpuDescriptor = ioctl(vmDescriptor, KVM_CREATE_VCPU, 0);
if (cpuDescriptor < 0) {
Helpers::panic("Failed to create VCPU");
}
int mmapSize = ioctl(kvmDescriptor, KVM_GET_VCPU_MMAP_SIZE, 0);
if (mmapSize < 0) {
Helpers::panic("Failed to get KVM shared memory size");
}
runInfo = (kvm_run*)mmap(nullptr, mmapSize, PROT_READ | PROT_WRITE, MAP_SHARED, cpuDescriptor, 0);
if (runInfo == MAP_FAILED) {
Helpers::panic("Failed to map KVM shared memory");
}
kvm_vcpu_init initParams;
if (ioctl(vmDescriptor, KVM_ARM_PREFERRED_TARGET, &initParams) < 0) {
Helpers::panic("Failed to fetch initialization parameters for vCPU");
}
initParams.features[0] |= 1 << KVM_ARM_VCPU_EL1_32BIT;
initParams.features[0] |= 1 << KVM_ARM_VCPU_PSCI_0_2;
if (ioctl(cpuDescriptor, KVM_ARM_VCPU_INIT, initParams) < 0) {
Helpers::panic("Failed to initialize vCPU");
}
kvm_reg_list tempRegList;
tempRegList.n = 0;
ioctl(cpuDescriptor, KVM_GET_REG_LIST, &tempRegList);
regList = (kvm_reg_list*)malloc(sizeof(kvm_reg_list) + tempRegList.n * sizeof(u64));
regList->n = tempRegList.n;
if (ioctl(cpuDescriptor, KVM_GET_REG_LIST, regList) < 0) {
Helpers::panic("Failed to get register list");
}
}
void setPC(u32 pc) {
u64 val = (u64)pc;
kvm_one_reg reg;
reg.id = AARCH64_CORE_REG(regs.pc);
reg.addr = (u64)&val;
if (ioctl(cpuDescriptor, KVM_SET_ONE_REG, &reg) < 0) [[unlikely]] {
printf("SetPC failed\n");
}
}
void run() {
if (ioctl(cpuDescriptor, KVM_RUN, 0) < 0) {
Helpers::panic("Failed to run vCPU");
} else {
printf("KVM run succeeded\n");
}
}
void mapHypervisorCode(const std::vector<u8>& code, u32 offset)
{
if (code.size() > hypervisorCodeSize) {
Helpers::panic("Launch code is too big");
}
memcpy((void*)((uintptr_t)hypervisorCodeRegion + offset), code.data(), code.size());
}
Memory& mem;
Kernel& kernel;
kvm_run* runInfo = nullptr;
kvm_reg_list* regList = nullptr;
void* hypervisorCodeRegion;
void* hypervisorDataRegion;
int kvmDescriptor = -1;
int vmDescriptor = -1;
int cpuDescriptor = -1;
};
class CPU {
Memory& mem;
Environment env;
GuestState state;
public:
static constexpr u64 ticksPerSec = 268111856;
CPU(Memory& mem, Kernel& kernel);
void reset() {}
void setReg(int index, u32 value) {}
u32 getReg(int index) {return 0;}
std::span<u32, 16> regs() { return state.regs; }
std::span<u32, 32> fprs() { return state.fprs; }
void setCPSR(u32 value) { state.cpsr = value; }
u32 getCPSR() { return state.cpsr; }
void setFPSCR(u32 value) { state.fpscr = value; }
u32 getFPSCR() { return state.fpscr; }
void setTLSBase(u32 value) {}
u64 getTicks() {return 0;}
u64& getTicksRef() {static u64 dummy; return dummy;}
void clearCache() {}
void runFrame() {}
// TODO: remove
void romLoaded();
};
#undef AARCH64_CORE_REG

View file

@ -17,12 +17,18 @@
namespace PhysicalAddrs {
enum : u32 {
VRAM = 0x18000000,
VRAMEnd = VRAM + 0x005FFFFF,
VRAMSize = 0x00600000,
VRAMEnd = VRAM + VRAMSize - 1,
FCRAM = 0x20000000,
FCRAMEnd = FCRAM + 0x07FFFFFF,
DSP_RAM = 0x1FF00000,
DSP_RAM_End = DSP_RAM + 0x0007FFFF
DSP_RAM_End = DSP_RAM + 0x0007FFFF,
FCRAMSize = 0x08000000,
FCRAMEnd = FCRAM + FCRAMSize - 1,
DSPMem = 0x1FF00000,
DSPMemSize = 0x00080000,
DSPMemEnd = DSPMem + DSPMemSize - 1
};
}
@ -284,6 +290,7 @@ private:
u8* getDSPDataMem() { return &dspRam[DSP_DATA_MEMORY_OFFSET]; }
u8* getDSPCodeMem() { return &dspRam[DSP_CODE_MEMORY_OFFSET]; }
u32 getUsedUserMem() { return usedUserMemory; }
u8* getVRAM() { return vram; }
void setVRAM(u8* pointer) { vram = pointer; }
void setDSPMem(u8* pointer) { dspRam = pointer; }

BIN
perf.data Normal file

Binary file not shown.

268
src/core/CPU/cpu_kvm.cpp Normal file
View file

@ -0,0 +1,268 @@
// #ifdef CPU_KVM
#include "cpu_kvm.hpp"
MmuTables* mmuTables = nullptr;
// ARMv6 MMU supports up to two levels of address lookup with 4KiB pages.
// The top level is called the level 1 table. It contains 4096 entries of 4 bytes each (16KiB total).
// The bottom level is called level 2, which contains 256 entries of 4 bytes each (1KiB total).
// The level 1 table supports 3 kind of entries: Pages, Sections and Supersections each corresponding to a page size.
// Pages are for 4KiB pages, Sections are for 1MiB pages and Supersections are for 16MiB pages.
// Sections and supersections don't use the level 2 table at all.
// This is because with a 32 bit vaddr and 4 KiB pages, the offset is 12 bits,
// the level 2 index is 8 bits and the level 1 index is 12 bits -> 12 + 8 + 12 = 32 for the vaddr
// However for sections, the offset is 20 bits, so you can only use
// the level 1 table (up to 4096 entries) because 20 for offset + 12 for level 1 -> 20 + 12 = 32 for the vaddr
// For supersections, you need a 24 bit offset, so the level 1 table actually has up to 256 entries because
// you're left with 8 bits -> 24 + 8 = 32 for the vaddr
// Level 2 entries
// Bits: 31-12 11 10 9 8-6 5-4 3 2 1 0
// Value: BADDR nG S APX TEX[2:0] AP C B 1 XN
// Access permission table:
/*
APX AP Privileged Unprivileged Description
0 00 No access No access Permission fault
0 01 Read/Write No access Privileged Access only
0 10 Read/Write Read No user-mode write
0 11 Read/Write Read/Write Full access
1 00 - - Reserved
1 01 Read No access Privileged Read only
1 10 Read Read Read only
1 11 - - Reserved
*/
constexpr u32 APX = 1 << 9;
constexpr u32 AP0 = 1 << 4;
constexpr u32 AP1 = 1 << 5;
enum Level2Flags : u32
{
Level2Flags_ExecuteNever = 1 << 0,
Level2Flags_Bufferable = 1 << 2,
Level2Flags_Cacheable = 1 << 3,
Level2Flags_Shared = 1 << 10,
Level2Flags_AP_NoUserModeWrite = AP1,
Level2Flags_AP_FullAccess = AP1 | AP0,
};
// Generated by passing the following code to godbolt:
// Thanks libn3ds
/*
// FCSE PID Register (FCSE PID = 0)
// Note: This must be 0 before disabling the MMU otherwise UB
__asm__ volatile ("mcr p15, 0, %0, c13, c0, 0" : : "r"(0));
// Context ID Register (ASID = 0, PROCID = 0)
__asm__ volatile ("mcr p15, 0, %0, c13, c0, 1" : : "r"(0));
// // TTBR0 address shared page table walk and outer cachable write-through, no allocate on write
uint32_t ttbr0 = mmuTableAddress | 0x12;
__asm__ volatile ("mcr p15, 0, %0, c2, c0, 0" : : "r" (ttbr0) : "memory");
// Use the 16 KiB L1 table only
__asm__ volatile ("mcr p15, 0, %0, c2, c0, 2" : : "r"(0));
// Domain 0 = client, remaining domains all = no access
__asm__ volatile("mcr p15, 0, %0, c3, c0, 0" : : "r"(1));
uint32_t* d = (uint32_t*)hypervisorCodeAddress;
*d = hypervisorCodeAddress;
*/
constexpr u8 mmuCodeBefore[] = {
0x00, 0x30, 0xb0, 0xe3, // movs r3, #0
0x10, 0x3f, 0x0d, 0xee, // mcr p15, #0, r3, c13, c0, #0
0x30, 0x3f, 0x0d, 0xee, // mcr p15, #0, r3, c13, c0, #1
0x14, 0x20, 0x9f, 0xe5, // ldr r2, [pc, #0x14]
0x10, 0x2f, 0x02, 0xee, // mcr p15, #0, r2, c2, c0, #0
0x50, 0x3f, 0x02, 0xee, // mcr p15, #0, r3, c2, c0, #2
0x01, 0x30, 0xb0, 0xe3, // movs r3, #1
0x10, 0x3f, 0x03, 0xee, // mcr p15, #0, r3, c3, c0, #0
0x0d, 0x32, 0xa0, 0xe3, // mov r3, #-0x30000000 TODO: instead jump to exit code
0x00, 0x30, 0x83, 0xe5, // str r3, [r3]
(mmuTableAddress & 0xFF) | 0x12, (mmuTableAddress >> 8) & 0xFF, (mmuTableAddress >> 16) & 0xFF, (mmuTableAddress >> 24) & 0xFF,
};
// Generated by passing the following code to godbolt:
// Thanks libn3ds
/*
// Invalidate TLB
__asm__ volatile("mcr p15, 0, %0, c8, c7, 0" : : "r"(0));
__asm__ volatile("dsb");
// Get ACR
uint32_t reg;
__asm__ volatile("mrc p15, 0, %0, c1, c0, 1" : "=r"(reg));
// Enable Return stack, Dynamic branch prediction, Static branch prediction,
// Instruction folding and SMP mode: the CPU is taking part in coherency
reg |= 0x2F;
__asm__ volatile("mcr p15, 0, %0, c1, c0, 1" : : "r"(reg));
// Get CR
__asm__ volatile("mrc p15, 0, %0, c1, c0, 0" : "=r"(reg));
// Enable MMU, D-Cache, Program flow prediction,
// I-Cache, high exception vectors, Unaligned data access,
// subpage AP bits disabled
reg |= 0xC03805;
__asm__ volatile("mcr p15, 0, %0, c1, c0, 0" : : "r"(reg));
// Invalidate both caches
__asm__ volatile("mcr p15, 0, %0, c7, c7, 0" : : "r" (0) : "memory");
__asm__ volatile("dsb");
__asm__ volatile("isb");
uint32_t* d = (uint32_t*)hypervisorCodeAddress;
*d = hypervisorCodeAddress;
*/
constexpr u8 mmuCodeAfter[] = {
0x00, 0x00, 0xb0, 0xe3, // movs r0, #0
0x17, 0x0f, 0x08, 0xee, // mcr p15, #0, r0, c8, c7, #0
0x4f, 0xf0, 0x7f, 0xf5, // dsb sy
0x30, 0x3f, 0x11, 0xee, // mrc p15, #0, r3, c1, c0, #1
0x2f, 0x30, 0x83, 0xe3, // orr r3, r3, #0x2f
0x30, 0x3f, 0x01, 0xee, // mcr p15, #0, r3, c1, c0, #1
0x10, 0x2f, 0x11, 0xee, // mrc p15, #0, r2, c1, c0, #0
0x05, 0x38, 0x03, 0xe3, // movw r3, #0x3805
0xc0, 0x30, 0x40, 0xe3, // movt r3, #0xc0
0x02, 0x30, 0x93, 0xe1, // orrs r3, r3, r2
0x10, 0x3f, 0x01, 0xee, // mcr p15, #0, r3, c1, c0, #0
0x17, 0x0f, 0x07, 0xee, // mcr p15, #0, r0, c7, c7, #0
0x4f, 0xf0, 0x7f, 0xf5, // dsb sy
0x6f, 0xf0, 0x7f, 0xf5, // isb sy
0x0d, 0x32, 0xa0, 0xe3, // mov r3, #-0x30000000 TODO: instead jump to exit code
0x00, 0x30, 0x83, 0xe5, // str r3, [r3]
};
// Store the CPU state and exit the VM, then return from SVC
// Generated from the following ARM32 assembly
/*
push {r0}
ldr r0, GuestStateAddr + 4
stmfd r0, {r1-r12, sp, lr, pc}^
pop {r0}
push {r1}
ldr r1, GuestStateAddr
str r0, [r1]
// Exit the VM
ldr r1, CodeAddr
str r1, [r1]
pop {r1}
CodeAddr:
.word 0xD0000000
GuestStateAddr:
.word 0xE0200000
*/
constexpr u8 svcHandlerCode[] = {
};
/// Level 1, page table entry
/// Bits: 31-10 9 8-5 4 3 2 1 0
/// Value: BADDR IMP Domain SBZ NS PXN 0 1
/// We don't use domains, so we can set it to 0
u32 pageTableEntry(u32 level2Address)
{
// Level 2 tables have 256 entries of 4 bytes each, so they must be aligned to 1KiB
if ((level2Address & 0x3FF) != 0) {
Helpers::panic("level2Address is not aligned to 1KiB");
}
return level2Address | 0b1;
}
u32 level2Entry(u32 physicalAddress, Level2Flags flags)
{
return (physicalAddress & 0xFFFFF000) | 0b10 | flags;
}
void mapPageTables(u32 virtualAddress, u32 physicalAddress, u8 pageCount, Level2Flags flags)
{
if ((virtualAddress & 0xFFFFF000) != 0) {
Helpers::panic("virtualAddress is not aligned to 4KiB");
}
if ((physicalAddress & 0xFFFFF000) != 0) {
Helpers::panic("physicalAddress is not aligned to 4KiB");
}
for (u32 i = 0; i < pageCount * 4096; i += 4096)
{
u8 level2Index = ((virtualAddress + i) >> 12) & 0xFF;
mmuTables->level2SectionTables[level2Index] = level2Entry(physicalAddress + i, flags);
}
u32 level2TableAddressVm = mmuTableAddress + offsetof(MmuTables, level2SectionTables);
mmuTables->level1[virtualAddress >> 20] = pageTableEntry(level2TableAddressVm);
}
CPU::CPU(Memory& mem, Kernel& kernel)
: mem(mem), env(mem, kernel)
{
}
void CPU::romLoaded()
{
NCCH* ncch = mem.getCXI();
if (!ncch) {
// TODO: what to do here?
Helpers::panic("Alber has decided to panic!");
}
// Map the VM exit code which stores all registers to shared hypervisor memory
// and exits the VM by writing to read-only memory.
// We map it at the start of hypervisorCodeAddress.
env.mapHypervisorCode(std::vector<u8>(vmExitCode, vmExitCode + sizeof(vmExitCode)), 0);
printf("Debug: Running pre mmu table code\n");
env.mapHypervisorCode(std::vector<u8>(mmuCodeBefore, mmuCodeBefore + sizeof(mmuCodeBefore)), customEntryOffset);
env.setPC(hypervisorCodeAddress + customEntryOffset);
env.run();
const auto& text = ncch->text;
const auto& rodata = ncch->rodata;
const auto& data = ncch->data;
mmuTables = (MmuTables*)((uintptr_t)env.hypervisorDataRegion + mmuTableOffset);
printf("Debug: level2sectionTables is at %p in host, %08x in guest\n", mmuTables->level2SectionTables, mmuTableAddress + offsetof(MmuTables, level2SectionTables));
mapPageTables(
text.address,
text.address,
text.pageCount,
(Level2Flags)(Level2Flags_Shared |
Level2Flags_Bufferable |
Level2Flags_Cacheable |
Level2Flags_AP_NoUserModeWrite)
);
mapPageTables(
rodata.address,
rodata.address,
rodata.pageCount,
(Level2Flags)(Level2Flags_Shared |
Level2Flags_Bufferable |
Level2Flags_Cacheable |
Level2Flags_AP_NoUserModeWrite |
Level2Flags_ExecuteNever)
);
mapPageTables(
data.address,
data.address,
data.pageCount,
(Level2Flags)(Level2Flags_Shared |
Level2Flags_Bufferable |
Level2Flags_Cacheable |
Level2Flags_AP_FullAccess)
);
printf("Debug: Running post mmu table code\n");
env.mapHypervisorCode(std::vector<u8>(mmuCodeAfter, mmuCodeAfter + sizeof(mmuCodeAfter)), customEntryOffset);
env.setPC(hypervisorCodeAddress + customEntryOffset);
env.run();
printf("Done\n");
}
// #endif