Refactor GambatteLink (#3002)

* refactor gambattelink to be more modular and add support for 3x/4x
This commit is contained in:
CasualPokePlayer 2021-11-19 19:59:27 -08:00 committed by GitHub
parent 1333813bb1
commit d053a0b414
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 851 additions and 490 deletions

View File

@ -33,11 +33,17 @@
this.gbPrefControl1 = new GBPrefControl();
this.tabPage2 = new System.Windows.Forms.TabPage();
this.gbPrefControl2 = new GBPrefControl();
this.tabPage3 = new System.Windows.Forms.TabPage();
this.gbPrefControl3 = new GBPrefControl();
this.tabPage4 = new System.Windows.Forms.TabPage();
this.gbPrefControl4 = new GBPrefControl();
this.buttonCancel = new System.Windows.Forms.Button();
this.buttonOK = new System.Windows.Forms.Button();
this.tabControl1.SuspendLayout();
this.tabPage1.SuspendLayout();
this.tabPage2.SuspendLayout();
this.tabPage3.SuspendLayout();
this.tabPage4.SuspendLayout();
this.SuspendLayout();
//
// tabControl1
@ -47,6 +53,8 @@
| System.Windows.Forms.AnchorStyles.Right)));
this.tabControl1.Controls.Add(this.tabPage1);
this.tabControl1.Controls.Add(this.tabPage2);
this.tabControl1.Controls.Add(this.tabPage3);
this.tabControl1.Controls.Add(this.tabPage4);
this.tabControl1.Location = new System.Drawing.Point(12, 12);
this.tabControl1.Name = "tabControl1";
this.tabControl1.SelectedIndex = 0;
@ -61,7 +69,7 @@
this.tabPage1.Padding = new System.Windows.Forms.Padding(3);
this.tabPage1.Size = new System.Drawing.Size(509, 308);
this.tabPage1.TabIndex = 0;
this.tabPage1.Text = "Left Gameboy";
this.tabPage1.Text = "Player 1 Gameboy";
this.tabPage1.UseVisualStyleBackColor = true;
//
// gbPrefControl1
@ -81,7 +89,7 @@
this.tabPage2.Padding = new System.Windows.Forms.Padding(3);
this.tabPage2.Size = new System.Drawing.Size(509, 308);
this.tabPage2.TabIndex = 1;
this.tabPage2.Text = "Right Gameboy";
this.tabPage2.Text = "Player 2 Gameboy";
this.tabPage2.UseVisualStyleBackColor = true;
//
// gbPrefControl2
@ -93,6 +101,46 @@
this.gbPrefControl2.Size = new System.Drawing.Size(503, 302);
this.gbPrefControl2.TabIndex = 0;
//
// tabPage3
//
this.tabPage3.Controls.Add(this.gbPrefControl3);
this.tabPage3.Location = new System.Drawing.Point(4, 22);
this.tabPage3.Name = "tabPage3";
this.tabPage3.Padding = new System.Windows.Forms.Padding(3);
this.tabPage3.Size = new System.Drawing.Size(509, 308);
this.tabPage3.TabIndex = 2;
this.tabPage3.Text = "Player 3 Gameboy";
this.tabPage3.UseVisualStyleBackColor = true;
//
// gbPrefControl3
//
this.gbPrefControl3.ColorGameBoy = false;
this.gbPrefControl3.Dock = System.Windows.Forms.DockStyle.Fill;
this.gbPrefControl3.Location = new System.Drawing.Point(3, 3);
this.gbPrefControl3.Name = "gbPrefControl3";
this.gbPrefControl3.Size = new System.Drawing.Size(503, 302);
this.gbPrefControl3.TabIndex = 0;
//
// tabPage4
//
this.tabPage4.Controls.Add(this.gbPrefControl4);
this.tabPage4.Location = new System.Drawing.Point(4, 22);
this.tabPage4.Name = "tabPage4";
this.tabPage4.Padding = new System.Windows.Forms.Padding(3);
this.tabPage4.Size = new System.Drawing.Size(509, 308);
this.tabPage4.TabIndex = 3;
this.tabPage4.Text = "Player 4 Gameboy";
this.tabPage4.UseVisualStyleBackColor = true;
//
// gbPrefControl4
//
this.gbPrefControl4.ColorGameBoy = false;
this.gbPrefControl4.Dock = System.Windows.Forms.DockStyle.Fill;
this.gbPrefControl4.Location = new System.Drawing.Point(3, 3);
this.gbPrefControl4.Name = "gbPrefControl4";
this.gbPrefControl4.Size = new System.Drawing.Size(503, 302);
this.gbPrefControl4.TabIndex = 0;
//
// buttonCancel
//
this.buttonCancel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
@ -130,6 +178,8 @@
this.tabControl1.ResumeLayout(false);
this.tabPage1.ResumeLayout(false);
this.tabPage2.ResumeLayout(false);
this.tabPage3.ResumeLayout(false);
this.tabPage4.ResumeLayout(false);
this.ResumeLayout(false);
}
@ -139,10 +189,14 @@
private System.Windows.Forms.TabControl tabControl1;
private System.Windows.Forms.TabPage tabPage1;
private System.Windows.Forms.TabPage tabPage2;
private System.Windows.Forms.TabPage tabPage3;
private System.Windows.Forms.TabPage tabPage4;
private System.Windows.Forms.Button buttonCancel;
private System.Windows.Forms.Button buttonOK;
private GBPrefControl gbPrefControl1;
private GBPrefControl gbPrefControl2;
private GBPrefControl gbPrefControl3;
private GBPrefControl gbPrefControl4;
}
}

View File

