Skip to content

Vulkan Renderer

github-actions[bot] edited this page Sep 13, 2026 · 7 revisions

Vulkan Renderer

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.

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.

PR #83 is a current example of this: increasing descriptor capacity removed one finish reason but did not establish a throughput win because waiting moved elsewhere.

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. PR #85 is active work on this boundary.

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 code 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.* 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

Current versus experimental architecture

Current main includes:

  • Exact Vulkan adapter selection/reporting from PR #75.
  • Cross-adapter host-copy presentation fallback from PR #76.
  • Persistent SPIR-V reuse from PR #70.
  • Guarded warm texture reuse from PR #80.

Current main does not include the full PR #71 ubershader/hybrid specialization architecture. That work remains an open draft/HOLD and is documented separately in Ubershader & Hybrid Specialization.

Likewise, PRs #85/#87/#89 are active stacked work around surface/vertex freshness and versioning, not current-main architecture.

Related pages

Clone this wiki locally