diff --git a/Core/Io/SmsIoBus.cs b/Core/Io/SmsIoBus.cs index fd413b8..9255b07 100644 --- a/Core/Io/SmsIoBus.cs +++ b/Core/Io/SmsIoBus.cs @@ -9,8 +9,9 @@ namespace Core.Io // public Psg AudioProcessor { get; set; } // Joypad State (0xFF means no buttons pressed - the SMS uses Active-Low logic!) - public byte Joypad1State { get; set; } = 0xFF; - public byte Joypad2State { get; set; } = 0xFF; + public byte Joypad1Keyboard = 0xFF; + public byte Joypad1Gamepad = 0xFF; + public byte Joypad2State = 0xFF; public byte ReadPort(ushort port) { @@ -30,16 +31,9 @@ namespace Core.Io if ((lowerPort & 0x01) == 0) return VideoProcessor.ReadDataPort(); else return VideoProcessor.ReadControlPort(); } - if (lowerPort == 0xDC) - { - // Port 0xDC: Player 1 (Up, Down, Left, Right, 1, 2) + Player 2 (Up, Down) - return Joypad1State; - } - if (lowerPort == 0xDD) - { - // Port 0xDD: Player 2 (Left, Right, 1, 2) + Reset Button - return Joypad2State; - } + if (lowerPort == 0xDC) return (byte)(Joypad1Keyboard & Joypad1Gamepad); + + if (lowerPort == 0xDD) return Joypad2State; return 0xFF; // Floating bus } diff --git a/Core/SmsMachine.cs b/Core/SmsMachine.cs index ed71cea..98a16f9 100644 --- a/Core/SmsMachine.cs +++ b/Core/SmsMachine.cs @@ -38,74 +38,33 @@ namespace Core Cpu.Reset(); } - public int StepMachine() - { - // 1. Tick the CPU - int tStates = Cpu.Step(); - - // 2. Tell the VDP how much time just passed - VideoProcessor.Update(tStates); - - // 3. Trigger interrupts if the VDP hit scanline 192 - if (VideoProcessor.InterruptPending) - { - tStates += Cpu.RequestInterrupt(); - } - - return tStates; - } - + public void RunFrame() { - long currentFrameTStates = 0; - - while (currentFrameTStates < TStatesPerFrame) + int tStatesThisFrame = 0; + while (tStatesThisFrame < 59736) // Standard NTSC frame time { - currentFrameTStates += StepMachine(); - string filePath = "captured_data.txt"; + // 1. Run one CPU instruction + int cycles = Cpu.Step(); + tStatesThisFrame += cycles; - // Mock data to loop through - //List sensorReadings = new List { Cpu.PC, Cpu.AF.Word, Cpu.BC.Word, Cpu.DE.Word, Cpu.HL.Word, Cpu.SP}; - //List type = new List {"PC: 0x", "AF: 0x", "BC: 0x", "DE: 0x", "HL: 0x", "SP: 0x" }; + // 2. Tell the VDP to catch up + VideoProcessor.Update(cycles); - //try - //{ - // // 2. Initialize StreamWriter within a 'using' block - // // The 'true' parameter means "append" to the file. Use 'false' to overwrite. - // using (StreamWriter writer = new StreamWriter(filePath, append: true)) - // { - // foreach (int reading in sensorReadings) - // { - // string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); + // 3. Check if the VDP is begging for attention! + if (VideoProcessor.InterruptPending && Cpu.IFF1) + { + int intCycles = Cpu.RequestInterrupt(); + tStatesThisFrame += intCycles; + VideoProcessor.Update(intCycles); // Keep VDP perfectly in sync + } - // // 3. Construct your string and write it - // foreach (string _type in type) - // { - // string line = $"{timestamp} | {_type} {reading}"; - // writer.WriteLine(line); - // } - - - // // Optional: Console feedback - // //Console.WriteLine($"Logged: {line}"); - // } - // } - // // File is automatically closed and saved here - - // //Console.WriteLine("Data capture complete."); - //} - //catch (IOException e) - //{ - // Console.WriteLine($"An error occurred: {e.Message}"); - //} - - // THE TRIPWIRE: Check the breakpoint after EVERY single instruction! + // 4. THE RESTORED BREAKPOINT TRAP if (Breakpoint.HasValue && Cpu.PC == Breakpoint.Value) { - break; // Abort the frame loop immediately! + break; // Instantly abort the frame so the debugger can take over! } } - } } } \ No newline at end of file diff --git a/Core/Video/SmsVdp.cs b/Core/Video/SmsVdp.cs index 3a3451a..d2b946c 100644 --- a/Core/Video/SmsVdp.cs +++ b/Core/Video/SmsVdp.cs @@ -113,37 +113,32 @@ namespace Core.Video if (_tStateCounter >= 228) { _tStateCounter -= 228; - //// --- LINE INTERRUPT LOGIC --- - //if (_currentScanline <= 192) - //{ - // _lineCounter--; - // if (_lineCounter < 0) - // { - // _lineCounter = Registers[10]; // Reload counter - // _statusRegister |= 0x40; // Set Line Interrupt Flag (Bit 6) - // } - //} - //else - //{ - // _lineCounter = Registers[10]; // Reload outside active display - //} + + // --- MISSING LINE INTERRUPT COUNTDOWN --- + if (_currentScanline <= 192) + { + _lineCounter--; + if (_lineCounter < 0) + { + _lineCounter = Registers[10]; // Reload counter + _statusRegister |= 0x40; // Set Line Interrupt Flag (Bit 6) + } + } + else + { + _lineCounter = Registers[10]; // Reload outside active display + } + // ---------------------------------------- + _currentScanline++; - // Line 192 is the exact moment the screen finishes drawing! - if (_currentScanline == 192) + if (_currentScanline < 192) { - _statusRegister |= 0x80; // Set Bit 7 (VBlank Flag) to 1 - - if ((Registers[1] & 0x40) != 0) - { - RenderBackground(); - RenderSprites(); - } - else - { - // Screen is off! Fill it with black (or the background color) - Array.Fill(FrameBuffer, unchecked((int)0xFF000000)); - } + RenderScanline(_currentScanline); + } + else if (_currentScanline == 192) + { + _statusRegister |= 0x80; // Set VBlank Flag } // End of the NTSC frame (262 lines) @@ -154,175 +149,159 @@ namespace Core.Video } } - private void RenderBackground() + private void RenderScanline(int screenY) { + // If the display is disabled, fill the line with black and exit + if ((Registers[1] & 0x40) == 0) + { + for (int x = 0; x < 256; x++) FrameBuffer[(screenY * 256) + x] = unchecked((int)0xFF000000); + return; + } + + // --- 1. RENDER BACKGROUND LINE --- ushort nameTableBase = (ushort)((Registers[2] & 0x0E) << 10); byte scrollX = Registers[8]; byte scrollY = Registers[9]; - bool lockRowScroll = (Registers[0] & 0x80) != 0; - bool lockColScroll = (Registers[0] & 0x40) != 0; + // 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!) + bool lockRowScroll = (Registers[0] & 0x40) != 0; // Bit 6: Locks top 2 rows (Fixes Bart!) + bool maskLeftCol = (Registers[0] & 0x20) != 0; // Bit 5: Hides leftmost column (Fixes Sonic 2!) - // Clear the priority mask for the new frame! - Array.Clear(_priorityBuffer, 0, _priorityBuffer.Length); - - for (int screenY = 0; screenY < 192; screenY++) + for (int screenX = 0; screenX < 256; screenX++) { - for (int screenX = 0; screenX < 256; screenX++) + // --- LEFT COLUMN MASKING (OVERSCAN CURTAIN) --- + if (maskLeftCol && screenX < 8) { - int effectiveScrollY = scrollY; - if (lockColScroll && screenX >= 192) effectiveScrollY = 0; + // Draw the physical backdrop color (from Sprite Palette + Reg 7 index) + byte bgSmsColor = CRAM[16 + (Registers[7] & 0x0F)]; + int bgR = (bgSmsColor & 0x03) * 85; + int bgG = ((bgSmsColor >> 2) & 0x03) * 85; + int bgB = ((bgSmsColor >> 4) & 0x03) * 85; - int vdpY = (screenY + effectiveScrollY) % 224; - int row = vdpY / 8; - int tileY = vdpY % 8; + int bgAddress = (screenY * 256) + screenX; + FrameBuffer[bgAddress] = (255 << 24) | (bgR << 16) | (bgG << 8) | bgB; - int effectiveScrollX = scrollX; - if (lockRowScroll && screenY < 16) effectiveScrollX = 0; + // Flag it as priority so sprites also hide behind the curtain! + _priorityBuffer[bgAddress] = true; + continue; + } - int vdpX = (screenX - effectiveScrollX) & 0xFF; - int col = vdpX / 8; - int tileX = vdpX % 8; + // Apply Vertical Scrolling (R-Type HUD protection) + int effectiveScrollY = scrollY; + if (lockColScroll && screenX >= 192) effectiveScrollY = 0; - // 1. Read the 16-bit Tile instruction - ushort nameTableAddr = (ushort)(nameTableBase + (row * 64) + (col * 2)); - byte lowByte = VRAM[nameTableAddr]; - byte highByte = VRAM[nameTableAddr + 1]; - ushort tileData = (ushort)((highByte << 8) | lowByte); + int vdpY = (screenY + effectiveScrollY) % 224; + int row = vdpY / 8; + int tileY = vdpY % 8; - // 2. EXTRACT ALL THE HARDWARE BITS! - int tileIndex = tileData & 0x01FF; - bool flipH = (tileData & 0x0200) != 0; // Bit 9 - bool flipV = (tileData & 0x0400) != 0; // Bit 10 - bool useSpritePalette = (tileData & 0x0800) != 0; // Bit 11 - bool priority = (tileData & 0x1000) != 0; // Bit 12 + // Apply Horizontal Scrolling (Bart's sky protection) + int effectiveScrollX = scrollX; + if (lockRowScroll && screenY < 16) effectiveScrollX = 0; - // 3. Apply Vertical Flip (Read from the bottom of the tile instead of the top) - int readY = flipV ? (7 - tileY) : tileY; - ushort tileAddress = (ushort)(tileIndex * 32); + int vdpX = (screenX - effectiveScrollX) & 0xFF; + int col = vdpX / 8; + int tileX = vdpX % 8; - byte bp0 = VRAM[tileAddress + (readY * 4) + 0]; - byte bp1 = VRAM[tileAddress + (readY * 4) + 1]; - byte bp2 = VRAM[tileAddress + (readY * 4) + 2]; - byte bp3 = VRAM[tileAddress + (readY * 4) + 3]; + ushort nameTableAddr = (ushort)(nameTableBase + (row * 64) + (col * 2)); + ushort tileData = (ushort)((VRAM[nameTableAddr + 1] << 8) | VRAM[nameTableAddr]); - // 4. Apply Horizontal Flip (Shift from right-to-left instead of left-to-right) - int readX = flipH ? tileX : (7 - tileX); - int colorIndex = ((bp0 >> readX) & 1) | - (((bp1 >> readX) & 1) << 1) | - (((bp2 >> readX) & 1) << 2) | - (((bp3 >> readX) & 1) << 3); + int tileIndex = tileData & 0x01FF; + bool flipH = (tileData & 0x0200) != 0; + bool flipV = (tileData & 0x0400) != 0; + bool useSpritePalette = (tileData & 0x0800) != 0; + bool priority = (tileData & 0x1000) != 0; - int paletteOffset = useSpritePalette ? 16 : 0; - byte smsColor = CRAM[paletteOffset + colorIndex]; + int readY = flipV ? (7 - tileY) : tileY; + ushort tileAddress = (ushort)(tileIndex * 32); + byte bp0 = VRAM[tileAddress + (readY * 4) + 0]; + byte bp1 = VRAM[tileAddress + (readY * 4) + 1]; + byte bp2 = VRAM[tileAddress + (readY * 4) + 2]; + byte bp3 = VRAM[tileAddress + (readY * 4) + 3]; + + int readX = flipH ? tileX : (7 - tileX); + int colorIndex = ((bp0 >> readX) & 1) | (((bp1 >> readX) & 1) << 1) | + (((bp2 >> readX) & 1) << 2) | (((bp3 >> readX) & 1) << 3); + + int paletteOffset = useSpritePalette ? 16 : 0; + byte smsColor = CRAM[paletteOffset + colorIndex]; + + int r = (smsColor & 0x03) * 85; + int g = ((smsColor >> 2) & 0x03) * 85; + int b = ((smsColor >> 4) & 0x03) * 85; + + int screenAddress = (screenY * 256) + screenX; + + // Draw background and reset priority mask for this exact pixel + FrameBuffer[screenAddress] = (255 << 24) | (r << 16) | (g << 8) | b; + _priorityBuffer[screenAddress] = (priority && colorIndex != 0); + } + + // --- 2. RENDER SPRITE LINE --- + ushort satBaseAddress = (ushort)((Registers[5] & 0x7E) << 7); + ushort spritePatternBase = (ushort)((Registers[6] & 0x04) << 11); + bool is8x16 = (Registers[1] & 0x02) != 0; + bool shiftSpritesLeft = (Registers[0] & 0x08) != 0; + int spriteHeight = is8x16 ? 16 : 8; + + // Step A: Find the visible sprites for THIS specific line + var visibleSprites = new System.Collections.Generic.List(); + for (int i = 0; i < 64; i++) + { + byte y = VRAM[satBaseAddress + i]; + if (y == 208) break; // End of Sprite List + + int spriteY = y + 1; // Physical hardware 1-pixel shift + if (screenY >= spriteY && screenY < spriteY + spriteHeight) + { + visibleSprites.Add(i); + // HARDWARE QUIRK: VDP stops drawing after 8 sprites on a single line! + if (visibleSprites.Count == 8) break; + } + } + + // Step B: Draw them backward so Sprite 0 (highest priority) draws LAST and stays on top + for (int v = visibleSprites.Count - 1; v >= 0; v--) + { + int i = visibleSprites[v]; + byte y = VRAM[satBaseAddress + i]; + byte x = VRAM[satBaseAddress + 0x80 + (i * 2)]; + byte tileIndex = VRAM[satBaseAddress + 0x80 + (i * 2) + 1]; + + if (is8x16) tileIndex = (byte)(tileIndex & 0xFE); + + // Calculate which row of the sprite we are physically on + int py = screenY - (y + 1); + ushort tileAddress = (ushort)(spritePatternBase + (tileIndex * 32) + (py * 4)); + + byte bp0 = VRAM[tileAddress + 0]; + byte bp1 = VRAM[tileAddress + 1]; + byte bp2 = VRAM[tileAddress + 2]; + byte bp3 = VRAM[tileAddress + 3]; + + for (int px = 0; px < 8; px++) + { + int screenX = x + px; + if (shiftSpritesLeft) screenX -= 8; + + if (screenX < 0 || screenX >= 256) continue; + if (_priorityBuffer[(screenY * 256) + screenX]) continue; + + int shift = 7 - px; + int colorIndex = ((bp0 >> shift) & 1) | (((bp1 >> shift) & 1) << 1) | + (((bp2 >> shift) & 1) << 2) | (((bp3 >> shift) & 1) << 3); + + if (colorIndex == 0) continue; + + byte smsColor = CRAM[16 + colorIndex]; int r = (smsColor & 0x03) * 85; int g = ((smsColor >> 2) & 0x03) * 85; int b = ((smsColor >> 4) & 0x03) * 85; - int screenAddress = (screenY * 256) + screenX; - FrameBuffer[screenAddress] = (255 << 24) | (r << 16) | (g << 8) | b; - - // 5. FLAG THE PRIORITY PIXEL! - // If this tile has priority AND the pixel isn't transparent (color 0), - // tell the sprite renderer not to draw over it! - if (priority && colorIndex != 0) - { - _priorityBuffer[screenAddress] = true; - } + FrameBuffer[(screenY * 256) + screenX] = (255 << 24) | (r << 16) | (g << 8) | b; } } } - - private void RenderSprites() - { - // 1. Find the Sprite Attribute Table (SAT) - // Register 5 contains the base address bits (Mask 0x7E, shifted by 7) - ushort satBaseAddress = (ushort)((Registers[5] & 0x7E) << 7); - - // 2. Register 6 determines where the Sprite Tile graphics are stored in VRAM - ushort spritePatternBase = (ushort)((Registers[6] & 0x04) << 11); - - // 3. Register 1 determines sprite size (8x8 or 8x16) - bool is8x16 = (Registers[1] & 0x02) != 0; - bool shiftSpritesLeft = (Registers[0] & 0x08) != 0; - - // The SMS can draw a maximum of 64 sprites - for (int i = 0; i < 64; i++) - { - // Read the Y coordinate from the first part of the SAT - byte y = VRAM[satBaseAddress + i]; - - // HARDWARE QUIRK: If Y == 208 in standard 192-line mode, - // it acts as a "Stop Drawing" marker. The VDP aborts the rest of the list! - if (y == 208) break; - - // The X coordinates and Tile Indices are interleaved starting at SAT + 0x80 - byte x = VRAM[satBaseAddress + 0x80 + (i * 2)]; - byte tileIndex = VRAM[satBaseAddress + 0x80 + (i * 2) + 1]; - - // If sprites are 8x16, the Tile Index always drops the lowest bit (forces even alignment) - if (is8x16) tileIndex = (byte)(tileIndex & 0xFE); - - // Calculate the pixel height for the drawing loop - int spriteHeight = is8x16 ? 16 : 8; - - // Draw the 8x8 (or 8x16) sprite block - for (int py = 0; py < spriteHeight; py++) - { - // Master System Sprites are physically shifted down 1 pixel on the CRT - int screenY = y + 1 + py; - - // If this row of the sprite is off the bottom of the screen, skip it - if (screenY >= 192) continue; - - // Calculate where the 4 bitplanes are for this specific row of the sprite - ushort tileAddress = (ushort)(spritePatternBase + (tileIndex * 32) + (py * 4)); - - byte bp0 = VRAM[tileAddress + 0]; - byte bp1 = VRAM[tileAddress + 1]; - byte bp2 = VRAM[tileAddress + 2]; - byte bp3 = VRAM[tileAddress + 3]; - - for (int px = 0; px < 8; px++) - { - int screenX = x + px; - // THE FIX: Shift the pixel left if commanded! - if (shiftSpritesLeft) screenX -= 8; - - // If it shifted off the left edge, skip it! - if (screenX < 0 || screenX >= 256) continue; - - // If this pixel is off the right side of the screen, skip it - if (screenX >= 256) continue; - - int shift = 7 - px; - int colorIndex = ((bp0 >> shift) & 1) | - (((bp1 >> shift) & 1) << 1) | - (((bp2 >> shift) & 1) << 2) | - (((bp3 >> shift) & 1) << 3); - - // HARDWARE TRANSPARENCY: - // If the color index is 0, DO NOT draw it! Let the background show through. - if (colorIndex == 0) continue; - - // If the background tile at this exact pixel claimed priority, hide the sprite! - if (_priorityBuffer[(screenY * 256) + screenX]) continue; - - // Sprites ALWAYS use the second half of CRAM (Palette 1: Indices 16-31) - byte smsColor = CRAM[16 + colorIndex]; - - int r = (smsColor & 0x03) * 85; - int g = ((smsColor >> 2) & 0x03) * 85; - int b = ((smsColor >> 4) & 0x03) * 85; - - // Because we only draw non-zero pixels, this safely overwrites the - // background FrameBuffer exactly where the sprite stands! - FrameBuffer[(screenY * 256) + screenX] = (255 << 24) | (r << 16) | (g << 8) | b; - } - } - } - } - } } \ No newline at end of file diff --git a/Desktop/Controller.cs b/Desktop/Controller.cs new file mode 100644 index 0000000..e1f6c29 --- /dev/null +++ b/Desktop/Controller.cs @@ -0,0 +1,27 @@ +using System.Runtime.InteropServices; + +public static class XInput +{ + // Reach into the Windows OS API + [DllImport("xinput1_4.dll")] + public static extern int XInputGetState(int dwUserIndex, out XINPUT_STATE pState); + + [StructLayout(LayoutKind.Sequential)] + public struct XINPUT_STATE + { + public uint dwPacketNumber; + public XINPUT_GAMEPAD Gamepad; + } + + [StructLayout(LayoutKind.Sequential)] + public struct XINPUT_GAMEPAD + { + public ushort wButtons; + public byte bLeftTrigger; + public byte bRightTrigger; + public short sThumbLX; + public short sThumbLY; + public short sThumbRX; + public short sThumbRY; + } +} \ No newline at end of file diff --git a/Desktop/DebuggerForm.cs b/Desktop/DebuggerForm.cs index 04c53e3..e7b9123 100644 --- a/Desktop/DebuggerForm.cs +++ b/Desktop/DebuggerForm.cs @@ -56,7 +56,7 @@ namespace Desktop try { // Ask the main form to step the WHOLE machine, not just the Z80! - _mainForm.StepEmulator(); + //_mainForm.StepEmulator(); UpdateDisplay(); } catch (Exception ex) diff --git a/Desktop/Form1.cs b/Desktop/Form1.cs index f9d2d33..36b15ba 100644 --- a/Desktop/Form1.cs +++ b/Desktop/Form1.cs @@ -100,7 +100,23 @@ namespace Desktop // Mark exactly when the emulator starts thinking double frameStartTime = _stopwatch.Elapsed.TotalMilliseconds; - // 1. Do the heavy lifting (Z80 and VDP) + // --- 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! @@ -161,12 +177,6 @@ namespace Desktop IsRunning = false; } - - public void StepEmulator() - { - _machine.StepMachine(); - } - private async void LoadRomAndStart(string filePath) { StopEmulator(); @@ -282,13 +292,11 @@ namespace Desktop if (isPressed) { - // Active-Low: Clear the specific bit to 0 using a bitwise AND with a NOT mask - _machine.IoBus.Joypad1State &= (byte)~bitMask; + _machine.IoBus.Joypad1Keyboard &= (byte)~bitMask; // Target Keyboard! } else { - // Active-Low: Reset the specific bit to 1 using a bitwise OR mask - _machine.IoBus.Joypad1State |= bitMask; + _machine.IoBus.Joypad1Keyboard |= bitMask; // Target Keyboard! } } }