From 86cac326d74064ff076fa2f7dea3ee0c55715a44 Mon Sep 17 00:00:00 2001 From: Sour Date: Sat, 14 Dec 2024 23:19:02 +0900 Subject: [PATCH] Debugger: Tile editor - Improved behavior when editing tiles from a tilemap in games that bankswitch vram in the middle of the screen -Also allows cropping the edit window when the size doesn't fit (e.g using 4x4 near the bottom of the screen can result in a 4x2 edit window, etc.) -Fixed a display issue when the number of edited columns & rows didn't match --- UI/Debugger/Utilities/ToolRefreshHelper.cs | 60 ++++++++++++++++++- .../ViewModels/SpriteViewerViewModel.cs | 6 +- UI/Debugger/ViewModels/TileEditorViewModel.cs | 2 +- UI/Debugger/ViewModels/TileViewerViewModel.cs | 11 +++- .../ViewModels/TilemapViewerViewModel.cs | 24 +++++++- UI/Debugger/Windows/TileEditorWindow.axaml.cs | 24 ++++++-- 6 files changed, 115 insertions(+), 12 deletions(-) diff --git a/UI/Debugger/Utilities/ToolRefreshHelper.cs b/UI/Debugger/Utilities/ToolRefreshHelper.cs index c86016ab2..553c6b0e1 100644 --- a/UI/Debugger/Utilities/ToolRefreshHelper.cs +++ b/UI/Debugger/Utilities/ToolRefreshHelper.cs @@ -11,6 +11,7 @@ using System.ComponentModel; using System.Runtime.InteropServices; using System.Collections.Concurrent; +using System.Threading.Tasks; namespace Mesen.Debugger.Utilities { @@ -39,13 +40,70 @@ public LastRefreshInfo(Window host) private static ConcurrentDictionary _activeWindows = new(); private static int _nextId = 0; + public static void ExecuteAt(int scanline, int cycle, CpuType cpuType, Action callback) + { + //Execute callback at the specified scanline/cycle + //If the callback is not called by the core within 300ms + //call it from the UI as a fallback + object lockObject = new(); + bool done = false; + + int viewerId = ToolRefreshHelper.GetNextId(); + NotificationListener? listener = new(); + listener.OnNotification += (NotificationEventArgs e) => { + switch(e.NotificationType) { + case ConsoleNotificationType.ViewerRefresh: + if(e.Parameter.ToInt32() == viewerId) { + lock(callback) { + if(!done) { + Task.Run(() => { + //DebugApi.RemoveViewerId() must not be called inside the notification callback (which is + //the same thread as the emulation thread). Otherwise, the viewer timing collection the + //debugger is iterating on will be modified, causing a crash. + DebugApi.RemoveViewerId(viewerId, cpuType); + }); + + listener.Dispose(); + listener = null; + callback(); + done = true; + } + } + } + break; + } + }; + + DebugApi.SetViewerUpdateTiming(viewerId, scanline, cycle, cpuType); + + Task.Run(async () => { + await Task.Delay(300); + if(listener != null) { + lock(callback) { + if(!done) { + //Give up after 300ms and call the callback + DebugApi.RemoveViewerId(viewerId, cpuType); + callback(); + listener.Dispose(); + done = true; + } + } + } + }); + } + + private static int GetNextId() + { + return Interlocked.Increment(ref _nextId); + } + private static int RegisterWindow(Window wnd, RefreshTimingConfig cfg, CpuType cpuType) { if(_activeWindows.ContainsKey(wnd)) { throw new Exception("Register window called twice"); } - int newId = Interlocked.Increment(ref _nextId); + int newId = GetNextId(); wnd.Closing += OnWindowClosingHandler; wnd.Closed += OnWindowClosedHandler; diff --git a/UI/Debugger/ViewModels/SpriteViewerViewModel.cs b/UI/Debugger/ViewModels/SpriteViewerViewModel.cs index 9b97f084a..eb9a80879 100644 --- a/UI/Debugger/ViewModels/SpriteViewerViewModel.cs +++ b/UI/Debugger/ViewModels/SpriteViewerViewModel.cs @@ -209,7 +209,11 @@ private ContextMenuAction GetEditTileAction(Window wnd) sprite.RealWidth / size.Width, sprite.Format, sprite.Palette + paletteOffset, - wnd); + wnd, + CpuType, + RefreshTiming.Config.RefreshScanline, + RefreshTiming.Config.RefreshCycle + ); } } }; diff --git a/UI/Debugger/ViewModels/TileEditorViewModel.cs b/UI/Debugger/ViewModels/TileEditorViewModel.cs index 006019e24..a7d07bd01 100644 --- a/UI/Debugger/ViewModels/TileEditorViewModel.cs +++ b/UI/Debugger/ViewModels/TileEditorViewModel.cs @@ -288,7 +288,7 @@ private unsafe void RefreshViewer() for(int y = 0; y < _rowCount; y++) { for(int x = 0; x < _columnCount; x++) { fixed(UInt32* ptr = _tileBuffer) { - AddressInfo addr = _tileAddresses[y * _rowCount + x]; + AddressInfo addr = _tileAddresses[y * _columnCount + x]; byte[] sourceData = DebugApi.GetMemoryValues(addr.Type, (uint)addr.Address, (uint)(addr.Address + bytesPerTile - 1)); DebugApi.GetTileView(_cpuType, GetOptions(x, y), sourceData, sourceData.Length, PaletteColors, (IntPtr)ptr); UInt32* viewer = (UInt32*)framebuffer.FrameBuffer.Address; diff --git a/UI/Debugger/ViewModels/TileViewerViewModel.cs b/UI/Debugger/ViewModels/TileViewerViewModel.cs index 4b7534ff5..472599e5a 100644 --- a/UI/Debugger/ViewModels/TileViewerViewModel.cs +++ b/UI/Debugger/ViewModels/TileViewerViewModel.cs @@ -572,7 +572,16 @@ private void EditTileGrid(int columnCount, int rowCount, Window wnd) addresses.Add(new AddressInfo() { Address = GetTileAddress(new PixelPoint(p.X + col*GridSizeX, p.Y + row*GridSizeY)), Type = Config.Source }); } } - TileEditorWindow.OpenAtTile(addresses, columnCount, Config.Format, SelectedPalette, wnd); + TileEditorWindow.OpenAtTile( + addresses, + columnCount, + Config.Format, + SelectedPalette, + wnd, + CpuType, + RefreshTiming.Config.RefreshScanline, + RefreshTiming.Config.RefreshCycle + ); } private void DrawNesChrPageDelimiters() diff --git a/UI/Debugger/ViewModels/TilemapViewerViewModel.cs b/UI/Debugger/ViewModels/TilemapViewerViewModel.cs index 749f24dc5..972fe58ef 100644 --- a/UI/Debugger/ViewModels/TilemapViewerViewModel.cs +++ b/UI/Debugger/ViewModels/TilemapViewerViewModel.cs @@ -705,7 +705,13 @@ private void EditTileGrid(int columnCount, int rowCount, Window wnd) for(int col = 0; col < columnCount; col++) { DebugTilemapTileInfo? tile = DebugApi.GetTilemapTileInfo((uint)(p.X + GridSizeX*col), (uint)(p.Y + GridSizeY*row), CpuType, GetOptions(SelectedTab), _data.Vram, _data.PpuState, _data.PpuToolsState); if(tile == null) { - return; + if(col == 0) { + rowCount = row; + break; + } else { + columnCount = col; + continue; + } } if(palette == -1) { @@ -714,8 +720,22 @@ private void EditTileGrid(int columnCount, int rowCount, Window wnd) addresses.Add(new AddressInfo() { Address = tile.Value.TileAddress, Type = memType }); } } + + if(rowCount <= 0 || columnCount <= 0) { + return; + } + palette = Math.Max(0, palette); - TileEditorWindow.OpenAtTile(addresses, columnCount, _data.TilemapInfo.Format, palette, wnd); + TileEditorWindow.OpenAtTile( + addresses, + columnCount, + _data.TilemapInfo.Format, + palette, + wnd, + CpuType, + RefreshTiming.Config.RefreshScanline, + RefreshTiming.Config.RefreshCycle + ); } private void DrawMode7Overlay() diff --git a/UI/Debugger/Windows/TileEditorWindow.axaml.cs b/UI/Debugger/Windows/TileEditorWindow.axaml.cs index 5c633edce..a2e21c16a 100644 --- a/UI/Debugger/Windows/TileEditorWindow.axaml.cs +++ b/UI/Debugger/Windows/TileEditorWindow.axaml.cs @@ -120,12 +120,22 @@ private void InitializeComponent() AvaloniaXamlLoader.Load(this); } - public static void OpenAtTile(AddressInfo tileAddr, TileFormat tileFormat, int selectedPalette, Window parent) + public static void OpenAtTile(List tileAddresses, int columnCount, TileFormat tileFormat, int selectedPalette, Window parent, CpuType cpuType, int scanline, int cycle) { - OpenAtTile(new List() { tileAddr }, 1, tileFormat, selectedPalette, parent); + if(EmuApi.IsPaused()) { + //If paused, use the current state - this might mismatch if viewer doesn't have "refresh on pause" enabled + InternalOpenAtTile(tileAddresses, columnCount, tileFormat, selectedPalette, parent); + } else { + //While running, delay the tile viewer's opening until we can grab the memory mappings + //at the same time as the viewer was refreshed the last time + //This allows mid-screen PPU bank switching to open up properly/reliably in the tile editor + ToolRefreshHelper.ExecuteAt(scanline, cycle, cpuType, () => { + InternalOpenAtTile(tileAddresses, columnCount, tileFormat, selectedPalette, parent); + }); + } } - public static void OpenAtTile(List tileAddresses, int columnCount, TileFormat tileFormat, int selectedPalette, Window parent) + private static void InternalOpenAtTile(List tileAddresses, int columnCount, TileFormat tileFormat, int selectedPalette, Window parent) { for(int i = 0; i < tileAddresses.Count; i++) { AddressInfo addr = tileAddresses[i]; @@ -138,9 +148,11 @@ public static void OpenAtTile(List tileAddresses, int columnCount, return; } - TileEditorViewModel model = new(tileAddresses, columnCount, tileFormat, selectedPalette); - TileEditorWindow wnd = DebugWindowManager.CreateDebugWindow(() => new TileEditorWindow(model)); - wnd.ShowCentered((Control)parent); + Dispatcher.UIThread.Post(() => { + TileEditorViewModel model = new(tileAddresses, columnCount, tileFormat, selectedPalette); + TileEditorWindow wnd = DebugWindowManager.CreateDebugWindow(() => new TileEditorWindow(model)); + wnd.ShowCentered((Control)parent); + }); } public void ProcessNotification(NotificationEventArgs e)