diff --git a/recompiler/src/code_generator.cpp b/recompiler/src/code_generator.cpp index a9011672..cbdca91e 100644 --- a/recompiler/src/code_generator.cpp +++ b/recompiler/src/code_generator.cpp @@ -2629,11 +2629,23 @@ GeneratedFunction CodeGenerator::generate_function( } body_ss << "#endif\n"; + // Cadence / RAM-prep mod hooks must run on CPS mid-function re-entry too: + // AOT interior blocks still `lw` guest RAM (e.g. SimVblankGate skip). Other + // entry hooks (debug log, widescreen) stay AFTER the switch so they only + // fire on a true prologue entry (cpu->pc == 0). + if (config_.mod_function_entry_funcs.count(func.start_addr)) { + body_ss << config_.indent + << fmt::format( + "psx_mod_function_entry(cpu, 0x{:08X}u);" + " /* trusted opt-in game-mod hook (pre-CPS) */\n", + func.start_addr); + } + // CPS entry-switch: when the unified flat trampoline dispatches a // continuation address (a callee published cpu->pc = $ra back to us), route - // into the owning block. Emitted BEFORE the entry hooks so a continuation - // re-entry bypasses debug_server_log_call_entry / widescreen entry hooks - // (those must run only on a true function entry, cpu->pc == 0). + // into the owning block. Emitted before debug/widescreen entry hooks so a + // continuation re-entry bypasses those (true-entry only). mod_function_entry + // is intentionally above so cadence plugins still run. // Overlay mode must emit the cpu->pc guard even when the continuation set // is EMPTY: a range re-entry can still request an interior PC of a // single-block function, and without the guard the body runs from its top @@ -2711,13 +2723,6 @@ GeneratedFunction CodeGenerator::generate_function( " /* data-shard: replay or arm capture */\n", func.start_addr); } - if (config_.mod_function_entry_funcs.count(func.start_addr)) { - body_ss << config_.indent - << fmt::format( - "psx_mod_function_entry(cpu, 0x{:08X}u);" - " /* trusted opt-in game-mod hook */\n", - func.start_addr); - } if (config_.ws_sprite_tag_funcs.count(func.start_addr)) { body_ss << config_.indent << "psx_ws_sprite_tag(cpu); /* widescreen: record prim ($a0) + anchor */\n"; diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index f5d8c449..6aaa8111 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -43,6 +43,26 @@ void gpu_get_display_info(GpuDisplayInfo* out); * packed RGB888 lives in the CPU mirror; treating A0 rects as 1555 FBO uploads * both wastes bandwidth and force-flushes when UP_RECTS_MAX is hit (MotK FMV). */ int gpu_display_is_depth24(void); +/* GP1(08h) bit 3: 0 = NTSC, 1 = PAL. */ +int gpu_display_is_pal(void); +/* Guest CPU cycles per CRTC frame (33.8688 MHz / 60 NTSC, / 50 PAL). */ +#define PSX_VBLANK_CYCLES_NTSC 564480u +#define PSX_VBLANK_CYCLES_PAL 677376u +uint32_t gpu_vblank_period_cycles(void); +/* + * Force guest cycles per VBlank regardless of GP1(08h) PAL/NTSC. 0 clears. + * Used so NTSC display mode can keep a 50 Hz IRQ (25 fps with stock skip) + * for audio-safe ports of PAL titles. Still divided by the Nx multiplier. + */ +void gpu_set_crtc_vblank_period_override(uint32_t cycles); +uint32_t gpu_get_crtc_vblank_period_override(void); +/* + * Experimental enhancement: GooseStation-style Nx video timing. Multiplier 2 + * halves the guest VBlank period (NTSC 120 Hz / PAL 100 Hz CRTC). Default 1. + * Host wall-clock pacing is separate (psx_mod_set_native_vblank_rate). + */ +void gpu_set_crtc_refresh_multiplier(uint32_t multiplier); +uint32_t gpu_get_crtc_refresh_multiplier(void); void gpu_display_pixel_rgb(const GpuDisplayInfo* di, uint32_t x, uint32_t y, uint8_t* r, uint8_t* g, uint8_t* b); uint32_t gpu_display_pixel_argb(const GpuDisplayInfo* di, uint32_t x, uint32_t y); diff --git a/runtime/include/interrupts.h b/runtime/include/interrupts.h index 7b7d2507..3bf6598e 100644 --- a/runtime/include/interrupts.h +++ b/runtime/include/interrupts.h @@ -76,7 +76,8 @@ void psx_check_interrupts_dispatch_entry(struct CPUState* cpu, uint32_t resume_p * Called from psx_advance_cycles() so the VBlank rate is gated on * guest cycles (correct PSX timing) rather than block-dispatch * count (which was 5-6x too fast and squeezed game-time to ~60% of - * real). One real-PSX VBlank = 564480 cycles (33.8688 MHz / 60). */ + * real). Period follows GP1(08h) video mode: 564480 cycles NTSC + * (33.8688 MHz / 60) or 677376 PAL (33.8688 MHz / 50). */ void interrupts_advance_cycles(uint32_t cycles); void interrupts_service_scheduled_events(void); uint32_t interrupts_cycles_to_vblank(void); diff --git a/runtime/include/mod_plugins.h b/runtime/include/mod_plugins.h index 610f245d..a9093f53 100644 --- a/runtime/include/mod_plugins.h +++ b/runtime/include/mod_plugins.h @@ -103,6 +103,20 @@ int psx_mod_set_adaptive_display_aspect(uint32_t max_numerator, */ int psx_mod_set_native_vblank_rate(uint32_t frames_per_second); +/* + * GooseStation-style Nx CRTC: divide guest cycles per VBlank by `multiplier` + * (1 = stock, 2 = NTSC 120 Hz / PAL 100 Hz). Pair with + * psx_mod_set_native_vblank_rate(base_hz * multiplier) so wall-clock and + * guest time stay aligned; otherwise audio and realtime speed drift. + */ +int psx_mod_set_crtc_refresh_multiplier(uint32_t multiplier); +/* + * Force the guest VBlank IRQ period to a fixed CRTC rate (50 or 60 Hz), or + * pass 0 to follow GP1(08h) again. Does not change the GPU video-mode bit — + * NTSC display programming can keep a 50 Hz IRQ for audio-safe PAL ports. + */ +int psx_mod_set_crtc_vblank_hz(uint32_t hz); + /* * Enable presentation-only frame interpolation while leaving guest VBlank, * game logic, timers, and audio at their stock cadence. The OpenGL presenter diff --git a/runtime/src/cdrom.c b/runtime/src/cdrom.c index 6e62504d..8ea24a1f 100644 --- a/runtime/src/cdrom.c +++ b/runtime/src/cdrom.c @@ -13,6 +13,7 @@ #include "cdrom.h" #include "cdrom_irq.h" #include "dma.h" +#include "gpu.h" #include "spu.h" #include "event_ring.h" #include "audio_trace.h" @@ -340,8 +341,7 @@ uint32_t g_cd_overwrite_last_frame = 0; * frames always advance. Paired with the per-source exception-progress * guarantee in interrupts.c (the real anti-starvation fix); this floor keeps * the exception VOLUME sane so the framerate doesn't collapse. Tunable via the - * MAX_PER_FRAME knob. */ -#define VBLANK_CYCLES_NTSC 564480 /* 33.8688 MHz / 60 Hz; matches interrupts.c */ + * MAX_PER_FRAME knob. PAL uses the live CRTC period from gpu_vblank_period_cycles(). */ #define CDROM_INSTANT_MAX_PER_FRAME_DEFAULT 32 #define CDROM_SINGLE_SPEED_SECTOR_CYCLES 451584 @@ -511,14 +511,14 @@ void cdrom_set_instant_rate(int per_frame) { int cdrom_get_instant_rate(void) { return g_instant_max_per_frame; } static int instant_period(void) { - int p = VBLANK_CYCLES_NTSC / g_instant_max_per_frame; + int p = (int)(gpu_vblank_period_cycles() / (uint32_t)g_instant_max_per_frame); return p < CDROM_MIN_DELAY ? CDROM_MIN_DELAY : p; } static int warm_route_period(void) { const CDROMWarmRoute *route = warm_route_current(); int rate = route ? route->rate : CDROM_INSTANT_MAX_PER_FRAME_DEFAULT; - int p = VBLANK_CYCLES_NTSC / rate; + int p = (int)(gpu_vblank_period_cycles() / (uint32_t)rate); return p < CDROM_MIN_DELAY ? CDROM_MIN_DELAY : p; } diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index c4b80f1a..fbc8ba74 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -3095,6 +3095,43 @@ int gpu_display_is_depth24(void) { return (int)(display_depth & 1u); } +int gpu_display_is_pal(void) { + return (int)(video_mode & 1u); +} + +/* Opt-in Nx CRTC (mod plugins). 1 = stock; 2 = half-period VBlank. */ +static uint32_t s_crtc_refresh_multiplier = 1u; +/* 0 = derive from GP1 video_mode; else absolute period before Nx divide. */ +static uint32_t s_crtc_period_override = 0u; + +void gpu_set_crtc_refresh_multiplier(uint32_t multiplier) { + if (multiplier < 1u) + multiplier = 1u; + if (multiplier > 8u) + multiplier = 8u; + s_crtc_refresh_multiplier = multiplier; +} + +uint32_t gpu_get_crtc_refresh_multiplier(void) { + return s_crtc_refresh_multiplier ? s_crtc_refresh_multiplier : 1u; +} + +void gpu_set_crtc_vblank_period_override(uint32_t cycles) { + s_crtc_period_override = cycles; +} + +uint32_t gpu_get_crtc_vblank_period_override(void) { + return s_crtc_period_override; +} + +uint32_t gpu_vblank_period_cycles(void) { + const uint32_t base = s_crtc_period_override + ? s_crtc_period_override + : (video_mode ? PSX_VBLANK_CYCLES_PAL : PSX_VBLANK_CYCLES_NTSC); + const uint32_t mult = gpu_get_crtc_refresh_multiplier(); + return base / mult; +} + void gpu_get_display_info(GpuDisplayInfo* out) { out->display_x = display_area_x; out->display_y = display_area_y; diff --git a/runtime/src/interrupts.c b/runtime/src/interrupts.c index 5c87cbe6..7964dd2c 100644 --- a/runtime/src/interrupts.c +++ b/runtime/src/interrupts.c @@ -177,8 +177,10 @@ void psx_irq_raise(uint32_t bit, uint32_t detail) /* Dispatch counter for vblank scheduling. */ #define VBLANK_INTERVAL 50000 /* legacy: dispatch-count fallback (unused for VBlank gating now) */ -#define VBLANK_CYCLES 564480u /* 33.8688 MHz / 60 Hz — real PSX NTSC VBlank period */ -#define VBLANK_DEFER_STALE_CYCLES (VBLANK_CYCLES * 10ull) +static uint32_t vblank_cycles(void) { + return gpu_vblank_period_cycles(); +} +#define VBLANK_DEFER_STALE_CYCLES_FRAMES 10ull static uint32_t dispatch_count; static uint64_t total_checks; static uint32_t cycles_since_vblank; /* incremented by interrupts_advance_cycles */ @@ -356,7 +358,7 @@ static int should_defer_vblank_for_sio(void) { uint64_t since_progress = now >= last_sio_progress_cycle ? now - last_sio_progress_cycle : 0; - return since_progress < VBLANK_DEFER_STALE_CYCLES; + return since_progress < ((uint64_t)vblank_cycles() * VBLANK_DEFER_STALE_CYCLES_FRAMES); } /* ---- Mid-dispatch audio pump ------------------------------------------- @@ -390,13 +392,14 @@ static void fire_vblank_edge(void) { /* Subtract one VBlank period rather than reset to 0 so cycle overshoot * carries forward. Prevents long-running blocks from rounding multiple * VBlanks together. */ - cycles_since_vblank -= VBLANK_CYCLES; + const uint32_t period = vblank_cycles(); + cycles_since_vblank -= period; dispatch_count = 0; /* DEQUEUE: this VBlank fired. ENQUEUE: next VBlank scheduled one period out. */ event_ring_record_aux(EV_DEQ, (uint8_t)SRC_VBLANK, (uint32_t)psx_get_cycle_count()); event_ring_record_aux(EV_ENQ, (uint8_t)SRC_VBLANK, - (uint32_t)(psx_get_cycle_count() + VBLANK_CYCLES)); + (uint32_t)(psx_get_cycle_count() + period)); psx_irq_raise(IRQ_VBLANK, 0); g_vblank_raise_count++; event_ring_record(EV_ISTAT_RAISE, IRQ_VBLANK); @@ -412,15 +415,17 @@ static void fire_vblank_edge(void) { void interrupts_service_scheduled_events(void) { note_sio_progress_cycle(); if (in_exception) return; - while (cycles_since_vblank >= VBLANK_CYCLES) { + const uint32_t period = vblank_cycles(); + while (cycles_since_vblank >= period) { if (should_defer_vblank_for_sio()) return; fire_vblank_edge(); } } uint32_t interrupts_cycles_to_vblank(void) { - if (cycles_since_vblank >= VBLANK_CYCLES) return 0; - return VBLANK_CYCLES - cycles_since_vblank; + const uint32_t period = vblank_cycles(); + if (cycles_since_vblank >= period) return 0; + return period - cycles_since_vblank; } uint32_t interrupts_get_cycles_since_vblank(void) { @@ -896,8 +901,9 @@ uint32_t cycles_to_next_event(void) { * card-SIO case only pushes VBlank LATER, so this estimate stays a safe * under-estimate. */ if (i_mask & (1u << IRQ_VBLANK)) { - uint32_t d = (cycles_since_vblank >= VBLANK_CYCLES) - ? 0u : (VBLANK_CYCLES - cycles_since_vblank); + const uint32_t period = vblank_cycles(); + uint32_t d = (cycles_since_vblank >= period) + ? 0u : (period - cycles_since_vblank); if (d < best) best = d; } uint32_t t = timers_cycles_to_irq(i_mask); if (t < best) best = t; diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 35058c4e..d225fad9 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -1170,10 +1170,33 @@ static_assert((int)PSX_MOD_CONTROLLER_ANALOG == static_assert((int)PSX_MOD_CONTROLLER_DIGITAL == (int)PSXRecompV4::PAD_MODE_DIGITAL); static double g_host_refresh_hz = 0.0; -static constexpr double PSX_FRAME_PERIOD_MS = 1000.0 / 59.94; +static constexpr double PSX_FRAME_HZ_NTSC = 59.94; +static constexpr double PSX_FRAME_HZ_PAL = 50.0; +static constexpr double PSX_FRAME_PERIOD_MS = 1000.0 / PSX_FRAME_HZ_NTSC; static double g_frame_period_ms = PSX_FRAME_PERIOD_MS; static bool g_mod_native_vblank_rate = false; static uint32_t g_mod_native_vblank_fps = 0; + +static double psx_crtc_frame_hz(void) { + const uint32_t period = gpu_vblank_period_cycles(); + if (period == 0u) + return PSX_FRAME_HZ_NTSC; + /* 33.8688 MHz CPU clock / cycles-per-vblank. */ + return 33868800.0 / (double)period; +} + +static void refresh_frame_pacer_period(void) { + if (g_mod_native_vblank_rate) + return; + const double psx_hz = psx_crtc_frame_hz(); + g_frame_period_ms = 1000.0 / psx_hz; + if (g_host_refresh_hz > 0.0) { + const double lo = psx_hz * 0.98; + const double hi = psx_hz * 1.02; + if (g_host_refresh_hz >= lo && g_host_refresh_hz <= hi) + g_frame_period_ms = 1000.0 / g_host_refresh_hz; + } +} /* Activation-time request. -1 means no enabled mod owns load acceleration. */ static int g_mod_load_wall_multiplier = -1; static int g_mod_load_release_frames = -1; @@ -1277,7 +1300,7 @@ extern "C" int psx_mod_set_adaptive_display_aspect( extern "C" int psx_mod_set_native_vblank_rate( uint32_t frames_per_second) { if (frames_per_second != 0 && - (frames_per_second < 60 || frames_per_second > 1000)) { + (frames_per_second < 50 || frames_per_second > 1000)) { std::fprintf(stderr, "psxrecomp: mod rejected invalid native VBlank rate %u FPS\n", (unsigned)frames_per_second); @@ -1307,6 +1330,46 @@ extern "C" int psx_mod_set_native_vblank_rate( return 1; } +extern "C" int psx_mod_set_crtc_refresh_multiplier( + uint32_t multiplier) { + if (multiplier < 1u || multiplier > 8u) { + std::fprintf(stderr, + "psxrecomp: mod rejected invalid CRTC refresh multiplier %u\n", + (unsigned)multiplier); + return 0; + } + gpu_set_crtc_refresh_multiplier(multiplier); + std::fprintf(stdout, + "psxrecomp: mod selected CRTC refresh multiplier %u " + "(guest VBlank period / %u)\n", + (unsigned)multiplier, (unsigned)multiplier); + return 1; +} + +extern "C" int psx_mod_set_crtc_vblank_hz(uint32_t hz) { + if (hz == 0u) { + gpu_set_crtc_vblank_period_override(0u); + std::fprintf(stdout, + "psxrecomp: mod cleared CRTC VBlank period override " + "(follow GP1 video mode)\n"); + return 1; + } + if (hz != 50u && hz != 60u) { + std::fprintf(stderr, + "psxrecomp: mod rejected invalid CRTC VBlank rate %u Hz " + "(use 50, 60, or 0)\n", + (unsigned)hz); + return 0; + } + const uint32_t cycles = + (hz == 50u) ? PSX_VBLANK_CYCLES_PAL : PSX_VBLANK_CYCLES_NTSC; + gpu_set_crtc_vblank_period_override(cycles); + std::fprintf(stdout, + "psxrecomp: mod forced CRTC VBlank to %u Hz (%u guest cycles)\n", + (unsigned)hz, (unsigned)cycles); + return 1; +} + extern "C" int psx_mod_set_frame_interpolation( uint32_t frames_per_second) { if (frames_per_second != 0 && @@ -2524,8 +2587,10 @@ static void apply_netplay_local_viewport_aspect(bool netplay_enabled) { * wall-clock pacer double-blocks the vblank callback (present is before the * guest resumes), which shows up as MotK FMV ~30–40 FPS in netplay vs ~50+ * offline. Force immediate swaps for the session; restore on soft-exit. */ -static int host_refresh_is_approx_60hz(void) { - return g_host_refresh_hz >= 58.8 && g_host_refresh_hz <= 61.2; +static int host_refresh_matches_crtc(void) { + const double psx_hz = psx_crtc_frame_hz(); + return g_host_refresh_hz >= psx_hz * 0.98 && + g_host_refresh_hz <= psx_hz * 1.02; } static int present_vsync_owns_cadence(void) { @@ -2537,7 +2602,7 @@ static int present_vsync_owns_cadence(void) { return 0; if (g_netplay_vsync_forced_off || psx_netplay_active()) return 0; - return host_refresh_is_approx_60hz(); + return host_refresh_matches_crtc(); } static int present_effective_swap_interval(void) { @@ -5924,7 +5989,7 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) { } else if (frequency && now - s_fps_last_time >= frequency) { const double seconds = (double)(now - s_fps_last_time) / (double)frequency; const double fps = (double)(s_frame_count - s_fps_last_frame) / seconds; - const double speed = fps / 59.94; + const double speed = fps / psx_crtc_frame_hz(); double display_fps = 0.0; if (g_frame_interpolation && g_gl_active) { display_fps = g_frame_interpolation_fps > 0 @@ -6418,12 +6483,14 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) { } /* Offline wall-clock pacing before present. Skipped when driver vsync - * owns cadence (~60 Hz panel) so the two waits cannot double-block. + * owns cadence (panel near the live CRTC rate) so the two waits cannot + * double-block. * Netplay paces in the epilogue AFTER present so Swap overlaps the * peer's guest quantum. Self-check replay runs uncapped like netplay * resim. */ if (!psx_netplay_active() && !psx_selfcheck_resim_active()) { uint64_t perf_start = runtime_perf_section_begin(); + refresh_frame_pacer_period(); if (!manual_turbo_active && !turbo_load_paced && present_should_wall_pace()) frame_pacer_wait(&s_frame_pacer, g_frame_period_ms); runtime_perf_section_end(perf_start, &g_runtime_perf.pacer_ticks); @@ -6944,6 +7011,7 @@ static void sdl_vblank_present(void) { return; } uint64_t perf_start = runtime_perf_section_begin(); + refresh_frame_pacer_period(); if (present_should_wall_pace()) frame_pacer_wait(&s_frame_pacer, g_frame_period_ms); runtime_perf_section_end(perf_start, &g_runtime_perf.pacer_ticks); @@ -12726,23 +12794,28 @@ session_reboot: } psx_apply_window_icon(sdl_window, argv[0]); - /* Host refresh: if the panel is within ~2% of 60 Hz, record it so driver - * vsync can own cadence (pacer skipped). Non-~60 Hz and unknown refresh - * (common on Wayland) keep PSX 59.94 Hz pacing and force swap interval 0 - * — vsync as the clock would run the sim at the panel rate. */ + /* Host refresh: if the panel is within ~2% of the live CRTC rate (NTSC + * 59.94 / PAL 50), record it so driver vsync can own cadence. Otherwise + * keep the PSX CRTC period and force swap interval 0. Mods that own + * native VBlank keep their cadence. */ { SDL_DisplayMode dm; int disp_idx = SDL_GetWindowDisplayIndex(sdl_window); - if (disp_idx >= 0 && SDL_GetCurrentDisplayMode(disp_idx, &dm) == 0 && dm.refresh_rate > 0) { - double host_hz = (double)dm.refresh_rate; - g_host_refresh_hz = host_hz; - if (host_hz >= 58.8 && host_hz <= 61.2) { - g_frame_period_ms = 1000.0 / host_hz; + if (disp_idx >= 0 && SDL_GetCurrentDisplayMode(disp_idx, &dm) == 0 && dm.refresh_rate > 0) + g_host_refresh_hz = (double)dm.refresh_rate; + refresh_frame_pacer_period(); + if (!g_mod_native_vblank_rate) { + const double psx_hz = psx_crtc_frame_hz(); + if (host_refresh_matches_crtc()) { std::printf("psxrecomp: sync-to-host-refresh: pacing to %d Hz panel " - "(%.4f ms/frame)\n", dm.refresh_rate, g_frame_period_ms); + "(%.4f ms/frame, %s CRTC)\n", + (int)(g_host_refresh_hz + 0.5), g_frame_period_ms, + gpu_display_is_pal() ? "PAL" : "NTSC"); } else { - std::printf("psxrecomp: host panel %d Hz not ~60 Hz; keeping PSX " - "59.94 Hz pacing\n", dm.refresh_rate); + std::printf("psxrecomp: host panel %.0f Hz; keeping %s %.2f Hz pacing\n", + g_host_refresh_hz, + gpu_display_is_pal() ? "PAL" : "NTSC", + psx_hz); } } } diff --git a/runtime/src/sio.c b/runtime/src/sio.c index 58e767a7..f5f6676b 100644 --- a/runtime/src/sio.c +++ b/runtime/src/sio.c @@ -17,6 +17,7 @@ #include "memcard.h" #include "debug_server.h" #include "event_ring.h" +#include "gpu.h" #include #include @@ -424,7 +425,7 @@ static int sio_txn_open = 0; static void sio_arm_card_ct_defer_guard(void) { extern uint64_t psx_get_cycle_count(void); uint64_t now = psx_get_cycle_count(); - uint64_t until = now + 564480ull * 2ull; + uint64_t until = now + (uint64_t)gpu_vblank_period_cycles() * 2ull; if (until > s_card_ct_defer_until_cyc) s_card_ct_defer_until_cyc = until; } @@ -547,7 +548,7 @@ static void ape_card_unstick_maybe(int allow_b4e38_synth) { psx_irq_raise(IRQ_SIO0, 0); /* ~2ms between pulses — faster than one VB/8 so two pops can land * before BIOS clears I_MASK.7 for good. */ - s_ape_unstick_cool_cyc = now + 564480ull / 32ull; + s_ape_unstick_cool_cyc = now + (uint64_t)gpu_vblank_period_cycles() / 32ull; if (s_card_handoff_armed) card_handoff_push(6, (uint8_t)(a6 & 0xffu)); if (++s_ape_torn_pulses >= 128) @@ -597,15 +598,15 @@ int sio_card_handoff_cap(void) { return CARD_HANDOFF_CAP; } int sio_card_handoff_armed(void) { return s_card_handoff_armed; } int sio_hold_present_for_card(void) { - /* NTSC VBlank period — matches interrupts.c VBLANK_CYCLES. */ + /* Current CRTC VBlank period — matches interrupts.c. */ enum { SIO_PRESENT_HOLD_STALE_VB = 10 }; - static const uint64_t stale_cycles = - 564480ull * (uint64_t)SIO_PRESENT_HOLD_STALE_VB; static uint32_t s_hold_seq; static uint64_t s_hold_progress_cyc; static int s_hold_armed; uint32_t seq; uint64_t now; + const uint64_t stale_cycles = + (uint64_t)gpu_vblank_period_cycles() * (uint64_t)SIO_PRESENT_HOLD_STALE_VB; if (!sio_card_protocol_active()) { s_hold_armed = 0; diff --git a/runtime/src/timers.c b/runtime/src/timers.c index 8e6f770d..316d44f0 100644 --- a/runtime/src/timers.c +++ b/runtime/src/timers.c @@ -16,6 +16,7 @@ #include "timers.h" #include "event_ring.h" +#include "gpu.h" #include /* Mode register bit definitions */ @@ -210,7 +211,7 @@ void timers_advance(uint32_t cycles) { timer_advance_counts(t, cycles); } else if (t == 1) { /* Timer 1 HBlank clock. NTSC has about 263 HBlanks per frame. */ - timer_advance_divided(t, cycles, 2146); + timer_advance_divided(t, cycles, gpu_display_is_pal() ? 2157u : 2146u); } else if (t == 0) { /* Timer 0 dotclock approximation; exact divider depends on GPU mode. */ timer_advance_divided(t, cycles, 5); @@ -223,7 +224,7 @@ static uint32_t timer_divisor(int t) { int src = (timers[t].mode >> 8) & 3; if (t == 2 && (src == 2 || src == 3)) return 8; /* sysclk/8 */ if (timer_uses_sysclk(t)) return 1; /* 1 cycle = 1 tick */ - if (t == 1) return 2146; /* HBlank approximation */ + if (t == 1) return gpu_display_is_pal() ? 2157u : 2146u; if (t == 0) return 5; /* dotclock approximation */ return 1; } @@ -334,9 +335,10 @@ void timers_write(uint32_t addr, uint32_t value) { } void timers_tick(int cycles) { - /* Timer 1 in HBlank mode: ~263 HBlanks per NTSC frame */ + /* Timer 1 in HBlank mode: one pulse per scanline this frame. */ if (!timer_uses_sysclk(1)) { - for (int h = 0; h < 263; h++) + const int lines = gpu_display_is_pal() ? 314 : 263; + for (int h = 0; h < lines; h++) timer_tick_one(1); }