Skip to content

Hot Path Vulkan Bounded Vertex Uploads

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

Vulkan Bounded Vertex Uploads

Lifecycle: Performance - Implemented · Type: Structural optimization
State: Present in current main.

Status

Field State at reviewed main
Underlying optimization Implemented
Advanced control Implemented, Upload only used vertex ranges
Default On
Apply boundary Live; next remapped vertex upload
Reference route Repack/copy from vertex zero through the draw's upper bound
Evidence counters No shared #94 artifact at reviewed main
Qualification Historical source-scoped evidence; current-main isolated GL/VK performance not rerun
Reviewed source 322986f4c5502b3fa887a9605ac8cd88e9b2c579, 2026-09-14
Primary history PR #7 archival provenance; control integration PR #37

Summary

Some NV2A vertex streams cannot be bound directly with their guest layout and are repacked into xemu's inline Vulkan vertex buffers. A draw may reference vertices beginning at start_vertex > 0. The bounded route offsets the input/output pointers to that first used vertex and copies only:

num_vertices - start_vertex

The reference route begins at vertex zero, preserving the older wider copy. Both routes retain the same destination layout and draw indices.

This optimization is separate from the later PR #85/#87/#89 work on surface freshness, ordered vertex-RAM updates, direct writes, and immutable versions. Those are correctness/lifetime experiments and are not controlled by this toggle.

Why It May Help

The potential saving is:

unused leading vertices
× active remapped attributes
× source/destination stride copy cost

It matters when indexed or ranged draws reference a suffix/subrange of a larger guest vertex allocation and when one or more attributes require repacking. It can reduce host memory reads/writes, per-vertex conversion/copy calls, and staging-buffer pressure.

It does little when start_vertex == 0, direct vertex binding is possible, ranges are already small, or draw/renderer/GPU work dominates.

Observed in xemu

PR #7 records that the historical path copied all remapped vertices and asserted start_vertex == 0. The integrated changes specialize common strides and pass the actual minimum element into the copy helper. Equivalent commits are already present in current main.

The PR was closed as archival/superseded rather than merged because main already contained patch-identical changes. Historical results remain provenance, not current-main qualification. No fresh isolated OpenGL/Vulkan retail campaign was performed solely for the control.

External Precedent

Related emulator pattern: Dolphin's vertex manager stages only the vertex/index data required by submitted draws and owns ring/buffer allocation around the current batch: Dolphin VertexManagerBase.cpp.

General transfer precedent: Vulkan buffer copies explicitly name source offset, destination offset, and size; applications commonly limit transfer regions to consumed data while providing the correct synchronization: Khronos synchronization examples.

These precedents support bounded copies. xemu must additionally reproduce NV2A stride, format conversion, DMA, and surface-coherency semantics.

Emulation Boundary

Xbox draw
vertex addresses, formats, strides, min/max element
    ↓
xemu fetch/repack plan
which attributes require host layout conversion
    ↓
host memory copy
vertex zero..end OR first used vertex..end
    ↓
Vulkan draw
same logical vertex values and indices

The Xbox does not expose xemu's repacked staging buffer. The shortcut changes copied host bytes, not the guest draw range.

Code and Ownership Map

Location Role Owner
hw/xbox/nv2a/pgraph/vk/draw.c Computes remap, first copied vertex, count, input/output offsets Renderer/PFIFO thread
vk/vertex.c and common vertex helpers Guest fetch spans, formats, DMA/VRAM validation Renderer/PGRAPH
vk/buffer.c Inline staging/device buffer capacity and offsets Renderer
vk/renderer.h Remap/buffer state Renderer
ui/xemu-tweaks.[ch] Live bounded-range bit UI publishes; renderer reads
PR #85/#87/#89 files Separate surface/freshness/versioning experiments Not part of this control

Actual Current Flow

flowchart TD
    A[Draw determines min/max required vertices] --> B[Build attribute remap plan]
    B --> C[For each active remapped attribute]
    C --> D[Read guest source offset and old/new stride]
    D --> E{Bounded upload control enabled?}
    E -->|Yes| F[first_vertex = start_vertex]
    E -->|No| G[first_vertex = 0]
    F --> H[copy_count = num_vertices - first_vertex]
    G --> H
    H --> I[Offset source pointer by first_vertex * old_stride]
    I --> J[Offset destination pointer by first_vertex * new_stride]
    J --> K[Copy/convert copy_count vertices]
    K --> L[Record staging-to-device transfer]
    L --> M[Bind same logical base/offset for draw]
Loading

The bounded route does not change num_vertices, the guest index stream, or the Vulkan draw. It changes which leading repack bytes are materialized.

