diff --git a/src/core/psxcounters.cc b/src/core/psxcounters.cc index 1c4ba9bae..70ab9d65c 100644 --- a/src/core/psxcounters.cc +++ b/src/core/psxcounters.cc @@ -43,8 +43,18 @@ inline void PCSX::Counters::writeCounterInternal(uint32_t index, uint32_t value) m_rcnts[index].cycleStart = PCSX::g_emulator->m_cpu->m_regs.cycle; m_rcnts[index].cycleStart -= value * m_rcnts[index].rate; - // TODO: <=. - if (value < m_rcnts[index].target) { + // Hardware tested (SCPH-5501): counter reaches target value briefly before + // resetting. Fast reads can observe the target value; slow reads (printf) cannot. + // The comparison boundary needs more precise measurement to determine if + // reset happens on the same cycle as reaching target or the next cycle. + // TODO: determine exact reset timing with cycle-accurate measurement. + // Schedule next event: target hit first (for flag/IRQ), then overflow. + // If RcCountToTarget is not set, the counter doesn't reset at target, + // but bit 11 (RcCountEqTarget) is still set when the counter passes + // the target value. However, if target == 0xFFFF, the target and + // overflow events coincide - skip straight to overflow to avoid + // redundant processing. + if (value < m_rcnts[index].target && m_rcnts[index].target != 0xffff) { m_rcnts[index].cycle = m_rcnts[index].target * m_rcnts[index].rate; m_rcnts[index].counterState = CountToTarget; } else { @@ -132,7 +142,15 @@ void PCSX::Counters::reset(uint32_t index) { m_rcnts[index].mode |= RcOverflow; } - m_rcnts[index].mode |= RcIrqRequest; + // IRQ request flag (bit 10) behavior depends on bit 7 (toggle mode): + // Pulse mode (bit7=0): bit 10 resets to 1 after IRQ (short pulse low) + // Toggle mode (bit7=1): bit 10 toggles (XOR) on each IRQ + // Hardware verified: toggle mode produces different bit10 state than pulse mode. + if (m_rcnts[index].mode & RcIrqToggle) { + m_rcnts[index].mode ^= RcIrqRequest; + } else { + m_rcnts[index].mode |= RcIrqRequest; + } set(); } @@ -179,6 +197,24 @@ void PCSX::Counters::update() { m_hSyncCount++; m_spuSyncCountdown--; + // Counter 0 gate: triggered by Hblank + if (isGateEnabled(0, m_rcnts[0].mode)) { + switch (gateSyncMode(m_rcnts[0].mode)) { + case 1: // Reset at Hblank + case 2: // Reset at Hblank + pause outside + writeCounterInternal(0, 0); + break; + case 3: // Pause until first Hblank, then free run + if (!m_rcnts[0].gateStarted) { + m_rcnts[0].gateStarted = 1; + recalculateRate(0); + writeCounterInternal(0, 0); + set(); + } + break; + } + } + // Update spu. if (m_spuSyncCountdown <= 0) { // Scanlines until next sync @@ -197,6 +233,24 @@ void PCSX::Counters::update() { if (m_hSyncCount == VBlankStart[PCSX::g_emulator->settings.get()]) { setIrq(0x01); PCSX::g_emulator->vsync(); + + // Counter 1 gate: triggered by VBlank start + if (isGateEnabled(1, m_rcnts[1].mode)) { + switch (gateSyncMode(m_rcnts[1].mode)) { + case 1: // Reset at VBlank + case 2: // Reset at VBlank + pause outside + writeCounterInternal(1, 0); + break; + case 3: // Pause until first VBlank, then free run + if (!m_rcnts[1].gateStarted) { + m_rcnts[1].gateStarted = 1; + recalculateRate(1); + writeCounterInternal(1, 0); + set(); + } + break; + } + } } if (m_hSyncCount >= m_HSyncTotal[PCSX::g_emulator->settings.get()]) { @@ -213,17 +267,22 @@ void PCSX::Counters::writeCounter(uint32_t index, uint32_t value) { set(); } -void PCSX::Counters::writeMode(uint32_t index, uint32_t value) { - verboseLog(1, "[RCNT %i] writeMode: %x\n", index, value); - - update(); - m_rcnts[index].mode = value; - m_rcnts[index].irqState = false; - +void PCSX::Counters::recalculateRate(uint32_t index) { + uint16_t value = m_rcnts[index].mode; switch (index) { case 0: if (value & Rc0PixelClock) { - m_rcnts[index].rate = 5; + // Dotclock rate depends on GPU horizontal resolution and video standard. + static constexpr uint32_t dotclockDividers[] = {10, 8, 5, 4, 7, 7}; + auto hres = PCSX::g_emulator->m_gpu->m_display.info.hres; + auto videoMode = PCSX::g_emulator->m_gpu->m_display.info.mode; + uint32_t divider = dotclockDividers[static_cast(hres)]; + uint32_t videoCyclesPerScanline = (videoMode == GPU::CtrlDisplayMode::VM_PAL) ? 3406 : 3413; + uint32_t dotsPerScanline = videoCyclesPerScanline / divider; + uint32_t cpuCyclesPerScanline = (PCSX::g_emulator->m_psxClockSpeed / + (FrameRate[PCSX::g_emulator->settings.get()] * + m_HSyncTotal[PCSX::g_emulator->settings.get()])); + m_rcnts[index].rate = std::max(cpuCyclesPerScanline / dotsPerScanline, 1); } else { m_rcnts[index].rate = 1; } @@ -243,13 +302,38 @@ void PCSX::Counters::writeMode(uint32_t index, uint32_t value) { } else { m_rcnts[index].rate = 1; } - - // TODO: wcount must work. + // Timer 2 sync modes: 0,3 = stop counter; 1,2 = free run + // Hardware verified (SCPH-5501): modes 0,3 produce delta=0, modes 1,2 run normally. if (value & Rc2Disable) { - m_rcnts[index].rate = 0xffffffff; + uint8_t syncMode = gateSyncMode(value); + if (syncMode == 0 || syncMode == 3) { + m_rcnts[index].rate = 0xffffffff; + } } break; } +} + +void PCSX::Counters::writeMode(uint32_t index, uint32_t value) { + verboseLog(1, "[RCNT %i] writeMode: %x\n", index, value); + + update(); + m_rcnts[index].mode = value | RcIrqRequest; // Hardware sets bit 10 on mode write + m_rcnts[index].irqState = false; + + recalculateRate(index); + + // Initialize gate state + m_rcnts[index].gateStarted = 0; + + if (isGateEnabled(index, value)) { + uint8_t syncMode = gateSyncMode(value); + if (syncMode == 2 || syncMode == 3) { + // Mode 2: pause outside blank. Mode 3: pause until first blank. + // Both start paused. + m_rcnts[index].rate = 0xffffffff; + } + } writeCounterInternal(index, 0); set(); diff --git a/src/core/psxcounters.h b/src/core/psxcounters.h index b8c1b3b3f..732a4dfb6 100644 --- a/src/core/psxcounters.h +++ b/src/core/psxcounters.h @@ -36,25 +36,30 @@ class Counters { void set(); void reset(uint32_t index); void calculateHsync(); + void recalculateRate(uint32_t index); struct Rcnt { uint16_t mode, target; uint32_t rate, irq, counterState, irqState; uint64_t cycle, cycleStart; + uint32_t gateStarted; // Gate mode 3: first blank has occurred, counter is free running }; + // Gate mode helpers - derived from mode register, not stored + static bool isGateEnabled(uint32_t index, uint16_t mode) { return (index <= 1) && (mode & RcSyncEnable); } + static uint8_t gateSyncMode(uint16_t mode) { return (mode >> 1) & 3; } + enum { - Rc0Gate = 0x0001, // 0 not implemented - Rc1Gate = 0x0001, // 0 not implemented - Rc2Disable = 0x0001, // 0 partially implemented - RcUnknown1 = 0x0002, // 1 ? - RcUnknown2 = 0x0004, // 2 ? + RcSyncEnable = 0x0001, // 0 Sync/gate enable + Rc2Disable = 0x0001, // 0 Timer 2: modes 0,3 = stop counter + RcSyncMode0 = 0x0002, // 1 Sync mode bit 0 + RcSyncMode1 = 0x0004, // 2 Sync mode bit 1 RcCountToTarget = 0x0008, // 3 RcIrqOnTarget = 0x0010, // 4 RcIrqOnOverflow = 0x0020, // 5 RcIrqRegenerate = 0x0040, // 6 - RcUnknown7 = 0x0080, // 7 ? - Rc0PixelClock = 0x0100, // 8 fake implementation + RcIrqToggle = 0x0080, // 7 IRQ toggle mode (0=pulse, 1=toggle bit10) + Rc0PixelClock = 0x0100, // 8 dotclock (varies with GPU hres) Rc1HSyncClock = 0x0100, // 8 Rc2Unknown8 = 0x0100, // 8 ? Rc0Unknown9 = 0x0200, // 9 ? diff --git a/src/core/sstate.cc b/src/core/sstate.cc index 27107c2a8..af2bce2b8 100644 --- a/src/core/sstate.cc +++ b/src/core/sstate.cc @@ -272,6 +272,7 @@ void PCSX::Counters::serialize(SaveStateWrapper* w) { counters.get().value[i].get().value = m_rcnts[i].irqState; counters.get().value[i].get().value = m_rcnts[i].cycle; counters.get().value[i].get().value = m_rcnts[i].cycleStart; + counters.get().value[i].get().value = m_rcnts[i].gateStarted; } counters.get().value = m_hSyncCount; counters.get().value = m_spuSyncCountdown; @@ -433,20 +434,20 @@ void PCSX::Counters::deserialize(const SaveStateWrapper* w) { m_rcnts[i].irqState = counters.get().value[i].get().value; m_rcnts[i].cycle = counters.get().value[i].get().value; m_rcnts[i].cycleStart = counters.get().value[i].get().value; + m_rcnts[i].gateStarted = counters.get().value[i].get().value; } m_hSyncCount = counters.get().value; m_spuSyncCountdown = counters.get().value; m_psxNextCounter = counters.get().value; calculateHsync(); - // iCB: recalculate target count in case overclock is changed + // Recalculate rates from mode registers (handles overclock changes and dotclock) m_rcnts[3].target = (g_emulator->m_psxClockSpeed / (FrameRate[g_emulator->settings.get()] * m_HSyncTotal[g_emulator->settings.get()])); - if (m_rcnts[1].rate != 1) - m_rcnts[1].rate = - (g_emulator->m_psxClockSpeed / (FrameRate[g_emulator->settings.get()] * - m_HSyncTotal[g_emulator->settings.get()])); + for (unsigned i = 0; i < 3; i++) { + recalculateRate(i); + } m_audioFrames = g_emulator->m_spu->getCurrentFrames(); } diff --git a/src/core/sstate.h b/src/core/sstate.h index 317b51685..2822be04d 100644 --- a/src/core/sstate.h +++ b/src/core/sstate.h @@ -257,8 +257,9 @@ typedef Protobuf::Field RcntCo typedef Protobuf::Field RcntIRQState; typedef Protobuf::Field RcntCycle; typedef Protobuf::Field RcntCycleStart; +typedef Protobuf::Field RcntGateStarted; typedef Protobuf::Message + RcntCycle, RcntCycleStart, RcntGateStarted> Rcnt; typedef Protobuf::RepeatedField Rcnts; typedef Protobuf::Field HSyncCount; diff --git a/src/mips/common/hardware/counters.h b/src/mips/common/hardware/counters.h index c3d610487..2efd392f7 100644 --- a/src/mips/common/hardware/counters.h +++ b/src/mips/common/hardware/counters.h @@ -38,3 +38,19 @@ struct Counter { }; #define COUNTERS ((volatile struct Counter *)0xbf801100) + +enum { + TM_SYNC_EN = 0x0001, + TM_RESET_TARGET = 0x0008, + TM_IRQ_TARGET = 0x0010, + TM_IRQ_OVERFLOW = 0x0020, + TM_IRQ_REPEAT = 0x0040, + TM_IRQ_TOGGLE = 0x0080, + TM_CLK_EXTERNAL = 0x0100, + TM_CLK_DIV8 = 0x0200, + TM_IRQ_REQUEST = 0x0400, + TM_HIT_TARGET = 0x0800, + TM_HIT_OVERFLOW = 0x1000, +}; + +#define TM_SYNC_MODE(n) (((n) & 3) << 1) diff --git a/src/mips/tests/Makefile b/src/mips/tests/Makefile index eb86927a2..83fd8656a 100644 --- a/src/mips/tests/Makefile +++ b/src/mips/tests/Makefile @@ -7,6 +7,7 @@ all: $(MAKE) -C memcpy all $(MAKE) -C memset all $(MAKE) -C pcdrv all + $(MAKE) -C timers all clean: $(MAKE) -C basic clean @@ -17,3 +18,4 @@ clean: $(MAKE) -C memcpy clean $(MAKE) -C memset clean $(MAKE) -C pcdrv clean + $(MAKE) -C timers clean diff --git a/src/mips/tests/common.mk b/src/mips/tests/common.mk new file mode 100644 index 000000000..24c28cda9 --- /dev/null +++ b/src/mips/tests/common.mk @@ -0,0 +1,43 @@ +USE_FUNCTION_SECTIONS = false +TYPE = ps-exe + +SRCS = \ +../uC-sdk-glue/BoardConsole.c \ +../uC-sdk-glue/BoardInit.c \ +../uC-sdk-glue/init.c \ +\ +../../../../third_party/uC-sdk/libc/src/cxx-glue.c \ +../../../../third_party/uC-sdk/libc/src/errno.c \ +../../../../third_party/uC-sdk/libc/src/initfini.c \ +../../../../third_party/uC-sdk/libc/src/malloc.c \ +../../../../third_party/uC-sdk/libc/src/qsort.c \ +../../../../third_party/uC-sdk/libc/src/rand.c \ +../../../../third_party/uC-sdk/libc/src/reent.c \ +../../../../third_party/uC-sdk/libc/src/stdio.c \ +../../../../third_party/uC-sdk/libc/src/string.c \ +../../../../third_party/uC-sdk/libc/src/strto.c \ +../../../../third_party/uC-sdk/libc/src/unistd.c \ +../../../../third_party/uC-sdk/libc/src/xprintf.c \ +../../../../third_party/uC-sdk/libc/src/xscanf.c \ +../../../../third_party/uC-sdk/libc/src/yscanf.c \ +../../../../third_party/uC-sdk/os/src/devfs.c \ +../../../../third_party/uC-sdk/os/src/filesystem.c \ +../../../../third_party/uC-sdk/os/src/fio.c \ +../../../../third_party/uC-sdk/os/src/hash-djb2.c \ +../../../../third_party/uC-sdk/os/src/init.c \ +../../../../third_party/uC-sdk/os/src/osdebug.c \ +../../../../third_party/uC-sdk/os/src/romfs.c \ +../../../../third_party/uC-sdk/os/src/sbrk.c \ +../../common/syscalls/printf.s \ +../../common/crt0/uC-sdk-crt0.s \ + +CPPFLAGS = -DNOFLOATINGPOINT +CPPFLAGS += -I. +CPPFLAGS += -I../../../../third_party/uC-sdk/libc/include +CPPFLAGS += -I../../../../third_party/uC-sdk/os/include +CPPFLAGS += -I../../../../third_party/libcester/include +CPPFLAGS += -I../../openbios/uC-sdk-glue + +ifeq ($(PCSX_TESTS),true) +CPPFLAGS += -DPCSX_TESTS=1 +endif diff --git a/src/mips/tests/timers/Makefile b/src/mips/tests/timers/Makefile new file mode 100644 index 000000000..ef4f273f4 --- /dev/null +++ b/src/mips/tests/timers/Makefile @@ -0,0 +1,8 @@ +TARGET = timers + +include ../common.mk + +SRCS += \ +timers.c \ + +include ../../common.mk diff --git a/src/mips/tests/timers/timers.c b/src/mips/tests/timers/timers.c new file mode 100644 index 000000000..af94dd569 --- /dev/null +++ b/src/mips/tests/timers/timers.c @@ -0,0 +1,561 @@ +/* + +MIT License + +Copyright (c) 2026 PCSX-Redux authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#ifndef PCSX_TESTS +#define PCSX_TESTS 0 +#endif + +#if PCSX_TESTS +#define CESTER_MAYBE_TEST CESTER_SKIP_TEST +#else +#define CESTER_MAYBE_TEST CESTER_TEST +#endif + +#include "common/hardware/counters.h" +#include "common/hardware/gpu.h" +#include "common/hardware/hwregs.h" +#include "common/syscalls/syscalls.h" + +#undef unix +#define CESTER_NO_SIGNAL +#define CESTER_NO_TIME +#define EXIT_SUCCESS 0 +#define EXIT_FAILURE 1 +#include "exotic/cester.h" + +// clang-format off + +#define BUSY_WAIT(n) do { for (int _bw = (n); _bw > 0; _bw--) __asm__ volatile(""); } while(0) + +CESTER_BODY( +static int s_interruptsWereEnabled; +) + +CESTER_BEFORE_ALL(timer_tests, + s_interruptsWereEnabled = enterCriticalSection(); + IMASK = 0; + IREG = 0; +) + +CESTER_AFTER_ALL(timer_tests, + if (s_interruptsWereEnabled) leaveCriticalSection(); +) + +CESTER_BODY( +static void waitVSync(void) { + while (!(GPU_STATUS & 0x80000000)) __asm__ volatile(""); + while (GPU_STATUS & 0x80000000) __asm__ volatile(""); +} +) + +/* ================================================================= + * Target reset: counter reaches target value, then resets. + * The target value IS briefly visible on reads. + * With target=N, reset-on-target: counter counts 0..N, then resets. + * The hit-target flag (bit 11) should be set after target is reached. + * ================================================================= */ +CESTER_TEST(timerTargetResetHitsTarget, timer_tests, + COUNTERS[2].target = 0x0010; + COUNTERS[2].mode = TM_RESET_TARGET; + BUSY_WAIT(500); + + uint16_t mode = COUNTERS[2].mode; + /* Hit-target flag should be set after counter reached target */ + cester_assert_cmp((int)(mode & TM_HIT_TARGET), !=, 0); +) + +/* Counter with reset-on-target should not overflow (bit 12 stays clear) + * if target is well below 0xFFFF. */ +CESTER_TEST(timerTargetResetNoOverflow, timer_tests, + COUNTERS[2].target = 0x0010; + COUNTERS[2].mode = TM_RESET_TARGET; + BUSY_WAIT(500); + + uint16_t mode = COUNTERS[2].mode; + /* Overflow flag should NOT be set - counter resets at target */ + cester_assert_int_eq(0, (int)(mode & TM_HIT_OVERFLOW)); +) + +/* ================================================================= + * Sysclock/8 divider ratio. + * Run both clock modes with same delay, ratio should be ~8. + * ================================================================= */ +CESTER_TEST(timerSysclockDiv8Ratio, timer_tests, + /* Use a short delay to avoid 16-bit counter wrap on sysclock. + * At ~33MHz sysclock, 1000 loop iters ~ a few thousand counts. */ + + /* Sysclock/8 mode first (slower, won't wrap) */ + COUNTERS[2].target = 0xFFFF; + COUNTERS[2].mode = TM_CLK_DIV8; + BUSY_WAIT(1000); + uint16_t div8_val = COUNTERS[2].value; + + /* Sysclock mode */ + COUNTERS[2].target = 0xFFFF; + COUNTERS[2].mode = 0; + BUSY_WAIT(1000); + uint16_t sys_val = COUNTERS[2].value; + + /* Ratio should be close to 8 (allow 6-10 for measurement jitter) */ + int ratio = sys_val / (div8_val ? div8_val : 1); + cester_assert_cmp(ratio, >=, 6); + cester_assert_cmp(ratio, <=, 10); +) + +/* ================================================================= + * Mode write resets counter to 0. + * ================================================================= */ +CESTER_TEST(timerModeWriteResetsCounter, timer_tests, + /* Warmup pass to prime icache */ + COUNTERS[2].mode = 0; + COUNTERS[2].target = 0xFFFF; + BUSY_WAIT(50000); + (void)COUNTERS[2].value; + COUNTERS[2].mode = 0; + (void)COUNTERS[2].value; + + /* Real measurement */ + COUNTERS[2].mode = 0; + COUNTERS[2].target = 0xFFFF; + BUSY_WAIT(50000); + + uint16_t before = COUNTERS[2].value; + cester_assert_cmp((int)before, >, 0); + + /* Write mode - should reset to 0 */ + COUNTERS[2].mode = 0; + int after = COUNTERS[2].value; + + /* Counter restarts immediately after reset, so a few ticks + * may elapse before the read. Allow small tolerance. */ + cester_assert_cmp(after, <, 10); +) + +/* ================================================================= + * Status bits: hit-target (bit 11) set on target, cleared on read. + * ================================================================= */ +CESTER_TEST(timerHitTargetFlagSetAndCleared, timer_tests, + /* Warmup pass to prime icache */ + COUNTERS[2].target = 0x1000; + COUNTERS[2].mode = TM_RESET_TARGET; + BUSY_WAIT(50000); + (void)COUNTERS[2].mode; + (void)COUNTERS[2].mode; + + /* Real measurement */ + COUNTERS[2].mode = TM_RESET_TARGET; + BUSY_WAIT(50000); + + uint16_t mode1 = COUNTERS[2].mode; + uint16_t mode2 = COUNTERS[2].mode; + + int flag1 = (mode1 & TM_HIT_TARGET) ? 1 : 0; + int flag2 = (mode2 & TM_HIT_TARGET) ? 1 : 0; + /* First read: bit 11 should be set */ + cester_assert_int_eq(1, flag1); + /* Second read: bit 11 should be cleared */ + cester_assert_int_eq(0, flag2); +) + +/* ================================================================= + * Status bits: hit-overflow (bit 12) set on 0xFFFF, cleared on read. + * ================================================================= */ +CESTER_TEST(timerHitOverflowFlagSetAndCleared, timer_tests, + COUNTERS[2].target = 0xFFFF; + COUNTERS[2].mode = 0; /* Free run */ + /* At sysclock ~33MHz, 0xFFFF counts = ~2ms */ + BUSY_WAIT(100000); + + uint16_t mode1 = COUNTERS[2].mode; + uint16_t mode2 = COUNTERS[2].mode; + + /* First read: bit 12 should be set */ + cester_assert_cmp((int)(mode1 & TM_HIT_OVERFLOW), !=, 0); + /* Second read: bit 12 should be cleared */ + cester_assert_int_eq(0, (int)(mode2 & TM_HIT_OVERFLOW)); +) + +/* ================================================================= + * IRQ request flag (bit 10): pulse mode vs toggle mode. + * Pulse mode (bit7=0): bit 10 stays 1 after IRQ. + * Toggle mode (bit7=1): bit 10 toggles, may read as 0. + * ================================================================= */ +CESTER_TEST(timerIrqPulseModeBit10, timer_tests, + COUNTERS[2].target = 0x0080; + COUNTERS[2].mode = TM_RESET_TARGET | TM_IRQ_TARGET | TM_IRQ_REPEAT; + BUSY_WAIT(1000); + + uint16_t mode = COUNTERS[2].mode; + /* Pulse mode: bit 10 should be 1 */ + cester_assert_cmp((int)(mode & TM_IRQ_REQUEST), !=, 0); +) + +CESTER_TEST(timerIrqToggleModeBit10, timer_tests, + /* In toggle mode, bit 10 alternates on each IRQ. + * Read mode twice with different target counts between reads + * to verify that bit 10 actually changes state. */ + COUNTERS[2].target = 0x0010; + COUNTERS[2].mode = TM_RESET_TARGET | TM_IRQ_TARGET | TM_IRQ_REPEAT | TM_IRQ_TOGGLE; + BUSY_WAIT(200); + + /* Sample bit 10 multiple times to detect toggling */ + int saw_zero = 0; + int saw_one = 0; + for (int i = 0; i < 50; i++) { + /* Re-read mode (which clears status bits but bit 10 reflects toggle state) */ + uint16_t mode = COUNTERS[2].mode; + if (mode & TM_IRQ_REQUEST) saw_one = 1; + else saw_zero = 1; + BUSY_WAIT(10); + } + + /* In toggle mode, we should see both states */ + cester_assert_int_eq(1, saw_zero); + cester_assert_int_eq(1, saw_one); +) + +/* ================================================================= + * Timer 2 sync modes (bit 0): + * Modes 0 and 3 = stop counter + * Modes 1 and 2 = free run + * ================================================================= */ +CESTER_TEST(timerRc2SyncMode0Stop, timer_tests, + COUNTERS[2].target = 0xFFFF; + COUNTERS[2].mode = TM_SYNC_EN | TM_SYNC_MODE(0); + BUSY_WAIT(50000); + uint16_t v1 = COUNTERS[2].value; + BUSY_WAIT(50000); + uint16_t v2 = COUNTERS[2].value; + + cester_assert_int_eq(0, (int)(uint16_t)(v2 - v1)); +) + +CESTER_TEST(timerRc2SyncMode1Free, timer_tests, + COUNTERS[2].target = 0xFFFF; + COUNTERS[2].mode = TM_SYNC_EN | TM_SYNC_MODE(1); + BUSY_WAIT(50000); + uint16_t v1 = COUNTERS[2].value; + BUSY_WAIT(50000); + uint16_t v2 = COUNTERS[2].value; + + cester_assert_cmp((int)(uint16_t)(v2 - v1), >, 0); +) + +CESTER_TEST(timerRc2SyncMode2Free, timer_tests, + COUNTERS[2].target = 0xFFFF; + COUNTERS[2].mode = TM_SYNC_EN | TM_SYNC_MODE(2); + BUSY_WAIT(50000); + uint16_t v1 = COUNTERS[2].value; + BUSY_WAIT(50000); + uint16_t v2 = COUNTERS[2].value; + + cester_assert_cmp((int)(uint16_t)(v2 - v1), >, 0); +) + +CESTER_TEST(timerRc2SyncMode3Stop, timer_tests, + COUNTERS[2].target = 0xFFFF; + COUNTERS[2].mode = TM_SYNC_EN | TM_SYNC_MODE(3); + BUSY_WAIT(50000); + uint16_t v1 = COUNTERS[2].value; + BUSY_WAIT(50000); + uint16_t v2 = COUNTERS[2].value; + + cester_assert_int_eq(0, (int)(uint16_t)(v2 - v1)); +) + +/* ================================================================= + * Counter 0 gate modes (Hblank-synced). + * Modes 1 and 3 are implemented. Modes 0 and 2 (pause logic) are + * not yet implemented - use CESTER_MAYBE_TEST to skip on emulator. + * ================================================================= */ + +/* Gate mode 0: pause during Hblank. + * Counter should advance slower than free run. */ +CESTER_MAYBE_TEST(timerC0GateMode0PauseDuringHblank, timer_tests, + COUNTERS[0].target = 0xFFFF; + + /* Measure gated first, then free run, to avoid ordering bias */ + COUNTERS[0].mode = TM_SYNC_EN | TM_SYNC_MODE(0); + BUSY_WAIT(20000); + uint16_t gated = COUNTERS[0].value; + + COUNTERS[0].mode = 0; + BUSY_WAIT(20000); + uint16_t free_val = COUNTERS[0].value; + + /* Gated value should be less than free run (paused during Hblank) */ + cester_assert_cmp((int)gated, <, (int)free_val); +) + +/* Gate mode 1: reset at Hblank. + * Counter keeps resetting, should show small values. */ +CESTER_TEST(timerC0GateMode1ResetAtHblank, timer_tests, + COUNTERS[0].target = 0xFFFF; + COUNTERS[0].mode = TM_SYNC_EN | TM_SYNC_MODE(1); + BUSY_WAIT(50000); + uint16_t val = COUNTERS[0].value; + + /* Value should be small - resets every scanline (~2130 sysclocks for NTSC) */ + cester_assert_cmp((int)val, <, 0x2000); +) + +/* Gate mode 2: reset at Hblank + pause outside. + * Counter only runs during Hblank and resets each time. */ +CESTER_MAYBE_TEST(timerC0GateMode2ResetPauseOutside, timer_tests, + COUNTERS[0].target = 0xFFFF; + COUNTERS[0].mode = TM_SYNC_EN | TM_SYNC_MODE(2); + BUSY_WAIT(50000); + uint16_t val = COUNTERS[0].value; + + /* Should be small - only counts during Hblank period itself. + * Hblank is ~200 sysclocks per scanline, so over many scanlines + * this accumulates but stays much smaller than free run (~45000). */ + cester_assert_cmp((int)val, <, 0x2000); +) + +/* Gate mode 3: pause until first Hblank, then free run. + * After the initial pause, should count at full speed. */ +CESTER_TEST(timerC0GateMode3FreeAfterHblank, timer_tests, + COUNTERS[0].target = 0xFFFF; + + COUNTERS[0].mode = 0; + BUSY_WAIT(50000); + uint16_t free_val = COUNTERS[0].value; + + COUNTERS[0].mode = TM_SYNC_EN | TM_SYNC_MODE(3); + BUSY_WAIT(50000); + uint16_t gated = COUNTERS[0].value; + + /* After first Hblank, should be close to free run */ + int diff = (int)free_val - (int)gated; + if (diff < 0) diff = -diff; + cester_assert_cmp(diff, <, 0x2000); +) + +/* ================================================================= + * PE2 scenario: Timer 2, sysclock/8, count to target, IRQ repeat. + * This is the exact configuration that triggers the PE2 jitter hack + * in the emulator (JITTER_FLAGS = Rc2OneEighthClock | RcIrqRegenerate | RcCountToTarget). + * Verify counter value is proportional to elapsed time. + * ================================================================= */ +CESTER_TEST(timerPE2Scenario, timer_tests, + /* Set up Timer 2 exactly as PE2 does */ + COUNTERS[2].target = 0x1000; + COUNTERS[2].mode = TM_CLK_DIV8 | TM_IRQ_REPEAT | TM_RESET_TARGET | TM_IRQ_TARGET; + + /* Wait a known amount, then read */ + BUSY_WAIT(50000); + uint16_t count = COUNTERS[2].value; + + /* Counter should be running and have a reasonable value. + * At sysclock/8, 5000 loop iterations at ~4 cycles each = ~20000 cycles, + * divided by 8 = ~2500 counts. Allow wide tolerance since loop timing + * varies with compiler optimization and cache state. */ + cester_assert_cmp((int)count, >, 100); + cester_assert_cmp((int)count, <, 0x1000); /* Should not have wrapped past target */ +) + +/* ================================================================= + * Dotclock rate measurement. + * Use Timer1 (hsync) to count 10 scanlines, read Timer0 (dotclock). + * The ratio gives dots per scanline which depends on GPU hres. + * + * Hardware verified on SCPH-5501 NTSC in 512px mode: + * 680 dots/scanline (expected 682 = 3413/5) + * ================================================================= */ +CESTER_TEST(timerDotclockRate, timer_tests, + /* Timer1: hsync clock, free run */ + COUNTERS[1].target = 0xFFFF; + COUNTERS[1].mode = TM_CLK_EXTERNAL; + + /* Timer0: dotclock, free run */ + COUNTERS[0].target = 0xFFFF; + COUNTERS[0].mode = TM_CLK_EXTERNAL; + + /* Reset both */ + COUNTERS[1].mode = TM_CLK_EXTERNAL; + COUNTERS[0].mode = TM_CLK_EXTERNAL; + + /* Wait for 10 scanlines */ + while (COUNTERS[1].value < 10) {} + + int dots = COUNTERS[0].value; + int lines = COUNTERS[1].value; + int dots_per_line = dots / lines; + + /* Dots per scanline should be reasonable for any resolution: + * Minimum: 256px mode = 341 dots/line (3413/10) + * Maximum: 640px mode = 853 dots/line (3413/4) + * Allow some tolerance for fractional rounding. */ + cester_assert_cmp(dots_per_line, >=, 330); + cester_assert_cmp(dots_per_line, <=, 860); +) + +/* ================================================================= + * Precise dotclock measurements per resolution. + * Measures dots per N scanlines at each GPU horizontal resolution + * in both NTSC and PAL modes. Uses 100 scanlines for precision. + * + * Hardware verified on SCPH-5501, 2026-04-08: + * 256px: NTSC 341, PAL 340 + * 320px: NTSC 426, PAL 426 (rounds UP from 425.75) + * 512px: NTSC 682, PAL 681 + * 640px: NTSC 853, PAL 851 + * 368px: NTSC 487, PAL 486 + * ================================================================= */ + +CESTER_BODY( +static int measureDotsPerLine(int scanlines) { + /* Timer1: hsync clock, free run */ + COUNTERS[1].target = 0xFFFF; + COUNTERS[1].mode = TM_CLK_EXTERNAL; + + /* Timer0: dotclock, free run */ + COUNTERS[0].target = 0xFFFF; + COUNTERS[0].mode = TM_CLK_EXTERNAL; + + /* Reset both by re-writing mode */ + COUNTERS[1].mode = TM_CLK_EXTERNAL; + COUNTERS[0].mode = TM_CLK_EXTERNAL; + + /* Wait for exactly N scanlines */ + while (COUNTERS[1].value < scanlines) {} + + int dots = COUNTERS[0].value; + int lines = COUNTERS[1].value; + return (dots + lines / 2) / lines; +} +) + +/* 256px NTSC: 3413/10 = 341.3 -> 341 */ +CESTER_MAYBE_TEST(timerDotclock256NTSC, timer_tests, + struct DisplayModeConfig cfg = { HR_256, VR_240, VM_NTSC, CD_15BITS, VI_OFF, HRE_NORMAL }; + setDisplayMode(&cfg); + waitVSync(); + waitVSync(); + measureDotsPerLine(50); + int dpl = measureDotsPerLine(50); + cester_assert_cmp(dpl, >=, 341 - 1); cester_assert_cmp(dpl, <=, 341 + 1); +) + +/* 256px PAL: 3406/10 = 340.6 -> 340 */ +CESTER_MAYBE_TEST(timerDotclock256PAL, timer_tests, + struct DisplayModeConfig cfg = { HR_256, VR_240, VM_PAL, CD_15BITS, VI_OFF, HRE_NORMAL }; + setDisplayMode(&cfg); + waitVSync(); + waitVSync(); + measureDotsPerLine(50); + int dpl = measureDotsPerLine(50); + cester_assert_cmp(dpl, >=, 340 - 1); cester_assert_cmp(dpl, <=, 340 + 1); +) + +/* 320px NTSC: 3413/8 = 426.625 -> 426 */ +CESTER_MAYBE_TEST(timerDotclock320NTSC, timer_tests, + struct DisplayModeConfig cfg = { HR_320, VR_240, VM_NTSC, CD_15BITS, VI_OFF, HRE_NORMAL }; + setDisplayMode(&cfg); + waitVSync(); + waitVSync(); + measureDotsPerLine(50); + int dpl = measureDotsPerLine(50); + cester_assert_cmp(dpl, >=, 426 - 1); cester_assert_cmp(dpl, <=, 426 + 1); +) + +/* 320px PAL: 3406/8 = 425.75 -> 426 (only mode that rounds up) */ +CESTER_MAYBE_TEST(timerDotclock320PAL, timer_tests, + struct DisplayModeConfig cfg = { HR_320, VR_240, VM_PAL, CD_15BITS, VI_OFF, HRE_NORMAL }; + setDisplayMode(&cfg); + waitVSync(); + waitVSync(); + measureDotsPerLine(50); + int dpl = measureDotsPerLine(50); + cester_assert_cmp(dpl, >=, 426 - 1); cester_assert_cmp(dpl, <=, 426 + 1); +) + +/* 512px NTSC: 3413/5 = 682.6 -> 682 */ +CESTER_MAYBE_TEST(timerDotclock512NTSC, timer_tests, + struct DisplayModeConfig cfg = { HR_512, VR_240, VM_NTSC, CD_15BITS, VI_OFF, HRE_NORMAL }; + setDisplayMode(&cfg); + waitVSync(); + waitVSync(); + measureDotsPerLine(50); + int dpl = measureDotsPerLine(50); + cester_assert_cmp(dpl, >=, 682 - 1); cester_assert_cmp(dpl, <=, 682 + 1); +) + +/* 512px PAL: 3406/5 = 681.2 -> 681 */ +CESTER_MAYBE_TEST(timerDotclock512PAL, timer_tests, + struct DisplayModeConfig cfg = { HR_512, VR_240, VM_PAL, CD_15BITS, VI_OFF, HRE_NORMAL }; + setDisplayMode(&cfg); + waitVSync(); + waitVSync(); + measureDotsPerLine(50); + int dpl = measureDotsPerLine(50); + cester_assert_cmp(dpl, >=, 681 - 1); cester_assert_cmp(dpl, <=, 681 + 1); +) + +/* 640px NTSC: 3413/4 = 853.25 -> 853 */ +CESTER_MAYBE_TEST(timerDotclock640NTSC, timer_tests, + struct DisplayModeConfig cfg = { HR_640, VR_240, VM_NTSC, CD_15BITS, VI_OFF, HRE_NORMAL }; + setDisplayMode(&cfg); + waitVSync(); + waitVSync(); + measureDotsPerLine(50); + int dpl = measureDotsPerLine(50); + cester_assert_cmp(dpl, >=, 853 - 1); cester_assert_cmp(dpl, <=, 853 + 1); +) + +/* 640px PAL: 3406/4 = 851.5 -> 851 */ +CESTER_MAYBE_TEST(timerDotclock640PAL, timer_tests, + struct DisplayModeConfig cfg = { HR_640, VR_240, VM_PAL, CD_15BITS, VI_OFF, HRE_NORMAL }; + setDisplayMode(&cfg); + waitVSync(); + waitVSync(); + measureDotsPerLine(50); + int dpl = measureDotsPerLine(50); + cester_assert_cmp(dpl, >=, 851 - 1); cester_assert_cmp(dpl, <=, 851 + 1); +) + +/* 368px NTSC: 3413/7 = 487.57 -> 487 */ +CESTER_MAYBE_TEST(timerDotclock368NTSC, timer_tests, + struct DisplayModeConfig cfg = { HR_256, VR_240, VM_NTSC, CD_15BITS, VI_OFF, HRE_368 }; + setDisplayMode(&cfg); + waitVSync(); + waitVSync(); + measureDotsPerLine(50); + int dpl = measureDotsPerLine(50); + cester_assert_cmp(dpl, >=, 487 - 1); cester_assert_cmp(dpl, <=, 487 + 1); +) + +/* 368px PAL: 3406/7 = 486.57 -> 486 */ +CESTER_MAYBE_TEST(timerDotclock368PAL, timer_tests, + struct DisplayModeConfig cfg = { HR_256, VR_240, VM_PAL, CD_15BITS, VI_OFF, HRE_368 }; + setDisplayMode(&cfg); + waitVSync(); + waitVSync(); + measureDotsPerLine(50); + int dpl = measureDotsPerLine(50); + cester_assert_cmp(dpl, >=, 486 - 1); cester_assert_cmp(dpl, <=, 486 + 1); +) diff --git a/tests/pcsxrunner/timers.cc b/tests/pcsxrunner/timers.cc new file mode 100644 index 000000000..280224b33 --- /dev/null +++ b/tests/pcsxrunner/timers.cc @@ -0,0 +1,35 @@ +/*************************************************************************** + * Copyright (C) 2026 PCSX-Redux authors * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * + ***************************************************************************/ + +#include "gtest/gtest.h" +#include "main/main.h" + +TEST(Timers, Interpreter) { + MainInvoker invoker("-no-ui", "-run", "-bios", "src/mips/openbios/openbios.bin", "-testmode", "-interpreter", + "-luacov", "-loadexe", "src/mips/tests/timers/timers.ps-exe"); + int ret = invoker.invoke(); + EXPECT_EQ(ret, 0); +} + +TEST(Timers, Dynarec) { + MainInvoker invoker("-no-ui", "-run", "-bios", "src/mips/openbios/openbios.bin", "-testmode", "-dynarec", + "-luacov", "-loadexe", "src/mips/tests/timers/timers.ps-exe"); + int ret = invoker.invoke(); + EXPECT_EQ(ret, 0); +} diff --git a/vsprojects/tests/pcsxrunner/pcsxrunner.vcxproj b/vsprojects/tests/pcsxrunner/pcsxrunner.vcxproj index c140389a0..dd4fdef08 100644 --- a/vsprojects/tests/pcsxrunner/pcsxrunner.vcxproj +++ b/vsprojects/tests/pcsxrunner/pcsxrunner.vcxproj @@ -260,6 +260,7 @@ + diff --git a/vsprojects/tests/pcsxrunner/pcsxrunner.vcxproj.filters b/vsprojects/tests/pcsxrunner/pcsxrunner.vcxproj.filters index 1b6304fc8..10d27629b 100644 --- a/vsprojects/tests/pcsxrunner/pcsxrunner.vcxproj.filters +++ b/vsprojects/tests/pcsxrunner/pcsxrunner.vcxproj.filters @@ -45,6 +45,9 @@ Source Files + + Source Files +