From 68ba1f6d7c764a7d0ea04cf7ea738b0439aa2ead Mon Sep 17 00:00:00 2001 From: Shane Michael Mathews Date: Wed, 22 Jul 2026 18:37:52 -0400 Subject: [PATCH] fix(test): make sceGsSyncV field-parity assertions robust to vblank timing --- ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp | 17 ++++++++++++ ps2xRuntime/src/lib/Kernel/Stubs/GS.h | 2 ++ ps2xTest/src/ps2_gs_tests.cpp | 35 +++++++++++++++++++++++-- 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp b/ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp index 0fd65656b..879b9e321 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp +++ b/ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp @@ -10,6 +10,7 @@ namespace ps2_stubs { std::mutex g_gs_sync_v_mutex; uint64_t g_gs_sync_v_base_tick = 0u; + uint64_t g_gs_sync_v_last_tick = 0u; // last tick consumed by sceGsSyncV (test observability) std::mutex g_gs_sync_v_callback_mutex; uint32_t g_gs_sync_v_callback_func = 0u; uint32_t g_gs_sync_v_callback_gp = 0u; @@ -615,6 +616,18 @@ namespace ps2_stubs return static_cast((tick - g_gs_sync_v_base_tick - 1u) & 1u); } + uint64_t lastGsSyncVConsumedTick() + { + std::lock_guard lock(g_gs_sync_v_mutex); + return g_gs_sync_v_last_tick; + } + + uint64_t gsSyncVBaseTick() + { + std::lock_guard lock(g_gs_sync_v_mutex); + return g_gs_sync_v_base_tick; + } + void resetGsSyncVCallbackState() { { @@ -1460,6 +1473,10 @@ namespace ps2_stubs void sceGsSyncV(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { const uint64_t tick = ps2_syscalls::WaitForNextVSyncTick(rdram, runtime); + { + std::lock_guard lock(g_gs_sync_v_mutex); + g_gs_sync_v_last_tick = tick; + } if (g_gparam.interlace != 0u) { setReturnS32(ctx, getGsSyncVFieldForTick(tick)); diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/GS.h b/ps2xRuntime/src/lib/Kernel/Stubs/GS.h index 8cc2e377f..7b0fcf5bd 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/GS.h +++ b/ps2xRuntime/src/lib/Kernel/Stubs/GS.h @@ -6,6 +6,8 @@ namespace ps2_stubs { void resetGsSyncVCallbackState(); void dispatchGsSyncVCallback(uint8_t *rdram, PS2Runtime *runtime, uint64_t tick); + uint64_t lastGsSyncVConsumedTick(); + uint64_t gsSyncVBaseTick(); void sceGifPkAddGsAD(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); void sceGifPkAddGsData(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); void sceGifPkCloseGifTag(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); diff --git a/ps2xTest/src/ps2_gs_tests.cpp b/ps2xTest/src/ps2_gs_tests.cpp index 4c3e8ec1c..d5f6ff282 100644 --- a/ps2xTest/src/ps2_gs_tests.cpp +++ b/ps2xTest/src/ps2_gs_tests.cpp @@ -3359,13 +3359,44 @@ void register_ps2_gs_tests() setRegU32(resetCtx, 7, 1u); ps2_stubs::sceGsResetGraph(rdram.data(), &resetCtx, &runtime); + // interlaced reset already done above; base is the value the field parity is anchored to. + const uint64_t base = ps2_stubs::gsSyncVBaseTick(); + R5900Context sync0{}; ps2_stubs::sceGsSyncV(rdram.data(), &sync0, &runtime); - t.Equals(static_cast(getRegU32Test(sync0, 2)), 0, "first interlaced sceGsSyncV should report even field"); + const uint64_t tick0 = ps2_stubs::lastGsSyncVConsumedTick(); + const int32_t field0 = static_cast(getRegU32Test(sync0, 2)); R5900Context sync1{}; ps2_stubs::sceGsSyncV(rdram.data(), &sync1, &runtime); - t.Equals(static_cast(getRegU32Test(sync1, 2)), 1, "second interlaced sceGsSyncV should report odd field"); + const uint64_t tick1 = ps2_stubs::lastGsSyncVConsumedTick(); + const int32_t field1 = static_cast(getRegU32Test(sync1, 2)); + + // Liveness. + t.IsTrue(tick1 > tick0, "each interlaced sceGsSyncV must consume a strictly newer vblank tick"); + + // Golden field: mirrors getGsSyncVFieldForTick exactly, incl. the tick<=base clause. + auto expectedField = [base](uint64_t tick) -> int32_t { + if (tick <= base) return 0; + return static_cast((tick - base - 1u) & 1u); + }; + + // Gap-aware absolute assertions — computed from measured gaps, never an assumed gap==1. + t.Equals(field0, expectedField(tick0), + "first interlaced field must match its consumed tick's parity relative to the reset base"); + t.Equals(field1, expectedField(tick1), + "second interlaced field must match its consumed tick's parity relative to the reset base"); + + // Relative XOR cross-check — alternation pinned independently of absolute phase. + t.Equals(field0 ^ field1, static_cast((tick1 - tick0) & 1u), + "consecutive interlaced fields differ iff an odd number of vblanks elapsed between them"); + + // Literal hardware anchor, asserted ONLY under the nominal gaps (base->tick0==1, delta==1). + if (tick0 == base + 1u && tick1 - tick0 == 1u) + { + t.Equals(field0, 0, "first interlaced sceGsSyncV should report even field"); + t.Equals(field1, 1, "second interlaced sceGsSyncV should report odd field"); + } R5900Context resetProgCtx{}; setRegU32(resetProgCtx, 4, 0u);