using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Eto.Forms; using Eto; using Eto.Drawing; using BizHawk.Client.Common; using BizHawk.Emulation.Common; using System.Threading; using System.Runtime.InteropServices; using System.IO; using System.Reflection; using System.Diagnostics; using BizHawk.Emulation.Cores.Nintendo.NES; using BizHawk.Emulation.Cores.Consoles.Nintendo.QuickNES; using BizHawk.Emulation.Common.IEmulatorExtensions; namespace BizHawk.Client.EtoHawk { public partial class MainForm : Form { private Thread _worker; private bool _running; private readonly Throttle _throttle; private bool _unthrottled; private bool _runloopFrameProgress; private long _frameAdvanceTimestamp; private long _frameRewindTimestamp; private int _runloopFps; private int _runloopLastFps; private bool _runloopFrameadvance; private long _runloopSecond; private bool _runloopLastFf; private bool _inResizeLoop; private bool _suspended; //True if a modal dialog appears private readonly InputCoalescer HotkeyCoalescer = new InputCoalescer(); public MainForm() { InitBizHawk(); _throttle = new Throttle(); InitializeComponent(); _running = true; _worker = new Thread(ProgramRunLoop); _worker.Start(); } private void InitBizHawk() { GlobalWin.MainForm = this; Global.Rewinder = new Rewinder { //MessageCallback = GlobalWin.OSD.AddMessage }; Global.ControllerInputCoalescer = new ControllerInputCoalescer(); Global.FirmwareManager = new FirmwareManager(); Global.MovieSession = new MovieSession { Movie = MovieService.DefaultInstance, MovieControllerAdapter = MovieService.DefaultInstance.LogGeneratorInstance().MovieControllerAdapter, //MessageCallback = GlobalWin.OSD.AddMessage, //AskYesNoCallback = StateErrorAskUser, PauseCallback = PauseEmulator, //ModeChangedCallback = SetMainformMovieInfo }; //Icon = Properties.Resources.logo; Global.Game = GameInfo.NullInstance; string iniPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "config.ini"); Global.Config = ConfigService.Load(iniPath); Global.Config.ResolveDefaults(); Input.Initialize(); InitControls(); var comm = CreateCoreComm(); CoreFileProvider.SyncCoreCommInputSignals(comm); Global.Emulator = new NullEmulator(comm, Global.Config.GetCoreSettings()); Global.ActiveController = new Controller(NullController.Instance.Definition); Global.AutoFireController = new AutofireController(NullController.Instance.Definition, Global.Emulator); Global.AutofireStickyXORAdapter.SetOnOffPatternFromConfig(); Global.Config.SoundOutputMethod = Config.ESoundOutputMethod.OpenAL; //Temporary, remove later try { GlobalWin.Sound = new Sound(IntPtr.Zero); } catch { string message = "Couldn't initialize sound device! Try changing the output method in Sound config."; if (Global.Config.SoundOutputMethod == Config.ESoundOutputMethod.DirectSound) message = "Couldn't initialize DirectSound! Things may go poorly for you. Try changing your sound driver to 44.1khz instead of 48khz in mmsys.cpl."; MessageBox.Show(message, "Initialization Error", MessageBoxButtons.OK, MessageBoxType.Error); Global.Config.SoundOutputMethod = Config.ESoundOutputMethod.Dummy; GlobalWin.Sound = new Sound(IntPtr.Zero); } GlobalWin.Sound.StartSound(); InputManager.RewireInputChain(); /*GlobalWin.Tools = new ToolManager(this);*/ RewireSound(); GlobalWin.DisplayManager = new DisplayManager(); } private static void InitControls() { var controls = new Controller( new ControllerDefinition { Name = "Emulator Frontend Controls", BoolButtons = Global.Config.HotkeyBindings.Select(x => x.DisplayName).ToList() }); foreach (var b in Global.Config.HotkeyBindings) { controls.BindMulti(b.DisplayName, b.Bindings); } Global.ClientControls = controls; } private void RewireSound() { /*if (_dumpProxy != null) { // we're video dumping, so async mode only and use the DumpProxy. // note that the avi dumper has already rewired the emulator itself in this case. GlobalWin.Sound.SetAsyncInputPin(_dumpProxy); } else*/ { //Global.Emulator.EndAsyncSound(); //GlobalWin.Sound.SetSyncInputPin(Global.Emulator.SyncSoundProvider); } } public void ProgramRunLoop() { //CheckMessages(); //LogConsole.PositionConsole(); while (_running) { if (_suspended) { Thread.Sleep(33); continue; } Input.Instance.Update(); // handle events and dispatch as a hotkey action, or a hotkey button, or an input button ProcessInput(); Global.ClientControls.LatchFromPhysical(HotkeyCoalescer); Global.ActiveController.LatchFromPhysical(Global.ControllerInputCoalescer); /*Global.ActiveController.ApplyAxisConstraints( (Global.Emulator is N64 && Global.Config.N64UseCircularAnalogConstraint) ? "Natural Circle" : null);*/ Global.ActiveController.OR_FromLogical(Global.ClickyVirtualPadController); Global.AutoFireController.LatchFromPhysical(Global.ControllerInputCoalescer); if (Global.ClientControls["Autohold"]) { Global.StickyXORAdapter.MassToggleStickyState(Global.ActiveController.PressedButtons); Global.AutofireStickyXORAdapter.MassToggleStickyState(Global.AutoFireController.PressedButtons); } else if (Global.ClientControls["Autofire"]) { Global.AutofireStickyXORAdapter.MassToggleStickyState(Global.ActiveController.PressedButtons); } // autohold/autofire must not be affected by the following inputs Global.ActiveController.Overrides(Global.LuaAndAdaptor); if (Global.Config.DisplayInput) // Input display wants to update even while paused { GlobalWin.DisplayManager.NeedsToPaint = true; } StepRunLoop_Core(); StepRunLoop_Throttle(); if (GlobalWin.DisplayManager.NeedsToPaint) { Render(); } //CheckMessages(); Thread.Sleep(0); } } private void Render() { if (_running) { //Invalidate doesn't force the update, which may skip frames, but Update blocks and slows things down. //So invalidate wins for the moment, until we switch to OpenGL. Application.Instance.Invoke(new Action(() => _viewport.Invalidate())); GlobalWin.DisplayManager.NeedsToPaint = false; } } private void StepRunLoop_Throttle() { SyncThrottle(); _throttle.signal_frameAdvance = _runloopFrameadvance; _throttle.signal_continuousframeAdvancing = _runloopFrameProgress; _throttle.Step(true, -1); } public void ProcessInput() { ControllerInputCoalescer conInput = Global.ControllerInputCoalescer as ControllerInputCoalescer; for (; ; ) { // loop through all available events var ie = Input.Instance.DequeueEvent(); if (ie == null) { break; } // useful debugging: // Console.WriteLine(ie); // TODO - wonder what happens if we pop up something interactive as a response to one of these hotkeys? may need to purge further processing // look for hotkey bindings for this key var triggers = Global.ClientControls.SearchBindings(ie.LogicalButton.ToString()); /*if (triggers.Count == 0) { // Maybe it is a system alt-key which hasnt been overridden if (ie.EventType == Input.InputEventType.Press) { if (ie.LogicalButton.Alt && ie.LogicalButton.Button.Length == 1) { var c = ie.LogicalButton.Button.ToLower()[0]; if ((c >= 'a' && c <= 'z') || c == ' ') { SendAltKeyChar(c); } } if (ie.LogicalButton.Alt && ie.LogicalButton.Button == "Space") { SendPlainAltKey(32); } } // ordinarily, an alt release with nothing else would move focus to the menubar. but that is sort of useless, and hard to implement exactly right. }*/ // zero 09-sep-2012 - all input is eligible for controller input. not sure why the above was done. // maybe because it doesnt make sense to me to bind hotkeys and controller inputs to the same keystrokes // adelikat 02-dec-2012 - implemented options for how to handle controller vs hotkey conflicts. This is primarily motivated by computer emulation and thus controller being nearly the entire keyboard bool handled; switch (Global.Config.Input_Hotkey_OverrideOptions) { default: case 0: // Both allowed conInput.Receive(ie); handled = false; if (ie.EventType == Input.InputEventType.Press) { handled = triggers.Aggregate(handled, (current, trigger) => current | CheckHotkey(trigger)); } // hotkeys which arent handled as actions get coalesced as pollable virtual client buttons if (!handled) { //HotkeyCoalescer.Receive(ie); } break; case 1: // Input overrides Hokeys conInput.Receive(ie); if (!Global.ActiveController.HasBinding(ie.LogicalButton.ToString())) { handled = false; if (ie.EventType == Input.InputEventType.Press) { handled = triggers.Aggregate(handled, (current, trigger) => current | CheckHotkey(trigger)); } // hotkeys which arent handled as actions get coalesced as pollable virtual client buttons if (!handled) { //HotkeyCoalescer.Receive(ie); } } break; case 2: // Hotkeys override Input handled = false; if (ie.EventType == Input.InputEventType.Press) { handled = triggers.Aggregate(handled, (current, trigger) => current | CheckHotkey(trigger)); } // hotkeys which arent handled as actions get coalesced as pollable virtual client buttons if (!handled) { //HotkeyCoalescer.Receive(ie); conInput.Receive(ie); } break; } } // foreach event // also handle floats /*conInput.AcceptNewFloats(Input.Instance.GetFloats().Select(o => { var video = Global.Emulator.VideoProvider(); // hackish if (o.Item1 == "WMouse X") { var P = GlobalWin.DisplayManager.UntransformPoint(new Point((int)o.Item2, 0)); float x = P.X / (float)video.BufferWidth; return new Tuple("WMouse X", x * 20000 - 10000); } if (o.Item1 == "WMouse Y") { var P = GlobalWin.DisplayManager.UntransformPoint(new Point(0, (int)o.Item2)); float y = P.Y / (float)video.BufferHeight; return new Tuple("WMouse Y", y * 20000 - 10000); } return o; }));*/ } public bool IsLagFrame { get { /*if (Global.Emulator.CanPollInput()) { return Global.Emulator.AsInputPollable().IsLagFrame; }*/ return false; } } private void StepRunLoop_Core(bool force = false) { var runFrame = false; _runloopFrameadvance = false; var currentTimestamp = Stopwatch.GetTimestamp(); var suppressCaptureRewind = false; double frameAdvanceTimestampDeltaMs = (double)(currentTimestamp - _frameAdvanceTimestamp) / Stopwatch.Frequency * 1000.0; bool frameProgressTimeElapsed = frameAdvanceTimestampDeltaMs >= Global.Config.FrameProgressDelayMs; if (Global.Config.SkipLagFrame && IsLagFrame && frameProgressTimeElapsed && Global.Emulator.Frame > 0) { runFrame = true; } if (Global.ClientControls["Frame Advance"] || PressFrameAdvance) { // handle the initial trigger of a frame advance if (_frameAdvanceTimestamp == 0) { PauseEmulator(); runFrame = true; _runloopFrameadvance = true; _frameAdvanceTimestamp = currentTimestamp; } else { // handle the timed transition from countdown to FrameProgress if (frameProgressTimeElapsed) { runFrame = true; _runloopFrameProgress = true; UnpauseEmulator(); } } } else { // handle release of frame advance: do we need to deactivate FrameProgress? if (_runloopFrameProgress) { _runloopFrameProgress = false; PauseEmulator(); } _frameAdvanceTimestamp = 0; } if (!EmulatorPaused) { runFrame = true; } bool isRewinding = suppressCaptureRewind = false;// Rewind(ref runFrame, currentTimestamp); if (UpdateFrame) { runFrame = true; } var genSound = false; var coreskipaudio = false; if (runFrame || force) { var isFastForwarding = Global.ClientControls["Fast Forward"] || IsTurboing; var updateFpsString = _runloopLastFf != isFastForwarding; _runloopLastFf = isFastForwarding; // client input-related duties //GlobalWin.OSD.ClearGUIText(); //Global.CheatList.Pulse(); //zero 03-may-2014 - moved this before call to UpdateToolsBefore(), since it seems to clear the state which a lua event.framestart is going to want to alter Global.ClickyVirtualPadController.FrameTick(); Global.LuaAndAdaptor.FrameTick(); /*if (GlobalWin.Tools.Has()) { GlobalWin.Tools.LuaConsole.LuaImp.CallFrameBeforeEvent(); }*/ /*if (!IsTurboing) { GlobalWin.Tools.UpdateToolsBefore(); } else { GlobalWin.Tools.FastUpdateBefore(); }*/ _runloopFps++; if ((double)(currentTimestamp - _runloopSecond) / Stopwatch.Frequency >= 1.0) { _runloopLastFps = _runloopFps; _runloopSecond = currentTimestamp; _runloopFps = 0; updateFpsString = true; } if (updateFpsString) { var fps_string = _runloopLastFps + " fps"; if (isRewinding) { if (IsTurboing || isFastForwarding) { fps_string += " <<<<"; } else { fps_string += " <<"; } } else if (IsTurboing) { fps_string += " >>>>"; } else if (isFastForwarding) { fps_string += " >>"; } //GlobalWin.OSD.FPS = fps_string; } //CaptureRewind(suppressCaptureRewind); if (!_runloopFrameadvance) { genSound = true; } else if (!Global.Config.MuteFrameAdvance) { genSound = true; } Global.MovieSession.HandleMovieOnFrameLoop(); coreskipaudio = IsTurboing;// && _currAviWriter == null; { bool render = !_throttle.skipnextframe;// || _currAviWriter != null; bool renderSound = !coreskipaudio; Global.Emulator.FrameAdvance(Global.ActiveController, render, renderSound); } Global.MovieSession.HandleMovieAfterFrameLoop(); GlobalWin.DisplayManager.NeedsToPaint = true; //Global.CheatList.Pulse(); /*if (!PauseAVI) { AvFrameAdvance(); }*/ if (IsLagFrame && Global.Config.AutofireLagFrames) { Global.AutoFireController.IncrementStarts(); } Global.AutofireStickyXORAdapter.IncrementLoops(IsLagFrame); PressFrameAdvance = false; /*if (GlobalWin.Tools.Has()) { GlobalWin.Tools.LuaConsole.LuaImp.CallFrameAfterEvent(); }*/ /*if (!IsTurboing) { UpdateToolsAfter(); } else { GlobalWin.Tools.FastUpdateAfter(); } if (IsSeeking && Global.Emulator.Frame == PauseOnFrame.Value) { PauseEmulator(); PauseOnFrame = null; }*/ } if (Global.ClientControls["Rewind"] || PressRewind) { //UpdateToolsAfter(); PressRewind = false; } if (UpdateFrame) { UpdateFrame = false; } bool outputSilence = !genSound || coreskipaudio; GlobalWin.Sound.UpdateSound(0.5f); } private void SyncThrottle() { // "unthrottled" = throttle was turned off with "Toggle Throttle" hotkey // "turbo" = throttle is off due to the "Turbo" hotkey being held // They are basically the same thing but one is a toggle and the other requires a // hotkey to be held. There is however slightly different behavior in that turbo // skips outputting the audio. There's also a third way which is when no throttle // method is selected, but the clock throttle determines that by itself and // everything appears normal here. var rewind = Global.Rewinder.RewindActive && (Global.ClientControls["Rewind"] || PressRewind); var fastForward = Global.ClientControls["Fast Forward"] || FastForward; var turbo = IsTurboing; int speedPercent = fastForward ? Global.Config.SpeedPercentAlternate : Global.Config.SpeedPercent; if (rewind) { speedPercent = Math.Max(speedPercent * Global.Config.RewindSpeedMultiplier / Global.Rewinder.RewindFrequency, 5); } Global.DisableSecondaryThrottling = _unthrottled || turbo || fastForward || rewind; // realtime throttle is never going to be so exact that using a double here is wrong _throttle.SetCoreFps(60.0); _throttle.signal_paused = EmulatorPaused; _throttle.signal_unthrottle = _unthrottled || turbo; _throttle.signal_overrideSecondaryThrottle = (fastForward || rewind) && (Global.Config.SoundThrottle || Global.Config.VSyncThrottle || Global.Config.VSync); _throttle.SetSpeedPercent(speedPercent); } public string CurrentlyOpenRom; public bool PauseAVI = false; public bool PressFrameAdvance = false; public bool PressRewind = false; public bool FastForward = false; public bool TurboFastForward = false; public bool RestoreReadWriteOnStop = false; public bool UpdateFrame = false; public bool AllowInput = true; private int? _pauseOnFrame; public int? PauseOnFrame // If set, upon completion of this frame, the client wil pause { get { return _pauseOnFrame; } set { _pauseOnFrame = value; //SetPauseStatusbarIcon(); if (value == null) // TODO: make an Event handler instead, but the logic here is that after turbo seeking, tools will want to do a real update when the emulator finally pauses { //GlobalWin.Tools.UpdateToolsBefore(); //GlobalWin.Tools.UpdateToolsAfter(); } } } public bool IsSeeking { get { return PauseOnFrame.HasValue; } } public bool IsTurboSeeking { get { return PauseOnFrame.HasValue && Global.Config.TurboSeek; } } public bool IsTurboing { get { return Global.ClientControls["Turbo"] || IsTurboSeeking; } } #region Pause private bool _emulatorPaused; public bool EmulatorPaused { get { return _emulatorPaused; } private set { _emulatorPaused = value; if (OnPauseChanged != null) { OnPauseChanged(this, new PauseChangedEventArgs(_emulatorPaused)); } } } public delegate void PauseChangedEventHandler(object sender, PauseChangedEventArgs e); public event PauseChangedEventHandler OnPauseChanged; public class PauseChangedEventArgs : EventArgs { public PauseChangedEventArgs(bool paused) { Paused = paused; } public bool Paused { get; private set; } } public void PauseEmulator() { EmulatorPaused = true; //SetPauseStatusbarIcon(); } public void UnpauseEmulator() { EmulatorPaused = false; //SetPauseStatusbarIcon(); } public void TogglePause() { EmulatorPaused ^= true; //SetPauseStatusbarIcon(); // TODO: have tastudio set a pause status change callback, or take control over pause /*if (GlobalWin.Tools.Has()) { GlobalWin.Tools.UpdateValues(); }*/ } #endregion private void MainForm_Closed(object sender, EventArgs e) { if (_running) { Shutdown(); } } private void Shutdown() { _running = false; _worker.Join(5000); Application.Instance.Quit(); } private void viewport_Paint(object sender, PaintEventArgs e) { if (Global.Emulator != null) { var video = Global.Emulator.AsVideoProviderOrDefault(); Bitmap img = new Bitmap(video.BufferWidth, video.BufferHeight, PixelFormat.Format32bppRgb); BitmapData data = img.Lock(); int[] buffer = (int[])(video.GetVideoBuffer().Clone()); //Buffer is cloned to prevent tearing. The emulation thread is running independent of drawing, //does not block, can (and will) update the framebuffer while we draw it. if (img.Platform.IsMac) { //Colors are reversed on OSX, even though it's Little Endian just like on Windows. //I think this is a bug in Eto framework. This hack won't be needed when I bring back OpenGL, or if Eto gets fixed. for (int i = 0; i < buffer.Length; i++) { int x = buffer [i]; x = (x >> 16 & 0xFF) + (x & 0xFF00) + ((x << 16) & 0xFF0000); buffer[i] = x; } } Marshal.Copy(buffer, 0, data.Data, buffer.Length); data.Dispose(); e.Graphics.DrawImage(img, 0, 0, _viewport.Width, _viewport.Height); } else { e.Graphics.FillRectangle(Brushes.Black, new RectangleF(0, 0, _viewport.Width, _viewport.Height)); } } private void OpenRom() { OpenFileDialog ofd = new OpenFileDialog(); _suspended = true; if (ofd.ShowDialog(this) == DialogResult.Ok) { _suspended = false; RomLoader loader = new RomLoader(); var nextComm = CreateCoreComm(); CoreFileProvider.SyncCoreCommInputSignals(nextComm); bool result = loader.LoadRom(ofd.FileName, nextComm); if (result) { Global.Emulator = loader.LoadedEmulator; Global.Game = loader.Game; CoreFileProvider.SyncCoreCommInputSignals(nextComm); InputManager.SyncControls(); /*if (Global.Emulator is TI83 && Global.Config.TI83autoloadKeyPad) { GlobalWin.Tools.Load(); }*/ if (loader.LoadedEmulator is NES) { var nes = loader.LoadedEmulator as NES; if (!string.IsNullOrWhiteSpace(nes.GameName)) { Global.Game.Name = nes.GameName; } Global.Game.Status = nes.RomStatus; } else if (loader.LoadedEmulator is QuickNES) { var qns = loader.LoadedEmulator as QuickNES; if (!string.IsNullOrWhiteSpace(qns.BootGodName)) { Global.Game.Name = qns.BootGodName; } if (qns.BootGodStatus.HasValue) { Global.Game.Status = qns.BootGodStatus.Value; } } //Global.Rewinder.ResetRewindBuffer(); /*if (Global.Emulator.CoreComm.RomStatusDetails == null && loader.Rom != null) { Global.Emulator.CoreComm.RomStatusDetails = string.Format( "{0}\r\nSHA1:{1}\r\nMD5:{2}\r\n", loader.Game.Name, loader.Rom.RomData.HashSHA1(), loader.Rom.RomData.HashMD5()); }*/ /*if (Global.Emulator.BoardName != null) { Console.WriteLine("Core reported BoardID: \"{0}\"", Global.Emulator.BoardName); }*/ // restarts the lua console if a different rom is loaded. // im not really a fan of how this is done.. /*if (Global.Config.RecentRoms.Empty || Global.Config.RecentRoms.MostRecent != loader.CanonicalFullPath) { GlobalWin.Tools.Restart(); }*/ Global.Config.RecentRoms.Add(loader.CanonicalFullPath); //JumpLists.AddRecentItem(loader.CanonicalFullPath); // Don't load Save Ram if a movie is being loaded /*if (!Global.MovieSession.MovieIsQueued && File.Exists(PathManager.SaveRamPath(loader.Game))) { LoadSaveRam(); } GlobalWin.Tools.Restart(); if (Global.Config.LoadCheatFileByGame) { if (Global.CheatList.AttemptToLoadCheatFile()) { GlobalWin.OSD.AddMessage("Cheats file loaded"); } } SetWindowText(); CurrentlyOpenRom = loader.CanonicalFullPath; HandlePlatformMenus(); _stateSlots.Clear(); UpdateCoreStatusBarButton(); UpdateDumpIcon(); SetMainformMovieInfo(); */ //Global.Rewinder.CaptureRewindState(); Global.StickyXORAdapter.ClearStickies(); Global.StickyXORAdapter.ClearStickyFloats(); Global.AutofireStickyXORAdapter.ClearStickies(); RewireSound(); /*ToolHelpers.UpdateCheatRelatedTools(null, null); if (Global.Config.AutoLoadLastSaveSlot && _stateSlots.HasSlot(Global.Config.SaveSlot)) { LoadQuickSave("QuickSave" + Global.Config.SaveSlot); } if (Global.FirmwareManager.RecentlyServed.Count > 0) { Console.WriteLine("Active Firmwares:"); foreach (var f in Global.FirmwareManager.RecentlyServed) { Console.WriteLine(" {0} : {1}", f.FirmwareId, f.Hash); } }*/ EnableControls(); //return true; } } _suspended = false; } CoreComm CreateCoreComm() { CoreComm ret = new CoreComm(ShowMessageCoreComm, NotifyCoreComm); //ret.RequestGLContext = () => GlobalWin.GLManager.CreateGLContext(); //ret.ActivateGLContext = (gl) => GlobalWin.GLManager.Activate((GLManager.ContextRef)gl); //ret.DeactivateGLContext = () => GlobalWin.GLManager.Deactivate(); return ret; } private void ShowMessageCoreComm(string message) { MessageBox.Show(Application.Instance.MainForm, message, "Warning", MessageBoxButtons.OK); } private void NotifyCoreComm(string message) { //GlobalWin.OSD.AddMessage(message); } } }