Skip to content

Vulkan Performance

github-actions[bot] edited this page Sep 14, 2026 · 4 revisions

Vulkan Performance

This page is a cost map for the Vulkan backend. It identifies where work occurs, what each measurement can prove, and which deeper pages explain the corresponding architecture.

It is intentionally not a chronological PR log.

Critical path map

flowchart TD
    A[Guest draw / GPU method] --> B[PGRAPH state resolution]
    B --> C[Surface / texture preparation]
    C --> D[Shader binding]
    D --> E[Descriptor / uniform preparation]
    E --> F[Vertex / index synchronization]
    F --> G[Graphics pipeline lookup]
    G --> H[Render-pass / command recording]
    H --> I[Queue submission]
    I --> J[GPU execution]
    J --> K[Fence / report / presentation dependency]
Loading

A slow guest interval can involve several of these regions. Optimize only after identifying which region is on the dependency path for the measured workload.

CPU-side regions

PGRAPH state resolution

Before Vulkan-specific work begins, shared NV2A/PGRAPH code determines the effective guest state.

Questions to answer:

  • Is the cost in shared guest-state decoding or Vulkan-specific translation?
  • Did the effective state actually change?
  • Is a dirty flag causing broad recomputation when a narrower identity would suffice?

Read: NV2A Graphics Architecture.

Shader binding

pgraph_vk_bind_shaders() can include much more than compilation.

Possible work includes:

  • Build/compare ShaderState.
  • Shader-binding cache lookup.
  • Per-stage module lookup.
  • GLSL generation on module miss.
  • Persistent SPIR-V lookup.
  • glslang compilation on artifact miss.
  • Vulkan module creation.
  • Reflection/layout processing.
  • Uniform/descriptor implications.

Do not equate a binding miss, module miss, pipeline miss and compiler invocation.

Read: Shader Binding & Compilation.

GLSL generation

Shared NV2A shader state is converted into generated GLSL. Generation can be cheaper than compilation but still matter when performed thousands of times or when an identity check unnecessarily regenerates source.

Measure generation separately from glslang so source-generation work is not hidden inside a broad “shader compile” bucket.

glslang compilation

A genuinely unseen stage source can synchronously compile to SPIR-V on current main when no compatible persisted artifact exists.

Current main's persistent SPIR-V reuse from PR #70 removes glslang for qualified warm artifacts, but not for truly new source.

For unseen specialization research, see Ubershader & Hybrid Specialization.

Vulkan shader-module creation

Creating a VkShaderModule from existing SPIR-V is a distinct Vulkan object operation. It can be measured separately and should not be counted as glslang work.

Reflection and layout work

xemu needs information about shader resources/uniform layout after compilation/artifact reuse. Reflection is a separate cost and ownership layer.

A persistent compiled artifact is therefore not automatically a ready complete shader binding.

Graphics pipeline creation

A graphics pipeline combines shaders with substantial raster/depth/blend/vertex/render-pass state.

stage modules ready
    ↓
complete PipelineKey
    ↓
pipeline cache lookup
    ├─ hit  → bind/reuse
    └─ miss → vkCreateGraphicsPipelines

Pipeline creation can remain expensive even when shader compilation is completely absent.

Measure:

  • Pipeline-key construction/hash.
  • xemu pipeline-cache hit/miss.
  • vkCreateGraphicsPipelines() wall/CPU time.
  • Driver pipeline-cache behavior where available.

Do not “fix shader stutter” by only counting pipeline misses; first prove which cache layer missed.

Descriptors and uniforms

Descriptors connect shader resources to textures/buffers. Uniform staging carries runtime values that should not always require new shader or pipeline objects.

Potential costs:

  • Descriptor-set allocation/rotation.
  • Descriptor writes.
  • Uniform staging copies.
  • Capacity-triggered submission/finish.
  • Rebinding unchanged state.

A capacity increase is only a real performance improvement if total completion/wait work decreases. PR #83 is an example where removing one finish reason did not establish a net throughput win.

Read: Vulkan Renderer and Performance Methodology.

Textures

Texture work can include:

  • Guest DMA/source identity.
  • Dirty/freshness checks.
  • Surface-overlap resolution.
  • Host texture creation/update.
  • Format conversion.
  • Sampler/binding state.

