Repository navigation
Graphics Transition Debugging Tools
Purpose: Reproduce and localize timing-sensitive graphics corruption across guest scene, resource, cache, and presentation transitions. Source investigation: Issue #106 — PGR2 race-to-menu corrupt textures Companion lesson: PGR2 Race-to-Menu Corruption — Lessons Learned
Issue #106 required more than a profiler, validation layer, or one GPU capture. The defect crossed several boundaries:
guest input and scene transition
→ guest VRAM ownership
→ cached render surfaces
→ texture upload and cache state
→ Vulkan image contents
→ guest display output
→ OpenGL host presentation
The useful toolchain therefore combines deterministic workload control, identity manifests, dual-boundary captures, direct Vulkan inspection, stable emulator resource IDs, and bounded writer traces.
This page documents the reusable tools and operating rules. It does not assume the private PGR2 image, HDD, input script, or GPU capture is publicly available.
Use the cheapest tool that can distinguish the remaining hypotheses.
flowchart TD
A[Visual or timing-sensitive transition defect] --> B[Deterministic retail runner]
B --> C{Repeats at exact checkpoint?}
C -->|No| D[Record identities and park as unreproduced]
C -->|Yes| E[Capture host window and guest/display output]
E --> F{Guest output wrong?}
F -->|No| G[Presentation handoff tools]
F -->|Yes| H[Backend control and direct GPU capture]
H --> I{Bound resource already wrong?}
I -->|No| J[Descriptor/view/shader/input trace]
I -->|Yes| K[Stable resource correlation]
K --> L[Upload-boundary or target-range writer trace]
L --> M[Focused ownership regression]
The order matters. A broad transition trace added before reproducibility can produce gigabytes of unrelated state. A final-frame capture cannot show an earlier writer when the affected resource is read-only in that frame.
Drive a real title through one exact state transition with fixed inputs and isolated mutable state. The runner is an oracle driver, not merely keyboard automation.
Issue #106 used a local PowerShell GUI pipe runner. Its exact private script was identified by SHA-256 in the evidence rather than published. Future reusable runners should expose the same contract even when implemented in Python, PowerShell, or another automation layer.
For the race-to-menu case, the fixed logical order was:
1. boot or load the selected seed
2. wait for active-race admission marker/image
3. drive for a fixed input window
4. pause when the route requires it
5. select exit race
6. wait for the first stable target menu
7. capture MENU_FIRST
8. wait five seconds without input
9. capture MENU_LATE
10. exit cleanly
The exact key names, durations, and admission markers belong in the runner manifest. Do not substitute a nearby menu or a pause screen.
Each role needs its own mutable state:
run-root/
manifest.json
config/
shader-cache/
hdd-private-copy/
logs/
screenshots/
guest-output/
gpu-captures/
Initial isolation rules:
- one immutable HDD seed;
- one private HDD copy per role;
- clean configuration directory per role;
- shader cache disabled or separately seeded as required by the experiment;
- no process from a prior role left running;
- no screenshot from a covered, minimized, or stale window;
- fixed host power/performance policy where practical.
The exact paths are environment-specific. The important behavior is copy isolation and identity logging:
$Seed = "D:\xemu-seeds\pgr2-base.qcow2"
$RunRoot = "D:\xemu-runs\issue106\A1"
$PrivateHdd = Join-Path $RunRoot "pgr2-A1.qcow2"
New-Item -ItemType Directory -Force $RunRoot | Out-Null
Copy-Item -LiteralPath $Seed -Destination $PrivateHdd
Get-FileHash -Algorithm SHA256 $Seed
Get-FileHash -Algorithm SHA256 $PrivateHdd
Get-FileHash -Algorithm SHA256 "C:\xemu\xemu.exe"After each run, verify cleanup rather than assuming the window closed:
Get-Process -Name xemu -ErrorAction SilentlyContinueIf a process remains, record the abnormal termination before stopping it. Silent cleanup can erase evidence of a hang or device loss.
Use same-executable roles before comparing different builds:
A1: Vulkan, Hybrid On, shortcut On
B1: Vulkan, Hybrid On, shortcut Off
C1: Vulkan, Hybrid Off, shortcut Off
A2: Vulkan, Hybrid On, shortcut On
B2: Vulkan, Hybrid On, shortcut Off
C2: Vulkan, Hybrid Off, shortcut Off
Run the alternate backend only after the target transition is stable:
D1/D2: OpenGL reference
The second cycle is not optional for a race. In issue #106, the first A/B/C cycle pointed toward the shortcut; the second cycle corrupted all Vulkan roles and disproved that attribution.
Record at least:
{
"source_commit": "full SHA",
"runtime_tree": "full SHA or tree identity",
"executable_sha256": "...",
"symbols_identity": "PDB/DWARF build identity",
"renderer": "vulkan",
"surface_scale": 4,
"hybrid_requested": true,
"hybrid_effective": true,
"shortcut_requested": true,
"shortcut_effective": true,
"shader_cache": "off",
"gpu_name": "...",
"gpu_uuid": "...",
"driver": "...",
"presentation_transport": "...",
"config_sha256": "...",
"hdd_seed_sha256": "...",
"input_runner_sha256": "..."
}Use the effective route, not only the requested setting.
Capture separately:
race_window.png
target_first_window.png
target_late_window.png
guest_scanout.bin or guest_scanout.png
run.log
manifest.json
results.csv row
Store dimensions and SHA-256 for every binary/image artifact. Raw retail images can remain private while hashes and classifications are published.
Prevent accidental comparisons between different executables, images, disks, configurations, or captures.
Get-FileHash -Algorithm SHA256 .\xemu.exe
Get-FileHash -Algorithm SHA256 .\menu_first_window.png
Get-FileHash -Algorithm SHA256 .\capture.rdcsha256sum xemu menu_first_window.png capture.rdcshasum -a 256 xemu menu_first_window.png capture.rdcgit rev-parse HEAD
git rev-parse HEAD^{tree}
git status --porcelainA clean git status --porcelain plus commit/tree identity distinguishes the intended source from an uncommitted diagnostic build.
The issue #106 evidence used a CSV shape similar to:
build,source_commit,role,hybrid,shortcut,shader_cache,
race_sha256,menu_first_sha256,menu_late_sha256,
menu_first_class,menu_late_class,outcome
Do not put subjective prose only in screenshots or chat. Preserve a machine-readable role/result ledger.
Separate guest renderer failure from final host presentation failure.
flowchart LR
A[NV2A work] --> B[Renderer-produced guest/display image]
B --> C[Shared-image or host-copy handoff]
C --> D[OpenGL UI/presentation]
D --> E[Host window]
B -. capture/hash .-> F[Guest-output artifact]
E -. capture/hash .-> G[Window artifact]
Interpretation:
guest wrong + window wrong
→ defect is before or inside display composition
guest correct + window wrong
→ presentation/shared-image/host-copy/UI route
first target wrong + late target correct
→ transient publication or transition ordering
first target wrong + late target wrong
→ persistent bad state or bad resource contents
A window screenshot alone cannot distinguish these cases.
xemu uses Vulkan for NV2A rendering and OpenGL for frontend/UI presentation. An external hotkey can capture the frontend OpenGL loop rather than the Vulkan guest frame that produced the image.
Issue #106 used RenderDoc 1.46 through its in-process capture API at the NV2A Vulkan guest-flip boundary. This produced a capture containing the actual guest Vulkan draws.
host present count wrong clock for guest rendering
frontend F12 capture may capture OpenGL presenter
NV2A guest flip useful boundary for guest Vulkan frame
resource upload event useful boundary for finding an earlier writer
The RenderDoc overlay reported roughly 2,003–2,097 FPS while the same corrupted menu remained visible. That was the host presentation loop, not Xbox FPS.
Exact integration depends on the RenderDoc API version already used by the project. The required control flow is:
if (capture_requested_at_guest_flip()) {
renderdoc_api->StartFrameCapture(vulkan_device, window_handle);
}
submit_guest_vulkan_frame();
if (capture_active_for_guest_flip()) {
renderdoc_api->EndFrameCapture(vulkan_device, window_handle);
}Do not begin/end capture on arbitrary host presents. Store the guest frame/flip sequence in the capture manifest.
Within RenderDoc:
- Confirm the capture is Vulkan and contains expected guest draws.
- Locate the first draw that introduces the visible bad region into the color target.
- Inspect fragment input resources for that draw.
- Export the suspected image and compare it with the visible corruption.
- Inspect resource usage history:
- reads in this capture;
- writes in this capture;
- creation/import events;
- barriers/layout transitions.
- Inspect an unaffected peer resource as a control.
- Decide:
bound image already corrupt
→ trace image creation/upload/later writer
image correct, draw output corrupt
→ descriptor, view, sampler, coordinates, uniforms, shader inputs
draw output correct, later target corrupt
→ later render-target writer or display composition
Issue #106 found a 256 × 256 BC3_UNORM image that was already corrupt before events 188, 275, and 297 sampled it. The capture contained no writes to that image, so another settled-menu capture could not identify the writer.
RenderDoc captures can contain proprietary shaders, textures, and game data. Keep raw captures private unless redistribution is authorized. Publish:
- capture SHA-256 and size;
- RenderDoc version;
- source/executable identity;
- trigger boundary;
- resource dimensions and format;
- event numbers;
- read/write classification;
- derived hashes and conclusions.
ResourceId::200807 identified the suspect image inside one RenderDoc capture. It was not stable across runs and did not identify the guest texture or xemu cache binding.
Add or expose already-computed fields:
typedef struct TextureTraceIdentity {
uint64_t binding_lifetime_id;
uint64_t image_lifetime_id;
uint64_t upload_sequence;
uint64_t key_hash;
uint64_t content_hash;
uint64_t guest_source_addr;
uint64_t guest_source_length;
uint64_t palette_source_addr;
uint64_t palette_source_length;
uint32_t format;
uint32_t width;
uint32_t height;
uint32_t levels;
} TextureTraceIdentity;Stable identity rules:
- lifetime IDs increment when host objects are created;
- same guest address with a new host lifetime receives a new ID;
- do not serialize host pointers;
- do not recompute expensive hashes solely for the tracer when retained hashes exist;
- record zero/sentinel for absent or invalid state;
- include upload sequence so several writes to one image can be ordered.
For a capture request, log:
guest flip/frame
binding lifetime
image lifetime
source range
format/dimensions
retained content hash
upload sequence
VkBufferImageCopy offset/extent
RenderDoc capture path/hash
This allows the external image to be traced back to its last emulator-side upload or copy.
Determine whether corruption exists in source bytes, staging bytes, post-copy image contents, or only later.
Issue #106 verified that the initial 65,536 bytes at VkBufferImageCopy.bufferOffset = 16384 decoded byte-for-byte to the correct 256 × 256 BC3 atlas.
A reusable verifier should accept:
staging buffer dump
buffer offset
copy extent
format
mip/layer
row/block layout
reference image or reference decoded hash
Expected report:
source/staging byte SHA-256 or fast retained hash
decoded pixel SHA-256
dimensions and format
first differing block and coordinates
number of differing blocks/pixels
For block-compressed formats, compare both:
- exact compressed block bytes;
- decoded pixels.
Equal decoded pixels do not prove byte identity, while equal compressed bytes do.
Do not use a display screenshot as proof that staging bytes were correct. Export or dump the exact copy source range.
Compare failing and passing runs at one narrow guest-frame window without allocating, formatting, or writing per-draw text.
The issue proposed this runtime-disabled interface:
XEMU_VK_STATE_TRACE=<output.bin>
XEMU_VK_STATE_TRACE_FRAME_BEGIN=<guest frame number>
XEMU_VK_STATE_TRACE_FRAME_END=<guest frame number>
XEMU_VK_STATE_TRACE_MAX_RECORDS=8192
Rules:
- no output path means no allocation and no recording;
- reject invalid frame ranges or capacities at startup;
- preallocate one bounded record array;
- fill records from state already computed for the draw;
- no string formatting in the draw path;
- on overflow, set
droppedand stop rather than wrapping over the first divergence; - write the binary file at close or explicit frame end;
- version the header and record size.
The full issue proposal includes these groups:
sequence / guest frame / draw index
shader-state, binding-key, and pipeline-key hashes
selected shader and pipeline lifetime IDs
Hybrid generation/route/selection epochs
requested and selected fragment route
descriptor-set identity and dynamic uniform offsets
fallback-control offset/hash
texture binding/key/content identities per stage
texture source and palette addresses
texture dirty/retry/surface-source masks
color/zeta surface lifetime, address, and draw time
scanout address and display generation
output dimensions, scale, and flags
Header shape:
typedef struct PGRAPHVkStateTraceHeader {
uint32_t magic;
uint32_t version;
uint32_t record_size;
uint32_t record_count;
uint32_t dropped;
uint32_t reserved;
uint64_t source_identity_hash;
} PGRAPHVkStateTraceHeader;The tracer must not alter the state being diagnosed:
- do not call dirty test-and-clear helpers;
- do not resolve DMA addresses again if a binding retains them;
- do not copy a large control block for every draw when a retained hash is enough;
- do not force uploads, waits, or cache misses;
- do not serialize host pointers;
- do not synchronously write a record per draw.
disabled trace allocates/writes nothing
frame range includes only requested frames
capacity stops without overwrite
equal snapshots serialize identically
changing each identity changes its record field
invalid bindings use explicit sentinels
partial initialization still writes valid header/count
record version/size mismatch is rejected by reader
The general trace was not the final decisive tool for issue #106, but its contract remains useful when failing and passing roles diverge in shader, descriptor, or route selection.
Follow one guest-memory range backward and forward across all representations. This was the decisive issue #106 tool.
Configure:
target guest address
target length
optional expected format/dimensions
maximum event count
frame/transition window
Record only operations that overlap the target range.
typedef enum RangeEventType {
RANGE_CPU_WRITE,
RANGE_PGRAPH_BLIT_WRITE,
RANGE_SURFACE_UPLOAD,
RANGE_SURFACE_DRAW,
RANGE_SURFACE_DOWNLOAD,
RANGE_TEXTURE_UPLOAD,
RANGE_VERTEX_UPLOAD,
RANGE_GUEST_READ,
} RangeEventType;
typedef struct RangeTraceEvent {
uint64_t sequence;
uint64_t guest_frame;
uint64_t host_object_lifetime;
uint64_t before_hash;
uint64_t after_hash;
uint64_t overlap_start;
uint64_t overlap_length;
uint32_t type;
uint32_t flags;
} RangeTraceEvent;Additional type-specific state:
surface address/size/shape/pitch/format
draw_dirty, download_pending, upload_pending
texture binding/image lifetime and upload sequence
staging buffer offset and payload hash
dirty client observed/cleared
accelerator
first consuming draw/event
flowchart TD
A[Choose exact corrupt resource source range] --> B[Log current range hash]
B --> C[Record every overlapping writer]
C --> D[Record before/after hash and object lifetime]
D --> E{Hash first becomes wrong?}
E -->|No| C
E -->|Yes| F[Name exact writer and authority state]
F --> G[Follow next texture/upload consumer]
G --> H[Build focused ownership regression]
This trace established that two surface downloads—not the texture upload—changed the atlas hash.
Find independent consumers that incorrectly share a consumable dirty notification.
Inventory every call to:
memory_region_set_log
memory_region_set_client_dirty
memory_region_test_and_clear_dirty
physical_memory_set_dirty_range
physical_memory_clear_dirty_range
For each dirty client, document:
| Field | Question |
|---|---|
| Producers | Which CPU/device/PGRAPH writes mark it? |
| Consumers | Which subsystem clears it? |
| Meaning | What authority transition does it represent? |
| Accelerator coverage | Is it consumed under TCG and accelerated execution? |
| Range granularity | Page, byte range, object, or full memory? |
| Destructive read | Can one consumer erase another's evidence? |
| Reset/snapshot | How is the client initialized and restored? |
Audit rule:
Consumers may share a dirty client only when clearing the notification for one consumer is semantically equivalent to acknowledging it for every other consumer.
That was false for surface coherency versus texture/vertex validation.
A useful diagnostic must have an outcome that would disprove it.
Examples from issue #106:
| Diagnostic | Positive result | Negative control |
|---|---|---|
| Shortcut ownership | A fails, B/C pass in both cycles | B/C also fail on repeat |
| Native BC ownership | Decoded route clears corruption | Decoded route still corrupts |
| Old serialization ownership | Restored guard clears corruption | Same exact menu still corrupts |
| Source hash/upload TOCTOU | Immutable snapshot clears repeated defect | One clean and one corrupt with zero source changes |
| Presentation ownership | Guest image correct, window wrong | Direct NV2A image already corrupt |
| Dedicated dirty client | Repeated retail runs clean | Shared-client candidates fail second run |
A first clean run is not a sufficient positive result for a nondeterministic race.
Stop broad instrumentation and move inward when:
exact transition repeats
backend control isolates subsystem
direct capture identifies bad resource class
resource export proves whether content is already wrong
stable identity names exact object/upload
range trace names exact writer
Stop and park rather than patch when:
symptom cannot repeat under pinned identities
runner cannot prove it reached the same checkpoint
window capture may be stale/covered
build or disk identity is uncertain
candidate route was never reached
Do not add product behavior changes until the failure boundary is narrower than the proposed fix.
A complete run directory should make these questions answerable without rerunning the title:
Which source and executable ran?
Which symbols match it?
Which configuration and effective routes were active?
Which HDD/input script created the transition?
Did the exact checkpoint occur?
What did guest output contain?
What did the host window contain?
Which GPU resource was bound?
What was its stable emulator identity?
When was it last written?
Which writer first changed the target bytes?
Was the process and private disk cleaned up?
Suggested bundle:
manifest.json
results.csv
run.log
input-timeline.json
race_window.sha256
menu_first_window.sha256
menu_late_window.sha256
guest_output.sha256
trace.bin + trace-schema-version.txt
capture.rdc.sha256
resource-export.sha256
range-events.csv
build-info.txt
cleanup-receipt.txt
- PGR2 Race-to-Menu Corruption — Lessons Learned
- Debugging Tools & Commands
- Debugging Lag & Hot-Path Analysis
- Memory & DMA
- Vulkan Renderer
- Performance Methodology
- Pull Request Lessons
- Issue #106
- PR #119
When a producer feeds byte comparison, hashing, reflection-sized copies, or cache keys, run it twice from identical guest state with differently canary-filled output storage. Compare exactly the byte ranges downstream consumes. A failure means the experiment is comparing memory history rather than semantic state.
Keep the guest handle constant while rewriting its RAMIN descriptor to a different physical source. This detects caches that confuse handle identity with source identity.
If an exact guest test produces zero records or an unexpected skip, confirm profile, test ID, and xemu-only enable flags before changing emulator code. A test-environment correction is not an emulator fix.
For a lock/progress fix, deliberately remove one required progress-service step in a local regression build and require the fixture to time out. This proves the test covers the dependency cycle.
When a focused leaf fails on the candidate, run the same leaf on current main. If both fail identically, the oracle may encode a different ownership/timing contract. Define the intended contract before changing production code.
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