Merge pull request #1071 from Asnivor/speccy

ZXHawk Initial
This commit is contained in:
alyosha-tas 2017-11-28 18:08:48 -05:00 committed by GitHub
commit dab3b85599
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 7643 additions and 10 deletions

View File

@ -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()));
}

View File

@ -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));
}
}
}

View File

@ -29,6 +29,7 @@
WonderSwan,
Libretro,
VirtualBoy,
NeoGeoPocket
NeoGeoPocket,
ZXSpectrum
}
}

View File

@ -149,6 +149,8 @@ namespace BizHawk.Client.Common
return SystemInfo.VirtualBoy;
case "NGP":
return SystemInfo.NeoGeoPocket;
case "ZXSpectrum":
return SystemInfo.ZXSpectrum;
}
}
}

View File

@ -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>(),
(C64.C64SyncSettings)GetCoreSyncSettings<C64>());
break;
case "ZXSpectrum":
nextEmulator = new ZXSpectrum(
nextComm,
xmlGame.Assets.Select(a => a.Value).First(),
(ZXSpectrum.ZXSpectrumSettings)GetCoreSettings<ZXSpectrum>(),
(ZXSpectrum.ZXSpectrumSyncSettings)GetCoreSyncSettings<ZXSpectrum>());
break;
case "PSX":
var entries = xmlGame.AssetFullPaths;
var discs = new List<Disc>();
@ -990,6 +998,10 @@ namespace BizHawk.Client.Common
var c64 = new C64(nextComm, Enumerable.Repeat(rom.RomData, 1), rom.GameInfo, GetCoreSettings<C64>(), GetCoreSyncSettings<C64>());
nextEmulator = c64;
break;
case "ZXSpectrum":
var zx = new ZXSpectrum(nextComm, rom.FileData, GetCoreSettings<ZXSpectrum>(), GetCoreSyncSettings<ZXSpectrum>());
nextEmulator = zx;
break;
case "GBA":
if (Global.Config.GBA_UsemGBA)
{

View File

@ -188,6 +188,11 @@ namespace BizHawk.Client.Common
/// </summary>
public static SystemInfo NeoGeoPocket { get; } = new SystemInfo("Neo-Geo Pocket", CoreSystem.NeoGeoPocket, 1);
/// <summary>
/// Gets the <see cref="SystemInfo"/> instance for ZXSpectrum
/// </summary>
public static SystemInfo ZXSpectrum { get; } = new SystemInfo("ZX Spectrum", CoreSystem.ZXSpectrum, 2);
#endregion Get SystemInfo
/// <summary>

View File

@ -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 },

View File

@ -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"
};
}

View File

@ -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", "*.*");
}

View File

@ -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));
}

View File

@ -52,6 +52,7 @@ namespace BizHawk.Client.EmuHawk
{ "GBC", "Game Boy Color" },
{ "PCFX", "PC-FX" },
{ "32X", "32X" },
{ "ZXSpectrum", "ZX Spectrum" }
};
public string TargetSystem = null;

View File

@ -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";

View File

@ -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)");

View File

@ -256,6 +256,49 @@
<Compile Include="Computers\Commodore64\MOS\Vic.VideoProvider.cs" />
<Compile Include="Computers\Commodore64\SaveState.cs" />
<Compile Include="Computers\Commodore64\User\UserPortDevice.cs" />
<Compile Include="Computers\SinclairSpectrum\Hardware\Buzzer.cs" />
<Compile Include="Computers\SinclairSpectrum\Hardware\DefaultTapeProvider.cs" />
<Compile Include="Computers\SinclairSpectrum\Hardware\Interfaces\IKeyboard.cs" />
<Compile Include="Computers\SinclairSpectrum\Hardware\Interfaces\ISaveToTapeProvider.cs" />
<Compile Include="Computers\SinclairSpectrum\Hardware\Interfaces\ISupportsTapeBlockPlayback.cs" />
<Compile Include="Computers\SinclairSpectrum\Hardware\Interfaces\ISupportsTapeBlockSetPlayback.cs" />
<Compile Include="Computers\SinclairSpectrum\Hardware\Interfaces\ITapeContentProvider.cs" />
<Compile Include="Computers\SinclairSpectrum\Hardware\Interfaces\ITapeData.cs" />
<Compile Include="Computers\SinclairSpectrum\Hardware\Interfaces\ITapeDataSerialization.cs" />
<Compile Include="Computers\SinclairSpectrum\Hardware\Interfaces\ITapeProvider.cs" />
<Compile Include="Computers\SinclairSpectrum\Hardware\TapeBlockSetPlayer.cs" />
<Compile Include="Computers\SinclairSpectrum\Hardware\TapeDataBlockPlayer.cs" />
<Compile Include="Computers\SinclairSpectrum\Hardware\TapeFilePlayer.cs" />
<Compile Include="Computers\SinclairSpectrum\Machine\MachineType.cs" />
<Compile Include="Computers\SinclairSpectrum\Machine\SpectrumBase.cs" />
<Compile Include="Computers\SinclairSpectrum\Machine\SpectrumBase.Input.cs" />
<Compile Include="Computers\SinclairSpectrum\Machine\SpectrumBase.Port.cs" />
<Compile Include="Computers\SinclairSpectrum\Machine\SpectrumBase.Memory.cs" />
<Compile Include="Computers\SinclairSpectrum\Machine\SpectrumBase.Sound.cs" />
<Compile Include="Computers\SinclairSpectrum\Machine\ZXSpectrum48K\ZX48.cs" />
<Compile Include="Computers\SinclairSpectrum\Media\Tape\TAP\TapDataBlock.cs" />
<Compile Include="Computers\SinclairSpectrum\Media\Tape\TAP\TapPlayer.cs" />
<Compile Include="Computers\SinclairSpectrum\Media\Tape\TAP\TapReader.cs" />
<Compile Include="Computers\SinclairSpectrum\Media\Tape\TZX\BlockAbstraction.cs" />
<Compile Include="Computers\SinclairSpectrum\Media\Tape\TZX\DataBlocks.cs" />
<Compile Include="Computers\SinclairSpectrum\Media\Tape\TZX\Info.cs" />
<Compile Include="Computers\SinclairSpectrum\Media\Tape\TZX\Types.cs" />
<Compile Include="Computers\SinclairSpectrum\Media\Tape\TZX\TzxException.cs" />
<Compile Include="Computers\SinclairSpectrum\Media\Tape\TZX\TzxHeader.cs" />
<Compile Include="Computers\SinclairSpectrum\Media\Tape\TZX\TzxPlayer.cs" />
<Compile Include="Computers\SinclairSpectrum\Media\Tape\TZX\TzxReader.cs" />
<Compile Include="Computers\SinclairSpectrum\Pulse.cs" />
<Compile Include="Computers\SinclairSpectrum\RomData.cs" />
<Compile Include="Computers\SinclairSpectrum\Hardware\Tape.cs" />
<Compile Include="Computers\SinclairSpectrum\ZXSpectrum.Controllers.cs" />
<Compile Include="Computers\SinclairSpectrum\ZXSpectrum.cs" />
<Compile Include="Computers\SinclairSpectrum\ZXSpectrum.IDebuggable.cs" />
<Compile Include="Computers\SinclairSpectrum\ZXSpectrum.IEmulator.cs" />
<Compile Include="Computers\SinclairSpectrum\ZXSpectrum.IInputPollable.cs" />
<Compile Include="Computers\SinclairSpectrum\ZXSpectrum.IMemoryDomains.cs" />
<Compile Include="Computers\SinclairSpectrum\ZXSpectrum.ISettable.cs" />
<Compile Include="Computers\SinclairSpectrum\ZXSpectrum.IStatable.cs" />
<Compile Include="Computers\SinclairSpectrum\ZXSpectrum.Util.cs" />
<Compile Include="Consoles\Atari\2600\Atari2600.cs" />
<Compile Include="Consoles\Atari\2600\Atari2600.Core.cs">
<DependentUpon>Atari2600.cs</DependentUpon>
@ -1331,6 +1374,9 @@
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Compile Include="Computers\SinclairSpectrum\Machine\SpectrumBase.Screen.cs" />
<Compile Include="Computers\SinclairSpectrum\Machine\ZXSpectrum48K\ZX48.Keyboard.cs" />
<None Include="Computers\SinclairSpectrum\readme.md" />
<None Include="Consoles\Atari\docs\stella.pdf" />
<None Include="Consoles\Coleco\docs\colecovision tech1.pdf" />
<None Include="Consoles\Coleco\docs\colecovision tech2.pdf" />
@ -1341,6 +1387,7 @@
<None Include="Resources\dmg_boot.bin.gz" />
<None Include="Resources\sgb-cart-present.spc.gz" />
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<PropertyGroup>
<PreBuildEvent Condition=" '$(OS)' == 'Windows_NT' ">"$(SolutionDir)subwcrev.bat" "$(ProjectDir)"</PreBuildEvent>

View File

@ -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
{
/// <summary>
/// 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
/// </summary>
public class Buzzer : ISoundProvider
{
/// <summary>
/// Supplied values are right for 48K spectrum
/// These will deviate for 128k and up (as there are more T-States per frame)
/// </summary>
//public int SampleRate = 44100; //35000;
//public int SamplesPerFrame = 882; //699;
//public int TStatesPerSample = 79; //100;
/// <summary>
/// Sample Rate
/// This usually has to be 44100 for ISoundProvider
/// </summary>
public int SampleRate
{
get { return _sampleRate; }
set { _sampleRate = value; }
}
/// <summary>
/// Number of samples in one frame
/// </summary>
public int SamplesPerFrame
{
get { return _samplesPerFrame; }
set { _samplesPerFrame = value; }
}
/// <summary>
/// Number of TStates in each sample
/// </summary>
public int TStatesPerSample
{
get { return _tStatesPerSample; }
set { _tStatesPerSample = value; }
}
private SpectrumBase _machine;
/// <summary>
/// State fields
/// </summary>
private long _frameStart;
private bool _tapeMode;
private int _tStatesPerFrame;
private int _sampleRate;
private int _samplesPerFrame;
private int _tStatesPerSample;
/// <summary>
/// Pulses collected during the last frame
/// </summary>
public List<Pulse> Pulses { get; private set; }
/// <summary>
/// The last pulse
/// </summary>
public bool LastPulse { get; set; }
/// <summary>
/// The last T-State (cpu cycle) that the last pulse was received
/// </summary>
public int LastPulseTState { get; set; }
#region Construction & Initialisation
public Buzzer(SpectrumBase machine)
{
_machine = machine;
}
/// <summary>
/// Initialises the buzzer
/// </summary>
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<Pulse>(1000);
}
#endregion
/// <summary>
/// When the pulse value from the EAR output changes it is processed here
/// </summary>
/// <param name="fromTape"></param>
/// <param name="earPulse"></param>
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;
}
/// <summary>
/// New frame starts
/// </summary>
public void StartFrame()
{
//DiscardSamples();
Pulses.Clear();
LastPulseTState = 0;
}
/// <summary>
/// Frame is completed
/// </summary>
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;
}
/// <summary>
/// When the spectrum is set to receive tape input, the EAR output on the ULA is disabled
/// (so no buzzer sound is emitted)
/// </summary>
/// <param name="tapeMode"></param>
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();
}
}
}

