feat(serial): model disconnected multiplayer configuration

This commit is contained in:
0xrsydn 2026-10-07 05:02:23 +07:00
commit c5e19baed4
14 changed files with 500 additions and 44 deletions

View file

@ -7,7 +7,10 @@ mod serial;
use crate::audio::Audio;
pub use crate::audio::{FIFO_A, FIFO_B, SOUNDBIAS, SOUNDCNT_H, SOUNDCNT_X, SOUND_START, WAVE_RAM};
pub(crate) use serial::Serial;
pub use serial::{JOYCNT, JOY_RECV, JOY_TRANS, RCNT, SIOCNT, SIODATA32, SIODATA8};
pub use serial::{
JOYCNT, JOY_RECV, JOY_TRANS, RCNT, SIOCNT, SIODATA32, SIODATA8, SIOMLT_SEND, SIOMULTI0,
SIOMULTI1, SIOMULTI2, SIOMULTI3,
};
mod timer_step;
pub(crate) use timer_step::TimerStep;
use timer_step::STAGED_IRQ_MASK;

View file

@ -1,9 +1,14 @@
//! Disconnected normal serial shifter with nominal internal clocks.
//! Disconnected serial: normal shifting and idle multiplayer child configuration.
use super::replace_byte;
pub const SIODATA32: u32 = 0x0400_0120;
pub const SIOMULTI0: u32 = SIODATA32;
pub const SIOMULTI1: u32 = 0x0400_0122;
pub const SIOMULTI2: u32 = 0x0400_0124;
pub const SIOMULTI3: u32 = 0x0400_0126;
pub const SIOCNT: u32 = 0x0400_0128;
pub const SIODATA8: u32 = 0x0400_012a;
pub const SIOMLT_SEND: u32 = SIODATA8;
pub const RCNT: u32 = 0x0400_0134;
pub const JOYCNT: u32 = 0x0400_0140;
pub const JOY_RECV: u32 = 0x0400_0150;
@ -11,8 +16,9 @@ pub const JOY_TRANS: u32 = 0x0400_0154;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Serial {
data: u32,
send: u8,
data: u32, // Shared SIODATA32 / SIOMULTI0-1 lanes.
multi_extra: u32, // SIOMULTI2-3; no connected parent can update these.
send: u16, // Shared low byte; high byte is accessible only in multiplayer format.
control: u16,
rcnt: u16,
bits_left: u8,
@ -22,14 +28,14 @@ pub(crate) struct Serial {
impl Serial {
pub(crate) fn mapped(address: u32) -> bool {
matches!(address,
SIODATA32..=0x0400_0123 | SIOCNT..=0x0400_012b |
SIODATA32..=0x0400_012b |
RCNT..=0x0400_0135 | JOYCNT..=0x0400_0141 |
JOY_RECV..=0x0400_0157)
}
pub(crate) fn unsupported(address: u32, value: u8) -> Option<&'static str> {
match address {
0x0400_0129 if value & 0x20 != 0 => Some("multiplayer/UART serial mode"),
0x0400_0129 if value & 0x30 == 0x30 => Some("UART serial mode"),
0x0400_0135 if value & 0xc0 == 0xc0 => Some("Joybus serial mode"),
0x0400_0135 if value & 0x81 == 0x81 => Some("GPIO serial interrupt enable"),
JOYCNT if value & 0x40 != 0 => Some("Joybus interrupt enable"),
@ -38,8 +44,14 @@ impl Serial {
}
}
fn gpio(&self) -> bool {
self.rcnt & 0x8000 != 0
}
fn multiplayer_format(&self) -> bool {
self.control & 0x3000 == 0x2000
}
fn normal(&self) -> bool {
self.rcnt & 0x8000 == 0
!self.gpio() && !self.multiplayer_format()
}
fn busy(&self) -> bool {
self.control & 0x80 != 0
@ -72,9 +84,21 @@ impl Serial {
}
let mut next = *self;
next.merge(address, bytes);
if !next.multiplayer_format()
&& (0..bytes.len())
.any(|offset| (SIOMULTI2..SIOCNT).contains(&(address + offset as u32)))
{
return Some((
address,
bytes[0],
"multiplayer receive registers outside multiplayer mode",
));
}
// Clearing start always permits cancellation/reconfiguration. Other
// live changes after a shifted bit lack an independently verified model.
if self.normal() && self.busy() && next.busy() {
// Check the written start bit before mode-specific read-only masking.
// A high-byte mode change must not silently cancel a clocked normal transfer.
if self.normal() && self.busy() && self.control_value(address, bytes) & 0x80 != 0 {
let changed_mode = self.normal() != next.normal();
let changed_clock = (self.control ^ next.control) & 0x1003 != 0;
let data_touched = (0..bytes.len()).any(|offset| {
@ -108,18 +132,33 @@ impl Serial {
if (SIODATA32..SIODATA32 + 4).contains(&address) {
return Some(self.data.to_le_bytes()[(address - SIODATA32) as usize]);
}
if (SIOMULTI2..SIOCNT).contains(&address) {
return self
.multiplayer_format()
.then(|| self.multi_extra.to_le_bytes()[(address - SIOMULTI2) as usize]);
}
let value = match address & !1 {
SIOCNT => {
let pins = if self.gpio() { self.gpio_pins() } else { 15 };
// SI is pulled high without a cable. Multiplayer drives SD high while idle.
// ID is a deterministic zero placeholder until a transfer (none can occur here).
self.control
| if self.normal() {
4
| (pins & 4)
| if self.multiplayer_format() {
(pins & 2) << 2
} else {
self.gpio_pins() & 4
0
}
}
SIODATA8 => u16::from(self.send),
SIODATA8 => {
if self.multiplayer_format() {
self.send
} else {
self.send & 255
}
}
RCNT => {
if self.normal() && address & 1 == 0 {
if !self.gpio() && address & 1 == 0 {
return None;
}
(self.rcnt & !15) | self.gpio_pins()
@ -130,16 +169,40 @@ impl Serial {
Some(value.to_le_bytes()[(address & 1) as usize])
}
fn control_value(&self, address: u32, bytes: &[u8]) -> u16 {
let mut control = self.control;
for (offset, &value) in bytes.iter().enumerate() {
let at = address + offset as u32;
if (SIOCNT..SIOCNT + 2).contains(&at) {
control = replace_byte(control, at, value);
}
}
control
}
fn merge(&mut self, address: u32, bytes: &[u8]) {
// Select the final format first: a word can change mode and write SEND together.
let control = self.control_value(address, bytes);
self.control = control
& if control & 0x3000 == 0x2000 {
0x6f03
} else {
0x508b
};
for (offset, &value) in bytes.iter().enumerate() {
let at = address + offset as u32;
if (SIODATA32..SIODATA32 + 4).contains(&at) {
let shift = (at - SIODATA32) * 8;
self.data = (self.data & !(255 << shift)) | (u32::from(value) << shift);
} else if (SIOMULTI2..SIOCNT).contains(&at) {
let shift = (at - SIOMULTI2) * 8;
self.multi_extra =
(self.multi_extra & !(255 << shift)) | (u32::from(value) << shift);
} else {
match at & !1 {
SIOCNT => self.control = replace_byte(self.control, at, value) & 0x508b,
SIODATA8 if at & 1 == 0 => self.send = value,
SIODATA8 if self.multiplayer_format() || at & 1 == 0 => {
self.send = replace_byte(self.send, at, value);
}
RCNT => self.rcnt = replace_byte(self.rcnt, at, value) & 0xc1ff,
_ => {}
}
@ -179,7 +242,7 @@ impl Serial {
if self.width() == 32 {
self.data = (self.data << 1) | 1;
} else {
self.send = (self.send << 1) | 1;
self.send = (self.send & 0xff00) | u16::from(((self.send as u8) << 1) | 1);
}
self.bits_left -= 1;
if self.bits_left == 0 {

View file

@ -11,6 +11,8 @@ mod iwram_bus;
#[cfg(test)]
mod iwram_history_tests;
#[cfg(test)]
mod multiplayer_step_tests;
#[cfg(test)]
mod prefetch_tests;
#[cfg(test)]
mod serial_step_tests;

View file

@ -0,0 +1,115 @@
//! Original multiplayer mode/alias checks at CPU bus phases and during preflight.
use super::{timing_event_tests::prepared, *};
use crate::{
io::{IF, SIOCNT, SIOMLT_SEND, SIOMULTI0, SIOMULTI2},
machine::Machine,
};
#[test]
fn mode_change_uses_normal_completion_phase_before_read_only_busy_masking() {
for elapsed in [56, 57] {
let (cpu, mut memory) = prepared(false, &[0xe5c10000], &[(0, 0x20), (1, SIOCNT + 1)]); // STRB r0,[r1]
memory.write16(SIOCNT, 0x4083).unwrap();
memory.advance_cycles(elapsed);
let serial = memory.io.serial;
let mut machine = Machine::new(cpu, memory);
let cpu = machine.cpu().clone();
if elapsed == 57 {
machine.step().unwrap(); // Normal completion, then multiplayer selection at cycle 64.
assert_eq!(machine.cycles(), 64);
assert_eq!(machine.memory().read16(SIOCNT).unwrap(), 0x200f);
assert_eq!(machine.memory().read16(IF).unwrap(), 0x80);
assert_eq!(machine.memory().io.serial.next_event_cycles(), None);
} else {
let error = machine.step().unwrap_err();
assert!(error.to_string().contains("serial reconfiguration"));
assert_eq!(machine.step(), Err(error));
assert_eq!(machine.cpu(), &cpu);
assert_eq!(machine.cycles(), 56);
assert_eq!(machine.memory().io.serial, serial);
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
}
}
}
#[test]
fn arm_block_store_validates_receive_aliases_and_later_mode_before_committing_any_word() {
for unsupported in [false, true] {
let (cpu, mut memory) = prepared(
false,
&[0xe8a0000e],
&[
(0, SIOMULTI0),
(1, 0x12345678),
(2, 0xabcdef90),
(3, if unsupported { 0x98763000 } else { 0x98762000 }),
],
);
memory.write16(SIOCNT, 0x2000).unwrap();
let serial = memory.io.serial;
let mut machine = Machine::new(cpu, memory);
let cpu = machine.cpu().clone();
if unsupported {
let error = machine.step().unwrap_err();
assert!(error.to_string().contains("UART serial mode"));
assert_eq!(machine.step(), Err(error));
assert_eq!(machine.memory().io.serial, serial);
assert_eq!(machine.cpu(), &cpu);
assert_eq!(machine.cycles(), 0);
} else {
machine.step().unwrap();
assert_eq!(machine.cpu().registers()[0], SIOMULTI0 + 12);
assert_eq!(machine.memory().read32(SIOMULTI0).unwrap(), 0x12345678);
assert_eq!(machine.memory().read32(SIOMULTI2).unwrap(), 0xabcdef90);
assert_eq!(machine.memory().read16(SIOMLT_SEND).unwrap(), 0x9876);
assert_eq!(machine.memory().read16(SIOCNT).unwrap(), 0x200c);
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
assert_eq!(machine.last_timing().data_cycles, 3);
}
}
}
#[test]
fn arm_block_load_observes_all_multiplayer_lanes_from_staged_registers() {
let (cpu, mut memory) = prepared(false, &[0xe8b0000e], &[(0, SIOMULTI0)]); // LDMIA r0!,{r1-r3}
memory.write32(SIOCNT, 0x98766083).unwrap();
memory.write32(SIOMULTI0, 0x12345678).unwrap();
memory.write32(SIOMULTI2, 0xabcdef90).unwrap();
let mut machine = Machine::new(cpu, memory);
machine.step().unwrap();
assert_eq!(
&machine.cpu().registers()[1..4],
&[0x12345678, 0xabcdef90, 0x9876600f]
);
assert_eq!(machine.cpu().registers()[0], SIOMULTI0 + 12);
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
}
#[test]
fn shadow_mode_changes_control_later_access_validation_and_preserve_atomicity() {
let (_, mut memory) = prepared(false, &[0xe1a00000], &[]);
memory
.write_words(&[(SIOCNT, 0x2000), (SIOMULTI2, 0x12345678), (SIOCNT, 0)])
.unwrap();
assert_eq!(
memory.read32(SIOMULTI2),
Err(MemoryError::Unmapped(SIOMULTI2))
);
memory.write16(SIOCNT, 0x2000).unwrap();
assert_eq!(memory.read32(SIOMULTI2).unwrap(), 0x12345678);
let serial = memory.io.serial;
assert!(memory
.write_words(&[(0x02000000, 0xabcd), (SIOCNT, 0), (SIOMULTI2, 0)])
.is_err());
assert_eq!(memory.io.serial, serial);
assert_eq!(memory.read32(0x02000000).unwrap(), 0);
// A staged read cannot fall back to committed multiplayer data after leaving the mode.
memory.begin_timer_step();
memory.write16(SIOCNT, 0).unwrap();
assert_eq!(
memory.read32(SIOMULTI2),
Err(MemoryError::Unmapped(SIOMULTI2))
);
memory.discard_timer_step();
assert_eq!(memory.read32(SIOMULTI2).unwrap(), 0x12345678);
}