Repository navigation
Hot Path Vulkan Clean Texture Stages
Lifecycle: Performance - Implemented · Type: Per-stage shortcut
State: Present in currentmain.
| 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 |
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.
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.
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.
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.
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.
| 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 |
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
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.
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]
The policy must be evaluated after the existing side-effectful checks and must not call them a second time merely for counters.
| 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.
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.
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.
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.
- 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
continuewas not used.
See issue #95, PR #79/#80 where applicable, texture-related PR lessons, and Pull Request Lessons.
- Pure final policy-gate test: eligible+Auto skips; eligible+Disabled falls through.
- Each guard independently rejects reuse.
- Dirty-page write and cache propagation, proving one evaluation.
- Texture and palette DMA remap without texture-state write.
- Surface-to-texture source and overlapping surface cases.
- Disabled-stage update followed by enable.
- Failed active bind/upload followed by successful retry.
- Auto/Disabled exact output and Vulkan validation.
- Route counters/accounting identity and hash/upload-byte evidence.
- PGR2/Morrowind workloads known to vary texture stages.
- Complete Vulkan XISO, teardown, cache eviction, and snapshot/lifecycle tests.
- 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.
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]
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.
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