Timers: Improve emulation accuracy

Fix overflow handling, set-counter-after-target, handle non-pulse mode
(nothing seems to use this), and don't compare immediately after
writing.
pull/3780/head
Stenzek 3 days ago
parent 9c7c23f48f
commit 57b952ab2d
No known key found for this signature in database

@ -24,6 +24,7 @@ namespace Timers {
namespace {
static constexpr u32 NUM_TIMERS = 3;
static constexpr u32 TIMER_OVERFLOW = 0x10000;
enum class SyncMode : u8
{
@ -211,8 +212,8 @@ TickCount Timers::GetTicksUntilIRQ(u32 timer)
TickCount ticks_until_irq = std::numeric_limits<TickCount>::max();
if (cs.mode.irq_at_target && cs.counter < cs.target)
ticks_until_irq = static_cast<TickCount>(cs.target - cs.counter);
if (cs.mode.irq_on_overflow)
ticks_until_irq = std::min(ticks_until_irq, static_cast<TickCount>(0xFFFFu - cs.counter));
if (cs.mode.irq_on_overflow && !cs.mode.reset_at_target)
ticks_until_irq = std::min(ticks_until_irq, static_cast<TickCount>(TIMER_OVERFLOW - cs.counter));
return ticks_until_irq;
}
@ -220,29 +221,59 @@ TickCount Timers::GetTicksUntilIRQ(u32 timer)
void Timers::AddTicks(u32 timer, TickCount count)
{
CounterState& cs = s_state.counters[timer];
const u32 old_counter = cs.counter;
cs.counter += static_cast<u32>(count);
CheckForIRQ(timer, old_counter);
// Needs slow path if using non-repeat mode, because unless it's resetting an even number of times
// the IRQ might not actually trigger, and we need to stop in that middle state.
if (cs.mode.irq_pulse_n)
{
do
{
const u32 reset_at = (cs.mode.reset_at_target && cs.counter < cs.target) ? cs.target : TIMER_OVERFLOW;
const u32 add = std::min(reset_at - cs.counter, static_cast<u32>(count));
const u32 old_counter = cs.counter;
cs.counter += add;
count -= static_cast<TickCount>(add);
CheckForIRQ(timer, old_counter);
} while (count > 0);
}
else
{
const u32 old_counter = cs.counter;
cs.counter += static_cast<u32>(count);
CheckForIRQ(timer, old_counter);
}
}
void Timers::CheckForIRQ(u32 timer, u32 old_counter)
{
CounterState& cs = s_state.counters[timer];
// The counter can be set above the target, in which case it _will_ wrap around and trigger an IRQ.
bool wrapped_overflow = false;
if (cs.counter >= TIMER_OVERFLOW)
{
wrapped_overflow = (!cs.mode.reset_at_target || old_counter >= cs.target);
old_counter = 0;
}
bool interrupt_request = false;
if (cs.counter >= cs.target && (old_counter < cs.target || cs.target == 0))
{
interrupt_request |= cs.mode.irq_at_target;
cs.mode.reached_target = true;
if (cs.mode.reset_at_target && cs.target > 0)
// This is pretty janky. If you set the target to 0xFFFF, the overflow IRQ still needs to fire.
// Presumably, the reset happens after the next increment. Also consider the case where the
// target is resetting below 0xFFFF, and the counter is incremented by a large amount. In this
// second case, the overflow IRQ shouldn't trigger.
if (cs.mode.reset_at_target && cs.target > 0 && cs.target != 0xFFFFu)
cs.counter %= cs.target;
}
if (cs.counter >= 0xFFFF)
if (cs.counter >= TIMER_OVERFLOW || wrapped_overflow)
{
interrupt_request |= cs.mode.irq_on_overflow;
cs.mode.reached_overflow = true;
cs.counter %= 0xFFFFu;
cs.counter &= 0xFFFFu;
}
if (interrupt_request)
@ -376,10 +407,9 @@ void Timers::WriteRegister(u32 offset, u32 value)
{
case 0x00:
{
const u32 old_counter = cs.counter;
// Writing the new counter does not compare against the target value.
DEBUG_LOG("Timer {} write counter {}", timer_index, value);
cs.counter = value & u32(0xFFFF);
CheckForIRQ(timer_index, old_counter);
if (timer_index == 2 || !cs.external_counting_enabled)
UpdateSysClkEvent();
}
@ -442,7 +472,6 @@ void Timers::WriteRegister(u32 offset, u32 value)
{
DEBUG_LOG("Timer {} write target 0x{:04X}", timer_index, ZeroExtend32(Truncate16(value)));
cs.target = value & u32(0xFFFF);
CheckForIRQ(timer_index, cs.counter);
if (timer_index == 2 || !cs.external_counting_enabled)
UpdateSysClkEvent();
}

Loading…
Cancel
Save