clarify semantics and implementation of ISaveRam.SaveRamModified

This commit is contained in:
Morilli 2025-06-04 09:09:55 +02:00
parent 5fa597ad8a
commit a2cf4e0797
43 changed files with 63 additions and 62 deletions

Binary file not shown.

Binary file not shown.

View File

@ -102,7 +102,7 @@ namespace BizHawk.Client.EmuHawk
MaxDropDownItems = 32,
Size = new(152, 21),
};
if (_emulator.HasSaveRam() && _emulator.AsSaveRam().CloneSaveRam() is not null) StartFromCombo.Items.Add(START_FROM_SAVERAM);
if (_emulator.HasSaveRam() && _emulator.AsSaveRam().CloneSaveRam(clearDirty: false) is not null) StartFromCombo.Items.Add(START_FROM_SAVERAM);
if (_emulator.HasSavestates()) StartFromCombo.Items.Add(START_FROM_SAVESTATE);
DefaultAuthorCheckBox = new()
@ -243,7 +243,7 @@ namespace BizHawk.Client.EmuHawk
{
var core = _emulator.AsSaveRam();
movieToRecord.StartsFromSaveRam = true;
movieToRecord.SaveRam = core.CloneSaveRam();
movieToRecord.SaveRam = core.CloneSaveRam(clearDirty: false);
}
_mainForm.StartNewMovie(movieToRecord, true);

View File

@ -55,7 +55,7 @@ namespace BizHawk.Client.EmuHawk
{
if (AskSaveChanges())
{
var saveRam = SaveRamEmulator?.CloneSaveRam() ?? throw new Exception("No SaveRam");
var saveRam = SaveRamEmulator?.CloneSaveRam(clearDirty: false) ?? throw new Exception("No SaveRam");
GoToFrame(TasView.AnyRowsSelected ? TasView.FirstSelectedRowIndex : 0);
var newProject = CurrentTasMovie.ConvertToSaveRamAnchoredMovie(saveRam);
MainForm.PauseEmulator();

View File

@ -31,13 +31,13 @@ namespace BizHawk.Emulation.Common
return false;
}
public byte[] CloneSaveRam()
public byte[] CloneSaveRam(bool clearDirty)
{
var linkedBuffers = new List<byte[]>();
int len = 0;
for (int i = 0; i < _numCores; i++)
{
linkedBuffers.Add(_linkedCores[i].AsSaveRam().CloneSaveRam() ?? Array.Empty<byte>());
linkedBuffers.Add(_linkedCores[i].AsSaveRam().CloneSaveRam(clearDirty) ?? Array.Empty<byte>());
len += linkedBuffers[i].Length;
}
byte[] ret = new byte[len];

View File

@ -14,7 +14,8 @@
/// Unfortunately, the core may think differently of a nonexisting (null) saveram vs a 0 size saveram.
/// Frontend users of the ISaveRam should treat null as nonexisting (and thus not even write the file, so that the "does not exist" condition can be roundtripped and not confused with an empty file)
/// </summary>
byte[]? CloneSaveRam();
/// <param name="clearDirty">Whether the saveram should be considered in a clean state after this call for purposes of <see cref="SaveRamModified"/></param>
byte[]? CloneSaveRam(bool clearDirty = true);
/// <summary>
/// store new SaveRAM to the emu core. the data should be the same size as the return from ReadSaveRam()
@ -22,12 +23,9 @@
void StoreSaveRam(byte[] data);
/// <summary>
/// Gets a value indicating whether or not SaveRAM has been modified since the last save
/// TODO: This is not the best interface. What defines a "save"? I suppose a Clone(), right? at least specify that here.
/// Clone() should probably take an option that says whether to clear the dirty flag.
/// And anyway, cores might not know if they can even track a functional dirty flag -- we should convey that fact somehow
/// (reminder: do that with flags, so we don't have to change the interface 10000 times)
/// Dirty SaveRAM can in principle be determined by the frontend in that case, but it could possibly be too slow for the file menu dropdown or other things
/// Gets a value indicating whether or not SaveRAM has been modified since the last call to either <see cref="StoreSaveRam"/> or <see cref="CloneSaveRam"/> (when passing true).
/// Cores may choose to always return true or return true for any non-default saveram.
/// This value should be considered a hint more than an absolute truth.
/// </summary>
bool SaveRamModified { get; }
}

View File

@ -18,7 +18,7 @@ namespace BizHawk.Emulation.Cores.Arcades.MAME
public bool SaveRamModified => _nvramFilenames.Count > 0;
public byte[] CloneSaveRam()
public byte[] CloneSaveRam(bool clearDirty)
{
if (_nvramFilenames.Count == 0)
{

View File

@ -16,7 +16,7 @@ namespace BizHawk.Emulation.Cores.Computers.AppleII
public bool SaveRamModified => true;
public byte[] CloneSaveRam()
public byte[] CloneSaveRam(bool clearDirty)
{
using var ms = new MemoryStream();
using var bw = new BinaryWriter(ms);

View File

@ -104,8 +104,6 @@ namespace BizHawk.Emulation.Cores.Computers.Commodore64.Cartridge
if (_chipB.CheckDataDirty() || _deltaB == null)
_deltaB = DeltaSerializer.GetDelta<byte>(_originalMediaB, _chipB.Data).ToArray();
_saveRamDirty = false;
}
protected override void SyncStateInternal(Serializer ser)
@ -274,7 +272,7 @@ namespace BizHawk.Emulation.Cores.Computers.Commodore64.Cartridge
);
}
public byte[] CloneSaveRam()
public byte[] CloneSaveRam(bool clearDirty)
{
FlushSaveRam();
@ -287,7 +285,7 @@ namespace BizHawk.Emulation.Cores.Computers.Commodore64.Cartridge
writer.Write(_deltaB);
writer.Flush();
_saveRamDirty = false;
if (clearDirty) _saveRamDirty = false;
return result.ToArray();
}

View File

@ -39,7 +39,7 @@ public sealed partial class Drive1541 : ISaveRam
public bool SaveRamModified { get; private set; } = false;
public byte[] CloneSaveRam()
public byte[] CloneSaveRam(bool clearDirty)
{
SaveDeltas();
@ -56,7 +56,7 @@ public sealed partial class Drive1541 : ISaveRam
}
}
SaveRamModified = false;
if (clearDirty) SaveRamModified = false;
return ms.ToArray();
}

