Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -615,6 +616,18 @@ namespace ps2_stubs
return static_cast<int32_t>((tick - g_gs_sync_v_base_tick - 1u) & 1u);
}

uint64_t lastGsSyncVConsumedTick()
{
std::lock_guard<std::mutex> lock(g_gs_sync_v_mutex);
return g_gs_sync_v_last_tick;
}

uint64_t gsSyncVBaseTick()
{
std::lock_guard<std::mutex> lock(g_gs_sync_v_mutex);
return g_gs_sync_v_base_tick;
}

void resetGsSyncVCallbackState()
{
{
Expand Down Expand Up @@ -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<std::mutex> lock(g_gs_sync_v_mutex);
g_gs_sync_v_last_tick = tick;
}
if (g_gparam.interlace != 0u)
{
setReturnS32(ctx, getGsSyncVFieldForTick(tick));
Expand Down
2 changes: 2 additions & 0 deletions ps2xRuntime/src/lib/Kernel/Stubs/GS.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
35 changes: 33 additions & 2 deletions ps2xTest/src/ps2_gs_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<int32_t>(getRegU32Test(sync0, 2)), 0, "first interlaced sceGsSyncV should report even field");
const uint64_t tick0 = ps2_stubs::lastGsSyncVConsumedTick();
const int32_t field0 = static_cast<int32_t>(getRegU32Test(sync0, 2));

R5900Context sync1{};
ps2_stubs::sceGsSyncV(rdram.data(), &sync1, &runtime);
t.Equals(static_cast<int32_t>(getRegU32Test(sync1, 2)), 1, "second interlaced sceGsSyncV should report odd field");
const uint64_t tick1 = ps2_stubs::lastGsSyncVConsumedTick();
const int32_t field1 = static_cast<int32_t>(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<int32_t>((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<int32_t>((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);
Expand Down