@ -22,25 +22,31 @@ namespace BizHawk.Client.EmuHawk
InitializeComponent();
gbPrefControl1.DialogParent = this;
gbPrefControl2.DialogParent = this;
gbPrefControl3.DialogParent = this;
gbPrefControl4.DialogParent = this;
Icon = Properties.Resources.DualIcon;
}
private void PutSettings(GambatteLink.GambatteLinkSettings s, GambatteLink.GambatteLinkSyncSettings ss)
{
gbPrefControl1.PutSettings(_config, _game, _movieSession, s.L, ss.L);
gbPrefControl2.PutSettings(_config, _game, _movieSession, s.R, ss.R);
gbPrefControl1.PutSettings(_config, _game, _movieSession, s._linkedSettings[0], ss._linkedSyncSettings[0]);
gbPrefControl2.PutSettings(_config, _game, _movieSession, s._linkedSettings[1], ss._linkedSyncSettings[1]);
gbPrefControl3.PutSettings(_config, _game, _movieSession, s._linkedSettings[2], ss._linkedSyncSettings[2]);
gbPrefControl4.PutSettings(_config, _game, _movieSession, s._linkedSettings[3], ss._linkedSyncSettings[3]);
}
private void GetSettings(out GambatteLink.GambatteLinkSettings s, out GambatteLink.GambatteLinkSyncSettings ss)
{
gbPrefControl1.GetSettings(out var sl, out var ssl);
gbPrefControl2.GetSettings(out var sr, out var ssr);
gbPrefControl1.GetSettings(out var s1, out var ss1);
gbPrefControl2.GetSettings(out var s2, out var ss2);
gbPrefControl3.GetSettings(out var s3, out var ss3);
gbPrefControl4.GetSettings(out var s4, out var ss4);
s = new GambatteLink.GambatteLinkSettings(sl, sr);
ss = new GambatteLink.GambatteLinkSyncSettings(ssl, ssr);
s = new GambatteLink.GambatteLinkSettings(s1, s2, s3, s4);
ss = new GambatteLink.GambatteLinkSyncSettings(ss1, ss2, ss3, ss4);
}
private bool SyncSettingsChanged => gbPrefControl1.SyncSettingsChanged || gbPrefControl2.SyncSettingsChanged;
private bool SyncSettingsChanged => gbPrefControl1.SyncSettingsChanged || gbPrefControl2.SyncSettingsChanged || gbPrefControl3.SyncSettingsChanged || gbPrefControl4.SyncSettingsChanged;
public static void DoDGBPrefsDialog(IMainFormForConfig mainForm, Config config, IGameInfo game, IMovieSession movieSession, GambatteLink gambatte)
{
@ -50,8 +56,10 @@ namespace BizHawk.Client.EmuHawk
using var dlg = new DGBPrefs(mainForm.DialogController, config, game, movieSession);
dlg.PutSettings(s, ss);
dlg.gbPrefControl1.ColorGameBoy = gambatte.IsCGBMode(false);
dlg.gbPrefControl2.ColorGameBoy = gambatte.IsCGBMode(true);
dlg.gbPrefControl1.ColorGameBoy = gambatte.IsCGBMode(0);
dlg.gbPrefControl2.ColorGameBoy = gambatte.IsCGBMode(1);
dlg.gbPrefControl3.ColorGameBoy = gambatte.IsCGBMode(2);
dlg.gbPrefControl4.ColorGameBoy = gambatte.IsCGBMode(3);
if (mainForm.ShowDialogAsChild(dlg) == DialogResult.OK)
{

View File

@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace BizHawk.Emulation.Common
{
/// <summary>
/// A generic linked implementation of IDebuggable that can be used by any link core
/// </summary>
/// <seealso cref="IDebuggable" />
public class LinkedDebuggable : IDebuggable
{
private readonly IEmulator[] _linkedCores;
private readonly int _numCores;
public LinkedDebuggable(IEmulator[] linkedCores, int numCores, MemoryCallbackSystem memoryCallbacks)
{
_linkedCores = linkedCores;
_numCores = numCores;
MemoryCallbacks = memoryCallbacks;
}
public IDictionary<string, RegisterValue> GetCpuFlagsAndRegisters()
{
var ret = new List<KeyValuePair<string, RegisterValue>>();
for (int i = 0; i < _numCores; i++)
{
ret.AddRange(_linkedCores[i].AsDebuggable().GetCpuFlagsAndRegisters()
.Select(reg => new KeyValuePair<string, RegisterValue>($"P{i + 1} " + reg.Key, reg.Value)).ToList());
}
return ret.ToDictionary(pair => pair.Key, pair => pair.Value);
}
public void SetCpuRegister(string register, int value)
{
for (int i = 0; i < _numCores; i++)
{
if (register.StartsWith($"P{i + 1} "))
{
_linkedCores[i].AsDebuggable().SetCpuRegister(register.Replace($"P{i + 1} ", ""), value);
}
}
}
public IMemoryCallbackSystem MemoryCallbacks { get; }
public bool CanStep(StepType type) => false;
[FeatureNotImplemented]
public void Step(StepType type) => throw new NotImplementedException();
[FeatureNotImplemented]
public long TotalExecutedCycles => throw new NotImplementedException();
}
}

View File

@ -0,0 +1,60 @@
#nullable disable
using System.Collections.Generic;
namespace BizHawk.Emulation.Common
{
/// <summary>
/// A generic linked implementation of IMemoryDomains that can be used by any link core
/// </summary>
/// <seealso cref="IMemoryDomains" />
public class LinkedMemoryDomains : MemoryDomainList
{
public LinkedMemoryDomains(IEmulator[] linkedCores, int numCores)
: base(LinkMemoryDomains(linkedCores, numCores))
{
SystemBus = linkedCores[0].AsMemoryDomains().SystemBus;
}
private static List<MemoryDomain> LinkMemoryDomains(IEmulator[] linkedCores, int numCores)
{
var mm = new List<MemoryDomain>();
for (int i = 0; i < numCores; i++)
{
foreach (var md in linkedCores[i].AsMemoryDomains() as MemoryDomainList)
{
mm.Add(new WrappedMemoryDomain($"P{i + 1} " + md.Name, md));
}
}
return mm;
}
private class WrappedMemoryDomain : MemoryDomain
{
private readonly MemoryDomain _m;
public WrappedMemoryDomain(string name, MemoryDomain m)
{
_m = m;
Name = name;
Size = m.Size;
WordSize = m.WordSize;
EndianType = m.EndianType;
Writable = m.Writable;
}
public override byte PeekByte(long addr)
{
return _m.PeekByte(addr);
}
public override void PokeByte(long addr, byte val)
{
_m.PokeByte(addr, val);
}
}
}
}

View File

@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
namespace BizHawk.Emulation.Common
{
/// <summary>
/// A generic linked implementation of ISaveRam that can be used by any link core
/// </summary>
/// <seealso cref="ISaveRam" />
public class LinkedSaveRam : ISaveRam
{
private readonly IEmulator[] _linkedCores;
private readonly int _numCores;
public LinkedSaveRam(IEmulator[] linkedCores, int numCores)
{
_linkedCores = linkedCores;
_numCores = numCores;
}
public bool SaveRamModified => LinkedSaveRamModified();
private bool LinkedSaveRamModified()
{
for (int i = 0; i < _numCores; i++)
{
if (_linkedCores[i].AsSaveRam().SaveRamModified)
{
return true;
}
}
return false;
}
public byte[] CloneSaveRam()
{
var linkedBuffers = new List<byte[]>();
int len = 0;
for (int i = 0; i < _numCores; i++)
{
linkedBuffers.Add(_linkedCores[i].AsSaveRam().CloneSaveRam()!);
len += linkedBuffers[i].Length;
}
byte[] ret = new byte[len];
int pos = 0;
for (int i = 0; i < _numCores; i++)
{
Buffer.BlockCopy(linkedBuffers[i], 0, ret, pos, linkedBuffers[i].Length);
pos += linkedBuffers[i].Length;
}
return ret;
}
public void StoreSaveRam(byte[] data)
{
int pos = 0;
for (int i = 0; i < _numCores; i++)
{
var b = new byte[_linkedCores[i].AsSaveRam().CloneSaveRam()!.Length];
Buffer.BlockCopy(data, pos, b, 0, b.Length);
pos += b.Length;
_linkedCores[i].AsSaveRam().StoreSaveRam(b);
}
}
}
}

View File

@ -7,21 +7,26 @@ namespace BizHawk.Emulation.Cores.Nintendo.Gameboy
{
public partial class Gameboy : ICodeDataLogger
{
public void SetCDL(ICodeDataLog cdl)
public void SetCDL(ICodeDataLog cdl) => SetCDL(cdl, "");
internal void SetCDL(ICodeDataLog cdl, string which)
{
_cdl = cdl;
_which = which;
LibGambatte.gambatte_setcdcallback(GambatteState, cdl == null ? null : _cdCallback);
}
public void NewCDL(ICodeDataLog cdl)
public void NewCDL(ICodeDataLog cdl) => NewCDL(cdl, "");
internal void NewCDL(ICodeDataLog cdl, string which)
{
cdl["ROM"] = new byte[MemoryDomains["ROM"]!.Size];
cdl[which + "ROM"] = new byte[MemoryDomains["ROM"]!.Size];
// cdl["HRAM"] = new byte[_memoryDomains["HRAM"]!.Size]; //this is probably useless, but it's here if someone needs it
cdl["WRAM"] = new byte[MemoryDomains["WRAM"]!.Size];
cdl[which + "WRAM"] = new byte[MemoryDomains["WRAM"]!.Size];
var found = MemoryDomains["CartRAM"];
if (found is not null) cdl["CartRAM"] = new byte[found.Size];
if (found is not null) cdl[which + "CartRAM"] = new byte[found.Size];
cdl.SubType = "GB";
cdl.SubVer = 0;
@ -33,6 +38,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.Gameboy
}
private ICodeDataLog _cdl;
private string _which;
private readonly LibGambatte.CDCallback _cdCallback;
private void CDCallbackProc(int addr, LibGambatte.CDLog_AddrType addrtype, LibGambatte.CDLog_Flags flags)
@ -51,7 +57,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.Gameboy
_ => throw new InvalidOperationException("Juniper lightbulb proxy"),
};
_cdl[key][addr] |= (byte)flags;
_cdl[_which + key][addr] |= (byte)flags;
}
}

View File

@ -60,6 +60,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.Gameboy
internal void ConnectMemoryCallbackSystem(MemoryCallbackSystem mcs)
{
_memorycallbacks = mcs;
_memorycallbacks.ActiveChanged += SetMemoryCallbacks;
}
private void InitMemoryCallbacks()
@ -78,12 +79,14 @@ namespace BizHawk.Emulation.Cores.Nintendo.Gameboy
_writecb = CreateCallback(MemoryCallbackFlags.AccessWrite, () => MemoryCallbacks.HasWrites);
_execcb = CreateCallback(MemoryCallbackFlags.AccessExecute, () => MemoryCallbacks.HasExecutes);
_memorycallbacks.ActiveChanged += () =>
{
LibGambatte.gambatte_setreadcallback(GambatteState, MemoryCallbacks.HasReads ? _readcb : null);
LibGambatte.gambatte_setwritecallback(GambatteState, MemoryCallbacks.HasWrites ? _writecb : null);
LibGambatte.gambatte_setexeccallback(GambatteState, MemoryCallbacks.HasExecutes ? _execcb : null);
};
_memorycallbacks.ActiveChanged += SetMemoryCallbacks;
}
private void SetMemoryCallbacks()
{
LibGambatte.gambatte_setreadcallback(GambatteState, MemoryCallbacks.HasReads ? _readcb : null);
LibGambatte.gambatte_setwritecallback(GambatteState, MemoryCallbacks.HasWrites ? _writecb : null);
LibGambatte.gambatte_setexeccallback(GambatteState, MemoryCallbacks.HasExecutes ? _execcb : null);
}
}
}

