Tilemap Viewer: Tile information + tooltip

This commit is contained in:
Sour 2021-12-27 23:07:34 -05:00
parent c074981bb6
commit f5d02d7607
18 changed files with 547 additions and 120 deletions

View file

@ -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;

View file

@ -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 };

View file

@ -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;

View file

@ -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<MMC5*>(_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;

View file

@ -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;
};

View file

@ -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 };

View file

@ -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;

View file

@ -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); }

View file

@ -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<TilemapViewerConfig>
{
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()
{

View file

@ -20,8 +20,17 @@
<ItemsPresenter.ItemTemplate>
<DataTemplate DataType="dc:TooltipEntry">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" FontWeight="Bold" MinWidth="70" />
<TextBlock Text="{Binding Value}" />
<TextBlock Text="{Binding Name}" FontWeight="Bold" MinWidth="100" Margin="0 0 10 0" />
<ContentControl Content="{Binding Value}">
<ContentControl.DataTemplates>
<DataTemplate DataType="x:String">
<TextBlock Text="{Binding}" />
</DataTemplate>
<DataTemplate DataType="x:Boolean">
<CheckBox IsChecked="{Binding}" />
</DataTemplate>
</ContentControl.DataTemplates>
</ContentControl>
</StackPanel>
</DataTemplate>
</ItemsPresenter.ItemTemplate>

View file

@ -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;

View file

@ -19,7 +19,7 @@ namespace Mesen.Debugger.Controls
{
public class PictureViewer : Control
{
public static readonly StyledProperty<Bitmap> SourceProperty = AvaloniaProperty.Register<PictureViewer, Bitmap>(nameof(Source));
public static readonly StyledProperty<IImage> SourceProperty = AvaloniaProperty.Register<PictureViewer, IImage>(nameof(Source));
public static readonly StyledProperty<int> ZoomProperty = AvaloniaProperty.Register<PictureViewer, int>(nameof(Zoom), 1, defaultBindingMode: BindingMode.TwoWay);
public static readonly StyledProperty<int> GridSizeXProperty = AvaloniaProperty.Register<PictureViewer, int>(nameof(GridSizeX), 8);
public static readonly StyledProperty<int> GridSizeYProperty = AvaloniaProperty.Register<PictureViewer, int>(nameof(GridSizeY), 8);
@ -30,7 +30,7 @@ namespace Mesen.Debugger.Controls
public static readonly StyledProperty<bool> ShowAltGridProperty = AvaloniaProperty.Register<PictureViewer, bool>(nameof(ShowAltGrid), false);
public static readonly StyledProperty<bool> AllowSelectionProperty = AvaloniaProperty.Register<PictureViewer, bool>(nameof(AllowSelection), true);
public static readonly StyledProperty<Rect> SelectionRectProperty = AvaloniaProperty.Register<PictureViewer, Rect>(nameof(SelectionRect), Rect.Empty);
public static readonly StyledProperty<Rect> SelectionRectProperty = AvaloniaProperty.Register<PictureViewer, Rect>(nameof(SelectionRect), Rect.Empty, defaultBindingMode: BindingMode.TwoWay);
public static readonly RoutedEvent<PositionClickedEventArgs> PositionClickedEvent = RoutedEvent.Register<PictureViewer, PositionClickedEventArgs>(nameof(PositionClicked), RoutingStrategies.Bubble);
public event EventHandler<PositionClickedEventArgs> 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<T>(AvaloniaPropertyChangedEventArgs<T> 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));
}
}
}

View file

@ -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<TilemapViewerTab> Tabs { get; set; } = new List<TilemapViewerTab>();
[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<TilemapViewerTab> Tabs { get; private set; } = new List<TilemapViewerTab>();
[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>(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<TooltipEntry> 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 };
}
}

View file