View File

@ -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;
/// <summary>
/// The directory files should be saved to
/// </summary>
public string SaveFileFolder { get; }
public DefaultTapeProvider(byte[] file, string saveFolder = null)
{
SaveFileFolder = string.IsNullOrWhiteSpace(saveFolder)
? DEFAULT_SAVE_FILE_DIR
: saveFolder;
_file = file;
}
/// <summary>
/// The component provider should be able to reset itself
/// </summary>
///
public void Reset()
{
_dataBlockCount = 0;
_suggestedName = null;
_fullFileName = null;
}
/// <summary>
/// Tha tape set to load the content from
/// </summary>
public string TapeSetName { get; set; }
/// <summary>
/// Gets a binary reader that provider TZX content
/// </summary>
/// <returns>BinaryReader instance to obtain the content from</returns>
public BinaryReader GetTapeContent()
{
Stream stream = new MemoryStream(_file);
var reader = new BinaryReader(stream);
return reader;
}
/// <summary>
/// Creates a tape file with the specified name
/// </summary>
/// <returns></returns>
public void CreateTapeFile()
{
//Reset();
}
/// <summary>
/// This method sets the name of the file according to the
/// Spectrum SAVE HEADER information
/// </summary>
/// <param name="name"></param>
public void SetName(string name)
{
_suggestedName = name;
}
/// <summary>
/// Appends the TZX block to the tape file
/// </summary>
/// <param name="block"></param>
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);
}
}
/// <summary>
/// The tape provider can finalize the tape when all
/// TZX blocks are written.
/// </summary>
public void FinalizeTapeFile()
{
}
/// <summary>
/// Obtains the specified resource stream ot the given assembly
/// </summary>
/// <param name="asm">Assembly to get the resource stream from</param>
/// <param name="resourceName">Resource name</param>
private static Stream GetFileResource(Assembly asm, string resourceName)
{
var resourceFullName = $"{asm.GetName().Name}.{RESOURCE_FOLDER}.{resourceName}";
return asm.GetManifestResourceStream(resourceFullName);
}
}
}

View File

@ -0,0 +1,69 @@

using BizHawk.Common;
namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
{
/// <summary>
/// Represents a spectrum keyboard
/// </summary>
public interface IKeyboard
{
/// <summary>
/// The calling spectrumbase class
/// </summary>
SpectrumBase _machine { get; }
/// <summary>
/// The keyboard matrix for a particular spectrum model
/// </summary>
string[] KeyboardMatrix { get; set; }
/// <summary>
/// For 16/48k models
/// </summary>
bool Issue2 { get; set; }
/// <summary>
/// The current keyboard line status
/// </summary>
//byte[] LineStatus { get; set; }
/// <summary>
/// Sets the spectrum key status
/// </summary>
/// <param name="key"></param>
/// <param name="isPressed"></param>
void SetKeyStatus(string key, bool isPressed);
/// <summary>
/// Gets the status of a spectrum key
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
bool GetKeyStatus(string key);
/// <summary>
/// Returns the query byte
/// </summary>
/// <param name="lines"></param>
/// <returns></returns>
byte GetLineStatus(byte lines);
/// <summary>
/// Reads a keyboard byte
/// </summary>
/// <param name="addr"></param>
/// <returns></returns>
byte ReadKeyboardByte(ushort addr);
/// <summary>
/// Looks up a key in the keyboard matrix and returns the relevent byte value
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
byte GetByteFromKeyMatrix(string key);
void SyncState(Serializer ser);
}
}

View File

@ -0,0 +1,35 @@

namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
{
/// <summary>
/// This interface describes the behavior of an object that
/// provides tape content
/// </summary>
public interface ISaveToTapeProvider
{
/// <summary>
/// Creates a tape file with the specified name
/// </summary>
/// <returns></returns>
void CreateTapeFile();
/// <summary>
/// This method sets the name of the file according to the
/// Spectrum SAVE HEADER information
/// </summary>
/// <param name="name"></param>
void SetName(string name);
/// <summary>
/// Appends the tape block to the tape file
/// </summary>
/// <param name="block"></param>
void SaveTapeBlock(ITapeDataSerialization block);
/// <summary>
/// The tape provider can finalize the tape when all
/// tape blocks are written.
/// </summary>
void FinalizeTapeFile();
}
}

View File

@ -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
{
/// <summary>
/// This interface represents that the implementing class supports
/// emulating tape playback of a single tape block
/// </summary>
public interface ISupportsTapeBlockPlayback
{
/// <summary>
/// The current playing phase
/// </summary>
PlayPhase PlayPhase { get; }
/// <summary>
/// The tact count of the CPU when playing starts
/// </summary>
long StartCycle { get; }
/// <summary>
/// Initializes the player
/// </summary>
void InitPlay(long startCycle);
/// <summary>
/// Gets the EAR bit value for the specified tact
/// </summary>
/// <param name="currentCycle">Tacts to retrieve the EAR bit</param>
/// <returns>
/// The EAR bit value to play back
/// </returns>
bool GetEarBit(long currentCycle);
}
}

View File

@ -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
{
/// <summary>
/// This interface represents that the implementing class supports
/// emulating tape playback of a set of subsequent tape blocks
/// </summary>
public interface ISupportsTapeBlockSetPlayback : ISupportsTapeBlockPlayback
{
/// <summary>
/// Moves the player to the next playable block
/// </summary>
/// <param name="currentCycle">Tacts time to start the next block</param>
void NextBlock(long currentCycle);
}
}

View File

@ -0,0 +1,25 @@

using System.IO;
namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
{
/// <summary>
/// This interface describes the behavior of an object that
/// provides tape content
/// </summary>
public interface ITapeContentProvider
{
/// <summary>
/// Tha tape set to load the content from
/// </summary>
string TapeSetName { get; set; }
/// <summary>
/// Gets a binary reader that provides tape content
/// </summary>
/// <returns>BinaryReader instance to obtain the content from</returns>
BinaryReader GetTapeContent();
void Reset();
}
}

View File

@ -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
{
/// <summary>
/// Represetns the data in the tape
/// </summary>
public interface ITapeData
{
/// <summary>
/// Block Data
/// </summary>
byte[] Data { get; }
/// <summary>
/// Pause after this block (given in milliseconds)
/// </summary>
ushort PauseAfter { get; }
}
}

View File

@ -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
{
/// <summary>
/// Defines the serialization operations of a TZX record
/// </summary>
public interface ITapeDataSerialization
{
/// <summary>
/// Reads the content of the block from the specified binary stream.
/// </summary>
/// <param name="reader">Stream to read the block from</param>
void ReadFrom(BinaryReader reader);
/// <summary>
/// Writes the content of the block to the specified binary stream.
/// </summary>
/// <param name="writer">Stream to write the block to</param>
void WriteTo(BinaryWriter writer);
}
}

View File

@ -0,0 +1,53 @@
using System.IO;
namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
{
/// <summary>
/// This interface describes the behavior of an object that
/// provides TZX tape content
/// </summary>
public interface ITapeProvider
{
/// <summary>
/// Tha tape set to load the content from
/// </summary>
string TapeSetName { get; set; }
/// <summary>
/// Gets a binary reader that provider TZX content
/// </summary>
/// <returns>BinaryReader instance to obtain the content from</returns>
BinaryReader GetTapeContent();
/// <summary>
/// Creates a tape file with the specified name
/// </summary>
/// <returns></returns>
void CreateTapeFile();
/// <summary>
/// This method sets the name of the file according to the
/// Spectrum SAVE HEADER information
/// </summary>
/// <param name="name"></param>
void SetName(string name);
/// <summary>
/// Appends the TZX block to the tape file
/// </summary>
/// <param name="block"></param>
void SaveTapeBlock(ITapeDataSerialization block);
/// <summary>
/// The tape provider can finalize the tape when all
/// TZX blocks are written.
/// </summary>
void FinalizeTapeFile();
/// <summary>
/// Provider can reset itself
/// </summary>
void Reset();
}
}

View File

