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:
Ranieri
2026-07-22 02:37:53 -03:00
committed by GitHub
parent 1176609890
commit f3687c5ae6
40 changed files with 1133 additions and 146 deletions
+135 -1
View File
@@ -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;
+15 -4
View File
@@ -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);
});
+15 -15
View File
@@ -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)