From 95ac0ec7c102cb61a6d90f91311c7a4e68bc0a50 Mon Sep 17 00:00:00 2001 From: parsons Date: Tue, 12 May 2026 00:30:28 +0100 Subject: [PATCH] Implemenmted rendering priorities --- Core/Video/SmsVdp.cs | 58 ++++++++++++++++--------- Desktop/DebuggerForm.cs | 10 ++--- Desktop/Form1.Designer.cs | 20 ++++----- Desktop/Form1.cs | 90 +++++++++++++++++++++++++++++++-------- Desktop/Program.cs | 2 +- 5 files changed, 125 insertions(+), 55 deletions(-) diff --git a/Core/Video/SmsVdp.cs b/Core/Video/SmsVdp.cs index df66b1e..0547b8f 100644 --- a/Core/Video/SmsVdp.cs +++ b/Core/Video/SmsVdp.cs @@ -9,6 +9,7 @@ namespace Core.Video 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]; + private bool[] _priorityBuffer = new bool[256 * 192]; // Tracks priority pixels! // The Control Port State Machine (Port 0xBF) private bool _isSecondControlByte = false; @@ -131,18 +132,19 @@ namespace Core.Video private void RenderBackground() { ushort nameTableBase = (ushort)((Registers[2] & 0x0E) << 10); - 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) + bool lockRowScroll = (Registers[0] & 0x80) != 0; + bool lockColScroll = (Registers[0] & 0x40) != 0; + + // Clear the priority mask for the new frame! + Array.Clear(_priorityBuffer, 0, _priorityBuffer.Length); for (int screenY = 0; screenY < 192; screenY++) { 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; @@ -150,7 +152,6 @@ namespace Core.Video 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; @@ -158,31 +159,34 @@ namespace Core.Video int col = vdpX / 8; int tileX = vdpX % 8; - // 1. Read the 16-bit Tile instruction from the Name Table + // 1. Read the 16-bit Tile instruction 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 Tile Index and Palette Info + // 2. EXTRACT ALL THE HARDWARE BITS! int tileIndex = tileData & 0x01FF; - bool useSpritePalette = (tileData & 0x0800) != 0; + bool flipH = (tileData & 0x0200) != 0; // Bit 9 + bool flipV = (tileData & 0x0400) != 0; // Bit 10 + bool useSpritePalette = (tileData & 0x0800) != 0; // Bit 11 + bool priority = (tileData & 0x1000) != 0; // Bit 12 - // 3. Find the tile data in VRAM + // 3. Apply Vertical Flip (Read from the bottom of the tile instead of the top) + int readY = flipV ? (7 - tileY) : tileY; ushort tileAddress = (ushort)(tileIndex * 32); - // 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]; + byte bp0 = VRAM[tileAddress + (readY * 4) + 0]; + byte bp1 = VRAM[tileAddress + (readY * 4) + 1]; + byte bp2 = VRAM[tileAddress + (readY * 4) + 2]; + byte bp3 = VRAM[tileAddress + (readY * 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); + // 4. Apply Horizontal Flip (Shift from right-to-left instead of left-to-right) + int readX = flipH ? tileX : (7 - tileX); + int colorIndex = ((bp0 >> readX) & 1) | + (((bp1 >> readX) & 1) << 1) | + (((bp2 >> readX) & 1) << 2) | + (((bp3 >> readX) & 1) << 3); int paletteOffset = useSpritePalette ? 16 : 0; byte smsColor = CRAM[paletteOffset + colorIndex]; @@ -191,7 +195,16 @@ namespace Core.Video int g = ((smsColor >> 2) & 0x03) * 85; int b = ((smsColor >> 4) & 0x03) * 85; - FrameBuffer[(screenY * 256) + screenX] = (255 << 24) | (r << 16) | (g << 8) | b; + int screenAddress = (screenY * 256) + screenX; + FrameBuffer[screenAddress] = (255 << 24) | (r << 16) | (g << 8) | b; + + // 5. FLAG THE PRIORITY PIXEL! + // If this tile has priority AND the pixel isn't transparent (color 0), + // tell the sprite renderer not to draw over it! + if (priority && colorIndex != 0) + { + _priorityBuffer[screenAddress] = true; + } } } } @@ -262,6 +275,9 @@ namespace Core.Video // If the color index is 0, DO NOT draw it! Let the background show through. if (colorIndex == 0) continue; + // If the background tile at this exact pixel claimed priority, hide the sprite! + if (_priorityBuffer[(screenY * 256) + screenX]) continue; + // Sprites ALWAYS use the second half of CRAM (Palette 1: Indices 16-31) byte smsColor = CRAM[16 + colorIndex]; diff --git a/Desktop/DebuggerForm.cs b/Desktop/DebuggerForm.cs index 43e324f..04c53e3 100644 --- a/Desktop/DebuggerForm.cs +++ b/Desktop/DebuggerForm.cs @@ -10,9 +10,9 @@ namespace Desktop { private readonly Z80 _cpu; private readonly SmsMemoryBus _memoryBus; - private readonly Form1 _mainForm; + private readonly ParsonsForm1 _mainForm; - public DebuggerForm(Z80 cpu, SmsMemoryBus memoryBus, Form1 mainForm) + public DebuggerForm(Z80 cpu, SmsMemoryBus memoryBus, ParsonsForm1 mainForm) { InitializeComponent(); _cpu = cpu; @@ -103,9 +103,9 @@ namespace Desktop lblIE.Text = $"Interrupt Mode: {_cpu.InterruptMode}"; lblFlags.Text = $"Flags: {_cpu.GetFlagsString()}"; lblTStates.Text = $"T-States: {_cpu.TotalTStates}"; - //lblFrames.Text = $"Frames Rendered: {_mainForm.TotalFrameCount}"; - //lblFrameTime.Text = $"Frame Time: {((float)_mainForm.FrameTime):F1}ms"; - //lblFPS.Text = $"FPS: {_mainForm.FramesPerSecond:F2}"; + lblFrames.Text = $"Frames Rendered: {_mainForm.TotalFrameCount}"; + lblFrameTime.Text = $"Frame Time: {((float)_mainForm.FrameTime):F1}ms"; + lblFPS.Text = $"FPS: {_mainForm.FramesPerSecond:F2}"; UpdateMemoryView(); UpdateStackView(); UpdateDisassemblyView(); diff --git a/Desktop/Form1.Designer.cs b/Desktop/Form1.Designer.cs index ddd0633..4543a60 100644 --- a/Desktop/Form1.Designer.cs +++ b/Desktop/Form1.Designer.cs @@ -1,6 +1,6 @@ namespace Desktop { - partial class Form1 + partial class ParsonsForm1 { /// /// Required designer variable. @@ -49,7 +49,7 @@ // pbScreen.Location = new Point(12, 36); pbScreen.Name = "pbScreen"; - pbScreen.Size = new Size(762, 672); + pbScreen.Size = new Size(768, 576); pbScreen.SizeMode = PictureBoxSizeMode.Zoom; pbScreen.TabIndex = 1; pbScreen.TabStop = false; @@ -60,7 +60,7 @@ menuStrip1.Items.AddRange(new ToolStripItem[] { fileToolStripMenuItem, viewToolStripMenuItem, machineToolStripMenuItem, helpToolStripMenuItem }); menuStrip1.Location = new Point(0, 0); menuStrip1.Name = "menuStrip1"; - menuStrip1.Size = new Size(785, 33); + menuStrip1.Size = new Size(791, 33); menuStrip1.TabIndex = 2; menuStrip1.Text = "menuStrip1"; // @@ -75,27 +75,27 @@ // openToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { includedToolStripMenuItem, selectROMToolStripMenuItem1 }); openToolStripMenuItem.Name = "openToolStripMenuItem"; - openToolStripMenuItem.Size = new Size(270, 34); + openToolStripMenuItem.Size = new Size(158, 34); openToolStripMenuItem.Text = "Open"; // // includedToolStripMenuItem // includedToolStripMenuItem.Name = "includedToolStripMenuItem"; - includedToolStripMenuItem.Size = new Size(270, 34); + includedToolStripMenuItem.Size = new Size(218, 34); includedToolStripMenuItem.Text = "Included"; includedToolStripMenuItem.Click += includedToolStripMenuItem_Click; // // selectROMToolStripMenuItem1 // selectROMToolStripMenuItem1.Name = "selectROMToolStripMenuItem1"; - selectROMToolStripMenuItem1.Size = new Size(270, 34); + selectROMToolStripMenuItem1.Size = new Size(218, 34); selectROMToolStripMenuItem1.Text = "Select ROM..."; selectROMToolStripMenuItem1.Click += selectROMToolStripMenuItem_Click; // // exitToolStripMenuItem // exitToolStripMenuItem.Name = "exitToolStripMenuItem"; - exitToolStripMenuItem.Size = new Size(270, 34); + exitToolStripMenuItem.Size = new Size(158, 34); exitToolStripMenuItem.Text = "Exit"; exitToolStripMenuItem.Click += exitToolStripMenuItem_Click; // @@ -140,16 +140,16 @@ aboutToolStripMenuItem.Size = new Size(164, 34); aboutToolStripMenuItem.Text = "About"; // - // Form1 + // ParsonsForm1 // AutoScaleDimensions = new SizeF(10F, 25F); AutoScaleMode = AutoScaleMode.Font; - ClientSize = new Size(785, 719); + ClientSize = new Size(791, 622); Controls.Add(pbScreen); Controls.Add(menuStrip1); MainMenuStrip = menuStrip1; Margin = new Padding(4); - Name = "Form1"; + Name = "ParsonsForm1"; Text = "Form1"; ((System.ComponentModel.ISupportInitialize)pbScreen).EndInit(); menuStrip1.ResumeLayout(false); diff --git a/Desktop/Form1.cs b/Desktop/Form1.cs index 9a08674..55ef98f 100644 --- a/Desktop/Form1.cs +++ b/Desktop/Form1.cs @@ -1,22 +1,26 @@ +using System.Diagnostics; +using System.Drawing.Imaging; using System.Reflection; using System.Reflection.PortableExecutable; -using Core; -using System.Threading.Tasks; -using System.Drawing.Imaging; using System.Runtime.InteropServices; +using System.Threading.Tasks; using System.Windows.Forms; +using Core; namespace Desktop { - public partial class Form1 : Form + public partial class ParsonsForm1 : Form { private SmsMachine _machine = null!; private DebuggerForm _debugger; private Bitmap _screenBitmap = new Bitmap(256, 192, PixelFormat.Format32bppArgb); - private Task _emulatorTask; - - + private double TargetFrameTime = 16.667f; + public int TotalFrameCount = 0; + public double FrameTime { get; private set; } = 0; + public double FramesPerSecond { get; private set; } = 0; + private string _currentRomName = "No ROM"; + private Stopwatch _stopwatch = new System.Diagnostics.Stopwatch(); public bool IsRunning { get; private set; } = false; public ushort? Breakpoint @@ -25,9 +29,10 @@ namespace Desktop set { if (_machine != null) _machine.Breakpoint = value; } } - public Form1() + public ParsonsForm1() { InitializeComponent(); + this.Text = $"Parsons Master System 2026 - {_currentRomName}"; _machine = new SmsMachine(); PopulateIncludedRomsMenu(); @@ -46,32 +51,81 @@ namespace Desktop // Update the PictureBox pbScreen.Image = _screenBitmap; + TotalFrameCount++; } public void StartEmulator() { if (IsRunning) return; IsRunning = true; + TotalFrameCount = 0; + + double TargetFrameTime = 1000.0 / 60.0; + _emulatorTask = Task.Run(() => { + _stopwatch.Restart(); + + double nextFrameTargetTime = _stopwatch.Elapsed.TotalMilliseconds + TargetFrameTime; + double lastFpsUpdate = _stopwatch.Elapsed.TotalMilliseconds; + int framesThisSecond = 0; + while (IsRunning) { + // Mark exactly when the emulator starts thinking + double frameStartTime = _stopwatch.Elapsed.TotalMilliseconds; + + // 1. Do the heavy lifting (Z80 and VDP) _machine.RunFrame(); - Invoke((System.Windows.Forms.MethodInvoker)delegate { DrawScreen(); }); + // 2. FIRE AND FORGET! Tell Windows to draw, but DO NOT WAIT for it to finish! + BeginInvoke((System.Windows.Forms.MethodInvoker)delegate { DrawScreen(); }); - // Safety catch: If we hit a breakpoint while running, stop the loop! + // 3. Safety catch if (_machine.Breakpoint.HasValue && _machine.Cpu.PC == _machine.Breakpoint.Value) { IsRunning = false; - - // Optional: Force the debugger UI to update immediately so you see it! - Invoke((System.Windows.Forms.MethodInvoker)delegate { _debugger?.uiUpdateTimer_Tick(null, EventArgs.Empty); }); + BeginInvoke((System.Windows.Forms.MethodInvoker)delegate { _debugger?.uiUpdateTimer_Tick(null, EventArgs.Empty); }); + break; } - else + + // 4. Calculate TRUE Frame Time (Compute Time) BEFORE we go to sleep! + FrameTime = _stopwatch.Elapsed.TotalMilliseconds - frameStartTime; + + // --- HIGH PRECISION THROTTLE --- + while (_stopwatch.Elapsed.TotalMilliseconds < nextFrameTargetTime) { - // Only throttle the speed if we are actively running - Thread.Sleep(8); + if (nextFrameTargetTime - _stopwatch.Elapsed.TotalMilliseconds > 2.0) + { + Thread.Sleep(1); + } + else + { + Thread.SpinWait(10); + } + } + + // --- METRICS MATH --- + double currentTime = _stopwatch.Elapsed.TotalMilliseconds; + TotalFrameCount++; + framesThisSecond++; + + if (currentTime - lastFpsUpdate >= 1000.0) + { + FramesPerSecond = framesThisSecond / ((currentTime - lastFpsUpdate) / 1000.0); + framesThisSecond = 0; + lastFpsUpdate = currentTime; + + BeginInvoke((System.Windows.Forms.MethodInvoker)delegate { + this.Text = $"Parsons Master System - {_currentRomName} [FPS/FT: {FramesPerSecond:F0}/{FrameTime:F1}]"; + }); + } + + nextFrameTargetTime += TargetFrameTime; + + if (currentTime > nextFrameTargetTime + TargetFrameTime) + { + nextFrameTargetTime = currentTime + TargetFrameTime; } } }); @@ -102,8 +156,8 @@ namespace Desktop // 3. Jam it into the Sega Mapper _machine.LoadCartridge(rom); - // 4. Update the Window title so it looks professional - this.Text = $"Parsons Master System 2026 - {Path.GetFileNameWithoutExtension(filePath)}"; + _currentRomName = Path.GetFileNameWithoutExtension(filePath); + this.Text = $"Parsons Master System - {_currentRomName}"; // 5. Turn the power on! diff --git a/Desktop/Program.cs b/Desktop/Program.cs index 804fa5c..72afec7 100644 --- a/Desktop/Program.cs +++ b/Desktop/Program.cs @@ -11,7 +11,7 @@ namespace Desktop // To customize application configuration such as set high DPI settings or default font, // see https://aka.ms/applicationconfiguration. ApplicationConfiguration.Initialize(); - Application.Run(new Form1()); + Application.Run(new ParsonsForm1()); } } } \ No newline at end of file