using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Windows.Forms;
using System.Security.AccessControl;
using System.Security.Principal;
using System.IO.Pipes;

using BizHawk.Common;
using BizHawk.Common.BufferExtensions;

using BizHawk.Client.Common;
using BizHawk.Bizware.BizwareGL;

using BizHawk.Emulation.Common;
using BizHawk.Emulation.Cores;
using BizHawk.Emulation.Cores.Consoles.Nintendo.QuickNES;
using BizHawk.Emulation.Cores.Nintendo.GBA;
using BizHawk.Emulation.Cores.Nintendo.NES;
using BizHawk.Emulation.Cores.Nintendo.SNES;
using BizHawk.Emulation.Cores.Nintendo.N64;
using BizHawk.Emulation.Cores.Nintendo.GBHawkLink;

using BizHawk.Client.EmuHawk.ToolExtensions;
using BizHawk.Client.EmuHawk.CoreExtensions;
using BizHawk.Client.EmuHawk.CustomControls;
using BizHawk.Common.PathExtensions;
using BizHawk.Common.StringExtensions;
using BizHawk.Emulation.Common.Base_Implementations;
using BizHawk.Emulation.Cores.Consoles.NEC.PCE;
using BizHawk.Emulation.Cores.Nintendo.SNES9X;
using BizHawk.Emulation.Cores.Consoles.SNK;
using BizHawk.Emulation.Cores.Consoles.Nintendo.Gameboy;
using BizHawk.Emulation.Cores.Consoles.Nintendo.Faust;

namespace BizHawk.Client.EmuHawk
{
	public partial class MainForm : FormBase, IDialogParent, IMainFormForApi, IMainFormForConfig, IMainFormForTools
	{
		/// <remarks><c>AppliesTo[0]</c> is used as the group label, and <c>Config.PreferredCores[AppliesTo[0]]</c> determines the currently selected option</remarks>
		private static readonly IReadOnlyCollection<(string[] AppliesTo, string[] CoreNames)> CoreData = new List<(string[], string[])> {
			(new[] { "NES" }, new[] { CoreNames.QuickNes, CoreNames.NesHawk, CoreNames.SubNesHawk }),
			(new[] { "SNES" }, new[] { CoreNames.Faust, CoreNames.Snes9X, CoreNames.Bsnes }),
			(new[] { "SGB" }, new[] { CoreNames.SameBoy, CoreNames.Bsnes }),
			(new[] { "GB", "GBC" }, new[] { CoreNames.Gambatte, CoreNames.GbHawk, CoreNames.SubGbHawk }),
			(new[] { "DGB" }, new[] { CoreNames.DualGambatte, CoreNames.GBHawkLink }),
			(new[] { "PCE", "PCECD", "SGX" }, new[] { CoreNames.TurboNyma, CoreNames.HyperNyma, CoreNames.PceHawk })
		};

		private void MainForm_Load(object sender, EventArgs e)
		{
			SetWindowText();

			foreach (var (appliesTo, coreNames) in CoreData)
			{
				var groupLabel = appliesTo[0];
				var submenu = new ToolStripMenuItem { Text = groupLabel };
				void ClickHandler(object clickSender, EventArgs clickArgs)
				{
					var coreName = ((ToolStripMenuItem) clickSender).Text;
					foreach (var system in appliesTo) Config.PreferredCores[system] = coreName;
					if (appliesTo.Contains(Emulator.SystemId)) FlagNeedsReboot(); //TODO don't alert if the loaded core was the one selected
				}
				submenu.DropDownItems.AddRange(coreNames.Select(coreName => {
					var entry = new ToolStripMenuItem { Text = coreName };
					entry.Click += ClickHandler;
					return (ToolStripItem) entry;
				}).ToArray());
				submenu.DropDownOpened += (openedSender, openedArgs) =>
				{
					Config.PreferredCores.TryGetValue(groupLabel, out var preferred);
					foreach (ToolStripMenuItem entry in ((ToolStripMenuItem) openedSender).DropDownItems) entry.Checked = entry.Text == preferred;
				};
				CoresSubMenu.DropDownItems.Add(submenu);
			}

			var GBInSGBMenuItem = new ToolStripMenuItem { Text = "GB in SGB" };
			GBInSGBMenuItem.Click += (clickSender, clickArgs) =>
			{
				Config.GbAsSgb ^= true;
				if (!Emulator.IsNull()) FlagNeedsReboot(); //TODO only alert if a GB or SGB core is loaded
			};
			var N64VideoPluginSettingsMenuItem = new ToolStripMenuItem { Image = Properties.Resources.Monitor, Text = "N64 Video Plugin Settings" };
			N64VideoPluginSettingsMenuItem.Click += N64PluginSettingsMenuItem_Click;
			var setLibretroCoreToolStripMenuItem = new ToolStripMenuItem { Text = "Set Libretro Core" };
			setLibretroCoreToolStripMenuItem.Click += (clickSender, clickArgs) => RunLibretroCoreChooser();
			CoresSubMenu.DropDownItems.AddRange(new ToolStripItem[] {
				GBInSGBMenuItem,
				new ToolStripSeparator { AutoSize = true },
				N64VideoPluginSettingsMenuItem,
				setLibretroCoreToolStripMenuItem
			});
			CoresSubMenu.DropDownOpened += (openedSender, openedArgs) => GBInSGBMenuItem.Checked = Config.GbAsSgb;

			// Hide Status bar icons and general StatusBar prep
			MainStatusBar.Padding = new Padding(MainStatusBar.Padding.Left, MainStatusBar.Padding.Top, MainStatusBar.Padding.Left, MainStatusBar.Padding.Bottom); // Workaround to remove extra padding on right
			PlayRecordStatusButton.Visible = false;
			AVIStatusLabel.Visible = false;
			SetPauseStatusBarIcon();
			Tools.UpdateCheatRelatedTools(null, null);
			RebootStatusBarIcon.Visible = false;
			UpdateNotification.Visible = false;
			_statusBarDiskLightOnImage = Properties.Resources.LightOn;
			_statusBarDiskLightOffImage = Properties.Resources.LightOff;
			_linkCableOn = Properties.Resources.Connect16X16;
			_linkCableOff = Properties.Resources.NoConnect16X16;
			UpdateCoreStatusBarButton();
			if (Config.FirstBoot)
			{
				ProfileFirstBootLabel.Visible = true;
			}

			HandleToggleLightAndLink();
			SetStatusBar();
			_stateSlots.Update(Emulator, MovieSession.Movie, SaveStatePrefix());

			// New version notification
			UpdateChecker.CheckComplete += (s2, e2) =>
			{
				if (IsDisposed)
				{
					return;
				}

				this.BeginInvoke(() => { UpdateNotification.Visible = UpdateChecker.IsNewVersionAvailable; });
			};
			UpdateChecker.GlobalConfig = Config;
			UpdateChecker.BeginCheck(); // Won't actually check unless enabled by user

			// open requested ext. tool
			var requestedExtToolDll = _argParser.openExtToolDll;
			if (requestedExtToolDll != null)
			{
				var enabled = ExtToolManager.ToolStripMenu.Where(item => item.Enabled)
					.Select(item => ((string, string)) item.Tag)
					.ToList();
				try
				{
					var found = enabled.First(tuple => tuple.Item1 == requestedExtToolDll
						|| Path.GetFileName(tuple.Item1) == requestedExtToolDll
						|| Path.GetFileNameWithoutExtension(tuple.Item1) == requestedExtToolDll);
					Tools.LoadExternalToolForm(found.Item1, found.Item2, skipExtToolWarning: true);
				}
				catch (Exception)
				{
					Console.WriteLine($"requested ext. tool dll {requestedExtToolDll} could not be loaded");
				}
			}
		}

		static MainForm()
		{
			// If this isn't here, then our assembly resolving hacks wont work due to the check for MainForm.INTERIM
			// its.. weird. don't ask.
		}

		public CoreComm CreateCoreComm()
		{
			var cfp = new CoreFileProvider(
				this,
				FirmwareManager,
				Config.PathEntries,
				Config.FirmwareUserSpecifications);
			var prefs = CoreComm.CorePreferencesFlags.None;
			if (Config.SkipWaterboxIntegrityChecks)
				prefs = CoreComm.CorePreferencesFlags.WaterboxMemoryConsistencyCheck;

			return new CoreComm(
				message => this.ModalMessageBox(message, "Warning", EMsgBoxIcon.Warning),
				AddOnScreenMessage,
				cfp,
				prefs);
		}

		private void SetImages()
		{
			OpenRomMenuItem.Image = Properties.Resources.OpenFile;
			RecentRomSubMenu.Image = Properties.Resources.Recent;
			CloseRomMenuItem.Image = Properties.Resources.Close;
			PreviousSlotMenuItem.Image = Properties.Resources.MoveLeft;
			NextSlotMenuItem.Image = Properties.Resources.MoveRight;
			ReadonlyMenuItem.Image = Properties.Resources.ReadOnly;
			RecentMovieSubMenu.Image = Properties.Resources.Recent;
			RecordMovieMenuItem.Image = Properties.Resources.Record;
			PlayMovieMenuItem.Image = Properties.Resources.Play;
			StopMovieMenuItem.Image = Properties.Resources.Stop;
			PlayFromBeginningMenuItem.Image = Properties.Resources.Restart;
			ImportMoviesMenuItem.Image = Properties.Resources.Import;
			SaveMovieMenuItem.Image = Properties.Resources.SaveAs;
			SaveMovieAsMenuItem.Image = Properties.Resources.SaveAs;
			StopMovieWithoutSavingMenuItem.Image = Properties.Resources.Stop;
			RecordAVMenuItem.Image = Properties.Resources.Record;
			ConfigAndRecordAVMenuItem.Image = Properties.Resources.Avi;
			StopAVIMenuItem.Image = Properties.Resources.Stop;
			ScreenshotMenuItem.Image = Properties.Resources.Camera;
			PauseMenuItem.Image = Properties.Resources.Pause;
			RebootCoreMenuItem.Image = Properties.Resources.Reboot;
			SwitchToFullscreenMenuItem.Image = Properties.Resources.Fullscreen;
			ControllersMenuItem.Image = Properties.Resources.GameController;
			HotkeysMenuItem.Image = Properties.Resources.HotKeys;
			DisplayConfigMenuItem.Image = Properties.Resources.TvIcon;
			SoundMenuItem.Image = Properties.Resources.Audio;
			PathsMenuItem.Image = Properties.Resources.CopyFolder;
			FirmwaresMenuItem.Image = Properties.Resources.Pcb;
			MessagesMenuItem.Image = Properties.Resources.MessageConfig;
			AutofireMenuItem.Image = Properties.Resources.Lightning;
			RewindOptionsMenuItem.Image = Properties.Resources.Previous;
			ProfilesMenuItem.Image = Properties.Resources.Profile;
			SaveConfigMenuItem.Image = Properties.Resources.Save;
			LoadConfigMenuItem.Image = Properties.Resources.LoadConfig;
			ToolBoxMenuItem.Image = Properties.Resources.ToolBox;
			RamWatchMenuItem.Image = Properties.Resources.Watch;
			RamSearchMenuItem.Image = Properties.Resources.Search;
			LuaConsoleMenuItem.Image = Properties.Resources.TextDoc;
			TAStudioMenuItem.Image = Properties.Resources.TAStudio;
			HexEditorMenuItem.Image = Properties.Resources.Poke;
			TraceLoggerMenuItem.Image = Properties.Resources.Pencil;
			DebuggerMenuItem.Image = Properties.Resources.Bug;
			CodeDataLoggerMenuItem.Image = Properties.Resources.CdLogger;
			VirtualPadMenuItem.Image = Properties.Resources.GameController;
			CheatsMenuItem.Image = Properties.Resources.Freeze;
			GameSharkConverterMenuItem.Image = Properties.Resources.Shark;
			MultiDiskBundlerFileMenuItem.Image = Properties.Resources.SaveConfig;
			NesControllerSettingsMenuItem.Image = Properties.Resources.GameController;
			NESGraphicSettingsMenuItem.Image = Properties.Resources.TvIcon;
			NESSoundChannelsMenuItem.Image = Properties.Resources.Audio;
			KeypadMenuItem.Image = Properties.Resources.Calculator;
			PSXControllerSettingsMenuItem.Image = Properties.Resources.GameController;
			SNESControllerConfigurationMenuItem.Image = Properties.Resources.GameController;
			SnesGfxDebuggerMenuItem.Image = Properties.Resources.Bug;
			ColecoControllerSettingsMenuItem.Image = Properties.Resources.GameController;
			N64PluginSettingsMenuItem.Image = Properties.Resources.Monitor;
			N64ControllerSettingsMenuItem.Image = Properties.Resources.GameController;
			IntVControllerSettingsMenuItem.Image = Properties.Resources.GameController;
			OnlineHelpMenuItem.Image = Properties.Resources.Help;
			ForumsMenuItem.Image = Properties.Resources.TAStudio;
			FeaturesMenuItem.Image = Properties.Resources.KitchenSink;
			AboutMenuItem.Image = Properties.Resources.CorpHawkSmall;
			DumpStatusButton.Image = Properties.Resources.Blank;
			PlayRecordStatusButton.Image = Properties.Resources.Blank;
			PauseStatusButton.Image = Properties.Resources.Blank;
			RebootStatusBarIcon.Image = Properties.Resources.Reboot;
			AVIStatusLabel.Image = Properties.Resources.Blank;
			LedLightStatusLabel.Image = Properties.Resources.LightOff;
			KeyPriorityStatusLabel.Image = Properties.Resources.Both;
			CoreNameStatusBarButton.Image = Properties.Resources.CorpHawkSmall;
			ProfileFirstBootLabel.Image = Properties.Resources.Profile;
			LinkConnectStatusBarButton.Image = Properties.Resources.Connect16X16;
			OpenRomContextMenuItem.Image = Properties.Resources.OpenFile;
			LoadLastRomContextMenuItem.Image = Properties.Resources.Recent;
			StopAVContextMenuItem.Image = Properties.Resources.Stop;
			RecordMovieContextMenuItem.Image = Properties.Resources.Record;
			PlayMovieContextMenuItem.Image = Properties.Resources.Play;
			RestartMovieContextMenuItem.Image = Properties.Resources.Restart;
			StopMovieContextMenuItem.Image = Properties.Resources.Stop;
			LoadLastMovieContextMenuItem.Image = Properties.Resources.Recent;
			StopNoSaveContextMenuItem.Image = Properties.Resources.Stop;
			SaveMovieContextMenuItem.Image = Properties.Resources.SaveAs;
			SaveMovieAsContextMenuItem.Image = Properties.Resources.SaveAs;
			UndoSavestateContextMenuItem.Image = Properties.Resources.Undo;
			toolStripMenuItem6.Image = Properties.Resources.GameController;
			toolStripMenuItem7.Image = Properties.Resources.HotKeys;
			toolStripMenuItem8.Image = Properties.Resources.TvIcon;
			toolStripMenuItem9.Image = Properties.Resources.Audio;
			toolStripMenuItem10.Image = Properties.Resources.CopyFolder;
			toolStripMenuItem11.Image = Properties.Resources.Pcb;
			toolStripMenuItem12.Image = Properties.Resources.MessageConfig;
			toolStripMenuItem13.Image = Properties.Resources.Lightning;
			toolStripMenuItem14.Image = Properties.Resources.Previous;
			toolStripMenuItem66.Image = Properties.Resources.Save;
			toolStripMenuItem67.Image = Properties.Resources.LoadConfig;
			ScreenshotContextMenuItem.Image = Properties.Resources.Camera;
			CloseRomContextMenuItem.Image = Properties.Resources.Close;
		}

