diff --git a/Core/Cpu/Z80.cs b/Core/Cpu/Z80.cs index 36e1489..04a3e00 100644 --- a/Core/Cpu/Z80.cs +++ b/Core/Cpu/Z80.cs @@ -1827,7 +1827,8 @@ namespace Core.Cpu return 16; } default: - throw new NotImplementedException($"Extended ED Opcode 0x{extendedOpcode:X2} at PC 0x{(PC - 1):X4} is not implemented."); + return 8; + //throw new NotImplementedException($"Extended ED Opcode 0x{extendedOpcode:X2} at PC 0x{(PC - 1):X4} is not implemented."); } } diff --git a/Core/Memory/SmsMemoryBus.cs b/Core/Memory/SmsMemoryBus.cs index c9273f1..178dabb 100644 --- a/Core/Memory/SmsMemoryBus.cs +++ b/Core/Memory/SmsMemoryBus.cs @@ -42,41 +42,80 @@ namespace Core.Memory public byte Read(ushort address) { - if (address < 0x4000) // ROM Slot 0 (0x0000 - 0x3FFF) + if (address < 0x4000) { - // SMS Hardware Quirk: The first 1KB (0x0000 - 0x03FF) is NEVER paged. - if (address < 0x0400) return ReadFromCartridge(0, address); - - return ReadFromCartridge(_romBank0, address); - } - if (address < 0x8000) // ROM Slot 1 (0x4000 - 0x7FFF) - { - return ReadFromCartridge(_romBank1, address & 0x3FFF); - } - if (address < 0xC000) // ROM Slot 2 (0x8000 - 0xBFFF) - { - // --- THE MISSING LINK: Read from Save RAM if enabled! --- - if ((_mapperControl & 0x08) != 0) + if (address < 0x0400) { - int ramBank = (_mapperControl & 0x04) != 0 ? 1 : 0; - int ramOffset = (ramBank * 0x4000) + (address & 0x1FFF); - return _cartridgeRam[ramOffset]; + return _cartridgeRom[address]; } - - // Otherwise, read from the ROM as usual - return ReadFromCartridge(_romBank2, address & 0x3FFF); + // Slot 0 + return _cartridgeRom[(_romBank0 * 0x4000) + address]; + } + if (address < 0x8000) + { + // Slot 1 + return _cartridgeRom[(_romBank1 * 0x4000) + (address - 0x4000)]; + } + if (address < 0xC000) + { + // Slot 2 (Or SRAM) + if (SramUsed && (_mapperControl & 0x08) != 0) + return _cartridgeRam[address - 0x8000]; + else + return _cartridgeRom[(_romBank2 * 0x4000) + (address - 0x8000)]; } - // System RAM (0xC000 - 0xFFFF) + // THE FIX 1: Bitwise AND perfectly forces 0xE000-0xFFFF to mirror down to 0xC000! return _workRam[address & 0x1FFF]; } + public void Write(ushort address, byte value) + { + if (address < 0x8000) return; // Cannot write to physical ROM + + if (address < 0xC000) + { + if (SramUsed && (_mapperControl & 0x08) != 0) + _cartridgeRam[address - 0x8000] = value; + return; + } + + // THE FIX 1: Save to RAM using the mirror mask + _workRam[address & 0x1FFF] = value; + + // THE FIX 2: Mapper Control Registers live at the very end of the mirrored RAM! + if (address >= 0xFFFC) + { + // Calculate the absolute maximum number of banks inside the currently loaded ROM + int totalBanks = _cartridgeRom.Length / 0x4000; + + // Safety catch for empty slots + if (totalBanks == 0) totalBanks = 1; + + if (address == 0xFFFC) + { + _mapperControl = value; + } + else if (address == 0xFFFD) + { + _romBank0 = value % totalBanks; // Force the bank to stay inside the array bounds! + } + else if (address == 0xFFFE) + { + _romBank1 = value % totalBanks; + } + else if (address == 0xFFFF) + { + _romBank2 = value % totalBanks; + } + } + } + //public byte Read(ushort address) //{ // if (address < 0x4000) // ROM Slot 0 (0x0000 - 0x3FFF) // { // // SMS Hardware Quirk: The first 1KB (0x0000 - 0x03FF) is NEVER paged. - // // It is hardwired to Bank 0 so the interrupt handlers don't crash. // if (address < 0x0400) return ReadFromCartridge(0, address); // return ReadFromCartridge(_romBank0, address); @@ -87,103 +126,73 @@ namespace Core.Memory // } // if (address < 0xC000) // ROM Slot 2 (0x8000 - 0xBFFF) // { - // // Check if Cartridge RAM is enabled (Bit 3 of Mapper Control) + // // --- THE MISSING LINK: Read from Save RAM if enabled! --- // if ((_mapperControl & 0x08) != 0) // { - // // Bit 2 decides if we read the first 16KB half or the second 16KB half of the RAM chip // int ramBank = (_mapperControl & 0x04) != 0 ? 1 : 0; - // int ramOffset = (ramBank * 0x4000) + (address & 0x3FFF); + // int ramOffset = (ramBank * 0x4000) + (address & 0x1FFF); // return _cartridgeRam[ramOffset]; // } + // // Otherwise, read from the ROM as usual // return ReadFromCartridge(_romBank2, address & 0x3FFF); // } - // // If we are here, we are in System RAM (0xC000 - 0xFFFF) - // // The & 0x1FFF handles the 8KB mirroring automatically! + // // System RAM (0xC000 - 0xFFFF) // return _workRam[address & 0x1FFF]; //} + //public void Write(ushort address, byte value) //{ - // // --- 1. CARTRIDGE RAM (Save Data) --- // if (address < 0xC000) // { + // // Bypass the lock if they are trying to save their game! // if (address >= 0x8000 && (_mapperControl & 0x08) != 0) // { + // SramUsed = true; // int ramBank = (_mapperControl & 0x04) != 0 ? 1 : 0; - // int ramOffset = (ramBank * 0x4000) + (address & 0x3FFF); + // int ramOffset = (ramBank * 0x4000) + (address & 0x1FFF); // _cartridgeRam[ramOffset] = value; + // return; // } // // You cannot write to a ROM cartridge! // return; // } - // // --- 2. SYSTEM RAM --- + // // Write to System RAM // _workRam[address & 0x1FFF] = value; - // // --- 3. SEGA MAPPER (With Hardware Mirroring!) --- - // // By checking the masked address (address & 0x1FFF), we perfectly - // // catch writes to 0xFFFC-0xFFFF *and* their 0xDFFC-0xDFFF mirrors! - // int mapperAddress = address & 0x1FFF; + // // --- THE SEGA MAPPER --- + // if (address == 0xFFFC) // <-- ADD THIS + // { + // _mapperControl = value; + // } + // else if (address == 0xFFFD) + // { + // _romBank0 = value; + // } - // if (mapperAddress == 0x1FFC) _mapperControl = value; - // else if (mapperAddress == 0x1FFD) _romBank0 = value; - // else if (mapperAddress == 0x1FFE) _romBank1 = value; - // else if (mapperAddress == 0x1FFF) _romBank2 = value; + // // Write to System RAM + // _workRam[address & 0x1FFF] = value; + + // // --- THE SEGA MAPPER --- + // // If the CPU wrote to the very top of memory, it is commanding a bank swap! + // if (address == 0xFFFD) + // { + // _romBank0 = value; + // } + // else if (address == 0xFFFE) + // { + // _romBank1 = value; + // } + // else if (address == 0xFFFF) + // { + // _romBank2 = value; + // } //} - public void Write(ushort address, byte value) - { - if (address < 0xC000) - { - // Bypass the lock if they are trying to save their game! - if (address >= 0x8000 && (_mapperControl & 0x08) != 0) - { - SramUsed = true; - int ramBank = (_mapperControl & 0x04) != 0 ? 1 : 0; - int ramOffset = (ramBank * 0x4000) + (address & 0x1FFF); - _cartridgeRam[ramOffset] = value; - return; - } - - // You cannot write to a ROM cartridge! - return; - } - - // Write to System RAM - _workRam[address & 0x1FFF] = value; - - // --- THE SEGA MAPPER --- - if (address == 0xFFFC) // <-- ADD THIS - { - _mapperControl = value; - } - else if (address == 0xFFFD) - { - _romBank0 = value; - } - - // Write to System RAM - _workRam[address & 0x1FFF] = value; - - // --- THE SEGA MAPPER --- - // If the CPU wrote to the very top of memory, it is commanding a bank swap! - if (address == 0xFFFD) - { - _romBank0 = value; - } - else if (address == 0xFFFE) - { - _romBank1 = value; - } - else if (address == 0xFFFF) - { - _romBank2 = value; - } - } - private byte ReadFromCartridge(int bankIndex, int offset) { if (!_isCartridgeLoaded) return 0xFF; // Floating bus if no game diff --git a/Desktop/Form1.cs b/Desktop/Form1.cs index b07ad16..799a77a 100644 --- a/Desktop/Form1.cs +++ b/Desktop/Form1.cs @@ -63,11 +63,17 @@ namespace Desktop PopulateIncludedRomsMenu(); } - private void DrawScreen() + private void DrawScreen(int[] safeFrame) { - // Rapidly copy our VDP FrameBuffer into the Windows Bitmap + // Rapidly the Frame 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); + + //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++; @@ -137,88 +143,108 @@ namespace Desktop double nextFrameTargetTime = _stopwatch.Elapsed.TotalMilliseconds + TargetFrameTime; double lastFpsUpdate = _stopwatch.Elapsed.TotalMilliseconds; int framesThisSecond = 0; - - while (IsRunning) + try { - //targetFps = isTurboMode ? 1000 : 59.94; - //TargetFrameTime = 1000.0 / targetFps; - BeginInvoke((System.Windows.Forms.MethodInvoker)delegate + while (IsRunning) { - 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(); - - // 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; -#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; - + //targetFps = isTurboMode ? 1000 : 59.94; + //TargetFrameTime = 1000.0 / targetFps; BeginInvoke((System.Windows.Forms.MethodInvoker)delegate { - this.Text = $"{_romType} Parsons Master System 2026 - {_currentRomName} [FPS: {FramesPerSecond:F0}]"; + 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; - nextFrameTargetTime += TargetFrameTime; - - if (currentTime > nextFrameTargetTime + TargetFrameTime) + this.Invoke((System.Windows.Forms.MethodInvoker)delegate { - nextFrameTargetTime = currentTime + TargetFrameTime; - } + 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); + }); } }); }