BizHawk/BizHawk.Emulation.Cores/Consoles/Nintendo/SNES/LibsnesCore.cs

563 lines
17 KiB
C#
Raw Normal View History

2012-09-04 07:09:00 +00:00
using System;
using System.Linq;
using System.Xml;
using System.IO;
using BizHawk.Common.BufferExtensions;
using BizHawk.Emulation.Common;
2017-04-19 16:40:41 +00:00
using BizHawk.Emulation.Cores.Components.W65816;
// TODO - add serializer (?)
2017-04-19 16:40:41 +00:00
// http://wiki.superfamicom.org/snes/show/Backgrounds
// TODO
// libsnes needs to be modified to support multiple instances - THIS IS NECESSARY - or else loading one game and then another breaks things
// edit - this is a lot of work
// wrap dll code around some kind of library-accessing interface so that it doesnt malfunction if the dll is unavailablecd
namespace BizHawk.Emulation.Cores.Nintendo.SNES
{
[CoreAttributes(
"BSNES",
"byuu",
isPorted: true,
2014-06-01 01:57:22 +00:00
isReleased: true,
portedVersion: "v87",
2017-04-19 16:40:41 +00:00
portedUrl: "http://byuu.org/")]
[ServiceNotApplicable(typeof(IDriveLight))]
public unsafe partial class LibsnesCore : IEmulator, IVideoProvider, ISaveRam, IStatable, IInputPollable, IRegionable, ICodeDataLogger,
IDebuggable, ISettable<LibsnesCore.SnesSettings, LibsnesCore.SnesSyncSettings>
{
public LibsnesCore(GameInfo game, byte[] romData, bool deterministicEmulation, byte[] xmlData, CoreComm comm, object Settings, object SyncSettings)
{
2017-04-19 16:40:41 +00:00
var ser = new BasicServiceProvider(this);
ServiceProvider = ser;
2017-04-19 16:40:41 +00:00
_tracer = new TraceBuffer
{
Header = "65816: PC, mnemonic, operands, registers (A, X, Y, S, D, DB, flags (NVMXDIZC), V, H)"
};
2017-04-19 16:40:41 +00:00
ser.Register<ITraceable>(_tracer);
ser.Register<IDisassemblable>(new W65816_DisassemblerService());
_game = game;
CoreComm = comm;
byte[] sgbRomData = null;
2017-04-19 16:40:41 +00:00
if (game["SGB"])
{
if ((romData[0x143] & 0xc0) == 0xc0)
2017-04-19 16:40:41 +00:00
{
throw new CGBNotSupportedException();
2017-04-19 16:40:41 +00:00
}
sgbRomData = CoreComm.CoreFileProvider.GetFirmware("SNES", "Rom_SGB", true, "SGB Rom is required for SGB emulation.");
game.FirmwareHash = sgbRomData.HashSHA1();
}
_settings = (SnesSettings)Settings ?? new SnesSettings();
_syncSettings = (SnesSyncSettings)SyncSettings ?? new SnesSyncSettings();
Api = new LibsnesApi(GetDllPath())
2017-04-19 16:40:41 +00:00
{
ReadHook = ReadHook,
ExecHook = ExecHook,
WriteHook = WriteHook
};
ScanlineHookManager = new MyScanlineHookManager(this);
_controllerDeck = new LibsnesControllerDeck(_syncSettings);
_controllerDeck.NativeInit(Api);
Api.CMD_init();
Api.QUERY_set_video_refresh(snes_video_refresh);
Api.QUERY_set_input_poll(snes_input_poll);
Api.QUERY_set_input_state(snes_input_state);
Api.QUERY_set_input_notify(snes_input_notify);
Api.QUERY_set_path_request(snes_path_request);
_scanlineStartCb = new LibsnesApi.snes_scanlineStart_t(snes_scanlineStart);
_tracecb = new LibsnesApi.snes_trace_t(snes_trace);
_soundcb = new LibsnesApi.snes_audio_sample_t(snes_audio_sample);
Api.QUERY_set_audio_sample(_soundcb);
RefreshPalette();
// start up audio resampler
InitAudio();
ser.Register<ISoundProvider>(_resampler);
2017-04-19 16:40:41 +00:00
// strip header
if ((romData?.Length & 0x7FFF) == 512)
{
var newData = new byte[romData.Length - 512];
Array.Copy(romData, 512, newData, 0, newData.Length);
romData = newData;
}
if (game["SGB"])
{
IsSGB = true;
SystemId = "SNES";
ser.Register<IBoardInfo>(new SGBBoardInfo());
_currLoadParams = new LoadParams()
{
type = LoadParamType.SuperGameBoy,
rom_xml = null,
rom_data = sgbRomData,
rom_size = (uint)sgbRomData.Length,
dmg_data = romData,
};
if (!LoadCurrent())
2017-04-19 16:40:41 +00:00
{
throw new Exception("snes_load_cartridge_normal() failed");
2017-04-19 16:40:41 +00:00
}
}
else
{
2017-04-19 16:40:41 +00:00
// we may need to get some information out of the cart, even during the following bootup/load process
if (xmlData != null)
{
_romxml = new XmlDocument();
_romxml.Load(new MemoryStream(xmlData));
2017-04-19 16:40:41 +00:00
// bsnes wont inspect the xml to load the necessary sfc file.
// so, we have to do that here and pass it in as the romData :/
2017-04-19 17:33:05 +00:00
if (_romxml["cartridge"]?["rom"] != null)
2017-04-19 16:40:41 +00:00
{
romData = File.ReadAllBytes(CoreComm.CoreFileProvider.PathSubfile(_romxml["cartridge"]["rom"].Attributes["name"].Value));
2017-04-19 16:40:41 +00:00
}
else
2017-04-19 16:40:41 +00:00
{
throw new Exception("Could not find rom file specification in xml file. Please check the integrity of your xml file");
2017-04-19 16:40:41 +00:00
}
}
SystemId = "SNES";
2017-04-19 17:33:05 +00:00
_currLoadParams = new LoadParams
{
type = LoadParamType.Normal,
xml_data = xmlData,
rom_data = romData
};
if (!LoadCurrent())
2017-04-19 16:40:41 +00:00
{
throw new Exception("snes_load_cartridge_normal() failed");
2017-04-19 16:40:41 +00:00
}
}
if (Api.Region == LibsnesApi.SNES_REGION.NTSC)
{
2017-04-19 16:40:41 +00:00
// similar to what aviout reports from snes9x and seems logical from bsnes first principles. bsnes uses that numerator (ntsc master clockrate) for sure.
CoreComm.VsyncNum = 21477272;
CoreComm.VsyncDen = 4 * 341 * 262;
}
else
{
2017-04-19 17:33:05 +00:00
// http://forums.nesdev.com/viewtopic.php?t=5367&start=19
CoreComm.VsyncNum = 21281370;
CoreComm.VsyncDen = 4 * 341 * 312;
}
Api.CMD_power();
SetupMemoryDomains(romData, sgbRomData);
// DeterministicEmulation = deterministicEmulation; // Note we don't respect the value coming in and force it instead
if (DeterministicEmulation) // save frame-0 savestate now
{
2017-04-19 16:40:41 +00:00
var ms = new MemoryStream();
var bw = new BinaryWriter(ms);
bw.Write(CoreSaveState());
bw.Write(true); // framezero, so no controller follows and don't frameadvance on load
bw.Close();
_savestatebuff = ms.ToArray();
}
}
private readonly GameInfo _game;
private readonly LibsnesControllerDeck _controllerDeck;
private readonly ITraceable _tracer;
private readonly XmlDocument _romxml;
private readonly LibsnesApi.snes_scanlineStart_t _scanlineStartCb;
private readonly LibsnesApi.snes_trace_t _tracecb;
private readonly LibsnesApi.snes_audio_sample_t _soundcb;
private IController _controller;
private LoadParams _currLoadParams;
private SpeexResampler _resampler;
private int _timeFrameCounter;
private bool _nocallbacks; // disable all external callbacks. the front end should not even know the core is frame advancing
private bool _disposed;
public bool IsSGB { get; }
private class SGBBoardInfo : IBoardInfo
{
public string BoardName => "SGB";
}
public string CurrentProfile
{
get
{
// TODO: This logic will only work until Accuracy is ready, would we really want to override the user's choice of Accuracy with Compatibility?
if (_game.OptionValue("profile") == "Compatibility")
{
return "Compatibility";
}
return _syncSettings.Profile;
}
}
public LibsnesApi Api { get; }
2017-04-15 22:27:04 +00:00
public SnesColors.ColorType CurrPalette { get; private set; }
public MyScanlineHookManager ScanlineHookManager { get; }
public class MyScanlineHookManager : ScanlineHookManager
{
private readonly LibsnesCore _core;
public MyScanlineHookManager(LibsnesCore core)
{
_core = core;
}
2017-04-19 16:40:41 +00:00
protected override void OnHooksChanged()
{
_core.OnScanlineHooksChanged();
}
}
2017-04-19 16:40:41 +00:00
private void OnScanlineHooksChanged()
{
if (_disposed)
{
return;
}
2017-04-19 17:33:05 +00:00
Api.QUERY_set_scanlineStart(ScanlineHookManager.HookCount == 0 ? null : _scanlineStartCb);
}
2017-04-19 16:40:41 +00:00
private void snes_scanlineStart(int line)
{
ScanlineHookManager.HandleScanline(line);
}
2017-04-19 16:40:41 +00:00
private string snes_path_request(int slot, string hint)
{
2017-04-19 16:40:41 +00:00
// every rom requests msu1.rom... why? who knows.
// also handle msu-1 pcm files here
bool isMsu1Rom = hint == "msu1.rom";
bool isMsu1Pcm = Path.GetExtension(hint).ToLower() == ".pcm";
if (isMsu1Rom || isMsu1Pcm)
{
2017-04-19 16:40:41 +00:00
// well, check if we have an msu-1 xml
2017-04-19 17:33:05 +00:00
if (_romxml?["cartridge"]?["msu1"] != null)
{
var msu1 = _romxml["cartridge"]["msu1"];
2017-04-19 17:33:05 +00:00
if (isMsu1Rom && msu1["rom"]?.Attributes["name"] != null)
2017-04-19 16:40:41 +00:00
{
return CoreComm.CoreFileProvider.PathSubfile(msu1["rom"].Attributes["name"].Value);
2017-04-19 16:40:41 +00:00
}
if (isMsu1Pcm)
{
2017-04-19 16:40:41 +00:00
// return @"D:\roms\snes\SuperRoadBlaster\SuperRoadBlaster-1.pcm";
// return "";
2017-04-19 17:33:05 +00:00
int wantsTrackNumber = int.Parse(hint.Replace("track-", string.Empty).Replace(".pcm", string.Empty));
wantsTrackNumber++;
string wantsTrackString = wantsTrackNumber.ToString();
foreach (var child in msu1.ChildNodes.Cast<XmlNode>())
{
if (child.Name == "track" && child.Attributes["number"].Value == wantsTrackString)
2017-04-19 16:40:41 +00:00
{
return CoreComm.CoreFileProvider.PathSubfile(child.Attributes["name"].Value);
2017-04-19 16:40:41 +00:00
}
}
}
}
2017-04-19 16:40:41 +00:00
// not found.. what to do? (every rom will get here when msu1.rom is requested)
return string.Empty;
}
// not MSU-1. ok.
2017-04-19 17:33:05 +00:00
string firmwareId;
switch (hint)
{
2017-04-19 17:33:05 +00:00
case "cx4.rom": firmwareId = "CX4"; break;
case "dsp1.rom": firmwareId = "DSP1"; break;
case "dsp1b.rom": firmwareId = "DSP1b"; break;
case "dsp2.rom": firmwareId = "DSP2"; break;
case "dsp3.rom": firmwareId = "DSP3"; break;
case "dsp4.rom": firmwareId = "DSP4"; break;
case "st010.rom": firmwareId = "ST010"; break;
case "st011.rom": firmwareId = "ST011"; break;
case "st018.rom": firmwareId = "ST018"; break;
default:
2017-04-19 16:40:41 +00:00
CoreComm.ShowMessage($"Unrecognized SNES firmware request \"{hint}\".");
return string.Empty;
}
2017-04-19 16:40:41 +00:00
// build romfilename
2017-04-19 17:33:05 +00:00
string test = CoreComm.CoreFileProvider.GetFirmwarePath("SNES", firmwareId, false, "Game may function incorrectly without the requested firmware.");
2017-04-19 16:40:41 +00:00
// we need to return something to bsnes
test = test ?? string.Empty;
Console.WriteLine("Served libsnes request for firmware \"{0}\" with \"{1}\"", hint, test);
2017-04-19 16:40:41 +00:00
// return the path we built
return test;
}
2017-04-19 16:40:41 +00:00
private void snes_trace(string msg)
{
// TODO: get them out of the core split up and remove this hackery
string splitStr = "A:";
var split = msg.Split(new[] {splitStr }, 2, StringSplitOptions.None);
2017-04-19 16:40:41 +00:00
_tracer.Put(new TraceInfo
{
Disassembly = split[0].PadRight(34),
RegisterInfo = splitStr + split[1]
});
}
private void SetPalette(SnesColors.ColorType pal)
{
CurrPalette = pal;
int[] tmp = SnesColors.GetLUT(pal);
fixed (int* p = &tmp[0])
Api.QUERY_set_color_lut((IntPtr)p);
}
2017-04-19 16:40:41 +00:00
private string GetDllPath()
{
2015-11-02 17:26:49 +00:00
var exename = "libsneshawk-32-" + CurrentProfile.ToLower() + ".dll";
2015-11-02 17:26:49 +00:00
string dllPath = Path.Combine(CoreComm.CoreFileProvider.DllPath(), exename);
2015-11-02 17:26:49 +00:00
if (!File.Exists(dllPath))
2017-04-19 16:40:41 +00:00
{
2015-11-02 17:26:49 +00:00
throw new InvalidOperationException("Couldn't locate the DLL for SNES emulation for profile: " + CurrentProfile + ". Please make sure you're using a fresh dearchive of a BizHawk distribution.");
2017-04-19 16:40:41 +00:00
}
2015-11-02 17:26:49 +00:00
return dllPath;
}
private void ReadHook(uint addr)
2013-11-12 02:34:56 +00:00
{
MemoryCallbacks.CallReads(addr);
2017-04-19 16:40:41 +00:00
// we RefreshMemoryCallbacks() after the trigger in case the trigger turns itself off at that point
// EDIT: for now, theres some IPC re-entrancy problem
// RefreshMemoryCallbacks();
2013-11-12 02:34:56 +00:00
}
2017-04-19 16:40:41 +00:00
private void ExecHook(uint addr)
2013-11-12 02:34:56 +00:00
{
MemoryCallbacks.CallExecutes(addr);
2017-04-19 16:40:41 +00:00
// we RefreshMemoryCallbacks() after the trigger in case the trigger turns itself off at that point
// EDIT: for now, theres some IPC re-entrancy problem
// RefreshMemoryCallbacks();
2013-11-12 02:34:56 +00:00
}
2017-04-19 16:40:41 +00:00
private void WriteHook(uint addr, byte val)
2013-11-12 02:34:56 +00:00
{
MemoryCallbacks.CallWrites(addr);
2017-04-19 16:40:41 +00:00
// we RefreshMemoryCallbacks() after the trigger in case the trigger turns itself off at that point
// EDIT: for now, theres some IPC re-entrancy problem
// RefreshMemoryCallbacks();
}
2017-04-19 16:40:41 +00:00
private enum LoadParamType
{
Normal, SuperGameBoy
}
2017-04-19 16:40:41 +00:00
private struct LoadParams
{
public LoadParamType type;
public byte[] xml_data;
public string rom_xml;
public byte[] rom_data;
public uint rom_size;
public byte[] dmg_data;
}
2017-04-19 16:40:41 +00:00
private bool LoadCurrent()
{
bool result = _currLoadParams.type == LoadParamType.Normal
? Api.CMD_load_cartridge_normal(_currLoadParams.xml_data, _currLoadParams.rom_data)
: Api.CMD_load_cartridge_super_game_boy(_currLoadParams.rom_xml, _currLoadParams.rom_data, _currLoadParams.rom_size, _currLoadParams.dmg_data);
_mapper = Api.Mapper;
return result;
}
/// <summary>
/// </summary>
/// <param name="port">0 or 1, corresponding to L and R physical ports on the snes</param>
/// <param name="device">LibsnesApi.SNES_DEVICE enum index specifying type of device</param>
/// <param name="index">meaningless for most controllers. for multitap, 0-3 for which multitap controller</param>
/// <param name="id">button ID enum; in the case of a regular controller, this corresponds to shift register position</param>
/// <returns>for regular controllers, one bit D0 of button status. for other controls, varying ranges depending on id</returns>
2017-04-19 16:40:41 +00:00
private short snes_input_state(int port, int device, int index, int id)
{
return _controllerDeck.CoreInputState(_controller, port, device, index, id);
}
2017-04-19 16:40:41 +00:00
private void snes_input_poll()
2012-09-23 15:57:01 +00:00
{
2012-10-06 13:34:04 +00:00
// this doesn't actually correspond to anything in the underlying bsnes;
// it gets called once per frame with video_refresh() and has nothing to do with anything
2012-09-23 15:57:01 +00:00
}
2017-04-19 16:40:41 +00:00
private void snes_input_notify(int index)
{
// gets called with the following numbers:
// 4xxx : lag frame related
// 0: signifies latch bit going to 0. should be reported as oninputpoll
// 1: signifies latch bit going to 1. should be reported as oninputpoll
if (index >= 0x4000)
{
IsLagFrame = false;
}
}
2017-04-19 16:40:41 +00:00
private void snes_video_refresh(int* data, int width, int height)
{
bool doubleSize = _settings.AlwaysDoubleSize;
bool lineDouble = doubleSize, dotDouble = doubleSize;
_videoWidth = width;
_videoHeight = height;
int yskip = 1, xskip = 1;
2017-04-19 16:40:41 +00:00
// if we are in high-res mode, we get double width. so, lets double the height here to keep it square.
if (width == 512)
{
_videoHeight *= 2;
yskip = 2;
lineDouble = true;
2017-04-19 16:40:41 +00:00
// we dont dot double here because the user wanted double res and the game provided double res
dotDouble = false;
}
else if (lineDouble)
{
_videoHeight *= 2;
yskip = 2;
}
int srcPitch = 1024;
int srcStart = 0;
2017-04-19 16:40:41 +00:00
bool interlaced = height == 478 || height == 448;
if (interlaced)
{
2017-04-19 16:40:41 +00:00
// from bsnes in interlaced mode we have each field side by side
// so we will come in with a dimension of 512x448, say
// but the fields are side by side, so it's actually 1024x224.
// copy the first scanline from row 0, then the 2nd scanline from row 0 (offset 512)
// EXAMPLE: yu yu hakushu legal screens
// EXAMPLE: World Class Service Super Nintendo Tester (double resolution vertically but not horizontally, in character test the stars should shrink)
lineDouble = false;
srcPitch = 512;
yskip = 1;
_videoHeight = height;
}
if (dotDouble)
{
_videoWidth *= 2;
xskip = 2;
}
int size = _videoWidth * _videoHeight;
if (_videoBuffer.Length != size)
2017-04-19 16:40:41 +00:00
{
_videoBuffer = new int[size];
2017-04-19 16:40:41 +00:00
}
for (int j = 0; j < 2; j++)
{
2017-04-19 16:40:41 +00:00
if (j == 1 && !dotDouble)
{
break;
}
int xbonus = j;
for (int i = 0; i < 2; i++)
{
2017-04-19 16:40:41 +00:00
// potentially do this twice, if we need to line double
if (i == 1 && !lineDouble)
{
break;
}
int bonus = (i * _videoWidth) + xbonus;
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
2017-04-19 17:33:05 +00:00
int si = (y * srcPitch) + x + srcStart;
int di = y * _videoWidth * yskip + x * xskip + bonus;
int rgb = data[si];
_videoBuffer[di] = rgb;
}
}
}
}
}
2017-04-19 16:40:41 +00:00
private void RefreshMemoryCallbacks(bool suppress)
2013-11-12 02:34:56 +00:00
{
var mcs = MemoryCallbacks;
Api.QUERY_set_state_hook_exec(!suppress && mcs.HasExecutes);
Api.QUERY_set_state_hook_read(!suppress && mcs.HasReads);
Api.QUERY_set_state_hook_write(!suppress && mcs.HasWrites);
2013-11-12 02:34:56 +00:00
}
//public byte[] snes_get_memory_data_read(LibsnesApi.SNES_MEMORY id)
//{
// var size = (int)api.snes_get_memory_size(id);
// if (size == 0) return new byte[0];
// var ret = api.snes_get_memory_data(id);
// return ret;
//}
private void InitAudio()
{
_resampler = new SpeexResampler(6, 64081, 88200, 32041, 44100);
}
private void snes_audio_sample(ushort left, ushort right)
{
_resampler.EnqueueSample((short)left, (short)right);
}
private void RefreshPalette()
{
SetPalette((SnesColors.ColorType)Enum.Parse(typeof(SnesColors.ColorType), _settings.Palette, false));
}
}
}