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; 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 Stopwatch(); private string _romType = "TBC"; private bool isTurboMode = false; public bool IsRunning { get; private set; } = false; public ushort? Breakpoint { get => _machine?.Breakpoint; set { if (_machine != null) _machine.Breakpoint = value; } } #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. public ParsonsForm1() #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. { InitializeComponent(); this.KeyPreview = true; #pragma warning disable CS8622 // Nullability of reference types in type of parameter doesn't match the target delegate (possibly because of nullability attributes). this.KeyDown += Form1_KeyDown; #pragma warning restore CS8622 // Nullability of reference types in type of parameter doesn't match the target delegate (possibly because of nullability attributes). #pragma warning disable CS8622 // Nullability of reference types in type of parameter doesn't match the target delegate (possibly because of nullability attributes). this.KeyUp += Form1_KeyUp; #pragma warning restore CS8622 // Nullability of reference types in type of parameter doesn't match the target delegate (possibly because of nullability attributes). this.DoubleBuffered = true; this.ResizeRedraw = true; } protected override void OnLoad(EventArgs e) { base.OnLoad(e); this.Text = $"{_romType} Parsons Master System 2026 - {_currentRomName}"; this.BackColor = Color.Black; _machine = new SmsMachine(); _audioPlayer = new NAudioPlayer(); _machine.AudioProcessor.AudioDevice = _audioPlayer; IsRunning = false; PopulateIncludedRomsMenu(); } private void DrawScreen(int[] safeFrame) { // Rapidly the Frame into the Windows Bitmap var data = _screenBitmap.LockBits(new Rectangle(0, 0, 256, 192), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); //Frame buffer draw Marshal.Copy(safeFrame, 0, data.Scan0, safeFrame.Length); //Direct VDP draw //Marshal.Copy(_machine.VideoProcessor.FrameBuffer, 0, data.Scan0, _machine.VideoProcessor.FrameBuffer.Length); _screenBitmap.UnlockBits(data); this.Invalidate(); TotalFrameCount++; } protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); if (_screenBitmap != null && menuStrip1 != null) { // 1. Maintain perfect, chunky retro pixels e.Graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor; e.Graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.Half; int topOffset = menuStrip1.Height; int availableWidth = this.ClientSize.Width; int availableHeight = this.ClientSize.Height - topOffset; // 2. THE LENS LENS: Determine what part of the VDP buffer we actually want to show! // (Safely check if the machine/video processor exists yet) bool isGG = _machine?.VideoProcessor?.IsGameGear ?? false; int sourceWidth = isGG ? 160 : 256; int sourceHeight = isGG ? 144 : 192; int sourceX = isGG ? 48 : 0; // Skip 48 pixels in! int sourceY = isGG ? 24 : 0; // Skip 24 pixels down! // Create a rectangle defining the specific pixels to extract from the raw frame buffer Rectangle sourceRect = new Rectangle(sourceX, sourceY, sourceWidth, sourceHeight); // 3. Calculate the maximum scale factor for the cropped image float scaleX = (float)availableWidth / sourceWidth; float scaleY = (float)availableHeight / sourceHeight; // Pick the smaller scale so the image never bleeds off the edge float scale = Math.Min(scaleX, scaleY); // 4. Calculate the new physical pixel dimensions int newWidth = (int)(sourceWidth * scale); int newHeight = (int)(sourceHeight * scale); // 5. Center the image (Letterboxing / Pillarboxing) int offsetX = (availableWidth - newWidth) / 2; int offsetY = topOffset + ((availableHeight - newHeight) / 2); // Create a rectangle defining exactly where on the Windows form to draw the extracted pixels Rectangle destRect = new Rectangle(offsetX, offsetY, newWidth, newHeight); // 6. Blast the perfectly cropped and scaled bitmap onto the screen! e.Graphics.DrawImage(_screenBitmap, destRect, sourceRect, GraphicsUnit.Pixel); } } public void StartEmulator() { if (IsRunning) return; IsRunning = true; TotalFrameCount = 0; _emulatorTask = Task.Run(() => { double targetFps = 59.94; double TargetFrameTime = 1000.0 / targetFps; _stopwatch.Restart(); double nextFrameTargetTime = _stopwatch.Elapsed.TotalMilliseconds + TargetFrameTime; double lastFpsUpdate = _stopwatch.Elapsed.TotalMilliseconds; int framesThisSecond = 0; try { while (IsRunning) { //targetFps = isTurboMode ? 1000 : 59.94; //TargetFrameTime = 1000.0 / targetFps; BeginInvoke((System.Windows.Forms.MethodInvoker)delegate { targetFps = isTurboMode ? 1000 : 59.94; TargetFrameTime = 1000.0 / targetFps; }); 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(); int[] frozenFrame = (int[])_machine.VideoProcessor.FrameBuffer.Clone(); // 2. FIRE AND FORGET! Tell Windows to draw, but DO NOT WAIT for it to finish! BeginInvoke((System.Windows.Forms.MethodInvoker)delegate { DrawScreen(frozenFrame); }); // 3. Safety catch if (_machine.Breakpoint.HasValue && _machine.Cpu.PC == _machine.Breakpoint.Value) { IsRunning = false; #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. BeginInvoke((System.Windows.Forms.MethodInvoker)delegate { _debugger?.uiUpdateTimer_Tick(null, EventArgs.Empty); }); #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. 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); } } 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 = $"{_romType} Parsons Master System 2026 - {_currentRomName} [FPS: {FramesPerSecond:F0}]"; }); } nextFrameTargetTime += TargetFrameTime; if (currentTime > nextFrameTargetTime + TargetFrameTime) { nextFrameTargetTime = currentTime + TargetFrameTime; } } } catch (Exception ex) { IsRunning = false; this.Invoke((System.Windows.Forms.MethodInvoker)delegate { MessageBox.Show( $"The Z80 CPU Panicked!\n\n" + $"PC Address: 0x{_machine.Cpu.PC:X4}\n" + $"Error: {ex.Message}\n\n" + $"Stack Trace:\n{ex.StackTrace}", "Fatal Emulator Crash", MessageBoxButtons.OK, MessageBoxIcon.Error); }); } }); } public void StopEmulator() { IsRunning = false; } private async void LoadRomDataAndStart(byte[] romData, string fileName, string uniqueId) { StopEmulator(); if (_emulatorTask != null) { await _emulatorTask; } SaveCurrentSram(); // Blast the bytes straight into the virtual cartridge slot! _machine.LoadCartridge(romData); // Check the hardware mode based on the file name bool isGG = fileName.EndsWith(".gg", StringComparison.OrdinalIgnoreCase); _machine.VideoProcessor.IsGameGear = isGG; // We use uniqueId to track the SRAM file path (works for both real paths and resource names) _currentRomPath = uniqueId; _currentRomName = Path.GetFileNameWithoutExtension(fileName); // Update the window title dynamically _romType = isGG ? "GG" : "MS"; this.Text = $"{_romType} Parsons Master System 2026 - {_currentRomName}"; string savPath = GetSaveFilePath(); if (savPath != null) { _machine.MemoryBus.LoadSaveData(savPath); } StartEmulator(); } private void LoadRomAndStart(string filePath) { if (!File.Exists(filePath)) return; // Read from the hard drive, then pass to the universal loader byte[] romData = File.ReadAllBytes(filePath); LoadRomDataAndStart(romData, Path.GetFileName(filePath), filePath); } private string GetSaveFilePath() { // Don't try to save if a game hasn't been loaded yet! if (string.IsNullOrEmpty(_currentRomName) || _currentRomName == "No ROM") #pragma warning disable CS8603 // Possible null reference return. return null; #pragma warning restore CS8603 // Possible null reference return. // Application.StartupPath is the exact folder containing your .exe string exeFolder = Application.StartupPath; // Combines the exe folder with "GameName.sav" return Path.Combine(exeFolder, _currentRomName + ".sav"); } private void SaveCurrentSram() { if (_machine != null) { string savPath = GetSaveFilePath(); if (savPath != null) { _machine.MemoryBus.SaveSaveData(savPath); } } } private void PopulateIncludedRomsMenu() { string romsDirectory = "Desktop.ROMS"; string[] files = GetFilesInResourceDirectory(romsDirectory); foreach (string resourceName in files) { // 1. Strip the "Desktop.ROMS." prefix string cleanFileName = resourceName.Replace("Desktop.ROMS.", ""); // 2. Strip the extension for a beautiful menu item (e.g. "Sonic") string menuName = Path.GetFileNameWithoutExtension(cleanFileName); ToolStripMenuItem romMenuItem = new ToolStripMenuItem(menuName); // 3. Point the click event to our new extraction helper! romMenuItem.Click += (sender, e) => LoadEmbeddedRom(resourceName); // 4. Sort into the correct menu based on extension! if (resourceName.EndsWith(".gg", StringComparison.OrdinalIgnoreCase)) { gameGearToolStripMenuItem.DropDownItems.Add(romMenuItem); } else if (resourceName.EndsWith(".sms", StringComparison.OrdinalIgnoreCase)) { masterSystemToolStripMenuItem.DropDownItems.Add(romMenuItem); } } } private void LoadEmbeddedRom(string resourceName) { // 1. Strip off the ugly prefix to get the real file name for the UI and the .gg check string cleanFileName = resourceName.Replace("Desktop.ROMS.", ""); Assembly assembly = Assembly.GetExecutingAssembly(); // 2. Open a direct pipe to the internal resource using (Stream stream = assembly.GetManifestResourceStream(resourceName)) { if (stream == null) return; // 3. Copy the stream entirely in RAM—never touching the hard drive! using (MemoryStream ms = new MemoryStream()) { stream.CopyTo(ms); byte[] romData = ms.ToArray(); // 4. Pass the raw bytes to our universal loader LoadRomDataAndStart(romData, cleanFileName, resourceName); } } } public static string[] GetFilesInResourceDirectory(string directoryPrefix) { // Get the assembly containing the embedded resources. // Change this if your resources are in a different project/assembly. Assembly assembly = Assembly.GetExecutingAssembly(); // Ensure the prefix ends with a dot so we don't accidentally match // similarly named folders (e.g., matching "Folder" but not "FolderTwo") if (!directoryPrefix.EndsWith(".")) { directoryPrefix += "."; } // Get all resource names and filter by our "directory" prefix string[] allResources = assembly.GetManifestResourceNames(); string[] filesInDirectory = allResources .Where(resource => resource.StartsWith(directoryPrefix, StringComparison.OrdinalIgnoreCase)) .ToArray(); return filesInDirectory; } 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|GG ROMs (*.gg)|*.gg|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 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(); } private async void saveStateToolStripMenuItem_Click(object sender, EventArgs e) { if (string.IsNullOrEmpty(_currentRomName) || _currentRomName == "No ROM") return; // 1. Politely ask the emulator loop to pause, and wait for it to finish its current frame! StopEmulator(); if (_emulatorTask != null) await _emulatorTask; // 2. Change the extension to .state and save it right next to the .exe string statePath = Path.ChangeExtension(GetSaveFilePath(), ".state"); _machine.SaveState(statePath); // 3. Resume the emulator seamlessly StartEmulator(); } private async void loadStateToolStripMenuItem_Click(object sender, EventArgs e) { if (string.IsNullOrEmpty(_currentRomName) || _currentRomName == "No ROM") return; string statePath = Path.ChangeExtension(GetSaveFilePath(), ".state"); if (!File.Exists(statePath)) return; // 1. Pause the emulator StopEmulator(); if (_emulatorTask != null) await _emulatorTask; // 2. Inject the frozen state into the silicon! _machine.LoadState(statePath); // 3. Resume StartEmulator(); } private void turboModeToolStripMenuItem_Click(object sender, EventArgs e) { turboModeToolStripMenuItem.Checked = !turboModeToolStripMenuItem.Checked; isTurboMode = turboModeToolStripMenuItem.Checked ? true : false; } private void aboutToolStripMenuItem_Click(object sender, EventArgs e) { MessageBox.Show( $"First full release of Parsons Master System Emulator 2026!\r\n\r\nFeatures (v1.0):\r\n\r\n" + $" *Full compatibility with all Master System and Game Gear ROMs\r\n" + $" *Full window scaling with fixed aspect ratio (SMS & GG)\r\n" + $" *Full 4 channel SN76489 implememtation using NAudio\r\n" + $" *SRAM auto save and load (Battery Backed RAM)\r\n" + $" *Keyboard and XInput control pad support\r\n" + $" *Comprehensive Debugger\r\n" + $" *VRAM Viewer\r\n" + $" *Single point Save State\r\n" + $" *Turbo mode\r\n" + $" *Includes a selection of commercial ROMs\r\n\r\n" + $"Planned updates (v1.1):\r\n\r\n" + $" *Architectural double buffering (prevent screen tear)\r\n" + $" *Graphics shaders (scanlines etc...)\r\n" + $" *Add multiple Save States per game\r\n\r\n\r\n" + $"(c) Marc Parsons 2026","Parsons Master System 2026 v1.0", MessageBoxButtons.OK, MessageBoxIcon.Information); } } }