using System; using System.Drawing; using System.Drawing.Imaging; using System.Runtime.InteropServices; using System.Windows.Forms; using Core.Cpu; using Core.Io; using Core.Memory; namespace Desktop { public partial class Form1 : Form { private Z80 _cpu = null!; private MemoryBus _memoryBus = null!; private IO_Bus _simpleIoBus = null!; private TapManager _tapManager = null!; private int _ulaFrameCount = 0; // The 16 physical colors of the ZX Spectrum (ARGB format) private readonly int[] SpectrumColors = new int[] { // Normal Colors (Bright = 0) unchecked((int)0xFF000000), // 0: Black unchecked((int)0xFF0000D7), // 1: Blue unchecked((int)0xFFD70000), // 2: Red unchecked((int)0xFFD700D7), // 3: Magenta unchecked((int)0xFF00D700), // 4: Green unchecked((int)0xFF00D7D7), // 5: Cyan unchecked((int)0xFFD7D700), // 6: Yellow unchecked((int)0xFFD7D7D7), // 7: White // Bright Colors (Bright = 1) unchecked((int)0xFF000000), // 8: Bright Black unchecked((int)0xFF0000FF), // 9: Bright Blue unchecked((int)0xFFFF0000), // 10: Bright Red unchecked((int)0xFFFF00FF), // 11: Bright Magenta unchecked((int)0xFF00FF00), // 12: Bright Green unchecked((int)0xFF00FFFF), // 13: Bright Cyan unchecked((int)0xFFFFFF00), // 14: Bright Yellow unchecked((int)0xFFFFFFFF) // 15: Bright White }; public Form1() { InitializeComponent(); InitializeEmulator(); } private void InitializeEmulator() { try { _memoryBus = new MemoryBus(); _simpleIoBus = new IO_Bus(); _tapManager = new TapManager(); _memoryBus.CrapRAMData(); byte[] romData = RomLoader.Load("48.rom"); _memoryBus.LoadRom(romData); _cpu = new Z80(_memoryBus, _simpleIoBus, _tapManager); // Pass 'this' so the DebuggerForm can talk back to this main window DebuggerForm debugger = new DebuggerForm(_cpu, _memoryBus, this); debugger.Show(); } catch (Exception ex) { MessageBox.Show($"Failed to initialize emulator:\n{ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } } // Inside Desktop/Form1.cs private void loadTAPToolStripMenuItem_Click(object sender, EventArgs e) { using (OpenFileDialog ofd = new OpenFileDialog()) { ofd.Filter = "Spectrum TAP Files|*.tap"; if (ofd.ShowDialog() == DialogResult.OK) { // The Desktop UI reads the file from the hard drive byte[] tapBytes = System.IO.File.ReadAllBytes(ofd.FileName); // The pure Core logic processes the bytes _cpu._tapManager.LoadTapData(tapBytes); //MessageBox.Show("Tape inserted! Type LOAD \"\" and press Enter.", "Tape Deck"); } } } // Public so the Debugger's background thread can call it 50 times a second public void RenderScreen() { _ulaFrameCount++; bool invertFlashPhase = (_ulaFrameCount % 32) >= 16; // --- NEW: Expanded screen size (32px border on all sides) --- const int screenWidth = 320; const int screenHeight = 256; const int borderSize = 32; int[] pixelData = new int[screenWidth * screenHeight]; // --- NEW: Fill the background with the Border Color --- // (Note: The hardware border always uses standard brightness, never bright) int currentBorderColor = SpectrumColors[_simpleIoBus.BorderColorIndex]; Array.Fill(pixelData, currentBorderColor); // Loop through the 6144 bytes of Pixel RAM for (int offset = 0; offset < 6144; offset++) { ushort address = (ushort)(0x4000 + offset); byte pixels = _memoryBus.Read(address); int y = ((offset & 0x0700) >> 8) | ((offset & 0x00E0) >> 2) | ((offset & 0x1800) >> 5); int x = (offset & 0x001F) * 8; int attrRow = y / 8; int attrCol = x / 8; ushort attrAddress = (ushort)(0x5800 + (attrRow * 32) + attrCol); byte attr = _memoryBus.Read(attrAddress); int ink = attr & 0x07; int paper = (attr >> 3) & 0x07; int brightOffset = (attr & 0x40) != 0 ? 8 : 0; bool isFlashSet = (attr & 0x80) != 0; int inkColor = SpectrumColors[ink + brightOffset]; int paperColor = SpectrumColors[paper + brightOffset]; if (isFlashSet && invertFlashPhase) { int temp = inkColor; inkColor = paperColor; paperColor = temp; } // Draw the 8 pixels for (int bit = 0; bit < 8; bit++) { bool isPixelSet = (pixels & (1 << (7 - bit))) != 0; // --- NEW: Add the 32px border offset to our X and Y calculations! --- int renderY = y + borderSize; int renderX = x + borderSize + bit; // Map it to our new, wider pixel array pixelData[(renderY * screenWidth) + renderX] = isPixelSet ? inkColor : paperColor; } } // --- NEW: Update Bitmap dimensions to match the new 320x256 array --- Bitmap bmp = new Bitmap(screenWidth, screenHeight, PixelFormat.Format32bppArgb); BitmapData bmpData = bmp.LockBits( new Rectangle(0, 0, screenWidth, screenHeight), ImageLockMode.WriteOnly, bmp.PixelFormat); Marshal.Copy(pixelData, 0, bmpData.Scan0, pixelData.Length); bmp.UnlockBits(bmpData); if (picScreen.Image != null) picScreen.Image.Dispose(); picScreen.Image = bmp; } private void UpdateMatrix(int row, int col, bool isPressed) { if (isPressed) { // Clear the bit to 0 (Active-Low = Pressed) _simpleIoBus.KeyboardRows[row] &= (byte)~(1 << col); } else { // Set the bit back to 1 (Unpressed) _simpleIoBus.KeyboardRows[row] |= (byte)(1 << col); } } // Hook this to Form1's KeyDown event protected override void OnKeyDown(KeyEventArgs e) { HandleKey(e.KeyCode, true); base.OnKeyDown(e); } // Hook this to Form1's KeyUp event protected override void OnKeyUp(KeyEventArgs e) { HandleKey(e.KeyCode, false); base.OnKeyUp(e); } private void HandleKey(Keys key, bool isPressed) { switch (key) { //Row 0: case Keys.ShiftKey: UpdateMatrix(0, 0, isPressed); break; case Keys.Z: UpdateMatrix(0, 1, isPressed); break; case Keys.X: UpdateMatrix(0, 2, isPressed); break; case Keys.C: UpdateMatrix(0, 3, isPressed); break; case Keys.V: UpdateMatrix(0, 4, isPressed); break; // Row 1 case Keys.A: UpdateMatrix(1, 0, isPressed); break; case Keys.S: UpdateMatrix(1, 1, isPressed); break; case Keys.D: UpdateMatrix(1, 2, isPressed); break; case Keys.F: UpdateMatrix(1, 3, isPressed); break; case Keys.G: UpdateMatrix(1, 4, isPressed); break; //Row 2: case Keys.Q: UpdateMatrix(2, 0, isPressed); break; case Keys.W: UpdateMatrix(2, 1, isPressed); break; case Keys.E: UpdateMatrix(2, 2, isPressed); break; case Keys.R: UpdateMatrix(2, 3, isPressed); break; case Keys.T: UpdateMatrix(2, 4, isPressed); break; // Row 3: case Keys.D1: UpdateMatrix(3, 0, isPressed); break; case Keys.D2: UpdateMatrix(3, 1, isPressed); break; case Keys.D3: UpdateMatrix(3, 2, isPressed); break; case Keys.D4: UpdateMatrix(3, 3, isPressed); break; case Keys.D5: UpdateMatrix(3, 4, isPressed); break; // Row 4: case Keys.D0: UpdateMatrix(4, 0, isPressed); break; case Keys.D9: UpdateMatrix(4, 1, isPressed); break; case Keys.D8: UpdateMatrix(4, 2, isPressed); break; case Keys.D7: UpdateMatrix(4, 3, isPressed); break; case Keys.D6: UpdateMatrix(4, 4, isPressed); break; // Row 5: case Keys.P: UpdateMatrix(5, 0, isPressed); break; case Keys.O: UpdateMatrix(5, 1, isPressed); break; case Keys.I: UpdateMatrix(5, 2, isPressed); break; case Keys.U: UpdateMatrix(5, 3, isPressed); break; case Keys.Y: UpdateMatrix(5, 4, isPressed); break; // Row 6 case Keys.Enter: UpdateMatrix(6, 0, isPressed); break; case Keys.L: UpdateMatrix(6, 1, isPressed); break; case Keys.K: UpdateMatrix(6, 2, isPressed); break; case Keys.J: UpdateMatrix(6, 3, isPressed); break; case Keys.H: UpdateMatrix(6, 4, isPressed); break; // Row 7 case Keys.Space: UpdateMatrix(7, 0, isPressed); break; case Keys.ControlKey: UpdateMatrix(7, 1, isPressed); break; // Symbol Shift case Keys.M: UpdateMatrix(7, 2, isPressed); break; case Keys.N: UpdateMatrix(7, 3, isPressed); break; case Keys.B: UpdateMatrix(7, 4, isPressed); break; } } } }