Skip to content

fix(test): make sceGsSyncV field-parity assertions robust to vblank timing#182

Draft
smmathews wants to merge 1 commit into
ran-j:mainfrom
smmathews:feature/20-gssyncv-test-determinism
Draft

fix(test): make sceGsSyncV field-parity assertions robust to vblank timing#182
smmathews wants to merge 1 commit into
ran-j:mainfrom
smmathews:feature/20-gssyncv-test-determinism

Conversation

@smmathews

Copy link
Copy Markdown
Contributor

Problem

The interlaced field-parity test for sceGsSyncV races the free-running vsync-tick source in
two ways:

  1. Pair incoherence — the field returned by each sceGsSyncV call and the tick it was
    computed from are never observed together; the test only reads the returned field, never the
    tick that produced it.
  2. Phase assumption — the hardcoded 0-then-1 expected fields silently assume the first wait
    consumes exactly base + 1, i.e. exactly one vblank elapses between the reset's base capture
    and the first wait.

On a loaded test runner, a vblank can land in that window between the reset and the first wait.
When it does, the true parity inverts relative to the hardcoded assumption, and the assertion
fails even though the field-parity logic itself is correct. This fails intermittently on loaded
Windows CI runners while passing reliably on Linux — a timing artifact, not a code regression.

Fix

sceGsSyncV now records the vblank tick it consumed under its existing mutex. Two read-only
accessors, lastGsSyncVConsumedTick() and gsSyncVBaseTick(), expose that consumed tick and the
parity-anchor base to the test. The test now computes each call's expected field directly from
the measured (base, tick) gap — (tick − base − 1) & 1, with the tick ≤ base ⇒ 0 clause
mirrored exactly — instead of assuming a fixed gap. It adds a phase-independent XOR cross-check
(field0 ^ field1 == (tick1 − tick0) & 1) and a liveness check (tick1 > tick0), and keeps the
original literal even/odd anchor, but only asserts it under the nominal gaps where it is
guaranteed true.

Why this is race-free by construction

Every assertion is a pure function of the coherent triple (base, tick0, tick1), all read back
after their respective calls return. Nothing re-samples a live counter, and nothing assumes a
fixed elapsed-tick count — whatever gap the loaded runner actually produced, the expectation is
derived from that same gap. The literal 0/1 anchor fires only when its precondition
(tick0 == base + 1 && tick1 - tick0 == 1) is literally true, so it can never see a false
mismatch. The failing interleaving becomes impossible, not merely rarer.

Accessors' zero-behavior-change argument

The only substantive runtime edit is the tick-recording write in sceGsSyncV. It is observation
state, not behavior: sceGsSyncV's observable effects are exactly (a) the value written to the
return register via setReturnS32, and (b) the vblank wait it performs. Both are byte-identical
to before this change — the same consumed tick still flows into the existing field-parity
computation unchanged; no guest memory, no other register, no field math, and no wait timing is
touched. The recorded value is written to a variable read only by the two new accessors, which
are pure lock-and-return functions mirroring the mutex discipline already used by the existing
field-parity helper. This can be confirmed by diff review plus a full green test suite.

Testing

Configure and build per the canonical CI commands, then run the test binary from the repository
root (one of the runtime tests is working-directory sensitive):

cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS=-msse4.1 -DCMAKE_CXX_FLAGS=-msse4.1
cmake --build build --config Release
./build/ps2xTest/ps2x_tests

To verify the rework actually catches regressions, the following single-line mutations to
getGsSyncVFieldForTick / sceGsSyncV in ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp each make
exactly the sceGsSyncV interlaced-parity test fail while the rest of the suite stays green:

  • Drop the -1u phase offset from the field computation.
  • Change the parity mask from & 1u to & 0u (field always even).
  • Replace the vblank wait with a constant that never advances the tick.

Windows-msvc CI is the real arbiter that the failing interleaving is eliminated, since the
original flake is specific to loaded Windows CI runners and is not expected to reproduce on an
unloaded Linux box.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant