mirror of
https://github.com/ran-j/PS2Recomp.git
synced 2026-09-26 16:59:35 -04:00
refactor: from guest threads to EE scheduler
This commit is contained in:
@@ -0,0 +1,435 @@
|
||||
#pragma once
|
||||
|
||||
#include "ps2_runtime.h"
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
// This exception is the EE equivalent of a longjmp to the dispatcher. It is
|
||||
// not an error and must only be caught at EeScheduler::run().
|
||||
struct EeDispatcherTransfer final
|
||||
{
|
||||
};
|
||||
|
||||
enum class EeThreadStatus : uint8_t
|
||||
{
|
||||
Running,
|
||||
Ready,
|
||||
Waiting,
|
||||
WaitingSuspended,
|
||||
Suspended,
|
||||
Dormant,
|
||||
};
|
||||
|
||||
enum class EeWaitReason : uint8_t
|
||||
{
|
||||
None,
|
||||
Sleep,
|
||||
Semaphore,
|
||||
EventFlag,
|
||||
VSync,
|
||||
External,
|
||||
Mpeg,
|
||||
};
|
||||
|
||||
struct EeSemaphoreWait
|
||||
{
|
||||
int id = 0;
|
||||
};
|
||||
|
||||
struct EeEventFlagWait
|
||||
{
|
||||
int id = 0;
|
||||
uint32_t bits = 0;
|
||||
uint32_t mode = 0;
|
||||
uint32_t resultAddress = 0;
|
||||
};
|
||||
|
||||
struct EeVSyncWait
|
||||
{
|
||||
uint64_t afterTick = 0;
|
||||
int fixedResult = -1;
|
||||
};
|
||||
|
||||
struct EeExternalWait
|
||||
{
|
||||
uint32_t type = 0;
|
||||
uint64_t token = 0;
|
||||
};
|
||||
|
||||
using EeWaitPayload = std::variant<std::monostate,
|
||||
EeSemaphoreWait,
|
||||
EeEventFlagWait,
|
||||
EeVSyncWait,
|
||||
EeExternalWait>;
|
||||
|
||||
struct EeWaitState
|
||||
{
|
||||
EeWaitReason reason = EeWaitReason::None;
|
||||
EeWaitPayload payload{};
|
||||
std::function<void(R5900Context &)> completion;
|
||||
};
|
||||
|
||||
enum class GuestInvocationKind : uint8_t
|
||||
{
|
||||
Interrupt,
|
||||
Alarm,
|
||||
GsCallback,
|
||||
RpcCallback,
|
||||
SyscallOverride,
|
||||
ExitHandler,
|
||||
HleCall,
|
||||
};
|
||||
|
||||
struct GuestInvocation
|
||||
{
|
||||
GuestInvocationKind kind = GuestInvocationKind::Interrupt;
|
||||
uint64_t sequence = 0;
|
||||
uint64_t tag = 0;
|
||||
R5900Context context{};
|
||||
std::function<void(const R5900Context &, R5900Context &)> onComplete;
|
||||
};
|
||||
|
||||
struct GuestThread
|
||||
{
|
||||
int id = 0;
|
||||
R5900Context context{};
|
||||
uint32_t entry = 0;
|
||||
uint32_t stack = 0;
|
||||
uint32_t stackSize = 0;
|
||||
uint32_t gp = 0;
|
||||
uint32_t attr = 0;
|
||||
uint32_t option = 0;
|
||||
uint32_t arg = 0;
|
||||
int initialPriority = 0;
|
||||
int currentPriority = 0;
|
||||
EeThreadStatus status = EeThreadStatus::Dormant;
|
||||
int suspendCount = 0;
|
||||
uint32_t wakeupCount = 0;
|
||||
bool ownsStack = false;
|
||||
uint32_t tlsBase = 0;
|
||||
EeWaitState wait{};
|
||||
std::function<void(R5900Context &)> resumeCompletion;
|
||||
std::vector<GuestInvocation> invocations;
|
||||
|
||||
[[nodiscard]] R5900Context &activeContext()
|
||||
{
|
||||
return invocations.empty() ? context : invocations.back().context;
|
||||
}
|
||||
|
||||
[[nodiscard]] const R5900Context &activeContext() const
|
||||
{
|
||||
return invocations.empty() ? context : invocations.back().context;
|
||||
}
|
||||
};
|
||||
|
||||
struct EeSemaphore
|
||||
{
|
||||
int id = 0;
|
||||
int count = 0;
|
||||
int maxCount = 0;
|
||||
int initCount = 0;
|
||||
uint32_t attr = 0;
|
||||
uint32_t option = 0;
|
||||
std::deque<int> waiters;
|
||||
};
|
||||
|
||||
struct EeEventFlag
|
||||
{
|
||||
int id = 0;
|
||||
uint32_t attr = 0;
|
||||
uint32_t option = 0;
|
||||
uint32_t initBits = 0;
|
||||
uint32_t bits = 0;
|
||||
std::deque<int> waiters;
|
||||
};
|
||||
|
||||
struct EeAlarm
|
||||
{
|
||||
int id = 0;
|
||||
uint16_t ticks = 0;
|
||||
uint32_t handler = 0;
|
||||
uint32_t argument = 0;
|
||||
uint32_t gp = 0;
|
||||
uint32_t sp = 0;
|
||||
};
|
||||
|
||||
struct EeIrqHandler
|
||||
{
|
||||
int id = 0;
|
||||
uint32_t cause = 0;
|
||||
uint32_t handler = 0;
|
||||
uint32_t argument = 0;
|
||||
uint32_t gp = 0;
|
||||
uint32_t sp = 0;
|
||||
bool enabled = true;
|
||||
int order = 0;
|
||||
};
|
||||
|
||||
struct EeThreadSnapshot
|
||||
{
|
||||
int id = 0;
|
||||
uint32_t pc = 0;
|
||||
uint32_t entry = 0;
|
||||
uint32_t stack = 0;
|
||||
uint32_t stackSize = 0;
|
||||
uint32_t gp = 0;
|
||||
int initialPriority = 0;
|
||||
int currentPriority = 0;
|
||||
EeThreadStatus status = EeThreadStatus::Dormant;
|
||||
EeWaitReason waitReason = EeWaitReason::None;
|
||||
int waitId = 0;
|
||||
int suspendCount = 0;
|
||||
uint32_t wakeupCount = 0;
|
||||
};
|
||||
|
||||
struct EeSemaphoreSnapshot
|
||||
{
|
||||
int id = 0;
|
||||
int count = 0;
|
||||
int maxCount = 0;
|
||||
uint32_t waiters = 0;
|
||||
};
|
||||
|
||||
struct EeEventFlagSnapshot
|
||||
{
|
||||
int id = 0;
|
||||
uint32_t bits = 0;
|
||||
uint32_t initBits = 0;
|
||||
uint32_t attr = 0;
|
||||
uint32_t waiters = 0;
|
||||
};
|
||||
|
||||
struct EeKernelSnapshot
|
||||
{
|
||||
uint64_t sequence = 0;
|
||||
int runningThreadId = 0;
|
||||
std::vector<EeThreadSnapshot> threads;
|
||||
std::vector<EeSemaphoreSnapshot> semaphores;
|
||||
std::vector<EeEventFlagSnapshot> eventFlags;
|
||||
};
|
||||
|
||||
enum class EeEventType : uint8_t
|
||||
{
|
||||
Stop,
|
||||
VBlankStart,
|
||||
VBlankEnd,
|
||||
Dmac,
|
||||
ExternalWake,
|
||||
Alarm,
|
||||
};
|
||||
|
||||
struct EeEvent
|
||||
{
|
||||
EeEventType type = EeEventType::ExternalWake;
|
||||
uint32_t id = 0;
|
||||
uint64_t value = 0;
|
||||
};
|
||||
|
||||
struct EeThreadCreateParams
|
||||
{
|
||||
uint32_t attr = 0;
|
||||
uint32_t entry = 0;
|
||||
uint32_t stack = 0;
|
||||
uint32_t stackSize = 0;
|
||||
uint32_t gp = 0;
|
||||
int priority = 0;
|
||||
uint32_t option = 0;
|
||||
};
|
||||
|
||||
class EeScheduler
|
||||
{
|
||||
public:
|
||||
static constexpr int kMainThreadId = 1;
|
||||
static constexpr int kFirstThreadId = 2;
|
||||
static constexpr int kLastThreadId = 255;
|
||||
static constexpr int kPriorityCount = 128;
|
||||
|
||||
explicit EeScheduler(PS2Runtime &runtime);
|
||||
~EeScheduler();
|
||||
|
||||
EeScheduler(const EeScheduler &) = delete;
|
||||
EeScheduler &operator=(const EeScheduler &) = delete;
|
||||
|
||||
void reset(uint8_t *rdram, const R5900Context &mainContext);
|
||||
void run();
|
||||
void requestStop();
|
||||
void postEvent(EeEvent event);
|
||||
[[nodiscard]] bool checkpointDue() const noexcept;
|
||||
[[nodiscard]] bool isExecutingGuest() const noexcept;
|
||||
|
||||
// Kernel object API. All calls except postEvent/requestStop execute on the
|
||||
// EE executor and therefore need no host synchronization.
|
||||
int createThread(const EeThreadCreateParams ¶ms);
|
||||
int deleteThread(int id, uint32_t &ownedStack);
|
||||
int startThread(int id, uint32_t arg, const R5900Context &caller, bool interruptSafe);
|
||||
[[noreturn]] void exitCurrent(bool deleteThread);
|
||||
int terminateThread(int id, uint32_t &ownedStack, bool interruptSafe);
|
||||
int suspendThread(int id, bool interruptSafe);
|
||||
int resumeThread(int id, bool interruptSafe);
|
||||
void sleepCurrent();
|
||||
int wakeupThread(int id, bool interruptSafe);
|
||||
int cancelWakeup(int id);
|
||||
int changePriority(int id, int priority, bool interruptSafe, int &oldPriority);
|
||||
int rotateReadyQueue(int priority, bool interruptSafe);
|
||||
int releaseWait(int id, bool interruptSafe);
|
||||
void transferIfRequested(bool interruptSafe);
|
||||
|
||||
int createSemaphore(int initCount, int maxCount, uint32_t attr, uint32_t option);
|
||||
int deleteSemaphore(int id, bool interruptSafe);
|
||||
int signalSemaphore(int id, bool interruptSafe);
|
||||
int pollSemaphore(int id);
|
||||
void waitSemaphore(int id);
|
||||
|
||||
int createEventFlag(uint32_t initialBits, uint32_t attr, uint32_t option);
|
||||
int deleteEventFlag(int id, bool interruptSafe);
|
||||
int setEventFlag(int id, uint32_t bits, bool interruptSafe);
|
||||
int clearEventFlag(int id, uint32_t mask);
|
||||
int pollEventFlag(int id, uint32_t bits, uint32_t mode, uint32_t &observedBits);
|
||||
void waitEventFlag(int id, uint32_t bits, uint32_t mode, uint32_t resultAddress);
|
||||
|
||||
int setAlarm(uint16_t ticks, uint32_t handler, uint32_t argument, uint32_t gp, uint32_t sp);
|
||||
int cancelAlarm(int id);
|
||||
void queueInvocation(GuestInvocation invocation);
|
||||
[[noreturn]] void invokeCurrent(GuestInvocation invocation);
|
||||
[[noreturn]] void invokeCurrentSequence(std::vector<GuestInvocation> invocations);
|
||||
[[nodiscard]] bool hasInvocation(GuestInvocationKind kind, uint64_t tag) const;
|
||||
[[nodiscard]] uint32_t invocationStackTop();
|
||||
|
||||
int addIrqHandler(bool dmac,
|
||||
uint32_t cause,
|
||||
uint32_t handler,
|
||||
bool append,
|
||||
uint32_t argument,
|
||||
uint32_t gp,
|
||||
uint32_t sp);
|
||||
int removeIrqHandler(bool dmac, uint32_t cause, int id);
|
||||
int setIrqHandlerEnabled(bool dmac, int id, bool enabled);
|
||||
int setIrqCauseEnabled(bool dmac, uint32_t cause, bool enabled);
|
||||
void dispatchIrq(bool dmac, uint32_t cause);
|
||||
void setVSyncFlag(uint32_t flagAddress, uint32_t tickAddress);
|
||||
[[nodiscard]] uint64_t currentVSyncTick() const noexcept;
|
||||
uint32_t setGsVSyncCallback(uint32_t callback, uint32_t gp, uint32_t sp);
|
||||
|
||||
[[noreturn]] void waitVSync(uint64_t afterTick, int fixedResult = -1);
|
||||
void completeVSync(uint64_t tick);
|
||||
void completeExternalWait(uint32_t type, uint64_t token, int result);
|
||||
[[noreturn]] void waitExternal(EeWaitReason reason,
|
||||
uint32_t type,
|
||||
uint64_t token,
|
||||
std::function<void(R5900Context &)> completion = {});
|
||||
|
||||
[[nodiscard]] GuestThread *thread(int id);
|
||||
[[nodiscard]] const GuestThread *thread(int id) const;
|
||||
[[nodiscard]] EeSemaphore *semaphore(int id);
|
||||
[[nodiscard]] const EeSemaphore *semaphore(int id) const;
|
||||
[[nodiscard]] EeEventFlag *eventFlag(int id);
|
||||
[[nodiscard]] const EeEventFlag *eventFlag(int id) const;
|
||||
[[nodiscard]] GuestThread *currentThread();
|
||||
[[nodiscard]] const GuestThread *currentThread() const;
|
||||
[[nodiscard]] int currentThreadId() const noexcept;
|
||||
[[nodiscard]] R5900Context *currentContext();
|
||||
[[nodiscard]] uint8_t *rdram() const noexcept;
|
||||
|
||||
// Direct syscall tests use the same main-thread record without starting a
|
||||
// second executor. Production execution calls reset() before run().
|
||||
void bindMainContextForSyscall(R5900Context &ctx, uint8_t *rdram);
|
||||
|
||||
[[nodiscard]] EeKernelSnapshot snapshot() const;
|
||||
void publishSnapshot();
|
||||
|
||||
private:
|
||||
struct ScheduledEvent
|
||||
{
|
||||
std::chrono::steady_clock::time_point deadline{};
|
||||
EeEvent event{};
|
||||
uint64_t sequence = 0;
|
||||
};
|
||||
|
||||
void assertExecutor() const;
|
||||
[[nodiscard]] int allocateThreadId();
|
||||
GuestThread &acquireInvocationThread();
|
||||
void enqueueReady(GuestThread &thread, bool front = false);
|
||||
void removeReady(GuestThread &thread);
|
||||
[[nodiscard]] GuestThread *selectReady();
|
||||
void makeRunning(GuestThread &thread);
|
||||
void makeDormant(GuestThread &thread);
|
||||
void removeFromWaitObject(GuestThread &thread);
|
||||
[[noreturn]] void blockCurrent(EeWaitState wait);
|
||||
void makeReady(GuestThread &thread, int result, bool interruptSafe);
|
||||
void requestPreemptionIfHigher(const GuestThread &readyThread, bool interruptSafe);
|
||||
void applyPendingPreemption();
|
||||
void processPendingEvents();
|
||||
void processDueDeadlines();
|
||||
void processEvent(const EeEvent &event);
|
||||
void finishEventWaiters(EeEventFlag &flag, bool interruptSafe);
|
||||
[[nodiscard]] static bool eventCondition(uint32_t current, uint32_t requested, uint32_t mode);
|
||||
static int waitObjectId(const EeWaitState &wait);
|
||||
void writeGuestU32(uint32_t address, uint32_t value);
|
||||
void waitForEvent();
|
||||
void scheduleEvent(std::chrono::steady_clock::time_point deadline, EeEvent event);
|
||||
void updateNextDeadline();
|
||||
void copyMainContextToRuntime();
|
||||
|
||||
PS2Runtime &m_runtime;
|
||||
uint8_t *m_rdram = nullptr;
|
||||
std::array<std::deque<int>, kPriorityCount> m_readyQueues{};
|
||||
std::unordered_map<int, GuestThread> m_threads;
|
||||
std::unordered_map<int, EeSemaphore> m_semaphores;
|
||||
std::unordered_map<int, EeEventFlag> m_eventFlags;
|
||||
std::unordered_map<int, EeAlarm> m_alarms;
|
||||
std::unordered_map<int, EeIrqHandler> m_intcHandlers;
|
||||
std::unordered_map<int, EeIrqHandler> m_dmacHandlers;
|
||||
int m_nextThreadId = kFirstThreadId;
|
||||
int m_nextInvocationThreadId = -1;
|
||||
int m_nextSemaphoreId = 1;
|
||||
int m_nextEventFlagId = 1;
|
||||
int m_nextAlarmId = 1;
|
||||
int m_nextIntcHandlerId = 1;
|
||||
int m_nextDmacHandlerId = 1;
|
||||
int m_intcHeadOrder = 0;
|
||||
int m_intcTailOrder = 1000;
|
||||
int m_dmacHeadOrder = 0;
|
||||
int m_dmacTailOrder = 1000;
|
||||
uint32_t m_enabledIntcMask = 0xFFFFFFFFu;
|
||||
uint32_t m_enabledDmacMask = 0xFFFFFFFFu;
|
||||
int m_currentThreadId = 0;
|
||||
bool m_rescheduleRequested = false;
|
||||
bool m_insideInterrupt = false;
|
||||
std::thread::id m_executorThread{};
|
||||
std::atomic<bool> m_running{false};
|
||||
std::atomic<bool> m_guestExecuting{false};
|
||||
std::atomic<bool> m_stopRequested{false};
|
||||
std::atomic<bool> m_checkpointPending{false};
|
||||
|
||||
mutable std::mutex m_eventMutex;
|
||||
std::condition_variable m_eventCv;
|
||||
std::deque<EeEvent> m_events;
|
||||
std::vector<ScheduledEvent> m_deadlines;
|
||||
std::deque<GuestInvocation> m_pendingInvocations;
|
||||
uint64_t m_eventSequence = 0;
|
||||
uint64_t m_invocationSequence = 0;
|
||||
uint64_t m_vsyncTick = 0;
|
||||
uint32_t m_vsyncFlagAddress = 0;
|
||||
uint32_t m_vsyncTickAddress = 0;
|
||||
uint32_t m_gsVSyncCallback = 0;
|
||||
uint32_t m_gsVSyncCallbackGp = 0;
|
||||
uint32_t m_gsVSyncCallbackSp = 0;
|
||||
std::unordered_map<uint64_t, uint32_t> m_invocationStackTops;
|
||||
std::atomic<int64_t> m_nextDeadlineNanoseconds{0};
|
||||
|
||||
mutable std::mutex m_snapshotMutex;
|
||||
EeKernelSnapshot m_snapshot;
|
||||
uint64_t m_snapshotSequence = 0;
|
||||
};
|
||||
@@ -204,20 +204,16 @@ struct GSRegisters
|
||||
uint64_t extdata; // External data
|
||||
uint64_t extwrite; // External write
|
||||
uint64_t bgcolor; // Background color
|
||||
// Status. Concurrency contract: the vsync worker thread toggles the FIELD bit
|
||||
// (bit 13) once per tick; guest threads issue write-one-to-clear writes against
|
||||
// the SIGNAL/FINISH status bits (0..1) via the MMIO path; the GIF sets SIGNAL
|
||||
// and FINISH from yet another thread. All three interleave, so this register
|
||||
// must be updated with atomic RMWs only (no load-then-store pairs anywhere).
|
||||
// Status remains atomic because the renderer/UI can observe it while the
|
||||
// single EE executor updates SIGNAL, FINISH and FIELD.
|
||||
std::atomic<uint64_t> csr;
|
||||
std::atomic<uint64_t> vsyncTick;
|
||||
uint64_t imr; // Interrupt mask
|
||||
uint64_t busdir; // Bus direction
|
||||
uint64_t siglblid; // Signal label ID
|
||||
};
|
||||
static_assert(sizeof(GSRegisters) == (19u * sizeof(uint64_t)), "GSRegisters layout changed unexpectedly");
|
||||
static_assert(sizeof(GSRegisters) == (20u * sizeof(uint64_t)), "GSRegisters layout changed unexpectedly");
|
||||
static_assert(alignof(GSRegisters) == alignof(uint64_t), "GSRegisters alignment must remain 64-bit");
|
||||
// CSR is written by the vsync worker while guest threads concurrently read/write it
|
||||
// (MMIO) and the GIF sets SIGNAL/FINISH; a lock-free atomic keeps that path wait-free.
|
||||
static_assert(std::atomic<uint64_t>::is_always_lock_free, "GS CSR atomic must be lock-free on all supported targets");
|
||||
|
||||
// PS2 VIF (VPU Interface) registers
|
||||
|
||||
Reference in New Issue
Block a user