1 using System;
2 using System.Drawing;
3 using System.Threading.Tasks;
4 using System.Windows.Forms;
5 using Mesen.GUI.Config;
6 using Mesen.GUI.Forms;
7 
8 namespace Mesen.GUI.Debugger
9 {
10 	public partial class frmPpuViewer : BaseForm
11 	{
12 		private DateTime _lastUpdate = DateTime.MinValue;
13 		private InteropEmu.NotificationListener _notifListener;
14 		private TabPage _selectedTab;
15 		private bool _refreshing = false;
16 		private Size _originalSize;
17 		private bool _isCompact = false;
18 		private bool _isZoomed = false;
19 		private PpuViewerMode _mode;
20 
21 		private static int _nextPpuViewerId = 0;
22 		private int _ppuViewerId = 0;
23 
frmPpuViewer(PpuViewerMode mode = PpuViewerMode.Combined)24 		public frmPpuViewer(PpuViewerMode mode = PpuViewerMode.Combined)
25 		{
26 			InitializeComponent();
27 
28 			_ppuViewerId = GetNextPpuViewerId();
29 			_mode = mode;
30 
31 			if(Program.IsMono) {
32 				btnToggleView.Top -= 1;
33 				chkToggleZoom.Top -= 1;
34 			}
35 
36 			this._selectedTab = this.tpgNametableViewer;
37 			this.mnuAutoRefresh.Checked = ConfigManager.Config.DebugInfo.PpuAutoRefresh;
38 			this.mnuRefreshOnBreak.Checked = ConfigManager.Config.DebugInfo.PpuRefreshOnBreak;
39 			this.mnuShowInformationOverlay.Checked = ConfigManager.Config.DebugInfo.PpuShowInformationOverlay;
40 			this.ctrlNametableViewer.Connect(this.ctrlChrViewer);
41 
42 			Point? startupLocation;
43 			switch(_mode) {
44 				case PpuViewerMode.NametableViewer: startupLocation = ConfigManager.Config.DebugInfo.PpuNametableViewerLocation; break;
45 				case PpuViewerMode.ChrViewer: startupLocation = ConfigManager.Config.DebugInfo.PpuChrViewerLocation; break;
46 				case PpuViewerMode.SpriteViewer: startupLocation = ConfigManager.Config.DebugInfo.PpuSpriteViewerLocation; break;
47 				case PpuViewerMode.PaletteViewer: startupLocation = ConfigManager.Config.DebugInfo.PpuPaletteViewerLocation; break;
48 				default: startupLocation = ConfigManager.Config.DebugInfo.PpuWindowLocation; break;
49 			}
50 
51 			if(startupLocation.HasValue) {
52 				this.StartPosition = FormStartPosition.Manual;
53 				this.Location = startupLocation.Value;
54 			}
55 		}
56 
GetNextPpuViewerId()57 		public static int GetNextPpuViewerId()
58 		{
59 			return _nextPpuViewerId++;
60 		}
61 
InitShortcuts()62 		private void InitShortcuts()
63 		{
64 			mnuRefresh.InitShortcut(this, nameof(DebuggerShortcutsConfig.Refresh));
65 		}
66 
OnLoad(EventArgs e)67 		protected override void OnLoad(EventArgs e)
68 		{
69  			base.OnLoad(e);
70 
71 			if(!this.DesignMode) {
72 				this._notifListener = new InteropEmu.NotificationListener(ConfigManager.Config.DebugInfo.DebugConsoleId);
73 				this._notifListener.OnNotification += this._notifListener_OnNotification;
74 
75 				this.ctrlScanlineCycle.Initialize(_ppuViewerId, ConfigManager.Config.DebugInfo.PpuDisplayScanline, ConfigManager.Config.DebugInfo.PpuDisplayCycle);
76 
77 				GetData();
78 
79 				this.ctrlNametableViewer.RefreshViewer();
80 				this.ctrlChrViewer.RefreshViewer();
81 				this.ctrlSpriteViewer.RefreshViewer();
82 				this.ctrlPaletteViewer.RefreshViewer();
83 
84 				this.InitShortcuts();
85 				this.UpdateRefreshSpeedMenu();
86 
87 				string toggleViewTooltip = "Toggle Compact/Normal View";
88 				if(ConfigManager.Config.DebugInfo.Shortcuts.PpuViewer_ToggleView != Keys.None) {
89 					toggleViewTooltip += " (" + DebuggerShortcutsConfig.GetShortcutDisplay(ConfigManager.Config.DebugInfo.Shortcuts.PpuViewer_ToggleView) + ")";
90 				}
91 				this.toolTip.SetToolTip(this.btnToggleView, toggleViewTooltip);
92 
93 				string toggleZoomTooltip = "Toggle 2x Zoom";
94 				if(ConfigManager.Config.DebugInfo.Shortcuts.PpuViewer_ToggleZoom != Keys.None) {
95 					toggleZoomTooltip += " (" + DebuggerShortcutsConfig.GetShortcutDisplay(ConfigManager.Config.DebugInfo.Shortcuts.PpuViewer_ToggleZoom) + ")";
96 				}
97 				this.toolTip.SetToolTip(this.chkToggleZoom, toggleZoomTooltip);
98 
99 				_selectedTab = tabMain.SelectedTab;
100 				if(_mode != PpuViewerMode.Combined) {
101 					TabPage page = _selectedTab;
102 					switch(_mode) {
103 						case PpuViewerMode.NametableViewer: page = tpgNametableViewer; break;
104 						case PpuViewerMode.ChrViewer: page = tpgChrViewer; break;
105 						case PpuViewerMode.SpriteViewer: page = tpgSpriteViewer; break;
106 						case PpuViewerMode.PaletteViewer: page = tpgPaletteViewer; break;
107 					}
108 
109 					_selectedTab = page;
110 					tabMain.SelectTab(page);
111 					ToggleView();
112 				}
113 			}
114 		}
115 
OnFormClosing(FormClosingEventArgs e)116 		protected override void OnFormClosing(FormClosingEventArgs e)
117 		{
118 			base.OnFormClosing(e);
119 			this._notifListener.OnNotification -= this._notifListener_OnNotification;
120 
121 			Point location = this.WindowState != FormWindowState.Normal ? this.RestoreBounds.Location : this.Location;
122 			switch(_mode) {
123 				case PpuViewerMode.NametableViewer: ConfigManager.Config.DebugInfo.PpuNametableViewerLocation = location; break;
124 				case PpuViewerMode.ChrViewer: ConfigManager.Config.DebugInfo.PpuChrViewerLocation = location; break;
125 				case PpuViewerMode.SpriteViewer: ConfigManager.Config.DebugInfo.PpuSpriteViewerLocation = location; break;
126 				case PpuViewerMode.PaletteViewer: ConfigManager.Config.DebugInfo.PpuPaletteViewerLocation = location; break;
127 				default: ConfigManager.Config.DebugInfo.PpuWindowLocation = location; break;
128 			}
129 
130 			ConfigManager.Config.DebugInfo.PpuDisplayScanline = ctrlScanlineCycle.Scanline;
131 			ConfigManager.Config.DebugInfo.PpuDisplayCycle = ctrlScanlineCycle.Cycle;
132 			ConfigManager.ApplyChanges();
133 			InteropEmu.DebugClearPpuViewerSettings(_ppuViewerId);
134 		}
135 
_notifListener_OnNotification(InteropEmu.NotificationEventArgs e)136 		private void _notifListener_OnNotification(InteropEmu.NotificationEventArgs e)
137 		{
138 			switch(e.NotificationType) {
139 				case InteropEmu.ConsoleNotificationType.CodeBreak:
140 				case InteropEmu.ConsoleNotificationType.GamePaused:
141 					if(ConfigManager.Config.DebugInfo.PpuRefreshOnBreak) {
142 						this.GetData();
143 						this.BeginInvoke((MethodInvoker)(() => this.RefreshViewers()));
144 					}
145 					break;
146 
147 				case InteropEmu.ConsoleNotificationType.PpuViewerDisplayFrame:
148 					if(e.Parameter.ToInt32() == _ppuViewerId) {
149 						int refreshDelay = 60;
150 						switch(ConfigManager.Config.DebugInfo.PpuAutoRefreshSpeed) {
151 							case RefreshSpeed.Low: refreshDelay= 60; break;
152 							case RefreshSpeed.Normal: refreshDelay = 30; break;
153 							case RefreshSpeed.High: refreshDelay = 12; break;
154 						}
155 						if(ConfigManager.Config.DebugInfo.PpuAutoRefresh && !_refreshing && (DateTime.Now - _lastUpdate).Milliseconds > refreshDelay) {
156 							_lastUpdate = DateTime.Now;
157 							this.GetData();
158 							this.BeginInvoke((MethodInvoker)(() => this.RefreshViewers()));
159 						}
160 					}
161 					break;
162 
163 				case InteropEmu.ConsoleNotificationType.GameLoaded:
164 					//Configuration is lost when debugger is restarted (when switching game or power cycling)
165 					ctrlScanlineCycle.RefreshSettings();
166 					break;
167 			}
168 		}
169 
GetData()170 		private void GetData()
171 		{
172 			if(_isCompact) {
173 				//In compact mode, only load data for the current tab
174 				if(_selectedTab == this.tpgNametableViewer) {
175 					this.ctrlNametableViewer.GetData();
176 				} else if(_selectedTab == this.tpgChrViewer) {
177 					this.ctrlChrViewer.GetData();
178 				} else if(_selectedTab == this.tpgSpriteViewer) {
179 					this.ctrlSpriteViewer.GetData();
180 				} else if(_selectedTab == this.tpgPaletteViewer) {
181 					this.ctrlPaletteViewer.GetData();
182 				}
183 			} else {
184 				this.ctrlNametableViewer.GetData();
185 				this.ctrlChrViewer.GetData();
186 				this.ctrlSpriteViewer.GetData();
187 				this.ctrlPaletteViewer.GetData();
188 			}
189 		}
190 
RefreshViewers()191 		private void RefreshViewers()
192 		{
193 			_refreshing = true;
194 			if(_selectedTab == this.tpgNametableViewer) {
195 				this.ctrlNametableViewer.RefreshViewer();
196 			} else if(_selectedTab == this.tpgChrViewer) {
197 				this.ctrlChrViewer.RefreshViewer();
198 			} else if(_selectedTab == this.tpgSpriteViewer) {
199 				this.ctrlSpriteViewer.RefreshViewer();
200 			} else if(_selectedTab == this.tpgPaletteViewer) {
201 				this.ctrlPaletteViewer.RefreshViewer();
202 			}
203 			_refreshing = false;
204 		}
205 
mnuRefresh_Click(object sender, EventArgs e)206 		private void mnuRefresh_Click(object sender, EventArgs e)
207 		{
208 			this.GetData();
209 			this.RefreshViewers();
210 		}
211 
mnuClose_Click(object sender, EventArgs e)212 		private void mnuClose_Click(object sender, EventArgs e)
213 		{
214 			this.Close();
215 		}
216 
mnuAutoRefresh_Click(object sender, EventArgs e)217 		private void mnuAutoRefresh_Click(object sender, EventArgs e)
218 		{
219 			ConfigManager.Config.DebugInfo.PpuAutoRefresh = this.mnuAutoRefresh.Checked;
220 			ConfigManager.ApplyChanges();
221 		}
222 
mnuRefreshOnBreak_Click(object sender, EventArgs e)223 		private void mnuRefreshOnBreak_Click(object sender, EventArgs e)
224 		{
225 			ConfigManager.Config.DebugInfo.PpuRefreshOnBreak = this.mnuRefreshOnBreak.Checked;
226 			ConfigManager.ApplyChanges();
227 		}
228 
mnuShowInformationOverlay_Click(object sender, EventArgs e)229 		private void mnuShowInformationOverlay_Click(object sender, EventArgs e)
230 		{
231 			ConfigManager.Config.DebugInfo.PpuShowInformationOverlay = this.mnuShowInformationOverlay.Checked;
232 			ConfigManager.ApplyChanges();
233 		}
234 
tabMain_SelectedIndexChanged(object sender, EventArgs e)235 		private void tabMain_SelectedIndexChanged(object sender, EventArgs e)
236 		{
237 			bool wasZoomedIn = _isZoomed;
238 			if(wasZoomedIn) {
239 				this.ToggleZoom();
240 			}
241 
242 			this._selectedTab = this.tabMain.SelectedTab;
243 
244 			if(wasZoomedIn) {
245 				this.ToggleZoom();
246 			}
247 
248 			if(InteropEmu.DebugIsExecutionStopped()) {
249 				//Refresh data when changing tabs when not running
250 				this.RefreshViewers();
251 			}
252 		}
253 
ToggleZoom()254 		private void ToggleZoom()
255 		{
256 			ICompactControl ctrl = null;
257 			if(_selectedTab == tpgChrViewer) {
258 				ctrl = ctrlChrViewer;
259 			} else if(_selectedTab == tpgPaletteViewer) {
260 				ctrl = ctrlPaletteViewer;
261 			} else if(_selectedTab == tpgSpriteViewer) {
262 				ctrl = ctrlSpriteViewer;
263 			} else if(_selectedTab == tpgNametableViewer) {
264 				ctrl = ctrlNametableViewer;
265 			}
266 
267 			if(!_isZoomed) {
268 				Size pictureSize = ctrl.GetCompactSize(false);
269 				this.Size += pictureSize;
270 				_originalSize += pictureSize;
271 				ctrl.ScaleImage(2);
272 				_isZoomed = true;
273 			} else {
274 				Size pictureSize = ctrl.GetCompactSize(false);
275 				Size halfSize = new Size(pictureSize.Width / 2, pictureSize.Height / 2);
276 				this.Size -= halfSize;
277 				_originalSize -= halfSize;
278 				ctrl.ScaleImage(0.5);
279 				_isZoomed = false;
280 			}
281 			chkToggleZoom.Checked = _isZoomed;
282 		}
283 
ProcessCmdKey(ref Message msg, Keys keyData)284 		protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
285 		{
286 			if(keyData == ConfigManager.Config.DebugInfo.Shortcuts.PpuViewer_ToggleZoom) {
287 				ToggleZoom();
288 				return true;
289 			} else if(keyData == ConfigManager.Config.DebugInfo.Shortcuts.PpuViewer_ToggleView) {
290 				ToggleView();
291 				return true;
292 			}
293 
294 			if(!this.ctrlScanlineCycle.ContainsFocus) {
295 				if(this.tabMain.SelectedTab == tpgChrViewer) {
296 					bool shift = keyData.HasFlag(Keys.Shift);
297 					keyData &= ~Keys.Shift;
298 
299 					if(keyData >= Keys.D1 && keyData <= Keys.D9) {
300 						if(shift) {
301 							this.ctrlChrViewer.SelectPalette(keyData - Keys.D1);
302 						} else {
303 							this.ctrlChrViewer.SelectColor((keyData - Keys.D1) % 4);
304 						}
305 						return true;
306 					}
307 					if(keyData >= Keys.NumPad1 && keyData <= Keys.NumPad9) {
308 						if(shift) {
309 							this.ctrlChrViewer.SelectPalette(keyData - Keys.NumPad1);
310 						} else {
311 							this.ctrlChrViewer.SelectColor((keyData - Keys.NumPad1) % 4);
312 						}
313 						return true;
314 					}
315 				}
316 			}
317 			return base.ProcessCmdKey(ref msg, keyData);
318 		}
319 
SelectChrTile(int tileIndex, int paletteIndex, bool allowOpenWindow)320 		public void SelectChrTile(int tileIndex, int paletteIndex, bool allowOpenWindow)
321 		{
322 			if(_isCompact && allowOpenWindow) {
323 				//If in compact mode, don't move to the CHR tab, open or use another window instead
324 				frmPpuViewer otherPpuViewer = null;
325 				foreach(BaseForm frm in DebugWindowManager.GetWindows()) {
326 					if(frm != this && frm is frmPpuViewer && (!((frmPpuViewer)frm)._isCompact || ((frmPpuViewer)frm)._selectedTab == ((frmPpuViewer)frm).tpgChrViewer)) {
327 						//If a window exists and is either not in compact mode, or in compact mode and showing the CHR viewer, use it
328 						otherPpuViewer = frm as frmPpuViewer;
329 						break;
330 					}
331 				}
332 				if(otherPpuViewer == null) {
333 					//Open up a new viewer, in compact mode
334 					otherPpuViewer = DebugWindowManager.OpenPpuViewer(PpuViewerMode.ChrViewer);
335 					otherPpuViewer.SelectChrTile(tileIndex, paletteIndex, false);
336 				} else {
337 					//Reuse an existing viewer that's not in compact mode
338 					otherPpuViewer.SelectChrTile(tileIndex, paletteIndex, false);
339 					otherPpuViewer.BringToFront();
340 				}
341 			} else {
342 				if(!InteropEmu.DebugIsExecutionStopped() || ConfigManager.Config.DebugInfo.PpuRefreshOnBreak) {
343 					//Only change the palette if execution is not stopped (or if we're configured to refresh the viewer on break/pause)
344 					//Otherwise, the CHR viewer will refresh its data (and it might not match the data we loaded at the specified scanline/cycle anymore)
345 					ctrlChrViewer.SelectedPaletteIndex = paletteIndex;
346 				}
347 				ctrlChrViewer.SelectedTileIndex = tileIndex;
348 				tabMain.SelectTab(tpgChrViewer);
349 				_selectedTab = tpgChrViewer;
350 			}
351 		}
352 
ctrlNametableViewer_OnSelectChrTile(int tileIndex, int paletteIndex)353 		private void ctrlNametableViewer_OnSelectChrTile(int tileIndex, int paletteIndex)
354 		{
355 			SelectChrTile(tileIndex, paletteIndex, true);
356 		}
357 
ctrlSpriteViewer_OnSelectTilePalette(int tileIndex, int paletteIndex)358 		private void ctrlSpriteViewer_OnSelectTilePalette(int tileIndex, int paletteIndex)
359 		{
360 			SelectChrTile(tileIndex, paletteIndex, true);
361 		}
362 
UpdateRefreshSpeedMenu()363 		private void UpdateRefreshSpeedMenu()
364 		{
365 			mnuAutoRefreshLow.Checked = ConfigManager.Config.DebugInfo.PpuAutoRefreshSpeed == RefreshSpeed.Low;
366 			mnuAutoRefreshNormal.Checked = ConfigManager.Config.DebugInfo.PpuAutoRefreshSpeed == RefreshSpeed.Normal;
367 			mnuAutoRefreshHigh.Checked = ConfigManager.Config.DebugInfo.PpuAutoRefreshSpeed == RefreshSpeed.High;
368 		}
369 
mnuAutoRefreshSpeed_Click(object sender, EventArgs e)370 		private void mnuAutoRefreshSpeed_Click(object sender, EventArgs e)
371 		{
372 			if(sender == mnuAutoRefreshLow) {
373 				ConfigManager.Config.DebugInfo.PpuAutoRefreshSpeed = RefreshSpeed.Low;
374 			} else if(sender == mnuAutoRefreshNormal) {
375 				ConfigManager.Config.DebugInfo.PpuAutoRefreshSpeed = RefreshSpeed.Normal;
376 			} else if(sender == mnuAutoRefreshHigh) {
377 				ConfigManager.Config.DebugInfo.PpuAutoRefreshSpeed = RefreshSpeed.High;
378 			}
379 			ConfigManager.ApplyChanges();
380 
381 			UpdateRefreshSpeedMenu();
382 		}
383 
ToggleCompactMode(PpuViewerMode mode, ICompactControl control, TabPage tab, string title)384 		private void ToggleCompactMode(PpuViewerMode mode, ICompactControl control, TabPage tab, string title)
385 		{
386 			if(!_isCompact) {
387 				Point tabTopLeft = tabMain.PointToScreen(Point.Empty);
388 				Point tabContentTopLeft = tab.PointToScreen(Point.Empty);
389 
390 				int heightGap = tabContentTopLeft.Y - tabTopLeft.Y + ctrlScanlineCycle.Height;
391 
392 				_isCompact = true;
393 				_originalSize = this.Size;
394 				Size size = control.GetCompactSize(true);
395 				int widthDiff = ((Control)control).Width - size.Width;
396 				int heightDiff = ((Control)control).Height - size.Height;
397 
398 				this.Controls.Add((Control)control);
399 				((Control)control).BringToFront();
400 
401 				tabMain.Visible = false;
402 				ctrlScanlineCycle.Visible = false;
403 				this.Text = title;
404 
405 				this.Size = new Size(this.Width - widthDiff, this.Height - heightDiff - heightGap + 3);
406 			} else {
407 				_mode = PpuViewerMode.Combined;
408 				_isCompact = false;
409 				this.Size = _originalSize;
410 				tabMain.Visible = true;
411 				tab.Controls.Add((Control)control);
412 				ctrlScanlineCycle.Visible = true;
413 				this.Text = "PPU Viewer";
414 			}
415 
416 			btnToggleView.Image = _isCompact ? Properties.Resources.Expand : Properties.Resources.Collapse;
417 		}
418 
ToggleView()419 		private void ToggleView()
420 		{
421 			if(_selectedTab == tpgChrViewer) {
422 				ToggleCompactMode(PpuViewerMode.ChrViewer, ctrlChrViewer, tpgChrViewer, "CHR Viewer");
423 			} else if(_selectedTab == tpgPaletteViewer) {
424 				ToggleCompactMode(PpuViewerMode.PaletteViewer, ctrlPaletteViewer, tpgPaletteViewer, "Palette Viewer");
425 			} else if(_selectedTab == tpgSpriteViewer) {
426 				ToggleCompactMode(PpuViewerMode.SpriteViewer, ctrlSpriteViewer, tpgSpriteViewer, "Sprite Viewer");
427 			} else if(_selectedTab == tpgNametableViewer) {
428 				ToggleCompactMode(PpuViewerMode.NametableViewer, ctrlNametableViewer, tpgNametableViewer, "Nametable Viewer");
429 			}
430 		}
431 
btnToggleView_Click(object sender, EventArgs e)432 		private void btnToggleView_Click(object sender, EventArgs e)
433 		{
434 			ToggleView();
435 		}
436 
chkToggleZoom_Click(object sender, EventArgs e)437 		private void chkToggleZoom_Click(object sender, EventArgs e)
438 		{
439 			ToggleZoom();
440 		}
441 	}
442 
443 	public enum PpuViewerMode
444 	{
445 		Combined = 0,
446 		NametableViewer = 1,
447 		ChrViewer = 2,
448 		SpriteViewer = 3,
449 		PaletteViewer = 4,
450 	}
451 
452 	public interface ICompactControl
453 	{
GetCompactSize(bool includeMargins)454 		Size GetCompactSize(bool includeMargins);
ScaleImage(double scale)455 		void ScaleImage(double scale);
456 	}
457 }
458