@ -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.
*/
/// <summary>
/// Represents the tape device (or DATACORDER as AMSTRAD liked to call it)
/// </summary>
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;
/// <summary>
/// Number of tacts after save mod can be exited automatically
/// </summary>
public const int SAVE_STOP_SILENCE = 17500000;
/// <summary>
/// The address of the ERROR routine in the Spectrum ROM
/// </summary>
public const ushort ERROR_ROM_ADDRESS = 0x0008;
/// <summary>
/// The maximum distance between two scans of the EAR bit
/// </summary>
public const int MAX_TACT_JUMP = 10000;
/// <summary>
/// The width tolerance of save pulses
/// </summary>
public const int SAVE_PULSE_TOLERANCE = 24;
/// <summary>
/// Minimum number of pilot pulses before SYNC1
/// </summary>
public const int MIN_PILOT_PULSE_COUNT = 3000;
/// <summary>
/// Lenght of the data buffer to allocate for the SAVE operation
/// </summary>
public const int DATA_BUFFER_LENGTH = 0x10000;
/// <summary>
/// Gets the tape content provider
/// </summary>
public ITapeProvider TapeProvider { get; }
/// <summary>
/// The TapeFilePlayer that can playback tape content
/// </summary>
public TapeFilePlayer TapeFilePlayer => _tapePlayer;
/// <summary>
/// The current operation mode of the tape
/// </summary>
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);
}
}
}
/// <summary>
/// Sets the current tape mode according to the current PC register
/// and the MIC bit state
/// </summary>
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;
}
}
/// <summary>
/// Puts the device in save mode. From now on, every MIC pulse is recorded
/// </summary>
private void EnterSaveMode()
{
_currentMode = TapeOperationMode.Save;
_savePhase = SavePhase.None;
_micBitState = true;
_lastMicBitActivityCycle = _cpu.TotalExecutedCycles;
_pilotPulseCount = 0;
_prevDataPulse = MicPulseType.None;
_dataBlockCount = 0;
TapeProvider?.CreateTapeFile();
}
/// <summary>
/// Leaves the save mode. Stops recording MIC pulses
/// </summary>
private void LeaveSaveMode()
{
_currentMode = TapeOperationMode.Passive;
TapeProvider?.FinalizeTapeFile();
}
/// <summary>
/// Puts the device in load mode. From now on, EAR pulses are played by a device
/// </summary>
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);
}
/// <summary>
/// Leaves the load mode. Stops the device that playes EAR pulses
/// </summary>
private void LeaveLoadMode()
{
_currentMode = TapeOperationMode.Passive;
_tapePlayer = null;
TapeProvider?.Reset();
_buzzer.SetTapeMode(false);
}
/// <summary>
/// Loads the next TZX player block instantly without emulation
/// EAR bit processing
/// </summary>
/// <returns>True, if fast load is operative</returns>
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;
}
/// <summary>
/// the EAR bit read from tape
/// </summary>
/// <param name="cpuCycles"></param>
/// <returns></returns>
public virtual bool GetEarBit(int cpuCycles)
{
if (_currentMode != TapeOperationMode.Load)
{
return true;
}
var earBit = _tapePlayer?.GetEarBit(cpuCycles) ?? true;
_buzzer.ProcessPulseValue(true, earBit);
return earBit;
}
/// <summary>
/// Processes the mic bit change
/// </summary>
/// <param name="micBit"></param>
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<TapeOperationMode>("_currentMode", ref _currentMode);
ser.SyncEnum<SavePhase>("_savePhase", ref _savePhase);
ser.SyncEnum<MicPulseType>("_prevDataPulse", ref _prevDataPulse);
/*
private TapeFilePlayer _tapePlayer;
*/
ser.EndSection();
}
}
/// <summary>
/// This enum represents the operation mode of the tape
/// </summary>
public enum TapeOperationMode : byte
{
/// <summary>
/// The tape device is passive
/// </summary>
Passive = 0,
/// <summary>
/// The tape device is saving information (MIC pulses)
/// </summary>
Save,
/// <summary>
/// The tape device generates EAR pulses from a player
/// </summary>
Load
}
/// <summary>
/// This class represents a spectrum tape header
/// </summary>
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;
/// <summary>
/// The bytes of the header
/// </summary>
public byte[] HeaderBytes { get; }
/// <summary>
/// Initializes a new instance of the <see cref="T:System.Object" /> class.
/// </summary>
public SpectrumTapeHeader()
{
HeaderBytes = new byte[HEADER_LEN];
for (var i = 0; i < HEADER_LEN; i++) HeaderBytes[i] = 0x00;
CalcChecksum();
}
/// <summary>
/// Initializes a new instance with the specified header data.
/// </summary>
/// <param name="header">Header data</param>
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();
}
/// <summary>
/// Gets or sets the type of the header
/// </summary>
public byte Type
{
get { return HeaderBytes[TYPE_OFFS]; }
set
{
HeaderBytes[TYPE_OFFS] = (byte)(value & 0x03);
CalcChecksum();
}
}
/// <summary>
/// Gets or sets the program name
/// </summary>
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();
}
}
/// <summary>
/// Gets or sets the Data Length
/// </summary>
public ushort DataLength
{
get { return GetWord(DATA_LEN_OFFS); }
set { SetWord(DATA_LEN_OFFS, value); }
}
/// <summary>
/// Gets or sets Parameter1
/// </summary>
public ushort Parameter1
{
get { return GetWord(PAR1_OFFS); }
set { SetWord(PAR1_OFFS, value); }
}
/// <summary>
/// Gets or sets Parameter2
/// </summary>
public ushort Parameter2
{
get { return GetWord(PAR2_OFFS); }
set { SetWord(PAR2_OFFS, value); }
}
/// <summary>
/// Gets the value of checksum
/// </summary>
public byte Checksum => HeaderBytes[CHK_OFFS];
/// <summary>
/// Calculate the checksum
/// </summary>
private void CalcChecksum()
{
var chk = 0x00;
for (var i = 0; i < HEADER_LEN - 1; i++) chk ^= HeaderBytes[i];
HeaderBytes[CHK_OFFS] = (byte)chk;
}
/// <summary>
/// Gets the word value from the specified offset
/// </summary>
private ushort GetWord(int offset) =>
(ushort)(HeaderBytes[offset] + 256 * HeaderBytes[offset + 1]);
/// <summary>
/// Sets the word value at the specified offset
/// </summary>
private void SetWord(int offset, ushort value)
{
HeaderBytes[offset] = (byte)(value & 0xff);
HeaderBytes[offset + 1] = (byte)(value >> 8);
CalcChecksum();
}
}
/// <summary>
/// This enum defines the MIC pulse types according to their widths
/// </summary>
public enum MicPulseType : byte
{
/// <summary>
/// No pulse information
/// </summary>
None = 0,
/// <summary>
/// Too short to be a valid pulse
/// </summary>
TooShort,
/// <summary>
/// Too long to be a valid pulse
/// </summary>
TooLong,
/// <summary>
/// PILOT pulse (Length: 2168 cycles)
/// </summary>
Pilot,
/// <summary>
/// SYNC1 pulse (Length: 667 cycles)
/// </summary>
Sync1,
/// <summary>
/// SYNC2 pulse (Length: 735 cycles)
/// </summary>
Sync2,
/// <summary>
/// BIT0 pulse (Length: 855 cycles)
/// </summary>
Bit0,
/// <summary>
/// BIT1 pulse (Length: 1710 cycles)
/// </summary>
Bit1,
/// <summary>
/// TERM_SYNC pulse (Length: 947 cycles)
/// </summary>
TermSync
}
/// <summary>
/// Represents the playing phase of the current block
/// </summary>
public enum PlayPhase
{
/// <summary>
/// The player is passive
/// </summary>
None = 0,
/// <summary>
/// Pilot signals
/// </summary>
Pilot,
/// <summary>
/// Sync signals at the end of the pilot
/// </summary>
Sync,
/// <summary>
/// Bits in the data block
/// </summary>
Data,
/// <summary>
/// Short terminating sync signal before pause
/// </summary>
TermSync,
/// <summary>
/// Pause after the data block
/// </summary>
Pause,
/// <summary>
/// The entire block has been played back
/// </summary>
Completed
}
/// <summary>
/// This enumeration defines the phases of the SAVE operation
/// </summary>
public enum SavePhase : byte
{
/// <summary>No SAVE operation is in progress</summary>
None = 0,
/// <summary>Emitting PILOT impulses</summary>
Pilot,
/// <summary>Emitting SYNC1 impulse</summary>
Sync1,
/// <summary>Emitting SYNC2 impulse</summary>
Sync2,
/// <summary>Emitting BIT0/BIT1 impulses</summary>
Data,
/// <summary>Unexpected pulse detected</summary>
Error
}
}

View File

@ -0,0 +1,102 @@
using System.Collections.Generic;
namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
{
/// <summary>
/// This class is responsible to "play" a tape file.
/// </summary>
public class TapeBlockSetPlayer : ISupportsTapeBlockSetPlayback
{
/// <summary>
/// All data blocks that can be played back
/// </summary>
public List<ISupportsTapeBlockPlayback> DataBlocks { get; }
/// <summary>
/// Signs that the player completed playing back the file
/// </summary>
public bool Eof { get; private set; }
/// <summary>
/// Gets the currently playing block's index
/// </summary>
public int CurrentBlockIndex { get; private set; }
/// <summary>
/// The current playable block
/// </summary>
public ISupportsTapeBlockPlayback CurrentBlock => DataBlocks[CurrentBlockIndex];
/// <summary>
/// The current playing phase
/// </summary>
public PlayPhase PlayPhase { get; private set; }
/// <summary>
/// The cycle count of the CPU when playing starts
/// </summary>
public long StartCycle { get; private set; }
public TapeBlockSetPlayer(List<ISupportsTapeBlockPlayback> dataBlocks)
{
DataBlocks = dataBlocks;
Eof = dataBlocks.Count == 0;
}
/// <summary>
/// Initializes the player
/// </summary>
public void InitPlay(long startTact)
{
CurrentBlockIndex = -1;
NextBlock(startTact);
PlayPhase = PlayPhase.None;
StartCycle = startTact;
}
/// <summary>
/// Gets the EAR bit value for the specified cycle
/// </summary>
/// <param name="currentCycle">Cycles to retrieve the EAR bit</param>
/// <returns>
/// A tuple of the EAR bit and a flag that indicates it is time to move to the next block
/// </returns>
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;
}
/// <summary>
/// Moves the current block index to the next playable block
/// </summary>
/// <param name="currentCycle">Cycles time to start the next block</param>
public void NextBlock(long currentCycle)
{
if (CurrentBlockIndex >= DataBlocks.Count - 1)
{
PlayPhase = PlayPhase.Completed;
Eof = true;
return;
}
CurrentBlockIndex++;
CurrentBlock.InitPlay(currentCycle);
}
}
}

View File

@ -0,0 +1,218 @@

namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
{
/// <summary>
/// Represents the standard speed data block in a tape file
/// </summary>
public class TapeDataBlockPlayer : ISupportsTapeBlockPlayback, ITapeData
{
/// <summary>
/// Pause after this block (default: 1000ms)
/// </summary>
public ushort PauseAfter { get; }
/// <summary>
/// Block Data
/// </summary>
public byte[] Data { get; }
/// <summary>
/// Initializes a new instance
/// </summary>
public TapeDataBlockPlayer(byte[] data, ushort pauseAfter)
{
PauseAfter = pauseAfter;
Data = data;
}
/// <summary>
/// Pilot pulse length
/// </summary>
public const int PILOT_PL = 2168;
/// <summary>
/// Pilot pulses in the ROM header block
/// </summary>
public const int HEADER_PILOT_COUNT = 8063;
/// <summary>
/// Pilot pulses in the ROM data block
/// </summary>
public const int DATA_PILOT_COUNT = 3223;
/// <summary>
/// Sync 1 pulse length
/// </summary>
public const int SYNC_1_PL = 667;
/// <summary>
/// Sync 2 pulse lenth
/// </summary>
public const int SYNC_2_PL = 735;
/// <summary>
/// Bit 0 pulse length
/// </summary>
public const int BIT_0_PL = 855;
/// <summary>
/// Bit 1 pulse length
/// </summary>
public const int BIT_1_PL = 1710;
/// <summary>
/// End sync pulse length
/// </summary>
public const int TERM_SYNC = 947;
/// <summary>
/// 1 millisecond pause
/// </summary>
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;
/// <summary>
/// The index of the currently playing byte
/// </summary>
public int ByteIndex { get; private set; }
/// <summary>
/// The mask of the currently playing bit in the current byte
/// </summary>
public byte BitMask { get; private set; }
/// <summary>
/// The current playing phase
/// </summary>
public PlayPhase PlayPhase { get; private set; }
/// <summary>
/// The cycle count of the CPU when playing starts
/// </summary>
public long StartCycle { get; private set; }
/// <summary>
/// Last cycle queried
/// </summary>
public long LastCycle { get; private set; }
/// <summary>
/// Initializes the player
/// </summary>
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;
}
/// <summary>
/// Gets the EAR bit value for the specified cycle
/// </summary>
/// <param name="currentCycle">Tacts to retrieve the EAR bit</param>
/// <returns>
/// The EAR bit value to play back
/// </returns>
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;
}
}
}

View File

@ -0,0 +1,117 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
{
/// <summary>
/// This class recognizes .TZX and .TAP files, and playes back
/// the content accordingly.
/// </summary>
public class TapeFilePlayer : ISupportsTapeBlockPlayback
{
private readonly BinaryReader _reader;
private TapeBlockSetPlayer _player;
/// <summary>
/// Data blocks to play back
/// </summary>
public List<ISupportsTapeBlockPlayback> DataBlocks { get; private set; }
/// <summary>
/// Signs that the player completed playing back the file
/// </summary>
public bool Eof => _player.Eof;
/// <summary>
/// Initializes the player from the specified reader
/// </summary>
/// <param name="reader">BinaryReader instance to get tape file data from</param>
public TapeFilePlayer(BinaryReader reader)
{
_reader = reader;
}
/// <summary>
/// Reads in the content of the tape file so that it can be played
/// </summary>
/// <returns>True, if read was successful; otherwise, false</returns>
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<ISupportsTapeBlockPlayback>()
.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<ISupportsTapeBlockPlayback>()
.ToList();
_player = new TapeBlockSetPlayer(DataBlocks);
return readerFound;
}
/// <summary>
/// Gets the currently playing block's index
/// </summary>
public int CurrentBlockIndex => _player.CurrentBlockIndex;
/// <summary>
/// The current playable block
/// </summary>
public ISupportsTapeBlockPlayback CurrentBlock => _player.CurrentBlock;
/// <summary>
/// The current playing phase
/// </summary>
public PlayPhase PlayPhase => _player.PlayPhase;
/// <summary>
/// The tact count of the CPU when playing starts
/// </summary>
public long StartCycle => _player.StartCycle;
/// <summary>
/// Initializes the player
/// </summary>
public void InitPlay(long startCycle)
{
_player.InitPlay(startCycle);
}
/// <summary>
/// Gets the EAR bit value for the specified cycle
/// </summary>
/// <param name="currentCycle">Tacts to retrieve the EAR bit</param>
/// <returns>
/// A tuple of the EAR bit and a flag that indicates it is time to move to the next block
/// </returns>
public bool GetEarBit(long currentCycle) => _player.GetEarBit(currentCycle);
/// <summary>
/// Moves the current block index to the next playable block
/// </summary>
/// <param name="currentTact">Tacts time to start the next block</param>
public void NextBlock(long currentCycle) => _player.NextBlock(currentCycle);
}
}

View File

@ -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
{
/// <summary>
/// Sinclair Spectrum 48K model
/// </summary>
ZXSpectrum48
}
}

View File

@ -0,0 +1,47 @@

namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
{
/// <summary>
/// Handles all ZX-level input
/// </summary>
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))
{
}
}
}
}

View File

@ -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
{
/// <summary>
/// The abstract class that all emulated models will inherit from
/// * Memory *
/// </summary>
public abstract partial class SpectrumBase
{
/// <summary>
/// ROM Banks
/// </summary>
public byte[] ROM0 = new byte[0x4000];
public byte[] ROM1 = new byte[0x4000];
public byte[] ROM2 = new byte[0x4000];
public byte[] ROM3 = new byte[0x4000];
/// <summary>
/// RAM Banks
/// </summary>
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
/// <summary>
/// Represents the addressable memory space of the spectrum
/// All banks for the emulated system should be added during initialisation
/// </summary>
public Dictionary<int, byte[]> Memory = new Dictionary<int, byte[]>();
/// <summary>
/// Simulates reading from the bus
/// Paging should be handled here
/// </summary>
/// <param name="addr"></param>
/// <returns></returns>
public virtual byte ReadBus(ushort addr)
{
throw new NotImplementedException("Must be overriden");
}
/// <summary>
/// Simulates writing to the bus
/// Paging should be handled here
/// </summary>
/// <param name="addr"></param>
/// <param name="value"></param>
public virtual void WriteBus(ushort addr, byte value)
{
throw new NotImplementedException("Must be overriden");
}
/// <summary>
/// Reads a byte of data from a specified memory address
/// (with memory contention if appropriate)
/// </summary>
/// <param name="addr"></param>
/// <returns></returns>
public virtual byte ReadMemory(ushort addr)
{
throw new NotImplementedException("Must be overriden");
}
/*
/// <summary>
/// Reads a byte of data from a specified memory address
/// (with no memory contention)
/// </summary>
/// <param name="addr"></param>
/// <returns></returns>
public virtual byte PeekMemory(ushort addr)
{
var data = ReadBus(addr);
return data;
}
*/
/// <summary>
/// Writes a byte of data to a specified memory address
/// (with memory contention if appropriate)
/// </summary>
/// <param name="addr"></param>
/// <param name="value"></param>
public virtual void WriteMemory(ushort addr, byte value)
{
throw new NotImplementedException("Must be overriden");
}
/*
/// <summary>
/// Writes a byte of data to a specified memory address
/// (without contention)
/// </summary>
/// <param name="addr"></param>
/// <param name="value"></param>
public virtual void PokeMemory(ushort addr, byte value)
{
if (addr < 0x4000)
{
// Do nothing - we cannot write to ROM
return;
}
WriteBus(addr, value);
}
*/
/// <summary>
/// Fills memory from buffer
/// </summary>
/// <param name="buffer"></param>
/// <param name="startAddress"></param>
public virtual void FillMemory(byte[] buffer, ushort startAddress)
{
//buffer?.CopyTo(RAM, startAddress);
}
/// <summary>
/// Sets up the ROM
/// </summary>
/// <param name="buffer"></param>
/// <param name="startAddress"></param>
public virtual void InitROM(RomData romData)
{
RomData = romData;
// for 16/48k machines only ROM0 is used (no paging)
RomData.RomBytes?.CopyTo(ROM0, 0);
}
/// <summary>
/// ULA reads the memory at the specified address
/// (No memory contention)
/// </summary>
/// <param name="addr"></param>
/// <returns></returns>
public virtual byte FetchScreenMemory(ushort addr)
{
//var value = RAM0[(addr & 0x3FFF)];// + 0x4000];
var value = ReadBus((ushort)((addr & 0x3FFF) + 0x4000));
return value;
}
/// <summary>
/// Helper function to refresh memory array (probably not the best way to do things)
/// </summary>
public virtual void ReInitMemory()
{
throw new NotImplementedException("Must be overriden");
}
/// <summary>
/// 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
/// </summary>
/// <param name="Cycle"></param>
/// <returns></returns>
public virtual byte GetContentionValue(int cycle)
{
var val = _renderingCycleTable[cycle % UlaFrameCycleCount].ContentionDelay;
return val;
}
}
}

View File

@ -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
{
/// <summary>
/// The abstract class that all emulated models will inherit from
/// * Port Access *
/// </summary>
public abstract partial class SpectrumBase
{
/// <summary>
/// The last OUT data that was sent to the ULA
/// </summary>
protected byte LastULAOutByte;
/// <summary>
/// Reads a byte of data from a specified port address
/// </summary>
/// <param name="port"></param>
/// <returns></returns>
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;
}
/// <summary>
/// Writes a byte of data to a specified port address
/// </summary>
/// <param name="port"></param>
/// <param name="value"></param>
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);
}
}
}
}

View File

