Sprite viewer v1

This commit is contained in:
Sour 2021-06-24 20:44:10 -04:00
parent 13a5053462
commit fd7be59a1f
18 changed files with 656 additions and 105 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -26,6 +26,8 @@ namespace Mesen.Debugger.Controls
public static readonly StyledProperty<int> AltGridSizeXProperty = AvaloniaProperty.Register<PictureViewer, int>(nameof(AltGridSizeX), 8);
public static readonly StyledProperty<int> AltGridSizeYProperty = AvaloniaProperty.Register<PictureViewer, int>(nameof(AltGridSizeY), 8);
public static readonly StyledProperty<bool> ShowAltGridProperty = AvaloniaProperty.Register<PictureViewer, bool>(nameof(ShowAltGrid), false);
public static readonly StyledProperty<Rect> SelectionRectProperty = AvaloniaProperty.Register<PictureViewer, Rect>(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<PictureViewer>(SourceProperty, ZoomProperty, GridSizeXProperty, GridSizeYProperty, ShowGridProperty);
AffectsRender<PictureViewer>(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);

View file

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

View file

@ -56,7 +56,7 @@
VerticalScrollBarVisibility="Auto"
Sorting="OnGridSort"
CanUserReorderColumns="False"
>
>
<DataGrid.Columns>
<DataGridTextColumn Header="{l:Translate colFunction}" Binding="{Binding FunctionName}" Width="*" CanUserResize="True" />
<DataGridTextColumn Header="{l:Translate colCallCount}" Binding="{Binding CallCount}" Width="*" CanUserResize="True" />

View file

@ -0,0 +1,76 @@
<Window
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:m="clr-namespace:Mesen"
xmlns:vm="using:Mesen.ViewModels"
xmlns:sys="using:System"
xmlns:v="using:Mesen.Views"
xmlns:c="using:Mesen.Controls"
xmlns:i="using:Mesen.Interop"
xmlns:l="using:Mesen.Localization"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:dvm="using:Mesen.Debugger.ViewModels"
xmlns:dc="using:Mesen.Debugger.Controls"
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="600"
x:Class="Mesen.Debugger.Windows.SpriteViewerWindow"
x:DataType="dvm:SpriteViewerViewModel"
Width="600" Height="600"
Title="{l:Translate wndTitle}"
Icon="/Assets/PerfTracker.png"
>
<Design.DataContext>
<dvm:SpriteViewerViewModel />
</Design.DataContext>
<Window.Styles>
<Style Selector="TabControl:singleitem">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="DataGridCell.rowStyle">
<Setter Property="Foreground" Value="{Binding Path=Foreground}"/>
<Setter Property="FontStyle" Value="{Binding Path=FontStyle}"/>
</Style>
</Window.Styles>
<Grid
ColumnDefinitions="1*,1*"
RowDefinitions="*"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
>
<Border BorderBrush="Gray" BorderThickness="1" Margin="1">
<ScrollViewer
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto"
AllowAutoHide="False"
>
<dc:PictureViewer
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
x:Name="picViewer"
/>
</ScrollViewer>
</Border>
<Border BorderBrush="Gray" BorderThickness="1" Grid.Column="1" Margin="1">
<dc:MesenDataGrid
Items="{CompiledBinding Sprites}"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
CanUserReorderColumns="False"
SelectionChanged="OnGridSelectionChanged"
>
<DataGrid.Columns>
<DataGridTextColumn Header="{l:Translate colSpriteIndex}" Binding="{Binding SpriteIndex}" Width="*" CanUserResize="True" CellStyleClasses="rowStyle" />
<DataGridTextColumn Header="{l:Translate colX}" Binding="{Binding X}" Width="*" CanUserResize="True" CellStyleClasses="rowStyle" />
<DataGridTextColumn Header="{l:Translate colY}" Binding="{Binding Y}" Width="*" CanUserResize="True" CellStyleClasses="rowStyle" />
<DataGridTextColumn Header="{l:Translate colSize}" Binding="{Binding Size}" Width="*" CanUserResize="True" CellStyleClasses="rowStyle" />
<DataGridTextColumn Header="{l:Translate colTileIndex}" Binding="{Binding TileIndex}" Width="*" CanUserResize="True" CellStyleClasses="rowStyle" />
<DataGridTextColumn Header="{l:Translate colPriority}" Binding="{Binding Priority}" Width="*" CanUserResize="True" CellStyleClasses="rowStyle" />
<DataGridTextColumn Header="{l:Translate colPalette}" Binding="{Binding Palette}" Width="*" CanUserResize="True" CellStyleClasses="rowStyle" />
<DataGridTextColumn Header="{l:Translate colFlags}" Binding="{Binding Flags}" Width="*" CanUserResize="True" CellStyleClasses="rowStyle" />
</DataGrid.Columns>
</dc:MesenDataGrid>
</Border>
</Grid>
</Window>

View file

@ -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<PictureViewer>("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<T>(bool forceListUpdate) where T : struct, BaseState
{
T ppuState = DebugApi.GetPpuState<T>(_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<PpuState>(forceListUpdate); break;
case CpuType.Nes: UpdateSprites<NesPpuState>(forceListUpdate); break;
case CpuType.Gameboy: UpdateSprites<GbPpuState>(forceListUpdate); break;
}
}
}
}
}

View file

@ -134,24 +134,22 @@ namespace Mesen.Interop
public static void GetTilemap<T>(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<T>(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<T>(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<T>(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) {

View file

@ -791,6 +791,19 @@
<Control ID="chkHighlightAttributeChanges">Highlight attribute changes</Control>
</Form>
<Form ID="SpriteViewerWindow">
<Control ID="wndTitle">Sprite Viewer</Control>
<Control ID="colSpriteIndex">#</Control>
<Control ID="colX">X</Control>
<Control ID="colY">Y</Control>
<Control ID="colSize">Size</Control>
<Control ID="colTileIndex">Tile</Control>
<Control ID="colPriority">Priority</Control>
<Control ID="colPalette">Palette</Control>
<Control ID="colFlags">Flags</Control>
</Form>
<Form ID="AssemblerWindow">
<Control ID="wndTitle">Assembler</Control>
<Control ID="lblStartAddress">Start address: $</Control>

View file

@ -218,6 +218,9 @@
<Compile Update="Debugger\Views\SnesPpuView.axaml.cs">
<DependentUpon>SnesPpuView.axaml</DependentUpon>
</Compile>
<Compile Update="Debugger\Windows\SpriteViewerWindow.axaml.cs">
<DependentUpon>SpriteViewerWindow.axaml</DependentUpon>
</Compile>
<Compile Update="Debugger\Windows\TilemapViewerWindow.axaml.cs">
<DependentUpon>TilemapViewerWindow.axaml</DependentUpon>
</Compile>

View file

@ -395,6 +395,11 @@
<Image Source="/Assets/VideoOptions.png" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="_Sprite Viewer" Click="OnSpriteViewerClick">
<MenuItem.Icon>
<Image Source="/Assets/PerfTracker.png" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="_Assembler" Click="OnAssemblerClick">
<MenuItem.Icon>
<Image Source="/Assets/Chip.png" />

View file

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