		public MainForm(Config config, IGL gl, Action<Sound> updateGlobalSound, string[] args, out IMovieSession movieSession)
		{
			movieSession = null;

			if (config.SingleInstanceMode)
			{
				if (SingleInstanceInit(args))
				{
					Dispose();
					return;
				}
			}

			//do this threaded stuff early so it has plenty of time to run in background
			Database.InitializeDatabase(Path.Combine(PathUtils.ExeDirectoryPath, "gamedb", "gamedb.txt"));
			BootGodDb.Initialize(Path.Combine(PathUtils.ExeDirectoryPath, "gamedb"));

			Config = config;
			GL = gl;
			_updateGlobalSound = updateGlobalSound;

			InputManager = new InputManager
			{
				ControllerInputCoalescer = new ControllerInputCoalescer(),
				GetMainFormMouseInfo = () =>
				{
					var b = Control.MouseButtons;
					return (
						Control.MousePosition,
						MouseWheelTracker,
						(b & MouseButtons.Left) != 0,
						(b & MouseButtons.Middle) != 0,
						(b & MouseButtons.Right) != 0,
						(b & MouseButtons.XButton1) != 0,
						(b & MouseButtons.XButton2) != 0
					);
				}
			};
			FirmwareManager = new FirmwareManager();
			movieSession = MovieSession = new MovieSession(
				Config.Movies,
				Config.PathEntries.MovieBackupsAbsolutePath(),
				this,
				AddOnScreenMessage,
				PauseEmulator,
				SetMainformMovieInfo);

			void MainForm_MouseClick(object sender, MouseEventArgs e)
			{
				AutohideCursor(false);
				if (Config.ShowContextMenu && e.Button == MouseButtons.Right)
				{
					MainFormContextMenu.Show(PointToScreen(new Point(e.X, e.Y + MainformMenu.Height)));
				}
			}
			void MainForm_MouseMove(object sender, MouseEventArgs e) => AutohideCursor(false);
			void MainForm_MouseWheel(object sender, MouseEventArgs e) => MouseWheelTracker += e.Delta;
			MouseClick += MainForm_MouseClick;
			MouseMove += MainForm_MouseMove;

			InitializeComponent();
			Icon = Properties.Resources.Logo;
			SetImages();
#if !DEBUG
			ZXSpectrumExportSnapshotMenuItemMenuItem.Enabled = false;
			ZXSpectrumExportSnapshotMenuItemMenuItem.Visible = false;
#endif

			Game = GameInfo.NullInstance;
			_throttle = new Throttle();
			Emulator = new NullEmulator();

			UpdateStatusSlots();
			UpdateKeyPriorityIcon();

			try
			{
				ArgParser.ParseArguments(out _argParser, args);
			}
			catch (ArgParser.ArgParserException e)
			{
				ShowMessageBox(owner: null, e.Message);
			}

			// TODO GL - a lot of disorganized wiring-up here
			// installed separately on Unix (via package manager or from https://developer.nvidia.com/cg-toolkit-download), look in $PATH
			_presentationPanel = new PresentationPanel(
				Config,
				GL,
				ToggleFullscreen,
				MainForm_MouseClick,
				MainForm_MouseMove,
				MainForm_MouseWheel)
			{
				GraphicsControl = { MainWindow = true }
			};
			DisplayManager = new DisplayManager(Config, Emulator, InputManager, MovieSession, GL, _presentationPanel, () => DisableSecondaryThrottling);
			Controls.Add(_presentationPanel);
			Controls.SetChildIndex(_presentationPanel, 0);

			Tools = new ToolManager(this, Config, DisplayManager, InputManager, Emulator, MovieSession, Game);
			ExtToolManager = new ExternalToolManager(Config.PathEntries, () => (EmuClientApi.SystemIdConverter.Convert(Emulator.SystemId), Game.Hash));

			// TODO GL - move these event handlers somewhere less obnoxious line in the On* overrides
			Load += (o, e) =>
			{
				AllowDrop = true;
				DragEnter += FormDragEnter;
				DragDrop += FormDragDrop;
			};

			Closing += (o, e) =>
			{
				if (Tools.AskSave())
				{
					// zero 03-nov-2015 - close game after other steps. tools might need to unhook themselves from a core.
					MovieSession.StopMovie();
					Tools.Close();
					CloseGame();
					SaveConfig();
				}
				else
				{
					e.Cancel = true;
				}
			};

			ResizeBegin += (o, e) =>
			{
				_inResizeLoop = true;
				Sound?.StopSound();
			};

			Resize += (o, e) =>
			{
				SetWindowText();
			};

			ResizeEnd += (o, e) =>
			{
				_inResizeLoop = false;
				SetWindowText();

				if (_presentationPanel != null)
				{
					_presentationPanel.Resized = true;
				}

				Sound?.StartSound();
			};

			Input.Instance = new Input(
				Handle,
				() => Config,
				yieldAlt => ActiveForm switch
				{
					null => Config.AcceptBackgroundInput // none of our forms are focused, check the background input config
						? Config.AcceptBackgroundInputControllerOnly
							? Input.AllowInput.OnlyController
							: Input.AllowInput.All
						: Input.AllowInput.None,
					TAStudio when yieldAlt => Input.AllowInput.None,
					FormBase { BlocksInputWhenFocused: false } => Input.AllowInput.All,
					ControllerConfig => Input.AllowInput.All,
					HotkeyConfig => Input.AllowInput.All,
					_ => Input.AllowInput.None
				}
			);
			InitControls();

			InputManager.ActiveController = new Controller(NullController.Instance.Definition);
			InputManager.AutoFireController = _autofireNullControls;
			InputManager.AutofireStickyXorAdapter.SetOnOffPatternFromConfig(Config.AutofireOn, Config.AutofireOff);
			try
			{
				Sound = new Sound(Handle, Config, () => Emulator.VsyncRate());
			}
			catch
			{
				string message = "Couldn't initialize sound device! Try changing the output method in Sound config.";
				if (Config.SoundOutputMethod == 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.";
				}

				ShowMessageBox(owner: null, message, "Initialization Error", EMsgBoxIcon.Error);

				Config.SoundOutputMethod = ESoundOutputMethod.Dummy;
				Sound = new Sound(Handle, Config, () => Emulator.VsyncRate());
			}

			Sound.StartSound();
			InputManager.SyncControls(Emulator, MovieSession, Config);
			CheatList = new CheatCollection(Config.Cheats);
			CheatList.Changed += Tools.UpdateCheatRelatedTools;
			RewireSound();

			// Workaround for windows, location is -32000 when minimized, if they close it during this time, that's what gets saved
			if (Config.MainWndx == -32000)
			{
				Config.MainWndx = 0;
			}

			if (Config.MainWndy == -32000)
			{
				Config.MainWndy = 0;
			}

			if (Config.MainWndx != -1 && Config.MainWndy != -1 && Config.SaveWindowPosition)
			{
				Location = new Point(Config.MainWndx, Config.MainWndy);
			}

			if (_argParser.cmdRom != null)
			{
				// Commandline should always override auto-load
				var ioa = OpenAdvancedSerializer.ParseWithLegacy(_argParser.cmdRom);
				LoadRom(_argParser.cmdRom, new LoadRomArgs { OpenAdvanced = ioa });
				if (Game == null)
				{
					ShowMessageBox(owner: null, $"Failed to load {_argParser.cmdRom} specified on commandline");
				}
			}
			else if (Config.RecentRoms.AutoLoad && !Config.RecentRoms.Empty)
			{
				LoadRomFromRecent(Config.RecentRoms.MostRecent);
			}

			if (_argParser.audiosync.HasValue)
			{
				Config.VideoWriterAudioSync = _argParser.audiosync.Value;
			}

			_autoDumpLength = _argParser._autoDumpLength;
			if (_argParser.cmdMovie != null)
			{
				_suppressSyncSettingsWarning = true; // We don't want to be nagged if we are attempting to automate
				if (Game == null)
				{
					OpenRom();
				}

				// If user picked a game, then do the commandline logic
				if (!Game.IsNullInstance())
				{
					var movie = MovieSession.Get(_argParser.cmdMovie);
					MovieSession.ReadOnly = true;

					// if user is dumping and didn't supply dump length, make it as long as the loaded movie
					if (_autoDumpLength == 0)
					{
						_autoDumpLength = movie.InputLogLength;
					}

					// Copy pasta from drag & drop
					if (MovieImport.IsValidMovieExtension(Path.GetExtension(_argParser.cmdMovie)))
					{
						ProcessMovieImport(_argParser.cmdMovie, true);
					}
					else
					{
						StartNewMovie(movie, false);
						Config.RecentMovies.Add(_argParser.cmdMovie);
					}

					_suppressSyncSettingsWarning = false;
				}
			}
			else if (Config.RecentMovies.AutoLoad && !Config.RecentMovies.Empty)
			{
				if (Game.IsNullInstance())
				{
					OpenRom();
				}

				// If user picked a game, then do the autoload logic
				if (!Game.IsNullInstance())
				{
					if (File.Exists(Config.RecentMovies.MostRecent))
					{
						StartNewMovie(MovieSession.Get(Config.RecentMovies.MostRecent), false);
					}
					else
					{
						Config.RecentMovies.HandleLoadError(this, Config.RecentMovies.MostRecent);
					}
				}
			}

			if (_argParser.startFullscreen || Config.StartFullscreen)
			{
				_needsFullscreenOnLoad = true;
			}

			if (!Game.IsNullInstance())
			{
				if (_argParser.cmdLoadState != null)
				{
					LoadState(_argParser.cmdLoadState, Path.GetFileName(_argParser.cmdLoadState));
				}
				else if (_argParser.cmdLoadSlot != null)
				{
					LoadQuickSave($"QuickSave{_argParser.cmdLoadSlot}");
				}
				else if (Config.AutoLoadLastSaveSlot)
				{
					LoadQuickSave($"QuickSave{Config.SaveSlot}");
				}
			}

			// set up networking before Lua
			byte[] NetworkingTakeScreenshot() => (byte[]) new ImageConverter().ConvertTo(MakeScreenshotImage().ToSysdrawingBitmap(), typeof(byte[]));
			NetworkingHelpers = (
				_argParser.HTTPAddresses == null
					? null
					: new HttpCommunication(NetworkingTakeScreenshot, _argParser.HTTPAddresses.Value.UrlGet, _argParser.HTTPAddresses.Value.UrlPost),
				_argParser.MMFFilename == null
					? null
					: new MemoryMappedFiles(NetworkingTakeScreenshot, _argParser.MMFFilename),
				_argParser.SocketAddress == null
					? null
					: new SocketServer(NetworkingTakeScreenshot, _argParser.SocketAddress.Value.IP, _argParser.SocketAddress.Value.Port)
			);

			//start Lua Console if requested in the command line arguments
			if (_argParser.luaConsole)
			{
				Tools.Load<LuaConsole>();
			}
			//load Lua Script if requested in the command line arguments
			if (_argParser.luaScript != null)
			{
				if (OSTailoredCode.IsUnixHost) Console.WriteLine($"The Lua environment can currently only be created on Windows, {_argParser.luaScript} will not be loaded.");
				else Tools.LuaConsole.LoadLuaFile(_argParser.luaScript);
			}

			SetStatusBar();

			if (Config.StartPaused)
			{
				PauseEmulator();
			}

			// start dumping, if appropriate
			if (_argParser.cmdDumpType != null && _argParser.cmdDumpName != null)
			{
				RecordAv(_argParser.cmdDumpType, _argParser.cmdDumpName);
			}

			SetMainformMovieInfo();

			SynchChrome();

			_presentationPanel.Control.Paint += (o, e) =>
			{
				// I would like to trigger a repaint here, but this isn't done yet
			};

			if (!OSTailoredCode.IsUnixHost && !Config.SkipOutdatedOsCheck)
			{
				var (winVersion, win10Release) = OSTailoredCode.HostWindowsVersion.Value;
				var message = winVersion switch
				{
					OSTailoredCode.WindowsVersion._10 when win10Release < 1909 => $"Quick reminder: version {win10Release} of Windows 10 is no longer supported by Microsoft. EmuHawk will continue to work, but please update to at least 1909 for increased security.",
					OSTailoredCode.WindowsVersion._10 => null,
					OSTailoredCode.WindowsVersion._8_1 => null, // still CBB
					_ => $"Quick reminder: Windows {winVersion.ToString().RemovePrefix('_').Replace("_", ".")} is no longer supported by Microsoft. EmuHawk will continue to work, but please get a new operating system for increased security (either Windows 8.1, Windows 10, or a GNU+Linux distro)."
				};
#if false
				if (message != null)
				{
					using var box = new ExceptionBox(message);
					box.ShowDialog();
				}
#endif
			}
		}

		private readonly bool _suppressSyncSettingsWarning;

		public override bool BlocksInputWhenFocused { get; } = false;

		public int ProgramRunLoop()
		{
			// needs to be done late, after the log console snaps on top
			// fullscreen should snap on top even harder!
			if (_needsFullscreenOnLoad)
			{
				_needsFullscreenOnLoad = false;
				ToggleFullscreen();
			}
			
			// Simply exit the program if the version is asked for
			if (_argParser.printVersion)
			{
				// Print the version
				Console.WriteLine(VersionInfo.GetEmuVersion());
				// Return and leave
				return _exitCode;
			}

			// incantation required to get the program reliably on top of the console window
			// we might want it in ToggleFullscreen later, but here, it needs to happen regardless
			BringToFront();
			Activate();
			BringToFront();

			InitializeFpsData();

			for (; ; )
			{
				Input.Instance.Update();

				// handle events and dispatch as a hotkey action, or a hotkey button, or an input button
				ProcessInput();
				InputManager.ClientControls.LatchFromPhysical(_hotkeyCoalescer);

				InputManager.ActiveController.LatchFromPhysical(InputManager.ControllerInputCoalescer);

				InputManager.ActiveController.ApplyAxisConstraints(
					(Emulator is N64 && Config.N64UseCircularAnalogConstraint) ? "Natural Circle" : null);

				InputManager.ActiveController.OR_FromLogical(InputManager.ClickyVirtualPadController);
				InputManager.AutoFireController.LatchFromPhysical(InputManager.ControllerInputCoalescer);

				if (InputManager.ClientControls["Autohold"])
				{
					InputManager.ToggleStickies();
				}
				else if (InputManager.ClientControls["Autofire"])
				{
					InputManager.ToggleAutoStickies();
				}

				// autohold/autofire must not be affected by the following inputs
				InputManager.ActiveController.Overrides(InputManager.ButtonOverrideAdapter);

				if (Tools.Has<LuaConsole>())
				{
					Tools.LuaConsole.ResumeScripts(false);
				}

				StepRunLoop_Core();
				StepRunLoop_Throttle();

				Render();

				CheckMessages();

				if (_exitRequestPending)
				{
					_exitRequestPending = false;
					Close();
				}

				if (IsDisposed || _windowClosedAndSafeToExitProcess)
				{
					break;
				}

				if (Config.DispSpeedupFeatures != 0)
				{
					Thread.Sleep(0);
				}
			}

			Shutdown();
			return _exitCode;
		}

		/// <summary>
		/// Clean up any resources being used.
		/// </summary>
		/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
		protected override void Dispose(bool disposing)
		{
			// NOTE: this gets called twice sometimes. once by using() in Program.cs and once from winforms internals when the form is closed...
			if (DisplayManager != null)
			{
				DisplayManager.Dispose();
				DisplayManager = null;
			}

			if (disposing)
			{
				components?.Dispose();
				SingleInstanceDispose();
			}

			base.Dispose(disposing);
		}

		private bool _emulatorPaused;
		public bool EmulatorPaused
		{
			get => _emulatorPaused;

			private set
			{
				if (_emulatorPaused && !value) // Unpausing
				{
					InitializeFpsData();
				}

				_emulatorPaused = value;
				OnPauseChanged?.Invoke(_emulatorPaused);
			}
		}

		public bool BlockFrameAdvance { get; set; }

		public event Action<bool> OnPauseChanged;

		public string CurrentlyOpenRom { get; private set; } // todo - delete me and use only args instead
		public LoadRomArgs CurrentlyOpenRomArgs { get; private set; }
		public bool PauseAvi { get; set; }
		public bool PressFrameAdvance { get; set; }
		public bool FrameInch { get; set; }
		public bool HoldFrameAdvance { get; set; } // necessary for tastudio > button
		public bool PressRewind { get; set; } // necessary for tastudio < button
		public bool FastForward { get; set; }

		/// <summary>
		/// Disables updates for video/audio, and enters "turbo" mode.
		/// Can be used to replicate Gens-rr's "latency compensation" that involves:
		/// <list type="bullet">
		/// <item><description>Saving a no-framebuffer state that is stored in RAM</description></item>
		/// <item><description>Emulating forth for some frames with updates disabled</description></item>
		/// <item><list type="bullet">
		/// <item><description>Optionally hacking in-game memory
		/// (like camera position, to show off-screen areas)</description></item>
		/// </list></item>
		/// <item><description>Updating the screen</description></item>
		/// <item><description>Loading the no-framebuffer state from RAM</description></item>
		/// </list>
		/// The most common use case is CamHack for Sonic games.
		/// Accessing this from Lua allows to keep internal code hacks to minimum.
		/// <list type="bullet">
		/// <item><description><see cref="ClientLuaLibrary.InvisibleEmulation(bool)"/></description></item>
		/// <item><description><see cref="ClientLuaLibrary.SeekFrame(int)"/></description></item>
		/// </list>
		/// </summary>
		public bool InvisibleEmulation { get; set; }

		private long MouseWheelTracker;

		private int? _pauseOnFrame;
		public int? PauseOnFrame // If set, upon completion of this frame, the client wil pause
		{
			get => _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
				{
					Tools.UpdateToolsBefore();
					Tools.UpdateToolsAfter();
				}
			}
		}

		public bool IsSeeking => PauseOnFrame.HasValue;
		private bool IsTurboSeeking => PauseOnFrame.HasValue && Config.TurboSeek;
		public bool IsTurboing => InputManager.ClientControls["Turbo"] || IsTurboSeeking;

		/// <summary>
		/// Used to disable secondary throttling (e.g. vsync, audio) for unthrottled modes or when the primary (clock) throttle is taking over (e.g. during fast forward/rewind).
		/// </summary>
		public static bool DisableSecondaryThrottling { get; set; }

		public void AddOnScreenMessage(string message) => OSD.AddMessage(message);

		public void ClearHolds()
		{
			InputManager.StickyXorAdapter.ClearStickies();
			InputManager.AutofireStickyXorAdapter.ClearStickies();

			if (Tools.Has<VirtualpadTool>())
			{
				Tools.VirtualPad.ClearVirtualPadHolds();
			}
		}

		public void FlagNeedsReboot()
		{
			RebootStatusBarIcon.Visible = true;
			AddOnScreenMessage("Core reboot needed for this setting");
		}

		/// <remarks>don't use this, use <see cref="Emulator"/></remarks>
		private IEmulator _emulator;

		public IEmulator Emulator
		{
			get => _emulator;

			private set
			{
				_emulator = value;
				_currentVideoProvider = value.AsVideoProviderOrDefault();
				_currentSoundProvider = value.AsSoundProviderOrDefault();
			}
		}

		public EmuClientApi EmuClient { get; set; }

		private readonly InputManager InputManager;

		private IVideoProvider _currentVideoProvider = NullVideo.Instance;

		private ISoundProvider _currentSoundProvider = new NullSound(44100 / 60); // Reasonable default until we have a core instance

		/// <remarks>don't use this, use <see cref="Config"/></remarks>
		private Config _config;

		private new Config Config
		{
			get => _config;
			set => base.Config = _config = value;
		}

		private readonly IGL GL;

		private readonly ExternalToolManager ExtToolManager;

		public readonly ToolManager Tools;

		private DisplayManager DisplayManager;

		private OSDManager OSD => DisplayManager.OSD;

		public IMovieSession MovieSession { get; }

		public GameInfo Game { get; private set; }

		/// <remarks>don't use this, use <see cref="Sound"/></remarks>
		private Sound _sound;

		private readonly Action<Sound> _updateGlobalSound;

		private Sound Sound
		{
			get => _sound;
			set => _updateGlobalSound(_sound = value);
		}

		public CheatCollection CheatList { get; }

		public (HttpCommunication HTTP, MemoryMappedFiles MMF, SocketServer Sockets) NetworkingHelpers { get; }

		public IRewinder Rewinder { get; private set; }

		public void CreateRewinder()
		{
			Rewinder?.Dispose();
			Rewinder = Emulator.HasSavestates() && Config.Rewind.Enabled
				? new Zwinder(Emulator.AsStatable(), Config.Rewind)
				: null;
			AddOnScreenMessage(Rewinder?.Active == true ? "Rewind started" : "Rewind disabled");
		}

		public FirmwareManager FirmwareManager { get; }

		protected override void OnActivated(EventArgs e)
		{
			base.OnActivated(e);
			Input.Instance.ControlInputFocus(this, ClientInputFocus.Mouse, true);
		}

		protected override void OnDeactivate(EventArgs e)
		{
			Input.Instance.ControlInputFocus(this, ClientInputFocus.Mouse, false);
			base.OnDeactivate(e);
		}

		private void ProcessInput()
		{
			var conInput = (ControllerInputCoalescer)InputManager.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 = InputManager.ClientControls.SearchBindings(ie.LogicalButton.ToString());
				if (triggers.Count == 0)
				{
					// Maybe it is a system alt-key which hasn't 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 doesn't make sense to me to bind hotkeys and controller inputs to the same keystrokes

				bool handled;
				switch (Config.InputHotkeyOverrideOptions)
				{
					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 aren't handled as actions get coalesced as pollable virtual client buttons
						if (!handled)
						{
							_hotkeyCoalescer.Receive(ie);
						}

						break;
					case 1: // Input overrides Hotkeys
						conInput.Receive(ie);
						if (!InputManager.ActiveController.HasBinding(ie.LogicalButton.ToString()))
						{
							handled = false;
							if (ie.EventType == Input.InputEventType.Press)
							{
								handled = triggers.Aggregate(false, (current, trigger) => current | CheckHotkey(trigger));
							}

							// hotkeys which aren't 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(false, (current, trigger) => current | CheckHotkey(trigger));
						}

						// hotkeys which aren't handled as actions get coalesced as pollable virtual client buttons
						if (!handled)
						{
							_hotkeyCoalescer.Receive(ie);

							// Check for hotkeys that may not be handled through CheckHotkey() method, reject controller input mapped to these
							if (!triggers.Any(IsInternalHotkey))
							{
								conInput.Receive(ie);
							}
						}

						break;
				}
			} // foreach event

			//also handle axes
			//we'll need to isolate the mouse coordinates so we can translate them
			KeyValuePair<string, int>? mouseX = null, mouseY = null;
			foreach (var f in Input.Instance.GetAxisValues())
			{
				if (f.Key == "WMouse X")
					mouseX = f;
				else if (f.Key == "WMouse Y")
					mouseY = f;
				else conInput.AcceptNewAxis(f.Key, f.Value);
			}

			//if we found mouse coordinates (and why wouldn't we?) then translate them now
			//NOTE: these must go together, because in the case of screen rotation, X and Y are transformed together
			if(mouseX != null && mouseY != null)
			{
				var p = DisplayManager.UntransformPoint(new Point((int) mouseX.Value.Value, (int) mouseY.Value.Value));
				float x = p.X / (float)_currentVideoProvider.BufferWidth;
				float y = p.Y / (float)_currentVideoProvider.BufferHeight;
				conInput.AcceptNewAxis("WMouse X", (int) ((x * 20000) - 10000));
				conInput.AcceptNewAxis("WMouse Y", (int) ((y * 20000) - 10000));
			}

		}

