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 SendLogMessage; protected virtual void OnSendLogMessage(SendLogMessageEventArgs e) { SendLogMessage?.Invoke(this, e); } public event EventHandler EmulationReset; protected virtual void OnEmulationReset(EventArgs e) { EmulationReset?.Invoke(this, e); } public event EventHandler RenderScreen { add { vdp.RenderScreen += value; } remove { vdp.RenderScreen -= value; } } public event EventHandler SizeScreen { add { vdp.SizeScreen += value; } remove { vdp.SizeScreen -= value; } } public event EventHandler ChangeViewport; protected virtual void OnChangeViewport(ChangeViewportEventArgs e) { ChangeViewport?.Invoke(this, e); } public event EventHandler PollInput; protected virtual void OnPollInput(PollInputEventArgs e) { PollInput?.Invoke(this, e); } public event EventHandler EnqueueSamples { add { psg.EnqueueSamples += value; } remove { psg.EnqueueSamples -= value; } } public event EventHandler SaveExtraData; protected virtual void OnSaveExtraData(SaveExtraDataEventArgs e) { SaveExtraData?.Invoke(this, e); } public event EventHandler 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 state) public void SetState(Dictionary state) { configuration.TVStandard = (TVStandard)state[nameof(configuration.TVStandard)]; SaveStateHandler.PerformSetState(cartridge, (Dictionary)state[nameof(cartridge)]); wram = (byte[])state[nameof(wram)]; SaveStateHandler.PerformSetState(cpu, (Dictionary)state[nameof(cpu)]); SaveStateHandler.PerformSetState(vdp, (Dictionary)state[nameof(vdp)]); SaveStateHandler.PerformSetState(psg, (Dictionary)state[nameof(psg)]); SaveStateHandler.PerformSetState(ppi, (Dictionary)state[nameof(ppi)]); keyboard = (bool[,])(state[nameof(keyboard)]); ReconfigureSystem(); } public Dictionary GetState() { return new Dictionary { [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 GetDebugInformation() { var dict = new Dictionary { { "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; } } } }