Step 13 · Loading and execution

The guest address space

The guest allocates from fixed bands and stores raw pointers assuming it got the addresses it asked for. The emulator therefore owns the whole address space up front and hands out pieces — a very different job from calling malloc.

Before this page: Virtual memory, briefly. View the learning route.

Read

src/kernel/memory.cpp implements three allocation models, page attributes, naming and queries — because games observe all of them.

Three ways a game gets memory

KindGuest APIModel
Direct sceKernelAllocateDirectMemory
sceKernelMapDirectMemory
sceKernelReleaseDirectMemory
A reservation out of physical memory, identified by a physical offset. Mapping it to a virtual address is a separate step, and the same physical range may be mapped more than once. Games use this for anything the GPU touches.
Flexible sceKernelMapFlexibleMemory
sceKernelAvailableFlexibleMemorySize
Ordinary paged memory from a fixed per-title budget, declared in param.json and applied before the memory subsystem initialises.
Pooled sceKernelMemoryPoolReserve
…Commit / …Decommit / …Batch
Two-phase reserve-then-commit, closer to VirtualAlloc. The batch interface lets a game perform dozens of map operations in a single call.

What the guest can ask about memory

sceKernelVirtualQuery is the one to notice, because it forces Kyty to keep real bookkeeping rather than just forwarding to the host allocator:

src/kernel/memory.h
struct VirtualQueryInfo {
    uintptr_t start;
    uintptr_t end;
    uint64_t  offset;
    int32_t   protection;
    int32_t   memory_type;
    uint32_t  is_flexible  : 1;
    uint32_t  is_direct    : 1;
    uint32_t  is_stack     : 1;
    uint32_t  is_pooled    : 1;
    uint32_t  is_committed : 1;
    uint32_t  is_gpu_prt   : 1;
    uint32_t  amm_usage    : 1;
    uint32_t  reserved     : 1;
    char      name[KERNEL_MAXIMUM_NAME_LENGTH];   // the game's own label for the range
    uint8_t   gpu_mask_id;
    uint8_t   reserved2;
};
static_assert(sizeof(VirtualQueryInfo) == 72);

The static_assert is not decoration. This struct is written into guest memory, so its layout must match the console's byte for byte. Several structs in the kernel headers carry the same guard.

The internal interface

Four allocation entry points serve different purposes, and choosing the wrong one is a real bug class:

uint64_t AllocateProgramMemory(uint64_t search_addr, uint64_t size, Mode mode, const char* name);
uint64_t AllocateRuntimeMemory(uint64_t search_addr, uint64_t size, Mode mode, const char* name,
                               bool fixed = false);
uint64_t AllocateGuestStackMemory(uint64_t search_addr, uint64_t size, Mode mode, const char* name);

bool ProtectGuestMemory(uint64_t vaddr, uint64_t size, Mode mode, Mode* old_mode = nullptr);
// Transient PageManager watch state; does NOT change the mapping's semantic protection.
bool ProtectGuestHostMemory(uint64_t vaddr, uint64_t size, Mode mode);

That last comment is the key distinction. ProtectGuestMemory changes what the guest believes the protection is, and a later sceKernelVirtualQuery will report it. ProtectGuestHostMemory changes only the host page bits so the tracker can catch a write — the guest must never see it.

The GPU-facing hooks

void RegisterCallbacks(callback_func_t alloc_func, callback_func_t free_func);
void InstallGpuResources(Graphics::RenderContext* resources) noexcept;
bool HandleGpuFault(Graphics::PageFaultAccess access, uint64_t fault_vaddr) noexcept;
void InvalidateMemory(uint64_t vaddr, uint64_t size);
bool TryWriteBacking(uint64_t vaddr, const void* data, uint64_t size);
bool TryReadBacking(uint64_t vaddr, void* data, uint64_t size);

The memory manager notifies the graphics layer whenever guest memory is mapped or unmapped, so caches can drop resources whose backing store has gone away — a common source of use-after-free bugs if it is missed.

The separate extended-memory arena

Ordinary guest mappings keep their existing low address bands. Extended allocations use a separate 512 GiB virtual arena at 0x080000000000, an 8 TiB base, described by kExtendedMemoryBase and kExtendedMemorySize in kernel/memory.h. Reserving that address range does not allocate 512 GiB of physical RAM. Extended requests and ordinary mapping searches have separate allocation rules.

GPU tracking accepts the low 1 TiB region and that explicit extended interval; the space between them is not a valid resource range. Sparse image tracking covers 44-bit addresses. Dense buffer/BDA tables pack the low and extended intervals together through BufferCache::PageIndex() (guest address to packed page index) and GuestAddress() (packed byte offset to guest address), avoiding tables for the entire gap.

Automatic backing is recycled through physical-allocation ownership. Reserved mappings are published only after backing commit succeeds, and partial POSIX unmaps preserve still-live mappings. These are allocation/alias invariants tested by VirtualMemoryAllocationTests.cpp. Host primitives are declared directly in common/virtualMemory.h and implemented by sysWindowsVirtual.cpp / sysLinuxVirtual.cpp.

Failure injection

Compiled behind a flag, the allocator exposes deliberate failure hooks so its error paths are actually exercised by tests rather than only in the field:

#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
void TestFailNextPhysicalMemoryUnmap();
void TestFailPhysicalMemoryUnmapAfter(uint32_t successful_unmaps);
void TestFailGuestBackingStoreUnmapAfter(uint32_t successful_unmaps);
void TestFailNextFixedReserveRangeRegistration();
bool TestPlaceholderRangeIsFree(uint64_t vaddr, uint64_t size);
bool TestGuestAddressRangeIsOwned(uint64_t vaddr, uint64_t size);
#endif
src/kernel/memory.htests/VirtualMemoryAllocationTests.cpp

Sparse texture uploads

BufferCache::ObtainBufferForImage() reads streamed mip ranges through Memory::TryReadSparseBacking(). The guest address-space owner validates the complete range and rejects private mappings before the backing store copies resident pieces and zero-fills holes. A hole inside an owned sparse reservation is not the same as an arbitrary invalid guest address; do not replace failed reads with unconditional zeroes.

Watch

The bands laid out, with the host reservation underneath. The guest's view and the host's view of the same bytes, side by side.

Try

Allocate, free, repeat — until the space is fragmented enough that a large request fails even though the free total is plenty. That failure is exactly what the Windows placeholder-coalescing path exists to avoid.

And take a single address apart: which band it belongs to, which page it lands in, and what the offset within that page is.

Do

Get the real numbers from a real run rather than trusting the diagram.

Checkpoint