View File

@ -4,19 +4,34 @@ using System.IO;
namespace BizHawk.Emulation.Cores.Nintendo.Gameboy
{
partial class GambatteLink
partial class GambatteLink : ICodeDataLogger
{
void ICodeDataLogger.SetCDL(ICodeDataLog cdl)
{
((ICodeDataLogger)L).SetCDL(cdl);
for (int i = 0; i < _numCores; i++)
{
_linkedCores[i].SetCDL(cdl, $"P{i + 1} ");
}
}
void ICodeDataLogger.NewCDL(ICodeDataLog cdl)
{
((ICodeDataLogger)L).NewCDL(cdl);
for (int i = 0; i < _numCores; i++)
{
_linkedCores[i].NewCDL(cdl, $"P{i + 1} ");
}
}
void ICodeDataLogger.DisassembleCDL(Stream s, ICodeDataLog cdl) { ((ICodeDataLogger)L).DisassembleCDL(s, cdl); }
[FeatureNotImplemented]
void ICodeDataLogger.DisassembleCDL(Stream s, ICodeDataLog cdl)
{
// this doesn't actually do anything
/*
for (int i = 0; i < _numCores; i++)
{
_linkedCores[i].DisassembleCDL(s, cdl);
}
*/
}
}
}

View File

@ -1,46 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using BizHawk.Emulation.Common;
namespace BizHawk.Emulation.Cores.Nintendo.Gameboy
{
public partial class GambatteLink : IDebuggable
{
public IDictionary<string, RegisterValue> GetCpuFlagsAndRegisters()
{
var left = L.GetCpuFlagsAndRegisters()
.Select(reg => new KeyValuePair<string, RegisterValue>("Left " + reg.Key, reg.Value));
var right = R.GetCpuFlagsAndRegisters()
.Select(reg => new KeyValuePair<string, RegisterValue>("Right " + reg.Key, reg.Value));
return left.Union(right).ToDictionary(pair => pair.Key, pair => pair.Value);
}
public void SetCpuRegister(string register, int value)
{
if (register.StartsWith("Left "))
{
L.SetCpuRegister(register.Replace("Left ", ""), value);
}
else if (register.StartsWith("Right "))
{
R.SetCpuRegister(register.Replace("Right ", ""), value);
}
}
public IMemoryCallbackSystem MemoryCallbacks { get; } = new MemoryCallbackSystem(new[] { "System Bus" });
public bool CanStep(StepType type) => false;
[FeatureNotImplemented]
public void Step(StepType type) => throw new NotImplementedException();
[FeatureNotImplemented]
public long TotalExecutedCycles => throw new NotImplementedException();
private readonly MemoryCallbackSystem _memorycallbacks = new MemoryCallbackSystem(new[] { "System Bus" });
}
}

View File