@ -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.
*/
/// <summary>
/// The abstract class that all emulated models will inherit from
/// * Screen *
/// </summary>
public abstract partial class SpectrumBase : IVideoProvider
{
#region State
/// <summary>
/// The main screen buffer
/// </summary>
protected byte[] _frameBuffer;
/// <summary>
/// Pixel and attribute info stored while rendering the screen
/// </summary>
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
/// <summary>
/// The standard ULA palette
/// </summary>
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
/// <summary>
/// The number of displayed pixels in a display row
/// </summary>
protected int DisplayWidth = 256;
/// <summary>
/// Number of display lines
/// </summary>
protected int DisplayLines = 192;
/// <summary>
/// The number of frames after the flash is toggled
/// </summary>
protected int FlashToggleFrames = 25;
/// <summary>
/// Number of lines used for vertical sync
/// </summary>
protected int VerticalSyncLines = 8;
/// <summary>
/// The number of top border lines that are not visible
/// when rendering the screen
/// </summary>
protected int NonVisibleBorderTopLines = 8;
/// <summary>
/// The number of border lines before the display
/// </summary>
protected int BorderTopLines = 48;
/// <summary>
/// The number of border lines after the display
/// </summary>
protected int BorderBottomLines = 48;
/// <summary>
/// The number of bottom border lines that are not visible
/// when rendering the screen
/// </summary>
protected int NonVisibleBorderBottomLines = 8;
/// <summary>
/// The total number of lines in the screen
/// </summary>
protected int ScreenLines;
/// <summary>
/// The first screen line that contains the top left display pixel
/// </summary>
protected int FirstDisplayLine;
/// <summary>
/// The last screen line that contains the bottom right display pixel
/// </summary>
protected int LastDisplayLine;
/// <summary>
/// The number of border pixels to the left of the display
/// </summary>
protected int BorderLeftPixels = 48;
/// <summary>
/// The number of border pixels to the right of the display
/// </summary>
protected int BorderRightPixels = 48;
/// <summary>
/// The total width of the screen in pixels
/// </summary>
protected int ScreenWidth;
/// <summary>
/// Horizontal blanking time (HSync+blanking).
/// Given in Z80 clock cycles.
/// </summary>
protected int HorizontalBlankingTime = 40;
/// <summary>
/// The time of displaying left part of the border.
/// Given in Z80 clock cycles.
/// </summary>
protected int BorderLeftTime = 24;
/// <summary>
/// The time of displaying a pixel row.
/// Given in Z80 clock cycles.
/// </summary>
protected int DisplayLineTime = 128;
/// <summary>
/// The time of displaying right part of the border.
/// Given in Z80 clock cycles.
/// </summary>
protected int BorderRightTime = 24;
/// <summary>
/// The time used to render the nonvisible right part of the border.
/// Given in Z80 clock cycles.
/// </summary>
protected int NonVisibleBorderRightTime = 8;
/// <summary>
/// The time of displaying a full screen line.
/// Given in Z80 clock cycles.
/// </summary>
protected int ScreenLineTime;
/// <summary>
/// The time the data of a particular pixel should be prefetched
/// before displaying it.
/// Given in Z80 clock cycles.
/// </summary>
protected int PixelDataPrefetchTime = 2;
/// <summary>
/// The time the data of a particular pixel attribute should be prefetched
/// before displaying it.
/// Given in Z80 clock cycles.
/// </summary>
protected int AttributeDataPrefetchTime = 1;
/// <summary>
/// The tact within the line that should display the first pixel.
/// Given in Z80 clock cycles.
/// </summary>
protected int FirstPixelCycleInLine;
/// <summary>
/// The tact in which the top left pixel should be displayed.
/// Given in Z80 clock cycles.
/// </summary>
protected int FirstDisplayPixelCycle;
/// <summary>
/// The tact in which the top left screen pixel (border) should be displayed
/// </summary>
protected int FirstScreenPixelCycle;
/// <summary>
/// Defines the number of Z80 clock cycles used for the full rendering
/// of the screen.
/// </summary>
public int UlaFrameCycleCount;
/// <summary>
/// The last rendered ULA cycle
/// </summary>
public int LastRenderedULACycle;
/// <summary>
/// This structure defines information related to a particular T-State
/// (cycle) of ULA screen rendering
/// </summary>
[StructLayout(LayoutKind.Explicit)]
public struct ScreenRenderingCycle
{
/// <summary>
/// Tha rendering phase to be applied for the particular tact
/// </summary>
[FieldOffset(0)]
public ScreenRenderingPhase Phase;
/// <summary>
/// Display memory contention delay
/// </summary>
[FieldOffset(1)]
public byte ContentionDelay;
/// <summary>
/// Display memory address used in the particular tact
/// </summary>
[FieldOffset(2)]
public ushort PixelByteToFetchAddress;
/// <summary>
/// Display memory address used in the particular tact
/// </summary>
[FieldOffset(4)]
public ushort AttributeToFetchAddress;
/// <summary>
/// Pixel X coordinate
/// </summary>
[FieldOffset(6)]
public ushort XPos;
/// <summary>
/// Pixel Y coordinate
/// </summary>
[FieldOffset(8)]
public ushort YPos;
}
/// <summary>
/// This enumeration defines the particular phases of ULA rendering
/// </summary>
public enum ScreenRenderingPhase : byte
{
/// <summary>
/// The ULA does not do any rendering
/// </summary>
None,
/// <summary>
/// The ULA simple sets the border color to display the current pixel.
/// </summary>
Border,
/// <summary>
/// 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.
/// </summary>
BorderAndFetchPixelByte,
/// <summary>
/// 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.
/// </summary>
BorderAndFetchPixelAttribute,
/// <summary>
/// The ULA displays the next two pixels of Byte1 sequentially during a
/// single Z80 clock cycle.
/// </summary>
DisplayByte1,
/// <summary>
/// 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.
/// </summary>
DisplayByte1AndFetchByte2,
/// <summary>
/// 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.
/// </summary>
DisplayByte1AndFetchAttribute2,
/// <summary>
/// The ULA displays the next two pixels of Byte2 sequentially during a
/// single Z80 clock cycle.
/// </summary>
DisplayByte2,
/// <summary>
/// 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.
/// </summary>
DisplayByte2AndFetchByte1,
/// <summary>
/// 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.
/// </summary>
DisplayByte2AndFetchAttribute1
}
#endregion
#region Border
private int _borderColour;
/// <summary>
/// Gets or sets the ULA border color
/// </summary>
public int BorderColour
{
get { return _borderColour; }
set { _borderColour = value & 0x07; }
}
protected virtual void ResetBorder()
{
BorderColour = 0;
}
#endregion
#region Screen Methods
/// <summary>
/// ULA renders the screen between two specified T-States (cycles)
/// </summary>
/// <param name="fromCycle"></param>
/// <param name="toCycle"></param>
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;
}
}
}
/// <summary>
/// Tests whether the specified cycle is in the visible area of the screen.
/// </summary>
/// <param name="line">Line index</param>
/// <param name="cycleInLine">Tacts index within the line</param>
/// <returns>
/// True, if the tact is visible on the screen; otherwise, false
/// </returns>
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;
}
/// <summary>
/// Tests whether the cycle is in the display area of the screen.
/// </summary>
/// <param name="line">Line index</param>
/// <param name="cycleInLine">Tacts index within the line</param>
/// <returns>
/// True, if the tact is within the display area of the screen; otherwise, false.
/// </returns>
public virtual bool IsCycleInDisplayArea(int line, int cycleInLine)
{
return line >= FirstDisplayLine
&& line <= LastDisplayLine
&& cycleInLine >= FirstPixelCycleInLine
&& cycleInLine < FirstPixelCycleInLine + DisplayLineTime;
}
/// <summary>
/// Sets the two adjacent screen pixels belonging to the specified cycle to the given
/// color
/// </summary>
/// <param name="colorIndex1">Color index of the first pixel</param>
/// <param name="colorIndex2">Color index of the second pixel</param>
protected virtual void SetPixels(int colorIndex1, int colorIndex2)
{
var pos = _yPos * ScreenWidth + _xPos;
_frameBuffer[pos++] = (byte)colorIndex1;
_frameBuffer[pos] = (byte)colorIndex2;
}
/// <summary>
/// Gets the color index for the specified pixel value according
/// to the given color attribute
/// </summary>
/// <param name="pixelValue">0 for paper pixel, non-zero for ink pixel</param>
/// <param name="attr">Color attribute</param>
/// <returns></returns>
protected virtual int GetColor(int pixelValue, byte attr)
{
var offset = (pixelValue == 0 ? 0 : 0x100) + attr;
return _flashPhase
? _flashOnColors[offset]
: _flashOffColors[offset];
}
/// <summary>
/// Resets the ULA cycle to start screen rendering from the beginning
/// </summary>
protected virtual void ResetULACycle()
{
LastRenderedULACycle = -1;
}
/// <summary>
/// Initializes the ULA cycle table
/// </summary>
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;
}
}
/// <summary>
/// Calculates the pixel address for the specified line and tact within
/// the line
/// </summary>
/// <param name="line">Line index</param>
/// <param name="tactInLine">Tacts index within the line</param>
/// <returns>ZX spectrum screen memory address</returns>
/// <remarks>
/// 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 |
/// =================================================================
/// </remarks>
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
}
/// <summary>
/// Calculates the pixel attribute address for the specified line and
/// tact within the line
/// </summary>
/// <param name="line">Line index</param>
/// <param name="tactInLine">Tacts index within the line</param>
/// <returns>ZX spectrum screen memory address</returns>
/// <remarks>
/// 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 |
/// =================================================================
/// </remarks>
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
/// <summary>
/// Initialises the screen configuration calculations
/// </summary>
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;
}
/// <summary>
/// Inits the screen
/// </summary>
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
/// <summary>
/// The longest instruction cycle count
/// </summary>
protected const int LONGEST_OP_CYCLES = 23;
/// <summary>
/// The ULA cycle to raise the interrupt at
/// </summary>
protected int InterruptCycle = 32;
/// <summary>
/// Signs that an interrupt has been raised in this frame.
/// </summary>
protected bool InterruptRaised;
/// <summary>
/// Signs that the interrupt signal has been revoked
/// </summary>
protected bool InterruptRevoked;
/// <summary>
/// Resets the interrupt - this should happen every frame in order to raise
/// the VBLANK interrupt in the proceding frame
/// </summary>
public virtual void ResetInterrupt()
{
InterruptRaised = false;
InterruptRevoked = false;
}
/// <summary>
/// Generates an interrupt in the current phase if needed
/// </summary>
/// <param name="currentCycle"></param>
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
}
}

View File

@ -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
{
/// <summary>
/// The abstract class that all emulated models will inherit from
/// * Sound *
/// </summary>
public abstract partial class SpectrumBase
{
}
}

View File

@ -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
{
/// <summary>
/// The abstract class that all emulated models will inherit from
/// * Main properties / fields / contruction*
/// </summary>
public abstract partial class SpectrumBase
{
/// <summary>
/// The calling ZXSpectrum class (piped in via constructor)
/// </summary>
public ZXSpectrum Spectrum { get; set; }
/// <summary>
/// Reference to the instantiated Z80 cpu (piped in via constructor)
/// </summary>
public Z80A CPU { get; set; }
/// <summary>
/// ROM and extended info
/// </summary>
public RomData RomData { get; set; }
/// <summary>
/// The spectrum buzzer/beeper
/// </summary>
public Buzzer BuzzerDevice { get; set; }
/// <summary>
/// The spectrum keyboard
/// </summary>
public virtual IKeyboard KeyboardDevice { get; set; }
/// <summary>
/// The spectrum datacorder device
/// </summary>
public virtual Tape TapeDevice { get; set; }
/// <summary>
/// The tape provider
/// </summary>
public virtual ITapeProvider TapeProvider { get; set; }
/// <summary>
/// Signs whether the frame has ended
/// </summary>
public bool FrameCompleted;
/// <summary>
/// Overflow from the previous frame (in Z80 cycles)
/// </summary>
public int OverFlow;
/// <summary>
/// The total number of frames rendered
/// </summary>
public int FrameCount;
/// <summary>
/// The current cycle (T-State) that we are at in the frame
/// </summary>
public int _frameCycles;
/// <summary>
/// Stores where we are in the frame after each CPU cycle
/// </summary>
public int LastFrameStartCPUTick;
/// <summary>
/// Gets the current frame cycle according to the CPU tick count
/// </summary>
public virtual int CurrentFrameCycle => CPU.TotalExecutedCycles - LastFrameStartCPUTick;
/// <summary>
/// Mask constants
/// </summary>
protected const int BORDER_BIT = 0x07;
protected const int EAR_BIT = 0x10;
protected const int MIC_BIT = 0x08;
protected const int TAPE_BIT = 0x40;
/// <summary>
/// Executes a single frame
/// </summary>
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);
}
/// <summary>
/// Hard reset of the emulated machine
/// </summary>
public virtual void HardReset()
{
ResetBorder();
ResetInterrupt();
}
/// <summary>
/// Soft reset of the emulated machine
/// </summary>
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();
}
}
}

