Step 29 · Graphics and performance · C9

The program cache

The register, shader and Vulkan lessons explain the inputs to a host program. Now follow how the renderer reuses or specializes that program through ProgramCache::Get in pipelineCache.cpp.

Before this page: Shaders: RDNA 2 → SPIR-V · The Vulkan backend. View the learning route.

Read

A draw arrives with registered RDNA 2 code, its cached hash, current user data, and stage input info. ShaderMapUserData validates the AGC code size and computes XXH3_64bits over those bytes when the code is registered; PrepareProgram retrieves that identity. Merged programs combine the front/back identities. Get returns a handle the renderer can bind. Everything between those two sentences is this function.

How the stage is chosen

Vertex-like input uses input_info.logical_stage: Vertex, Mesh, Local, TessellationControl or TessellationEvaluation. Pixel and compute are selected by input-info type. Merged ES/GS stages use Vulkan mesh shaders; enabled tessellation uses three programs prepared by PrepareTessellationPrograms(). Read the logical stage rather than inferring it from the mesh thread count.

The lookup key

The program cache is keyed on stage, shader hash, user-data count, code size and a packed static-state blob from the input info — not on the current descriptors. Two draws of the same binary share an entry even when they bind different textures. What they do not share is the SPIR-V permutation:

src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp — ProgramCache::Get(), cache hit
// Schematic hit path; resources remain owned by the source entry.
auto& source = entry->second;
EXIT_IF(!IR::MaterializeResources(source.resource_plan, runtime,
                                  source.resources, source.specialization));
// Find matching specialization AND push-constant placement.
input_info.stage = { .program = &permutation->program,
                     .resources = &source.resources };
permutation->program.bindings.AdvancePushData(push_data_cursor);
return permutation->handle;

On a hit, MaterializeResources refreshes cached storage even when the program is already known. A failed read is EXIT_IF. If the resulting ResourceSpecialization matches a permutation already compiled, the function returns. That is the common case after the first few frames.

The two misses

A program miss calls TranslateProgram(), extracts a retained ResourcePlan, materializes its first snapshot and compiles a permutation. A permutation miss reuses the retained plan and refreshed specialization, but also calls TranslateProgram() again: translated blocks are temporary and are not retained by the cache. Both paths call CompilePermutation(), which runs CompileProgram() and creates a Vulkan shader module.

A stable hit performs neither translation nor compilation. Supported BVH shaders use the same program/permutation path. The user-data base is 8 for ordinary vertex/local/evaluation stages and 0 for mesh and tessellation control. Wave size comes from stage metadata, with tessellation control using 64.

What the renderer receives

ShaderStageRuntime holds pointers to the compiled info and the cache entry's refreshed ResourceSnapshot. They are borrowed views consumed during renderer-serialized preparation, not an owned snapshot moved into each draw. Prepared bindings are reused across draws and dispatches. AdvancePushData() prevents stages from overlapping push constants; mesh draws reserve six dwords.

The lookup key stores stage, binary hash, user-data count, code size and exact static state. Its hash groups same-shape states using the static-state length, then equality compares the full state. ShaderParams holds up to 40 user-data dwords inline: 32 user SGPRs plus an eight-dword merged-stage prefix.

Three small surfaces next to it

These sit outside the graphics cache but show up in the same runs, so they belong on a page you read while debugging a title:

umtx wait / wake

UmtxOp implements operations 2 (wait) and 3 (wake) through SyncOnAddress. Both untimed and valid timed waits work. A supplied KernelTimespec becomes a bounded nanosecond duration and shares the address-wait deadline and pending-signal path. Negative seconds or nanoseconds outside [0, 1e9) return −1 with POSIX_EINVAL; expiration follows the POSIX timeout mapping.

DualSense light bar and triggers
src/libs/controller.cpp — PadSetLightBar / PadSetTriggerEffect
int KYTY_SYSV_ABI PadSetLightBar(int handle, const PadLightBarParam* param) {
    PRINT_NAME();
    if (handle != 1) return PAD_ERROR_INVALID_HANDLE;
    if (param == nullptr) return PAD_ERROR_INVALID_ARG;
    g_controller->SetLightBar(param->r, param->g, param->b);
    return OK;
}

Handle 1 is the only pad. The HIDAPI driver then drives the real DualSense. PadSetTriggerEffect is the same shape: validate handle and pointer, forward the struct, map a failed host call to PAD_ERROR_INVALID_ARG. The NIDs are RR4novUEENY (light bar) and 2JgFB2n9oUM (triggers) in libPad.cpp.

libCes — a whole module in one file

libCes.cpp implements CP932 / Shift-JIS ↔ UTF-8 conversion, registered as LIB_VERSION("Ces", 1, "Ces", 1, 1). CesDecodeUtf8 is a textbook UTF-8 decoder with the replacement character 0xfffd on a truncated or invalid sequence. If you want a first HLE file to read that is not inlined inside libs.cpp, start here — then compare it to LibContentExport in HLE libraries.

GPU-selected descriptors require a bounded set of compatible candidates. Resource tracking discovers buffer/image tables and nested selectors, materialization refreshes their current values, and binding/emission maps the selected candidate to native resources. Compatible image views can share a contiguous descriptor array. Unsupported shapes retain explicit guards: deleting a guard does not supply the missing candidate discovery or bindings.

Raw scalar/descriptor reads use the buffer backing path (TryReadBufferBacking()); formatted image synchronization is handled by the appropriate resource binding. Compute materialization also receives SrtRuntime::workgroup_counts. During compilation, host evaluation values are discarded while dependencies still used by GPU memory operations are retained.

Watch

The register/resource lesson's SRT walk is the plan this function re-executes. The pipeline-key lab is the other cache — Vulkan pipelines, not shader permutations.

Try

Open pipelineCache.cpp at ProgramCache::Get. Identify the program miss, permutation miss, permutation hit and compiled-permutation returns. Check which paths refresh resources and which rebuild temporary IR.

Do

Checkpoint