From 2ac2ce632082ff8b7683f0380361ddcbc410bdbc Mon Sep 17 00:00:00 2001 From: Sinan KARAKAYA Date: Tue, 18 Aug 2026 15:33:24 +0200 Subject: [PATCH] Start the main thread with COP0 Status.IE set Guest code reads COP0 Status to decide whether interrupts are enabled, and two different bits are involved: IE (bit 0) the architectural MIPS interrupt enable, set once by the kernel during boot and normally left set. EIE (bit 16) the EE-specific enable that `ei` and `di` toggle. We never execute the boot ROM, so nothing was setting either one, and R5900Context started with Status at zero. That is not cosmetic. libkernel's StartThread opens with `mfc0 Status; xori 1; andi 1` and bails out with -1 when IE is clear -- its "you must call iStartThread from an interrupt handler" guard. With Status at zero that guard fired every time, so every StartThread failed. Dragon Quest VIII hits this during boot: it creates its CD streaming thread, gets -1, prints "Can't start thread for streaming." and then deadlocks with every thread blocked and none runnable. Nothing in the runtime logs anything, because from its point of view the guest simply asked a question and got an answer. EIE matters for the matching reason: DIntr reports whether it was set so the caller knows whether to pair it with an EIntr. Starting at zero makes DIntr always answer "already disabled", so the re-enable never happens. Two changes, both needed: - R5900Context's constructor now sets Status to EIE | IE rather than 0. BEV is deliberately left clear -- that selects the boot exception vectors, which is the pre-handoff state, not this one. - PS2Runtime's constructor no longer memsets m_cpuContext. R5900Context already zeroes itself before applying its reset values, so the memset only threw those values away. Threads created later were unaffected because EeScheduler::startThread assigns `R5900Context{}`, which is why this presented as "the main thread cannot start threads" rather than something more obviously global. Co-Authored-By: Claude Opus 5 --- ps2xRuntime/include/ps2_runtime.h | 31 +++++++++++++++++++++++++---- ps2xRuntime/src/lib/ps2_runtime.cpp | 10 +++++++++- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/ps2xRuntime/include/ps2_runtime.h b/ps2xRuntime/include/ps2_runtime.h index a899408..961e78e 100644 --- a/ps2xRuntime/include/ps2_runtime.h +++ b/ps2xRuntime/include/ps2_runtime.h @@ -150,10 +150,33 @@ struct alignas(16) R5900Context // Reset COP0 registers cop0_random = 47; // Start at maximum value - // cop0_status = 0x400000; // BEV set, ERL clear, kernel mode - // 0x00400000 = BEV (Boot Exception Vectors). - // 0x00000000 = Normal mode (after BIOS handoff). - cop0_status = 0x00000000; + // Status as the EE kernel leaves it when it hands control to the game, + // which is the state recompiled code starts in -- we never execute the + // boot ROM that would otherwise set this up. + // + // Both interrupt-enable bits matter, and they are not the same bit: + // + // IE (bit 0) the architectural MIPS interrupt enable. The kernel + // sets it once during boot and it normally stays set. + // EIE (bit 16) the EE-specific enable that the `ei` and `di` + // instructions toggle. + // + // Interrupts are only really on when both are set, and guest code reads + // them separately. libkernel's StartThread, for instance, opens with + // `mfc0 Status; xori 1; andi 1` and refuses to run when IE is clear -- + // that is its "you must call iStartThread from an interrupt handler" + // guard. Leaving Status at zero made that guard fire forever, so every + // StartThread returned -1 and any game that creates a thread stalled + // with no diagnostic. + // + // EIE matters for the matching reason: DIntr reports whether it was set + // so the caller knows whether to pair it with an EIntr. Starting at zero + // makes DIntr always answer "already disabled" and the re-enable never + // happens. + // + // BEV (0x00400000) is deliberately not set: that selects the boot + // exception vectors, which is the pre-handoff state, not this one. + cop0_status = 0x00010001; // EIE | IE cop0_prid = 0x00002e20; // CPU ID for R5900 in_delay_slot = false; diff --git a/ps2xRuntime/src/lib/ps2_runtime.cpp b/ps2xRuntime/src/lib/ps2_runtime.cpp index ddacb0c..e5d23d9 100644 --- a/ps2xRuntime/src/lib/ps2_runtime.cpp +++ b/ps2xRuntime/src/lib/ps2_runtime.cpp @@ -491,7 +491,15 @@ PS2Runtime::PS2Runtime() } #endif - std::memset(&m_cpuContext, 0, sizeof(m_cpuContext)); + // Assign a default-constructed context rather than memset-ing this one. + // R5900Context's constructor already zeroes itself and then applies the + // architectural reset values on top -- COP0 Status, PRId, Random. A raw + // memset here silently threw those away, leaving Status at 0 for the main + // thread while every thread created later, which goes through + // `target->context = R5900Context{}` in EeScheduler::startThread, got the + // correct values. Guest code reads Status.IE to decide whether interrupts + // are enabled, so the main thread believed they were permanently off. + m_cpuContext = R5900Context{}; // R0 is always zero in MIPS m_cpuContext.r[0] = _mm_set1_epi32(0);