Skip to content

Hot Path Vulkan Color Download Folding

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

Vulkan Color-Download Folding

Lifecycle: Performance - Implemented · Type: Submission shortcut
State: Present in current main.

Status

Field State at reviewed main
Underlying optimization Implemented
Advanced control Implemented, Combine color downloads with rendering
Default On
Apply boundary Live; next eligible surface download
Reference route Finish/submit active work, then use auxiliary single-time download commands
Evidence counters Existing NV2A/Vulkan profiling; shared #94 route artifact absent
Qualification Favorable historical PGR2 sample; not freshly isolated on reviewed main
Reviewed source 322986f4c5502b3fa887a9605ac8cd88e9b2c579, 2026-09-14
Primary history PR #5 archival evidence; later current-main safety corrections

Summary

When xemu must download a GPU-authored color surface back to host/guest-visible memory, the copy must execute after the commands that produced the surface and before CPU-side consumption. The conservative route finishes active work and records the download in a separate auxiliary command buffer.

For eligible color surfaces, current main can instead record the transition and image-to-buffer copy into the active producer command buffer. Existing command order provides the dependency, and the caller later waits for the containing submission rather than creating an extra submit boundary solely for this copy.

Depth/stencil conversion and other unsupported cases retain the auxiliary path.

Why It May Help

The GPU copy itself remains. The expected saving is:

eligible color downloads
× avoided active-buffer finish / separate command-buffer begin / extra submit boundary

This can reduce host submission work, driver calls, and completion waits in workloads that frequently read color surfaces while a producer command buffer is active.

It may not help when downloads occur outside active command buffers, surface production predates the active buffer, depth/stencil conversion is required, capacity growth already submitted the buffer, or GPU copy/readback latency dominates regardless of submission placement.

Observed in xemu

PR #5 preserved a historical PGR2 Vulkan pair where guest display-write cadence improved about 2.27%, p95 about 4%, p99 about 2.21%, and mean host CPU fell about 12% relative to its source-scoped parent. Morrowind was near neutral/slightly adverse in that isolated observation.

Those results support the mechanism in one workload but are not a current-main claim. Current main also includes a later correction: buffer-capacity growth can submit the active command buffer, so eligibility is recomputed after capacity checks before the command buffer is selected.

External Precedent

Direct API ordering precedent: Vulkan command order plus explicit barriers can keep a producer and dependent copy in one command stream when their resource/layout dependencies are correct: Khronos synchronization examples.

Related batching precedent: Khronos' command-buffer performance sample explains that excessive small command buffers and frequent allocation/reset work add CPU overhead, while grouping a reasonable amount of work can reduce it: Command buffer usage.

These precedents justify investigating the boundary. They do not prove xemu's surface lifetime, layout transitions, or guest readback ordering.

Emulation Boundary

Xbox-visible event
GPU-authored surface becomes CPU/guest-visible
    ↓
xemu coherency decision
which surface, which bytes, when producer work completes
    ↓
Vulkan realization
copy in active producer command buffer OR auxiliary submission
    ↓
required result
same downloaded bytes available before CPU use

A Vulkan submission boundary is not an NV2A hardware concept. xemu can reorganize it only while preserving guest-visible surface data and ordering.

Code and Ownership Map

Location Role Owner
hw/xbox/nv2a/pgraph/vk/surface.c::download_surface_to_buffer() Determines finish/fold route, records transitions/copy, maps result Renderer/PFIFO thread
pgraph_vk_finish() Submits/waits active command work for required reasons Renderer thread
pgraph_vk_begin_nondraw_commands() Records dependent nondraw work in active buffer Renderer thread
pgraph_vk_begin_single_time_commands() Auxiliary reference route Renderer thread
vk/buffer.c capacity helpers May finish and replace staging/compute buffers Renderer thread
ui/xemu-tweaks.[ch] Live route bit UI publishes; renderer reads

Actual Current Flow

flowchart TD
    A[Dirty surface download requested] --> B[Classify color/depth-stencil and conversion]
    B --> C[Check active command buffer and surface draw time]
    C --> D{Color surface produced after active buffer start and folding enabled?}
    D -->|No| R[Reference finish/auxiliary route]
    D -->|Yes| E[Initial folded eligibility]
    E --> F[Ensure staging/compute capacity]
    F --> G{Capacity growth submitted/replaced active buffer?}
    G -->|Yes| H[Recompute eligibility from current ownership]
    G -->|No| H
    H --> I{Still eligible color download?}
    I -->|No| R
    I -->|Yes| J[Use active nondraw command buffer]
    J --> K[Transition producer image to transfer source]
    K --> L[Record image-to-buffer copy]
    L --> M[Restore image layout]
    M --> N[Containing submission establishes completion]
    R --> O[Finish active work if ordering requires]
    O --> P[Begin single-time command buffer]
    P --> K
    N --> Q[Map/convert/swizzle and publish guest-visible bytes]