@ -6,56 +6,82 @@ namespace BizHawk.Emulation.Cores.Nintendo.Gameboy
{
public partial class GambatteLink : IEmulator, IBoardInfo
{
public IEmulatorServiceProvider ServiceProvider { get; }
public IEmulatorServiceProvider ServiceProvider => _serviceProvider;
public ControllerDefinition ControllerDefinition => DualGbController;
public ControllerDefinition ControllerDefinition => GBLinkController;
public bool FrameAdvance(IController controller, bool render, bool rendersound = true)
{
LCont.Clear();
RCont.Clear();
for (int i = 0; i < _numCores; i++)
{
_linkedConts[i].Clear();
}
foreach (var s in DualGbController.BoolButtons)
foreach (var s in GBLinkController.BoolButtons)
{
if (controller.IsPressed(s))
{
if (s.Contains("P1 "))
for (int i = 0; i < _numCores; i++)
{
LCont.Set(s.Replace("P1 ", ""));
}
else if (s.Contains("P2 "))
{
RCont.Set(s.Replace("P2 ", ""));
if (s.Contains($"P{i + 1} "))
{
_linkedConts[i].Set(s.Replace($"P{i + 1} ", ""));
}
}
}
}
bool cablediscosignalNew = controller.IsPressed("Toggle Cable");
if (cablediscosignalNew && !_cablediscosignal)
bool cableDiscoSignalNew = controller.IsPressed("Toggle Cable Connection");
if (cableDiscoSignalNew && !_cableDiscoSignal)
{
_cableconnected ^= true;
Console.WriteLine("Cable connect status to {0}", _cableconnected);
_cableConnected ^= true;
Console.WriteLine("Cable connect status to {0}", _cableConnected);
}
_cablediscosignal = cablediscosignalNew;
_cableDiscoSignal = cableDiscoSignalNew;
L.FrameAdvancePrep(LCont);
R.FrameAdvancePrep(RCont);
if (_numCores > 2)
{
bool cableShiftSignalNew = controller.IsPressed("Toggle Cable Shift");
if (cableShiftSignalNew && !_cableShiftSignal)
{
_cableShifted ^= true;
Console.WriteLine("Cable shift status to {0}", _cableShifted);
}
_cableShifted = cableShiftSignalNew;
bool cableSpaceSignalNew = controller.IsPressed("Toggle Cable Spacing");
if (cableSpaceSignalNew && !_cableSpaceSignal)
{
_cableSpaced ^= true;
Console.WriteLine("Cable spacing status to {0}", _cableSpaceSignal);
}
_cableSpaceSignal = cableSpaceSignalNew;
}
for (int i = 0; i < _numCores; i++)
{
_linkedCores[i].FrameAdvancePrep(_linkedConts[i]);
}
unsafe
{
fixed (int* leftfbuff = &FrameBuffer[0])
fixed (int* fbuff = &FrameBuffer[0])
{
// use pitch to have both cores write to the same frame buffer, interleaved
int* rightfbuff = leftfbuff + 160;
const int Pitch = 160 * 2;
int Pitch = 160 * _numCores;
fixed (short* leftsbuff = LeftBuffer, rightsbuff = RightBuffer)
fixed (short* sbuff = &SoundBuffer[0])
{
const int Step = 32; // could be 1024 for GB
int nL = _overflowL;
int nR = _overflowR;
int[] n = new int[_numCores];
for (int i = 0; i < _numCores; i++)
{
n[i] = _linkedOverflow[i];
}
// slowly step our way through the frame, while continually checking and resolving link cable status
for (int target = 0; target < SampPerFrame;)
@ -66,65 +92,94 @@ namespace BizHawk.Emulation.Cores.Nintendo.Gameboy
target = SampPerFrame; // don't run for slightly too long depending on step
}
// gambatte_runfor() aborts early when a frame is produced, but we don't want that, hence the while()
while (nL < target)
for (int i = 0; i < _numCores; i++)
{
uint nsamp = (uint)(target - nL);
if (LibGambatte.gambatte_runfor(L.GambatteState, leftfbuff, Pitch, leftsbuff + (nL * 2), ref nsamp) > 0)
// gambatte_runfor() aborts early when a frame is produced, but we don't want that, hence the while()
while (n[i] < target)
{
for (int i = 0; i < 144; i++)
uint nsamp = (uint)(target - n[i]);
if (LibGambatte.gambatte_runfor(_linkedCores[i].GambatteState, fbuff + (i * 160), Pitch, sbuff + (i * MaxSampsPerFrame) + (n[i] * 2), ref nsamp) > 0)
{
Array.Copy(FrameBuffer, Pitch * i, VideoBuffer, Pitch * i, 160);
for (int j = 0; j < 144; j++)
{
Array.Copy(FrameBuffer, (i * 160) + (j * Pitch), VideoBuffer, (i * 160) + (j * Pitch), 160);
}
}
n[i] += (int)nsamp;
}
nL += (int)nsamp;
}
while (nR < target)
{
uint nsamp = (uint)(target - nR);
if (LibGambatte.gambatte_runfor(R.GambatteState, rightfbuff, Pitch, rightsbuff + (nR * 2), ref nsamp) > 0)
{
for (int i = 0; i < 144; i++)
{
Array.Copy(FrameBuffer, 160 + Pitch * i, VideoBuffer, 160 + Pitch * i, 160);
}
}
nR += (int)nsamp;
}
// poll link cable statuses, but not when the cable is disconnected
if (!_cableconnected)
if (!_cableConnected)
{
continue;
}
if (LibGambatte.gambatte_linkstatus(L.GambatteState, 256) != 0) // ClockTrigger
switch (_numCores)
{
LibGambatte.gambatte_linkstatus(L.GambatteState, 257); // ack
int lo = LibGambatte.gambatte_linkstatus(L.GambatteState, 258); // GetOut
int ro = LibGambatte.gambatte_linkstatus(R.GambatteState, 258);
LibGambatte.gambatte_linkstatus(L.GambatteState, ro & 0xff); // ShiftIn
LibGambatte.gambatte_linkstatus(R.GambatteState, lo & 0xff); // ShiftIn
}
if (LibGambatte.gambatte_linkstatus(R.GambatteState, 256) != 0) // ClockTrigger
{
LibGambatte.gambatte_linkstatus(R.GambatteState, 257); // ack
int lo = LibGambatte.gambatte_linkstatus(L.GambatteState, 258); // GetOut
int ro = LibGambatte.gambatte_linkstatus(R.GambatteState, 258);
LibGambatte.gambatte_linkstatus(L.GambatteState, ro & 0xff); // ShiftIn
LibGambatte.gambatte_linkstatus(R.GambatteState, lo & 0xff); // ShiftIn
case 2:
{
TryCableBytewiseTransfer(P1, P2);
TryCableBytewiseTransfer(P2, P1);
break;
}
case 3:
{
if (_cableSpaced)
{
TryCableBytewiseTransfer(P1, P3);
TryCableBytewiseTransfer(P3, P1);
}
else if (_cableShifted)
{
TryCableBytewiseTransfer(P2, P3);
TryCableBytewiseTransfer(P3, P2);
}
else
{
TryCableBytewiseTransfer(P1, P2);
TryCableBytewiseTransfer(P2, P1);
}
break;
}
case 4:
{
if (_cableSpaced)
{
TryCableBytewiseTransfer(P1, P3);
TryCableBytewiseTransfer(P3, P1);
TryCableBytewiseTransfer(P2, P4);
TryCableBytewiseTransfer(P4, P2);
}
else if (_cableShifted)
{
TryCableBytewiseTransfer(P2, P3);
TryCableBytewiseTransfer(P3, P2);
TryCableBytewiseTransfer(P4, P1);
TryCableBytewiseTransfer(P1, P4);
}
else
{
TryCableBytewiseTransfer(P1, P2);
TryCableBytewiseTransfer(P2, P1);
TryCableBytewiseTransfer(P3, P4);
TryCableBytewiseTransfer(P4, P3);
}
break;
}
default:
throw new Exception();
}
}
_overflowL = nL - SampPerFrame;
_overflowR = nR - SampPerFrame;
if (_overflowL < 0 || _overflowR < 0)
for (int i = 0; i < _numCores; i++)
{
throw new Exception("Timing problem?");
_linkedOverflow[i] = n[i] - SampPerFrame;
if (_linkedOverflow[i] < 0)
{
throw new Exception("Timing problem?");
}
}
if (rendersound)
@ -133,22 +188,25 @@ namespace BizHawk.Emulation.Cores.Nintendo.Gameboy
}
// copy extra samples back to beginning
for (int i = 0; i < _overflowL * 2; i++)
for (int i = 0; i < _numCores; i++)
{
LeftBuffer[i] = LeftBuffer[i + (SampPerFrame * 2)];
}
for (int i = 0; i < _overflowR * 2; i++)
{
RightBuffer[i] = RightBuffer[i + (SampPerFrame * 2)];
for (int j = 0; j < _linkedOverflow[i] * 2; j++)
{
SoundBuffer[(i * MaxSampsPerFrame) + j] = SoundBuffer[(i * MaxSampsPerFrame) + j + (SampPerFrame * 2)];
}
}
}
}
}
L.FrameAdvancePost();
R.FrameAdvancePost();
IsLagFrame = L.IsLagFrame && R.IsLagFrame;
IsLagFrame = true;
for (int i = 0; i < _numCores; i++)
{
_linkedCores[i].FrameAdvancePost();
IsLagFrame &= _linkedCores[i].IsLagFrame;
}
if (IsLagFrame)
{
LagCount++;
@ -159,38 +217,71 @@ namespace BizHawk.Emulation.Cores.Nintendo.Gameboy
return true;
}
private void TryCableBytewiseTransfer(int master, int slave)
{
if (LibGambatte.gambatte_linkstatus(_linkedCores[master].GambatteState, 256) != 0) // ClockTrigger
{
LibGambatte.gambatte_linkstatus(_linkedCores[master].GambatteState, 257); // ack
int om = LibGambatte.gambatte_linkstatus(_linkedCores[master].GambatteState, 258); // GetOut
int os = LibGambatte.gambatte_linkstatus(_linkedCores[slave].GambatteState, 258);
LibGambatte.gambatte_linkstatus(_linkedCores[slave].GambatteState, om & 0xff); // ShiftIn
LibGambatte.gambatte_linkstatus(_linkedCores[master].GambatteState, os & 0xff); // ShiftIn
}
}
public int Frame { get; private set; }
public string SystemId => VSystemID.Raw.DGB;
public bool DeterministicEmulation => L.DeterministicEmulation && R.DeterministicEmulation;
public bool DeterministicEmulation => LinkedDeterministicEmulation();
public string BoardName => L.BoardName + '|' + R.BoardName;
private bool LinkedDeterministicEmulation()
{
bool deterministicEmulation = true;
for (int i = 0; i < _numCores; i++)
{
deterministicEmulation &= _linkedCores[i].DeterministicEmulation;
}
return deterministicEmulation;
}
public string BoardName => LinkedBoardName();
private string LinkedBoardName()
{
string boardName = "";
for (int i = 0; i < _numCores; i++)
{
boardName += _linkedCores[i].BoardName + "|";
}
return boardName.Remove(boardName.Length - 1);
}
public void ResetCounters()
{
Frame = 0;
LagCount = 0;
IsLagFrame = false;
for (int i = 0; i < _numCores; i++)
{
_linkedOverflow[i] = 0;
}
}
public void Dispose()
{
if (!_disposed)
if (_numCores > 0)
{
L.Dispose();
L = null;
for (int i = 0; i < _numCores; i++)
{
_linkedCores[i].Dispose();
_linkedCores[i] = null;
_linkedBlips[i].Dispose();
_linkedBlips[i] = null;
}
R.Dispose();
R = null;
_blipLeft.Dispose();
_blipLeft = null;
_blipRight.Dispose();
_blipRight = null;
_disposed = true;
_numCores = 0;
}
}
}

