From fd7be59a1f468ee3034501a95cdb09b112f83fa3 Mon Sep 17 00:00:00 2001 From: Sour Date: Thu, 24 Jun 2021 20:44:10 -0400 Subject: [PATCH] Sprite viewer v1 --- Core/Debugger/PpuTools.h | 18 +++ Core/Gameboy/Debugger/GbPpuTools.cpp | 67 ++++++--- Core/Gameboy/Debugger/GbPpuTools.h | 4 + Core/NES/Debugger/NesPpuTools.cpp | 82 ++++++++++- Core/NES/Debugger/NesPpuTools.h | 3 + Core/SNES/Debugger/SnesPpuTools.cpp | 132 +++++++++++------- Core/SNES/Debugger/SnesPpuTools.h | 4 + InteropDLL/DebugApiWrapper.cpp | 10 +- NewUI/Debugger/Controls/PictureViewer.cs | 36 +++-- .../ViewModels/SpriteViewerViewModel.cs | 81 +++++++++++ NewUI/Debugger/Windows/ProfilerWindow.axaml | 2 +- .../Debugger/Windows/SpriteViewerWindow.axaml | 76 ++++++++++ .../Windows/SpriteViewerWindow.axaml.cs | 131 +++++++++++++++++ NewUI/Interop/DebugApi.cs | 86 ++++++++++-- NewUI/Localization/resources.en.xml | 13 ++ NewUI/NewUI.csproj | 3 + NewUI/Views/MainMenuView.axaml | 5 + NewUI/Views/MainMenuView.axaml.cs | 8 ++ 18 files changed, 656 insertions(+), 105 deletions(-) create mode 100644 NewUI/Debugger/ViewModels/SpriteViewerViewModel.cs create mode 100644 NewUI/Debugger/Windows/SpriteViewerWindow.axaml create mode 100644 NewUI/Debugger/Windows/SpriteViewerWindow.axaml.cs diff --git a/Core/Debugger/PpuTools.h b/Core/Debugger/PpuTools.h index bd01143b..e6442d31 100644 --- a/Core/Debugger/PpuTools.h +++ b/Core/Debugger/PpuTools.h @@ -14,6 +14,23 @@ struct ViewerRefreshConfig uint16_t Cycle; }; +struct DebugSpriteInfo +{ + uint16_t SpriteIndex; + uint16_t TileIndex; + int16_t X; + int16_t Y; + + uint8_t Palette; + uint8_t Priority; + uint8_t Width; + uint8_t Height; + bool HorizontalMirror; + bool VerticalMirror; + bool UseSecondTable; + bool Visible; +}; + class PpuTools { protected: @@ -37,6 +54,7 @@ public: virtual FrameInfo GetSpritePreviewSize(GetSpritePreviewOptions options, BaseState& state) = 0; virtual void GetSpritePreview(GetSpritePreviewOptions options, BaseState& state, uint8_t* vram, uint8_t* oamRam, uint32_t* palette, uint32_t* outBuffer) = 0; + virtual uint32_t GetSpriteList(GetSpritePreviewOptions options, BaseState& baseState, uint8_t* oamRam, DebugSpriteInfo outBuffer[]) = 0; void SetViewerUpdateTiming(uint32_t viewerId, uint16_t scanline, uint16_t cycle); void RemoveViewer(uint32_t viewerId); diff --git a/Core/Gameboy/Debugger/GbPpuTools.cpp b/Core/Gameboy/Debugger/GbPpuTools.cpp index e77c7f17..8f54a5ac 100644 --- a/Core/Gameboy/Debugger/GbPpuTools.cpp +++ b/Core/Gameboy/Debugger/GbPpuTools.cpp @@ -2,7 +2,6 @@ #include "Gameboy/Debugger/GbPpuTools.h" #include "Debugger/DebugTypes.h" #include "Shared/SettingTypes.h" -#include "SNES/SnesDefaultVideoFilter.h" #include "Gameboy/GbTypes.h" GbPpuTools::GbPpuTools(Debugger* debugger, Emulator *emu) : PpuTools(debugger, emu) @@ -71,21 +70,14 @@ void GbPpuTools::GetSpritePreview(GetSpritePreviewOptions options, BaseState& ba std::fill(filled, filled + 256, 0xFF); for(int i = 0; i < 0xA0; i += 4) { - uint8_t sprY = oamRam[i]; - if(sprY > row || sprY + height <= row) { + DebugSpriteInfo sprite = GetSpriteInfo(i / 4, options, state, oamRam); + if(sprite.Y > row || sprite.Y + height <= row) { continue; } - int y = row - sprY; - uint8_t sprX = oamRam[i + 1]; - uint8_t tileIndex = oamRam[i + 2]; - uint8_t attributes = oamRam[i + 3]; - - uint16_t tileBank = isCgb ? ((attributes & 0x08) ? 0x2000 : 0x0000) : 0; - uint8_t palette = isCgb ? (attributes & 0x07) << 2 : 0; - bool hMirror = (attributes & 0x20) != 0; - bool vMirror = (attributes & 0x40) != 0; - + int y = row - sprite.Y; + uint8_t tileIndex = (uint8_t)sprite.TileIndex; + uint16_t tileBank = sprite.UseSecondTable ? 0x2000 : 0x0000; if(largeSprites) { tileIndex &= 0xFE; } @@ -93,25 +85,25 @@ void GbPpuTools::GetSpritePreview(GetSpritePreviewOptions options, BaseState& ba uint16_t tileStart = tileIndex * 16; tileStart |= tileBank; - uint16_t pixelStart = tileStart + (vMirror ? (height - 1 - y) : y) * 2; + uint16_t pixelStart = tileStart + (sprite.VerticalMirror ? (height - 1 - y) : y) * 2; for(int x = 0; x < width; x++) { - if(sprX + x >= 256) { + if(sprite.X + x >= 256) { break; - } else if(filled[sprX + x] < sprX) { + } else if(filled[sprite.X + x] < sprite.X) { continue; } - uint8_t shift = hMirror ? (x & 0x07) : (7 - (x & 0x07)); + uint8_t shift = sprite.HorizontalMirror ? (x & 0x07) : (7 - (x & 0x07)); uint8_t color = GetTilePixelColor(vram, 0x3FFF, 2, pixelStart, shift, 1); if(color > 0) { + uint32_t outOffset = (row * 256) + sprite.X + x; if(!isCgb) { - color = (((attributes & 0x10) ? state.ObjPalette1 : state.ObjPalette0) >> (color * 2)) & 0x03; + outBuffer[outOffset] = palette[4 + (sprite.Palette * 4) + color]; + } else { + outBuffer[outOffset] = palette[32 + (sprite.Palette * 4) + color]; } - - uint32_t outOffset = (row * 256) + sprX + x; - outBuffer[outOffset] = SnesDefaultVideoFilter::ToArgb(state.CgbObjPalettes[palette + color]); - filled[sprX + x] = sprX; + filled[sprite.X + x] = (uint8_t)sprite.X; } } } @@ -127,3 +119,34 @@ FrameInfo GbPpuTools::GetSpritePreviewSize(GetSpritePreviewOptions options, Base { return { 256, 256 }; } + +DebugSpriteInfo GbPpuTools::GetSpriteInfo(uint16_t i, GetSpritePreviewOptions& options, GbPpuState& state, uint8_t* oamRam) +{ + DebugSpriteInfo sprite = {}; + + sprite.SpriteIndex = i; + sprite.Y = oamRam[i*4]; + + sprite.X = oamRam[i * 4 + 1]; + sprite.TileIndex = oamRam[i * 4 + 2]; + uint8_t attributes = oamRam[i * 4 + 3]; + + sprite.UseSecondTable = (state.CgbEnabled && (attributes & 0x08)) ? true : false; + sprite.Palette = state.CgbEnabled ? (attributes & 0x07) : ((attributes & 0x10) ? 1 : 0); + sprite.HorizontalMirror = (attributes & 0x20) != 0; + sprite.VerticalMirror = (attributes & 0x40) != 0; + sprite.Visible = sprite.X > 0 && sprite.Y > 0 && sprite.Y < 160 && sprite.X < 168; + sprite.Width = 8; + sprite.Height = state.LargeSprites ? 16 : 8; + + return sprite; +} + +uint32_t GbPpuTools::GetSpriteList(GetSpritePreviewOptions options, BaseState& baseState, uint8_t* oamRam, DebugSpriteInfo outBuffer[]) +{ + GbPpuState& state = (GbPpuState&)baseState; + for(int i = 0; i < 40; i++) { + outBuffer[i] = GetSpriteInfo(i, options, state, oamRam); + } + return 40; +} diff --git a/Core/Gameboy/Debugger/GbPpuTools.h b/Core/Gameboy/Debugger/GbPpuTools.h index 4e069253..c3d8c2eb 100644 --- a/Core/Gameboy/Debugger/GbPpuTools.h +++ b/Core/Gameboy/Debugger/GbPpuTools.h @@ -7,6 +7,9 @@ class Emulator; class GbPpuTools : public PpuTools { +private: + DebugSpriteInfo GetSpriteInfo(uint16_t spriteIndex, GetSpritePreviewOptions& options, GbPpuState& state, uint8_t* oamRam); + public: GbPpuTools(Debugger* debugger, Emulator *emu); @@ -15,4 +18,5 @@ public: 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; + uint32_t GetSpriteList(GetSpritePreviewOptions options, BaseState& baseState, uint8_t* oamRam, DebugSpriteInfo outBuffer[]) override; }; \ No newline at end of file diff --git a/Core/NES/Debugger/NesPpuTools.cpp b/Core/NES/Debugger/NesPpuTools.cpp index 987d2d05..6fb142d8 100644 --- a/Core/NES/Debugger/NesPpuTools.cpp +++ b/Core/NES/Debugger/NesPpuTools.cpp @@ -83,6 +83,54 @@ void NesPpuTools::GetTilemap(GetTilemapOptions options, BaseState& baseState, ui void NesPpuTools::GetSpritePreview(GetSpritePreviewOptions options, BaseState& baseState, uint8_t* vram, uint8_t* oamRam, uint32_t* palette, uint32_t* outBuffer) { + NesPpuState& state = (NesPpuState&)baseState; + + bool largeSprites = (state.ControlReg & 0x20) ? true : false; + uint16_t sprAddr = (state.ControlReg & 0x08) ? 0x1000 : 0x0000; + + std::fill(outBuffer, outBuffer + 256 * 240, 0xFF666666); + std::fill(outBuffer + 256 * 240, outBuffer + 256 * 256, 0xFF333333); + + for(int i = 0x100 - 4; i >= 0; i -= 4) { + DebugSpriteInfo sprite = GetSpriteInfo(i / 4, options, state, oamRam); + + uint16_t tileStart; + if(largeSprites) { + if(sprite.TileIndex & 0x01) { + tileStart = 0x1000 | ((sprite.TileIndex & 0xFE) * 16); + } else { + tileStart = 0x0000 | (sprite.TileIndex * 16); + } + } else { + tileStart = (sprite.TileIndex * 16) | sprAddr; + } + + for(int y = 0; y < sprite.Height; y++) { + if(sprite.Y + y >= 256) { + break; + } + + uint8_t lineOffset = sprite.VerticalMirror ? (sprite.Height - 1 - y) : y; + uint16_t pixelStart = tileStart + lineOffset; + if(largeSprites && lineOffset >= 8) { + pixelStart += 8; + } + + for(int x = 0; x < sprite.Width; x++) { + if(sprite.X + x >= 256) { + break; + } + + uint8_t shift = sprite.HorizontalMirror ? (x & 0x07) : (7 - (x & 0x07)); + uint8_t color = GetTilePixelColor(vram, 0x3FFF, 2, pixelStart, shift, 8); + + if(color > 0) { + uint32_t outOffset = ((sprite.Y + y) * 256) + sprite.X + x; + outBuffer[outOffset] = palette[16 + (sprite.Palette * 4) + color]; + } + } + } + } } FrameInfo NesPpuTools::GetTilemapSize(GetTilemapOptions options, BaseState& state) @@ -90,7 +138,39 @@ FrameInfo NesPpuTools::GetTilemapSize(GetTilemapOptions options, BaseState& stat return { 512, 480 }; } +DebugSpriteInfo NesPpuTools::GetSpriteInfo(uint32_t i, GetSpritePreviewOptions& options, NesPpuState& state, uint8_t* oamRam) +{ + DebugSpriteInfo sprite = {}; + + sprite.SpriteIndex = i; + sprite.Y = oamRam[i * 4] + 1; + sprite.X = oamRam[i * 4 + 3]; + sprite.TileIndex = oamRam[i * 4 + 1]; + + uint8_t attributes = oamRam[i * 4 + 2]; + sprite.Palette = (attributes & 0x03); + sprite.HorizontalMirror = (attributes & 0x40) != 0; + sprite.VerticalMirror = (attributes & 0x80) != 0; + sprite.Priority = (attributes & 0x20) ? 0 : 1; + sprite.Visible = sprite.Y < 240; + sprite.Width = 8; + + bool largeSprites = (state.ControlReg & 0x20) ? true : false; + sprite.Height = largeSprites ? 16 : 8; + + return sprite; +} + +uint32_t NesPpuTools::GetSpriteList(GetSpritePreviewOptions options, BaseState& baseState, uint8_t* oamRam, DebugSpriteInfo outBuffer[]) +{ + NesPpuState& state = (NesPpuState&)baseState; + for(int i = 0; i < 64; i++) { + outBuffer[i] = GetSpriteInfo(i, options, state, oamRam); + } + return 64; +} + FrameInfo NesPpuTools::GetSpritePreviewSize(GetSpritePreviewOptions options, BaseState& state) { - return { 256, 240 }; + return { 256, 256 }; } diff --git a/Core/NES/Debugger/NesPpuTools.h b/Core/NES/Debugger/NesPpuTools.h index 5be90daa..b11cb745 100644 --- a/Core/NES/Debugger/NesPpuTools.h +++ b/Core/NES/Debugger/NesPpuTools.h @@ -6,11 +6,13 @@ class Debugger; class Emulator; class BaseMapper; class NesConsole; +struct NesPpuState; class NesPpuTools : public PpuTools { private: BaseMapper* _mapper = nullptr; + DebugSpriteInfo GetSpriteInfo(uint32_t spriteIndex, GetSpritePreviewOptions& options, NesPpuState& state, uint8_t* oamRam); public: NesPpuTools(Debugger* debugger, Emulator *emu, NesConsole* console); @@ -20,4 +22,5 @@ public: 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; + uint32_t GetSpriteList(GetSpritePreviewOptions options, BaseState& baseState, uint8_t* oamRam, DebugSpriteInfo outBuffer[]) override; }; \ No newline at end of file diff --git a/Core/SNES/Debugger/SnesPpuTools.cpp b/Core/SNES/Debugger/SnesPpuTools.cpp index 163b754a..d22d28e2 100644 --- a/Core/SNES/Debugger/SnesPpuTools.cpp +++ b/Core/SNES/Debugger/SnesPpuTools.cpp @@ -115,67 +115,43 @@ static constexpr uint8_t _oamSizes[8][2][2] = { void SnesPpuTools::GetSpritePreview(GetSpritePreviewOptions options, BaseState& baseState, uint8_t *vram, uint8_t *oamRam, uint32_t* palette, uint32_t* outBuffer) { PpuState& state = (PpuState&)baseState; - + FrameInfo size = GetSpritePreviewSize(options, state); //TODO //uint16_t baseAddr = state.EnableOamPriority ? (_internalOamAddress & 0x1FC) : 0; uint16_t baseAddr = 0; - bool filled[256 * 240] = {}; - int lastScanline = state.OverscanMode ? 239 : 224; - std::fill(outBuffer, outBuffer + 256 * lastScanline, 0xFF888888); - std::fill(outBuffer + 256 * lastScanline, outBuffer + 256 * 240, 0xFF000000); + bool filled[512*256] = {}; + std::fill(outBuffer, outBuffer + size.Width * size.Height, 0xFF333333); + for(int i = 0; i < (state.OverscanMode ? 239 : 224); i++) { + std::fill(outBuffer + size.Width * i + 256, outBuffer + size.Width * i + 512, 0xFF888888); + } - for(int screenY = 0; screenY < lastScanline; screenY++) { + for(int scanline = 0; scanline < (int)size.Height; scanline++) { for(int i = 508; i >= 0; i -= 4) { - uint16_t addr = (baseAddr + i) & 0x1FF; - uint8_t y = oamRam[addr + 1]; - - uint8_t highTableOffset = addr >> 4; - uint8_t shift = ((addr >> 2) & 0x03) << 1; - uint8_t highTableValue = oamRam[0x200 | highTableOffset] >> shift; - uint8_t largeSprite = (highTableValue & 0x02) >> 1; - uint8_t height = _oamSizes[state.OamMode][largeSprite][1] << 3; - - uint8_t endY = (y + (state.ObjInterlace ? (height >> 1): height)) & 0xFF; - - bool visible = (screenY >= y && screenY < endY) || (endY < y && screenY < endY); + DebugSpriteInfo sprite = GetSpriteInfo(i / 4, options, state, oamRam); + + uint8_t endY = (sprite.Y + (state.ObjInterlace ? (sprite.Height >> 1): sprite.Height)) & 0xFF; + bool visible = (scanline >= sprite.Y && scanline < endY) || (endY < sprite.Y && scanline < endY); if(!visible) { //Not visible on this scanline continue; } - uint8_t width = _oamSizes[state.OamMode][largeSprite][0] << 3; - uint16_t sign = (highTableValue & 0x01) << 8; - int16_t x = (int16_t)((sign | oamRam[addr]) << 7) >> 7; - - if(x != -256 && (x + width <= 0 || x > 255)) { - //Sprite is not visible (and must be ignored for time/range flag calculations) - //Sprites at X=-256 are always used when considering Time/Range flag calculations, but not actually drawn. - continue; - } - - int tileRow = (oamRam[addr + 2] & 0xF0) >> 4; - int tileColumn = oamRam[addr + 2] & 0x0F; - - uint8_t flags = oamRam[addr + 3]; - bool useSecondTable = (flags & 0x01) != 0; - uint8_t paletteIndex = (flags >> 1) & 0x07; - //uint8_t priority = (flags >> 4) & 0x03; - bool horizontalMirror = (flags & 0x40) != 0; - bool verticalMirror = (flags & 0x80) != 0; - + int tileRow = (sprite.TileIndex & 0xF0) >> 4; + int tileColumn = sprite.TileIndex & 0x0F; + uint8_t yOffset; int rowOffset; - int yGap = (screenY - y); + int yGap = (scanline - sprite.Y); if(state.ObjInterlace) { yGap <<= 1; yGap |= (state.FrameCount & 0x01); } - if(verticalMirror) { - yOffset = (height - 1 - yGap) & 0x07; - rowOffset = (height - 1 - yGap) >> 3; + if(sprite.VerticalMirror) { + yOffset = (sprite.Height - 1 - yGap) & 0x07; + rowOffset = (sprite.Height - 1 - yGap) >> 3; } else { yOffset = yGap & 0x07; rowOffset = yGap >> 3; @@ -183,38 +159,90 @@ void SnesPpuTools::GetSpritePreview(GetSpritePreviewOptions options, BaseState& uint8_t row = (tileRow + rowOffset) & 0x0F; - for(int j = std::max(x, 0); j < x + width && j < 256; j++) { - uint32_t outOffset = screenY * 256 + j; + for(int j = sprite.X; j < sprite.X + sprite.Width && j < 256; j++) { + uint32_t outOffset = scanline * size.Width + (256 + j); if(filled[outOffset]) { continue; } uint8_t xOffset; int columnOffset; - if(horizontalMirror) { - xOffset = (width - (j - x) - 1) & 0x07; - columnOffset = (width - (j - x) - 1) >> 3; + if(sprite.HorizontalMirror) { + xOffset = (sprite.Width - (j - sprite.X) - 1) & 0x07; + columnOffset = (sprite.Width - (j - sprite.X) - 1) >> 3; } else { - xOffset = (j - x) & 0x07; - columnOffset = (j - x) >> 3; + xOffset = (j - sprite.X) & 0x07; + columnOffset = (j - sprite.X) >> 3; } uint8_t column = (tileColumn + columnOffset) & 0x0F; uint8_t tileIndex = (row << 4) | column; - uint16_t tileStart = ((state.OamBaseAddress + (tileIndex << 4) + (useSecondTable ? state.OamAddressOffset : 0)) & 0x7FFF) << 1; + uint16_t tileStart = ((state.OamBaseAddress + (tileIndex << 4) + (sprite.UseSecondTable ? state.OamAddressOffset : 0)) & 0x7FFF) << 1; uint8_t color = GetTilePixelColor(vram, Ppu::VideoRamSize - 1, 4, tileStart + yOffset * 2, 7 - xOffset, 1); if(color != 0) { if(options.SelectedSprite == i / 4) { filled[outOffset] = true; } - outBuffer[outOffset] = GetRgbPixelColor(palette, color, paletteIndex, 4, false, 256); + outBuffer[outOffset] = GetRgbPixelColor(palette, color, sprite.Palette + 8, 4, false, 0); } } } } } +DebugSpriteInfo SnesPpuTools::GetSpriteInfo(uint16_t spriteIndex, GetSpritePreviewOptions& options, PpuState& state, uint8_t* oamRam) +{ + uint16_t addr = (spriteIndex * 4) & 0x1FF; + + uint8_t highTableOffset = addr >> 4; + uint8_t shift = ((addr >> 2) & 0x03) << 1; + uint8_t highTableValue = oamRam[0x200 | highTableOffset] >> shift; + uint8_t largeSprite = (highTableValue & 0x02) >> 1; + uint8_t height = _oamSizes[state.OamMode][largeSprite][1] << 3; + + uint8_t width = _oamSizes[state.OamMode][largeSprite][0] << 3; + uint16_t sign = (highTableValue & 0x01) << 8; + int16_t x = (int16_t)((sign | oamRam[addr]) << 7) >> 7; + uint8_t y = oamRam[addr + 1]; + uint8_t flags = oamRam[addr + 3]; + + bool visible = true; + if(x + width <= 0 || x > 255) { + visible = false; + } else { + uint16_t scanlineCount = state.OverscanMode ? 239 : 224; + uint8_t endY = (y + (state.ObjInterlace ? (height >> 1) : height)) & 0xFF; + if(endY >= scanlineCount && y >= scanlineCount) { + visible = false; + } + } + + DebugSpriteInfo sprite = {}; + sprite.SpriteIndex = spriteIndex; + sprite.X = x; + sprite.Y = y; + sprite.Height = height; + sprite.Width = width; + sprite.TileIndex = oamRam[addr + 2]; + sprite.Palette = ((flags >> 1) & 0x07); + sprite.Priority = (flags >> 4) & 0x03; + sprite.HorizontalMirror = (flags & 0x40) != 0; + sprite.VerticalMirror = (flags & 0x80) != 0; + sprite.UseSecondTable = (flags & 0x01) != 0; + sprite.Visible = visible; + return sprite; +} + +uint32_t SnesPpuTools::GetSpriteList(GetSpritePreviewOptions options, BaseState& baseState, uint8_t* oamRam, DebugSpriteInfo outBuffer[]) +{ + PpuState& state = (PpuState&)baseState; + for(int i = 0; i < 128; i++) { + outBuffer[i] = GetSpriteInfo(i, options, state, oamRam); + } + return 128; +} + FrameInfo SnesPpuTools::GetTilemapSize(GetTilemapOptions options, BaseState& baseState) { FrameInfo size = { 256, 256 }; @@ -247,5 +275,5 @@ FrameInfo SnesPpuTools::GetTilemapSize(GetTilemapOptions options, BaseState& bas FrameInfo SnesPpuTools::GetSpritePreviewSize(GetSpritePreviewOptions options, BaseState& state) { - return { 256,240 }; + return { 512, 256 }; } diff --git a/Core/SNES/Debugger/SnesPpuTools.h b/Core/SNES/Debugger/SnesPpuTools.h index 070ccd34..9107ce5e 100644 --- a/Core/SNES/Debugger/SnesPpuTools.h +++ b/Core/SNES/Debugger/SnesPpuTools.h @@ -8,6 +8,9 @@ struct BaseState; class SnesPpuTools : public PpuTools { +private: + DebugSpriteInfo GetSpriteInfo(uint16_t spriteIndex, GetSpritePreviewOptions& options, PpuState& state, uint8_t* oamRam); + public: SnesPpuTools(Debugger* debugger, Emulator *emu); @@ -15,5 +18,6 @@ public: FrameInfo GetTilemapSize(GetTilemapOptions 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* oamRam, DebugSpriteInfo outBuffer[]) override; FrameInfo GetSpritePreviewSize(GetSpritePreviewOptions options, BaseState& state) override; }; \ No newline at end of file diff --git a/InteropDLL/DebugApiWrapper.cpp b/InteropDLL/DebugApiWrapper.cpp index 4f1491b7..7c826e16 100644 --- a/InteropDLL/DebugApiWrapper.cpp +++ b/InteropDLL/DebugApiWrapper.cpp @@ -94,12 +94,14 @@ extern "C" DllExport void __stdcall SetCdlData(CpuType cpuType, uint8_t* cdlData, uint32_t length) { GetDebugger()->SetCdlData(cpuType, cdlData, length); } DllExport void __stdcall MarkBytesAs(CpuType cpuType, uint32_t start, uint32_t end, uint8_t flags) { GetDebugger()->MarkBytesAs(cpuType, start, end, flags); } - DllExport FrameInfo __stdcall GetTilemapSize(CpuType cpuType, GetTilemapOptions options, BaseState& state) { return GetDebugger()->GetPpuTools(cpuType)->GetTilemapSize(options, state); } - DllExport FrameInfo __stdcall GetSpritePreviewSize(CpuType cpuType, GetSpritePreviewOptions options, BaseState& state) { return GetDebugger()->GetPpuTools(cpuType)->GetSpritePreviewSize(options, state); } - - 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 void __stdcall GetTileView(CpuType cpuType, GetTileViewOptions options, uint8_t *source, uint32_t srcSize, uint32_t *colors, uint32_t *buffer) { GetDebugger()->GetPpuTools(cpuType)->GetTileView(options, source, srcSize, colors, buffer); } + + 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 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); } + DllExport uint32_t __stdcall GetSpriteList(CpuType cpuType, GetSpritePreviewOptions options, BaseState& state, uint8_t* oamRam, DebugSpriteInfo sprites[]) { return GetDebugger()->GetPpuTools(cpuType)->GetSpriteList(options, state, oamRam, sprites); } DllExport void __stdcall SetViewerUpdateTiming(uint32_t viewerId, uint16_t scanline, uint16_t cycle, CpuType cpuType) { GetDebugger()->GetPpuTools(cpuType)->SetViewerUpdateTiming(viewerId, scanline, cycle); } diff --git a/NewUI/Debugger/Controls/PictureViewer.cs b/NewUI/Debugger/Controls/PictureViewer.cs index dc3ccacd..b7671969 100644 --- a/NewUI/Debugger/Controls/PictureViewer.cs +++ b/NewUI/Debugger/Controls/PictureViewer.cs @@ -26,6 +26,8 @@ namespace Mesen.Debugger.Controls public static readonly StyledProperty AltGridSizeXProperty = AvaloniaProperty.Register(nameof(AltGridSizeX), 8); public static readonly StyledProperty AltGridSizeYProperty = AvaloniaProperty.Register(nameof(AltGridSizeY), 8); public static readonly StyledProperty ShowAltGridProperty = AvaloniaProperty.Register(nameof(ShowAltGrid), false); + + public static readonly StyledProperty SelectionRectProperty = AvaloniaProperty.Register(nameof(SelectionRect), Rect.Empty); private Stopwatch _stopWatch = Stopwatch.StartNew(); private DispatcherTimer _timer = new DispatcherTimer(); @@ -78,12 +80,17 @@ namespace Mesen.Debugger.Controls set { SetValue(ShowAltGridProperty, value); } } + public Rect SelectionRect + { + get { return GetValue(SelectionRectProperty); } + set { SetValue(SelectionRectProperty, value); } + } + private Point? _mousePosition = null; - private PixelPoint? _selectedTile = null; static PictureViewer() { - AffectsRender(SourceProperty, ZoomProperty, GridSizeXProperty, GridSizeYProperty, ShowGridProperty); + AffectsRender(SourceProperty, ZoomProperty, GridSizeXProperty, GridSizeYProperty, ShowGridProperty, SelectionRectProperty); } protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) @@ -148,8 +155,14 @@ namespace Mesen.Debugger.Controls base.OnPointerPressed(e); Point p = e.GetCurrentPoint(this).Position; p = new Point(Math.Min(p.X, MinWidth - 1), Math.Min(p.Y, MinHeight - 1)); - _selectedTile = new PixelPoint((int)(p.X / GridSizeX / Zoom), (int)(p.Y / GridSizeY / Zoom)); - InvalidateVisual(); + + Rect selection = new Rect( + (int)p.X / GridSizeX * GridSizeX / Zoom, + (int)p.Y / GridSizeY * GridSizeY / Zoom, + GridSizeX, + GridSizeY + ); + SelectionRect = selection; } private void DrawGrid(DrawingContext context, bool show, int gridX, int gridY, Color color) @@ -183,7 +196,7 @@ namespace Mesen.Debugger.Controls int width = Source.PixelSize.Width * Zoom; int height = Source.PixelSize.Height * Zoom; - context.FillRectangle(new SolidColorBrush(0xFF333333), new Rect(Bounds.Size)); + context.FillRectangle(new SolidColorBrush(0xFFFFFFFF), new Rect(Bounds.Size)); context.DrawImage( Source, @@ -195,15 +208,12 @@ namespace Mesen.Debugger.Controls DrawGrid(context, ShowGrid, GridSizeX, GridSizeY, Color.FromArgb(128, Colors.LightBlue.R, Colors.LightBlue.G, Colors.LightBlue.B)); DrawGrid(context, ShowAltGrid, AltGridSizeX, AltGridSizeY, Color.FromArgb(128, Colors.Red.R, Colors.Red.G, Colors.Red.B)); - if(_selectedTile.HasValue) { - int gridSizeX = GridSizeX * Zoom; - int gridSizeY = GridSizeY * Zoom; - + if(SelectionRect != Rect.Empty) { Rect rect = new Rect( - _selectedTile.Value.X * gridSizeX - 0.5, - _selectedTile.Value.Y * gridSizeY - 0.5, - gridSizeX + 1, - gridSizeY + 1 + SelectionRect.X * Zoom - 0.5, + SelectionRect.Y * Zoom - 0.5, + SelectionRect.Width * Zoom + 1, + SelectionRect.Height * Zoom + 1 ); DashStyle dashes = new DashStyle(DashStyle.Dash.Dashes, (double)(_stopWatch.ElapsedMilliseconds / 50) % 100 / 5); diff --git a/NewUI/Debugger/ViewModels/SpriteViewerViewModel.cs b/NewUI/Debugger/ViewModels/SpriteViewerViewModel.cs new file mode 100644 index 00000000..52089ebc --- /dev/null +++ b/NewUI/Debugger/ViewModels/SpriteViewerViewModel.cs @@ -0,0 +1,81 @@ +using Avalonia.Controls; +using Avalonia.Media; +using Mesen.Interop; +using Mesen.ViewModels; +using ReactiveUI.Fody.Helpers; +using System; +using System.Collections.Generic; + +namespace Mesen.Debugger.ViewModels +{ + public class SpriteViewerViewModel : ViewModelBase + { + public CpuType CpuType { get; } + public ConsoleType ConsoleType { get; } + + [Reactive] public SpriteRowModel[] Sprites { get; set; } = new SpriteRowModel[0]; + + //For designer + public SpriteViewerViewModel() : this(CpuType.Cpu, ConsoleType.Snes) { } + + public SpriteViewerViewModel(CpuType cpuType, ConsoleType consoleType) + { + CpuType = cpuType; + ConsoleType = consoleType; + } + + public void UpdateSprites(DebugSpriteInfo[] newSprites) + { + if(Sprites.Length != newSprites.Length) { + SpriteRowModel[] sprites = new SpriteRowModel[newSprites.Length]; + for(int i = 0; i < newSprites.Length; i++) { + sprites[i] = new SpriteRowModel(); + } + Sprites = sprites; + } + + for(int i = 0; i < newSprites.Length; i++) { + Sprites[i].Init(ref newSprites[i]); + } + } + + public class SpriteRowModel : ViewModelBase + { + [Reactive] public int SpriteIndex { get; set; } + [Reactive] public int X { get; set; } + [Reactive] public int Y { get; set; } + [Reactive] public int Width { get; set; } + [Reactive] public int Height { get; set; } + [Reactive] public int TileIndex { get; set; } + [Reactive] public int Priority { get; set; } + [Reactive] public int Palette { get; set; } + [Reactive] public bool Visible { get; set; } + [Reactive] public ISolidColorBrush? Foreground { get; set; } + [Reactive] public FontStyle FontStyle { get; set; } + [Reactive] public string Size { get; set; } = ""; + [Reactive] public string Flags { get; set; } = ""; + + public void Init(ref DebugSpriteInfo sprite) + { + SpriteIndex = sprite.SpriteIndex; + X = sprite.X; + Y = sprite.Y; + Width = sprite.Width; + Height = sprite.Height; + TileIndex = sprite.TileIndex; + Priority = sprite.Priority; + Palette = sprite.Palette; + Size = sprite.Width + "x" + sprite.Height; + + Visible = sprite.Visible; + FontStyle = FontStyle.Normal; + Foreground = sprite.Visible ? Brushes.Black : Brushes.Gray; + + string flags = sprite.HorizontalMirror ? "H" : ""; + flags += sprite.VerticalMirror ? "V" : ""; + flags += sprite.UseSecondTable ? "N" : ""; + Flags = flags; + } + } + } +} diff --git a/NewUI/Debugger/Windows/ProfilerWindow.axaml b/NewUI/Debugger/Windows/ProfilerWindow.axaml index 80d58b63..c9f8c325 100644 --- a/NewUI/Debugger/Windows/ProfilerWindow.axaml +++ b/NewUI/Debugger/Windows/ProfilerWindow.axaml @@ -56,7 +56,7 @@ VerticalScrollBarVisibility="Auto" Sorting="OnGridSort" CanUserReorderColumns="False" - > + > diff --git a/NewUI/Debugger/Windows/SpriteViewerWindow.axaml b/NewUI/Debugger/Windows/SpriteViewerWindow.axaml new file mode 100644 index 00000000..9b1f08e2 --- /dev/null +++ b/NewUI/Debugger/Windows/SpriteViewerWindow.axaml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/NewUI/Debugger/Windows/SpriteViewerWindow.axaml.cs b/NewUI/Debugger/Windows/SpriteViewerWindow.axaml.cs new file mode 100644 index 00000000..d31fcfcd --- /dev/null +++ b/NewUI/Debugger/Windows/SpriteViewerWindow.axaml.cs @@ -0,0 +1,131 @@ +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Media.Imaging; +using Avalonia.Threading; +using System; +using Mesen.Debugger.Controls; +using Mesen.Debugger.ViewModels; +using Avalonia.Platform; +using Mesen.Interop; +using System.ComponentModel; +using Avalonia.Media; + +namespace Mesen.Debugger.Windows +{ + public class SpriteViewerWindow : Window + { + private NotificationListener _listener; + private SpriteViewerViewModel _model; + private PictureViewer _picViewer; + private WriteableBitmap _viewerBitmap; + private int _updateCounter = 0; + + public SpriteViewerWindow() + { + InitializeComponent(); +#if DEBUG + this.AttachDevTools(); +#endif + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + protected override void OnOpened(EventArgs e) + { + base.OnOpened(e); + + if(Design.IsDesignMode) { + return; + } + + _picViewer = this.FindControl("picViewer"); + _picViewer.Source = _viewerBitmap; + InitBitmap(256, 256); + + _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 SpriteViewerViewModel model) { + _model = model; + } else { + throw new Exception("Unexpected model"); + } + } + + private void OnGridSelectionChanged(object? sender, SelectionChangedEventArgs e) + { + if(e.AddedItems[0] is SpriteViewerViewModel.SpriteRowModel row) { + int offset = 0; + if(_model.CpuType == CpuType.Cpu) { + offset = 256; + } + _picViewer.SelectionRect = new Rect(row.X + offset, row.Y, row.Width, row.Height); + } + } + + private void UpdateSprites(bool forceListUpdate) where T : struct, BaseState + { + T ppuState = DebugApi.GetPpuState(_model.CpuType); + byte[] vram = DebugApi.GetMemoryState(_model.CpuType.GetVramMemoryType()); + byte[] spriteRam = DebugApi.GetMemoryState(_model.CpuType.GetSpriteRamMemoryType()); + UInt32[] palette = PaletteHelper.GetConvertedPalette(_model.CpuType, _model.ConsoleType); + + Dispatcher.UIThread.Post(() => { + GetSpritePreviewOptions options = new GetSpritePreviewOptions() { + SelectedSprite = -1 + }; + UInt32[] palette = PaletteHelper.GetConvertedPalette(_model.CpuType, _model.ConsoleType); + + FrameInfo size = DebugApi.GetSpritePreviewSize(_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.GetSpritePreview(_model.CpuType, options, ppuState, vram, spriteRam, palette, framebuffer.Address); + } + + _picViewer.Source = _viewerBitmap; + _picViewer.InvalidateVisual(); + + if(forceListUpdate || _updateCounter % 4 == 0) { //15 fps + DebugSpriteInfo[] sprites = DebugApi.GetSpriteList(_model.CpuType, options, ppuState, spriteRam); + _model.UpdateSprites(sprites); + } + _updateCounter++; + }); + } + + private void listener_OnNotification(NotificationEventArgs e) + { + if(e.NotificationType == ConsoleNotificationType.PpuFrameDone || e.NotificationType == ConsoleNotificationType.CodeBreak) { + bool forceListUpdate = e.NotificationType == ConsoleNotificationType.CodeBreak; + switch(_model.CpuType) { + case CpuType.Cpu: UpdateSprites(forceListUpdate); break; + case CpuType.Nes: UpdateSprites(forceListUpdate); break; + case CpuType.Gameboy: UpdateSprites(forceListUpdate); break; + } + } + } + } +} diff --git a/NewUI/Interop/DebugApi.cs b/NewUI/Interop/DebugApi.cs index 6b7665aa..5bb80f22 100644 --- a/NewUI/Interop/DebugApi.cs +++ b/NewUI/Interop/DebugApi.cs @@ -134,24 +134,22 @@ namespace Mesen.Interop public static void GetTilemap(CpuType cpuType, GetTilemapOptions options, T state, byte[] vram, UInt32[] palette, IntPtr outputBuffer) where T : struct, BaseState { GCHandle? handle = null; - IntPtr pointer = IntPtr.Zero; - handle?.Free(); + IntPtr compareVramPtr = IntPtr.Zero; if(options.CompareVram != null) { handle = GCHandle.Alloc(options.CompareVram, GCHandleType.Pinned); - pointer = handle.Value.AddrOfPinnedObject(); + compareVramPtr = handle.Value.AddrOfPinnedObject(); } int len = Marshal.SizeOf(typeof(T)); - IntPtr ptr = Marshal.AllocHGlobal(len); - Marshal.StructureToPtr(state, ptr, false); + IntPtr statePtr = Marshal.AllocHGlobal(len); + Marshal.StructureToPtr(state, statePtr, false); InteropGetTilemapOptions interopOptions = options.ToInterop(); - interopOptions.CompareVram = pointer; - DebugApi.GetTilemap(cpuType, interopOptions, ptr, vram, palette, outputBuffer); + interopOptions.CompareVram = compareVramPtr; + DebugApi.GetTilemap(cpuType, interopOptions, statePtr, vram, palette, outputBuffer); - if(handle.HasValue) { - handle.Value.Free(); - } + Marshal.FreeHGlobal(statePtr); + handle?.Free(); } [DllImport(DllPath)] private static extern FrameInfo GetTilemapSize(CpuType cpuType, InteropGetTilemapOptions options, IntPtr state); @@ -160,11 +158,46 @@ namespace Mesen.Interop int len = Marshal.SizeOf(typeof(T)); IntPtr ptr = Marshal.AllocHGlobal(len); Marshal.StructureToPtr(state, ptr, false); - return DebugApi.GetTilemapSize(cpuType, options.ToInterop(), ptr); + FrameInfo size = DebugApi.GetTilemapSize(cpuType, options.ToInterop(), ptr); + Marshal.FreeHGlobal(ptr); + return size; } [DllImport(DllPath)] public static extern void GetTileView(CpuType cpuType, GetTileViewOptions options, byte[] source, int srcSize, UInt32[] palette, IntPtr buffer); - [DllImport(DllPath)] public static extern void GetSpritePreview(CpuType cpuType, GetSpritePreviewOptions options, PpuState state, byte[] vram, byte[] oamRam, 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); + public static void GetSpritePreview(CpuType cpuType, GetSpritePreviewOptions options, T state, byte[] vram, byte[] spriteRam, UInt32[] palette, IntPtr outputBuffer) where T : struct, BaseState + { + int len = Marshal.SizeOf(typeof(T)); + IntPtr statePtr = Marshal.AllocHGlobal(len); + Marshal.StructureToPtr(state, statePtr, false); + DebugApi.GetSpritePreview(cpuType, options, statePtr, vram, spriteRam, palette, outputBuffer); + Marshal.FreeHGlobal(statePtr); + } + + [DllImport(DllPath)] private static extern FrameInfo GetSpritePreviewSize(CpuType cpuType, GetSpritePreviewOptions options, IntPtr state); + public static FrameInfo GetSpritePreviewSize(CpuType cpuType, GetSpritePreviewOptions options, T state) where T : struct, BaseState + { + int len = Marshal.SizeOf(typeof(T)); + IntPtr ptr = Marshal.AllocHGlobal(len); + Marshal.StructureToPtr(state, ptr, false); + FrameInfo size = DebugApi.GetSpritePreviewSize(cpuType, options, ptr); + Marshal.FreeHGlobal(ptr); + return size; + } + + [DllImport(DllPath)] private static extern UInt32 GetSpriteList(CpuType cpuType, GetSpritePreviewOptions options, IntPtr state, byte[] spriteRam, [In,Out]DebugSpriteInfo[] sprites); + public static DebugSpriteInfo[] GetSpriteList(CpuType cpuType, GetSpritePreviewOptions options, T state, byte[] spriteRam) where T : struct, BaseState + { + DebugSpriteInfo[] sprites = new DebugSpriteInfo[256]; + int len = Marshal.SizeOf(typeof(T)); + IntPtr statePtr = Marshal.AllocHGlobal(len); + Marshal.StructureToPtr(state, statePtr, false); + UInt32 spriteCount = DebugApi.GetSpriteList(cpuType, options, statePtr, spriteRam, sprites); + Array.Resize(ref sprites, (int)spriteCount); + Marshal.FreeHGlobal(statePtr); + return sprites; + } [DllImport(DllPath)] public static extern void SetViewerUpdateTiming(Int32 viewerId, Int32 scanline, Int32 cycle, CpuType cpuType); @@ -720,6 +753,23 @@ namespace Mesen.Interop public Int32 SelectedSprite; } + public struct DebugSpriteInfo + { + public UInt16 SpriteIndex; + public UInt16 TileIndex; + public Int16 X; + public Int16 Y; + + public byte Palette; + public byte Priority; + public byte Width; + public byte Height; + [MarshalAs(UnmanagedType.I1)] public bool HorizontalMirror; + [MarshalAs(UnmanagedType.I1)] public bool VerticalMirror; + [MarshalAs(UnmanagedType.I1)] public bool UseSecondTable; + [MarshalAs(UnmanagedType.I1)] public bool Visible; + } + public enum TileFormat { Bpp2, @@ -820,6 +870,18 @@ namespace Mesen.Interop } } + public static SnesMemoryType GetSpriteRamMemoryType(this CpuType cpuType) + { + switch(cpuType) { + case CpuType.Cpu: return SnesMemoryType.SpriteRam; + case CpuType.Gameboy: return SnesMemoryType.GbSpriteRam; + case CpuType.Nes: return SnesMemoryType.NesSpriteRam; + + default: + throw new Exception("Invalid CPU type"); + } + } + public static int GetAddressSize(this CpuType cpuType) { switch(cpuType) { diff --git a/NewUI/Localization/resources.en.xml b/NewUI/Localization/resources.en.xml index 4d9a6dcb..0bbf540a 100644 --- a/NewUI/Localization/resources.en.xml +++ b/NewUI/Localization/resources.en.xml @@ -791,6 +791,19 @@ Highlight attribute changes +
+ Sprite Viewer + + # + X + Y + Size + Tile + Priority + Palette + Flags +
+
Assembler Start address: $ diff --git a/NewUI/NewUI.csproj b/NewUI/NewUI.csproj index 7c104f21..1bfe2320 100644 --- a/NewUI/NewUI.csproj +++ b/NewUI/NewUI.csproj @@ -218,6 +218,9 @@ SnesPpuView.axaml + + SpriteViewerWindow.axaml + TilemapViewerWindow.axaml diff --git a/NewUI/Views/MainMenuView.axaml b/NewUI/Views/MainMenuView.axaml index da18779c..435494d5 100644 --- a/NewUI/Views/MainMenuView.axaml +++ b/NewUI/Views/MainMenuView.axaml @@ -395,6 +395,11 @@ + + + + + diff --git a/NewUI/Views/MainMenuView.axaml.cs b/NewUI/Views/MainMenuView.axaml.cs index 54ac554a..2b1ae02c 100644 --- a/NewUI/Views/MainMenuView.axaml.cs +++ b/NewUI/Views/MainMenuView.axaml.cs @@ -67,6 +67,14 @@ namespace Mesen.Views }.Show(); } + private void OnSpriteViewerClick(object sender, RoutedEventArgs e) + { + RomInfo romInfo = EmuApi.GetRomInfo(); + new SpriteViewerWindow { + DataContext = new SpriteViewerViewModel(romInfo.ConsoleType.GetMainCpuType(), romInfo.ConsoleType), + }.Show(); + } + private void OnMemoryToolsClick(object sender, RoutedEventArgs e) { new MemoryToolsWindow {