BizHawk/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/Gambatte.cs

532 lines
15 KiB
C#
Raw Normal View History

2012-09-08 21:36:04 +00:00
using System;
2014-07-03 19:20:34 +00:00
using BizHawk.Common.BufferExtensions;
using BizHawk.Emulation.Common;
2017-06-15 23:00:41 +00:00
using BizHawk.Emulation.Cores.Consoles.Nintendo.Gameboy;
namespace BizHawk.Emulation.Cores.Nintendo.Gameboy
2012-09-08 21:36:04 +00:00
{
/// <summary>
/// a gameboy/gameboy color emulator wrapped around native C++ libgambatte
/// </summary>
[CoreAttributes(
"Gambatte",
2014-06-01 01:57:22 +00:00
"",
isPorted: true,
2014-06-01 01:57:22 +00:00
isReleased: true,
portedVersion: "SVN 344",
2017-04-25 15:11:43 +00:00
portedUrl: "http://gambatte.sourceforge.net/")]
[ServiceNotApplicable(typeof(IDriveLight), typeof(IDriveLight))]
public partial class Gameboy : IEmulator, IVideoProvider, ISoundProvider, ISaveRam, IStatable, IInputPollable, ICodeDataLogger,
2017-06-15 23:00:41 +00:00
IBoardInfo, IDebuggable, ISettable<Gameboy.GambatteSettings, Gameboy.GambatteSyncSettings>,
IGameboyCommon
2012-09-08 21:36:04 +00:00
{
[CoreConstructor("GB", "GBC")]
public Gameboy(CoreComm comm, GameInfo game, byte[] file, object settings, object syncSettings, bool deterministic)
2012-09-08 21:36:04 +00:00
{
var ser = new BasicServiceProvider(this);
ser.Register<IDisassemblable>(new GBDisassembler());
ServiceProvider = ser;
2016-02-28 14:48:51 +00:00
Tracer = new TraceBuffer
{
Header = "Z80: PC, opcode, registers (A, B, C, D, E, F, H, L, LY, SP, CY)"
2016-02-28 14:48:51 +00:00
};
ser.Register<ITraceable>(Tracer);
2014-12-05 02:39:42 +00:00
InitMemoryCallbacks();
CoreComm = comm;
comm.RomStatusAnnotation = null;
comm.RomStatusDetails = null;
comm.NominalWidth = 160;
comm.NominalHeight = 144;
ThrowExceptionForBadRom(file);
BoardName = MapperName(file);
DeterministicEmulation = deterministic;
GambatteState = LibGambatte.gambatte_create();
if (GambatteState == IntPtr.Zero)
2017-04-25 15:11:43 +00:00
{
throw new InvalidOperationException("gambatte_create() returned null???");
2017-04-25 15:11:43 +00:00
}
2017-06-24 17:05:00 +00:00
Console.WriteLine(game.System);
byte[] BiosRom;
if (game.System == "GB")
{
BiosRom = new byte[256];
BiosRom = comm.CoreFileProvider.GetFirmware("GB", "World", false);
}
else
{
BiosRom = new byte[2304];
BiosRom = comm.CoreFileProvider.GetFirmware("GBC", "World", false);
}
int bios_length = BiosRom == null ? 0 : BiosRom.Length;
try
{
_syncSettings = (GambatteSyncSettings)syncSettings ?? new GambatteSyncSettings();
2017-04-25 15:11:43 +00:00
// copy over non-loadflag syncsettings now; they won't take effect if changed later
2017-04-25 15:11:43 +00:00
zerotime = (uint)_syncSettings.RTCInitialTime;
real_rtc_time = !DeterministicEmulation && _syncSettings.RealTimeRTC;
LibGambatte.LoadFlags flags = 0;
2017-06-24 20:01:07 +00:00
if (_syncSettings.ForceDMG)
{
flags |= LibGambatte.LoadFlags.FORCE_DMG;
// we need to change the BIOS to GB bios
if (game.System == "GBC")
{
BiosRom = null;
BiosRom = new byte[256];
BiosRom = comm.CoreFileProvider.GetFirmware("GB", "World", false);
}
}
2017-06-24 17:05:00 +00:00
if (_syncSettings.EnableBIOS && BiosRom == null)
{
2017-06-24 20:01:07 +00:00
throw new MissingFirmwareException("Boot Rom not found");
2017-06-24 17:05:00 +00:00
}
// to disable BIOS loading into gambatte, just set bios_length to 0
if (!_syncSettings.EnableBIOS)
{
bios_length = 0;
}
2017-04-25 15:11:43 +00:00
if (_syncSettings.GBACGB)
{
flags |= LibGambatte.LoadFlags.GBA_CGB;
2017-04-25 15:11:43 +00:00
}
if (_syncSettings.MulticartCompat)
{
flags |= LibGambatte.LoadFlags.MULTICART_COMPAT;
2017-04-25 15:11:43 +00:00
}
2017-06-24 17:05:00 +00:00
if (LibGambatte.gambatte_load(GambatteState, file, (uint)file.Length, BiosRom, (uint)bios_length, GetCurrentTime(), flags) != 0)
2017-04-25 15:11:43 +00:00
{
throw new InvalidOperationException("gambatte_load() returned non-zero (is this not a gb or gbc rom?)");
2017-04-25 15:11:43 +00:00
}
2012-09-08 21:36:04 +00:00
// set real default colors (before anyone mucks with them at all)
PutSettings((GambatteSettings)settings ?? new GambatteSettings());
InitSound();
2012-09-09 14:17:57 +00:00
Frame = 0;
LagCount = 0;
IsLagFrame = false;
2012-09-09 14:17:57 +00:00
InputCallback = new LibGambatte.InputGetter(ControllerCallback);
2012-09-09 14:17:57 +00:00
LibGambatte.gambatte_setinputgetter(GambatteState, InputCallback);
InitMemoryDomains();
2017-04-25 15:11:43 +00:00
CoreComm.RomStatusDetails = $"{game.Name}\r\nSHA1:{file.HashSHA1()}\r\nMD5:{file.HashMD5()}\r\n";
2017-04-25 15:11:43 +00:00
byte[] buff = new byte[32];
LibGambatte.gambatte_romtitle(GambatteState, buff);
string romname = System.Text.Encoding.ASCII.GetString(buff);
Console.WriteLine("Core reported rom name: {0}", romname);
2014-06-16 15:59:39 +00:00
TimeCallback = new LibGambatte.RTCCallback(GetCurrentTime);
LibGambatte.gambatte_setrtccallback(GambatteState, TimeCallback);
2014-05-10 04:22:12 +00:00
2017-04-25 16:06:50 +00:00
_cdCallback = new LibGambatte.CDCallback(CDCallbackProc);
2015-10-27 00:16:38 +00:00
2014-05-10 04:22:12 +00:00
NewSaveCoreSetBuff();
}
catch
{
Dispose();
throw;
}
2012-09-08 21:36:04 +00:00
}
/// <summary>
/// the nominal length of one frame
/// </summary>
private const uint TICKSINFRAME = 35112;
/// <summary>
/// number of ticks per second
/// </summary>
private const uint TICKSPERSECOND = 2097152;
/// <summary>
/// keep a copy of the input callback delegate so it doesn't get GCed
/// </summary>
private LibGambatte.InputGetter InputCallback;
/// <summary>
/// whatever keys are currently depressed
/// </summary>
private LibGambatte.Buttons CurrentButtons = 0;
#region RTC
/// <summary>
/// RTC time when emulation begins.
/// </summary>
private readonly uint zerotime = 0;
/// <summary>
/// if true, RTC will run off of real elapsed time
/// </summary>
private bool real_rtc_time = false;
private LibGambatte.RTCCallback TimeCallback;
private static long GetUnixNow()
{
// because internally the RTC works off of relative time, we don't need to base
// this off of any particular canonical epoch.
return DateTime.UtcNow.Ticks / 10000000L - 60000000000L;
}
private uint GetCurrentTime()
{
if (real_rtc_time)
{
return (uint)GetUnixNow();
}
ulong fn = (ulong)Frame;
// as we're exactly tracking cpu cycles, this can be pretty accurate
fn *= 4389;
fn /= 262144;
fn += zerotime;
return (uint)fn;
}
private uint GetInitialTime()
{
if (real_rtc_time)
{
return (uint)GetUnixNow();
}
// setting the initial boot time to 0 will cause our zerotime
// to function as an initial offset, which is what we want
return 0;
}
#endregion
2017-04-15 20:37:30 +00:00
#region ALL SAVESTATEABLE STATE GOES HERE
2017-04-15 20:37:30 +00:00
/// <summary>
/// internal gambatte state
/// </summary>
2017-04-25 15:11:43 +00:00
internal IntPtr GambatteState { get; private set; } = IntPtr.Zero;
2017-04-15 20:37:30 +00:00
public int LagCount { get; set; }
public bool IsLagFrame { get; set; }
2017-04-15 20:37:30 +00:00
// all cycle counts are relative to a 2*1024*1024 mhz refclock
2017-04-15 20:37:30 +00:00
/// <summary>
/// total cycles actually executed
/// </summary>
private ulong _cycleCount = 0;
2017-04-15 20:37:30 +00:00
/// <summary>
/// number of extra cycles we overran in the last frame
/// </summary>
private uint frameOverflow = 0;
2017-04-25 15:11:43 +00:00
public ulong CycleCount => _cycleCount;
2017-04-15 20:37:30 +00:00
#endregion
2014-08-04 22:25:07 +00:00
#region controller
2012-09-09 02:06:07 +00:00
public static readonly ControllerDefinition GbController = new ControllerDefinition
2012-09-08 21:36:04 +00:00
{
Name = "Gameboy Controller",
BoolButtons =
{
"Up", "Down", "Left", "Right", "Start", "Select", "B", "A", "Power"
2012-09-08 21:36:04 +00:00
}
};
2017-04-25 15:11:43 +00:00
private LibGambatte.Buttons ControllerCallback()
{
InputCallbacks.Call();
IsLagFrame = false;
return CurrentButtons;
}
2014-08-04 22:25:07 +00:00
#endregion
/// <summary>
/// true if the emulator is currently emulating CGB
/// </summary>
public bool IsCGBMode()
{
2017-04-25 15:11:43 +00:00
return LibGambatte.gambatte_iscgb(GambatteState);
}
2012-09-09 14:17:57 +00:00
2014-12-05 02:39:42 +00:00
private InputCallbackSystem _inputCallbacks = new InputCallbackSystem();
2017-04-25 15:11:43 +00:00
2014-12-05 02:39:42 +00:00
// low priority TODO: due to certain aspects of the core implementation,
// we don't smartly use the ActiveChanged event here.
2017-04-25 15:11:43 +00:00
public IInputCallbackSystem InputCallbacks => _inputCallbacks;
2014-12-05 02:39:42 +00:00
/// <summary>
/// for use in dual core
/// </summary>
public void ConnectInputCallbackSystem(InputCallbackSystem ics)
{
_inputCallbacks = ics;
}
internal void FrameAdvancePrep(IController controller)
2012-09-08 21:36:04 +00:00
{
Frame++;
2012-09-09 14:17:57 +00:00
// update our local copy of the controller data
CurrentButtons = 0;
if (controller.IsPressed("Up"))
2012-09-09 14:17:57 +00:00
CurrentButtons |= LibGambatte.Buttons.UP;
if (controller.IsPressed("Down"))
2012-09-09 14:17:57 +00:00
CurrentButtons |= LibGambatte.Buttons.DOWN;
if (controller.IsPressed("Left"))
2012-09-09 14:17:57 +00:00
CurrentButtons |= LibGambatte.Buttons.LEFT;
if (controller.IsPressed("Right"))
2012-09-09 14:17:57 +00:00
CurrentButtons |= LibGambatte.Buttons.RIGHT;
if (controller.IsPressed("A"))
2012-09-09 14:17:57 +00:00
CurrentButtons |= LibGambatte.Buttons.A;
if (controller.IsPressed("B"))
2012-09-09 14:17:57 +00:00
CurrentButtons |= LibGambatte.Buttons.B;
if (controller.IsPressed("Select"))
2012-09-09 14:17:57 +00:00
CurrentButtons |= LibGambatte.Buttons.SELECT;
if (controller.IsPressed("Start"))
2012-09-09 14:17:57 +00:00
CurrentButtons |= LibGambatte.Buttons.START;
// the controller callback will set this to false if it actually gets called during the frame
IsLagFrame = true;
if (controller.IsPressed("Power"))
2017-04-25 15:11:43 +00:00
{
LibGambatte.gambatte_reset(GambatteState, GetCurrentTime());
2017-04-25 15:11:43 +00:00
}
if (Tracer.Enabled)
2017-04-25 15:11:43 +00:00
{
2017-04-25 16:06:50 +00:00
_tracecb = MakeTrace;
2017-04-25 15:11:43 +00:00
}
2012-11-02 19:44:31 +00:00
else
2017-04-25 15:11:43 +00:00
{
2017-04-25 16:06:50 +00:00
_tracecb = null;
2017-04-25 15:11:43 +00:00
}
2017-04-25 16:06:50 +00:00
LibGambatte.gambatte_settracecallback(GambatteState, _tracecb);
2016-02-08 08:18:24 +00:00
2016-04-12 08:02:11 +00:00
LibGambatte.gambatte_setlayers(GambatteState, (_settings.DisplayBG ? 1 : 0) | (_settings.DisplayOBJ ? 2 : 0) | (_settings.DisplayWindow ? 4 : 0 ) );
}
internal void FrameAdvancePost()
{
if (IsLagFrame)
2014-08-04 22:25:07 +00:00
{
2017-04-25 15:11:43 +00:00
LagCount++;
2013-10-25 01:00:31 +00:00
}
2017-04-25 15:11:43 +00:00
endofframecallback?.Invoke(LibGambatte.gambatte_cpuread(GambatteState, 0xff40));
}
2017-04-25 15:11:43 +00:00
private static string MapperName(byte[] romdata)
{
switch (romdata[0x147])
{
case 0x00: return "Plain ROM"; // = PLAIN; break;
case 0x01: return "MBC1 ROM"; // = MBC1; break;
case 0x02: return "MBC1 ROM+RAM"; // = MBC1; break;
case 0x03: return "MBC1 ROM+RAM+BATTERY"; // = MBC1; break;
case 0x05: return "MBC2 ROM"; // = MBC2; break;
case 0x06: return "MBC2 ROM+BATTERY"; // = MBC2; break;
case 0x08: return "Plain ROM+RAM"; // = PLAIN; break;
case 0x09: return "Plain ROM+RAM+BATTERY"; // = PLAIN; break;
case 0x0F: return "MBC3 ROM+TIMER+BATTERY"; // = MBC3; break;
case 0x10: return "MBC3 ROM+TIMER+RAM+BATTERY"; // = MBC3; break;
case 0x11: return "MBC3 ROM"; // = MBC3; break;
case 0x12: return "MBC3 ROM+RAM"; // = MBC3; break;
case 0x13: return "MBC3 ROM+RAM+BATTERY"; // = MBC3; break;
case 0x19: return "MBC5 ROM"; // = MBC5; break;
case 0x1A: return "MBC5 ROM+RAM"; // = MBC5; break;
case 0x1B: return "MBC5 ROM+RAM+BATTERY"; // = MBC5; break;
case 0x1C: return "MBC5 ROM+RUMBLE"; // = MBC5; break;
case 0x1D: return "MBC5 ROM+RUMBLE+RAM"; // = MBC5; break;
case 0x1E: return "MBC5 ROM+RUMBLE+RAM+BATTERY"; // = MBC5; break;
case 0xFF: return "HuC1 ROM+RAM+BATTERY"; // = HUC1; break;
default: return "UNKNOWN";
}
}
/// <summary>
/// throw exception with intelligible message on some kinds of bad rom
/// </summary>
2017-04-25 15:11:43 +00:00
private static void ThrowExceptionForBadRom(byte[] romdata)
{
if (romdata.Length < 0x148)
2017-04-25 15:11:43 +00:00
{
throw new ArgumentException("ROM is far too small to be a valid GB\\GBC rom!");
2017-04-25 15:11:43 +00:00
}
2012-09-09 14:17:57 +00:00
switch (romdata[0x147])
{
case 0x00: break;
case 0x01: break;
case 0x02: break;
case 0x03: break;
case 0x05: break;
case 0x06: break;
case 0x08: break;
case 0x09: break;
case 0x0b: throw new UnsupportedGameException("\"MM01\" Mapper not supported!");
case 0x0c: throw new UnsupportedGameException("\"MM01\" Mapper not supported!");
case 0x0d: throw new UnsupportedGameException("\"MM01\" Mapper not supported!");
case 0x0f: break;
case 0x10: break;
case 0x11: break;
case 0x12: break;
case 0x13: break;
case 0x15: throw new UnsupportedGameException("\"MBC4\" Mapper not supported!");
case 0x16: throw new UnsupportedGameException("\"MBC4\" Mapper not supported!");
case 0x17: throw new UnsupportedGameException("\"MBC4\" Mapper not supported!");
case 0x19: break;
case 0x1a: break;
case 0x1b: break;
case 0x1c: break; // rumble
case 0x1d: break; // rumble
case 0x1e: break; // rumble
case 0x20: throw new UnsupportedGameException("\"MBC6\" Mapper not supported!");
case 0x22: throw new UnsupportedGameException("\"MBC7\" Mapper not supported!");
case 0xfc: throw new UnsupportedGameException("\"Pocket Camera\" Mapper not supported!");
case 0xfd: throw new UnsupportedGameException("\"Bandai TAMA5\" Mapper not supported!");
case 0xfe: throw new UnsupportedGameException("\"HuC3\" Mapper not supported!");
case 0xff: break;
2017-04-25 15:11:43 +00:00
default: throw new UnsupportedGameException($"Unknown mapper: {romdata[0x147]:x2}");
}
2012-09-08 21:36:04 +00:00
}
#region ppudebug
2014-08-04 22:25:07 +00:00
public bool GetGPUMemoryAreas(out IntPtr vram, out IntPtr bgpal, out IntPtr sppal, out IntPtr oam)
{
IntPtr _vram = IntPtr.Zero;
IntPtr _bgpal = IntPtr.Zero;
IntPtr _sppal = IntPtr.Zero;
IntPtr _oam = IntPtr.Zero;
int unused = 0;
if (!LibGambatte.gambatte_getmemoryarea(GambatteState, LibGambatte.MemoryAreas.vram, ref _vram, ref unused)
|| !LibGambatte.gambatte_getmemoryarea(GambatteState, LibGambatte.MemoryAreas.bgpal, ref _bgpal, ref unused)
|| !LibGambatte.gambatte_getmemoryarea(GambatteState, LibGambatte.MemoryAreas.sppal, ref _sppal, ref unused)
|| !LibGambatte.gambatte_getmemoryarea(GambatteState, LibGambatte.MemoryAreas.oam, ref _oam, ref unused))
{
vram = IntPtr.Zero;
bgpal = IntPtr.Zero;
sppal = IntPtr.Zero;
oam = IntPtr.Zero;
return false;
}
vram = _vram;
bgpal = _bgpal;
sppal = _sppal;
oam = _oam;
return true;
}
/// <summary>
/// </summary>
/// <param name="lcdc">current value of register $ff40 (LCDC)</param>
public delegate void ScanlineCallback(int lcdc);
/// <summary>
/// set up callback
/// </summary>
/// <param name="line">scanline. -1 = end of frame, -2 = RIGHT NOW</param>
public void SetScanlineCallback(ScanlineCallback callback, int line)
{
if (GambatteState == IntPtr.Zero)
2017-04-25 15:11:43 +00:00
{
return; // not sure how this is being reached. tried the debugger...
}
endofframecallback = null;
if (callback == null || line == -1 || line == -2)
{
scanlinecb = null;
LibGambatte.gambatte_setscanlinecallback(GambatteState, null, 0);
if (line == -1)
2017-04-25 15:11:43 +00:00
{
endofframecallback = callback;
2017-04-25 15:11:43 +00:00
}
else if (line == -2)
2017-04-25 15:11:43 +00:00
{
callback(LibGambatte.gambatte_cpuread(GambatteState, 0xff40));
2017-04-25 15:11:43 +00:00
}
}
else if (line >= 0 && line <= 153)
{
2017-04-25 15:11:43 +00:00
scanlinecb = delegate
{
callback(LibGambatte.gambatte_cpuread(GambatteState, 0xff40));
};
LibGambatte.gambatte_setscanlinecallback(GambatteState, scanlinecb, line);
}
else
{
2017-04-10 12:36:42 +00:00
throw new ArgumentOutOfRangeException(nameof(line), "line must be in [0, 153]");
}
}
LibGambatte.ScanlineCallback scanlinecb;
ScanlineCallback endofframecallback;
#endregion
#region palette
2012-09-12 22:18:51 +00:00
/// <summary>
/// update gambatte core's internal colors
/// </summary>
public void ChangeDMGColors(int[] colors)
2012-09-12 22:18:51 +00:00
{
for (int i = 0; i < 12; i++)
2017-04-25 15:11:43 +00:00
{
LibGambatte.gambatte_setdmgpalettecolor(GambatteState, (LibGambatte.PalType)(i / 4), (uint)i % 4, (uint)colors[i]);
2017-04-25 15:11:43 +00:00
}
2012-09-12 22:18:51 +00:00
}
public void SetCGBColors(GBColors.ColorType type)
{
int[] lut = GBColors.GetLut(type);
2012-11-19 17:59:57 +00:00
LibGambatte.gambatte_setcgbpalette(GambatteState, lut);
}
2012-09-08 21:36:04 +00:00
#endregion
}
}