diff --git a/ExternalCoreProjects/Virtu/Disk525.cs b/ExternalCoreProjects/Virtu/Disk525.cs index 65f22147d3..2e11089494 100644 --- a/ExternalCoreProjects/Virtu/Disk525.cs +++ b/ExternalCoreProjects/Virtu/Disk525.cs @@ -4,21 +4,28 @@ namespace Jellyfish.Virtu { internal abstract class Disk525 { - protected byte[] Data; + protected readonly byte[] Data; + private readonly byte[] Original; public bool IsWriteProtected; protected Disk525(byte[] data, bool isWriteProtected) { Data = data; + Original = (byte[])data.Clone(); IsWriteProtected = isWriteProtected; } public virtual void Sync(IComponentSerializer ser) { - ser.Sync(nameof(Data), ref Data, false); + ser.SyncDelta("DataDelta", Original, Data); ser.Sync(nameof(IsWriteProtected), ref IsWriteProtected); } + public void DeltaUpdate(Action callback) + { + callback(Data, Original); + } + public static Disk525 CreateDisk(string name, byte[] data, bool isWriteProtected) { if (name == null) diff --git a/ExternalCoreProjects/Virtu/DiskIIDrive.cs b/ExternalCoreProjects/Virtu/DiskIIDrive.cs index 8ad31f65d0..b22982ff8b 100644 --- a/ExternalCoreProjects/Virtu/DiskIIDrive.cs +++ b/ExternalCoreProjects/Virtu/DiskIIDrive.cs @@ -1,4 +1,6 @@ -namespace Jellyfish.Virtu +using System; + +namespace Jellyfish.Virtu { public sealed class DiskIIDrive { @@ -29,8 +31,6 @@ ser.Sync(nameof(_trackNumber), ref _trackNumber); ser.Sync(nameof(_trackOffset), ref _trackOffset); ser.Sync(nameof(_trackData), ref _trackData, false); - - // TODO: save the delta, this is saving the rom into save states _disk?.Sync(ser); } @@ -120,5 +120,11 @@ private const int TrackNumberMax = 0x44; private const int PhaseCount = 4; + + public void DeltaUpdate(Action callback) + { + FlushTrack(); + _disk.DeltaUpdate(callback); + } } } diff --git a/ExternalCoreProjects/Virtu/IComponentSerializer.cs b/ExternalCoreProjects/Virtu/IComponentSerializer.cs index 5a60c95a66..85804cbfc3 100644 --- a/ExternalCoreProjects/Virtu/IComponentSerializer.cs +++ b/ExternalCoreProjects/Virtu/IComponentSerializer.cs @@ -13,5 +13,7 @@ void Sync(string name, ref byte[] val, bool useNull); void Sync(string name, ref ushort[] val, bool useNull); void Sync(string name, ref int[] val, bool useNull); + + void SyncDelta(string name, T[] original, T[] current) where T : unmanaged; } } diff --git a/References/Virtu.dll b/References/Virtu.dll index 8b0923f614..effa151391 100644 Binary files a/References/Virtu.dll and b/References/Virtu.dll differ diff --git a/src/BizHawk.Client.Common/config/PathEntryCollection.cs b/src/BizHawk.Client.Common/config/PathEntryCollection.cs index 32a8066b47..2d9505a26e 100644 --- a/src/BizHawk.Client.Common/config/PathEntryCollection.cs +++ b/src/BizHawk.Client.Common/config/PathEntryCollection.cs @@ -198,7 +198,7 @@ namespace BizHawk.Client.Common CommonEntriesFor(VSystemID.Raw.AmstradCPC, basePath: Path.Combine(".", "AmstradCPC"), omitSaveRAM: true), - CommonEntriesFor(VSystemID.Raw.AppleII, basePath: Path.Combine(".", "Apple II"), omitSaveRAM: true), + CommonEntriesFor(VSystemID.Raw.AppleII, basePath: Path.Combine(".", "Apple II")), CommonEntriesFor(VSystemID.Raw.Arcade, basePath: Path.Combine(".", "Arcade")), diff --git a/src/BizHawk.Client.EmuHawk/MainForm.cs b/src/BizHawk.Client.EmuHawk/MainForm.cs index 4d384a20b0..09501d7810 100644 --- a/src/BizHawk.Client.EmuHawk/MainForm.cs +++ b/src/BizHawk.Client.EmuHawk/MainForm.cs @@ -26,6 +26,7 @@ using BizHawk.Emulation.Common.Base_Implementations; using BizHawk.Emulation.Cores; using BizHawk.Emulation.Cores.Arcades.MAME; using BizHawk.Emulation.Cores.Calculators.TI83; +using BizHawk.Emulation.Cores.Computers.AppleII; using BizHawk.Emulation.Cores.Computers.Commodore64; using BizHawk.Emulation.Cores.Consoles.NEC.PCE; using BizHawk.Emulation.Cores.Consoles.Nintendo.Ares64; @@ -1922,7 +1923,7 @@ namespace BizHawk.Client.EmuHawk byte[] sram; // some cores might not know how big the saveram ought to be, so just send it the whole file - if (Emulator is C64 or MGBAHawk or NeoGeoPort or NES { BoardName: "FDS" }) + if (Emulator is AppleII or C64 or MGBAHawk or NeoGeoPort or NES { BoardName: "FDS" }) { sram = File.ReadAllBytes(saveRamPath); } diff --git a/src/BizHawk.Emulation.Common/Database/Database.cs b/src/BizHawk.Emulation.Common/Database/Database.cs index ec51ea3bb1..bf670ee524 100644 --- a/src/BizHawk.Emulation.Common/Database/Database.cs +++ b/src/BizHawk.Emulation.Common/Database/Database.cs @@ -379,6 +379,7 @@ namespace BizHawk.Emulation.Common case ".PO": case ".DO": + case ".NIB": game.System = VSystemID.Raw.AppleII; break; diff --git a/src/BizHawk.Emulation.Cores/Computers/AppleII/AppleII.ISaveRam.cs b/src/BizHawk.Emulation.Cores/Computers/AppleII/AppleII.ISaveRam.cs new file mode 100644 index 0000000000..1f35281cb5 --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Computers/AppleII/AppleII.ISaveRam.cs @@ -0,0 +1,78 @@ +using System; +using System.IO; + +using BizHawk.Common; +using BizHawk.Emulation.Common; + +namespace BizHawk.Emulation.Cores.Computers.AppleII +{ + public partial class AppleII : ISaveRam + { + private byte[][] _diskDeltas; + + private void InitSaveRam() + { + _diskDeltas = new byte[DiskCount][]; + } + + public bool SaveRamModified => true; + + public byte[] CloneSaveRam() + { + using var ms = new MemoryStream(); + using var bw = new BinaryWriter(ms); + + SaveDelta(); + bw.Write(DiskCount); + for (var i = 0; i < DiskCount; i++) + { + bw.WriteByteBuffer(_diskDeltas[i]); + } + + return ms.ToArray(); + } + + public void StoreSaveRam(byte[] data) + { + using var ms = new MemoryStream(data, false); + using var br = new BinaryReader(ms); + + var ndisks = br.ReadInt32(); + + if (ndisks != DiskCount) + { + throw new InvalidOperationException("Disk count mismatch!"); + } + + for (var i = 0; i < DiskCount; i++) + { + _diskDeltas[i] = br.ReadByteBuffer(returnNull: true); + } + + LoadDelta(true); + } + + private void SaveDelta() + { + _machine.DiskIIController.Drive1.DeltaUpdate((original, current) => + { + _diskDeltas[CurrentDisk] = DeltaSerializer.GetDelta(original, current).ToArray(); + }); + } + + private void LoadDelta(bool maybeDifferent) + { + _machine.DiskIIController.Drive1.DeltaUpdate((original, current) => + { + if (_diskDeltas[CurrentDisk] is not null) + { + DeltaSerializer.ApplyDelta(original, current, _diskDeltas[CurrentDisk]); + } + else if (maybeDifferent) + { + original.AsSpan().CopyTo(current); + } + }); + } + } +} diff --git a/src/BizHawk.Emulation.Cores/Computers/AppleII/AppleII.IStatable.cs b/src/BizHawk.Emulation.Cores/Computers/AppleII/AppleII.IStatable.cs index ec94cd1e2b..5540d2163b 100644 --- a/src/BizHawk.Emulation.Cores/Computers/AppleII/AppleII.IStatable.cs +++ b/src/BizHawk.Emulation.Cores/Computers/AppleII/AppleII.IStatable.cs @@ -30,6 +30,7 @@ namespace BizHawk.Emulation.Cores.Computers.AppleII private void SyncState(AppleSerializer ser) { int version = 2; + var oldCurrentDisk = CurrentDisk; ser.BeginSection(nameof(AppleII)); ser.Sync(nameof(version), ref version); ser.Sync("Frame", ref _frame); @@ -66,10 +67,27 @@ namespace BizHawk.Emulation.Cores.Computers.AppleII _machine.NoSlotClock.Sync(ser); ser.EndSection(); + // disk change, we need to swap disks so SyncDelta works later + if (CurrentDisk != oldCurrentDisk) + { + _machine.DiskIIController.Drive1.InsertDisk("junk" + _romSet[CurrentDisk].Extension, (byte[])_romSet[CurrentDisk].Data.Clone(), false); + } + ser.BeginSection("DiskIIController"); _machine.DiskIIController.Sync(ser); ser.EndSection(); + ser.BeginSection("InactiveDisks"); + for (var i = 0; i < DiskCount; i++) + { + // the current disk is handled in DiskIIController + if (i != CurrentDisk) + { + ser.Sync($"DiskDelta{i}", ref _diskDeltas[i], useNull: true); + } + } + ser.EndSection(); + ser.EndSection(); } diff --git a/src/BizHawk.Emulation.Cores/Computers/AppleII/AppleII.cs b/src/BizHawk.Emulation.Cores/Computers/AppleII/AppleII.cs index 7444af4ae3..4645488e46 100644 --- a/src/BizHawk.Emulation.Cores/Computers/AppleII/AppleII.cs +++ b/src/BizHawk.Emulation.Cores/Computers/AppleII/AppleII.cs @@ -23,22 +23,30 @@ namespace BizHawk.Emulation.Cores.Computers.AppleII [CoreConstructor(VSystemID.Raw.AppleII)] public AppleII(CoreLoadParameters lp) { - _romSet = lp.Roms.Select(r => r.RomData).ToList(); + static (byte[], string) GetRomAndExt(IRomAsset romAssert) + { + var ext = romAssert.Extension.ToUpperInvariant(); + return ext switch + { + ".DSK" or ".PO" or ".DO" or ".NIB" => (romAssert.FileData, ext), + ".2mg" => throw new NotSupportedException("Unsupported extension .2mg!"), // TODO: add a way to support this (we have hashes of this format in our db it seems?) + _ => (romAssert.FileData, ".DSK") // no idea, let's assume it's just a .DSK? + }; + } + + _romSet = lp.Roms.Select(GetRomAndExt).ToList(); var ser = new BasicServiceProvider(this); ServiceProvider = ser; const string TRACE_HEADER = "6502: PC, opcode, register (A, X, Y, P, SP, Cy), flags (NVTBDIZC)"; _tracer = new TraceBuffer(TRACE_HEADER); - _disk1 = _romSet[0]; - _appleIIRom = lp.Comm.CoreFileProvider.GetFirmwareOrThrow(new(SystemId, "AppleIIe"), "The Apple IIe BIOS firmware is required"); _diskIIRom = lp.Comm.CoreFileProvider.GetFirmwareOrThrow(new(SystemId, "DiskII"), "The DiskII firmware is required"); _machine = new Components(_appleIIRom, _diskIIRom); - // make a writable memory stream cloned from the rom. - // for junk.dsk the .dsk is important because it determines the format from that + InitSaveRam(); InitDisk(); ser.Register(_tracer); @@ -55,11 +63,10 @@ namespace BizHawk.Emulation.Cores.Computers.AppleII private static readonly ControllerDefinition AppleIIController; - private readonly List _romSet = new List(); + private readonly List<(byte[] Data, string Extension)> _romSet; private readonly ITraceable _tracer; private readonly Components _machine; - private byte[] _disk1; private readonly byte[] _appleIIRom; private readonly byte[] _diskIIRom; @@ -75,12 +82,14 @@ namespace BizHawk.Emulation.Cores.Computers.AppleII public void SetDisk(int discNum) { + SaveDelta(); CurrentDisk = discNum; InitDisk(); } private void IncrementDisk() { + SaveDelta(); CurrentDisk++; if (CurrentDisk >= _romSet.Count) { @@ -92,6 +101,7 @@ namespace BizHawk.Emulation.Cores.Computers.AppleII private void DecrementDisk() { + SaveDelta(); CurrentDisk--; if (CurrentDisk < 0) { @@ -103,11 +113,10 @@ namespace BizHawk.Emulation.Cores.Computers.AppleII private void InitDisk() { - _disk1 = _romSet[CurrentDisk]; - // make a writable memory stream cloned from the rom. - // for junk.dsk the .dsk is important because it determines the format from that - _machine.Memory.DiskIIController.Drive1.InsertDisk("junk.dsk", (byte[])_disk1.Clone(), false); + // the extension is important here because it determines the format from that + _machine.DiskIIController.Drive1.InsertDisk("junk" + _romSet[CurrentDisk].Extension, (byte[])_romSet[CurrentDisk].Data.Clone(), false); + LoadDelta(false); } private static readonly List RealButtons = new List(Keyboard.GetKeyNames() @@ -120,7 +129,7 @@ namespace BizHawk.Emulation.Cores.Computers.AppleII }; public bool DriveLightEnabled => true; - public bool DriveLightOn => _machine.Memory.DiskIIController.DriveLight; + public bool DriveLightOn => _machine.DiskIIController.DriveLight; private bool _nextPressed; private bool _prevPressed; diff --git a/src/BizHawk.Emulation.Cores/Computers/AppleII/Components.cs b/src/BizHawk.Emulation.Cores/Computers/AppleII/Components.cs index 6a7ae54a3c..48217ba081 100644 --- a/src/BizHawk.Emulation.Cores/Computers/AppleII/Components.cs +++ b/src/BizHawk.Emulation.Cores/Computers/AppleII/Components.cs @@ -32,7 +32,7 @@ namespace BizHawk.Emulation.Cores.Computers.AppleII emptySlot, emptySlot, emptySlot, - new DiskIIController(Video, diskIIRom), + DiskIIController, emptySlot); Cpu.Reset();