Feature/ee timers and fixes (#203)

* refactor: from guest  threads to EE scheduler

* feat: bad wip mpeg fix for code veronica

* feat: cheap copy from host
feat: small perf o vsync tick

* feat: added EE clock Hz
fix: fix MPEG out of sync with new EE refactor

* fix: fix lotr tests

* 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

* feat: revert wrong changes
This commit is contained in:
Ranieri
2026-08-12 12:11:06 -03:00
committed by GitHub
parent f4309cd18c
commit 8d7e8a5a46
18 changed files with 893 additions and 141 deletions
+3 -6
View File
@@ -1181,13 +1181,10 @@ namespace ps2x::iop::detail
continue;
}
appendToSjrmtData(sjrmt->second,
chunkDataAddress,
chunkLength);
appendToSjrmtData(sjrmt->second, chunkDataAddress, chunkLength);
(void)writeGuestPod(m_host, commandAddress + 4u, sjx->second.eeObjectAddress);
constexpr uint8_t roomLine = 0u;
(void)m_host.writeGuest(commandAddress + 1u,
&roomLine,
sizeof(roomLine));
(void)m_host.writeGuest(commandAddress + 1u, &roomLine, sizeof(roomLine));
}
consumeActivePs2RnaStreamsLocked();
}
@@ -456,7 +456,7 @@ namespace ps2recomp
{
// Swaps halfwords 1<->3 and 5<->7 within the 128-bit register
return fmt::format("SET_GPR_VEC(ctx, {}, _mm_shufflelo_epi16(_mm_shufflehi_epi16(GPR_VEC(ctx, {}), _MM_SHUFFLE(2,3,0,1)), _MM_SHUFFLE(2,3,0,1)));",
inst.rd, inst.rs);
inst.rd, inst.rt);
}
@@ -465,7 +465,7 @@ namespace ps2recomp
// Reverses the order of the 8 halfwords
return fmt::format("{{ __m128i mask = _mm_setr_epi8(14,15, 12,13, 10,11, 8,9, 6,7, 4,5, 2,3, 0,1); "
"SET_GPR_VEC(ctx, {}, PS2_SHUFFLE_EPI8(GPR_VEC(ctx, {}), mask)); }}",
inst.rd, inst.rs);
inst.rd, inst.rt);
}
@@ -514,7 +514,7 @@ namespace ps2recomp
std::string CodeGenerator::translatePEXEW(const Instruction &inst)
{
return fmt::format("SET_GPR_VEC(ctx, {}, PS2_PEXEW(GPR_VEC(ctx, {})));",
inst.rd, inst.rs);
inst.rd, inst.rt);
}
@@ -522,7 +522,7 @@ namespace ps2recomp
{
// Rotates words left by 3: [d,c,b,a] -> [a,d,c,b]
return fmt::format("SET_GPR_VEC(ctx, {}, _mm_shuffle_epi32(GPR_VEC(ctx, {}), _MM_SHUFFLE(0,3,2,1)));",
inst.rd, inst.rs);
inst.rd, inst.rt);
}
@@ -562,7 +562,7 @@ namespace ps2recomp
{
// Parallel Exchange Center Halfword (same as MMI2 PEXEH)
return fmt::format("SET_GPR_VEC(ctx, {}, _mm_shufflelo_epi16(_mm_shufflehi_epi16(GPR_VEC(ctx, {}), _MM_SHUFFLE(2,3,0,1)), _MM_SHUFFLE(2,3,0,1)));",
inst.rd, inst.rs);
inst.rd, inst.rt);
}
@@ -571,7 +571,7 @@ namespace ps2recomp
// Parallel Copy Halfword (Broadcast lower 16 bits of each 64-bit half)
return fmt::format("{{ __m128i src = GPR_VEC(ctx, {}); uint16_t l = _mm_extract_epi16(src, 0); uint16_t h = _mm_extract_epi16(src, 4); \n"
" SET_GPR_VEC(ctx, {}, _mm_set_epi16(h,h,h,h, l,l,l,l)); }}",
inst.rs, inst.rd);
inst.rt, inst.rd);
}
@@ -579,7 +579,7 @@ namespace ps2recomp
{
// Parallel Exchange Center Word (Swaps words 0<>2, 1<>3)
return fmt::format("SET_GPR_VEC(ctx, {}, _mm_shuffle_epi32(GPR_VEC(ctx, {}), _MM_SHUFFLE(1,0,3,2)));",
inst.rd, inst.rs);
inst.rd, inst.rt);
}
@@ -279,6 +279,7 @@ public:
// Kernel object API. All calls except postEvent/requestStop execute on the
// EE executor and therefore need no host synchronization.
void setupCurrentThread(uint32_t stack, uint32_t stackSize, uint32_t gp);
int createThread(const EeThreadCreateParams &params);
int deleteThread(int id, uint32_t &ownedStack);
int startThread(int id, uint32_t arg, const R5900Context &caller, bool interruptSafe);
@@ -416,6 +417,7 @@ private:
bool m_rescheduleRequested = false;
bool m_timeSliceExpired = false;
bool m_insideInterrupt = false;
uint32_t m_pendingEeTimerInterrupts = 0;
uint64_t m_eeCycle = 0;
uint64_t m_sliceEndCycle = kDefaultTimeSliceCycles;
std::thread::id m_executorThread{};
+17 -3
View File
@@ -3,6 +3,7 @@
#include <cstddef>
#include <cstdint>
#include <array>
#include <functional>
#include <vector>
#include <unordered_map>
@@ -306,6 +307,12 @@ public:
bool writeIORegister(uint32_t address, uint32_t value);
uint32_t readIORegister(uint32_t address);
// EE timers advance from the scheduler's emulated EE-cycle clock. The
// returned mask uses bits 0..3 for newly raised TIM0..TIM3 interrupts.
uint32_t advanceEeTimers(uint64_t eeCycles) noexcept;
[[nodiscard]] uint64_t cyclesUntilNextEeTimerInterrupt() const noexcept;
void resetEeTimers() noexcept;
using GifPacketCallback = std::function<void(const uint8_t *, uint32_t)>;
void setGifPacketCallback(GifPacketCallback cb) { m_gifPacketCallback = std::move(cb); }
void setGifArbiter(GifArbiter *arbiter) { m_gifArbiter = arbiter; }
@@ -432,10 +439,17 @@ public:
bool isScratchpad(uint32_t address) const;
uint8_t *mapVuMemory(uint32_t physAddr, uint32_t size, uint32_t &offset, uint32_t &limit);
const uint8_t *mapVuMemory(uint32_t physAddr, uint32_t size, uint32_t &offset, uint32_t &limit) const;
void updateEeTimer0Counter();
struct EeTimer
{
uint32_t count = 0;
uint32_t mode = 0;
uint32_t compare = 0;
uint32_t hold = 0;
uint64_t clockRemainder = 0;
};
std::array<EeTimer, 4> m_eeTimers{};
void queueCompletedDmacCause(uint32_t cause);
uint64_t m_timer0LastHostNs = 0;
uint64_t m_timer0FractionNs = 0;
};
#endif // PS2_MEMORY_H
+82 -14
View File
@@ -41,6 +41,15 @@ namespace
return (microseconds * EeScheduler::kEeClockHz + 999999ull) / 1000000ull;
}
std::chrono::nanoseconds eeCyclesToHostDuration(uint64_t cycles)
{
constexpr uint64_t kNanosecondsPerSecond = 1000000000ull;
const uint64_t wholeSeconds = cycles / EeScheduler::kEeClockHz;
const uint64_t remainingCycles = cycles % EeScheduler::kEeClockHz;
const uint64_t remainingNanoseconds = (remainingCycles * kNanosecondsPerSecond + EeScheduler::kEeClockHz - 1u) / EeScheduler::kEeClockHz;
return std::chrono::seconds(wholeSeconds) + std::chrono::nanoseconds(remainingNanoseconds);
}
constexpr uint64_t kVBlankPeriodCycles = microsecondsToEeCycles(16667u);
constexpr uint64_t kVBlankDurationCycles = microsecondsToEeCycles(500u);
constexpr uint64_t kAlarmTickCycles = microsecondsToEeCycles(kAlarmTickMicroseconds);
@@ -101,6 +110,7 @@ void EeScheduler::reset(uint8_t *rdram, const R5900Context &mainContext)
m_rescheduleRequested = false;
m_timeSliceExpired = false;
m_insideInterrupt = false;
m_pendingEeTimerInterrupts = 0u;
m_eeCycle = 0u;
m_sliceEndCycle = kDefaultTimeSliceCycles;
m_stopRequested.store(false, std::memory_order_release);
@@ -121,12 +131,15 @@ void EeScheduler::reset(uint8_t *rdram, const R5900Context &mainContext)
m_gsVSyncCallbackGp = 0;
m_gsVSyncCallbackSp = 0;
m_runtime.memory().gs().vsyncTick.store(0u, std::memory_order_release);
m_runtime.memory().resetEeTimers();
GuestThread main{};
main.id = kMainThreadId;
main.context = mainContext;
main.entry = mainContext.pc;
main.stack = getRegU32(&mainContext, 29);
// $sp is live execution state, not the stable initial stack descriptor
// returned by ReferThreadStatus. SetupThread records that metadata.
main.stack = 0u;
main.gp = getRegU32(&mainContext, 28);
main.initialPriority = 0;
main.currentPriority = 0;
@@ -375,7 +388,13 @@ bool EeScheduler::checkpointDue(uint32_t cycles) noexcept
void EeScheduler::accountCycles(uint32_t cycles) noexcept
{
m_eeCycle += std::max<uint64_t>(1u, cycles);
const uint64_t elapsed = std::max<uint64_t>(1u, cycles);
m_eeCycle += elapsed;
m_pendingEeTimerInterrupts |= m_runtime.memory().advanceEeTimers(elapsed);
if (m_pendingEeTimerInterrupts != 0u)
{
m_checkpointPending.store(true, std::memory_order_release);
}
}
bool EeScheduler::isExecutingGuest() const noexcept
@@ -383,6 +402,21 @@ bool EeScheduler::isExecutingGuest() const noexcept
return m_guestExecuting.load(std::memory_order_acquire);
}
void EeScheduler::setupCurrentThread(uint32_t stack, uint32_t stackSize, uint32_t gp)
{
assertExecutor();
GuestThread *target = currentThread();
if (!target)
{
return;
}
target->stack = stack;
target->stackSize = stackSize;
target->gp = gp;
publishSnapshot();
}
int EeScheduler::createThread(const EeThreadCreateParams &params)
{
assertExecutor();
@@ -1702,6 +1736,15 @@ void EeScheduler::processPendingEvents()
{
assertExecutor();
processDueDeadlines();
const uint32_t timerInterrupts = m_pendingEeTimerInterrupts;
m_pendingEeTimerInterrupts = 0u;
for (uint32_t timer = 0u; timer < 4u; ++timer)
{
if ((timerInterrupts & (1u << timer)) != 0u)
{
dispatchIrq(false, 9u + timer);
}
}
std::deque<EeEvent> pending;
{
std::lock_guard lock(m_eventMutex);
@@ -1950,30 +1993,55 @@ void EeScheduler::waitForEvent()
{
return;
}
if (m_deadlines.empty())
const uint64_t timerCycles = m_runtime.memory().cyclesUntilNextEeTimerInterrupt();
const bool hasTimerDeadline = timerCycles != std::numeric_limits<uint64_t>::max();
if (m_deadlines.empty() && !hasTimerDeadline)
{
m_eventCv.wait(lock, [this]()
{ return !m_events.empty() || m_stopRequested.load(std::memory_order_acquire); });
return;
}
const auto next = std::min_element(m_deadlines.begin(), m_deadlines.end(),
[](const ScheduledEvent &left, const ScheduledEvent &right)
{
if (left.deadlineCycle != right.deadlineCycle)
uint64_t deadlineCycle = 0u;
auto hostDeadline = std::chrono::steady_clock::time_point::max();
if (!m_deadlines.empty())
{
const auto next = std::min_element(m_deadlines.begin(), m_deadlines.end(),
[](const ScheduledEvent &left, const ScheduledEvent &right)
{
return left.deadlineCycle < right.deadlineCycle;
}
return left.sequence < right.sequence;
});
const uint64_t deadlineCycle = next->deadlineCycle;
const auto hostDeadline = next->hostDeadline;
if (left.deadlineCycle != right.deadlineCycle)
{
return left.deadlineCycle < right.deadlineCycle;
}
return left.sequence < right.sequence;
});
deadlineCycle = next->deadlineCycle;
hostDeadline = next->hostDeadline;
}
if (hasTimerDeadline)
{
const auto timerHostDeadline = std::chrono::steady_clock::now() + eeCyclesToHostDuration(timerCycles);
if (timerHostDeadline < hostDeadline)
{
deadlineCycle = m_eeCycle + timerCycles;
hostDeadline = timerHostDeadline;
}
}
const bool signaled = m_eventCv.wait_until(lock, hostDeadline, [this]()
{ return !m_events.empty() ||
m_stopRequested.load(std::memory_order_acquire); });
if (!signaled)
{
m_eeCycle = std::max(m_eeCycle, deadlineCycle);
const uint64_t elapsed = deadlineCycle > m_eeCycle ? deadlineCycle - m_eeCycle : 0u;
lock.unlock();
uint64_t remaining = elapsed;
while (remaining > 0u)
{
const uint32_t step = static_cast<uint32_t>(std::min<uint64_t>(remaining, std::numeric_limits<uint32_t>::max()));
accountCycles(step);
remaining -= step;
}
m_checkpointPending.store(true, std::memory_order_release);
}
}
@@ -29,8 +29,8 @@ namespace
uint32_t g_cdStreamingEndLbn = 0xFFFFFFFFu;
bool g_cdInitialized = false;
constexpr uint32_t kIopHeapBase = 0x01A00000;
constexpr uint32_t kIopHeapLimit = 0x01F00000;
constexpr uint32_t kIopHeapBase = 0x04000000;
constexpr uint32_t kIopHeapLimit = 0x04500000;
constexpr uint32_t kIopHeapAlign = 64;
uint32_t g_iopHeapNext = kIopHeapBase;
+152 -16
View File
@@ -4,7 +4,10 @@
#include "../../ps2_iop_transport.h"
#include "runtime/ps2_address.h"
#include <algorithm>
#include <limits>
#include <map>
#include <vector>
namespace ps2_stubs
{
@@ -59,6 +62,7 @@ namespace ps2_stubs
std::unordered_map<uint32_t, uint32_t> g_sifSregs;
std::unordered_map<uint32_t, uint32_t> g_sifCmdHandlers;
std::map<uint32_t, uint32_t> g_sifHeapAllocations;
std::array<uint8_t, kIopHeapLimit - kIopHeapBase> g_sifHeapStorage{};
uint32_t g_sifCmdBuffer = 0u;
uint32_t g_sifSysCmdBuffer = 0u;
bool g_sifCmdInitialized = false;
@@ -158,6 +162,9 @@ namespace ps2_stubs
}
g_sifHeapAllocations[candidate] = alignedSize;
std::fill_n(g_sifHeapStorage.data() + (candidate - kIopHeapBase),
alignedSize,
uint8_t{0});
g_iopHeapNext = candidate + alignedSize;
return candidate;
}
@@ -183,9 +190,29 @@ namespace ps2_stubs
{
std::lock_guard<std::mutex> lock(g_sifHeapMutex);
g_sifHeapAllocations.clear();
g_sifHeapStorage.fill(0u);
g_iopHeapNext = kIopHeapBase;
}
bool isAllocatedSifHeapRangeLocked(uint32_t address, size_t size)
{
if (address < kIopHeapBase || address >= kIopHeapLimit || size > static_cast<size_t>(kIopHeapLimit - address))
{
return false;
}
auto it = g_sifHeapAllocations.upper_bound(address);
if (it == g_sifHeapAllocations.begin())
{
return false;
}
--it;
const uint64_t allocationEnd = static_cast<uint64_t>(it->first) + it->second;
const uint64_t rangeEnd = static_cast<uint64_t>(address) + size;
return address >= it->first && rangeEnd <= allocationEnd;
}
bool isCopyableGuestAddress(uint32_t addr)
{
if (Ps2AddressInRange(addr, PS2_SCRATCHPAD_BASE, PS2_SCRATCHPAD_SIZE))
@@ -211,39 +238,40 @@ namespace ps2_stubs
return false;
}
bool canCopyGuestByteRange(const uint8_t *rdram, uint32_t dstAddr, uint32_t srcAddr, uint32_t sizeBytes)
bool canCopyAddressRange(const uint8_t *rdram, uint32_t address, uint32_t sizeBytes)
{
if (!rdram)
if (isSifIopHeapRange(address, sizeBytes))
{
return true;
}
if (isSifIopHeapAddress(address) || !rdram)
{
return false;
}
if (sizeBytes == 0u)
{
return true;
}
if (sizeBytes - 1u > std::numeric_limits<uint32_t>::max() - address)
{
return false;
}
for (uint32_t i = 0u; i < sizeBytes; ++i)
{
const uint32_t srcByteAddr = srcAddr + i;
const uint32_t dstByteAddr = dstAddr + i;
if (!isCopyableGuestAddress(srcByteAddr) || !isCopyableGuestAddress(dstByteAddr))
{
return false;
}
const uint8_t *src = getConstMemPtr(rdram, srcByteAddr);
const uint8_t *dst = getConstMemPtr(rdram, dstByteAddr);
if (!src || !dst)
const uint32_t byteAddress = address + i;
if (!isCopyableGuestAddress(byteAddress) ||getConstMemPtr(rdram, byteAddress) == nullptr)
{
return false;
}
}
return true;
}
bool canCopyGuestByteRange(const uint8_t *rdram, uint32_t dstAddr, uint32_t srcAddr, uint32_t sizeBytes)
{
return canCopyAddressRange(rdram, srcAddr, sizeBytes) && canCopyAddressRange(rdram, dstAddr, sizeBytes);
}
bool copyGuestByteRange(uint8_t *rdram, uint32_t dstAddr, uint32_t srcAddr, uint32_t sizeBytes)
{
if (!canCopyGuestByteRange(rdram, dstAddr, srcAddr, sizeBytes))
@@ -256,6 +284,49 @@ namespace ps2_stubs
return true;
}
const bool sourceIsIop = isSifIopHeapRange(srcAddr, sizeBytes);
const bool destinationIsIop = isSifIopHeapRange(dstAddr, sizeBytes);
if (sourceIsIop || destinationIsIop)
{
std::vector<uint8_t> payload(sizeBytes);
if (sourceIsIop)
{
if (!readSifIopHeap(srcAddr, payload.data(), payload.size()))
{
return false;
}
}
else
{
for (uint32_t i = 0u; i < sizeBytes; ++i)
{
const uint8_t *src = getConstMemPtr(rdram, srcAddr + i);
if (!src)
{
return false;
}
payload[i] = *src;
}
}
if (destinationIsIop)
{
return writeSifIopHeap(dstAddr, payload.data(), payload.size());
}
ps2TraceGuestRangeWrite(rdram, dstAddr, sizeBytes, "sifCopyGuestByteRange", nullptr);
for (uint32_t i = 0u; i < sizeBytes; ++i)
{
uint8_t *dst = getMemPtr(rdram, dstAddr + i);
if (!dst)
{
return false;
}
*dst = payload[i];
}
return true;
}
ps2TraceGuestRangeWrite(rdram, dstAddr, sizeBytes, "sifCopyGuestByteRange", nullptr);
const uint64_t srcBegin = srcAddr;
@@ -293,6 +364,71 @@ namespace ps2_stubs
}
}
bool isSifIopHeapAddress(uint32_t address)
{
return address >= kIopHeapBase && address < kIopHeapLimit;
}
bool isSifIopHeapRange(uint32_t address, size_t size)
{
std::lock_guard<std::mutex> lock(g_sifHeapMutex);
return isAllocatedSifHeapRangeLocked(address, size);
}
bool readSifIopHeap(uint32_t address, void *destination, size_t size)
{
if (!destination && size != 0u)
{
return false;
}
std::lock_guard<std::mutex> lock(g_sifHeapMutex);
if (!isAllocatedSifHeapRangeLocked(address, size))
{
return false;
}
if (size != 0u)
{
std::memcpy(destination,
g_sifHeapStorage.data() + (address - kIopHeapBase),
size);
}
return true;
}
bool writeSifIopHeap(uint32_t address, const void *source, size_t size)
{
if (!source && size != 0u)
{
return false;
}
std::lock_guard<std::mutex> lock(g_sifHeapMutex);
if (!isAllocatedSifHeapRangeLocked(address, size))
{
return false;
}
if (size != 0u)
{
std::memcpy(g_sifHeapStorage.data() + (address - kIopHeapBase),
source,
size);
}
return true;
}
bool zeroSifIopHeap(uint32_t address, size_t size)
{
std::lock_guard<std::mutex> lock(g_sifHeapMutex);
if (!isAllocatedSifHeapRangeLocked(address, size))
{
return false;
}
if (size != 0u)
{
std::memset(g_sifHeapStorage.data() + (address - kIopHeapBase), 0, size);
}
return true;
}
void resetSifState()
{
std::lock_guard<std::mutex> lock(g_sifCmdStateMutex);
+8
View File
@@ -2,8 +2,16 @@
#include "ps2_stubs.h"
#include <cstddef>
namespace ps2_stubs
{
bool isSifIopHeapAddress(uint32_t address);
bool isSifIopHeapRange(uint32_t address, size_t size);
bool readSifIopHeap(uint32_t address, void *destination, size_t size);
bool writeSifIopHeap(uint32_t address, const void *source, size_t size);
bool zeroSifIopHeap(uint32_t address, size_t size);
void sceSifCmdIntrHdlr(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime);
void sceSifLoadModule(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime);
void sceSifSendCmd(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime);
@@ -495,6 +495,8 @@ namespace ps2_syscalls
const uint32_t stack = getRegU32(ctx, 5);
const int32_t stackSizeSigned = static_cast<int32_t>(getRegU32(ctx, 6));
const uint32_t currentSp = getRegU32(ctx, 29);
EeScheduler &scheduler = runtime->eeScheduler();
scheduler.bindMainContextForSyscall(*ctx, rdram);
if (gp != 0u)
{
@@ -502,6 +504,10 @@ namespace ps2_syscalls
}
uint32_t sp = currentSp;
uint32_t initialStack = 0u;
const uint32_t stackSize = stackSizeSigned > 0
? static_cast<uint32_t>(stackSizeSigned)
: 0u;
if (stack == 0xFFFFFFFFu)
{
if (stackSizeSigned > 0)
@@ -534,6 +540,16 @@ namespace ps2_syscalls
}
sp &= ~0xFu;
if (stack == 0xFFFFFFFFu)
{
initialStack = sp;
}
else if (stack != 0u)
{
initialStack = stack;
}
scheduler.setupCurrentThread(initialStack, stackSize, getRegU32(ctx, 28));
setReturnU32(ctx, sp);
}
+22
View File
@@ -2,6 +2,7 @@
#include "ps2_runtime.h"
#include "ps2_stubs.h"
#include "Kernel/Stubs/SIF.h"
#include "runtime/ps2_memory.h"
#include "Kernel/Stubs/MemoryCard.h"
#include "Kernel/Syscalls/Common.h"
@@ -134,6 +135,11 @@ bool PS2IopHostAdapter::readGuest(uint32_t address, void *destination, size_t si
{
return false;
}
if (ps2_stubs::isSifIopHeapAddress(address))
{
return ps2_stubs::readSifIopHeap(address, destination, size);
}
uint8_t *source = nullptr;
if (!guestRange(address, size, source))
{
@@ -152,6 +158,11 @@ bool PS2IopHostAdapter::writeGuest(uint32_t address, const void *source, size_t
{
return false;
}
if (ps2_stubs::isSifIopHeapAddress(address))
{
return ps2_stubs::writeSifIopHeap(address, source, size);
}
uint8_t *destination = nullptr;
if (!guestRange(address, size, destination))
{
@@ -168,6 +179,11 @@ bool PS2IopHostAdapter::writeGuest(uint32_t address, const void *source, size_t
bool PS2IopHostAdapter::zeroGuest(uint32_t address, size_t size)
{
if (ps2_stubs::isSifIopHeapAddress(address))
{
return ps2_stubs::zeroSifIopHeap(address, size);
}
uint8_t *destination = nullptr;
if (!guestRange(address, size, destination))
{
@@ -184,6 +200,12 @@ bool PS2IopHostAdapter::zeroGuest(uint32_t address, size_t size)
bool PS2IopHostAdapter::normalizeGuestAddress(uint32_t address, uint32_t &normalized) const
{
if (ps2_stubs::isSifIopHeapAddress(address))
{
normalized = address;
return ps2_stubs::isSifIopHeapRange(address, 0u);
}
bool scratchpad = false;
if (!ps2ResolveGuestPointer(address, normalized, scratchpad) || scratchpad)
{
+199 -66
View File
@@ -3,8 +3,8 @@
#include "runtime/ps2_gs_gpu.h"
#include "ps2_log.h"
#include <atomic>
#include <chrono>
#include <cstring>
#include <limits>
#include <stdexcept>
#include <algorithm>
#include <string>
@@ -141,26 +141,55 @@ namespace
} while (!csr.compare_exchange_weak(expected, desired));
}
constexpr uint32_t kEeTimer0Count = 0x10000000u;
constexpr uint32_t kEeTimer0Mode = 0x10000010u;
constexpr uint32_t kEeTimer0Compare = 0x10000020u;
constexpr uint32_t kEeTimer0Hold = 0x10000030u;
constexpr std::array<uint32_t, 4> kEeTimerBases = {
0x10000000u,
0x10000800u,
0x10001000u,
0x10001800u,
};
constexpr uint32_t kEeTimerCountOffset = 0x00u;
constexpr uint32_t kEeTimerModeOffset = 0x10u;
constexpr uint32_t kEeTimerCompareOffset = 0x20u;
constexpr uint32_t kEeTimerHoldOffset = 0x30u;
constexpr uint32_t kEeTimerModeClksMask = 0x3u;
constexpr uint32_t kEeTimerModeConfigMask = 0x3FFu;
constexpr uint32_t kEeTimerModeStatusMask = 0xC00u;
constexpr uint32_t kEeTimerModeZret = 1u << 6;
constexpr uint32_t kEeTimerModeCue = 1u << 7;
constexpr uint64_t kEeTimer0TicksPerSecond = 15720ull;
constexpr uint64_t kNanosecondsPerSecond = 1000000000ull;
constexpr uint32_t kEeTimerModeCmpe = 1u << 8;
constexpr uint32_t kEeTimerModeOvfe = 1u << 9;
constexpr uint32_t kEeTimerModeEquf = 1u << 10;
constexpr uint32_t kEeTimerModeOvff = 1u << 11;
constexpr uint64_t kEeClockHz = 294912000ull;
constexpr std::array<uint64_t, 4> kEeTimerClockHz = {
147456000ull,
9216000ull,
576000ull,
15734ull,
};
inline bool isEeTimer0Register(uint32_t address)
inline bool decodeEeTimerRegister(uint32_t address, size_t &timerIndex, uint32_t &offset)
{
return address == kEeTimer0Count ||
address == kEeTimer0Mode ||
address == kEeTimer0Compare ||
address == kEeTimer0Hold;
for (size_t index = 0; index < kEeTimerBases.size(); ++index)
{
const uint32_t candidateOffset = address - kEeTimerBases[index];
if (candidateOffset == kEeTimerCountOffset ||
candidateOffset == kEeTimerModeOffset ||
candidateOffset == kEeTimerCompareOffset ||
(index < 2u && candidateOffset == kEeTimerHoldOffset))
{
timerIndex = index;
offset = candidateOffset;
return true;
}
}
return false;
}
inline uint64_t steadyClockNs()
constexpr uint64_t ticksUntilMatch(uint32_t count, uint32_t target)
{
using namespace std::chrono;
return static_cast<uint64_t>(duration_cast<nanoseconds>(steady_clock::now().time_since_epoch()).count());
const uint32_t distance = (target - count) & 0xFFFFu;
return distance == 0u ? 0x10000ull : static_cast<uint64_t>(distance);
}
struct DmaTagView
@@ -302,8 +331,7 @@ bool PS2Memory::initialize(size_t ramSize)
m_path3MaskedFifo.clear();
m_vif1PendingPath2ImageQwc = 0u;
m_vif1PendingPath2DirectHl = false;
m_timer0LastHostNs = 0;
m_timer0FractionNs = 0;
resetEeTimers();
try
{
@@ -371,37 +399,121 @@ bool PS2Memory::initialize(size_t ramSize)
}
}
void PS2Memory::updateEeTimer0Counter()
void PS2Memory::resetEeTimers() noexcept
{
const uint64_t nowNs = steadyClockNs();
if (m_timer0LastHostNs == 0u)
m_eeTimers = {};
}
uint32_t PS2Memory::advanceEeTimers(uint64_t eeCycles) noexcept
{
if (eeCycles == 0u)
{
m_timer0LastHostNs = nowNs;
return;
return 0u;
}
const uint32_t mode = m_ioRegisters.count(kEeTimer0Mode) ? m_ioRegisters[kEeTimer0Mode] : 0u;
if ((mode & kEeTimerModeCue) == 0u)
uint32_t interruptMask = 0u;
for (size_t index = 0; index < m_eeTimers.size(); ++index)
{
m_timer0LastHostNs = nowNs;
m_timer0FractionNs = 0u;
return;
}
EeTimer &timer = m_eeTimers[index];
if ((timer.mode & kEeTimerModeCue) == 0u)
{
continue;
}
const uint64_t elapsedNs = nowNs - m_timer0LastHostNs;
m_timer0LastHostNs = nowNs;
if (elapsedNs == 0u)
{
return;
}
const uint64_t clockHz = kEeTimerClockHz[timer.mode & kEeTimerModeClksMask];
const uint64_t wholeSeconds = eeCycles / kEeClockHz;
const uint64_t remainingCycles = eeCycles % kEeClockHz;
const uint64_t scaled = remainingCycles * clockHz + timer.clockRemainder;
const uint64_t ticks = wholeSeconds * clockHz + scaled / kEeClockHz;
timer.clockRemainder = scaled % kEeClockHz;
if (ticks == 0u)
{
continue;
}
const uint64_t scaled = elapsedNs * kEeTimer0TicksPerSecond + m_timer0FractionNs;
const uint64_t ticks = scaled / kNanosecondsPerSecond;
m_timer0FractionNs = scaled % kNanosecondsPerSecond;
if (ticks != 0u)
{
m_ioRegisters[kEeTimer0Count] = m_ioRegisters[kEeTimer0Count] + static_cast<uint32_t>(ticks);
const uint32_t oldCount = timer.count & 0xFFFFu;
const uint32_t compare = timer.compare & 0xFFFFu;
const uint64_t compareDistance = ticksUntilMatch(oldCount, compare);
const uint64_t overflowDistance = 0x10000ull - oldCount;
const bool zeroReturn = (timer.mode & kEeTimerModeZret) != 0u;
const bool compareReached = ticks >= compareDistance;
bool overflowReached = false;
if (zeroReturn)
{
overflowReached = ticks >= overflowDistance && overflowDistance <= compareDistance;
if (compareReached)
{
const uint64_t remaining = ticks - compareDistance;
timer.count = compare == 0u
? static_cast<uint32_t>(remaining & 0xFFFFu)
: static_cast<uint32_t>(remaining % compare);
}
else
{
timer.count = static_cast<uint32_t>((oldCount + ticks) & 0xFFFFu);
}
}
else
{
overflowReached = ticks >= overflowDistance;
timer.count = static_cast<uint32_t>((oldCount + ticks) & 0xFFFFu);
}
if (compareReached && (timer.mode & kEeTimerModeCmpe) != 0u && (timer.mode & kEeTimerModeEquf) == 0u)
{
timer.mode |= kEeTimerModeEquf;
interruptMask |= 1u << index;
}
if (overflowReached && (timer.mode & kEeTimerModeOvfe) != 0u && (timer.mode & kEeTimerModeOvff) == 0u)
{
timer.mode |= kEeTimerModeOvff;
interruptMask |= 1u << index;
}
}
return interruptMask;
}
uint64_t PS2Memory::cyclesUntilNextEeTimerInterrupt() const noexcept
{
uint64_t nearest = std::numeric_limits<uint64_t>::max();
for (const EeTimer &timer : m_eeTimers)
{
if ((timer.mode & kEeTimerModeCue) == 0u)
{
continue;
}
const uint32_t count = timer.count & 0xFFFFu;
const uint32_t compare = timer.compare & 0xFFFFu;
const uint64_t compareDistance = ticksUntilMatch(count, compare);
const uint64_t overflowDistance = 0x10000ull - count;
uint64_t eventTicks = std::numeric_limits<uint64_t>::max();
if ((timer.mode & kEeTimerModeCmpe) != 0u &&
(timer.mode & kEeTimerModeEquf) == 0u)
{
eventTicks = compareDistance;
}
const bool overflowCanOccur = (timer.mode & kEeTimerModeZret) == 0u ||
overflowDistance <= compareDistance;
if (overflowCanOccur &&
(timer.mode & kEeTimerModeOvfe) != 0u &&
(timer.mode & kEeTimerModeOvff) == 0u)
{
eventTicks = std::min(eventTicks, overflowDistance);
}
if (eventTicks == std::numeric_limits<uint64_t>::max())
{
continue;
}
const uint64_t clockHz = kEeTimerClockHz[timer.mode & kEeTimerModeClksMask];
const uint64_t numerator = eventTicks * kEeClockHz - timer.clockRemainder;
const uint64_t cycles = (numerator + clockHz - 1u) / clockHz;
nearest = std::min(nearest, std::max<uint64_t>(1u, cycles));
}
return nearest;
}
bool PS2Memory::isScratchpad(uint32_t address) const
@@ -991,22 +1103,36 @@ void PS2Memory::write128(uint32_t address, __m128i value)
bool PS2Memory::writeIORegister(uint32_t address, uint32_t value)
{
if (isEeTimer0Register(address))
size_t timerIndex = 0u;
uint32_t timerOffset = 0u;
if (decodeEeTimerRegister(address, timerIndex, timerOffset))
{
if (address == kEeTimer0Count)
EeTimer &timer = m_eeTimers[timerIndex];
switch (timerOffset)
{
m_ioRegisters[address] = value;
m_timer0LastHostNs = steadyClockNs();
m_timer0FractionNs = 0u;
return true;
case kEeTimerCountOffset:
timer.count = value & 0xFFFFu;
timer.clockRemainder = 0u;
break;
case kEeTimerModeOffset:
{
const uint32_t previousMode = timer.mode;
const uint32_t status = (previousMode & kEeTimerModeStatusMask) &~(value & kEeTimerModeStatusMask);
timer.mode = (value & kEeTimerModeConfigMask) | status;
if (((previousMode ^ timer.mode) & (kEeTimerModeClksMask | kEeTimerModeCue)) != 0u)
{
timer.clockRemainder = 0u;
}
break;
}
updateEeTimer0Counter();
m_ioRegisters[address] = value;
m_timer0LastHostNs = steadyClockNs();
if (address == kEeTimer0Mode)
{
m_timer0FractionNs = 0u;
case kEeTimerCompareOffset:
timer.compare = value & 0xFFFFu;
break;
case kEeTimerHoldOffset:
timer.hold = value & 0xFFFFu;
break;
default:
return false;
}
return true;
}
@@ -2037,6 +2163,26 @@ int PS2Memory::pollDmaRegisters()
uint32_t PS2Memory::readIORegister(uint32_t address)
{
size_t timerIndex = 0u;
uint32_t timerOffset = 0u;
if (decodeEeTimerRegister(address, timerIndex, timerOffset))
{
const EeTimer &timer = m_eeTimers[timerIndex];
switch (timerOffset)
{
case kEeTimerCountOffset:
return timer.count & 0xFFFFu;
case kEeTimerModeOffset:
return timer.mode & (kEeTimerModeConfigMask | kEeTimerModeStatusMask);
case kEeTimerCompareOffset:
return timer.compare & 0xFFFFu;
case kEeTimerHoldOffset:
return timer.hold & 0xFFFFu;
default:
return 0u;
}
}
if (isGsPrivReg(address))
{
// NB: unreachable from read8/16/32/64 today, same reasoning as the write
@@ -2077,19 +2223,6 @@ uint32_t PS2Memory::readIORegister(uint32_t address)
}
if (address >= 0x10000000 && address < 0x10010000)
{
if (address >= 0x10000000 && address < 0x10000100)
{
if (isEeTimer0Register(address))
{
if (address == kEeTimer0Count)
{
updateEeTimer0Counter();
}
auto timerIt = m_ioRegisters.find(address);
return timerIt != m_ioRegisters.end() ? timerIt->second : 0u;
}
}
if (address >= 0x10008000 && address < 0x1000F000)
{
if ((address & 0xFF) == 0x00)
+1 -7
View File
@@ -1320,14 +1320,13 @@ bool PS2Runtime::dispatchGuestBranch(uint8_t *rdram,
return false;
}
if (kind == GuestBranchKind::Return)
if (!isCall)
{
if (!hasFunction(targetPc))
{
reportMissingFunction(rdram, ctx, targetPc, sourcePc, kind, debugName);
}
// Prevent nested dispatch.
ctx->pc = targetPc;
return false;
}
@@ -1362,11 +1361,6 @@ bool PS2Runtime::dispatchGuestBranch(uint8_t *rdram,
return false;
}
if (!isCall)
{
return false;
}
if (ctx->pc == entryPc)
{
ctx->pc = fallthroughPc;
+38 -11
View File
@@ -1042,7 +1042,7 @@ void register_code_generator_tests()
"QFSRV should map to PS2_QFSRV with rs/rt ordering");
});
tc.Run("PCPYLD and PEXEW use runtime helper macros", [](TestCase &t) {
tc.Run("PCPYLD uses runtime helper macro", [](TestCase &t) {
CodeGenerator gen({}, {});
Instruction pcpyld{};
@@ -1057,18 +1057,45 @@ void register_code_generator_tests()
std::string pcpyldOut = gen.translateInstruction(pcpyld);
t.IsTrue(pcpyldOut.find("PS2_PCPYLD(GPR_VEC(ctx, 7), GPR_VEC(ctx, 8))") != std::string::npos,
"PCPYLD should use PS2_PCPYLD helper");
});
Instruction pexew{};
pexew.isMMI = true;
pexew.opcode = OPCODE_MMI;
pexew.function = MMI_MMI2;
pexew.sa = MMI2_PEXEW;
pexew.rd = 9;
pexew.rs = 10;
tc.Run("Unary MMI permutations read their source from rt", [](TestCase &t) {
CodeGenerator gen({}, {});
std::string pexewOut = gen.translateInstruction(pexew);
t.IsTrue(pexewOut.find("PS2_PEXEW(GPR_VEC(ctx, 10))") != std::string::npos,
"PEXEW should use PS2_PEXEW helper");
struct UnaryMmiCase
{
const char *name;
uint8_t function;
uint8_t subfunction;
};
const std::vector<UnaryMmiCase> cases = {
{"PEXEH", MMI_MMI2, MMI2_PEXEH},
{"PREVH", MMI_MMI2, MMI2_PREVH},
{"PEXEW", MMI_MMI2, MMI2_PEXEW},
{"PROT3W", MMI_MMI2, MMI2_PROT3W},
{"PEXCH", MMI_MMI3, MMI3_PEXCH},
{"PCPYH", MMI_MMI3, MMI3_PCPYH},
{"PEXCW", MMI_MMI3, MMI3_PEXCW},
};
for (const UnaryMmiCase &item : cases)
{
Instruction inst{};
inst.isMMI = true;
inst.opcode = OPCODE_MMI;
inst.function = item.function;
inst.sa = item.subfunction;
inst.rd = 3;
inst.rs = 4;
inst.rt = 5;
const std::string out = gen.translateInstruction(inst);
t.IsTrue(out.find("GPR_VEC(ctx, 5)") != std::string::npos,
std::string(item.name) + " should read its source from rt");
t.IsTrue(out.find("GPR_VEC(ctx, 4)") == std::string::npos,
std::string(item.name) + " should not read its source from rs");
}
});
tc.Run("VU0 macro mappings cover all S1/S2 enums", [](TestCase &t) {
+101 -9
View File
@@ -8,10 +8,8 @@
#include "Stubs/GS.h"
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <cstring>
#include <thread>
#include <vector>
namespace
@@ -194,7 +192,7 @@ void register_ps2_memory_tests()
t.Equals(mem.translateAddress(PS2_SCRATCHPAD_ALIAS_BASE + 0x123u), 0x123u, "0xF000 scratchpad alias should translate to local offset");
});
tc.Run("EE timer0 count advances while enabled and can be reset", [](TestCase &t)
tc.Run("EE timer0 count advances from scheduler cycles and can be reset", [](TestCase &t)
{
PS2Memory mem;
t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed");
@@ -204,17 +202,111 @@ void register_ps2_memory_tests()
constexpr uint32_t kTimer0Compare = 0x10000020u;
t.IsTrue(mem.writeIORegister(kTimer0Count, 0u), "timer count reset write should succeed");
t.IsTrue(mem.writeIORegister(kTimer0Compare, 1u), "timer compare write should succeed");
t.IsTrue(mem.writeIORegister(kTimer0Mode, 0x283u), "timer mode write should be retained");
t.Equals(mem.readIORegister(kTimer0Mode), 0x283u, "timer mode should be readable");
t.IsTrue(mem.writeIORegister(kTimer0Compare, 0xFFFFu), "timer compare write should succeed");
t.IsTrue(mem.writeIORegister(kTimer0Mode, 0x82u), "timer mode write should be retained");
t.Equals(mem.readIORegister(kTimer0Mode), 0x82u, "timer mode should be readable");
std::this_thread::sleep_for(std::chrono::milliseconds(3));
mem.advanceEeTimers(8u * 512u);
const uint32_t firstCount = mem.readIORegister(kTimer0Count);
t.IsTrue(firstCount > 0u, "enabled timer count should advance from host time");
t.Equals(firstCount, 8u, "BUSCLK/256 should increment once per 512 EE cycles");
t.IsTrue(mem.writeIORegister(kTimer0Count, 0u), "timer count second reset should succeed");
mem.advanceEeTimers(512u);
const uint32_t resetCount = mem.readIORegister(kTimer0Count);
t.IsTrue(resetCount <= firstCount, "timer reset should restart the count window");
t.Equals(resetCount, 1u, "timer reset should restart the deterministic count window");
});
tc.Run("EE timers 0 through 3 expose independent COUNT MODE and COMP registers", [](TestCase &t)
{
PS2Memory mem;
t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed");
constexpr uint32_t kTimerBases[] = {
0x10000000u,
0x10000800u,
0x10001000u,
0x10001800u,
};
constexpr uint32_t kBusClockDiv256Cue = 0x82u;
for (uint32_t index = 0u; index < 4u; ++index)
{
const uint32_t base = kTimerBases[index];
t.IsTrue(mem.writeIORegister(base, 0x100u + index), "timer COUNT write should succeed");
t.IsTrue(mem.writeIORegister(base + 0x10u, kBusClockDiv256Cue), "timer MODE write should succeed");
t.IsTrue(mem.writeIORegister(base + 0x20u, 0x200u + index), "timer COMP write should succeed");
}
mem.advanceEeTimers(512u);
for (uint32_t index = 0u; index < 4u; ++index)
{
const uint32_t base = kTimerBases[index];
t.Equals(mem.readIORegister(base), 0x101u + index, "each timer should advance its own COUNT");
t.Equals(mem.readIORegister(base + 0x10u), kBusClockDiv256Cue, "each timer should retain MODE");
t.Equals(mem.readIORegister(base + 0x20u), 0x200u + index, "each timer should retain COMP");
}
t.IsTrue(mem.writeIORegister(kTimerBases[0] + 0x30u, 0x12345u), "Timer0 HOLD write should succeed");
t.IsTrue(mem.writeIORegister(kTimerBases[1] + 0x30u, 0x23456u), "Timer1 HOLD write should succeed");
t.Equals(mem.readIORegister(kTimerBases[0] + 0x30u), 0x2345u, "Timer0 HOLD should be 16-bit");
t.Equals(mem.readIORegister(kTimerBases[1] + 0x30u), 0x3456u, "Timer1 HOLD should be 16-bit");
});
tc.Run("EE Timer2 compare and overflow flags raise INTC_TIM2 and clear on write-one", [](TestCase &t)
{
PS2Memory mem;
t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed");
constexpr uint32_t kTimer2Count = 0x10001000u;
constexpr uint32_t kTimer2Mode = 0x10001010u;
constexpr uint32_t kTimer2Compare = 0x10001020u;
constexpr uint32_t kCue = 1u << 7u;
constexpr uint32_t kCmpe = 1u << 8u;
constexpr uint32_t kOvfe = 1u << 9u;
constexpr uint32_t kEquf = 1u << 10u;
constexpr uint32_t kOvff = 1u << 11u;
constexpr uint32_t kBusClockDiv256 = 2u;
mem.writeIORegister(kTimer2Count, 0u);
mem.writeIORegister(kTimer2Compare, 8u);
mem.writeIORegister(kTimer2Mode, kBusClockDiv256 | kCue | kCmpe | kEquf | kOvff);
t.Equals(mem.advanceEeTimers(7u * 512u), 0u, "compare should not fire before COUNT reaches COMP");
t.Equals(mem.readIORegister(kTimer2Count), 7u, "Timer2 should expose its live 16-bit count");
t.Equals(mem.advanceEeTimers(512u), 1u << 2u, "Timer2 compare should raise the TIM2 interrupt bit");
t.IsTrue((mem.readIORegister(kTimer2Mode) & kEquf) != 0u, "Timer2 compare should latch EQUF");
mem.writeIORegister(kTimer2Mode, mem.readIORegister(kTimer2Mode) | kEquf);
t.IsTrue((mem.readIORegister(kTimer2Mode) & kEquf) == 0u, "writing one should clear EQUF");
mem.writeIORegister(kTimer2Count, 0xFFFFu);
mem.writeIORegister(kTimer2Mode, kBusClockDiv256 | kCue | kOvfe | kOvff);
t.Equals(mem.advanceEeTimers(512u), 1u << 2u, "Timer2 overflow should raise the TIM2 interrupt bit");
t.Equals(mem.readIORegister(kTimer2Count), 0u, "Timer2 count should wrap at 16 bits");
t.IsTrue((mem.readIORegister(kTimer2Mode) & kOvff) != 0u, "Timer2 overflow should latch OVFF");
mem.writeIORegister(kTimer2Mode, mem.readIORegister(kTimer2Mode) | kOvff);
t.IsTrue((mem.readIORegister(kTimer2Mode) & kOvff) == 0u, "writing one should clear OVFF");
});
tc.Run("EE timer zero-return clears COUNT on compare", [](TestCase &t)
{
PS2Memory mem;
t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed");
constexpr uint32_t kTimer0Count = 0x10000000u;
constexpr uint32_t kTimer0Mode = 0x10000010u;
constexpr uint32_t kTimer0Compare = 0x10000020u;
constexpr uint32_t kZret = 1u << 6u;
constexpr uint32_t kCue = 1u << 7u;
constexpr uint32_t kCmpe = 1u << 8u;
constexpr uint32_t kEquf = 1u << 10u;
mem.writeIORegister(kTimer0Count, 0u);
mem.writeIORegister(kTimer0Compare, 3u);
mem.writeIORegister(kTimer0Mode, kZret | kCue | kCmpe | kEquf);
t.Equals(mem.advanceEeTimers(6u), 1u, "Timer0 compare should raise TIM0 after three BUSCLK ticks");
t.Equals(mem.readIORegister(kTimer0Count), 0u, "ZRET should clear COUNT when it equals COMP");
});
tc.Run("scratchpad alias accesses the same bytes as base", [](TestCase &t)
@@ -166,6 +166,13 @@ namespace
}
}
std::atomic<uint32_t> gGuestJumpTargetCount{0u};
void testGuestJumpTargetHandler(uint8_t *, R5900Context *, PS2Runtime *)
{
gGuestJumpTargetCount.fetch_add(1u, std::memory_order_relaxed);
}
std::atomic<uint32_t> gMpegStreamCallbackCount{0u};
std::atomic<uint32_t> gMpegStreamCallbackMpeg{0u};
std::atomic<uint32_t> gMpegStreamCallbackType{0u};
@@ -399,6 +406,32 @@ void register_ps2_runtime_expansion_tests()
"callee should still execute normally");
});
tc.Run("dispatchGuestBranch jump returns to central dispatcher without nesting", [](TestCase &t)
{
PS2Runtime runtime;
runtime.registerFunction(0x3400u, &testGuestJumpTargetHandler);
gGuestJumpTargetCount.store(0u, std::memory_order_relaxed);
R5900Context ctx{};
ctx.pc = 0x2000u;
const bool continuedInCaller = runtime.dispatchGuestBranch(
nullptr,
&ctx,
0x3400u,
0x2000u,
0u,
PS2Runtime::GuestBranchKind::IndirectJump,
"test-jr");
t.IsFalse(continuedInCaller,
"jump should stop the current generated wrapper");
t.Equals(gGuestJumpTargetCount.load(std::memory_order_relaxed), 0u,
"jump target must not execute on a nested host stack frame");
t.Equals(ctx.pc, 0x3400u,
"central dispatcher should receive the exact jump target");
});
tc.Run("dispatchGuestBranch call returns false when callee transfers elsewhere", [](TestCase &t)
{
PS2Runtime runtime;
@@ -59,6 +59,17 @@ namespace
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;
@@ -71,6 +82,7 @@ namespace
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)
{
@@ -247,6 +259,41 @@ namespace
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()
@@ -478,6 +525,52 @@ void register_ps2_runtime_interrupt_tests()
}
});
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;
+40
View File
@@ -1049,6 +1049,46 @@ void register_ps2_runtime_kernel_tests()
t.Equals(setupSp & 0xFu, 0u, "SetupThread should always return a 16-byte aligned stack pointer");
});
tc.Run("SetupThread exposes stable main stack metadata through ReferThreadStatus", [](TestCase &t)
{
TestEnv env;
constexpr uint32_t kInitialLoaderSp = PS2_RAM_SIZE - 0x10u;
constexpr uint32_t kMainStackSize = 0x00020000u;
constexpr uint32_t kExpectedStack = PS2_RAM_SIZE - kMainStackSize;
constexpr uint32_t kMainGp = 0x0036A7F0u;
env.ctx.pc = 0x00100000u;
setRegU32(env.ctx, 29, kInitialLoaderSp);
setRegU32(env.ctx, 4, kMainGp);
setRegU32(env.ctx, 5, 0xFFFFFFFFu);
setRegU32(env.ctx, 6, kMainStackSize);
t.IsTrue(callSyscall(0x3Cu, env.rdram.data(), &env.ctx, &env.runtime),
"SetupThread syscall should dispatch");
t.Equals(::getRegU32(&env.ctx, 2), kExpectedStack,
"automatic main stack should start below the reserved top-of-RDRAM area");
// ReferThreadStatus can be called after many nested frames have moved $sp.
// It must report the initial stack recorded by SetupThread, not this live snapshot.
constexpr uint32_t kTransientSp = kExpectedStack - 0x80u;
setRegU32(env.ctx, 29, kTransientSp);
setRegU32(env.ctx, 4, 0u);
setRegU32(env.ctx, 5, K_STATUS_ADDR);
t.IsTrue(callSyscall(0x30u, env.rdram.data(), &env.ctx, &env.runtime),
"ReferThreadStatus syscall should dispatch");
t.Equals(getRegS32(env.ctx, 2), KE_OK, "ReferThreadStatus should accept the current-thread id alias");
EeThreadStatusAbi status{};
std::memcpy(&status, env.rdram.data() + K_STATUS_ADDR, sizeof(status));
t.Equals(status.stack, kExpectedStack,
"main thread status must expose SetupThread's stable initial stack");
t.Equals(status.stack_size, static_cast<int32_t>(kMainStackSize),
"main thread status must preserve SetupThread's stack size");
t.Equals(status.gp_reg, kMainGp,
"main thread status must preserve SetupThread's global pointer");
t.IsTrue(status.stack != kInitialLoaderSp && status.stack != kTransientSp,
"main thread status must never expose a live stack-pointer snapshot");
});
tc.Run("OSD config2 syscalls round-trip extended config", [](TestCase &t)
{
TestEnv env;
+77
View File
@@ -1,8 +1,10 @@
#include "MiniTest.h"
#include "ps2_runtime.h"
#include "ps2_iop_host.h"
#include "ps2_iop_transport.h"
#include "ps2_syscalls.h"
#include "ps2_stubs.h"
#include "Kernel/Stubs/SIF.h"
#include "runtime/ee_scheduler.h"
#include <array>
@@ -197,6 +199,79 @@ void register_ps2_sif_dma_tests()
t.IsTrue(getRegS32(env.ctx, 2) < 0, "sceSifDmaStat should be negative when transfer is complete");
});
tc.Run("IOP heap DMA uses private backing instead of aliasing EE RDRAM", [](TestCase &t)
{
TestEnv env;
constexpr uint32_t kDescAddr = 0x00020040u;
constexpr uint32_t kSrcAddr = 0x00020140u;
constexpr uint32_t kRoundTripAddr = 0x00020240u;
constexpr uint32_t kFormerAliasAddr = 0x01A53880u;
constexpr uint32_t kIopBlockSize = 0x880u;
std::array<uint8_t, 32> payload{};
for (size_t i = 0; i < payload.size(); ++i)
{
payload[i] = static_cast<uint8_t>(0x80u + i);
}
std::memcpy(env.rdram.data() + kSrcAddr, payload.data(), payload.size());
std::memset(env.rdram.data() + kRoundTripAddr, 0, payload.size());
std::memset(env.rdram.data() + kFormerAliasAddr, 0x5Au, payload.size());
setRegU32(env.ctx, 4, kIopBlockSize);
ps2_stubs::sceSifAllocIopHeap(env.rdram.data(), &env.ctx, &env.runtime);
const uint32_t iopAddress = ::getRegU32(&env.ctx, 2);
t.IsTrue(iopAddress >= PS2_RAM_SIZE,
"sceSifAllocIopHeap should return an address outside EE RDRAM");
Ps2SifDmaTransfer desc{
kSrcAddr,
iopAddress,
static_cast<int32_t>(payload.size()),
0};
std::memcpy(env.rdram.data() + kDescAddr, &desc, sizeof(desc));
setRegU32(env.ctx, 4, kDescAddr);
setRegU32(env.ctx, 5, 1u);
ps2_stubs::sceSifSetDma(env.rdram.data(), &env.ctx, &env.runtime);
t.IsTrue(getRegS32(env.ctx, 2) > 0,
"EE-to-IOP DMA should accept a private IOP heap destination");
const std::array<uint8_t, 32> aliasSentinel{
0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A,
0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A,
0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A,
0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A};
t.IsTrue(std::memcmp(env.rdram.data() + kFormerAliasAddr,
aliasSentinel.data(), aliasSentinel.size()) == 0,
"IOP DMA must not overwrite the old 0x01A00000 EE alias range");
PS2IopHostAdapter host(env.runtime);
auto scope = host.enterCall(&env.ctx, env.rdram.data());
uint32_t normalized = 0u;
std::array<uint8_t, 32> hostReadback{};
t.IsTrue(host.normalizeGuestAddress(iopAddress, normalized) &&
normalized == iopAddress,
"IOP modules should preserve private IOP heap addresses");
t.IsTrue(host.readGuest(iopAddress, hostReadback.data(), hostReadback.size()) &&
hostReadback == payload,
"IOP modules should read the private heap backing");
desc = {
iopAddress,
kRoundTripAddr,
static_cast<int32_t>(payload.size()),
0};
std::memcpy(env.rdram.data() + kDescAddr, &desc, sizeof(desc));
setRegU32(env.ctx, 4, kDescAddr);
setRegU32(env.ctx, 5, 1u);
ps2_stubs::sceSifSetDma(env.rdram.data(), &env.ctx, &env.runtime);
t.IsTrue(getRegS32(env.ctx, 2) > 0,
"IOP-to-EE DMA should accept a private IOP heap source");
t.IsTrue(std::memcmp(env.rdram.data() + kRoundTripAddr,
payload.data(), payload.size()) == 0,
"IOP-to-EE DMA should round-trip the payload");
});
tc.Run("isceSifSetDma and isceSifSetDChain alias the SIF DMA helpers", [](TestCase &t)
{
TestEnv env;
@@ -438,6 +513,8 @@ void register_ps2_sif_dma_tests()
t.IsTrue(getRegS32(env.ctx, 2) > 0, "sceSifSetDma should succeed for the SJX transport");
t.Equals(env.rdram[kEeWorkAddr + 0x11u], static_cast<uint8_t>(0u),
"SJX DMA ack should rewrite the response line to room so EE recycles the chunk");
t.Equals(readGuestU32(env.rdram.data(), kEeWorkAddr + 0x14u), 0x12345678u,
"SJX DMA ack should translate the remote handle back to the EE callback object");
t.Equals(readGuestU32(env.rdram.data(), kEeWorkAddr + kWorkLen - sizeof(uint32_t)), 2u,
"SJX DMA ack should still advance the EE footer ticket");