From f5d02d76073569df53e02a9a966a4d79fb4a20a7 Mon Sep 17 00:00:00 2001 From: Sour Date: Mon, 27 Dec 2021 23:07:34 -0500 Subject: [PATCH] Tilemap Viewer: Tile information + tooltip --- Core/Debugger/PpuTools.h | 28 ++++ Core/Gameboy/Debugger/GbPpuTools.cpp | 48 +++++++ Core/Gameboy/Debugger/GbPpuTools.h | 1 + Core/NES/Debugger/NesPpuTools.cpp | 49 +++++++ Core/NES/Debugger/NesPpuTools.h | 7 +- Core/SNES/Debugger/SnesPpuTools.cpp | 69 +++++++++- Core/SNES/Debugger/SnesPpuTools.h | 3 +- InteropDLL/DebugApiWrapper.cpp | 1 + NewUI/Config/Debugger/TilemapViewerConfig.cs | 24 ++-- NewUI/Debugger/Controls/DynamicTooltip.axaml | 13 +- .../Debugger/Controls/DynamicTooltip.axaml.cs | 4 +- NewUI/Debugger/Controls/PictureViewer.cs | 78 ++++++----- .../ViewModels/TilemapViewerViewModel.cs | 129 +++++++++++++++++- .../Windows/EventViewerWindow.axaml.cs | 13 +- .../Windows/TilemapViewerWindow.axaml | 83 ++++++++--- .../Windows/TilemapViewerWindow.axaml.cs | 76 ++++++----- NewUI/Interop/DebugApi.cs | 40 ++++++ NewUI/Windows/MainWindow.axaml.cs | 1 + 18 files changed, 547 insertions(+), 120 deletions(-) diff --git a/Core/Debugger/PpuTools.h b/Core/Debugger/PpuTools.h index 81592868..4390ecc8 100644 --- a/Core/Debugger/PpuTools.h +++ b/Core/Debugger/PpuTools.h @@ -33,6 +33,33 @@ struct DebugSpriteInfo uint32_t SpritePreview[64 * 64]; }; +enum class NullableBoolean +{ + Undefined = -1, + False = 0, + True = 1 +}; + +struct DebugTilemapTileInfo +{ + int32_t Row = -1; + int32_t Column = -1; + int32_t Width = -1; + int32_t Height = -1; + + int32_t TileMapAddress = -1; + + int32_t TileIndex = -1; + int32_t TileAddress = -1; + + int32_t PaletteIndex = -1; + int32_t PaletteAddress = -1; + + NullableBoolean HorizontalMirroring = NullableBoolean::Undefined; + NullableBoolean VerticalMirroring = NullableBoolean::Undefined; + NullableBoolean HighPriority = NullableBoolean::Undefined; +}; + class PpuTools { protected: @@ -51,6 +78,7 @@ public: void GetTileView(GetTileViewOptions options, uint8_t *source, uint32_t srcSize, uint32_t* palette, uint32_t *outBuffer); + virtual DebugTilemapTileInfo GetTilemapTileInfo(uint32_t x, uint32_t y, uint8_t* vram, GetTilemapOptions options, BaseState& baseState) = 0; virtual FrameInfo GetTilemapSize(GetTilemapOptions options, BaseState& state) = 0; virtual void GetTilemap(GetTilemapOptions options, BaseState& state, uint8_t* vram, uint32_t* palette, uint32_t* outBuffer) = 0; diff --git a/Core/Gameboy/Debugger/GbPpuTools.cpp b/Core/Gameboy/Debugger/GbPpuTools.cpp index 2b24d198..15021626 100644 --- a/Core/Gameboy/Debugger/GbPpuTools.cpp +++ b/Core/Gameboy/Debugger/GbPpuTools.cpp @@ -88,6 +88,54 @@ FrameInfo GbPpuTools::GetTilemapSize(GetTilemapOptions options, BaseState& state return { 256, 256 }; } +DebugTilemapTileInfo GbPpuTools::GetTilemapTileInfo(uint32_t x, uint32_t y, uint8_t* vram, GetTilemapOptions options, BaseState& baseState) +{ + DebugTilemapTileInfo result; + + FrameInfo size = GetTilemapSize(options, baseState); + + if(x >= size.Width || y >= size.Height) { + return result; + } + + GbPpuState& state = (GbPpuState&)baseState; + bool isCgb = state.CgbEnabled; + + int row = y / 8; + int column = x / 8; + + int offset = options.Layer == 1 ? 0x1C00 : 0x1800; + uint16_t baseOffset = offset + ((row & 0x1F) << 5); + uint16_t addr = (baseOffset + column); + uint8_t tileIndex = vram[addr]; + + uint8_t attributes = isCgb ? vram[addr | 0x2000] : 0; + + uint16_t baseTile = state.BgTileSelect ? 0 : 0x1000; + uint16_t tileStart = baseTile + (baseTile ? (int8_t)tileIndex * 16 : tileIndex * 16); + + uint16_t tileBank = (attributes & 0x08) ? 0x2000 : 0x0000; + tileStart |= tileBank; + + result.Column = column; + result.Row = row; + result.Height = 8; + result.Width = 8; + result.TileMapAddress = addr; + result.TileIndex = tileIndex; + result.TileAddress = tileStart; + + if(isCgb) { + result.PaletteIndex = (attributes & 0x07); + result.PaletteAddress = (result.PaletteIndex << 2); + result.HorizontalMirroring = (NullableBoolean)((attributes & 0x20) != 0); + result.VerticalMirroring = (NullableBoolean)((attributes & 0x40) != 0); + result.HighPriority = (NullableBoolean)((attributes & 0x80) != 0); + } + + return result; +} + FrameInfo GbPpuTools::GetSpritePreviewSize(GetSpritePreviewOptions options, BaseState& state) { return { 256, 256 }; diff --git a/Core/Gameboy/Debugger/GbPpuTools.h b/Core/Gameboy/Debugger/GbPpuTools.h index c4a199f0..b0892217 100644 --- a/Core/Gameboy/Debugger/GbPpuTools.h +++ b/Core/Gameboy/Debugger/GbPpuTools.h @@ -15,6 +15,7 @@ public: void GetTilemap(GetTilemapOptions options, BaseState& state, uint8_t* vram, uint32_t* palette, uint32_t *outBuffer) override; FrameInfo GetTilemapSize(GetTilemapOptions options, BaseState& state) override; + DebugTilemapTileInfo GetTilemapTileInfo(uint32_t x, uint32_t y, uint8_t* vram, GetTilemapOptions options, BaseState& baseState) override; void GetSpritePreview(GetSpritePreviewOptions options, BaseState& state, uint8_t* vram, uint8_t* oamRam, uint32_t* palette, uint32_t *outBuffer) override; FrameInfo GetSpritePreviewSize(GetSpritePreviewOptions options, BaseState& state) override; diff --git a/Core/NES/Debugger/NesPpuTools.cpp b/Core/NES/Debugger/NesPpuTools.cpp index c2dec820..8cf8029a 100644 --- a/Core/NES/Debugger/NesPpuTools.cpp +++ b/Core/NES/Debugger/NesPpuTools.cpp @@ -119,6 +119,55 @@ FrameInfo NesPpuTools::GetTilemapSize(GetTilemapOptions options, BaseState& stat return { 512, 480 }; } +DebugTilemapTileInfo NesPpuTools::GetTilemapTileInfo(uint32_t x, uint32_t y, uint8_t* vram, GetTilemapOptions options, BaseState& baseState) +{ + DebugTilemapTileInfo result; + + FrameInfo size = GetTilemapSize(options, baseState); + if(x >= size.Width || y >= size.Height) { + return result; + } + + uint8_t row = y / 8; + uint8_t column = x / 8; + uint8_t nametableIndex = (column >= 32 ? 1 : 0) | (row >= 30 ? 2 : 0); + + column &= 0x1F; + if(row > 30) { + row -= 30; + } + + MMC5* mmc5 = dynamic_cast(_mapper); + NesPpuState& state = (NesPpuState&)baseState; + + uint16_t bgAddr = (state.ControlReg & 0x10) ? 0x1000 : 0; + uint16_t baseAddr = 0x2000 + nametableIndex * 0x400; + uint16_t baseAttributeAddr = baseAddr + 960; + uint16_t ntIndex = (row << 5) + column; + uint16_t attributeAddress = baseAttributeAddr + ((row & 0xFC) << 1) + (column >> 2); + uint8_t attribute = vram[attributeAddress]; + uint8_t shift = (column & 0x02) | ((row & 0x02) << 1); + + uint8_t paletteBaseAddr; + if(mmc5 && mmc5->IsExtendedAttributes()) { + paletteBaseAddr = mmc5->GetExAttributeNtPalette(ntIndex) << 2; + } else { + paletteBaseAddr = ((attribute >> shift) & 0x03) << 2; + } + + result.Row = row; + result.Column = column; + result.Width = 8; + result.Height = 8; + result.TileMapAddress = baseAddr + ntIndex; + result.TileIndex = vram[result.TileMapAddress]; + result.TileAddress = bgAddr + (result.TileIndex << 4); + result.PaletteIndex = paletteBaseAddr >> 2; + result.PaletteAddress = 0x3F00 | paletteBaseAddr; + + return result; +} + void NesPpuTools::GetSpriteInfo(DebugSpriteInfo& sprite, uint32_t i, GetSpritePreviewOptions& options, NesPpuState& state, uint8_t* vram, uint8_t* oamRam, uint32_t* palette) { sprite.SpriteIndex = i; diff --git a/Core/NES/Debugger/NesPpuTools.h b/Core/NES/Debugger/NesPpuTools.h index 274501d5..ec574813 100644 --- a/Core/NES/Debugger/NesPpuTools.h +++ b/Core/NES/Debugger/NesPpuTools.h @@ -18,9 +18,10 @@ public: NesPpuTools(Debugger* debugger, Emulator *emu, NesConsole* console); void GetTilemap(GetTilemapOptions options, BaseState& state, uint8_t* vram, uint32_t* palette, uint32_t *outBuffer) override; - FrameInfo GetSpritePreviewSize(GetSpritePreviewOptions options, BaseState& state) override; - - void GetSpritePreview(GetSpritePreviewOptions options, BaseState& state, uint8_t* vram, uint8_t* oamRam, uint32_t* palette, uint32_t *outBuffer) override; FrameInfo GetTilemapSize(GetTilemapOptions options, BaseState& state) override; + DebugTilemapTileInfo GetTilemapTileInfo(uint32_t x, uint32_t y, uint8_t* vram, GetTilemapOptions options, BaseState& baseState) override; + + FrameInfo GetSpritePreviewSize(GetSpritePreviewOptions options, BaseState& state) override; + void GetSpritePreview(GetSpritePreviewOptions options, BaseState& state, uint8_t* vram, uint8_t* oamRam, uint32_t* palette, uint32_t *outBuffer) override; uint32_t GetSpriteList(GetSpritePreviewOptions options, BaseState& baseState, uint8_t* vram, uint8_t* oamRam, uint32_t* palette, DebugSpriteInfo outBuffer[]) override; }; \ No newline at end of file diff --git a/Core/SNES/Debugger/SnesPpuTools.cpp b/Core/SNES/Debugger/SnesPpuTools.cpp index 93049a0c..6064f149 100644 --- a/Core/SNES/Debugger/SnesPpuTools.cpp +++ b/Core/SNES/Debugger/SnesPpuTools.cpp @@ -5,6 +5,10 @@ #include "SNES/SnesDefaultVideoFilter.h" #include "SNES/Ppu.h" +static constexpr uint8_t layerBpp[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 } +}; + SnesPpuTools::SnesPpuTools(Debugger* debugger, Emulator *emu) : PpuTools(debugger, emu) { } @@ -14,10 +18,6 @@ void SnesPpuTools::GetTilemap(GetTilemapOptions options, BaseState& baseState, u PpuState& state = (PpuState&)baseState; FrameInfo outputSize = GetTilemapSize(options, state); - static constexpr uint8_t layerBpp[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 } - }; - bool directColor = state.DirectColorMode && (state.BgMode == 3 || state.BgMode == 4 || state.BgMode == 7); uint16_t basePaletteOffset = 0; @@ -281,6 +281,67 @@ FrameInfo SnesPpuTools::GetTilemapSize(GetTilemapOptions options, BaseState& bas return size; } +DebugTilemapTileInfo SnesPpuTools::GetTilemapTileInfo(uint32_t x, uint32_t y, uint8_t* vram, GetTilemapOptions options, BaseState& baseState) +{ + DebugTilemapTileInfo result; + + FrameInfo size = GetTilemapSize(options, baseState); + if(x >= size.Width || y >= size.Height) { + return result; + } + + PpuState& state = (PpuState&)baseState; + LayerConfig layer = state.Layers[options.Layer]; + + uint8_t bpp = layerBpp[state.BgMode][options.Layer]; + if(bpp == 0) { + return result; + } + + uint16_t basePaletteOffset = 0; + if(state.BgMode == 0) { + basePaletteOffset = options.Layer * 64; + } + + uint32_t row = y / 8; + uint32_t column = x / 8; + + result.Row = row; + result.Column = column; + + if(state.BgMode == 7) { + result.TileMapAddress = row * 256 + column * 2; + result.TileIndex = vram[result.TileMapAddress]; + result.TileAddress = result.TileIndex * 128; + result.Height = 8; + result.Width = 8; + } else { + bool largeTileWidth = layer.LargeTiles || state.BgMode == 5 || state.BgMode == 6; + bool largeTileHeight = layer.LargeTiles; + + uint16_t addrVerticalScrollingOffset = layer.DoubleHeight ? ((row & 0x20) << (layer.DoubleWidth ? 6 : 5)) : 0; + uint16_t baseOffset = layer.TilemapAddress + addrVerticalScrollingOffset + ((row & 0x1F) << 5); + + uint16_t addr = (baseOffset + (column & 0x1F) + (layer.DoubleWidth ? ((column & 0x20) << 5) : 0)) << 1; + + result.Height = largeTileHeight ? 16 : 8; + result.Width = largeTileWidth ? 16 : 8; + result.VerticalMirroring = (NullableBoolean)((vram[addr + 1] & 0x80) != 0); + result.HorizontalMirroring = (NullableBoolean)((vram[addr + 1] & 0x40) != 0); + result.HighPriority = (NullableBoolean)((vram[addr + 1] & 0x20) != 0); + result.PaletteIndex = bpp == 8 ? 0 : (vram[addr + 1] >> 2) & 0x07; + result.TileIndex = ((vram[addr + 1] & 0x03) << 8) | vram[addr]; + result.TileMapAddress = addr; + + uint16_t tileStart = (layer.ChrAddress << 1) + result.TileIndex * 8 * bpp; + result.TileAddress = tileStart; + + result.PaletteAddress = basePaletteOffset + (result.PaletteIndex * (1 << bpp)); + } + + return result; +} + FrameInfo SnesPpuTools::GetSpritePreviewSize(GetSpritePreviewOptions options, BaseState& state) { return { 512, 256 }; diff --git a/Core/SNES/Debugger/SnesPpuTools.h b/Core/SNES/Debugger/SnesPpuTools.h index 87a1f758..cbc0d6bd 100644 --- a/Core/SNES/Debugger/SnesPpuTools.h +++ b/Core/SNES/Debugger/SnesPpuTools.h @@ -16,7 +16,8 @@ public: void GetTilemap(GetTilemapOptions options, BaseState& state, uint8_t* vram, uint32_t* palette, uint32_t* outBuffer) override; FrameInfo GetTilemapSize(GetTilemapOptions options, BaseState& state) override; - + DebugTilemapTileInfo GetTilemapTileInfo(uint32_t x, uint32_t y, uint8_t* vram, GetTilemapOptions options, BaseState& baseState) override; + void GetSpritePreview(GetSpritePreviewOptions options, BaseState& state, uint8_t* vram, uint8_t* oamRam, uint32_t* palette, uint32_t* outBuffer) override; uint32_t GetSpriteList(GetSpritePreviewOptions options, BaseState& baseState, uint8_t* vram, uint8_t* oamRam, uint32_t* palette, DebugSpriteInfo outBuffer[]) override; FrameInfo GetSpritePreviewSize(GetSpritePreviewOptions options, BaseState& state) override; diff --git a/InteropDLL/DebugApiWrapper.cpp b/InteropDLL/DebugApiWrapper.cpp index da6305b8..9422e05f 100644 --- a/InteropDLL/DebugApiWrapper.cpp +++ b/InteropDLL/DebugApiWrapper.cpp @@ -99,6 +99,7 @@ extern "C" DllExport void __stdcall GetTilemap(CpuType cpuType, GetTilemapOptions options, BaseState& state, uint8_t *vram, uint32_t* palette, uint32_t *outputBuffer) { GetDebugger()->GetPpuTools(cpuType)->GetTilemap(options, state, vram, palette, outputBuffer); } DllExport FrameInfo __stdcall GetTilemapSize(CpuType cpuType, GetTilemapOptions options, BaseState& state) { return GetDebugger()->GetPpuTools(cpuType)->GetTilemapSize(options, state); } + DllExport DebugTilemapTileInfo __stdcall GetTilemapTileInfo(uint32_t x, uint32_t y, CpuType cpuType, GetTilemapOptions options, uint8_t* vram, BaseState& state) { return GetDebugger()->GetPpuTools(cpuType)->GetTilemapTileInfo(x, y, vram, options, state); } DllExport FrameInfo __stdcall GetSpritePreviewSize(CpuType cpuType, GetSpritePreviewOptions options, BaseState& state) { return GetDebugger()->GetPpuTools(cpuType)->GetSpritePreviewSize(options, state); } DllExport void __stdcall GetSpritePreview(CpuType cpuType, GetSpritePreviewOptions options, BaseState& state, uint8_t* vram, uint8_t *oamRam, uint32_t* palette, uint32_t *buffer) { GetDebugger()->GetPpuTools(cpuType)->GetSpritePreview(options, state, vram, oamRam, palette, buffer); } diff --git a/NewUI/Config/Debugger/TilemapViewerConfig.cs b/NewUI/Config/Debugger/TilemapViewerConfig.cs index b0c0c791..c079af61 100644 --- a/NewUI/Config/Debugger/TilemapViewerConfig.cs +++ b/NewUI/Config/Debugger/TilemapViewerConfig.cs @@ -8,23 +8,27 @@ using System.Xml; using System.Xml.Serialization; using Avalonia; using Mesen.Debugger; +using Mesen.Interop; +using ReactiveUI.Fody.Helpers; namespace Mesen.Config { - public class TilemapViewerConfig + public class TilemapViewerConfig : BaseWindowConfig { - public Size WindowSize = new Size(0, 0); - public Point WindowLocation; + [Reactive] public bool ShowSettingsPanel { get; set; } = true; - public int ImageScale = 1; - public bool ShowScrollOverlay = false; - public bool ShowTileGrid = false; + [Reactive] public int ImageScale { get; set; } = 1; - public RefreshSpeed AutoRefreshSpeed = RefreshSpeed.Low; + [Reactive] public bool ShowGrid { get; set; } + [Reactive] public bool ShowAltGrid { get; set; } + [Reactive] public bool ShowScrollOverlay { get; set; } + [Reactive] public bool HighlightTileChanges { get; set; } + [Reactive] public bool HighlightAttributeChanges { get; set; } + [Reactive] public TilemapDisplayMode DisplayMode { get; set; } = TilemapDisplayMode.Default; - public bool AutoRefresh = true; - public int RefreshScanline = 240; - public int RefreshCycle = 0; + [Reactive] public bool AutoRefresh { get; set; } = true; + [Reactive] public int RefreshScanline { get; set; } = 240; + [Reactive] public int RefreshCycle { get; set; } = 0; public TilemapViewerConfig() { diff --git a/NewUI/Debugger/Controls/DynamicTooltip.axaml b/NewUI/Debugger/Controls/DynamicTooltip.axaml index b04dc560..f098cd95 100644 --- a/NewUI/Debugger/Controls/DynamicTooltip.axaml +++ b/NewUI/Debugger/Controls/DynamicTooltip.axaml @@ -20,8 +20,17 @@ - - + + + + + + + + + + + diff --git a/NewUI/Debugger/Controls/DynamicTooltip.axaml.cs b/NewUI/Debugger/Controls/DynamicTooltip.axaml.cs index cad5f9ac..775c52bb 100644 --- a/NewUI/Debugger/Controls/DynamicTooltip.axaml.cs +++ b/NewUI/Debugger/Controls/DynamicTooltip.axaml.cs @@ -35,9 +35,9 @@ namespace Mesen.Debugger.Controls public class TooltipEntry { public string Name { get; set; } = ""; - public string Value { get; set; } = ""; + public object Value { get; set; } = ""; - public TooltipEntry(string name, string value) + public TooltipEntry(string name, object value) { Name = name; Value = value; diff --git a/NewUI/Debugger/Controls/PictureViewer.cs b/NewUI/Debugger/Controls/PictureViewer.cs index ad24acbf..586814df 100644 --- a/NewUI/Debugger/Controls/PictureViewer.cs +++ b/NewUI/Debugger/Controls/PictureViewer.cs @@ -19,7 +19,7 @@ namespace Mesen.Debugger.Controls { public class PictureViewer : Control { - public static readonly StyledProperty SourceProperty = AvaloniaProperty.Register(nameof(Source)); + public static readonly StyledProperty SourceProperty = AvaloniaProperty.Register(nameof(Source)); public static readonly StyledProperty ZoomProperty = AvaloniaProperty.Register(nameof(Zoom), 1, defaultBindingMode: BindingMode.TwoWay); public static readonly StyledProperty GridSizeXProperty = AvaloniaProperty.Register(nameof(GridSizeX), 8); public static readonly StyledProperty GridSizeYProperty = AvaloniaProperty.Register(nameof(GridSizeY), 8); @@ -30,7 +30,7 @@ namespace Mesen.Debugger.Controls public static readonly StyledProperty ShowAltGridProperty = AvaloniaProperty.Register(nameof(ShowAltGrid), false); public static readonly StyledProperty AllowSelectionProperty = AvaloniaProperty.Register(nameof(AllowSelection), true); - public static readonly StyledProperty SelectionRectProperty = AvaloniaProperty.Register(nameof(SelectionRect), Rect.Empty); + public static readonly StyledProperty SelectionRectProperty = AvaloniaProperty.Register(nameof(SelectionRect), Rect.Empty, defaultBindingMode: BindingMode.TwoWay); public static readonly RoutedEvent PositionClickedEvent = RoutedEvent.Register(nameof(PositionClicked), RoutingStrategies.Bubble); public event EventHandler PositionClicked @@ -44,7 +44,7 @@ namespace Mesen.Debugger.Controls private Stopwatch _stopWatch = Stopwatch.StartNew(); private DispatcherTimer _timer = new DispatcherTimer(); - public Bitmap Source + public IImage Source { get { return GetValue(SourceProperty); } set { SetValue(SourceProperty, value); } @@ -156,8 +156,8 @@ namespace Mesen.Debugger.Controls private void UpdateSize() { - MinWidth = Source.PixelSize.Width * Zoom; - MinHeight = Source.PixelSize.Height * Zoom; + MinWidth = (int)Source.Size.Width * Zoom; + MinHeight = (int)Source.Size.Height * Zoom; } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) @@ -184,29 +184,46 @@ namespace Mesen.Debugger.Controls protected override void OnPointerPressed(PointerPressedEventArgs e) { - base.OnPointerPressed(e); - Point p = e.GetCurrentPoint(this).Position; - p = new Point(Math.Min(p.X, MinWidth - 1) / Zoom, Math.Min(p.Y, MinHeight - 1) / Zoom); + Point p = GetGridPointFromMousePoint(e.GetCurrentPoint(this).Position); PositionClickedEventArgs args = new() { RoutedEvent = PositionClickedEvent, Position = p }; RaiseEvent(args); if(!args.Handled && AllowSelection) { - Rect selection = new Rect( - (int)p.X / GridSizeX * GridSizeX, - (int)p.Y / GridSizeY * GridSizeY, - GridSizeX, - GridSizeY - ); - SelectionRect = selection; + SelectionRect = GetTileRect(p); } } + private Point GetGridPointFromMousePoint(Point p) + { + return new Point(Math.Min(p.X, MinWidth - 1) / Zoom, Math.Min(p.Y, MinHeight - 1) / Zoom); + } + + private Rect GetTileRect(Point p) + { + return new Rect( + (int)p.X / GridSizeX * GridSizeX, + (int)p.Y / GridSizeY * GridSizeY, + GridSizeX, + GridSizeY + ); + } + + private Rect ToDrawRect(Rect r) + { + return new Rect( + r.X * Zoom - 0.5, + r.Y * Zoom - 0.5, + r.Width * Zoom + 1, + r.Height * Zoom + 1 + ); + } + private void DrawGrid(DrawingContext context, bool show, int gridX, int gridY, Color color) { if(show) { - int width = Source.PixelSize.Width * Zoom; - int height = Source.PixelSize.Height * Zoom; + int width = (int)Source.Size.Width * Zoom; + int height = (int)Source.Size.Height * Zoom; int gridSizeX = gridX * Zoom; int gridSizeY = gridY * Zoom; @@ -230,14 +247,14 @@ namespace Mesen.Debugger.Controls return; } - int width = Source.PixelSize.Width * Zoom; - int height = Source.PixelSize.Height * Zoom; + int width = (int)Source.Size.Width * Zoom; + int height = (int)Source.Size.Height * Zoom; context.FillRectangle(new SolidColorBrush(0xFFFFFFFF), new Rect(Bounds.Size)); context.DrawImage( Source, - new Rect(0, 0, Source.PixelSize.Width, Source.PixelSize.Height), + new Rect(0, 0, (int)Source.Size.Width, (int)Source.Size.Height), new Rect(0, 0, width, height), Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode.Default ); @@ -246,24 +263,19 @@ namespace Mesen.Debugger.Controls DrawGrid(context, ShowAltGrid, AltGridSizeX, AltGridSizeY, Color.FromArgb(128, Colors.Red.R, Colors.Red.G, Colors.Red.B)); if(SelectionRect != Rect.Empty) { - Rect rect = new Rect( - SelectionRect.X * Zoom - 0.5, - SelectionRect.Y * Zoom - 0.5, - SelectionRect.Width * Zoom + 1, - SelectionRect.Height * Zoom + 1 - ); - + Rect rect = ToDrawRect(SelectionRect); + DashStyle dashes = new DashStyle(DashStyle.Dash.Dashes, (double)(_stopWatch.ElapsedMilliseconds / 50) % 100 / 5); context.DrawRectangle(new Pen(0x40000000, 2), rect.Inflate(0.5)); context.DrawRectangle(new Pen(Brushes.White, 2, dashes), rect.Inflate(0.5)); } - //TODO delete? - /*if(_mousePosition.HasValue) { - Point p = new Point(Math.Floor(_mousePosition.Value.X) + 0.5, Math.Floor(_mousePosition.Value.Y) + 0.5); - context.DrawLine(new Pen(Brushes.Black), new Point(p.X, 0), new Point(p.X, Bounds.Height)); - context.DrawLine(new Pen(Brushes.Black), new Point(0, p.Y), new Point(Bounds.Width, p.Y)); - }*/ + if(_mousePosition.HasValue) { + Point p = GetGridPointFromMousePoint(_mousePosition.Value); + Rect rect = ToDrawRect(GetTileRect(p)); + context.DrawRectangle(new Pen(0x40000000, 2), rect.Inflate(0.5)); + context.DrawRectangle(new Pen(Brushes.White, 2), rect.Inflate(0.5)); + } } } diff --git a/NewUI/Debugger/ViewModels/TilemapViewerViewModel.cs b/NewUI/Debugger/ViewModels/TilemapViewerViewModel.cs index f1ca797d..9b83df99 100644 --- a/NewUI/Debugger/ViewModels/TilemapViewerViewModel.cs +++ b/NewUI/Debugger/ViewModels/TilemapViewerViewModel.cs @@ -1,6 +1,13 @@ -using Avalonia.Controls; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Mesen.Config; +using Mesen.Debugger.Controls; using Mesen.Interop; using Mesen.ViewModels; +using ReactiveUI; using ReactiveUI.Fody.Helpers; using System; using System.Collections.Generic; @@ -12,19 +19,26 @@ namespace Mesen.Debugger.ViewModels public CpuType CpuType { get; } public ConsoleType ConsoleType { get; } - [Reactive] public bool ShowGrid { get; set; } - [Reactive] public bool ShowAltGrid { get; set; } - [Reactive] public bool HighlightTileChanges { get; set; } - [Reactive] public bool HighlightAttributeChanges { get; set; } - [Reactive] public TilemapDisplayMode DisplayMode { get; set; } + public TilemapViewerConfig Config { get; } - [Reactive] public List Tabs { get; set; } = new List(); + [Reactive] public Rect SelectionRect { get; set; } + + [Reactive] public WriteableBitmap ViewerBitmap { get; private set; } + [Reactive] public BaseState? PpuState { get; set; } + [Reactive] public byte[]? PrevVram { get; set; } + + [Reactive] public DynamicTooltip? PreviewPanel { get; private set; } + + [Reactive] public List Tabs { get; private set; } = new List(); + [Reactive] public bool ShowTabs { get; private set; } + [Reactive] public TilemapViewerTab SelectedTab { get; set; } //For designer public TilemapViewerViewModel() : this(CpuType.Cpu, ConsoleType.Snes) { } public TilemapViewerViewModel(CpuType cpuType, ConsoleType consoleType) { + Config = ConfigManager.Config.Debug.TilemapViewer; CpuType = cpuType; ConsoleType = consoleType; @@ -51,6 +65,107 @@ namespace Mesen.Debugger.ViewModels }; break; } + + SelectedTab = Tabs[0]; + + InitBitmap(256, 256); + + this.WhenAnyValue(x => x.Tabs).Subscribe(x => ShowTabs = x.Count > 1); + + this.WhenAnyValue(x => x.SelectionRect).Subscribe(x => { + if(x.IsEmpty) { + PreviewPanel = null; + } else { + PreviewPanel = GetPreviewPanel(PixelPoint.FromPoint(x.TopLeft, 1)); + } + }); + } + + public void InitBitmap(int width, int height) + { + if(ViewerBitmap == null || ViewerBitmap.PixelSize.Width != width || ViewerBitmap.PixelSize.Height != height) { + ViewerBitmap = new WriteableBitmap(new PixelSize(width, height), new Vector(96, 96), PixelFormat.Bgra8888, AlphaFormat.Premul); + } + } + + private GetTilemapOptions GetOptions(byte[]? prevVram = null) + { + return new GetTilemapOptions() { + Layer = (byte)SelectedTab.Layer, + CompareVram = prevVram, + HighlightTileChanges = Config.HighlightTileChanges, + HighlightAttributeChanges = Config.HighlightAttributeChanges, + DisplayMode = Config.DisplayMode + }; + } + + public void UpdateBitmap(T ppuState, byte[] vram, byte[]? prevVram) where T : struct, BaseState + { + GetTilemapOptions options = GetOptions(prevVram); + UInt32[] palette = PaletteHelper.GetConvertedPalette(CpuType, ConsoleType); + + FrameInfo size = DebugApi.GetTilemapSize(CpuType, options, ppuState); + InitBitmap((int)size.Width, (int)size.Height); + + using(var framebuffer = ViewerBitmap.Lock()) { + DebugApi.GetTilemap(CpuType, options, ppuState, vram, palette, framebuffer.Address); + } + } + + public DynamicTooltip? GetPreviewPanel(PixelPoint p) + { + if(PpuState == null || PrevVram == null) { + return null; + } + + DebugTilemapTileInfo? result = DebugApi.GetTilemapTileInfo((uint)p.X, (uint)p.Y, CpuType, GetOptions(), PrevVram, PpuState); + if(result == null) { + return null; + } + + DebugTilemapTileInfo tileInfo = result.Value; + CroppedBitmap preview = new CroppedBitmap(ViewerBitmap, new PixelRect(p.X / tileInfo.Width * tileInfo.Width, p.Y / tileInfo.Height * tileInfo.Height, tileInfo.Width, tileInfo.Height)); + + List entries = new(); + entries.Add(new("Tile", new Border() { + BorderBrush = Brushes.Gray, + BorderThickness = new Thickness(1), + Child = new PictureViewer() { + Source = preview, + Width = tileInfo.Width * 6, + Height = tileInfo.Height * 6, + Zoom = 6 + } + })); + entries.Add(new("Column, Row", tileInfo.Column + ", " + tileInfo.Row)); + entries.Add(new("Size", tileInfo.Width + "x" + tileInfo.Height)); + + if(tileInfo.TileMapAddress >= 0) { + entries.Add(new("Tilemap address", "$" + tileInfo.TileMapAddress.ToString("X4"))); + } + if(tileInfo.TileIndex >= 0) { + entries.Add(new("Tile index", "$" + tileInfo.TileIndex.ToString("X2"))); + } + if(tileInfo.TileAddress >= 0) { + entries.Add(new("Tile address", "$" + tileInfo.TileAddress.ToString("X4"))); + } + if(tileInfo.PaletteIndex >= 0) { + entries.Add(new("Palette index", tileInfo.PaletteIndex.ToString())); + } + if(tileInfo.PaletteAddress >= 0) { + entries.Add(new("Palette address", "$" + tileInfo.PaletteAddress.ToString("X2"))); + } + if(tileInfo.HorizontalMirroring != NullableBoolean.Undefined) { + entries.Add(new("Horizontal mirror", tileInfo.HorizontalMirroring == NullableBoolean.True)); + } + if(tileInfo.VerticalMirroring != NullableBoolean.Undefined) { + entries.Add(new("Vertical mirror", tileInfo.VerticalMirroring == NullableBoolean.True)); + } + if(tileInfo.HighPriority != NullableBoolean.Undefined) { + entries.Add(new("High priority", tileInfo.HighPriority == NullableBoolean.True)); + } + + return new DynamicTooltip() { Items = entries }; } } diff --git a/NewUI/Debugger/Windows/EventViewerWindow.axaml.cs b/NewUI/Debugger/Windows/EventViewerWindow.axaml.cs index 6c07eb31..c3eef772 100644 --- a/NewUI/Debugger/Windows/EventViewerWindow.axaml.cs +++ b/NewUI/Debugger/Windows/EventViewerWindow.axaml.cs @@ -40,16 +40,25 @@ namespace Mesen.Debugger.Windows _model.Config.LoadWindowSettings(this); DataContext = _model; - viewer.PointerMoved += Viewer_PointerMoved; - if(Design.IsDesignMode) { return; } + viewer.PointerMoved += Viewer_PointerMoved; + viewer.PointerLeave += Viewer_PointerLeave; + _timer = new DispatcherTimer(TimeSpan.FromMilliseconds(100), DispatcherPriority.Normal, (s, e) => UpdateConfig()); _listener = new NotificationListener(); } + private void Viewer_PointerLeave(object? sender, PointerEventArgs e) + { + if(sender is PictureViewer viewer) { + ToolTip.SetTip(viewer, null); + ToolTip.SetIsOpen(viewer, false); + } + } + private void Viewer_PointerMoved(object? sender, PointerEventArgs e) { if(sender is PictureViewer viewer) { diff --git a/NewUI/Debugger/Windows/TilemapViewerWindow.axaml b/NewUI/Debugger/Windows/TilemapViewerWindow.axaml index 38ae0eed..cd0807eb 100644 --- a/NewUI/Debugger/Windows/TilemapViewerWindow.axaml +++ b/NewUI/Debugger/Windows/TilemapViewerWindow.axaml @@ -24,36 +24,76 @@ - + + - - - - - - - - + + + + + + + + + + + + + + + + - + + + + + + - + - + @@ -66,11 +106,12 @@ HorizontalAlignment="Stretch" VerticalAlignment="Stretch" x:Name="picViewer" - ShowGrid="{CompiledBinding ShowGrid}" - + Zoom="{CompiledBinding Config.ImageScale}" + ShowGrid="{CompiledBinding Config.ShowGrid}" + SelectionRect="{CompiledBinding SelectionRect}" AltGridSizeX="16" AltGridSizeY="16" - ShowAltGrid="{CompiledBinding ShowAltGrid}" + ShowAltGrid="{CompiledBinding Config.ShowAltGrid}" /> diff --git a/NewUI/Debugger/Windows/TilemapViewerWindow.axaml.cs b/NewUI/Debugger/Windows/TilemapViewerWindow.axaml.cs index c6cc418f..46ac592e 100644 --- a/NewUI/Debugger/Windows/TilemapViewerWindow.axaml.cs +++ b/NewUI/Debugger/Windows/TilemapViewerWindow.axaml.cs @@ -11,6 +11,8 @@ using Mesen.Debugger.ViewModels; using Avalonia.Platform; using Mesen.Interop; using System.ComponentModel; +using Avalonia.Input; +using Avalonia.Interactivity; namespace Mesen.Debugger.Windows { @@ -19,10 +21,7 @@ namespace Mesen.Debugger.Windows private NotificationListener _listener; private TilemapViewerViewModel _model; private PictureViewer _picViewer; - private WriteableBitmap _viewerBitmap; - private byte[]? _prevVram = null; - private TilemapViewerTab _selectedTab = new TilemapViewerTab(); - + public TilemapViewerWindow() { InitializeComponent(); @@ -46,24 +45,20 @@ namespace Mesen.Debugger.Windows //Renderer.DrawFps = true; _picViewer = this.FindControl("picViewer"); - _picViewer.Source = _viewerBitmap; - InitBitmap(256, 256); + _picViewer.PointerMoved += PicViewer_PointerMoved; + _picViewer.PointerLeave += PicViewer_PointerLeave; + _picViewer.Source = _model.ViewerBitmap; _listener = new NotificationListener(); _listener.OnNotification += listener_OnNotification; } - + protected override void OnClosing(CancelEventArgs e) { _listener?.Dispose(); base.OnClosing(e); } - private void InitBitmap(int width, int height) - { - _viewerBitmap = new WriteableBitmap(new PixelSize(width, height), new Vector(96, 96), PixelFormat.Bgra8888, AlphaFormat.Premul); - } - protected override void OnDataContextChanged(EventArgs e) { if(this.DataContext is TilemapViewerViewModel model) { @@ -73,41 +68,52 @@ namespace Mesen.Debugger.Windows } } - private void OnTabSelected(object? sender, SelectionChangedEventArgs e) + private void PicViewer_PointerMoved(object? sender, PointerEventArgs e) { - if(e.AddedItems.Count > 0) { - _selectedTab = (TilemapViewerTab)e.AddedItems[0]!; + if(sender is PictureViewer viewer) { + PixelPoint p = PixelPoint.FromPoint(e.GetCurrentPoint(viewer).Position / viewer.Zoom, 1); + DynamicTooltip? tooltip = _model.GetPreviewPanel(p); + + if(tooltip != null) { + ToolTip.SetTip(viewer, tooltip); + + //Force tooltip to update its position + ToolTip.SetHorizontalOffset(viewer, 4); + ToolTip.SetHorizontalOffset(viewer, 5); + ToolTip.SetIsOpen(viewer, true); + } else { + ToolTip.SetTip(viewer, null); + ToolTip.SetIsOpen(viewer, false); + } } } + private void PicViewer_PointerLeave(object? sender, PointerEventArgs e) + { + if(sender is PictureViewer viewer) { + ToolTip.SetTip(viewer, null); + ToolTip.SetIsOpen(viewer, false); + } + } + + private void OnSettingsClick(object sender, RoutedEventArgs e) + { + _model.Config.ShowSettingsPanel = !_model.Config.ShowSettingsPanel; + } + private void UpdateTilemap() where T : struct, BaseState { T ppuState = DebugApi.GetPpuState(_model.CpuType); byte[] vram = DebugApi.GetMemoryState(_model.CpuType.GetVramMemoryType()); - byte[]? prevVram = _prevVram; - _prevVram = vram; + byte[]? prevVram = _model.PrevVram; + _model.PpuState = ppuState; + _model.PrevVram = vram; UInt32[] palette = PaletteHelper.GetConvertedPalette(_model.CpuType, _model.ConsoleType); Dispatcher.UIThread.Post(() => { - GetTilemapOptions options = new GetTilemapOptions() { - Layer = (byte)_selectedTab.Layer, - CompareVram = prevVram, - HighlightTileChanges = _model.HighlightTileChanges, - HighlightAttributeChanges = _model.HighlightAttributeChanges, - DisplayMode = _model.DisplayMode - }; - UInt32[] palette = PaletteHelper.GetConvertedPalette(_model.CpuType, _model.ConsoleType); + _model.UpdateBitmap(ppuState, vram, prevVram); - FrameInfo size = DebugApi.GetTilemapSize(_model.CpuType, options, ppuState); - if(_viewerBitmap.PixelSize.Width != size.Width || _viewerBitmap.PixelSize.Height != size.Height) { - InitBitmap((int)size.Width, (int)size.Height); - } - - using(var framebuffer = _viewerBitmap.Lock()) { - DebugApi.GetTilemap(_model.CpuType, options, ppuState, vram, palette, framebuffer.Address); - } - - _picViewer.Source = _viewerBitmap; + _picViewer.Source = _model.ViewerBitmap; _picViewer.InvalidateVisual(); }); } diff --git a/NewUI/Interop/DebugApi.cs b/NewUI/Interop/DebugApi.cs index a69f0312..12562936 100644 --- a/NewUI/Interop/DebugApi.cs +++ b/NewUI/Interop/DebugApi.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Linq; @@ -155,6 +156,17 @@ namespace Mesen.Interop return DebugApi.GetTilemapSize(cpuType, options.ToInterop(), (IntPtr)ptr); } + [DllImport(DllPath)] private static extern DebugTilemapTileInfo GetTilemapTileInfo(UInt32 x, UInt32 y, CpuType cpuType, InteropGetTilemapOptions options, byte[] vram, IntPtr state); + public unsafe static DebugTilemapTileInfo? GetTilemapTileInfo(UInt32 x, UInt32 y, CpuType cpuType, GetTilemapOptions options, byte[] vram, BaseState state) + { + Debug.Assert(state.GetType().IsValueType); + + byte* ptr = stackalloc byte[Marshal.SizeOf(state.GetType())]; + Marshal.StructureToPtr(state, (IntPtr)ptr, false); + DebugTilemapTileInfo info = DebugApi.GetTilemapTileInfo(x, y, cpuType, options.ToInterop(), vram, (IntPtr)ptr); + return info.Row >= 0 ? info : null; + } + [DllImport(DllPath)] public static extern void GetTileView(CpuType cpuType, GetTileViewOptions options, byte[] source, int srcSize, UInt32[] palette, IntPtr buffer); [DllImport(DllPath)] private static extern void GetSpritePreview(CpuType cpuType, GetSpritePreviewOptions options, IntPtr state, byte[] vram, byte[] spriteRam, UInt32[] palette, IntPtr buffer); @@ -748,6 +760,34 @@ namespace Mesen.Interop Magenta = 4 } + public enum NullableBoolean + { + Undefined = -1, + False = 0, + True = 1 + } + + public struct DebugTilemapTileInfo + { + public Int32 Row; + public Int32 Column; + + public Int32 Width; + public Int32 Height; + + public Int32 TileMapAddress; + + public Int32 TileIndex; + public Int32 TileAddress; + + public Int32 PaletteIndex; + public Int32 PaletteAddress; + + public NullableBoolean HorizontalMirroring; + public NullableBoolean VerticalMirroring; + public NullableBoolean HighPriority; + }; + public struct GetTileViewOptions { public TileFormat Format; diff --git a/NewUI/Windows/MainWindow.axaml.cs b/NewUI/Windows/MainWindow.axaml.cs index 8f868b6f..4837429d 100644 --- a/NewUI/Windows/MainWindow.axaml.cs +++ b/NewUI/Windows/MainWindow.axaml.cs @@ -57,6 +57,7 @@ namespace Mesen.Windows protected override void OnClosed(EventArgs e) { base.OnClosed(e); + ConfigManager.SaveConfig(); EmuApi.Release(); }