diff --git a/BizHawk.Client.ApiHawk/Classes/BizHawkSystemIdToCoreSystemEnumConverter.cs b/BizHawk.Client.ApiHawk/Classes/BizHawkSystemIdToCoreSystemEnumConverter.cs
index 17b9e26884..a4b66f55cd 100644
--- a/BizHawk.Client.ApiHawk/Classes/BizHawkSystemIdToCoreSystemEnumConverter.cs
+++ b/BizHawk.Client.ApiHawk/Classes/BizHawkSystemIdToCoreSystemEnumConverter.cs
@@ -98,6 +98,7 @@ namespace BizHawk.Client.ApiHawk
case "VB":
case "NGP":
+ case "DNGP":
return 0; // like I give a shit
default:
diff --git a/BizHawk.Emulation.Common/Database/Database.cs b/BizHawk.Emulation.Common/Database/Database.cs
index 5068cd22ed..8363845f12 100644
--- a/BizHawk.Emulation.Common/Database/Database.cs
+++ b/BizHawk.Emulation.Common/Database/Database.cs
@@ -338,7 +338,7 @@ namespace BizHawk.Emulation.Common
case ".NGP":
case ".NGC":
- game.System = "NGP";
+ game.System = "DNGP";
break;
}
diff --git a/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj b/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj
index 9897fac5b4..aa0d5e360a 100644
--- a/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj
+++ b/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj
@@ -229,7 +229,7 @@
-
+
@@ -1210,6 +1210,7 @@
+
@@ -1290,6 +1291,8 @@
+
+
@@ -1395,4 +1398,4 @@
-->
-
+
\ No newline at end of file
diff --git a/BizHawk.Emulation.Cores/Consoles/SNK/DualNeoGeoPort.cs b/BizHawk.Emulation.Cores/Consoles/SNK/DualNeoGeoPort.cs
new file mode 100644
index 0000000000..623984cc5a
--- /dev/null
+++ b/BizHawk.Emulation.Cores/Consoles/SNK/DualNeoGeoPort.cs
@@ -0,0 +1,109 @@
+using BizHawk.Emulation.Common;
+using BizHawk.Emulation.Cores.Sound;
+using BizHawk.Emulation.Cores.Waterbox;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace BizHawk.Emulation.Cores.Consoles.SNK
+{
+ [CoreAttributes("Dual NeoPop", "Thomas Klausner and natt", true, false, "0.9.44.1",
+ "https://mednafen.github.io/releases/", false)]
+ public class DualNeoGeoPort : IEmulator
+ {
+ private NeoGeoPort _left;
+ private NeoGeoPort _right;
+ private readonly BasicServiceProvider _serviceProvider;
+ private bool _disposed = false;
+ private readonly DualSyncSound _soundProvider;
+ private readonly SideBySideVideo _videoProvider;
+
+ [CoreConstructor("DNGP")]
+ public DualNeoGeoPort(CoreComm comm, byte[] rom, bool deterministic)
+ {
+ CoreComm = comm;
+ _left = new NeoGeoPort(comm, rom, null, deterministic, PeRunner.CanonicalStart);
+ _right = new NeoGeoPort(comm, rom, null, deterministic, PeRunner.AlternateStart);
+
+ _serviceProvider = new BasicServiceProvider(this);
+ _soundProvider = new DualSyncSound(_left, _right);
+ _serviceProvider.Register(_soundProvider);
+ _videoProvider = new SideBySideVideo(_left, _right);
+ _serviceProvider.Register(_videoProvider);
+ }
+
+ public void FrameAdvance(IController controller, bool render, bool rendersound = true)
+ {
+ _left.FrameAdvance(new PrefixController(controller, "P1 "), render, rendersound);
+ _right.FrameAdvance(new PrefixController(controller, "P2 "), render, rendersound);
+ Frame++;
+ _soundProvider.Fetch();
+ _videoProvider.Fetch();
+ }
+
+ private class PrefixController : IController
+ {
+ public PrefixController(IController controller, string prefix)
+ {
+ _controller = controller;
+ _prefix = prefix;
+ }
+
+ private readonly IController _controller;
+ private readonly string _prefix;
+
+ public ControllerDefinition Definition => null;
+
+ public float GetFloat(string name)
+ {
+ return _controller.GetFloat(_prefix + name);
+ }
+
+ public bool IsPressed(string button)
+ {
+ return _controller.IsPressed(_prefix + button);
+ }
+ }
+
+ public ControllerDefinition ControllerDefinition => DualNeoGeoPortController;
+
+ private static readonly ControllerDefinition DualNeoGeoPortController = new ControllerDefinition
+ {
+ BoolButtons =
+ {
+ "P1 Up", "P1 Down", "P1 Left", "P1 Right", "P1 A", "P1 B", "P1 Option", "P1 Power",
+ "P2 Up", "P2 Down", "P2 Left", "P2 Right", "P2 A", "P2 B", "P2 Option", "P2 Power"
+ },
+ Name = "Dual NeoGeo Portable Controller"
+ };
+
+ public void ResetCounters()
+ {
+ Frame = 0;
+ }
+
+ public int Frame { get; private set; }
+
+ public IEmulatorServiceProvider ServiceProvider => _serviceProvider;
+
+ public CoreComm CoreComm { get; }
+
+ public bool DeterministicEmulation => _left.DeterministicEmulation && _right.DeterministicEmulation;
+
+ public string SystemId => "DNGP";
+
+ public void Dispose()
+ {
+ if (!_disposed)
+ {
+ _left.Dispose();
+ _right.Dispose();
+ _left = null;
+ _right = null;
+ _disposed = true;
+ }
+ }
+ }
+}
diff --git a/BizHawk.Emulation.Cores/Consoles/SNK/NeoGeoPort.cs b/BizHawk.Emulation.Cores/Consoles/SNK/NeoGeoPort.cs
index 957e547718..108473d2c1 100644
--- a/BizHawk.Emulation.Cores/Consoles/SNK/NeoGeoPort.cs
+++ b/BizHawk.Emulation.Cores/Consoles/SNK/NeoGeoPort.cs
@@ -25,6 +25,11 @@ namespace BizHawk.Emulation.Cores.Consoles.SNK
[CoreConstructor("NGP")]
public NeoGeoPort(CoreComm comm, byte[] rom, SyncSettings syncSettings, bool deterministic)
+ :this(comm, rom, syncSettings, deterministic, PeRunner.CanonicalStart)
+ {
+ }
+
+ internal NeoGeoPort(CoreComm comm, byte[] rom, SyncSettings syncSettings, bool deterministic, ulong startAddress)
{
ServiceProvider = new BasicServiceProvider(this);
CoreComm = comm;
@@ -37,7 +42,8 @@ namespace BizHawk.Emulation.Cores.Consoles.SNK
SbrkHeapSizeKB = 256,
SealedHeapSizeKB = 10 * 1024, // must be a bit larger than twice the ROM size
InvisibleHeapSizeKB = 4,
- PlainHeapSizeKB = 4
+ PlainHeapSizeKB = 4,
+ StartAddress = startAddress
});
_neopop = BizInvoker.GetInvoker(_exe, _exe);
diff --git a/BizHawk.Emulation.Cores/SideBySideVideo.cs b/BizHawk.Emulation.Cores/SideBySideVideo.cs
new file mode 100644
index 0000000000..f43495d634
--- /dev/null
+++ b/BizHawk.Emulation.Cores/SideBySideVideo.cs
@@ -0,0 +1,59 @@
+using BizHawk.Emulation.Common;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace BizHawk.Emulation.Cores
+{
+ public class SideBySideVideo : IVideoProvider
+ {
+ public SideBySideVideo(IVideoProvider l, IVideoProvider r)
+ {
+ _l = l;
+ _r = r;
+ }
+
+ private static unsafe void Blit(int* src, int srcp, int* dst, int dstp, int w, int h)
+ {
+ int* srcend = src + h * srcp;
+ while (src < srcend)
+ {
+ for (int j = 0; j < w; j++)
+ dst[j] = src[j];
+ src += srcp;
+ dst += dstp;
+ }
+ }
+
+ public unsafe void Fetch()
+ {
+ int h = BufferHeight;
+ int w = BufferWidth;
+ if (_buff.Length < w * h)
+ _buff = new int[w * h];
+
+ fixed(int* _pl = _l.GetVideoBuffer(), _pr = _r.GetVideoBuffer(), _pd = _buff)
+ {
+ Blit(_pl, w / 2, _pd, w, w / 2, h);
+ Blit(_pr, w / 2, _pd + w / 2, w, w / 2, h);
+ }
+ }
+ private readonly IVideoProvider _l;
+ private readonly IVideoProvider _r;
+ private int[] _buff = new int[0];
+ public int BackgroundColor => _l.BackgroundColor;
+ public int BufferHeight => _l.BufferHeight;
+ public int BufferWidth => _l.BufferWidth * 2;
+ public int VirtualHeight => _l.VirtualHeight;
+ public int VirtualWidth => _l.VirtualWidth * 2;
+ public int VsyncDenominator => _l.VsyncDenominator;
+ public int VsyncNumerator => _l.VsyncNumerator;
+
+ public int[] GetVideoBuffer()
+ {
+ return _buff;
+ }
+ }
+}
diff --git a/BizHawk.Emulation.Cores/Sound/DualSyncSound.cs b/BizHawk.Emulation.Cores/Sound/DualSyncSound.cs
new file mode 100644
index 0000000000..fd61031f22
--- /dev/null
+++ b/BizHawk.Emulation.Cores/Sound/DualSyncSound.cs
@@ -0,0 +1,96 @@
+using BizHawk.Emulation.Common;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace BizHawk.Emulation.Cores.Sound
+{
+ ///
+ /// this thing more or less ASSumes that the two cores input to it both provide the same number of samples in each go
+ ///
+ public class DualSyncSound : ISoundProvider
+ {
+ private ISoundProvider _left;
+ private ISoundProvider _right;
+ private int _nsamp;
+ private short[] _samp = new short[0];
+
+ private short[] _leftOverflow = new short[32];
+ private int _leftOverflowCount = 0;
+ private short[] _rightOverflow = new short[32];
+ private int _rightOverflowCount = 0;
+
+
+ public DualSyncSound(ISoundProvider left, ISoundProvider right)
+ {
+ _left = left;
+ _right = right;
+ }
+
+ private static short Mix(short[] buff, int idx)
+ {
+ int s = buff[idx * 2] + buff[idx * 2 + 1];
+ if (s > 32767)
+ s = 32767;
+ if (s < -32768)
+ s = -32768;
+ return (short)s;
+ }
+
+ public void Fetch()
+ {
+ int nsampl, nsampr;
+ short[] sampl, sampr;
+ _left.GetSamplesSync(out sampl, out nsampl);
+ _right.GetSamplesSync(out sampr, out nsampr);
+
+ int n = Math.Min(nsampl + _leftOverflowCount, nsampr + _rightOverflowCount);
+
+ if (_samp.Length < n * 2)
+ _samp = new short[n * 2];
+
+ int i, j;
+ for (i = 0, j = 0; i < _leftOverflowCount; i++, j++)
+ _samp[j * 2] = Mix(_leftOverflow, i);
+ for (i = 0; j < n; i++, j++)
+ _samp[j * 2] = Mix(sampl, i);
+ _leftOverflowCount = Math.Min(nsampl - i, 16);
+ Array.Copy(sampl, i * 2, _leftOverflow, 0, _leftOverflowCount * 2);
+ for (i = 0, j = 0; i < _rightOverflowCount; i++, j++)
+ _samp[j * 2 + 1] = Mix(_rightOverflow, i);
+ for (i = 0; j < n; i++, j++)
+ _samp[j * 2 + 1] = Mix(sampr, i);
+ _rightOverflowCount = Math.Min(nsampr - i, 16);
+ Array.Copy(sampr, i * 2, _rightOverflow, 0, _rightOverflowCount * 2);
+
+ _nsamp = n;
+ }
+
+ public bool CanProvideAsync => false;
+
+ public SyncSoundMode SyncMode => SyncSoundMode.Sync;
+
+ public void DiscardSamples()
+ {
+ }
+
+ public void GetSamplesAsync(short[] samples)
+ {
+ throw new InvalidOperationException();
+ }
+
+ public void GetSamplesSync(out short[] samples, out int nsamp)
+ {
+ samples = _samp;
+ nsamp = _nsamp;
+ }
+
+ public void SetSyncMode(SyncSoundMode mode)
+ {
+ if (mode != SyncSoundMode.Sync)
+ throw new InvalidOperationException();
+ }
+ }
+}
diff --git a/BizHawk.Emulation.Cores/Waterbox/PeRunner.cs b/BizHawk.Emulation.Cores/Waterbox/PeRunner.cs
index a15521678a..3fc1fbf913 100644
--- a/BizHawk.Emulation.Cores/Waterbox/PeRunner.cs
+++ b/BizHawk.Emulation.Cores/Waterbox/PeRunner.cs
@@ -53,8 +53,12 @@ namespace BizHawk.Emulation.Cores.Waterbox
/// can be 0, but mmap calls will crash.
///
public uint MmapHeapSizeKB { get; set; }
- }
+ ///
+ /// start address in memory
+ ///
+ public ulong StartAddress { get; set; } = PeRunner.CanonicalStart;
+ }
public class PeRunner : Swappable, IImportResolver, IBinaryStateable
{
@@ -539,8 +543,12 @@ namespace BizHawk.Emulation.Cores.Waterbox
}
- // usual starting address for the executable
- private static readonly ulong CanonicalStart = 0x0000036f00000000;
+ ///
+ /// usual starting point for the executable
+ ///
+ public const ulong CanonicalStart = 0x0000036f00000000;
+
+ public const ulong AlternateStart = 0x0000036e00000000;
///
/// the next place where we can put a module or heap
@@ -642,6 +650,7 @@ namespace BizHawk.Emulation.Cores.Waterbox
public PeRunner(PeRunnerOptions opt)
{
+ _nextStart = opt.StartAddress;
Initialize(_nextStart);
using (this.EnterExit())
{
diff --git a/BizHawk.Emulation.Cores/Waterbox/Swappable.cs b/BizHawk.Emulation.Cores/Waterbox/Swappable.cs
index cff108ada9..25d3d8d0e9 100644
--- a/BizHawk.Emulation.Cores/Waterbox/Swappable.cs
+++ b/BizHawk.Emulation.Cores/Waterbox/Swappable.cs
@@ -17,7 +17,7 @@ namespace BizHawk.Emulation.Cores.Waterbox
///
/// start address
///
- private ulong _lockkey;
+ private uint _lockkey;
///
/// the the relevant lockinfo for this core
@@ -39,17 +39,17 @@ namespace BizHawk.Emulation.Cores.Waterbox
_memoryBlocks = null;
}
- protected void Initialize(ulong lockkey)
+ protected void Initialize(ulong startAddress)
{
+ // any Swappables in the same 4G range are assumed to conflict
+ var lockkey = (uint)(startAddress >> 32);
+
_lockkey = lockkey;
if (lockkey == 0)
throw new NullReferenceException();
_currentLockInfo = LockInfos.GetOrAdd(_lockkey, new LockInfo { Sync = new object() });
}
- // any Swappable is assumed to conflict with any other Swappable at the same base address,
- // but not any other starting address. so don't put them too close together!
-
private class LockInfo
{
public object Sync;
@@ -70,7 +70,7 @@ namespace BizHawk.Emulation.Cores.Waterbox
}
}
- private static readonly ConcurrentDictionary LockInfos = new ConcurrentDictionary();
+ private static readonly ConcurrentDictionary LockInfos = new ConcurrentDictionary();
///
/// acquire lock and swap this into memory