Skip to content

Hot Path Vulkan Clean Texture Stages

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

Vulkan Clean Texture-Stage Skipping

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

Status

Field State at reviewed main
Underlying optimization Implemented
Advanced control Planned in issue #95, absent from reviewed main
Current behavior Guarded skip always permitted when eligibility passes
Planned modes Auto / Disabled
Planned apply boundary Live; next texture bind
Maintained reference Re-enter normal create/update/bind path for an otherwise reusable stage
Evidence counters Planned; absent from reviewed main
Qualification Source-confirmed route; isolated same-binary benefit not yet measured
Reviewed source 322986f4c5502b3fa887a9605ac8cd88e9b2c579, 2026-09-14
Primary history Underlying current-main texture work; issue #95 control/evidence plan

Summary

pgraph_vk_bind_textures() can enter its slow binding loop because one texture stage needs work. Other active stages may still have a complete valid TextureBinding whose guest source, palette, DMA mapping, surface relationship, and dirty state have not changed.

Current main revalidates each such stage and immediately continues to the next stage when no pending work exists. That avoids rebuilding/rebinding the clean stage merely because another stage triggered the slow path.

Issue #95 proposes an Auto / Disabled control around only that final continue. Disabled would still run every eligibility check, then deliberately fall through to the maintained create/update/bind route for same-binary diagnosis.

Why It May Help

The shortcut does not avoid entering texture binding. Its opportunity is:

slow-bind calls caused by one or more changed stages
× other active stages proven clean
× avoided texture lookup/hash/upload/bind work

It is most useful when games frequently change one stage while reusing others. It can reduce dirty-page checks downstream, texture-key/cache activity, hashing, image creation/upload, descriptor changes, and surface-to-texture work—depending on what the reference route would rediscover.

It will not help when every active stage changes, bindings are missing/dummy, guest texture/palette pages are dirty, a surface alias exists, DMA descriptors remap, or the workload rarely enters the slow bind.

Observed in xemu

The route is present in current source and its comment explicitly states that another stage caused the slow bind while the current stage has no pending source/effective-state work.

Issue #95 exists because the route is difficult to isolate today. The reviewed binary has no policy gate or route counters for clean-stage eligibility, skips, or forced-reference binds. Therefore a normal game run cannot yet prove how often this shortcut matters without adding diagnostic instrumentation or a dedicated candidate.

External Precedent

Direct emulator analogue: Dolphin's texture cache reuses host texture entries while validating guest memory ranges, hashes, aliases, and rendering relationships: Dolphin TextureCacheBase.cpp.

Related regression practice: Dolphin's FIFO CI preserves graphics captures and compares renderer output across revisions: Dolphin fifoci. This is useful precedent for proving a reuse path does not change output.

Dolphin's guest GPU and cache invalidation differ. The analogue supports the idea of guarded texture reuse, not xemu's exact eligibility expression.

Emulation Boundary

Xbox texture-stage state and guest memory
    ↓
xemu cached TextureBinding + dirty/surface/DMA proof
    ↓
host choice
keep existing VkImage/binding OR run normal update/create path
    ↓
required sampled texture and sampler state

The Xbox does not expose a Vulkan texture cache. Reuse is valid only while the cached host object still represents the guest source and effective texture state.

Code and Ownership Map

Location Role Owner
hw/xbox/nv2a/pgraph/vk/texture.c::pgraph_vk_bind_textures() Per-stage skip predicate and normal bind path Renderer/PFIFO thread
check_texture_possibly_dirty() Tests/clears dirty pages and marks overlapping cache entries Renderer/PGRAPH dirty tracking
pgraph_vk_surface_overlaps_range() Rejects host surface aliases Renderer surface list
TextureBinding key/state VRAM/palette ranges, hashes, image/layout, dirty hint Texture-cache owner
texture-binding-state.h Focused state/retry helpers Pure/focused tests
ui/xemu-tweaks.[ch] Planned #95 effective bit, not in reviewed main Future control layer

Actual Current Flow

flowchart TD
    A[Texture slow bind entered] --> B[Iterate active stages]
    B --> C[Read current stage binding and source kind]
    C --> D{Stage dirty, missing, dummy, possibly dirty or surface-backed?}
    D -->|Yes| R[Normal create/update/bind route]
    D -->|No| E[Test texture and palette dirty pages once]
    E -->|Dirty found| R
    E -->|Clean| F{Texture or palette overlaps active surface?}
    F -->|Yes| R
    F -->|No| G{Current texture DMA address matches binding key?}
    G -->|No| R
    G -->|Yes| H{Palette absent or current palette address matches?}
    H -->|No| R
    H -->|Yes| I[Reuse existing binding and continue to next stage]
    R --> J{Lookup/create/upload/bind succeeds?}
    J -->|No| K[Preserve retry requirement and fail preparation]
    J -->|Yes| L[Publish active valid binding]
    I --> M[Next stage]
    L --> M
Loading

The dirty test is not a passive boolean lookup. It test-and-clears dirty-memory bits and can mark cached bindings that overlap the changed pages as possibly dirty.

Maintained Reference Flow

Planned #95 Disabled mode would use:

flowchart TD
    A[Stage passes every current reuse guard] --> B[Record clean-stage eligible]
    B --> C{Planned policy Auto?}
    C -->|Yes| D[Continue with existing binding]
    C -->|Disabled| E[Fall through to current normal path]
    E --> F[Resolve source/surface relationship]
    F --> G[Build texture key and lookup cache]
    G --> H[Hash/upload/copy when required]
    H --> I[Bind texture and apply state]
Loading

The policy must be evaluated after the existing side-effectful checks and must not call them a second time merely for counters.

Eligibility and Protected Invariants

