Step 33 · Graphics and performance · C7

Performance: why this design is fast

Use the completed boot and draw traces to examine performance: which work is avoided, which work is cached or overlapped, and which costs remain. Connect each design choice to a measurable path before attempting an optimization.

Before this page: One draw to GPU completion · The program cache. View the learning route.

Read

The headline — guest code runs natively — is one of five design choices, and it would not be enough on its own. The others are about not paying for the parts of a real console that don't matter.

Choice one — native guest CPU execution

Most guest x86-64 instructions execute directly on the host CPU. TLS rewrites, red-zone protection and unsupported-instruction patches add work where the host environment differs. Native execution avoids a general CPU interpreter, but page faults, HLE calls and synchronization still cost time. Windows HLE entry points use the System V ABI annotation to match the guest calling convention.

Choice two — resolve imports ahead of calls

The runtime linker writes resolved HLE or module addresses into relocation slots. Those calls enter the resolved function directly; unresolved imports use diagnostic thunks generated in runtimeLinker.cpp. The thunk’s helper call is therefore not the cost of every successful import. An HLE function may still allocate, lock, wait or call the host OS.

Choice three — GPU work is batched

The guest builds PM4 command buffers. A GPU worker consumes submissions and records Vulkan work; timeline ticks track completion and keep resources alive while work is in flight. This permits overlap with guest execution, subject to waits and dependencies. It does not remove command processing or synchronization costs.

Choice four — cache source programs and specializations

ShaderMapUserData() validates the registered AGC code size and hashes those code bytes with XXH3. The renderer’s ProgramCache keys source programs by code and static stage information, retains resource analysis, and selects a compiled permutation for the current specialization. Resource materialization still runs on reuse. A new permutation can require translation and emission even when the source hash is familiar.

src/graphics/shader/shader.cpp — registration excerpt, after validation
const auto hash = XXH3_64bits(reinterpret_cast<const void*>(addr), data.code_size_bytes);
std::scoped_lock lock(g_shader_map_mutex);
(*g_shader_map)[addr] = {data, hash};

Compiled programs and Vulkan pipeline objects are reused in memory. A separate persisted Vulkan driver cache can reduce driver pipeline-creation work after restart; it does not persist Kyty’s translated IR or SPIR-V program cache. See The program cache for the two cache levels and their keys.

Choice five — track dirty ranges

Page protection tracks CPU writes and accesses to GPU-owned data. The caches upload and download relevant dirty ranges, with copies, barriers, fault handling and completion waits where required. This avoids unconditional full-resource transfers; it is not zero-copy or free synchronization. Reprotection and ownership transitions can cause repeated faults on the same page.

Where the time actually goes

CostEvidenceFollow-up
Shader translation and emissionPhase logs with stage, hash and elapsed timeDistinguish a source miss from a new specialization.
Vulkan pipeline creationProfiler scopes around pipeline creationSeparate driver cache behavior from the program cache.
Transfers, faults and waitsDirty ranges, completion ticks and blocked threadsTrace CPU/GPU ownership and the dependency that requires waiting.
HLE and command processingProfiler scopes and focused call tracesCheck work per call and frequency; avoid broad logging during timing.
Build and diagnostic settingsBuild label, optimization flags and enabled validationCompare the same build type, title state and settings.

Measuring, not guessing

--profile enables Tracy scopes. --shader-log-direction File provides compiler artifacts; phase lines also require a visible logger destination such as --printf-direction File. A slow first frame can involve compilation, asset I/O, uploads or initialization. Measure the interval before attributing it to one subsystem, and distinguish cold-start timing from steady-state timing.

Learn more · performance
  • Book
    Agner Fog — optimization manualsThe microarchitecture half: why native execution is cheap and where the hidden costs (cache, branch, alignment) live.
  • Article
    Chips and CheeseReadable deep dives on CPU/GPU design that make the guest/host split in this project make sense.
  • Guide
    Vulkan timeline semaphoresThe mechanism behind "the CPU can run ahead" — what pWaitSemaphoreValues from topic C4 actually does.
Watch

The one animation that captures "batched and overlapping": the GPU thread consuming a PM4 stream while the register file fills, draw by draw.

Try

Prove the shader-cache claim without a game — the Lab's pm4build and pipekey tools show the state that drives pipeline creation.

A ten-minute lab

Open the Lab's timeline tool, fill the initial command-buffer batch, trigger growth, then advance the completed timeline tick and watch deferred resources release. Then run a title twice with --shader-log-direction File and compare the second run's log, including the persisted Vulkan driver pipeline-cache behavior.

Do

These checkpoints turn the five choices into things you can point at in the source.

Checkpoint