diff --git a/BizHawk.Client.Common/RomLoader.cs b/BizHawk.Client.Common/RomLoader.cs index 57348573ba..03a63e22bd 100644 --- a/BizHawk.Client.Common/RomLoader.cs +++ b/BizHawk.Client.Common/RomLoader.cs @@ -14,6 +14,7 @@ using BizHawk.Emulation.Cores.Nintendo.Gameboy; using BizHawk.Emulation.Cores.Nintendo.GBA; using BizHawk.Emulation.Cores.Nintendo.N64; using BizHawk.Emulation.Cores.Nintendo.NES; +using BizHawk.Emulation.Cores.Consoles.Nintendo.QuickNES; using BizHawk.Emulation.Cores.Nintendo.SNES; using BizHawk.Emulation.Cores.PCEngine; using BizHawk.Emulation.Cores.Sega.MasterSystem; @@ -350,12 +351,19 @@ namespace BizHawk.Client.Common nextEmulator = new TI83(nextComm, game, rom.RomData); break; case "NES": - nextEmulator = new NES( - nextComm, - game, - rom.FileData, - GetCoreSettings(), - GetCoreSyncSettings()); + if (false) + { + nextEmulator = new NES( + nextComm, + game, + rom.FileData, + GetCoreSettings(), + GetCoreSyncSettings()); + } + else + { + nextEmulator = new QuickNES(nextComm, rom.FileData); + } break; case "GB": case "GBC": diff --git a/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj b/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj index b8f7f9f388..f502281ff3 100644 --- a/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj +++ b/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj @@ -392,6 +392,8 @@ + + diff --git a/BizHawk.Emulation.Cores/Consoles/Nintendo/QuickNES/LibQuickNES.cs b/BizHawk.Emulation.Cores/Consoles/Nintendo/QuickNES/LibQuickNES.cs index b5c1c7a4f2..909e793572 100644 --- a/BizHawk.Emulation.Cores/Consoles/Nintendo/QuickNES/LibQuickNES.cs +++ b/BizHawk.Emulation.Cores/Consoles/Nintendo/QuickNES/LibQuickNES.cs @@ -54,7 +54,7 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.QuickNES /// 8bpp, at least as big as qn_get_image_dimensions() /// byte pitch [DllImport(dllname, CallingConvention = CallingConvention.Cdecl)] - public static extern void qn_set_pixels(IntPtr e, byte[] dest, int pitch); + public static extern void qn_set_pixels(IntPtr e, IntPtr dest, int pitch); /// /// emulate a single frame /// @@ -65,6 +65,13 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.QuickNES [DllImport(dllname, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr qn_emulate_frame(IntPtr e, int pad1, int pad2); /// + /// blit to rgb32 + /// + /// Context + /// rgb32 256x240 packed + [DllImport(dllname, CallingConvention = CallingConvention.Cdecl)] + public static extern void qn_blit(IntPtr e, IntPtr dest); + /// /// get number of times joypad was read in most recent frame /// /// context diff --git a/BizHawk.Emulation.Cores/Consoles/Nintendo/QuickNES/QuickNES.cs b/BizHawk.Emulation.Cores/Consoles/Nintendo/QuickNES/QuickNES.cs new file mode 100644 index 0000000000..0e342e6fd8 --- /dev/null +++ b/BizHawk.Emulation.Cores/Consoles/Nintendo/QuickNES/QuickNES.cs @@ -0,0 +1,362 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using BizHawk.Emulation.Common; +using System.Runtime.InteropServices; + +namespace BizHawk.Emulation.Cores.Consoles.Nintendo.QuickNES +{ + public class QuickNES : IEmulator, IVideoProvider, ISyncSoundProvider + { + public QuickNES(CoreComm nextComm, byte[] Rom) + { + CoreComm = nextComm; + + Context = LibQuickNES.qn_new(); + if (Context == IntPtr.Zero) + throw new InvalidOperationException("qn_new() returned NULL"); + try + { + LibQuickNES.ThrowStringError(LibQuickNES.qn_loadines(Context, Rom, Rom.Length)); + + InitSaveRamBuff(); + InitSaveStateBuff(); + InitVideo(); + InitAudio(); + } + catch + { + Dispose(); + throw; + } + } + + #region Controller + + public ControllerDefinition ControllerDefinition { get { return Emulation.Cores.Nintendo.NES.NES.NESController; } } + public IController Controller { get; set; } + + void SetPads(out int j1, out int j2) + { + j1 = 0; + j2 = 0; + if (Controller["P1 A"]) + j1 |= 1; + if (Controller["P1 B"]) + j1 |= 2; + if (Controller["P1 Select"]) + j1 |= 4; + if (Controller["P1 Start"]) + j1 |= 8; + if (Controller["P1 Up"]) + j1 |= 16; + if (Controller["P1 Down"]) + j1 |= 32; + if (Controller["P1 Left"]) + j1 |= 64; + if (Controller["P1 Right"]) + j1 |= 128; + if (Controller["P2 A"]) + j2 |= 1; + if (Controller["P2 B"]) + j2 |= 2; + if (Controller["P2 Select"]) + j2 |= 4; + if (Controller["P2 Start"]) + j2 |= 8; + if (Controller["P2 Up"]) + j2 |= 16; + if (Controller["P2 Down"]) + j2 |= 32; + if (Controller["P2 Left"]) + j2 |= 64; + if (Controller["P2 Right"]) + j2 |= 128; + } + + #endregion + + public void FrameAdvance(bool render, bool rendersound = true) + { + if (Controller["Power"]) + LibQuickNES.qn_reset(Context, true); + if (Controller["Reset"]) + LibQuickNES.qn_reset(Context, false); + + int j1, j2; + SetPads(out j1, out j2); + + Frame++; + LibQuickNES.ThrowStringError(LibQuickNES.qn_emulate_frame(Context, j1, j2)); + IsLagFrame = LibQuickNES.qn_get_joypad_read_count(Context) == 0; + if (IsLagFrame) + LagCount++; + + Blit(); + DrainAudio(); + } + + #region state + + IntPtr Context; + + public int Frame { get; private set; } + public int LagCount { get; set; } + public bool IsLagFrame { get; private set; } + + #endregion + + public string SystemId { get { return "NES"; } } + public bool DeterministicEmulation { get { return true; } } + public string BoardName { get { return null; } } // TODO + + #region saveram + + byte[] SaveRamBuff; + + void InitSaveRamBuff() + { + int size = 0; + LibQuickNES.ThrowStringError(LibQuickNES.qn_battery_ram_size(Context, ref size)); + SaveRamBuff = new byte[size]; + } + + public byte[] ReadSaveRam() + { + LibQuickNES.ThrowStringError(LibQuickNES.qn_battery_ram_save(Context, SaveRamBuff, SaveRamBuff.Length)); + return SaveRamBuff; + } + + public void StoreSaveRam(byte[] data) + { + LibQuickNES.ThrowStringError(LibQuickNES.qn_battery_ram_load(Context, data, data.Length)); + } + + public void ClearSaveRam() + { + LibQuickNES.ThrowStringError(LibQuickNES.qn_battery_ram_clear(Context)); + } + + public bool SaveRamModified + { + get + { + return LibQuickNES.qn_has_battery_ram(Context); + } + set + { + throw new Exception(); + } + } + + #endregion + + public void ResetCounters() + { + Frame = 0; + IsLagFrame = false; + LagCount = 0; + } + + #region savestates + + byte[] SaveStateBuff; + byte[] SaveStateBuff2; + + void InitSaveStateBuff() + { + int size = 0; + LibQuickNES.ThrowStringError(LibQuickNES.qn_state_size(Context, ref size)); + SaveStateBuff = new byte[size]; + SaveStateBuff2 = new byte[size + 13]; + } + + public void SaveStateText(System.IO.TextWriter writer) + { + throw new NotImplementedException(); + } + + public void LoadStateText(System.IO.TextReader reader) + { + throw new NotImplementedException(); + } + + public void SaveStateBinary(System.IO.BinaryWriter writer) + { + LibQuickNES.ThrowStringError(LibQuickNES.qn_state_save(Context, SaveStateBuff, SaveStateBuff.Length)); + writer.Write(SaveStateBuff.Length); + writer.Write(SaveStateBuff); + // other variables + writer.Write(IsLagFrame); + writer.Write(LagCount); + writer.Write(Frame); + } + + public void LoadStateBinary(System.IO.BinaryReader reader) + { + int len = reader.ReadInt32(); + if (len != SaveStateBuff.Length) + throw new InvalidOperationException("Unexpected savestate buffer length!"); + reader.Read(SaveStateBuff, 0, SaveStateBuff.Length); + LibQuickNES.ThrowStringError(LibQuickNES.qn_state_load(Context, SaveStateBuff, SaveStateBuff.Length)); + // other variables + IsLagFrame = reader.ReadBoolean(); + LagCount = reader.ReadInt32(); + Frame = reader.ReadInt32(); + } + + public byte[] SaveStateBinary() + { + var ms = new System.IO.MemoryStream(SaveStateBuff2, true); + var bw = new System.IO.BinaryWriter(ms); + SaveStateBinary(bw); + bw.Flush(); + if (ms.Position != SaveStateBuff2.Length) + throw new InvalidOperationException("Unexpected savestate length!"); + bw.Close(); + return SaveStateBuff2; + } + + public bool BinarySaveStatesPreferred { get { return true; } } + + #endregion + + public CoreComm CoreComm + { + get; + private set; + } + + public MemoryDomainList MemoryDomains + { + get { return MemoryDomainList.GetDummyList(); } + } + + public List> GetCpuFlagsAndRegisters() + { + return new List>(); + } + + #region settings + + public object GetSettings() + { + return null; + } + + public object GetSyncSettings() + { + return null; + } + + public bool PutSettings(object o) + { + return false; + } + + public bool PutSyncSettings(object o) + { + return false; + } + + #endregion + + public void Dispose() + { + if (Context != IntPtr.Zero) + { + LibQuickNES.qn_delete(Context); + Context = IntPtr.Zero; + } + if (VideoInput != null) + { + VideoInputH.Free(); + VideoInput = null; + } + if (VideoOutput != null) + { + VideoOutputH.Free(); + VideoOutput = null; + } + } + + #region VideoProvider + + int[] VideoOutput; + byte[] VideoInput; + GCHandle VideoInputH; + GCHandle VideoOutputH; + + void InitVideo() + { + int w = 0, h = 0; + LibQuickNES.qn_get_image_dimensions(Context, ref w, ref h); + VideoInput = new byte[w * h]; + VideoInputH = GCHandle.Alloc(VideoInput, GCHandleType.Pinned); + LibQuickNES.qn_set_pixels(Context, VideoInputH.AddrOfPinnedObject(), w); + VideoOutput = new int[256 * 240]; + VideoOutputH = GCHandle.Alloc(VideoOutput, GCHandleType.Pinned); + } + + void Blit() + { + LibQuickNES.qn_blit(Context, VideoOutputH.AddrOfPinnedObject()); + } + + public IVideoProvider VideoProvider { get { return this; } } + public int[] GetVideoBuffer() { return VideoOutput; } + public int VirtualWidth { get { return 292; } } // probably different on pal + public int BufferWidth { get { return 256; } } + public int BufferHeight { get { return 240; } } + public int BackgroundColor { get { return unchecked((int)0xff000000); } } + + #endregion + + #region SoundProvider + + public ISoundProvider SoundProvider { get { return null; } } + public ISyncSoundProvider SyncSoundProvider { get { return this; } } + public bool StartAsyncSound() { return false; } + public void EndAsyncSound() { } + + void InitAudio() + { + LibQuickNES.ThrowStringError(LibQuickNES.qn_set_sample_rate(Context, 44100)); + } + + void DrainAudio() + { + NumSamples = LibQuickNES.qn_read_audio(Context, MonoBuff, 1024); + unsafe + { + fixed (short *_src = &MonoBuff[0], _dst = &StereoBuff[0]) + { + short* src = _src; + short* dst = _dst; + for (int i = 0; i < NumSamples; i++) + { + *dst++ = *src; + *dst++ = *src++; + } + } + } + } + + short[] MonoBuff = new short[1024]; + short[] StereoBuff = new short[2048]; + int NumSamples = 0; + + public void GetSamples(out short[] samples, out int nsamp) + { + samples = StereoBuff; + nsamp = NumSamples; + } + + public void DiscardSamples() + { + } + + #endregion + } +} diff --git a/output/dll/libquicknes.dll b/output/dll/libquicknes.dll new file mode 100644 index 0000000000..1e9392ab7b Binary files /dev/null and b/output/dll/libquicknes.dll differ diff --git a/quicknes/bizinterface.cpp b/quicknes/bizinterface.cpp index 23207a48c5..2f3b29aaf5 100644 --- a/quicknes/bizinterface.cpp +++ b/quicknes/bizinterface.cpp @@ -59,6 +59,31 @@ EXPORT const char *qn_emulate_frame(Nes_Emu *e, int pad1, int pad2) return e->emulate_frame(pad1, pad2); } +EXPORT void qn_blit(Nes_Emu *e, char *dest) +{ + // what is the point of the 256 color bitmap and the dynamic color allocation to it? + // why not just render directly to a 512 color bitmap with static palette positions? + + const char *src = (const char *)e->frame().pixels; + const int srcpitch = e->frame().pitch; + const char *srcend = src + e->image_height * srcpitch; + + const short *lut = e->frame().palette; + const Nes_Emu::rgb_t *colors = e->nes_colors; + + for (; src < srcend; src += srcpitch) + { + for (int i = 0; i < 256; i++) + { + const Nes_Emu::rgb_t *c = colors + lut[src[i]]; + *dest++ = c->blue; + *dest++ = c->green; + *dest++ = c->red; + *dest++ = 0xff; + } + } +} + EXPORT int qn_get_joypad_read_count(Nes_Emu *e) { return e->frame().joypad_read_count; diff --git a/quicknes/libquicknes/libquicknes.vcxproj b/quicknes/libquicknes/libquicknes.vcxproj index 63d052808a..e07d809c69 100644 --- a/quicknes/libquicknes/libquicknes.vcxproj +++ b/quicknes/libquicknes/libquicknes.vcxproj @@ -86,7 +86,7 @@ Disabled 4244;4800;4804;4996 $(ProjectDir)\.. - _WINDLL;%(PreprocessorDefinitions);DISABLE_AUTO_FILE + _WINDLL;%(PreprocessorDefinitions);DISABLE_AUTO_FILE;__LIBRETRO__ true @@ -103,7 +103,7 @@ true 4244;4800;4804;4996 $(ProjectDir)\.. - _WINDLL;%(PreprocessorDefinitions);DISABLE_AUTO_FILE + _WINDLL;%(PreprocessorDefinitions);DISABLE_AUTO_FILE;__LIBRETRO__ true