Some refactoring of N64 code. Extracted mupen plugins to their own classes.
This commit is contained in:
parent
5df77aac29
commit
5a36b50f8b
|
@ -223,11 +223,15 @@
|
||||||
<Compile Include="Consoles\Nintendo\Gameboy\LibGambatte.cs" />
|
<Compile Include="Consoles\Nintendo\Gameboy\LibGambatte.cs" />
|
||||||
<Compile Include="Consoles\Nintendo\GBA\LibMeteor.cs" />
|
<Compile Include="Consoles\Nintendo\GBA\LibMeteor.cs" />
|
||||||
<Compile Include="Consoles\Nintendo\GBA\Meteor.cs" />
|
<Compile Include="Consoles\Nintendo\GBA\Meteor.cs" />
|
||||||
<Compile Include="Consoles\Nintendo\N64\mupen64plusApi.cs" />
|
<Compile Include="Consoles\Nintendo\N64\N64Input.cs" />
|
||||||
|
<Compile Include="Consoles\Nintendo\N64\NativeApi\mupen64plusAudioApi.cs" />
|
||||||
|
<Compile Include="Consoles\Nintendo\N64\NativeApi\mupen64plusCoreApi.cs" />
|
||||||
<Compile Include="Consoles\Nintendo\N64\N64.cs" />
|
<Compile Include="Consoles\Nintendo\N64\N64.cs" />
|
||||||
<Compile Include="Consoles\Nintendo\N64\N64Audio.cs" />
|
<Compile Include="Consoles\Nintendo\N64\N64Audio.cs" />
|
||||||
<Compile Include="Consoles\Nintendo\N64\N64SyncSettings.cs" />
|
<Compile Include="Consoles\Nintendo\N64\N64SyncSettings.cs" />
|
||||||
<Compile Include="Consoles\Nintendo\N64\N64VideoProvider.cs" />
|
<Compile Include="Consoles\Nintendo\N64\N64VideoProvider.cs" />
|
||||||
|
<Compile Include="Consoles\Nintendo\N64\NativeApi\mupen64plusInputApi.cs" />
|
||||||
|
<Compile Include="Consoles\Nintendo\N64\NativeApi\mupen64plusVideoApi.cs" />
|
||||||
<Compile Include="Consoles\Nintendo\NES\APU.cs" />
|
<Compile Include="Consoles\Nintendo\NES\APU.cs" />
|
||||||
<Compile Include="Consoles\Nintendo\NES\BoardSystem.cs" />
|
<Compile Include="Consoles\Nintendo\NES\BoardSystem.cs" />
|
||||||
<Compile Include="Consoles\Nintendo\NES\Boards\AVE-NINA.cs" />
|
<Compile Include="Consoles\Nintendo\NES\Boards\AVE-NINA.cs" />
|
||||||
|
|
|
@ -2,11 +2,11 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
using BizHawk.Common;
|
using BizHawk.Common;
|
||||||
using BizHawk.Emulation.Common;
|
using BizHawk.Emulation.Common;
|
||||||
using BizHawk.Emulation.Cores.Consoles.Nintendo.N64;
|
using BizHawk.Emulation.Cores.Consoles.Nintendo.N64;
|
||||||
|
using BizHawk.Emulation.Cores.Nintendo.N64.NativeApi;
|
||||||
|
|
||||||
namespace BizHawk.Emulation.Cores.Nintendo.N64
|
namespace BizHawk.Emulation.Cores.Nintendo.N64
|
||||||
{
|
{
|
||||||
|
@ -77,42 +77,20 @@ namespace BizHawk.Emulation.Cores.Nintendo.N64
|
||||||
public bool StartAsyncSound() { return false; }
|
public bool StartAsyncSound() { return false; }
|
||||||
public void EndAsyncSound() { }
|
public void EndAsyncSound() { }
|
||||||
|
|
||||||
public ControllerDefinition ControllerDefinition { get { return N64ControllerDefinition; } }
|
private N64Input inputProvider;
|
||||||
public IController Controller { get; set; }
|
public ControllerDefinition ControllerDefinition { get { return inputProvider.ControllerDefinition; } }
|
||||||
public static readonly ControllerDefinition N64ControllerDefinition = new ControllerDefinition
|
public IController Controller
|
||||||
{
|
{
|
||||||
Name = "Nintento 64 Controller",
|
get { return inputProvider.Controller; }
|
||||||
BoolButtons =
|
set { inputProvider.Controller = value; }
|
||||||
{
|
}
|
||||||
"P1 A Up", "P1 A Down", "P1 A Left", "P1 A Right", "P1 DPad U", "P1 DPad D", "P1 DPad L", "P1 DPad R", "P1 Start", "P1 Z", "P1 B", "P1 A", "P1 C Up", "P1 C Down", "P1 C Right", "P1 C Left", "P1 L", "P1 R",
|
|
||||||
"P2 A Up", "P2 A Down", "P2 A Left", "P2 A Right", "P2 DPad U", "P2 DPad D", "P2 DPad L", "P2 DPad R", "P2 Start", "P2 Z", "P2 B", "P2 A", "P2 C Up", "P2 C Down", "P2 C Right", "P2 C Left", "P2 L", "P2 R",
|
|
||||||
"P3 A Up", "P3 A Down", "P3 A Left", "P3 A Right", "P3 DPad U", "P3 DPad D", "P3 DPad L", "P3 DPad R", "P3 Start", "P3 Z", "P3 B", "P3 A", "P3 C Up", "P3 C Down", "P3 C Right", "P3 C Left", "P3 L", "P3 R",
|
|
||||||
"P4 A Up", "P4 A Down", "P4 A Left", "P4 A Right", "P4 DPad U", "P4 DPad D", "P4 DPad L", "P4 DPad R", "P4 Start", "P4 Z", "P4 B", "P4 A", "P4 C Up", "P4 C Down", "P4 C Right", "P4 C Left", "P4 L", "P4 R",
|
|
||||||
"Reset", "Power"
|
|
||||||
},
|
|
||||||
FloatControls =
|
|
||||||
{
|
|
||||||
"P1 X Axis", "P1 Y Axis",
|
|
||||||
"P2 X Axis", "P2 Y Axis",
|
|
||||||
"P3 X Axis", "P3 Y Axis",
|
|
||||||
"P4 X Axis", "P4 Y Axis"
|
|
||||||
},
|
|
||||||
FloatRanges =
|
|
||||||
{
|
|
||||||
new[] {-128.0f, 0.0f, 127.0f},
|
|
||||||
new[] {-128.0f, 0.0f, 127.0f},
|
|
||||||
new[] {-128.0f, 0.0f, 127.0f},
|
|
||||||
new[] {-128.0f, 0.0f, 127.0f},
|
|
||||||
new[] {-128.0f, 0.0f, 127.0f},
|
|
||||||
new[] {-128.0f, 0.0f, 127.0f},
|
|
||||||
new[] {-128.0f, 0.0f, 127.0f},
|
|
||||||
new[] {-128.0f, 0.0f, 127.0f}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public int Frame { get; private set; }
|
public int Frame { get; private set; }
|
||||||
public int LagCount { get; set; }
|
public int LagCount { get; set; }
|
||||||
public bool IsLagFrame { get; private set; }
|
public bool IsLagFrame {
|
||||||
|
get { return !inputProvider.LastFrameInputPolled; }
|
||||||
|
set { inputProvider.LastFrameInputPolled = !value; }
|
||||||
|
}
|
||||||
public void ResetCounters()
|
public void ResetCounters()
|
||||||
{
|
{
|
||||||
Frame = 0;
|
Frame = 0;
|
||||||
|
@ -133,70 +111,12 @@ namespace BizHawk.Emulation.Cores.Nintendo.N64
|
||||||
api.hard_reset();
|
api.hard_reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
IsLagFrame = true;
|
|
||||||
api.frame_advance();
|
api.frame_advance();
|
||||||
|
|
||||||
if (IsLagFrame) LagCount++;
|
if (IsLagFrame) LagCount++;
|
||||||
Frame++;
|
Frame++;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Translates controller input from EmuHawk into
|
|
||||||
/// N64 controller data
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="i">Id of controller to update and shove</param>
|
|
||||||
public int GetControllerInput(int i)
|
|
||||||
{
|
|
||||||
CoreComm.InputCallback.Call();
|
|
||||||
IsLagFrame = false;
|
|
||||||
|
|
||||||
// Analog stick right = +X
|
|
||||||
// Analog stick up = +Y
|
|
||||||
string p = "P" + (i + 1);
|
|
||||||
sbyte x;
|
|
||||||
if (Controller.IsPressed(p + " A Left")) { x = -127; }
|
|
||||||
else if (Controller.IsPressed(p + " A Right")) { x = 127; }
|
|
||||||
else { x = (sbyte)Controller.GetFloat(p + " X Axis"); }
|
|
||||||
|
|
||||||
sbyte y;
|
|
||||||
if (Controller.IsPressed(p + " A Up")) { y = 127; }
|
|
||||||
else if (Controller.IsPressed(p + " A Down")) { y = -127; }
|
|
||||||
else { y = (sbyte)Controller.GetFloat(p + " Y Axis"); }
|
|
||||||
|
|
||||||
int value = ReadController(i + 1);
|
|
||||||
value |= (x & 0xFF) << 16;
|
|
||||||
value |= (y & 0xFF) << 24;
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Read all buttons from a controller and translate them
|
|
||||||
/// into a form the N64 understands
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="num">Number of controller to translate</param>
|
|
||||||
/// <returns>Bitlist of pressed buttons</returns>
|
|
||||||
public int ReadController(int num)
|
|
||||||
{
|
|
||||||
int buttons = 0;
|
|
||||||
|
|
||||||
if (Controller["P" + num + " DPad R"]) buttons |= (1 << 0);
|
|
||||||
if (Controller["P" + num + " DPad L"]) buttons |= (1 << 1);
|
|
||||||
if (Controller["P" + num + " DPad D"]) buttons |= (1 << 2);
|
|
||||||
if (Controller["P" + num + " DPad U"]) buttons |= (1 << 3);
|
|
||||||
if (Controller["P" + num + " Start"]) buttons |= (1 << 4);
|
|
||||||
if (Controller["P" + num + " Z"]) buttons |= (1 << 5);
|
|
||||||
if (Controller["P" + num + " B"]) buttons |= (1 << 6);
|
|
||||||
if (Controller["P" + num + " A"]) buttons |= (1 << 7);
|
|
||||||
if (Controller["P" + num + " C Right"]) buttons |= (1 << 8);
|
|
||||||
if (Controller["P" + num + " C Left"]) buttons |= (1 << 9);
|
|
||||||
if (Controller["P" + num + " C Down"]) buttons |= (1 << 10);
|
|
||||||
if (Controller["P" + num + " C Up"]) buttons |= (1 << 11);
|
|
||||||
if (Controller["P" + num + " R"]) buttons |= (1 << 12);
|
|
||||||
if (Controller["P" + num + " L"]) buttons |= (1 << 13);
|
|
||||||
|
|
||||||
return buttons;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool DeterministicEmulation { get { return false; } }
|
public bool DeterministicEmulation { get { return false; } }
|
||||||
|
|
||||||
public byte[] ReadSaveRam()
|
public byte[] ReadSaveRam()
|
||||||
|
@ -455,16 +375,19 @@ namespace BizHawk.Emulation.Cores.Nintendo.N64
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
api = new mupen64plusApi(this, rom, this.SyncSettings.GetVPS(game), SaveType);
|
var videosettings = this.SyncSettings.GetVPS(game);
|
||||||
api.SetM64PInputCallback(new mupen64plusApi.InputCallback(GetControllerInput));
|
api = new mupen64plusApi(this, rom, videosettings, SaveType);
|
||||||
|
// Order is important because the register with the mupen core
|
||||||
|
videoProvider = new N64VideoProvider(api, videosettings);
|
||||||
audioProvider = new N64Audio(api);
|
audioProvider = new N64Audio(api);
|
||||||
videoProvider = new N64VideoProvider(api);
|
inputProvider = new N64Input(api, comm);
|
||||||
api.FrameFinished += videoProvider.DoVideoFrame;
|
api.AttachPlugin(mupen64plusApi.m64p_plugin_type.M64PLUGIN_RSP,
|
||||||
api.VInterrupt += audioProvider.DoAudioFrame;
|
"mupen64plus-rsp-hle.dll");
|
||||||
|
|
||||||
InitMemoryDomains();
|
InitMemoryDomains();
|
||||||
RefreshMemoryCallbacks();
|
RefreshMemoryCallbacks();
|
||||||
|
|
||||||
|
api.AsyncExecuteEmulator();
|
||||||
}
|
}
|
||||||
|
|
||||||
N64SyncSettings SyncSettings;
|
N64SyncSettings SyncSettings;
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using BizHawk.Emulation.Common;
|
using BizHawk.Emulation.Common;
|
||||||
|
using BizHawk.Emulation.Cores.Nintendo.N64.NativeApi;
|
||||||
|
|
||||||
namespace BizHawk.Emulation.Cores.Nintendo.N64
|
namespace BizHawk.Emulation.Cores.Nintendo.N64
|
||||||
{
|
{
|
||||||
|
@ -11,7 +9,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.N64
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// mupen64 DLL Api
|
/// mupen64 DLL Api
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private mupen64plusApi api;
|
private mupen64plusAudioApi api;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Buffer for audio data
|
/// Buffer for audio data
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -42,12 +40,15 @@ namespace BizHawk.Emulation.Cores.Nintendo.N64
|
||||||
/// Creates a N64 Audio subsystem
|
/// Creates a N64 Audio subsystem
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="api">Mupen64 api which is used for fetching sound</param>
|
/// <param name="api">Mupen64 api which is used for fetching sound</param>
|
||||||
public N64Audio(mupen64plusApi api)
|
public N64Audio(mupen64plusApi core)
|
||||||
{
|
{
|
||||||
this.api = api;
|
this.api = new mupen64plusAudioApi(core);
|
||||||
|
|
||||||
_samplingRate = api.GetSamplingRate();
|
_samplingRate = api.GetSamplingRate();
|
||||||
Resampler = new SpeexResampler(6, SamplingRate, 44100,
|
Resampler = new SpeexResampler(6, SamplingRate, 44100,
|
||||||
SamplingRate, 44100);
|
SamplingRate, 44100);
|
||||||
|
|
||||||
|
core.VInterrupt += DoAudioFrame;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -77,7 +78,6 @@ namespace BizHawk.Emulation.Cores.Nintendo.N64
|
||||||
if(Resampler != null)
|
if(Resampler != null)
|
||||||
Resampler.Dispose();
|
Resampler.Dispose();
|
||||||
Resampler = null;
|
Resampler = null;
|
||||||
// Api is disposed by N64
|
|
||||||
api = null;
|
api = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,120 @@
|
||||||
|
using BizHawk.Emulation.Common;
|
||||||
|
using BizHawk.Emulation.Cores.Nintendo.N64.NativeApi;
|
||||||
|
|
||||||
|
namespace BizHawk.Emulation.Cores.Nintendo.N64
|
||||||
|
{
|
||||||
|
class N64Input
|
||||||
|
{
|
||||||
|
private mupen64plusInputApi api;
|
||||||
|
public CoreComm CoreComm { get; private set; }
|
||||||
|
public IController Controller { get; set; }
|
||||||
|
|
||||||
|
public bool LastFrameInputPolled { get; set; }
|
||||||
|
public bool ThisFrameInputPolled { get; set; }
|
||||||
|
public ControllerDefinition ControllerDefinition { get { return N64ControllerDefinition; } }
|
||||||
|
|
||||||
|
public static readonly ControllerDefinition N64ControllerDefinition = new ControllerDefinition
|
||||||
|
{
|
||||||
|
Name = "Nintento 64 Controller",
|
||||||
|
BoolButtons =
|
||||||
|
{
|
||||||
|
"P1 A Up", "P1 A Down", "P1 A Left", "P1 A Right", "P1 DPad U", "P1 DPad D", "P1 DPad L", "P1 DPad R", "P1 Start", "P1 Z", "P1 B", "P1 A", "P1 C Up", "P1 C Down", "P1 C Right", "P1 C Left", "P1 L", "P1 R",
|
||||||
|
"P2 A Up", "P2 A Down", "P2 A Left", "P2 A Right", "P2 DPad U", "P2 DPad D", "P2 DPad L", "P2 DPad R", "P2 Start", "P2 Z", "P2 B", "P2 A", "P2 C Up", "P2 C Down", "P2 C Right", "P2 C Left", "P2 L", "P2 R",
|
||||||
|
"P3 A Up", "P3 A Down", "P3 A Left", "P3 A Right", "P3 DPad U", "P3 DPad D", "P3 DPad L", "P3 DPad R", "P3 Start", "P3 Z", "P3 B", "P3 A", "P3 C Up", "P3 C Down", "P3 C Right", "P3 C Left", "P3 L", "P3 R",
|
||||||
|
"P4 A Up", "P4 A Down", "P4 A Left", "P4 A Right", "P4 DPad U", "P4 DPad D", "P4 DPad L", "P4 DPad R", "P4 Start", "P4 Z", "P4 B", "P4 A", "P4 C Up", "P4 C Down", "P4 C Right", "P4 C Left", "P4 L", "P4 R",
|
||||||
|
"Reset", "Power"
|
||||||
|
},
|
||||||
|
FloatControls =
|
||||||
|
{
|
||||||
|
"P1 X Axis", "P1 Y Axis",
|
||||||
|
"P2 X Axis", "P2 Y Axis",
|
||||||
|
"P3 X Axis", "P3 Y Axis",
|
||||||
|
"P4 X Axis", "P4 Y Axis"
|
||||||
|
},
|
||||||
|
FloatRanges =
|
||||||
|
{
|
||||||
|
new[] {-128.0f, 0.0f, 127.0f},
|
||||||
|
new[] {-128.0f, 0.0f, 127.0f},
|
||||||
|
new[] {-128.0f, 0.0f, 127.0f},
|
||||||
|
new[] {-128.0f, 0.0f, 127.0f},
|
||||||
|
new[] {-128.0f, 0.0f, 127.0f},
|
||||||
|
new[] {-128.0f, 0.0f, 127.0f},
|
||||||
|
new[] {-128.0f, 0.0f, 127.0f},
|
||||||
|
new[] {-128.0f, 0.0f, 127.0f}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public N64Input(mupen64plusApi core, CoreComm comm)
|
||||||
|
{
|
||||||
|
api = new mupen64plusInputApi(core);
|
||||||
|
CoreComm = comm;
|
||||||
|
|
||||||
|
api.SetM64PInputCallback(new mupen64plusInputApi.InputCallback(GetControllerInput));
|
||||||
|
|
||||||
|
core.VInterrupt += ShiftInputPolledBools;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShiftInputPolledBools()
|
||||||
|
{
|
||||||
|
LastFrameInputPolled = ThisFrameInputPolled;
|
||||||
|
ThisFrameInputPolled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Translates controller input from EmuHawk into
|
||||||
|
/// N64 controller data
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="i">Id of controller to update and shove</param>
|
||||||
|
public int GetControllerInput(int i)
|
||||||
|
{
|
||||||
|
CoreComm.InputCallback.Call();
|
||||||
|
ThisFrameInputPolled = true;
|
||||||
|
|
||||||
|
// Analog stick right = +X
|
||||||
|
// Analog stick up = +Y
|
||||||
|
string p = "P" + (i + 1);
|
||||||
|
sbyte x;
|
||||||
|
if (Controller.IsPressed(p + " A Left")) { x = -127; }
|
||||||
|
else if (Controller.IsPressed(p + " A Right")) { x = 127; }
|
||||||
|
else { x = (sbyte)Controller.GetFloat(p + " X Axis"); }
|
||||||
|
|
||||||
|
sbyte y;
|
||||||
|
if (Controller.IsPressed(p + " A Up")) { y = 127; }
|
||||||
|
else if (Controller.IsPressed(p + " A Down")) { y = -127; }
|
||||||
|
else { y = (sbyte)Controller.GetFloat(p + " Y Axis"); }
|
||||||
|
|
||||||
|
int value = ReadController(i + 1);
|
||||||
|
value |= (x & 0xFF) << 16;
|
||||||
|
value |= (y & 0xFF) << 24;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Read all buttons from a controller and translate them
|
||||||
|
/// into a form the N64 understands
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="num">Number of controller to translate</param>
|
||||||
|
/// <returns>Bitlist of pressed buttons</returns>
|
||||||
|
public int ReadController(int num)
|
||||||
|
{
|
||||||
|
int buttons = 0;
|
||||||
|
|
||||||
|
if (Controller["P" + num + " DPad R"]) buttons |= (1 << 0);
|
||||||
|
if (Controller["P" + num + " DPad L"]) buttons |= (1 << 1);
|
||||||
|
if (Controller["P" + num + " DPad D"]) buttons |= (1 << 2);
|
||||||
|
if (Controller["P" + num + " DPad U"]) buttons |= (1 << 3);
|
||||||
|
if (Controller["P" + num + " Start"]) buttons |= (1 << 4);
|
||||||
|
if (Controller["P" + num + " Z"]) buttons |= (1 << 5);
|
||||||
|
if (Controller["P" + num + " B"]) buttons |= (1 << 6);
|
||||||
|
if (Controller["P" + num + " A"]) buttons |= (1 << 7);
|
||||||
|
if (Controller["P" + num + " C Right"]) buttons |= (1 << 8);
|
||||||
|
if (Controller["P" + num + " C Left"]) buttons |= (1 << 9);
|
||||||
|
if (Controller["P" + num + " C Down"]) buttons |= (1 << 10);
|
||||||
|
if (Controller["P" + num + " C Up"]) buttons |= (1 << 11);
|
||||||
|
if (Controller["P" + num + " R"]) buttons |= (1 << 12);
|
||||||
|
if (Controller["P" + num + " L"]) buttons |= (1 << 13);
|
||||||
|
|
||||||
|
return buttons;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,6 @@
|
||||||
using System;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using BizHawk.Emulation.Common;
|
using BizHawk.Emulation.Common;
|
||||||
using BizHawk.Emulation.Cores.Nintendo.N64;
|
using BizHawk.Emulation.Cores.Nintendo.N64.NativeApi;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace BizHawk.Emulation.Cores.Consoles.Nintendo.N64
|
namespace BizHawk.Emulation.Cores.Consoles.Nintendo.N64
|
||||||
|
|
|
@ -1,27 +1,27 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using BizHawk.Emulation.Common;
|
using BizHawk.Emulation.Common;
|
||||||
|
using BizHawk.Emulation.Cores.Nintendo.N64.NativeApi;
|
||||||
|
|
||||||
namespace BizHawk.Emulation.Cores.Nintendo.N64
|
namespace BizHawk.Emulation.Cores.Nintendo.N64
|
||||||
{
|
{
|
||||||
class N64VideoProvider : IVideoProvider, IDisposable
|
class N64VideoProvider : IVideoProvider, IDisposable
|
||||||
{
|
{
|
||||||
private int[] frameBuffer;
|
private int[] frameBuffer;
|
||||||
private mupen64plusApi api;
|
private mupen64plusVideoApi api;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates N64 Video system with mupen64plus backend
|
/// Creates N64 Video system with mupen64plus backend
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="api">mupen64plus DLL that is used</param>
|
/// <param name="api">mupen64plus DLL that is used</param>
|
||||||
public N64VideoProvider(mupen64plusApi api)
|
public N64VideoProvider(mupen64plusApi core, VideoPluginSettings videosettings)
|
||||||
{
|
{
|
||||||
this.api = api;
|
this.api = new mupen64plusVideoApi(core, videosettings);
|
||||||
int width = 0;
|
int width = 0;
|
||||||
int height = 0;
|
int height = 0;
|
||||||
api.GetScreenDimensions(ref width, ref height);
|
api.GetScreenDimensions(ref width, ref height);
|
||||||
SetBufferSize(width, height);
|
SetBufferSize(width, height);
|
||||||
|
|
||||||
|
core.FrameFinished += DoVideoFrame;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int[] GetVideoBuffer()
|
public int[] GetVideoBuffer()
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using BizHawk.Emulation.Cores.Nintendo.N64;
|
||||||
|
|
||||||
|
namespace BizHawk.Emulation.Cores.Nintendo.N64.NativeApi
|
||||||
|
{
|
||||||
|
class mupen64plusAudioApi
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Handle to native audio plugin
|
||||||
|
/// </summary>
|
||||||
|
private IntPtr AudDll;
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll")]
|
||||||
|
public static extern IntPtr GetProcAddress(IntPtr hModule, string procedureName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the size of the mupen64plus audio buffer
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The size of the mupen64plus audio buffer</returns>
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||||
|
private delegate int GetBufferSize();
|
||||||
|
GetBufferSize dllGetBufferSize;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the audio buffer from mupen64plus, and then clears it
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dest">The buffer to fill with samples</param>
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||||
|
private delegate void ReadAudioBuffer(short[] dest);
|
||||||
|
ReadAudioBuffer dllReadAudioBuffer;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current audio rate from mupen64plus
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The current audio rate</returns>
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||||
|
private delegate int GetAudioRate();
|
||||||
|
GetAudioRate dllGetAudioRate;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads native functions and attaches itself to the core
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="core">Core with loaded core api</param>
|
||||||
|
public mupen64plusAudioApi(mupen64plusApi core)
|
||||||
|
{
|
||||||
|
AudDll = core.AttachPlugin(mupen64plusApi.m64p_plugin_type.M64PLUGIN_AUDIO,
|
||||||
|
"mupen64plus-audio-bkm.dll");
|
||||||
|
|
||||||
|
// Connect dll functions
|
||||||
|
dllGetBufferSize = (GetBufferSize)Marshal.GetDelegateForFunctionPointer(GetProcAddress(AudDll, "GetBufferSize"), typeof(GetBufferSize));
|
||||||
|
dllReadAudioBuffer = (ReadAudioBuffer)Marshal.GetDelegateForFunctionPointer(GetProcAddress(AudDll, "ReadAudioBuffer"), typeof(ReadAudioBuffer));
|
||||||
|
dllGetAudioRate = (GetAudioRate)Marshal.GetDelegateForFunctionPointer(GetProcAddress(AudDll, "GetAudioRate"), typeof(GetAudioRate));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns currently used sampling rate
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public uint GetSamplingRate()
|
||||||
|
{
|
||||||
|
return (uint)dllGetAudioRate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns size of bytes currently in the audio buffer
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public int GetAudioBufferSize()
|
||||||
|
{
|
||||||
|
return dllGetBufferSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns bytes currently in the audiobuffer
|
||||||
|
/// Afterwards audio buffer is cleared
|
||||||
|
/// buffer.Length must be greater than GetAudioBufferSize()
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="buffer"></param>
|
||||||
|
public void GetAudioBuffer(short[] buffer)
|
||||||
|
{
|
||||||
|
dllReadAudioBuffer(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +1,10 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
using BizHawk.Emulation.Common;
|
|
||||||
using BizHawk.Emulation.Cores.Consoles.Nintendo.N64;
|
using BizHawk.Emulation.Cores.Consoles.Nintendo.N64;
|
||||||
|
|
||||||
namespace BizHawk.Emulation.Cores.Nintendo.N64
|
namespace BizHawk.Emulation.Cores.Nintendo.N64.NativeApi
|
||||||
{
|
{
|
||||||
public class mupen64plusApi : IDisposable
|
public class mupen64plusApi : IDisposable
|
||||||
{
|
{
|
||||||
|
@ -33,7 +29,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.N64
|
||||||
[DllImport("kernel32.dll")]
|
[DllImport("kernel32.dll")]
|
||||||
public static extern bool FreeLibrary(IntPtr hModule);
|
public static extern bool FreeLibrary(IntPtr hModule);
|
||||||
|
|
||||||
enum m64p_error
|
public enum m64p_error
|
||||||
{
|
{
|
||||||
M64ERR_SUCCESS = 0,
|
M64ERR_SUCCESS = 0,
|
||||||
M64ERR_NOT_INIT, /* Function is disallowed before InitMupen64Plus() is called */
|
M64ERR_NOT_INIT, /* Function is disallowed before InitMupen64Plus() is called */
|
||||||
|
@ -52,7 +48,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.N64
|
||||||
M64ERR_WRONG_TYPE /* A given input type parameter cannot be used for desired operation */
|
M64ERR_WRONG_TYPE /* A given input type parameter cannot be used for desired operation */
|
||||||
};
|
};
|
||||||
|
|
||||||
enum m64p_plugin_type
|
public enum m64p_plugin_type
|
||||||
{
|
{
|
||||||
M64PLUGIN_NULL = 0,
|
M64PLUGIN_NULL = 0,
|
||||||
M64PLUGIN_RSP = 1,
|
M64PLUGIN_RSP = 1,
|
||||||
|
@ -62,7 +58,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.N64
|
||||||
M64PLUGIN_CORE
|
M64PLUGIN_CORE
|
||||||
};
|
};
|
||||||
|
|
||||||
enum m64p_command
|
private enum m64p_command
|
||||||
{
|
{
|
||||||
M64CMD_NOP = 0,
|
M64CMD_NOP = 0,
|
||||||
M64CMD_ROM_OPEN,
|
M64CMD_ROM_OPEN,
|
||||||
|
@ -88,14 +84,14 @@ namespace BizHawk.Emulation.Cores.Nintendo.N64
|
||||||
M64CMD_SET_VI_CALLBACK
|
M64CMD_SET_VI_CALLBACK
|
||||||
};
|
};
|
||||||
|
|
||||||
enum m64p_emu_state
|
public enum m64p_emu_state
|
||||||
{
|
{
|
||||||
M64EMU_STOPPED = 1,
|
M64EMU_STOPPED = 1,
|
||||||
M64EMU_RUNNING,
|
M64EMU_RUNNING,
|
||||||
M64EMU_PAUSED
|
M64EMU_PAUSED
|
||||||
};
|
};
|
||||||
|
|
||||||
enum m64p_type
|
public enum m64p_type
|
||||||
{
|
{
|
||||||
M64TYPE_INT = 1,
|
M64TYPE_INT = 1,
|
||||||
M64TYPE_FLOAT,
|
M64TYPE_FLOAT,
|
||||||
|
@ -261,102 +257,24 @@ namespace BizHawk.Emulation.Cores.Nintendo.N64
|
||||||
delegate m64p_error CoreDoCommandVICallback(m64p_command Command, int ParamInt, VICallback ParamPtr);
|
delegate m64p_error CoreDoCommandVICallback(m64p_command Command, int ParamInt, VICallback ParamPtr);
|
||||||
CoreDoCommandVICallback m64pCoreDoCommandVICallback;
|
CoreDoCommandVICallback m64pCoreDoCommandVICallback;
|
||||||
|
|
||||||
|
|
||||||
// Graphics plugin specific
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Fills a provided buffer with the mupen64plus framebuffer
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="framebuffer">The buffer to fill</param>
|
|
||||||
/// <param name="width">A pointer to a variable to fill with the width of the framebuffer</param>
|
|
||||||
/// <param name="height">A pointer to a variable to fill with the height of the framebuffer</param>
|
|
||||||
/// <param name="buffer">Which buffer to read: 0 = front, 1 = back</param>
|
|
||||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
|
||||||
private delegate void ReadScreen2(int[] framebuffer, ref int width, ref int height, int buffer);
|
|
||||||
ReadScreen2 GFXReadScreen2;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the width and height of the mupen64plus framebuffer
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dummy">Use IntPtr.Zero</param>
|
|
||||||
/// <param name="width">A pointer to a variable to fill with the width of the framebuffer</param>
|
|
||||||
/// <param name="height">A pointer to a variable to fill with the height of the framebuffer</param>
|
|
||||||
/// <param name="buffer">Which buffer to read: 0 = front, 1 = back</param>
|
|
||||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
|
||||||
private delegate void ReadScreen2Res(IntPtr dummy, ref int width, ref int height, int buffer);
|
|
||||||
ReadScreen2Res GFXReadScreen2Res;
|
|
||||||
|
|
||||||
|
|
||||||
// Audio plugin specific
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the size of the mupen64plus audio buffer
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The size of the mupen64plus audio buffer</returns>
|
|
||||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
|
||||||
private delegate int GetBufferSize();
|
|
||||||
GetBufferSize AudGetBufferSize;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the audio buffer from mupen64plus, and then clears it
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dest">The buffer to fill with samples</param>
|
|
||||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
|
||||||
private delegate void ReadAudioBuffer(short[] dest);
|
|
||||||
ReadAudioBuffer AudReadAudioBuffer;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the current audio rate from mupen64plus
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The current audio rate</returns>
|
|
||||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
|
||||||
private delegate int GetAudioRate();
|
|
||||||
GetAudioRate AudGetAudioRate;
|
|
||||||
|
|
||||||
|
|
||||||
// Input plugin specific
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sets a callback to use when the mupen core wants controller buttons
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="inputCallback">The delegate to use</param>
|
|
||||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
|
||||||
private delegate void SetInputCallback(InputCallback inputCallback);
|
|
||||||
SetInputCallback InpSetInputCallback;
|
|
||||||
|
|
||||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
|
||||||
public delegate int InputCallback(int i);
|
|
||||||
InputCallback InpInputCallback;
|
|
||||||
|
|
||||||
|
|
||||||
// These are common for all four plugins
|
// These are common for all four plugins
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes the plugin
|
/// Initializes the plugin
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="CoreHandle">The DLL handle for the core DLL</param>
|
/// <param name="CoreHandle">The DLL handle for the core DLL</param>
|
||||||
/// <param name="Context">Use "Video", "Audio", "Input", or "RSP" depending on the plugin</param>
|
/// <param name="Context">Giving a context to the DebugCallback</param>
|
||||||
/// <param name="DebugCallback">A function to use when the pluging wants to output debug messages</param>
|
/// <param name="DebugCallback">A function to use when the pluging wants to output debug messages</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||||
private delegate m64p_error PluginStartup(IntPtr CoreHandle, string Context, DebugCallback DebugCallback);
|
public delegate m64p_error PluginStartup(IntPtr CoreHandle, string Context, DebugCallback DebugCallback);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cleans up the plugin
|
/// Cleans up the plugin
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||||
private delegate m64p_error PluginShutdown();
|
public delegate m64p_error PluginShutdown();
|
||||||
|
|
||||||
PluginStartup GfxPluginStartup;
|
|
||||||
PluginStartup RspPluginStartup;
|
|
||||||
PluginStartup AudPluginStartup;
|
|
||||||
PluginStartup InpPluginStartup;
|
|
||||||
|
|
||||||
PluginShutdown GfxPluginShutdown;
|
|
||||||
PluginShutdown RspPluginShutdown;
|
|
||||||
PluginShutdown AudPluginShutdown;
|
|
||||||
PluginShutdown InpPluginShutdown;
|
|
||||||
|
|
||||||
// Callback functions
|
// Callback functions
|
||||||
|
|
||||||
|
@ -418,11 +336,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.N64
|
||||||
GetRegisters m64pGetRegisters;
|
GetRegisters m64pGetRegisters;
|
||||||
|
|
||||||
// DLL handles
|
// DLL handles
|
||||||
IntPtr CoreDll;
|
public IntPtr CoreDll { get; private set; }
|
||||||
IntPtr GfxDll;
|
|
||||||
IntPtr RspDll;
|
|
||||||
IntPtr AudDll;
|
|
||||||
IntPtr InpDll;
|
|
||||||
|
|
||||||
public mupen64plusApi(N64 bizhawkCore, byte[] rom, VideoPluginSettings video_settings, int SaveType)
|
public mupen64plusApi(N64 bizhawkCore, byte[] rom, VideoPluginSettings video_settings, int SaveType)
|
||||||
{
|
{
|
||||||
|
@ -434,40 +348,9 @@ namespace BizHawk.Emulation.Cores.Nintendo.N64
|
||||||
}
|
}
|
||||||
this.bizhawkCore = bizhawkCore;
|
this.bizhawkCore = bizhawkCore;
|
||||||
|
|
||||||
string VidDllName;
|
|
||||||
if (video_settings.Plugin == PLUGINTYPE.RICE)
|
|
||||||
{
|
|
||||||
VidDllName = "mupen64plus-video-rice.dll";
|
|
||||||
}
|
|
||||||
else if (video_settings.Plugin == PLUGINTYPE.GLIDE)
|
|
||||||
{
|
|
||||||
VidDllName = "mupen64plus-video-glide64.dll";
|
|
||||||
}
|
|
||||||
else if (video_settings.Plugin == PLUGINTYPE.GLIDE64MK2)
|
|
||||||
{
|
|
||||||
VidDllName = "mupen64plus-video-glide64mk2.dll";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(string.Format("Unknown plugin {0}", video_settings.Plugin));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load each of the DLLs
|
|
||||||
CoreDll = LoadLibrary("mupen64plus.dll");
|
CoreDll = LoadLibrary("mupen64plus.dll");
|
||||||
if (CoreDll == IntPtr.Zero)
|
if (CoreDll == IntPtr.Zero)
|
||||||
throw new InvalidOperationException(string.Format("Failed to load mupen64plus.dll"));
|
throw new InvalidOperationException(string.Format("Failed to load mupen64plus.dll"));
|
||||||
GfxDll = LoadLibrary(VidDllName);
|
|
||||||
if (GfxDll == IntPtr.Zero)
|
|
||||||
throw new InvalidOperationException(string.Format("Failed to load " + VidDllName));
|
|
||||||
RspDll = LoadLibrary("mupen64plus-rsp-hle.dll");
|
|
||||||
if (RspDll == IntPtr.Zero)
|
|
||||||
throw new InvalidOperationException(string.Format("Failed to load mupen64plus-rsp-hle.dll"));
|
|
||||||
AudDll = LoadLibrary("mupen64plus-audio-bkm.dll");
|
|
||||||
if (AudDll == IntPtr.Zero)
|
|
||||||
throw new InvalidOperationException(string.Format("Failed to load mupen64plus-audio-bkm.dll"));
|
|
||||||
InpDll = LoadLibrary("mupen64plus-input-bkm.dll");
|
|
||||||
if (InpDll == IntPtr.Zero)
|
|
||||||
throw new InvalidOperationException(string.Format("Failed to load mupen64plus-input-bkm.dll"));
|
|
||||||
|
|
||||||
connectFunctionPointers();
|
connectFunctionPointers();
|
||||||
|
|
||||||
|
@ -496,23 +379,6 @@ namespace BizHawk.Emulation.Cores.Nintendo.N64
|
||||||
|
|
||||||
set_video_parameters(video_settings);
|
set_video_parameters(video_settings);
|
||||||
|
|
||||||
// Order of plugin loading is important, do not change!
|
|
||||||
// Set up and connect the graphics plugin
|
|
||||||
result = GfxPluginStartup(CoreDll, "Video", null);
|
|
||||||
result = m64pCoreAttachPlugin(m64p_plugin_type.M64PLUGIN_GFX, GfxDll);
|
|
||||||
|
|
||||||
// Set up our audio plugin
|
|
||||||
result = AudPluginStartup(CoreDll, "Audio", null);
|
|
||||||
result = m64pCoreAttachPlugin(m64p_plugin_type.M64PLUGIN_AUDIO, AudDll);
|
|
||||||
|
|
||||||
// Set up our input plugin
|
|
||||||
result = InpPluginStartup(CoreDll, "Input", null);
|
|
||||||
result = m64pCoreAttachPlugin(m64p_plugin_type.M64PLUGIN_INPUT, InpDll);
|
|
||||||
|
|
||||||
// Set up and connect the RSP plugin
|
|
||||||
result = RspPluginStartup(CoreDll, "RSP", null);
|
|
||||||
result = m64pCoreAttachPlugin(m64p_plugin_type.M64PLUGIN_RSP, RspDll);
|
|
||||||
|
|
||||||
InitSaveram();
|
InitSaveram();
|
||||||
|
|
||||||
// Initialize event invoker
|
// Initialize event invoker
|
||||||
|
@ -521,22 +387,31 @@ namespace BizHawk.Emulation.Cores.Nintendo.N64
|
||||||
m64pVICallback = new VICallback(FireVIEvent);
|
m64pVICallback = new VICallback(FireVIEvent);
|
||||||
result = m64pCoreDoCommandVICallback(m64p_command.M64CMD_SET_VI_CALLBACK, 0, m64pVICallback);
|
result = m64pCoreDoCommandVICallback(m64p_command.M64CMD_SET_VI_CALLBACK, 0, m64pVICallback);
|
||||||
|
|
||||||
// Start the emulator in another thread
|
// Prepare to start the emulator in a different thread
|
||||||
m64pEmulator = new Thread(ExecuteEmulator);
|
m64pEmulator = new Thread(ExecuteEmulator);
|
||||||
m64pEmulator.Start();
|
|
||||||
|
|
||||||
// Wait for the core to boot up
|
|
||||||
m64pStartupComplete.WaitOne();
|
|
||||||
|
|
||||||
AttachedCore = this;
|
AttachedCore = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
volatile bool emulator_running = false;
|
volatile bool emulator_running = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts executing the emulator asynchronously
|
||||||
|
/// Waits until the emulator booted up and than returns
|
||||||
|
/// </summary>
|
||||||
|
public void AsyncExecuteEmulator()
|
||||||
|
{
|
||||||
|
m64pEmulator.Start();
|
||||||
|
|
||||||
|
// Wait for the core to boot up
|
||||||
|
m64pStartupComplete.WaitOne();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Starts execution of mupen64plus
|
/// Starts execution of mupen64plus
|
||||||
/// Does not return until the emulator stops
|
/// Does not return until the emulator stops
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void ExecuteEmulator()
|
private void ExecuteEmulator()
|
||||||
{
|
{
|
||||||
emulator_running = true;
|
emulator_running = true;
|
||||||
var cb = new StartupCallback(() => m64pStartupComplete.Set());
|
var cb = new StartupCallback(() => m64pStartupComplete.Set());
|
||||||
|
@ -574,24 +449,6 @@ namespace BizHawk.Emulation.Cores.Nintendo.N64
|
||||||
m64pSetWriteCallback = (SetWriteCallback)Marshal.GetDelegateForFunctionPointer(GetProcAddress(CoreDll, "SetWriteCallback"), typeof(SetWriteCallback));
|
m64pSetWriteCallback = (SetWriteCallback)Marshal.GetDelegateForFunctionPointer(GetProcAddress(CoreDll, "SetWriteCallback"), typeof(SetWriteCallback));
|
||||||
|
|
||||||
m64pGetRegisters = (GetRegisters)Marshal.GetDelegateForFunctionPointer(GetProcAddress(CoreDll, "GetRegisters"), typeof(GetRegisters));
|
m64pGetRegisters = (GetRegisters)Marshal.GetDelegateForFunctionPointer(GetProcAddress(CoreDll, "GetRegisters"), typeof(GetRegisters));
|
||||||
|
|
||||||
GfxPluginStartup = (PluginStartup)Marshal.GetDelegateForFunctionPointer(GetProcAddress(GfxDll, "PluginStartup"), typeof(PluginStartup));
|
|
||||||
GfxPluginShutdown = (PluginShutdown)Marshal.GetDelegateForFunctionPointer(GetProcAddress(GfxDll, "PluginShutdown"), typeof(PluginShutdown));
|
|
||||||
GFXReadScreen2 = (ReadScreen2)Marshal.GetDelegateForFunctionPointer(GetProcAddress(GfxDll, "ReadScreen2"), typeof(ReadScreen2));
|
|
||||||
GFXReadScreen2Res = (ReadScreen2Res)Marshal.GetDelegateForFunctionPointer(GetProcAddress(GfxDll, "ReadScreen2"), typeof(ReadScreen2Res));
|
|
||||||
|
|
||||||
AudPluginStartup = (PluginStartup)Marshal.GetDelegateForFunctionPointer(GetProcAddress(AudDll, "PluginStartup"), typeof(PluginStartup));
|
|
||||||
AudPluginShutdown = (PluginShutdown)Marshal.GetDelegateForFunctionPointer(GetProcAddress(AudDll, "PluginShutdown"), typeof(PluginShutdown));
|
|
||||||
AudGetBufferSize = (GetBufferSize)Marshal.GetDelegateForFunctionPointer(GetProcAddress(AudDll, "GetBufferSize"), typeof(GetBufferSize));
|
|
||||||
AudReadAudioBuffer = (ReadAudioBuffer)Marshal.GetDelegateForFunctionPointer(GetProcAddress(AudDll, "ReadAudioBuffer"), typeof(ReadAudioBuffer));
|
|
||||||
AudGetAudioRate = (GetAudioRate)Marshal.GetDelegateForFunctionPointer(GetProcAddress(AudDll, "GetAudioRate"), typeof(GetAudioRate));
|
|
||||||
|
|
||||||
InpPluginStartup = (PluginStartup)Marshal.GetDelegateForFunctionPointer(GetProcAddress(InpDll, "PluginStartup"), typeof(PluginStartup));
|
|
||||||
InpPluginShutdown = (PluginShutdown)Marshal.GetDelegateForFunctionPointer(GetProcAddress(InpDll, "PluginShutdown"), typeof(PluginShutdown));
|
|
||||||
InpSetInputCallback = (SetInputCallback)Marshal.GetDelegateForFunctionPointer(GetProcAddress(InpDll, "SetInputCallback"), typeof(SetInputCallback));
|
|
||||||
|
|
||||||
RspPluginStartup = (PluginStartup)Marshal.GetDelegateForFunctionPointer(GetProcAddress(RspDll, "PluginStartup"), typeof(PluginStartup));
|
|
||||||
RspPluginShutdown = (PluginShutdown)Marshal.GetDelegateForFunctionPointer(GetProcAddress(RspDll, "PluginShutdown"), typeof(PluginShutdown));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -633,42 +490,6 @@ namespace BizHawk.Emulation.Cores.Nintendo.N64
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int[] m64pBuffer = new int[0];
|
|
||||||
/// <summary>
|
|
||||||
/// This function copies the frame buffer from mupen64plus
|
|
||||||
/// </summary>
|
|
||||||
public void Getm64pFrameBuffer(int[] buffer, ref int width, ref int height)
|
|
||||||
{
|
|
||||||
if(m64pBuffer.Length != width * height)
|
|
||||||
m64pBuffer = new int[width * height];
|
|
||||||
// Actually get the frame buffer
|
|
||||||
GFXReadScreen2(m64pBuffer, ref width, ref height, 0);
|
|
||||||
|
|
||||||
// vflip
|
|
||||||
int fromindex = width * (height - 1) * 4;
|
|
||||||
int toindex = 0;
|
|
||||||
|
|
||||||
for (int j = 0; j < height; j++)
|
|
||||||
{
|
|
||||||
Buffer.BlockCopy(m64pBuffer, fromindex, buffer, toindex, width * 4);
|
|
||||||
fromindex -= width * 4;
|
|
||||||
toindex += width * 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
// opaque
|
|
||||||
unsafe
|
|
||||||
{
|
|
||||||
fixed (int* ptr = &buffer[0])
|
|
||||||
{
|
|
||||||
int l = buffer.Length;
|
|
||||||
for (int i = 0; i < l; i++)
|
|
||||||
{
|
|
||||||
ptr[i] |= unchecked((int)0xff000000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int get_memory_size(N64_MEMORY id)
|
public int get_memory_size(N64_MEMORY id)
|
||||||
{
|
{
|
||||||
return m64pMemGetSize(id);
|
return m64pMemGetSize(id);
|
||||||
|
@ -695,12 +516,6 @@ namespace BizHawk.Emulation.Cores.Nintendo.N64
|
||||||
m64pFrameComplete.WaitOne();
|
m64pFrameComplete.WaitOne();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetM64PInputCallback(InputCallback inputCallback)
|
|
||||||
{
|
|
||||||
InpInputCallback = inputCallback;
|
|
||||||
InpSetInputCallback(InpInputCallback);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int SaveState(byte[] buffer)
|
public int SaveState(byte[] buffer)
|
||||||
{
|
{
|
||||||
return m64pCoreSaveState(buffer);
|
return m64pCoreSaveState(buffer);
|
||||||
|
@ -787,22 +602,10 @@ namespace BizHawk.Emulation.Cores.Nintendo.N64
|
||||||
// Backup the saveram in case bizhawk wants to get at is after we've freed the libraries
|
// Backup the saveram in case bizhawk wants to get at is after we've freed the libraries
|
||||||
saveram_backup = SaveSaveram();
|
saveram_backup = SaveSaveram();
|
||||||
|
|
||||||
m64pCoreDetachPlugin(m64p_plugin_type.M64PLUGIN_GFX);
|
DetachPlugin(m64p_plugin_type.M64PLUGIN_GFX);
|
||||||
m64pCoreDetachPlugin(m64p_plugin_type.M64PLUGIN_AUDIO);
|
DetachPlugin(m64p_plugin_type.M64PLUGIN_AUDIO);
|
||||||
m64pCoreDetachPlugin(m64p_plugin_type.M64PLUGIN_INPUT);
|
DetachPlugin(m64p_plugin_type.M64PLUGIN_INPUT);
|
||||||
m64pCoreDetachPlugin(m64p_plugin_type.M64PLUGIN_RSP);
|
DetachPlugin(m64p_plugin_type.M64PLUGIN_RSP);
|
||||||
|
|
||||||
GfxPluginShutdown();
|
|
||||||
FreeLibrary(GfxDll);
|
|
||||||
|
|
||||||
AudPluginShutdown();
|
|
||||||
FreeLibrary(AudDll);
|
|
||||||
|
|
||||||
InpPluginShutdown();
|
|
||||||
FreeLibrary(InpDll);
|
|
||||||
|
|
||||||
RspPluginShutdown();
|
|
||||||
FreeLibrary(RspDll);
|
|
||||||
|
|
||||||
m64pCoreDoCommandPtr(m64p_command.M64CMD_ROM_CLOSE, 0, IntPtr.Zero);
|
m64pCoreDoCommandPtr(m64p_command.M64CMD_ROM_CLOSE, 0, IntPtr.Zero);
|
||||||
m64pCoreShutdown();
|
m64pCoreShutdown();
|
||||||
|
@ -812,24 +615,49 @@ namespace BizHawk.Emulation.Cores.Nintendo.N64
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void GetScreenDimensions(ref int width, ref int height)
|
struct AttachedPlugin
|
||||||
{
|
{
|
||||||
GFXReadScreen2Res(IntPtr.Zero, ref width, ref height, 0);
|
public PluginStartup dllStartup;
|
||||||
|
public PluginShutdown dllShutdown;
|
||||||
|
public IntPtr dllHandle;
|
||||||
|
}
|
||||||
|
Dictionary<m64p_plugin_type, AttachedPlugin> plugins = new Dictionary<m64p_plugin_type, AttachedPlugin>();
|
||||||
|
|
||||||
|
public IntPtr AttachPlugin(m64p_plugin_type type, string PluginName)
|
||||||
|
{
|
||||||
|
if (plugins.ContainsKey(type))
|
||||||
|
DetachPlugin(type);
|
||||||
|
|
||||||
|
AttachedPlugin plugin;
|
||||||
|
plugin.dllHandle = LoadLibrary(PluginName);
|
||||||
|
if (plugin.dllHandle == IntPtr.Zero)
|
||||||
|
throw new InvalidOperationException(string.Format("Failed to load plugin {0}", PluginName));
|
||||||
|
|
||||||
|
plugin.dllStartup = (PluginStartup)Marshal.GetDelegateForFunctionPointer(GetProcAddress(plugin.dllHandle, "PluginStartup"), typeof(PluginStartup));
|
||||||
|
plugin.dllShutdown = (PluginShutdown)Marshal.GetDelegateForFunctionPointer(GetProcAddress(plugin.dllHandle, "PluginShutdown"), typeof(PluginShutdown));
|
||||||
|
plugin.dllStartup(CoreDll, null, null);
|
||||||
|
|
||||||
|
m64p_error result = m64pCoreAttachPlugin(type, plugin.dllHandle);
|
||||||
|
if (result != m64p_error.M64ERR_SUCCESS)
|
||||||
|
{
|
||||||
|
FreeLibrary(plugin.dllHandle);
|
||||||
|
throw new InvalidOperationException(string.Format("Error during attaching plugin {0}", PluginName));
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins.Add(type, plugin);
|
||||||
|
return plugin.dllHandle;
|
||||||
}
|
}
|
||||||
|
|
||||||
public uint GetSamplingRate()
|
public void DetachPlugin(m64p_plugin_type type)
|
||||||
{
|
{
|
||||||
return (uint)AudGetAudioRate();
|
AttachedPlugin plugin;
|
||||||
}
|
if (plugins.TryGetValue(type, out plugin))
|
||||||
|
{
|
||||||
public int GetAudioBufferSize()
|
plugins.Remove(type);
|
||||||
{
|
m64pCoreDetachPlugin(type);
|
||||||
return AudGetBufferSize();
|
plugin.dllShutdown();
|
||||||
}
|
FreeLibrary(plugin.dllHandle);
|
||||||
|
}
|
||||||
public void GetAudioBuffer(short[] buffer)
|
|
||||||
{
|
|
||||||
AudReadAudioBuffer(buffer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public event Action FrameFinished;
|
public event Action FrameFinished;
|
||||||
|
@ -855,22 +683,4 @@ namespace BizHawk.Emulation.Cores.Nintendo.N64
|
||||||
m64pFrameComplete.Set();
|
m64pFrameComplete.Set();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class VideoPluginSettings
|
|
||||||
{
|
|
||||||
public PLUGINTYPE Plugin;
|
|
||||||
//public Dictionary<string, int> IntParameters = new Dictionary<string,int>();
|
|
||||||
//public Dictionary<string, string> StringParameters = new Dictionary<string,string>();
|
|
||||||
|
|
||||||
public Dictionary<string, object> Parameters = new Dictionary<string, object>();
|
|
||||||
public int Height;
|
|
||||||
public int Width;
|
|
||||||
|
|
||||||
public VideoPluginSettings (PLUGINTYPE Plugin, int Width, int Height)
|
|
||||||
{
|
|
||||||
this.Plugin = Plugin;
|
|
||||||
this.Width = Width;
|
|
||||||
this.Height = Height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using BizHawk.Emulation.Cores.Nintendo.N64;
|
||||||
|
|
||||||
|
namespace BizHawk.Emulation.Cores.Nintendo.N64.NativeApi
|
||||||
|
{
|
||||||
|
class mupen64plusInputApi
|
||||||
|
{
|
||||||
|
IntPtr InpDll;
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll")]
|
||||||
|
public static extern IntPtr GetProcAddress(IntPtr hModule, string procedureName);// Input plugin specific
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets a callback to use when the mupen core wants controller buttons
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="inputCallback">The delegate to use</param>
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||||
|
private delegate void SetInputCallback(InputCallback inputCallback);
|
||||||
|
SetInputCallback InpSetInputCallback;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Callback to use when mupen64plus wants input
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="i"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||||
|
public delegate int InputCallback(int i);
|
||||||
|
InputCallback InpInputCallback;
|
||||||
|
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||||
|
delegate mupen64plusApi.m64p_error SetRumbleCallback(RumbleCallback ParamPtr);
|
||||||
|
SetRumbleCallback InpSetRumbleCallback;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This will be called every time the N64 changes
|
||||||
|
/// rumble
|
||||||
|
/// </summary>
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||||
|
public delegate void RumbleCallback(int Control, int on);
|
||||||
|
RumbleCallback m64pRumbleCallback;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event fired when mupen changes rumble pak status
|
||||||
|
/// </summary>
|
||||||
|
event RumbleCallback OnRumbleChange;
|
||||||
|
|
||||||
|
public mupen64plusInputApi(mupen64plusApi core)
|
||||||
|
{
|
||||||
|
InpDll = core.AttachPlugin(mupen64plusApi.m64p_plugin_type.M64PLUGIN_INPUT,
|
||||||
|
"mupen64plus-input-bkm.dll");
|
||||||
|
|
||||||
|
mupen64plusApi.m64p_error result;
|
||||||
|
InpSetInputCallback = (SetInputCallback)Marshal.GetDelegateForFunctionPointer(GetProcAddress(InpDll, "SetInputCallback"), typeof(SetInputCallback));
|
||||||
|
InpSetRumbleCallback = (SetRumbleCallback)Marshal.GetDelegateForFunctionPointer(GetProcAddress(InpDll, "SetRumbleCallback"), typeof(SetRumbleCallback));
|
||||||
|
|
||||||
|
m64pRumbleCallback = new RumbleCallback(FireOnRumbleChange);
|
||||||
|
result = InpSetRumbleCallback(m64pRumbleCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetM64PInputCallback(InputCallback inputCallback)
|
||||||
|
{
|
||||||
|
InpInputCallback = inputCallback;
|
||||||
|
InpSetInputCallback(InpInputCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FireOnRumbleChange(int Control, int on)
|
||||||
|
{
|
||||||
|
if (OnRumbleChange != null)
|
||||||
|
OnRumbleChange(Control, on);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,123 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using BizHawk.Emulation.Cores.Consoles.Nintendo.N64;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace BizHawk.Emulation.Cores.Nintendo.N64.NativeApi
|
||||||
|
{
|
||||||
|
class mupen64plusVideoApi
|
||||||
|
{
|
||||||
|
IntPtr GfxDll;// Graphics plugin specific
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll")]
|
||||||
|
public static extern IntPtr GetProcAddress(IntPtr hModule, string procedureName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fills a provided buffer with the mupen64plus framebuffer
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="framebuffer">The buffer to fill</param>
|
||||||
|
/// <param name="width">A pointer to a variable to fill with the width of the framebuffer</param>
|
||||||
|
/// <param name="height">A pointer to a variable to fill with the height of the framebuffer</param>
|
||||||
|
/// <param name="buffer">Which buffer to read: 0 = front, 1 = back</param>
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||||
|
private delegate void ReadScreen2(int[] framebuffer, ref int width, ref int height, int buffer);
|
||||||
|
ReadScreen2 GFXReadScreen2;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the width and height of the mupen64plus framebuffer
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dummy">Use IntPtr.Zero</param>
|
||||||
|
/// <param name="width">A pointer to a variable to fill with the width of the framebuffer</param>
|
||||||
|
/// <param name="height">A pointer to a variable to fill with the height of the framebuffer</param>
|
||||||
|
/// <param name="buffer">Which buffer to read: 0 = front, 1 = back</param>
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||||
|
private delegate void ReadScreen2Res(IntPtr dummy, ref int width, ref int height, int buffer);
|
||||||
|
ReadScreen2Res GFXReadScreen2Res;
|
||||||
|
|
||||||
|
|
||||||
|
public mupen64plusVideoApi(mupen64plusApi core, VideoPluginSettings settings)
|
||||||
|
{
|
||||||
|
string videoplugin;
|
||||||
|
switch (settings.Plugin)
|
||||||
|
{
|
||||||
|
default:
|
||||||
|
case PLUGINTYPE.RICE:
|
||||||
|
videoplugin = "mupen64plus-video-rice.dll";
|
||||||
|
break;
|
||||||
|
case PLUGINTYPE.GLIDE:
|
||||||
|
videoplugin = "mupen64plus-video-glide64.dll";
|
||||||
|
break;
|
||||||
|
case PLUGINTYPE.GLIDE64MK2:
|
||||||
|
videoplugin = "mupen64plus-video-glide64mk2.dll";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
GfxDll = core.AttachPlugin(mupen64plusApi.m64p_plugin_type.M64PLUGIN_GFX,
|
||||||
|
videoplugin);
|
||||||
|
GFXReadScreen2 = (ReadScreen2)Marshal.GetDelegateForFunctionPointer(GetProcAddress(GfxDll, "ReadScreen2"), typeof(ReadScreen2));
|
||||||
|
GFXReadScreen2Res = (ReadScreen2Res)Marshal.GetDelegateForFunctionPointer(GetProcAddress(GfxDll, "ReadScreen2"), typeof(ReadScreen2Res));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void GetScreenDimensions(ref int width, ref int height)
|
||||||
|
{
|
||||||
|
GFXReadScreen2Res(IntPtr.Zero, ref width, ref height, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int[] m64pBuffer = new int[0];
|
||||||
|
/// <summary>
|
||||||
|
/// This function copies the frame buffer from mupen64plus
|
||||||
|
/// </summary>
|
||||||
|
public void Getm64pFrameBuffer(int[] buffer, ref int width, ref int height)
|
||||||
|
{
|
||||||
|
if (m64pBuffer.Length != width * height)
|
||||||
|
m64pBuffer = new int[width * height];
|
||||||
|
// Actually get the frame buffer
|
||||||
|
GFXReadScreen2(m64pBuffer, ref width, ref height, 0);
|
||||||
|
|
||||||
|
// vflip
|
||||||
|
int fromindex = width * (height - 1) * 4;
|
||||||
|
int toindex = 0;
|
||||||
|
|
||||||
|
for (int j = 0; j < height; j++)
|
||||||
|
{
|
||||||
|
Buffer.BlockCopy(m64pBuffer, fromindex, buffer, toindex, width * 4);
|
||||||
|
fromindex -= width * 4;
|
||||||
|
toindex += width * 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// opaque
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
fixed (int* ptr = &buffer[0])
|
||||||
|
{
|
||||||
|
int l = buffer.Length;
|
||||||
|
for (int i = 0; i < l; i++)
|
||||||
|
{
|
||||||
|
ptr[i] |= unchecked((int)0xff000000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class VideoPluginSettings
|
||||||
|
{
|
||||||
|
public PLUGINTYPE Plugin;
|
||||||
|
//public Dictionary<string, int> IntParameters = new Dictionary<string,int>();
|
||||||
|
//public Dictionary<string, string> StringParameters = new Dictionary<string,string>();
|
||||||
|
|
||||||
|
public Dictionary<string, object> Parameters = new Dictionary<string, object>();
|
||||||
|
public int Height;
|
||||||
|
public int Width;
|
||||||
|
|
||||||
|
public VideoPluginSettings(PLUGINTYPE Plugin, int Width, int Height)
|
||||||
|
{
|
||||||
|
this.Plugin = Plugin;
|
||||||
|
this.Width = Width;
|
||||||
|
this.Height = Height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue