Step 14 · Loading and execution · C10

Pointers, references & memory

An emulator is a memory program: it maps a foreign address space, hands the CPU raw addresses, and shares memory between a CPU and a GPU that both think they own it. This page is how KytyPS5 spells all that in C++ — what it means by * versus &, who owns what, and the one idea that makes the rest of it ordinary.

Before this page: Objects, owners and borrowed memory · C++ language, by example · The guest address space. View the learning route.

Read

You have seen the guest address-space map and the project's C++ syntax. Now identify which addresses may be used as pointers, which objects own storage, and which views borrow it. This prepares the ELF and mapping lessons that follow. The graphics examples exercise the same ownership rules; their draw and synchronization paths are traced later.

A guest pointer is a host pointer

Many emulators translate a guest address into a location in host-managed storage. KytyPS5's native path instead reserves and maps supported guest ranges at usable virtual addresses inside its host process. A guest address can therefore identify the same mapped location when used by guest code and emulator code. This applies only to valid mappings with suitable permissions, bounds and object-access rules; casting an arbitrary integer does not make it safe to dereference.

src/loader/elf.cpp — copying a segment straight to its guest address
m_f->Read(reinterpret_cast<void*>(static_cast<uintptr_t>(vaddr)), size);

vaddr is a uint64_t the ELF says the segment lives at; the loader reads the file bytes into that host address. Elsewhere guest pointer arithmetic is just integer arithmetic cast to a pointer:

src/loader/gamePatch.cpp
const auto* begin = reinterpret_cast<const uint8_t*>(program.base_vaddr + segment.p_vaddr);

This is the direct consequence of there being no CPU emulator: the guest's own machine code passes raw addresses around and dereferences them itself, so those addresses have to be real. There is no translation step to insert because there is nothing translating the code. The cost is that the emulator gives up the low part of its own address space to the guest — and that a bad guest pointer is a real host fault, which is exactly the notification channel the rest of the emulator is built on.

A reference is a bound alias; a pointer is a nullable handle

The spelling helps identify likely usage, but the complete contract also depends on documented preconditions, callers, lifetime and implementation:

  • T& — a reference. Non-null, bound once, never re-seated. Read it as “I have been handed this thing, I will use it, I do not own it, and it is not null.” A const T& parameter is the default way to pass anything bigger than a register without copying it: “read-only borrow.”
  • T* — a pointer. Nullable and re-seatable. Used when null is a real state (a lookup that can miss), for the start of a buffer, and at every C-ABI boundary — the guest's functions are C, so their parameters are pointers.

Const attaches to whichever side you mean: const T* is “a pointer to something I won't change”; T* const is “a pointer I won't re-point.” You will mostly see the first — a promise about the pointee, not the pointer.

Ownership handles and borrowing conventions

RAII owners arrange cleanup when their lifetimes end. Ownership can be exclusive, shared or managed by a subsystem. Types such as unique_ptr make part of that policy explicit; raw pointers and references alone do not tell you who releases a resource or how long a borrow is valid.

TypeMeansWho frees
std::unique_ptr<T>sole owner; moving it transfers ownershipits destructor, exactly once
std::shared_ptr<T>shared owner (only where lifetime must be shared)the last owner standing
T* or T&non-owning — a borrow, by conventionsomeone else; not you

So a member like the renderer's GPU is a unique_ptr — the render context owns it and tears it down with the context:

src/graphics/host_gpu/renderer/renderContext.h
std::unique_ptr<GuestGpu> m_gpu;

The reading rule: a raw pointer or reference in a signature is almost always a borrow; a unique_ptr passed by value means “I am taking ownership.” (The move-only UniqueFunction from C1 is the same idea for a closure that owns a captured resource.)

Borrowed views — std::span and string_view

A std::span<T> represents an element range: a window onto memory someone else owns, with no copy and no ownership. The shader recompiler takes the guest's shader words this way:

src/graphics/shader/shaderCompiler.h
std::span<const uint32_t> code;        // the RDNA 2 binary — borrowed, not copied
std::span<const uint32_t> back_code;

A fixed extent such as std::span<const T, 8> records the element count in the type; the caller still supplies a valid range. For either extent, the elements must remain alive and the view must not be invalidated by storage changes. For queued work, identify who consumes the range and when: command-buffer consumption and completion of GPU work using its referenced resources can be different boundaries. A span itself owns nothing and implements no synchronization.

Reading structured bytes — reinterpret_cast at the boundary

PM4 is a stream of 32-bit dwords; a register write might carry a float. The command processor reads the typed value straight out of the raw buffer:

src/graphics/guest_gpu/command_processor/pm4Handlers.cpp
const auto value  = buffer[i];                              // a raw dword
const auto fvalue = *reinterpret_cast<const float*>(&value);   // same bits, read as float

This is an existing project excerpt, not a portable type-punning recipe. A reinterpret_cast does not create an object, validate alignment or mapping, or grant permission to read an object through an incompatible type. For a same-size value representation conversion in new ordinary C++ code, use std::bit_cast; the complete C1 program demonstrates it. Low-level JIT and mapping code requires separate platform and layout reasoning. For example, the JIT excerpt below patches a machine-code immediate:

src/loader/jit.h
// JmpRax: patch the immediate in mov rax, imm64; jmp rax
template <class Handler>
void SetFunc(Handler func) { *reinterpret_cast<Handler*>(&code[2]) = func; }

Because the tree reads memory as bytes, layout has to be exact — which is why the pipeline key is #pragma pack(1) and compared with memcmp: it is its bytes. That packing habit is C1's territory; here just note why it exists — to match the required layout. Packing alone does not establish object lifetime, alignment or type-aliasing safety. For copying a bit pattern between ordinary C++ values, prefer std::bit_cast or memcpy.

Building into memory you already have — placement new

Ordinary new allocates and constructs. Placement new only constructs — at an address you give it. The TLS patch builds its 9-byte Call9 object directly on top of the guest instruction it is replacing:

src/loader/runtimeLinker.cpp
auto* code = new (inst_ptr) Jit::Call9;   // construct Call9 at inst_ptr — no allocation

There is no memory allocated: inst_ptr already points at mapped, writable guest code, and the constructor lays the exact patch bytes there. Placement new is how the loader and the JIT write structured objects into memory whose address is fixed for them — the guest's own pages.

Where runtime memory lives — owners and borrows

Classify these common lifetime roles when reading a path. The final category is a borrow, not an owner:

  1. The guest heap — direct and flexible memory the game reserved through sceKernel…. Backed by the host reservation from VirtualMemory::Alloc; the game owns it and frees it.
  2. The GPU allocations — images and buffers, owned by VMA (VulkanMemoryAllocator), wrapped by renderer buffer/image objects. Freed when the emulator drops the resource — but not while a submission still references it.
  3. The caches — compiled programs and pipelines, held by cache entries. Follow entry retirement and any deferred retention; cache ownership does not by itself promise that an object lives for the entire run.
  4. The borrows — every span, T* and T& that points into the first three without owning anything.

The one that trips people is (2): a texture the GPU is still reading cannot be freed just because the CPU is done with it. A monotonic timeline semaphore tracks GPU completion, and a resource retained by an in-flight submission is only released once the GPU signals past it — ownership stretched across the CPU/GPU boundary.

A pointer can fault on purpose

Because guest pointers are host pointers, a page's permission is part of what the pointer means. After the loader protects the code and GOT, a write through a perfectly valid-looking pointer into .text traps — and the emulator wants it to. The same trick catches a CPU write to a page the GPU is reading, so the two stay coherent without the guest knowing. So “a pointer” here is an address and a permission: dereferencing one can hand control to the fault handler, which is a feature, not a bug. The patching and coherency topics live in that space.

const, and reading a signature

A signature gives clues, not the complete function contract. const T& or const T* restricts mutation through that access path; it does not prevent all side effects. A non-const reference permits mutation but need not perform it. A unique_ptr passed by value transfers an exclusive handle; a span borrows a range. Check nullability, bounds, ownership, retention after return and synchronization in the documented contract and callers.

Coming from a garbage-collected language

Unlike a tracing garbage collector, a raw pointer or reference does not keep the referenced object alive. By the tree's usual convention it is a borrow, but raw pointers can also appear in manual ownership APIs; inspect the contract. Using a dangling pointer or invalidated span can produce undefined behavior. Smart pointers help manage ownership, while the programmer must still keep borrows within their valid lifetime.

Current resource borrows and deferred owners

RenderContext directly owns the page manager and buffer/texture caches. ShaderStageRuntime borrows compiled-info and resource-snapshot pointers from a program-cache entry; the snapshot is refreshed in place, so it is not an immutable per-draw copy. The renderer serializes preparation and consumption.

For a concrete move-only lifetime, read BufferCache::DownloadBufferMemory(): an oversized readback's temporary unique_ptr<Buffer> moves into a deferred callback. A raw mapped pointer remains usable because that callback retains the actual owner through GPU completion. Extended guest addresses also occupy a separate 512 GiB arena at 0x080000000000; a numerically high pointer is not automatically a host-only allocation.

Watch

Since a guest pointer is a host pointer, decoding one is decoding an address. Type any guest address and watch which band and module it lands in, and which host pages it covers.

Try

Practice valid ownership and bounds before inspecting low-level casts.

A ten-minute lab

Run the complete B3 ownership exercise and B2 byte-reader boundary checks. Predict the result of moving the owner, then explain why a huge offset is rejected. For representation conversion, run C1's bit_cast example. Compiler output can illustrate a chosen build, but an optimized-away cast does not prove that an access is valid.

Do

Each of these ends at the source, so you can check the selected source revision, callers and actual lifetime contract.

Checkpoint