View File

@ -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
{
/// <summary>
/// The 48k keyboard device
/// </summary>
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();
}
}
}

View File

@ -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
/// <summary>
/// Main constructor
/// </summary>
/// <param name="spectrum"></param>
/// <param name="cpu"></param>
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 +--------+
*/
/// <summary>
/// Simulates reading from the bus (no contention)
/// Paging should be handled here
/// </summary>
/// <param name="addr"></param>
/// <returns></returns>
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];
}
/// <summary>
/// Simulates writing to the bus (no contention)
/// Paging should be handled here
/// </summary>
/// <param name="addr"></param>
/// <param name="value"></param>
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;
}
/// <summary>
/// Reads a byte of data from a specified memory address
/// (with memory contention if appropriate)
/// </summary>
/// <param name="addr"></param>
/// <returns></returns>
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;
}
/// <summary>
/// Writes a byte of data to a specified memory address
/// (with memory contention if appropriate)
/// </summary>
/// <param name="addr"></param>
/// <param name="value"></param>
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
}
}

View File

@ -0,0 +1,90 @@

using System.IO;
namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
{
/// <summary>
/// This class describes a TAP Block
/// </summary>
public sealed class TapDataBlock :
ITapeData,
ITapeDataSerialization,
ISupportsTapeBlockPlayback
{
private TapeDataBlockPlayer _player;
/// <summary>
/// Block Data
/// </summary>
public byte[] Data { get; private set; }
/// <summary>
/// Pause after this block (given in milliseconds)
/// </summary>
public ushort PauseAfter => 1000;
/// <summary>
/// Reads the content of the block from the specified binary stream.
/// </summary>
/// <param name="reader">Stream to read the block from</param>
public void ReadFrom(BinaryReader reader)
{
var length = reader.ReadUInt16();
Data = reader.ReadBytes(length);
}
/// <summary>
/// Writes the content of the block to the specified binary stream.
/// </summary>
/// <param name="writer">Stream to write the block to</param>
public void WriteTo(BinaryWriter writer)
{
writer.Write((ushort)Data.Length);
writer.Write(Data);
}
/// <summary>
/// The index of the currently playing byte
/// </summary>
/// <remarks>This proprty is made public for test purposes</remarks>
public int ByteIndex => _player.ByteIndex;
/// <summary>
/// The mask of the currently playing bit in the current byte
/// </summary>
public byte BitMask => _player.BitMask;
/// <summary>
/// The current playing phase
/// </summary>
public PlayPhase PlayPhase => _player.PlayPhase;
/// <summary>
/// The tact count of the CPU when playing starts
/// </summary>
public long StartCycle => _player.StartCycle;
/// <summary>
/// Last tact queried
/// </summary>
public long LastCycle => _player.LastCycle;
/// <summary>
/// Initializes the player
/// </summary>
public void InitPlay(long startTact)
{
_player = new TapeDataBlockPlayer(Data, PauseAfter);
_player.InitPlay(startTact);
}
/// <summary>
/// Gets the EAR bit value for the specified tact
/// </summary>
/// <param name="currentTact">Tacts to retrieve the EAR bit</param>
/// <returns>
/// The EAR bit value to play back
/// </returns>
public bool GetEarBit(long currentTact) => _player.GetEarBit(currentTact);
}
}

View File

@ -0,0 +1,85 @@

using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
{
/// <summary>
/// This class is responsible to "play" a TAP file.
/// </summary>
public class TapPlayer : TapReader, ISupportsTapeBlockPlayback
{
private TapeBlockSetPlayer _player;
/// <summary>
/// Signs that the player completed playing back the file
/// </summary>
public bool Eof => _player.Eof;
/// <summary>
/// Initializes the player from the specified reader
/// </summary>
/// <param name="reader">BinaryReader instance to get TZX file data from</param>
public TapPlayer(BinaryReader reader) : base(reader)
{
}
/// <summary>
/// Reads in the content of the TZX file so that it can be played
/// </summary>
/// <returns>True, if read was successful; otherwise, false</returns>
public override bool ReadContent()
{
var success = base.ReadContent();
var blocks = DataBlocks.Cast<ISupportsTapeBlockPlayback>()
.ToList();
_player = new TapeBlockSetPlayer(blocks);
return success;
}
/// <summary>
/// Gets the currently playing block's index
/// </summary>
public int CurrentBlockIndex => _player.CurrentBlockIndex;
/// <summary>
/// The current playable block
/// </summary>
public ISupportsTapeBlockPlayback CurrentBlock => _player.CurrentBlock;
/// <summary>
/// The current playing phase
/// </summary>
public PlayPhase PlayPhase => _player.PlayPhase;
/// <summary>
/// The tact count of the CPU when playing starts
/// </summary>
public long StartCycle => _player.StartCycle;
/// <summary>
/// Initializes the player
/// </summary>
public void InitPlay(long startCycle)
{
_player.InitPlay(startCycle);
}
/// <summary>
/// Gets the EAR bit value for the specified tact
/// </summary>
/// <param name="currentCycle">Tacts to retrieve the EAR bit</param>
/// <returns>
/// A tuple of the EAR bit and a flag that indicates it is time to move to the next block
/// </returns>
public bool GetEarBit(long currentCycle) => _player.GetEarBit(currentCycle);
/// <summary>
/// Moves the current block index to the next playable block
/// </summary>
/// <param name="currentCycle">Tacts time to start the next block</param>
public void NextBlock(long currentCycle) => _player.NextBlock(currentCycle);
}
}

View File

@ -0,0 +1,52 @@

using System.Collections.Generic;
using System.IO;
namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
{
/// <summary>
/// This class reads a TAP file
/// </summary>
public class TapReader
{
private readonly BinaryReader _reader;
/// <summary>
/// Data blocks of this TZX file
/// </summary>
public IList<TapDataBlock> DataBlocks { get; }
/// <summary>
/// Initializes the player from the specified reader
/// </summary>
/// <param name="reader"></param>
public TapReader(BinaryReader reader)
{
_reader = reader;
DataBlocks = new List<TapDataBlock>();
}
/// <summary>
/// Reads in the content of the TZX file so that it can be played
/// </summary>
/// <returns>True, if read was successful; otherwise, false</returns>
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;
}
}
}
}

View File

@ -0,0 +1,172 @@

