Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions ps2xRuntime/src/lib/Kernel/Stubs/Helpers/Support.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
#include <algorithm>
#include <cctype>

namespace ps2_syscalls
{
void raisePendingIntc(uint32_t cause);
}

namespace
{
constexpr uint32_t kCdSectorSize = 2048;
Expand Down Expand Up @@ -1436,6 +1441,43 @@ namespace
ps2_syscalls::dispatchDmacHandlersForCause(rdram, runtime, completedCause);
}

// VIF1 (channel 0x10009000) completes with INTC cause 5, and sce libdma
// registers that cause-5 handler AFTER the kick returns, so delivery must
// be deferred (drained next tick), not synchronous here.
//
// This raises cause 5 unconditionally on every VIF1 kick, not only when an
// interrupt was actually requested. That is a deliberate over-approximation.
// Honest fidelity gap: when a cause-5 handler is registered, this
// unconditional raise delivers on every VIF1 kick regardless of whether real
// hardware would have raised the interrupt.
//
// Why not gate it? The runtime DOES decode both relevant interrupt sources,
// but neither is usable as a clean per-kick gate at this call site:
// - The VIFcode "i" bit IS decoded, into vif*_regs.stat bit 11 (INT), at
// ps2_vif1_interpreter.cpp:311-312 (VIF1) and :79-80 (VIF0). But that
// flag is STICKY -- cleared only by FBRST (RST memset at
// ps2_memory.cpp:1018, or STC at :1024) or a CPU write -- and currently
// inert: nothing in this runtime consumes the INT bit (the only stat
// reads, ps2_vif1_interpreter.cpp:380/398, test bit 7 / DBF; the guest
// MMIO read path does not expose vif1_regs.stat). With no per-kick clear
// point and no consumer, it is not a clean edge signal; gating on it
// would need new snapshot-and-clear semantics, and getting that wrong
// reintroduces the very interrupt-drop this raise exists to avoid.
// - The per-tag DMAtag IRQ bit IS decoded, at ps2_memory.cpp:1216, but in
// the register-path DMA chain walker; this sce-libdma stub kick path
// parses only the HEAD tag (for logging).
// Over-raising is chosen because it is benign when unused: the pending latch
// is level-triggered, so an unconsumed cause 5 ages out in
// kPendingIntcMaxAgeTicks (~2 s) drain ticks with no side effect, while DQ8's
// per-frame VIF1 display-list path consumes cause 5 every frame. A mis-gated
// raise, by contrast, would silently DROP legitimate interrupts. If the
// runtime ever makes the INT stat bit a clean per-kick edge (clear-on-read or
// a consumer with defined clear semantics), gate here instead.
if (channelBase == 0x10009000u) // VIF1
{
ps2_syscalls::raisePendingIntc(5u);
}

return 0;
}

Expand Down
108 changes: 105 additions & 3 deletions ps2xRuntime/src/lib/Kernel/Syscalls/Interrupt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
#include "ps2_log.h"
#include "Stubs/GS.h"

#include <bit>

namespace ps2_syscalls
{
namespace interrupt_state
Expand All @@ -23,6 +25,11 @@ namespace ps2_syscalls
uint32_t g_enabled_dmac_mask = 0xFFFFFFFFu;
uint64_t g_vsync_tick_counter = 0u;
VSyncFlagRegistration g_vsync_registration{};

std::atomic<uint32_t> g_pending_intc_causes{0u}; // bitmask, one pending bit per cause
std::atomic<uint32_t> g_pending_intc_age[32] = {}; // drain ticks since raise, per cause
// The age entries are atomic because raisePendingIntc (any thread) resets
// an age while the interrupt worker thread increments it in drainPendingIntc.
}

using namespace interrupt_state;
Expand Down Expand Up @@ -95,19 +102,19 @@ namespace ps2_syscalls
return (s_cachedStackTop != 0u) ? s_cachedStackTop : (PS2_RAM_SIZE - 0x10u);
}

static void dispatchIntcHandlersForCause(uint8_t *rdram, PS2Runtime *runtime, uint32_t cause)
static int dispatchIntcHandlersForCause(uint8_t *rdram, PS2Runtime *runtime, uint32_t cause)
{
if (!rdram || !runtime)
{
return;
return 0;
}

std::vector<IrqHandlerInfo> handlers;
{
std::lock_guard<std::mutex> lock(g_irq_handler_mutex);
if (cause < 32u && (g_enabled_intc_mask & (1u << cause)) == 0u)
{
return;
return 0;
}

handlers.reserve(g_intcHandlers.size());
Expand All @@ -132,6 +139,7 @@ namespace ps2_syscalls
{ return a.order < b.order; });
}

int dispatchedCount = 0;
for (const IrqHandlerInfo &info : handlers)
{
if (!runtime->hasFunction(info.handler))
Expand All @@ -156,6 +164,7 @@ namespace ps2_syscalls
continue;
}

++dispatchedCount;
try
{
R5900Context irqCtx{};
Expand Down Expand Up @@ -194,6 +203,98 @@ namespace ps2_syscalls
}
}
}

return dispatchedCount;
}

