Mesen2/UI/Debugger/ViewModels/SpriteViewerViewModel.cs
Sour 1975ab958c Debugger: Fixed crashes when switching between games from different consoles (in AOT build)
Caused by what looks like slight differences between how AOT treats an empty array (vs JIT builds) when it marshals the array (AOT sends a null pointer, JIT sends a pointer to an empty array)
This was causing issues in both builds (AOT crashed immediately because of the null pointer, JIT read from out-of-bounds memory)
2025-03-28 18:03:30 +09:00

719 lines
24 KiB
C#

using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Media;
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.Localization;
using Mesen.Utilities;
using Mesen.ViewModels;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace Mesen.Debugger.ViewModels
{
public class SpriteViewerViewModel : DisposableViewModel, ICpuTypeModel, IMouseOverViewerModel
{
public SpriteViewerConfig Config { get; }
public RefreshTimingViewModel RefreshTiming { get; }
public CpuType CpuType { get; set; }
[Reactive] public SpritePreviewModel? SelectedSprite { get; set; }
[Reactive] public DynamicTooltip? SelectedPreviewPanel { get; set; }
[Reactive] public DynamicTooltip? PreviewPanelTooltip { get; set; }
[Reactive] public SpritePreviewModel? PreviewPanelSprite { get; set; }
[Reactive] public DynamicTooltip? ViewerTooltip { get; set; }
[Reactive] public PixelPoint? ViewerMousePos { get; set; }
[Reactive] public DynamicBitmap ViewerBitmap { get; private set; }
[Reactive] public Rect SelectionRect { get; set; }
[Reactive] public Rect? MouseOverRect { get; set; }
[Reactive] public int TopClipSize { get; set; }
[Reactive] public int BottomClipSize { get; set; }
[Reactive] public int LeftClipSize { get; set; }
[Reactive] public int RightClipSize { get; set; }
[Reactive] public List<SpritePreviewModel> SpritePreviews { get; set; } = new();
public SpriteViewerListViewModel ListView { get; }
[Reactive] public int MaxSourceOffset { get; set; } = 0;
public List<object> FileMenuActions { get; } = new();
public List<object> ViewMenuActions { get; } = new();
private Grid _spriteGrid;
private object _updateLock = new();
private SpriteViewerData _data = new();
private SpriteViewerData _coreData = new();
private byte[] _baseVram = Array.Empty<byte>();
private byte[] _baseSpriteRam = Array.Empty<byte>();
private byte[] _extVram = Array.Empty<byte>();
private byte[] _extSpriteRam = Array.Empty<byte>();
private DebugSpriteInfo[] _spriteList = Array.Empty<DebugSpriteInfo>();
private UInt32[] _spritePreviews = Array.Empty<UInt32>();
private bool _refreshPending;
[Obsolete("For designer only")]
public SpriteViewerViewModel() : this(CpuType.Snes, new PictureViewer(), new Grid(), new Control(), null) { }
public SpriteViewerViewModel(CpuType cpuType, PictureViewer picViewer, Grid spriteGrid, Control listView, Window? wnd)
{
Config = ConfigManager.Config.Debug.SpriteViewer.Clone();
ListView = AddDisposable(new SpriteViewerListViewModel(this));
CpuType = cpuType;
RefreshTiming = new RefreshTimingViewModel(Config.RefreshTiming, cpuType);
_spriteGrid = spriteGrid;
_spriteGrid.PointerExited += SpriteGrid_PointerExited;
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 ContextMenuAction() {
ActionType = ActionType.ShowListView,
IsSelected = () => Config.ShowListView,
OnClick = () => Config.ShowListView = !Config.ShowListView
},
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;
}
AddDisposables(DebugShortcutManager.CreateContextMenu(picViewer, new List<object> {
GetEditTileAction(wnd),
GetViewInMemoryViewerAction(),
GetViewInTileViewerAction(),
GetCopyHdPackFormatActionSeparator(),
GetCopyHdPackFormatAction()
}));
AddDisposables(DebugShortcutManager.CreateContextMenu(_spriteGrid, new List<object> {
GetEditTileAction(wnd),
GetViewInMemoryViewerAction(),
GetViewInTileViewerAction(),
GetCopyHdPackFormatActionSeparator(),
GetCopyHdPackFormatAction()
}));
AddDisposables(DebugShortcutManager.CreateContextMenu(listView, new List<object> {
GetEditTileAction(wnd),
GetViewInMemoryViewerAction(),
GetViewInTileViewerAction(),
GetCopyHdPackFormatActionSeparator(),
GetCopyHdPackFormatAction()
}));
AddDisposable(this.WhenAnyValue(x => x.SelectedSprite).Subscribe(x => {
UpdateSelectionPreview();
if(x != null) {
ListView.SelectSprite(x.SpriteIndex);
}
}));
AddDisposable(this.WhenAnyValue(x => x.ViewerMousePos, x => x.PreviewPanelSprite).Subscribe(x => UpdateMouseOverRect()));
AddDisposable(this.WhenAnyValue(x => x.Config.Source, x => x.Config.SourceOffset).Subscribe(x => RefreshData()));
AddDisposable(this.WhenAnyValue(x => x.Config.ShowOffscreenRegions).Subscribe(x => RefreshTab()));
ListView.InitListViewObservers();
AddDisposable(ReactiveHelper.RegisterRecursiveObserver(Config, Config_PropertyChanged));
DebugShortcutManager.RegisterActions(wnd, FileMenuActions);
DebugShortcutManager.RegisterActions(wnd, ViewMenuActions);
}
private ContextMenuAction GetEditTileAction(Window wnd)
{
return new ContextMenuAction() {
ActionType = ActionType.EditSprite,
Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.SpriteViewer_EditSprite),
IsEnabled = () => GetSelectedSprite() != null,
OnClick = () => {
SpritePreviewModel? sprite = GetSelectedSprite();
if(sprite?.TileAddress >= 0 && _data.Palette != null) {
PixelSize size = sprite.Format.GetTileSize();
DebugPaletteInfo pal = _data.Palette.Value;
int paletteOffset = (int)(pal.SpritePaletteOffset / pal.ColorsPerPalette);
TileEditorWindow.OpenAtTile(
sprite.TileAddresses.Select(x => new AddressInfo() { Address = (int)x, Type = CpuType.GetVramMemoryType(sprite.UseExtendedVram) }).ToList(),
sprite.RealWidth / size.Width,
sprite.Format,
sprite.Palette + paletteOffset,
wnd,
CpuType,
RefreshTiming.Config.RefreshScanline,
RefreshTiming.Config.RefreshCycle
);
}
}
};
}
private ContextMenuAction GetViewInMemoryViewerAction()
{
return new ContextMenuAction() {
ActionType = ActionType.ViewInMemoryViewer,
Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.SpriteViewer_ViewInMemoryViewer),
IsEnabled = () => GetSelectedSprite() != null,
OnClick = () => {
SpritePreviewModel? sprite = GetSelectedSprite();
if(sprite?.TileAddress >= 0) {
MemoryToolsWindow.ShowInMemoryTools(CpuType.GetVramMemoryType(sprite.UseExtendedVram), sprite.TileAddress);
}
}
};
}
private ContextMenuAction GetViewInTileViewerAction()
{
return new ContextMenuAction() {
ActionType = ActionType.ViewInTileViewer,
Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.SpriteViewer_ViewInTileViewer),
OnClick = () => {
SpritePreviewModel? sprite = GetSelectedSprite();
if(sprite?.TileAddress >= 0 && _data.Palette != null) {
DebugPaletteInfo pal = _data.Palette.Value;
int paletteOffset = (int)(pal.SpritePaletteOffset / pal.ColorsPerPalette);
TileViewerWindow.OpenAtTile(CpuType, CpuType.GetVramMemoryType(sprite.UseExtendedVram), sprite.TileAddress, sprite.Format, TileLayout.Normal, sprite.Palette + paletteOffset);
}
}
};
}
private ContextMenuAction GetCopyHdPackFormatActionSeparator()
{
return new ContextMenuSeparator() { IsVisible = () => CpuType == CpuType.Nes };
}
private ContextMenuAction GetCopyHdPackFormatAction()
{
return new ContextMenuAction() {
ActionType = ActionType.CopyToHdPackFormat,
IsVisible = () => CpuType == CpuType.Nes,
IsEnabled = () => {
SpritePreviewModel? sprite = GetSelectedSprite();
return sprite != null && HdPackCopyHelper.IsActionAllowed(CpuType.GetVramMemoryType(sprite.UseExtendedVram));
},
OnClick = () => {
SpritePreviewModel? sprite = GetSelectedSprite();
if(sprite != null && sprite.TileAddress >= 0 && _data.Palette != null) {
HdPackCopyHelper.CopyToHdPackFormat(sprite.TileAddress, CpuType.GetVramMemoryType(sprite.UseExtendedVram), _data.Palette.Value.GetRawPalette(), sprite.Palette, true, sprite.Height > 8);
}
}
};
}
private SpritePreviewModel? GetSelectedSprite()
{
return PreviewPanelSprite ?? SelectedSprite;
}
private void Config_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
RefreshTab();
}
public DynamicTooltip? GetPreviewPanel(PixelPoint p, DynamicTooltip? tooltipToUpdate)
{
SpritePreviewModel? sprite = GetMatchingSprite(p, out _);
return sprite == null ? null : GetPreviewPanel(sprite, tooltipToUpdate);
}
public DynamicTooltip? GetPreviewPanel(SpritePreviewModel sprite, DynamicTooltip? existingTooltip)
{
if(_data.Palette == null) {
return null;
}
TooltipEntries entries = existingTooltip?.Items ?? new();
entries.StartUpdate();
if(sprite.SpritePreview != null) {
entries.AddPicture("Sprite", sprite.SpritePreview, 48.0 / sprite.Width);
}
DebugPaletteInfo palette = _data.Palette.Value;
int paletteSize = (int)Math.Pow(2, sprite.Bpp);
int paletteIndex = (sprite.Palette >= 0 ? sprite.Palette : 0) + (int)(palette.SpritePaletteOffset / paletteSize);
if(sprite.Bpp > 1 && sprite.Bpp < 8) {
entries.AddEntry("Palette", new TooltipPaletteEntry(paletteIndex, paletteSize, palette.GetRgbPalette(), palette.GetRawPalette(), palette.RawFormat));
}
entries.AddEntry("Sprite index", sprite.SpriteIndex.ToString());
entries.AddEntry("X, Y",
"$" + sprite.RawX.ToString("X2") + ", $" + sprite.RawY.ToString("X2") + Environment.NewLine +
sprite.X + ", " + sprite.Y
);
entries.AddEntry("Size", sprite.Width + "x" + sprite.Height);
entries.AddSeparator("TileSeparator");
entries.AddEntry("Tile index", "$" + sprite.TileIndex.ToString("X2"));
MemoryType memType = CpuType.GetVramMemoryType(sprite.UseExtendedVram);
if(memType.IsRelativeMemory()) {
entries.AddEntry("Tile address (" + memType.GetShortName() + ")", FormatAddress(sprite.TileAddress, memType));
AddressInfo absAddress = DebugApi.GetAbsoluteAddress(new AddressInfo() { Address = sprite.TileAddress, Type = memType });
if(absAddress.Address >= 0) {
entries.AddEntry("Tile address (" + absAddress.Type.GetShortName() + ")", FormatAddress(absAddress.Address, absAddress.Type));
}
} else {
entries.AddEntry("Tile address", FormatAddress(sprite.TileAddress, memType));
}
entries.AddSeparator("PaletteSeparator");
entries.AddEntry("Palette index", sprite.Palette.ToString());
if(sprite.PaletteAddress >= 0) {
entries.AddEntry("Palette address", "$" + sprite.PaletteAddress.ToString("X2"));
}
entries.AddSeparator("MiscSeparator");
entries.AddEntry("Visibility", ResourceHelper.GetEnumText(sprite.Visibility));
entries.AddEntry("Horizontal mirror", sprite.HorizontalMirror);
entries.AddEntry("Vertical mirror", sprite.VerticalMirror);
if(sprite.TransformEnabled != NullableBoolean.Undefined) {
entries.AddEntry("Transform", sprite.TransformEnabled);
if(sprite.TransformParamIndex >= 0) {
entries.AddEntry("Transform Param Index", sprite.TransformParamIndex);
}
}
entries.AddEntry("Blending", sprite.BlendingEnabled);
entries.AddEntry("Window", sprite.WindowMode);
entries.AddEntry("Mosaic", sprite.MosaicEnabled);
entries.AddEntry("Second table", sprite.UseSecondTable);
if(sprite.Priority != DebugSpritePriority.Undefined) {
entries.AddEntry("Priority", ResourceHelper.GetEnumText(sprite.Priority));
}
entries.EndUpdate();
if(existingTooltip != null) {
return existingTooltip;
} else {
return new DynamicTooltip() { Items = entries };
}
}
private string FormatAddress(int address, MemoryType memType)
{
if(memType.IsWordAddressing()) {
return $"${address / 2:X4}.w";
} else {
return $"${address:X4}";
}
}
[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 void InitGrid(int spriteCount)
{
double dpiScale = LayoutHelper.GetLayoutScale(_spriteGrid);
if(_spriteGrid.Children.Count != spriteCount) {
_spriteGrid.Children.Clear();
_spriteGrid.ColumnDefinitions.Clear();
_spriteGrid.RowDefinitions.Clear();
for(int i = 0; i < 8; i++) {
_spriteGrid.ColumnDefinitions.Add(new() { Width = new GridLength(1, GridUnitType.Auto) });
}
for(int i = 0; i < spriteCount / 8; i++) {
_spriteGrid.RowDefinitions.Add(new() { Height = new GridLength(1, GridUnitType.Auto) });
}
for(int i = 0; i < spriteCount; i++) {
SpritePreviewPanel preview = new SpritePreviewPanel(SpritePreviews[i], Config);
preview.InnerHeight = (32.0 / dpiScale);
preview.InnerWidth = (32.0 / dpiScale);
Grid.SetColumn(preview, i % 8);
Grid.SetRow(preview, i / 8);
preview.DataContext = SpritePreviews[i];
preview.PointerPressed += SpritePreview_PointerPressed;
preview.PointerMoved += SpritePreview_PointerMoved;
_spriteGrid.Children.Add(preview);
}
}
}
private void SpriteGrid_PointerExited(object? sender, PointerEventArgs e)
{
TooltipHelper.HideTooltip(_spriteGrid);
PreviewPanelTooltip = null;
PreviewPanelSprite = null;
}
private void SpritePreview_PointerMoved(object? sender, PointerEventArgs e)
{
if(sender is Control ctrl && ctrl.DataContext is SpritePreviewModel sprite) {
PreviewPanelTooltip = GetPreviewPanel(sprite, PreviewPanelTooltip);
PreviewPanelSprite = sprite;
TooltipHelper.ShowTooltip(_spriteGrid, PreviewPanelTooltip, 15);
}
}
private void SpritePreview_PointerPressed(object? sender, PointerPressedEventArgs e)
{
if(sender is Control ctrl && ctrl.DataContext is SpritePreviewModel sprite) {
SelectedSprite = sprite;
UpdateSelection(sprite);
}
}
public void SelectSprite(int spriteIndex)
{
SelectedSprite = SpritePreviews[spriteIndex];
UpdateSelection(SelectedSprite);
}
private void InitPreviews(DebugSpriteInfo[] sprites, UInt32[] spritePreviews, DebugSpritePreviewInfo previewInfo)
{
if(SpritePreviews.Count != sprites.Length) {
List<SpritePreviewModel> previews = new();
for(int i = 0; i < sprites.Length; i++) {
SpritePreviewModel model = new SpritePreviewModel();
model.Init(ref sprites[i], spritePreviews, previewInfo);
previews.Add(model);
}
SpritePreviews = previews;
} else {
for(int i = 0; i < sprites.Length; i++) {
SpritePreviews[i].Init(ref sprites[i], spritePreviews, previewInfo);
}
}
InitGrid(sprites.Length);
}
private void UpdateRamData(ref byte[] baseRam, ref byte[] extRam, ref byte[] combinedRam, MemoryType baseType, MemoryType extType)
{
if(baseType == MemoryType.None) {
Array.Resize(ref baseRam, 0);
Array.Resize(ref extRam, 0);
Array.Resize(ref combinedRam, 0);
} else if(extType != baseType) {
DebugApi.GetMemoryState(extType, ref extRam);
if(extRam.Length > 0) {
DebugApi.GetMemoryState(baseType, ref baseRam);
Array.Resize(ref combinedRam, extRam.Length + baseRam.Length);
Array.Copy(baseRam, 0, combinedRam, 0, baseRam.Length);
Array.Copy(extRam, 0, combinedRam, baseRam.Length, extRam.Length);
} else {
DebugApi.GetMemoryState(baseType, ref combinedRam);
}
} else {
Array.Resize(ref extRam, 0);
DebugApi.GetMemoryState(baseType, ref combinedRam);
}
}
private void UpdateVramData()
{
UpdateRamData(ref _baseVram, ref _extVram, ref _coreData.Vram, CpuType.GetVramMemoryType(), CpuType.GetVramMemoryType(getExtendedRam: true));
}
private void UpdateSpriteRamData()
{
UpdateRamData(ref _baseSpriteRam, ref _extSpriteRam, ref _coreData.SpriteRam, CpuType.GetSpriteRamMemoryType(), CpuType.GetSpriteRamMemoryType(getExtendedRam: true));
}
public void RefreshData()
{
lock(_updateLock) {
_coreData.PpuState = DebugApi.GetPpuState(CpuType);
_coreData.PpuToolsState = DebugApi.GetPpuToolsState(CpuType);
UpdateVramData();
int spriteRamSize;
MemoryType spriteRamType = CpuType.GetSpriteRamMemoryType();
if(spriteRamType == MemoryType.None) {
//SMS-specific
spriteRamSize = 0x100;
} else {
spriteRamSize = DebugApi.GetMemorySize(spriteRamType);
MemoryType spriteRamExtType = CpuType.GetSpriteRamMemoryType(getExtendedRam: true);
if(spriteRamType != spriteRamExtType) {
spriteRamSize += DebugApi.GetMemorySize(spriteRamExtType);
}
}
if(Config.Source == SpriteViewerSource.SpriteRam) {
UpdateSpriteRamData();
} else {
MemoryType cpuMemory = CpuType.ToMemoryType();
DebugApi.GetMemoryValues(cpuMemory, (uint)Config.SourceOffset, (uint)(Config.SourceOffset + spriteRamSize - 1), ref _coreData.SpriteRam);
Dispatcher.UIThread.Post(() => {
MaxSourceOffset = Math.Max(0, DebugApi.GetMemorySize(cpuMemory) - spriteRamSize);
});
}
DebugPaletteInfo palette = DebugApi.GetPaletteInfo(CpuType);
_coreData.Palette = palette.ColorCount > 0 ? palette : null;
}
RefreshTab();
}
private void RefreshTab()
{
if(_refreshPending) {
return;
}
_refreshPending = true;
Dispatcher.UIThread.Post(() => {
InternalRefreshTab();
_refreshPending = false;
});
}
private void InternalRefreshTab()
{
if(Disposed) {
return;
}
lock(_updateLock) {
_coreData.CopyTo(_data);
}
if(_data.PpuState == null || _data.Palette == null || _data.PpuToolsState == null) {
return;
}
GetSpritePreviewOptions options = new GetSpritePreviewOptions() {
Background = Config.Background
};
DebugSpritePreviewInfo previewInfo = DebugApi.GetSpritePreviewInfo(CpuType, options, _data.PpuState, _data.PpuToolsState);
InitBitmap((int)previewInfo.Width, (int)previewInfo.Height);
UInt32[] palette = _data.Palette.Value.GetRgbPalette();
LeftClipSize = Config.ShowOffscreenRegions ? 0 : (int)previewInfo.VisibleX;
RightClipSize = Config.ShowOffscreenRegions ? 0 : (int)(previewInfo.Width - (previewInfo.VisibleWidth + previewInfo.VisibleX));
TopClipSize = Config.ShowOffscreenRegions ? 0 : (int)previewInfo.VisibleY;
BottomClipSize = Config.ShowOffscreenRegions ? 0 : (int)(previewInfo.Height - (previewInfo.VisibleHeight + previewInfo.VisibleY));
using(var framebuffer = ViewerBitmap.Lock(true)) {
DebugApi.GetSpriteList(ref _spriteList, ref _spritePreviews, CpuType, options, _data.PpuState, _data.PpuToolsState, _data.Vram, _data.SpriteRam, palette, framebuffer.FrameBuffer.Address);
}
InitPreviews(_spriteList, _spritePreviews, previewInfo);
if(Config.ShowOutline) {
List<Rect> spriteRects = new List<Rect>();
foreach(SpritePreviewModel sprite in SpritePreviews) {
(Rect mainRect, Rect alt1, Rect alt2, Rect alt3) = sprite.GetPreviewRect();
spriteRects.Add(mainRect);
if(alt1 != default) {
spriteRects.Add(alt1);
}
if(alt2 != default) {
spriteRects.Add(alt2);
}
if(alt3 != default) {
spriteRects.Add(alt3);
}
}
ViewerBitmap.HighlightRects = spriteRects;
} else {
ViewerBitmap.HighlightRects = null;
}
ViewerBitmap.Invalidate();
int selectedIndex = SelectedSprite?.SpriteIndex ?? -1;
if(selectedIndex >= 0 && selectedIndex < SpritePreviews.Count) {
SelectedSprite = SpritePreviews[selectedIndex];
UpdateSelectionPreview();
} else {
SelectedSprite = null;
}
ListView.RefreshList();
UpdateTooltips();
UpdateSelection(SelectedSprite);
UpdateMouseOverRect();
}
private void UpdateSelectionPreview()
{
if(SelectedSprite != null) {
SelectedPreviewPanel = GetPreviewPanel(SelectedSprite, SelectedPreviewPanel);
} else {
SelectedPreviewPanel = null;
}
}
private void UpdateMouseOverRect()
{
if(PreviewPanelSprite != null) {
MouseOverRect = PreviewPanelSprite.GetPreviewRect().Item1;
} else if(ViewerMousePos != null && GetMatchingSprite(ViewerMousePos.Value, out Rect matchingRect) is SpritePreviewModel sprite) {
MouseOverRect = matchingRect;
} else {
MouseOverRect = null;
}
}
private void UpdateTooltips()
{
if(PreviewPanelSprite != null && PreviewPanelTooltip != null) {
GetPreviewPanel(PreviewPanelSprite, PreviewPanelTooltip);
} else if(ViewerMousePos != null && ViewerTooltip != null) {
SpritePreviewModel? sprite = GetMatchingSprite(ViewerMousePos.Value, out _);
if(sprite != null) {
GetPreviewPanel(sprite, ViewerTooltip);
}
}
}
public void UpdateSelection(SpritePreviewModel? sprite)
{
if(sprite != null) {
SelectionRect = sprite.GetPreviewRect().Item1;
} else {
SelectionRect = default;
}
}
public SpritePreviewModel? GetMatchingSprite(PixelPoint p, out Rect matchingRect)
{
Point point = p.ToPoint(1);
for(int i = SpritePreviews.Count - 1; i >= 0; i--) {
SpritePreviewModel sprite = SpritePreviews[i];
(Rect mainRect, Rect alt1, Rect alt2, Rect alt3) = sprite.GetPreviewRect();
if(mainRect.Contains(point)) {
matchingRect = mainRect;
return sprite;
} else if(alt1.Contains(point)) {
matchingRect = alt1;
return sprite;
} else if(alt2.Contains(point)) {
matchingRect = alt2;
return sprite;
} else if(alt3.Contains(point)) {
matchingRect = alt3;
return sprite;
}
}
matchingRect = default;
return null;
}
public void OnGameLoaded()
{
RefreshData();
}
}
public class SpriteViewerData
{
public BaseState? PpuState;
public BaseState? PpuToolsState;
public byte[] SpriteRam = Array.Empty<byte>();
public byte[] Vram = Array.Empty<byte>();
public DebugPaletteInfo? Palette = null;
public void CopyTo(SpriteViewerData dst)
{
dst.PpuState = PpuState;
dst.Palette = Palette;
dst.PpuToolsState = PpuToolsState;
CopyArray(SpriteRam, ref dst.SpriteRam);
CopyArray(Vram, ref dst.Vram);
}
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);
}
}
}