315 lines
12 KiB
C#
315 lines
12 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;
|
|
using Desktop.Audio;
|
|
|
|
namespace Desktop
|
|
{
|
|
public partial class ParsonsForm1 : Form
|
|
{
|
|
public SmsMachine _machine = null!;
|
|
private DebuggerForm _debugger;
|
|
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 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 / 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;
|
|
|
|
// --- 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;
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
_machine.IoBus.Joypad1Keyboard &= (byte)~bitMask; // Target Keyboard!
|
|
}
|
|
else
|
|
{
|
|
_machine.IoBus.Joypad1Keyboard |= bitMask; // Target Keyboard!
|
|
}
|
|
}
|
|
|
|
private void ParsonsForm1_FormClosing(object sender, FormClosingEventArgs e)
|
|
{
|
|
IsRunning = false;
|
|
_audioPlayer?.Stop();
|
|
}
|
|
}
|
|
}
|