using System;
using System.IO;
using System.Text;
namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
{
/// <summary>
/// This class describes a TZX Block
/// </summary>
public abstract class TzxDataBlockBase : ITapeDataSerialization
{
/// <summary>
/// The ID of the block
/// </summary>
public abstract int BlockId { get; }
/// <summary>
/// Reads the content of the block from the specified binary stream.
/// </summary>
/// <param name="reader">Stream to read the block from</param>
public abstract void ReadFrom(BinaryReader reader);
/// <summary>
/// Writes the content of the block to the specified binary stream.
/// </summary>
/// <param name="writer">Stream to write the block to</param>
public abstract void WriteTo(BinaryWriter writer);
/// <summary>
/// Override this method to check the content of the block
/// </summary>
public virtual bool IsValid => true;
/// <summary>
/// Reads the specified number of words from the reader.
/// </summary>
/// <param name="reader">Reader to obtain the input from</param>
/// <param name="count">Number of words to get</param>
/// <returns>Word array read from the input</returns>
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;
}
/// <summary>
/// Writes the specified array of words to the writer
/// </summary>
/// <param name="writer">Output</param>
/// <param name="words">Word array</param>
public static void WriteWords(BinaryWriter writer, ushort[] words)
{
foreach (var word in words)
{
writer.Write(word);
}
}
/// <summary>
/// Converts the provided bytes to an ASCII string
/// </summary>
/// <param name="bytes">Bytes to convert</param>
/// <param name="offset">First byte offset</param>
/// <param name="count">Number of bytes</param>
/// <returns>ASCII string representation</returns>
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();
}
}
/// <summary>
/// Base class for all TZX block type with data length of 3 bytes
/// </summary>
public abstract class Tzx3ByteDataBlockBase : TzxDataBlockBase
{
/// <summary>
/// Used bits in the last byte (other bits should be 0)
/// </summary>
/// <remarks>
/// (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)
/// </remarks>
public byte LastByteUsedBits { get; set; }
/// <summary>
/// Lenght of block data
/// </summary>
public byte[] DataLength { get; set; }
/// <summary>
/// Block Data
/// </summary>
public byte[] Data { get; set; }
/// <summary>
/// Override this method to check the content of the block
/// </summary>
public override bool IsValid => GetLength() == Data.Length;
/// <summary>
/// Calculates data length
/// </summary>
protected int GetLength()
{
return DataLength[0] + DataLength[1] << 8 + DataLength[2] << 16;
}
}
/// <summary>
/// This class represents a TZX data block with empty body
/// </summary>
public abstract class TzxBodylessDataBlockBase : TzxDataBlockBase
{
/// <summary>
/// Reads the content of the block from the specified binary stream.
/// </summary>
/// <param name="reader">Stream to read the block from</param>
public override void ReadFrom(BinaryReader reader)
{
}
/// <summary>
/// Writes the content of the block to the specified binary stream.
/// </summary>
/// <param name="writer">Stream to write the block to</param>
public override void WriteTo(BinaryWriter writer)
{
}
}
/// <summary>
/// This class represents a deprecated block
/// </summary>
public abstract class TzxDeprecatedDataBlockBase : TzxDataBlockBase
{
/// <summary>
/// Reads through the block infromation, and does not store it
/// </summary>
/// <param name="reader">Stream to read the block from</param>
public abstract void ReadThrough(BinaryReader reader);
/// <summary>
/// Reads the content of the block from the specified binary stream.
/// </summary>
/// <param name="reader">Stream to read the block from</param>
public override void ReadFrom(BinaryReader reader)
{
}
/// <summary>
/// Writes the content of the block to the specified binary stream.
/// </summary>
/// <param name="writer">Stream to write the block to</param>
public override void WriteTo(BinaryWriter writer)
{
throw new InvalidOperationException("Deprecated TZX data blocks cannot be written.");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,250 @@

using System.IO;
namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
{
/// <summary>
/// This blocks contains information about the hardware that the programs on this tape use.
/// </summary>
public class TzxHwInfo : ITapeDataSerialization
{
/// <summary>
/// Hardware type
/// </summary>
public byte HwType { get; set; }
/// <summary>
/// Hardwer Id
/// </summary>
public byte HwId { get; set; }
/// <summary>
/// Information about the tape
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public byte TapeInfo;
/// <summary>
/// Reads the content of the block from the specified binary stream.
/// </summary>
/// <param name="reader">Stream to read the block from</param>
public void ReadFrom(BinaryReader reader)
{
HwType = reader.ReadByte();
HwId = reader.ReadByte();
TapeInfo = reader.ReadByte();
}
/// <summary>
/// Writes the content of the block to the specified binary stream.
/// </summary>
/// <param name="writer">Stream to write the block to</param>
public void WriteTo(BinaryWriter writer)
{
writer.Write(HwType);
writer.Write(HwId);
writer.Write(TapeInfo);
}
}
/// <summary>
/// Symbol repetitions
/// </summary>
public struct TzxPrle
{
/// <summary>
/// Symbol represented
/// </summary>
public byte Symbol;
/// <summary>
/// Number of repetitions
/// </summary>
public ushort Repetitions;
}
/// <summary>
/// This block represents an extremely wide range of data encoding techniques.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public class TzxSymDef : ITapeDataSerialization
{
/// <summary>
/// Bit 0 - Bit 1: Starting symbol polarity
/// </summary>
/// <remarks>
/// 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
/// </remarks>
public byte SymbolFlags;
/// <summary>
/// The array of pulse lengths
/// </summary>
public ushort[] PulseLengths;
public TzxSymDef(byte maxPulses)
{
PulseLengths = new ushort[maxPulses];
}
/// <summary>
/// Reads the content of the block from the specified binary stream.
/// </summary>
/// <param name="reader">Stream to read the block from</param>
public void ReadFrom(BinaryReader reader)
{
SymbolFlags = reader.ReadByte();
PulseLengths = TzxDataBlockBase.ReadWords(reader, PulseLengths.Length);
}
/// <summary>
/// Writes the content of the block to the specified binary stream.
/// </summary>
/// <param name="writer">Stream to write the block to</param>
public void WriteTo(BinaryWriter writer)
{
writer.Write(SymbolFlags);
TzxDataBlockBase.WriteWords(writer, PulseLengths);
}
}
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public class TzxText : ITapeDataSerialization
{
/// <summary>
/// Text identification byte.
/// </summary>
/// <remarks>
/// 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)
/// </remarks>
public byte Type { get; set; }
/// <summary>
/// Length of the description
/// </summary>
public byte Length { get; set; }
/// <summary>
/// The description bytes
/// </summary>
public byte[] TextBytes;
/// <summary>
/// The string form of description
/// </summary>
public string Text => TzxDataBlockBase.ToAsciiString(TextBytes);
/// <summary>
/// Reads the content of the block from the specified binary stream.
/// </summary>
/// <param name="reader">Stream to read the block from</param>
public void ReadFrom(BinaryReader reader)
{
Type = reader.ReadByte();
Length = reader.ReadByte();
TextBytes = reader.ReadBytes(Length);
}
/// <summary>
/// Writes the content of the block to the specified binary stream.
/// </summary>
/// <param name="writer">Stream to write the block to</param>
public void WriteTo(BinaryWriter writer)
{
writer.Write(Type);
writer.Write(Length);
writer.Write(TextBytes);
}
}
/// <summary>
/// This block represents select structure
/// </summary>
public class TzxSelect : ITapeDataSerialization
{
/// <summary>
/// Bit 0 - Bit 1: Starting symbol polarity
/// </summary>
/// <remarks>
/// 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
/// </remarks>
public ushort BlockOffset;
/// <summary>
/// Length of the description
/// </summary>
public byte DescriptionLength { get; set; }
/// <summary>
/// The description bytes
/// </summary>
public byte[] Description;
/// <summary>
/// The string form of description
/// </summary>
public string DescriptionText => TzxDataBlockBase.ToAsciiString(Description);
public TzxSelect(byte length)
{
DescriptionLength = length;
}
/// <summary>
/// Reads the content of the block from the specified binary stream.
/// </summary>
/// <param name="reader">Stream to read the block from</param>
public void ReadFrom(BinaryReader reader)
{
BlockOffset = reader.ReadUInt16();
DescriptionLength = reader.ReadByte();
Description = reader.ReadBytes(DescriptionLength);
}
/// <summary>
/// Writes the content of the block to the specified binary stream.
/// </summary>
/// <param name="writer">Stream to write the block to</param>
public void WriteTo(BinaryWriter writer)
{
writer.Write(BlockOffset);
writer.Write(DescriptionLength);
writer.Write(Description);
}
}
}

View File

@ -0,0 +1,282 @@

namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
{
/// <summary>
/// Identified AD or DA converter types
/// </summary>
public enum TzxAdOrDaConverterType : byte
{
HarleySystemsAdc8P2 = 0x00,
BlackboardElectronics = 0x01
}
/// <summary>
/// Identified computer types
/// </summary>
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
}
/// <summary>
/// Identified digitizer types
/// </summary>
public enum TzxDigitizerType : byte
{
RdDigitalTracer = 0x00,
DkTronicsLightPen = 0x01,
MicrographPad = 0x02,
RomnticRobotVideoface = 0x03
}
/// <summary>
/// Identified EPROM programmer types
/// </summary>
public enum TzxEpromProgrammerType : byte
{
OrmeElectronics = 0x00
}
/// <summary>
/// Identified external storage types
/// </summary>
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
}
/// <summary>
/// Identified graphics types
/// </summary>
public enum TzxGraphicsType : byte
{
WrxHiRes = 0x00,
G007 = 0x01,
Memotech = 0x02,
LambdaColour = 0x03
}
/// <summary>
/// Represents the hardware types that can be defined
/// </summary>
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
}
/// <summary>
/// Identified joystick types
/// </summary>
public enum TzxJoystickType
{
Kempston = 0x00,
ProtekCursor = 0x01,
Sinclair2Left = 0x02,
Sinclair1Right = 0x03,
Fuller = 0x04
}
/// <summary>
/// Identified keyboard and keypad types
/// </summary>
public enum TzxKeyboardType : byte
{
KeypadForZxSpectrum128K = 0x00
}
/// <summary>
/// Identified modem types
/// </summary>
public enum TzxModemTypes : byte
{
PrismVtx5000 = 0x00,
Westridge2050 = 0x01
}
/// <summary>
/// Identified mouse types
/// </summary>
public enum TzxMouseType : byte
{
AmxMouse = 0x00,
KempstonMouse = 0x01
}
/// <summary>
/// Identified network adapter types
/// </summary>
public enum TzxNetworkAdapterType : byte
{
ZxInterface1 = 0x00
}
/// <summary>
/// Identified other controller types
/// </summary>
public enum TzxOtherControllerType : byte
{
Trisckstick = 0x00,
ZxLightGun = 0x01,
ZebraGraphicTablet = 0x02,
DefnederLightGun = 0x03
}
/// <summary>
/// Identified parallel port types
/// </summary>
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
}
/// <summary>
/// Identified printer types
/// </summary>
public enum TzxPrinterType : byte
{
ZxPrinter = 0x00,
GenericPrinter = 0x01,
EpsonCompatible = 0x02
}
/// <summary>
/// Identifier ROM or RAM add-on types
/// </summary>
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
}
/// <summary>
/// Identified serial port types
/// </summary>
public enum TzxSerialPortType : byte
{
ZxInterface1 = 0x00,
ZxSpectrum128 = 0x01
}
/// <summary>
/// Identified sound device types
/// </summary>
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
}
}

View File

@ -0,0 +1,28 @@
using System;
namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
{
/// <summary>
/// This class represents a TZX-related exception
/// </summary>
public class TzxException : Exception
{
/// <summary>
/// Initializes the exception with the specified message
/// </summary>
/// <param name="message">Exception message</param>
public TzxException(string message) : base(message)
{
}
/// <summary>
/// Initializes the exception with the specified message
/// and inner exception
/// </summary>
/// <param name="message">Exception message</param>
/// <param name="innerException">Inner exception</param>
public TzxException(string message, Exception innerException) : base(message, innerException)
{
}
}
}

View File

@ -0,0 +1,68 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
{
/// <summary>
/// Represents the header of the TZX file
/// </summary>
public class TzxHeader : TzxDataBlockBase
{
public static IReadOnlyList<byte> TzxSignature =
new ReadOnlyCollection<byte>(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;
}
/// <summary>
/// The ID of the block
/// </summary>
public override int BlockId => 0x00;
/// <summary>
/// Reads the content of the block from the specified binary stream.
/// </summary>
/// <param name="reader">Stream to read the block from</param>
public override void ReadFrom(BinaryReader reader)
{
Signature = reader.ReadBytes(7);
Eot = reader.ReadByte();
MajorVersion = reader.ReadByte();
MinorVersion = reader.ReadByte();
}
/// <summary>
/// Writes the content of the block to the specified binary stream.
/// </summary>
/// <param name="writer">Stream to write the block to</param>
public override void WriteTo(BinaryWriter writer)
{
writer.Write(Signature);
writer.Write(Eot);
writer.Write(MajorVersion);
writer.Write(MinorVersion);
}
#region Overrides of TzxDataBlockBase
/// <summary>
/// Override this method to check the content of the block
/// </summary>
public override bool IsValid => Signature.SequenceEqual(TzxSignature)
&& Eot == 0x1A
&& MajorVersion == 1;
#endregion
}
}

View File

