From f825e102a219660c8302392c4377557d952dbb7b Mon Sep 17 00:00:00 2001 From: parsons Date: Mon, 11 May 2026 22:50:38 +0100 Subject: [PATCH] Implemented OAM, SAT so sprites and background scrolling now work --- Core/Cpu/Z80.cs | 43 ++++++++++++ Core/Video/SmsVdp.cs | 157 +++++++++++++++++++++++++++++++++---------- Desktop/Form1.cs | 44 ++++++++++++ 3 files changed, 209 insertions(+), 35 deletions(-) diff --git a/Core/Cpu/Z80.cs b/Core/Cpu/Z80.cs index 943f318..23e27d5 100644 --- a/Core/Cpu/Z80.cs +++ b/Core/Cpu/Z80.cs @@ -1506,6 +1506,49 @@ namespace Core.Cpu if ((n & 0x02) != 0) AF.Low |= 0x20; // Bit 5 from bit 1 return 16; } + case 0xA2: // INI + { + // 1. Read from the port specified by BC + byte inValA2 = _simpleIoBus.ReadPort(BC.Word); + + // 2. Write the value to memory at HL + WriteMemory(HL.Word, inValA2); + + // 3. Decrement B (the byte counter) + BC.High--; + + // 4. Increment HL (the memory pointer) + HL.Word++; + + // 5. Update Flags + AF.Low |= 0x02; // N is always set (1) + if (BC.High == 0) AF.Low |= 0x40; // Z is set if B reaches 0 + else AF.Low &= 0xBF; // Z is cleared otherwise + + return 16; // Takes 16 T-States + } + case 0xB2: // INIR + { + // This does exactly the same thing as INI, but loops until B == 0 + byte inValB2 = _simpleIoBus.ReadPort(BC.Word); + WriteMemory(HL.Word, inValB2); + BC.High--; + HL.Word++; + + AF.Low |= 0x02; // N is always set + + if (BC.High != 0) + { + AF.Low &= 0xBF; // Z is reset + PC -= 2; // Loop back and execute ED B2 again! + return 21; + } + else + { + AF.Low |= 0x40; // Z is set + return 16; + } + } case 0xA3: // OUTI { // 1. Read data from memory at HL diff --git a/Core/Video/SmsVdp.cs b/Core/Video/SmsVdp.cs index 78b05fd..df66b1e 100644 --- a/Core/Video/SmsVdp.cs +++ b/Core/Video/SmsVdp.cs @@ -117,6 +117,7 @@ namespace Core.Video _statusRegister |= 0x80; // Set Bit 7 (VBlank Flag) to 1 RenderBackground(); // <--- DRAW THE FRAME! + RenderSprites(); } // End of the NTSC frame (262 lines) @@ -129,62 +130,148 @@ namespace Core.Video private void RenderBackground() { - // The Name Table base address is stored in VDP Register 2. - // It tells us where in VRAM the 32x24 screen grid starts. ushort nameTableBase = (ushort)((Registers[2] & 0x0E) << 10); - // Loop through all 24 rows and 32 columns of the screen - for (int row = 0; row < 24; row++) + byte scrollX = Registers[8]; + byte scrollY = Registers[9]; + + bool lockRowScroll = (Registers[0] & 0x80) != 0; // Top 2 rows (Y < 16) + bool lockColScroll = (Registers[0] & 0x40) != 0; // Right 8 columns (X >= 192) + + for (int screenY = 0; screenY < 192; screenY++) { - for (int col = 0; col < 32; col++) + for (int screenX = 0; screenX < 256; screenX++) { + // Apply Vertical Scrolling (Depends on X for column locking!) + int effectiveScrollY = scrollY; + if (lockColScroll && screenX >= 192) effectiveScrollY = 0; + + int vdpY = (screenY + effectiveScrollY) % 224; + int row = vdpY / 8; + int tileY = vdpY % 8; + + // Apply Horizontal Scrolling (Depends on Y for row locking!) + int effectiveScrollX = scrollX; + if (lockRowScroll && screenY < 16) effectiveScrollX = 0; + + int vdpX = (screenX - effectiveScrollX) & 0xFF; + int col = vdpX / 8; + int tileX = vdpX % 8; + // 1. Read the 16-bit Tile instruction from the Name Table ushort nameTableAddr = (ushort)(nameTableBase + (row * 64) + (col * 2)); byte lowByte = VRAM[nameTableAddr]; byte highByte = VRAM[nameTableAddr + 1]; ushort tileData = (ushort)((highByte << 8) | lowByte); - // 2. Extract the Tile Index and Palette Info + // 2. Extract Tile Index and Palette Info int tileIndex = tileData & 0x01FF; bool useSpritePalette = (tileData & 0x0800) != 0; - // 3. Find the actual pixel data for this tile in VRAM - // Each 8x8 tile takes exactly 32 bytes in memory + // 3. Find the tile data in VRAM ushort tileAddress = (ushort)(tileIndex * 32); - // 4. Draw the 8x8 block of pixels! - for (int y = 0; y < 8; y++) + // 4. Fetch the 4 bitplanes + byte bp0 = VRAM[tileAddress + (tileY * 4) + 0]; + byte bp1 = VRAM[tileAddress + (tileY * 4) + 1]; + byte bp2 = VRAM[tileAddress + (tileY * 4) + 2]; + byte bp3 = VRAM[tileAddress + (tileY * 4) + 3]; + + // 5. Extract color index + int shift = 7 - tileX; + int colorIndex = ((bp0 >> shift) & 1) | + (((bp1 >> shift) & 1) << 1) | + (((bp2 >> shift) & 1) << 2) | + (((bp3 >> shift) & 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; + + 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; + + // 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++) { - // The SMS uses 4 bitplanes to make a single row of pixels. - byte bp0 = VRAM[tileAddress + (y * 4) + 0]; - byte bp1 = VRAM[tileAddress + (y * 4) + 1]; - byte bp2 = VRAM[tileAddress + (y * 4) + 2]; - byte bp3 = VRAM[tileAddress + (y * 4) + 3]; + int screenX = x + px; - for (int x = 0; x < 8; x++) - { - // Combine 1 bit from each bitplane to get a color index (0-15) - int shift = 7 - x; - int colorIndex = ((bp0 >> shift) & 1) | - (((bp1 >> shift) & 1) << 1) | - (((bp2 >> shift) & 1) << 2) | - (((bp3 >> shift) & 1) << 3); + // If this pixel is off the right side of the screen, skip it + if (screenX >= 256) continue; - // Find the raw SMS color in CRAM - int paletteOffset = useSpritePalette ? 16 : 0; - byte smsColor = CRAM[paletteOffset + colorIndex]; + int shift = 7 - px; + int colorIndex = ((bp0 >> shift) & 1) | + (((bp1 >> shift) & 1) << 1) | + (((bp2 >> shift) & 1) << 2) | + (((bp3 >> shift) & 1) << 3); - // Translate SMS 00BBGGRR format to Windows 32-bit ARGB - int r = (smsColor & 0x03) * 85; - int g = ((smsColor >> 2) & 0x03) * 85; - int b = ((smsColor >> 4) & 0x03) * 85; + // HARDWARE TRANSPARENCY: + // If the color index is 0, DO NOT draw it! Let the background show through. + if (colorIndex == 0) continue; - // Calculate where this pixel goes on the final 256x192 screen - int pixelX = (col * 8) + x; - int pixelY = (row * 8) + y; + // Sprites ALWAYS use the second half of CRAM (Palette 1: Indices 16-31) + byte smsColor = CRAM[16 + colorIndex]; - FrameBuffer[(pixelY * 256) + pixelX] = (255 << 24) | (r << 16) | (g << 8) | b; - } + 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; } } } diff --git a/Desktop/Form1.cs b/Desktop/Form1.cs index 007f140..9a08674 100644 --- a/Desktop/Form1.cs +++ b/Desktop/Form1.cs @@ -31,6 +31,10 @@ namespace Desktop _machine = new SmsMachine(); PopulateIncludedRomsMenu(); + + this.KeyPreview = true; + this.KeyDown += Form1_KeyDown; + this.KeyUp += Form1_KeyUp; } private void DrawScreen() @@ -168,5 +172,45 @@ namespace Desktop { 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) + { + // Active-Low: Clear the specific bit to 0 using a bitwise AND with a NOT mask + _machine.IoBus.Joypad1State &= (byte)~bitMask; + } + else + { + // Active-Low: Reset the specific bit to 1 using a bitwise OR mask + _machine.IoBus.Joypad1State |= bitMask; + } + } } }