Step 09 · Emulation foundations
Virtual memory, briefly
Because guest code runs natively, the emulator cannot instrument a memory access. What it can do is take away permission to a page and let the CPU tell it when something touched it. Page protection is not background knowledge here — it is the primary tool.
Before this page: Objects, owners and borrowed memory · Why there's no CPU emulator. View the learning route.
Page protection is not a side topic in this project — it is the mechanism by which the emulator detects that the game has touched something the GPU cares about. You need to understand it properly.
Addresses are lies
When a program reads address 0x900001000, that is a virtual address. The CPU's memory management unit translates it, through page tables the OS maintains, into a physical address in RAM. The translation happens per page — a fixed-size block, 4 KB on PC, 16 KB on PS5.
Each page table entry carries permission bits: readable, writable, executable. If a program accesses a page in a way the bits forbid, the CPU raises a fault. The OS delivers it to the process as an exception (Windows) or a signal (Linux, macOS).
Reserving address space claims a range of virtual addresses so nothing else can use them, without allocating any physical memory. Committing backs part of that range with real memory.
This split is why an emulator can claim a terabyte of address space up front (see the memory map below) while using only a few gigabytes of RAM. Kyty reserves entire bands and hands out committed pieces from inside them.
The three things Kyty does with protection
sceKernelMprotect, Kyty applies the equivalent host protection so that a genuine guest bug still faults the way it would on console.All three funnel into one function — the exception handler installed by the loader — which is why Page faults & CPU/GPU coherency exists.
The page-size mismatch
The PS5 uses 16 KB pages; PCs use 4 KB. Kyty's guest allocator works in 0x4000-byte units:
constexpr uint64_t GUEST_PAGE_SIZE = 0x4000; program->base_size_aligned = AlignUp(program->base_size, GUEST_PAGE_SIZE);
One 16 KB guest page spans four 4 KB host pages. Guest VM alignment and renderer tracking are separate: TRACKER_PAGE_SIZE is 4 KB, so a tracking fault does not inherently dirty the entire 16 KB guest page. Host protection calls accept ranges and can cover several pages in one call.
The address map Kyty imposes
Kyty does not let the host OS choose where things go. It divides the address space into bands and enforces them:
macOS shifts these ranges and pins them with linker flags — -segaddr SYSTEM_RESERVED,0x7ffffc000 -segaddr USER_AREA,0x7000000000 — so the dynamic loader can never place a system library where the guest needs to allocate. CMakeLists.txt
Console software hard-codes addresses and assumes allocations land in particular ranges. It also passes pointers to the GPU, which in Kyty means those pointers must be distinguishable from emulator pointers. Fixing the bands makes both problems tractable: use the actual range bounds and loaded-segment metadata to classify an address. A hexadecimal prefix alone does not prove that a value is guest code, heap data or a host pointer.
The detail that poisons everything downstream: the guest is certain pages are 16 KB, the host insists they are 4 KB. Watch what happens to a single guest-level protection change.
Take an address apart by hand. Change it and watch which page it lands in, and where the boundary sits under each page size.
This topic is groundwork, but it is groundwork you will use in the memory and coherency topics constantly.
Checkpoint