		public void RebootCore()
		{
			if (IsSlave && Master.WantsToControlReboot)
			{
				Master.RebootCore();
			}
			else
			{
				if (CurrentlyOpenRomArgs == null)
				{
					return;
				}

				LoadRom(CurrentlyOpenRomArgs.OpenAdvanced.SimplePath, CurrentlyOpenRomArgs);
			}
		}

		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 (Tools.Has<TAStudio>())
			{
				Tools.UpdateValues<TAStudio>();
			}
		}

		public void TakeScreenshotToClipboard()
		{
			using var bb = Config.ScreenshotCaptureOsd ? CaptureOSD() : MakeScreenshotImage();
			bb.ToSysdrawingBitmap().ToClipBoard();
			AddOnScreenMessage("Screenshot (raw) saved to clipboard.");
		}

		private void TakeScreenshotClientToClipboard()
		{
			using var bb = DisplayManager.RenderOffscreen(_currentVideoProvider, Config.ScreenshotCaptureOsd);
			bb.ToSysdrawingBitmap().ToClipBoard();
			AddOnScreenMessage("Screenshot (client) saved to clipboard.");
		}

		private string ScreenshotPrefix()
		{
			var screenPath = Config.PathEntries.ScreenshotAbsolutePathFor(Game.System);
			var name = Game.FilesystemSafeName();
			return Path.Combine(screenPath, name);
		}

		public void TakeScreenshot()
		{
			var basename = $"{ScreenshotPrefix()}.{DateTime.Now:yyyy-MM-dd HH.mm.ss}";

			var fnameBare = $"{basename}.png";
			var fname = $"{basename} (0).png";

			// if the (0) filename exists, do nothing. we'll bump up the number later
			// if the bare filename exists, move it to (0)
			// otherwise, no related filename exists, and we can proceed with the bare filename
			if (!File.Exists(fname))
			{
				if (File.Exists(fnameBare)) File.Move(fnameBare, fname);
				else fname = fnameBare;
			}

			for (var seq = 0; File.Exists(fname); seq++)
				fname = $"{basename} ({seq}).png";

			TakeScreenshot(fname);
		}

		public void TakeScreenshot(string path)
		{
			var fi = new FileInfo(path);
			if (fi.Directory != null && !fi.Directory.Exists)
			{
				fi.Directory.Create();
			}

			using (var bb = Config.ScreenshotCaptureOsd ? CaptureOSD() : MakeScreenshotImage())
			{
				using var img = bb.ToSysdrawingBitmap();
				img.Save(fi.FullName, ImageFormat.Png);
			}

			AddOnScreenMessage($"{fi.Name} saved.");
		}

		public void FrameBufferResized()
		{
			// run this entire thing exactly twice, since the first resize may adjust the menu stacking
			for (int i = 0; i < 2; i++)
			{
				int zoom = Config.TargetZoomFactors[Emulator.SystemId];
				var area = Screen.FromControl(this).WorkingArea;

				int borderWidth = Size.Width - _presentationPanel.Control.Size.Width;
				int borderHeight = Size.Height - _presentationPanel.Control.Size.Height;

				// start at target zoom and work way down until we find acceptable zoom
				Size lastComputedSize = new Size(1, 1);
				for (; zoom >= 1; zoom--)
				{
					lastComputedSize = DisplayManager.CalculateClientSize(_currentVideoProvider, zoom);
					if (lastComputedSize.Width + borderWidth < area.Width
						&& lastComputedSize.Height + borderHeight < area.Height)
					{
						break;
					}
				}


				Util.DebugWriteLine($"For emulator framebuffer {new Size(_currentVideoProvider.BufferWidth, _currentVideoProvider.BufferHeight)}:");
				Util.DebugWriteLine($"  For virtual size {new Size(_currentVideoProvider.VirtualWidth, _currentVideoProvider.VirtualHeight)}:");
				Util.DebugWriteLine($"  Selecting display size {lastComputedSize}");

				// Change size
				Size = new Size(lastComputedSize.Width + borderWidth, lastComputedSize.Height + borderHeight);
				PerformLayout();
				_presentationPanel.Resized = true;

				// Is window off the screen at this size?
				if (!area.Contains(Bounds))
				{
					if (Bounds.Right > area.Right) // Window is off the right edge
					{
						Location = new Point(area.Right - Size.Width, Location.Y);
					}

					if (Bounds.Bottom > area.Bottom) // Window is off the bottom edge
					{
						Location = new Point(Location.X, area.Bottom - Size.Height);
					}
				}
			}
		}

		private void SynchChrome()
		{
			if (_inFullscreen)
			{
				// TODO - maybe apply a hack tracked during fullscreen here to override it
				FormBorderStyle = FormBorderStyle.None;
				MainMenuStrip.Visible = Config.DispChromeMenuFullscreen && !_argParser._chromeless;
				MainStatusBar.Visible = Config.DispChromeStatusBarFullscreen && !_argParser._chromeless;
			}
			else
			{
				MainStatusBar.Visible = Config.DispChromeStatusBarWindowed && !_argParser._chromeless;
				MainMenuStrip.Visible = Config.DispChromeMenuWindowed && !_argParser._chromeless;
				MaximizeBox = MinimizeBox = Config.DispChromeCaptionWindowed && !_argParser._chromeless;
				if (Config.DispChromeFrameWindowed == 0 || _argParser._chromeless)
				{
					FormBorderStyle = FormBorderStyle.None;
				}
				else if (Config.DispChromeFrameWindowed == 1)
				{
					FormBorderStyle = FormBorderStyle.SizableToolWindow;
				}
				else if (Config.DispChromeFrameWindowed == 2)
				{
					FormBorderStyle = FormBorderStyle.Sizable;
				}
			}
		}

		public void ToggleFullscreen(bool allowSuppress = false)
		{
			AutohideCursor(false);

			// prohibit this operation if the current controls include LMouse
			if (allowSuppress)
			{
				if (InputManager.ActiveController.HasBinding("WMouse L"))
				{
					return;
				}
			}

			if (!_inFullscreen)
			{
				SuspendLayout();

				// Work around an AMD driver bug in >= vista:
				// It seems windows will activate opengl fullscreen mode when a GL control is occupying the exact space of a screen (0,0 and dimensions=screensize)
				// AMD cards manifest a problem under these circumstances, flickering other monitors. 
				// It isn't clear whether nvidia cards are failing to employ this optimization, or just not flickering.
				// (this could be determined with more work; other side affects of the fullscreen mode include: corrupted TaskBar, no modal boxes on top of GL control, no screenshots)
				// At any rate, we can solve this by adding a 1px black border around the GL control
				// Please note: It is important to do this before resizing things, otherwise momentarily a GL control without WS_BORDER will be at the magic dimensions and cause the flakeout
				if (!OSTailoredCode.IsUnixHost
					&& Config.DispFullscreenHacks
					&& Config.DispMethod == EDispMethod.OpenGL)
				{
					// ATTENTION: this causes the StatusBar to not work well, since the backcolor is now set to black instead of SystemColors.Control.
					// It seems that some StatusBar elements composite with the backcolor. 
					// Maybe we could add another control under the StatusBar. with a different backcolor
					Padding = new Padding(1);
					BackColor = Color.Black;

					// FUTURE WORK:
					// re-add this padding back into the display manager (so the image will get cut off a little but, but a few more resolutions will fully fit into the screen)
				}

				_windowedLocation = Location;

				_inFullscreen = true;
				SynchChrome();
				WindowState = FormWindowState.Maximized; // be sure to do this after setting the chrome, otherwise it wont work fully
				ResumeLayout();

				_presentationPanel.Resized = true;
			}
			else
			{
				SuspendLayout();

				WindowState = FormWindowState.Normal;

				if (!OSTailoredCode.IsUnixHost)
				{
					// do this even if DispFullscreenHacks aren't enabled, to restore it in case it changed underneath us or something
					Padding = new Padding(0);

					// it's important that we set the form color back to this, because the StatusBar icons blend onto the mainform, not onto the StatusBar--
					// so we need the StatusBar and mainform backdrop color to match
					BackColor = SystemColors.Control;
				}

				_inFullscreen = false;

				SynchChrome();
				Location = _windowedLocation;
				ResumeLayout();

				FrameBufferResized();
			}
		}

		private void OpenLuaConsole()
		{
			Tools.Load<LuaConsole>();
		}

		public void ClickSpeedItem(int num)
		{
			if ((ModifierKeys & Keys.Control) != 0)
			{
				SetSpeedPercentAlternate(num);
			}
			else
			{
				SetSpeedPercent(num);
			}
		}

		public void Unthrottle()
		{
			_unthrottled = true;
		}

		public void Throttle()
		{
			_unthrottled = false;
		}

		private void ThrottleMessage()
		{
			string type = ":(none)";
			if (Config.SoundThrottle)
			{
				type = ":Sound";
			}

			if (Config.VSyncThrottle)
			{
				type = $":Vsync{(Config.VSync ? "[ena]" : "[dis]")}";
			}

			if (Config.ClockThrottle)
			{
				type = ":Clock";
			}

			string throttled = _unthrottled ? "Unthrottled" : "Throttled";
			string msg = $"{throttled}{type} ";

			AddOnScreenMessage(msg);
		}

		public void FrameSkipMessage()
		{
			AddOnScreenMessage($"Frameskipping set to {Config.FrameSkip}");
		}

		public void UpdateCheatStatus()
		{
			if (CheatList.ActiveCount > 0)
			{
				CheatStatusButton.ToolTipText = "Cheats are currently active";
				CheatStatusButton.Image = Properties.Resources.Freeze;
				CheatStatusButton.Visible = true;
			}
			else
			{
				CheatStatusButton.ToolTipText = "";
				CheatStatusButton.Image = Properties.Resources.Blank;
				CheatStatusButton.Visible = false;
			}
		}

		private void SNES_ToggleBg(int layer)
		{
			if (!(Emulator is LibsnesCore || Emulator is Snes9x) || !1.RangeTo(4).Contains(layer))
			{
				return;
			}

			bool result = false;
			if (Emulator is LibsnesCore bsnes)
			{
				var s = bsnes.GetSettings();
				switch (layer)
				{
					case 1:
						result = s.ShowBG1_0 = s.ShowBG1_1 ^= true;
						break;
					case 2:
						result = s.ShowBG2_0 = s.ShowBG2_1 ^= true;
						break;
					case 3:
						result = s.ShowBG3_0 = s.ShowBG3_1 ^= true;
						break;
					case 4:
						result = s.ShowBG4_0 = s.ShowBG4_1 ^= true;
						break;
				}

				bsnes.PutSettings(s);
			}
			else if (Emulator is Snes9x snes9X)
			{
				var s = snes9X.GetSettings();
				switch (layer)
				{
					case 1:
						result = s.ShowBg0 ^= true;
						break;
					case 2:
						result = s.ShowBg1 ^= true;
						break;
					case 3:
						result = s.ShowBg2 ^= true;
						break;
					case 4:
						result = s.ShowBg3 ^= true;
						break;
				}

				snes9X.PutSettings(s);
			}

			AddOnScreenMessage($"BG {layer} Layer {(result ? "On" : "Off")}");
		}

		private void SNES_ToggleObj(int layer)
		{
			if (!(Emulator is LibsnesCore || Emulator is Snes9x) || !1.RangeTo(4).Contains(layer))
			{
				return;
			}

			bool result = false;
			if (Emulator is LibsnesCore bsnes)
			{
				var s = bsnes.GetSettings();
				result = layer switch
				{
					1 => (s.ShowOBJ_0 ^= true),
					2 => (s.ShowOBJ_1 ^= true),
					3 => (s.ShowOBJ_2 ^= true),
					4 => (s.ShowOBJ_3 ^= true),
					_ => result
				};

				bsnes.PutSettings(s);
				AddOnScreenMessage($"Obj {layer} Layer {(result ? "On" : "Off")}");
			}
			else if (Emulator is Snes9x snes9X)
			{
				var s = snes9X.GetSettings();
				result = layer switch
				{
					1 => (s.ShowSprites0 ^= true),
					2 => (s.ShowSprites1 ^= true),
					3 => (s.ShowSprites2 ^= true),
					4 => (s.ShowSprites3 ^= true),
					_ => result
				};

				snes9X.PutSettings(s);
				AddOnScreenMessage($"Sprite {layer} Layer {(result ? "On" : "Off")}");
			}
		}

		public bool RunLibretroCoreChooser()
		{
			using var ofd = new OpenFileDialog();

			if (Config.LibretroCore != null)
			{
				ofd.FileName = Path.GetFileName(Config.LibretroCore);
				ofd.InitialDirectory = Path.GetDirectoryName(Config.LibretroCore);
			}
			else
			{
				ofd.InitialDirectory = Config.PathEntries.AbsolutePathForType("Libretro", "Cores");
				if (!Directory.Exists(ofd.InitialDirectory))
				{
					Directory.CreateDirectory(ofd.InitialDirectory);
				}
			}

			ofd.RestoreDirectory = true;
			ofd.Filter = new FilesystemFilter("Libretro Cores", new[] { "dll" }).ToString();

			if (ofd.ShowDialog() == DialogResult.Cancel)
			{
				return false;
			}

			Config.LibretroCore = ofd.FileName;

			return true;
		}

		private Size _lastVideoSize = new Size(-1, -1), _lastVirtualSize = new Size(-1, -1);
		private readonly SaveSlotManager _stateSlots = new SaveSlotManager();

		// AVI/WAV state
		private IVideoWriter _currAviWriter;

		private AutofireController _autofireNullControls;

		// Sound refactor TODO: we can enforce async mode here with a property that gets/sets this but does an async check
		private ISoundProvider _aviSoundInputAsync; // Note: This sound provider must be in async mode!

		private SimpleSyncSoundProvider _dumpProxy; // an audio proxy used for dumping
		private bool _dumpaudiosync; // set true to for experimental AV dumping
		private int _avwriterResizew;
		private int _avwriterResizeh;
		private bool _avwriterpad;

		private bool _windowClosedAndSafeToExitProcess;
		private int _exitCode;
		private bool _exitRequestPending;
		private bool _runloopFrameProgress;
		private long _frameAdvanceTimestamp;
		private long _frameRewindTimestamp;
		private bool _frameRewindWasPaused;
		private bool _runloopFrameAdvance;
		private bool _lastFastForwardingOrRewinding;
		private bool _inResizeLoop;

		private readonly double _fpsUpdatesPerSecond = 4.0;
		private readonly double _fpsSmoothing = 8.0;
		private double _lastFps;
		private int _framesSinceLastFpsUpdate;
		private long _timestampLastFpsUpdate;

		private readonly Throttle _throttle;
		private bool _unthrottled;

		// For handling automatic pausing when entering the menu
		private bool _wasPaused;
		private bool _didMenuPause;

		private bool _cursorHidden;
		private bool _inFullscreen;
		private Point _windowedLocation;
		private bool _needsFullscreenOnLoad;

		private int _lastOpenRomFilter;

		private readonly ParsedCLIFlags _argParser;

		private int _autoDumpLength;

		// Resources
		private Bitmap _statusBarDiskLightOnImage;
		private Bitmap _statusBarDiskLightOffImage;
		private Bitmap _linkCableOn;
		private Bitmap _linkCableOff;

		// input state which has been destined for game controller inputs are coalesced here
		// public static ControllerInputCoalescer ControllerInputCoalescer = new ControllerInputCoalescer();
		// input state which has been destined for client hotkey consumption are colesced here
		private readonly InputCoalescer _hotkeyCoalescer = new InputCoalescer();

		private readonly PresentationPanel _presentationPanel;

		// countdown for saveram autoflushing
		public int AutoFlushSaveRamIn { get; set; }

		private void SetStatusBar()
		{
			if (!_inFullscreen)
			{
				MainStatusBar.Visible = Config.DispChromeStatusBarWindowed;
				PerformLayout();
				FrameBufferResized();
			}
		}

		protected override string WindowTitle
		{
			get
			{
				if (!Config.DispChromeCaptionWindowed || _argParser._chromeless) return string.Empty; //TODO why would you want this? was this a previous attempt at static window titles?

				var sb = new StringBuilder();

				if (_inResizeLoop)
				{
					var size = _presentationPanel.NativeSize;
					sb.Append($"({size.Width}x{size.Height})={(float) size.Width / size.Height} - ");
				}

				if (Config.DispSpeedupFeatures == 0)
				{
					// we need to display FPS somewhere, in this case
					sb.Append($"({_lastFps:0} fps) - ");
				}

				if (!string.IsNullOrEmpty(VersionInfo.CustomBuildString))
				{
					sb.Append($"{VersionInfo.CustomBuildString} ");
				}

				sb.Append(Emulator.IsNull() ? "BizHawk" : Emulator.GetSystemDisplayName());

				if (VersionInfo.DeveloperBuild)
				{
					sb.Append(" (interim)");
				}

				if (!Emulator.IsNull())
				{
					sb.Append($" - {Game.Name}");
					if (MovieSession.Movie.IsActive())
					{
						sb.Append($" - {Path.GetFileName(MovieSession.Movie.Filename)}");
					}
				}

				return sb.ToString();
			}
		}

		protected override string WindowTitleStatic
		{
			get
			{
				var sb = new StringBuilder();
				if (!string.IsNullOrEmpty(VersionInfo.CustomBuildString)) sb.Append($"{VersionInfo.CustomBuildString} ");
				sb.Append("BizHawk");
				if (VersionInfo.DeveloperBuild) sb.Append(" (interim)");
				return sb.ToString();
			}
		}

		public void SetWindowText() => UpdateWindowTitle();

		private void ClearAutohold()
		{
			ClearHolds();
			AddOnScreenMessage("Autohold keys cleared");
		}

		private void UpdateToolsLoadstate()
		{
			if (Tools.Has<SNESGraphicsDebugger>())
			{
				Tools.SNESGraphicsDebugger.UpdateToolsLoadstate();
			}
		}

		private void UpdateToolsAfter()
		{
			Tools.UpdateToolsAfter();
			HandleToggleLightAndLink();
		}

		public void UpdateDumpInfo(RomStatus? newStatus = null)
		{
			DumpStatusButton.Image = Properties.Resources.Blank;
			DumpStatusButton.ToolTipText = "";

			if (Emulator.IsNull() || Game.IsNullInstance())
			{
				return;
			}

			var status = newStatus == null
				? Game.Status
				: (Game.Status = newStatus.Value);
			if (status == RomStatus.BadDump)
			{
				DumpStatusButton.Image = Properties.Resources.ExclamationRed;
				DumpStatusButton.ToolTipText = "Warning: Bad ROM Dump";
			}
			else if (status == RomStatus.Overdump)
			{
				DumpStatusButton.Image = Properties.Resources.ExclamationRed;
				DumpStatusButton.ToolTipText = "Warning: Overdump";
			}
			else if (status == RomStatus.NotInDatabase)
			{
				DumpStatusButton.Image = Properties.Resources.RetroQuestion;
				DumpStatusButton.ToolTipText = "Warning: Unknown ROM";
			}
			else if (status == RomStatus.TranslatedRom)
			{
				DumpStatusButton.Image = Properties.Resources.Translation;
				DumpStatusButton.ToolTipText = "Translated ROM";
			}
			else if (status == RomStatus.Homebrew)
			{
				DumpStatusButton.Image = Properties.Resources.HomeBrew;
				DumpStatusButton.ToolTipText = "Homebrew ROM";
			}
			else if (Game.Status == RomStatus.Hack)
			{
				DumpStatusButton.Image = Properties.Resources.Hack;
				DumpStatusButton.ToolTipText = "Hacked ROM";
			}
			else if (Game.Status == RomStatus.Unknown)
			{
				DumpStatusButton.Image = Properties.Resources.Hack;
				DumpStatusButton.ToolTipText = "Warning: ROM of Unknown Character";
			}
			else
			{
				DumpStatusButton.Image = Properties.Resources.GreenCheck;
				DumpStatusButton.ToolTipText = "Verified good dump";
			}

			if (_multiDiskMode)
			{
				DumpStatusButton.ToolTipText = "Multi-disk bundler";
				DumpStatusButton.Image = Properties.Resources.RetroQuestion;
			}
		}

		private bool _multiDiskMode;

		// Rom details as decided by MainForm, which shouldn't happen, the RomLoader or Core should be doing this
		// Better is to just keep the game and rom hashes as properties and then generate the rom info from this
		private string _defaultRomDetails = "";

		private void LoadSaveRam()
		{
			if (Emulator.HasSaveRam())
			{
				try // zero says: this is sort of sketchy... but this is no time for rearchitecting
				{
					var saveRamPath = Config.PathEntries.SaveRamAbsolutePath(Game, MovieSession.Movie);
					if (Config.AutosaveSaveRAM)
					{
						var saveram = new FileInfo(saveRamPath);
						var autosave = new FileInfo(Config.PathEntries.AutoSaveRamAbsolutePath(Game, MovieSession.Movie));
						if (autosave.Exists && autosave.LastWriteTime > saveram.LastWriteTime)
						{
							AddOnScreenMessage("AutoSaveRAM is newer than last saved SaveRAM");
						}
					}

					byte[] sram;

					// some cores might not know how big the saveram ought to be, so just send it the whole file
					if (Emulator is MGBAHawk || Emulator is NeoGeoPort)
					{
						sram = File.ReadAllBytes(saveRamPath);
					}
					else
					{
						var oldRam = Emulator.AsSaveRam().CloneSaveRam();
						if (oldRam == null)
						{
							// we're eating this one now. The possible negative consequence is that a user could lose
							// their saveram and not know why
//							ShowMessageBox(owner: null, "Error: tried to load saveram, but core would not accept it?");
							return;
						}

						// why do we silently truncate\pad here instead of warning\erroring?
						sram = new byte[oldRam.Length];
						using var reader = new BinaryReader(new FileStream(saveRamPath, FileMode.Open, FileAccess.Read));
						reader.Read(sram, 0, sram.Length);
					}

					Emulator.AsSaveRam().StoreSaveRam(sram);
					AutoFlushSaveRamIn = Config.FlushSaveRamFrames;
				}
				catch (IOException)
				{
					AddOnScreenMessage("An error occurred while loading Sram");
				}
			}
		}

		public bool FlushSaveRAM(bool autosave = false)
		{
			if (Emulator.HasSaveRam())
			{
				string path;
				if (autosave)
				{
					path = Config.PathEntries.AutoSaveRamAbsolutePath(Game, MovieSession.Movie);
					AutoFlushSaveRamIn = Config.FlushSaveRamFrames;
				}
				else
				{
					path =  Config.PathEntries.SaveRamAbsolutePath(Game, MovieSession.Movie);
				}

				var file = new FileInfo(path);
				var newPath = $"{path}.new";
				var newFile = new FileInfo(newPath);
				var backupPath = $"{path}.bak";
				var backupFile = new FileInfo(backupPath);
				if (file.Directory != null && !file.Directory.Exists)
				{
					try
					{
						file.Directory.Create();
					}
					catch
					{
						AddOnScreenMessage($"Unable to flush SaveRAM to: {newFile.Directory}");
						return false;
					}
				}

				var saveram = Emulator.AsSaveRam().CloneSaveRam();
				if (saveram == null)
					return true;

				using (var writer = new BinaryWriter(new FileStream(newPath, FileMode.Create, FileAccess.Write)))
					writer.Write(saveram, 0, saveram.Length);

				if (file.Exists)
				{
					if (Config.BackupSaveram)
					{
						if (backupFile.Exists)
						{
							backupFile.Delete();
						}

						file.MoveTo(backupPath);
					}
					else
					{
						file.Delete();
					}
				}

				newFile.MoveTo(path);
			}

			return true;
		}

		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.
				Sound.SetInputPin(_dumpProxy);
			}
			else
			{
				bool useAsyncMode = _currentSoundProvider.CanProvideAsync && !Config.SoundThrottle;
				_currentSoundProvider.SetSyncMode(useAsyncMode ? SyncSoundMode.Async : SyncSoundMode.Sync);
				Sound.SetInputPin(_currentSoundProvider);
			}
		}

		private void HandlePlatformMenus()
		{
			GenericCoreSubMenu.Visible = false;
			TI83SubMenu.Visible = false;
			NESSubMenu.Visible = false;
			GBSubMenu.Visible = false;
			NDSSubMenu.Visible = false;
			A7800SubMenu.Visible = false;
			SNESSubMenu.Visible = false;
			PSXSubMenu.Visible = false;
			ColecoSubMenu.Visible = false;
			N64SubMenu.Visible = false;
			DGBSubMenu.Visible = false;
			AppleSubMenu.Visible = false;
			C64SubMenu.Visible = false;
			IntvSubMenu.Visible = false;
			zXSpectrumToolStripMenuItem.Visible = false;
			amstradCPCToolStripMenuItem.Visible = false;

			switch (Emulator.SystemId)
			{
				default:
					DisplayDefaultCoreMenu();
					break;
				case "NULL":
					break;
				case "TI83":
					TI83SubMenu.Visible = true;
					break;
				case "NES":
					NESSubMenu.Visible = true;
					break;
				case "GB":
				case "GBC":
					GBSubMenu.Visible = true;
					break;
				case "NDS":
					NDSSubMenu.Visible = true;
					break;
				case "A78":
					A7800SubMenu.Visible = true;
					break;
				case "PSX":
					PSXSubMenu.Visible = true;
					break;
				case "SNES":
				case "SGB":
					if (Emulator is LibsnesCore bsnes)
					{
						SNESSubMenu.Text = bsnes.IsSGB ? "&SGB" : "&SNES";
						SNESSubMenu.Visible = true;
					}
					else if (Emulator is Snes9x || Emulator is Faust)
					{
						DisplayDefaultCoreMenu();
					}
					else if (Emulator is Sameboy)
					{
						GBSubMenu.Visible = true;
					}
					break;
				case "Coleco":
					ColecoSubMenu.Visible = true;
					break;
				case "N64":
					N64SubMenu.Visible = true;
					break;
				case "DGB":
					if (Emulator is GBHawkLink)
					{
						DisplayDefaultCoreMenu();
					}
					else
					{
						DGBSubMenu.Visible = true;
					}
					break;
				case "AppleII":
					AppleSubMenu.Visible = true;
					break;
				case "C64":
					C64SubMenu.Visible = true;
					break;
				case "INTV":
					IntvSubMenu.Visible = true;
					break;
				case "ZXSpectrum":
					zXSpectrumToolStripMenuItem.Visible = true;
					break;
				case "AmstradCPC":
					amstradCPCToolStripMenuItem.Visible = true;
					break;
			}
		}

		private static readonly IList<Type> SpecializedTools = ReflectionCache.Types
			.Where(t => typeof(IToolForm).IsAssignableFrom(t) && !t.IsAbstract)
			.Where(t => t.GetCustomAttribute<SpecializedToolAttribute>() != null)
			.ToList();

		private void DisplayDefaultCoreMenu()
		{
			GenericCoreSubMenu.Visible = true;
			GenericCoreSubMenu.Text = "&" + EmulatorExtensions.DisplayName(Emulator);
			GenericCoreSubMenu.DropDownItems.Clear();

			var settingsMenuItem = new ToolStripMenuItem { Text = "&Settings" };
			settingsMenuItem.Click += GenericCoreSettingsMenuItem_Click;
			GenericCoreSubMenu.DropDownItems.Add(settingsMenuItem);

			var specializedTools = SpecializedTools
				.Where(t => Tools.IsAvailable(t))
				.OrderBy(t => t.Name)
				.ToList();

			if (specializedTools.Any())
			{
				GenericCoreSubMenu.DropDownItems.Add(new ToolStripSeparator());
				foreach (var tool in specializedTools)
				{
					var dispName = tool.GetCustomAttribute<SpecializedToolAttribute>().DisplayName;
					var item = new ToolStripMenuItem
					{
						Text = "&" + dispName
					};

					item.Click += (o, e) =>
					{
						Tools.Load(tool);
					};

					GenericCoreSubMenu.DropDownItems.Add(item);
				}
			}
		}

		private void InitControls()
		{
			var controls = new Controller(
				new ControllerDefinition
				{
					Name = "Emulator Frontend Controls",
					BoolButtons = Config.HotkeyBindings.Select(x => x.DisplayName).ToList()
				});

			foreach (var b in Config.HotkeyBindings)
			{
				controls.BindMulti(b.DisplayName, b.Bindings);
			}

			InputManager.ClientControls = controls;
			_autofireNullControls = new AutofireController(
				Emulator,
				Config.AutofireOn,
				Config.AutofireOff);
		}

		private void LoadMoviesFromRecent(string path)
		{
			if (File.Exists(path))
			{
				var movie = MovieSession.Get(path);
				MovieSession.ReadOnly = true;
				StartNewMovie(movie, false);
			}
			else
			{
				Config.RecentMovies.HandleLoadError(this, path);
			}
		}

		private void LoadRomFromRecent(string rom)
		{
			var ioa = OpenAdvancedSerializer.ParseWithLegacy(rom);

			var args = new LoadRomArgs
			{
				OpenAdvanced = ioa
			};

			// if(ioa is this or that) - for more complex behaviour
			string romPath = ioa.SimplePath;

			if (!LoadRom(romPath, args, out var failureIsFromAskSave))
			{
				if (failureIsFromAskSave) OSD.AddMessage("ROM loading cancelled; a tool had unsaved changes");
				else Config.RecentRoms.HandleLoadError(this, romPath, rom);
			}
		}

		private void SetPauseStatusBarIcon()
		{
			if (EmulatorPaused)
			{
				PauseStatusButton.Image = Properties.Resources.Pause;
				PauseStatusButton.Visible = true;
				PauseStatusButton.ToolTipText = "Emulator Paused";
			}
			else if (IsTurboSeeking)
			{
				PauseStatusButton.Image = Properties.Resources.Lightning;
				PauseStatusButton.Visible = true;
				// ReSharper disable once PossibleInvalidOperationException
				PauseStatusButton.ToolTipText = $"Emulator is turbo seeking to frame {PauseOnFrame.Value} click to stop seek";
			}
			else if (PauseOnFrame.HasValue)
			{
				PauseStatusButton.Image = Properties.Resources.YellowRight;
				PauseStatusButton.Visible = true;
				PauseStatusButton.ToolTipText = $"Emulator is playing to frame {PauseOnFrame.Value} click to stop seek";
			}
			else
			{
				PauseStatusButton.Image = Properties.Resources.Blank;
				PauseStatusButton.Visible = false;
				PauseStatusButton.ToolTipText = "";
			}
		}

		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 = Rewinder?.Active == true && (InputManager.ClientControls["Rewind"] || PressRewind);
			var fastForward = InputManager.ClientControls["Fast Forward"] || FastForward;
			var turbo = IsTurboing;

			int speedPercent = fastForward ? Config.SpeedPercentAlternate : Config.SpeedPercent;

			if (rewind)
			{
				speedPercent = Math.Max(speedPercent / Rewinder.RewindFrequency, 5);
			}

			DisableSecondaryThrottling = _unthrottled || turbo || fastForward || rewind;

			// realtime throttle is never going to be so exact that using a double here is wrong
			_throttle.SetCoreFps(Emulator.VsyncRate());
			_throttle.signal_paused = EmulatorPaused;
			_throttle.signal_unthrottle = _unthrottled || turbo;

			// zero 26-mar-2016 - vsync and vsync throttle here both is odd, but see comments elsewhere about triple buffering
			_throttle.signal_overrideSecondaryThrottle = (fastForward || rewind) && (Config.SoundThrottle || Config.VSyncThrottle || Config.VSync);
			_throttle.SetSpeedPercent(speedPercent);
		}

		private void SetSpeedPercentAlternate(int value)
		{
			Config.SpeedPercentAlternate = value;
			SyncThrottle();
			AddOnScreenMessage($"Alternate Speed: {value}%");
		}

		private void SetSpeedPercent(int value)
		{
			Config.SpeedPercent = value;
			SyncThrottle();
			AddOnScreenMessage($"Speed: {value}%");
		}

		private void Shutdown()
		{
			if (_currAviWriter != null)
			{
				_currAviWriter.CloseFile();
				_currAviWriter = null;
			}
		}

		private void CheckMessages()
		{
			Application.DoEvents();
			if (ActiveForm != null)
			{
				ScreenSaver.ResetTimerPeriodically();
			}

			List<string[]> todo = new();
			lock (_singleInstanceForwardedArgs)
			{
				if (_singleInstanceForwardedArgs.Count > 0)
				{
					todo = new List<string[]>(_singleInstanceForwardedArgs);
					_singleInstanceForwardedArgs.Clear();
				}
			}
			foreach (var args in todo) SingleInstanceProcessArgs(args);
		}

		private void AutohideCursor(bool hide)
		{
			if (hide && !_cursorHidden)
			{
				_presentationPanel.Control.Cursor = Properties.Resources.BlankCursor;
				_cursorHidden = true;
			}
			else if (!hide && _cursorHidden)
			{
				_presentationPanel.Control.Cursor = Cursors.Default;
				timerMouseIdle.Stop();
				timerMouseIdle.Start();
				_cursorHidden = false;
			}
		}

		public BitmapBuffer MakeScreenshotImage()
		{
			return DisplayManager.RenderVideoProvider(_currentVideoProvider);
		}

		private void SaveSlotSelectedMessage()
		{
			int slot = Config.SaveSlot;
			string emptyPart = HasSlot(slot) ? "" : " (empty)";
			string message = $"Slot {slot}{emptyPart} selected.";
			AddOnScreenMessage(message);
		}

		/*internal*/public void Render()
		{
			if (Config.DispSpeedupFeatures == 0)
			{
				return;
			}

			var video = _currentVideoProvider;
			Size currVideoSize = new Size(video.BufferWidth, video.BufferHeight);
			Size currVirtualSize = new Size(video.VirtualWidth, video.VirtualHeight);


			bool resizeFramebuffer = currVideoSize != _lastVideoSize || currVirtualSize != _lastVirtualSize;

			bool isZero = currVideoSize.Width == 0 || currVideoSize.Height == 0 || currVirtualSize.Width == 0 || currVirtualSize.Height == 0;

			//don't resize if the new size is 0 somehow; we'll wait until we have a sensible size
			if (isZero)
			{
				resizeFramebuffer = false;
			}

			if (resizeFramebuffer)
			{
				_lastVideoSize = currVideoSize;
				_lastVirtualSize = currVirtualSize;
				FrameBufferResized();
			}

			//rendering flakes out egregiously if we have a zero size
			//can we fix it later not to?
			if (isZero)
				DisplayManager.Blank();
			else
				DisplayManager.UpdateSource(video);
		}

		// sends a simulation of a plain alt key keystroke
		private void SendPlainAltKey(int lparam)
		{
			var m = new Message { WParam = new IntPtr(0xF100), LParam = new IntPtr(lparam), Msg = 0x0112, HWnd = Handle };
			base.WndProc(ref m);
		}

		// sends an alt+mnemonic combination
		private void SendAltKeyChar(char c)
		{
			switch (OSTailoredCode.CurrentOS)
			{
				case OSTailoredCode.DistinctOS.Linux:
				case OSTailoredCode.DistinctOS.macOS:
					// no mnemonics for you
					break;
				case OSTailoredCode.DistinctOS.Windows:
					//HACK
					var _ = typeof(ToolStrip).InvokeMember(
						"ProcessMnemonicInternal",
						BindingFlags.NonPublic | BindingFlags.InvokeMethod | BindingFlags.Instance,
						null,
						MainformMenu,
						new object[] { c });
					break;
			}
		}

		public static readonly string ConfigFileFSFilterString = new FilesystemFilter("Config File", new[] { "ini" }).ToString();

		private void OpenRom()
		{
			using var ofd = new OpenFileDialog
			{
				InitialDirectory = Config.PathEntries.RomAbsolutePath(Emulator.SystemId),
				Filter = RomLoader.RomFilter,
				RestoreDirectory = false,
				FilterIndex = _lastOpenRomFilter
			};

			if (this.ShowDialogWithTempMute(ofd) != DialogResult.OK) return;

			var file = new FileInfo(ofd.FileName);
			_lastOpenRomFilter = ofd.FilterIndex;

			var lra = new LoadRomArgs { OpenAdvanced = new OpenAdvanced_OpenRom { Path = file.FullName } };
			LoadRom(file.FullName, lra);
		}

		private void CoreSyncSettings(object sender, RomLoader.SettingsLoadArgs e)
		{
			if (MovieSession.NewMovieQueued)
			{
				if (!string.IsNullOrWhiteSpace(MovieSession.QueuedSyncSettings))
				{
					e.Settings = ConfigService.LoadWithType(MovieSession.QueuedSyncSettings);
				}
				else
				{
					e.Settings = Config.GetCoreSyncSettings(e.Core, e.SettingsType);

					// Only show this nag if the core actually has sync settings, not all cores do
					if (e.Settings != null && !_suppressSyncSettingsWarning)
					{
						ShowMessageBox(
							owner: null,
							"No sync settings found, using currently configured settings for this core.",
							"No sync settings found",
							EMsgBoxIcon.Warning);
					}
				}
			}
			else
			{
				e.Settings = Config.GetCoreSyncSettings(e.Core, e.SettingsType);
			}
		}

		private void CoreSettings(object sender, RomLoader.SettingsLoadArgs e)
		{
			e.Settings = Config.GetCoreSettings(e.Core, e.SettingsType);
		}

		/// <summary>
		/// send core settings to emu, setting reboot flag if needed
		/// </summary>
		public void PutCoreSettings(object o)
		{
			var settable = new SettingsAdapter(Emulator);
			if (!settable.HasSettings)
				return;
			var dirty = settable.PutSettings(o);
			if(dirty.HasFlag(PutSettingsDirtyBits.RebootCore))
				FlagNeedsReboot();
			if (dirty.HasFlag(PutSettingsDirtyBits.ScreenLayoutChanged))
				FrameBufferResized();
		}

		// TODO: Get/Put settings/sync settings methods could become a service we instantiate and use and pass to other forms
		/// <summary>
		/// send core sync settings to emu, setting reboot flag if needed
		/// </summary>
		public void PutCoreSyncSettings(object o)
		{
			var settable = new SettingsAdapter(Emulator);
			if (MovieSession.Movie.IsActive())
			{
				AddOnScreenMessage("Attempt to change sync-relevant settings while recording BLOCKED.");
			}
			else
			{
				if (!settable.HasSyncSettings)
					return;
				var dirty = settable.PutSyncSettings(o);
				if(dirty.HasFlag(PutSettingsDirtyBits.RebootCore))
					FlagNeedsReboot();
			}
		}

		private void SaveConfig(string path = "")
		{
			if (Config.SaveWindowPosition)
			{
				if (Config.MainWndx != -32000) // When minimized location is -32000, don't save this into the config file!
				{
					Config.MainWndx = Location.X;
				}

				if (Config.MainWndy != -32000)
				{
					Config.MainWndy = Location.Y;
				}
			}
			else
			{
				Config.MainWndx = -1;
				Config.MainWndy = -1;
			}

			Config.LastWrittenFrom = VersionInfo.MainVersion;
			Config.LastWrittenFromDetailed = VersionInfo.GetEmuVersion();

			if (string.IsNullOrEmpty(path))
			{
				path = Config.DefaultIniPath;
			}

			ConfigService.Save(path, Config);
		}

		private void ToggleFps()
		{
			Config.DisplayFps ^= true;
		}

		private void ToggleFrameCounter()
		{
			Config.DisplayFrameCounter ^= true;
		}

		private void ToggleLagCounter()
		{
			Config.DisplayLagCounter ^= true;
		}

		private void ToggleInputDisplay()
		{
			Config.DisplayInput ^= true;
		}

		public void ToggleSound()
		{
			Config.SoundEnabled ^= true;
			Sound.StopSound();
			Sound.StartSound();
		}

		private void VolumeUp()
		{
			Config.SoundVolume += 10;
			if (Config.SoundVolume > 100)
			{
				Config.SoundVolume = 100;
			}

			AddOnScreenMessage($"Volume {Config.SoundVolume}");
		}

		private void VolumeDown()
		{
			Config.SoundVolume -= 10;
			if (Config.SoundVolume < 0)
			{
				Config.SoundVolume = 0;
			}

			AddOnScreenMessage($"Volume {Config.SoundVolume}");
		}

		private void SoftReset()
		{
			// is it enough to run this for one frame? maybe..
			if (Emulator.ControllerDefinition.BoolButtons.Contains("Reset")
				&& !MovieSession.Movie.IsPlaying())
			{
				InputManager.ClickyVirtualPadController.Click("Reset");
				AddOnScreenMessage("Reset button pressed.");
			}
		}

		private void HardReset()
		{
			// is it enough to run this for one frame? maybe..
			if (Emulator.ControllerDefinition.BoolButtons.Contains("Power")
				&& !MovieSession.Movie.IsPlaying())
			{
				InputManager.ClickyVirtualPadController.Click("Power");
				AddOnScreenMessage("Power button pressed.");
			}
		}

		private Color SlotForeColor(int slot)
		{
			return HasSlot(slot)
				? Config.SaveSlot == slot
					? SystemColors.HighlightText
					: SystemColors.WindowText
				: SystemColors.GrayText;
		}

		private Color SlotBackColor(int slot)
		{
			return  Config.SaveSlot == slot
				? SystemColors.Highlight
				: SystemColors.Control;
		}

		public void UpdateStatusSlots()
		{
			_stateSlots.Update(Emulator, MovieSession.Movie, SaveStatePrefix());

			Slot0StatusButton.ForeColor = SlotForeColor(0);
			Slot1StatusButton.ForeColor = SlotForeColor(1);
			Slot2StatusButton.ForeColor = SlotForeColor(2);
			Slot3StatusButton.ForeColor = SlotForeColor(3);
			Slot4StatusButton.ForeColor = SlotForeColor(4);
			Slot5StatusButton.ForeColor = SlotForeColor(5);
			Slot6StatusButton.ForeColor = SlotForeColor(6);
			Slot7StatusButton.ForeColor = SlotForeColor(7);
			Slot8StatusButton.ForeColor = SlotForeColor(8);
			Slot9StatusButton.ForeColor = SlotForeColor(9);

			Slot0StatusButton.BackColor = SlotBackColor(0);
			Slot1StatusButton.BackColor = SlotBackColor(1);
			Slot2StatusButton.BackColor = SlotBackColor(2);
			Slot3StatusButton.BackColor = SlotBackColor(3);
			Slot4StatusButton.BackColor = SlotBackColor(4);
			Slot5StatusButton.BackColor = SlotBackColor(5);
			Slot6StatusButton.BackColor = SlotBackColor(6);
			Slot7StatusButton.BackColor = SlotBackColor(7);
			Slot8StatusButton.BackColor = SlotBackColor(8);
			Slot9StatusButton.BackColor = SlotBackColor(9);

			SaveSlotsStatusLabel.Visible =
				Slot0StatusButton.Visible =
				Slot1StatusButton.Visible =
				Slot2StatusButton.Visible =
				Slot3StatusButton.Visible =
				Slot4StatusButton.Visible =
				Slot5StatusButton.Visible =
				Slot6StatusButton.Visible =
				Slot7StatusButton.Visible =
				Slot8StatusButton.Visible =
				Slot9StatusButton.Visible =
				Emulator.HasSavestates();
		}

		public BitmapBuffer CaptureOSD()
		{
			var bb = DisplayManager.RenderOffscreen(_currentVideoProvider, true);
			bb.DiscardAlpha();
			return bb;
		}
		public BitmapBuffer CaptureLua()
		{
			var bb = DisplayManager.RenderOffscreenLua(_currentVideoProvider);
			bb.DiscardAlpha();
			return bb;
		}

		private void IncreaseWindowSize()
		{
			switch (Config.TargetZoomFactors[Emulator.SystemId])
			{
				case 1:
					Config.TargetZoomFactors[Emulator.SystemId] = 2;
					break;
				case 2:
					Config.TargetZoomFactors[Emulator.SystemId] = 3;
					break;
				case 3:
					Config.TargetZoomFactors[Emulator.SystemId] = 4;
					break;
				case 4:
					Config.TargetZoomFactors[Emulator.SystemId] = 5;
					break;
				case 5:
					Config.TargetZoomFactors[Emulator.SystemId] = 10;
					break;
				case 10:
					return;
			}

			AddOnScreenMessage($"Screensize set to {Config.TargetZoomFactors[Emulator.SystemId]}x");
			FrameBufferResized();
		}

		private void DecreaseWindowSize()
		{
			switch (Config.TargetZoomFactors[Emulator.SystemId])
			{
				case 1:
					return;
				case 2:
					Config.TargetZoomFactors[Emulator.SystemId] = 1;
					break;
				case 3:
					Config.TargetZoomFactors[Emulator.SystemId] = 2;
					break;
				case 4:
					Config.TargetZoomFactors[Emulator.SystemId] = 3;
					break;
				case 5:
					Config.TargetZoomFactors[Emulator.SystemId] = 4;
					break;
				case 10:
					Config.TargetZoomFactors[Emulator.SystemId] = 5;
					return;
			}

			AddOnScreenMessage($"Screensize set to {Config.TargetZoomFactors[Emulator.SystemId]}x");
			FrameBufferResized();
		}

		private static readonly int[] SpeedPercents = { 1, 3, 6, 12, 25, 50, 75, 100, 150, 200, 300, 400, 800, 1600, 3200, 6400 };

		private bool CheckCanSetSpeed()
		{
			if (Config.ClockThrottle)
				return true;
			
			AddOnScreenMessage("Unable to change speed, please switch to clock throttle");
			return false;
		}

		private void ResetSpeed()
		{
			if (!CheckCanSetSpeed())
				return;

			SetSpeedPercent(100);
		}

		private void IncreaseSpeed()
		{
			if (!CheckCanSetSpeed())
				return;

			var oldPercent = Config.SpeedPercent;
			int newPercent;

			int i = 0;
			do
			{
				i++;
				newPercent = SpeedPercents[i];
			}
			while (newPercent <= oldPercent && i < SpeedPercents.Length - 1);

			SetSpeedPercent(newPercent);
		}

		private void DecreaseSpeed()
		{
			if (!CheckCanSetSpeed())
				return;

			var oldPercent = Config.SpeedPercent;
			int newPercent;

			int i = SpeedPercents.Length - 1;
			do
			{
				i--;
				newPercent = SpeedPercents[i];
			}
			while (newPercent >= oldPercent && i > 0);

			SetSpeedPercent(newPercent);
		}

		private void SaveMovie()
		{
			if (MovieSession.Movie.IsActive())
			{
				MovieSession.Movie.Save();
				AddOnScreenMessage($"{MovieSession.Movie.Filename} saved.");
			}
		}

		private void HandleToggleLightAndLink()
		{
			if (MainStatusBar.Visible)
			{
				var hasDriveLight = Emulator.HasDriveLight() && Emulator.AsDriveLight().DriveLightEnabled;

				if (hasDriveLight)
				{
					if (!LedLightStatusLabel.Visible)
					{
						LedLightStatusLabel.Visible = true;
					}

					LedLightStatusLabel.Image = Emulator.AsDriveLight().DriveLightOn
						? _statusBarDiskLightOnImage
						: _statusBarDiskLightOffImage;
				}
				else
				{
					if (LedLightStatusLabel.Visible)
					{
						LedLightStatusLabel.Visible = false;
					}
				}

				if (Emulator.UsesLinkCable())
				{
					if (!LinkConnectStatusBarButton.Visible)
					{
						LinkConnectStatusBarButton.Visible = true;
					}

					LinkConnectStatusBarButton.Image = Emulator.AsLinkable().LinkConnected
						? _linkCableOn
						: _linkCableOff;

					LinkConnectStatusBarButton.ToolTipText = $"Link connection is currently {(Emulator.AsLinkable().LinkConnected ? "enabled" : "disabled")}";
				}
				else
				{
					if (LinkConnectStatusBarButton.Visible)
					{
						LinkConnectStatusBarButton.Visible = false;
					}
				}
			}
		}

		private void UpdateKeyPriorityIcon()
		{
			switch (Config.InputHotkeyOverrideOptions)
			{
				default:
				case 0:
					KeyPriorityStatusLabel.Image = Properties.Resources.Both;
					KeyPriorityStatusLabel.ToolTipText = "Key priority: Allow both hotkeys and controller buttons";
					break;
				case 1:
					KeyPriorityStatusLabel.Image = Properties.Resources.GameController;
					KeyPriorityStatusLabel.ToolTipText = "Key priority: Controller buttons will override hotkeys";
					break;
				case 2:
					KeyPriorityStatusLabel.Image = Properties.Resources.HotKeys;
					KeyPriorityStatusLabel.ToolTipText = "Key priority: Hotkeys will override controller buttons";
					break;
			}
		}

		private void ToggleBackgroundInput()
		{
			Config.AcceptBackgroundInput ^= true;
			AddOnScreenMessage($"Background Input {(Config.AcceptBackgroundInput ? "enabled" : "disabled")}");
		}

		private void VsyncMessage()
		{
			AddOnScreenMessage($"Display Vsync set to {(Config.VSync ? "on" : "off")}");
		}

		private void FdsInsertDiskMenuAdd(string name, string button, string msg)
		{
			FDSControlsMenuItem.DropDownItems.Add(name, null, (sender, e) =>
			{
				if (Emulator.ControllerDefinition.BoolButtons.Contains(button)
					&& !MovieSession.Movie.IsPlaying())
				{
					InputManager.ClickyVirtualPadController.Click(button);
					AddOnScreenMessage(msg);
				}
			});
		}

		private const int WmDeviceChange = 0x0219;

		// Alt key hacks
		protected override void WndProc(ref Message m)
		{
			if (m.Msg == WmDeviceChange) Input.Instance.Adapter.ReInitGamepads(Handle);

			// this is necessary to trap plain alt keypresses so that only our hotkey system gets them
			if (m.Msg == 0x0112) // WM_SYSCOMMAND
			{
				if (m.WParam.ToInt32() == 0xF100) // SC_KEYMENU
				{
					return;
				}
			}

			base.WndProc(ref m);
		}

		protected override bool ProcessDialogChar(char charCode)
		{
			// this is necessary to trap alt+char combinations so that only our hotkey system gets them
			return (ModifierKeys & Keys.Alt) != 0 || base.ProcessDialogChar(charCode);
		}

		private void UpdateCoreStatusBarButton()
		{
			var coreDispName = CoreExtensions.CoreExtensions.DisplayName(Emulator);
			LoadedCoreNameMenuItem.Text = $"Loaded core: {coreDispName} ({Emulator.SystemId})";
			if (Emulator.IsNull())
			{
				CoreNameStatusBarButton.Visible = false;
				return;
			}

			CoreNameStatusBarButton.Visible = true;
			var attributes = Emulator.Attributes();

			CoreNameStatusBarButton.Text = coreDispName;
			CoreNameStatusBarButton.Image = Emulator.Icon();
			CoreNameStatusBarButton.ToolTipText = attributes.Ported ? "(ported) " : "";


			if (Emulator.SystemId == "ZXSpectrum")
			{
				var core = (Emulation.Cores.Computers.SinclairSpectrum.ZXSpectrum)Emulator;
				CoreNameStatusBarButton.ToolTipText = core.GetMachineType();
			}

			if (Emulator.SystemId == "AmstradCPC")
			{
				var core = (Emulation.Cores.Computers.AmstradCPC.AmstradCPC)Emulator;
				CoreNameStatusBarButton.ToolTipText = core.GetMachineType();
			}
		}

		private void ToggleKeyPriority()
		{
			Config.InputHotkeyOverrideOptions++;
			if (Config.InputHotkeyOverrideOptions > 2)
			{
				Config.InputHotkeyOverrideOptions = 0;
			}

			if (Config.NoMixedInputHokeyOverride && Config.InputHotkeyOverrideOptions == 0)
			{
				Config.InputHotkeyOverrideOptions = 1;
			}

			UpdateKeyPriorityIcon();
			switch (Config.InputHotkeyOverrideOptions)
			{
				case 0:
					AddOnScreenMessage("Key priority set to Both Hotkey and Input");
					break;
				case 1:
					AddOnScreenMessage("Key priority set to Input over Hotkey");
					break;
				case 2:
					AddOnScreenMessage("Key priority set to Input");
					break;
			}
		}

		private void LoadConfigFile(string iniPath)
		{
			if (!VersionInfo.DeveloperBuild && !ConfigService.IsFromSameVersion(iniPath, out var msg))
			{
				new MsgBox(msg, "Mismatched version in config file", MessageBoxIcon.Warning).ShowDialog();
			}
			Config = ConfigService.Load<Config>(iniPath);
			Config.ResolveDefaults();
			InitControls(); // rebind hotkeys
			InputManager.SyncControls(Emulator, MovieSession, Config);
			Tools.Restart(Config, Emulator, Game);
			ExtToolManager.Restart(Config.PathEntries);
			Sound.Config = Config;
			DisplayManager.UpdateGlobals(Config, Emulator);
			AddOnScreenMessage($"Config file loaded: {iniPath}");
		}

		/*internal*/public void StepRunLoop_Throttle()
		{
			SyncThrottle();
			_throttle.signal_frameAdvance = _runloopFrameAdvance;
			_throttle.signal_continuousFrameAdvancing = _runloopFrameProgress;

			_throttle.Step(Config, Sound, allowSleep: true, forceFrameSkip: -1);
		}

		public void FrameAdvance()
		{
			PressFrameAdvance = true;
			StepRunLoop_Core(true);
		}

		public void SeekFrameAdvance()
		{
			PressFrameAdvance = true;
			StepRunLoop_Core(true);
			PressFrameAdvance = false;
		}

		private void StepRunLoop_Core(bool force = false)
		{
			var runFrame = false;
			_runloopFrameAdvance = false;
			var currentTimestamp = Stopwatch.GetTimestamp();

			double frameAdvanceTimestampDeltaMs = (double)(currentTimestamp - _frameAdvanceTimestamp) / Stopwatch.Frequency * 1000.0;
			bool frameProgressTimeElapsed = frameAdvanceTimestampDeltaMs >= Config.FrameProgressDelayMs;

			if (Config.SkipLagFrame && Emulator.CanPollInput() && Emulator.AsInputPollable().IsLagFrame && frameProgressTimeElapsed && Emulator.Frame > 0)
			{
				runFrame = true;
			}


			bool oldFrameAdvanceCondition = InputManager.ClientControls["Frame Advance"] || PressFrameAdvance || HoldFrameAdvance;
			if (FrameInch)
			{
				FrameInch = false;
				if (EmulatorPaused || oldFrameAdvanceCondition)
				{
					oldFrameAdvanceCondition = true;
				}
				else
				{
					PauseEmulator();
					oldFrameAdvanceCondition = false;
				}
			}

			if (oldFrameAdvanceCondition || FrameInch)
			{
				FrameInch = false;
				_runloopFrameAdvance = true;

				// handle the initial trigger of a frame advance
				if (_frameAdvanceTimestamp == 0)
				{
					PauseEmulator();
					runFrame = 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 = Rewind(ref runFrame, currentTimestamp, out var returnToRecording);

			float atten = 0;

			// BlockFrameAdvance (true when input it being editted in TAStudio) supercedes all other frame advance conditions
			if ((runFrame || force) && !BlockFrameAdvance)
			{
				var isFastForwarding = InputManager.ClientControls["Fast Forward"] || IsTurboing || InvisibleEmulation;
				var isFastForwardingOrRewinding = isFastForwarding || isRewinding || _unthrottled;

				if (isFastForwardingOrRewinding != _lastFastForwardingOrRewinding)
				{
					InitializeFpsData();
				}

				_lastFastForwardingOrRewinding = isFastForwardingOrRewinding;

				// client input-related duties
				OSD.ClearGuiText();

				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
				InputManager.ClickyVirtualPadController.FrameTick();
				InputManager.ButtonOverrideAdapter.FrameTick();

				if (IsTurboing)
				{
					Tools.FastUpdateBefore();
				}
				else
				{
					Tools.UpdateToolsBefore();
				}

				if (!InvisibleEmulation)
				{
					CaptureRewind(isRewinding);
				}

				// Set volume, if enabled
				if (Config.SoundEnabledNormal && !InvisibleEmulation)
				{
					atten = Config.SoundVolume / 100.0f;

					if (isFastForwardingOrRewinding)
					{
						if (Config.SoundEnabledRWFF)
						{
							atten *= Config.SoundVolumeRWFF / 100.0f;
						}
						else
						{
							atten = 0;
						}
					}

					// Mute if using Frame Advance/Frame Progress
					if (_runloopFrameAdvance && Config.MuteFrameAdvance)
					{
						atten = 0;
					}
				}

				MovieSession.HandleFrameBefore();

				if (Config.AutosaveSaveRAM)
				{
					if (AutoFlushSaveRamIn-- <= 0)
					{
						FlushSaveRAM(true);
					}
				}
				// why not skip audio if the user doesn't want sound
				bool renderSound = (Config.SoundEnabled && !IsTurboing)
					|| (_currAviWriter?.UsesAudio ?? false);
				if (!renderSound)
				{
					atten = 0;
				}

				bool render = !InvisibleEmulation && (!_throttle.skipNextFrame || (_currAviWriter?.UsesVideo ?? false));
				bool newFrame = Emulator.FrameAdvance(InputManager.ControllerOutput, render, renderSound);

				MovieSession.HandleFrameAfter();

				if (returnToRecording)
				{
					MovieSession.Movie.SwitchToRecord();
				}

				if (isRewinding && !IsRewindSlave && MovieSession.Movie.IsRecording())
				{
					MovieSession.Movie.Truncate(Emulator.Frame);
				}

				CheatList.Pulse();

				if (Emulator.CanPollInput() && Emulator.AsInputPollable().IsLagFrame && Config.AutofireLagFrames)
				{
					InputManager.AutoFireController.IncrementStarts();
				}

				InputManager.AutofireStickyXorAdapter.IncrementLoops(Emulator.CanPollInput() && Emulator.AsInputPollable().IsLagFrame);

				PressFrameAdvance = false;

				if (IsTurboing)
				{
					Tools.FastUpdateAfter();
				}
				else
				{
					UpdateToolsAfter();
				}

				if (!PauseAvi && newFrame && !InvisibleEmulation)
				{
					AvFrameAdvance();
				}

				if (newFrame)
				{
					_framesSinceLastFpsUpdate++;

					UpdateFpsDisplay(currentTimestamp, isRewinding, isFastForwarding);
				}

				if (Tools.IsLoaded<TAStudio>() &&
					Tools.TAStudio.LastPositionFrame == Emulator.Frame)
				{
					if (PauseOnFrame.HasValue &&
						PauseOnFrame.Value <= Tools.TAStudio.LastPositionFrame)
					{
						var record = (MovieSession.Movie as ITasMovie)[Emulator.Frame];
						if (!record.Lagged.HasValue && IsSeeking)
						{
							// haven't yet greenzoned the frame, hence it's after editing
							// then we want to pause here. taseditor fashion
							PauseEmulator();
						}
					}
				}

				if (IsSeeking && Emulator.Frame == PauseOnFrame.Value)
				{
					PauseEmulator();
					if (Tools.IsLoaded<TAStudio>())
					{
						Tools.TAStudio.StopSeeking();
					}
					PauseOnFrame = null;
				}
			}

			if (InputManager.ClientControls["Rewind"] || PressRewind)
			{
				UpdateToolsAfter();
			}

			Sound.UpdateSound(atten, DisableSecondaryThrottling);
		}

		private void UpdateFpsDisplay(long currentTimestamp, bool isRewinding, bool isFastForwarding)
		{
			double elapsedSeconds = (currentTimestamp - _timestampLastFpsUpdate) / (double)Stopwatch.Frequency;

			if (elapsedSeconds < 1.0 / _fpsUpdatesPerSecond)
			{
				return;
			}

			if (_lastFps == 0) // Initial calculation
			{
				_lastFps = (_framesSinceLastFpsUpdate - 1) / elapsedSeconds;
			}
			else
			{
				_lastFps = (_lastFps + (_framesSinceLastFpsUpdate * _fpsSmoothing)) / (1.0 + (elapsedSeconds * _fpsSmoothing));
			}

			_framesSinceLastFpsUpdate = 0;
			_timestampLastFpsUpdate = currentTimestamp;

			var fpsString = $"{_lastFps:0} fps";
			if (isRewinding)
			{
				fpsString += IsTurboing || isFastForwarding ?
					" <<<<" :
					" <<";
			}
			else if (isFastForwarding)
			{
				fpsString += IsTurboing ?
					" >>>>" :
					" >>";
			}

			OSD.Fps = fpsString;

			// need to refresh window caption in this case
			if (Config.DispSpeedupFeatures == 0)
			{
				SetWindowText();
			}
		}

		private void InitializeFpsData()
		{
			_lastFps = 0;
			_timestampLastFpsUpdate = Stopwatch.GetTimestamp();
			_framesSinceLastFpsUpdate = 0;
		}

		/// <summary>
		/// start AVI recording, unattended
		/// </summary>
		/// <param name="videoWriterName">match the short name of an <seealso cref="IVideoWriter"/></param>
		/// <param name="filename">filename to save to</param>
		private void RecordAv(string videoWriterName, string filename)
		{
			RecordAvBase(videoWriterName, filename, true);
		}

		/// <summary>
		/// start AV recording, asking user for filename and options
		/// </summary>
		private void RecordAv()
		{
			RecordAvBase(null, null, false);
		}

		/// <summary>
		/// start AV recording
		/// </summary>
		private void RecordAvBase(string videoWriterName, string filename, bool unattended)
		{
			if (_currAviWriter != null) return;

			// select IVideoWriter to use
			IVideoWriter aw;

			if (string.IsNullOrEmpty(videoWriterName) && !string.IsNullOrEmpty(Config.VideoWriter))
			{
				videoWriterName = Config.VideoWriter;
			}

			_dumpaudiosync = Config.VideoWriterAudioSync;
			if (unattended && !string.IsNullOrEmpty(videoWriterName))
			{
				aw = VideoWriterInventory.GetVideoWriter(videoWriterName, this);
			}
			else
			{
				aw = VideoWriterChooserForm.DoVideoWriterChooserDlg(
					VideoWriterInventory.GetAllWriters(),
					this,
					Emulator,
					Config,
					out _avwriterResizew,
					out _avwriterResizeh,
					out _avwriterpad,
					ref _dumpaudiosync);
			}

			if (aw == null)
			{
				AddOnScreenMessage(
					unattended ? $"Couldn't start video writer \"{videoWriterName}\"" : "A/V capture canceled.");

				return;
			}

			try
			{
				bool usingAvi = aw is AviWriter; // SO GROSS!

				if (_dumpaudiosync)
				{
					aw = new VideoStretcher(aw);
				}
				else
				{
					aw = new AudioStretcher(aw);
				}

				aw.SetMovieParameters(Emulator.VsyncNumerator(), Emulator.VsyncDenominator());
				if (_avwriterResizew > 0 && _avwriterResizeh > 0)
				{
					aw.SetVideoParameters(_avwriterResizew, _avwriterResizeh);
				}
				else
				{
					aw.SetVideoParameters(_currentVideoProvider.BufferWidth, _currentVideoProvider.BufferHeight);
				}

				aw.SetAudioParameters(44100, 2, 16);

				// select codec token
				// do this before save dialog because ffmpeg won't know what extension it wants until it's been configured
				if (unattended && !string.IsNullOrEmpty(filename))
				{
					aw.SetDefaultVideoCodecToken(Config);
				}
				else
				{
					// THIS IS REALLY SLOPPY!
					// PLEASE REDO ME TO NOT CARE WHICH AVWRITER IS USED!
					if (usingAvi && !string.IsNullOrEmpty(Config.AviCodecToken))
					{
						aw.SetDefaultVideoCodecToken(Config);
					}

					var token = aw.AcquireVideoCodecToken(Config);
					if (token == null)
					{
						AddOnScreenMessage("A/V capture canceled.");
						aw.Dispose();
						return;
					}

					aw.SetVideoCodecToken(token);
				}

				// select file to save to
				if (unattended && !string.IsNullOrEmpty(filename))
				{
					aw.OpenFile(filename);
				}
				else
				{
					string ext = aw.DesiredExtension();
					string pathForOpenFile;

					// handle directories first
					if (ext == "<directory>")
					{
						using var fbd = new FolderBrowserEx();
						if (fbd.ShowDialog() == DialogResult.Cancel)
						{
							aw.Dispose();
							return;
						}

						pathForOpenFile = fbd.SelectedPath;
					}
					else
					{
						using var sfd = new SaveFileDialog();
						if (Game != null)
						{
							sfd.FileName = $"{Game.FilesystemSafeName()}.{ext}"; // don't use Path.ChangeExtension, it might wreck game names with dots in them
							sfd.InitialDirectory = Config.PathEntries.AvAbsolutePath();
						}
						else
						{
							sfd.FileName = "NULL";
							sfd.InitialDirectory = Config.PathEntries.AvAbsolutePath();
						}

						sfd.Filter = new FilesystemFilterSet(new FilesystemFilter(ext, new[] { ext })).ToString();

						if (this.ShowDialogWithTempMute(sfd) == DialogResult.Cancel)
						{
							aw.Dispose();
							return;
						}

						pathForOpenFile = sfd.FileName;
					}

					aw.OpenFile(pathForOpenFile);
				}

				// commit the avi writing last, in case there were any errors earlier
				_currAviWriter = aw;
				AddOnScreenMessage("A/V capture started");
				AVIStatusLabel.Image = Properties.Resources.Avi;
				AVIStatusLabel.ToolTipText = "A/V capture in progress";
				AVIStatusLabel.Visible = true;
			}
			catch
			{
				AddOnScreenMessage("A/V capture failed!");
				aw.Dispose();
				throw;
			}

			if (_dumpaudiosync)
			{
				_currentSoundProvider.SetSyncMode(SyncSoundMode.Sync);
			}
			else
			{
				if (_currentSoundProvider.CanProvideAsync)
				{
					_currentSoundProvider.SetSyncMode(SyncSoundMode.Async);
					_aviSoundInputAsync = _currentSoundProvider;
				}
				else
				{
					_currentSoundProvider.SetSyncMode(SyncSoundMode.Sync);
					_aviSoundInputAsync = new SyncToAsyncProvider(() => Emulator.VsyncRate(), _currentSoundProvider);
				}
			}

			_dumpProxy = new SimpleSyncSoundProvider();
			RewireSound();
		}

		private void AbortAv()
		{
			if (_currAviWriter == null)
			{
				_dumpProxy = null;
				RewireSound();
				return;
			}

			_currAviWriter.Dispose();
			_currAviWriter = null;
			AddOnScreenMessage("A/V capture aborted");
			AVIStatusLabel.Image = Properties.Resources.Blank;
			AVIStatusLabel.ToolTipText = "";
			AVIStatusLabel.Visible = false;
			_aviSoundInputAsync = null;
			_dumpProxy = null; // return to normal sound output
			RewireSound();
		}

		private void StopAv()
		{
			if (_currAviWriter == null)
			{
				_dumpProxy = null;
				RewireSound();
				return;
			}

			_currAviWriter.CloseFile();
			_currAviWriter.Dispose();
			_currAviWriter = null;
			AddOnScreenMessage("A/V capture stopped");
			AVIStatusLabel.Image = Properties.Resources.Blank;
			AVIStatusLabel.ToolTipText = "";
			AVIStatusLabel.Visible = false;
			_aviSoundInputAsync = null;
			_dumpProxy = null; // return to normal sound output
			RewireSound();
		}

		private void AvFrameAdvance()
		{
			if (_currAviWriter == null) return;

			// is this the best time to handle this? or deeper inside?
			if (_argParser._currAviWriterFrameList?.Contains(Emulator.Frame) != false)
			{
				// TODO ZERO - this code is pretty jacked. we'll want to frugalize buffers better for speedier dumping, and we might want to rely on the GL layer for padding
				try
				{
					IVideoProvider output;
					IDisposable disposableOutput = null;
					if (_avwriterResizew > 0 && _avwriterResizeh > 0)
					{
						BitmapBuffer bbIn = null;
						Bitmap bmpIn = null;
						try
						{
							bbIn = Config.AviCaptureOsd
								? CaptureOSD()
								: new BitmapBuffer(_currentVideoProvider.BufferWidth, _currentVideoProvider.BufferHeight, _currentVideoProvider.GetVideoBuffer());

							bbIn.DiscardAlpha();

							var bmpOut = new Bitmap(_avwriterResizew, _avwriterResizeh, PixelFormat.Format32bppArgb);
							bmpIn = bbIn.ToSysdrawingBitmap();
							using (var g = Graphics.FromImage(bmpOut))
							{
								if (_avwriterpad)
								{
									g.Clear(Color.FromArgb(_currentVideoProvider.BackgroundColor));
									g.DrawImageUnscaled(bmpIn, (bmpOut.Width - bmpIn.Width) / 2, (bmpOut.Height - bmpIn.Height) / 2);
								}
								else
								{
									g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
									g.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.Half;
									g.DrawImage(bmpIn, new Rectangle(0, 0, bmpOut.Width, bmpOut.Height));
								}
							}

							output = new BmpVideoProvider(bmpOut, _currentVideoProvider.VsyncNumerator, _currentVideoProvider.VsyncDenominator);
							disposableOutput = (IDisposable) output;
						}
						finally
						{
							bbIn?.Dispose();
							bmpIn?.Dispose();
						}
					}
					else
					{
						if (Config.AviCaptureOsd)
						{
							output = new BitmapBufferVideoProvider(CaptureOSD());
							disposableOutput = (IDisposable) output;
						}
						else if (Config.AviCaptureLua)
						{
							output = new BitmapBufferVideoProvider(CaptureLua());
							disposableOutput = (IDisposable) output;
						}
						else
						{
							output = _currentVideoProvider;
						}
					}

					_currAviWriter.SetFrame(Emulator.Frame);

					short[] samp;
					int nsamp;
					if (_dumpaudiosync)
					{
						((VideoStretcher) _currAviWriter).DumpAV(output, _currentSoundProvider, out samp, out nsamp);
					}
					else
					{
						((AudioStretcher) _currAviWriter).DumpAV(output, _aviSoundInputAsync, out samp, out nsamp);
					}

					disposableOutput?.Dispose();

					_dumpProxy.PutSamples(samp, nsamp);
				}
				catch (Exception e)
				{
					ShowMessageBox(owner: null, $"Video dumping died:\n\n{e}");
					AbortAv();
				}
			}

			if (_autoDumpLength > 0) //TODO this is probably not necessary because of the call to StopAv --yoshi
			{
				_autoDumpLength--;
				if (_autoDumpLength == 0) // finish
				{
					StopAv();
					if (_argParser._autoCloseOnDump)
					{
						_exitRequestPending = true;
					}
				}
			}
		}

		private int? LoadArchiveChooser(HawkFile file)
		{
			using var ac = new ArchiveChooser(file);
			if (this.ShowDialogAsChild(ac).IsOk())
			{
				return ac.SelectedMemberIndex;
			}

			return null;
		}

		public string SaveStatePrefix()
		{
			var name = Game.FilesystemSafeName();
			name += $".{Emulator.Attributes().CoreName}";

			// Bsnes profiles have incompatible savestates so save the profile name
			if (Emulator is LibsnesCore bsnes)
			{
				name += $".{bsnes.CurrentProfile}";
			}

			if (MovieSession.Movie.IsActive())
			{
				name += $".{Path.GetFileNameWithoutExtension(MovieSession.Movie.Filename)}";
			}

			var pathEntry = Config.PathEntries.SaveStateAbsolutePath(Game.System);

			return Path.Combine(pathEntry, name);
		}

		private void ShowLoadError(object sender, RomLoader.RomErrorArgs e)
		{
			if (e.Type == RomLoader.LoadErrorType.MissingFirmware)
			{
				var result = ShowMessageBox2(
					owner: null,
					"You are missing the needed firmware files to load this Rom\n\nWould you like to open the firmware manager now and configure your firmwares?",
					e.Message,
					EMsgBoxIcon.Error);
				if (result)
				{
					FirmwaresMenuItem_Click(null, e);
					if (e.Retry)
					{
						// Retry loading the ROM here. This leads to recursion, as the original call to LoadRom has not exited yet,
						// but unless the user tries and fails to set his firmware a lot of times, nothing should happen.
						// Refer to how RomLoader implemented its LoadRom method for a potential fix on this.
						LoadRom(e.RomPath, _currentLoadRomArgs);
					}
				}
			}
			else
			{
				string title = "load error";
				if (e.AttemptedCoreLoad != null)
				{
					title = $"{e.AttemptedCoreLoad} load error";
				}

				this.ModalMessageBox(e.Message, title, EMsgBoxIcon.Error);
			}
		}

		private string ChoosePlatformForRom(RomGame rom)
		{
			using var platformChooser = new PlatformChooser(Config)
			{
				RomGame = rom
			};

			platformChooser.ShowDialog();
			return platformChooser.PlatformChoice;
		}

		private LoadRomArgs _currentLoadRomArgs;
		private bool _isLoadingRom;

		public bool LoadRom(string path, LoadRomArgs args) => LoadRom(path, args, out _);

		public bool LoadRom(string path, LoadRomArgs args, out bool failureIsFromAskSave)
		{
			if (!LoadRomInternal(path, args, out failureIsFromAskSave))
				return false;

			// what's the meaning of the last rom path when opening an archive? based on the archive file location
			if (args.OpenAdvanced is OpenAdvanced_OpenRom)
			{
				var leftPart = path.Split('|')[0];
				Config.PathEntries.LastRomPath = Path.GetFullPath(Path.GetDirectoryName(leftPart) ?? "");
			}

			return true;
		}

		// Still needs a good bit of refactoring
		private bool LoadRomInternal(string path, LoadRomArgs args, out bool failureIsFromAskSave)
		{
			failureIsFromAskSave = false;
			if (path == null)
				throw new ArgumentNullException(nameof(path));
			if (args == null)
				throw new ArgumentNullException(nameof(args));

			_isLoadingRom = true;
			path = EmuHawkUtil.ResolveShortcut(path);

			// if this is the first call to LoadRom (they will come in recursively) then stash the args
			bool firstCall = false;
			if (_currentLoadRomArgs == null)
			{
				firstCall = true;
				_currentLoadRomArgs = args;
			}
			else
			{
				args = _currentLoadRomArgs;
			}

			try
			{
				// movies should require deterministic emulation in ALL cases
				// if the core is managing its own DE through SyncSettings a 'deterministic' bool can be passed into the core's constructor
				// it is then up to the core itself to override its own local DeterministicEmulation setting
				bool deterministic = args.Deterministic ?? MovieSession.NewMovieQueued;

				if (!Tools.AskSave())
				{
					failureIsFromAskSave = true;
					return false;
				}

				var loader = new RomLoader(Config)
				{
					ChooseArchive = LoadArchiveChooser,
					ChoosePlatform = ChoosePlatformForRom,
					Deterministic = deterministic,
					MessageCallback = OSD.AddMessage,
					OpenAdvanced = args.OpenAdvanced
				};
				FirmwareManager.RecentlyServed.Clear();

				loader.OnLoadError += ShowLoadError;
				loader.OnLoadSettings += CoreSettings;
				loader.OnLoadSyncSettings += CoreSyncSettings;

				// this also happens in CloseGame(). But it needs to happen here since if we're restarting with the same core,
				// any settings changes that we made need to make it back to config before we try to instantiate that core with
				// the new settings objects
				CommitCoreSettingsToConfig(); // adelikat: I Think by reordering things, this isn't necessary anymore
				CloseGame();

				var nextComm = CreateCoreComm();

				IOpenAdvanced ioa = args.OpenAdvanced;
				var oaOpenrom = ioa as OpenAdvanced_OpenRom;
				var oaMame = ioa as OpenAdvanced_MAME;
				var oaRetro = ioa as OpenAdvanced_Libretro;
				var ioaRetro = ioa as IOpenAdvancedLibretro;

				// we need to inform LoadRom which Libretro core to use...
				if (ioaRetro != null)
				{
					// prepare a core specification
					// if it wasn't already specified, use the current default
					if (ioaRetro.CorePath == null)
					{
						ioaRetro.CorePath = Config.LibretroCore;
					}

					if (ioaRetro.CorePath == null)
					{
						throw new InvalidOperationException("Can't load a file via Libretro until a core is specified");
					}
				}

				if (oaOpenrom != null)
				{
					// path already has the right value, while ioa.Path is null (interestingly, these are swapped below)
					// I doubt null is meant to be assigned here, and it just prevents game load
					//path = ioa_openrom.Path;
				}

				var oldGame = Game;
				var result = loader.LoadRom(path, nextComm, ioaRetro?.CorePath, forcedCoreName: MovieSession.QueuedCoreName);

				Game = result ? loader.Game : oldGame;

				// we need to replace the path in the OpenAdvanced with the canonical one the user chose.
				// It can't be done until loader.LoadRom happens (for CanonicalFullPath)
				// i'm not sure this needs to be more abstractly engineered yet until we have more OpenAdvanced examples
				if (oaRetro != null)
				{
					oaRetro.token.Path = loader.CanonicalFullPath;
				}

				if (oaOpenrom != null)
				{
					oaOpenrom.Path = loader.CanonicalFullPath;
				}

				if (oaMame != null)
				{
					oaMame.Path = loader.CanonicalFullPath;
				}

				if (result)
				{
					string openAdvancedArgs = $"*{OpenAdvancedSerializer.Serialize(ioa)}";
					Emulator.Dispose();
					Emulator = loader.LoadedEmulator;
					InputManager.SyncControls(Emulator, MovieSession, Config);

					if (oaOpenrom != null && Path.GetExtension(oaOpenrom.Path.Replace("|", "")).ToLowerInvariant() == ".xml" && !(Emulator is LibsnesCore))
					{
						// this is a multi-disk bundler file
						// determine the xml assets and create RomStatusDetails for all of them
						var xmlGame = XmlGame.Create(new HawkFile(oaOpenrom.Path));

						using var xSw = new StringWriter();

						for (int xg = 0; xg < xmlGame.Assets.Count; xg++)
						{
							var ext = Path.GetExtension(xmlGame.AssetFullPaths[xg])?.ToLowerInvariant();

							if (ext == ".cue" || ext == ".ccd" || ext == ".toc" || ext == ".mds")
							{
								xSw.WriteLine(Path.GetFileNameWithoutExtension(xmlGame.Assets[xg].Key));
								xSw.WriteLine("SHA1:N/A");
								xSw.WriteLine("MD5:N/A");
								xSw.WriteLine();
							}
							else
							{
								xSw.WriteLine(xmlGame.Assets[xg].Key);
								xSw.WriteLine($"SHA1:{xmlGame.Assets[xg].Value.HashSHA1()}");
								xSw.WriteLine($"MD5:{xmlGame.Assets[xg].Value.HashMD5()}");
								xSw.WriteLine();
							}
						}

						_defaultRomDetails = xSw.ToString();
						_multiDiskMode = true;
					}

					if (loader.LoadedEmulator is NES nes)
					{
						if (!string.IsNullOrWhiteSpace(nes.GameName))
						{
							Game.Name = nes.GameName;
						}

						Game.Status = nes.RomStatus;
					}
					else if (loader.LoadedEmulator is QuickNES qns)
					{
						if (!string.IsNullOrWhiteSpace(qns.BootGodName))
						{
							Game.Name = qns.BootGodName;
						}

						if (qns.BootGodStatus.HasValue)
						{
							Game.Status = qns.BootGodStatus.Value;
						}
					}

					var romDetails = Emulator.RomDetails();
					if (string.IsNullOrWhiteSpace(romDetails) && loader.Rom != null)
					{
						_defaultRomDetails = $"{loader.Game.Name}\r\nSHA1:{loader.Rom.RomData.HashSHA1()}\r\nMD5:{loader.Rom.RomData.HashMD5()}\r\n";
					}
					else if (string.IsNullOrWhiteSpace(romDetails) && loader.Rom == null)
					{
						// single disc game
						_defaultRomDetails = $"{loader.Game.Name}\r\nSHA1:N/A\r\nMD5:N/A\r\n";
					}

					if (Emulator.HasBoardInfo())
					{
						Console.WriteLine("Core reported BoardID: \"{0}\"", Emulator.AsBoardInfo().BoardName);
					}

					// restarts the lua console if a different rom is loaded.
					// im not really a fan of how this is done..
					if (Config.RecentRoms.Empty || Config.RecentRoms.MostRecent != openAdvancedArgs)
					{
						Tools.Restart<LuaConsole>();
					}

					Config.RecentRoms.Add(openAdvancedArgs);
					JumpLists.AddRecentItem(openAdvancedArgs, ioa.DisplayName);

					// Don't load Save Ram if a movie is being loaded
					if (!MovieSession.NewMovieQueued)
					{
						if (File.Exists(Config.PathEntries.SaveRamAbsolutePath(loader.Game, MovieSession.Movie)))
						{
							LoadSaveRam();
						}
						else if (Config.AutosaveSaveRAM && File.Exists(Config.PathEntries.SaveRamAbsolutePath(loader.Game, MovieSession.Movie)))
						{
							AddOnScreenMessage("AutoSaveRAM found, but SaveRAM was not saved");
						}
					}

					Tools.Restart(Config, Emulator, Game);

					if (Config.Cheats.LoadFileByGame && Emulator.HasMemoryDomains())
					{
						CheatList.SetDefaultFileName(Tools.GenerateDefaultCheatFilename());
						if (CheatList.AttemptToLoadCheatFile(Emulator.AsMemoryDomains()))
						{
							AddOnScreenMessage("Cheats file loaded");
						}
						else if (CheatList.Any())
						{
							CheatList.Clear();
						}
					}

					CurrentlyOpenRom = oaOpenrom?.Path ?? openAdvancedArgs;
					CurrentlyOpenRomArgs = args;
					OnRomChanged();
					DisplayManager.UpdateGlobals(Config, Emulator);
					DisplayManager.Blank();
					CreateRewinder();

					InputManager.StickyXorAdapter.ClearStickies();
					InputManager.StickyXorAdapter.ClearStickyAxes();
					InputManager.AutofireStickyXorAdapter.ClearStickies();

					RewireSound();
					Tools.UpdateCheatRelatedTools(null, null);
					if (Config.AutoLoadLastSaveSlot && HasSlot(Config.SaveSlot))
					{
						LoadQuickSave($"QuickSave{Config.SaveSlot}");
					}

					if (FirmwareManager.RecentlyServed.Count > 0)
					{
						Console.WriteLine("Active Firmwares:");
						foreach (var f in FirmwareManager.RecentlyServed)
						{
							Console.WriteLine($"  {f.ID} : {f.Hash}");
						}
					}

					ExtToolManager.BuildToolStrip();

					EmuClient.OnRomLoaded();
					return true;
				}
				else if (Emulator.IsNull())
				{
					// This shows up if there's a problem
					OnRomChanged();
					return false;
				}
				else
				{
					// The ROM has been loaded by a recursive invocation of the LoadROM method.
					EmuClient.OnRomLoaded();
					return true;
				}
			}
			finally
			{
				if (firstCall)
				{
					_currentLoadRomArgs = null;
				}

				_isLoadingRom = false;
			}
		}

		private void OnRomChanged()
		{
			SetWindowText();
			HandlePlatformMenus();
			_stateSlots.ClearRedoList();
			UpdateStatusSlots();
			UpdateCoreStatusBarButton();
			UpdateDumpInfo();
			SetMainformMovieInfo();
		}

		private void CommitCoreSettingsToConfig()
		{
			// save settings object
			var t = Emulator.GetType();
			var settable = new SettingsAdapter(Emulator);

			if (settable.HasSettings)
			{
				Config.PutCoreSettings(settable.GetSettings(), t);
			}

			if (settable.HasSyncSettings && !MovieSession.Movie.IsActive())
			{
				// don't trample config with loaded-from-movie settings
				Config.PutCoreSyncSettings(settable.GetSyncSettings(), t);
			}
		}

		// whats the difference between these two methods??
		// its very tricky. rename to be more clear or combine them.
		// This gets called whenever a core related thing is changed.
		// Like reboot core.
		private void CloseGame(bool clearSram = false)
		{
			GameIsClosing = true;
			if (clearSram)
			{
				var path = Config.PathEntries.SaveRamAbsolutePath(Game, MovieSession.Movie);
				if (File.Exists(path))
				{
					File.Delete(path);
					AddOnScreenMessage("SRAM cleared.");
				}
			}
			else if (Emulator.HasSaveRam() && Emulator.AsSaveRam().SaveRamModified)
			{
				if (!FlushSaveRAM())
				{
					var msgRes = ShowMessageBox2(
						owner: null,
						"Failed flushing the game's Save RAM to your disk.\nClose without flushing Save RAM?",
						"Directory IO Error",
						EMsgBoxIcon.Error);

					if (!msgRes)
					{
						return;
					}
				}
			}

			StopAv();

			CommitCoreSettingsToConfig();
			Rewinder?.Dispose();
			Rewinder = null;

			if (MovieSession.Movie.IsActive()) // Note: this must be called after CommitCoreSettingsToConfig()
			{
				StopMovie();
			}

			CheatList.SaveOnClose();
			Emulator.Dispose();
			Emulator = new NullEmulator();
			InputManager.ActiveController = new Controller(NullController.Instance.Definition);
			InputManager.AutoFireController = _autofireNullControls;
			RewireSound();
			RebootStatusBarIcon.Visible = false;
			GameIsClosing = false;
		}

		public bool GameIsClosing { get; private set; } // Lets tools make better decisions when being called by CloseGame

		public void CloseRom(bool clearSram = false)
		{
			// This gets called after Close Game gets called.
			// Tested with NESHawk and SMB3 (U)
			if (Tools.AskSave())
			{
				CloseGame(clearSram);
				Emulator.Dispose();
				Emulator = new NullEmulator();
				Game = GameInfo.NullInstance;
				CreateRewinder();
				Tools.Restart(Config, Emulator, Game);
				RewireSound();
				ClearHolds();
				DisplayManager.UpdateGlobals(Config, Emulator);
				InputManager.SyncControls(Emulator, MovieSession, Config);
				Tools.UpdateCheatRelatedTools(null, null);
				PauseOnFrame = null;
				CurrentlyOpenRom = null;
				CurrentlyOpenRomArgs = null;
				OnRomChanged();
			}
		}

		private void ProcessMovieImport(string fn, bool start)
		{
			var result = MovieImport.ImportFile(this, MovieSession, Emulator, fn, Config);

			if (result.Errors.Any())
			{
				ShowMessageBox(owner: null, string.Join("\n", result.Errors), "Conversion error", EMsgBoxIcon.Error);
			}

			if (result.Warnings.Any())
			{
				AddOnScreenMessage(result.Warnings.First()); // For now, just show the first warning
			}

			AddOnScreenMessage($"{Path.GetFileName(fn)} imported as {result.Movie.Filename}");

			if (start)
			{
				StartNewMovie(result.Movie, false);
				Config.RecentMovies.Add(result.Movie.Filename);
			}
		}

		public void EnableRewind(bool enabled)
		{
			if (!Emulator.HasSavestates())
			{
				return;
			}

			if (Rewinder == null)
			{
				CreateRewinder();
			}

			// CreateRewinder doesn't necessarily create an instance of rewinder, still need to check null
			if (Rewinder != null)
			{
				if (enabled)
				{
					Rewinder.Resume();
				}
				else
				{
					Rewinder.Suspend();
				}

				AddOnScreenMessage($"Rewind {(enabled ? "enabled" : "suspended")}");
			}
		}

		public void DisableRewind()
		{
			Rewinder?.Dispose();
			Rewinder = null;
		}

		// TODO: move me
		public IControlMainform Master { get; private set; }

		private bool IsSlave => Master != null;

		private bool IsSavestateSlave => IsSlave && Master.WantsToControlSavestates;

		private bool IsRewindSlave => IsSlave && Master.WantsToControlRewind;

		public void RelinquishControl(IControlMainform master)
		{
			Master = master;
		}

		public void TakeBackControl()
		{
			Master = null;
		}

		private int SlotToInt(string slot)
		{
			return int.Parse(slot.Substring(slot.Length - 1, 1));
		}

		public void LoadState(string path, string userFriendlyStateName, bool suppressOSD = false) // Move to client.common
		{
			if (!Emulator.HasSavestates())
			{
				return;
			}

			if (IsSavestateSlave)
			{
				Master.LoadState();
				return;
			}

			if (new SavestateFile(Emulator, MovieSession, MovieSession.UserBag).Load(path))
			{
				OSD.ClearGuiText();
				EmuClient.OnStateLoaded(this, userFriendlyStateName);

				if (Tools.Has<LuaConsole>())
				{
					Tools.LuaConsole.LuaImp.CallLoadStateEvent(userFriendlyStateName);
				}

				SetMainformMovieInfo();
				Tools.UpdateToolsBefore();
				UpdateToolsAfter();
				UpdateToolsLoadstate();
				InputManager.AutoFireController.ClearStarts();

				//we don't want to analyze how to intermix movies, rewinding, and states
				//so purge rewind history when loading a state while doing a movie
				if (!IsRewindSlave && MovieSession.Movie.IsActive())
				{
					Rewinder?.Clear();
				}

				if (!suppressOSD)
				{
					AddOnScreenMessage($"Loaded state: {userFriendlyStateName}");
				}
			}
			else
			{
				AddOnScreenMessage("Loadstate error!");
			}
		}

		public void LoadQuickSave(string quickSlotName, bool suppressOSD = false)
		{
			if (!Emulator.HasSavestates())
			{
				return;
			}

			EmuClient.OnBeforeQuickLoad(this, quickSlotName, out var handled);
			if (handled)
			{
				return;
			}

			if (IsSavestateSlave)
			{
				Master.LoadQuickSave(SlotToInt(quickSlotName));
				return;
			}

			var path = $"{SaveStatePrefix()}.{quickSlotName}.State";
			if (!File.Exists(path))
			{
				AddOnScreenMessage($"Unable to load {quickSlotName}.State");

				return;
			}

			LoadState(path, quickSlotName, suppressOSD);
		}

		public void SaveState(string path, string userFriendlyStateName, bool fromLua = false, bool suppressOSD = false)
		{
			if (!Emulator.HasSavestates())
			{
				return;
			}

			if (IsSavestateSlave)
			{
				Master.SaveState();
				return;
			}

			try
			{
				new SavestateFile(Emulator, MovieSession, MovieSession.UserBag).Create(path, Config.Savestates);

				EmuClient.OnStateSaved(this, userFriendlyStateName);

				if (!suppressOSD)
				{
					AddOnScreenMessage($"Saved state: {userFriendlyStateName}");
				}
			}
			catch (IOException)
			{
				AddOnScreenMessage($"Unable to save state {path}");
			}

			if (!fromLua)
			{
				UpdateStatusSlots();
			}
		}

		// TODO: should backup logic be stuffed in into Client.Common.SaveStateManager?
		public void SaveQuickSave(string quickSlotName, bool fromLua = false, bool suppressOSD = false)
		{
			if (!Emulator.HasSavestates())
			{
				return;
			}

			EmuClient.OnBeforeQuickSave(this, quickSlotName, out var handled);
			if (handled)
			{
				return;
			}

			if (IsSavestateSlave)
			{
				Master.SaveQuickSave(SlotToInt(quickSlotName));
				return;
			}

			var path = $"{SaveStatePrefix()}.{quickSlotName}.State";

			var file = new FileInfo(path);
			if (file.Directory != null && !file.Directory.Exists)
			{
				file.Directory.Create();
			}

			// Make backup first
			if (Config.Savestates.MakeBackups)
			{
				Util.TryMoveBackupFile(path, $"{path}.bak");
			}

			SaveState(path, quickSlotName, fromLua, suppressOSD);

			if (Tools.Has<LuaConsole>())
			{
				Tools.LuaConsole.LuaImp.CallSaveStateEvent(quickSlotName);
			}
		}

		public bool EnsureCoreIsAccurate()
		{
			bool PromptToSwitchCore(string currentCore, string recommendedCore, Action disableCurrentCore)
			{
				using var box = new MsgBox(
					$"While the {currentCore} core is faster, it is not nearly as accurate as {recommendedCore}.{Environment.NewLine}It is recommended that you switch to the {recommendedCore} core for movie recording.{Environment.NewLine}Switch to {recommendedCore}?",
					"Accuracy Warning",
					MessageBoxIcon.Warning);

				box.SetButtons(
					new[] { "Switch", "Continue" },
					new[] { DialogResult.Yes, DialogResult.Cancel });

				box.MaximumSize = UIHelper.Scale(new Size(575, 175));
				box.SetMessageToAutoSize();

				var result = box.ShowDialog();

				if (result != DialogResult.Yes)
				{
					return false;
				}

				disableCurrentCore();
				RebootCore();
				return true;
			}

			return Emulator switch
			{
				Snes9x => PromptToSwitchCore(CoreNames.Snes9X, CoreNames.Bsnes, () => Config.PreferredCores["SNES"] = CoreNames.Bsnes),
				QuickNES => PromptToSwitchCore(CoreNames.QuickNes, CoreNames.NesHawk, () => Config.PreferredCores["NES"] = CoreNames.NesHawk),
				HyperNyma => PromptToSwitchCore(CoreNames.HyperNyma, CoreNames.TurboNyma, () => Config.PreferredCores["PCE"] = CoreNames.TurboNyma),
				_ => true
			};
		}

		private void SaveStateAs()
		{
			if (!Emulator.HasSavestates())
			{
				return;
			}

			// allow named state export for tastudio, since it's safe, unlike loading one
			// todo: make it not save laglog in that case
			if (Tools.IsLoaded<TAStudio>())
			{
				Tools.TAStudio.NamedStatePending = true;
			}

			if (IsSavestateSlave)
			{
				Master.SaveStateAs();
				return;
			}

			var path = Config.PathEntries.SaveStateAbsolutePath(Game.System);

			var file = new FileInfo(path);
			if (file.Directory != null && !file.Directory.Exists)
			{
				file.Directory.Create();
			}

			using var sfd = new SaveFileDialog
			{
				AddExtension = true,
				DefaultExt = "State",
				Filter = new FilesystemFilterSet(FilesystemFilter.EmuHawkSaveStates).ToString(),
				InitialDirectory = path,
				FileName = $"{SaveStatePrefix()}.QuickSave0.State"
			};

			if (this.ShowDialogWithTempMute(sfd) == DialogResult.OK)
			{
				SaveState(sfd.FileName, sfd.FileName);
			}

			if (Tools.IsLoaded<TAStudio>())
			{
				Tools.TAStudio.NamedStatePending = false;
			}
		}

		private void LoadStateAs()
		{
			if (!Emulator.HasSavestates())
			{
				return;
			}

			if (IsSavestateSlave)
			{
				Master.LoadStateAs();
				return;
			}

			using var ofd = new OpenFileDialog
			{
				InitialDirectory = Config.PathEntries.SaveStateAbsolutePath(Game.System),
				Filter = new FilesystemFilterSet(FilesystemFilter.EmuHawkSaveStates).ToString(),
				RestoreDirectory = true
			};

			if (this.ShowDialogWithTempMute(ofd) != DialogResult.OK) return;

			if (!File.Exists(ofd.FileName))
			{
				return;
			}

			LoadState(ofd.FileName, Path.GetFileName(ofd.FileName));
		}

		private void SelectSlot(int slot)
		{
			if (Emulator.HasSavestates())
			{
				if (IsSavestateSlave)
				{
					var handled = Master.SelectSlot(slot);
					if (handled)
					{
						return;
					}
				}

				Config.SaveSlot = slot;
				SaveSlotSelectedMessage();
				UpdateStatusSlots();
			}
		}

		private void PreviousSlot()
		{
			if (Emulator.HasSavestates())
			{
				if (IsSavestateSlave)
				{
					var handled = Master.PreviousSlot();
					if (handled)
					{
						return;
					}
				}

				if (Config.SaveSlot == 0)
				{
					Config.SaveSlot = 9; // Wrap to end of slot list
				}
				else if (Config.SaveSlot > 9)
				{
					Config.SaveSlot = 9; // Meh, just in case
				}
				else
				{
					Config.SaveSlot--;
				}

				SaveSlotSelectedMessage();
				UpdateStatusSlots();
			}
		}

		private void NextSlot()
		{
			if (Emulator.HasSavestates())
			{
				if (IsSavestateSlave)
				{
					var handled = Master.NextSlot();
					if (handled)
					{
						return;
					}
				}

				if (Config.SaveSlot >= 9)
				{
					Config.SaveSlot = 0; // Wrap to beginning of slot list
				}
				else if (Config.SaveSlot < 0)
				{
					Config.SaveSlot = 0; // Meh, just in case
				}
				else
				{
					Config.SaveSlot++;
				}

				SaveSlotSelectedMessage();
				UpdateStatusSlots();
			}
		}

		private void CaptureRewind(bool suppressCaptureRewind)
		{
			if (IsRewindSlave)
			{
				Master.CaptureRewind();
			}
			else if (!suppressCaptureRewind && Rewinder?.Active == true)
			{
				Rewinder.Capture(Emulator.Frame);
			}
		}

		private bool Rewind(ref bool runFrame, long currentTimestamp, out bool returnToRecording)
		{
			var isRewinding = false;

			returnToRecording = false;

			if (IsRewindSlave)
			{
				if (InputManager.ClientControls["Rewind"] || PressRewind)
				{
					if (_frameRewindTimestamp == 0)
					{
						isRewinding = true;
						_frameRewindTimestamp = currentTimestamp;
						_frameRewindWasPaused = EmulatorPaused;
					}
					else
					{
						double timestampDeltaMs = (double)(currentTimestamp - _frameRewindTimestamp) / Stopwatch.Frequency * 1000.0;
						isRewinding = timestampDeltaMs >= Config.FrameProgressDelayMs;

						// clear this flag once we get out of the progress stage
						if (isRewinding)
						{
							_frameRewindWasPaused = false;
						}

						// if we're freely running, there's no need for reverse frame progress semantics (that may be debatable though)
						if (!EmulatorPaused)
						{
							isRewinding = true;
						}

						if (_frameRewindWasPaused)
						{
							if (IsSeeking)
							{
								isRewinding = false;
							}
						}
					}

					if (isRewinding)
					{
						runFrame = Emulator.Frame > 1; // TODO: the master should be deciding this!
						Master.Rewind();
					}
				}
				else
				{
					_frameRewindTimestamp = 0;
				}

				return isRewinding;
			}

			if (Rewinder?.Active == true && (InputManager.ClientControls["Rewind"] || PressRewind))
			{
				if (EmulatorPaused)
				{
					if (_frameRewindTimestamp == 0)
					{
						isRewinding = true;
						_frameRewindTimestamp = currentTimestamp;
					}
					else
					{
						double timestampDeltaMs = (double)(currentTimestamp - _frameRewindTimestamp) / Stopwatch.Frequency * 1000.0;
						isRewinding = timestampDeltaMs >= Config.FrameProgressDelayMs;
					}
				}
				else
				{
					isRewinding = true;
				}

				if (isRewinding)
				{
					// Try to avoid the previous frame:  We want to frame advance right after rewinding so we can give a useful
					// framebuffer.
					var frameToAvoid = Emulator.Frame - 1;
					runFrame = Rewinder.Rewind(frameToAvoid);
					if (Emulator.Frame == frameToAvoid)
					{
						// The rewinder was unable to satisfy our request.  Prefer showing a stale framebuffer to
						// advancing in a way that essentially no-ops the entire rewind.
						runFrame = false;
					}

					if (runFrame && MovieSession.Movie.IsRecording())
					{
						MovieSession.Movie.SwitchToPlay();
						returnToRecording = true;
					}
				}
			}
			else
			{
				_frameRewindTimestamp = 0;
			}

			return isRewinding;
		}

		public IDialogController DialogController => this;

		public void ShowMessageBox(
			IDialogParent/*?*/ owner,
			string text,
			string/*?*/ caption = null,
			EMsgBoxIcon? icon = null)
				=> this.ShowMessageBox(
					owner: owner,
					text: text,
					caption: caption,
					buttons: MessageBoxButtons.OK,
					icon: icon);

		public bool ShowMessageBox2(
			IDialogParent/*?*/ owner,
			string text,
			string/*?*/ caption = null,
			EMsgBoxIcon? icon = null,
			bool useOKCancel = false)
				=> this.ShowMessageBox(
					owner: owner,
					text: text,
					caption: caption,
					buttons: useOKCancel ? MessageBoxButtons.OKCancel : MessageBoxButtons.YesNo,
					icon: icon) switch
				{
					DialogResult.OK => true,
					DialogResult.Yes => true,
					_ => false
				};

		public bool? ShowMessageBox3(
			IDialogParent/*?*/ owner,
			string text,
			string/*?*/ caption = null,
			EMsgBoxIcon? icon = null)
				=> this.ShowMessageBox(
					owner: owner,
					text: text,
					caption: caption,
					buttons: MessageBoxButtons.YesNoCancel,
					icon: icon) switch
				{
					DialogResult.Yes => true,
					DialogResult.No => false,
					_ => null
				};

		public void StartSound() => Sound.StartSound();
		public void StopSound() => Sound.StopSound();

		private Mutex _singleInstanceMutex;
		private NamedPipeServerStream _singleInstanceServer;
		private readonly List<string[]> _singleInstanceForwardedArgs = new();

		private bool SingleInstanceInit(string[] args)
		{
			//note: this isn't 100% reliable, it's just a user convenience
			_singleInstanceMutex = new Mutex(true, "mutex-{84125ACB-F570-4458-9748-321F887FE795}", out bool createdNew);
			if (createdNew)
			{
				StartSingleInstanceServer();
				return false;
			}
			else
			{
				ForwardSingleInstanceStartup(args);
				return true;
			}
		}

		private void SingleInstanceDispose()
		{
			_singleInstanceServer?.Dispose();
		}

		private void ForwardSingleInstanceStartup(string[] args)
		{
			using var namedPipeClientStream = new NamedPipeClientStream(".", "pipe-{84125ACB-F570-4458-9748-321F887FE795}", PipeDirection.Out);
			try
			{
				namedPipeClientStream.Connect(0);
				//do this a bit cryptically to avoid loading up another big assembly (especially ones as frail as http and/or web ones)
				var payloadString = string.Join("|", args.Select(a => Encoding.UTF8.GetBytes(a).BytesToHexString()));
				var payloadBytes = Encoding.ASCII.GetBytes(payloadString);
				namedPipeClientStream.Write(payloadBytes, 0, payloadBytes.Length);
			}
			catch
			{
				Console.WriteLine("Failed forwarding args to already-running single instance");
			}
		}

		private void StartSingleInstanceServer()
		{
			//MIT LICENSE - https://www.autoitconsulting.com/site/development/single-instance-winform-app-csharp-mutex-named-pipes/

			// Create a new pipe accessible by local authenticated users, disallow network
			var sidNetworkService = new SecurityIdentifier(WellKnownSidType.NetworkServiceSid, null);
			var sidWorld = new SecurityIdentifier(WellKnownSidType.WorldSid, null);

			var pipeSecurity = new PipeSecurity();

			// Deny network access to the pipe
			var accessRule = new PipeAccessRule(sidNetworkService, PipeAccessRights.ReadWrite, AccessControlType.Deny);
			pipeSecurity.AddAccessRule(accessRule);

			// Alow Everyone to read/write
			accessRule = new PipeAccessRule(sidWorld, PipeAccessRights.ReadWrite, AccessControlType.Allow);
			pipeSecurity.AddAccessRule(accessRule);

			// Current user is the owner
			SecurityIdentifier sidOwner = WindowsIdentity.GetCurrent().Owner;
			if (sidOwner != null)
			{
				accessRule = new PipeAccessRule(sidOwner, PipeAccessRights.FullControl, AccessControlType.Allow);
				pipeSecurity.AddAccessRule(accessRule);
			}

			// Create pipe and start the async connection wait
			_singleInstanceServer = new NamedPipeServerStream(
					"pipe-{84125ACB-F570-4458-9748-321F887FE795}",
					PipeDirection.In,
					1,
					PipeTransmissionMode.Message,
					PipeOptions.Asynchronous,
					0,
					0,
					pipeSecurity);

			// Begin async wait for connections
			_singleInstanceServer.BeginWaitForConnection(SingleInstanceServerPipeCallback, null);
		}

		//Note: This method is called on a non-UI thread.
		//Note: this seems really frail. I don't think it's industrial strength. Pipes are weak compared to sockets.
		//It was probably frail in the first place with the old vbnet impl
		private void SingleInstanceServerPipeCallback(IAsyncResult iAsyncResult)
		{
			try
			{
				_singleInstanceServer.EndWaitForConnection(iAsyncResult);

				//a bit over-engineered in case someone wants to send a script or a rom or something
				//buffer size is set to something tiny so that we are continually testing it 
				var payloadBytes = new MemoryStream();
				while (true)
				{
					var bytes = new byte[16];
					int did = _singleInstanceServer.Read(bytes, 0, bytes.Length);
					payloadBytes.Write(bytes, 0, did);
					if (_singleInstanceServer.IsMessageComplete) break;
				}

				var payloadString = System.Text.Encoding.ASCII.GetString(payloadBytes.GetBuffer(), 0, (int)payloadBytes.Length);
				var args = payloadString.Split('|').Select(a => Encoding.UTF8.GetString(a.HexStringToBytes())).ToArray();

				Console.WriteLine("RECEIVED SINGLE INSTANCE FORWARDED ARGS:");
				lock (_singleInstanceForwardedArgs)
					_singleInstanceForwardedArgs.Add(args);
			}
			catch (ObjectDisposedException)
			{
				// EndWaitForConnection will exception when someone calls closes the pipe before connection made
				// In that case we dont create any more pipes and just return
				// This will happen when app is closing and our pipe is closed/disposed
				return;
			}
			catch (Exception)
			{
				// ignored
			}
			finally
			{
				// Close the original pipe (we will create a new one each time)
				_singleInstanceServer.Dispose();
			}

			// Create a new pipe for next connection
			StartSingleInstanceServer();
		}

		private void SingleInstanceProcessArgs(string[] args)
		{
			//ulp. it's not clear how to handle these.
			//we only have a legacy case where we can tell the form to load a rom, if it's in a sensible condition for that.
			//er.. let's assume it's always in a sensible condition
			//in case this all sounds insanely sketchy to you, remember, the main 99% use case is double clicking roms in explorer

			//BANZAIIIIIIIIIIIIIIIIIIIIIIIIIII
			LoadRom(args[0]);
		}
	}
}
