Skip to content

Vulkan Renderer

OpenAI Codex edited this page Nov 1, 2026 · 7 revisions

Vulkan Renderer

Reviewed: November 1, 2026 · Source: main b7140848. Release availability: Hybrid #71, freshness #85/#125, vertex versions #89, the three persistent cache layers, Prewarm default and #228 readiness are in v0.8.136-0-g458730bf5373 (September 30, 2026). #335/#336 and October depth foundations are reviewed-main only.

The Vulkan backend realizes NV2A/PGRAPH state through explicit Vulkan objects, command buffers, synchronization, and caches. It shares the same guest-facing PGRAPH model as OpenGL, but Vulkan exposes much more of the host GPU lifetime and ordering directly to xemu.

See NV2A Graphics Architecture before this page.

Backend role

flowchart TD
    A[PGRAPH guest state] --> B[Vulkan renderer]
    B --> C[Surface / texture resolution]
    B --> D[Shader binding / stage modules]
    B --> E[Vertex / index data]
    B --> F[Pipeline key + graphics pipeline]
    C --> G[Descriptors / buffers]
    D --> F
    E --> G
    F --> H[Record command buffer]
    G --> H
    H --> I[Submit batch]
    I --> J[Fence / completion]
    J --> K[Guest-visible results / presentation]
Loading

Unlike OpenGL, there is no single driver-owned state machine that implicitly remembers every draw setting. xemu must explicitly construct or select most executable state and explicitly order memory/resource use.

Initialization

hw/xbox/nv2a/pgraph/vk/renderer.c coordinates renderer initialization.

Current initialization includes these major stages:

create Vulkan instance / select device
    ↓
initialize performance/diagnostic state
    ↓
initialize command buffers
    ↓
initialize storage/staging buffers
    ↓
initialize surfaces
    ↓
initialize shaders and shader caches
    ↓
initialize graphics pipelines/render passes
    ↓
initialize textures
    ↓
initialize reports
    ↓
initialize compute helpers
    ↓
initialize display/presentation
    ↓
seed vertex-RAM mirror from guest VRAM

Finalization tears down the renderer in an order that preserves object dependencies and waits for owned asynchronous work where required.

Device selection

Current main can enumerate/select Vulkan adapters by stable identity rather than relying only on display names. The selected device determines capabilities, Vulkan target environment, memory properties, and renderer behavior.

This matters for correctness and benchmarking:

  • A requested GPU should be distinguishable from the actual initialized GPU.
  • Vulkan API/SPIR-V target policy must be compatible with the selected device.
  • Presentation may involve a different OpenGL context/device.
  • A performance result is not meaningful if the tested GPU silently differs from the intended GPU.

Merged PR #75 established much of the exact-adapter selection/reporting path.

Presentation transport

Vulkan rendering and window presentation are separate concerns.

Current main can choose between:

Transport Use
Shared external memory Vulkan and OpenGL presentation contexts are compatible for the required memory sharing
Host copy Cross-device/incompatible presentation path where direct external-memory sharing is unsafe

The compatibility decision considers actual device/driver identity and required extensions rather than assuming any Vulkan image can be imported into any GL context.

Merged PR #76 introduced the bounded host-copy fallback for incompatible/cross-adapter presentation.

Command-buffer model

Vulkan work is recorded before it executes on the GPU. xemu therefore has to reason about both record time and execution time.

sequenceDiagram
    participant P as PGRAPH
    participant R as Vulkan renderer
    participant C as Command buffer
    participant G as Host GPU

    P->>R: Draw / copy / query requirement
    R->>C: Record commands and barriers
    R->>C: Record more guest work
    R->>G: Submit batch
    G-->>R: Fence / completion
    R-->>P: Publish guest-visible completion when allowed
Loading

A host buffer write that happens after an earlier draw is recorded can still affect what that earlier draw reads if both commands reference the same memory and no version/order rule prevents it. This is why vertex mirror updates require command-buffer lifetime reasoning.

Draw preparation

A representative Vulkan draw can include:

  1. Surface update and render-target selection.
  2. Texture lookup/update.
  3. Shader binding selection.
  4. Stage-module lookup/creation.
  5. Descriptor/uniform updates.
  6. Vertex/index buffer synchronization.
  7. Graphics pipeline lookup/creation.
  8. Render-pass selection/start.
  9. Dynamic state commands.
  10. Draw recording.
  11. Resource lifetime/bookkeeping updates.

Not every draw executes every stage. Dirty tracking and caches attempt to keep steady-state draws much cheaper than first-seen state.

Shader cache layers

Vulkan has several distinct reuse layers. Confusing them leads to incorrect performance conclusions.

