diff --git a/BizHawk.Client.EmuHawk/MainForm.Designer.cs b/BizHawk.Client.EmuHawk/MainForm.Designer.cs index 399cedf8a9..5f47cb66d0 100644 --- a/BizHawk.Client.EmuHawk/MainForm.Designer.cs +++ b/BizHawk.Client.EmuHawk/MainForm.Designer.cs @@ -334,7 +334,8 @@ this.ColecoSubMenu = new System.Windows.Forms.ToolStripMenuItem(); this.ColecoControllerSettingsMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.ColecoSkipBiosMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.N64SubMenu = new System.Windows.Forms.ToolStripMenuItem(); + this.ColecoUseSGMMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.N64SubMenu = new System.Windows.Forms.ToolStripMenuItem(); this.N64PluginSettingsMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.N64ControllerSettingsMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.toolStripSeparator23 = new System.Windows.Forms.ToolStripSeparator(); @@ -3034,7 +3035,8 @@ this.ColecoSubMenu.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { this.ColecoControllerSettingsMenuItem, this.toolStripSeparator35, - this.ColecoSkipBiosMenuItem}); + this.ColecoSkipBiosMenuItem, + this.ColecoUseSGMMenuItem}); this.ColecoSubMenu.Name = "ColecoSubMenu"; this.ColecoSubMenu.Size = new System.Drawing.Size(56, 19); this.ColecoSubMenu.Text = "&Coleco"; @@ -3059,10 +3061,17 @@ this.ColecoSkipBiosMenuItem.Size = new System.Drawing.Size(253, 22); this.ColecoSkipBiosMenuItem.Text = "&Skip BIOS intro (When Applicable)"; this.ColecoSkipBiosMenuItem.Click += new System.EventHandler(this.ColecoSkipBiosMenuItem_Click); - // - // N64SubMenu - // - this.N64SubMenu.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { + // + // ColecoUseSGMMenuItem + // + this.ColecoUseSGMMenuItem.Name = "ColecoUseSGMMenuItem"; + this.ColecoUseSGMMenuItem.Size = new System.Drawing.Size(253, 22); + this.ColecoUseSGMMenuItem.Text = "&Use the Super Game Module"; + this.ColecoUseSGMMenuItem.Click += new System.EventHandler(this.ColecoUseSGMMenuItem_Click); + // + // N64SubMenu + // + this.N64SubMenu.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { this.N64PluginSettingsMenuItem, this.N64ControllerSettingsMenuItem, this.toolStripSeparator23, @@ -4257,6 +4266,7 @@ private System.Windows.Forms.ToolStripSeparator toolStripSeparator28; private System.Windows.Forms.ToolStripMenuItem ColecoSubMenu; private System.Windows.Forms.ToolStripMenuItem ColecoSkipBiosMenuItem; + private System.Windows.Forms.ToolStripMenuItem ColecoUseSGMMenuItem; private System.Windows.Forms.ToolStripMenuItem ColecoControllerSettingsMenuItem; private System.Windows.Forms.ToolStripStatusLabel LedLightStatusLabel; private System.Windows.Forms.ToolStripMenuItem GBASubMenu; diff --git a/BizHawk.Client.EmuHawk/MainForm.Events.cs b/BizHawk.Client.EmuHawk/MainForm.Events.cs index 5e5b535397..aafb6e0eaf 100644 --- a/BizHawk.Client.EmuHawk/MainForm.Events.cs +++ b/BizHawk.Client.EmuHawk/MainForm.Events.cs @@ -2206,6 +2206,7 @@ namespace BizHawk.Client.EmuHawk { var ss = ((ColecoVision)Emulator).GetSyncSettings(); ColecoSkipBiosMenuItem.Checked = ss.SkipBiosIntro; + ColecoUseSGMMenuItem.Checked = ss.UseSGM; ColecoControllerSettingsMenuItem.Enabled = !Global.MovieSession.Movie.IsActive; } @@ -2216,6 +2217,13 @@ namespace BizHawk.Client.EmuHawk PutCoreSyncSettings(ss); } + private void ColecoUseSGMMenuItem_Click(object sender, EventArgs e) + { + var ss = ((ColecoVision)Emulator).GetSyncSettings(); + ss.UseSGM ^= true; + PutCoreSyncSettings(ss); + } + private void ColecoControllerSettingsMenuItem_Click(object sender, EventArgs e) { new ColecoControllerSettings().ShowDialog(); diff --git a/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj b/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj index cd7fe103fc..1d9458f597 100644 --- a/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj +++ b/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj @@ -415,6 +415,7 @@ ColecoVision.cs + @@ -1361,4 +1362,4 @@ --> - + \ No newline at end of file diff --git a/BizHawk.Emulation.Cores/Consoles/Coleco/AY_3_8910.cs b/BizHawk.Emulation.Cores/Consoles/Coleco/AY_3_8910.cs new file mode 100644 index 0000000000..efcf38b8f7 --- /dev/null +++ b/BizHawk.Emulation.Cores/Consoles/Coleco/AY_3_8910.cs @@ -0,0 +1,400 @@ +using System; + +using BizHawk.Common; +using BizHawk.Common.NumberExtensions; +using BizHawk.Emulation.Common; + +namespace BizHawk.Emulation.Cores.ColecoVision +{ + public sealed class AY_3_8910 : ISoundProvider + { + private readonly BlipBuffer _blip = new BlipBuffer(4096); + private short[] _sampleBuffer = new short[0]; + + + public AY_3_8910() + { + _blip.SetRates(894866 / 4.0, 44100); + Reset(); + } + + public byte[] Register = new byte[16]; + + public byte port_sel; + + public int total_clock; // TODO: what is this used for? + + public void Reset() + { + clock_A = clock_B = clock_C = 0x1000; + noise_clock = 0x20; + port_sel = 0; + + for (int i = 0; i < 16; i++) + { + Register[i] = 0x0000; + } + sync_psg_state(); + DiscardSamples(); + } + + public void DiscardSamples() + { + _blip.Clear(); + _sampleClock = 0; + } + + public void GetSamplesAsync(short[] samples) + { + throw new NotSupportedException("Async is not available"); + } + + 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 GetSamplesSync(out short[] samples, out int nsamp) + { + _blip.EndFrame((uint)_sampleClock); + _sampleClock = 0; + + nsamp = _blip.SamplesAvailable(); + int targetLength = nsamp * 2; + if (_sampleBuffer.Length != targetLength) + { + _sampleBuffer = new short[targetLength]; + } + + _blip.ReadSamplesLeft(_sampleBuffer, nsamp); + for (int i = 0; i < _sampleBuffer.Length; i += 2) + { + _sampleBuffer[i + 1] = _sampleBuffer[i]; + } + + samples = _sampleBuffer; + } + + public void GetSamples(short[] samples) + { + throw new Exception(); + } + + private static readonly int[] VolumeTable = + { + 0x0000, 0x0055, 0x0079, 0x00AB, 0x00F1, 0x0155, 0x01E3, 0x02AA, + 0x03C5, 0x0555, 0x078B, 0x0AAB, 0x0F16, 0x1555, 0x1E2B, 0x2AAA + }; + + private int _sampleClock; + private int _latchedSample; + + private int TotalExecutedCycles; + private int PendingCycles; + private int psg_clock; + private int sq_per_A, sq_per_B, sq_per_C; + private int clock_A, clock_B, clock_C; + private int vol_A, vol_B, vol_C; + private bool A_on, B_on, C_on; + private bool A_up, B_up, C_up; + private bool A_noise, B_noise, C_noise; + + private int env_per; + private int env_clock; + private int env_shape; + private int env_E; + private int E_up_down; + private int env_vol_A, env_vol_B, env_vol_C; + + private int noise_clock; + private int noise_per; + private int noise = 0x1; + + public Func ReadMemory; + public Func WriteMemory; + + public void SyncState(Serializer ser) + { + ser.BeginSection("PSG"); + + ser.Sync("Register", ref Register, false); + ser.Sync("Toal_executed_cycles", ref TotalExecutedCycles); + ser.Sync("Pending_Cycles", ref PendingCycles); + + ser.Sync("psg_clock", ref psg_clock); + ser.Sync("clock_A", ref clock_A); + ser.Sync("clock_B", ref clock_B); + ser.Sync("clock_C", ref clock_C); + ser.Sync("noise_clock", ref noise_clock); + ser.Sync("env_clock", ref env_clock); + ser.Sync("A_up", ref A_up); + ser.Sync("B_up", ref B_up); + ser.Sync("C_up", ref C_up); + ser.Sync("noise", ref noise); + ser.Sync("env_E", ref env_E); + ser.Sync("E_up_down", ref E_up_down); + ser.Sync("port_sel", ref port_sel); + + sync_psg_state(); + + ser.EndSection(); + } + + public byte ReadPSG() + { + return Register[port_sel]; + } + + private void sync_psg_state() + { + sq_per_A = (Register[0] & 0xFF) | (((Register[4] & 0xF) << 8)); + if (sq_per_A == 0) + { + sq_per_A = 0x1000; + } + + sq_per_B = (Register[1] & 0xFF) | (((Register[5] & 0xF) << 8)); + if (sq_per_B == 0) + { + sq_per_B = 0x1000; + } + + sq_per_C = (Register[2] & 0xFF) | (((Register[6] & 0xF) << 8)); + if (sq_per_C == 0) + { + sq_per_C = 0x1000; + } + + env_per = (Register[3] & 0xFF) | (((Register[7] & 0xFF) << 8)); + if (env_per == 0) + { + env_per = 0x10000; + } + + env_per *= 2; + + A_on = Register[8].Bit(0); + B_on = Register[8].Bit(1); + C_on = Register[8].Bit(2); + A_noise = Register[8].Bit(3); + B_noise = Register[8].Bit(4); + C_noise = Register[8].Bit(5); + + noise_per = Register[9] & 0x1F; + if (noise_per == 0) + { + noise_per = 0x20; + } + + var shape_select = Register[10] & 0xF; + + if (shape_select < 4) + env_shape = 0; + else if (shape_select < 8) + env_shape = 1; + else + env_shape = 2 + (shape_select - 8); + + vol_A = Register[11] & 0xF; + env_vol_A = (Register[11] >> 4) & 0x3; + + vol_B = Register[12] & 0xF; + env_vol_B = (Register[12] >> 4) & 0x3; + + vol_C = Register[13] & 0xF; + env_vol_C = (Register[13] >> 4) & 0x3; + } + + public bool WritePSG(byte value) + { + value &= 0xFF; + + if (port_sel == 4 || port_sel == 5 || port_sel == 6 || port_sel == 10) + value &= 0xF; + + if (port_sel == 9) + value &= 0x1F; + + if (port_sel == 11 || port_sel == 12 || port_sel == 13) + value &= 0x3F; + + Register[port_sel] = value; + + sync_psg_state(); + + if (port_sel == 10) + { + env_clock = env_per; + + if (env_shape == 0 || env_shape == 2 || env_shape == 3 || env_shape == 4 || env_shape == 5) + { + env_E = 15; + E_up_down = -1; + } + else + { + env_E = 0; + E_up_down = 1; + } + } + + return true; + } + + public void generate_sound(int cycles_to_do) + { + // there are 4 cpu cycles for every psg cycle + bool sound_out_A; + bool sound_out_B; + bool sound_out_C; + + for (int i = 0; i < cycles_to_do; i++) + { + psg_clock++; + + if (psg_clock == 4) + { + psg_clock = 0; + + total_clock++; + + clock_A--; + clock_B--; + clock_C--; + + noise_clock--; + env_clock--; + + // clock noise + if (noise_clock == 0) + { + noise = (noise >> 1) ^ (noise.Bit(0) ? 0x10004 : 0); + noise_clock = noise_per; + } + + if (env_clock == 0) + { + env_clock = env_per; + + env_E += E_up_down; + + if (env_E == 16 || env_E == -1) + { + + // we just completed a period of the envelope, determine what to do now based on the envelope shape + if (env_shape == 0 || env_shape == 1 || env_shape == 3 || env_shape == 9) + { + E_up_down = 0; + env_E = 0; + } + else if (env_shape == 5 || env_shape == 7) + { + E_up_down = 0; + env_E = 15; + } + else if (env_shape == 4 || env_shape == 8) + { + if (env_E == 16) + { + env_E = 15; + E_up_down = -1; + } + else + { + env_E = 0; + E_up_down = 1; + } + } + else if (env_shape == 2) + { + env_E = 15; + } + else + { + env_E = 0; + } + } + } + + if (clock_A == 0) + { + A_up = !A_up; + clock_A = sq_per_A; + } + + if (clock_B == 0) + { + B_up = !B_up; + clock_B = sq_per_B; + } + + if (clock_C == 0) + { + C_up = !C_up; + clock_C = sq_per_C; + } + + + sound_out_A = (noise.Bit(0) | A_noise) & (A_on | A_up); + sound_out_B = (noise.Bit(0) | B_noise) & (B_on | B_up); + sound_out_C = (noise.Bit(0) | C_noise) & (C_on | C_up); + + // now calculate the volume of each channel and add them together + int v; + + if (env_vol_A == 0) + { + v = (short)(sound_out_A ? VolumeTable[vol_A] : 0); + } + else + { + int shift_A = 3 - env_vol_A; + if (shift_A < 0) + shift_A = 0; + v = (short)(sound_out_A ? (VolumeTable[env_E] >> shift_A) : 0); + } + + if (env_vol_B == 0) + { + v += (short)(sound_out_B ? VolumeTable[vol_B] : 0); + + } + else + { + int shift_B = 3 - env_vol_B; + if (shift_B < 0) + shift_B = 0; + v += (short)(sound_out_B ? (VolumeTable[env_E] >> shift_B) : 0); + } + + if (env_vol_C == 0) + { + v += (short)(sound_out_C ? VolumeTable[vol_C] : 0); + } + else + { + int shift_C = 3 - env_vol_C; + if (shift_C < 0) + shift_C = 0; + v += (short)(sound_out_C ? (VolumeTable[env_E] >> shift_C) : 0); + } + + if (v != _latchedSample) + { + _blip.AddDelta((uint)_sampleClock, v - _latchedSample); + _latchedSample = v; + } + + _sampleClock++; + } + } + } + } +} diff --git a/BizHawk.Emulation.Cores/Consoles/Coleco/ColecoVision.IEmulator.cs b/BizHawk.Emulation.Cores/Consoles/Coleco/ColecoVision.IEmulator.cs index 22e67db446..cc4cfc8e56 100644 --- a/BizHawk.Emulation.Cores/Consoles/Coleco/ColecoVision.IEmulator.cs +++ b/BizHawk.Emulation.Cores/Consoles/Coleco/ColecoVision.IEmulator.cs @@ -51,6 +51,14 @@ namespace BizHawk.Emulation.Cores.ColecoVision } } + public bool use_SGM = false; + public bool is_MC = false; + public int MC_bank = 0; + public bool enable_SGM_high = false; + public bool enable_SGM_low = false; + public byte port_0x53, port_0x7F; + + public int Frame => _frame; public string SystemId => "Coleco"; diff --git a/BizHawk.Emulation.Cores/Consoles/Coleco/ColecoVision.ISettable.cs b/BizHawk.Emulation.Cores/Consoles/Coleco/ColecoVision.ISettable.cs index b043990793..5dfee99534 100644 --- a/BizHawk.Emulation.Cores/Consoles/Coleco/ColecoVision.ISettable.cs +++ b/BizHawk.Emulation.Cores/Consoles/Coleco/ColecoVision.ISettable.cs @@ -46,6 +46,7 @@ namespace BizHawk.Emulation.Cores.ColecoVision public class ColecoSyncSettings { public bool SkipBiosIntro { get; set; } + public bool UseSGM { get; set; } private string _port1 = ColecoVisionControllerDeck.DefaultControllerName; private string _port2 = ColecoVisionControllerDeck.DefaultControllerName; diff --git a/BizHawk.Emulation.Cores/Consoles/Coleco/ColecoVision.ISoundProvider.cs b/BizHawk.Emulation.Cores/Consoles/Coleco/ColecoVision.ISoundProvider.cs index dc0d7b7a77..93cff88b54 100644 --- a/BizHawk.Emulation.Cores/Consoles/Coleco/ColecoVision.ISoundProvider.cs +++ b/BizHawk.Emulation.Cores/Consoles/Coleco/ColecoVision.ISoundProvider.cs @@ -5,6 +5,7 @@ namespace BizHawk.Emulation.Cores.ColecoVision public partial class ColecoVision { private SN76489 PSG; + private AY_3_8910 SGM_sound; private readonly FakeSyncSound _fakeSyncSound; } } diff --git a/BizHawk.Emulation.Cores/Consoles/Coleco/ColecoVision.IStatable.cs b/BizHawk.Emulation.Cores/Consoles/Coleco/ColecoVision.IStatable.cs index d079fa50f4..313fa40f30 100644 --- a/BizHawk.Emulation.Cores/Consoles/Coleco/ColecoVision.IStatable.cs +++ b/BizHawk.Emulation.Cores/Consoles/Coleco/ColecoVision.IStatable.cs @@ -55,7 +55,16 @@ namespace BizHawk.Emulation.Cores.ColecoVision ser.BeginSection("Coleco"); _vdp.SyncState(ser); PSG.SyncState(ser); + SGM_sound.SyncState(ser); + ser.Sync("UseSGM", ref use_SGM); + ser.Sync("is_MC", ref is_MC); + ser.Sync("EnableSGMhigh", ref enable_SGM_high); + ser.Sync("EnableSGMlow", ref enable_SGM_low); + ser.Sync("Port_0x53", ref port_0x53); + ser.Sync("Port_0x7F", ref port_0x7F); ser.Sync("RAM", ref _ram, false); + ser.Sync("SGM_high_RAM", ref SGM_high_RAM, false); + ser.Sync("SGM_low_RAM", ref SGM_low_RAM, false); ser.Sync("Frame", ref _frame); ser.Sync("LagCount", ref _lagCount); ser.Sync("IsLag", ref _isLag); diff --git a/BizHawk.Emulation.Cores/Consoles/Coleco/ColecoVision.cs b/BizHawk.Emulation.Cores/Consoles/Coleco/ColecoVision.cs index 070c8a2655..230cda9897 100644 --- a/BizHawk.Emulation.Cores/Consoles/Coleco/ColecoVision.cs +++ b/BizHawk.Emulation.Cores/Consoles/Coleco/ColecoVision.cs @@ -1,6 +1,7 @@ using BizHawk.Emulation.Common; using BizHawk.Emulation.Cores.Components; using BizHawk.Emulation.Cores.Components.Z80A; +using System; namespace BizHawk.Emulation.Cores.ColecoVision { @@ -36,6 +37,8 @@ namespace BizHawk.Emulation.Cores.ColecoVision _fakeSyncSound = new FakeSyncSound(PSG, 735); ser.Register(_fakeSyncSound); + SGM_sound = new AY_3_8910(); + ControllerDeck = new ColecoVisionControllerDeck(_syncSettings.Port1, _syncSettings.Port2); _vdp = new TMS9918A(_cpu); @@ -56,6 +59,9 @@ namespace BizHawk.Emulation.Cores.ColecoVision _tracer.Header = _cpu.TraceHeader; ser.Register(_cpu); ser.Register(_tracer); + + use_SGM = _syncSettings.UseSGM; + Console.WriteLine(use_SGM); } private readonly Z80A _cpu; @@ -65,6 +71,9 @@ namespace BizHawk.Emulation.Cores.ColecoVision private byte[] _romData; private byte[] _ram = new byte[1024]; + public byte[] SGM_high_RAM = new byte[0x6000]; + public byte[] SGM_low_RAM = new byte[0x2000]; + private int _frame; private IController _controller; @@ -79,10 +88,22 @@ namespace BizHawk.Emulation.Cores.ColecoVision private void LoadRom(byte[] rom, bool skipbios) { - _romData = new byte[0x8000]; - for (int i = 0; i < 0x8000; i++) + if (rom.Length <= 32768) { - _romData[i] = rom[i % rom.Length]; + _romData = new byte[0x8000]; + for (int i = 0; i < 0x8000; i++) + { + _romData[i] = rom[i % rom.Length]; + } + } + else + { + // all original ColecoVision games had 32k or less of ROM + // so, if we have more then that, we must be using a MegaCart mapper + is_MC = true; + + _romData = rom; + } // hack to skip colecovision title screen @@ -117,6 +138,29 @@ namespace BizHawk.Emulation.Cores.ColecoVision return ReadController2(); } + if (use_SGM) + { + if (port == 0x50) + { + return SGM_sound.port_sel; + } + + if (port == 0x52) + { + return SGM_sound.Register[SGM_sound.port_sel]; + } + + if (port == 0x53) + { + return port_0x53; + } + + if (port == 0x7F) + { + return port_0x7F; + } + } + return 0xFF; } @@ -154,6 +198,48 @@ namespace BizHawk.Emulation.Cores.ColecoVision { PSG.WritePsgData(value, _cpu.TotalExecutedCycles); } + + if (use_SGM) + { + if (port == 0x50) + { + SGM_sound.port_sel = (byte)(value & 0xF); + } + + if (port == 0x51) + { + SGM_sound.Register[SGM_sound.port_sel] = value; + } + + if (port == 0x53) + { + if ((value & 1) > 0) + { + enable_SGM_high = true; + } + else + { + // NOTE: the documentation states that you shouldn't turn RAM back off once enabling it + // so we won't do anything here + } + + port_0x53 = value; + } + + if (port == 0x7F) + { + if (value == 0xF) + { + enable_SGM_low = false; + } + else if (value == 0xD) + { + enable_SGM_low = true; + } + + port_0x7F = value; + } + } } private byte ReadController1() @@ -198,17 +284,57 @@ namespace BizHawk.Emulation.Cores.ColecoVision { if (addr >= 0x8000) { - return _romData[addr & 0x7FFF]; + if (!is_MC) + { + return _romData[addr & 0x7FFF]; + } + else + { + // reading from 0xFFC0 to 0xFFFF triggers bank switching + // I don't know if it happens before or after the read though + + if (addr >= 0xFFC0) + { + MC_bank = (addr - 0xFFC0) & (_romData.Length / 0x4000 - 1); + } + + // the first 16K of the map is always the last 16k of the ROM + if (addr < 0xC000) + { + return _romData[_romData.Length - 0x4000 + (addr - 0x8000)]; + } + else + { + return _romData[MC_bank * 0x4000 + (addr - 0xC000)]; + } + } } - if (addr >= 0x6000) + if (!enable_SGM_high) { - return _ram[addr & 1023]; + if (addr >= 0x6000) + { + return _ram[addr & 1023]; + } + } + else + { + if (addr >= 0x2000) + { + return SGM_high_RAM[addr - 0x2000]; + } } if (addr < 0x2000) { - return _biosRom[addr]; + if (!enable_SGM_low) + { + return _biosRom[addr]; + } + else + { + return SGM_low_RAM[addr]; + } } ////Console.WriteLine("Unhandled read at {0:X4}", addr); @@ -217,11 +343,28 @@ namespace BizHawk.Emulation.Cores.ColecoVision private void WriteMemory(ushort addr, byte value) { - if (addr >= 0x6000 && addr < 0x8000) + if (!enable_SGM_high) { - _ram[addr & 1023] = value; + if (addr >= 0x6000 && addr < 0x8000) + { + _ram[addr & 1023] = value; + } + } + else + { + if (addr >= 0x2000 && addr < 0x8000) + { + SGM_high_RAM[addr - 0x2000] = value; + } } + if (addr < 0x2000) + { + if (enable_SGM_low) + { + SGM_low_RAM[addr] = value; + } + } ////Console.WriteLine("Unhandled write at {0:X4}:{1:X2}", addr, value); }