diff --git a/Core/Audio/SmsApu.cs b/Core/Audio/SmsApu.cs index 338d34c..30e7253 100644 --- a/Core/Audio/SmsApu.cs +++ b/Core/Audio/SmsApu.cs @@ -22,6 +22,7 @@ namespace Core.Audio private int[] _counters = new int[4]; private int[] _polarities = new int[4] { 1, 1, 1, 1 }; // 1 = High, -1 = Low private ushort _lfsr = 0x8000; // Linear Feedback Shift Register (For Noise) + private bool _noiseFlipFlop = false; // The SN76489 Volume Table reduces amplitude by exactly 2 decibels per step. private static readonly float[] VolumeTable = { @@ -83,27 +84,33 @@ namespace Core.Audio _counters[3]--; if (_counters[3] <= 0) { - // Noise rate depends on Bits 0-1 of Register 6 + // Reload the counter int shiftRate = Registers[6] & 0x03; if (shiftRate == 0) _counters[3] = 0x10; // Fast else if (shiftRate == 1) _counters[3] = 0x20; // Medium else if (shiftRate == 2) _counters[3] = 0x40; // Slow - else _counters[3] = (Registers[4] == 0) ? 1024 : Registers[4]; // Linked to Tone 2! + else _counters[3] = (Registers[4] == 0) ? 1024 : Registers[4]; // Linked to Tone 2 - // Shift the Noise LFSR - int tappedBit = _lfsr & 1; - _lfsr >>= 1; + // THE FIX: Toggle the internal clock (Divide by 2!) + _noiseFlipFlop = !_noiseFlipFlop; - if (tappedBit == 1) + // Only shift the random static when the clock goes High + if (_noiseFlipFlop) { - bool isWhiteNoise = (Registers[6] & 0x04) != 0; - // The Sega Master System physically tapped bits 0 and 3 for its white noise - if (isWhiteNoise) _lfsr ^= 0x0009; + int tappedBit = _lfsr & 1; + _lfsr >>= 1; - _lfsr |= 0x8000; // Inject the high bit + if (tappedBit == 1) + { + bool isWhiteNoise = (Registers[6] & 0x04) != 0; + if (isWhiteNoise) _lfsr ^= 0x0009; // SMS-specific XOR mask + + _lfsr |= 0x8000; // Inject the high bit + } + + // The noise channel output is driven directly by the tapped bit + _polarities[3] = (tappedBit == 1) ? 1 : -1; } - - _polarities[3] = (tappedBit == 1) ? 1 : -1; } } @@ -149,12 +156,14 @@ namespace Core.Audio // Volume registers (1, 3, 5, 7) and Noise Control (6) only hold 4 bits total. // We completely overwrite them. Registers[_latchedRegister] = (ushort)data; + if (_latchedRegister == 6) _lfsr = 0x8000; } else { // Tone registers (0, 2, 4) hold 10 bits. // A Latch byte ONLY overwrites the bottom 4 bits and leaves the top 6 alone! Registers[_latchedRegister] = (ushort)((Registers[_latchedRegister] & 0x03F0) | data); + if (_latchedRegister == 6) _lfsr = 0x8000; } } else diff --git a/Desktop/Desktop.csproj b/Desktop/Desktop.csproj index 9b6c28d..3441ec4 100644 --- a/Desktop/Desktop.csproj +++ b/Desktop/Desktop.csproj @@ -90,255 +90,257 @@ + - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never - Always + Never + - Always + Never diff --git a/Desktop/Form1.Designer.cs b/Desktop/Form1.Designer.cs index fa9b97c..6786aa3 100644 --- a/Desktop/Form1.Designer.cs +++ b/Desktop/Form1.Designer.cs @@ -40,6 +40,7 @@ resetToolStripMenuItem = new ToolStripMenuItem(); helpToolStripMenuItem = new ToolStripMenuItem(); aboutToolStripMenuItem = new ToolStripMenuItem(); + vRAMViewerToolStripMenuItem = new ToolStripMenuItem(); menuStrip1.SuspendLayout(); SuspendLayout(); // @@ -90,7 +91,7 @@ // // viewToolStripMenuItem // - viewToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { debuggerToolStripMenuItem }); + viewToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { debuggerToolStripMenuItem, vRAMViewerToolStripMenuItem }); viewToolStripMenuItem.Name = "viewToolStripMenuItem"; viewToolStripMenuItem.Size = new Size(65, 29); viewToolStripMenuItem.Text = "View"; @@ -98,7 +99,7 @@ // debuggerToolStripMenuItem // debuggerToolStripMenuItem.Name = "debuggerToolStripMenuItem"; - debuggerToolStripMenuItem.Size = new Size(194, 34); + debuggerToolStripMenuItem.Size = new Size(270, 34); debuggerToolStripMenuItem.Text = "Debugger"; debuggerToolStripMenuItem.Click += debuggerToolStripMenuItem_Click; // @@ -129,6 +130,13 @@ aboutToolStripMenuItem.Size = new Size(164, 34); aboutToolStripMenuItem.Text = "About"; // + // vRAMViewerToolStripMenuItem + // + vRAMViewerToolStripMenuItem.Name = "vRAMViewerToolStripMenuItem"; + vRAMViewerToolStripMenuItem.Size = new Size(270, 34); + vRAMViewerToolStripMenuItem.Text = "VRAM Viewer"; + vRAMViewerToolStripMenuItem.Click += vramViewerToolStripMenuItem_Click; + // // ParsonsForm1 // AutoScaleDimensions = new SizeF(10F, 25F); @@ -159,5 +167,6 @@ private ToolStripMenuItem aboutToolStripMenuItem; private ToolStripMenuItem includedToolStripMenuItem; private ToolStripMenuItem selectROMToolStripMenuItem1; + private ToolStripMenuItem vRAMViewerToolStripMenuItem; } } diff --git a/Desktop/Form1.cs b/Desktop/Form1.cs index be26a00..b9859c8 100644 --- a/Desktop/Form1.cs +++ b/Desktop/Form1.cs @@ -14,6 +14,7 @@ namespace Desktop { public SmsMachine _machine = null!; private DebuggerForm _debugger; + private VramViewerForm _vramViewer; private Bitmap _screenBitmap = new Bitmap(256, 192, PixelFormat.Format32bppArgb); private NAudioPlayer _audioPlayer; private Task _emulatorTask; @@ -296,6 +297,16 @@ namespace Desktop _debugger.Show(); } + private void vramViewerToolStripMenuItem_Click(object sender, EventArgs e) + { + if (_vramViewer == null || _vramViewer.IsDisposed) + { + // Pass the VDP directly to the viewer! + _vramViewer = new VramViewerForm(_machine.VideoProcessor); + } + _vramViewer.Show(); + } + private void includedToolStripMenuItem_Click(object sender, EventArgs e) { diff --git a/Desktop/ROMS/Golden Axe Warrior.sav b/Desktop/ROMS/Golden Axe Warrior.sav new file mode 100644 index 0000000..724dc97 Binary files /dev/null and b/Desktop/ROMS/Golden Axe Warrior.sav differ diff --git a/Desktop/VramViewerForm.cs b/Desktop/VramViewerForm.cs new file mode 100644 index 0000000..22c37e1 --- /dev/null +++ b/Desktop/VramViewerForm.cs @@ -0,0 +1,100 @@ +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.Runtime.InteropServices; +using System.Windows.Forms; +using Core.Video; + +namespace Desktop +{ + public class VramViewerForm : Form + { + private readonly SmsVdp _vdp; + private readonly System.Windows.Forms.Timer _refreshTimer; + private readonly Bitmap _vramBitmap; + private readonly int[] _pixelData; + + public VramViewerForm(SmsVdp vdp) + { + _vdp = vdp; + this.Text = "VRAM Viewer (512 Tiles)"; + + // 256x128 native resolution, scaled up by 2 for visibility! + this.ClientSize = new Size(512, 256); + this.DoubleBuffered = true; + this.FormBorderStyle = FormBorderStyle.FixedToolWindow; + + _vramBitmap = new Bitmap(256, 128, PixelFormat.Format32bppArgb); + _pixelData = new int[256 * 128]; + + // Update the window 10 times a second so it doesn't drain CPU power + _refreshTimer = new System.Windows.Forms.Timer(); + _refreshTimer.Interval = 100; + _refreshTimer.Tick += (s, e) => UpdateVramImage(); + _refreshTimer.Start(); + } + + private void UpdateVramImage() + { + if (_vdp == null) return; + + // Loop through all 512 tiles in VRAM + for (int tile = 0; tile < 512; tile++) + { + // Calculate grid position (32 columns, 16 rows) + int tileX = (tile % 32) * 8; + int tileY = (tile / 32) * 8; + int tileAddress = tile * 32; + + // Decode the 8x8 pixels + for (int y = 0; y < 8; y++) + { + byte bp0 = _vdp.VRAM[tileAddress + (y * 4) + 0]; + byte bp1 = _vdp.VRAM[tileAddress + (y * 4) + 1]; + byte bp2 = _vdp.VRAM[tileAddress + (y * 4) + 2]; + byte bp3 = _vdp.VRAM[tileAddress + (y * 4) + 3]; + + for (int x = 0; x < 8; x++) + { + int shift = 7 - x; + int colorIndex = ((bp0 >> shift) & 1) | (((bp1 >> shift) & 1) << 1) | + (((bp2 >> shift) & 1) << 2) | (((bp3 >> shift) & 1) << 3); + + // Use the Sprite Palette (Offset 16) so characters and enemies are colored correctly + byte smsColor = _vdp.CRAM[16 + colorIndex]; + int r = (smsColor & 0x03) * 85; + int g = ((smsColor >> 2) & 0x03) * 85; + int b = ((smsColor >> 4) & 0x03) * 85; + + // Background color (Index 0) is technically transparent for sprites, so we render it as magenta + if (colorIndex == 0) { r = 255; g = 0; b = 255; } + + _pixelData[((tileY + y) * 256) + (tileX + x)] = (255 << 24) | (r << 16) | (g << 8) | b; + } + } + } + + // Blast the array into the Bitmap + var data = _vramBitmap.LockBits(new Rectangle(0, 0, 256, 128), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); + Marshal.Copy(_pixelData, 0, data.Scan0, _pixelData.Length); + _vramBitmap.UnlockBits(data); + + this.Invalidate(); + } + + protected override void OnPaint(PaintEventArgs e) + { + base.OnPaint(e); + // Draw it chunky and pixel-perfect + e.Graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor; + e.Graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.Half; + e.Graphics.DrawImage(_vramBitmap, new Rectangle(0, 0, this.ClientSize.Width, this.ClientSize.Height)); + } + + protected override void OnFormClosing(FormClosingEventArgs e) + { + _refreshTimer.Stop(); + base.OnFormClosing(e); + } + } +} \ No newline at end of file