diff --git a/Core/Audio/SmsApu.cs b/Core/Audio/SmsApu.cs
index da9efeb..e261d62 100644
--- a/Core/Audio/SmsApu.cs
+++ b/Core/Audio/SmsApu.cs
@@ -1,4 +1,5 @@
using Core.Interfaces;
+using System.IO;
namespace Core.Audio
{
@@ -196,50 +197,29 @@ namespace Core.Audio
}
}
}
+
+ public void SaveState(BinaryWriter bw)
+ {
+ for (int i = 0; i < 8; i++) bw.Write(Registers[i]);
+ bw.Write(_latchedRegister);
+ bw.Write(_sampleCycleTracker);
+ bw.Write(_psgCycleTracker);
+ for (int i = 0; i < 4; i++) { bw.Write(_counters[i]); bw.Write(_polarities[i]); }
+ bw.Write(_lfsr);
+ bw.Write(_previousSample);
+ bw.Write(_previousFiltered);
+ }
+
+ public void LoadState(BinaryReader br)
+ {
+ for (int i = 0; i < 8; i++) Registers[i] = br.ReadUInt16();
+ _latchedRegister = br.ReadInt32();
+ _sampleCycleTracker = br.ReadDouble();
+ _psgCycleTracker = br.ReadInt32();
+ for (int i = 0; i < 4; i++) { _counters[i] = br.ReadInt32(); _polarities[i] = br.ReadInt32(); }
+ _lfsr = br.ReadUInt16();
+ _previousSample = br.ReadSingle();
+ _previousFiltered = br.ReadSingle();
+ }
}
}
-
-
-
-
-
-
-
-
-
-
-
-
-
-//using System;
-
-//namespace Core.Audio
-//{
-// public class SmsApu
-// {
-// // The 8 internal registers of the PSG
-// // 0: Tone 0 Frequency (10 bits)
-// // 1: Tone 0 Volume (4 bits)
-// // 2: Tone 1 Frequency (10 bits)
-// // 3: Tone 1 Volume (4 bits)
-// // 4: Tone 2 Frequency (10 bits)
-// // 5: Tone 2 Volume (4 bits)
-// // 6: Noise Control (3 bits)
-// // 7: Noise Volume (4 bits)
-// public ushort[] Registers { get; private set; } = new ushort[8];
-
-// // Remembers which register the CPU is currently talking to
-// private int _latchedRegister = 0;
-
-// public SmsApu()
-// {
-// // Volumes default to 0x0F (Silence! 0 = max volume, 15 = off)
-// Registers[1] = 0x0F;
-// Registers[3] = 0x0F;
-// Registers[5] = 0x0F;
-// Registers[7] = 0x0F;
-// }
-
-
-// }
-//}
\ No newline at end of file
diff --git a/Core/Cpu/Z80.cs b/Core/Cpu/Z80.cs
index df53c9c..3f10abc 100644
--- a/Core/Cpu/Z80.cs
+++ b/Core/Cpu/Z80.cs
@@ -1,4 +1,5 @@
using System;
+using System.IO;
using Core.Interfaces;
using Core.Io;
@@ -97,6 +98,32 @@ namespace Core.Cpu
TotalTStates = 0;
}
+ public void SaveState(BinaryWriter bw)
+ {
+ bw.Write(TotalTStates);
+ bw.Write(InterruptMode);
+ bw.Write(IFF1);
+ bw.Write(IFF2);
+ bw.Write(InterruptRequested);
+ bw.Write(AF.Word); bw.Write(BC.Word); bw.Write(DE.Word); bw.Write(HL.Word);
+ bw.Write(AF_Prime.Word); bw.Write(BC_Prime.Word); bw.Write(DE_Prime.Word); bw.Write(HL_Prime.Word);
+ bw.Write(IX.Word); bw.Write(IY.Word);
+ bw.Write(PC); bw.Write(SP); bw.Write(I); bw.Write(R);
+ }
+
+ public void LoadState(BinaryReader br)
+ {
+ TotalTStates = br.ReadInt64();
+ InterruptMode = br.ReadInt32();
+ IFF1 = br.ReadBoolean();
+ IFF2 = br.ReadBoolean();
+ InterruptRequested = br.ReadBoolean();
+ AF.Word = br.ReadUInt16(); BC.Word = br.ReadUInt16(); DE.Word = br.ReadUInt16(); HL.Word = br.ReadUInt16();
+ AF_Prime.Word = br.ReadUInt16(); BC_Prime.Word = br.ReadUInt16(); DE_Prime.Word = br.ReadUInt16(); HL_Prime.Word = br.ReadUInt16();
+ IX.Word = br.ReadUInt16(); IY.Word = br.ReadUInt16();
+ PC = br.ReadUInt16(); SP = br.ReadUInt16(); I = br.ReadByte(); R = br.ReadByte();
+ }
+
private void ApplyWaitStates(ushort address)
{
// If a system (like a ULA) is attached and listening, ask it for the delay
diff --git a/Core/Memory/SmsMemoryBus.cs b/Core/Memory/SmsMemoryBus.cs
index 10bbe81..c9273f1 100644
--- a/Core/Memory/SmsMemoryBus.cs
+++ b/Core/Memory/SmsMemoryBus.cs
@@ -1,5 +1,6 @@
using Core.Interfaces;
using System;
+using System.IO;
namespace Core.Memory
{
@@ -225,6 +226,28 @@ namespace Core.Memory
}
}
+ public void SaveState(BinaryWriter bw)
+ {
+ bw.Write(_workRam);
+ bw.Write(_cartridgeRam);
+ bw.Write(SramUsed);
+ bw.Write(_mapperControl);
+ bw.Write(_romBank0);
+ bw.Write(_romBank1);
+ bw.Write(_romBank2);
+ }
+
+ public void LoadState(BinaryReader br)
+ {
+ Array.Copy(br.ReadBytes(_workRam.Length), _workRam, _workRam.Length);
+ Array.Copy(br.ReadBytes(_cartridgeRam.Length), _cartridgeRam, _cartridgeRam.Length);
+ SramUsed = br.ReadBoolean();
+ _mapperControl = br.ReadByte();
+ _romBank0 = br.ReadInt32();
+ _romBank1 = br.ReadInt32();
+ _romBank2 = br.ReadInt32();
+ }
+
public void CleanRAMData()
{
Array.Clear(_workRam, 0, _workRam.Length);
diff --git a/Core/SmsMachine.cs b/Core/SmsMachine.cs
index bf87c61..0d280c7 100644
--- a/Core/SmsMachine.cs
+++ b/Core/SmsMachine.cs
@@ -73,5 +73,30 @@ namespace Core
}
}
}
+ public void SaveState(string filePath)
+ {
+ using (var fs = new System.IO.FileStream(filePath, System.IO.FileMode.Create))
+ using (var bw = new System.IO.BinaryWriter(fs))
+ {
+ Cpu.SaveState(bw);
+ MemoryBus.SaveState(bw);
+ VideoProcessor.SaveState(bw);
+ AudioProcessor.SaveState(bw);
+ }
+ }
+
+ public void LoadState(string filePath)
+ {
+ if (!System.IO.File.Exists(filePath)) return;
+
+ using (var fs = new System.IO.FileStream(filePath, System.IO.FileMode.Open))
+ using (var br = new System.IO.BinaryReader(fs))
+ {
+ Cpu.LoadState(br);
+ MemoryBus.LoadState(br);
+ VideoProcessor.LoadState(br);
+ AudioProcessor.LoadState(br);
+ }
+ }
}
}
\ No newline at end of file
diff --git a/Core/Video/SmsVdp.cs b/Core/Video/SmsVdp.cs
index d2b946c..7a67b33 100644
--- a/Core/Video/SmsVdp.cs
+++ b/Core/Video/SmsVdp.cs
@@ -1,4 +1,5 @@
using System;
+using System.IO;
namespace Core.Video
{
@@ -11,6 +12,10 @@ namespace Core.Video
public int[] FrameBuffer { get; private set; } = new int[256 * 192];
private bool[] _priorityBuffer = new bool[256 * 192]; // Tracks priority pixels!
+ // Hardware Latches
+ private int _latchedHScroll = 0;
+ private int _latchedVScroll = 0;
+
// The Control Port State Machine (Port 0xBF)
private bool _isSecondControlByte = false;
private ushort _controlWord = 0;
@@ -109,12 +114,20 @@ namespace Core.Video
{
_tStateCounter += tStates;
- // 228 T-States per scanline
if (_tStateCounter >= 228)
{
_tStateCounter -= 228;
- // --- MISSING LINE INTERRUPT COUNTDOWN ---
+ // 1. RENDER THE CURRENT LINE FIRST!
+ // The CPU just finished spending 228 cycles on this exact line.
+ // We draw it now using whatever scroll values the CPU set during that time.
+ if (_currentScanline < 192)
+ {
+ RenderScanline(_currentScanline);
+ }
+
+ // 2. CHECK LINE INTERRUPTS
+ // Now that the line is drawn, we check if we need to alert the CPU for the NEXT line.
if (_currentScanline <= 192)
{
_lineCounter--;
@@ -128,24 +141,21 @@ namespace Core.Video
{
_lineCounter = Registers[10]; // Reload outside active display
}
- // ----------------------------------------
+ // 3. MOVE TO THE NEXT LINE
_currentScanline++;
- if (_currentScanline < 192)
- {
- RenderScanline(_currentScanline);
- }
- else if (_currentScanline == 192)
- {
- _statusRegister |= 0x80; // Set VBlank Flag
- }
-
- // End of the NTSC frame (262 lines)
if (_currentScanline > 261)
{
_currentScanline = 0;
}
+
+ // 4. TRIGGER VBLANK
+ // The Master System sets the VBlank flag at the exact start of scanline 192.
+ if (_currentScanline == 192)
+ {
+ _statusRegister |= 0x80;
+ }
}
}
@@ -160,8 +170,8 @@ namespace Core.Video
// --- 1. RENDER BACKGROUND LINE ---
ushort nameTableBase = (ushort)((Registers[2] & 0x0E) << 10);
- byte scrollX = Registers[8];
- byte scrollY = Registers[9];
+ int scrollX = Registers[8];
+ int scrollY = Registers[9];
// THE FIX: The bits are now in the correct order!
bool lockColScroll = (Registers[0] & 0x80) != 0; // Bit 7: Locks right 8 columns (Fixes R-Type!)
@@ -303,5 +313,40 @@ namespace Core.Video
}
}
}
+ public void SaveState(BinaryWriter bw)
+ {
+ bw.Write(VRAM);
+ bw.Write(CRAM);
+ bw.Write(Registers);
+ bw.Write(_isSecondControlByte);
+ bw.Write(_controlWord);
+ bw.Write(_readBuffer);
+ bw.Write(_tStateCounter);
+ bw.Write(_currentScanline);
+ bw.Write(_lineCounter);
+ bw.Write(_statusRegister);
+
+ // ADD THESE:
+ bw.Write(_latchedHScroll);
+ bw.Write(_latchedVScroll);
+ }
+
+ public void LoadState(BinaryReader br)
+ {
+ Array.Copy(br.ReadBytes(VRAM.Length), VRAM, VRAM.Length);
+ Array.Copy(br.ReadBytes(CRAM.Length), CRAM, CRAM.Length);
+ Array.Copy(br.ReadBytes(Registers.Length), Registers, Registers.Length);
+ _isSecondControlByte = br.ReadBoolean();
+ _controlWord = br.ReadUInt16();
+ _readBuffer = br.ReadByte();
+ _tStateCounter = br.ReadInt32();
+ _currentScanline = br.ReadInt32();
+ _lineCounter = br.ReadInt32();
+ _statusRegister = br.ReadByte();
+
+ // ADD THESE:
+ _latchedHScroll = br.ReadInt32();
+ _latchedVScroll = br.ReadInt32();
+ }
}
}
\ No newline at end of file
diff --git a/Desktop/Desktop.csproj b/Desktop/Desktop.csproj
index f47696b..e5cba67 100644
--- a/Desktop/Desktop.csproj
+++ b/Desktop/Desktop.csproj
@@ -13,6 +13,8 @@
true
true
win-x64
+ Desktop.Program
+ Parsons Master System
diff --git a/Desktop/Form1.Designer.cs b/Desktop/Form1.Designer.cs
index 6786aa3..73f1a78 100644
--- a/Desktop/Form1.Designer.cs
+++ b/Desktop/Form1.Designer.cs
@@ -36,11 +36,13 @@
exitToolStripMenuItem = new ToolStripMenuItem();
viewToolStripMenuItem = new ToolStripMenuItem();
debuggerToolStripMenuItem = new ToolStripMenuItem();
+ vRAMViewerToolStripMenuItem = new ToolStripMenuItem();
machineToolStripMenuItem = new ToolStripMenuItem();
resetToolStripMenuItem = new ToolStripMenuItem();
helpToolStripMenuItem = new ToolStripMenuItem();
aboutToolStripMenuItem = new ToolStripMenuItem();
- vRAMViewerToolStripMenuItem = new ToolStripMenuItem();
+ saveStateToolStripMenuItem = new ToolStripMenuItem();
+ loadStateToolStripMenuItem = new ToolStripMenuItem();
menuStrip1.SuspendLayout();
SuspendLayout();
//
@@ -99,13 +101,20 @@
// debuggerToolStripMenuItem
//
debuggerToolStripMenuItem.Name = "debuggerToolStripMenuItem";
- debuggerToolStripMenuItem.Size = new Size(270, 34);
+ debuggerToolStripMenuItem.Size = new Size(221, 34);
debuggerToolStripMenuItem.Text = "Debugger";
debuggerToolStripMenuItem.Click += debuggerToolStripMenuItem_Click;
//
+ // vRAMViewerToolStripMenuItem
+ //
+ vRAMViewerToolStripMenuItem.Name = "vRAMViewerToolStripMenuItem";
+ vRAMViewerToolStripMenuItem.Size = new Size(221, 34);
+ vRAMViewerToolStripMenuItem.Text = "VRAM Viewer";
+ vRAMViewerToolStripMenuItem.Click += vramViewerToolStripMenuItem_Click;
+ //
// machineToolStripMenuItem
//
- machineToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { resetToolStripMenuItem });
+ machineToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { resetToolStripMenuItem, saveStateToolStripMenuItem, loadStateToolStripMenuItem });
machineToolStripMenuItem.Name = "machineToolStripMenuItem";
machineToolStripMenuItem.Size = new Size(94, 29);
machineToolStripMenuItem.Text = "Machine";
@@ -113,7 +122,7 @@
// resetToolStripMenuItem
//
resetToolStripMenuItem.Name = "resetToolStripMenuItem";
- resetToolStripMenuItem.Size = new Size(156, 34);
+ resetToolStripMenuItem.Size = new Size(270, 34);
resetToolStripMenuItem.Text = "Reset";
resetToolStripMenuItem.Click += resetToolStripMenuItem_Click;
//
@@ -130,12 +139,19 @@
aboutToolStripMenuItem.Size = new Size(164, 34);
aboutToolStripMenuItem.Text = "About";
//
- // vRAMViewerToolStripMenuItem
+ // saveStateToolStripMenuItem
//
- vRAMViewerToolStripMenuItem.Name = "vRAMViewerToolStripMenuItem";
- vRAMViewerToolStripMenuItem.Size = new Size(270, 34);
- vRAMViewerToolStripMenuItem.Text = "VRAM Viewer";
- vRAMViewerToolStripMenuItem.Click += vramViewerToolStripMenuItem_Click;
+ saveStateToolStripMenuItem.Name = "saveStateToolStripMenuItem";
+ saveStateToolStripMenuItem.Size = new Size(270, 34);
+ saveStateToolStripMenuItem.Text = "Save State";
+ saveStateToolStripMenuItem.Click += saveStateToolStripMenuItem_Click;
+ //
+ // loadStateToolStripMenuItem
+ //
+ loadStateToolStripMenuItem.Name = "loadStateToolStripMenuItem";
+ loadStateToolStripMenuItem.Size = new Size(270, 34);
+ loadStateToolStripMenuItem.Text = "Load State";
+ loadStateToolStripMenuItem.Click += loadStateToolStripMenuItem_Click;
//
// ParsonsForm1
//
@@ -168,5 +184,7 @@
private ToolStripMenuItem includedToolStripMenuItem;
private ToolStripMenuItem selectROMToolStripMenuItem1;
private ToolStripMenuItem vRAMViewerToolStripMenuItem;
+ private ToolStripMenuItem saveStateToolStripMenuItem;
+ private ToolStripMenuItem loadStateToolStripMenuItem;
}
}
diff --git a/Desktop/Form1.cs b/Desktop/Form1.cs
index d4a834f..198dbcc 100644
--- a/Desktop/Form1.cs
+++ b/Desktop/Form1.cs
@@ -37,13 +37,8 @@ namespace Desktop
public ParsonsForm1()
{
InitializeComponent();
- this.Text = $"Parsons Master System 2026 - {_currentRomName}";
- _machine = new SmsMachine();
- _audioPlayer = new NAudioPlayer();
- _machine.AudioProcessor.AudioDevice = _audioPlayer;
-
- PopulateIncludedRomsMenu();
+ // These are perfectly safe for the Visual Studio Designer!
this.KeyPreview = true;
this.KeyDown += Form1_KeyDown;
this.KeyUp += Form1_KeyUp;
@@ -51,6 +46,21 @@ namespace Desktop
this.ResizeRedraw = true;
}
+ // The Designer ignores this completely, but the compiled game runs it instantly!
+ protected override void OnLoad(EventArgs e)
+ {
+ base.OnLoad(e);
+
+ this.Text = $"Parsons Master System - {_currentRomName}";
+
+ // Safe to initialize hardware and files here!
+ _machine = new SmsMachine();
+ _audioPlayer = new NAudioPlayer();
+ _machine.AudioProcessor.AudioDevice = _audioPlayer;
+
+ PopulateIncludedRomsMenu();
+ }
+
private void DrawScreen()
{
// Rapidly copy our VDP FrameBuffer into the Windows Bitmap
@@ -65,7 +75,8 @@ namespace Desktop
// Always call the base method so Windows can draw your MenuStrip!
base.OnPaint(e);
- if (_screenBitmap != null)
+ // THE FIX: We MUST ensure the designer has actually built the menu strip before asking for its height!
+ if (_screenBitmap != null && menuStrip1 != null)
{
// 1. Set the rendering mode for perfect, chunky retro pixels!
e.Graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
@@ -73,7 +84,7 @@ namespace Desktop
// 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)
+ int topOffset = menuStrip1.Height;
Rectangle renderArea = new Rectangle(
0,
topOffset,
@@ -205,42 +216,37 @@ namespace Desktop
_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);
+ // 5. LOAD THE NEW SAVE DATA FROM THE EXE FOLDER!
+ string savPath = GetSaveFilePath();
+ if (savPath != null)
+ {
+ _machine.MemoryBus.LoadSaveData(savPath);
+ }
// 6. Turn the power on!
StartEmulator();
}
+ private string GetSaveFilePath()
+ {
+ // Don't try to save if a game hasn't been loaded yet!
+ if (string.IsNullOrEmpty(_currentRomName) || _currentRomName == "No ROM")
+ return null;
- //private async void LoadRomAndStart(string filePath)
- //{
- // StopEmulator();
- // if (_emulatorTask != null)
- // {
- // await _emulatorTask;
- // }
+ // Application.StartupPath is the exact folder containing your .exe
+ string exeFolder = Application.StartupPath;
- // // 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();
- //}
+ // Combines the exe folder with "GameName.sav"
+ return Path.Combine(exeFolder, _currentRomName + ".sav");
+ }
private void SaveCurrentSram()
{
- if (!string.IsNullOrEmpty(_currentRomPath) && _machine != null)
+ if (_machine != null)
{
- // Swaps ".sms" for ".sav"
- string savPath = Path.ChangeExtension(_currentRomPath, ".sav");
- _machine.MemoryBus.SaveSaveData(savPath);
+ string savPath = GetSaveFilePath();
+ if (savPath != null)
+ {
+ _machine.MemoryBus.SaveSaveData(savPath);
+ }
}
}
@@ -361,5 +367,38 @@ namespace Desktop
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();
+ }
}
}