using System.Diagnostics; using System.Drawing.Imaging; using System.Reflection; using System.Reflection.PortableExecutable; using System.Runtime.InteropServices; using System.Threading.Tasks; using System.Windows.Forms; using Core; using Desktop.Audio; namespace Desktop { public partial class ParsonsForm1 : Form { public SmsMachine _machine = null!; private DebuggerForm _debugger; private VramViewerForm _vramViewer; private Bitmap _screenBitmap = new Bitmap(256, 192, PixelFormat.Format32bppArgb); private NAudioPlayer _audioPlayer; private Task _emulatorTask; private double TargetFrameTime = 16.667f; public int TotalFrameCount = 0; public double FrameTime { get; private set; } = 0; public double FramesPerSecond { get; private set; } = 0; private string _currentRomName = "No ROM"; private string _currentRomPath = ""; private Stopwatch _stopwatch = new System.Diagnostics.Stopwatch(); public bool IsRunning { get; private set; } = false; public ushort? Breakpoint { get => _machine?.Breakpoint; set { if (_machine != null) _machine.Breakpoint = value; } } public ParsonsForm1() { InitializeComponent(); this.Text = $"Parsons Master System 2026 - {_currentRomName}"; _machine = new SmsMachine(); _audioPlayer = new NAudioPlayer(); _machine.AudioProcessor.AudioDevice = _audioPlayer; PopulateIncludedRomsMenu(); this.KeyPreview = true; this.KeyDown += Form1_KeyDown; this.KeyUp += Form1_KeyUp; this.DoubleBuffered = true; this.ResizeRedraw = true; } private void DrawScreen() { // Rapidly copy our VDP FrameBuffer into the Windows Bitmap var data = _screenBitmap.LockBits(new Rectangle(0, 0, 256, 192), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); Marshal.Copy(_machine.VideoProcessor.FrameBuffer, 0, data.Scan0, _machine.VideoProcessor.FrameBuffer.Length); _screenBitmap.UnlockBits(data); this.Invalidate(); TotalFrameCount++; } protected override void OnPaint(PaintEventArgs e) { // Always call the base method so Windows can draw your MenuStrip! base.OnPaint(e); if (_screenBitmap != null) { // 1. Set the rendering mode for perfect, chunky retro pixels! e.Graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor; e.Graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.Half; // 2. Calculate the drawing area. // We start drawing BELOW the MenuStrip so it doesn't get covered up. int topOffset = menuStrip1.Height; // Change 'menuStrip1' if your menu has a different (Name) Rectangle renderArea = new Rectangle( 0, topOffset, this.ClientSize.Width, this.ClientSize.Height - topOffset ); // 3. Blast the bitmap directly onto the graphics card buffer! e.Graphics.DrawImage(_screenBitmap, renderArea); } } public void StartEmulator() { if (IsRunning) return; IsRunning = true; TotalFrameCount = 0; double TargetFrameTime = 1000.0 / 59.92274; _emulatorTask = Task.Run(() => { _stopwatch.Restart(); double nextFrameTargetTime = _stopwatch.Elapsed.TotalMilliseconds + TargetFrameTime; double lastFpsUpdate = _stopwatch.Elapsed.TotalMilliseconds; int framesThisSecond = 0; while (IsRunning) { // Mark exactly when the emulator starts thinking double frameStartTime = _stopwatch.Elapsed.TotalMilliseconds; // --- POLL PHYSICAL CONTROLLER --- if (XInput.XInputGetState(0, out XInput.XINPUT_STATE state) == 0) { ushort btns = state.Gamepad.wButtons; byte padState = 0xFF; if ((btns & 0x0001) != 0) padState &= 0xFE; // Up if ((btns & 0x0002) != 0) padState &= 0xFD; // Down if ((btns & 0x0004) != 0) padState &= 0xFB; // Left if ((btns & 0x0008) != 0) padState &= 0xF7; // Right if ((btns & 0x1000) != 0) padState &= 0xEF; // Button 1 if ((btns & 0x2000) != 0) padState &= 0xDF; // Button 2 // THE FIX: Constantly update the gamepad state, even when it's 0xFF! _machine.IoBus.Joypad1Gamepad = padState; } // -------------------------------- _machine.RunFrame(); // 2. FIRE AND FORGET! Tell Windows to draw, but DO NOT WAIT for it to finish! BeginInvoke((System.Windows.Forms.MethodInvoker)delegate { DrawScreen(); }); // 3. Safety catch if (_machine.Breakpoint.HasValue && _machine.Cpu.PC == _machine.Breakpoint.Value) { IsRunning = false; BeginInvoke((System.Windows.Forms.MethodInvoker)delegate { _debugger?.uiUpdateTimer_Tick(null, EventArgs.Empty); }); break; } // 4. Calculate TRUE Frame Time (Compute Time) BEFORE we go to sleep! FrameTime = _stopwatch.Elapsed.TotalMilliseconds - frameStartTime; // --- HIGH PRECISION THROTTLE --- while (_stopwatch.Elapsed.TotalMilliseconds < nextFrameTargetTime) { if (nextFrameTargetTime - _stopwatch.Elapsed.TotalMilliseconds > 2.0) { Thread.Sleep(1); } else { Thread.SpinWait(10); } } // --- METRICS MATH --- double currentTime = _stopwatch.Elapsed.TotalMilliseconds; TotalFrameCount++; framesThisSecond++; if (currentTime - lastFpsUpdate >= 1000.0) { FramesPerSecond = framesThisSecond / ((currentTime - lastFpsUpdate) / 1000.0); framesThisSecond = 0; lastFpsUpdate = currentTime; BeginInvoke((System.Windows.Forms.MethodInvoker)delegate { this.Text = $"Parsons Master System - {_currentRomName} [FPS/FT: {FramesPerSecond:F0}/{FrameTime:F1}]"; }); } nextFrameTargetTime += TargetFrameTime; if (currentTime > nextFrameTargetTime + TargetFrameTime) { nextFrameTargetTime = currentTime + TargetFrameTime; } } }); } public void StopEmulator() { IsRunning = false; } private async void LoadRomAndStart(string filePath) { StopEmulator(); if (_emulatorTask != null) { await _emulatorTask; } // 1. SAVE THE PREVIOUS GAME! SaveCurrentSram(); // 2. Load the file byte[] rom = File.ReadAllBytes(filePath); // 3. Jam it into the Sega Mapper _machine.LoadCartridge(rom); // 4. Update the path tracking _currentRomPath = filePath; _currentRomName = Path.GetFileNameWithoutExtension(filePath); this.Text = $"Parsons Master System - {_currentRomName}"; // 5. LOAD THE NEW SAVE DATA! string savPath = Path.ChangeExtension(_currentRomPath, ".sav"); _machine.MemoryBus.LoadSaveData(savPath); // 6. Turn the power on! StartEmulator(); } //private async void LoadRomAndStart(string filePath) //{ // StopEmulator(); // if (_emulatorTask != null) // { // await _emulatorTask; // } // // 2. Load the file // byte[] rom = File.ReadAllBytes(filePath); // // 3. Jam it into the Sega Mapper // _machine.LoadCartridge(rom); // _currentRomName = Path.GetFileNameWithoutExtension(filePath); // this.Text = $"Parsons Master System - {_currentRomName}"; // // 5. Turn the power on! // StartEmulator(); //} private void SaveCurrentSram() { if (!string.IsNullOrEmpty(_currentRomPath) && _machine != null) { // Swaps ".sms" for ".sav" string savPath = Path.ChangeExtension(_currentRomPath, ".sav"); _machine.MemoryBus.SaveSaveData(savPath); } } private void PopulateIncludedRomsMenu() { // The folder you used for Golden Axe Warrior string romsDirectory = @"C:\Parsons\Local Code Projects\ParsonsMasterSystem2026\Desktop\ROMS\"; if (Directory.Exists(romsDirectory)) { string[] romFiles = Directory.GetFiles(romsDirectory, "*.sms"); foreach (string file in romFiles) { // Create a new dropdown item for each .sms file it finds string romName = Path.GetFileNameWithoutExtension(file); ToolStripMenuItem romMenuItem = new ToolStripMenuItem(romName); // When clicked, pass the file path to our helper method romMenuItem.Click += (sender, e) => LoadRomAndStart(file); // Add it to the "Included" submenu (make sure the name matches your designer element!) includedToolStripMenuItem.DropDownItems.Add(romMenuItem); } } } private void selectROMToolStripMenuItem_Click(object sender, EventArgs e) { using (OpenFileDialog ofd = new OpenFileDialog()) { ofd.Title = "Select Master System ROM"; ofd.Filter = "SMS ROMs (*.sms)|*.sms|All Files (*.*)|*.*"; if (ofd.ShowDialog() == DialogResult.OK) { LoadRomAndStart(ofd.FileName); _machine.Reset(); } } } private void resetToolStripMenuItem_Click(object sender, EventArgs e) { _machine.Reset(); } private void debuggerToolStripMenuItem_Click(object sender, EventArgs e) { if (_debugger == null || _debugger.IsDisposed) { _debugger = new DebuggerForm(_machine.Cpu, _machine.MemoryBus, this); } _debugger.Show(); } private void vramViewerToolStripMenuItem_Click(object sender, EventArgs e) { if (_vramViewer == null || _vramViewer.IsDisposed) { // Pass the VDP directly to the viewer! _vramViewer = new VramViewerForm(_machine.VideoProcessor); } _vramViewer.Show(); } private void includedToolStripMenuItem_Click(object sender, EventArgs e) { } private void exitToolStripMenuItem_Click(object sender, EventArgs e) { this.Close(); } private void Form1_KeyDown(object sender, KeyEventArgs e) { UpdateJoypad(e.KeyCode, true); } private void Form1_KeyUp(object sender, KeyEventArgs e) { UpdateJoypad(e.KeyCode, false); } private void UpdateJoypad(Keys key, bool isPressed) { if (_machine == null) return; byte bitMask = 0; // Map your keys to the Sega hardware bits switch (key) { case Keys.W: bitMask = 0x01; break; // Bit 0: Up case Keys.S: bitMask = 0x02; break; // Bit 1: Down case Keys.A: bitMask = 0x04; break; // Bit 2: Left case Keys.D: bitMask = 0x08; break; // Bit 3: Right case Keys.O: bitMask = 0x10; break; // Bit 4: Button 1 (Start/Action) case Keys.P: bitMask = 0x20; break; // Bit 5: Button 2 default: return; // Ignore any other keys } if (isPressed) { _machine.IoBus.Joypad1Keyboard &= (byte)~bitMask; // Target Keyboard! } else { _machine.IoBus.Joypad1Keyboard |= bitMask; // Target Keyboard! } } private void ParsonsForm1_FormClosing(object sender, FormClosingEventArgs e) { IsRunning = false; SaveCurrentSram(); _audioPlayer?.Stop(); } } }