Mesen2/UI/Debugger/ViewModels/TilemapViewerViewModel.cs

813 lines
27 KiB
C#

using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Threading;
using Mesen.Config;
using Mesen.Debugger.Controls;
using Mesen.Debugger.Utilities;
using Mesen.Debugger.Windows;
using Mesen.Interop;
using Mesen.Utilities;
using Mesen.ViewModels;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
namespace Mesen.Debugger.ViewModels
{
public class TilemapViewerViewModel : DisposableViewModel, ICpuTypeModel, IMouseOverViewerModel
{
[Reactive] public CpuType CpuType { get; set; }
[Reactive] public bool IsNes { get; private set; }
public TilemapViewerConfig Config { get; }
public RefreshTimingViewModel RefreshTiming { get; }
[Reactive] public Rect SelectionRect { get; set; }
[Reactive] public int GridSizeX { get; set; } = 8;
[Reactive] public int GridSizeY { get; set; } = 8;
[Reactive] public List<GridDefinition>? CustomGrids { get; set; } = null;
[Reactive] public DynamicBitmap ViewerBitmap { get; private set; }
[Reactive] public DynamicTooltip TilemapInfoPanel { get; private set; } = new DynamicTooltip();
[Reactive] public bool IsTilemapInfoVisible { get; private set; }
[Reactive] public DynamicTooltip? PreviewPanel { get; private set; }
[Reactive] public DynamicTooltip? ViewerTooltip { get; set; }
[Reactive] public PixelPoint? ViewerMousePos { get; 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; }
[Reactive] public Rect ScrollOverlayRect { get; private set; }
[Reactive] public List<PictureViewerLine>? OverlayLines { get; private set; } = null;
[Reactive] public Enum[] AvailableDisplayModes { get; set; } = Array.Empty<Enum>();
public List<object> FileMenuActions { get; } = new();
public List<object> ViewMenuActions { get; } = new();
private object _updateLock = new();
private TilemapViewerData _data = new();
private TilemapViewerData _coreData = new();
private PictureViewer _picViewer;
private bool _refreshDataOnTabChange;
private bool _inGameLoaded;
[Obsolete("For designer only")]
public TilemapViewerViewModel() : this(CpuType.Snes, new PictureViewer(), null) { }
public TilemapViewerViewModel(CpuType cpuType, PictureViewer picViewer, Window? wnd)
{
Config = ConfigManager.Config.Debug.TilemapViewer.Clone();
CpuType = cpuType;
RefreshTiming = new RefreshTimingViewModel(Config.RefreshTiming, cpuType);
_picViewer = picViewer;
InitForCpuType();
SelectedTab = Tabs[0];
InitBitmap(256, 256);
FileMenuActions = AddDisposables(new List<object>() {
new ContextMenuAction() {
ActionType = ActionType.ExportToPng,
Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.SaveAsPng),
OnClick = () => _picViewer.ExportToPng()
},
new ContextMenuSeparator(),
new ContextMenuAction() {
ActionType = ActionType.Exit,
OnClick = () => wnd?.Close()
}
});
ViewMenuActions = AddDisposables(new List<object>() {
new ContextMenuAction() {
ActionType = ActionType.Refresh,
Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.Refresh),
OnClick = () => RefreshData()
},
new ContextMenuSeparator(),
new ContextMenuAction() {
ActionType = ActionType.EnableAutoRefresh,
IsSelected = () => Config.RefreshTiming.AutoRefresh,
OnClick = () => Config.RefreshTiming.AutoRefresh = !Config.RefreshTiming.AutoRefresh
},
new ContextMenuAction() {
ActionType = ActionType.RefreshOnBreakPause,
IsSelected = () => Config.RefreshTiming.RefreshOnBreakPause,
OnClick = () => Config.RefreshTiming.RefreshOnBreakPause = !Config.RefreshTiming.RefreshOnBreakPause
},
new ContextMenuSeparator(),
new ContextMenuAction() {
ActionType = ActionType.ShowSettingsPanel,
Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.ToggleSettingsPanel),
IsSelected = () => Config.ShowSettingsPanel,
OnClick = () => Config.ShowSettingsPanel = !Config.ShowSettingsPanel
},
new ContextMenuSeparator(),
new ContextMenuAction() {
ActionType = ActionType.ZoomIn,
Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.ZoomIn),
OnClick = () => _picViewer.ZoomIn()
},
new ContextMenuAction() {
ActionType = ActionType.ZoomOut,
Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.ZoomOut),
OnClick = () => _picViewer.ZoomOut()
},
});
if(Design.IsDesignMode || wnd == null) {
return;
}
DebugShortcutManager.CreateContextMenu(picViewer, new List<object>() {
new ContextMenuAction() {
ActionType = ActionType.ViewInMemoryViewer,
HintText = () => {
DebugTilemapTileInfo? tile = GetSelectedTileInfo();
return tile?.TileMapAddress > 0 ? $"${tile?.TileMapAddress:X4}" : "";
},
Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.TilemapViewer_ViewInMemoryViewer),
OnClick = () => {
DebugTilemapTileInfo? tile = GetSelectedTileInfo();
if(tile != null && tile.Value.TileMapAddress >= 0) {
MemoryToolsWindow.ShowInMemoryTools(GetVramMemoryType(), tile.Value.TileMapAddress);
}
}
},
new ContextMenuAction() {
ActionType = ActionType.ViewInTileViewer,
Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.TilemapViewer_ViewInTileViewer),
OnClick = () => {
DebugTilemapTileInfo? tile = GetSelectedTileInfo();
if(tile != null && tile.Value.TileAddress >= 0) {
TileViewerWindow.OpenAtTile(CpuType, GetVramMemoryType(), tile.Value.TileAddress, _data.TilemapInfo.Format, TileLayout.Normal, tile.Value.PaletteIndex);
}
}
},
new ContextMenuSeparator(),
new ContextMenuAction() {
ActionType = ActionType.EditTile,
Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.TilemapViewer_EditTile),
OnClick = () => EditTileGrid(1, 1, wnd)
},
new ContextMenuAction() {
ActionType = ActionType.EditTiles,
SubActions = new() {
new ContextMenuAction() {
ActionType = ActionType.Custom,
CustomText = $"1x2 ({GridSizeX}px x {GridSizeY*2}px)",
OnClick = () => EditTileGrid(1, 2, wnd)
},
new ContextMenuAction() {
ActionType = ActionType.Custom,
CustomText = $"2x1 ({GridSizeX*2}px x {GridSizeY}px)",
OnClick = () => EditTileGrid(2, 1, wnd)
},
new ContextMenuAction() {
ActionType = ActionType.Custom,
CustomText = $"2x2 ({GridSizeX*2}px x {GridSizeY*2}px)",
OnClick = () => EditTileGrid(2, 2, wnd)
},
new ContextMenuAction() {
ActionType = ActionType.Custom,
CustomText = $"4x4 ({GridSizeX*4}px x {GridSizeY*4}px)",
OnClick = () => EditTileGrid(4, 4, wnd)
}
}
},
new ContextMenuSeparator(),
new ContextMenuAction() {
ActionType = ActionType.EditTilemapBreakpoint,
HintText = () => {
DebugTilemapTileInfo? tile = GetSelectedTileInfo();
return tile?.TileMapAddress > 0 ? $"${tile?.TileMapAddress:X4}" : "";
},
Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.TilemapViewer_EditTilemapBreakpoint),
OnClick = () => {
DebugTilemapTileInfo? tile = GetSelectedTileInfo();
if(tile != null && tile.Value.TileMapAddress >= 0) {
EditBreakpoint(wnd, tile.Value.TileMapAddress);
}
}
},
new ContextMenuAction() {
ActionType = ActionType.EditAttributeBreakpoint,
HintText = () => {
DebugTilemapTileInfo? tile = GetSelectedTileInfo();
return tile?.AttributeAddress > 0 ? $"${tile?.AttributeAddress:X4}" : "";
},
IsVisible = () => IsNes,
Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.TilemapViewer_EditAttributeBreakpoint),
OnClick = () => {
DebugTilemapTileInfo? tile = GetSelectedTileInfo();
if(tile != null && tile.Value.AttributeAddress >= 0) {
EditBreakpoint(wnd, tile.Value.AttributeAddress);
}
}
},
new ContextMenuSeparator() { IsVisible = () => CpuType == CpuType.Nes },
new ContextMenuAction() {
ActionType = ActionType.CopyToHdPackFormat,
IsVisible = () => CpuType == CpuType.Nes,
IsEnabled = () => HdPackCopyHelper.IsActionAllowed(GetVramMemoryType()),
OnClick = () => {
DebugTilemapTileInfo? tile = GetSelectedTileInfo();
if(tile != null && tile?.TileAddress >= 0) {
HdPackCopyHelper.CopyToHdPackFormat(tile.Value.TileAddress, GetVramMemoryType(), _data.RawPalette, tile.Value.PaletteIndex, false);
}
}
}
});
AddDisposable(this.WhenAnyValue(x => x.Tabs).Subscribe(x => ShowTabs = x.Count > 1));
AddDisposable(this.WhenAnyValue(x => x.SelectedTab).Subscribe(x => {
if(_inGameLoaded) {
//Skip refresh data/tab if this is triggered while processing a gameloaded event
//Otherwise RefreshTab will be called on the old game's data, causing a crash.
return;
}
if(_refreshDataOnTabChange) {
RefreshData();
} else {
RefreshTab();
}
}));
AddDisposable(this.WhenAnyValue(x => x.SelectionRect).Subscribe(x => UpdatePreviewPanel()));
AddDisposable(ReactiveHelper.RegisterRecursiveObserver(Config, Config_PropertyChanged));
InitNesGridOptions();
DebugShortcutManager.RegisterActions(wnd, FileMenuActions);
DebugShortcutManager.RegisterActions(wnd, ViewMenuActions);
}
private void InitNesGridOptions()
{
AddDisposable(this.WhenAnyValue(x => x.Config.NesShowAttributeGrid, x => x.Config.NesShowAttributeByteGrid, x => x.Config.NesShowTilemapGrid, x => x.CpuType).Subscribe(x => {
if(CpuType == CpuType.Nes) {
List<GridDefinition> grids = new();
if(Config.NesShowAttributeGrid) {
grids.Add(new() { SizeX = 16, SizeY = 16, Color = Colors.Red });
}
if(Config.NesShowAttributeByteGrid) {
grids.Add(new() { SizeX = 32, SizeY = 32, Color = Colors.LightGreen, RestartY = 240 });
}
if(Config.NesShowTilemapGrid) {
grids.Add(new() { SizeX = 256, SizeY = 240, Color = Colors.LightGray });
}
CustomGrids = grids;
} else {
CustomGrids = null;
}
}));
}
private async void EditBreakpoint(Window wnd, int address)
{
MemoryType memType = GetVramMemoryType();
AddressInfo addr = new AddressInfo() { Address = address, Type = memType };
if(memType.IsRelativeMemory()) {
AddressInfo absAddr = DebugApi.GetAbsoluteAddress(addr);
if(absAddr.Address >= 0) {
addr = absAddr;
}
}
if(addr.Address >= 0) {
Breakpoint? bp = BreakpointManager.GetMatchingBreakpoint(addr, CpuType);
if(bp == null) {
bp = new Breakpoint() {
BreakOnWrite = true,
BreakOnRead = true,
CpuType = CpuType,
StartAddress = (uint)addr.Address,
EndAddress = (uint)addr.Address,
MemoryType = addr.Type
};
}
bool result = await BreakpointEditWindow.EditBreakpointAsync(bp, wnd);
if(result && DebugWindowManager.GetDebugWindow<DebuggerWindow>(x => x.CpuType == CpuType) == null) {
DebuggerWindow.GetOrOpenWindow(CpuType);
}
}
}
private void InitForCpuType()
{
IsNes = CpuType == CpuType.Nes;
if(IsNes) {
AvailableDisplayModes = new Enum[] { TilemapDisplayMode.Default, TilemapDisplayMode.Grayscale, TilemapDisplayMode.AttributeView };
} else {
AvailableDisplayModes = new Enum[] { TilemapDisplayMode.Default, TilemapDisplayMode.Grayscale };
}
_refreshDataOnTabChange = false;
switch(CpuType) {
case CpuType.Snes:
Tabs = new List<TilemapViewerTab>() {
new() { Title = "Layer 1", Layer = 0 },
new() { Title = "Layer 2", Layer = 1 },
new() { Title = "Layer 3", Layer = 2 },
new() { Title = "Layer 4", Layer = 3 },
new() { Title = "Main", Layer = 4 },
new() { Title = "Sub", Layer = 5 },
};
break;
case CpuType.Nes:
FrameInfo size = DebugApi.GetTilemapSize(CpuType.Nes, new GetTilemapOptions() { Layer = 1 }, new NesPpuState());
if(size.Width != 0 && size.Height != 0) {
Tabs = new List<TilemapViewerTab>() {
new() { Title = "Nametables", Layer = 0 },
new() { Title = "Window", Layer = 1 }
};
} else {
Tabs = new List<TilemapViewerTab>() {
new() { Title = "", Layer = 0 }
};
}
break;
case CpuType.Sms:
Tabs = new List<TilemapViewerTab>() {
new() { Title = "", Layer = 0 }
};
break;
case CpuType.Pce:
if(DebugApi.GetConsoleState<PceState>(ConsoleType.PcEngine).IsSuperGrafx) {
_refreshDataOnTabChange = true;
Tabs = new List<TilemapViewerTab>() {
new() { Title = "VDC1", Layer = 0 },
new() { Title = "VDC2", Layer = 1, VramMemoryType = MemoryType.PceVideoRamVdc2 }
};
} else {
Tabs = new List<TilemapViewerTab>() {
new() { Title = "", Layer = 0 }
};
}
break;
case CpuType.Gameboy:
Tabs = new List<TilemapViewerTab>() {
new() { Title = "$9800", Layer = 0 },
new() { Title = "$9C00", Layer = 1 }
};
break;
case CpuType.Gba: {
Tabs = new() {
new() { Title = "BG0", Layer = 0 },
new() { Title = "BG1", Layer = 1 },
new() { Title = "BG2", Layer = 2 },
new() { Title = "BG3", Layer = 3 },
new() { Title = "Mem Access", Layer = 4 }
};
break;
}
case CpuType.Ws:
Tabs = new() {
new() { Title = "BG0", Layer = 0 },
new() { Title = "BG1", Layer = 1 }
};
break;
default:
throw new Exception("unsupported cpu type");
}
}
private DebugTilemapTileInfo? GetSelectedTileInfo()
{
if(_data.PpuState == null || _data.PpuToolsState == null || _data.Vram == null) {
return null;
} else {
PixelPoint p;
if(ViewerMousePos.HasValue) {
p = ViewerMousePos.Value;
} else {
if(SelectionRect == default) {
return null;
}
p = PixelPoint.FromPoint(SelectionRect.TopLeft, 1);
}
return DebugApi.GetTilemapTileInfo((uint)p.X, (uint)p.Y, CpuType, GetOptions(SelectedTab), _data.Vram, _data.PpuState, _data.PpuToolsState);
}
}
private void UpdatePreviewPanel()
{
if(SelectionRect == default) {
PreviewPanel = null;
} else {
PreviewPanel = GetPreviewPanel(PixelPoint.FromPoint(SelectionRect.TopLeft, 1), PreviewPanel);
}
if(ViewerTooltip != null && ViewerMousePos != null) {
GetPreviewPanel(ViewerMousePos.Value, ViewerTooltip);
}
}
private void Config_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
RefreshTab();
}
[MemberNotNull(nameof(ViewerBitmap))]
private void InitBitmap(int width, int height)
{
if(ViewerBitmap == null || ViewerBitmap.PixelSize.Width != width || ViewerBitmap.PixelSize.Height != height) {
ViewerBitmap = new DynamicBitmap(new PixelSize(width, height), new Vector(96, 96), PixelFormat.Bgra8888, AlphaFormat.Premul);
}
}
private GetTilemapOptions GetOptions(TilemapViewerTab tab, byte[]? prevVram = null, AddressCounters[]? accessCounters = null)
{
return new GetTilemapOptions() {
Layer = (byte)tab.Layer,
CompareVram = prevVram?.Length > 0 ? prevVram : null,
AccessCounters = accessCounters,
TileHighlightMode = Config.TileHighlightMode,
AttributeHighlightMode = Config.AttributeHighlightMode,
DisplayMode = Config.DisplayMode
};
}
private MemoryType GetVramMemoryType()
{
return SelectedTab?.VramMemoryType ?? CpuType.GetVramMemoryType();
}
public void RefreshData()
{
lock(_updateLock) {
_coreData.MasterClock = EmuApi.GetTimingInfo(CpuType).MasterClock;
BaseState ppuState = DebugApi.GetPpuState(CpuType);
_coreData.PpuState = ppuState;
_coreData.PpuToolsState = DebugApi.GetPpuToolsState(CpuType);
_coreData.PrevVram = _coreData.Vram;
DebugApi.GetMemoryState(GetVramMemoryType(), ref _coreData.Vram);
DebugApi.GetMemoryAccessCounts(GetVramMemoryType(), ref _coreData.AccessCounters);
DebugPaletteInfo palette = DebugApi.GetPaletteInfo(CpuType);
_coreData.RgbPalette = palette.GetRgbPalette();
_coreData.RawPalette = palette.GetRawPalette();
_coreData.RawFormat = palette.RawFormat;
}
RefreshTab();
}
private void RefreshTab()
{
Dispatcher.UIThread.Post(() => {
lock(_updateLock) {
_coreData.CopyTo(_data);
}
if(_data.PpuState == null || _data.PpuToolsState == null) {
return;
}
GetTilemapOptions options;
FrameInfo size;
foreach(TilemapViewerTab tab in Tabs) {
options = GetOptions(tab);
size = DebugApi.GetTilemapSize(CpuType, options, _data.PpuState);
tab.Enabled = size.Width != 0 && size.Height != 0;
}
if(!SelectedTab.Enabled) {
foreach(TilemapViewerTab tab in Tabs) {
if(tab.Enabled) {
SelectedTab = tab;
break;
}
}
}
options = GetOptions(SelectedTab, _data.PrevVram, _data.AccessCounters);
options.MasterClock = Interlocked.Read(ref _data.MasterClock);
size = DebugApi.GetTilemapSize(CpuType, options, _data.PpuState);
InitBitmap((int)size.Width, (int)size.Height);
using(var framebuffer = ViewerBitmap.Lock()) {
_data.TilemapInfo = DebugApi.GetTilemap(CpuType, options, _data.PpuState, _data.PpuToolsState, _data.Vram, _data.RgbPalette, framebuffer.FrameBuffer.Address);
}
if(_data.TilemapInfo.Bpp == 0) {
GridSizeX = 8;
GridSizeY = 8;
ScrollOverlayRect = default;
OverlayLines = null;
PreviewPanel = null;
IsTilemapInfoVisible = false;
return;
}
IsTilemapInfoVisible = true;
GridSizeX = (int)_data.TilemapInfo.TileWidth;
GridSizeY = (int)_data.TilemapInfo.TileHeight;
UpdatePreviewPanel();
UpdateTilemapInfo();
if(Config.ShowScrollOverlay) {
ScrollOverlayRect = new Rect(
_data.TilemapInfo.ScrollX % size.Width,
_data.TilemapInfo.ScrollY % size.Height,
_data.TilemapInfo.ScrollWidth,
_data.TilemapInfo.ScrollHeight
);
DrawMode7Overlay();
} else {
ScrollOverlayRect = default;
OverlayLines = null;
}
});
}
private void UpdateTilemapInfo()
{
TooltipEntries entries = TilemapInfoPanel.Items ?? new TooltipEntries();
DebugTilemapInfo info = _data.TilemapInfo;
entries.StartUpdate();
entries.AddEntry("Size", info.ColumnCount + "x" + info.RowCount);
entries.AddEntry("Size (px)", info.ColumnCount* info.TileWidth + "x" + info.RowCount* info.TileHeight);
entries.AddEntry("Tilemap Address", FormatAddress((int)info.TilemapAddress));
entries.AddEntry("Tileset Address", FormatAddress((int)info.TilesetAddress));
entries.AddEntry("Tile Format", info.Format);
if(info.Mirroring != TilemapMirroring.None) {
entries.AddEntry("Mirroring", info.Mirroring);
}
if(info.Priority >= 0) {
entries.AddEntry("Priority", info.Priority);
}
entries.EndUpdate();
TilemapInfoPanel.Items = entries;
}
public DynamicTooltip? GetPreviewPanel(PixelPoint p, DynamicTooltip? tooltipToUpdate)
{
if(_data.PpuState == null || _data.PpuToolsState == null) {
return null;
}
DebugTilemapTileInfo? result = DebugApi.GetTilemapTileInfo((uint)p.X, (uint)p.Y, CpuType, GetOptions(SelectedTab), _data.Vram, _data.PpuState, _data.PpuToolsState);
if(result == null) {
return null;
}
DebugTilemapTileInfo tileInfo = result.Value;
TooltipEntries entries = tooltipToUpdate?.Items ?? new();
PixelRect cropRect = new PixelRect(p.X / tileInfo.Width * tileInfo.Width, p.Y / tileInfo.Height * tileInfo.Height, tileInfo.Width, tileInfo.Height);
entries.StartUpdate();
if(tileInfo.Width == 1 && tileInfo.Height == 1) {
entries.AddPicture("Tile", ViewerBitmap, 32, cropRect);
} else {
entries.AddPicture("Tile", ViewerBitmap, 6, cropRect);
}
if(_data.TilemapInfo.Bpp >= 2 && _data.TilemapInfo.Bpp <= 4) {
int paletteSize = (int)Math.Pow(2, _data.TilemapInfo.Bpp);
int paletteIndex = tileInfo.PaletteIndex >= 0 ? tileInfo.PaletteIndex : 0;
entries.AddEntry("Palette", new TooltipPaletteEntry(paletteIndex, paletteSize, _data.RgbPalette, _data.RawPalette, _data.RawFormat));
}
if(tileInfo.Width != 1 || tileInfo.Height != 1) {
entries.AddEntry("Column, Row", $"{tileInfo.Column}, {tileInfo.Row}");
}
entries.AddEntry("X, Y", $"{tileInfo.Column*tileInfo.Width}, {tileInfo.Row*tileInfo.Height}");
entries.AddEntry("Size", tileInfo.Width + "x" + tileInfo.Height);
if(tileInfo.TileMapAddress >= 0) {
entries.AddEntry("Tilemap address", FormatAddress(tileInfo.TileMapAddress));
}
entries.AddSeparator("TileSeparator");
if(tileInfo.TileIndex >= 0) {
entries.AddEntry("Tile index", "$" + tileInfo.TileIndex.ToString("X2"));
}
if(tileInfo.TileAddress >= 0) {
MemoryType memType = GetVramMemoryType();
if(memType.IsRelativeMemory()) {
entries.AddEntry("Tile address (" + memType.GetShortName() + ")", "$" + tileInfo.TileAddress.ToString("X4"));
AddressInfo absAddress = DebugApi.GetAbsoluteAddress(new AddressInfo() { Address = tileInfo.TileAddress, Type = memType });
if(absAddress.Address >= 0) {
entries.AddEntry("Tile address (" + absAddress.Type.GetShortName() + ")", "$" + absAddress.Address.ToString("X4"));
}
} else {
entries.AddEntry("Tile address", FormatAddress(tileInfo.TileAddress));
}
}
entries.AddSeparator("PaletteSeparator");
if(tileInfo.PaletteIndex >= 0) {
if(tileInfo.BasePaletteIndex >= 0) {
entries.AddEntry("Palette index", $"{tileInfo.BasePaletteIndex} ({tileInfo.PaletteIndex})");
} else {
entries.AddEntry("Palette index", tileInfo.PaletteIndex.ToString());
}
}
if(tileInfo.PaletteAddress >= 0) {
entries.AddEntry("Palette address", "$" + tileInfo.PaletteAddress.ToString("X2"));
}
entries.AddSeparator("AttributeSeparator");
if(tileInfo.AttributeAddress >= 0) {
entries.AddEntry("Attribute address", "$" + tileInfo.AttributeAddress.ToString("X4"));
}
if(tileInfo.AttributeData >= 0) {
entries.AddEntry("Attribute data", "$" + tileInfo.AttributeData.ToString("X2"));
}
entries.AddSeparator("MiscSeparator");
if(tileInfo.PixelData >= 0) {
entries.AddEntry("Pixel data", "$" + tileInfo.PixelData.ToString("X2"));
}
entries.AddEntry("Horizontal mirror", tileInfo.HorizontalMirroring);
entries.AddEntry("Vertical mirror", tileInfo.VerticalMirroring);
entries.AddEntry("High priority", tileInfo.HighPriority);
entries.EndUpdate();
if(tooltipToUpdate != null) {
return tooltipToUpdate;
} else {
return new DynamicTooltip() { Items = entries };
}
}
private string FormatAddress(int address)
{
if(GetVramMemoryType().IsWordAddressing()) {
return $"${address / 2:X4}.w";
} else {
return $"${address:X4}";
}
}
private void EditTileGrid(int columnCount, int rowCount, Window wnd)
{
if(_data.PpuState == null || _data.PpuToolsState == null) {
return;
}
PixelPoint p = ViewerMousePos ?? PixelPoint.FromPoint(SelectionRect.TopLeft, 1);
List<AddressInfo> addresses = new();
MemoryType memType = GetVramMemoryType();
int palette = -1;
for(int row = 0; row < rowCount; row++) {
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(palette == -1) {
palette = tile.Value.PaletteIndex;
}
addresses.Add(new AddressInfo() { Address = tile.Value.TileAddress, Type = memType });
}
}
palette = Math.Max(0, palette);
TileEditorWindow.OpenAtTile(addresses, columnCount, _data.TilemapInfo.Format, palette, wnd);
}
private void DrawMode7Overlay()
{
if(_data.PpuToolsState is SnesPpuToolsState toolsState && _data.PpuState is SnesPpuState ppuState && ppuState.BgMode == 7) {
List<PictureViewerLine> lines = new();
Point prevStart = new();
Point prevEnd = new();
void AddLine(Point start, Point end, Color color) {
if(start != prevStart && end != prevEnd) {
lines.Add(new PictureViewerLine() { Start = start, End = end, Width = 1.5, Color = color });
prevStart = start;
prevEnd = end;
}
}
Mesen.Utilities.HslColor baseColor = ColorHelper.RgbToHsl(Color.FromRgb(255, 0, 255));
for(int i = 0; i < 239; i++) {
if(toolsState.ScanlineBgMode[i] == 7) {
Color lineColor = ColorHelper.HslToRgb(baseColor);
Color alphaColor = Color.FromArgb(0xA0, lineColor.R, lineColor.G, lineColor.B);
int startX = toolsState.Mode7StartX[i] >> 8;
int startY = toolsState.Mode7StartY[i] >> 8;
int endX = toolsState.Mode7EndX[i] >> 8;
int endY = toolsState.Mode7EndY[i] >> 8;
AddLine(new Point(startX, startY), new Point(endX, endY), alphaColor);
if(!ppuState.Mode7.LargeMap) {
void Translate(ref int start, ref int end, int offset, Func<int, bool> predicate) {
while(predicate(start) || predicate(end)) {
start += offset;
end += offset;
AddLine(new Point(startX, startY), new Point(endX, endY), alphaColor);
}
}
Translate(ref startX, ref endX, 1024, x => x < 0);
Translate(ref startY, ref endY, 1024, x => x < 0);
Translate(ref startX, ref endX, -1024, x => x >= 1024);
Translate(ref startY, ref endY, -1024, x => x >= 1024);
}
}
baseColor.H = (baseColor.H + 1) % 360;
}
OverlayLines = lines;
} else {
OverlayLines = null;
}
}
public void OnGameLoaded()
{
Dispatcher.UIThread.Post(() => {
_inGameLoaded = true;
InitForCpuType();
RefreshData();
_inGameLoaded = false;
});
}
}
public class TilemapViewerTab : ViewModelBase
{
[Reactive] public string Title { get; set; } = "";
[Reactive] public int Layer { get; set; } = 0;
[Reactive] public MemoryType? VramMemoryType { get; set; }
[Reactive] public bool Enabled { get; set; } = true;
}
public class TilemapViewerData
{
public DebugTilemapInfo TilemapInfo;
public UInt64 MasterClock;
public BaseState? PpuState;
public BaseState? PpuToolsState;
public byte[] PrevVram = Array.Empty<byte>();
public byte[] Vram = Array.Empty<byte>();
public UInt32[] RgbPalette = Array.Empty<UInt32>();
public UInt32[] RawPalette = Array.Empty<UInt32>();
public RawPaletteFormat RawFormat;
public AddressCounters[] AccessCounters = Array.Empty<AddressCounters>();
public void CopyTo(TilemapViewerData dst)
{
dst.TilemapInfo = TilemapInfo;
dst.MasterClock = MasterClock;
dst.PpuState = PpuState;
dst.PpuToolsState = PpuToolsState;
dst.RawFormat = RawFormat;
CopyArray(PrevVram, ref dst.PrevVram);
CopyArray(Vram, ref dst.Vram);
CopyArray(RgbPalette, ref dst.RgbPalette);
CopyArray(RawPalette, ref dst.RawPalette);
CopyArray(AccessCounters, ref dst.AccessCounters);
}
private void CopyArray<T>(T[] src, ref T[] dst)
{
if(src.Length != dst.Length) {
Array.Resize(ref dst, src.Length);
}
Array.Copy(src, dst, src.Length);
}
}
}