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.
The same distinction mattered for the Grabbed by the Ghoulies effect investigation: an external capture contained one OpenGL presentation draw, while an in-process capture contained the guest Vulkan draws. A small .rdc file or a visible screenshot is not proof that the correct API was captured.
The build must explicitly enable RenderDoc. The default renderdoc=auto is not proof that CONFIG_RENDERDOC was defined in the resulting executable. Use the project's normal toolchain/build instructions with -Drenderdoc=enabled, then inspect the generated configuration before using the executable:
meson configure BUILD_DIR -Drenderdoc=enabled
meson compile -C BUILD_DIR xemu
rg 'CONFIG_RENDERDOC' BUILD_DIR/config-host.hAdjust the configuration-header path to the selected Meson build tree. Confirm it defines CONFIG_RENDERDOC; an #undef means F10 will not request a guest capture. Record the source commit, executable SHA-256, RenderDoc version, renderer, and configuration used for the run.
On Windows, launch the diagnostic executable through RenderDoc's CLI in the interactive desktop session. This is the command shape used for the Ghoulies capture; substitute your own paths and snapshot name:
$rdoc = 'C:\Tools\RenderDoc\renderdoccmd.exe'
$exe = 'C:\Path\To\xemu.exe'
$config = 'C:\Path\To\xemu.toml'
$capturePrefix = 'C:\Path\To\PrivateCaptures\guest-target'
& $rdoc capture -d (Split-Path $exe) -c $capturePrefix $exe `
-name 'xemu-capture,debug-threads=on' -config_path $config `
-loadvm SNAPSHOT_NAMEFor automated Windows GUI runs, invoke that command through the existing interactive PowerShell pipe. A noninteractive SSH desktop is not equivalent. Check that renderdoc.dll is loaded in xemu before sending any game input.
The current Vulkan pgraph_vk_debug_frame_terminator() also requires IsTargetControlConnected() before honoring the F10 request. renderdoccmd capture can inject the API while reporting no target-control connection. If F10 produces no capture, check this condition before changing gameplay input or extending the run. The Ghoulies investigation used a private, exact-source diagnostic build that removed only this capture-control gate, logged API/start/end markers, and ended capture on the first guest flip after five seconds. It was not a rendering fix and must not silently become a release change.
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.
With CONFIG_RENDERDOC, xemu's F10 requests one guest flip and Shift+F10 requests five guest flips; these counts are frames, not seconds. Ctrl+F10 adds NV2A tracing. xemu's own F12 takes a screenshot; an external RenderDoc F12 hotkey may capture only the OpenGL presenter. A short animation can disappear during a five-frame capture or before a delayed replay trigger.
For a time-limited, automatic diagnostic capture, arm it while the game is paused, wait for an explicit StartFrameCapture marker, then send the resume input. End it on the first guest flip after the chosen elapsed time, with a safety timeout. The validated Ghoulies sequence was controller connected → Start to reach pause → arm capture → verify start marker → B for about 250 ms → automatic end at about five seconds. No attack or movement input was needed. The marker check matters: a queued F10 request does not prove the guest Vulkan capture actually started.
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.
The PGR2 recurrence was captured without clicking through the RenderDoc UI. On the Windows GUI test session, use RenderDoc's CLI to launch a RenderDoc-enabled diagnostic xemu. A normal release binary may lack CONFIG_RENDERDOC and therefore cannot service xemu's F10 guest-flip request. Use a fresh private HDD and the same effective renderer/settings as the reproduction.
$rdoc = 'C:\tools\RenderDoc_1.46_64\renderdoccmd.exe'
$xemu = 'C:\builds\xemu-renderdoc-diagnostic.exe'
$config = 'C:\captures\pgr2\xemu.toml'
& $rdoc capture `
--capture-file='C:\captures\pgr2\guest-vulkan' `
--opt-ref-all-resources `
$xemu -name 'xemu-capture,debug-threads=on' -config_path $configLaunch from the interactive GUI PowerShell session, not an SSH service session. Wait until the corrupted menu is visible, then request the capture with xemu's F10 shortcut. On the test rig the input was sent by:
& 'C:\xemu-lab\suite\send-xemu-key.ps1' `
-ProcessId $xemuPid -VirtualKey 121 -HoldMilliseconds 350The private timed diagnostic used for this run logged RENDERDOC_CAPTURE_START and RENDERDOC_CAPTURE_END elapsed_ms=5003 saved=1 at NV2A flips. Its five-second duration is a diagnostic patch, not the stock F10 behavior. Verify the saved capture by replaying it: the overlay can say “Capturing OpenGL” because xemu also has an OpenGL frontend. The decisive replay result here was GraphicsAPI.Vulkan. The 245 MB capture covered 10,979 guest draws; a capture that large should be retained privately and narrowed around the writer for the next attempt.
RenderDoc 1.46 also supports automated replay through its embedded Python interpreter. This command runs a script supplied as a file, so resource inspection does not require manual GUI navigation:
& 'C:\tools\RenderDoc_1.46_64\qrenderdoc.exe' `
--python 'C:\captures\pgr2\inspect.py'The script can open the .rdc with renderdoc.OpenCaptureFile(), call OpenCapture(renderdoc.ReplayOptions(), None), verify GetAPIProperties().pipelineType, enumerate GetTextures(), export a candidate with SaveTexture(), and obtain its in-capture history with GetUsage(resourceId). RenderDoc's texture export example and resource-usage example show the APIs; the CLI source documents launch/injection behavior. A GetUsage() list containing only shader reads means only that this capture has no image writer; it says nothing about image creation or uploads before capture began.
The recurrence capture had a 256 × 256 BC3_UNORM image with the same colored strips visible in the menu text. RenderDoc reported 894 fragment-shader reads and no writes for that image inside the five-second capture. A separate UI-icon image was intact. The captured ResourceId is transient; correlate it with xemu's guest source address, texture key/hash, image lifetime, and upload sequence before using it in a later run. A Windows focus request can fail even when the xemu window handle exists; check screenshots, guest progress, and the capture markers rather than treating that one failure as a graphics result.
For cross-run comparison, export the raw BC3 mip with GetTextureData() and hash the same 65,536 bytes in each capture. In this case, seven of nine one-level 256 × 256 BC3 images had byte-identical clean-run peers; the colored-text image and a mostly white image did not. That localizes the damage, but capture-local resource IDs and image ordering do not establish a guest address. The corrupt capture lacked same-run upload markers, so the comparison cannot identify the earlier writer.
First reject a presenter-only capture. renderdoccmd can convert the saved .rdc for a quick, scriptable API/event inventory:
$capture = 'C:\Path\To\PrivateCaptures\capture.rdc'
& $rdoc convert -f $capture -o "$capture.zip.xml" -c zip.xml
Get-FileHash -Algorithm SHA256 $captureInspect the converted XML driver/API and draw calls. A useful Vulkan capture must contain the expected guest vkCmdDraw*, render passes, submissions, and the game color/depth attachments. An OpenGL capture with only the final blit is the wrong layer even if the window image shows the bug. The Ghoulies capture contained Vulkan guest draws and a 640×480 game target; the earlier external attempt contained one OpenGL presentation draw.
renderdoccmd supports capture, conversion, replay, and thumbnail operations, but does not expose a pixel-history subcommand. Use QRenderDoc's embedded Python replay for repeatable analysis; the verified qrenderdoc.exe --python script.py launch above avoids manual navigation. When a capture is already open in QRenderDoc's Python shell, pyrenderdoc.Replay().BlockInvoke(callback) supplies a replay controller. For a known game render target and pixel:
import renderdoc as rd
target = next(t.resourceId for t in pyrenderdoc.GetTextures()
if t.width == 640 and t.height == 480
and str(t.resourceId) == 'ResourceId::TARGET_ID')
def inspect(replay):
replay.SetFrameEvent(LAST_CAPTURE_EVENT, False)
changes = replay.PixelHistory(target, X, Y, rd.Subresource(),
rd.CompType.Typeless)
for change in changes:
print(change.eventId, change.Passed(), change.depthTestFailed,
change.shaderDiscarded, change.preMod, change.postMod)
replay.SetFrameEvent(EVENT_ID, False)
state = replay.GetPipelineState()
print(state.GetDepthTestState(), state.GetColorBlends())
pyrenderdoc.Replay().BlockInvoke(inspect)Replace TARGET_ID, X, Y, EVENT_ID, and LAST_CAPTURE_EVENT with values from this capture. Move replay to the last event before requesting whole-capture pixel history: leaving replay on an earlier event can silently return only the earlier part of the history. Resource IDs and event IDs are not stable across captures. RenderDoc's Python replay API, pixel-history guide, and QRenderDoc command-line implementation document the relevant interfaces and options.
Then, in the replay:
- 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