From ec40e04ff3c2f6c331bd9ee011199c4accb065ef Mon Sep 17 00:00:00 2001 From: parsons Date: Fri, 15 May 2026 23:38:40 +0100 Subject: [PATCH] Fixed per scanline interrupts. No artifacts in MMCOI --- Core/Audio/SmsApu.cs | 70 +++++++++-------------- Core/Cpu/Z80.cs | 27 +++++++++ Core/Memory/SmsMemoryBus.cs | 23 ++++++++ Core/SmsMachine.cs | 25 +++++++++ Core/Video/SmsVdp.cs | 75 ++++++++++++++++++++----- Desktop/Desktop.csproj | 2 + Desktop/Form1.Designer.cs | 36 +++++++++--- Desktop/Form1.cs | 109 ++++++++++++++++++++++++------------ 8 files changed, 263 insertions(+), 104 deletions(-) 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(); + } } }