Files
PS2Recomp/ps2xTest/src/ps2_runtime_interrupt_tests.cpp
T
Ran-j 4fff58373c fix: fix cri dtx loading
fix: fix wrong mmi instruction translation
fix: fix thread info params
feat: added EE  timers decoder and consumer
feat: split SFI and IOP memory to prevent collision and overrides
2026-08-12 11:49:30 -03:00

617 lines
25 KiB
C++

#include "MiniTest.h"
#include "ps2_runtime.h"
#include "ps2_syscalls.h"
#include "runtime/ee_scheduler.h"
#include <atomic>
#include <chrono>
#include <cstdint>
#include <cstring>
#include <exception>
#include <thread>
#include <vector>
using namespace ps2_syscalls;
namespace
{
constexpr int KE_OK = 0;
constexpr int KE_EVF_COND = -421;
constexpr uint32_t WEF_OR = 1u;
constexpr uint32_t WEF_CLEAR = 0x10u;
constexpr uint32_t WEF_CLEAR_ALL = 0x20u;
struct Ps2EventFlagInfo
{
uint32_t attr;
uint32_t option;
uint32_t initBits;
uint32_t currBits;
int32_t numThreads;
int32_t reserved1;
int32_t reserved2;
};
static_assert(sizeof(Ps2EventFlagInfo) == 28u, "Unexpected Ps2EventFlagInfo layout.");
struct TestEnv
{
std::vector<uint8_t> rdram;
PS2Runtime runtime;
TestEnv() : rdram(PS2_RAM_SIZE, 0u)
{
}
};
std::atomic<uint32_t> g_lastIntcArg{0u};
constexpr uint32_t kIdleVSyncWaitPc = 0x00160000u;
constexpr uint32_t kVSyncWaitPc = 0x00160100u;
constexpr uint32_t kVSyncResumePc = 0x00160110u;
constexpr uint32_t kIrqWaitPc = 0x00160200u;
constexpr uint32_t kIrqResumePc = 0x00160210u;
constexpr uint32_t kIntcHandlerPc = 0x00160220u;
constexpr uint32_t kISemaWaitPc = 0x00160300u;
constexpr uint32_t kISemaResumePc = 0x00160310u;
constexpr uint32_t kISemaDriverPc = 0x00160320u;
constexpr uint32_t kISemaHandlerPc = 0x00160330u;
constexpr uint32_t kEventWaitPc = 0x00160400u;
constexpr uint32_t kEventResumePc = 0x00160410u;
constexpr uint32_t kEventProducerPc = 0x00160420u;
constexpr uint32_t kTimer2WaitPc = 0x00160500u;
constexpr uint32_t kTimer2ResumePc = 0x00160510u;
constexpr uint32_t kTimer2HandlerPc = 0x00160520u;
constexpr uint32_t kTimer2Count = 0x10001000u;
constexpr uint32_t kTimer2Mode = 0x10001010u;
constexpr uint32_t kTimer2Compare = 0x10001020u;
constexpr uint32_t kTimerModeBusClockDiv256 = 2u;
constexpr uint32_t kTimerModeCue = 1u << 7u;
constexpr uint32_t kTimerModeCmpe = 1u << 8u;
constexpr uint32_t kTimerModeEquf = 1u << 10u;
constexpr uint32_t kVSyncFlagAddr = 0x1800u;
constexpr uint32_t kVSyncTickAddr = 0x1810u;
constexpr uint32_t kEventResultAddr = 0x1820u;
std::vector<int> g_dispatchTrace;
int g_testSemaphoreId = 0;
int g_testEventFlagId = 0;
int32_t g_resumedResult = 0;
uint32_t g_vsyncFlag = 0;
uint64_t g_vsyncTick = 0;
uint64_t g_vsyncCsr = 0;
std::atomic<bool> g_timer2Resumed{false};
void setRegU32(R5900Context &ctx, int reg, uint32_t value)
{
ctx.r[reg] = _mm_set_epi64x(0, static_cast<int64_t>(value));
}
int32_t getRegS32(const R5900Context &ctx, int reg)
{
return static_cast<int32_t>(::getRegU32(&ctx, reg));
}
bool callSyscall(uint32_t syscallNumber, uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
return dispatchNumericSyscall(syscallNumber, rdram, ctx, runtime);
}
void writeGuestU32(uint8_t *rdram, uint32_t addr, uint32_t value)
{
std::memcpy(rdram + addr, &value, sizeof(value));
}
uint32_t readGuestU32(const uint8_t *rdram, uint32_t addr)
{
uint32_t value = 0;
std::memcpy(&value, rdram + addr, sizeof(value));
return value;
}
uint64_t readGuestU64(const uint8_t *rdram, uint32_t addr)
{
uint64_t value = 0;
std::memcpy(&value, rdram + addr, sizeof(value));
return value;
}
template <typename Predicate>
bool waitUntil(Predicate pred, std::chrono::milliseconds timeout)
{
const auto deadline = std::chrono::steady_clock::now() + timeout;
while (std::chrono::steady_clock::now() < deadline)
{
if (pred())
{
return true;
}
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
return pred();
}
void cleanupRuntime(TestEnv &env)
{
env.runtime.requestStop();
}
void idleVSyncWait(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
WaitVSyncTick(rdram, ctx, runtime, -1);
}
void schedulerVSyncWait(uint8_t *, R5900Context *ctx, PS2Runtime *runtime)
{
EeScheduler &scheduler = runtime->eeScheduler();
scheduler.setVSyncFlag(kVSyncFlagAddr, kVSyncTickAddr);
ctx->pc = kVSyncResumePc;
scheduler.waitVSync(scheduler.currentVSyncTick());
}
void schedulerVSyncResume(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
g_vsyncFlag = readGuestU32(rdram, kVSyncFlagAddr);
g_vsyncTick = readGuestU64(rdram, kVSyncTickAddr);
g_vsyncCsr = runtime->memory().gs().csr.load(std::memory_order_acquire);
g_resumedResult = getRegS32(*ctx, 2);
ctx->pc = 0u;
runtime->requestStop();
}
void schedulerIntcHandler(uint8_t *, R5900Context *ctx, PS2Runtime *)
{
g_dispatchTrace.push_back(2);
g_lastIntcArg.store(getRegU32(ctx, 5), std::memory_order_relaxed);
ctx->pc = 0u;
}
void schedulerIrqWait(uint8_t *, R5900Context *ctx, PS2Runtime *runtime)
{
g_dispatchTrace.push_back(1);
EeScheduler &scheduler = runtime->eeScheduler();
scheduler.addIrqHandler(false, 2u, kIntcHandlerPc, true, 0xCAFEu, 0u, 0u);
ctx->pc = kIrqResumePc;
scheduler.waitVSync(scheduler.currentVSyncTick());
}
void schedulerIrqResume(uint8_t *, R5900Context *ctx, PS2Runtime *runtime)
{
g_dispatchTrace.push_back(3);
ctx->pc = 0u;
runtime->requestStop();
}
void schedulerISemaHandler(uint8_t *, R5900Context *ctx, PS2Runtime *runtime)
{
g_dispatchTrace.push_back(3);
runtime->eeScheduler().signalSemaphore(g_testSemaphoreId, true);
g_dispatchTrace.push_back(4);
ctx->pc = 0u;
}
void schedulerISemaDriver(uint8_t *, R5900Context *ctx, PS2Runtime *runtime)
{
g_dispatchTrace.push_back(2);
ctx->pc = 0u;
runtime->eeScheduler().dispatchIrq(true, 5u);
}
void schedulerISemaWait(uint8_t *, R5900Context *ctx, PS2Runtime *runtime)
{
g_dispatchTrace.push_back(1);
EeScheduler &scheduler = runtime->eeScheduler();
g_testSemaphoreId = scheduler.createSemaphore(0, 1, 0u, 0u);
scheduler.addIrqHandler(true, 5u, kISemaHandlerPc, true, 0u, 0u, 0u);
EeThreadCreateParams driver{};
driver.entry = kISemaDriverPc;
driver.stack = 0x1C000u;
driver.stackSize = 0x1000u;
driver.priority = 10;
const int driverId = scheduler.createThread(driver);
scheduler.startThread(driverId, 0u, *ctx, false);
ctx->pc = kISemaResumePc;
scheduler.waitSemaphore(g_testSemaphoreId);
}
void schedulerISemaResume(uint8_t *, R5900Context *ctx, PS2Runtime *runtime)
{
g_dispatchTrace.push_back(5);
g_resumedResult = getRegS32(*ctx, 2);
ctx->pc = 0u;
runtime->requestStop();
}
void schedulerEventProducer(uint8_t *, R5900Context *ctx, PS2Runtime *runtime)
{
g_dispatchTrace.push_back(2);
ctx->pc = 0u;
runtime->eeScheduler().setEventFlag(g_testEventFlagId, 0x6u, false);
runtime->eeScheduler().transferIfRequested(false);
}
void schedulerEventWait(uint8_t *, R5900Context *ctx, PS2Runtime *runtime)
{
g_dispatchTrace.push_back(1);
EeScheduler &scheduler = runtime->eeScheduler();
g_testEventFlagId = scheduler.createEventFlag(0u, 0u, 0u);
EeThreadCreateParams producer{};
producer.entry = kEventProducerPc;
producer.stack = 0x1D000u;
producer.stackSize = 0x1000u;
producer.priority = 10;
const int producerId = scheduler.createThread(producer);
scheduler.startThread(producerId, 0u, *ctx, false);
ctx->pc = kEventResumePc;
scheduler.waitEventFlag(g_testEventFlagId, 0x2u, WEF_OR | WEF_CLEAR, kEventResultAddr);
}
void schedulerEventResume(uint8_t *, R5900Context *ctx, PS2Runtime *runtime)
{
g_dispatchTrace.push_back(3);
g_resumedResult = getRegS32(*ctx, 2);
ctx->pc = 0u;
runtime->requestStop();
}
void schedulerTimer2Handler(uint8_t *, R5900Context *ctx, PS2Runtime *runtime)
{
g_dispatchTrace.push_back(2);
PS2Memory &memory = runtime->memory();
memory.writeIORegister(kTimer2Mode, memory.readIORegister(kTimer2Mode) | kTimerModeEquf);
runtime->eeScheduler().signalSemaphore(g_testSemaphoreId, true);
ctx->pc = 0u;
}
void schedulerTimer2Wait(uint8_t *, R5900Context *ctx, PS2Runtime *runtime)
{
g_dispatchTrace.push_back(1);
EeScheduler &scheduler = runtime->eeScheduler();
g_testSemaphoreId = scheduler.createSemaphore(0, 1, 0u, 0u);
scheduler.addIrqHandler(false, 11u, kTimer2HandlerPc, true, 0u, 0u, 0u);
PS2Memory &memory = runtime->memory();
memory.writeIORegister(kTimer2Count, 0u);
memory.writeIORegister(kTimer2Compare, 8u);
memory.writeIORegister(kTimer2Mode,
kTimerModeBusClockDiv256 | kTimerModeCue | kTimerModeCmpe | kTimerModeEquf);
ctx->pc = kTimer2ResumePc;
scheduler.waitSemaphore(g_testSemaphoreId);
}
void schedulerTimer2Resume(uint8_t *, R5900Context *ctx, PS2Runtime *runtime)
{
g_dispatchTrace.push_back(3);
g_resumedResult = getRegS32(*ctx, 2);
g_timer2Resumed.store(true, std::memory_order_release);
ctx->pc = 0u;
runtime->requestStop();
}
}
void register_ps2_runtime_interrupt_tests()
{
MiniTest::Case("PS2RuntimeInterrupt", [](TestCase &tc)
{
tc.Run("negative interrupt-safe EE syscall ids dispatch", [](TestCase &t)
{
TestEnv env;
constexpr uint32_t kEventParamAddr = 0x1200u;
constexpr uint32_t kStatusAddr = 0x1210u;
const uint32_t eventParam[3] = {
0u,
0u,
0u
};
std::memcpy(env.rdram.data() + kEventParamAddr, eventParam, sizeof(eventParam));
R5900Context createCtx{};
setRegU32(createCtx, 4, kEventParamAddr);
CreateEventFlag(env.rdram.data(), &createCtx, &env.runtime);
const int32_t eid = getRegS32(createCtx, 2);
t.IsTrue(eid > 0, "CreateEventFlag should return a valid event id");
R5900Context disableIntcCtx{};
setRegU32(disableIntcCtx, 4, 2u);
t.IsTrue(callSyscall(static_cast<uint32_t>(-0x1B), env.rdram.data(), &disableIntcCtx, &env.runtime),
"negative iDisableIntc syscall id should dispatch");
t.Equals(getRegS32(disableIntcCtx, 2), KE_OK, "negative iDisableIntc should return KE_OK");
R5900Context enableIntcCtx{};
setRegU32(enableIntcCtx, 4, 2u);
t.IsTrue(callSyscall(static_cast<uint32_t>(-0x1A), env.rdram.data(), &enableIntcCtx, &env.runtime),
"negative iEnableIntc syscall id should dispatch");
t.Equals(getRegS32(enableIntcCtx, 2), KE_OK, "negative iEnableIntc should return KE_OK");
R5900Context disableDmacCtx{};
setRegU32(disableDmacCtx, 4, 5u);
t.IsTrue(callSyscall(static_cast<uint32_t>(-0x1D), env.rdram.data(), &disableDmacCtx, &env.runtime),
"negative iDisableDmac syscall id should dispatch");
t.Equals(getRegS32(disableDmacCtx, 2), KE_OK, "negative iDisableDmac should return KE_OK");
R5900Context enableDmacCtx{};
setRegU32(enableDmacCtx, 4, 5u);
t.IsTrue(callSyscall(static_cast<uint32_t>(-0x1C), env.rdram.data(), &enableDmacCtx, &env.runtime),
"negative iEnableDmac syscall id should dispatch");
t.Equals(getRegS32(enableDmacCtx, 2), KE_OK, "negative iEnableDmac should return KE_OK");
R5900Context setEventFlagCtx{};
setRegU32(setEventFlagCtx, 4, static_cast<uint32_t>(eid));
setRegU32(setEventFlagCtx, 5, 0x6u);
t.IsTrue(callSyscall(static_cast<uint32_t>(-0x53), env.rdram.data(), &setEventFlagCtx, &env.runtime),
"negative iSetEventFlag syscall id should dispatch");
t.Equals(getRegS32(setEventFlagCtx, 2), KE_OK, "negative iSetEventFlag should return KE_OK");
R5900Context referCtx{};
setRegU32(referCtx, 4, static_cast<uint32_t>(eid));
setRegU32(referCtx, 5, kStatusAddr);
ReferEventFlagStatus(env.rdram.data(), &referCtx, &env.runtime);
t.Equals(getRegS32(referCtx, 2), KE_OK, "ReferEventFlagStatus should succeed after iSetEventFlag");
t.Equals(readGuestU32(env.rdram.data(), kStatusAddr + 12u), 0x6u,
"negative iSetEventFlag should publish the requested bits");
R5900Context deleteCtx{};
setRegU32(deleteCtx, 4, static_cast<uint32_t>(eid));
DeleteEventFlag(env.rdram.data(), &deleteCtx, &env.runtime);
cleanupRuntime(env);
});
tc.Run("PollEventFlag WEF_CLEAR clears only matched bits", [](TestCase &t)
{
TestEnv env;
constexpr uint32_t kParamAddr = 0x1400u;
constexpr uint32_t kResBitsAddr = 0x1410u;
constexpr uint32_t kStatusAddr = 0x1420u;
const uint32_t eventParam[3] = {
0u, // attr
0u, // option
0x7u // init bits: 0b111
};
std::memcpy(env.rdram.data() + kParamAddr, eventParam, sizeof(eventParam));
R5900Context createCtx{};
setRegU32(createCtx, 4, kParamAddr);
CreateEventFlag(env.rdram.data(), &createCtx, &env.runtime);
const int32_t eid = getRegS32(createCtx, 2);
t.IsTrue(eid > 0, "CreateEventFlag should return a valid id");
R5900Context pollCtx{};
setRegU32(pollCtx, 4, static_cast<uint32_t>(eid));
setRegU32(pollCtx, 5, 0x1u);
setRegU32(pollCtx, 6, WEF_OR | WEF_CLEAR);
setRegU32(pollCtx, 7, kResBitsAddr);
PollEventFlag(env.rdram.data(), &pollCtx, &env.runtime);
t.Equals(getRegS32(pollCtx, 2), KE_OK, "PollEventFlag should succeed when condition is met");
t.Equals(readGuestU32(env.rdram.data(), kResBitsAddr), 0x7u, "PollEventFlag should report bits before clear");
R5900Context referCtx{};
setRegU32(referCtx, 4, static_cast<uint32_t>(eid));
setRegU32(referCtx, 5, kStatusAddr);
ReferEventFlagStatus(env.rdram.data(), &referCtx, &env.runtime);
t.Equals(getRegS32(referCtx, 2), KE_OK, "ReferEventFlagStatus should succeed");
Ps2EventFlagInfo info{};
std::memcpy(&info, env.rdram.data() + kStatusAddr, sizeof(info));
t.Equals(info.currBits, 0x6u, "WEF_CLEAR should clear only requested bits, not all bits");
R5900Context pollMissCtx{};
setRegU32(pollMissCtx, 4, static_cast<uint32_t>(eid));
setRegU32(pollMissCtx, 5, 0x1u);
setRegU32(pollMissCtx, 6, WEF_OR);
setRegU32(pollMissCtx, 7, 0u);
PollEventFlag(env.rdram.data(), &pollMissCtx, &env.runtime);
t.Equals(getRegS32(pollMissCtx, 2), KE_EVF_COND,
"after clearing bit 0, polling for bit 0 should fail condition");
R5900Context deleteCtx{};
setRegU32(deleteCtx, 4, static_cast<uint32_t>(eid));
DeleteEventFlag(env.rdram.data(), &deleteCtx, &env.runtime);
t.Equals(getRegS32(deleteCtx, 2), KE_OK, "DeleteEventFlag should succeed");
cleanupRuntime(env);
});
tc.Run("VBlank deadline resumes the waiter and publishes flag tick and FIELD atomically", [](TestCase &t)
{
TestEnv env;
t.IsTrue(env.runtime.memory().initialize(), "runtime memory initialize should succeed");
env.runtime.registerFunction(kVSyncWaitPc, schedulerVSyncWait);
env.runtime.registerFunction(kVSyncResumePc, schedulerVSyncResume);
g_resumedResult = -1;
g_vsyncFlag = 0u;
g_vsyncTick = 0u;
g_vsyncCsr = 0u;
R5900Context mainContext{};
mainContext.pc = kVSyncWaitPc;
env.runtime.eeScheduler().reset(env.rdram.data(), mainContext);
env.runtime.eeScheduler().run();
t.Equals(g_vsyncFlag, 1u, "VBlank start should set the registered guest flag");
t.Equals(g_vsyncTick, 1ull, "the first centralized VBlank deadline should publish tick one");
t.Equals(g_resumedResult, 0, "the first VBlank field should return even-field parity");
t.Equals(g_vsyncCsr & 0x2000ull, 0x2000ull,
"the first VBlank should publish GS CSR.FIELD before resuming guest code");
});
tc.Run("VBlank IRQ invocation completes before the resumed base context", [](TestCase &t)
{
TestEnv env;
env.runtime.registerFunction(kIrqWaitPc, schedulerIrqWait);
env.runtime.registerFunction(kIrqResumePc, schedulerIrqResume);
env.runtime.registerFunction(kIntcHandlerPc, schedulerIntcHandler);
g_dispatchTrace.clear();
g_lastIntcArg.store(0u, std::memory_order_relaxed);
R5900Context mainContext{};
mainContext.pc = kIrqWaitPc;
env.runtime.eeScheduler().reset(env.rdram.data(), mainContext);
env.runtime.eeScheduler().run();
const std::vector<int> expected{1, 2, 3};
t.IsTrue(g_dispatchTrace == expected,
"the dispatcher should run wait, IRQ frame, then the resumed base context in exact order");
t.Equals(g_lastIntcArg.load(std::memory_order_relaxed), 0xCAFEu,
"the IRQ frame should receive its registered argument");
});
tc.Run("iSignalSema defers selection until IRQ return", [](TestCase &t)
{
TestEnv env;
env.runtime.registerFunction(kISemaWaitPc, schedulerISemaWait);
env.runtime.registerFunction(kISemaResumePc, schedulerISemaResume);
env.runtime.registerFunction(kISemaDriverPc, schedulerISemaDriver);
env.runtime.registerFunction(kISemaHandlerPc, schedulerISemaHandler);
g_dispatchTrace.clear();
g_resumedResult = -1;
R5900Context mainContext{};
mainContext.pc = kISemaWaitPc;
env.runtime.eeScheduler().reset(env.rdram.data(), mainContext);
env.runtime.eeScheduler().run();
const std::vector<int> expected{1, 2, 3, 4, 5};
t.IsTrue(g_dispatchTrace == expected,
"iSignalSema should make the waiter ready but finish the IRQ frame before selecting it");
t.Equals(g_resumedResult, g_testSemaphoreId,
"the resumed waiter should receive the semaphore id from the direct FIFO handoff");
const EeSemaphore *semaphore = env.runtime.eeScheduler().semaphore(g_testSemaphoreId);
t.IsTrue(semaphore != nullptr, "the signaled semaphore should still exist");
if (semaphore)
{
t.Equals(semaphore->count, 0, "direct handoff must not increment the semaphore count");
t.Equals(static_cast<uint32_t>(semaphore->waiters.size()), 0u,
"the awakened waiter must be removed from the semaphore queue");
}
});
tc.Run("event-flag completion writes observed bits before strict-priority resume", [](TestCase &t)
{
TestEnv env;
env.runtime.registerFunction(kEventWaitPc, schedulerEventWait);
env.runtime.registerFunction(kEventResumePc, schedulerEventResume);
env.runtime.registerFunction(kEventProducerPc, schedulerEventProducer);
g_dispatchTrace.clear();
g_resumedResult = -1;
R5900Context mainContext{};
mainContext.pc = kEventWaitPc;
env.runtime.eeScheduler().reset(env.rdram.data(), mainContext);
env.runtime.eeScheduler().run();
const std::vector<int> expected{1, 2, 3};
t.IsTrue(g_dispatchTrace == expected,
"the higher-priority event waiter should resume at the producer scheduling point");
t.Equals(g_resumedResult, KE_OK, "the resumed event waiter should receive KE_OK");
t.Equals(readGuestU32(env.rdram.data(), kEventResultAddr), 0x6u,
"the event output should contain the bits observed before clear mode is applied");
const EeEventFlag *flag = env.runtime.eeScheduler().eventFlag(g_testEventFlagId);
t.IsTrue(flag != nullptr, "the event flag should still exist");
if (flag)
{
t.Equals(flag->bits, 0x4u, "WEF_CLEAR should remove only the requested matched bit");
}
});
tc.Run("EE Timer2 compare IRQ wakes a DelayThread-style semaphore wait", [](TestCase &t)
{
TestEnv env;
t.IsTrue(env.runtime.memory().initialize(), "runtime memory initialize should succeed");
env.runtime.registerFunction(kTimer2WaitPc, schedulerTimer2Wait);
env.runtime.registerFunction(kTimer2ResumePc, schedulerTimer2Resume);
env.runtime.registerFunction(kTimer2HandlerPc, schedulerTimer2Handler);
g_dispatchTrace.clear();
g_resumedResult = -1;
g_timer2Resumed.store(false, std::memory_order_release);
R5900Context mainContext{};
mainContext.pc = kTimer2WaitPc;
std::atomic<bool> schedulerThrew{false};
std::thread gameThread([&]()
{
try
{
env.runtime.eeScheduler().reset(env.rdram.data(), mainContext);
env.runtime.eeScheduler().run();
}
catch (...)
{
schedulerThrew.store(true, std::memory_order_release);
}
});
const bool resumed = waitUntil([]()
{
return g_timer2Resumed.load(std::memory_order_acquire);
}, std::chrono::milliseconds(150));
if (!resumed)
{
env.runtime.requestStop();
}
gameThread.join();
t.IsTrue(resumed, "Timer2 compare should dispatch INTC_TIM2 and wake the semaphore waiter");
t.IsFalse(schedulerThrew.load(std::memory_order_acquire), "Timer2 IRQ path should not throw");
const std::vector<int> expected{1, 2, 3};
t.IsTrue(g_dispatchTrace == expected,
"Timer2 flow should run wait, interrupt handler, then the resumed thread");
t.Equals(g_resumedResult, g_testSemaphoreId,
"the Timer2 handler should hand the semaphore directly to the waiter");
});
tc.Run("scheduler stop wakes an idle VSync wait without a timeout", [](TestCase &t)
{
TestEnv env;
env.runtime.registerFunction(kIdleVSyncWaitPc, idleVSyncWait);
R5900Context mainContext{};
mainContext.pc = kIdleVSyncWaitPc;
std::atomic<bool> schedulerDone{false};
std::atomic<bool> schedulerThrew{false};
std::thread gameThread([&]()
{
try
{
env.runtime.eeScheduler().reset(env.rdram.data(), mainContext);
env.runtime.eeScheduler().run();
}
catch (...)
{
schedulerThrew.store(true, std::memory_order_release);
}
schedulerDone.store(true, std::memory_order_release);
});
const bool becameIdle = waitUntil([&]() {
const EeKernelSnapshot snapshot = env.runtime.eeScheduler().snapshot();
return snapshot.runningThreadId == 0 &&
!snapshot.threads.empty() &&
snapshot.threads.front().waitReason == EeWaitReason::VSync;
}, std::chrono::milliseconds(80));
env.runtime.requestStop();
gameThread.join();
t.IsTrue(becameIdle, "VSync wait should leave the sole guest thread waiting");
t.IsTrue(schedulerDone.load(std::memory_order_acquire),
"requestStop should wake the scheduler's event wait");
t.IsFalse(schedulerThrew.load(std::memory_order_acquire),
"the scheduler stop path should not throw");
cleanupRuntime(env);
});
});
}