Layer Reuses Miss does not necessarily mean
Shader binding cache Combined shader state/binding decision A compiler invocation
Per-stage module cache Vertex/geometry/fragment stage module A graphics pipeline miss
Persistent SPIR-V artifact cache Previously compiled exact generated source across processes A ready VkShaderModule or graphics pipeline
Graphics pipeline cache/LRU Complete graphics pipeline state A shader compilation
Vulkan driver pipeline cache Driver-internal pipeline data xemu stage-module reuse

For the detailed shader path see Shader Binding & Compilation.

Three persistent cache layers

Layer Retained data and bounds What it does not guarantee
Exact-source SPIR-V, #70 Stage/source/compiler/target-compatible compiled bytes No GLSL generation, reflection, module or pipeline work
Learned fallback history, #143/#144 Recipes, at most 1,024 records / 8 MiB; 32 launch candidates and prewarm inflight ceiling of four Executable pipelines or first-launch coverage
Driver pipeline cache, #147 Device-compatible bytes, at most 64 MiB; vendor/device/UUID checks Every pipeline hits or a game-FPS gain

These persist across launches; live bindings, stage modules and pipeline LRU entries are separate process-local layers. OpenGL program-binary caching has a different backend contract. Driver restore failures use an empty cache. See Vulkan Shader Caching for lifecycle, identity and dated component measurements.

Persistent SPIR-V reuse

Current main includes the persistent shader-artifact path merged from PR #70.

The important architecture is:

stage-module key miss
    ↓
generate exact GLSL
    ↓
look for compatible persisted SPIR-V artifact
    ├─ hit  → reuse SPIR-V
    └─ miss → run glslang and retain bounded artifact
    ↓
create Vulkan shader module
    ↓
reflect layout/uniform information

A warm artifact hit removes glslang from that stage-module miss, but it does not remove GLSL generation, Vulkan module creation, reflection, or graphics-pipeline creation.

Graphics pipelines

Vulkan graphics pipelines combine a large amount of effective state. vkCreateGraphicsPipelines() can therefore be a significant first-seen cost even when all shader modules already exist.

Pipeline state can include concepts such as:

  • Shader stages.
  • Vertex input.
  • Primitive topology.
  • Rasterization.
  • Depth/stencil.
  • Blend state.
  • Render-pass/attachment compatibility.
  • Pipeline layout.

The backend maintains its own graphics pipeline cache/LRU and also supplies a Vulkan pipeline-cache object to the driver. These are different cache layers.

Render passes

A render pass represents a region of compatible drawing to the active attachments. Non-draw operations or resource transitions can require ending a pass before other commands are recorded.

This creates an important performance distinction:

Time inside render passes
    ≠
removable render-pass overhead

Most time inside a render pass is expected useful GPU drawing. The optimization target is unnecessary pass splitting, redundant state replay, avoidable transitions, or synchronization around the pass—not “remove all render-pass time.”

See Vertex Uploads & Render-Pass Lifetime.

Buffers and staging

Vulkan makes transfer ownership explicit. xemu commonly uses:

  • Host-visible mapped buffers.
  • Device-consumed buffers.
  • Staging buffers.
  • Ordered vkCmdCopyBuffer transfers.
  • Access/stage barriers.

The correct path depends on whether a resource can be safely updated directly or must preserve ordering against already recorded GPU reads.

Example: ordered vertex mirror update

previous vertex reads
    ↓
VERTEX_ATTRIBUTE_READ → TRANSFER_WRITE barrier
    ↓
staged vkCmdCopyBuffer
    ↓
TRANSFER_WRITE → VERTEX_ATTRIBUTE_READ barrier
    ↓
later draw

A direct mapped write can avoid that command traffic only when lifetime analysis proves it cannot alter bytes already consumed by an earlier recorded draw.

Descriptor sets and uniform data

Descriptors connect shaders to buffers, textures, and other resources. xemu tracks descriptor-set capacity, binding identity, and uniform staging data.

Descriptor updates can be a meaningful CPU cost when guest state changes frequently. But increasing capacity or suppressing updates does not automatically improve frame time: a submission/fence can simply move to a later resource limit or guest synchronization point.

The closed/unmerged #83 experiment illustrates this: increasing descriptor capacity removed one finish reason but did not establish a throughput win because waiting moved elsewhere. It is retained research, not current renderer capacity policy.

Textures

Vulkan texture binding uses guest texture state plus host resource/cache state. Current main includes the guarded warm-texture reuse merged in PR #80.

The reuse decision must consider more than a dirty flag. It includes effective source/DMA identity and freshness rules so a remapped guest source cannot silently reuse stale host texture content.

Surfaces and coherency

A Vulkan surface can contain the newest version of data after GPU rendering. Guest memory may later become authoritative through CPU writes. xemu must determine the direction of synchronization before using overlapping data.