Merged PR #80 provides guarded warm texture-binding reuse on current main. The guard matters: a cache hit is valid only if the effective guest source and freshness still match the cached host object.

Measure cache hit rate and bytes/conversions avoided, but also verify output and DMA-remap cases.

Surfaces and readback

A large synchronization cost can originate from deciding that GPU-authored surface data must be downloaded before a CPU/vertex/texture consumer uses guest memory.

overlap detected
    ↓
is GPU surface newer?
    ├─ no  → no readback required
    └─ yes → synchronize/download before guest-memory consumer

Two opposite failure modes exist:

  • False positive overlap/freshness → unnecessary expensive readback.
  • False negative overlap/freshness → stale bytes / incorrect rendering.

Performance work here must carry a correctness oracle. PR #85 is active work around this boundary.

Vertex uploads

The Vulkan backend mirrors guest vertex RAM. Updating that mirror is cheap only when command-buffer ordering remains correct.

Potential paths:

  • Direct mapped host write before an active command buffer depends on the bytes.
  • Direct mapped write proven fresh relative to all earlier recorded draws.
  • Ordered staging copy with barriers when earlier recorded reads exist.
  • Submission/finish when staging capacity or another lifetime boundary requires it.

Read: Vertex Uploads & Render-Pass Lifetime.

Render-pass lifetime

Render passes group compatible draws. Copies or other non-draw commands can require the current pass to end.

Investigate:

  • Number of passes.
  • Why each pass ended.
  • Amount of useful draw work per pass.
  • State replay after pass restart.
  • Copies/transitions adjacent to pass boundaries.
  • Whether a versioning/reordering design can legally keep more work inside the pass.

Do not interpret “73.84% of GPU time inside render passes” as “73.84% removable overhead.” That span primarily contains useful rendering.

PR #87 measured one Morrowind trace where render-pass spans were the majority of main-GPU time while ordered staged copies were another substantial region. The useful conclusion was to inspect pass/copy interaction, not to subtract all pass time.

Command-state replay

After a render-pass restart, Vulkan state may need to be re-established before subsequent draws.

Potential costs include repeated:

  • Pipeline binds.
  • Descriptor binds.
  • Viewport/scissor/dynamic state.
  • Vertex/index buffer binds.

PR #88 investigates reuse across restarts. Its current mixed/adverse results are important: reducing command replay locally does not guarantee improved guest progress if the changed lifetime/synchronization pattern costs more elsewhere.

Queue submission and command-buffer finishes

A submission can be triggered by many reasons:

  • Explicit guest synchronization.
  • Resource capacity.
  • Surface/report dependency.
  • Buffer growth/reuse.
  • Flush/sync request.
  • Frame/presentation boundary.

Always record the finish/submission reason. Counting submissions without reason attribution makes it difficult to tell whether a patch removed a dependency or merely shifted it to another category.

Fence waits

Fence waits are often where accumulated GPU work becomes visible as a CPU stall.

A wait duration answers:

How much completion time remained when the CPU reached this dependency?

It does not by itself identify which preceding draw/copy caused that GPU time.

Combine:

  • CPU wait timing.
  • Submission identity/reason.
  • GPU timestamp spans.
  • Guest progress marker.

This prevents optimizing the waiter while leaving the producer cost untouched.

Reports and queries

NV2A report/query operations can require GPU completion before a guest-visible result may be written.

A report-heavy title can therefore turn earlier command-buffer work into a later CPU-visible fence wait.

When a change makes a draw function look faster, verify report/query retirement did not simply absorb the same delay.

GPU execution

GPU time should be divided into meaningful spans rather than one frame total when diagnosing a backend path.

Useful regions can include:

  • Render-pass/draw spans.
  • Ordered copies/transfers.
  • Clears.
  • Readbacks.
  • Presentation/copy work.
  • Work outside classified regions.

Timestamp instrumentation is itself a diagnostic tool. Keep the diagnostic build separate from final product A/B acceptance where possible.

Presentation

Current main can present Vulkan output through either:

  • Shared external memory to the OpenGL presentation context.
  • Host-copy fallback for incompatible/cross-adapter combinations.

A Vulkan performance result should record:

  • Actual rendering GPU.
  • Presentation transport.
  • Resolution scale.
  • VSync/present policy.