View File

@ -4,7 +4,7 @@ namespace BizHawk.Emulation.Cores.Computers.MSX
{
public partial class MSX : ISaveRam
{
public byte[] CloneSaveRam()
public byte[] CloneSaveRam(bool clearDirty)
{
return (byte[]) SaveRAM?.Clone();
}

View File

@ -4,7 +4,7 @@ namespace BizHawk.Emulation.Cores.Atari.A7800Hawk
{
public partial class A7800Hawk : ISaveRam
{
public byte[] CloneSaveRam()
public byte[] CloneSaveRam(bool clearDirty)
{
return (byte[])_hsram.Clone();
}

View File

@ -97,7 +97,7 @@ namespace BizHawk.Emulation.Cores.Atari.Jaguar
public abstract bool SaveRamIsDirty();
[BizImport(CC)]
public abstract void GetSaveRam(byte[] dst);
public abstract void GetSaveRam(byte[] dst, bool clearDirty);
[BizImport(CC)]
public abstract void PutSaveRam(byte[] src);

View File

@ -8,7 +8,7 @@ namespace BizHawk.Emulation.Cores.Atari.Jaguar
public new bool SaveRamModified => _saveRamSize > 0 && _core.SaveRamIsDirty();
public new byte[] CloneSaveRam()
public new byte[] CloneSaveRam(bool clearDirty)
{
if (_saveRamSize == 0)
{
@ -16,7 +16,7 @@ namespace BizHawk.Emulation.Cores.Atari.Jaguar
}
byte[] ret = new byte[_saveRamSize];
_core.GetSaveRam(ret);
_core.GetSaveRam(ret, clearDirty);
return ret;
}

View File

@ -6,7 +6,7 @@ namespace BizHawk.Emulation.Cores.Atari.Lynx
{
public partial class Lynx : ISaveRam
{
public byte[] CloneSaveRam()
public byte[] CloneSaveRam(bool clearDirty)
{
if (!LibLynx.GetSaveRamPtr(Core, out var size, out var data))
{

View File

@ -4,7 +4,7 @@ namespace BizHawk.Emulation.Cores.Consoles.Vectrex
{
public partial class VectrexHawk : ISaveRam
{
public byte[] CloneSaveRam()
public byte[] CloneSaveRam(bool clearDirty)
{
return null;
}

View File

@ -4,7 +4,7 @@ namespace BizHawk.Emulation.Cores.Consoles.O2Hawk
{
public partial class O2Hawk : ISaveRam
{
public byte[] CloneSaveRam()
public byte[] CloneSaveRam(bool clearDirty)
{
return (byte[])cart_RAM?.Clone();
}

View File

@ -9,10 +9,9 @@ namespace BizHawk.Emulation.Cores.Nintendo.BSNES
private IntPtr _saveRam;
private int _saveRamSize;
// yeah this is not the best. this will basically always return true as long as the saveRam exists.
public bool SaveRamModified => _saveRamSize != 0;
public byte[] CloneSaveRam()
public byte[] CloneSaveRam(bool clearDirty)
{
if (_saveRamSize == 0) return null;

View File

@ -9,7 +9,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.GBA
{
private readonly byte[] _saveScratch = new byte[262144];
public byte[] CloneSaveRam()
public byte[] CloneSaveRam(bool clearDirty)
{
int len = LibmGBA.BizGetSaveRam(Core, _saveScratch, _saveScratch.Length);
if (len == _saveScratch.Length)

View File

@ -4,7 +4,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.GBHawk
{
public partial class GBHawk : ISaveRam
{
public byte[] CloneSaveRam()
public byte[] CloneSaveRam(bool clearDirty)
{
return (byte[])cart_RAM?.Clone();
}

View File

@ -4,7 +4,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.GBHawkLink
{
public partial class GBHawkLink : ISaveRam
{
public byte[] CloneSaveRam()
public byte[] CloneSaveRam(bool clearDirty)
{
if (L.cart_RAM != null || R.cart_RAM != null)
{

View File

@ -4,7 +4,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.GBHawkLink3x
{
public partial class GBHawkLink3x : ISaveRam
{
public byte[] CloneSaveRam()
public byte[] CloneSaveRam(bool clearDirty)
{
if (L.cart_RAM != null || C.cart_RAM != null || R.cart_RAM != null)
{

View File

@ -4,7 +4,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.GBHawkLink4x
{
public partial class GBHawkLink4x : ISaveRam
{
public byte[] CloneSaveRam()
public byte[] CloneSaveRam(bool clearDirty)
{
if (A.cart_RAM != null || B.cart_RAM != null || C.cart_RAM != null || D.cart_RAM != null)
{

View File

@ -7,7 +7,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.Gameboy
// need to wire more stuff into the core to actually know this
public bool SaveRamModified => LibGambatte.gambatte_getsavedatalength(GambatteState) != 0;
public byte[] CloneSaveRam()
public byte[] CloneSaveRam(bool clearDirty)
{
var length = LibGambatte.gambatte_getsavedatalength(GambatteState);

View File

@ -4,7 +4,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.N64
{
public partial class N64 : ISaveRam
{
public byte[] CloneSaveRam()
public byte[] CloneSaveRam(bool clearDirty)
{
return api.SaveSaveram();
}

View File

@ -142,7 +142,7 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.NDS
public abstract void PutSaveRam(IntPtr console, byte[] data, uint len);
[BizImport(CC)]
public abstract void GetSaveRam(IntPtr console, byte[] data);
public abstract void GetSaveRam(IntPtr console, byte[] data, bool clearDirty);
[BizImport(CC)]
public abstract int GetSaveRamLength(IntPtr console);

View File

@ -9,7 +9,7 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.NDS
public new bool SaveRamModified => IsDSiWare ? DSiWareSaveLength != 0 : _core.SaveRamIsDirty();
public new byte[] CloneSaveRam()
public new byte[] CloneSaveRam(bool clearDirty)
{
if (IsDSiWare)
{
@ -43,7 +43,7 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.NDS
if (length > 0)
{
var ret = new byte[length];
_core.GetSaveRam(_console, ret);
_core.GetSaveRam(_console, ret, clearDirty);
return ret;
}
@ -73,4 +73,4 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.NDS
}
}
}
}
}

View File

@ -15,7 +15,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.NES
}
}
public byte[] CloneSaveRam()
public byte[] CloneSaveRam(bool clearDirty)
{
if (Board is FDS fds)
{

View File

@ -4,7 +4,7 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.QuickNES
{
public partial class QuickNES : ISaveRam
{
public byte[] CloneSaveRam()
public byte[] CloneSaveRam(bool clearDirty)
{
LibQuickNES.ThrowStringError(QN.qn_battery_ram_save(Context, _saveRamBuff, _saveRamBuff.Length));
return (byte[])_saveRamBuff.Clone();

View File

@ -11,7 +11,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.SNES
Api.QUERY_get_memory_size(LibsnesApi.SNES_MEMORY.CARTRIDGE_RAM) != 0
|| Api.QUERY_get_memory_size(LibsnesApi.SNES_MEMORY.SGB_CARTRAM) != 0;
public byte[] CloneSaveRam()
public byte[] CloneSaveRam(bool clearDirty)
{
using (Api.EnterExit())
{

View File

@ -6,7 +6,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.Sameboy
{
public bool SaveRamModified => LibSameboy.sameboy_sramlen(SameboyState) != 0;
public byte[] CloneSaveRam()
public byte[] CloneSaveRam(bool clearDirty)
{
int length = LibSameboy.sameboy_sramlen(SameboyState);

View File

@ -6,8 +6,9 @@ namespace BizHawk.Emulation.Cores.PCEngine
{
public bool SaveRamModified { get; private set; }
public byte[] CloneSaveRam()
public byte[] CloneSaveRam(bool clearDirty)
{
if (clearDirty) SaveRamModified = false;
return (byte[]) BRAM?.Clone();
}
@ -17,6 +18,8 @@ namespace BizHawk.Emulation.Cores.PCEngine
{
Array.Copy(data, BRAM, data.Length);
}
SaveRamModified = false;
}
}
}

View File

@ -51,7 +51,7 @@ namespace BizHawk.Emulation.Cores.Consoles.SNK
}
}
public new byte[] CloneSaveRam()
public new byte[] CloneSaveRam(bool clearDirty)
{
_exe.AddTransientFile(new byte[0], "SAV:flash");

View File

@ -4,7 +4,7 @@ namespace BizHawk.Emulation.Cores.Sega.GGHawkLink
{
public partial class GGHawkLink : ISaveRam
{
public byte[] CloneSaveRam()
public byte[] CloneSaveRam(bool clearDirty)
{
if ((L.SaveRAM != null) || (R.SaveRAM != null))
{

View File

@ -4,8 +4,9 @@ namespace BizHawk.Emulation.Cores.Sega.MasterSystem
{
public partial class SMS : ISaveRam
{
public byte[] CloneSaveRam()
public byte[] CloneSaveRam(bool clearDirty)
{
if (clearDirty) SaveRamModified = false;
return (byte[]) SaveRAM?.Clone();
}
@ -15,6 +16,8 @@ namespace BizHawk.Emulation.Cores.Sega.MasterSystem
{
Array.Copy(data, SaveRAM, data.Length);
}
SaveRamModified = false;
}
public bool SaveRamModified { get; private set; }

View File

@ -118,7 +118,7 @@ namespace BizHawk.Emulation.Cores.Consoles.Sega.Saturn
public new bool SaveRamModified => true;
public new byte[] CloneSaveRam()
public new byte[] CloneSaveRam(bool clearDirty)
{
var data = new byte[_saturnus.GetSaveRamLength()];
_saturnus.GetSaveRam(data);

View File

@ -7,7 +7,7 @@ namespace BizHawk.Emulation.Cores.Consoles.Sega.gpgx
{
public partial class GPGX : ISaveRam
{
public byte[] CloneSaveRam()
public byte[] CloneSaveRam(bool clearDirty)
{
var size = 0;
var area = Core.gpgx_get_sram(ref size);

View File

@ -922,7 +922,7 @@ namespace BizHawk.Emulation.Cores.Sony.PSX
throw new InvalidOperationException("Async mode is not supported.");
}
public byte[] CloneSaveRam()
public byte[] CloneSaveRam(bool clearDirty)
{
var cfg = _SyncSettings.FIOConfig.ToLogical();
int nMemcards = cfg.NumMemcards;

View File

@ -11,7 +11,7 @@ namespace BizHawk.Emulation.Cores.WonderSwan
_saveramBuff = new byte[BizSwan.bizswan_saveramsize(Core)];
}
public byte[] CloneSaveRam()
public byte[] CloneSaveRam(bool clearDirty)
{
if (!BizSwan.bizswan_saveramsave(Core, _saveramBuff, _saveramBuff.Length))
{

View File

@ -12,7 +12,7 @@ namespace BizHawk.Emulation.Cores.Libretro
public bool SaveRamModified => _saveramSize > 0;
public byte[] CloneSaveRam()
public byte[] CloneSaveRam(bool clearDirty)
{
if (_saveramSize > 0)
{

View File

@ -158,7 +158,7 @@ namespace BizHawk.Emulation.Cores.Waterbox
}
}
public byte[] CloneSaveRam()
public byte[] CloneSaveRam(bool clearDirty)
{
if (_saveramSize == 0)
return null;

View File

@ -33,7 +33,7 @@ ECL_EXPORT void PutSaveRam(melonDS::NDS* nds, u8* data, u32 len)
}
}
ECL_EXPORT void GetSaveRam(melonDS::NDS* nds, u8* data)
ECL_EXPORT void GetSaveRam(melonDS::NDS* nds, u8* data, bool clearDirty)
{
const u32 ndsSaveLen = nds->GetNDSSaveLength();
const u32 gbaSaveLen = nds->GetGBASaveLength();
@ -41,13 +41,13 @@ ECL_EXPORT void GetSaveRam(melonDS::NDS* nds, u8* data)
if (ndsSaveLen)
{
memcpy(data, nds->GetNDSSave(), ndsSaveLen);
NdsSaveRamIsDirty = false;
if (clearDirty) NdsSaveRamIsDirty = false;
}
if (gbaSaveLen)
{
memcpy(data + ndsSaveLen, nds->GetGBASave(), gbaSaveLen);
GbaSaveRamIsDirty = false;
if (clearDirty) GbaSaveRamIsDirty = false;
}
}
@ -108,7 +108,7 @@ ECL_EXPORT void DSiWareSavsLength(melonDS::DSi* dsi, u32 titleId, u32* publicSav
// TODO - I don't like this approach with NAND
// Perhaps instead it would be better to use FileFlush to write to disk
// (guarded by frontend determinism switch, of course)
// (guarded by frontend determinism switch, of course)
ECL_EXPORT u32 GetNANDSize(melonDS::DSi* dsi)
{

View File

@ -107,17 +107,17 @@ ECL_EXPORT bool SaveRamIsDirty()
return IsMemTrack() ? mtDirty : eeprom_dirty;
}
ECL_EXPORT void GetSaveRam(u8* dst)
ECL_EXPORT void GetSaveRam(u8* dst, bool clearDirty)
{
if (IsMemTrack())
{
memcpy(dst, mtMem, sizeof(mtMem));
mtDirty = false;
if (clearDirty) mtDirty = false;
}
else
{
memcpy(dst, eeprom_ram, sizeof(eeprom_ram));
eeprom_dirty = false;
if (clearDirty) eeprom_dirty = false;
}
}