Files
ParsonsMasterSystem2026/Desktop/Form1.cs

271 lines
9.4 KiB
C#

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;
namespace Desktop
{
public partial class ParsonsForm1 : Form
{
private SmsMachine _machine = null!;
private DebuggerForm _debugger;
private Bitmap _screenBitmap = new Bitmap(256, 192, PixelFormat.Format32bppArgb);
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 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();
PopulateIncludedRomsMenu();
this.KeyPreview = true;
this.KeyDown += Form1_KeyDown;
this.KeyUp += Form1_KeyUp;
}
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);
// Update the PictureBox
pbScreen.Image = _screenBitmap;
TotalFrameCount++;
}
public void StartEmulator()
{
if (IsRunning) return;
IsRunning = true;
TotalFrameCount = 0;
double TargetFrameTime = 1000.0 / 60.0;
_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;
// 1. Do the heavy lifting (Z80 and VDP)
_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;
}
public void StepEmulator()
{
_machine.StepMachine();
}
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 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 includedToolStripMenuItem_Click(object sender, EventArgs e)
{
}
private void exitToolStripMenuItem_Click(object sender, EventArgs e)
{
Environment.Exit(0);
}
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)
{
// Active-Low: Clear the specific bit to 0 using a bitwise AND with a NOT mask
_machine.IoBus.Joypad1State &= (byte)~bitMask;
}
else
{
// Active-Low: Reset the specific bit to 1 using a bitwise OR mask
_machine.IoBus.Joypad1State |= bitMask;
}
}
}
}