Merged PRs #75 and #76 make these identities explicit enough to diagnose mixed-GPU setups.

Host cost versus emulated GPU cost

Vulkan makes host-side implementation costs unusually visible. Pipeline creation, descriptor rotation, render-pass boundaries, command-buffer submission and fence waits are not native NV2A objects.

flowchart TD
    A[Guest NV2A work] --> B[Required semantic work]
    B --> C[Host translation/bookkeeping]
    C --> D[Vulkan driver/API work]
    D --> E[Host GPU execution]
    E --> F[Guest-visible completion]
Loading

A valid optimization can remove or reorganize the middle layers even though no corresponding Xbox-hardware operation is removed. The invariant is the same guest-visible state, ordering and result.

This also means GPU timestamp regions are host-GPU regions. They are excellent for finding Vulkan inefficiency but should not be described as literal NV2A pipeline utilization.

Current-main performance foundations

Change Mainline role
PR #70 Persistent SPIR-V reuse; removes glslang from qualified warm stage-module misses
PR #75 Exact Vulkan physical-device selection/reporting
PR #76 Host-copy presentation fallback when direct external-memory sharing is incompatible
PR #80 Guarded reuse of unchanged warm texture bindings

These are current architecture, not active experiments.

Active investigations

Investigation Main question Status
PR #71 Can admitted unseen fragment state draw through a runtime fallback while specialization happens off the draw dependency? Draft / HOLD
PR #83 Can descriptor-capacity pressure be reduced without merely moving the submission/wait boundary? Draft / HOLD
PR #85 Is guest vertex memory fresh relative to GPU-authored overlapping surfaces? Draft / HOLD
PR #87 Can fresh vertex pages be written directly without violating earlier recorded draws? Draft / HOLD
PR #88 Can command state survive render-pass restarts with less CPU replay and no adverse side effects? Draft / HOLD
PR #89 Can bounded immutable vertex versions reduce ordered-copy pressure while preserving draw-time bytes? Draft

See Development for details and current PR links.

Investigation matrix

Hot region First measurement Causal evidence Acceptance metric
Shader binding CPU samples + per-substage timers exact binding/module/source/cache identities guest interval tails + total binding CPU
Pipeline creation create counts/time exact PipelineKey miss identity guest tails + pipeline preparation time
Descriptors write/rotation/finish counts finish reason + capacity state total guest progress + waits
Textures bind/cache/update counts DMA/source/freshness identity output correctness + CPU/GPU cost
Surface readback bytes/readbacks/waits exact overlap/freshness reason output correctness + total stalls
Vertex upload direct/staged pages/bytes read-page/version identity guest progress + copy/pass cost
Render passes count + GPU spans pass-end reason total GPU/guest progress, not pass count alone
Fence waits CPU wait durations submission and preceding GPU work end-to-end progress
Presentation transport/device identity shared-memory versus copy path valid presented-FPS/latency capture

Avoid common false conclusions

“The hottest region is all overhead”

A profiler region often contains useful work. Render-pass GPU time and generated guest CPU code are obvious examples.

“A cache miss means compilation”

Only if the missed layer actually requires compilation. Shader binding, stage module, persisted SPIR-V and graphics pipeline are separate layers.

“Fewer submissions means faster”

Not if a later submission becomes larger and the same fence dependency still blocks guest progress.

“OpenGL progressed faster, therefore Vulkan GPU execution is slower”

Not until the progress marker is proven equivalent relative to host GPU completion and presentation.

“A diagnostic build improved the metric”

Instrumentation can change scheduling and timing. Reproduce the change in a normal product build.

Recommended workflow

Profile broad CPU/GPU region
    ↓
classify exact cache/sync/lifetime decision
    ↓
add focused correctness oracle
    ↓
make one minimal behavioral change
    ↓
run exact normal A/B pair
    ↓
check p95/p99/max/stalls
    ↓
check total CPU + GPU + wait work
    ↓
run broad XISO / retail / lifecycle gates

For the full measurement rules, see Performance Methodology.

Deep pages

From GPU/CPU trace to causal hot path

Debugging Lag & Hot Paths contains the detailed Release profiling workflow and worked examples for shader compilation/pipeline misses, render-pass versus ordered-copy attribution, and cold-tail investigation.

Clone this wiki locally