Got main system and VDP working! There is a display!
This commit is contained in:
@@ -1275,6 +1275,18 @@ namespace Core.Cpu
|
||||
|
||||
switch (extendedOpcode)
|
||||
{
|
||||
case 0x41: // OUT (C), B
|
||||
_simpleIoBus.WritePort(BC.Word, BC.High);
|
||||
return 12;
|
||||
case 0x49: // OUT (C), C
|
||||
_simpleIoBus.WritePort(BC.Word, BC.Low);
|
||||
return 12;
|
||||
case 0x61: // OUT (C), H
|
||||
_simpleIoBus.WritePort(BC.Word, HL.High);
|
||||
return 12;
|
||||
case 0x69: // OUT (C), L
|
||||
_simpleIoBus.WritePort(BC.Word, HL.Low);
|
||||
return 12;
|
||||
case 0x43: // LD (nn), BC
|
||||
ushort dest43 = FetchWord();
|
||||
WriteMemory(dest43, BC.Low);
|
||||
@@ -1494,6 +1506,48 @@ namespace Core.Cpu
|
||||
if ((n & 0x02) != 0) AF.Low |= 0x20; // Bit 5 from bit 1
|
||||
return 16;
|
||||
}
|
||||
case 0xA3: // OUTI
|
||||
{
|
||||
// 1. Read data from memory at HL
|
||||
byte valA3 = ReadMemory(HL.Word);
|
||||
|
||||
// 2. Decrement the B register
|
||||
BC.High--;
|
||||
|
||||
// 3. Output the data to the port specified by C
|
||||
_simpleIoBus.WritePort(BC.Word, valA3);
|
||||
|
||||
// 4. Increment the memory pointer
|
||||
HL.Word++;
|
||||
|
||||
// 5. Update Flags (N is always set. Z is set if B reached 0)
|
||||
AF.Low |= 0x02;
|
||||
if (BC.High == 0) AF.Low |= 0x40;
|
||||
else AF.Low &= 0xBF;
|
||||
|
||||
return 16;
|
||||
}
|
||||
case 0xB3: // OTIR
|
||||
{
|
||||
// This does exactly the same thing as OUTI, but loops until B == 0
|
||||
byte valB3 = ReadMemory(HL.Word);
|
||||
BC.High--;
|
||||
_simpleIoBus.WritePort(BC.Word, valB3);
|
||||
HL.Word++;
|
||||
|
||||
AF.Low |= 0x02;
|
||||
if (BC.High != 0)
|
||||
{
|
||||
AF.Low &= 0xBF; // Z is reset
|
||||
PC -= 2; // Loop back and execute ED B3 again!
|
||||
return 21;
|
||||
}
|
||||
else
|
||||
{
|
||||
AF.Low |= 0x40; // Z is set
|
||||
return 16;
|
||||
}
|
||||
}
|
||||
case 0xB0: // LDIR
|
||||
{
|
||||
byte val00 = ReadMemory(HL.Word);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
using Core.Interfaces;
|
||||
using Core.Video;
|
||||
|
||||
namespace Core.Io
|
||||
{
|
||||
public class SmsIoBus : IIoBus
|
||||
{
|
||||
// We will wire these up in the next phases!
|
||||
// public Vdp VideoProcessor { get; set; }
|
||||
public SmsVdp VideoProcessor { get; set; }
|
||||
// public Psg AudioProcessor { get; set; }
|
||||
|
||||
// Joypad State (0xFF means no buttons pressed - the SMS uses Active-Low logic!)
|
||||
@@ -18,11 +18,17 @@ namespace Core.Io
|
||||
// hardware only physically wires up the bottom 8 bits.
|
||||
byte lowerPort = (byte)(port & 0xFF);
|
||||
|
||||
if (lowerPort == 0x7E)
|
||||
{
|
||||
// VDP V-Counter (Vertical Scanline Position)
|
||||
return VideoProcessor.ReadVCounter();
|
||||
}
|
||||
|
||||
if (lowerPort >= 0x80 && lowerPort <= 0xBF)
|
||||
{
|
||||
// VDP Read (Usually 0xBE for VRAM Data, 0xBF for Status Flags)
|
||||
// return VideoProcessor.ReadPort(lowerPort);
|
||||
return 0x00;
|
||||
// Even ports (like 0xBE) are Data. Odd ports (like 0xBF) are Control.
|
||||
if ((lowerPort & 0x01) == 0) return VideoProcessor.ReadDataPort();
|
||||
else return VideoProcessor.ReadControlPort();
|
||||
}
|
||||
if (lowerPort == 0xDC)
|
||||
{
|
||||
@@ -49,8 +55,8 @@ namespace Core.Io
|
||||
}
|
||||
else if (lowerPort >= 0x80 && lowerPort <= 0xBF)
|
||||
{
|
||||
// VDP Write (Usually 0xBE for VRAM Data, 0xBF for Control Registers)
|
||||
// VideoProcessor.WritePort(lowerPort, value);
|
||||
if ((lowerPort & 0x01) == 0) VideoProcessor.WriteDataPort(value);
|
||||
else VideoProcessor.WriteControlPort(value);
|
||||
}
|
||||
else if (lowerPort <= 0x3F)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using Core.Cpu;
|
||||
using Core.Io;
|
||||
using Core.Memory;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Core
|
||||
{
|
||||
@@ -9,18 +12,17 @@ namespace Core
|
||||
public Z80 Cpu { get; private set; }
|
||||
public SmsMemoryBus MemoryBus { get; private set; }
|
||||
public SmsIoBus IoBus { get; private set; }
|
||||
public Core.Video.SmsVdp VideoProcessor { get; private set; }
|
||||
public ushort? Breakpoint { get; set; } = null;
|
||||
|
||||
// NTSC SMS T-States per frame
|
||||
public const int TStatesPerFrame = 59736;
|
||||
public long TotalFrameCount { get; private set; } = 0;
|
||||
public double FramesPerSecond { get; private set; } = 0;
|
||||
public double FrameTime { get; private set; } = 0;
|
||||
|
||||
public SmsMachine()
|
||||
{
|
||||
MemoryBus = new SmsMemoryBus();
|
||||
IoBus = new SmsIoBus();
|
||||
VideoProcessor = new Core.Video.SmsVdp();
|
||||
IoBus = new SmsIoBus { VideoProcessor = this.VideoProcessor };
|
||||
Cpu = new Z80(MemoryBus, IoBus);
|
||||
}
|
||||
|
||||
@@ -34,8 +36,23 @@ namespace Core
|
||||
{
|
||||
MemoryBus.CleanRAMData();
|
||||
Cpu.Reset();
|
||||
}
|
||||
|
||||
// We will reset the VDP and PSG here later!
|
||||
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()
|
||||
@@ -44,18 +61,51 @@ namespace Core
|
||||
|
||||
while (currentFrameTStates < TStatesPerFrame)
|
||||
{
|
||||
int tStates = Cpu.Step();
|
||||
currentFrameTStates += tStates;
|
||||
currentFrameTStates += StepMachine();
|
||||
string filePath = "captured_data.txt";
|
||||
|
||||
// --- FUTURE EXPANSION ---
|
||||
// VideoProcessor.Update(tStates);
|
||||
// AudioProcessor.Update(tStates);
|
||||
// Mock data to loop through
|
||||
//List<ushort> sensorReadings = new List<ushort> { Cpu.PC, Cpu.AF.Word, Cpu.BC.Word, Cpu.DE.Word, Cpu.HL.Word, Cpu.SP};
|
||||
//List<string> type = new List<string> {"PC: 0x", "AF: 0x", "BC: 0x", "DE: 0x", "HL: 0x", "SP: 0x" };
|
||||
|
||||
// if (VideoProcessor.IsVBlanking && VideoProcessor.InterruptsEnabled)
|
||||
// {
|
||||
// Cpu.RequestInterrupt();
|
||||
// }
|
||||
//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. 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!
|
||||
if (Breakpoint.HasValue && Cpu.PC == Breakpoint.Value)
|
||||
{
|
||||
break; // Abort the frame loop immediately!
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
194
Core/Video/SmsVdp.cs
Normal file
194
Core/Video/SmsVdp.cs
Normal file
@@ -0,0 +1,194 @@
|
||||
using System;
|
||||
|
||||
namespace Core.Video
|
||||
{
|
||||
public class SmsVdp
|
||||
{
|
||||
// The VDP's private memory! The CPU cannot touch these arrays directly.
|
||||
public byte[] VRAM { get; private set; } = new byte[0x4000]; // 16KB Video RAM
|
||||
public byte[] CRAM { get; private set; } = new byte[0x20]; // 32 Bytes Color Palette
|
||||
public byte[] Registers { get; private set; } = new byte[16]; // 11 Hardware Control Registers
|
||||
public int[] FrameBuffer { get; private set; } = new int[256 * 192];
|
||||
|
||||
// The Control Port State Machine (Port 0xBF)
|
||||
private bool _isSecondControlByte = false;
|
||||
private ushort _controlWord = 0;
|
||||
private byte _readBuffer = 0;
|
||||
|
||||
private int _tStateCounter = 0;
|
||||
private int _currentScanline = 0;
|
||||
private byte _statusRegister = 0x00;
|
||||
|
||||
public bool InterruptPending => (_statusRegister & 0x80) != 0 && (Registers[1] & 0x20) != 0;
|
||||
|
||||
public byte ReadDataPort() // Port 0xBE
|
||||
{
|
||||
_isSecondControlByte = false; // Reading data resets the control latch
|
||||
byte value = _readBuffer;
|
||||
_readBuffer = VRAM[_controlWord & 0x3FFF];
|
||||
_controlWord++;
|
||||
return value;
|
||||
}
|
||||
|
||||
public byte ReadControlPort() // Port 0xBF
|
||||
{
|
||||
_isSecondControlByte = false;
|
||||
byte currentStatus = _statusRegister;
|
||||
|
||||
// CRITICAL HARDWARE QUIRK: Reading the status port physically
|
||||
// clears the flags inside the chip! If we don't clear this,
|
||||
// the interrupt line gets stuck on forever.
|
||||
_statusRegister = 0x00;
|
||||
|
||||
return currentStatus;
|
||||
}
|
||||
|
||||
public void WriteDataPort(byte value) // Port 0xBE
|
||||
{
|
||||
_isSecondControlByte = false;
|
||||
_readBuffer = value;
|
||||
|
||||
int address = _controlWord & 0x3FFF;
|
||||
int command = (_controlWord >> 14) & 0x03;
|
||||
|
||||
if (command == 3) // Code 3: Write to Color Palette (CRAM)
|
||||
{
|
||||
CRAM[address & 0x1F] = value;
|
||||
}
|
||||
else // Code 0, 1, 2: Write to VRAM
|
||||
{
|
||||
VRAM[address] = value;
|
||||
}
|
||||
_controlWord++; // Auto-increment so the Z80 can blast data fast
|
||||
}
|
||||
|
||||
public void WriteControlPort(byte value) // Port 0xBF
|
||||
{
|
||||
if (!_isSecondControlByte)
|
||||
{
|
||||
// First byte arrives: Store it in the lower 8 bits
|
||||
_controlWord = (ushort)((_controlWord & 0xFF00) | value);
|
||||
_isSecondControlByte = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Second byte arrives: Store it in the upper 8 bits and execute!
|
||||
_controlWord = (ushort)((_controlWord & 0x00FF) | (value << 8));
|
||||
_isSecondControlByte = false;
|
||||
|
||||
int command = (_controlWord >> 14) & 0x03;
|
||||
|
||||
if (command == 0) // Code 0: Prep for VRAM Read
|
||||
{
|
||||
_readBuffer = VRAM[_controlWord & 0x3FFF];
|
||||
_controlWord++;
|
||||
}
|
||||
else if (command == 2) // Code 2: Write to Internal VDP Register
|
||||
{
|
||||
int regIndex = value & 0x0F;
|
||||
byte regData = (byte)(_controlWord & 0xFF);
|
||||
|
||||
if (regIndex < 16) Registers[regIndex] = regData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public byte ReadVCounter()
|
||||
{
|
||||
// Note: On real NTSC hardware, the V-Counter jumps slightly around
|
||||
// the VBlank period to keep the math 8-bit, but simply returning
|
||||
// the raw current scanline is perfectly fine to get us booting!
|
||||
return (byte)_currentScanline;
|
||||
}
|
||||
|
||||
public void Update(int tStates)
|
||||
{
|
||||
_tStateCounter += tStates;
|
||||
|
||||
// 228 T-States per scanline
|
||||
if (_tStateCounter >= 228)
|
||||
{
|
||||
_tStateCounter -= 228;
|
||||
_currentScanline++;
|
||||
|
||||
// Line 192 is the exact moment the screen finishes drawing!
|
||||
if (_currentScanline == 192)
|
||||
{
|
||||
_statusRegister |= 0x80; // Set Bit 7 (VBlank Flag) to 1
|
||||
|
||||
RenderBackground(); // <--- DRAW THE FRAME!
|
||||
}
|
||||
|
||||
// End of the NTSC frame (262 lines)
|
||||
if (_currentScanline > 261)
|
||||
{
|
||||
_currentScanline = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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++)
|
||||
{
|
||||
for (int col = 0; col < 32; col++)
|
||||
{
|
||||
// 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
|
||||
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
|
||||
ushort tileAddress = (ushort)(tileIndex * 32);
|
||||
|
||||
// 4. Draw the 8x8 block of pixels!
|
||||
for (int y = 0; y < 8; y++)
|
||||
{
|
||||
// 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];
|
||||
|
||||
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);
|
||||
|
||||
// Find the raw SMS color in CRAM
|
||||
int paletteOffset = useSpritePalette ? 16 : 0;
|
||||
byte smsColor = CRAM[paletteOffset + colorIndex];
|
||||
|
||||
// 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;
|
||||
|
||||
// Calculate where this pixel goes on the final 256x192 screen
|
||||
int pixelX = (col * 8) + x;
|
||||
int pixelY = (row * 8) + y;
|
||||
|
||||
FrameBuffer[(pixelY * 256) + pixelX] = (255 << 24) | (r << 16) | (g << 8) | b;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user