This repository has been archived on 2026-08-27. You can view files and clone it, but cannot push or open issues or pull requests.
gopher64-netplay/src/device/vi.rs
Claude a0dce58c55
Some checks failed
Rust-lint / lint-linux (pull_request) Failing after 1s
Rust / build-linux (linux-x86_64, X64, ubuntu-24.04, x86_64-unknown-linux-gnu) (pull_request) Failing after 1s
Rust / build-android (29.0.14206865, ubuntu-24.04, 33) (pull_request) Failing after 1s
Rust / build-linux (linux-aarch64, ARM64, ubuntu-24.04-arm, aarch64-unknown-linux-gnu) (pull_request) Has been cancelled
Rust / build-windows (windows-aarch64, woa64, windows-11-arm, aarch64-pc-windows-msvc) (pull_request) Has been cancelled
Rust / build-windows (windows-x86_64, win64, windows-2025, x86_64-pc-windows-msvc) (pull_request) Has been cancelled
Rust / build-macos (macos-aarch64, ARM64, aarch64-apple-darwin) (pull_request) Has been cancelled
Rust / Collect release artifacts (pull_request) Has been cancelled
Allow savestate creation/loading during netplay (#2)
2026-08-25 04:37:36 -05:00

267 lines
9.4 KiB
Rust

use crate::{cheats, device, netplay, retroachievements, savestates, ui};
const VI_STATUS_REG: usize = 0;
const VI_ORIGIN_REG: usize = 1;
//const VI_WIDTH_REG: usize = 2;
const VI_V_INTR_REG: usize = 3;
const VI_CURRENT_REG: usize = 4;
//const VI_BURST_REG: usize = 5;
const VI_V_SYNC_REG: usize = 6;
const VI_H_SYNC_REG: usize = 7;
//const VI_LEAP_REG: usize = 8;
//const VI_H_START_REG: usize = 9;
const VI_V_START_REG: usize = 10;
//const VI_V_BURST_REG: usize = 11;
//const VI_X_SCALE_REG: usize = 12;
//const VI_Y_SCALE_REG: usize = 13;
pub const VI_REGS_COUNT: usize = 14;
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct Vi {
pub regs: [u32; VI_REGS_COUNT],
pub clock: u64,
pub delay: u64,
pub field: u32,
pub count_per_scanline: u64,
pub frame_time: f64,
pub elapsed_time: f64,
}
const MAX_LIMIT_FREQ: u64 = 3;
pub fn set_expected_refresh_rate(device: &mut device::Device) {
let expected_refresh_rate = device.vi.clock as f64
/ (device.vi.regs[VI_V_SYNC_REG] + 1) as f64
/ ((device.vi.regs[VI_H_SYNC_REG] & 0xFFF) + 1) as f64
* 2.0;
device.vi.frame_time = 1.0 / expected_refresh_rate;
device.vi.delay = (device.cpu.clock_rate as f64 / expected_refresh_rate) as u64;
device.vi.count_per_scanline = device.vi.delay / (device.vi.regs[VI_V_SYNC_REG] + 1) as u64;
}
fn reset_pace_deadline(device: &mut device::Device) {
device.speed_limiter.next_pace_deadline = None;
}
fn set_vertical_interrupt(device: &mut device::Device) {
if device::events::get_event(device, device::events::EVENT_TYPE_VI).is_none() {
device::events::create_event(device, device::events::EVENT_TYPE_VI, device.vi.delay)
}
}
fn set_current_line(device: &mut device::Device) {
if let Some(next_vi) = device::events::get_event(device, device::events::EVENT_TYPE_VI) {
device.vi.regs[VI_CURRENT_REG] =
((device.vi.delay.saturating_sub(
next_vi.count - device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG],
)) / device.vi.count_per_scanline) as u32;
// wrap around VI_CURRENT_REG if needed
if device.vi.regs[VI_CURRENT_REG] >= device.vi.regs[VI_V_SYNC_REG] {
device.vi.regs[VI_CURRENT_REG] -= device.vi.regs[VI_V_SYNC_REG]
}
}
/* update current field */
device.vi.regs[VI_CURRENT_REG] = (device.vi.regs[VI_CURRENT_REG] & !1) | device.vi.field;
ui::video::set_register(VI_CURRENT_REG as u32, device.vi.regs[VI_CURRENT_REG])
}
pub fn read_regs(
device: &mut device::Device,
address: u64,
_access_size: device::memory::AccessSize,
) -> u32 {
let reg = (address & 0xFFFF) >> 2;
if reg as usize == VI_CURRENT_REG {
set_current_line(device)
}
device::cop0::add_cycles(device, 20);
device.vi.regs[reg as usize]
}
pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u32) {
let reg = (address & 0xFFFF) >> 2;
match reg as usize {
VI_CURRENT_REG => device::mi::clear_rcp_interrupt(device, device::mi::MI_INTR_VI),
VI_V_SYNC_REG => {
if device.vi.regs[reg as usize] != value & mask {
device::memory::masked_write_32(&mut device.vi.regs[reg as usize], value, mask);
set_vertical_interrupt(device);
set_expected_refresh_rate(device);
}
}
VI_H_SYNC_REG => {
if device.vi.regs[reg as usize] != value & mask {
device::memory::masked_write_32(&mut device.vi.regs[reg as usize], value, mask);
set_expected_refresh_rate(device);
}
}
VI_ORIGIN_REG => {
let current_origin = device.vi.regs[reg as usize];
device::memory::masked_write_32(&mut device.vi.regs[reg as usize], value, mask);
if current_origin != device.vi.regs[reg as usize] {
// Savestate requests are processed during netplay too.
// `process_savestates` routes explicit saves/loads to the
// in-memory netplay savestate when a session is active, while
// auto-rewind stays offline-only. Offline behavior is
// unchanged (this previously ran unconditionally for
// `netplay.is_none()`).
savestates::process_savestates(device);
if !netplay::in_rollback(device.netplay.as_ref()) {
let _ = device.ui.video.fps_tx.as_ref().unwrap().send(true);
}
}
}
_ => {
device::memory::masked_write_32(&mut device.vi.regs[reg as usize], value, mask);
}
}
ui::video::set_register(reg as u32, device.vi.regs[reg as usize])
}
pub fn update_screen(device: &mut device::Device) {
if !netplay::in_rollback(device.netplay.as_ref()) {
ui::video::render_frame();
}
let (speed_limiter_toggled, paused) = ui::video::check_callback(device);
if speed_limiter_toggled {
reset_pace_deadline(device);
}
if !netplay::in_rollback(device.netplay.as_ref())
&& device
.speed_limiter
.frame_counter
.is_multiple_of(device.speed_limiter.limit_freq)
&& device.speed_limiter.enabled
{
speed_limiter(device);
}
if !netplay::in_rollback(device.netplay.as_ref()) {
ui::video::update_screen();
device.speed_limiter.frame_counter += 1;
let _ = device.ui.video.vis_tx.as_ref().unwrap().send(true);
}
if device.netplay.is_some() {
device.netplay.as_mut().unwrap().inputs = netplay::process_requests(device);
} else if paused {
if retroachievements::get_hardcore() {
ui::video::onscreen_message(
"Cannot pause in RA hardcore mode",
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
);
} else {
ui::video::pause_loop(&mut device.ui, device.vi.frame_time);
}
}
}
pub fn vertical_interrupt_event(device: &mut device::Device) {
device.vi.elapsed_time += device.vi.frame_time;
if device.cheats.enabled {
cheats::execute_cheats(device, device.cheats.cheats.clone());
}
retroachievements::do_frame();
/* toggle vi field if in interlaced mode */
device.vi.field ^= (device.vi.regs[VI_STATUS_REG] >> 6) & 0x1;
device::mi::set_rcp_interrupt(device, device::mi::MI_INTR_VI);
device::events::create_event_at(
device,
device::events::EVENT_TYPE_VI,
device.cpu.next_event_count + device.vi.delay,
);
let v_intr = device.vi.regs[VI_V_INTR_REG] & 0x3FF;
let v_start = (device.vi.regs[VI_V_START_REG] >> 16) & 0x3FF;
if v_start > v_intr {
device::events::create_event(
device,
device::events::EVENT_TYPE_UPDATE_SCREEN,
(v_start - v_intr) as u64 * device.vi.count_per_scanline,
);
}
}
pub fn init(device: &mut device::Device) {
if device.cart.pal {
device.vi.clock = 49656530
} else {
device.vi.clock = 48681812
}
}
fn speed_limiter(device: &mut device::Device) {
let mut speed_limiter_toggled = false;
let mut interval = std::time::Duration::from_secs_f64(
device.vi.frame_time * device.speed_limiter.limit_freq as f64,
);
if let Some(netplay) = &device.netplay {
let ahead = netplay.session.frames_ahead();
if ahead > 0 {
interval = interval.mul_f64(1.0 + 0.05 * ahead.min(2) as f64);
} else if ahead < 0 {
interval = interval.mul_f64(1.0 - 0.05 * (-ahead).min(2) as f64);
}
}
let now = std::time::Instant::now();
match device.speed_limiter.next_pace_deadline {
None => {
device.speed_limiter.next_pace_deadline = Some(now + interval);
speed_limiter_toggled = true;
}
Some(deadline) => {
if now < deadline {
let dur = deadline - now;
spin_sleep::sleep(dur);
if dur < device.speed_limiter.min_wait_time {
device.speed_limiter.min_wait_time = dur;
}
} else {
//println!("did not sleep");
device.speed_limiter.min_wait_time = std::time::Duration::from_secs(0);
}
let mut next = deadline + interval;
let t = std::time::Instant::now();
while next <= t {
next += interval;
}
device.speed_limiter.next_pace_deadline = Some(next);
}
}
if std::time::Instant::now().duration_since(device.speed_limiter.limit_freq_check)
> std::time::Duration::from_secs(1)
{
if !speed_limiter_toggled {
if device.speed_limiter.min_wait_time == std::time::Duration::from_secs(0)
&& device.speed_limiter.limit_freq < MAX_LIMIT_FREQ
{
device.speed_limiter.limit_freq += 1;
reset_pace_deadline(device);
} else if device.speed_limiter.min_wait_time
> std::time::Duration::from_secs_f64(device.vi.frame_time)
&& device.speed_limiter.limit_freq > 1
{
device.speed_limiter.limit_freq -= 1;
reset_pace_deadline(device);
}
}
//println!("limit freq: {}", device.vi.limit_freq);
device.speed_limiter.min_wait_time = std::time::Duration::from_secs(1);
device.speed_limiter.limit_freq_check = std::time::Instant::now();
}
}