flowchart TD
    A[Resource requested] --> B{Newest data?}
    B -->|Host GPU surface| C[Read back when guest memory consumer requires it]
    B -->|Guest RAM / VRAM| D[Upload / mirror update when GPU consumer requires it]
    C --> E[Coherent consumer]
    D --> E
Loading

False overlap tests cause expensive unnecessary readbacks; missed overlaps cause stale data. Merged #85 and #125 reconcile actual checked vertex spans before CPU decode/upload. Merged #89 adds direct unread-page handling and bounded immutable private versions with ordered fallback. Fresh source ownership and preserving earlier recorded reads are separate proofs; see Vertex Uploads.

Reports and fence waits

Guest reports/queries can require actual GPU completion before their results are correct. Vulkan often makes that dependency visible as command-buffer submission and fence waiting.

A performance trace must distinguish:

  • CPU time preparing a report.
  • Queue submission.
  • Fence wait.
  • GPU execution preceding the report.
  • CPU serialization of the result to guest memory.

Moving a fence wait from one function to another is not an optimization if total completion dependency is unchanged.

process_pending

pgraph_vk_process_pending() services renderer work that must be coordinated with PGRAPH ownership. Current paths include items such as:

  • Pending surface downloads.
  • Dirty-surface downloads.
  • Sync/flush requests.
  • Persistent SPIR-V cache writeback.

The function temporarily coordinates PFIFO/PGRAPH locking so pending renderer work is executed under the correct ownership boundary.

This is an important design pattern for future workers: perform only worker-safe work off-thread, then publish or mutate Vulkan renderer state under the renderer/PGRAPH ownership rules.

Save and shutdown

Current Vulkan lifecycle includes pre-save and pre-shutdown hooks.

Before save

GPU-authored guest-visible surfaces may need to be downloaded so the snapshot contains authoritative guest state.

Before shutdown

Persistent cache publication and other deferred state need producer quiescence. A cache file must not be written while another thread can still mutate the data it serializes.

A safe general pattern is:

stop new producers
    ↓
wait/join worker work
    ↓
process/publish final renderer-owned results
    ↓
write bounded persistent state
    ↓
destroy Vulkan objects

Main source files

File Responsibility
vk/renderer.c Backend lifecycle, pending work, save/shutdown coordination
vk/instance.c / device-selection.c Vulkan instance/device discovery and selection
vk/draw.c Draw preparation, render pass and graphics pipeline usage
vk/shaders.c Shader binding, stage module caches, descriptor/uniform integration
vk/glsl.c GLSL → SPIR-V compilation and shader module construction support
vk/spirv-prewarm.c Persistent SPIR-V artifact store/reuse
vk/buffer.c Storage/staging buffer management
vk/vertex.c Vertex/index data and vertex-RAM mirror updates
vk/texture.c Texture cache/update path
vk/surface.c Surface ownership/readback/upload
vk/reports.c Report/query synchronization and publication
vk/display.c Vulkan output composition/presentation transport
vk/perf.c Vulkan-specific opt-in measurements/telemetry
vk/renderer.h Renderer-owned Vulkan state

Common performance regions

Region Typical question
Shader binding Is this state lookup, GLSL generation, glslang, module creation, reflection, or uniform work?
Pipeline creation Is this a genuinely new complete pipeline or redundant state identity?
Descriptor updates Is data actually changed, and does reducing updates simply move a capacity boundary?
Render-pass lifetime Which non-draw operation is breaking the pass, and can it be safely reordered/versioned?
Surface download Is the overlapping GPU surface actually newer?
Vertex upload Has any already recorded draw consumed the page being overwritten?
Fence wait Which guest-visible dependency requires completion?
Presentation Shared external memory versus host-copy transport and adapter choice

Vulkan objects are host implementation state

The Xbox does not contain Vulkan pipelines, descriptor sets, render passes, fences or SPIR-V. These objects are xemu's host-side way of realizing NV2A behavior.

flowchart LR
    A[NV2A effective state] --> B[xemu identity/lifetime proof]
    B --> C[VkShaderModule / descriptors / VkPipeline]
    C --> D[Command buffer]
    D --> E[Host GPU]
    E --> F[Guest-visible draw/report/memory result]
Loading

This distinction gives Vulkan optimization work freedom to change host object structure aggressively, but only behind a stable guest contract.

Important fast-path invariants

Fast path Required proof
Persistent SPIR-V hit Artifact corresponds to exact generated shader/compiler identity and current target requirements
Pipeline cache hit Complete effective pipeline identity still matches
Warm texture reuse DMA/source identity, format/state and freshness still match
Direct vertex-mirror write No earlier recorded draw can observe the overwritten bytes and guest RAM is authoritative
Reuse across pass restart Required bindings/dynamic state remain valid for the restarted pass
Deferred worker publication Renderer generation/ticket/lifetime still owns the result