View File

@ -1,56 +0,0 @@
using System.Collections.Generic;
using BizHawk.Emulation.Common;
namespace BizHawk.Emulation.Cores.Nintendo.Gameboy
{
public partial class GambatteLink
{
private IMemoryDomains _memoryDomains;
private void SetMemoryDomains()
{
var mm = new List<MemoryDomain>();
foreach (var md in L.MemoryDomains)
{
mm.Add(new WrappedMemoryDomain("L " + md.Name, md));
}
foreach (var md in R.MemoryDomains)
{
mm.Add(new WrappedMemoryDomain("R " + md.Name, md));
}
_memoryDomains = new MemoryDomainList(mm);
(ServiceProvider as BasicServiceProvider).Register<IMemoryDomains>(_memoryDomains);
}
// todo: clean this up
private class WrappedMemoryDomain : MemoryDomain
{
private readonly MemoryDomain _m;
public WrappedMemoryDomain(string name, MemoryDomain m)
{
_m = m;
Name = name;
Size = m.Size;
WordSize = m.WordSize;
EndianType = m.EndianType;
Writable = m.Writable;
}
public override byte PeekByte(long addr)
{
return _m.PeekByte(addr);
}
public override void PokeByte(long addr, byte val)
{
_m.PokeByte(addr, val);
}
}
}
}

View File

@ -1,31 +0,0 @@
using System;
using BizHawk.Emulation.Common;
namespace BizHawk.Emulation.Cores.Nintendo.Gameboy
{
public partial class GambatteLink : ISaveRam
{
public bool SaveRamModified => L.SaveRamModified || R.SaveRamModified;
public byte[] CloneSaveRam()
{
var lb = L.CloneSaveRam()!;
var rb = R.CloneSaveRam()!;
byte[] ret = new byte[lb.Length + rb.Length];
Buffer.BlockCopy(lb, 0, ret, 0, lb.Length);
Buffer.BlockCopy(rb, 0, ret, lb.Length, rb.Length);
return ret;
}
public void StoreSaveRam(byte[] data)
{
var lb = new byte[L.CloneSaveRam()!.Length];
var rb = new byte[R.CloneSaveRam()!.Length];
Buffer.BlockCopy(data, 0, lb, 0, lb.Length);
Buffer.BlockCopy(data, lb.Length, rb, 0, rb.Length);
L.StoreSaveRam(lb);
R.StoreSaveRam(rb);
}
}
}

View File

