fix(runtime): make GS CSR atomic to fix vsync-worker/guest data race#145
Merged
Conversation
updateGsCsrFieldForVSync runs on the detached vsync worker and updated the FIELD bit with a non-atomic read-modify-write of GSRegisters::csr while guest threads concurrently read/write the same word (MMIO read32/read64 and the W1C handling in write32/write64) and the GIF path sets SIGNAL/FINISH. Even though the writers touch disjoint bits, a whole-word RMW loses the other side's update: a clobbered SIGNAL/FINISH set hangs a game synchronizing on GS completion, a clobbered W1C clear re-asserts a handled interrupt, and a clobbered FIELD toggle stalls interlace field polling. ThreadSanitizer flags the race on main (updateGsCsrFieldForVSync vs PS2Memory::write64 and GS::writeRegister). Make the field std::atomic<uint64_t> and perform every update as a single atomic RMW: - vsync FIELD toggle -> fetch_or / fetch_and - MMIO W1C writes -> compare_exchange loop in shared helpers (a load-then-store pair would still race); a 32-bit store to the CSR's upper dword previously bypassed the W1C special case entirely and went through the plain merge branch - both halves now share the same atomic helper with unchanged guest-visible semantics - GIF SIGNAL/FINISH -> fetch_or - reads -> load() std::atomic<uint64_t> is lock-free on all supported targets (static_assert added), the struct's size/alignment asserts are unchanged, GSRegisters is never copied by value, and default (seq_cst) ordering is used throughout - these operations are rare (vblank ticks, GIF signals, CSR MMIO), so reviewability wins over micro-optimization. New regression test: two racer threads each own one status bit (SIGNAL / FINISH) and loop 80k GIF-set + W1C-clear cycles verifying their own bit after each half-op while the vsync worker toggles FIELD. Fails 20/20 runs against the previous code, passes 50/50 with the fix, ~350ms runtime, no sanitizer needed.
364bc93 to
bf82474
Compare
Owner
|
Hey thnak for your work. I will review later |
Owner
|
Are you on discord ? @tealalchemist is refactoring the GS so its good to ask his review as well |
Contributor
Author
yeah, I'm shanemmathews in the discord |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix(runtime): make GS CSR atomic to fix vsync-worker/guest data race
Problem
updateGsCsrFieldForVSync(Interrupt.cpp) runs on the detached vsync/interrupt worker thread and updates the FIELD bit with a non-atomic read-modify-write of the sharedGSRegisters::csr:Concurrently, guest EE threads read and write the same 64-bit word through the MMIO paths (
ps2_memory.cppread32/read64, and the write-1-to-clear handling inwrite32/write64), and the GIF path sets SIGNAL/FINISH (ps2_gs_gpu.cpp:csr |= 0x1/csr |= 0x2) on the guest thread driving the GIF. All of these are plain, unsynchronized accesses to oneuint64_t.Although the vsync worker and the guest touch disjoint bits, a non-atomic RMW of the whole word still loses the other side's update. Concrete failure modes:
ThreadSanitizer confirms the race (report below).
Hardware background
GS CSR (0x12001000) is a status/control register that is legitimately written from two directions on real hardware: the GS side updates read-only status (FIELD bit 13 toggles every vblank in interlaced modes, NFIELD, FIFO state), while the EE side performs write-1-to-clear on the interrupt status bits (SIGNAL/FINISH/HSINT/VSINT/EDWINT, bits 0–4) and writes the FLUSH/RESET control bits. The emulator mirrors that split across threads — the vsync worker plays the GS role for FIELD, guest threads play the EE role — so the shared word needs atomic bit-level updates for the same word-level interleavings the silicon handles natively.
Fix
Make the field
std::atomic<uint64_t>and convert every RMW to a single atomic RMW instruction:ps2_memory.h:std::atomic<uint64_t> csr;with a comment stating the concurrency contract.std::atomic<uint64_t>is lock-free on every supported target (static_assert added), same size/alignment (existing static_asserts unchanged), andGSRegistersis never copied or memcpy'd as a value.fetch_or(kGsCsrFieldMask)/fetch_and(~kGsCsrFieldMask).fetch_and(~clearMask)(a load+store pair would still race).fetch_or(0x1)/fetch_or(0x2).read32/read64, debug panel, tests) →.load().0x12001004) previously fell through to the plain non-atomic merge branch even though the low dword had W1C special-casing; both halves now go through the same atomic RMW helper with unchanged guest-visible semantics.Default (seq_cst) memory order everywhere: these operations are rare (vblank ticks, GIF signals, guest MMIO on the CSR), so there is nothing to micro-optimize and the code stays trivially reviewable.
Testing
ps2_runtime_interrupt_tests.cpp). Two racer threads each own one status bit (SIGNAL / FINISH) and loop 80k GIF-set + MMIO-W1C-clear cycles, each verifying only its own bit after every half-op, while the real vsync worker toggles FIELD. Post-fix each racer is the sole writer of its bit, so the assertions are exact and deterministic; pre-fix the whole-word RMWs clobber the other racer's bit — fails 20/20 runs against current main, passes 50/50 with the fix, and costs ~350 ms. No sanitizer required, so it guards the invariant on the existing CI matrix. (The racer-vs-racer design is deliberate: the vsync writer's once-per-16.7 ms RMW window is too narrow to hit deterministically in a bounded test, but the corrupting mechanism — a non-atomic whole-word RMW clobbering a concurrently-written disjoint bit — is identical.)ps2x_testssuite passes on Linux (gcc); MSVC and clang via CI.updateGsCsrFieldForVSyncvs CSR-reader race is reported on main and gone with this patch.TSan report on main (excerpt)
Scope
Single-word atomicity fix; no guest-visible behavior change on single-threaded paths, no new dependencies, ~10 edit sites across 5 files + 1 test.