Guard/invariant Why required Failure if removed
pg->texture_dirty[i] == false Guest stage registers/effective state unchanged Wrong format/filter/source state
Binding exists and is not dummy There is a complete object to reuse Missing/placeholder texture
binding->possibly_dirty == false Cache entry has no pending memory concern Stale guest payload
Not surface-backed source Surface copy/change time needs separate handling Stale render-target texture
Dirty-page test/clear once Detect guest texture/palette writes and publish to cache Stale data or lost dirty evidence
No active surface overlap Host surface may be newer than guest RAM Read old VRAM instead of GPU result
Texture physical address matches RAMIN/DMA can remap without texture-state write Bind wrong memory
Palette physical address matches Indexed source can remap separately Wrong colors
Failed bind retains retry state A failed active bind is not complete Permanent stale/dummy binding
Disabled stages retain dirtiness for later enable Inactive stage may become active later Missed update on re-enable

The planned control changes only the final reuse decision.

State, Lifetime, Invalidation, and Fallback

TextureBinding objects are owned by the Vulkan texture cache and retain image/layout, key, source ranges, palette ranges, hash, draw time, and dirtiness hints. The active per-stage pointer references one of those objects.

Invalidators include:

  • texture register writes;
  • guest VRAM/palette dirty pages;
  • cache entry marked possibly dirty by an overlapping write;
  • surface alias/change;
  • DMA A/B remap;
  • palette address change;
  • cache eviction/destruction;
  • failed upload or bind;
  • renderer teardown.

Current main always takes the skip after all checks pass. Planned Disabled mode would force the normal route but would not erase or weaken invalidation.

Expected Profile Signature

After #95 exists, a useful workload should show:

texture slow-bind calls > 0
    + stage checks > slow-bind calls
    + clean_stage_eligible > 0
Auto:
    clean_stage_skips == eligible
Disabled:
    clean_stage_forced_reference == eligible
    + more bind/hash/upload work

Expected local effects can include lower texture-binding CPU, fewer cache lookups/hashes/uploads, and fewer descriptor changes. End-to-end improvement depends on the path being frequent and limiting.

If clean_stage_eligible == 0, the workload did not test the shortcut. If eligible is high but frame metrics do not move, stage rebuild was not limiting.

Counters and Evidence

Planned minimum:

Counter Unit Meaning
vk.texture_bind.slow_bind_calls calls Full stage loop entered
stage_checks stages Active stages evaluated
clean_stage_eligible stages Every mandatory reuse guard passed
clean_stage_skips stages Existing binding reused
clean_stage_forced_reference stages Eligible but policy Disabled
blocked_dirty/missing/dummy/possibly_dirty stages Early guard reasons
blocked_surface_source/overlap stages Surface-coherency reasons
blocked_texture_address/palette_address stages DMA remap reasons
bind_failures stages Reference path failed and must retry
hash/upload bytes and CPU bytes/ns Downstream work affected

For a fixed-policy window:

clean_stage_eligible
    = clean_stage_skips
    + clean_stage_forced_reference

Do not add expensive per-guard clocks or evaluate the dirty helper twice to satisfy the equation.

Risks and Known Bugs

  • The slow-bind context is easy to misdescribe; the optimization skips only clean stages after another stage caused work.
  • Dirty test-and-clear is side-effectful. Reordering or duplicate evaluation can lose evidence.
  • RAMIN may change a DMA descriptor without a texture register write.
  • Palette and texture sources need independent revalidation.
  • Surface overlap/address boundary mistakes can create stale textures.
  • A failed upload/bind must not clear dirty/retry state.
  • Detailed block-reason counters can perturb a short-circuit expression and should be optional if costly.
  • A reference route may hit the same cache entry and avoid upload; “forced reference” does not guarantee a full decode/upload, only that the early continue was not used.

See issue #95, PR #79/#80 where applicable, texture-related PR lessons, and Pull Request Lessons.

Qualification

  1. Pure final policy-gate test: eligible+Auto skips; eligible+Disabled falls through.
  2. Each guard independently rejects reuse.
  3. Dirty-page write and cache propagation, proving one evaluation.
  4. Texture and palette DMA remap without texture-state write.
  5. Surface-to-texture source and overlapping surface cases.
  6. Disabled-stage update followed by enable.
  7. Failed active bind/upload followed by successful retry.
  8. Auto/Disabled exact output and Vulkan validation.
  9. Route counters/accounting identity and hash/upload-byte evidence.
  10. PGR2/Morrowind workloads known to vary texture stages.
  11. Complete Vulkan XISO, teardown, cache eviction, and snapshot/lifecycle tests.
  12. p50/p95/p99/max, texture-binding CPU, host CPU/GPU, RAM/VRAM.

The route should remain Auto only if output equivalence and meaningful end-to-end benefit survive this same-binary qualification.

Recent evidence — a clean handle is not a clean source

Merged PR #124 adds a required guard to all-clean texture reuse: re-resolve physical texture and palette sources before trusting a cached binding.

A RAMIN DMA descriptor can change behind the same guest handle.

same DMA handle != same guest physical source
flowchart TD
    A[Stage appears clean] --> B[Resolve texture source again]
    B --> C[Resolve palette source again]
    C --> D{Source identity still matches?}
    D -->|No| E[Reference update/rebind]
    D -->|Yes| F[Continue clean eligibility]
Loading

Use a focused negative control: bind red bytes through handle H, rewrite H to green bytes without a normal texture-state write, and require the next draw to observe green.

Future reuse keys must list every identity that can mutate behind a stable handle: DMA base/limit/class/target, palette source, surface lifetime, and mapping generation.

Draft PR #134 adds a related rule: an already-dummy disabled stage may bypass slow work, but its guest dirty state must survive for a later re-enable.

Clone this wiki locally