A Vulkan validation-clean patch can still be an Xbox correctness bug if one of those guest-state proofs is wrong.

Current and held architecture

Current main includes exact adapter selection/reporting #75, cross-adapter host-copy transport #76/#101, SPIR-V reuse #70, guarded warm texture reuse #80, hybrid rendering #71 and vertex freshness/versions #85/#89/#125. #87 closed unmerged; its useful lifetime direction was partly incorporated into #89. #83 descriptor capacity and #88 state reuse also closed unmerged.

Hybrid policy and readiness

Prewarm is the fresh default, following #150, with Off/Fallback/Prewarm/Always choices. Explicit saved choices remain selected. The mode is latched at process startup, so restart xemu to apply it. Cross-launch prewarm requires Cache shaders. #228 asynchronously prepares missing vertex, optional geometry and fallback-fragment stages instead of discarding the learned family. A matching binding and complete pipeline are required before specialized takeover.

BEFORE #335/#336
pending pipeline -> immediate timer -> pending scan -> timer
served specialized draw -> learn family -> optional live fallback build
AFTER
pending pipeline -> completion wake -> renderer-owned publication
failed work -> bounded timed retry
served specialized draw -> record once -> future-launch recipe
launch-loaded history -> bounded prewarm

#335 and #336 reduce polling and optional live compile contention. Their historical Deck PGR2 route results improved, but #335 retains an accepted 2.40% texture-control tail increase and #336 retains an unresolved seeded-history black/stopped-flip observation that passed on the parent. Later success did not erase that failure. Windows cache qualification and missing readiness oracles remain bounded limits. See Hybrid Specialization and Learned Fallback Readiness.

Rendering semantics, ownership and progress

  • #264 implements DOT_ZW depth replacement; #265 removes the issue #149 effect-suppression workaround. This is the current Ghoulies correctness story. The issue is closed, but that state is not proof of every requested frame-exact transient comparison. The optional Accurate NV20 vertex arithmetic diagnostic is a separate mechanism.
  • #267 reconciles PTIMER scheduling/semantics. #271 separates PGRAPH interrupt/control handling from renderer serialization, avoiding guest-control waits behind shader/pipeline work. Retained Windows Vulkan Def Jam evidence progressed beyond the earlier freeze; it is bounded responsiveness/correctness evidence, not an accelerated guest clock or a general FPS result. #81 is older closed/unmerged research.
  • Surface/texture fixes #104/#105/#124/#127 cover copy/visibility, layout and failed-generation cleanup. #119/#158 preserve newer guest writes against stale GPU readback. A stable resource handle alone does not establish backing-byte authority.

Delivered depth foundations and held consumers

The October main-only landings preserve read-only depth/stencil ownership (#282), directly upload eligible unscaled surfaces on Linux (#283), and retain byte-identical depth images across alias views (#284). #286–#289 supply guest Morton mapping, bounded dispatch, extent qualification and a GPU conversion recorder.

The end-to-end GPU-resident runtime consumer #290 and small-color-upload consumer #291 remain open/HOLD at review. pgraph_vk_convert_depth_alias() has a definition/declaration but no runtime caller in this snapshot. The foundations do not imply complete zero-copy aliases, and prospective consumer gains do not belong to main. Their retained reset/snapshot/oracle gaps remain in the owning PRs.

Additional prepared-work candidates

Each page separates source-confirmed repeated work from unmeasured performance potential and retains the current coherency/lifetime route as the reference.

Assumption-based Vulkan research

The validated research set adds Frame-Scoped Texture Trust, Direct Surface Sampling, and Approximate Occlusion Reports. Only direct surface sampling targets exact behavior for an admitted subset; the other two intentionally change freshness or report semantics and require explicit compatibility scopes.

Unproven throughput candidates

The current renderer contains several source-confirmed synchronization, presentation, descriptor, PVIDEO, coherency, and memory-placement targets. See Vulkan Throughput Candidates.

Surface/guest-memory coherence lesson from issue #106

The PGR2 race-to-menu corruption was not caused by the consuming shader, descriptor, initial BC upload, or final presentation. A cached dirty surface later downloaded older contents over newer overlapping guest bytes; the texture cache then correctly uploaded those corrupted bytes.

When diagnosing a corrupt sampled image:

inspect exact bound image
    → if already corrupt, identify its last writer
    → correlate external resource ID with stable xemu lifetime/key identity
    → trace overlapping authority transitions and before/after hashes

A cached surface must surrender authority after a newer overlapping guest/PGRAPH write. The repair history is PR #119; the canonical explanation is PGR2 Race-to-Menu Corruption — Lessons Learned. The capture and writer-trace methods are documented in Graphics Transition Debugging Tools.

Related pages

Clone this wiki locally