@ -4,76 +4,78 @@ namespace BizHawk.Emulation.Cores.Nintendo.Gameboy
{
public partial class GambatteLink : ISettable<GambatteLink.GambatteLinkSettings, GambatteLink.GambatteLinkSyncSettings>
{
private GambatteLinkSettings _settings;
private GambatteLinkSyncSettings _syncSettings;
public GambatteLinkSettings GetSettings()
{
return new GambatteLinkSettings
(
L.GetSettings(),
R.GetSettings()
);
return _settings.Clone();
}
public GambatteLinkSyncSettings GetSyncSettings()
{
return new GambatteLinkSyncSettings
(
L.GetSyncSettings(),
R.GetSyncSettings()
);
return _syncSettings.Clone();
}
public PutSettingsDirtyBits PutSettings(GambatteLinkSettings o)
{
return (PutSettingsDirtyBits)((int)L.PutSettings(o.L) | (int)R.PutSettings(o.R));
_settings = o;
var ret = PutSettingsDirtyBits.None;
for (int i = 0; i < _numCores; i++)
{
ret |= _linkedCores[i].PutSettings(o._linkedSettings[i]);
}
return ret;
}
public PutSettingsDirtyBits PutSyncSettings(GambatteLinkSyncSettings o)
{
return (PutSettingsDirtyBits)((int)L.PutSyncSettings(o.L) | (int)R.PutSyncSettings(o.R));
_syncSettings = o;
var ret = PutSettingsDirtyBits.None;
for (int i = 0; i < _numCores; i++)
{
ret |= _linkedCores[i].PutSyncSettings(o._linkedSyncSettings[i]);
}
return ret;
}
public class GambatteLinkSettings
{
public Gameboy.GambatteSettings L;
public Gameboy.GambatteSettings R;
public Gameboy.GambatteSettings[] _linkedSettings;
public GambatteLinkSettings()
{
L = new Gameboy.GambatteSettings();
R = new Gameboy.GambatteSettings();
_linkedSettings = new Gameboy.GambatteSettings[MAX_PLAYERS] { new(), new(), new(), new() };
}
public GambatteLinkSettings(Gameboy.GambatteSettings L, Gameboy.GambatteSettings R)
public GambatteLinkSettings(Gameboy.GambatteSettings one, Gameboy.GambatteSettings two, Gameboy.GambatteSettings three, Gameboy.GambatteSettings four)
{
this.L = L;
this.R = R;
_linkedSettings = new Gameboy.GambatteSettings[MAX_PLAYERS] { one, two, three, four };
}
public GambatteLinkSettings Clone()
{
return new GambatteLinkSettings(L.Clone(), R.Clone());
return new GambatteLinkSettings(_linkedSettings[P1].Clone(), _linkedSettings[P2].Clone(), _linkedSettings[P3].Clone(), _linkedSettings[P4].Clone());
}
}
public class GambatteLinkSyncSettings
{
public Gameboy.GambatteSyncSettings L;
public Gameboy.GambatteSyncSettings R;
public Gameboy.GambatteSyncSettings[] _linkedSyncSettings;
public GambatteLinkSyncSettings()
{
L = new Gameboy.GambatteSyncSettings();
R = new Gameboy.GambatteSyncSettings();
_linkedSyncSettings = new Gameboy.GambatteSyncSettings[MAX_PLAYERS] { new(), new(), new(), new() };
}
public GambatteLinkSyncSettings(Gameboy.GambatteSyncSettings L, Gameboy.GambatteSyncSettings R)
public GambatteLinkSyncSettings(Gameboy.GambatteSyncSettings one, Gameboy.GambatteSyncSettings two, Gameboy.GambatteSyncSettings three, Gameboy.GambatteSyncSettings four)
{
this.L = L;
this.R = R;
_linkedSyncSettings = new Gameboy.GambatteSyncSettings[MAX_PLAYERS] { one, two, three, four };
}
public GambatteLinkSyncSettings Clone()
{
return new GambatteLinkSyncSettings(L.Clone(), R.Clone());
return new GambatteLinkSyncSettings(_linkedSyncSettings[P1].Clone(), _linkedSyncSettings[P2].Clone(), _linkedSyncSettings[P3].Clone(), _linkedSyncSettings[P4].Clone());
}
}
}

View File

@ -1,111 +1,154 @@
using System;
using BizHawk.Emulation.Common;
namespace BizHawk.Emulation.Cores.Nintendo.Gameboy
{
public partial class GambatteLink : ISoundProvider
{
public bool CanProvideAsync => false;
public void SetSyncMode(SyncSoundMode mode)
{
if (mode == SyncSoundMode.Async)
{
throw new NotSupportedException("Async mode is not supported.");
}
}
public SyncSoundMode SyncMode => SyncSoundMode.Sync;
public void GetSamplesSync(out short[] samples, out int nsamp)
{
nsamp = _sampleBufferContains;
samples = SampleBuffer;
}
public void GetSamplesAsync(short[] samples)
{
throw new InvalidOperationException("Async mode is not supported.");
}
public void DiscardSamples()
{
_sampleBufferContains = 0;
}
// i tried using the left and right buffers and then mixing them together... it was kind of a mess of code, and slow
private BlipBuffer _blipLeft;
private BlipBuffer _blipRight;
private readonly short[] LeftBuffer = new short[(35112 + 2064) * 2];
private readonly short[] RightBuffer = new short[(35112 + 2064) * 2];
private readonly short[] SampleBuffer = new short[1536];
private int _sampleBufferContains = 0;
private int _latchLeft;
private int _latchRight;
private unsafe void PrepSound()
{
fixed (short* sl = LeftBuffer, sr = RightBuffer)
{
for (uint i = 0; i < SampPerFrame * 2; i += 2)
{
int s = (sl[i] + sl[i + 1]) / 2;
if (s != _latchLeft)
{
_blipLeft.AddDelta(i, s - _latchLeft);
_latchLeft = s;
}
s = (sr[i] + sr[i + 1]) / 2;
if (s != _latchRight)
{
_blipRight.AddDelta(i, s - _latchRight);
_latchRight = s;
}
}
}
_blipLeft.EndFrame(SampPerFrame * 2);
_blipRight.EndFrame(SampPerFrame * 2);
int count = _blipLeft.SamplesAvailable();
if (count != _blipRight.SamplesAvailable())
{
throw new Exception("Sound problem?");
}
// calling blip.Clear() causes rounding fractions to be reset,
// and if only one channel is muted, in subsequent frames we can be off by a sample or two
// not a big deal, but we didn't account for it. so we actually complete the entire
// audio read and then stamp it out if muted.
_blipLeft.ReadSamplesLeft(SampleBuffer, count);
if (L.Muted)
{
fixed (short* p = SampleBuffer)
{
for (int i = 0; i < SampleBuffer.Length; i += 2)
{
p[i] = 0;
}
}
}
_blipRight.ReadSamplesRight(SampleBuffer, count);
if (R.Muted)
{
fixed (short* p = SampleBuffer)
{
for (int i = 1; i < SampleBuffer.Length; i += 2)
{
p[i] = 0;
}
}
}
_sampleBufferContains = count;
}
}
}
using BizHawk.Emulation.Common;
namespace BizHawk.Emulation.Cores.Nintendo.Gameboy
{
public partial class GambatteLink : ISoundProvider
{
public bool CanProvideAsync => false;
public void SetSyncMode(SyncSoundMode mode)
{
if (mode == SyncSoundMode.Async)
{
throw new NotSupportedException("Async mode is not supported.");
}
}
public SyncSoundMode SyncMode => SyncSoundMode.Sync;
public void GetSamplesSync(out short[] samples, out int nsamp)
{
nsamp = _sampleBufferContains;
samples = SampleBuffer;
}
public void GetSamplesAsync(short[] samples)
{
throw new InvalidOperationException("Async mode is not supported.");
}
public void DiscardSamples()
{
_sampleBufferContains = 0;
}
// i tried using the left and right buffers and then mixing them together... it was kind of a mess of code, and slow
private readonly BlipBuffer[] _linkedBlips;
private readonly short[] SoundBuffer;
private readonly short[] ScratchBuffer = new short[1536];
private readonly short[] SampleBuffer = new short[1536];
private int _sampleBufferContains = 0;
private readonly int[] _linkedLatches;
private unsafe void PrepSound()
{
fixed (short* sb = &SoundBuffer[0])
{
for (int i = 0; i < _numCores; i++)
{
for (uint j = 0; j < SampPerFrame * 2; j += 2)
{
int s = (sb[(i * MaxSampsPerFrame) + j] + sb[(i * MaxSampsPerFrame) + j + 1]) / 2;
if (s != _linkedLatches[i])
{
_linkedBlips[i].AddDelta(j, s - _linkedLatches[i]);
_linkedLatches[i] = s;
}
}
_linkedBlips[i].EndFrame(SampPerFrame * 2);
}
}
int count = _linkedBlips[P1].SamplesAvailable();
for (int i = 1; i < _numCores; i++)
{
if (count != _linkedBlips[i].SamplesAvailable())
{
throw new Exception("Sound problem?");
}
}
// calling blip.Clear() causes rounding fractions to be reset,
// and if only one channel is muted, in subsequent frames we can be off by a sample or two
// not a big deal, but we didn't account for it. so we actually complete the entire
// audio read and then stamp it out if muted.
switch (_numCores)
{
case 2:
{
// no need to do any complicated mixing
_linkedBlips[P1].ReadSamplesLeft(_settings._linkedSettings[P1].Muted ? ScratchBuffer : SampleBuffer, count);
_linkedBlips[P2].ReadSamplesRight(_settings._linkedSettings[P2].Muted ? ScratchBuffer : SampleBuffer, count);
break;
}
case 3:
{
// since P2 is center, mix its samples with P1 and P3
_linkedBlips[P1].ReadSamplesLeft(_settings._linkedSettings[P1].Muted ? ScratchBuffer : SampleBuffer, count);
_linkedBlips[P3].ReadSamplesRight(_settings._linkedSettings[P3].Muted ? ScratchBuffer : SampleBuffer, count);
_linkedBlips[P2].ReadSamples(ScratchBuffer, count, false);
if (!_settings._linkedSettings[P2].Muted)
{
fixed (short* p = SampleBuffer, q = ScratchBuffer)
{
for (int i = 0; i < SampleBuffer.Length; i++)
{
int s = (p[i] + q[i]) / 2;
p[i] = (short)s;
}
}
}
break;
}
case 4:
{
// since P1 and P2 are left side and P3 and P4 are right side, mix their samples accordingly
_linkedBlips[P1].ReadSamplesLeft(_settings._linkedSettings[P1].Muted ? ScratchBuffer : SampleBuffer, count);
_linkedBlips[P3].ReadSamplesRight(_settings._linkedSettings[P3].Muted ? ScratchBuffer : SampleBuffer, count);
_linkedBlips[P2].ReadSamplesLeft(ScratchBuffer, count);
_linkedBlips[P4].ReadSamplesRight(ScratchBuffer, count);
if (_settings._linkedSettings[P2].Muted)
{
fixed (short* p = ScratchBuffer)
{
for (int i = 0; i < ScratchBuffer.Length; i += 2)
{
p[i] = 0;
}
}
}
if (_settings._linkedSettings[P4].Muted)
{
fixed (short* p = ScratchBuffer)
{
for (int i = 1; i < ScratchBuffer.Length; i += 2)
{
p[i] = 0;
}
}
}
fixed (short* p = SampleBuffer, q = ScratchBuffer)
{
for (int i = 0; i < SampleBuffer.Length; i++)
{
int s = (p[i] + q[i]) / 2;
p[i] = (short)s;
}
}
break;
}
default:
throw new Exception();
}
_sampleBufferContains = count;
}
}
}

View File

@ -1,4 +1,5 @@
using System.IO;
using System;
using System.IO;
using Newtonsoft.Json;
@ -10,87 +11,119 @@ namespace BizHawk.Emulation.Cores.Nintendo.Gameboy
{
public void SaveStateText(TextWriter writer)
{
var s = new DGBSerialized
{
L = L.SaveState(),
R = R.SaveState(),
IsLagFrame = IsLagFrame,
LagCount = LagCount,
Frame = Frame,
overflowL = _overflowL,
overflowR = _overflowR,
LatchL = _latchLeft,
LatchR = _latchRight,
cableconnected = _cableconnected,
cablediscosignal = _cablediscosignal
};
ser.Serialize(writer, s);
ser.Serialize(writer, new DGBSerialized(this));
}
public void LoadStateText(TextReader reader)
{
var s = (DGBSerialized)ser.Deserialize(reader, typeof(DGBSerialized));
L.LoadState(s.L);
R.LoadState(s.R);
if (s.NumCores != _numCores)
{
throw new InvalidOperationException("Core number mismatch!");
}
for (int i = 0; i < _numCores; i++)
{
_linkedCores[i].LoadState(s.LinkedStates[i]);
_linkedOverflow[i] = s.LinkedOverflow[i];
_linkedLatches[i] = s.LinkedLatches[i];
}
IsLagFrame = s.IsLagFrame;
LagCount = s.LagCount;
Frame = s.Frame;
_overflowL = s.overflowL;
_overflowR = s.overflowR;
_latchLeft = s.LatchL;
_latchRight = s.LatchR;
_cableconnected = s.cableconnected;
_cablediscosignal = s.cablediscosignal;
_cableConnected = s.CableConnected;
_cableDiscoSignal = s.CableDiscoSignal;
_cableShifted = s.CableShifted;
_cableShiftSignal = s.CableShiftSignal;
_cableSpaced = s.CableSpaced;
_cableSpaceSignal = s.CableSpaceSignal;
}
public void SaveStateBinary(BinaryWriter writer)
{
L.SaveStateBinary(writer);
R.SaveStateBinary(writer);
writer.Write(_numCores);
for (int i = 0; i < _numCores; i++)
{
_linkedCores[i].SaveStateBinary(writer);
writer.Write(_linkedOverflow[i]);
writer.Write(_linkedLatches[i]);
}
// other variables
writer.Write(IsLagFrame);
writer.Write(LagCount);
writer.Write(Frame);
writer.Write(_overflowL);
writer.Write(_overflowR);
writer.Write(_latchLeft);
writer.Write(_latchRight);
writer.Write(_cableconnected);
writer.Write(_cablediscosignal);
writer.Write(_cableConnected);
writer.Write(_cableDiscoSignal);
writer.Write(_cableShifted);
writer.Write(_cableShiftSignal);
writer.Write(_cableSpaced);
writer.Write(_cableSpaceSignal);
}
public void LoadStateBinary(BinaryReader reader)
{
L.LoadStateBinary(reader);
R.LoadStateBinary(reader);
if (_numCores != reader.ReadInt32())
{
throw new InvalidOperationException("Core number mismatch!");
}
for (int i = 0; i < _numCores; i++)
{
_linkedCores[i].LoadStateBinary(reader);
_linkedOverflow[i] = reader.ReadInt32();
_linkedLatches[i] = reader.ReadInt32();
}
// other variables
IsLagFrame = reader.ReadBoolean();
LagCount = reader.ReadInt32();
Frame = reader.ReadInt32();
_overflowL = reader.ReadInt32();
_overflowR = reader.ReadInt32();
_latchLeft = reader.ReadInt32();
_latchRight = reader.ReadInt32();
_cableconnected = reader.ReadBoolean();
_cablediscosignal = reader.ReadBoolean();
_cableConnected = reader.ReadBoolean();
_cableDiscoSignal = reader.ReadBoolean();
_cableShifted = reader.ReadBoolean();
_cableShiftSignal = reader.ReadBoolean();
_cableSpaced = reader.ReadBoolean();
_cableSpaceSignal = reader.ReadBoolean();
}
private readonly JsonSerializer ser = new JsonSerializer { Formatting = Formatting.Indented };
private class DGBSerialized
{
public TextState<Gameboy.TextStateData> L;
public TextState<Gameboy.TextStateData> R;
public int NumCores;
public TextState<Gameboy.TextStateData>[] LinkedStates;
// other data
public bool IsLagFrame;
public int LagCount;
public int Frame;
public int overflowL;
public int overflowR;
public int LatchL;
public int LatchR;
public bool cableconnected;
public bool cablediscosignal;
public int[] LinkedOverflow;
public int[] LinkedLatches;
public bool CableConnected;
public bool CableDiscoSignal;
public bool CableShifted;
public bool CableShiftSignal;
public bool CableSpaced;
public bool CableSpaceSignal;
public DGBSerialized(GambatteLink linkcore)
{
NumCores = linkcore._numCores;
LinkedStates = new TextState<Gameboy.TextStateData>[NumCores];
LinkedOverflow = new int[NumCores];
LinkedLatches = new int[NumCores];
for (int i = 0; i < NumCores; i++)
{
LinkedStates[i] = linkcore._linkedCores[i].SaveState();
LinkedOverflow[i] = linkcore._linkedOverflow[i];
LinkedLatches[i] = linkcore._linkedLatches[i];
}
IsLagFrame = linkcore.IsLagFrame;
LagCount = linkcore.LagCount;
Frame = linkcore.Frame;
CableConnected = linkcore._cableConnected;
CableDiscoSignal = linkcore._cableDiscoSignal;
CableShifted = linkcore._cableShifted;
CableShiftSignal = linkcore._cableShiftSignal;
CableSpaced = linkcore._cableSpaced;
CableSpaceSignal = linkcore._cableSpaceSignal;
}
}
}
}

