using Mesen.GUI.Config; using Mesen.GUI.Forms; using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Drawing.Imaging; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace Mesen.GUI.Debugger { public partial class frmTilemapViewer : BaseForm { private int[,] _layerBpp = new int[8, 4] { { 2,2,2,2 }, { 4,4,2,0 }, { 4,4,0,0 }, { 8,4,0,0 }, { 8,2,0,0 }, { 4,2,0,0 }, { 4,0,0,0 }, { 8,0,0,0 } }; private NotificationListener _notifListener; private GetTilemapOptions _options; private DebugState _state; private byte[] _cgram; private byte[] _vram; private byte[] _tilemapData; private Bitmap _tilemapImage; private int _selectedRow = 0; private int _selectedColumn = 0; private DateTime _lastUpdate = DateTime.MinValue; private bool _autoRefresh = false; public frmTilemapViewer() { InitializeComponent(); } protected override void OnLoad(EventArgs e) { base.OnLoad(e); if(DesignMode) { return; } _notifListener = new NotificationListener(); _notifListener.OnNotification += OnNotificationReceived; _tilemapData = new byte[1024 * 1024 * 4]; _tilemapImage = new Bitmap(1024, 1024, PixelFormat.Format32bppArgb); ctrlImagePanel.Image = _tilemapImage; InitShortcuts(); TilemapViewerConfig config = ConfigManager.Config.Debug.TilemapViewer; if(!config.WindowSize.IsEmpty) { this.StartPosition = FormStartPosition.Manual; this.Size = config.WindowSize; this.Location = config.WindowLocation; } mnuAutoRefresh.Checked = config.AutoRefresh; chkShowTileGrid.Checked = config.ShowTileGrid; chkShowScrollOverlay.Checked = config.ShowScrollOverlay; ctrlImagePanel.ImageScale = config.ImageScale; ctrlScanlineCycleSelect.Initialize(config.RefreshScanline, config.RefreshCycle); _autoRefresh = config.AutoRefresh; _options.BgMode = 0; RefreshData(); RefreshViewer(); } private void InitShortcuts() { mnuRefresh.InitShortcut(this, nameof(DebuggerShortcutsConfig.Refresh)); mnuZoomIn.InitShortcut(this, nameof(DebuggerShortcutsConfig.ZoomIn)); mnuZoomOut.InitShortcut(this, nameof(DebuggerShortcutsConfig.ZoomOut)); mnuCopyToClipboard.InitShortcut(this, nameof(DebuggerShortcutsConfig.Copy)); mnuSaveAsPng.InitShortcut(this, nameof(DebuggerShortcutsConfig.SaveAsPng)); mnuCopyToClipboard.Click += (s, e) => { ctrlImagePanel.CopyToClipboard(); }; mnuSaveAsPng.Click += (s, e) => { ctrlImagePanel.SaveAsPng(); }; } protected override void OnFormClosed(FormClosedEventArgs e) { base.OnFormClosed(e); _notifListener?.Dispose(); TilemapViewerConfig config = ConfigManager.Config.Debug.TilemapViewer; config.WindowSize = this.WindowState != FormWindowState.Normal ? this.RestoreBounds.Size : this.Size; config.WindowLocation = this.WindowState != FormWindowState.Normal ? this.RestoreBounds.Location : this.Location; config.AutoRefresh = mnuAutoRefresh.Checked; config.ShowTileGrid = chkShowTileGrid.Checked; config.ShowScrollOverlay = chkShowScrollOverlay.Checked; config.RefreshScanline = ctrlScanlineCycleSelect.Scanline; config.RefreshCycle = ctrlScanlineCycleSelect.Cycle; config.ImageScale = ctrlImagePanel.ImageScale; ConfigManager.ApplyChanges(); } private void RefreshContent() { _lastUpdate = DateTime.Now; RefreshData(); this.BeginInvoke((Action)(() => { this.RefreshViewer(); })); } private void OnNotificationReceived(NotificationEventArgs e) { switch(e.NotificationType) { case ConsoleNotificationType.CodeBreak: RefreshContent(); break; case ConsoleNotificationType.ViewerRefresh: if(_autoRefresh && e.Parameter.ToInt32() == ctrlScanlineCycleSelect.ViewerId && (DateTime.Now - _lastUpdate).Milliseconds > 10) { RefreshContent(); } break; case ConsoleNotificationType.GameLoaded: //Configuration is lost when debugger is restarted (when switching game or power cycling) ctrlScanlineCycleSelect.RefreshSettings(); break; } } private void RefreshData() { _state = DebugApi.GetState(); _vram = DebugApi.GetMemoryState(SnesMemoryType.VideoRam); _cgram = DebugApi.GetMemoryState(SnesMemoryType.CGRam); } private bool IsLargeTileWidth { get { return _state.Ppu.Layers[_options.Layer].LargeTiles || _state.Ppu.BgMode == 5 || _state.Ppu.BgMode == 6; } } private bool IsLargeTileHeight { get { return _state.Ppu.Layers[_options.Layer].LargeTiles; } } private int GetWidth() { if(_state.Ppu.BgMode == 7) { return 1024; } LayerConfig layer = _state.Ppu.Layers[_options.Layer]; bool largeTileWidth = layer.LargeTiles || _state.Ppu.BgMode == 5 || _state.Ppu.BgMode == 6; bool largeTileHeight = layer.LargeTiles; int width = 256; if(layer.DoubleWidth) { width *= 2; } if(largeTileWidth) { width *= 2; } return width; } private int GetHeight() { if(_state.Ppu.BgMode == 7) { return 1024; } LayerConfig layer = _state.Ppu.Layers[_options.Layer]; int height = 256; if(layer.DoubleHeight) { height *= 2; } if(layer.LargeTiles) { height *= 2; } return height; } private void RefreshViewer() { if(_layerBpp[_state.Ppu.BgMode, _options.Layer] == 0) { _options.Layer = 0; } DebugApi.GetTilemap(_options, _vram, _cgram, _tilemapData); int mapWidth = GetWidth(); int mapHeight = GetHeight(); if(_tilemapImage.Width != mapWidth || _tilemapImage.Height != mapHeight) { _tilemapImage = new Bitmap(mapWidth, mapHeight, PixelFormat.Format32bppArgb); ctrlImagePanel.Image = _tilemapImage; } using(Graphics g = Graphics.FromImage(_tilemapImage)) { GCHandle handle = GCHandle.Alloc(_tilemapData, GCHandleType.Pinned); Bitmap source = new Bitmap(mapWidth, mapHeight, 4 * 1024, PixelFormat.Format32bppArgb, handle.AddrOfPinnedObject()); g.DrawImage(source, 0, 0); handle.Free(); } btnLayer1.BackColor = _options.Layer == 0 ? SystemColors.GradientActiveCaption : Color.Empty; btnLayer2.BackColor = _options.Layer == 1 ? SystemColors.GradientActiveCaption : Color.Empty; btnLayer3.BackColor = _options.Layer == 2 ? SystemColors.GradientActiveCaption : Color.Empty; btnLayer4.BackColor = _options.Layer == 3 ? SystemColors.GradientActiveCaption : Color.Empty; btnLayer1.Enabled = _layerBpp[_state.Ppu.BgMode, 0] > 0; btnLayer2.Enabled = _layerBpp[_state.Ppu.BgMode, 1] > 0; btnLayer3.Enabled = _layerBpp[_state.Ppu.BgMode, 2] > 0; btnLayer4.Enabled = _layerBpp[_state.Ppu.BgMode, 3] > 0; ctrlImagePanel.ImageSize = new Size(GetWidth(), GetHeight()); ctrlImagePanel.Selection = new Rectangle(_selectedColumn * 8, _selectedRow * 8, IsLargeTileWidth ? 16 : 8, IsLargeTileHeight ? 16 : 8); ctrlImagePanel.GridSizeX = chkShowTileGrid.Checked ? (IsLargeTileWidth ? 16 : 8): 0; ctrlImagePanel.GridSizeY = chkShowTileGrid.Checked ? (IsLargeTileHeight ? 16 : 8): 0; if(chkShowScrollOverlay.Checked) { LayerConfig layer = _state.Ppu.Layers[_options.Layer]; bool largeTileWidth = layer.LargeTiles || _state.Ppu.BgMode == 5 || _state.Ppu.BgMode == 6; bool largeTileHeight = layer.LargeTiles; int hScroll = _state.Ppu.BgMode == 7 ? (int)_state.Ppu.Mode7.HScroll : layer.HScroll; int vScroll = _state.Ppu.BgMode == 7 ? (int)_state.Ppu.Mode7.VScroll : layer.VScroll; int height = _state.Ppu.OverscanMode ? 239 : 224; ctrlImagePanel.Overlay = new Rectangle(hScroll, vScroll, largeTileWidth ? 512 : 256, largeTileHeight ? height*2 : height); } else { ctrlImagePanel.Overlay = Rectangle.Empty; } UpdateFields(); } private void UpdateFields() { if(_state.Ppu.BgMode == 7) { //Selected tile txtMapNumber.Text = "0"; txtPosition.Text = _selectedColumn.ToString() + ", " + _selectedRow.ToString(); int address = _selectedRow * 128 + _selectedColumn; int value = _vram[address] | (_vram[address + 1] << 8); txtAddress.Text = address.ToString("X4"); txtValue.Text = value.ToString("X4"); txtTileNumber.Text = (value & 0xFF).ToString(); txtPalette.Text = "0"; chkPriorityFlag.Checked = false; chkHorizontalMirror.Checked = false; chkVerticalMirror.Checked = false; //Tilemap txtMapSize.Text = "128x128"; txtMapAddress.Text = "0000"; txtTilesetAddress.Text = "0000"; txtTileSize.Text = "8x8"; txtBitDepth.Text = "8"; } else { int row = (IsLargeTileHeight ? _selectedRow / 2 : _selectedRow); int column = (IsLargeTileWidth ? _selectedColumn / 2 : _selectedColumn); LayerConfig layer = _state.Ppu.Layers[_options.Layer]; int addrVerticalScrollingOffset = layer.DoubleHeight ? ((row & 0x20) << (layer.DoubleWidth ? 6 : 5)) : 0; int baseOffset = (layer.TilemapAddress >> 1) + addrVerticalScrollingOffset + ((row & 0x1F) << 5); int address = (baseOffset + (column & 0x1F) + (layer.DoubleWidth ? ((column & 0x20) << 5) : 0)) << 1; int value = _vram[address] | (_vram[address + 1] << 8); //Selected tile txtMapNumber.Text = ((column >= 32 ? 1 : 0) + (row >= 32 ? 1 : 0)).ToString(); txtPosition.Text = (column & 0x1F).ToString() + ", " + (row & 0x1F).ToString(); txtAddress.Text = address.ToString("X4"); txtValue.Text = value.ToString("X4"); txtTileNumber.Text = (value & 0x3FF).ToString(); txtPalette.Text = ((value >> 10) & 0x07).ToString(); chkPriorityFlag.Checked = (value & 0x2000) != 0; chkHorizontalMirror.Checked = (value & 0x4000) != 0; chkVerticalMirror.Checked = (value & 0x8000) != 0; //Tilemap txtMapSize.Text = (layer.DoubleWidth ? "64" : "32") + "x" + (layer.DoubleHeight ? "64" : "32"); txtMapAddress.Text = layer.TilemapAddress.ToString("X4"); txtTilesetAddress.Text = layer.ChrAddress.ToString("X4"); txtTileSize.Text = (IsLargeTileWidth ? "16" : "8") + "x" + (IsLargeTileHeight ? "16" : "8"); txtBitDepth.Text = _layerBpp[_state.Ppu.BgMode, _options.Layer].ToString(); } } private void btnLayer1_Click(object sender, EventArgs e) { _options.Layer = 0; RefreshViewer(); } private void btnLayer2_Click(object sender, EventArgs e) { _options.Layer = 1; RefreshViewer(); } private void btnLayer3_Click(object sender, EventArgs e) { _options.Layer = 2; RefreshViewer(); } private void btnLayer4_Click(object sender, EventArgs e) { _options.Layer = 3; RefreshViewer(); } private void chkShowTileGrid_Click(object sender, EventArgs e) { RefreshViewer(); } private void chkShowScrollOverlay_Click(object sender, EventArgs e) { RefreshViewer(); } private void mnuRefresh_Click(object sender, EventArgs e) { RefreshData(); RefreshViewer(); } private void mnuZoomIn_Click(object sender, EventArgs e) { ctrlImagePanel.ZoomIn(); } private void mnuZoomOut_Click(object sender, EventArgs e) { ctrlImagePanel.ZoomOut(); } private void mnuClose_Click(object sender, EventArgs e) { Close(); } private void ctrlImagePanel_MouseClick(object sender, MouseEventArgs e) { _selectedColumn = e.X / (8 * ctrlImagePanel.ImageScale); _selectedRow = e.Y / (8 * ctrlImagePanel.ImageScale); if(IsLargeTileWidth) { _selectedColumn &= 0xFE; } if(IsLargeTileHeight) { _selectedRow &= 0xFE; } RefreshViewer(); } private void mnuAutoRefresh_CheckedChanged(object sender, EventArgs e) { _autoRefresh = mnuAutoRefresh.Checked; } } }