Essgee.Unity/Assets/Plugins/Essgee/Emulation/Machines/SC3000.cs

614 lines
19 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Essgee.Emulation.Configuration;
using Essgee.Emulation.CPU;
using Essgee.Emulation.Video;
using Essgee.Emulation.Audio;
using Essgee.Emulation.Cartridges;
using Essgee.Emulation.Cartridges.Sega;
using Essgee.Emulation.Peripherals;
using Essgee.EventArguments;
using Essgee.Exceptions;
using Essgee.Utilities;
namespace Essgee.Emulation.Machines
{
/* TODO: verify everything, the SC-3000 isn't that well-documented...
*
* Tape cassette notes: PPI port B bit 7 == input from cassette, port C bit 4 == output to cassette
*/
[MachineIndex(1)]
public class SC3000 : IMachine
{
const double masterClockNtsc = 10738635;
const double masterClockPal = 10640684;
const double refreshRateNtsc = 59.922743;
const double refreshRatePal = 49.701459;
const int ramSize = 1 * 2048;
double masterClock;
double vdpClock, psgClock;
public event EventHandler<SendLogMessageEventArgs> SendLogMessage;
protected virtual void OnSendLogMessage(SendLogMessageEventArgs e) { SendLogMessage?.Invoke(this, e); }
public event EventHandler<EventArgs> EmulationReset;
protected virtual void OnEmulationReset(EventArgs e) { EmulationReset?.Invoke(this, e); }
public event EventHandler<RenderScreenEventArgs> RenderScreen
{
add { vdp.RenderScreen += value; }
remove { vdp.RenderScreen -= value; }
}
public event EventHandler<SizeScreenEventArgs> SizeScreen
{
add { vdp.SizeScreen += value; }
remove { vdp.SizeScreen -= value; }
}
public event EventHandler<ChangeViewportEventArgs> ChangeViewport;
protected virtual void OnChangeViewport(ChangeViewportEventArgs e) { ChangeViewport?.Invoke(this, e); }
public event EventHandler<PollInputEventArgs> PollInput;
protected virtual void OnPollInput(PollInputEventArgs e) { PollInput?.Invoke(this, e); }
public event EventHandler<EnqueueSamplesEventArgs> EnqueueSamples
{
add { psg.EnqueueSamples += value; }
remove { psg.EnqueueSamples -= value; }
}
public event EventHandler<SaveExtraDataEventArgs> SaveExtraData;
protected virtual void OnSaveExtraData(SaveExtraDataEventArgs e) { SaveExtraData?.Invoke(this, e); }
public event EventHandler<EventArgs> EnableRumble { add { } remove { } }
public string ManufacturerName => "Sega";
public string ModelName => "SC-3000";
public string DatFilename => "Sega - SG-1000.dat"; // TODO: SC-3000 .dat does not exist?
public (string Extension, string Description) FileFilter => (".sc", "SC-3000 ROMs");
public bool HasBootstrap => false;
public double RefreshRate { get; private set; }
public double PixelAspectRatio => 8.0 / 7.0;
public (string Name, string Description)[] RuntimeOptions => vdp.RuntimeOptions.Concat(psg.RuntimeOptions).ToArray();
ICartridge cartridge;
byte[] wram;
Z80A cpu;
TMS99xxA vdp;
SN76489 psg;
Intel8255 ppi;
bool[,] keyboard;
[Flags]
enum PortAInputs : byte
{
P1Up = (1 << 0),
P1Down = (1 << 1),
P1Left = (1 << 2),
P1Right = (1 << 3),
P1Button1 = (1 << 4),
P1Button2 = (1 << 5),
P2Up = (1 << 6),
P2Down = (1 << 7),
}
[Flags]
enum PortBInputs : byte
{
P2Left = (1 << 0),
P2Right = (1 << 1),
P2Button1 = (1 << 2),
P2Button2 = (1 << 3),
}
PortAInputs portAInputsPressed;
PortBInputs portBInputsPressed;
bool resetButtonPressed, resetButtonToggle;
bool keyboardMode;
bool changeInputButtonPressed;
enum TapeUpdateModes
{
Reading,
Writing
}
bool isTapePlaying;
bool tapePlayButtonPressed;
int currentMasterClockCyclesInFrame, totalMasterClockCyclesInFrame;
Configuration.SC3000 configuration;
public SC3000() { }
public void Initialize()
{
cartridge = null;
wram = new byte[ramSize];
cpu = new Z80A(ReadMemory, WriteMemory, ReadPort, WritePort);
vdp = new TMS99xxA();
psg = new SN76489();
ppi = new Intel8255();
keyboard = new bool[12, 8];
vdp.EndOfScanline += (s, e) =>
{
PollInputEventArgs pollInputEventArgs = new PollInputEventArgs();
OnPollInput(pollInputEventArgs);
ParseInput(pollInputEventArgs);
};
}
public void SetConfiguration(IConfiguration config)
{
configuration = (Configuration.SC3000)config;
ReconfigureSystem();
}
public object GetRuntimeOption(string name)
{
if (name.StartsWith("Graphics"))
return vdp.GetRuntimeOption(name);
else if (name.StartsWith("Audio"))
return psg.GetRuntimeOption(name);
else
return null;
}
public void SetRuntimeOption(string name, object value)
{
if (name.StartsWith("Graphics"))
vdp.SetRuntimeOption(name, value);
else if (name.StartsWith("Audio"))
psg.SetRuntimeOption(name, value);
}
private void ReconfigureSystem()
{
if (configuration.TVStandard == TVStandard.NTSC)
{
masterClock = masterClockNtsc;
RefreshRate = refreshRateNtsc;
}
else
{
masterClock = masterClockPal;
RefreshRate = refreshRatePal;
}
vdpClock = (masterClock / 1.0);
psgClock = (masterClock / 3.0);
vdp?.SetClockRate(vdpClock);
vdp?.SetRefreshRate(RefreshRate);
vdp?.SetRevision(0);
psg?.SetSampleRate(StandInfo.Configuration.SampleRate);
psg?.SetOutputChannels(2);
psg?.SetClockRate(psgClock);
psg?.SetRefreshRate(RefreshRate);
currentMasterClockCyclesInFrame = 0;
totalMasterClockCyclesInFrame = (int)Math.Round(masterClock / RefreshRate);
OnChangeViewport(new ChangeViewportEventArgs(vdp.Viewport));
}
public void Startup()
{
cpu.Startup();
vdp.Startup();
psg.Startup();
}
public void Reset()
{
cpu.Reset();
cpu.SetStackPointer(0xDFF0);
vdp.Reset();
psg.Reset();
ppi.Reset();
for (int i = 0; i < keyboard.GetLength(0); i++)
for (int j = 0; j < keyboard.GetLength(1); j++)
keyboard[i, j] = false;
portAInputsPressed = 0;
portBInputsPressed = 0;
resetButtonPressed = resetButtonToggle = false;
keyboardMode = true;
changeInputButtonPressed = false;
isTapePlaying = false;
tapePlayButtonPressed = false;
OnEmulationReset(EventArgs.Empty);
}
public void Shutdown()
{
cpu?.Shutdown();
vdp?.Shutdown();
psg?.Shutdown();
}
//public void SetState(Dictionary<string, dynamic> state)
public void SetState(Dictionary<string, object> state)
{
configuration.TVStandard = (TVStandard)state[nameof(configuration.TVStandard)];
SaveStateHandler.PerformSetState(cartridge, (Dictionary<string, object>)state[nameof(cartridge)]);
wram = (byte[])state[nameof(wram)];
SaveStateHandler.PerformSetState(cpu, (Dictionary<string, object>)state[nameof(cpu)]);
SaveStateHandler.PerformSetState(vdp, (Dictionary<string, object>)state[nameof(vdp)]);
SaveStateHandler.PerformSetState(psg, (Dictionary<string, object>)state[nameof(psg)]);
SaveStateHandler.PerformSetState(ppi, (Dictionary<string, object>)state[nameof(ppi)]);
keyboard = (bool[,])(state[nameof(keyboard)]);
ReconfigureSystem();
}
public Dictionary<string, dynamic> GetState()
{
return new Dictionary<string, dynamic>
{
[nameof(configuration.TVStandard)] = configuration.TVStandard,
[nameof(cartridge)] = SaveStateHandler.PerformGetState(cartridge),
[nameof(wram)] = wram,
[nameof(cpu)] = SaveStateHandler.PerformGetState(cpu),
[nameof(vdp)] = SaveStateHandler.PerformGetState(vdp),
[nameof(psg)] = SaveStateHandler.PerformGetState(psg),
[nameof(ppi)] = SaveStateHandler.PerformGetState(ppi),
[nameof(keyboard)] = keyboard
};
}
public Dictionary<string, dynamic> GetDebugInformation()
{
var dict = new Dictionary<string, dynamic>
{
{ "CyclesInFrame", currentMasterClockCyclesInFrame },
};
return dict;
}
public void Load(byte[] romData, byte[] ramData, Type mapperType)
{
if (mapperType == null)
mapperType = typeof(SegaSGCartridge);
cartridge = (ICartridge)Activator.CreateInstance(mapperType, new object[] { romData.Length, ramData.Length });
cartridge.LoadRom(romData);
cartridge.LoadRam(ramData);
}
public byte[] GetCartridgeRam()
{
return cartridge.GetRamData();
}
public bool IsCartridgeRamSaveNeeded()
{
return cartridge.IsRamSaveNeeded();
}
public virtual void RunFrame()
{
while (currentMasterClockCyclesInFrame < totalMasterClockCyclesInFrame)
RunStep();
currentMasterClockCyclesInFrame -= totalMasterClockCyclesInFrame;
}
public void RunStep()
{
double currentCpuClockCycles = 0.0;
currentCpuClockCycles += cpu.Step();
double currentMasterClockCycles = (currentCpuClockCycles * 3.0);
vdp.Step((int)Math.Round(currentMasterClockCycles));
if (resetButtonPressed)
{
resetButtonPressed = false;
cpu.SetInterruptLine(InterruptType.NonMaskable, InterruptState.Assert);
}
cpu.SetInterruptLine(InterruptType.Maskable, vdp.InterruptLine);
psg.Step((int)Math.Round(currentCpuClockCycles));
cartridge?.Step((int)Math.Round(currentCpuClockCycles));
currentMasterClockCyclesInFrame += (int)Math.Round(currentMasterClockCycles);
}
private void ParseInput(PollInputEventArgs eventArgs)
{
/* Get variables */
var keysDown = eventArgs.Keyboard;
/* Handle reset button */
var resetPressed = keysDown.Contains(configuration.InputReset);
var resetButtonHeld = (resetButtonToggle && resetPressed);
if (resetPressed)
{
if (!resetButtonHeld) resetButtonPressed = true;
resetButtonToggle = true;
}
else if (resetButtonToggle)
resetButtonToggle = false;
/* Toggle input mode (keyboard or controllers) */
if (keysDown.Contains(configuration.InputChangeMode) && !changeInputButtonPressed)
{
keyboardMode = !keyboardMode;
var modeString = (keyboardMode ? "keyboard" : "controller");
SendLogMessage(this, new SendLogMessageEventArgs($"Selected {modeString} mode."));
}
changeInputButtonPressed = keysDown.Contains(configuration.InputChangeMode);
/* Toggle tape playback */
if (keysDown.Contains(configuration.InputPlayTape) && !tapePlayButtonPressed)
{
isTapePlaying = !isTapePlaying;
var playString = (isTapePlaying ? "playing" : "stopped");
SendLogMessage(this, new SendLogMessageEventArgs($"Tape is {playString}."));
}
tapePlayButtonPressed = keysDown.Contains(configuration.InputPlayTape);
if (keyboardMode)
{
/* Handle keyboard */
// TODO: Replace hardcoded English layout w/ user-configurable settings
keyboard[0, 0] = keysDown.Contains(Keys.D1);
keyboard[0, 1] = keysDown.Contains(Keys.D2);
keyboard[0, 2] = keysDown.Contains(Keys.D3);
keyboard[0, 3] = keysDown.Contains(Keys.D4);
keyboard[0, 4] = keysDown.Contains(Keys.D5);
keyboard[0, 5] = keysDown.Contains(Keys.D6);
keyboard[0, 6] = keysDown.Contains(Keys.D7);
keyboard[1, 0] = keysDown.Contains(Keys.Q);
keyboard[1, 1] = keysDown.Contains(Keys.W);
keyboard[1, 2] = keysDown.Contains(Keys.E);
keyboard[1, 3] = keysDown.Contains(Keys.R);
keyboard[1, 4] = keysDown.Contains(Keys.T);
keyboard[1, 5] = keysDown.Contains(Keys.Y);
keyboard[1, 6] = keysDown.Contains(Keys.U);
keyboard[2, 0] = keysDown.Contains(Keys.A);
keyboard[2, 1] = keysDown.Contains(Keys.S);
keyboard[2, 2] = keysDown.Contains(Keys.D);
keyboard[2, 3] = keysDown.Contains(Keys.F);
keyboard[2, 4] = keysDown.Contains(Keys.G);
keyboard[2, 5] = keysDown.Contains(Keys.H);
keyboard[2, 6] = keysDown.Contains(Keys.J);
keyboard[3, 0] = keysDown.Contains(Keys.Z);
keyboard[3, 1] = keysDown.Contains(Keys.X);
keyboard[3, 2] = keysDown.Contains(Keys.C);
keyboard[3, 3] = keysDown.Contains(Keys.V);
keyboard[3, 4] = keysDown.Contains(Keys.B);
keyboard[3, 5] = keysDown.Contains(Keys.N);
keyboard[3, 6] = keysDown.Contains(Keys.M);
keyboard[4, 0] = keysDown.Contains(Keys.None); // Alphanumerics, Eng Dier's
keyboard[4, 1] = keysDown.Contains(Keys.Space);
keyboard[4, 2] = keysDown.Contains(Keys.Home); // Clr, Home
keyboard[4, 3] = keysDown.Contains(Keys.Back); // Del, Ins
keyboard[4, 4] = keysDown.Contains(Keys.None); // Not on English keyboard?
keyboard[4, 5] = keysDown.Contains(Keys.None); // ""
keyboard[4, 6] = keysDown.Contains(Keys.None); // ""
keyboard[5, 0] = keysDown.Contains(Keys.Oemcomma);
keyboard[5, 1] = keysDown.Contains(Keys.OemPeriod);
keyboard[5, 2] = keysDown.Contains(Keys.OemQuestion); // Forward slash
keyboard[5, 3] = keysDown.Contains(Keys.None); // Pi
keyboard[5, 4] = keysDown.Contains(Keys.Down);
keyboard[5, 5] = keysDown.Contains(Keys.Left);
keyboard[5, 6] = keysDown.Contains(Keys.Right);
keyboard[6, 0] = keysDown.Contains(Keys.K);
keyboard[6, 1] = keysDown.Contains(Keys.L);
keyboard[6, 2] = keysDown.Contains(Keys.Oemplus); // Semicolon
keyboard[6, 3] = keysDown.Contains(Keys.OemSemicolon); // Colon
keyboard[6, 4] = keysDown.Contains(Keys.OemCloseBrackets);
keyboard[6, 5] = keysDown.Contains(Keys.Enter);
keyboard[6, 6] = keysDown.Contains(Keys.Up);
keyboard[7, 0] = keysDown.Contains(Keys.I);
keyboard[7, 1] = keysDown.Contains(Keys.O);
keyboard[7, 2] = keysDown.Contains(Keys.P);
keyboard[7, 3] = keysDown.Contains(Keys.PageUp); // @
keyboard[7, 4] = keysDown.Contains(Keys.OemOpenBrackets);
keyboard[7, 5] = keysDown.Contains(Keys.None); // Not on English keyboard?
keyboard[7, 6] = keysDown.Contains(Keys.None); // ""
keyboard[8, 0] = keysDown.Contains(Keys.D8);
keyboard[8, 1] = keysDown.Contains(Keys.D9);
keyboard[8, 2] = keysDown.Contains(Keys.D0);
keyboard[8, 3] = keysDown.Contains(Keys.OemMinus);
keyboard[8, 4] = keysDown.Contains(Keys.Oemtilde); // ^, ~
keyboard[8, 5] = keysDown.Contains(Keys.OemPipe); // Yen, Pipe, Pound?
keyboard[8, 6] = keysDown.Contains(Keys.PageDown); // Break
keyboard[9, 6] = keysDown.Contains(Keys.RControlKey); // Graph
keyboard[10, 6] = keysDown.Contains(Keys.LControlKey); // Ctrl
keyboard[11, 5] = keysDown.Contains(Keys.Tab); // Func
keyboard[11, 6] = keysDown.Contains(Keys.ShiftKey); // Shift
}
else
{
/* Handle controllers */
portAInputsPressed = 0;
portBInputsPressed = 0;
/* Keyboard */
if (keysDown.Contains(configuration.Joypad1Up)) portAInputsPressed |= PortAInputs.P1Up;
if (keysDown.Contains(configuration.Joypad1Down)) portAInputsPressed |= PortAInputs.P1Down;
if (keysDown.Contains(configuration.Joypad1Left)) portAInputsPressed |= PortAInputs.P1Left;
if (keysDown.Contains(configuration.Joypad1Right)) portAInputsPressed |= PortAInputs.P1Right;
if (keysDown.Contains(configuration.Joypad1Button1)) portAInputsPressed |= PortAInputs.P1Button1;
if (keysDown.Contains(configuration.Joypad1Button2)) portAInputsPressed |= PortAInputs.P1Button2;
if (keysDown.Contains(configuration.Joypad2Up)) portAInputsPressed |= PortAInputs.P2Up;
if (keysDown.Contains(configuration.Joypad2Down)) portAInputsPressed |= PortAInputs.P2Down;
if (keysDown.Contains(configuration.Joypad2Left)) portBInputsPressed |= PortBInputs.P2Left;
if (keysDown.Contains(configuration.Joypad2Right)) portBInputsPressed |= PortBInputs.P2Right;
if (keysDown.Contains(configuration.Joypad2Button1)) portBInputsPressed |= PortBInputs.P2Button1;
if (keysDown.Contains(configuration.Joypad2Button2)) portBInputsPressed |= PortBInputs.P2Button2;
/* XInput controller */
if (eventArgs.ControllerState.IsAnyUpDirectionPressed() && !eventArgs.ControllerState.IsAnyDownDirectionPressed()) portAInputsPressed |= PortAInputs.P1Up;
if (eventArgs.ControllerState.IsAnyDownDirectionPressed() && !eventArgs.ControllerState.IsAnyUpDirectionPressed()) portAInputsPressed |= PortAInputs.P1Down;
if (eventArgs.ControllerState.IsAnyLeftDirectionPressed() && !eventArgs.ControllerState.IsAnyRightDirectionPressed()) portAInputsPressed |= PortAInputs.P1Left;
if (eventArgs.ControllerState.IsAnyRightDirectionPressed() && !eventArgs.ControllerState.IsAnyLeftDirectionPressed()) portAInputsPressed |= PortAInputs.P1Right;
if (eventArgs.ControllerState.IsAPressed()) portAInputsPressed |= PortAInputs.P1Button1;
if (eventArgs.ControllerState.IsXPressed() || eventArgs.ControllerState.IsBPressed()) portAInputsPressed |= PortAInputs.P1Button2;
}
}
private void UpdateInput()
{
byte portA = 0xFF, portB = 0xFF;
byte row = (byte)(ppi.PortCOutput & 0x07);
if (row == 0x07)
{
/* Controller ports */
portA &= (byte)~portAInputsPressed;
portB &= (byte)~portBInputsPressed;
}
else
{
/* Keyboard matrix */
for (int i = 0; i < 8; i++)
if (keyboard[i, row]) portA &= (byte)~(1 << i);
for (int i = 0; i < 4; i++)
if (keyboard[8 + i, row]) portB &= (byte)~(1 << i);
}
ppi.PortAInput = portA;
ppi.PortBInput = (byte)((ppi.PortBInput & 0xF0) | (portB & 0x0F));
}
private void UpdateTape(TapeUpdateModes updateMode)
{
if (!isTapePlaying) return;
// TODO: errr, try to actually emulate this? so far just seems to write repeating bit patterns, no ex. recognizable basic program data...
switch (updateMode)
{
case TapeUpdateModes.Reading:
var read = ((ppi.PortBInput >> 7) & 0b1); // TODO: correct?
//
break;
case TapeUpdateModes.Writing:
var write = ((ppi.PortCOutput >> 4) & 0b1); // TODO: correct?
//
break;
}
}
/* Basic memory maps (via SC-3000 Service Manual, chp 2-8)
*
* IIa IIb IIIa IIIb
* 8000 -- CartRAM CartRAM CartRAM
* 8800 -- -- CartRAM CartRAM
* C000 WRAM WRAM WRAM CartRAM
* C800 -- -- -- CartRAM
*/
private byte ReadMemory(ushort address)
{
if (cartridge != null && address >= cartridge.GetLowerBound() && address <= cartridge.GetUpperBound())
{
return cartridge.Read(address);
}
else if (address >= 0xC000 && address <= 0xFFFF)
{
return wram[address & (ramSize - 1)];
}
/* Cannot read from address, return 0 */
return 0x00;
}
private void WriteMemory(ushort address, byte value)
{
if (cartridge != null && address >= cartridge.GetLowerBound() && address <= cartridge.GetUpperBound())
{
cartridge.Write(address, value);
}
else if (address >= 0xC000 && address <= 0xFFFF)
{
wram[address & (ramSize - 1)] = value;
}
}
private byte ReadPort(byte port)
{
switch (port & 0xC0)
{
case 0x80:
return vdp.ReadPort(port);
case 0xC0:
UpdateTape(TapeUpdateModes.Reading);
UpdateInput();
return ppi.ReadPort(port);
default:
// TODO: handle properly
return 0x00;
}
}
public void WritePort(byte port, byte value)
{
switch (port & 0xC0)
{
case 0x40:
psg.WritePort(port, value);
break;
case 0x80:
vdp.WritePort(port, value);
break;
case 0xC0:
ppi.WritePort(port, value);
UpdateTape(TapeUpdateModes.Writing);
break;
default:
// TODO: handle properly
break;
}
}
}
}