Implemented OAM, SAT so sprites and background scrolling now work
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user