void raisePendingIntc(uint32_t cause)
{
if (cause >= 32u || cause == kIntcVblankStart || cause == kIntcVblankEnd)
{
return;
}

const uint32_t bit = 1u << cause;
const uint32_t prev = g_pending_intc_causes.fetch_or(bit, std::memory_order_acq_rel);
if ((prev & bit) == 0u)
{
g_pending_intc_age[cause].store(0u, std::memory_order_relaxed); // fresh raise: age restarts
PS2_IF_AGRESSIVE_LOGS({
static std::atomic<uint32_t> s_raiseLogCount{0u};
const uint32_t logIndex = s_raiseLogCount.fetch_add(1u, std::memory_order_relaxed);
if (logIndex < 16u || (logIndex % 256u) == 0u)
{
RUNTIME_LOG("[INTC:raise] cause=" << cause);
}
});
}
}

void drainPendingIntc(uint8_t *rdram, PS2Runtime *runtime)
{
uint32_t pending = g_pending_intc_causes.load(std::memory_order_acquire);
while (pending != 0u)
{
const uint32_t cause = static_cast<uint32_t>(std::countr_zero(pending));
const uint32_t bit = 1u << cause;
pending &= ~bit;

const int ran = dispatchIntcHandlersForCause(rdram, runtime, cause);
if (ran > 0)
{
// Level-triggered by design: delivery clears the single pending
// bit. If another raise of this same cause lands mid-drain
// (between the dispatch above and this fetch_and), the two raises
// collapse into one delivery. Accepted under the
// level-triggered design -- a set bit means "at least one pending",
// not a count -- a deliberate tradeoff, not a lost-wakeup bug.
g_pending_intc_causes.fetch_and(~bit, std::memory_order_acq_rel);
g_pending_intc_age[cause].store(0u, std::memory_order_relaxed);
PS2_IF_AGRESSIVE_LOGS({
static std::atomic<uint32_t> s_deliverLogCount{0u};
const uint32_t logIndex = s_deliverLogCount.fetch_add(1u, std::memory_order_relaxed);
if (logIndex < 16u || (logIndex % 256u) == 0u)
{
RUNTIME_LOG("[INTC:deliver] cause=" << cause << " handlers=" << ran);
}
});
}
else if (g_pending_intc_age[cause].fetch_add(1u, std::memory_order_relaxed) + 1u > kPendingIntcMaxAgeTicks)
{
g_pending_intc_causes.fetch_and(~bit, std::memory_order_acq_rel);
g_pending_intc_age[cause].store(0u, std::memory_order_relaxed);
PS2_IF_AGRESSIVE_LOGS({
static std::atomic<uint32_t> s_dropLogCount{0u};
const uint32_t logIndex = s_dropLogCount.fetch_add(1u, std::memory_order_relaxed);
if (logIndex < 16u || (logIndex % 256u) == 0u)
{
RUNTIME_LOG("[INTC:drop] cause=" << cause << " aged out with no registered/enabled handler");
}
});
}
}
}

void resetInterruptHandlerState()
{
{
std::lock_guard<std::mutex> lock(g_irq_handler_mutex);
g_intcHandlers.clear();
g_dmacHandlers.clear();
g_nextIntcHandlerId = 1;
g_nextDmacHandlerId = 1;
g_intc_head_order = 0;
g_intc_tail_order = 1000;
g_dmac_head_order = 0;
g_dmac_tail_order = 1000;
g_enabled_intc_mask = 0xFFFFFFFFu;
g_enabled_dmac_mask = 0xFFFFFFFFu;
}
g_pending_intc_causes.store(0u, std::memory_order_release);
for (auto &age : g_pending_intc_age)
{
age.store(0u, std::memory_order_relaxed);
}
}

void dispatchDmacHandlersForCause(uint8_t *rdram, PS2Runtime *runtime, uint32_t cause)
Expand Down Expand Up @@ -358,6 +459,7 @@ namespace ps2_syscalls
dispatchIntcHandlersForCause(rdram, runtime, kIntcVblankStart);
std::this_thread::sleep_for(std::chrono::microseconds(500));
dispatchIntcHandlersForCause(rdram, runtime, kIntcVblankEnd);
drainPendingIntc(rdram, runtime);
}
}

Expand Down
11 changes: 11 additions & 0 deletions ps2xRuntime/src/lib/Kernel/Syscalls/Interrupt.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#pragma once

#include <atomic>
#include <condition_variable>
#include <cstdint>
#include "ps2_syscalls.h"

namespace ps2_syscalls
Expand All @@ -24,9 +26,18 @@ namespace ps2_syscalls
extern uint32_t g_enabled_dmac_mask;
extern uint64_t g_vsync_tick_counter;
extern VSyncFlagRegistration g_vsync_registration;
extern std::atomic<uint32_t> g_pending_intc_causes;
constexpr uint32_t kPendingIntcMaxAgeTicks = 120u;
}

void dispatchDmacHandlersForCause(uint8_t *rdram, PS2Runtime *runtime, uint32_t cause);
void raisePendingIntc(uint32_t cause);
void drainPendingIntc(uint8_t *rdram, PS2Runtime *runtime);
// Test-support: restore all INTC/DMAC handler bookkeeping to process-start
// defaults so regression tests are order-independent. Must live in
// Interrupt.cpp: the handler tables are static per-TU and the pending-age
// array is not header-exposed, so only that TU can reset the live state.
void resetInterruptHandlerState();
void EnsureVSyncWorkerRunning(uint8_t *rdram, PS2Runtime *runtime);
uint64_t GetCurrentVSyncTick();
void stopInterruptWorker();
Expand Down
Loading