diff --git a/BizHawk.Client.ApiHawk/Classes/BizHawkSystemIdToCoreSystemEnumConverter.cs b/BizHawk.Client.ApiHawk/Classes/BizHawkSystemIdToCoreSystemEnumConverter.cs index 31b65c5df5..fcce1118cf 100644 --- a/BizHawk.Client.ApiHawk/Classes/BizHawkSystemIdToCoreSystemEnumConverter.cs +++ b/BizHawk.Client.ApiHawk/Classes/BizHawkSystemIdToCoreSystemEnumConverter.cs @@ -96,6 +96,9 @@ namespace BizHawk.Client.ApiHawk case "WSWAN": return CoreSystem.WonderSwan; + case "ZXSpectrum": + return CoreSystem.ZXSpectrum; + case "VB": case "NGP": case "DNGP": @@ -205,6 +208,9 @@ namespace BizHawk.Client.ApiHawk case CoreSystem.WonderSwan: return "WSWAN"; + case CoreSystem.ZXSpectrum: + return "ZXSpectrum"; + default: throw new IndexOutOfRangeException(string.Format("{0} is missing in convert list", value.ToString())); } diff --git a/BizHawk.Client.ApiHawk/Classes/ClientApi.cs b/BizHawk.Client.ApiHawk/Classes/ClientApi.cs index 12ee67277c..767d755105 100644 --- a/BizHawk.Client.ApiHawk/Classes/ClientApi.cs +++ b/BizHawk.Client.ApiHawk/Classes/ClientApi.cs @@ -427,11 +427,11 @@ namespace BizHawk.Client.ApiHawk } else { - return SystemInfo.DualGB; + return SystemInfo.DualGB; } default: - return SystemInfo.FindByCoreSystem(SystemIdConverter.Convert(Global.Emulator.SystemId)); + return SystemInfo.FindByCoreSystem(SystemIdConverter.Convert(Global.Emulator.SystemId)); } } } diff --git a/BizHawk.Client.Common/Api/CoreSystem.cs b/BizHawk.Client.Common/Api/CoreSystem.cs index 132a832b98..9bdc7d8232 100644 --- a/BizHawk.Client.Common/Api/CoreSystem.cs +++ b/BizHawk.Client.Common/Api/CoreSystem.cs @@ -29,6 +29,7 @@ WonderSwan, Libretro, VirtualBoy, - NeoGeoPocket + NeoGeoPocket, + ZXSpectrum } } diff --git a/BizHawk.Client.Common/Global.cs b/BizHawk.Client.Common/Global.cs index b592f1696b..edd287ef49 100644 --- a/BizHawk.Client.Common/Global.cs +++ b/BizHawk.Client.Common/Global.cs @@ -149,6 +149,8 @@ namespace BizHawk.Client.Common return SystemInfo.VirtualBoy; case "NGP": return SystemInfo.NeoGeoPocket; + case "ZXSpectrum": + return SystemInfo.ZXSpectrum; } } } diff --git a/BizHawk.Client.Common/RomLoader.cs b/BizHawk.Client.Common/RomLoader.cs index 99e355081e..cd1ce95f22 100644 --- a/BizHawk.Client.Common/RomLoader.cs +++ b/BizHawk.Client.Common/RomLoader.cs @@ -19,6 +19,7 @@ using BizHawk.Emulation.Cores.PCEngine; using BizHawk.Emulation.Cores.Sega.Saturn; using BizHawk.Emulation.Cores.Sony.PSP; using BizHawk.Emulation.Cores.Sony.PSX; +using BizHawk.Emulation.Cores.Computers.SinclairSpectrum; using BizHawk.Emulation.DiscSystem; using GPGX64 = BizHawk.Emulation.Cores.Consoles.Sega.gpgx; @@ -657,6 +658,13 @@ namespace BizHawk.Client.Common (C64.C64Settings)GetCoreSettings(), (C64.C64SyncSettings)GetCoreSyncSettings()); break; + case "ZXSpectrum": + nextEmulator = new ZXSpectrum( + nextComm, + xmlGame.Assets.Select(a => a.Value).First(), + (ZXSpectrum.ZXSpectrumSettings)GetCoreSettings(), + (ZXSpectrum.ZXSpectrumSyncSettings)GetCoreSyncSettings()); + break; case "PSX": var entries = xmlGame.AssetFullPaths; var discs = new List(); @@ -990,6 +998,10 @@ namespace BizHawk.Client.Common var c64 = new C64(nextComm, Enumerable.Repeat(rom.RomData, 1), rom.GameInfo, GetCoreSettings(), GetCoreSyncSettings()); nextEmulator = c64; break; + case "ZXSpectrum": + var zx = new ZXSpectrum(nextComm, rom.FileData, GetCoreSettings(), GetCoreSyncSettings()); + nextEmulator = zx; + break; case "GBA": if (Global.Config.GBA_UsemGBA) { diff --git a/BizHawk.Client.Common/SystemInfo.cs b/BizHawk.Client.Common/SystemInfo.cs index 82414adc50..2d65f1729c 100644 --- a/BizHawk.Client.Common/SystemInfo.cs +++ b/BizHawk.Client.Common/SystemInfo.cs @@ -188,6 +188,11 @@ namespace BizHawk.Client.Common /// public static SystemInfo NeoGeoPocket { get; } = new SystemInfo("Neo-Geo Pocket", CoreSystem.NeoGeoPocket, 1); + /// + /// Gets the instance for ZXSpectrum + /// + public static SystemInfo ZXSpectrum { get; } = new SystemInfo("ZX Spectrum", CoreSystem.ZXSpectrum, 2); + #endregion Get SystemInfo /// diff --git a/BizHawk.Client.Common/config/PathEntry.cs b/BizHawk.Client.Common/config/PathEntry.cs index 0be34e26c7..22fcad0a76 100644 --- a/BizHawk.Client.Common/config/PathEntry.cs +++ b/BizHawk.Client.Common/config/PathEntry.cs @@ -290,7 +290,13 @@ namespace BizHawk.Client.Common new PathEntry { System = "C64", SystemDisplayName = "Commodore 64", Type = "Screenshots", Path = Path.Combine(".", "Screenshots"), Ordinal = 4 }, new PathEntry { System = "C64", SystemDisplayName = "Commodore 64", Type = "Cheats", Path = Path.Combine(".", "Cheats"), Ordinal = 5 }, - new PathEntry { System = "PSX", SystemDisplayName = "Playstation", Type = "Base", Path = Path.Combine(".", "PSX"), Ordinal = 0 }, + new PathEntry { System = "ZXSpectrum", SystemDisplayName = "Sinclair ZX Spectrum", Type = "Base", Path = Path.Combine(".", "ZXSpectrum"), Ordinal = 0 }, + new PathEntry { System = "ZXSpectrum", SystemDisplayName = "Sinclair ZX Spectrum", Type = "ROM", Path = ".", Ordinal = 1 }, + new PathEntry { System = "ZXSpectrum", SystemDisplayName = "Sinclair ZX Spectrum", Type = "Savestates", Path = Path.Combine(".", "State"), Ordinal = 2 }, + new PathEntry { System = "ZXSpectrum", SystemDisplayName = "Sinclair ZX Spectrum", Type = "Screenshots", Path = Path.Combine(".", "Screenshots"), Ordinal = 4 }, + new PathEntry { System = "ZXSpectrum", SystemDisplayName = "Sinclair ZX Spectrum", Type = "Cheats", Path = Path.Combine(".", "Cheats"), Ordinal = 5 }, + + new PathEntry { System = "PSX", SystemDisplayName = "Playstation", Type = "Base", Path = Path.Combine(".", "PSX"), Ordinal = 0 }, new PathEntry { System = "PSX", SystemDisplayName = "Playstation", Type = "ROM", Path = ".", Ordinal = 1 }, new PathEntry { System = "PSX", SystemDisplayName = "Playstation", Type = "Savestates", Path = Path.Combine(".", "State"), Ordinal = 2 }, new PathEntry { System = "PSX", SystemDisplayName = "Playstation", Type = "Save RAM", Path = Path.Combine(".", "SaveRAM"), Ordinal = 3 }, diff --git a/BizHawk.Client.EmuHawk/FileLoader.cs b/BizHawk.Client.EmuHawk/FileLoader.cs index 0009646fb4..db3c00d76d 100644 --- a/BizHawk.Client.EmuHawk/FileLoader.cs +++ b/BizHawk.Client.EmuHawk/FileLoader.cs @@ -51,7 +51,7 @@ namespace BizHawk.Client.EmuHawk return new[] { ".NES", ".FDS", ".UNF", ".SMS", ".GG", ".SG", ".GB", ".GBC", ".GBA", ".PCE", ".SGX", ".BIN", ".SMD", ".GEN", ".MD", ".SMC", ".SFC", ".A26", ".A78", ".LNX", ".COL", ".ROM", ".M3U", ".CUE", ".CCD", ".SGB", ".Z64", ".V64", ".N64", ".WS", ".WSC", ".XML", ".DSK", ".DO", ".PO", ".PSF", ".MINIPSF", ".NSF", - ".EXE", ".PRG", ".D64", "*G64", ".CRT", ".TAP", ".32X", ".MDS" + ".EXE", ".PRG", ".D64", "*G64", ".CRT", ".TAP", ".32X", ".MDS", ".TZX" }; } diff --git a/BizHawk.Client.EmuHawk/MainForm.cs b/BizHawk.Client.EmuHawk/MainForm.cs index fa6977cd9f..f52e3a64d5 100644 --- a/BizHawk.Client.EmuHawk/MainForm.cs +++ b/BizHawk.Client.EmuHawk/MainForm.cs @@ -2077,7 +2077,7 @@ namespace BizHawk.Client.EmuHawk if (VersionInfo.DeveloperBuild) { return FormatFilter( - "Rom Files", "*.nes;*.fds;*.unf;*.sms;*.gg;*.sg;*.pce;*.sgx;*.bin;*.smd;*.rom;*.a26;*.a78;*.lnx;*.m3u;*.cue;*.ccd;*.mds;*.exe;*.gb;*.gbc;*.gba;*.gen;*.md;*.32x;*.col;*.int;*.smc;*.sfc;*.prg;*.d64;*.g64;*.crt;*.tap;*.sgb;*.xml;*.z64;*.v64;*.n64;*.ws;*.wsc;*.dsk;*.do;*.po;*.vb;*.ngp;*.ngc;*.psf;*.minipsf;*.nsf;%ARCH%", + "Rom Files", "*.nes;*.fds;*.unf;*.sms;*.gg;*.sg;*.pce;*.sgx;*.bin;*.smd;*.rom;*.a26;*.a78;*.lnx;*.m3u;*.cue;*.ccd;*.mds;*.exe;*.gb;*.gbc;*.gba;*.gen;*.md;*.32x;*.col;*.int;*.smc;*.sfc;*.prg;*.d64;*.g64;*.crt;*.tap;*.sgb;*.xml;*.z64;*.v64;*.n64;*.ws;*.wsc;*.dsk;*.do;*.po;*.vb;*.ngp;*.ngc;*.psf;*.minipsf;*.nsf;*.tzx;%ARCH%", "Music Files", "*.psf;*.minipsf;*.sid;*.nsf", "Disc Images", "*.cue;*.ccd;*.mds;*.m3u", "NES", "*.nes;*.fds;*.unf;*.nsf;%ARCH%", @@ -2105,6 +2105,7 @@ namespace BizHawk.Client.EmuHawk "Apple II", "*.dsk;*.do;*.po;%ARCH%", "Virtual Boy", "*.vb;%ARCH%", "Neo Geo Pocket", "*.ngp;*.ngc;%ARCH%", + "Sinclair ZX Spectrum", "*.tzx;*.tap;%ARCH%", "All Files", "*.*"); } diff --git a/BizHawk.Client.EmuHawk/config/ControllerConfig.cs b/BizHawk.Client.EmuHawk/config/ControllerConfig.cs index ab488b1724..9800e5ec13 100644 --- a/BizHawk.Client.EmuHawk/config/ControllerConfig.cs +++ b/BizHawk.Client.EmuHawk/config/ControllerConfig.cs @@ -176,7 +176,7 @@ namespace BizHawk.Client.EmuHawk if (buckets[0].Count > 0) { - string tabname = Global.Emulator.SystemId == "C64" ? "Keyboard" : "Console"; // hack + string tabname = (Global.Emulator.SystemId == "C64" || Global.Emulator.SystemId == "ZXSpectrum") ? "Keyboard" : "Console"; // hack tt.TabPages.Add(tabname); tt.TabPages[pageidx].Controls.Add(createpanel(settings, buckets[0], tt.Size)); } diff --git a/BizHawk.Client.EmuHawk/config/FirmwaresConfig.cs b/BizHawk.Client.EmuHawk/config/FirmwaresConfig.cs index 5b2e5eca92..7b016a8cab 100644 --- a/BizHawk.Client.EmuHawk/config/FirmwaresConfig.cs +++ b/BizHawk.Client.EmuHawk/config/FirmwaresConfig.cs @@ -52,6 +52,7 @@ namespace BizHawk.Client.EmuHawk { "GBC", "Game Boy Color" }, { "PCFX", "PC-FX" }, { "32X", "32X" }, + { "ZXSpectrum", "ZX Spectrum" } }; public string TargetSystem = null; diff --git a/BizHawk.Emulation.Common/Database/Database.cs b/BizHawk.Emulation.Common/Database/Database.cs index df1a2562c7..d2d2bfccee 100644 --- a/BizHawk.Emulation.Common/Database/Database.cs +++ b/BizHawk.Emulation.Common/Database/Database.cs @@ -5,6 +5,7 @@ using System.Text; using System.Threading; using BizHawk.Common.BufferExtensions; +using System.Linq; namespace BizHawk.Emulation.Common { @@ -298,12 +299,23 @@ namespace BizHawk.Emulation.Common case ".D64": case ".T64": case ".G64": - case ".CRT": - case ".TAP": + case ".CRT": game.System = "C64"; break; - case ".Z64": + case ".TZX": + game.System = "ZXSpectrum"; + break; + + case ".TAP": + byte[] head = File.ReadAllBytes(fileName).Take(8).ToArray(); + if (System.Text.Encoding.Default.GetString(head).Contains("C64-TAPE")) + game.System = "C64"; + else + game.System = "ZXSpectrum"; + break; + + case ".Z64": case ".V64": case ".N64": game.System = "N64"; diff --git a/BizHawk.Emulation.Common/Database/FirmwareDatabase.cs b/BizHawk.Emulation.Common/Database/FirmwareDatabase.cs index dde7623f46..7d8eb5fe63 100644 --- a/BizHawk.Emulation.Common/Database/FirmwareDatabase.cs +++ b/BizHawk.Emulation.Common/Database/FirmwareDatabase.cs @@ -50,6 +50,9 @@ namespace BizHawk.Emulation.Common FirmwareAndOption("AB16F56989B27D89BABE5F89C5A8CB3DA71A82F0", 16384, "C64", "Drive1541", "drive-1541.bin", "1541 Disk Drive Rom"); FirmwareAndOption("D3B78C3DBAC55F5199F33F3FE0036439811F7FB3", 16384, "C64", "Drive1541II", "drive-1541ii.bin", "1541-II Disk Drive Rom"); + // ZX Spectrum + FirmwareAndOption("5EA7C2B824672E914525D1D5C419D71B84A426A2", 16384, "ZXSpectrum", "48ROM", "48.ROM", "Spectrum 48K ROM"); + // for saturn, we think any bios region can pretty much run any iso // so, we're going to lay this out carefully so that we choose things in a sensible order, but prefer the correct region var ss_100_j = File("2B8CB4F87580683EB4D760E4ED210813D667F0A2", 524288, "saturn-1.00-(J).bin", "Bios v1.00 (J)"); diff --git a/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj b/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj index 4a00050be4..251a064158 100644 --- a/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj +++ b/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj @@ -256,6 +256,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Atari2600.cs @@ -1331,6 +1374,9 @@ + + + @@ -1341,6 +1387,7 @@ + "$(SolutionDir)subwcrev.bat" "$(ProjectDir)" diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Buzzer.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Buzzer.cs new file mode 100644 index 0000000000..ace668868f --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Buzzer.cs @@ -0,0 +1,299 @@ + +using BizHawk.Common; +using BizHawk.Emulation.Common; +using BizHawk.Emulation.Cores.Components; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// Represents the piezoelectric buzzer used in the Spectrum to produce sound + /// The beeper is controlled by rapidly toggling bit 4 of port &FE + /// + /// For the purposes of emulation this devices is locked to a frame + /// a list of Pulses is built up over the course of the frame and outputted at the end of the frame + /// + public class Buzzer : ISoundProvider + { + /// + /// Supplied values are right for 48K spectrum + /// These will deviate for 128k and up (as there are more T-States per frame) + /// + //public int SampleRate = 44100; //35000; + //public int SamplesPerFrame = 882; //699; + //public int TStatesPerSample = 79; //100; + + /// + /// Sample Rate + /// This usually has to be 44100 for ISoundProvider + /// + public int SampleRate + { + get { return _sampleRate; } + set { _sampleRate = value; } + } + + /// + /// Number of samples in one frame + /// + public int SamplesPerFrame + { + get { return _samplesPerFrame; } + set { _samplesPerFrame = value; } + } + + /// + /// Number of TStates in each sample + /// + public int TStatesPerSample + { + get { return _tStatesPerSample; } + set { _tStatesPerSample = value; } + } + + private SpectrumBase _machine; + + /// + /// State fields + /// + private long _frameStart; + private bool _tapeMode; + private int _tStatesPerFrame; + private int _sampleRate; + private int _samplesPerFrame; + private int _tStatesPerSample; + + /// + /// Pulses collected during the last frame + /// + public List Pulses { get; private set; } + + /// + /// The last pulse + /// + public bool LastPulse { get; set; } + + /// + /// The last T-State (cpu cycle) that the last pulse was received + /// + public int LastPulseTState { get; set; } + + #region Construction & Initialisation + + public Buzzer(SpectrumBase machine) + { + _machine = machine; + } + + /// + /// Initialises the buzzer + /// + public void Init(int sampleRate, int tStatesPerFrame) + { + _sampleRate = sampleRate; + _tStatesPerFrame = tStatesPerFrame; + + // get divisors + var divs = from a in Enumerable.Range(2, _tStatesPerFrame / 2) + where _tStatesPerFrame % a == 0 + select a; + + // get the highest int value under 120 (this will be TStatesPerSample) + _tStatesPerSample = divs.Where(a => a < 100).Last(); + + // get _samplesPerFrame + _samplesPerFrame = _tStatesPerFrame / _tStatesPerSample; + + Pulses = new List(1000); + } + + #endregion + + /// + /// When the pulse value from the EAR output changes it is processed here + /// + /// + /// + public void ProcessPulseValue(bool fromTape, bool earPulse) + { + if (!fromTape && _tapeMode) + { + // tape mode is active but the pulse value came from an OUT instruction + // do not process the value + //return; + } + + if (earPulse == LastPulse) + { + // no change detected + return; + } + + // set the lastpulse + LastPulse = earPulse; + + // get where we are in the frame + var currentULACycle = _machine.CurrentFrameCycle; + var currentBuzzerCycle = currentULACycle <= _tStatesPerFrame ? currentULACycle : _tStatesPerFrame; + var length = currentBuzzerCycle - LastPulseTState; + + if (length == 0) + { + // the first T-State has changed the pulse + // do not add it + } + else if (length > 0) + { + // add the pulse + Pulse p = new Pulse + { + State = !earPulse, + Length = length + }; + Pulses.Add(p); + } + + // set the last pulse tstate + LastPulseTState = currentBuzzerCycle; + } + + /// + /// New frame starts + /// + public void StartFrame() + { + //DiscardSamples(); + Pulses.Clear(); + LastPulseTState = 0; + } + + /// + /// Frame is completed + /// + public void EndFrame() + { + // store the last pulse information + if (LastPulseTState <= _tStatesPerFrame - 1) + { + Pulse p = new Pulse + { + State = LastPulse, + Length = _tStatesPerFrame - LastPulseTState + }; + Pulses.Add(p); + } + + // create the sample array + var firstSampleOffset = _frameStart % TStatesPerSample == 0 ? 0 : TStatesPerSample - (_frameStart + TStatesPerSample) % TStatesPerSample; + var samplesInFrame = (_tStatesPerFrame - firstSampleOffset - 1) / TStatesPerSample + 1; + var samples = new short[samplesInFrame]; + + // convert pulses to samples + var sampleIndex = 0; + var currentEnd = _frameStart; + + foreach (var pulse in Pulses) + { + var firstSample = currentEnd % TStatesPerSample == 0 + ? currentEnd : currentEnd + TStatesPerSample - currentEnd % TStatesPerSample; + + for (var i = firstSample; i < currentEnd + pulse.Length; i += TStatesPerSample) + { + samples[sampleIndex++] = pulse.State ? (short)(short.MaxValue / 2) : (short)0; + + //resampler.EnqueueSample(samples[sampleIndex - 1], samples[sampleIndex - 1]); + + } + + + currentEnd += pulse.Length; + } + + // fill the _sampleBuffer for ISoundProvider + soundBufferContains = (int)samplesInFrame; + + if (soundBuffer.Length != soundBufferContains) + soundBuffer = new short[soundBufferContains]; + + samples.CopyTo(soundBuffer, 0); + + _frameStart += _tStatesPerFrame; + } + + /// + /// When the spectrum is set to receive tape input, the EAR output on the ULA is disabled + /// (so no buzzer sound is emitted) + /// + /// + public void SetTapeMode(bool tapeMode) + { + _tapeMode = tapeMode; + } + + + #region ISoundProvider + + private short[] soundBuffer = new short[882]; + private int soundBufferContains = 0; + + public bool CanProvideAsync => false; + + public SyncSoundMode SyncMode => SyncSoundMode.Sync; + + public void SetSyncMode(SyncSoundMode mode) + { + if (mode != SyncSoundMode.Sync) + throw new InvalidOperationException("Only Sync mode is supported."); + } + + public void GetSamplesAsync(short[] samples) + { + throw new NotSupportedException("Async is not available"); + } + + public void DiscardSamples() + { + soundBufferContains = 0; + soundBuffer = new short[SamplesPerFrame]; + } + + public void GetSamplesSync(out short[] samples, out int nsamp) + { + // convert to stereo + short[] stereoBuffer = new short[soundBufferContains * 2]; + int index = 0; + for (int i = 0; i < soundBufferContains; i++) + { + stereoBuffer[index++] = soundBuffer[i]; + stereoBuffer[index++] = soundBuffer[i]; + } + + samples = stereoBuffer; + nsamp = soundBufferContains; + } + + #endregion + + + public void SyncState(Serializer ser) + { + ser.BeginSection("Buzzer"); + ser.Sync("_frameStart", ref _frameStart); + ser.Sync("_tapeMode", ref _tapeMode); + ser.Sync("_tStatesPerFrame", ref _tStatesPerFrame); + ser.Sync("_sampleRate", ref _sampleRate); + ser.Sync("_samplesPerFrame", ref _samplesPerFrame); + ser.Sync("_tStatesPerSample", ref _tStatesPerSample); + + ser.Sync("soundBuffer", ref soundBuffer, false); + ser.Sync("soundBufferContains", ref soundBufferContains); + ser.EndSection(); + } + + + } + + +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/DefaultTapeProvider.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/DefaultTapeProvider.cs new file mode 100644 index 0000000000..2ee8ef6f63 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/DefaultTapeProvider.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public class DefaultTapeProvider : ITapeProvider + { + public const string RESOURCE_FOLDER = "TzxResources"; + public const string DEFAULT_SAVE_FILE_DIR = @"C:\Temp\ZxSpectrumSavedFiles"; + public const string DEFAULT_NAME = "SavedFile"; + public const string DEFAULT_EXT = ".tzx"; + private string _suggestedName; + private string _fullFileName; + private int _dataBlockCount; + + private byte[] _file; + + /// + /// The directory files should be saved to + /// + public string SaveFileFolder { get; } + + + + public DefaultTapeProvider(byte[] file, string saveFolder = null) + { + SaveFileFolder = string.IsNullOrWhiteSpace(saveFolder) + ? DEFAULT_SAVE_FILE_DIR + : saveFolder; + + _file = file; + } + + /// + /// The component provider should be able to reset itself + /// + /// + + public void Reset() + { + _dataBlockCount = 0; + _suggestedName = null; + _fullFileName = null; + } + + + /// + /// Tha tape set to load the content from + /// + public string TapeSetName { get; set; } + + /// + /// Gets a binary reader that provider TZX content + /// + /// BinaryReader instance to obtain the content from + public BinaryReader GetTapeContent() + { + Stream stream = new MemoryStream(_file); + var reader = new BinaryReader(stream); + return reader; + } + + /// + /// Creates a tape file with the specified name + /// + /// + public void CreateTapeFile() + { + //Reset(); + } + + /// + /// This method sets the name of the file according to the + /// Spectrum SAVE HEADER information + /// + /// + public void SetName(string name) + { + _suggestedName = name; + } + + /// + /// Appends the TZX block to the tape file + /// + /// + public void SaveTapeBlock(ITapeDataSerialization block) + { + if (_dataBlockCount == 0) + { + if (!Directory.Exists(SaveFileFolder)) + { + Directory.CreateDirectory(SaveFileFolder); + } + var baseFileName = $"{_suggestedName ?? DEFAULT_NAME}_{DateTime.Now:yyyyMMdd_HHmmss}{DEFAULT_EXT}"; + _fullFileName = Path.Combine(SaveFileFolder, baseFileName); + using (var writer = new BinaryWriter(File.Create(_fullFileName))) + { + var header = new TzxHeader(); + header.WriteTo(writer); + } + } + _dataBlockCount++; + + var stream = File.Open(_fullFileName, FileMode.Append); + using (var writer = new BinaryWriter(stream)) + { + block.WriteTo(writer); + } + } + + /// + /// The tape provider can finalize the tape when all + /// TZX blocks are written. + /// + public void FinalizeTapeFile() + { + } + + /// + /// Obtains the specified resource stream ot the given assembly + /// + /// Assembly to get the resource stream from + /// Resource name + private static Stream GetFileResource(Assembly asm, string resourceName) + { + var resourceFullName = $"{asm.GetName().Name}.{RESOURCE_FOLDER}.{resourceName}"; + return asm.GetManifestResourceStream(resourceFullName); + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Interfaces/IKeyboard.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Interfaces/IKeyboard.cs new file mode 100644 index 0000000000..fe4f188ca6 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Interfaces/IKeyboard.cs @@ -0,0 +1,69 @@ + + +using BizHawk.Common; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// Represents a spectrum keyboard + /// + public interface IKeyboard + { + /// + /// The calling spectrumbase class + /// + SpectrumBase _machine { get; } + + /// + /// The keyboard matrix for a particular spectrum model + /// + string[] KeyboardMatrix { get; set; } + + /// + /// For 16/48k models + /// + bool Issue2 { get; set; } + + /// + /// The current keyboard line status + /// + //byte[] LineStatus { get; set; } + + /// + /// Sets the spectrum key status + /// + /// + /// + void SetKeyStatus(string key, bool isPressed); + + /// + /// Gets the status of a spectrum key + /// + /// + /// + bool GetKeyStatus(string key); + + /// + /// Returns the query byte + /// + /// + /// + byte GetLineStatus(byte lines); + + /// + /// Reads a keyboard byte + /// + /// + /// + byte ReadKeyboardByte(ushort addr); + + /// + /// Looks up a key in the keyboard matrix and returns the relevent byte value + /// + /// + /// + byte GetByteFromKeyMatrix(string key); + + void SyncState(Serializer ser); + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Interfaces/ISaveToTapeProvider.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Interfaces/ISaveToTapeProvider.cs new file mode 100644 index 0000000000..05a4de9e06 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Interfaces/ISaveToTapeProvider.cs @@ -0,0 +1,35 @@ + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// This interface describes the behavior of an object that + /// provides tape content + /// + public interface ISaveToTapeProvider + { + /// + /// Creates a tape file with the specified name + /// + /// + void CreateTapeFile(); + + /// + /// This method sets the name of the file according to the + /// Spectrum SAVE HEADER information + /// + /// + void SetName(string name); + + /// + /// Appends the tape block to the tape file + /// + /// + void SaveTapeBlock(ITapeDataSerialization block); + + /// + /// The tape provider can finalize the tape when all + /// tape blocks are written. + /// + void FinalizeTapeFile(); + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Interfaces/ISupportsTapeBlockPlayback.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Interfaces/ISupportsTapeBlockPlayback.cs new file mode 100644 index 0000000000..969944fa92 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Interfaces/ISupportsTapeBlockPlayback.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// This interface represents that the implementing class supports + /// emulating tape playback of a single tape block + /// + public interface ISupportsTapeBlockPlayback + { + /// + /// The current playing phase + /// + PlayPhase PlayPhase { get; } + + /// + /// The tact count of the CPU when playing starts + /// + long StartCycle { get; } + + /// + /// Initializes the player + /// + void InitPlay(long startCycle); + + /// + /// Gets the EAR bit value for the specified tact + /// + /// Tacts to retrieve the EAR bit + /// + /// The EAR bit value to play back + /// + bool GetEarBit(long currentCycle); + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Interfaces/ISupportsTapeBlockSetPlayback.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Interfaces/ISupportsTapeBlockSetPlayback.cs new file mode 100644 index 0000000000..1312761e51 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Interfaces/ISupportsTapeBlockSetPlayback.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// This interface represents that the implementing class supports + /// emulating tape playback of a set of subsequent tape blocks + /// + public interface ISupportsTapeBlockSetPlayback : ISupportsTapeBlockPlayback + { + /// + /// Moves the player to the next playable block + /// + /// Tacts time to start the next block + void NextBlock(long currentCycle); + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Interfaces/ITapeContentProvider.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Interfaces/ITapeContentProvider.cs new file mode 100644 index 0000000000..62f8c6f814 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Interfaces/ITapeContentProvider.cs @@ -0,0 +1,25 @@ + +using System.IO; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// This interface describes the behavior of an object that + /// provides tape content + /// + public interface ITapeContentProvider + { + /// + /// Tha tape set to load the content from + /// + string TapeSetName { get; set; } + + /// + /// Gets a binary reader that provides tape content + /// + /// BinaryReader instance to obtain the content from + BinaryReader GetTapeContent(); + + void Reset(); + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Interfaces/ITapeData.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Interfaces/ITapeData.cs new file mode 100644 index 0000000000..5879c57536 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Interfaces/ITapeData.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// Represetns the data in the tape + /// + public interface ITapeData + { + /// + /// Block Data + /// + byte[] Data { get; } + + /// + /// Pause after this block (given in milliseconds) + /// + ushort PauseAfter { get; } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Interfaces/ITapeDataSerialization.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Interfaces/ITapeDataSerialization.cs new file mode 100644 index 0000000000..9f48e9097e --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Interfaces/ITapeDataSerialization.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// Defines the serialization operations of a TZX record + /// + public interface ITapeDataSerialization + { + /// + /// Reads the content of the block from the specified binary stream. + /// + /// Stream to read the block from + void ReadFrom(BinaryReader reader); + + /// + /// Writes the content of the block to the specified binary stream. + /// + /// Stream to write the block to + void WriteTo(BinaryWriter writer); + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Interfaces/ITapeProvider.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Interfaces/ITapeProvider.cs new file mode 100644 index 0000000000..91a4457a31 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Interfaces/ITapeProvider.cs @@ -0,0 +1,53 @@ +using System.IO; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// This interface describes the behavior of an object that + /// provides TZX tape content + /// + public interface ITapeProvider + { + /// + /// Tha tape set to load the content from + /// + string TapeSetName { get; set; } + + /// + /// Gets a binary reader that provider TZX content + /// + /// BinaryReader instance to obtain the content from + BinaryReader GetTapeContent(); + + /// + /// Creates a tape file with the specified name + /// + /// + void CreateTapeFile(); + + /// + /// This method sets the name of the file according to the + /// Spectrum SAVE HEADER information + /// + /// + void SetName(string name); + + /// + /// Appends the TZX block to the tape file + /// + /// + void SaveTapeBlock(ITapeDataSerialization block); + + /// + /// The tape provider can finalize the tape when all + /// TZX blocks are written. + /// + void FinalizeTapeFile(); + + /// + /// Provider can reset itself + /// + void Reset(); + + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Tape.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Tape.cs new file mode 100644 index 0000000000..203fef91b2 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Tape.cs @@ -0,0 +1,817 @@ +using BizHawk.Common; +using BizHawk.Emulation.Cores.Components.Z80A; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /* + * Much of the TAPE implementation has been taken from: https://github.com/Dotneteer/spectnetide + * + * MIT License + + Copyright (c) 2017 Istvan Novak + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + */ + + /// + /// Represents the tape device (or DATACORDER as AMSTRAD liked to call it) + /// + public class Tape + { + private SpectrumBase _machine { get; set; } + private Z80A _cpu { get; set; } + private Buzzer _buzzer { get; set; } + + private TapeOperationMode _currentMode; + private TapeFilePlayer _tapePlayer; + private bool _micBitState; + private long _lastMicBitActivityCycle; + private SavePhase _savePhase; + private int _pilotPulseCount; + private int _bitOffset; + private byte _dataByte; + private int _dataLength; + private byte[] _dataBuffer; + private int _dataBlockCount; + private MicPulseType _prevDataPulse; + + /// + /// Number of tacts after save mod can be exited automatically + /// + public const int SAVE_STOP_SILENCE = 17500000; + + /// + /// The address of the ERROR routine in the Spectrum ROM + /// + public const ushort ERROR_ROM_ADDRESS = 0x0008; + + /// + /// The maximum distance between two scans of the EAR bit + /// + public const int MAX_TACT_JUMP = 10000; + + /// + /// The width tolerance of save pulses + /// + public const int SAVE_PULSE_TOLERANCE = 24; + + /// + /// Minimum number of pilot pulses before SYNC1 + /// + public const int MIN_PILOT_PULSE_COUNT = 3000; + + /// + /// Lenght of the data buffer to allocate for the SAVE operation + /// + public const int DATA_BUFFER_LENGTH = 0x10000; + + /// + /// Gets the tape content provider + /// + public ITapeProvider TapeProvider { get; } + + /// + /// The TapeFilePlayer that can playback tape content + /// + public TapeFilePlayer TapeFilePlayer => _tapePlayer; + + /// + /// The current operation mode of the tape + /// + public TapeOperationMode CurrentMode => _currentMode; + + + private bool _fastLoad = false; + + + public virtual void Init(SpectrumBase machine) + { + _machine = machine; + _cpu = _machine.CPU; + _buzzer = machine.BuzzerDevice; + Reset(); + } + + public Tape(ITapeProvider tapeProvider) + { + TapeProvider = tapeProvider; + } + + public virtual void Reset() + { + TapeProvider?.Reset(); + _tapePlayer = null; + _currentMode = TapeOperationMode.Passive; + _savePhase = SavePhase.None; + _micBitState = true; + } + + public void CPUFrameCompleted() + { + SetTapeMode(); + if (CurrentMode == TapeOperationMode.Load + && _fastLoad + && TapeFilePlayer != null + && TapeFilePlayer.PlayPhase != PlayPhase.Completed + && _cpu.RegPC == 1529) //_machine.RomData.LoadBytesRoutineAddress) + { + + if (FastLoadFromTzx()) + { + //FastLoadCompleted?.Invoke(this, EventArgs.Empty); + } + + } + } + + /// + /// Sets the current tape mode according to the current PC register + /// and the MIC bit state + /// + public void SetTapeMode() + { + switch (_currentMode) + { + case TapeOperationMode.Passive: + if (_cpu.RegPC == 1523) // _machine.RomData.LoadBytesRoutineAddress) //1529 + { + EnterLoadMode(); + } + else if (_cpu.RegPC == 2416) // _machine.RomData.SaveBytesRoutineAddress) + { + EnterSaveMode(); + } + + var res = _cpu.RegPC; + var res2 = _machine.Spectrum.RegPC; + + return; + case TapeOperationMode.Save: + if (_cpu.RegPC == ERROR_ROM_ADDRESS + || (int)(_cpu.TotalExecutedCycles - _lastMicBitActivityCycle) > SAVE_STOP_SILENCE) + { + LeaveSaveMode(); + } + return; + case TapeOperationMode.Load: + if ((_tapePlayer?.Eof ?? false) || _cpu.RegPC == ERROR_ROM_ADDRESS) + { + LeaveLoadMode(); + } + return; + } + } + + /// + /// Puts the device in save mode. From now on, every MIC pulse is recorded + /// + private void EnterSaveMode() + { + _currentMode = TapeOperationMode.Save; + _savePhase = SavePhase.None; + _micBitState = true; + _lastMicBitActivityCycle = _cpu.TotalExecutedCycles; + _pilotPulseCount = 0; + _prevDataPulse = MicPulseType.None; + _dataBlockCount = 0; + TapeProvider?.CreateTapeFile(); + } + + /// + /// Leaves the save mode. Stops recording MIC pulses + /// + private void LeaveSaveMode() + { + _currentMode = TapeOperationMode.Passive; + TapeProvider?.FinalizeTapeFile(); + } + + /// + /// Puts the device in load mode. From now on, EAR pulses are played by a device + /// + private void EnterLoadMode() + { + _currentMode = TapeOperationMode.Load; + + var contentReader = TapeProvider?.GetTapeContent(); + if (contentReader == null) return; + + // --- Play the content + _tapePlayer = new TapeFilePlayer(contentReader); + _tapePlayer.ReadContent(); + _tapePlayer.InitPlay(_cpu.TotalExecutedCycles); + _buzzer.SetTapeMode(true); + } + + /// + /// Leaves the load mode. Stops the device that playes EAR pulses + /// + private void LeaveLoadMode() + { + _currentMode = TapeOperationMode.Passive; + _tapePlayer = null; + TapeProvider?.Reset(); + _buzzer.SetTapeMode(false); + } + + /// + /// Loads the next TZX player block instantly without emulation + /// EAR bit processing + /// + /// True, if fast load is operative + private bool FastLoadFromTzx() + { + var c = _machine.Spectrum; + + var blockType = TapeFilePlayer.CurrentBlock.GetType(); + bool canFlash = TapeFilePlayer.CurrentBlock is ITapeData; + + // --- Check, if we can load the current block in a fast way + if (!(TapeFilePlayer.CurrentBlock is ITapeData) + || TapeFilePlayer.PlayPhase == PlayPhase.Completed) + { + // --- We cannot play this block + return false; + } + + var currentData = TapeFilePlayer.CurrentBlock as ITapeData; + + var regs = _cpu.Regs; + + //regs.AF = regs._AF_; + //c.Set16BitAF(c.Get16BitAF_()); + _cpu.A = _cpu.A_s; + _cpu.F = _cpu.F_s; + + // --- Check if the operation is LOAD or VERIFY + var isVerify = (c.RegAF & 0xFF01) == 0xFF00; + + // --- At this point IX contains the address to load the data, + // --- DE shows the #of bytes to load. A contains 0x00 for header, + // --- 0xFF for data block + var data = currentData.Data; + if (data[0] != regs[_cpu.A]) + { + // --- This block has a different type we're expecting + regs[_cpu.A] = (byte)(regs[_cpu.A] ^ regs[_cpu.L]); + regs[_cpu.F] &= (byte)ZXSpectrum.FlagsResetMask.Z; + regs[_cpu.F] &= (byte)ZXSpectrum.FlagsResetMask.C; + c.RegPC = _machine.RomData.LoadBytesInvalidHeaderAddress; + + // --- Get the next block + TapeFilePlayer.NextBlock(_cpu.TotalExecutedCycles); + return true; + } + + // --- It is time to load the block + var curIndex = 1; + //var memory = _machine.me MemoryDevice; + regs[_cpu.H] = regs[_cpu.A]; + while (c.RegDE > 0) + { + var de16 = c.RegDE; + var ix16 = c.RegIX; + if (curIndex > data.Length - 1) + { + // --- No more data to read + //break; + } + + regs[_cpu.L] = data[curIndex]; + if (isVerify && regs[_cpu.L] != _machine.ReadBus(c.RegIX)) + { + // --- Verify failed + regs[_cpu.A] = (byte)(_machine.ReadBus(c.RegIX) ^ regs[_cpu.L]); + regs[_cpu.F] &= (byte)ZXSpectrum.FlagsResetMask.Z; + regs[_cpu.F] &= (byte)ZXSpectrum.FlagsResetMask.C; + c.RegPC = _machine.RomData.LoadBytesInvalidHeaderAddress; + return true; + } + + // --- Store the loaded data byte + _machine.WriteBus(c.RegIX, (byte)regs[_cpu.L]); + regs[_cpu.H] ^= regs[_cpu.L]; + curIndex++; + //regs.IX++; + //c.Set16BitIX((ushort)((int)c.Get16BitIX() + 1)); + c.RegIX++; + //regs.DE--; + //c.Set16BitDE((ushort)((int)c.Get16BitDE() - 1)); + //_cpu.Regs[_cpu.E]--; + c.RegDE--; + var te = c.RegDE; + } + + // --- Check the parity byte at the end of the data stream + if (curIndex > data.Length - 1 || regs[_cpu.H] != data[curIndex]) + { + // --- Carry is reset to sign an error + regs[_cpu.F] &= (byte)ZXSpectrum.FlagsResetMask.C; + } + else + { + // --- Carry is set to sign success + regs[_cpu.F] |= (byte)ZXSpectrum.FlagsSetMask.C; + } + c.RegPC = _machine.RomData.LoadBytesResumeAddress; + + // --- Get the next block + TapeFilePlayer.NextBlock(_cpu.TotalExecutedCycles); + return true; + } + + + /// + /// the EAR bit read from tape + /// + /// + /// + public virtual bool GetEarBit(int cpuCycles) + { + if (_currentMode != TapeOperationMode.Load) + { + return true; + } + + var earBit = _tapePlayer?.GetEarBit(cpuCycles) ?? true; + _buzzer.ProcessPulseValue(true, earBit); + return earBit; + } + + /// + /// Processes the mic bit change + /// + /// + public virtual void ProcessMicBit(bool micBit) + { + if (_currentMode != TapeOperationMode.Save + || _micBitState == micBit) + { + return; + } + + var length = _cpu.TotalExecutedCycles - _lastMicBitActivityCycle; + + // --- Classify the pulse by its width + var pulse = MicPulseType.None; + if (length >= TapeDataBlockPlayer.BIT_0_PL - SAVE_PULSE_TOLERANCE + && length <= TapeDataBlockPlayer.BIT_0_PL + SAVE_PULSE_TOLERANCE) + { + pulse = MicPulseType.Bit0; + } + else if (length >= TapeDataBlockPlayer.BIT_1_PL - SAVE_PULSE_TOLERANCE + && length <= TapeDataBlockPlayer.BIT_1_PL + SAVE_PULSE_TOLERANCE) + { + pulse = MicPulseType.Bit1; + } + if (length >= TapeDataBlockPlayer.PILOT_PL - SAVE_PULSE_TOLERANCE + && length <= TapeDataBlockPlayer.PILOT_PL + SAVE_PULSE_TOLERANCE) + { + pulse = MicPulseType.Pilot; + } + else if (length >= TapeDataBlockPlayer.SYNC_1_PL - SAVE_PULSE_TOLERANCE + && length <= TapeDataBlockPlayer.SYNC_1_PL + SAVE_PULSE_TOLERANCE) + { + pulse = MicPulseType.Sync1; + } + else if (length >= TapeDataBlockPlayer.SYNC_2_PL - SAVE_PULSE_TOLERANCE + && length <= TapeDataBlockPlayer.SYNC_2_PL + SAVE_PULSE_TOLERANCE) + { + pulse = MicPulseType.Sync2; + } + else if (length >= TapeDataBlockPlayer.TERM_SYNC - SAVE_PULSE_TOLERANCE + && length <= TapeDataBlockPlayer.TERM_SYNC + SAVE_PULSE_TOLERANCE) + { + pulse = MicPulseType.TermSync; + } + else if (length < TapeDataBlockPlayer.SYNC_1_PL - SAVE_PULSE_TOLERANCE) + { + pulse = MicPulseType.TooShort; + } + else if (length > TapeDataBlockPlayer.PILOT_PL + 2 * SAVE_PULSE_TOLERANCE) + { + pulse = MicPulseType.TooLong; + } + + _micBitState = micBit; + _lastMicBitActivityCycle = _cpu.TotalExecutedCycles; + + // --- Lets process the pulse according to the current SAVE phase and pulse width + var nextPhase = SavePhase.Error; + switch (_savePhase) + { + case SavePhase.None: + if (pulse == MicPulseType.TooShort || pulse == MicPulseType.TooLong) + { + nextPhase = SavePhase.None; + } + else if (pulse == MicPulseType.Pilot) + { + _pilotPulseCount = 1; + nextPhase = SavePhase.Pilot; + } + break; + case SavePhase.Pilot: + if (pulse == MicPulseType.Pilot) + { + _pilotPulseCount++; + nextPhase = SavePhase.Pilot; + } + else if (pulse == MicPulseType.Sync1 && _pilotPulseCount >= MIN_PILOT_PULSE_COUNT) + { + nextPhase = SavePhase.Sync1; + } + break; + case SavePhase.Sync1: + if (pulse == MicPulseType.Sync2) + { + nextPhase = SavePhase.Sync2; + } + break; + case SavePhase.Sync2: + if (pulse == MicPulseType.Bit0 || pulse == MicPulseType.Bit1) + { + // --- Next pulse starts data, prepare for receiving it + _prevDataPulse = pulse; + nextPhase = SavePhase.Data; + _bitOffset = 0; + _dataByte = 0; + _dataLength = 0; + _dataBuffer = new byte[DATA_BUFFER_LENGTH]; + } + break; + case SavePhase.Data: + if (pulse == MicPulseType.Bit0 || pulse == MicPulseType.Bit1) + { + if (_prevDataPulse == MicPulseType.None) + { + // --- We are waiting for the second half of the bit pulse + _prevDataPulse = pulse; + nextPhase = SavePhase.Data; + } + else if (_prevDataPulse == pulse) + { + // --- We received a full valid bit pulse + nextPhase = SavePhase.Data; + _prevDataPulse = MicPulseType.None; + + // --- Add this bit to the received data + _bitOffset++; + _dataByte = (byte)(_dataByte * 2 + (pulse == MicPulseType.Bit0 ? 0 : 1)); + if (_bitOffset == 8) + { + // --- We received a full byte + _dataBuffer[_dataLength++] = _dataByte; + _dataByte = 0; + _bitOffset = 0; + } + } + } + else if (pulse == MicPulseType.TermSync) + { + // --- We received the terminating pulse, the datablock has been completed + nextPhase = SavePhase.None; + _dataBlockCount++; + + // --- Create and save the data block + var dataBlock = new TzxStandardSpeedDataBlock + { + Data = _dataBuffer, + DataLength = (ushort)_dataLength + }; + + // --- If this is the first data block, extract the name from the header + if (_dataBlockCount == 1 && _dataLength == 0x13) + { + // --- It's a header! + var sb = new StringBuilder(16); + for (var i = 2; i <= 11; i++) + { + sb.Append((char)_dataBuffer[i]); + } + var name = sb.ToString().TrimEnd(); + TapeProvider?.SetName(name); + } + TapeProvider?.SaveTapeBlock(dataBlock); + } + break; + } + _savePhase = nextPhase; + } + + public void SyncState(Serializer ser) + { + ser.BeginSection("TapeDevice"); + ser.Sync("_micBitState", ref _micBitState); + ser.Sync("_lastMicBitActivityCycle", ref _lastMicBitActivityCycle); + ser.Sync("_pilotPulseCount", ref _pilotPulseCount); + ser.Sync("_bitOffset", ref _bitOffset); + ser.Sync("_dataByte", ref _dataByte); + ser.Sync("_dataLength", ref _dataLength); + ser.Sync("_dataBlockCount", ref _dataBlockCount); + ser.Sync("_dataBuffer", ref _dataBuffer, false); + ser.SyncEnum("_currentMode", ref _currentMode); + ser.SyncEnum("_savePhase", ref _savePhase); + ser.SyncEnum("_prevDataPulse", ref _prevDataPulse); + /* + private TapeFilePlayer _tapePlayer; + */ + + ser.EndSection(); + } + } + + /// + /// This enum represents the operation mode of the tape + /// + public enum TapeOperationMode : byte + { + /// + /// The tape device is passive + /// + Passive = 0, + + /// + /// The tape device is saving information (MIC pulses) + /// + Save, + + /// + /// The tape device generates EAR pulses from a player + /// + Load + } + + /// + /// This class represents a spectrum tape header + /// + public class SpectrumTapeHeader + { + private const int HEADER_LEN = 19; + private const int TYPE_OFFS = 1; + private const int NAME_OFFS = 2; + private const int NAME_LEN = 10; + private const int DATA_LEN_OFFS = 12; + private const int PAR1_OFFS = 14; + private const int PAR2_OFFS = 16; + private const int CHK_OFFS = 18; + + /// + /// The bytes of the header + /// + public byte[] HeaderBytes { get; } + + /// + /// Initializes a new instance of the class. + /// + public SpectrumTapeHeader() + { + HeaderBytes = new byte[HEADER_LEN]; + for (var i = 0; i < HEADER_LEN; i++) HeaderBytes[i] = 0x00; + CalcChecksum(); + } + + /// + /// Initializes a new instance with the specified header data. + /// + /// Header data + public SpectrumTapeHeader(byte[] header) + { + if (header == null) throw new ArgumentNullException(nameof(header)); + if (header.Length != HEADER_LEN) + { + throw new ArgumentException($"Header must be exactly {HEADER_LEN} bytes long"); + } + HeaderBytes = new byte[HEADER_LEN]; + header.CopyTo(HeaderBytes, 0); + CalcChecksum(); + } + + /// + /// Gets or sets the type of the header + /// + public byte Type + { + get { return HeaderBytes[TYPE_OFFS]; } + set + { + HeaderBytes[TYPE_OFFS] = (byte)(value & 0x03); + CalcChecksum(); + } + } + + /// + /// Gets or sets the program name + /// + public string Name + { + get + { + var name = new StringBuilder(NAME_LEN + 4); + for (var i = NAME_OFFS; i < NAME_OFFS + NAME_LEN; i++) + { + name.Append((char)HeaderBytes[i]); + } + return name.ToString().TrimEnd(); + } + set + { + if (value == null) throw new ArgumentNullException(nameof(value)); + if (value.Length > NAME_LEN) value = value.Substring(0, NAME_LEN); + else if (value.Length < NAME_LEN) value = value.PadRight(NAME_LEN, ' '); + for (var i = NAME_OFFS; i < NAME_OFFS + NAME_LEN; i++) + { + HeaderBytes[i] = (byte)value[i - NAME_OFFS]; + } + CalcChecksum(); + } + } + + /// + /// Gets or sets the Data Length + /// + public ushort DataLength + { + get { return GetWord(DATA_LEN_OFFS); } + set { SetWord(DATA_LEN_OFFS, value); } + } + + /// + /// Gets or sets Parameter1 + /// + public ushort Parameter1 + { + get { return GetWord(PAR1_OFFS); } + set { SetWord(PAR1_OFFS, value); } + } + + /// + /// Gets or sets Parameter2 + /// + public ushort Parameter2 + { + get { return GetWord(PAR2_OFFS); } + set { SetWord(PAR2_OFFS, value); } + } + + /// + /// Gets the value of checksum + /// + public byte Checksum => HeaderBytes[CHK_OFFS]; + + /// + /// Calculate the checksum + /// + private void CalcChecksum() + { + var chk = 0x00; + for (var i = 0; i < HEADER_LEN - 1; i++) chk ^= HeaderBytes[i]; + HeaderBytes[CHK_OFFS] = (byte)chk; + } + + /// + /// Gets the word value from the specified offset + /// + private ushort GetWord(int offset) => + (ushort)(HeaderBytes[offset] + 256 * HeaderBytes[offset + 1]); + + /// + /// Sets the word value at the specified offset + /// + private void SetWord(int offset, ushort value) + { + HeaderBytes[offset] = (byte)(value & 0xff); + HeaderBytes[offset + 1] = (byte)(value >> 8); + CalcChecksum(); + } + } + + /// + /// This enum defines the MIC pulse types according to their widths + /// + public enum MicPulseType : byte + { + /// + /// No pulse information + /// + None = 0, + + /// + /// Too short to be a valid pulse + /// + TooShort, + + /// + /// Too long to be a valid pulse + /// + TooLong, + + /// + /// PILOT pulse (Length: 2168 cycles) + /// + Pilot, + + /// + /// SYNC1 pulse (Length: 667 cycles) + /// + Sync1, + + /// + /// SYNC2 pulse (Length: 735 cycles) + /// + Sync2, + + /// + /// BIT0 pulse (Length: 855 cycles) + /// + Bit0, + + /// + /// BIT1 pulse (Length: 1710 cycles) + /// + Bit1, + + /// + /// TERM_SYNC pulse (Length: 947 cycles) + /// + TermSync + } + + /// + /// Represents the playing phase of the current block + /// + public enum PlayPhase + { + /// + /// The player is passive + /// + None = 0, + + /// + /// Pilot signals + /// + Pilot, + + /// + /// Sync signals at the end of the pilot + /// + Sync, + + /// + /// Bits in the data block + /// + Data, + + /// + /// Short terminating sync signal before pause + /// + TermSync, + + /// + /// Pause after the data block + /// + Pause, + + /// + /// The entire block has been played back + /// + Completed + } + + /// + /// This enumeration defines the phases of the SAVE operation + /// + public enum SavePhase : byte + { + /// No SAVE operation is in progress + None = 0, + + /// Emitting PILOT impulses + Pilot, + + /// Emitting SYNC1 impulse + Sync1, + + /// Emitting SYNC2 impulse + Sync2, + + /// Emitting BIT0/BIT1 impulses + Data, + + /// Unexpected pulse detected + Error + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/TapeBlockSetPlayer.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/TapeBlockSetPlayer.cs new file mode 100644 index 0000000000..c08b488acf --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/TapeBlockSetPlayer.cs @@ -0,0 +1,102 @@ +using System.Collections.Generic; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// This class is responsible to "play" a tape file. + /// + public class TapeBlockSetPlayer : ISupportsTapeBlockSetPlayback + { + /// + /// All data blocks that can be played back + /// + public List DataBlocks { get; } + + /// + /// Signs that the player completed playing back the file + /// + public bool Eof { get; private set; } + + /// + /// Gets the currently playing block's index + /// + public int CurrentBlockIndex { get; private set; } + + /// + /// The current playable block + /// + public ISupportsTapeBlockPlayback CurrentBlock => DataBlocks[CurrentBlockIndex]; + + /// + /// The current playing phase + /// + public PlayPhase PlayPhase { get; private set; } + + /// + /// The cycle count of the CPU when playing starts + /// + public long StartCycle { get; private set; } + + public TapeBlockSetPlayer(List dataBlocks) + { + DataBlocks = dataBlocks; + Eof = dataBlocks.Count == 0; + } + + /// + /// Initializes the player + /// + public void InitPlay(long startTact) + { + CurrentBlockIndex = -1; + NextBlock(startTact); + PlayPhase = PlayPhase.None; + StartCycle = startTact; + } + + /// + /// Gets the EAR bit value for the specified cycle + /// + /// Cycles to retrieve the EAR bit + /// + /// A tuple of the EAR bit and a flag that indicates it is time to move to the next block + /// + public bool GetEarBit(long currentCycle) + { + // --- Check for EOF + if (CurrentBlockIndex == DataBlocks.Count - 1 + && (CurrentBlock.PlayPhase == PlayPhase.Pause || CurrentBlock.PlayPhase == PlayPhase.Completed)) + { + Eof = true; + } + if (CurrentBlockIndex >= DataBlocks.Count || CurrentBlock == null) + { + // --- After all playable block played back, there's nothing more to do + PlayPhase = PlayPhase.Completed; + return true; + } + var earbit = CurrentBlock.GetEarBit(currentCycle); + if (CurrentBlock.PlayPhase == PlayPhase.Completed) + { + NextBlock(currentCycle); + } + return earbit; + } + + /// + /// Moves the current block index to the next playable block + /// + /// Cycles time to start the next block + public void NextBlock(long currentCycle) + { + if (CurrentBlockIndex >= DataBlocks.Count - 1) + { + PlayPhase = PlayPhase.Completed; + Eof = true; + return; + } + CurrentBlockIndex++; + CurrentBlock.InitPlay(currentCycle); + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/TapeDataBlockPlayer.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/TapeDataBlockPlayer.cs new file mode 100644 index 0000000000..547d7698b9 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/TapeDataBlockPlayer.cs @@ -0,0 +1,218 @@ + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// Represents the standard speed data block in a tape file + /// + public class TapeDataBlockPlayer : ISupportsTapeBlockPlayback, ITapeData + { + /// + /// Pause after this block (default: 1000ms) + /// + public ushort PauseAfter { get; } + + /// + /// Block Data + /// + public byte[] Data { get; } + + /// + /// Initializes a new instance + /// + public TapeDataBlockPlayer(byte[] data, ushort pauseAfter) + { + PauseAfter = pauseAfter; + Data = data; + } + + /// + /// Pilot pulse length + /// + public const int PILOT_PL = 2168; + + /// + /// Pilot pulses in the ROM header block + /// + public const int HEADER_PILOT_COUNT = 8063; + + /// + /// Pilot pulses in the ROM data block + /// + public const int DATA_PILOT_COUNT = 3223; + + /// + /// Sync 1 pulse length + /// + public const int SYNC_1_PL = 667; + + /// + /// Sync 2 pulse lenth + /// + public const int SYNC_2_PL = 735; + + /// + /// Bit 0 pulse length + /// + public const int BIT_0_PL = 855; + + /// + /// Bit 1 pulse length + /// + public const int BIT_1_PL = 1710; + + /// + /// End sync pulse length + /// + public const int TERM_SYNC = 947; + + /// + /// 1 millisecond pause + /// + public const int PAUSE_MS = 3500; + + private int _pilotEnds; + private int _sync1Ends; + private int _sync2Ends; + private int _bitStarts; + private int _bitPulseLength; + private bool _currentBit; + private long _termSyncEnds; + private long _pauseEnds; + + /// + /// The index of the currently playing byte + /// + public int ByteIndex { get; private set; } + + /// + /// The mask of the currently playing bit in the current byte + /// + public byte BitMask { get; private set; } + + /// + /// The current playing phase + /// + public PlayPhase PlayPhase { get; private set; } + + /// + /// The cycle count of the CPU when playing starts + /// + public long StartCycle { get; private set; } + + /// + /// Last cycle queried + /// + public long LastCycle { get; private set; } + + /// + /// Initializes the player + /// + public void InitPlay(long startTact) + { + PlayPhase = PlayPhase.Pilot; + StartCycle = LastCycle = startTact; + _pilotEnds = ((Data[0] & 0x80) == 0 ? HEADER_PILOT_COUNT : DATA_PILOT_COUNT) * PILOT_PL; + _sync1Ends = _pilotEnds + SYNC_1_PL; + _sync2Ends = _sync1Ends + SYNC_2_PL; + ByteIndex = 0; + BitMask = 0x80; + } + + /// + /// Gets the EAR bit value for the specified cycle + /// + /// Tacts to retrieve the EAR bit + /// + /// The EAR bit value to play back + /// + public bool GetEarBit(long currentCycle) + { + var pos = (int)(currentCycle - StartCycle); + LastCycle = currentCycle; + + if (PlayPhase == PlayPhase.Pilot || PlayPhase == PlayPhase.Sync) + { + // --- Generate the appropriate pilot or sync EAR bit + if (pos <= _pilotEnds) + { + // --- Alternating pilot pulses + return (pos / PILOT_PL) % 2 == 0; + } + if (pos <= _sync1Ends) + { + // --- 1st sync pulse + PlayPhase = PlayPhase.Sync; + return false; + } + if (pos <= _sync2Ends) + { + // --- 2nd sync pulse + PlayPhase = PlayPhase.Sync; + return true; + } + PlayPhase = PlayPhase.Data; + _bitStarts = _sync2Ends; + _currentBit = (Data[ByteIndex] & BitMask) != 0; + _bitPulseLength = _currentBit ? BIT_1_PL : BIT_0_PL; + } + if (PlayPhase == PlayPhase.Data) + { + // --- Data block playback + // --- Generate current bit pulse + var bitPos = pos - _bitStarts; + if (bitPos < _bitPulseLength) + { + // --- First pulse of the bit + return false; + } + if (bitPos < 2 * _bitPulseLength) + { + // --- Second pulse of the bit + return true; + } + + // --- Move to the next bit, or byte + if ((BitMask >>= 1) == 0) + { + BitMask = 0x80; + ByteIndex++; + } + + // --- Prepare the next bit + if (ByteIndex < Data.Length) + { + _bitStarts += 2 * _bitPulseLength; + _currentBit = (Data[ByteIndex] & BitMask) != 0; + _bitPulseLength = _currentBit ? BIT_1_PL : BIT_0_PL; + // --- We're in the first pulse of the next bit + return false; + } + + // --- We've played back all data bytes, send terminating pulse + PlayPhase = PlayPhase.TermSync; + _termSyncEnds = currentCycle + TERM_SYNC; + return false; + } + + if (PlayPhase == PlayPhase.TermSync) + { + if (currentCycle < _termSyncEnds) + { + return false; + } + + // --- We've played back all data, not, it's pause time + PlayPhase = PlayPhase.Pause; + _pauseEnds = currentCycle + PAUSE_MS * PauseAfter; + return true; + } + + // --- We need to produce pause signs + if (currentCycle > _pauseEnds) + { + PlayPhase = PlayPhase.Completed; + } + return true; + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/TapeFilePlayer.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/TapeFilePlayer.cs new file mode 100644 index 0000000000..25c75df0f8 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/TapeFilePlayer.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// This class recognizes .TZX and .TAP files, and playes back + /// the content accordingly. + /// + public class TapeFilePlayer : ISupportsTapeBlockPlayback + { + private readonly BinaryReader _reader; + private TapeBlockSetPlayer _player; + + /// + /// Data blocks to play back + /// + public List DataBlocks { get; private set; } + + /// + /// Signs that the player completed playing back the file + /// + public bool Eof => _player.Eof; + + /// + /// Initializes the player from the specified reader + /// + /// BinaryReader instance to get tape file data from + public TapeFilePlayer(BinaryReader reader) + { + _reader = reader; + } + + /// + /// Reads in the content of the tape file so that it can be played + /// + /// True, if read was successful; otherwise, false + public bool ReadContent() + { + // --- First try TzxReader + var tzxReader = new TzxReader(_reader); + var readerFound = false; + try + { + readerFound = tzxReader.ReadContent(); + } + catch (Exception) + { + // --- This exception is intentionally ingnored + } + + if (readerFound) + { + // --- This is a .TZX format + DataBlocks = tzxReader.DataBlocks.Where(b => b is ISupportsTapeBlockPlayback) + .Cast() + .ToList(); + _player = new TapeBlockSetPlayer(DataBlocks); + return true; + } + + // --- Let's assume .TAP tap format + _reader.BaseStream.Seek(0, SeekOrigin.Begin); + var tapReader = new TapReader(_reader); + readerFound = tapReader.ReadContent(); + DataBlocks = tapReader.DataBlocks.Cast() + .ToList(); + _player = new TapeBlockSetPlayer(DataBlocks); + return readerFound; + } + + /// + /// Gets the currently playing block's index + /// + public int CurrentBlockIndex => _player.CurrentBlockIndex; + + /// + /// The current playable block + /// + public ISupportsTapeBlockPlayback CurrentBlock => _player.CurrentBlock; + + /// + /// The current playing phase + /// + public PlayPhase PlayPhase => _player.PlayPhase; + + /// + /// The tact count of the CPU when playing starts + /// + public long StartCycle => _player.StartCycle; + + /// + /// Initializes the player + /// + public void InitPlay(long startCycle) + { + _player.InitPlay(startCycle); + } + + /// + /// Gets the EAR bit value for the specified cycle + /// + /// Tacts to retrieve the EAR bit + /// + /// A tuple of the EAR bit and a flag that indicates it is time to move to the next block + /// + public bool GetEarBit(long currentCycle) => _player.GetEarBit(currentCycle); + + /// + /// Moves the current block index to the next playable block + /// + /// Tacts time to start the next block + public void NextBlock(long currentCycle) => _player.NextBlock(currentCycle); + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/MachineType.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/MachineType.cs new file mode 100644 index 0000000000..829f1a0e5b --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/MachineType.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public enum MachineType + { + /// + /// Sinclair Spectrum 48K model + /// + ZXSpectrum48 + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Input.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Input.cs new file mode 100644 index 0000000000..01ac5ab51a --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Input.cs @@ -0,0 +1,47 @@ + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// Handles all ZX-level input + /// + public abstract partial class SpectrumBase + { + string Play = "Play Tape"; + string Stop = "Stop Tape"; + string RTZ = "RTZ Tape"; + string Record = "Record Tape"; + + public void PollInput() + { + Spectrum.InputCallbacks.Call(); + + for (var i = 0; i < KeyboardDevice.KeyboardMatrix.Length; i++) + { + string key = KeyboardDevice.KeyboardMatrix[i]; + bool prevState = KeyboardDevice.GetKeyStatus(key); + bool currState = Spectrum._controller.IsPressed(key); + + if (currState != prevState) + KeyboardDevice.SetKeyStatus(key, currState); + } + + // Tape control + if (Spectrum._controller.IsPressed(Play)) + { + + } + if (Spectrum._controller.IsPressed(Stop)) + { + + } + if (Spectrum._controller.IsPressed(RTZ)) + { + + } + if (Spectrum._controller.IsPressed(Record)) + { + + } + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Memory.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Memory.cs new file mode 100644 index 0000000000..d993b15f32 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Memory.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// The abstract class that all emulated models will inherit from + /// * Memory * + /// + public abstract partial class SpectrumBase + { + /// + /// ROM Banks + /// + public byte[] ROM0 = new byte[0x4000]; + public byte[] ROM1 = new byte[0x4000]; + public byte[] ROM2 = new byte[0x4000]; + public byte[] ROM3 = new byte[0x4000]; + + /// + /// RAM Banks + /// + public byte[] RAM0 = new byte[0x4000]; // Bank 0 + public byte[] RAM1 = new byte[0x4000]; // Bank 1 + public byte[] RAM2 = new byte[0x4000]; // Bank 2 + public byte[] RAM3 = new byte[0x4000]; // Bank 3 + public byte[] RAM4 = new byte[0x4000]; // Bank 4 + public byte[] RAM5 = new byte[0x4000]; // Bank 5 + public byte[] RAM6 = new byte[0x4000]; // Bank 6 + public byte[] RAM7 = new byte[0x4000]; // Bank 7 + + /// + /// Represents the addressable memory space of the spectrum + /// All banks for the emulated system should be added during initialisation + /// + public Dictionary Memory = new Dictionary(); + + /// + /// Simulates reading from the bus + /// Paging should be handled here + /// + /// + /// + public virtual byte ReadBus(ushort addr) + { + throw new NotImplementedException("Must be overriden"); + } + + /// + /// Simulates writing to the bus + /// Paging should be handled here + /// + /// + /// + public virtual void WriteBus(ushort addr, byte value) + { + throw new NotImplementedException("Must be overriden"); + } + + /// + /// Reads a byte of data from a specified memory address + /// (with memory contention if appropriate) + /// + /// + /// + public virtual byte ReadMemory(ushort addr) + { + throw new NotImplementedException("Must be overriden"); + } + /* + /// + /// Reads a byte of data from a specified memory address + /// (with no memory contention) + /// + /// + /// + public virtual byte PeekMemory(ushort addr) + { + var data = ReadBus(addr); + return data; + } + */ + + /// + /// Writes a byte of data to a specified memory address + /// (with memory contention if appropriate) + /// + /// + /// + public virtual void WriteMemory(ushort addr, byte value) + { + throw new NotImplementedException("Must be overriden"); + } + + /* + /// + /// Writes a byte of data to a specified memory address + /// (without contention) + /// + /// + /// + public virtual void PokeMemory(ushort addr, byte value) + { + if (addr < 0x4000) + { + // Do nothing - we cannot write to ROM + return; + } + + WriteBus(addr, value); + } + */ + + /// + /// Fills memory from buffer + /// + /// + /// + public virtual void FillMemory(byte[] buffer, ushort startAddress) + { + //buffer?.CopyTo(RAM, startAddress); + } + + /// + /// Sets up the ROM + /// + /// + /// + public virtual void InitROM(RomData romData) + { + RomData = romData; + // for 16/48k machines only ROM0 is used (no paging) + RomData.RomBytes?.CopyTo(ROM0, 0); + } + + /// + /// ULA reads the memory at the specified address + /// (No memory contention) + /// + /// + /// + public virtual byte FetchScreenMemory(ushort addr) + { + //var value = RAM0[(addr & 0x3FFF)];// + 0x4000]; + var value = ReadBus((ushort)((addr & 0x3FFF) + 0x4000)); + return value; + } + + /// + /// Helper function to refresh memory array (probably not the best way to do things) + /// + public virtual void ReInitMemory() + { + throw new NotImplementedException("Must be overriden"); + } + + /// + /// Returns the memory contention value for the specified T-State (cycle) + /// The ZX Spectrum memory access is contended when the ULA is accessing the lower 16k of RAM + /// + /// + /// + public virtual byte GetContentionValue(int cycle) + { + var val = _renderingCycleTable[cycle % UlaFrameCycleCount].ContentionDelay; + return val; + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Port.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Port.cs new file mode 100644 index 0000000000..a8b715a214 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Port.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// The abstract class that all emulated models will inherit from + /// * Port Access * + /// + public abstract partial class SpectrumBase + { + /// + /// The last OUT data that was sent to the ULA + /// + protected byte LastULAOutByte; + + /// + /// Reads a byte of data from a specified port address + /// + /// + /// + public virtual byte ReadPort(ushort port) + { + CPU.TotalExecutedCycles += 4; + + byte result = 0xFF; + + // get the high byte from Regs[6] + ushort high = CPU.Regs[6]; + + // combine the low byte (passed in as port) and the high byte (maybe not needed) + ushort word = Convert.ToUInt16((port << 8 | high)); + + // Check whether the low bit is reset + // Technically the ULA should respond to every even I/O address + bool lowBitReset = (port & 0x0001) == 0; + + // Kempston Joystick + //not implemented yet + + if (lowBitReset) + { + // Even I/O address so get input + // The high byte indicates which half-row of keys is being polled + /* + IN: Reads keys (bit 0 to bit 4 inclusive) + 0xfefe SHIFT, Z, X, C, V 0xeffe 0, 9, 8, 7, 6 + 0xfdfe A, S, D, F, G 0xdffe P, O, I, U, Y + 0xfbfe Q, W, E, R, T 0xbffe ENTER, L, K, J, H + 0xf7fe 1, 2, 3, 4, 5 0x7ffe SPACE, SYM SHFT, M, N, B + */ + + // read keyboard input + if (high != 0) + result = KeyboardDevice.GetLineStatus((byte)high); + + var ear = TapeDevice.GetEarBit(CPU.TotalExecutedCycles); + if (!ear) + { + result = (byte)(result & Convert.ToInt32("10111111", 2)); + } + } + else + { + // devices other than the ULA will respond here + // (e.g. the AY sound chip in a 128k spectrum + + // AY register activate + // Kemptson Mouse + + + // if unused port the floating memory bus should be returned (still todo) + } + + return result; + } + + /// + /// Writes a byte of data to a specified port address + /// + /// + /// + public virtual void WritePort(ushort port, byte value) + { + CPU.TotalExecutedCycles += 4; + + // Check whether the low bit is reset + // Technically the ULA should respond to every even I/O address + bool lowBitReset = (port & 0x0001) == 0; + + // Only even addresses address the ULA + if (lowBitReset) + { + // store the last OUT byte + LastULAOutByte = value; + + /* + Bit 7 6 5 4 3 2 1 0 + +-------------------------------+ + | | | | E | M | Border | + +-------------------------------+ + */ + + // Border - LSB 3 bits hold the border colour + BorderColour = value & BORDER_BIT; + + // Buzzer + BuzzerDevice.ProcessPulseValue(false, (value & EAR_BIT) != 0); + + // Tape + TapeDevice.ProcessMicBit((value & MIC_BIT) != 0); + } + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Screen.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Screen.cs new file mode 100644 index 0000000000..bdada81b35 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Screen.cs @@ -0,0 +1,925 @@ +using BizHawk.Common; +using BizHawk.Emulation.Common; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /* + * Much of the SCREEN implementation has been taken from: https://github.com/Dotneteer/spectnetide + * + * MIT License + + Copyright (c) 2017 Istvan Novak + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + */ + + /// + /// The abstract class that all emulated models will inherit from + /// * Screen * + /// + public abstract partial class SpectrumBase : IVideoProvider + { + #region State + + /// + /// The main screen buffer + /// + protected byte[] _frameBuffer; + + /// + /// Pixel and attribute info stored while rendering the screen + /// + protected byte _pixelByte1; + protected byte _pixelByte2; + protected byte _attrByte1; + protected byte _attrByte2; + protected int _xPos; + protected int _yPos; + protected int[] _flashOffColors; + protected int[] _flashOnColors; + protected ScreenRenderingCycle[] _renderingCycleTable; + protected bool _flashPhase; + + #endregion + + #region Statics + + /// + /// The standard ULA palette + /// + private static readonly int[] ULAPalette = + { + Colors.ARGB(0x00, 0x00, 0x00), // Black + Colors.ARGB(0x00, 0x00, 0xD7), // Blue + Colors.ARGB(0xD7, 0x00, 0x00), // Red + Colors.ARGB(0xD7, 0x00, 0xD7), // Magenta + Colors.ARGB(0x00, 0xD7, 0x00), // Green + Colors.ARGB(0x00, 0xD7, 0xD7), // Cyan + Colors.ARGB(0xD7, 0xD7, 0x00), // Yellow + Colors.ARGB(0xD7, 0xD7, 0xD7), // White + Colors.ARGB(0x00, 0x00, 0x00), // Bright Black + Colors.ARGB(0x00, 0x00, 0xFF), // Bright Blue + Colors.ARGB(0xFF, 0x00, 0x00), // Bright Red + Colors.ARGB(0xFF, 0x00, 0xFF), // Bright Magenta + Colors.ARGB(0x00, 0xFF, 0x00), // Bright Green + Colors.ARGB(0x00, 0xFF, 0xFF), // Bright Cyan + Colors.ARGB(0xFF, 0xFF, 0x00), // Bright Yellow + Colors.ARGB(0xFF, 0xFF, 0xFF), // Bright White + }; + + #endregion + + #region ScreenConfig + + /// + /// The number of displayed pixels in a display row + /// + protected int DisplayWidth = 256; + + /// + /// Number of display lines + /// + protected int DisplayLines = 192; + + /// + /// The number of frames after the flash is toggled + /// + protected int FlashToggleFrames = 25; + + /// + /// Number of lines used for vertical sync + /// + protected int VerticalSyncLines = 8; + + /// + /// The number of top border lines that are not visible + /// when rendering the screen + /// + protected int NonVisibleBorderTopLines = 8; + + /// + /// The number of border lines before the display + /// + protected int BorderTopLines = 48; + + /// + /// The number of border lines after the display + /// + protected int BorderBottomLines = 48; + + /// + /// The number of bottom border lines that are not visible + /// when rendering the screen + /// + protected int NonVisibleBorderBottomLines = 8; + + /// + /// The total number of lines in the screen + /// + protected int ScreenLines; + + /// + /// The first screen line that contains the top left display pixel + /// + protected int FirstDisplayLine; + + /// + /// The last screen line that contains the bottom right display pixel + /// + protected int LastDisplayLine; + + /// + /// The number of border pixels to the left of the display + /// + protected int BorderLeftPixels = 48; + + /// + /// The number of border pixels to the right of the display + /// + protected int BorderRightPixels = 48; + + /// + /// The total width of the screen in pixels + /// + protected int ScreenWidth; + + /// + /// Horizontal blanking time (HSync+blanking). + /// Given in Z80 clock cycles. + /// + protected int HorizontalBlankingTime = 40; + + /// + /// The time of displaying left part of the border. + /// Given in Z80 clock cycles. + /// + protected int BorderLeftTime = 24; + + /// + /// The time of displaying a pixel row. + /// Given in Z80 clock cycles. + /// + protected int DisplayLineTime = 128; + + /// + /// The time of displaying right part of the border. + /// Given in Z80 clock cycles. + /// + protected int BorderRightTime = 24; + + /// + /// The time used to render the nonvisible right part of the border. + /// Given in Z80 clock cycles. + /// + protected int NonVisibleBorderRightTime = 8; + + /// + /// The time of displaying a full screen line. + /// Given in Z80 clock cycles. + /// + protected int ScreenLineTime; + + /// + /// The time the data of a particular pixel should be prefetched + /// before displaying it. + /// Given in Z80 clock cycles. + /// + protected int PixelDataPrefetchTime = 2; + + /// + /// The time the data of a particular pixel attribute should be prefetched + /// before displaying it. + /// Given in Z80 clock cycles. + /// + protected int AttributeDataPrefetchTime = 1; + + /// + /// The tact within the line that should display the first pixel. + /// Given in Z80 clock cycles. + /// + protected int FirstPixelCycleInLine; + + /// + /// The tact in which the top left pixel should be displayed. + /// Given in Z80 clock cycles. + /// + protected int FirstDisplayPixelCycle; + + /// + /// The tact in which the top left screen pixel (border) should be displayed + /// + protected int FirstScreenPixelCycle; + + /// + /// Defines the number of Z80 clock cycles used for the full rendering + /// of the screen. + /// + public int UlaFrameCycleCount; + + /// + /// The last rendered ULA cycle + /// + public int LastRenderedULACycle; + + + /// + /// This structure defines information related to a particular T-State + /// (cycle) of ULA screen rendering + /// + [StructLayout(LayoutKind.Explicit)] + public struct ScreenRenderingCycle + { + /// + /// Tha rendering phase to be applied for the particular tact + /// + [FieldOffset(0)] + public ScreenRenderingPhase Phase; + + /// + /// Display memory contention delay + /// + [FieldOffset(1)] + public byte ContentionDelay; + + /// + /// Display memory address used in the particular tact + /// + [FieldOffset(2)] + public ushort PixelByteToFetchAddress; + + /// + /// Display memory address used in the particular tact + /// + [FieldOffset(4)] + public ushort AttributeToFetchAddress; + + /// + /// Pixel X coordinate + /// + [FieldOffset(6)] + public ushort XPos; + + /// + /// Pixel Y coordinate + /// + [FieldOffset(8)] + public ushort YPos; + } + + /// + /// This enumeration defines the particular phases of ULA rendering + /// + public enum ScreenRenderingPhase : byte + { + /// + /// The ULA does not do any rendering + /// + None, + + /// + /// The ULA simple sets the border color to display the current pixel. + /// + Border, + + /// + /// The ULA sets the border color to display the current pixel. It + /// prepares to display the fist pixel in the row with prefetching the + /// corresponding byte from the display memory. + /// + BorderAndFetchPixelByte, + + /// + /// The ULA sets the border color to display the current pixel. It has + /// already fetched the 8 pixel bits to display. It carries on + /// preparing to display the fist pixel in the row with prefetching the + /// corresponding attribute byte from the display memory. + /// + BorderAndFetchPixelAttribute, + + /// + /// The ULA displays the next two pixels of Byte1 sequentially during a + /// single Z80 clock cycle. + /// + DisplayByte1, + + /// + /// The ULA displays the next two pixels of Byte1 sequentially during a + /// single Z80 clock cycle. It prepares to display the pixels of the next + /// byte in the row with prefetching the corresponding byte from the + /// display memory. + /// + DisplayByte1AndFetchByte2, + + /// + /// The ULA displays the next two pixels of Byte1 sequentially during a + /// single Z80 clock cycle. It prepares to display the pixels of the next + /// byte in the row with prefetching the corresponding attribute from the + /// display memory. + /// + DisplayByte1AndFetchAttribute2, + + /// + /// The ULA displays the next two pixels of Byte2 sequentially during a + /// single Z80 clock cycle. + /// + DisplayByte2, + + /// + /// The ULA displays the next two pixels of Byte2 sequentially during a + /// single Z80 clock cycle. It prepares to display the pixels of the next + /// byte in the row with prefetching the corresponding byte from the + /// display memory. + /// + DisplayByte2AndFetchByte1, + + /// + /// The ULA displays the next two pixels of Byte2 sequentially during a + /// single Z80 clock cycle. It prepares to display the pixels of the next + /// byte in the row with prefetching the corresponding attribute from the + /// display memory. + /// + DisplayByte2AndFetchAttribute1 + } + + #endregion + + #region Border + + private int _borderColour; + + /// + /// Gets or sets the ULA border color + /// + public int BorderColour + { + get { return _borderColour; } + set { _borderColour = value & 0x07; } + } + + protected virtual void ResetBorder() + { + BorderColour = 0; + } + + #endregion + + #region Screen Methods + + /// + /// ULA renders the screen between two specified T-States (cycles) + /// + /// + /// + public void RenderScreen(int fromCycle, int toCycle) + { + // Adjust cycle boundaries + fromCycle = fromCycle % UlaFrameCycleCount; + toCycle = toCycle % UlaFrameCycleCount; + + // Do rendering action for cycles based on the rendering phase + for (int curr = fromCycle; curr <= toCycle; curr++) + { + var ulaCycle = _renderingCycleTable[curr]; + _xPos = ulaCycle.XPos; + _yPos = ulaCycle.YPos; + + switch (ulaCycle.Phase) + { + case ScreenRenderingPhase.None: + // --- Invisible screen area, nothing to do + break; + + case ScreenRenderingPhase.Border: + // --- Fetch the border color from ULA and set the corresponding border pixels + SetPixels(BorderColour, BorderColour); + break; + + case ScreenRenderingPhase.BorderAndFetchPixelByte: + // --- Fetch the border color from ULA and set the corresponding border pixels + SetPixels(BorderColour, BorderColour); + // --- Obtain the future pixel byte + _pixelByte1 = FetchScreenMemory(ulaCycle.PixelByteToFetchAddress); + break; + + case ScreenRenderingPhase.BorderAndFetchPixelAttribute: + // --- Fetch the border color from ULA and set the corresponding border pixels + SetPixels(BorderColour, BorderColour); + // --- Obtain the future attribute byte + _attrByte1 = FetchScreenMemory(ulaCycle.AttributeToFetchAddress); + break; + + case ScreenRenderingPhase.DisplayByte1: + // --- Display bit 7 and 6 according to the corresponding color + SetPixels( + GetColor(_pixelByte1 & 0x80, _attrByte1), + GetColor(_pixelByte1 & 0x40, _attrByte1)); + // --- Shift in the subsequent bits + _pixelByte1 <<= 2; + break; + + case ScreenRenderingPhase.DisplayByte1AndFetchByte2: + // --- Display bit 7 and 6 according to the corresponding color + SetPixels( + GetColor(_pixelByte1 & 0x80, _attrByte1), + GetColor(_pixelByte1 & 0x40, _attrByte1)); + // --- Shift in the subsequent bits + _pixelByte1 <<= 2; + // --- Obtain the next pixel byte + _pixelByte2 = FetchScreenMemory(ulaCycle.PixelByteToFetchAddress); + break; + + case ScreenRenderingPhase.DisplayByte1AndFetchAttribute2: + // --- Display bit 7 and 6 according to the corresponding color + SetPixels( + GetColor(_pixelByte1 & 0x80, _attrByte1), + GetColor(_pixelByte1 & 0x40, _attrByte1)); + // --- Shift in the subsequent bits + _pixelByte1 <<= 2; + // --- Obtain the next attribute + _attrByte2 = FetchScreenMemory(ulaCycle.AttributeToFetchAddress); + break; + + case ScreenRenderingPhase.DisplayByte2: + // --- Display bit 7 and 6 according to the corresponding color + SetPixels( + GetColor(_pixelByte2 & 0x80, _attrByte2), + GetColor(_pixelByte2 & 0x40, _attrByte2)); + // --- Shift in the subsequent bits + _pixelByte2 <<= 2; + break; + + case ScreenRenderingPhase.DisplayByte2AndFetchByte1: + // --- Display bit 7 and 6 according to the corresponding color + SetPixels( + GetColor(_pixelByte2 & 0x80, _attrByte2), + GetColor(_pixelByte2 & 0x40, _attrByte2)); + // --- Shift in the subsequent bits + _pixelByte2 <<= 2; + // --- Obtain the next pixel byte + _pixelByte1 = FetchScreenMemory(ulaCycle.PixelByteToFetchAddress); + break; + + case ScreenRenderingPhase.DisplayByte2AndFetchAttribute1: + // --- Display bit 7 and 6 according to the corresponding color + SetPixels( + GetColor(_pixelByte2 & 0x80, _attrByte2), + GetColor(_pixelByte2 & 0x40, _attrByte2)); + // --- Shift in the subsequent bits + _pixelByte2 <<= 2; + // --- Obtain the next attribute + _attrByte1 = FetchScreenMemory(ulaCycle.AttributeToFetchAddress); + break; + } + } + } + + /// + /// Tests whether the specified cycle is in the visible area of the screen. + /// + /// Line index + /// Tacts index within the line + /// + /// True, if the tact is visible on the screen; otherwise, false + /// + public virtual bool IsCycleVisible(int line, int cycleInLine) + { + var firstVisibleLine = VerticalSyncLines + NonVisibleBorderTopLines; + var lastVisibleLine = firstVisibleLine + BorderTopLines + DisplayLines + BorderBottomLines; + return + line >= firstVisibleLine + && line < lastVisibleLine + && cycleInLine >= HorizontalBlankingTime + && cycleInLine < ScreenLineTime - NonVisibleBorderRightTime; + } + + /// + /// Tests whether the cycle is in the display area of the screen. + /// + /// Line index + /// Tacts index within the line + /// + /// True, if the tact is within the display area of the screen; otherwise, false. + /// + public virtual bool IsCycleInDisplayArea(int line, int cycleInLine) + { + return line >= FirstDisplayLine + && line <= LastDisplayLine + && cycleInLine >= FirstPixelCycleInLine + && cycleInLine < FirstPixelCycleInLine + DisplayLineTime; + } + + /// + /// Sets the two adjacent screen pixels belonging to the specified cycle to the given + /// color + /// + /// Color index of the first pixel + /// Color index of the second pixel + protected virtual void SetPixels(int colorIndex1, int colorIndex2) + { + var pos = _yPos * ScreenWidth + _xPos; + _frameBuffer[pos++] = (byte)colorIndex1; + _frameBuffer[pos] = (byte)colorIndex2; + } + + /// + /// Gets the color index for the specified pixel value according + /// to the given color attribute + /// + /// 0 for paper pixel, non-zero for ink pixel + /// Color attribute + /// + protected virtual int GetColor(int pixelValue, byte attr) + { + var offset = (pixelValue == 0 ? 0 : 0x100) + attr; + return _flashPhase + ? _flashOnColors[offset] + : _flashOffColors[offset]; + } + + /// + /// Resets the ULA cycle to start screen rendering from the beginning + /// + protected virtual void ResetULACycle() + { + LastRenderedULACycle = -1; + } + + /// + /// Initializes the ULA cycle table + /// + protected virtual void InitULACycleTable() + { + _renderingCycleTable = new ScreenRenderingCycle[UlaFrameCycleCount]; + + // loop through every cycle + for (var cycle = 0; cycle < UlaFrameCycleCount; cycle++) + { + var line = cycle / ScreenLineTime; + var cycleInLine = cycle % ScreenLineTime; + + var cycleItem = new ScreenRenderingCycle + { + Phase = ScreenRenderingPhase.None, + ContentionDelay = 0 + }; + + if (IsCycleVisible(line, cycleInLine)) + { + // calculate pixel positions + cycleItem.XPos = (ushort)((cycleInLine - HorizontalBlankingTime) * 2); + cycleItem.YPos = (ushort)(line - VerticalSyncLines - NonVisibleBorderTopLines); + + if (!IsCycleInDisplayArea(line, cycleInLine)) + { + // we are in the border + cycleItem.Phase = ScreenRenderingPhase.Border; + // set the border colour + if (line >= FirstDisplayLine && + line <= LastDisplayLine) + { + if (cycleInLine == FirstPixelCycleInLine - PixelDataPrefetchTime) + { + // left or right border beside the display area + // fetch the first pixel data byte of the current line (2 cycles away) + cycleItem.Phase = ScreenRenderingPhase.BorderAndFetchPixelByte; + cycleItem.PixelByteToFetchAddress = CalculatePixelByteAddress(line, cycleInLine + 2); + cycleItem.ContentionDelay = 6; + } + else if (cycleInLine == FirstPixelCycleInLine - AttributeDataPrefetchTime) + { + // fetch the first attribute data byte of the current line (1 cycle away) + cycleItem.Phase = ScreenRenderingPhase.BorderAndFetchPixelAttribute; + cycleItem.AttributeToFetchAddress = CalculateAttributeAddress(line, cycleInLine + 1); + cycleItem.ContentionDelay = 5; + } + } + } + else + { + var pixelCycle = cycleInLine - FirstPixelCycleInLine; + // the ULA will perform a different action based on the current cycle (T-State) + switch (pixelCycle & 7) + { + case 0: + // Display the current cycle pixels + cycleItem.Phase = ScreenRenderingPhase.DisplayByte1; + cycleItem.ContentionDelay = 4; + break; + case 1: + // Display the current cycle pixels + cycleItem.Phase = ScreenRenderingPhase.DisplayByte1; + cycleItem.ContentionDelay = 3; + break; + case 2: + // While displaying the current cycle pixels, we need to prefetch the + // pixel data byte 2 cycles away + cycleItem.Phase = ScreenRenderingPhase.DisplayByte1AndFetchByte2; + cycleItem.PixelByteToFetchAddress = CalculatePixelByteAddress(line, cycleInLine + 2); + cycleItem.ContentionDelay = 2; + break; + case 3: + // While displaying the current cycle pixels, we need to prefetch the + // attribute data byte 1 cycle away + cycleItem.Phase = ScreenRenderingPhase.DisplayByte1AndFetchAttribute2; + cycleItem.AttributeToFetchAddress = CalculateAttributeAddress(line, cycleInLine + 1); + cycleItem.ContentionDelay = 1; + break; + case 4: + case 5: + // Display the current cycle pixels + cycleItem.Phase = ScreenRenderingPhase.DisplayByte2; + break; + case 6: + if (cycleInLine < FirstPixelCycleInLine + DisplayLineTime - 2) + { + // There are still more bytes to display in this line. + // While displaying the current cycle pixels, we need to prefetch the + // pixel data byte 2 cycles away + cycleItem.Phase = ScreenRenderingPhase.DisplayByte2AndFetchByte1; + cycleItem.PixelByteToFetchAddress = CalculatePixelByteAddress(line, cycleInLine + 2); + cycleItem.ContentionDelay = 6; + } + else + { + // Last byte in this line. + // Display the current cycle pixels + cycleItem.Phase = ScreenRenderingPhase.DisplayByte2; + } + break; + case 7: + if (cycleInLine < FirstPixelCycleInLine + DisplayLineTime - 1) + { + // There are still more bytes to display in this line. + // While displaying the current cycle pixels, we need to prefetch the + // attribute data byte 1 cycle away + cycleItem.Phase = ScreenRenderingPhase.DisplayByte2AndFetchAttribute1; + cycleItem.AttributeToFetchAddress = CalculateAttributeAddress(line, cycleInLine + 1); + cycleItem.ContentionDelay = 5; + } + else + { + // Last byte in this line. + // Display the current cycle pixels + cycleItem.Phase = ScreenRenderingPhase.DisplayByte2; + } + break; + } + } + } + + // Store the calulated cycle item + _renderingCycleTable[cycle] = cycleItem; + } + } + + /// + /// Calculates the pixel address for the specified line and tact within + /// the line + /// + /// Line index + /// Tacts index within the line + /// ZX spectrum screen memory address + /// + /// Memory address bits: + /// C0-C2: pixel count within a byte -- not used in address calculation + /// C3-C7: pixel byte within a line + /// V0-V7: pixel line address + /// + /// Direct Pixel Address (da) + /// ================================================================= + /// |A15|A14|A13|A12|A11|A10|A9 |A8 |A7 |A6 |A5 |A4 |A3 |A2 |A1 |A0 | + /// ================================================================= + /// | 0 | 0 | 0 |V7 |V6 |V5 |V4 |V3 |V2 |V1 |V0 |C7 |C6 |C5 |C4 |C3 | + /// ================================================================= + /// | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 0xF81F + /// ================================================================= + /// | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0x0700 + /// ================================================================= + /// | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0x00E0 + /// ================================================================= + /// + /// Spectrum Pixel Address + /// ================================================================= + /// |A15|A14|A13|A12|A11|A10|A9 |A8 |A7 |A6 |A5 |A4 |A3 |A2 |A1 |A0 | + /// ================================================================= + /// | 0 | 0 | 0 |V7 |V6 |V2 |V1 |V0 |V5 |V4 |V3 |C7 |C6 |C5 |C4 |C3 | + /// ================================================================= + /// + protected virtual ushort CalculatePixelByteAddress(int line, int cycleInLine) + { + var row = line - FirstDisplayLine; + var column = 2 * (cycleInLine - (HorizontalBlankingTime + BorderLeftTime)); + var da = 0x4000 | (column >> 3) | (row << 5); + return (ushort)((da & 0xF81F) // --- Reset V5, V4, V3, V2, V1 + | ((da & 0x0700) >> 3) // --- Keep V5, V4, V3 only + | ((da & 0x00E0) << 3)); // --- Exchange the V2, V1, V0 bit + // --- group with V5, V4, V3 + } + + /// + /// Calculates the pixel attribute address for the specified line and + /// tact within the line + /// + /// Line index + /// Tacts index within the line + /// ZX spectrum screen memory address + /// + /// Memory address bits: + /// C0-C2: pixel count within a byte -- not used in address calculation + /// C3-C7: pixel byte within a line + /// V0-V7: pixel line address + /// + /// Spectrum Attribute Address + /// ================================================================= + /// |A15|A14|A13|A12|A11|A10|A9 |A8 |A7 |A6 |A5 |A4 |A3 |A2 |A1 |A0 | + /// ================================================================= + /// | 0 | 1 | 0 | 1 | 1 | 0 |V7 |V6 |V5 |V4 |V3 |C7 |C6 |C5 |C4 |C3 | + /// ================================================================= + /// + protected virtual ushort CalculateAttributeAddress(int line, int cycleInLine) + { + var row = line - FirstDisplayLine; + var column = 2 * (cycleInLine - (HorizontalBlankingTime + BorderLeftTime)); + var da = (column >> 3) | ((row >> 3) << 5); + return (ushort)(0x5800 + da); + } + + #endregion + + #region Initialisation + + /// + /// Initialises the screen configuration calculations + /// + protected virtual void InitScreenConfig() + { + ScreenLines = BorderTopLines + DisplayLines + BorderBottomLines; + FirstDisplayLine = VerticalSyncLines + NonVisibleBorderTopLines + BorderTopLines; + LastDisplayLine = FirstDisplayLine + DisplayLines - 1; + ScreenWidth = BorderLeftPixels + DisplayWidth + BorderRightPixels; + FirstPixelCycleInLine = HorizontalBlankingTime + BorderLeftTime; + ScreenLineTime = FirstPixelCycleInLine + DisplayLineTime + BorderRightTime + NonVisibleBorderRightTime; + UlaFrameCycleCount = (FirstDisplayLine + DisplayLines + BorderBottomLines + NonVisibleBorderTopLines) * ScreenLineTime; + FirstScreenPixelCycle = (VerticalSyncLines + NonVisibleBorderTopLines) * ScreenLineTime + HorizontalBlankingTime; + } + + /// + /// Inits the screen + /// + protected virtual void InitScreen() + { + //BorderDevice.Reset(); + _flashPhase = false; + + _frameBuffer = new byte[ScreenWidth * ScreenLines]; + + InitULACycleTable(); + + // --- Calculate color conversion table + _flashOffColors = new int[0x200]; + _flashOnColors = new int[0x200]; + + for (var attr = 0; attr < 0x100; attr++) + { + var ink = (attr & 0x07) | ((attr & 0x40) >> 3); + var paper = ((attr & 0x38) >> 3) | ((attr & 0x40) >> 3); + _flashOffColors[attr] = paper; + _flashOffColors[0x100 + attr] = ink; + _flashOnColors[attr] = (attr & 0x80) != 0 ? ink : paper; + _flashOnColors[0x100 + attr] = (attr & 0x80) != 0 ? paper : ink; + } + + FrameCount = 0; + } + + #endregion + + #region VBLANK Interrupt + + /// + /// The longest instruction cycle count + /// + protected const int LONGEST_OP_CYCLES = 23; + + /// + /// The ULA cycle to raise the interrupt at + /// + protected int InterruptCycle = 32; + + /// + /// Signs that an interrupt has been raised in this frame. + /// + protected bool InterruptRaised; + + /// + /// Signs that the interrupt signal has been revoked + /// + protected bool InterruptRevoked; + + /// + /// Resets the interrupt - this should happen every frame in order to raise + /// the VBLANK interrupt in the proceding frame + /// + public virtual void ResetInterrupt() + { + InterruptRaised = false; + InterruptRevoked = false; + } + + /// + /// Generates an interrupt in the current phase if needed + /// + /// + protected virtual void CheckForInterrupt(int currentCycle) + { + if (InterruptRevoked) + { + // interrupt has already been handled + return; + } + + if (currentCycle < InterruptCycle) + { + // interrupt does not need to be raised yet + return; + } + + if (currentCycle > InterruptCycle + LONGEST_OP_CYCLES) + { + // interrupt should have already been raised and the cpu may or + // may not have caught it. The time has passed so revoke the signal + InterruptRevoked = true; + //CPU.IFF1 = true; + CPU.FlagI = false; + //CPU.NonMaskableInterruptPending = true; + } + + if (InterruptRaised) + { + // INT is raised but not yet revoked + // CPU has NOT handled it yet + return; + } + + // if CPU is masking the interrupt do not raise it + //if (!CPU.IFF1) + //return; + + // Raise the interrupt + InterruptRaised = true; + //CPU.IFF1 = false; + //CPU.IFF2 = false; + CPU.FlagI = true; + FrameCount++; + } + + #endregion + + #region IVideoProvider + + public int VirtualWidth => ScreenWidth; + public int VirtualHeight => ScreenLines; + public int BufferWidth => ScreenWidth; + public int BufferHeight => ScreenLines; + public int BackgroundColor => ULAPalette[BorderColour]; + + public int VsyncNumerator + { + get { return 3500000; } + } + + public int VsyncDenominator + { + get { return UlaFrameCycleCount; } + } + /* + public int VsyncNumerator => NullVideo.DefaultVsyncNum; + public int VsyncDenominator => NullVideo.DefaultVsyncDen; + */ + public int[] GetVideoBuffer() + { + // convert the generated _framebuffer into ARGB colours via the ULAPalette + int[] trans = new int[_frameBuffer.Length]; + for (int i = 0; i < _frameBuffer.Length; i++) + trans[i] = ULAPalette[_frameBuffer[i]]; + return trans; //_frameBuffer; + } + + #endregion + + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Sound.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Sound.cs new file mode 100644 index 0000000000..994e32df80 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Sound.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// The abstract class that all emulated models will inherit from + /// * Sound * + /// + public abstract partial class SpectrumBase + { + + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.cs new file mode 100644 index 0000000000..6183843006 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.cs @@ -0,0 +1,228 @@ +using BizHawk.Common; +using BizHawk.Emulation.Common; +using BizHawk.Emulation.Cores.Components.Z80A; +using System; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// The abstract class that all emulated models will inherit from + /// * Main properties / fields / contruction* + /// + public abstract partial class SpectrumBase + { + /// + /// The calling ZXSpectrum class (piped in via constructor) + /// + public ZXSpectrum Spectrum { get; set; } + + /// + /// Reference to the instantiated Z80 cpu (piped in via constructor) + /// + public Z80A CPU { get; set; } + + /// + /// ROM and extended info + /// + public RomData RomData { get; set; } + + /// + /// The spectrum buzzer/beeper + /// + public Buzzer BuzzerDevice { get; set; } + + /// + /// The spectrum keyboard + /// + public virtual IKeyboard KeyboardDevice { get; set; } + + /// + /// The spectrum datacorder device + /// + public virtual Tape TapeDevice { get; set; } + + /// + /// The tape provider + /// + public virtual ITapeProvider TapeProvider { get; set; } + + /// + /// Signs whether the frame has ended + /// + public bool FrameCompleted; + + /// + /// Overflow from the previous frame (in Z80 cycles) + /// + public int OverFlow; + + /// + /// The total number of frames rendered + /// + public int FrameCount; + + /// + /// The current cycle (T-State) that we are at in the frame + /// + public int _frameCycles; + + /// + /// Stores where we are in the frame after each CPU cycle + /// + public int LastFrameStartCPUTick; + + /// + /// Gets the current frame cycle according to the CPU tick count + /// + public virtual int CurrentFrameCycle => CPU.TotalExecutedCycles - LastFrameStartCPUTick; + + /// + /// Mask constants + /// + protected const int BORDER_BIT = 0x07; + protected const int EAR_BIT = 0x10; + protected const int MIC_BIT = 0x08; + protected const int TAPE_BIT = 0x40; + + /// + /// Executes a single frame + /// + public virtual void ExecuteFrame() + { + FrameCompleted = false; + BuzzerDevice.StartFrame(); + + PollInput(); + + while (CurrentFrameCycle <= UlaFrameCycleCount) + { + // check for interrupt + CheckForInterrupt(CurrentFrameCycle); + + // run a single CPU instruction + CPU.ExecuteOne(); + + // run a rendering cycle according to the current CPU cycle count + var lastCycle = CurrentFrameCycle; + RenderScreen(LastRenderedULACycle + 1, lastCycle); + LastRenderedULACycle = lastCycle; + + } + + // we have reached the end of a frame + LastFrameStartCPUTick = CPU.TotalExecutedCycles - OverFlow; + LastRenderedULACycle = OverFlow; + + BuzzerDevice.EndFrame(); + + TapeDevice.CPUFrameCompleted(); + + FrameCount++; + + // setup for next frame + OverFlow = CurrentFrameCycle % UlaFrameCycleCount; + ResetInterrupt(); + FrameCompleted = true; + + if (FrameCount % FlashToggleFrames == 0) + { + _flashPhase = !_flashPhase; + } + + RenderScreen(0, OverFlow); + } + + /// + /// Hard reset of the emulated machine + /// + public virtual void HardReset() + { + ResetBorder(); + ResetInterrupt(); + } + + /// + /// Soft reset of the emulated machine + /// + public virtual void SoftReset() + { + ResetBorder(); + ResetInterrupt(); + } + + public void SyncState(Serializer ser) + { + ser.BeginSection("ZXMachine"); + ser.Sync("FrameCompleted", ref FrameCompleted); + ser.Sync("OverFlow", ref OverFlow); + ser.Sync("FrameCount", ref FrameCount); + ser.Sync("_frameCycles", ref _frameCycles); + ser.Sync("LastFrameStartCPUTick", ref LastFrameStartCPUTick); + ser.Sync("LastULAOutByte", ref LastULAOutByte); + ser.Sync("_flashPhase", ref _flashPhase); + ser.Sync("_frameBuffer", ref _frameBuffer, false); + ser.Sync("_flashOffColors", ref _flashOffColors, false); + ser.Sync("_flashOnColors", ref _flashOnColors, false); + ser.Sync("InterruptCycle", ref InterruptCycle); + ser.Sync("InterruptRaised", ref InterruptRaised); + ser.Sync("InterruptRevoked", ref InterruptRevoked); + ser.Sync("UlaFrameCycleCount", ref UlaFrameCycleCount); + ser.Sync("FirstScreenPixelCycle", ref FirstScreenPixelCycle); + ser.Sync("FirstDisplayPixelCycle", ref FirstDisplayPixelCycle); + ser.Sync("FirstPixelCycleInLine", ref FirstPixelCycleInLine); + ser.Sync("AttributeDataPrefetchTime", ref AttributeDataPrefetchTime); + ser.Sync("PixelDataPrefetchTime", ref PixelDataPrefetchTime); + ser.Sync("ScreenLineTime", ref ScreenLineTime); + ser.Sync("NonVisibleBorderRightTime", ref NonVisibleBorderRightTime); + ser.Sync("BorderRightTime", ref BorderRightTime); + ser.Sync("DisplayLineTime", ref DisplayLineTime); + ser.Sync("BorderLeftTime", ref BorderLeftTime); + ser.Sync("HorizontalBlankingTime", ref HorizontalBlankingTime); + ser.Sync("ScreenWidth", ref ScreenWidth); + ser.Sync("BorderRightPixels", ref BorderRightPixels); + ser.Sync("BorderLeftPixels", ref BorderLeftPixels); + ser.Sync("FirstDisplayLine", ref FirstDisplayLine); + ser.Sync("ScreenLines", ref ScreenLines); + ser.Sync("NonVisibleBorderBottomLines", ref NonVisibleBorderBottomLines); + ser.Sync("BorderBottomLines", ref BorderBottomLines); + ser.Sync("BorderTopLines", ref BorderTopLines); + ser.Sync("NonVisibleBorderTopLines", ref NonVisibleBorderTopLines); + ser.Sync("VerticalSyncLines", ref VerticalSyncLines); + ser.Sync("FlashToggleFrames", ref FlashToggleFrames); + ser.Sync("DisplayLines", ref DisplayLines); + ser.Sync("DisplayWidth", ref DisplayWidth); + ser.Sync("_pixelByte1", ref _pixelByte1); + ser.Sync("_pixelByte2", ref _pixelByte2); + ser.Sync("_attrByte1", ref _attrByte1); + ser.Sync("_attrByte2", ref _attrByte2); + ser.Sync("_xPos", ref _xPos); + ser.Sync("_yPos", ref _yPos); + ser.Sync("DisplayWidth", ref DisplayWidth); + ser.Sync("DisplayWidth", ref DisplayWidth); + ser.Sync("DisplayWidth", ref DisplayWidth); + ser.Sync("DisplayWidth", ref DisplayWidth); + ser.Sync("_borderColour", ref _borderColour); + ser.Sync("ROM0", ref ROM0, false); + ser.Sync("ROM1", ref ROM1, false); + ser.Sync("ROM2", ref ROM2, false); + ser.Sync("ROM3", ref ROM3, false); + ser.Sync("RAM0", ref RAM0, false); + ser.Sync("RAM1", ref RAM1, false); + ser.Sync("RAM2", ref RAM2, false); + ser.Sync("RAM3", ref RAM3, false); + ser.Sync("RAM4", ref RAM4, false); + ser.Sync("RAM5", ref RAM5, false); + ser.Sync("RAM6", ref RAM6, false); + ser.Sync("RAM7", ref RAM7, false); + + RomData.SyncState(ser); + KeyboardDevice.SyncState(ser); + BuzzerDevice.SyncState(ser); + TapeDevice.SyncState(ser); + + ser.EndSection(); + + ReInitMemory(); + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum48K/ZX48.Keyboard.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum48K/ZX48.Keyboard.cs new file mode 100644 index 0000000000..501dd6186b --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum48K/ZX48.Keyboard.cs @@ -0,0 +1,109 @@ +using BizHawk.Common; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// The 48k keyboard device + /// + public class Keyboard48 : IKeyboard + { + public SpectrumBase _machine { get; set; } + private byte[] LineStatus; + public bool Issue2 { get; set; } + private string[] _keyboardMatrix; + + public string[] KeyboardMatrix + { + get { return _keyboardMatrix; } + set { _keyboardMatrix = value; } + } + + public Keyboard48(SpectrumBase machine) + { + _machine = machine; + + KeyboardMatrix = new string[] + { + // 0xfefe - 0 - 4 + "Key Caps Shift", "Key Z", "Key X", "Key C", "Key V", + // 0xfdfe - 5 - 9 + "Key A", "Key S", "Key D", "Key F", "Key G", + // 0xfbfe - 10 - 14 + "Key Q", "Key W", "Key E", "Key R", "Key T", + // 0xf7fe - 15 - 19 + "Key 1", "Key 2", "Key 3", "Key 4", "Key 5", + // 0xeffe - 20 - 24 + "Key 0", "Key 9", "Key 8", "Key 7", "Key 6", + // 0xdffe - 25 - 29 + "Key P", "Key O", "Key I", "Key U", "Key Y", + // 0xbffe - 30 - 34 + "Key Return", "Key L", "Key K", "Key J", "Key H", + // 0x7ffe - 35 - 39 + "Key Space", "Key Sym Shift", "Key M", "Key N", "Key B" + }; + + LineStatus = new byte[8]; + } + + public void SetKeyStatus(string key, bool isPressed) + { + byte keyByte = GetByteFromKeyMatrix(key); + var lineIndex = keyByte / 5; + var lineMask = 1 << keyByte % 5; + + LineStatus[lineIndex] = isPressed ? (byte)(LineStatus[lineIndex] | lineMask) + : (byte)(LineStatus[lineIndex] & ~lineMask); + } + + public bool GetKeyStatus(string key) + { + byte keyByte = GetByteFromKeyMatrix(key); + var lineIndex = keyByte / 5; + var lineMask = 1 << keyByte % 5; + return (LineStatus[lineIndex] & lineMask) != 0; + } + + public byte GetLineStatus(byte lines) + { + lock(this) + { + byte status = 0; + lines = (byte)~lines; + var lineIndex = 0; + while (lines > 0) + { + if ((lines & 0x01) != 0) + status |= LineStatus[lineIndex]; + lineIndex++; + lines >>= 1; + } + var result = (byte)~status; + + return result; + } + } + + public byte ReadKeyboardByte(ushort addr) + { + return GetLineStatus((byte)(addr >> 8)); + } + + public byte GetByteFromKeyMatrix(string key) + { + int index = Array.IndexOf(KeyboardMatrix, key); + return (byte)index; + } + + public void SyncState(Serializer ser) + { + ser.BeginSection("Keyboard"); + ser.Sync("LineStatus", ref LineStatus, false); + ser.EndSection(); + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum48K/ZX48.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum48K/ZX48.cs new file mode 100644 index 0000000000..ffbc38c13c --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum48K/ZX48.cs @@ -0,0 +1,186 @@ +using BizHawk.Emulation.Cores.Components.Z80A; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public class ZX48 : SpectrumBase + { + #region Construction + + /// + /// Main constructor + /// + /// + /// + public ZX48(ZXSpectrum spectrum, Z80A cpu, byte[] file) + { + Spectrum = spectrum; + CPU = cpu; + + // init addressable memory from ROM and RAM banks + /* + Memory.Add(0, ROM0); + Memory.Add(1, RAM0); + Memory.Add(2, RAM1); + Memory.Add(3, RAM2); + */ + ReInitMemory(); + + //RAM = new byte[0x4000 + 0xC000]; + + InitScreenConfig(); + InitScreen(); + + ResetULACycle(); + + BuzzerDevice = new Buzzer(this); + BuzzerDevice.Init(44100, UlaFrameCycleCount); + + KeyboardDevice = new Keyboard48(this); + + TapeProvider = new DefaultTapeProvider(file); + + TapeDevice = new Tape(TapeProvider); + TapeDevice.Init(this); + } + + #endregion + + #region MemoryMapping + + /* 48K Spectrum has NO memory paging + * + * 0xffff +--------+ + | Bank 2 | + | | + | | + | | + 0xc000 +--------+ + | Bank 1 | + | | + | | + | | + 0x8000 +--------+ + | Bank 0 | + | | + | | + | screen | + 0x4000 +--------+ + | ROM 0 | + | | + | | + | | + 0x0000 +--------+ + */ + + /// + /// Simulates reading from the bus (no contention) + /// Paging should be handled here + /// + /// + /// + public override byte ReadBus(ushort addr) + { + int divisor = addr / 0x4000; + // paging logic goes here + + var bank = Memory[divisor]; + var index = addr % 0x4000; + return bank[index]; + } + + /// + /// Simulates writing to the bus (no contention) + /// Paging should be handled here + /// + /// + /// + public override void WriteBus(ushort addr, byte value) + { + int divisor = addr / 0x4000; + // paging logic goes here + + var bank = Memory[divisor]; + var index = addr % 0x4000; + bank[index] = value; + } + + /// + /// Reads a byte of data from a specified memory address + /// (with memory contention if appropriate) + /// + /// + /// + public override byte ReadMemory(ushort addr) + { + var data = ReadBus(addr); + if ((addr & 0xC000) == 0x4000) + { + // addr is in RAM not ROM - apply memory contention if neccessary + var delay = GetContentionValue(CurrentFrameCycle); + CPU.TotalExecutedCycles += delay; + } + return data; + } + + /// + /// Writes a byte of data to a specified memory address + /// (with memory contention if appropriate) + /// + /// + /// + public override void WriteMemory(ushort addr, byte value) + { + if (addr < 0x4000) + { + // Do nothing - we cannot write to ROM + return; + } + else if (addr < 0xC000) + { + // possible contended RAM + var delay = GetContentionValue(CurrentFrameCycle); + CPU.TotalExecutedCycles += delay; + } + + WriteBus(addr, value); + } + + public override void ReInitMemory() + { + if (Memory.ContainsKey(0)) + Memory[0] = ROM0; + else + Memory.Add(0, ROM0); + + if (Memory.ContainsKey(1)) + Memory[1] = RAM0; + else + Memory.Add(1, RAM0); + + if (Memory.ContainsKey(2)) + Memory[2] = RAM1; + else + Memory.Add(2, RAM1); + + if (Memory.ContainsKey(3)) + Memory[3] = RAM2; + else + Memory.Add(3, RAM2); + + if (Memory.ContainsKey(4)) + Memory[4] = RAM3; + else + Memory.Add(4, RAM3); + } + + + #endregion + + + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TAP/TapDataBlock.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TAP/TapDataBlock.cs new file mode 100644 index 0000000000..3c4858b533 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TAP/TapDataBlock.cs @@ -0,0 +1,90 @@ + +using System.IO; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// This class describes a TAP Block + /// + public sealed class TapDataBlock : + ITapeData, + ITapeDataSerialization, + ISupportsTapeBlockPlayback + { + private TapeDataBlockPlayer _player; + + /// + /// Block Data + /// + public byte[] Data { get; private set; } + + /// + /// Pause after this block (given in milliseconds) + /// + public ushort PauseAfter => 1000; + + /// + /// Reads the content of the block from the specified binary stream. + /// + /// Stream to read the block from + public void ReadFrom(BinaryReader reader) + { + var length = reader.ReadUInt16(); + Data = reader.ReadBytes(length); + } + + /// + /// Writes the content of the block to the specified binary stream. + /// + /// Stream to write the block to + public void WriteTo(BinaryWriter writer) + { + writer.Write((ushort)Data.Length); + writer.Write(Data); + } + + /// + /// The index of the currently playing byte + /// + /// This proprty is made public for test purposes + public int ByteIndex => _player.ByteIndex; + + /// + /// The mask of the currently playing bit in the current byte + /// + public byte BitMask => _player.BitMask; + + /// + /// The current playing phase + /// + public PlayPhase PlayPhase => _player.PlayPhase; + + /// + /// The tact count of the CPU when playing starts + /// + public long StartCycle => _player.StartCycle; + + /// + /// Last tact queried + /// + public long LastCycle => _player.LastCycle; + + /// + /// Initializes the player + /// + public void InitPlay(long startTact) + { + _player = new TapeDataBlockPlayer(Data, PauseAfter); + _player.InitPlay(startTact); + } + + /// + /// Gets the EAR bit value for the specified tact + /// + /// Tacts to retrieve the EAR bit + /// + /// The EAR bit value to play back + /// + public bool GetEarBit(long currentTact) => _player.GetEarBit(currentTact); + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TAP/TapPlayer.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TAP/TapPlayer.cs new file mode 100644 index 0000000000..faa7d23c02 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TAP/TapPlayer.cs @@ -0,0 +1,85 @@ + +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// This class is responsible to "play" a TAP file. + /// + public class TapPlayer : TapReader, ISupportsTapeBlockPlayback + { + private TapeBlockSetPlayer _player; + + /// + /// Signs that the player completed playing back the file + /// + public bool Eof => _player.Eof; + + /// + /// Initializes the player from the specified reader + /// + /// BinaryReader instance to get TZX file data from + public TapPlayer(BinaryReader reader) : base(reader) + { + } + + /// + /// Reads in the content of the TZX file so that it can be played + /// + /// True, if read was successful; otherwise, false + public override bool ReadContent() + { + var success = base.ReadContent(); + + var blocks = DataBlocks.Cast() + .ToList(); + _player = new TapeBlockSetPlayer(blocks); + return success; + } + + /// + /// Gets the currently playing block's index + /// + public int CurrentBlockIndex => _player.CurrentBlockIndex; + + /// + /// The current playable block + /// + public ISupportsTapeBlockPlayback CurrentBlock => _player.CurrentBlock; + + /// + /// The current playing phase + /// + public PlayPhase PlayPhase => _player.PlayPhase; + + /// + /// The tact count of the CPU when playing starts + /// + public long StartCycle => _player.StartCycle; + + /// + /// Initializes the player + /// + public void InitPlay(long startCycle) + { + _player.InitPlay(startCycle); + } + + /// + /// Gets the EAR bit value for the specified tact + /// + /// Tacts to retrieve the EAR bit + /// + /// A tuple of the EAR bit and a flag that indicates it is time to move to the next block + /// + public bool GetEarBit(long currentCycle) => _player.GetEarBit(currentCycle); + + /// + /// Moves the current block index to the next playable block + /// + /// Tacts time to start the next block + public void NextBlock(long currentCycle) => _player.NextBlock(currentCycle); + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TAP/TapReader.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TAP/TapReader.cs new file mode 100644 index 0000000000..b915daf0fc --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TAP/TapReader.cs @@ -0,0 +1,52 @@ + +using System.Collections.Generic; +using System.IO; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// This class reads a TAP file + /// + public class TapReader + { + private readonly BinaryReader _reader; + + /// + /// Data blocks of this TZX file + /// + public IList DataBlocks { get; } + + /// + /// Initializes the player from the specified reader + /// + /// + public TapReader(BinaryReader reader) + { + _reader = reader; + DataBlocks = new List(); + } + + /// + /// Reads in the content of the TZX file so that it can be played + /// + /// True, if read was successful; otherwise, false + public virtual bool ReadContent() + { + try + { + while (_reader.BaseStream.Position != _reader.BaseStream.Length) + { + var tapBlock = new TapDataBlock(); + tapBlock.ReadFrom(_reader); + DataBlocks.Add(tapBlock); + } + return true; + } + catch + { + // --- This exception is intentionally ignored + return false; + } + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/BlockAbstraction.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/BlockAbstraction.cs new file mode 100644 index 0000000000..a5145928e9 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/BlockAbstraction.cs @@ -0,0 +1,172 @@ + +using System; +using System.IO; +using System.Text; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// This class describes a TZX Block + /// + public abstract class TzxDataBlockBase : ITapeDataSerialization + { + /// + /// The ID of the block + /// + public abstract int BlockId { get; } + + /// + /// Reads the content of the block from the specified binary stream. + /// + /// Stream to read the block from + public abstract void ReadFrom(BinaryReader reader); + + /// + /// Writes the content of the block to the specified binary stream. + /// + /// Stream to write the block to + public abstract void WriteTo(BinaryWriter writer); + + /// + /// Override this method to check the content of the block + /// + public virtual bool IsValid => true; + + /// + /// Reads the specified number of words from the reader. + /// + /// Reader to obtain the input from + /// Number of words to get + /// Word array read from the input + public static ushort[] ReadWords(BinaryReader reader, int count) + { + var result = new ushort[count]; + var bytes = reader.ReadBytes(2 * count); + for (var i = 0; i < count; i++) + { + result[i] = (ushort)(bytes[i * 2] + bytes[i * 2 + 1] << 8); + } + return result; + } + + /// + /// Writes the specified array of words to the writer + /// + /// Output + /// Word array + public static void WriteWords(BinaryWriter writer, ushort[] words) + { + foreach (var word in words) + { + writer.Write(word); + } + } + + /// + /// Converts the provided bytes to an ASCII string + /// + /// Bytes to convert + /// First byte offset + /// Number of bytes + /// ASCII string representation + public static string ToAsciiString(byte[] bytes, int offset = 0, int count = -1) + { + if (count < 0) count = bytes.Length - offset; + var sb = new StringBuilder(); + for (var i = offset; i < count; i++) + { + sb.Append(Convert.ToChar(bytes[i])); + } + return sb.ToString(); + } + } + + /// + /// Base class for all TZX block type with data length of 3 bytes + /// + public abstract class Tzx3ByteDataBlockBase : TzxDataBlockBase + { + /// + /// Used bits in the last byte (other bits should be 0) + /// + /// + /// (e.g. if this is 6, then the bits used(x) in the last byte are: + /// xxxxxx00, where MSb is the leftmost bit, LSb is the rightmost bit) + /// + public byte LastByteUsedBits { get; set; } + + /// + /// Lenght of block data + /// + public byte[] DataLength { get; set; } + + /// + /// Block Data + /// + public byte[] Data { get; set; } + + /// + /// Override this method to check the content of the block + /// + public override bool IsValid => GetLength() == Data.Length; + + /// + /// Calculates data length + /// + protected int GetLength() + { + return DataLength[0] + DataLength[1] << 8 + DataLength[2] << 16; + } + } + + /// + /// This class represents a TZX data block with empty body + /// + public abstract class TzxBodylessDataBlockBase : TzxDataBlockBase + { + /// + /// Reads the content of the block from the specified binary stream. + /// + /// Stream to read the block from + public override void ReadFrom(BinaryReader reader) + { + } + + /// + /// Writes the content of the block to the specified binary stream. + /// + /// Stream to write the block to + public override void WriteTo(BinaryWriter writer) + { + } + } + + /// + /// This class represents a deprecated block + /// + public abstract class TzxDeprecatedDataBlockBase : TzxDataBlockBase + { + /// + /// Reads through the block infromation, and does not store it + /// + /// Stream to read the block from + public abstract void ReadThrough(BinaryReader reader); + + /// + /// Reads the content of the block from the specified binary stream. + /// + /// Stream to read the block from + public override void ReadFrom(BinaryReader reader) + { + } + + /// + /// Writes the content of the block to the specified binary stream. + /// + /// Stream to write the block to + public override void WriteTo(BinaryWriter writer) + { + throw new InvalidOperationException("Deprecated TZX data blocks cannot be written."); + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/DataBlocks.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/DataBlocks.cs new file mode 100644 index 0000000000..18ce828a09 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/DataBlocks.cs @@ -0,0 +1,1411 @@ + +using System.IO; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// Represents the standard speed data block in a TZX file + /// + public class TzxArchiveInfoDataBlock : Tzx3ByteDataBlockBase + { + /// + /// Length of the whole block (without these two bytes) + /// + public ushort Length { get; set; } + + /// + /// Number of text strings + /// + public byte StringCount { get; set; } + + /// + /// List of text strings + /// + public TzxText[] TextStrings { get; set; } + + /// + /// The ID of the block + /// + public override int BlockId => 0x32; + + /// + /// Reads the content of the block from the specified binary stream. + /// + /// Stream to read the block from + public override void ReadFrom(BinaryReader reader) + { + Length = reader.ReadUInt16(); + StringCount = reader.ReadByte(); + TextStrings = new TzxText[StringCount]; + for (var i = 0; i < StringCount; i++) + { + var text = new TzxText(); + text.ReadFrom(reader); + TextStrings[i] = text; + } + } + + /// + /// Writes the content of the block to the specified binary stream. + /// + /// Stream to write the block to + public override void WriteTo(BinaryWriter writer) + { + writer.Write(Length); + writer.Write(StringCount); + foreach (var text in TextStrings) + { + text.WriteTo(writer); + } + } + } + + /// + /// This block was created to support the Commodore 64 standard + /// ROM and similar tape blocks. + /// + public class TzxC64RomTypeDataBlock : TzxDeprecatedDataBlockBase + { + /// + /// The ID of the block + /// + public override int BlockId => 0x16; + + /// + /// Reads through the block infromation, and does not store it + /// + /// Stream to read the block from + public override void ReadThrough(BinaryReader reader) + { + var length = reader.ReadInt32(); + reader.ReadBytes(length - 4); + } + } + + /// + /// This block is made to support another type of encoding that is + /// commonly used by the C64. + /// + public class TzxC64TurboTapeDataBlock : TzxDeprecatedDataBlockBase + { + /// + /// The ID of the block + /// + public override int BlockId => 0x17; + + /// + /// Reads through the block infromation, and does not store it + /// + /// Stream to read the block from + public override void ReadThrough(BinaryReader reader) + { + var length = reader.ReadInt32(); + reader.ReadBytes(length - 4); + } + } + + /// + /// This block is an analogue of the CALL Subroutine statement. + /// + /// + /// It basically executes a sequence of blocks that are somewhere + /// else and then goes back to the next block. Because more than + /// one call can be normally used you can include a list of sequences + /// to be called. The 'nesting' of call blocks is also not allowed + /// for the simplicity reasons. You can, of course, use the CALL + /// blocks in the LOOP sequences and vice versa. + /// + public class TzxCallSequenceDataBlock : TzxDataBlockBase + { + /// + /// Number of group name + /// + public byte NumberOfCalls { get; set; } + + /// + /// Group name bytes + /// + public ushort[] BlockOffsets { get; set; } + + /// + /// The ID of the block + /// + public override int BlockId => 0x26; + + /// + /// Reads the content of the block from the specified binary stream. + /// + /// Stream to read the block from + public override void ReadFrom(BinaryReader reader) + { + NumberOfCalls = reader.ReadByte(); + BlockOffsets = ReadWords(reader, NumberOfCalls); + } + + /// + /// Writes the content of the block to the specified binary stream. + /// + /// Stream to write the block to + public override void WriteTo(BinaryWriter writer) + { + writer.Write(NumberOfCalls); + WriteWords(writer, BlockOffsets); + } + } + + /// + /// Represents the standard speed data block in a TZX file + /// + public class TzxCswRecordingDataBlock : TzxDataBlockBase + { + /// + /// Block length (without these four bytes) + /// + public uint BlockLength { get; set; } + + /// + /// Pause after this block + /// + public ushort PauseAfter { get; set; } + + /// + /// Sampling rate + /// + public byte[] SamplingRate { get; set; } + + /// + /// Compression type + /// + /// + /// 0x01=RLE, 0x02=Z-RLE + /// + public byte CompressionType { get; set; } + + /// + /// Number of stored pulses (after decompression, for validation purposes) + /// + public uint PulseCount { get; set; } + + /// + /// CSW data, encoded according to the CSW file format specification + /// + public byte[] Data { get; set; } + + /// + /// The ID of the block + /// + public override int BlockId => 0x18; + + /// + /// Reads the content of the block from the specified binary stream. + /// + /// Stream to read the block from + public override void ReadFrom(BinaryReader reader) + { + BlockLength = reader.ReadUInt32(); + PauseAfter = reader.ReadUInt16(); + SamplingRate = reader.ReadBytes(3); + CompressionType = reader.ReadByte(); + PulseCount = reader.ReadUInt32(); + var length = (int)BlockLength - 4 /* PauseAfter*/ - 3 /* SamplingRate */ + - 1 /* CompressionType */ - 4 /* PulseCount */; + Data = reader.ReadBytes(length); + } + + /// + /// Writes the content of the block to the specified binary stream. + /// + /// Stream to write the block to + public override void WriteTo(BinaryWriter writer) + { + writer.Write(BlockLength); + writer.Write(PauseAfter); + writer.Write(SamplingRate); + writer.Write(CompressionType); + writer.Write(PulseCount); + writer.Write(Data); + } + + /// + /// Override this method to check the content of the block + /// + public override bool IsValid => BlockLength == 4 + 3 + 1 + 4 + Data.Length; + } + + /// + /// Represents the standard speed data block in a TZX file + /// + public class TzxCustomInfoDataBlock : Tzx3ByteDataBlockBase + { + /// + /// Identification string (in ASCII) + /// + public byte[] Id { get; set; } + + /// + /// String representation of the ID + /// + public string IdText => ToAsciiString(Id); + + /// + /// Length of the custom info + /// + public uint Length { get; set; } + + /// + /// Custom information + /// + public byte[] CustomInfo { get; set; } + + /// + /// The ID of the block + /// + public override int BlockId => 0x35; + + /// + /// Reads the content of the block from the specified binary stream. + /// + /// Stream to read the block from + public override void ReadFrom(BinaryReader reader) + { + Id = reader.ReadBytes(10); + Length = reader.ReadUInt32(); + CustomInfo = reader.ReadBytes((int)Length); + } + + /// + /// Writes the content of the block to the specified binary stream. + /// + /// Stream to write the block to + public override void WriteTo(BinaryWriter writer) + { + writer.Write(Id); + writer.Write(Length); + writer.Write(CustomInfo); + } + } + + /// + /// Represents the standard speed data block in a TZX file + /// + public class TzxDirectRecordingDataBlock : Tzx3ByteDataBlockBase + { + /// + /// Number of T-states per sample (bit of data) + /// + public ushort TactsPerSample { get; set; } + + /// + /// Pause after this block + /// + public ushort PauseAfter { get; set; } + + /// + /// The ID of the block + /// + public override int BlockId => 0x15; + + /// + /// Reads the content of the block from the specified binary stream. + /// + /// Stream to read the block from + public override void ReadFrom(BinaryReader reader) + { + TactsPerSample = reader.ReadUInt16(); + PauseAfter = reader.ReadUInt16(); + LastByteUsedBits = reader.ReadByte(); + DataLength = reader.ReadBytes(3); + Data = reader.ReadBytes(GetLength()); + } + + /// + /// Writes the content of the block to the specified binary stream. + /// + /// Stream to write the block to + public override void WriteTo(BinaryWriter writer) + { + writer.Write(TactsPerSample); + writer.Write(PauseAfter); + writer.Write(LastByteUsedBits); + writer.Write(DataLength); + writer.Write(Data); + } + } + + /// + /// This is a special block that would normally be generated only by emulators. + /// + public class TzxEmulationInfoDataBlock : TzxDeprecatedDataBlockBase + { + /// + /// The ID of the block + /// + public override int BlockId => 0x34; + + /// + /// Reads through the block infromation, and does not store it + /// + /// Stream to read the block from + public override void ReadThrough(BinaryReader reader) + { + reader.ReadBytes(8); + } + } + + /// + /// Represents a generalized data block in a TZX file + /// + public class TzxGeneralizedDataBlock : TzxDataBlockBase + { + /// + /// Block length (without these four bytes) + /// + public uint BlockLength { get; set; } + + /// + /// Pause after this block + /// + public ushort PauseAfter { get; set; } + + /// + /// Total number of symbols in pilot/sync block (can be 0) + /// + public uint Totp { get; set; } + + /// + /// Maximum number of pulses per pilot/sync symbol + /// + public byte Npp { get; set; } + + /// + /// Number of pilot/sync symbols in the alphabet table (0=256) + /// + public byte Asp { get; set; } + + /// + /// Total number of symbols in data stream (can be 0) + /// + public uint Totd { get; set; } + + /// + /// Maximum number of pulses per data symbol + /// + public byte Npd { get; set; } + + /// + /// Number of data symbols in the alphabet table (0=256) + /// + public byte Asd { get; set; } + + /// + /// Pilot and sync symbols definition table + /// + /// + /// This field is present only if Totp > 0 + /// + public TzxSymDef[] PilotSymDef { get; set; } + + /// + /// Pilot and sync data stream + /// + /// + /// This field is present only if Totd > 0 + /// + public TzxPrle[] PilotStream { get; set; } + + /// + /// Data symbols definition table + /// + /// + /// This field is present only if Totp > 0 + /// + public TzxSymDef[] DataSymDef { get; set; } + + /// + /// Data stream + /// + /// + /// This field is present only if Totd > 0 + /// + public TzxPrle[] DataStream { get; set; } + + /// + /// The ID of the block + /// + public override int BlockId => 0x19; + + /// + /// Reads the content of the block from the specified binary stream. + /// + /// Stream to read the block from + public override void ReadFrom(BinaryReader reader) + { + BlockLength = reader.ReadUInt32(); + PauseAfter = reader.ReadUInt16(); + Totp = reader.ReadUInt32(); + Npp = reader.ReadByte(); + Asp = reader.ReadByte(); + Totd = reader.ReadUInt32(); + Npd = reader.ReadByte(); + Asd = reader.ReadByte(); + + PilotSymDef = new TzxSymDef[Asp]; + for (var i = 0; i < Asp; i++) + { + var symDef = new TzxSymDef(Npp); + symDef.ReadFrom(reader); + PilotSymDef[i] = symDef; + } + + PilotStream = new TzxPrle[Totp]; + for (var i = 0; i < Totp; i++) + { + PilotStream[i].Symbol = reader.ReadByte(); + PilotStream[i].Repetitions = reader.ReadUInt16(); + } + + DataSymDef = new TzxSymDef[Asd]; + for (var i = 0; i < Asd; i++) + { + var symDef = new TzxSymDef(Npd); + symDef.ReadFrom(reader); + DataSymDef[i] = symDef; + } + + DataStream = new TzxPrle[Totd]; + for (var i = 0; i < Totd; i++) + { + DataStream[i].Symbol = reader.ReadByte(); + DataStream[i].Repetitions = reader.ReadUInt16(); + } + } + + /// + /// Writes the content of the block to the specified binary stream. + /// + /// Stream to write the block to + public override void WriteTo(BinaryWriter writer) + { + writer.Write(BlockLength); + writer.Write(PauseAfter); + writer.Write(Totp); + writer.Write(Npp); + writer.Write(Asp); + writer.Write(Totd); + writer.Write(Npd); + writer.Write(Asd); + for (var i = 0; i < Asp; i++) + { + PilotSymDef[i].WriteTo(writer); + } + for (var i = 0; i < Totp; i++) + { + writer.Write(PilotStream[i].Symbol); + writer.Write(PilotStream[i].Repetitions); + } + + for (var i = 0; i < Asd; i++) + { + DataSymDef[i].WriteTo(writer); + } + + for (var i = 0; i < Totd; i++) + { + writer.Write(DataStream[i].Symbol); + writer.Write(DataStream[i].Repetitions); + } + } + } + + /// + /// This block is generated when you merge two ZX Tape files together. + /// + /// + /// It is here so that you can easily copy the files together and use + /// them. Of course, this means that resulting file would be 10 bytes + /// longer than if this block was not used. All you have to do if + /// you encounter this block ID is to skip next 9 bytes. + /// + public class TzxGlueDataBlock : TzxDataBlockBase + { + /// + /// Value: { "XTape!", 0x1A, MajorVersion, MinorVersion } + /// + /// + /// Just skip these 9 bytes and you will end up on the next ID. + /// + public byte[] Glue { get; set; } + + /// + /// The ID of the block + /// + public override int BlockId => 0x5A; + + /// + /// Reads the content of the block from the specified binary stream. + /// + /// Stream to read the block from + public override void ReadFrom(BinaryReader reader) + { + Glue = reader.ReadBytes(9); + } + + /// + /// Writes the content of the block to the specified binary stream. + /// + /// Stream to write the block to + public override void WriteTo(BinaryWriter writer) + { + writer.Write(Glue); + } + } + + /// + /// This indicates the end of a group. This block has no body. + /// + public class TzxGroupEndDataBlock : TzxBodylessDataBlockBase + { + /// + /// The ID of the block + /// + public override int BlockId => 0x22; + } + + /// + /// This block marks the start of a group of blocks which are + /// to be treated as one single (composite) block. + /// + public class TzxGroupStartDataBlock : TzxDataBlockBase + { + /// + /// Number of group name + /// + public byte Length { get; set; } + + /// + /// Group name bytes + /// + public byte[] Chars { get; set; } + + /// + /// Gets the group name + /// + public string GroupName => ToAsciiString(Chars); + + /// + /// The ID of the block + /// + public override int BlockId => 0x21; + + /// + /// Reads the content of the block from the specified binary stream. + /// + /// Stream to read the block from + public override void ReadFrom(BinaryReader reader) + { + Length = reader.ReadByte(); + Chars = reader.ReadBytes(Length); + } + + /// + /// Writes the content of the block to the specified binary stream. + /// + /// Stream to write the block to + public override void WriteTo(BinaryWriter writer) + { + writer.Write(Length); + writer.Write(Chars); + } + } + + /// + /// + /// + public class TzxHardwareInfoDataBlock : TzxDataBlockBase + { + /// + /// Number of machines and hardware types for which info is supplied + /// + public byte HwCount { get; set; } + + /// + /// List of machines and hardware + /// + public TzxHwInfo[] HwInfo { get; set; } + + /// + /// The ID of the block + /// + public override int BlockId => 0x33; + + /// + /// Reads the content of the block from the specified binary stream. + /// + /// Stream to read the block from + public override void ReadFrom(BinaryReader reader) + { + HwCount = reader.ReadByte(); + HwInfo = new TzxHwInfo[HwCount]; + for (var i = 0; i < HwCount; i++) + { + var hw = new TzxHwInfo(); + hw.ReadFrom(reader); + HwInfo[i] = hw; + } + } + + /// + /// Writes the content of the block to the specified binary stream. + /// + /// Stream to write the block to + public override void WriteTo(BinaryWriter writer) + { + writer.Write(HwCount); + foreach (var hw in HwInfo) + { + hw.WriteTo(writer); + } + } + } + + /// + /// This block will enable you to jump from one block to another within the file. + /// + /// + /// Jump 0 = 'Loop Forever' - this should never happen + /// Jump 1 = 'Go to the next block' - it is like NOP in assembler + /// Jump 2 = 'Skip one block' + /// Jump -1 = 'Go to the previous block' + /// + public class TzxJumpDataBlock : TzxDataBlockBase + { + /// + /// Relative jump value + /// + /// + /// + public short Jump { get; set; } + + /// + /// The ID of the block + /// + public override int BlockId => 0x23; + + /// + /// Reads the content of the block from the specified binary stream. + /// + /// Stream to read the block from + public override void ReadFrom(BinaryReader reader) + { + Jump = reader.ReadInt16(); + } + + /// + /// Writes the content of the block to the specified binary stream. + /// + /// Stream to write the block to + public override void WriteTo(BinaryWriter writer) + { + writer.Write(Jump); + } + } + + /// + /// It means that the utility should jump back to the start + /// of the loop if it hasn't been run for the specified number + /// of times. + /// + public class TzxLoopEndDataBlock : TzxBodylessDataBlockBase + { + /// + /// The ID of the block + /// + public override int BlockId => 0x25; + } + + /// + /// If you have a sequence of identical blocks, or of identical + /// groups of blocks, you can use this block to tell how many + /// times they should be repeated. + /// + public class TzxLoopStartDataBlock : TzxDataBlockBase + { + /// + /// Number of repetitions (greater than 1) + /// + public ushort Loops { get; set; } + + /// + /// The ID of the block + /// + public override int BlockId => 0x24; + + /// + /// Reads the content of the block from the specified binary stream. + /// + /// Stream to read the block from + public override void ReadFrom(BinaryReader reader) + { + Loops = reader.ReadUInt16(); + } + + /// + /// Writes the content of the block to the specified binary stream. + /// + /// Stream to write the block to + public override void WriteTo(BinaryWriter writer) + { + writer.Write(Loops); + } + } + + /// + /// This will enable the emulators to display a message for a given time. + /// + /// + /// This should not stop the tape and it should not make silence. If the + /// time is 0 then the emulator should wait for the user to press a key. + /// + public class TzxMessageDataBlock : TzxDataBlockBase + { + /// + /// Time (in seconds) for which the message should be displayed + /// + public byte Time { get; set; } + + /// + /// Length of the description + /// + public byte MessageLength { get; set; } + + /// + /// The description bytes + /// + public byte[] Message; + + /// + /// The string form of description + /// + public string MessageText => ToAsciiString(Message); + + /// + /// The ID of the block + /// + public override int BlockId => 0x31; + + /// + /// Reads the content of the block from the specified binary stream. + /// + /// Stream to read the block from + public override void ReadFrom(BinaryReader reader) + { + Time = reader.ReadByte(); + MessageLength = reader.ReadByte(); + Message = reader.ReadBytes(MessageLength); + } + + /// + /// Writes the content of the block to the specified binary stream. + /// + /// Stream to write the block to + public override void WriteTo(BinaryWriter writer) + { + writer.Write(Time); + writer.Write(MessageLength); + writer.Write(Message); + } + } + + /// + /// Represents the standard speed data block in a TZX file + /// + public class TzxPulseSequenceDataBlock : TzxDataBlockBase + { + /// + /// Pause after this block + /// + public byte PulseCount { get; set; } + + /// + /// Lenght of block data + /// + public ushort[] PulseLengths { get; set; } + + /// + /// The ID of the block + /// + public override int BlockId => 0x13; + + /// + /// Reads the content of the block from the specified binary stream. + /// + /// Stream to read the block from + public override void ReadFrom(BinaryReader reader) + { + PulseCount = reader.ReadByte(); + PulseLengths = ReadWords(reader, PulseCount); + } + + /// + /// Writes the content of the block to the specified binary stream. + /// + /// Stream to write the block to + public override void WriteTo(BinaryWriter writer) + { + writer.Write(PulseCount); + WriteWords(writer, PulseLengths); + } + + /// + /// Override this method to check the content of the block + /// + public override bool IsValid => PulseCount == PulseLengths.Length; + } + + /// + /// Represents the standard speed data block in a TZX file + /// + public class TzxPureDataBlock : Tzx3ByteDataBlockBase + { + /// + /// Length of the zero bit + /// + public ushort ZeroBitPulseLength { get; set; } + + /// + /// Length of the one bit + /// + public ushort OneBitPulseLength { get; set; } + + /// + /// Pause after this block + /// + public ushort PauseAfter { get; set; } + + /// + /// The ID of the block + /// + public override int BlockId => 0x14; + + /// + /// Reads the content of the block from the specified binary stream. + /// + /// Stream to read the block from + public override void ReadFrom(BinaryReader reader) + { + ZeroBitPulseLength = reader.ReadUInt16(); + OneBitPulseLength = reader.ReadUInt16(); + LastByteUsedBits = reader.ReadByte(); + PauseAfter = reader.ReadUInt16(); + DataLength = reader.ReadBytes(3); + Data = reader.ReadBytes(GetLength()); + } + + /// + /// Writes the content of the block to the specified binary stream. + /// + /// Stream to write the block to + public override void WriteTo(BinaryWriter writer) + { + writer.Write(ZeroBitPulseLength); + writer.Write(OneBitPulseLength); + writer.Write(LastByteUsedBits); + writer.Write(PauseAfter); + writer.Write(DataLength); + writer.Write(Data); + } + } + + /// + /// Represents the standard speed data block in a TZX file + /// + public class TzxPureToneDataBlock : TzxDataBlockBase + { + /// + /// Pause after this block + /// + public ushort PulseLength { get; private set; } + + /// + /// Lenght of block data + /// + public ushort PulseCount { get; private set; } + + /// + /// The ID of the block + /// + public override int BlockId => 0x12; + + /// + /// Reads the content of the block from the specified binary stream. + /// + /// Stream to read the block from + public override void ReadFrom(BinaryReader reader) + { + PulseLength = reader.ReadUInt16(); + PulseCount = reader.ReadUInt16(); + } + + /// + /// Writes the content of the block to the specified binary stream. + /// + /// Stream to write the block to + public override void WriteTo(BinaryWriter writer) + { + writer.Write(PulseLength); + writer.Write(PulseCount); + } + } + + /// + /// This block indicates the end of the Called Sequence. + /// The next block played will be the block after the last + /// CALL block + /// + public class TzxReturnFromSequenceDataBlock : TzxBodylessDataBlockBase + { + /// + /// The ID of the block + /// + public override int BlockId => 0x27; + } + + /// + /// Pause (silence) or 'Stop the Tape' block + /// + public class TzxSelectDataBlock : TzxDataBlockBase + { + /// + /// Length of the whole block (without these two bytes) + /// + public ushort Length { get; set; } + + /// + /// Number of selections + /// + public byte SelectionCount { get; set; } + + /// + /// List of selections + /// + public TzxSelect[] Selections { get; set; } + + /// + /// The ID of the block + /// + public override int BlockId => 0x28; + + /// + /// Reads the content of the block from the specified binary stream. + /// + /// Stream to read the block from + public override void ReadFrom(BinaryReader reader) + { + Length = reader.ReadUInt16(); + SelectionCount = reader.ReadByte(); + Selections = new TzxSelect[SelectionCount]; + foreach (var selection in Selections) + { + selection.ReadFrom(reader); + } + } + + /// + /// Writes the content of the block to the specified binary stream. + /// + /// Stream to write the block to + public override void WriteTo(BinaryWriter writer) + { + writer.Write(Length); + writer.Write(SelectionCount); + foreach (var selection in Selections) + { + selection.WriteTo(writer); + } + } + } + + /// + /// This block sets the current signal level to the specified value (high or low). + /// + public class TzxSetSignalLevelDataBlock : TzxDataBlockBase + { + /// + /// Length of the block without these four bytes + /// + public uint Lenght { get; } = 1; + + /// + /// Signal level (0=low, 1=high) + /// + public byte SignalLevel { get; set; } + + /// + /// The ID of the block + /// + public override int BlockId => 0x2B; + + /// + /// Reads the content of the block from the specified binary stream. + /// + /// Stream to read the block from + public override void ReadFrom(BinaryReader reader) + { + reader.ReadUInt32(); + SignalLevel = reader.ReadByte(); + } + + /// + /// Writes the content of the block to the specified binary stream. + /// + /// Stream to write the block to + public override void WriteTo(BinaryWriter writer) + { + writer.Write(Lenght); + writer.Write(SignalLevel); + } + } + + /// + /// Pause (silence) or 'Stop the Tape' block + /// + public class TzxSilenceDataBlock : TzxDataBlockBase + { + /// + /// Duration of silence + /// + /// + /// This will make a silence (low amplitude level (0)) for a given time + /// in milliseconds. If the value is 0 then the emulator or utility should + /// (in effect) STOP THE TAPE, i.e. should not continue loading until + /// the user or emulator requests it. + /// + public ushort Duration { get; set; } + + /// + /// The ID of the block + /// + public override int BlockId => 0x20; + + /// + /// Reads the content of the block from the specified binary stream. + /// + /// Stream to read the block from + public override void ReadFrom(BinaryReader reader) + { + Duration = reader.ReadUInt16(); + } + + /// + /// Writes the content of the block to the specified binary stream. + /// + /// Stream to write the block to + public override void WriteTo(BinaryWriter writer) + { + writer.Write(Duration); + } + } + + /// + /// This block was created to support the Commodore 64 standard + /// ROM and similar tape blocks. + /// + public class TzxSnapshotBlock : TzxDeprecatedDataBlockBase + { + /// + /// The ID of the block + /// + public override int BlockId => 0x40; + + /// + /// Reads through the block infromation, and does not store it + /// + /// Stream to read the block from + public override void ReadThrough(BinaryReader reader) + { + var length = reader.ReadInt32(); + length = length & 0x00FFFFFF; + reader.ReadBytes(length); + } + } + + /// + /// Represents the standard speed data block in a TZX file + /// + public class TzxStandardSpeedDataBlock : TzxDataBlockBase, ISupportsTapeBlockPlayback, ITapeData + { + private TapeDataBlockPlayer _player; + + /// + /// Pause after this block (default: 1000ms) + /// + public ushort PauseAfter { get; set; } = 1000; + + /// + /// Lenght of block data + /// + public ushort DataLength { get; set; } + + /// + /// Block Data + /// + public byte[] Data { get; set; } + + /// + /// The ID of the block + /// + public override int BlockId => 0x10; + + /// + /// Reads the content of the block from the specified binary stream. + /// + /// Stream to read the block from + public override void ReadFrom(BinaryReader reader) + { + PauseAfter = reader.ReadUInt16(); + DataLength = reader.ReadUInt16(); + Data = reader.ReadBytes(DataLength); + } + + /// + /// Writes the content of the block to the specified binary stream. + /// + /// Stream to write the block to + public override void WriteTo(BinaryWriter writer) + { + writer.Write((byte)BlockId); + writer.Write(PauseAfter); + writer.Write(DataLength); + writer.Write(Data, 0, DataLength); + } + + /// + /// The index of the currently playing byte + /// + /// This proprty is made public for test purposes + public int ByteIndex => _player.ByteIndex; + + /// + /// The mask of the currently playing bit in the current byte + /// + public byte BitMask => _player.BitMask; + + /// + /// The current playing phase + /// + public PlayPhase PlayPhase => _player.PlayPhase; + + /// + /// The tact count of the CPU when playing starts + /// + public long StartCycle=> _player.StartCycle; + + /// + /// Last tact queried + /// + public long LastTact => _player.LastCycle; + + /// + /// Initializes the player + /// + public void InitPlay(long startCycle) + { + _player = new TapeDataBlockPlayer(Data, PauseAfter); + _player.InitPlay(startCycle); + } + + /// + /// Gets the EAR bit value for the specified tact + /// + /// Tacts to retrieve the EAR bit + /// + /// The EAR bit value to play back + /// + public bool GetEarBit(long currentCycle) => _player.GetEarBit(currentCycle); + } + + /// + /// When this block is encountered, the tape will stop ONLY if + /// the machine is an 48K Spectrum. + /// + /// + /// This block is to be used for multiloading games that load one + /// level at a time in 48K mode, but load the entire tape at once + /// if in 128K mode. This block has no body of its own, but follows + /// the extension rule. + /// + public class TzxStopTheTape48DataBlock : TzxDataBlockBase + { + /// + /// Length of the block without these four bytes (0) + /// + public uint Lenght { get; } = 0; + + /// + /// The ID of the block + /// + public override int BlockId => 0x2A; + + /// + /// Reads the content of the block from the specified binary stream. + /// + /// Stream to read the block from + public override void ReadFrom(BinaryReader reader) + { + reader.ReadUInt32(); + } + + /// + /// Writes the content of the block to the specified binary stream. + /// + /// Stream to write the block to + public override void WriteTo(BinaryWriter writer) + { + writer.Write(Lenght); + } + } + + /// + /// This is meant to identify parts of the tape, so you know where level 1 starts, + /// where to rewind to when the game ends, etc. + /// + /// + /// This description is not guaranteed to be shown while the tape is playing, + /// but can be read while browsing the tape or changing the tape pointer. + /// + public class TzxTextDescriptionDataBlock : TzxDataBlockBase + { + /// + /// Length of the description + /// + public byte DescriptionLength { get; set; } + + /// + /// The description bytes + /// + public byte[] Description; + + /// + /// The string form of description + /// + public string DescriptionText => ToAsciiString(Description); + + /// + /// The ID of the block + /// + public override int BlockId => 0x30; + + /// + /// Reads the content of the block from the specified binary stream. + /// + /// Stream to read the block from + public override void ReadFrom(BinaryReader reader) + { + DescriptionLength = reader.ReadByte(); + Description = reader.ReadBytes(DescriptionLength); + } + + /// + /// Writes the content of the block to the specified binary stream. + /// + /// Stream to write the block to + public override void WriteTo(BinaryWriter writer) + { + writer.Write(DescriptionLength); + writer.Write(Description); + } + } + + /// + /// Represents the standard speed data block in a TZX file + /// + public class TzxTurboSpeedDataBlock : Tzx3ByteDataBlockBase + { + /// + /// Length of pilot pulse + /// + public ushort PilotPulseLength { get; set; } + + /// + /// Length of the first sync pulse + /// + public ushort Sync1PulseLength { get; set; } + + /// + /// Length of the second sync pulse + /// + public ushort Sync2PulseLength { get; set; } + + /// + /// Length of the zero bit + /// + public ushort ZeroBitPulseLength { get; set; } + + /// + /// Length of the one bit + /// + public ushort OneBitPulseLength { get; set; } + + /// + /// Length of the pilot tone + /// + public ushort PilotToneLength { get; set; } + + /// + /// Pause after this block + /// + public ushort PauseAfter { get; set; } + + public TzxTurboSpeedDataBlock() + { + PilotPulseLength = 2168; + Sync1PulseLength = 667; + Sync2PulseLength = 735; + ZeroBitPulseLength = 855; + OneBitPulseLength = 1710; + PilotToneLength = 8063; + LastByteUsedBits = 8; + } + + /// + /// The ID of the block + /// + public override int BlockId => 0x11; + + /// + /// Reads the content of the block from the specified binary stream. + /// + /// Stream to read the block from + public override void ReadFrom(BinaryReader reader) + { + PilotPulseLength = reader.ReadUInt16(); + Sync1PulseLength = reader.ReadUInt16(); + Sync2PulseLength = reader.ReadUInt16(); + ZeroBitPulseLength = reader.ReadUInt16(); + OneBitPulseLength = reader.ReadUInt16(); + PilotToneLength = reader.ReadUInt16(); + LastByteUsedBits = reader.ReadByte(); + PauseAfter = reader.ReadUInt16(); + DataLength = reader.ReadBytes(3); + Data = reader.ReadBytes(GetLength()); + } + + /// + /// Writes the content of the block to the specified binary stream. + /// + /// Stream to write the block to + public override void WriteTo(BinaryWriter writer) + { + writer.Write(PilotPulseLength); + writer.Write(Sync1PulseLength); + writer.Write(Sync2PulseLength); + writer.Write(ZeroBitPulseLength); + writer.Write(OneBitPulseLength); + writer.Write(PilotToneLength); + writer.Write(LastByteUsedBits); + writer.Write(PauseAfter); + writer.Write(DataLength); + writer.Write(Data); + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/Info.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/Info.cs new file mode 100644 index 0000000000..0a83aa3bfc --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/Info.cs @@ -0,0 +1,250 @@ + +using System.IO; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// This blocks contains information about the hardware that the programs on this tape use. + /// + public class TzxHwInfo : ITapeDataSerialization + { + /// + /// Hardware type + /// + public byte HwType { get; set; } + + /// + /// Hardwer Id + /// + public byte HwId { get; set; } + + /// + /// Information about the tape + /// + /// + /// 00 - The tape RUNS on this machine or with this hardware, + /// but may or may not use the hardware or special features of the machine. + /// 01 - The tape USES the hardware or special features of the machine, + /// such as extra memory or a sound chip. + /// 02 - The tape RUNS but it DOESN'T use the hardware + /// or special features of the machine. + /// 03 - The tape DOESN'T RUN on this machine or with this hardware. + /// + public byte TapeInfo; + + /// + /// Reads the content of the block from the specified binary stream. + /// + /// Stream to read the block from + public void ReadFrom(BinaryReader reader) + { + HwType = reader.ReadByte(); + HwId = reader.ReadByte(); + TapeInfo = reader.ReadByte(); + } + + /// + /// Writes the content of the block to the specified binary stream. + /// + /// Stream to write the block to + public void WriteTo(BinaryWriter writer) + { + writer.Write(HwType); + writer.Write(HwId); + writer.Write(TapeInfo); + } + } + + /// + /// Symbol repetitions + /// + public struct TzxPrle + { + /// + /// Symbol represented + /// + public byte Symbol; + + /// + /// Number of repetitions + /// + public ushort Repetitions; + } + + /// + /// This block represents an extremely wide range of data encoding techniques. + /// + /// + /// The basic idea is that each loading component (pilot tone, sync pulses, data) + /// is associated to a specific sequence of pulses, where each sequence (wave) can + /// contain a different number of pulses from the others. In this way we can have + /// a situation where bit 0 is represented with 4 pulses and bit 1 with 8 pulses. + /// + public class TzxSymDef : ITapeDataSerialization + { + /// + /// Bit 0 - Bit 1: Starting symbol polarity + /// + /// + /// 00: opposite to the current level (make an edge, as usual) - default + /// 01: same as the current level(no edge - prolongs the previous pulse) + /// 10: force low level + /// 11: force high level + /// + public byte SymbolFlags; + + /// + /// The array of pulse lengths + /// + public ushort[] PulseLengths; + + public TzxSymDef(byte maxPulses) + { + PulseLengths = new ushort[maxPulses]; + } + + /// + /// Reads the content of the block from the specified binary stream. + /// + /// Stream to read the block from + public void ReadFrom(BinaryReader reader) + { + SymbolFlags = reader.ReadByte(); + PulseLengths = TzxDataBlockBase.ReadWords(reader, PulseLengths.Length); + } + + /// + /// Writes the content of the block to the specified binary stream. + /// + /// Stream to write the block to + public void WriteTo(BinaryWriter writer) + { + writer.Write(SymbolFlags); + TzxDataBlockBase.WriteWords(writer, PulseLengths); + } + } + + /// + /// This is meant to identify parts of the tape, so you know where level 1 starts, + /// where to rewind to when the game ends, etc. + /// + /// + /// This description is not guaranteed to be shown while the tape is playing, + /// but can be read while browsing the tape or changing the tape pointer. + /// + public class TzxText : ITapeDataSerialization + { + /// + /// Text identification byte. + /// + /// + /// 00 - Full title + /// 01 - Software house/publisher + /// 02 - Author(s) + /// 03 - Year of publication + /// 04 - Language + /// 05 - Game/utility type + /// 06 - Price + /// 07 - Protection scheme/loader + /// 08 - Origin + /// FF - Comment(s) + /// + public byte Type { get; set; } + + /// + /// Length of the description + /// + public byte Length { get; set; } + + /// + /// The description bytes + /// + public byte[] TextBytes; + + /// + /// The string form of description + /// + public string Text => TzxDataBlockBase.ToAsciiString(TextBytes); + + /// + /// Reads the content of the block from the specified binary stream. + /// + /// Stream to read the block from + public void ReadFrom(BinaryReader reader) + { + Type = reader.ReadByte(); + Length = reader.ReadByte(); + TextBytes = reader.ReadBytes(Length); + } + + /// + /// Writes the content of the block to the specified binary stream. + /// + /// Stream to write the block to + public void WriteTo(BinaryWriter writer) + { + writer.Write(Type); + writer.Write(Length); + writer.Write(TextBytes); + } + } + + /// + /// This block represents select structure + /// + public class TzxSelect : ITapeDataSerialization + { + /// + /// Bit 0 - Bit 1: Starting symbol polarity + /// + /// + /// 00: opposite to the current level (make an edge, as usual) - default + /// 01: same as the current level(no edge - prolongs the previous pulse) + /// 10: force low level + /// 11: force high level + /// + public ushort BlockOffset; + + /// + /// Length of the description + /// + public byte DescriptionLength { get; set; } + + /// + /// The description bytes + /// + public byte[] Description; + + /// + /// The string form of description + /// + public string DescriptionText => TzxDataBlockBase.ToAsciiString(Description); + + public TzxSelect(byte length) + { + DescriptionLength = length; + } + + /// + /// Reads the content of the block from the specified binary stream. + /// + /// Stream to read the block from + public void ReadFrom(BinaryReader reader) + { + BlockOffset = reader.ReadUInt16(); + DescriptionLength = reader.ReadByte(); + Description = reader.ReadBytes(DescriptionLength); + } + + /// + /// Writes the content of the block to the specified binary stream. + /// + /// Stream to write the block to + public void WriteTo(BinaryWriter writer) + { + writer.Write(BlockOffset); + writer.Write(DescriptionLength); + writer.Write(Description); + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/Types.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/Types.cs new file mode 100644 index 0000000000..8742a35679 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/Types.cs @@ -0,0 +1,282 @@ + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// Identified AD or DA converter types + /// + public enum TzxAdOrDaConverterType : byte + { + HarleySystemsAdc8P2 = 0x00, + BlackboardElectronics = 0x01 + } + + /// + /// Identified computer types + /// + public enum TzxComputerType : byte + { + ZxSpectrum16 = 0x00, + ZxSpectrum48OrPlus = 0x01, + ZxSpectrum48Issue1 = 0x02, + ZxSpectrum128 = 0x03, + ZxSpectrum128P2 = 0x04, + ZxSpectrum128P2AOr3 = 0x05, + Tc2048 = 0x06, + Ts2068 = 0x07, + Pentagon128 = 0x08, + SamCoupe = 0x09, + DidaktikM = 0x0A, + DidaktikGama = 0x0B, + Zx80 = 0x0C, + Zx81 = 0x0D, + ZxSpectrum128Spanish = 0x0E, + ZxSpectrumArabic = 0x0F, + Tk90X = 0x10, + Tk95 = 0x11, + Byte = 0x12, + Elwro800D3 = 0x13, + ZsScorpion256 = 0x14, + AmstradCpc464 = 0x15, + AmstradCpc664 = 0x16, + AmstradCpc6128 = 0x17, + AmstradCpc464P = 0x18, + AmstradCpc6128P = 0x19, + JupiterAce = 0x1A, + Enterprise = 0x1B, + Commodore64 = 0x1C, + Commodore128 = 0x1D, + InvesSpectrumP = 0x1E, + Profi = 0x1F, + GrandRomMax = 0x20, + Kay1024 = 0x21, + IceFelixHc91 = 0x22, + IceFelixHc2000 = 0x23, + AmaterskeRadioMistrum = 0x24, + Quorum128 = 0x25, + MicroArtAtm = 0x26, + MicroArtAtmTurbo2 = 0x27, + Chrome = 0x28, + ZxBadaloc = 0x29, + Ts1500 = 0x2A, + Lambda = 0x2B, + Tk65 = 0x2C, + Zx97 = 0x2D + } + + /// + /// Identified digitizer types + /// + public enum TzxDigitizerType : byte + { + RdDigitalTracer = 0x00, + DkTronicsLightPen = 0x01, + MicrographPad = 0x02, + RomnticRobotVideoface = 0x03 + } + + /// + /// Identified EPROM programmer types + /// + public enum TzxEpromProgrammerType : byte + { + OrmeElectronics = 0x00 + } + + /// + /// Identified external storage types + /// + public enum TzxExternalStorageType : byte + { + ZxMicroDrive = 0x00, + OpusDiscovery = 0x01, + MgtDisciple = 0x02, + MgtPlusD = 0x03, + RobotronicsWafaDrive = 0x04, + TrDosBetaDisk = 0x05, + ByteDrive = 0x06, + Watsford = 0x07, + Fiz = 0x08, + Radofin = 0x09, + DidaktikDiskDrive = 0x0A, + BsDos = 0x0B, + ZxSpectrumP3DiskDrive = 0x0C, + JloDiskInterface = 0x0D, + TimexFdd3000 = 0x0E, + ZebraDiskDrive = 0x0F, + RamexMillenia = 0x10, + Larken = 0x11, + KempstonDiskInterface = 0x12, + Sandy = 0x13, + ZxSpectrumP3EHardDisk = 0x14, + ZxAtaSp = 0x15, + DivIde = 0x16, + ZxCf = 0x17 + } + + /// + /// Identified graphics types + /// + public enum TzxGraphicsType : byte + { + WrxHiRes = 0x00, + G007 = 0x01, + Memotech = 0x02, + LambdaColour = 0x03 + } + + /// + /// Represents the hardware types that can be defined + /// + public enum TzxHwType : byte + { + Computer = 0x00, + ExternalStorage = 0x01, + RomOrRamTypeAddOn = 0x02, + SoundDevice = 0x03, + JoyStick = 0x04, + Mouse = 0x05, + OtherController = 0x06, + SerialPort = 0x07, + ParallelPort = 0x08, + Printer = 0x09, + Modem = 0x0A, + Digitizer = 0x0B, + NetworkAdapter = 0x0C, + Keyboard = 0x0D, + AdOrDaConverter = 0x0E, + EpromProgrammer = 0x0F, + Graphics = 0x10 + } + + /// + /// Identified joystick types + /// + public enum TzxJoystickType + { + Kempston = 0x00, + ProtekCursor = 0x01, + Sinclair2Left = 0x02, + Sinclair1Right = 0x03, + Fuller = 0x04 + } + + /// + /// Identified keyboard and keypad types + /// + public enum TzxKeyboardType : byte + { + KeypadForZxSpectrum128K = 0x00 + } + + /// + /// Identified modem types + /// + public enum TzxModemTypes : byte + { + PrismVtx5000 = 0x00, + Westridge2050 = 0x01 + } + + /// + /// Identified mouse types + /// + public enum TzxMouseType : byte + { + AmxMouse = 0x00, + KempstonMouse = 0x01 + } + + /// + /// Identified network adapter types + /// + public enum TzxNetworkAdapterType : byte + { + ZxInterface1 = 0x00 + } + + /// + /// Identified other controller types + /// + public enum TzxOtherControllerType : byte + { + Trisckstick = 0x00, + ZxLightGun = 0x01, + ZebraGraphicTablet = 0x02, + DefnederLightGun = 0x03 + } + + /// + /// Identified parallel port types + /// + public enum TzxParallelPortType : byte + { + KempstonS = 0x00, + KempstonE = 0x01, + ZxSpectrum3P = 0x02, + Tasman = 0x03, + DkTronics = 0x04, + Hilderbay = 0x05, + InesPrinterface = 0x06, + ZxLprintInterface3 = 0x07, + MultiPrint = 0x08, + OpusDiscovery = 0x09, + Standard8255 = 0x0A + } + + /// + /// Identified printer types + /// + public enum TzxPrinterType : byte + { + ZxPrinter = 0x00, + GenericPrinter = 0x01, + EpsonCompatible = 0x02 + } + + /// + /// Identifier ROM or RAM add-on types + /// + public enum TzxRomRamAddOnType : byte + { + SamRam = 0x00, + MultifaceOne = 0x01, + Multiface128K = 0x02, + MultifaceP3 = 0x03, + MultiPrint = 0x04, + Mb02 = 0x05, + SoftRom = 0x06, + Ram1K = 0x07, + Ram16K = 0x08, + Ram48K = 0x09, + Mem8To16KUsed = 0x0A + } + + /// + /// Identified serial port types + /// + public enum TzxSerialPortType : byte + { + ZxInterface1 = 0x00, + ZxSpectrum128 = 0x01 + } + + /// + /// Identified sound device types + /// + public enum TzxSoundDeviceType : byte + { + ClassicAy = 0x00, + FullerBox = 0x01, + CurrahMicroSpeech = 0x02, + SpectDrum = 0x03, + MelodikAyAcbStereo = 0x04, + AyAbcStereo = 0x05, + RamMusinMachine = 0x06, + Covox = 0x07, + GeneralSound = 0x08, + IntecEdiB8001 = 0x09, + ZonXAy = 0x0A, + QuickSilvaAy = 0x0B, + JupiterAce = 0x0C + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/TzxException.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/TzxException.cs new file mode 100644 index 0000000000..8ebe4921e5 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/TzxException.cs @@ -0,0 +1,28 @@ +using System; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// This class represents a TZX-related exception + /// + public class TzxException : Exception + { + /// + /// Initializes the exception with the specified message + /// + /// Exception message + public TzxException(string message) : base(message) + { + } + + /// + /// Initializes the exception with the specified message + /// and inner exception + /// + /// Exception message + /// Inner exception + public TzxException(string message, Exception innerException) : base(message, innerException) + { + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/TzxHeader.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/TzxHeader.cs new file mode 100644 index 0000000000..e6901b4d7b --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/TzxHeader.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// Represents the header of the TZX file + /// + public class TzxHeader : TzxDataBlockBase + { + public static IReadOnlyList TzxSignature = + new ReadOnlyCollection(new byte[] { 0x5A, 0x58, 0x54, 0x61, 0x70, 0x65, 0x21 }); + public byte[] Signature { get; private set; } + public byte Eot { get; private set; } + public byte MajorVersion { get; private set; } + public byte MinorVersion { get; private set; } + + public TzxHeader(byte majorVersion = 1, byte minorVersion = 20) + { + Signature = TzxSignature.ToArray(); + Eot = 0x1A; + MajorVersion = majorVersion; + MinorVersion = minorVersion; + } + + /// + /// The ID of the block + /// + public override int BlockId => 0x00; + + /// + /// Reads the content of the block from the specified binary stream. + /// + /// Stream to read the block from + public override void ReadFrom(BinaryReader reader) + { + Signature = reader.ReadBytes(7); + Eot = reader.ReadByte(); + MajorVersion = reader.ReadByte(); + MinorVersion = reader.ReadByte(); + } + + /// + /// Writes the content of the block to the specified binary stream. + /// + /// Stream to write the block to + public override void WriteTo(BinaryWriter writer) + { + writer.Write(Signature); + writer.Write(Eot); + writer.Write(MajorVersion); + writer.Write(MinorVersion); + } + + #region Overrides of TzxDataBlockBase + + /// + /// Override this method to check the content of the block + /// + public override bool IsValid => Signature.SequenceEqual(TzxSignature) + && Eot == 0x1A + && MajorVersion == 1; + + #endregion + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/TzxPlayer.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/TzxPlayer.cs new file mode 100644 index 0000000000..55797b80df --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/TzxPlayer.cs @@ -0,0 +1,83 @@ +using System.IO; +using System.Linq; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// This class is responsible to "play" a TZX file. + /// + public class TzxPlayer : TzxReader, ISupportsTapeBlockPlayback + { + private TapeBlockSetPlayer _player; + + /// + /// Signs that the player completed playing back the file + /// + public bool Eof => _player.Eof; + + /// + /// Initializes the player from the specified reader + /// + /// BinaryReader instance to get TZX file data from + public TzxPlayer(BinaryReader reader) : base(reader) + { + } + + /// + /// Reads in the content of the TZX file so that it can be played + /// + /// True, if read was successful; otherwise, false + public override bool ReadContent() + { + var success = base.ReadContent(); + var blocks = DataBlocks.Where(b => b is ISupportsTapeBlockPlayback) + .Cast() + .ToList(); + _player = new TapeBlockSetPlayer(blocks); + return success; + } + + /// + /// Gets the currently playing block's index + /// + public int CurrentBlockIndex => _player.CurrentBlockIndex; + + /// + /// The current playable block + /// + public ISupportsTapeBlockPlayback CurrentBlock => _player.CurrentBlock; + + /// + /// The current playing phase + /// + public PlayPhase PlayPhase => _player.PlayPhase; + + /// + /// The tact count of the CPU when playing starts + /// + public long StartCycle => _player.StartCycle; + + /// + /// Initializes the player + /// + public void InitPlay(long startTact) + { + _player.InitPlay(startTact); + } + + /// + /// Gets the EAR bit value for the specified tact + /// + /// Tacts to retrieve the EAR bit + /// + /// A tuple of the EAR bit and a flag that indicates it is time to move to the next block + /// + public bool GetEarBit(long currentTact) => _player.GetEarBit(currentTact); + + /// + /// Moves the current block index to the next playable block + /// + /// Tacts time to start the next block + public void NextBlock(long currentTact) => _player.NextBlock(currentTact); + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/TzxReader.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/TzxReader.cs new file mode 100644 index 0000000000..ad7ea786ab --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/TzxReader.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// This class reads a TZX file + /// + public class TzxReader + { + private readonly BinaryReader _reader; + + public static Dictionary DataBlockTypes = new Dictionary + { + {0x10, typeof(TzxStandardSpeedDataBlock)}, + {0x11, typeof(TzxTurboSpeedDataBlock)}, + {0x12, typeof(TzxPureToneDataBlock)}, + {0x13, typeof(TzxPulseSequenceDataBlock)}, + {0x14, typeof(TzxPureDataBlock)}, + {0x15, typeof(TzxDirectRecordingDataBlock)}, + {0x16, typeof(TzxC64RomTypeDataBlock)}, + {0x17, typeof(TzxC64TurboTapeDataBlock)}, + {0x18, typeof(TzxCswRecordingDataBlock)}, + {0x19, typeof(TzxGeneralizedDataBlock)}, + {0x20, typeof(TzxSilenceDataBlock)}, + {0x21, typeof(TzxGroupStartDataBlock)}, + {0x22, typeof(TzxGroupEndDataBlock)}, + {0x23, typeof(TzxJumpDataBlock)}, + {0x24, typeof(TzxLoopStartDataBlock)}, + {0x25, typeof(TzxLoopEndDataBlock)}, + {0x26, typeof(TzxCallSequenceDataBlock)}, + {0x27, typeof(TzxReturnFromSequenceDataBlock)}, + {0x28, typeof(TzxSelectDataBlock)}, + {0x2A, typeof(TzxStopTheTape48DataBlock)}, + {0x2B, typeof(TzxSetSignalLevelDataBlock)}, + {0x30, typeof(TzxTextDescriptionDataBlock)}, + {0x31, typeof(TzxMessageDataBlock)}, + {0x32, typeof(TzxArchiveInfoDataBlock)}, + {0x33, typeof(TzxHardwareInfoDataBlock)}, + {0x34, typeof(TzxEmulationInfoDataBlock)}, + {0x35, typeof(TzxCustomInfoDataBlock)}, + {0x40, typeof(TzxSnapshotBlock)}, + {0x5A, typeof(TzxGlueDataBlock)}, + }; + + /// + /// Data blocks of this TZX file + /// + public IList DataBlocks { get; } + + /// + /// Major version number of the file + /// + public byte MajorVersion { get; private set; } + + /// + /// Minor version number of the file + /// + public byte MinorVersion { get; private set; } + + /// + /// Initializes the player from the specified reader + /// + /// + public TzxReader(BinaryReader reader) + { + _reader = reader; + DataBlocks = new List(); + } + + /// + /// Reads in the content of the TZX file so that it can be played + /// + /// True, if read was successful; otherwise, false + public virtual bool ReadContent() + { + var header = new TzxHeader(); + try + { + header.ReadFrom(_reader); + if (!header.IsValid) + { + throw new TzxException("Invalid TZX header"); + } + MajorVersion = header.MajorVersion; + MinorVersion = header.MinorVersion; + + while (_reader.BaseStream.Position != _reader.BaseStream.Length) + { + var blockType = _reader.ReadByte(); + Type type; + if (!DataBlockTypes.TryGetValue(blockType, out type)) + { + throw new TzxException($"Unkonwn TZX block type: {blockType}"); + } + + try + { + var block = Activator.CreateInstance(type) as TzxDataBlockBase; + if (block.GetType() == typeof(TzxDeprecatedDataBlockBase)) + { + ((TzxDeprecatedDataBlockBase)block as TzxDeprecatedDataBlockBase).ReadThrough(_reader); + } + else + { + block?.ReadFrom(_reader); + } + DataBlocks.Add(block); + } + catch (Exception ex) + { + throw new TzxException($"Cannot read TZX data block {type}.", ex); + } + } + return true; + } + catch + { + // --- This exception is intentionally ignored + return false; + } + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Pulse.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Pulse.cs new file mode 100644 index 0000000000..af14d5cea7 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Pulse.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// The MIC and EAR pins in the spectrum deal in on/off pulses of varying lengths + /// This struct therefore represents 1 of these pulses + /// + public struct Pulse + { + /// + /// True: High State + /// False: Low State + /// + public bool State { get; set; } + + /// + /// Pulse length in Z80 T-States (cycles) + /// + public int Length { get; set; } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/RomData.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/RomData.cs new file mode 100644 index 0000000000..97dffe206b --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/RomData.cs @@ -0,0 +1,91 @@ +using BizHawk.Common; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + + public class RomData + { + /// + /// ROM Contents + /// + public byte[] RomBytes + { + get { return _romBytes; } + set { _romBytes = value; } + } + + /// + /// Useful ROM addresses that are needed during tape operations + /// + public ushort SaveBytesRoutineAddress + { + get { return _saveBytesRoutineAddress; } + set { _saveBytesRoutineAddress = value; } + } + public ushort LoadBytesRoutineAddress + { + get { return _loadBytesRoutineAddress; } + set { _loadBytesRoutineAddress = value; } + } + public ushort SaveBytesResumeAddress + { + get { return _saveBytesResumeAddress; } + set { _saveBytesResumeAddress = value; } + } + public ushort LoadBytesResumeAddress + { + get { return _loadBytesResumeAddress; } + set { _loadBytesResumeAddress = value; } + } + public ushort LoadBytesInvalidHeaderAddress + { + get { return _loadBytesInvalidHeaderAddress; } + set { _loadBytesInvalidHeaderAddress = value; } + } + + private byte[] _romBytes; + private ushort _saveBytesRoutineAddress; + private ushort _loadBytesRoutineAddress; + private ushort _saveBytesResumeAddress; + private ushort _loadBytesResumeAddress; + private ushort _loadBytesInvalidHeaderAddress; + + + public static RomData InitROM(MachineType machineType, byte[] rom) + { + RomData RD = new RomData(); + RD.RomBytes = new byte[rom.Length]; + RD.RomBytes = rom; + + switch (machineType) + { + case MachineType.ZXSpectrum48: + RD.SaveBytesRoutineAddress = 0x04C2; + RD.SaveBytesResumeAddress = 0x0000; + RD.LoadBytesRoutineAddress = 0x0808; //0x0556; //0x056C; + RD.LoadBytesResumeAddress = 0x05E2; + RD.LoadBytesInvalidHeaderAddress = 0x05B6; + break; + } + + return RD; + } + + public void SyncState(Serializer ser) + { + ser.BeginSection("RomData"); + ser.Sync("RomBytes", ref _romBytes, false); + ser.Sync("_saveBytesRoutineAddress", ref _saveBytesRoutineAddress); + ser.Sync("_loadBytesRoutineAddress", ref _loadBytesRoutineAddress); + ser.Sync("_saveBytesResumeAddress", ref _saveBytesResumeAddress); + ser.Sync("_loadBytesResumeAddress", ref _loadBytesResumeAddress); + ser.Sync("_loadBytesInvalidHeaderAddress", ref _loadBytesInvalidHeaderAddress); + ser.EndSection(); + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.Controllers.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.Controllers.cs new file mode 100644 index 0000000000..6d06bc54c3 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.Controllers.cs @@ -0,0 +1,85 @@ + +using BizHawk.Emulation.Common; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public partial class ZXSpectrum + { + + /// + /// The standard 48K Spectrum keyboard + /// https://upload.wikimedia.org/wikipedia/commons/thumb/3/33/ZXSpectrum48k.jpg/1200px-ZXSpectrum48k.jpg + /// + private static readonly ControllerDefinition ZXSpectrumControllerDefinition48 = new ControllerDefinition + { + Name = "ZXSpectrum Controller 48K", + BoolButtons = + { + // Joystick interface (not yet emulated) - Could be Kempston/Cursor/Protek + "P1 Up", "P1 Down", "P1 Left", "P1 Right", "P1 Button", + // Keyboard - row 1 + "Key 1", "Key 2", "Key 3", "Key 4", "Key 5", "Key 6", "Key 7", "Key 8", "Key 9", "Key 0", + // Keyboard - row 2 + "Key Q", "Key W", "Key E", "Key R", "Key T", "Key Y", "Key U", "Key I", "Key O", "Key P", + // Keyboard - row 3 + "Key A", "Key S", "Key D", "Key F", "Key G", "Key H", "Key J", "Key K", "Key L", "Key Return", + // Keyboard - row 4 + "Key Caps Shift", "Key Z", "Key X", "Key C", "Key V", "Key B", "Key N", "Key M", "Key Sym Shift", "Key Space", + // Tape functions + "Play Tape", "Stop Tape", "RTZ Tape", "Record Tape" + } + }; + + /// + /// The newer spectrum keyboard (models 48k+, 128k) + /// https://upload.wikimedia.org/wikipedia/commons/c/ca/ZX_Spectrum%2B.jpg + /// + private static readonly ControllerDefinition ZXSpectrumControllerDefinition128 = new ControllerDefinition + { + Name = "ZXSpectrum Controller 48KPlus", + BoolButtons = + { + // Joystick interface (not yet emulated) - Could be Kempston/Cursor/Protek + "P1 Up", "P1 Down", "P1 Left", "P1 Right", "P1 Button", + // Keyboard - row 1 + "Key True Video", "Key Inv Video", "Key 1", "Key 2", "Key 3", "Key 4", "Key 5", "Key 6", "Key 7", "Key 8", "Key 9", "Key 0", "Key Break", + // Keyboard - row 2 + "Key Delete", "Key Graph", "Key Q", "Key W", "Key E", "Key R", "Key T", "Key Y", "Key U", "Key I", "Key O", "Key P", + // Keyboard - row 3 + "Key Extend Mode", "Key Edit", "Key A", "Key S", "Key D", "Key F", "Key G", "Key H", "Key J", "Key K", "Key L", "Key Return", + // Keyboard - row 4 + "Key Caps Shift", "Key Caps Lock", "Key Z", "Key X", "Key C", "Key V", "Key B", "Key N", "Key M", "Key Period", + // Keyboard - row 5 + "Key Symbol Shift", "Key Semi-Colon", "Key Inverted-Comma", "Key Left Cursor", "Key Right Cursor", "Key Space", "Key Up Cursor", "Key Down Cursor", "Key Comma", "Key Symbol Shift", + // Tape functions + "Play Tape", "Stop Tape", "RTZ Tape", "Record Tape" + } + }; + + /// + /// The amstrad models - same as the previous keyboard layout but with built in ZX Interface 2 joystick ports + /// https://upload.wikimedia.org/wikipedia/commons/c/ca/ZX_Spectrum%2B.jpg + /// + private static readonly ControllerDefinition ZXSpectrumControllerDefinitionPlus = new ControllerDefinition + { + Name = "ZXSpectrum Controller 48KPlus", + BoolButtons = + { + // Joystick interface (not yet emulated) + "P1 Up", "P1 Down", "P1 Left", "P1 Right", "P1 Button", + // Keyboard - row 1 + "Key True Video", "Key Inv Video", "Key 1", "Key 2", "Key 3", "Key 4", "Key 5", "Key 6", "Key 7", "Key 8", "Key 9", "Key 0", "Key Break", + // Keyboard - row 2 + "Key Delete", "Key Graph", "Key Q", "Key W", "Key E", "Key R", "Key T", "Key Y", "Key U", "Key I", "Key O", "Key P", + // Keyboard - row 3 + "Key Extend Mode", "Key Edit", "Key A", "Key S", "Key D", "Key F", "Key G", "Key H", "Key J", "Key K", "Key L", "Key Return", + // Keyboard - row 4 + "Key Caps Shift", "Key Caps Lock", "Key Z", "Key X", "Key C", "Key V", "Key B", "Key N", "Key M", "Key Period", + // Keyboard - row 5 + "Key Symbol Shift", "Key Semi-Colon", "Key Inverted-Comma", "Key Left Cursor", "Key Right Cursor", "Key Space", "Key Up Cursor", "Key Down Cursor", "Key Comma", "Key Symbol Shift", + // Tape functions + "Play Tape", "Stop Tape", "RTZ Tape", "Record Tape" + } + }; + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.IDebuggable.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.IDebuggable.cs new file mode 100644 index 0000000000..b473bac3f8 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.IDebuggable.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; + +using BizHawk.Common.NumberExtensions; +using BizHawk.Emulation.Common; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public partial class ZXSpectrum : IDebuggable + { + public IDictionary GetCpuFlagsAndRegisters() + { + return new Dictionary + { + ["A"] = _cpu.Regs[_cpu.A], + ["AF"] = _cpu.Regs[_cpu.F] + (_cpu.Regs[_cpu.A] << 8), + ["B"] = _cpu.Regs[_cpu.B], + ["BC"] = _cpu.Regs[_cpu.C] + (_cpu.Regs[_cpu.B] << 8), + ["C"] = _cpu.Regs[_cpu.C], + ["D"] = _cpu.Regs[_cpu.D], + ["DE"] = _cpu.Regs[_cpu.E] + (_cpu.Regs[_cpu.D] << 8), + ["E"] = _cpu.Regs[_cpu.E], + ["F"] = _cpu.Regs[_cpu.F], + ["H"] = _cpu.Regs[_cpu.H], + ["HL"] = _cpu.Regs[_cpu.L] + (_cpu.Regs[_cpu.H] << 8), + ["I"] = _cpu.Regs[_cpu.I], + ["IX"] = _cpu.Regs[_cpu.Ixl] + (_cpu.Regs[_cpu.Ixh] << 8), + ["IY"] = _cpu.Regs[_cpu.Iyl] + (_cpu.Regs[_cpu.Iyh] << 8), + ["L"] = _cpu.Regs[_cpu.L], + ["PC"] = _cpu.Regs[_cpu.PCl] + (_cpu.Regs[_cpu.PCh] << 8), + ["R"] = _cpu.Regs[_cpu.R], + ["Shadow AF"] = _cpu.Regs[_cpu.F_s] + (_cpu.Regs[_cpu.A_s] << 8), + ["Shadow BC"] = _cpu.Regs[_cpu.C_s] + (_cpu.Regs[_cpu.B_s] << 8), + ["Shadow DE"] = _cpu.Regs[_cpu.E_s] + (_cpu.Regs[_cpu.D_s] << 8), + ["Shadow HL"] = _cpu.Regs[_cpu.L_s] + (_cpu.Regs[_cpu.H_s] << 8), + ["SP"] = _cpu.Regs[_cpu.Iyl] + (_cpu.Regs[_cpu.Iyh] << 8), + ["Flag C"] = _cpu.FlagC, + ["Flag N"] = _cpu.FlagN, + ["Flag P/V"] = _cpu.FlagP, + ["Flag 3rd"] = _cpu.Flag3, + ["Flag H"] = _cpu.FlagH, + ["Flag 5th"] = _cpu.Flag5, + ["Flag Z"] = _cpu.FlagZ, + ["Flag S"] = _cpu.FlagS + }; + } + + public void SetCpuRegister(string register, int value) + { + switch (register) + { + default: + throw new InvalidOperationException(); + case "A": + _cpu.Regs[_cpu.A] = (ushort)value; + break; + case "AF": + _cpu.Regs[_cpu.F] = (ushort)(value & 0xFF); + _cpu.Regs[_cpu.A] = (ushort)(value & 0xFF00); + break; + case "B": + _cpu.Regs[_cpu.B] = (ushort)value; + break; + case "BC": + _cpu.Regs[_cpu.C] = (ushort)(value & 0xFF); + _cpu.Regs[_cpu.B] = (ushort)(value & 0xFF00); + break; + case "C": + _cpu.Regs[_cpu.C] = (ushort)value; + break; + case "D": + _cpu.Regs[_cpu.D] = (ushort)value; + break; + case "DE": + _cpu.Regs[_cpu.E] = (ushort)(value & 0xFF); + _cpu.Regs[_cpu.D] = (ushort)(value & 0xFF00); + break; + case "E": + _cpu.Regs[_cpu.E] = (ushort)value; + break; + case "F": + _cpu.Regs[_cpu.F] = (ushort)value; + break; + case "H": + _cpu.Regs[_cpu.H] = (ushort)value; + break; + case "HL": + _cpu.Regs[_cpu.L] = (ushort)(value & 0xFF); + _cpu.Regs[_cpu.H] = (ushort)(value & 0xFF00); + break; + case "I": + _cpu.Regs[_cpu.I] = (ushort)value; + break; + case "IX": + _cpu.Regs[_cpu.Ixl] = (ushort)(value & 0xFF); + _cpu.Regs[_cpu.Ixh] = (ushort)(value & 0xFF00); + break; + case "IY": + _cpu.Regs[_cpu.Iyl] = (ushort)(value & 0xFF); + _cpu.Regs[_cpu.Iyh] = (ushort)(value & 0xFF00); + break; + case "L": + _cpu.Regs[_cpu.L] = (ushort)value; + break; + case "PC": + _cpu.Regs[_cpu.PCl] = (ushort)(value & 0xFF); + _cpu.Regs[_cpu.PCh] = (ushort)(value & 0xFF00); + break; + case "R": + _cpu.Regs[_cpu.R] = (ushort)value; + break; + case "Shadow AF": + _cpu.Regs[_cpu.F_s] = (ushort)(value & 0xFF); + _cpu.Regs[_cpu.A_s] = (ushort)(value & 0xFF00); + break; + case "Shadow BC": + _cpu.Regs[_cpu.C_s] = (ushort)(value & 0xFF); + _cpu.Regs[_cpu.B_s] = (ushort)(value & 0xFF00); + break; + case "Shadow DE": + _cpu.Regs[_cpu.E_s] = (ushort)(value & 0xFF); + _cpu.Regs[_cpu.D_s] = (ushort)(value & 0xFF00); + break; + case "Shadow HL": + _cpu.Regs[_cpu.L_s] = (ushort)(value & 0xFF); + _cpu.Regs[_cpu.H_s] = (ushort)(value & 0xFF00); + break; + case "SP": + _cpu.Regs[_cpu.SPl] = (ushort)(value & 0xFF); + _cpu.Regs[_cpu.SPh] = (ushort)(value & 0xFF00); + break; + } + } + + public IMemoryCallbackSystem MemoryCallbacks { get; } + + public bool CanStep(StepType type) => false; + + [FeatureNotImplemented] + public void Step(StepType type) + { + throw new NotImplementedException(); + } + + public int TotalExecutedCycles => _cpu.TotalExecutedCycles; + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.IEmulator.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.IEmulator.cs new file mode 100644 index 0000000000..8b10763146 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.IEmulator.cs @@ -0,0 +1,51 @@ +using BizHawk.Emulation.Common; +using BizHawk.Common.NumberExtensions; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public partial class ZXSpectrum : IEmulator + { + public IEmulatorServiceProvider ServiceProvider { get; } + + public ControllerDefinition ControllerDefinition { get; set; } + + public void FrameAdvance(IController controller, bool render, bool renderSound) + { + _controller = controller; + + if (_tracer.Enabled) + { + _cpu.TraceCallback = s => _tracer.Put(s); + } + else + { + _cpu.TraceCallback = null; + } + + _machine.ExecuteFrame(); + } + + public int Frame => _machine.FrameCount; + + public string SystemId => "ZXSpectrum"; + + public bool DeterministicEmulation => true; + + public void ResetCounters() + { + _machine.FrameCount = 0; + _lagCount = 0; + _isLag = false; + } + + public CoreComm CoreComm { get; } + + public void Dispose() + { + if (_machine != null) + { + _machine = null; + } + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.IInputPollable.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.IInputPollable.cs new file mode 100644 index 0000000000..70a3b15712 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.IInputPollable.cs @@ -0,0 +1,26 @@ + +using System; +using BizHawk.Emulation.Common; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public partial class ZXSpectrum : IInputPollable + { + public int LagCount + { + get { return _lagCount; } + set { _lagCount = value; } + } + + public bool IsLagFrame + { + get { return _isLag; } + set { _isLag = value; } + } + + public IInputCallbackSystem InputCallbacks { get; } + + private int _lagCount = 0; + private bool _isLag = true; + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.IMemoryDomains.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.IMemoryDomains.cs new file mode 100644 index 0000000000..85083f3fcc --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.IMemoryDomains.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using BizHawk.Emulation.Common; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public partial class ZXSpectrum //: IMemoryDomains + { + private MemoryDomainList memoryDomains; + private readonly Dictionary _byteArrayDomains = new Dictionary(); + private bool _memoryDomainsInit = false; + + private void SetupMemoryDomains() + { + var domains = new List + { + new MemoryDomainDelegate("System Bus", 0x10000, MemoryDomain.Endian.Little, + (addr) => + { + if (addr < 0 || addr >= 65536) + { + throw new ArgumentOutOfRangeException(); + } + return _machine.ReadBus((ushort)addr); + }, + (addr, value) => + { + if (addr < 0 || addr >= 65536) + { + throw new ArgumentOutOfRangeException(); + } + + _machine.WriteBus((ushort)addr, value); + }, 1) + }; + + SyncAllByteArrayDomains(); + + memoryDomains = new MemoryDomainList(_byteArrayDomains.Values.Concat(domains).ToList()); + (ServiceProvider as BasicServiceProvider).Register(memoryDomains); + + _memoryDomainsInit = true; + + + } + + private void SyncAllByteArrayDomains() + { + + SyncByteArrayDomain("ROM0", _machine.ROM0); + SyncByteArrayDomain("ROM1", _machine.ROM1); + SyncByteArrayDomain("ROM2", _machine.ROM2); + SyncByteArrayDomain("ROM3", _machine.ROM3); + SyncByteArrayDomain("RAM0", _machine.RAM0); + SyncByteArrayDomain("RAM1", _machine.RAM1); + SyncByteArrayDomain("RAM2", _machine.RAM2); + SyncByteArrayDomain("RAM3", _machine.RAM3); + SyncByteArrayDomain("RAM4", _machine.RAM4); + SyncByteArrayDomain("RAM5", _machine.RAM5); + SyncByteArrayDomain("RAM6", _machine.RAM6); + SyncByteArrayDomain("RAM7", _machine.RAM7); + + } + + private void SyncByteArrayDomain(string name, byte[] data) + { + + if (_memoryDomainsInit || _byteArrayDomains.ContainsKey(name)) + { + var m = _byteArrayDomains[name]; + m.Data = data; + } + else + { + var m = new MemoryDomainByteArray(name, MemoryDomain.Endian.Little, data, true, 1); + _byteArrayDomains.Add(name, m); + } + + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.ISettable.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.ISettable.cs new file mode 100644 index 0000000000..33dddae230 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.ISettable.cs @@ -0,0 +1,97 @@ +using System; +using Newtonsoft.Json; + +using BizHawk.Common; +using BizHawk.Emulation.Common; +using System.ComponentModel; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public partial class ZXSpectrum : ISettable + { + internal ZXSpectrumSettings Settings = new ZXSpectrumSettings(); + internal ZXSpectrumSyncSettings SyncSettings = new ZXSpectrumSyncSettings(); + + public ZXSpectrumSettings GetSettings() + { + return Settings.Clone(); + } + + public ZXSpectrumSyncSettings GetSyncSettings() + { + return SyncSettings.Clone(); + } + + public bool PutSettings(ZXSpectrumSettings o) + { + Settings = o; + return false; + } + + public bool PutSyncSettings(ZXSpectrumSyncSettings o) + { + SyncSettings = o; + return false; + } + + + + public class ZXSpectrumSettings + { + [DisplayName("Spectrum model")] + [Description("The model of spectrum to be emulated")] + [DefaultValue(MachineType.ZXSpectrum48)] + public MachineType MachineType { get; set; } + + [DisplayName("Border type")] + [Description("Select how to show the border area")] + [DefaultValue(BorderType.Full)] + public BorderType BorderType { get; set; } + + + + public ZXSpectrumSettings Clone() + { + return (ZXSpectrumSettings)MemberwiseClone(); + } + + public ZXSpectrumSettings() + { + BizHawk.Common.SettingsUtil.SetDefaultValues(this); + } + } + + public class ZXSpectrumSyncSettings + { + [DisplayName("Tape Load Speed")] + [Description("Select how fast the spectrum loads the game from tape")] + [DefaultValue(TapeLoadSpeed.Accurate)] + public TapeLoadSpeed TapeLoadSpeed { get; set; } + + public ZXSpectrumSyncSettings Clone() + { + return (ZXSpectrumSyncSettings)MemberwiseClone(); + } + } + + /// + /// The size of the Spectrum border + /// + public enum BorderType + { + Full, + Medium, + Small + } + + /// + /// The speed at which the tape is loaded + /// + public enum TapeLoadSpeed + { + Accurate, + Fast, + Fastest + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.IStatable.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.IStatable.cs new file mode 100644 index 0000000000..a9c407ca14 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.IStatable.cs @@ -0,0 +1,71 @@ +using System.IO; + +using BizHawk.Common; +using BizHawk.Emulation.Common; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public partial class ZXSpectrum : IStatable + { + public bool BinarySaveStatesPreferred + { + get { return true; } + } + + public void SaveStateText(TextWriter writer) + { + SyncState(new Serializer(writer)); + } + + public void LoadStateText(TextReader reader) + { + SyncState(new Serializer(reader)); + } + + public void SaveStateBinary(BinaryWriter bw) + { + SyncState(new Serializer(bw)); + } + + public void LoadStateBinary(BinaryReader br) + { + SyncState(new Serializer(br)); + } + + public byte[] SaveStateBinary() + { + MemoryStream ms = new MemoryStream(); + BinaryWriter bw = new BinaryWriter(ms); + SaveStateBinary(bw); + bw.Flush(); + return ms.ToArray(); + } + + private void SyncState(Serializer ser) + { + byte[] core = null; + if (ser.IsWriter) + { + var ms = new MemoryStream(); + ms.Close(); + core = ms.ToArray(); + } + _cpu.SyncState(ser); + + ser.BeginSection("ZXSpectrum"); + _cpu.SyncState(ser); + _machine.SyncState(ser); + ser.Sync("Frame", ref _machine.FrameCount); + ser.Sync("LagCount", ref _lagCount); + ser.Sync("IsLag", ref _isLag); + //ser.Sync("_memoryDomainsInit", ref _memoryDomainsInit); + + ser.EndSection(); + + if (ser.IsReader) + { + SyncAllByteArrayDomains(); + } + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.Util.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.Util.cs new file mode 100644 index 0000000000..893edbae8a --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.Util.cs @@ -0,0 +1,252 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public partial class ZXSpectrum + { + /* + * CPU Helper Methods + */ + + public ushort RegPC + { + get { return (ushort)((_cpu.Regs[0] << 8 | _cpu.Regs[1])); } + set + { + _cpu.Regs[1] = (ushort)(value & 0xFF); + _cpu.Regs[0] = (ushort)((value >> 8) & 0xFF); + } + } + + public ushort RegIX + { + get { return (ushort)((_cpu.Regs[15] << 8 | _cpu.Regs[16] )); } + set + { + _cpu.Regs[16] = (ushort)(value & 0xFF); + _cpu.Regs[15] = (ushort)((value >> 8) & 0xFF); + } + } + + public ushort RegDE + { + get { return (ushort)((_cpu.Regs[8] << 8 | _cpu.Regs[9] )); } + set + { + _cpu.Regs[9] = (ushort)(value & 0xFF); + _cpu.Regs[8] = (ushort)((value >> 8) & 0xFF); + } + } + + public ushort RegAF + { + get { return (ushort)((_cpu.Regs[4] << 8 | _cpu.Regs[5])); } + set + { + _cpu.Regs[5] = (ushort)(value & 0xFF); + _cpu.Regs[4] = (ushort)((value >> 8) & 0xFF); + } + } + + + /// + /// Gets the IX word value + /// + /// + public ushort Get16BitIX() + { + return Convert.ToUInt16(_cpu.Regs[_cpu.Ixh] | _cpu.Regs[_cpu.Ixl] << 8); + } + + /// + /// Set the IX word value + /// + /// + /// + public void Set16BitIX(ushort IX) + { + _cpu.Regs[_cpu.Ixh] = (ushort)(IX & 0xFF); + _cpu.Regs[_cpu.Ixl] = (ushort)((IX >> 8) & 0xff); + } + + /// + /// Gets the AF word value + /// + /// + public ushort Get16BitAF() + { + return Convert.ToUInt16(_cpu.Regs[_cpu.A] | _cpu.Regs[_cpu.F] << 8); + } + + /// + /// Set the AF word value + /// + /// + /// + public void Set16BitAF(ushort AF) + { + _cpu.Regs[_cpu.A] = (ushort)(AF & 0xFF); + _cpu.Regs[_cpu.F] = (ushort)((AF >> 8) & 0xff); + } + + /// + /// Gets the AF shadow word value + /// + /// + public ushort Get16BitAF_() + { + return Convert.ToUInt16(_cpu.Regs[_cpu.A_s] | _cpu.Regs[_cpu.F_s] << 8); + } + + /// + /// Set the AF shadow word value + /// + /// + /// + public void Set16BitAF_(ushort AF_) + { + _cpu.Regs[_cpu.A_s] = (ushort)(AF_ & 0xFF); + _cpu.Regs[_cpu.F_s] = (ushort)((AF_ >> 8) & 0xff); + } + + /// + /// Gets the DE word value + /// + /// + public ushort Get16BitDE() + { + return Convert.ToUInt16(_cpu.Regs[_cpu.E] | _cpu.Regs[_cpu.D] << 8); + } + + /// + /// Set the DE word value + /// + /// + /// + public void Set16BitDE(ushort DE) + { + _cpu.Regs[_cpu.D] = (ushort)(DE & 0xFF); + _cpu.Regs[_cpu.E] = (ushort)((DE >> 8) & 0xff); + } + + + /// + /// Z80 Status Indicator Flag Reset masks + /// + /// + [Flags] + public enum FlagsResetMask : byte + { + /// Sign Flag + S = 0x7F, + + /// Zero Flag + Z = 0xBF, + + /// This flag is not used. + R5 = 0xDF, + + /// Half Carry Flag + H = 0xEF, + + /// This flag is not used. + R3 = 0xF7, + + /// Parity/Overflow Flag + PV = 0xFB, + + /// Add/Subtract Flag + N = 0xFD, + + /// Carry Flag + C = 0xFE, + } + + /// + /// Z80 Status Indicator Flag Set masks + /// + /// + [Flags] + public enum FlagsSetMask : byte + { + /// Sign Flag + /// + /// The Sign Flag (S) stores the state of the most-significant bit of + /// the Accumulator (bit 7). When the Z80 CPU performs arithmetic + /// operations on signed numbers, the binary twos complement notation + /// is used to represent and process numeric information. + /// + S = 0x80, + + /// + /// Zero Flag + /// + /// + /// The Zero Flag is set (1) or cleared (0) if the result generated by + /// the execution of certain instructions is 0. For 8-bit arithmetic and + /// logical operations, the Z flag is set to a 1 if the resulting byte in + /// the Accumulator is 0. If the byte is not 0, the Z flag is reset to 0. + /// + Z = 0x40, + + /// This flag is not used. + R5 = 0x20, + + /// Half Carry Flag + /// + /// The Half Carry Flag (H) is set (1) or cleared (0) depending on the + /// Carry and Borrow status between bits 3 and 4 of an 8-bit arithmetic + /// operation. This flag is used by the Decimal Adjust Accumulator (DAA) + /// instruction to correct the result of a packed BCD add or subtract operation. + /// + H = 0x10, + + /// This flag is not used. + R3 = 0x08, + + /// Parity/Overflow Flag + /// + /// The Parity/Overflow (P/V) Flag is set to a specific state depending on + /// the operation being performed. For arithmetic operations, this flag + /// indicates an overflow condition when the result in the Accumulator is + /// greater than the maximum possible number (+127) or is less than the + /// minimum possible number (–128). This overflow condition is determined by + /// examining the sign bits of the operands. + /// + PV = 0x04, + + /// Add/Subtract Flag + /// + /// The Add/Subtract Flag (N) is used by the Decimal Adjust Accumulator + /// instruction (DAA) to distinguish between the ADD and SUB instructions. + /// For ADD instructions, N is cleared to 0. For SUB instructions, N is set to 1. + /// + N = 0x02, + + /// Carry Flag + /// + /// The Carry Flag (C) is set or cleared depending on the operation being performed. + /// + C = 0x01, + + /// + /// Combination of S, Z, and PV + /// + SZPV = S | Z | PV, + + /// + /// Combination of N, and H + /// + NH = N | H, + + /// + /// Combination of R3, and R5 + /// + R3R5 = R3 | R5 + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.cs new file mode 100644 index 0000000000..cb94079bad --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.cs @@ -0,0 +1,134 @@ +using BizHawk.Emulation.Common; +using BizHawk.Emulation.Cores.Components; +using BizHawk.Emulation.Cores.Components.Z80A; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + [Core( + "ZXHawk", + "Asnivor", + isPorted: false, + isReleased: false)] + [ServiceNotApplicable(typeof(IDriveLight))] + public partial class ZXSpectrum : IDebuggable, IInputPollable, IStatable, IRegionable + { + [CoreConstructor("ZXSpectrum")] + public ZXSpectrum(CoreComm comm, byte[] file, object settings, object syncSettings) + { + PutSyncSettings((ZXSpectrumSyncSettings)syncSettings ?? new ZXSpectrumSyncSettings()); + PutSettings((ZXSpectrumSettings)settings ?? new ZXSpectrumSettings()); + + var ser = new BasicServiceProvider(this); + ServiceProvider = ser; + InputCallbacks = new InputCallbackSystem(); + + CoreComm = comm; + + _cpu = new Z80A(); + + _tracer = new TraceBuffer { Header = _cpu.TraceHeader }; + + switch (Settings.MachineType) + { + case MachineType.ZXSpectrum48: + ControllerDefinition = ZXSpectrumControllerDefinition48; + Init(MachineType.ZXSpectrum48, Settings.BorderType, SyncSettings.TapeLoadSpeed, file); + break; + default: + throw new InvalidOperationException("Machine not yet emulated"); + } + + + + _cpu.MemoryCallbacks = MemoryCallbacks; + + HardReset = _machine.HardReset; + SoftReset = _machine.SoftReset; + + _cpu.FetchMemory = _machine.ReadMemory; + _cpu.ReadMemory = _machine.ReadMemory; + _cpu.WriteMemory = _machine.WriteMemory; + _cpu.ReadHardware = _machine.ReadPort; + _cpu.WriteHardware = _machine.WritePort; + + ser.Register(_tracer); + ser.Register(_cpu); + ser.Register(_machine); + ser.Register(_machine.BuzzerDevice); + + HardReset(); + + + + List romDis = new List(); + List disas = new List(); + for (int i = 0x00; i < 0x4000; i++) + { + DISA d = new DISA(); + ushort size; + d.Dis = _cpu.Disassemble((ushort)i, _machine.ReadMemory, out size); + d.Size = size; + disas.Add(d); + romDis.Add(d.Dis); + //i = i + size - 1; + //romDis.Add(s); + } + } + + public class DISA + { + public ushort Size { get; set; } + public string Dis { get; set; } + } + + //private int _cyclesPerFrame; + + public Action HardReset; + public Action SoftReset; + + private readonly Z80A _cpu; + //private byte[] _systemRom; + private readonly TraceBuffer _tracer; + public IController _controller; + private SpectrumBase _machine; + + private byte[] _file; + + private byte[] GetFirmware(int length, params string[] names) + { + var result = names.Select(n => CoreComm.CoreFileProvider.GetFirmware("ZXSpectrum", n, false)).FirstOrDefault(b => b != null && b.Length == length); + if (result == null) + { + throw new MissingFirmwareException($"At least one of these firmwares is required: {string.Join(", ", names)}"); + } + + return result; + } + + + private void Init(MachineType machineType, BorderType borderType, TapeLoadSpeed tapeLoadSpeed, byte[] file) + { + // setup the emulated model based on the MachineType + switch (machineType) + { + case MachineType.ZXSpectrum48: + _machine = new ZX48(this, _cpu, file); + var _systemRom = GetFirmware(0x4000, "48ROM"); + var romData = RomData.InitROM(machineType, _systemRom); + _machine.InitROM(romData); + break; + } + } + + #region IRegionable + + public DisplayType Region => DisplayType.PAL; + + #endregion + + + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/readme.md b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/readme.md new file mode 100644 index 0000000000..eacc65afa1 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/readme.md @@ -0,0 +1,29 @@ +## ZXHawk + +At this moment this is still *very* experimental and needs a lot more work. + +### Implemented and sorta working +* IEmulator +* ZX Spectrum 48k model +* ULA video output (implementing IVideoProvider) +* ULA Mode 1 VBLANK interrupt generation +* Beeper/Buzzer output (implementing ISoundProvider) +* Keyboard input (implementing IInputPollable) +* Tape device that will load spectrum games in realtime (*.tzx and *.tap) +* IStatable (although this is not currently working/implemented properly during tape load operations) + +### Some progress +* ISettable - There are some Settings and SyncSettings instantiated, although they are not really used and I haven't yet figured out how to wire these up to the front-end yet + +### Not working +* Interrupt Mode 2 (Z80A) - usually invokes a soft reboot when the game raises one (can be seen in 'Chaos - Battle of the Wizards' after initial game setup when the game board tries to load) +* IMemoryDomains - I started looking at this but didn't really know what I was doing yet +* IDebuggable +* Default keyboard keymappings (you have to configure yourself in the core controller settings) +* Joystick support (I still need to implement a Kemptston joystick and interface) +* Manual tape device control (at the moment the tape device detects when the spectrum goes into 'loadbytes' mode and auto-plays the tape. This is not ideal and manual control should be implemented so the user can start/stop manually, return to zero etc..) +* Only standard spectrum tape blocks currently work. Any fancy SpeedLock encoded (and similar) blocks do not + +### Known bugs +* The 'return' keyboard key is acting the same as Space/Break when doing a BASIC RUN or LOAD "" command. The upshot of this is that upon boot, when you go to load the attached spectrum cassette (you have to type: "J", then "SYMSHIFT + P", then "SYMSHIFT + P", then RETURN) it more often than not interrupts the load routine. You then have to try again but hitting the RETURN key at the end of the sequence for as small a time as possible. Rinse and repeat until the load process starts. Clearly NOT ideal. +* Audible 'popping' from the emulated buzzer after a load state operation