View File

@ -1,30 +1,32 @@
using BizHawk.Emulation.Common;
using System;
using BizHawk.Emulation.Common;
namespace BizHawk.Emulation.Cores.Nintendo.Gameboy
{
public partial class GambatteLink : IVideoProvider
{
public int VirtualWidth => 320;
public int VirtualWidth => 160 * _numCores;
public int VirtualHeight => 144;
public int BufferWidth => 320;
public int BufferWidth => 160 * _numCores;
public int BufferHeight => 144;
public int VsyncNumerator => L.VsyncNumerator;
public int VsyncNumerator => _linkedCores[P1].VsyncNumerator;
public int VsyncDenominator => L.VsyncDenominator;
public int VsyncDenominator => _linkedCores[P1].VsyncDenominator;
public int BackgroundColor => unchecked((int)0xff000000);
private readonly int[] FrameBuffer = CreateVideoBuffer();
private readonly int[] FrameBuffer;
public int[] GetVideoBuffer() => VideoBuffer;
private readonly int[] VideoBuffer = CreateVideoBuffer();
private readonly int[] VideoBuffer;
private static int[] CreateVideoBuffer()
private int[] CreateVideoBuffer()
{
var b = new int[160 * 2 * 144];
for (int i = 0; i < (160 * 2 * 144); i++)
var b = new int[BufferWidth * BufferHeight];
for (int i = 0; i < b.Length; i++)
{
b[i] = -1; // GB/C screen is disabled on bootup, so it always starts as white, not black
}

View File

@ -1,4 +1,5 @@
using System;
using System.Linq;
using BizHawk.Emulation.Common;
@ -6,32 +7,42 @@ namespace BizHawk.Emulation.Cores.Nintendo.Gameboy
{
[PortedCore(CoreNames.DualGambatte, "sinamas/natt")]
[ServiceNotApplicable(new[] { typeof(IDriveLight) })]
public partial class GambatteLink : IEmulator, IVideoProvider, ISoundProvider, IInputPollable, ISaveRam, IStatable, ILinkable,
IBoardInfo, IRomInfo, IDebuggable, ISettable<GambatteLink.GambatteLinkSettings, GambatteLink.GambatteLinkSyncSettings>, ICodeDataLogger
public partial class GambatteLink : ILinkable, IRomInfo
{
[CoreConstructor(VSystemID.Raw.DGB)]
public GambatteLink(CoreLoadParameters<GambatteLinkSettings, GambatteLinkSyncSettings> lp)
{
if (lp.Roms.Count != 2)
if (lp.Roms.Count < MIN_PLAYERS || lp.Roms.Count > MAX_PLAYERS)
throw new InvalidOperationException("Wrong number of roms");
ServiceProvider = new BasicServiceProvider(this);
GambatteLinkSettings linkSettings = lp.Settings ?? new GambatteLinkSettings();
GambatteLinkSyncSettings linkSyncSettings = lp.SyncSettings ?? new GambatteLinkSyncSettings();
_numCores = lp.Roms.Count;
L = new Gameboy(lp.Comm, lp.Roms[0].Game, lp.Roms[0].RomData, linkSettings.L, linkSyncSettings.L, lp.DeterministicEmulationRequested);
R = new Gameboy(lp.Comm, lp.Roms[1].Game, lp.Roms[1].RomData, linkSettings.R, linkSyncSettings.R, lp.DeterministicEmulationRequested);
_serviceProvider = new BasicServiceProvider(this);
_settings = lp.Settings ?? new GambatteLinkSettings();
_syncSettings = lp.SyncSettings ?? new GambatteLinkSyncSettings();
// connect link cable
LibGambatte.gambatte_linkstatus(L.GambatteState, 259);
LibGambatte.gambatte_linkstatus(R.GambatteState, 259);
_linkedCores = new Gameboy[_numCores];
_linkedConts = new SaveController[_numCores];
_linkedBlips = new BlipBuffer[_numCores];
_linkedLatches = new int[_numCores];
_linkedOverflow = new int[_numCores];
L.ConnectInputCallbackSystem(_inputCallbacks);
R.ConnectInputCallbackSystem(_inputCallbacks);
L.ConnectMemoryCallbackSystem(_memorycallbacks);
R.ConnectMemoryCallbackSystem(_memorycallbacks);
RomDetails = "";
_memoryCallbacks = new MemoryCallbackSystem(new[] { "System Bus" });
RomDetails = "LEFT:\r\n" + L.RomDetails + "RIGHT:\r\n" + R.RomDetails;
for (int i = 0; i < _numCores; i++)
{
_linkedCores[i] = new Gameboy(lp.Comm, lp.Roms[i].Game, lp.Roms[i].RomData, _settings._linkedSettings[i], _syncSettings._linkedSyncSettings[i], lp.DeterministicEmulationRequested);
LibGambatte.gambatte_linkstatus(_linkedCores[i].GambatteState, 259); // connect link cable
_linkedCores[i].ConnectInputCallbackSystem(_inputCallbacks);
_linkedCores[i].ConnectMemoryCallbackSystem(_memoryCallbacks);
_linkedConts[i] = new SaveController(Gameboy.CreateControllerDefinition(false, false));
_linkedBlips[i] = new BlipBuffer(1024);
_linkedBlips[i].SetRates(2097152 * 2, 44100);
_linkedOverflow[i] = 0;
_linkedLatches[i] = 0;
RomDetails += $"P{i + 1}:\r\n" + _linkedCores[i].RomDetails;
}
LinkConnected = true;
@ -39,56 +50,99 @@ namespace BizHawk.Emulation.Cores.Nintendo.Gameboy
LagCount = 0;
IsLagFrame = false;
_blipLeft = new BlipBuffer(1024);
_blipRight = new BlipBuffer(1024);
_blipLeft.SetRates(2097152 * 2, 44100);
_blipRight.SetRates(2097152 * 2, 44100);
SoundBuffer = new short[MaxSampsPerFrame * _numCores];
SetMemoryDomains();
FrameBuffer = CreateVideoBuffer();
VideoBuffer = CreateVideoBuffer();
GBLinkController = CreateControllerDefinition();
_linkedSaveRam = new LinkedSaveRam(_linkedCores, _numCores);
_serviceProvider.Register<ISaveRam>(_linkedSaveRam);
_linkedMemoryDomains = new LinkedMemoryDomains(_linkedCores, _numCores);
_serviceProvider.Register<IMemoryDomains>(_linkedMemoryDomains);
_linkedDebuggable = new LinkedDebuggable(_linkedCores, _numCores, _memoryCallbacks);
_serviceProvider.Register<IDebuggable>(_linkedDebuggable);
}
private readonly BasicServiceProvider _serviceProvider;
private readonly MemoryCallbackSystem _memoryCallbacks;
public string RomDetails { get; }
public bool LinkConnected
{
get => _cableconnected;
set => _cableconnected = value;
get => _cableConnected;
set => _cableConnected = value;
}
private bool _disposed = false;
private int _numCores = 0;
private readonly Gameboy[] _linkedCores;
private Gameboy L;
private Gameboy R;
private readonly LinkedSaveRam _linkedSaveRam;
private readonly LinkedMemoryDomains _linkedMemoryDomains;
private readonly LinkedDebuggable _linkedDebuggable;
// counter to ensure we do 35112 samples per frame
private int _overflowL = 0;
private int _overflowR = 0;
// counters to ensure we do 35112 samples per frame
private readonly int[] _linkedOverflow;
// if true, the link cable is currently connected
private bool _cableconnected = true;
private bool _cableConnected = true;
// if true, the link cable is currently shifted (3x/4x only)
private bool _cableShifted = false;
// if true, the link cable is currently spaced outwards (3x/4x only)
private bool _cableSpaced = false;
// if true, the link cable toggle signal is currently asserted
private bool _cablediscosignal = false;
private bool _cableDiscoSignal = false;
// if true, the link cable shift signal is currently asserted
private bool _cableShiftSignal = false;
// if true, the link cable spacing signal is currently asserted
private bool _cableSpaceSignal = false;
private const int SampPerFrame = 35112;
private const int MaxSampsPerFrame = (SampPerFrame + 2064) * 2;
private readonly SaveController LCont = new SaveController(Gameboy.CreateControllerDefinition(false, false));
private readonly SaveController RCont = new SaveController(Gameboy.CreateControllerDefinition(false, false));
private readonly SaveController[] _linkedConts;
public bool IsCGBMode(bool right)
public bool IsCGBMode(int which)
{
return right ? R.IsCGBMode() : L.IsCGBMode();
return which < _numCores && _linkedCores[which].IsCGBMode();
}
private static readonly ControllerDefinition DualGbController = new ControllerDefinition
private ControllerDefinition GBLinkController { get; }
private ControllerDefinition CreateControllerDefinition()
{
Name = "Dual Gameboy Controller",
BoolButtons =
var ret = new ControllerDefinition { Name = $"GB Link {_numCores}x Controller" };
for (int i = 0; i < _numCores; i++)
{
"P1 Up", "P1 Down", "P1 Left", "P1 Right", "P1 A", "P1 B", "P1 Select", "P1 Start", "P1 Power",
"P2 Up", "P2 Down", "P2 Left", "P2 Right", "P2 A", "P2 B", "P2 Select", "P2 Start", "P2 Power",
"Toggle Cable"
ret.BoolButtons.AddRange(
new[] { "Up", "Down", "Left", "Right", "A", "B", "Select", "Start", "Power" }
.Select(s => $"P{i + 1} {s}"));
}
};
ret.BoolButtons.Add("Toggle Cable Connection");
if (_numCores > 2)
{
ret.BoolButtons.Add("Toggle Cable Shift");
ret.BoolButtons.Add("Toggle Cable Spacing");
}
return ret;
}
private const int P1 = 0;
private const int P2 = 1;
private const int P3 = 2;
private const int P4 = 3;
private const int MIN_PLAYERS = 2;
private const int MAX_PLAYERS = 4;
}
}