@ -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) {

View file

@ -24,36 +24,76 @@
</Design.DataContext>
<Window.Styles>
<Style Selector="TabControl:singleitem">
<Style Selector="TabStrip:singleitem">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="Button">
<Setter Property="MinWidth" Value="0" />
<Setter Property="Padding" Value="1" />
</Style>
<Style Selector="Button Image">
<Setter Property="Margin" Value="1 1 0 0" />
<Setter Property="Width" Value="16" />
<Setter Property="Height" Value="16" />
</Style>
</Window.Styles>
<DockPanel>
<Grid
DockPanel.Dock="Right"
ColumnDefinitions="Auto,*"
RowDefinitions="Auto,Auto,Auto,Auto,Auto"
Margin="5 0 5 0"
Width="180"
>
<TextBlock Text="{l:Translate lblDisplayMode}" />
<c:EnumComboBox Grid.Row="0" Grid.Column="1" EnumType="{x:Type i:TilemapDisplayMode}" SelectedItem="{CompiledBinding DisplayMode}" />
<CheckBox Grid.Row="1" Grid.ColumnSpan="2" Content="{l:Translate chkShowGrid}" IsChecked="{CompiledBinding ShowGrid}" />
<CheckBox Grid.Row="2" Grid.ColumnSpan="2" Content="{l:Translate chkShowAltGrid}" IsChecked="{CompiledBinding ShowAltGrid}" />
<CheckBox Grid.Row="3" Grid.ColumnSpan="2" Content="{l:Translate chkHighlightTileChanges}" IsChecked="{CompiledBinding HighlightTileChanges}" />
<CheckBox Grid.Row="4" Grid.ColumnSpan="2" Content="{l:Translate chkHighlightAttributeChanges}" IsChecked="{CompiledBinding HighlightAttributeChanges}" />
</Grid>
<Panel DockPanel.Dock="Top">
<Menu DockPanel.Dock="Top">
<MenuItem Header="File" />
<MenuItem Header="View" />
</Menu>
<Button
HorizontalAlignment="Right"
DockPanel.Dock="Right"
ToolTip.Tip="Toggle settings panel"
Click="OnSettingsClick"
>
<Image Source="/Assets/Settings.png" />
</Button>
</Panel>
<StackPanel DockPanel.Dock="Right" IsVisible="{CompiledBinding Config.ShowSettingsPanel}">
<Grid
ColumnDefinitions="Auto,*"
RowDefinitions="Auto,Auto,Auto,Auto,Auto"
Margin="5 0 5 0"
Width="180"
>
<TextBlock Text="{l:Translate lblDisplayMode}" />
<c:EnumComboBox Grid.Row="0" Grid.Column="1" EnumType="{x:Type i:TilemapDisplayMode}" SelectedItem="{CompiledBinding Config.DisplayMode}" />
<CheckBox Grid.Row="1" Grid.ColumnSpan="2" Content="{l:Translate chkShowGrid}" IsChecked="{CompiledBinding Config.ShowGrid}" />
<CheckBox Grid.Row="2" Grid.ColumnSpan="2" Content="{l:Translate chkShowAltGrid}" IsChecked="{CompiledBinding Config.ShowAltGrid}" />
<CheckBox Grid.Row="3" Grid.ColumnSpan="2" Content="{l:Translate chkHighlightTileChanges}" IsChecked="{CompiledBinding Config.HighlightTileChanges}" />
<CheckBox Grid.Row="4" Grid.ColumnSpan="2" Content="{l:Translate chkHighlightAttributeChanges}" IsChecked="{CompiledBinding Config.HighlightAttributeChanges}" />
</Grid>
<TabControl Items="{CompiledBinding Tabs}" SelectionChanged="OnTabSelected" Padding="1" DockPanel.Dock="Top">
<Border
BorderBrush="Gray"
BorderThickness="1"
Margin="3"
Padding="3"
IsVisible="{CompiledBinding PreviewPanel, Converter={x:Static ObjectConverters.IsNotNull}}"
>
<ContentControl Content="{CompiledBinding PreviewPanel}" Margin="0 0 10 0"/>
</Border>
</StackPanel>
<TabControl
Items="{CompiledBinding Tabs}"
SelectedItem="{CompiledBinding SelectedTab}"
IsVisible="{CompiledBinding ShowTabs}"
Padding="1"
DockPanel.Dock="Top"
>
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Title}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.DataTemplates>
<TabControl.ContentTemplate>
<DataTemplate></DataTemplate>
</TabControl.DataTemplates>
</TabControl.ContentTemplate>
</TabControl>
<Border BorderBrush="Gray" BorderThickness="1">
@ -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}"
/>
</ScrollViewer>
</Border>

View file

@ -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<PictureViewer>("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<T>() where T : struct, BaseState
{
T ppuState = DebugApi.GetPpuState<T>(_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();
});
}

View file

@ -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;

View file

@ -57,6 +57,7 @@ namespace Mesen.Windows
protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);
ConfigManager.SaveConfig();
EmuApi.Release();
}