mirror of
https://github.com/ran-j/PS2Recomp.git
synced 2026-10-02 02:31:51 -04:00
Feature/random fixes platform support (#179)
* feat: implement memory card IOP * feat: IOP trace * feat: move IOP logic to ps2xIOP refactor: small refactor on audio api on runtime * feat: android support feat: prevent race on GS feat: a bit cleanup and reimplement on memory card * feat: finish guest thread feat: android build support feat: vita build support with suspicious setup scripts * feat: remove idea from track
This commit is contained in:
@@ -238,7 +238,7 @@ namespace
|
||||
|
||||
constexpr uint32_t kAsyncCounterAddr = 0x2400u;
|
||||
|
||||
void testWaitForAsyncCounter(uint8_t *rdram, R5900Context *ctx, PS2Runtime *)
|
||||
void testWaitForAsyncCounter(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
if (!rdram || !ctx)
|
||||
{
|
||||
@@ -251,6 +251,10 @@ namespace
|
||||
std::memcpy(&counter, rdram + kAsyncCounterAddr, sizeof(counter));
|
||||
if (counter == 0u)
|
||||
{
|
||||
if (runtime != nullptr)
|
||||
{
|
||||
runtime->yieldGuestExecutionAfterWake();
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
} while (counter == 0u);
|
||||
@@ -437,6 +441,136 @@ void register_ps2_runtime_expansion_tests()
|
||||
t.IsTrue(peerRanAfterHandoff, "wake handoff should let the peer acquire guest execution before returning");
|
||||
});
|
||||
|
||||
tc.Run("recursive guest execution acquisition does not advance the handoff epoch", [](TestCase &t)
|
||||
{
|
||||
PS2Runtime runtime;
|
||||
const uint64_t initial = runtime.guestExecutionHandoffEpochSnapshot();
|
||||
|
||||
PS2Runtime::GuestExecutionScope outer(&runtime);
|
||||
const uint64_t afterOuter = runtime.guestExecutionHandoffEpochSnapshot();
|
||||
t.Equals(afterOuter, initial + 1u, "outer acquisition should advance the epoch exactly once");
|
||||
|
||||
{
|
||||
PS2Runtime::GuestExecutionScope inner(&runtime);
|
||||
t.Equals(runtime.guestExecutionHandoffEpochSnapshot(), afterOuter,
|
||||
"recursive acquisition must not advance the epoch");
|
||||
|
||||
PS2Runtime::GuestExecutionScope innermost(&runtime);
|
||||
t.Equals(runtime.guestExecutionHandoffEpochSnapshot(), afterOuter,
|
||||
"deeper recursive acquisitions must not advance the epoch either");
|
||||
}
|
||||
|
||||
t.Equals(runtime.guestExecutionHandoffEpochSnapshot(), afterOuter,
|
||||
"releasing recursive acquisitions must not advance the epoch");
|
||||
});
|
||||
|
||||
tc.Run("reacquiring a depth-4 release advances the handoff epoch exactly once", [](TestCase &t)
|
||||
{
|
||||
PS2Runtime runtime;
|
||||
|
||||
PS2Runtime::GuestExecutionScope s1(&runtime);
|
||||
PS2Runtime::GuestExecutionScope s2(&runtime);
|
||||
PS2Runtime::GuestExecutionScope s3(&runtime);
|
||||
PS2Runtime::GuestExecutionScope s4(&runtime);
|
||||
|
||||
const uint64_t before = runtime.guestExecutionHandoffEpochSnapshot();
|
||||
{
|
||||
PS2Runtime::GuestExecutionReleaseScope release(&runtime);
|
||||
t.Equals(runtime.guestExecutionHandoffEpochSnapshot(), before,
|
||||
"releasing guest execution must not advance the epoch");
|
||||
}
|
||||
|
||||
t.Equals(runtime.guestExecutionHandoffEpochSnapshot(), before + 1u,
|
||||
"reacquiring a depth-4 release should advance the epoch exactly once");
|
||||
});
|
||||
|
||||
tc.Run("handoff completed before the wait does not count as a timeout", [](TestCase &t)
|
||||
{
|
||||
PS2Runtime runtime;
|
||||
const uint64_t timeoutsBefore = runtime.guestExecutionHandoffTimeouts();
|
||||
|
||||
std::atomic<bool> holderAcquired{false};
|
||||
std::atomic<bool> releaseHolder{false};
|
||||
uint64_t baseline = 0u;
|
||||
std::thread holder;
|
||||
|
||||
{
|
||||
PS2Runtime::GuestExecutionScope mainScope(&runtime);
|
||||
|
||||
holder = std::thread([&]()
|
||||
{
|
||||
PS2Runtime::GuestExecutionScope scope(&runtime);
|
||||
holderAcquired.store(true, std::memory_order_release);
|
||||
while (!releaseHolder.load(std::memory_order_acquire))
|
||||
{
|
||||
std::this_thread::sleep_for(std::chrono::microseconds(100));
|
||||
}
|
||||
});
|
||||
|
||||
const bool holderContending = waitUntil([&]()
|
||||
{
|
||||
return runtime.guestExecutionWaiterCountForTesting() > 0u;
|
||||
}, std::chrono::milliseconds(250));
|
||||
t.IsTrue(holderContending, "holder thread should be queued before the release");
|
||||
|
||||
// Token captured BEFORE releasing guest execution, like the dispatchers do
|
||||
baseline = runtime.guestExecutionHandoffEpochSnapshot();
|
||||
}
|
||||
|
||||
const bool acquired = waitUntil([&]()
|
||||
{
|
||||
return holderAcquired.load(std::memory_order_acquire);
|
||||
}, std::chrono::milliseconds(250));
|
||||
t.IsTrue(acquired, "holder should acquire guest execution after the release");
|
||||
|
||||
// Second waiter keeps waiters > 0 so the wait below cannot take the
|
||||
// no-waiters fast path: it must recognize the epoch advance instead.
|
||||
std::thread secondWaiter([&]()
|
||||
{
|
||||
PS2Runtime::GuestExecutionScope scope(&runtime);
|
||||
});
|
||||
const bool secondContending = waitUntil([&]()
|
||||
{
|
||||
return runtime.guestExecutionWaiterCountForTesting() > 0u;
|
||||
}, std::chrono::milliseconds(250));
|
||||
t.IsTrue(secondContending, "second waiter should be queued while the holder owns guest execution");
|
||||
|
||||
runtime.waitForGuestExecutionHandoff(baseline);
|
||||
|
||||
t.Equals(runtime.guestExecutionHandoffTimeouts(), timeoutsBefore,
|
||||
"a handoff that completed before the wait must not count as a timeout");
|
||||
|
||||
releaseHolder.store(true, std::memory_order_release);
|
||||
if (holder.joinable())
|
||||
{
|
||||
holder.join();
|
||||
}
|
||||
if (secondWaiter.joinable())
|
||||
{
|
||||
secondWaiter.join();
|
||||
}
|
||||
});
|
||||
|
||||
tc.Run("nested DeferredGuestYieldScope delivers pending only to the outermost scope", [](TestCase &t)
|
||||
{
|
||||
PS2Runtime runtime;
|
||||
bool outerPending = false;
|
||||
bool innerPending = false;
|
||||
|
||||
{
|
||||
PS2Runtime::DeferredGuestYieldScope outer(outerPending);
|
||||
{
|
||||
PS2Runtime::DeferredGuestYieldScope inner(innerPending);
|
||||
runtime.yieldGuestExecutionAfterWake(); // must defer instead of yielding
|
||||
}
|
||||
t.IsFalse(innerPending, "inner scope must not consume the deferred yield");
|
||||
t.IsFalse(outerPending, "pending must only be delivered when the outermost scope closes");
|
||||
}
|
||||
|
||||
t.IsTrue(outerPending, "outermost scope should deliver the deferred yield");
|
||||
t.IsFalse(innerPending, "inner scope must stay untouched");
|
||||
});
|
||||
|
||||
tc.Run("guest preemption policy requests a dispatcher handoff when another guest thread contends", [](TestCase &t)
|
||||
{
|
||||
PS2Runtime runtime;
|
||||
|
||||
@@ -183,7 +183,7 @@ void register_ps2_runtime_interrupt_tests()
|
||||
{
|
||||
MiniTest::Case("PS2RuntimeInterrupt", [](TestCase &tc)
|
||||
{
|
||||
tc.Run("SetVSyncFlag updates guest flag and monotonic tick", [](TestCase &t)
|
||||
tc.Run("SetVSyncFlag arms a one-shot vblank notification", [](TestCase &t)
|
||||
{
|
||||
notifyRuntimeStop();
|
||||
TestEnv env;
|
||||
@@ -211,12 +211,23 @@ void register_ps2_runtime_interrupt_tests()
|
||||
const uint64_t firstTick = readGuestU64(env.rdram.data(), kTickAddr);
|
||||
t.IsTrue(firstTick > 0u, "First observed VSync tick should be positive");
|
||||
t.Equals(readGuestU32(env.rdram.data(), kFlagAddr), 1u, "VSync worker should set flag to one");
|
||||
|
||||
const bool tickRewritten = waitUntil([&]() {
|
||||
return readGuestU64(env.rdram.data(), kTickAddr) != firstTick;
|
||||
}, std::chrono::milliseconds(100));
|
||||
t.IsTrue(!tickRewritten, "consumed registration should not be written again");
|
||||
|
||||
const bool secondTickSeen = waitUntil([&]() {
|
||||
// Re-arming registers a fresh one-shot notification.
|
||||
writeGuestU32(env.rdram.data(), kFlagAddr, 0u);
|
||||
R5900Context rearmCtx{};
|
||||
setRegU32(rearmCtx, 4, kFlagAddr);
|
||||
setRegU32(rearmCtx, 5, kTickAddr);
|
||||
t.IsTrue(callSyscall(0x73u, env.rdram.data(), &rearmCtx, &env.runtime), "SetVSyncFlag re-arm should dispatch");
|
||||
const bool rearmedTickSeen = waitUntil([&]() {
|
||||
return readGuestU64(env.rdram.data(), kTickAddr) > firstTick;
|
||||
}, std::chrono::milliseconds(300));
|
||||
t.IsTrue(secondTickSeen, "VSync tick should continue to advance");
|
||||
t.IsTrue(readGuestU64(env.rdram.data(), kTickAddr) > firstTick, "tick should be monotonic");
|
||||
t.IsTrue(rearmedTickSeen, "re-armed registration should observe a later tick");
|
||||
t.Equals(readGuestU32(env.rdram.data(), kFlagAddr), 1u, "re-armed registration should set flag to one");
|
||||
|
||||
cleanupRuntime(env);
|
||||
});
|
||||
|
||||
@@ -89,12 +89,6 @@ namespace
|
||||
std::memset(&ctx, 0, sizeof(ctx));
|
||||
}
|
||||
|
||||
void writeStackArg(std::vector<uint8_t> &rdram, R5900Context &ctx, uint32_t slotIndex, uint32_t value)
|
||||
{
|
||||
const uint32_t sp = ::getRegU32(&ctx, 29);
|
||||
writeGuestU32(rdram.data(), sp + 16u + slotIndex * sizeof(uint32_t), value);
|
||||
}
|
||||
|
||||
int32_t syncMc(std::vector<uint8_t> &rdram, int32_t *cmdOut = nullptr)
|
||||
{
|
||||
R5900Context syncCtx{};
|
||||
@@ -446,9 +440,9 @@ void register_ps2_runtime_io_tests()
|
||||
setRegU32(test.ctx, 5, 0u);
|
||||
setRegU32(test.ctx, 6, patternAddr);
|
||||
setRegU32(test.ctx, 7, 0u);
|
||||
setRegU32(test.ctx, 29, GUEST_STACK_AREA_START);
|
||||
writeStackArg(test.rdram, test.ctx, 0u, 8u);
|
||||
writeStackArg(test.rdram, test.ctx, 1u, GUEST_MC_TABLE_ADDR);
|
||||
// EE n32 ABI: arguments 5 and 6 travel in $t0/$t1
|
||||
setRegU32(test.ctx, 8, 8u);
|
||||
setRegU32(test.ctx, 9, GUEST_MC_TABLE_ADDR);
|
||||
|
||||
ps2_stubs::sceMcGetDir(test.rdram.data(), &test.ctx, nullptr);
|
||||
|
||||
@@ -480,8 +474,8 @@ void register_ps2_runtime_io_tests()
|
||||
setRegU32(test.ctx, 5, 0u);
|
||||
setRegU32(test.ctx, 6, typeAddr);
|
||||
setRegU32(test.ctx, 7, freeAddr);
|
||||
setRegU32(test.ctx, 29, GUEST_STACK_AREA_START);
|
||||
writeStackArg(test.rdram, test.ctx, 0u, formatAddr);
|
||||
// EE n32 ABI: the fifth argument travels in $t0
|
||||
setRegU32(test.ctx, 8, formatAddr);
|
||||
ps2_stubs::sceMcGetInfo(test.rdram.data(), &test.ctx, nullptr);
|
||||
|
||||
int32_t cmd = 0;
|
||||
@@ -501,8 +495,7 @@ void register_ps2_runtime_io_tests()
|
||||
setRegU32(test.ctx, 5, 0u);
|
||||
setRegU32(test.ctx, 6, typeAddr);
|
||||
setRegU32(test.ctx, 7, freeAddr);
|
||||
setRegU32(test.ctx, 29, GUEST_STACK_AREA_START);
|
||||
writeStackArg(test.rdram, test.ctx, 0u, formatAddr);
|
||||
setRegU32(test.ctx, 8, formatAddr);
|
||||
ps2_stubs::sceMcGetInfo(test.rdram.data(), &test.ctx, nullptr);
|
||||
|
||||
t.Equals(syncMc(test.rdram, &cmd), -2, "unformatted cards should report sceMcResNoFormat through sceMcSync");
|
||||
@@ -533,8 +526,15 @@ void register_ps2_runtime_io_tests()
|
||||
ps2_stubs::sceMcEnd(test.rdram.data(), &test.ctx, nullptr);
|
||||
t.Equals(getRegS32(&test.ctx, 2), 0, "sceMcEnd should succeed");
|
||||
|
||||
t.Equals(syncMc(test.rdram, &cmd), 0, "sceMcSync should report cleared result after sceMcEnd");
|
||||
t.Equals(cmd, 0, "sceMcEnd should clear the last active libmc command");
|
||||
// libmc semantics: with no async command pending, sceMcSync returns -1
|
||||
// and leaves the cmd/result out-parameters untouched.
|
||||
R5900Context syncCtx{};
|
||||
setRegU32(syncCtx, 4, 0u);
|
||||
setRegU32(syncCtx, 5, GUEST_MC_SYNC_CMD_ADDR);
|
||||
setRegU32(syncCtx, 6, GUEST_MC_SYNC_RESULT_ADDR);
|
||||
ps2_stubs::sceMcSync(test.rdram.data(), &syncCtx, nullptr);
|
||||
t.Equals(getRegS32(&syncCtx, 2), -1,
|
||||
"sceMcSync after sceMcEnd should report that no command is active");
|
||||
});
|
||||
|
||||
tc.Run("sceIoctl cmd1 updates wait flag state", [](TestCase &t)
|
||||
|
||||
Reference in New Issue
Block a user