Maintained Reference Flow

flowchart TD
    A[Remapped attribute] --> B[first_vertex = 0]
    B --> C[Copy every vertex through draw upper bound]
    C --> D[Populate staging/device buffer]
    D --> E[Bind and draw requested subset]
Loading

The reference route is deliberately wider but useful for diagnosing an incorrect minimum, offset, or sparse-range calculation.

Eligibility and Protected Invariants

Invariant Why required Failure if weakened
start_vertex <= num_vertices Count subtraction must not underflow Huge/out-of-bounds copy
Required min/max derived from actual draw/index data Copy must include every fetched vertex Missing/corrupt vertices
Complete strided fetch span within DMA/VRAM Last component may extend beyond base vertex Out-of-range read
Per-attribute old/new stride and format preserved Repack layout must match shader input Corrupt attributes
Input/output pointers both offset consistently Bounded copy must land at expected logical index Shifted vertex stream
Surface overlap/readback occurs before CPU consumption where required Guest RAM may be stale behind GPU surface Stale vertex values
Staging capacity/alignment valid Copy cannot overrun ring Memory corruption
Draw base/index semantics unchanged Host copy is invisible to guest Wrong primitive

Fetch bounds and surface coherency are mandatory correctness work. They are not disabled when the reference copy begins at zero.

State, Lifetime, Invalidation, and Fallback

Remapped bytes live in transient staging/device buffers for the command-buffer lifetime. The current route relies on the draw's computed range and buffer offset; it does not retain an independent reusable vertex version across future guest writes.

A live control change affects later repacks. It does not rewrite data already recorded into an active command buffer.

Unsupported/direct-binding cases follow their existing paths. Any validation, capacity, or surface-readback requirement remains in force before the copy.

Expected Profile Signature

A range-heavy workload should show:

same draw count and fetched vertex values
    + start_vertex > 0 on many remapped draws
    + fewer copied vertices/bytes
    + lower CPU memcpy/repack work
    + lower inline staging consumption

The hypothesis is weakened when:

  • most ranges start at zero;
  • no attributes require remapping;
  • copy bytes fall but ordered vertex-RAM/surface waits dominate;
  • local memcpy falls but frame distributions do not move;
  • wider reference copies improve locality enough to offset saved bytes (must be measured, not assumed).

Counters and Evidence

Counter Unit Meaning
vk.vertex.remap_draws draws Draws requiring repack
vk.vertex.range_start_nonzero draws Bounded opportunity exists
vk.vertex.vertices_required vertices Logical draw range
vk.vertex.vertices_copied vertices Actual repack work
vk.vertex.bytes_required/copied bytes Better fixed-work denominator
vk.vertex.attributes_remapped attributes Active conversion work
vk.vertex.reference_leading_bytes bytes Extra bytes copied with control Off
buffer rollover/growth/finish counts events Secondary pressure

Counters must distinguish this remap copy from vertex-RAM mirror uploads and PR #87/#89 version paths.

Risks and Known Bugs

  • An underestimated start_vertex/fetch span omits bytes the GPU reads.
  • Indexed min/max analysis can be wrong for unsupported primitive/index cases.
  • Offset only one side and the repacked stream shifts.
  • Page-aligned dirty ranges must not replace precise fetch ranges for surface-overlap decisions; PR #85 documents this broader coherency risk.
  • A GPU-authored overlapping surface may require readback before CPU repack.
  • Later PR #87/#89 work addresses active-batch overwrite/version problems, not this leading-range optimization.
  • Fewer copied bytes can increase fragmentation/offset complexity or reveal staging capacity behavior.
  • Historical PR #7 data is not a current-main default-benefit proof.

See PR #7, PR #85, PR #87, PR #89, and Pull Request Lessons.

Qualification

  1. Non-indexed and indexed draws with start_vertex 0, 1, and near upper bound.
  2. Multiple active attribute formats/strides, including common specialized strides and generic copies.
  3. Complete strided fetch-span boundary at DMA/VRAM limit.
  4. True and false surface-overlap/readback cases.
  5. Control On/Off exact framebuffer/work hash equivalence.
  6. Copied vertex/byte accounting for reference and bounded routes.
  7. Staging capacity, alignment, rollover, and command-buffer lifetime tests.
  8. Vulkan validation and full applicable XISO.
  9. Representative PGR2/Morrowind cells with p95/p99/max and host CPU/memory bandwidth where available.
  10. Explicit separation from PR #85/#87/#89 freshness/version telemetry.

A default claim requires real workloads with substantial nonzero starts and repeated end-to-end benefit, not merely a lower byte count in a synthetic draw.

Clone this wiki locally