private static void RenderInstructionHistory(IntPtr surface, PacManPCB pcb) { FontRenderer.RenderString(surface, $"{COLOR_BRIGHT_WHITE}------------------------------[HISTORY]-----------------------------------------", 0, 0 * ROW_HEIGHT); var recentInstructions = FormatRecentInstructions(pcb._addressHistory, pcb, 50); for (var i = 0; i < recentInstructions.Count && i < 50; i++) { FontRenderer.RenderString(surface, recentInstructions[i], 0, (i + 2) * ROW_HEIGHT); } FontRenderer.RenderString(surface, $"{COLOR_BRIGHT_WHITE}--------------------------------------------------------------------------------", 0, (50 + 3) * ROW_HEIGHT); FontRenderer.RenderString(surface, $"Press {COLOR_BRIGHT_YELLOW}[ESCAPE] {COLOR_WHITE}to go back.", 0, (50 + 4) * ROW_HEIGHT); FontRenderer.RenderString(surface, $"{COLOR_BRIGHT_WHITE}--------------------------------------------------------------------------------", 0, (50 + 9) * ROW_HEIGHT); }
private static void RenderCpuStateAndDisassembly(IntPtr surface, DebuggerState state, PacManPCB pcb, bool showAnnotatedDisassembly) { FontRenderer.RenderString(surface, $"{COLOR_BRIGHT_WHITE}------------------------------[STATS]-------------------------------------------", 0, 0 * ROW_HEIGHT); var statusColor = $"{COLOR_GREEN}"; var status = $"Running"; var cycleCount = (state == DebuggerState.Breakpoint ? String.Format("{0:n0}", pcb._totalCycles) : "---").PadRight(13); var opcodeCount = (state == DebuggerState.Breakpoint ? String.Format("{0:n0}", pcb._totalOpcodes) : "---").PadRight(13); if (state == DebuggerState.Breakpoint) { statusColor = $"{COLOR_BRIGHT_RED}"; status = "Breakpoint"; } else if (state == DebuggerState.SingleStepping) { statusColor = $"{COLOR_BRIGHT_YELLOW}"; status = "Stepping..."; } status = status.PadRight(12); FontRenderer.RenderString(surface, $"{COLOR_BRIGHT_WHITE}[Status]: {statusColor}{status} {COLOR_BRIGHT_WHITE}[Cycles]: {COLOR_WHITE}{cycleCount} {COLOR_BRIGHT_WHITE}[Opcodes]: {COLOR_WHITE}{opcodeCount}", 0, 2 * ROW_HEIGHT); FontRenderer.RenderString(surface, "------------------------------[CPU STATE]---------------------------------------", 0, 4 * ROW_HEIGHT); var pc = EMPTY_16BIT_HEX_VALUE_DISPLAY; var sp = EMPTY_16BIT_HEX_VALUE_DISPLAY; var regA = EMPTY_8BIT_HEX_VALUE_DISPLAY; var regB = EMPTY_8BIT_HEX_VALUE_DISPLAY; var regC = EMPTY_8BIT_HEX_VALUE_DISPLAY; var regD = EMPTY_8BIT_HEX_VALUE_DISPLAY; var regE = EMPTY_8BIT_HEX_VALUE_DISPLAY; var regH = EMPTY_8BIT_HEX_VALUE_DISPLAY; var regL = EMPTY_8BIT_HEX_VALUE_DISPLAY; var regDE = EMPTY_16BIT_HEX_VALUE_DISPLAY; var regHL = EMPTY_16BIT_HEX_VALUE_DISPLAY; var regIX = EMPTY_8BIT_HEX_VALUE_DISPLAY; var regIY = EMPTY_8BIT_HEX_VALUE_DISPLAY; var regF = EMPTY_8BIT_HEX_VALUE_DISPLAY; var memDE = EMPTY_8BIT_HEX_VALUE_DISPLAY; var memHL = EMPTY_8BIT_HEX_VALUE_DISPLAY; var sign = EMPTY_BIT_VALUE_DISPLAY; var zero = EMPTY_BIT_VALUE_DISPLAY; var parityOverflow = EMPTY_BIT_VALUE_DISPLAY; var subtract = EMPTY_BIT_VALUE_DISPLAY; var carry = EMPTY_BIT_VALUE_DISPLAY; var halfCarry = EMPTY_BIT_VALUE_DISPLAY; var interrupts = EMPTY_8BIT_HEX_VALUE_DISPLAY; if (state == DebuggerState.Breakpoint && pcb._cpu != null) { pc = String.Format("{0:X4}", pcb._cpu.Registers.PC); sp = String.Format("{0:X4}", pcb._cpu.Registers.SP); regA = String.Format("{0:X2}", pcb._cpu.Registers.A); regB = String.Format("{0:X2}", pcb._cpu.Registers.B); regC = String.Format("{0:X2}", pcb._cpu.Registers.C); regD = String.Format("{0:X2}", pcb._cpu.Registers.D); regE = String.Format("{0:X2}", pcb._cpu.Registers.E); regH = String.Format("{0:X2}", pcb._cpu.Registers.H); regL = String.Format("{0:X2}", pcb._cpu.Registers.L); regDE = String.Format("{0:X4}", pcb._cpu.Registers.DE); regHL = String.Format("{0:X4}", pcb._cpu.Registers.HL); regIX = String.Format("{0:X2}", pcb._cpu.Registers.IX); regIY = String.Format("{0:X2}", pcb._cpu.Registers.IY); regF = String.Format("{0:X2}", pcb._cpu.Flags.ToByte()); try { var value = pcb._cpu.Memory.Read(pcb._cpu.Registers.DE); memDE = String.Format("{0:X2}", value); } catch { memDE = EMPTY_8BIT_HEX_VALUE_DISPLAY; } try { var value = pcb._cpu.Memory.Read(pcb._cpu.Registers.HL); memHL = String.Format("{0:X2}", value); } catch { memDE = EMPTY_8BIT_HEX_VALUE_DISPLAY; } sign = pcb._cpu.Flags.Sign ? "1" : "0"; zero = pcb._cpu.Flags.Zero ? "1" : "0"; parityOverflow = pcb._cpu.Flags.ParityOverflow ? "1" : "0"; subtract = pcb._cpu.Flags.Subtract ? "1" : "0"; carry = pcb._cpu.Flags.Carry ? "1" : "0"; halfCarry = pcb._cpu.Flags.HalfCarry ? "1" : "0"; interrupts = $"{(pcb._cpu.InterruptsEnabled ? "Enabled" : "Disabled" )} (Mode {(int)pcb._cpu.InterruptMode})"; } FontRenderer.RenderString(surface, $"{COLOR_BRIGHT_WHITE}[PC]: {COLOR_WHITE}{pc} {COLOR_BRIGHT_WHITE}[SP]: {COLOR_WHITE}{sp} {COLOR_BRIGHT_WHITE}[Flags]: {COLOR_WHITE}{regF} {COLOR_BRIGHT_WHITE}[INT]: {COLOR_WHITE}{interrupts}", 0, 6 * ROW_HEIGHT); FontRenderer.RenderString(surface, $"{COLOR_BRIGHT_WHITE}[A]: {COLOR_WHITE}{regA} {COLOR_BRIGHT_WHITE}[B]: {COLOR_WHITE}{regB} {COLOR_BRIGHT_WHITE}[C]: {COLOR_WHITE}{regC} {COLOR_BRIGHT_WHITE}[D]: {COLOR_WHITE}{regD} {COLOR_BRIGHT_WHITE}[E]: {COLOR_WHITE}{regE} {COLOR_BRIGHT_WHITE}[H]: {COLOR_WHITE}{regH} {COLOR_BRIGHT_WHITE}[L]: {COLOR_WHITE}{regL}", 0, 8 * ROW_HEIGHT); FontRenderer.RenderString(surface, $"{COLOR_BRIGHT_WHITE}[DE]: {COLOR_WHITE}{regDE} {COLOR_BRIGHT_WHITE}[HL]: {COLOR_WHITE}{regHL} {COLOR_BRIGHT_WHITE}[(DE)]: {COLOR_WHITE}{memDE} {COLOR_BRIGHT_WHITE}[(HL)]: {COLOR_WHITE}{memHL}", 0, 10 * ROW_HEIGHT); FontRenderer.RenderString(surface, $"{COLOR_BRIGHT_WHITE}[Sign]: {COLOR_WHITE}{sign} {COLOR_BRIGHT_WHITE}[Zero]: {COLOR_WHITE}{zero} {COLOR_BRIGHT_WHITE}[Parity/Overflow]: {COLOR_WHITE}{parityOverflow}", 0, 12 * ROW_HEIGHT); FontRenderer.RenderString(surface, $"{COLOR_BRIGHT_WHITE}[Subtract]: {COLOR_WHITE}{subtract} {COLOR_BRIGHT_WHITE}[Carry]: {COLOR_WHITE}{carry} {COLOR_BRIGHT_WHITE}[Half-Carry]: {COLOR_WHITE}{halfCarry}", 0, 14 * ROW_HEIGHT); FontRenderer.RenderString(surface, "------------------------------[DISASSEMBLY]-------------------------------------", 0, 16 * ROW_HEIGHT); if (state == DebuggerState.Breakpoint) { var disassembly = Disassembler.FormatDisassemblyForDisplay(pcb._cpu.Registers.PC, pcb._cpu.Memory, 16, 16, showAnnotatedDisassembly ? pcb.Annotations : null); var disassemblyLines = disassembly.Split(Environment.NewLine.ToCharArray()); for (var i = 0; i < DIASSEMBLY_ROW_COUNT; i++) { if (i >= disassemblyLines.Length) { continue; } // Double tabs followed by a semi-colon indicate a split between the address/instruction disassembly // and the generated psuedocode or annotations. var disassemblyLineParts = disassemblyLines[i].Split("\t\t; ".ToCharArray()); var disassemblyInstruction = disassemblyLineParts[0]; var disassemblyComments = disassemblyLineParts.Length > 1 ? disassemblyLineParts[1] : ""; // Convert tabs to spaces (there is no tab character in the font set, only 8x8 glyphs). disassemblyInstruction = disassemblyInstruction.Replace("\t", " "); var instructionColor = $"{COLOR_WHITE}"; var commentColor = $"{COLOR_BLUE}"; if (disassemblyInstruction.Contains(Disassembler.CURRENT_LINE_MARKER)) { instructionColor = $"{COLOR_BRIGHT_WHITE}"; if (showAnnotatedDisassembly) { commentColor = $"{COLOR_BRIGHT_GREEN}"; } else { commentColor = $"{COLOR_BRIGHT_BLUE}"; } } else { if (showAnnotatedDisassembly) { commentColor = $"{COLOR_GREEN}"; } } disassemblyInstruction = disassemblyInstruction.PadRight(30); var line = $"{instructionColor}{disassemblyInstruction} {commentColor};{disassemblyComments}"; FontRenderer.RenderString(surface, line, 0, (DISASSEMBLY_START_ROW + i) * ROW_HEIGHT); } } var commandsStartRow = (DISASSEMBLY_START_ROW + DIASSEMBLY_ROW_COUNT); FontRenderer.RenderString(surface, "------------------------------[COMMANDS]----------------------------------------", 0, (commandsStartRow + 1) * ROW_HEIGHT); if (state == DebuggerState.Breakpoint) { var stepBackwards = pcb.ReverseStepEnabled && pcb._executionHistory.Count > 0 ? $"{COLOR_BRIGHT_WHITE}[F9] {COLOR_WHITE}Step Backwards" : " "; FontRenderer.RenderString(surface, $"{COLOR_BRIGHT_WHITE}[F1] {COLOR_WHITE}Save State {COLOR_BRIGHT_WHITE}[F2] {COLOR_WHITE}Load State {COLOR_BRIGHT_WHITE}[F4] {COLOR_WHITE}Edit Breakpoints", 0, (commandsStartRow + 3) * ROW_HEIGHT); FontRenderer.RenderString(surface, $"{COLOR_BRIGHT_WHITE}[F5] {COLOR_WHITE}Continue {stepBackwards} {COLOR_BRIGHT_WHITE}[F10] {COLOR_WHITE}Single Step", 0, (commandsStartRow + 4) * ROW_HEIGHT); FontRenderer.RenderString(surface, $"{COLOR_BRIGHT_WHITE}[F11] {COLOR_WHITE}Toggle Annotated Disassembly {COLOR_BRIGHT_WHITE}[F12] {COLOR_WHITE}Print Last 50 Opcodes", 0, (commandsStartRow + 5) * ROW_HEIGHT); } else if (state == DebuggerState.SingleStepping) { FontRenderer.RenderString(surface, $" {COLOR_BRIGHT_YELLOW}Single stepping; please wait...", 0, (commandsStartRow + 4) * ROW_HEIGHT); } else { FontRenderer.RenderString(surface, $" {COLOR_YELLOW}Press [BREAK], [PAUSE], or [F3] to interrupt execution and start debugging...", 0, (commandsStartRow + 4) * ROW_HEIGHT); } FontRenderer.RenderString(surface, "--------------------------------------------------------------------------------", 0, (commandsStartRow + 7) * ROW_HEIGHT); }
public static void Render(IntPtr surface, DebuggerState state, string inputString, List <string> fileList, PacManPCB pcb, bool showAnnotatedDisassembly) { SDL.SDL_SetRenderDrawColor(surface, 0, 0, 0, 255); SDL.SDL_RenderClear(surface); if (state == DebuggerState.EditBreakpoints) // User editing breakpoints { RenderBreakpointEditor(surface, inputString, pcb); } else if (state == DebuggerState.SaveState || state == DebuggerState.LoadState) // Save/Load state menu { RenderSavedStateFileBrowser(surface, state, inputString, fileList, pcb); } else if (state == DebuggerState.InstructionHistory) // "Show Last 50 Opcodes" menu { RenderInstructionHistory(surface, pcb); } else // Running, at a breakpoint, or stepping over a breakpoint. { RenderCpuStateAndDisassembly(surface, state, pcb, showAnnotatedDisassembly); } SDL.SDL_RenderPresent(surface); }
private static void RenderSavedStateFileBrowser(IntPtr surface, DebuggerState state, string inputString, List <string> fileList, PacManPCB pcb) { var title = state == DebuggerState.LoadState ? $"{COLOR_BRIGHT_RED}LOAD STATE" : $"{COLOR_BRIGHT_GREEN}SAVE STATE"; FontRenderer.RenderString(surface, $"{COLOR_BRIGHT_WHITE}------------------------------[{title}{COLOR_BRIGHT_WHITE}]--------------------------------------", 0, 0 * ROW_HEIGHT); for (var i = 0; i < fileList.Count && i < 50; i++) { var file = fileList[i]; var selected = file == inputString; var color = selected ? $"{COLOR_BRIGHT_BLUE}" : $"{COLOR_WHITE}"; FontRenderer.RenderString(surface, $"{color}{(selected ? "--->" : " ")} {file}", 0, (i + 2) * ROW_HEIGHT); } if (pcb.BreakAtAddresses.Count >= 50) { FontRenderer.RenderString(surface, $" {COLOR_YELLOW}(only showing the first 50 of {fileList.Count} files)", 0, (50 + 2) * ROW_HEIGHT); } FontRenderer.RenderString(surface, $"{COLOR_BRIGHT_WHITE}--------------------------------------------------------------------------------", 0, (50 + 3) * ROW_HEIGHT); FontRenderer.RenderString(surface, $"{COLOR_WHITE}Enter a file name or use the arrows to select and then", 0, (50 + 4) * ROW_HEIGHT); FontRenderer.RenderString(surface, $"{COLOR_WHITE}press {COLOR_BRIGHT_GREEN}[ENTER] {COLOR_WHITE}to {title}{COLOR_WHITE}.", 0, (50 + 5) * ROW_HEIGHT); FontRenderer.RenderString(surface, $"> {inputString}", 0, (50 + 7) * ROW_HEIGHT); FontRenderer.RenderString(surface, $"{COLOR_BRIGHT_WHITE}--------------------------------------------------------------------------------", 0, (50 + 9) * ROW_HEIGHT); }
private static void RenderBreakpointEditor(IntPtr surface, string inputString, PacManPCB pcb) { FontRenderer.RenderString(surface, $"{COLOR_BRIGHT_WHITE}------------------------------[BREAKPOINTS]-------------------------------------", 0, 0 * ROW_HEIGHT); for (var i = 0; i < pcb.BreakAtAddresses.Count && i < 50; i++) { var address = String.Format("0x{0:X4}", pcb.BreakAtAddresses[i]); FontRenderer.RenderString(surface, $"{COLOR_WHITE} * {address}", 0, (i + 2) * ROW_HEIGHT); } if (pcb.BreakAtAddresses.Count >= 50) { FontRenderer.RenderString(surface, $" {COLOR_YELLOW}(only showing the first 50 of {pcb.BreakAtAddresses.Count} breakpoints)", 0, (50 + 2) * ROW_HEIGHT); } FontRenderer.RenderString(surface, $"{COLOR_BRIGHT_WHITE}--------------------------------------------------------------------------------", 0, (50 + 3) * ROW_HEIGHT); FontRenderer.RenderString(surface, $"{COLOR_WHITE}Enter an address in hexidecimal format and press {COLOR_BRIGHT_GREEN}[ENTER] {COLOR_WHITE}to toggle a breakpoint.", 0, (50 + 4) * ROW_HEIGHT); FontRenderer.RenderString(surface, $"{COLOR_WHITE}Type \"{COLOR_BRIGHT_RED}clear{COLOR_WHITE}\" to remove all breakpoints. Press {COLOR_BRIGHT_YELLOW}[ESCAPE] {COLOR_WHITE}to abort.", 0, (50 + 5) * ROW_HEIGHT); FontRenderer.RenderString(surface, $"> {inputString}", 0, (50 + 7) * ROW_HEIGHT); FontRenderer.RenderString(surface, $"{COLOR_BRIGHT_WHITE}--------------------------------------------------------------------------------", 0, (50 + 9) * ROW_HEIGHT); }
/** * Used to start the interactive debugger session with the given game PCB state. * This enables the user to enter commands (continue, step, etc) and examine CPU state etc. */ public void StartInteractiveDebugger(PacManPCB pcb) { _debuggerPcb = pcb; _debuggerState = DebuggerState.Breakpoint; SignalDebuggerNeedsRendering(); }
private void HandleDebuggerEventForBreakpointState(SDL.SDL_Event sdlEvent) { if (sdlEvent.type != SDL.SDL_EventType.SDL_KEYDOWN) { return; } var keycode = sdlEvent.key.keysym.sym; switch (keycode) { case SDL.SDL_Keycode.SDLK_F1: // F1 = Save State _debuggerState = DebuggerState.SaveState; _debuggerFileList = GetDebuggerSaveStateFileList(); _debuggerInputString = String.Empty; SignalDebuggerNeedsRendering(); break; case SDL.SDL_Keycode.SDLK_F2: // F2 = Load State _debuggerState = DebuggerState.LoadState; _debuggerFileList = GetDebuggerSaveStateFileList(); _debuggerInputString = _debuggerFileList.Count > 0 ? _debuggerFileList.Last() : String.Empty; SignalDebuggerNeedsRendering(); break; case SDL.SDL_Keycode.SDLK_F4: // F4 = Edit Breakpoints _debuggerState = DebuggerState.EditBreakpoints; SignalDebuggerNeedsRendering(); break; case SDL.SDL_Keycode.SDLK_F5: // F5 = Continue _debuggerState = DebuggerState.Idle; _debuggerPcb = null; SignalDebuggerNeedsRendering(); OnDebugCommand?.Invoke(new DebugCommandEventArgs() { Action = DebugAction.ResumeContinue }); break; case SDL.SDL_Keycode.SDLK_F9: // F9 = Step Backwards if (_debuggerPcb.ReverseStepEnabled && _debuggerPcb._executionHistory.Count > 0) { _debuggerState = DebuggerState.SingleStepping; _debuggerPcb = null; SignalDebuggerNeedsRendering(); OnDebugCommand?.Invoke(new DebugCommandEventArgs() { Action = DebugAction.ReverseStep }); } break; case SDL.SDL_Keycode.SDLK_F10: // F10 = Single Step _debuggerState = DebuggerState.SingleStepping; _debuggerPcb = null; SignalDebuggerNeedsRendering(); OnDebugCommand?.Invoke(new DebugCommandEventArgs() { Action = DebugAction.ResumeStep }); break; case SDL.SDL_Keycode.SDLK_F11: // F11 = Toggle Annotated Disassembly _debuggerShowAnnotatedDisassembly = !_debuggerShowAnnotatedDisassembly; SignalDebuggerNeedsRendering(); break; case SDL.SDL_Keycode.SDLK_F12: // F12 = Print Last 50 Opcodes _debuggerState = DebuggerState.InstructionHistory; SignalDebuggerNeedsRendering(); break; } }
public static void Start(EmulatorConfig config) { if (String.IsNullOrWhiteSpace(config.RomPath)) { throw new Exception("A directory containing Pac-Man arcade hardware compatible ROM files is required."); } if (!Directory.Exists(config.RomPath)) { throw new Exception($"Could not locate a directory at path {config.RomPath}"); } // Load and validate all of the ROM files needed. var enforceValidChecksums = !config.SkipChecksums; var romData = ROMLoader.LoadFromDisk(config.RomSet, config.RomPath, enforceValidChecksums); // Name the current thread so we can distinguish between the emulator's // CPU thread when using a debugger. Thread.CurrentThread.Name = "Platform (SDL)"; // Initialize the platform wrapper which allows us to interact with // the platform's graphics, audio, and input devices. Wire an event // handler that will handle receiving user input as well as sending // the framebuffer to be rendered. _platform = new Platform(); _platform.OnTick += Platform_OnTick; _platform.OnDebugCommand += Platform_OnDebugCommand; _platform.Initialize("Pac-Man Arcade Hardware Emulator", VideoHardware.RESOLUTION_WIDTH, VideoHardware.RESOLUTION_HEIGHT, GAME_WINDOW_SCALE_X, GAME_WINDOW_SCALE_Y); // Initialize the Pac-Man arcade hardware/emulator and wire event // handlers to receive the framebuffer/samples to be rendered/played. _game = new PacManPCB(); _game.ROMSet = config.RomSet; _game.AllowWritableROM = config.WritableRom; _game.OnRender += PacManPCB_OnRender; _game.OnAudioSample += PacManPCB_OnAudioSample; _game.OnBreakpointHitEvent += PacManPCB_OnBreakpointHit; #region Set Game Options // Use the default values for the hardware DIP switches. var dipSwitchState = new DIPSwitches(); // Look in the current working directory for a settings file. var dipSwitchesPath = "dip-switches.json"; // If the user specified a path for the settings file, use it instead. if (!String.IsNullOrWhiteSpace(config.DipSwitchesConfigPath)) { dipSwitchesPath = config.DipSwitchesConfigPath; } if (File.Exists(dipSwitchesPath)) { // We found a settings file! Parse and load it. var json = File.ReadAllText(dipSwitchesPath); try { dipSwitchState = JsonConvert.DeserializeObject <DIPSwitches>(json); } catch (Exception parseException) { throw new Exception($"Error parsing DIP switch settings JSON file at: {dipSwitchesPath}", parseException); } } else { // If the file doesn't exist and the user specified the path, fail fast. // There must have been a typo or something. In any case the user should fix it. if (!String.IsNullOrWhiteSpace(config.DipSwitchesConfigPath)) { throw new ArgumentException($"Unable to locate a DIP switch settings JSON file at: {dipSwitchesPath}"); } } _game.DIPSwitchState = dipSwitchState; #endregion #region Load State // If the path to a save state was specified to be loaded, deserialize // it from disk and ensure it gets passed into the emulator on start. EmulatorState state = null; if (!String.IsNullOrWhiteSpace(config.LoadStateFilePath)) { var json = File.ReadAllText(config.LoadStateFilePath); state = JsonConvert.DeserializeObject <EmulatorState>(json); } #endregion #region Set Debugging Flags if (config.Debug) { _game.Debug = true; _platform.InitializeDebugger(DEBUG_WINDOW_SCALE_X, DEBUG_WINDOW_SCALE_Y); if (config.Breakpoints != null) { _game.BreakAtAddresses = config.Breakpoints; } if (config.ReverseStep) { _game.ReverseStepEnabled = true; } if (!String.IsNullOrWhiteSpace(config.AnnotationsFilePath)) { if (!File.Exists(config.AnnotationsFilePath)) { throw new Exception($"Could not locate an annotations file at path {config.AnnotationsFilePath}"); } try { var annotations = Annotations.ParseFile(config.AnnotationsFilePath); _game.Annotations = annotations; } catch (Exception ex) { throw new Exception($"Error parsing annotations file.", ex); } } } #endregion // Start the emulation; this occurs in a seperate thread and // therefore this call is non-blocking. _game.Start(romData, state); // Starts the event loop for the user interface; this occurs on // the same thread and is a blocking call. Once this method returns // we know that the user closed the window or quit the program via // the OS (e.g. ALT+F4 / CMD+Q). _platform.StartLoop(); // Ensure the platform resources are cleaned up and stop the emulation. _platform.Dispose(); _game.Stop(); }