@ -0,0 +1,83 @@
using System.IO;
using System.Linq;
namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
{
/// <summary>
/// This class is responsible to "play" a TZX file.
/// </summary>
public class TzxPlayer : TzxReader, ISupportsTapeBlockPlayback
{
private TapeBlockSetPlayer _player;
/// <summary>
/// Signs that the player completed playing back the file
/// </summary>
public bool Eof => _player.Eof;
/// <summary>
/// Initializes the player from the specified reader
/// </summary>
/// <param name="reader">BinaryReader instance to get TZX file data from</param>
public TzxPlayer(BinaryReader reader) : base(reader)
{
}
/// <summary>
/// Reads in the content of the TZX file so that it can be played
/// </summary>
/// <returns>True, if read was successful; otherwise, false</returns>
public override bool ReadContent()
{
var success = base.ReadContent();
var blocks = DataBlocks.Where(b => b is ISupportsTapeBlockPlayback)
.Cast<ISupportsTapeBlockPlayback>()
.ToList();
_player = new TapeBlockSetPlayer(blocks);
return success;
}
/// <summary>
/// Gets the currently playing block's index
/// </summary>
public int CurrentBlockIndex => _player.CurrentBlockIndex;
/// <summary>
/// The current playable block
/// </summary>
public ISupportsTapeBlockPlayback CurrentBlock => _player.CurrentBlock;
/// <summary>
/// The current playing phase
/// </summary>
public PlayPhase PlayPhase => _player.PlayPhase;
/// <summary>
/// The tact count of the CPU when playing starts
/// </summary>
public long StartCycle => _player.StartCycle;
/// <summary>
/// Initializes the player
/// </summary>
public void InitPlay(long startTact)
{
_player.InitPlay(startTact);
}
/// <summary>
/// Gets the EAR bit value for the specified tact
/// </summary>
/// <param name="currentTact">Tacts to retrieve the EAR bit</param>
/// <returns>
/// A tuple of the EAR bit and a flag that indicates it is time to move to the next block
/// </returns>
public bool GetEarBit(long currentTact) => _player.GetEarBit(currentTact);
/// <summary>
/// Moves the current block index to the next playable block
/// </summary>
/// <param name="currentTact">Tacts time to start the next block</param>
public void NextBlock(long currentTact) => _player.NextBlock(currentTact);
}
}

View File

@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
{
/// <summary>
/// This class reads a TZX file
/// </summary>
public class TzxReader
{
private readonly BinaryReader _reader;
public static Dictionary<byte, Type> DataBlockTypes = new Dictionary<byte, Type>
{
{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)},
};
/// <summary>
/// Data blocks of this TZX file
/// </summary>
public IList<TzxDataBlockBase> DataBlocks { get; }
/// <summary>
/// Major version number of the file
/// </summary>
public byte MajorVersion { get; private set; }
/// <summary>
/// Minor version number of the file
/// </summary>
public byte MinorVersion { get; private set; }
/// <summary>
/// Initializes the player from the specified reader
/// </summary>
/// <param name="reader"></param>
public TzxReader(BinaryReader reader)
{
_reader = reader;
DataBlocks = new List<TzxDataBlockBase>();
}
/// <summary>
/// Reads in the content of the TZX file so that it can be played
/// </summary>
/// <returns>True, if read was successful; otherwise, false</returns>
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;
}
}
}
}

View File

@ -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
{
/// <summary>
/// The MIC and EAR pins in the spectrum deal in on/off pulses of varying lengths
/// This struct therefore represents 1 of these pulses
/// </summary>
public struct Pulse
{
/// <summary>
/// True: High State
/// False: Low State
/// </summary>
public bool State { get; set; }
/// <summary>
/// Pulse length in Z80 T-States (cycles)
/// </summary>
public int Length { get; set; }
}
}

View File

@ -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
{
/// <summary>
/// ROM Contents
/// </summary>
public byte[] RomBytes
{
get { return _romBytes; }
set { _romBytes = value; }
}
/// <summary>
/// Useful ROM addresses that are needed during tape operations
/// </summary>
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();
}
}
}

View File

@ -0,0 +1,85 @@

using BizHawk.Emulation.Common;
namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
{
public partial class ZXSpectrum
{
/// <summary>
/// The standard 48K Spectrum keyboard
/// https://upload.wikimedia.org/wikipedia/commons/thumb/3/33/ZXSpectrum48k.jpg/1200px-ZXSpectrum48k.jpg
/// </summary>
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"
}
};
/// <summary>
/// The newer spectrum keyboard (models 48k+, 128k)
/// https://upload.wikimedia.org/wikipedia/commons/c/ca/ZX_Spectrum%2B.jpg
/// </summary>
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"
}
};
/// <summary>
/// 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
/// </summary>
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"
}
};
}
}

View File

@ -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<string, RegisterValue> GetCpuFlagsAndRegisters()
{
return new Dictionary<string, RegisterValue>
{
["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;
}
}

View File

@ -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;
}
}
}
}

View File

@ -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;
}
}

View File

@ -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<string, MemoryDomainByteArray> _byteArrayDomains = new Dictionary<string, MemoryDomainByteArray>();
private bool _memoryDomainsInit = false;
private void SetupMemoryDomains()
{
var domains = new List<MemoryDomain>
{
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<IMemoryDomains>(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);
}
}
}
}

View File

@ -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<ZXSpectrum.ZXSpectrumSettings, ZXSpectrum.ZXSpectrumSyncSettings>
{
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();
}
}
/// <summary>
/// The size of the Spectrum border
/// </summary>
public enum BorderType
{
Full,
Medium,
Small
}
/// <summary>
/// The speed at which the tape is loaded
/// </summary>
public enum TapeLoadSpeed
{
Accurate,
Fast,
Fastest
}
}
}

View File

@ -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();
}
}
}
}

View File

@ -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);
}
}
/// <summary>
/// Gets the IX word value
/// </summary>
/// <returns></returns>
public ushort Get16BitIX()
{
return Convert.ToUInt16(_cpu.Regs[_cpu.Ixh] | _cpu.Regs[_cpu.Ixl] << 8);
}
/// <summary>
/// Set the IX word value
/// </summary>
/// <param name="Ixh"></param>
/// <param name="Ixl"></param>
public void Set16BitIX(ushort IX)
{
_cpu.Regs[_cpu.Ixh] = (ushort)(IX & 0xFF);
_cpu.Regs[_cpu.Ixl] = (ushort)((IX >> 8) & 0xff);
}
/// <summary>
/// Gets the AF word value
/// </summary>
/// <returns></returns>
public ushort Get16BitAF()
{
return Convert.ToUInt16(_cpu.Regs[_cpu.A] | _cpu.Regs[_cpu.F] << 8);
}
/// <summary>
/// Set the AF word value
/// </summary>
/// <param name="Ixh"></param>
/// <param name="Ixl"></param>
public void Set16BitAF(ushort AF)
{
_cpu.Regs[_cpu.A] = (ushort)(AF & 0xFF);
_cpu.Regs[_cpu.F] = (ushort)((AF >> 8) & 0xff);
}
/// <summary>
/// Gets the AF shadow word value
/// </summary>
/// <returns></returns>
public ushort Get16BitAF_()
{
return Convert.ToUInt16(_cpu.Regs[_cpu.A_s] | _cpu.Regs[_cpu.F_s] << 8);
}
/// <summary>
/// Set the AF shadow word value
/// </summary>
/// <param name="Ixh"></param>
/// <param name="Ixl"></param>
public void Set16BitAF_(ushort AF_)
{
_cpu.Regs[_cpu.A_s] = (ushort)(AF_ & 0xFF);
_cpu.Regs[_cpu.F_s] = (ushort)((AF_ >> 8) & 0xff);
}
/// <summary>
/// Gets the DE word value
/// </summary>
/// <returns></returns>
public ushort Get16BitDE()
{
return Convert.ToUInt16(_cpu.Regs[_cpu.E] | _cpu.Regs[_cpu.D] << 8);
}
/// <summary>
/// Set the DE word value
/// </summary>
/// <param name="Ixh"></param>
/// <param name="Ixl"></param>
public void Set16BitDE(ushort DE)
{
_cpu.Regs[_cpu.D] = (ushort)(DE & 0xFF);
_cpu.Regs[_cpu.E] = (ushort)((DE >> 8) & 0xff);
}
/// <summary>
/// Z80 Status Indicator Flag Reset masks
/// </summary>
/// <seealso cref="FlagsSetMask"/>
[Flags]
public enum FlagsResetMask : byte
{
/// <summary>Sign Flag</summary>
S = 0x7F,
/// <summary>Zero Flag</summary>
Z = 0xBF,
/// <summary>This flag is not used.</summary>
R5 = 0xDF,
/// <summary>Half Carry Flag</summary>
H = 0xEF,
/// <summary>This flag is not used.</summary>
R3 = 0xF7,
/// <summary>Parity/Overflow Flag</summary>
PV = 0xFB,
/// <summary>Add/Subtract Flag</summary>
N = 0xFD,
/// <summary>Carry Flag</summary>
C = 0xFE,
}
/// <summary>
/// Z80 Status Indicator Flag Set masks
/// </summary>
/// <seealso cref="FlagsResetMask"/>
[Flags]
public enum FlagsSetMask : byte
{
/// <summary>Sign Flag</summary>
/// <remarks>
/// 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.
/// </remarks>
S = 0x80,
/// <summary>
/// Zero Flag
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
Z = 0x40,
/// <summary>This flag is not used.</summary>
R5 = 0x20,
/// <summary>Half Carry Flag</summary>
/// <remarks>
/// 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.
/// </remarks>
H = 0x10,
/// <summary>This flag is not used.</summary>
R3 = 0x08,
/// <summary>Parity/Overflow Flag</summary>
/// <remarks>
/// 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.
/// </remarks>
PV = 0x04,
/// <summary>Add/Subtract Flag</summary>
/// <remarks>
/// 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.
/// </remarks>
N = 0x02,
/// <summary>Carry Flag</summary>
/// <remarks>
/// The Carry Flag (C) is set or cleared depending on the operation being performed.
/// </remarks>
C = 0x01,
/// <summary>
/// Combination of S, Z, and PV
/// </summary>
SZPV = S | Z | PV,
/// <summary>
/// Combination of N, and H
/// </summary>
NH = N | H,
/// <summary>
/// Combination of R3, and R5
/// </summary>
R3R5 = R3 | R5
}
}
}

View File

@ -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<ITraceable>(_tracer);
ser.Register<IDisassemblable>(_cpu);
ser.Register<IVideoProvider>(_machine);
ser.Register<ISoundProvider>(_machine.BuzzerDevice);
HardReset();
List<string> romDis = new List<string>();
List<DISA> disas = new List<DISA>();
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
}
}

View File

@ -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