Loading

The source re-evaluates eligibility after capacity preparation because resource growth is allowed to finish the active submission.

Maintained Reference Flow

flowchart TD
    A[Surface download] --> B{Active producer work may own surface?}
    B -->|Yes| C[pgraph_vk_finish with surface-down reason]
    B -->|No| D[Continue]
    C --> D
    D --> E[Begin auxiliary single-time command buffer]
    E --> F[Transitions and image-to-buffer copy]
    F --> G[Submit and wait auxiliary work]
    G --> H[CPU conversion/copy to guest memory]
Loading

Depth/stencil compute packing, older surfaces, inactive command-buffer cases, and any failed folded proof use this path.

Eligibility and Protected Invariants

Invariant Why required Failure if weakened
Active command buffer exists There must be a producer submission to fold into Invalid command handle/order
surface->draw_time > command_buffer_start_time Active buffer actually produced current contents Copy before producer work
Surface is color Current folded route excludes special depth/stencil packing Wrong bytes/layout
No depth/stencil compute conversion Conversion has separate buffers/order Missing conversion or hazards
Capacity remains valid after preparation Growth may finish/replace command state Record into stale/non-active buffer
Image transitions/barriers preserved Copy needs correct access/layout Validation error or stale data
Caller-visible completion unchanged CPU cannot consume early Guest reads stale/incomplete bytes
Swizzle/downscale/packing semantics unchanged Submission placement must not change data Different downloaded image

The control gates placement of the eligible copy. It does not disable surface coherency or completion.

State, Lifetime, Invalidation, and Fallback

The active command buffer, its start time, and the surface draw time establish producer ownership. Storage buffers used by the copy have their own allocation/mapping lifetime. Capacity growth can invalidate the initial assumption by submitting active work and replacing an allocation.

The folded route therefore has two decision points: before capacity preparation and immediately before command-buffer selection. Any lost condition falls back to the auxiliary route.

After recording, CPU-side access remains blocked until the containing submission is complete. The shortcut cannot treat “copy recorded” as “bytes available.”

Expected Profile Signature

A useful workload should show:

same surface download count and bytes
    + folded color downloads > 0
    + fewer SURFACE_DOWN finishes / auxiliary submissions
    + fewer submit-and-wait boundaries
    + unchanged readback output

Potential next bottlenecks:

  • GPU copy or layout transition time;
  • command-buffer completion wait;
  • CPU swizzle/downscale/format conversion;
  • staging-buffer growth;
  • unrelated surface readbacks.

If finish counts fall but total waits do not, the dependency may have moved to the containing submission.

Counters and Evidence

Counter Unit Meaning
vk.surface.download_requests downloads All surface readbacks
download_color/depth_stencil downloads Route class
download_fold_eligible downloads Initial/current proof passed
download_folded downloads Copy recorded in active buffer
download_auxiliary downloads Reference route used
download_finish_surface_down finishes Active producer explicitly finished
download_capacity_invalidations downloads Initial eligibility lost after growth
download_bytes bytes Fixed-work denominator
submit/wait CPU and GPU time ns/us Boundary impact

For a fixed mode, fold_eligible should be explainable as folded plus forced-reference/fallback categories. Record bytes and guest progress so fewer downloads are not mistaken for optimization.

Risks and Known Bugs

  • Folding a surface whose producer is outside the active command buffer copies stale data.
  • Reusing the initial eligibility after capacity growth records against invalid ownership; current main explicitly recomputes.
  • Depth/stencil and compute-packing cases cannot be treated like ordinary color copies.
  • Removing an explicit finish can relocate the same wait to the containing submission.
  • Incorrect transitions/barriers may pass on one driver and fail on another.
  • A CPU map before containing submission completion is a correctness failure.
  • Historical PR #5 evidence is favorable but source-scoped and lacks direct submission counters.

See PR #5, PR #83's wait-relocation lesson, and Pull Request Lessons.

Qualification

  1. Eligible active color surface produced inside current command buffer.
  2. Surface produced before active buffer start (must use reference).
  3. No active command buffer.
  4. Depth/stencil and compute-conversion cases.
  5. Capacity-growth case that submits/replaces active work before recomputation.
  6. Downscale, swizzle, linear, and format-specific output oracles.
  7. Exact downloaded-byte/hash comparison On versus Off.
  8. Vulkan validation across multiple drivers where practical.
  9. Counters for finishes, folded/auxiliary copies, submissions, waits, and bytes.
  10. PGR2/Morrowind same-binary cells with p95/p99/max and host CPU/GPU.
  11. Command-buffer rollover, failure injection, teardown, and snapshot/lifecycle checks.

A performance claim requires evidence that a synchronization boundary was removed rather than merely renamed or moved.

Clone this wiki locally