From cf68d04f313ace26ac777dd13c78d551f386eb1c Mon Sep 17 00:00:00 2001 From: Andrew Hutchings Date: Thu, 23 Jul 2026 15:46:25 +0100 Subject: [PATCH 1/2] akiko: keep NVRAM dirty when the backing-file write fails Clearing the dirty bit before the write meant a transient host error (full disk, missing directory, permissions) silently dropped the EEPROM contents: no later I2C STOP would retry, so nothing persisted until the guest wrote the NVRAM again. Clear it only after the write lands; a failure leaves it armed and the flush retries at the next STOP. Retries stay naturally rate-limited because only a STOP with dirty contents attempts a write. Same fix as the RP5C01 battmem flush in #265, which inherited the pattern from here. --- src/akiko.rs | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/akiko.rs b/src/akiko.rs index 1cd49dcd..16292cab 100644 --- a/src/akiko.rs +++ b/src/akiko.rs @@ -327,12 +327,16 @@ impl Nvram { self.sda_drive_low = false; self.phase = I2cPhase::DeviceAddress; if self.dirty { - self.dirty = false; if let Some(path) = &self.path { if let Err(e) = std::fs::write(path, &self.memory) { + // Stay dirty so the next STOP retries: a transient + // host error must not lose the EEPROM contents (save + // games) until the guest happens to write them again. log::warn!("cd32 nvram: writing {}: {e}", path.display()); + return; } } + self.dirty = false; } } } @@ -1666,6 +1670,36 @@ mod tests { i2c::stop(&mut akiko, &mut chip); } + /// A failed backing-file write must leave the EEPROM dirty so a + /// later STOP retries; clearing it up front would silently drop the + /// NVRAM contents (save games) on a transient host error. + #[test] + fn nvram_retries_the_backing_file_write_after_a_failure() { + let mut chip = no_chip(); + let mut akiko = Akiko::new(); + let dir = std::env::temp_dir().join(format!( + "copperline-akiko-nvram-retry-{}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&dir); + let path = dir.join("cd32-nvram.bin"); // parent dir missing + akiko.set_nvram_path(path.clone()); + + i2c::start(&mut akiko, &mut chip); + assert!(!i2c::write_byte(&mut akiko, &mut chip, 0xA2)); + assert!(!i2c::write_byte(&mut akiko, &mut chip, 0x42)); + assert!(!i2c::write_byte(&mut akiko, &mut chip, 0xDE)); + i2c::stop(&mut akiko, &mut chip); // flush fails + assert!(!path.exists()); + + std::fs::create_dir(&dir).unwrap(); + i2c::start(&mut akiko, &mut chip); + i2c::stop(&mut akiko, &mut chip); // still dirty: retried and lands + let bytes = std::fs::read(&path).unwrap(); + assert_eq!(bytes[0x142], 0xDE); + let _ = std::fs::remove_dir_all(&dir); + } + #[test] fn audio_play_streams_sectors_into_mixer_ring_and_notifies_end() { // Disc: 2 data sectors then 4 audio sectors of a known sample. From 5e11a5c3f34bfb0a89e4aeaa71dedd5d34d72034 Mon Sep 17 00:00:00 2001 From: Andrew Hutchings Date: Thu, 23 Jul 2026 16:21:34 +0100 Subject: [PATCH 2/2] akiko: give the nvram retry test a per-use unique temp directory Match the temp-naming convention the other filesystem-touching tests in this module use (pid + atomic counter). The pid-only name could not collide today -- only this one test uses it and the harness runs it once per process -- but the suffix keeps the name safe if another test ever reuses the pattern. --- src/akiko.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/akiko.rs b/src/akiko.rs index 16292cab..1e4b7db6 100644 --- a/src/akiko.rs +++ b/src/akiko.rs @@ -1677,8 +1677,12 @@ mod tests { fn nvram_retries_the_backing_file_write_after_a_failure() { let mut chip = no_chip(); let mut akiko = Akiko::new(); + let unique = { + static UNIQUE: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); + UNIQUE.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + }; let dir = std::env::temp_dir().join(format!( - "copperline-akiko-nvram-retry-{}", + "copperline-akiko-nvram-retry-{}-{unique}", std::process::id() )); let _ = std::fs::remove_dir_all(&dir);