Repository navigation
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.
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]
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.
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.
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.
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.
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
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.
A representative Vulkan draw can include:
- Surface update and render-target selection.
- Texture lookup/update.
- Shader binding selection.
- Stage-module lookup/creation.
- Descriptor/uniform updates.
- Vertex/index buffer synchronization.
- Graphics pipeline lookup/creation.
- Render-pass selection/start.
- Dynamic state commands.
- Draw recording.
- 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.
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.
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.
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.
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.
Vulkan makes transfer ownership explicit. xemu commonly uses:
- Host-visible mapped buffers.
- Device-consumed buffers.
- Staging buffers.
- Ordered
vkCmdCopyBuffertransfers. - 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.
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.
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.
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.
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
False overlap tests cause expensive unnecessary readbacks; missed overlaps cause stale data. PR #85 is active work on this boundary.
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.
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.
Current Vulkan lifecycle includes pre-save and pre-shutdown hooks.
GPU-authored guest-visible surfaces may need to be downloaded so the snapshot contains authoritative guest state.
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
| 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 |
| 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 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.
DSP tooling guide and navigation updated November 2, 2026 for experimental PR #384, source 8f51d237. Capture/replay usage and limits.
The earlier wiki-wide review was published November 1, 2026. Emulator source: b7140848. Reviewed wiki content: 1d2b671. Current status · Release availability · Publication process.
- Home
- Current status
- Stable versus main
- Implemented mechanisms
- Controls and defaults
- Measured results and limits
- Complete change register
- System
- CPU and vCPU
- Memory and DMA
- Graphics / NV2A
- APU and audio
- Timing and timers
- Platform and I/O
- Hardware observations
- Fidelity boundaries
- Methodology
- PGR2 routes
- Debugging lag / hot paths
- Tools and commands
- DSP capture, replay and fuzzing
- HLE readiness and public XISO proof
- Concurrent DSP shadow and OpenAL proof
- DSP HLE operation inventory
- DSP HLE fix examples and testing
- Development
- Upstream shortlist
- Wiki maintenance
- Research / retired work
- Optimization catalog
- Hot-path shortcuts
- Researched experiments
- Unproven proposal directory
- PR lessons
- September26 snapshot
Repository · PRs · Issues