Mesen2/Core/SMS/Debugger/SmsVdpTools.cpp
Sour 5823761791 SMS: Implemented fairly accurate sprite eval+fetch timings, for all modes
+ Fixed VRAM read to wait until next available CPU access slot
+ Fixed NMI triggering on the wrong scanline on SMS
+ Fixed NMI issues on ColecoVision
+ Fixed random ram option not working properly on SG-1000
+ Added "Mem access" tab to tilemap viewer to see fetch patterns
2025-03-01 00:09:36 +09:00

510 lines
18 KiB
C++

#include "pch.h"
#include "SMS/Debugger/SmsVdpTools.h"
#include "SMS/SmsTypes.h"
#include "SMS/SmsConsole.h"
#include "SMS/SmsVdp.h"
#include "Debugger/DebugTypes.h"
#include "Debugger/MemoryDumper.h"
#include "Shared/SettingTypes.h"
SmsVdpTools::SmsVdpTools(Debugger* debugger, Emulator *emu, SmsConsole* console) : PpuTools(debugger, emu)
{
_console = console;
}
void SmsVdpTools::SetMemoryAccessData(uint16_t scanline, SmsVdpMemAccess* data, uint16_t scanlineCount)
{
_scanlineCount = scanlineCount;
memcpy(_memAccess + scanline * 342, data, 342);
}
FrameInfo SmsVdpTools::GetTilemapSize(GetTilemapOptions options, BaseState& baseState)
{
SmsVdpState& state = (SmsVdpState&)baseState;
if(options.Layer == 1) {
//Mem accesses
return { 342, _scanlineCount };
}
bool isTextMode = !state.UseMode4 && state.M1_Use224LineMode;
return { isTextMode ? 240u : 256u, state.NametableHeight };
}
DebugTilemapInfo SmsVdpTools::GetTilemap(GetTilemapOptions options, BaseState& baseState, BaseState& ppuToolsState, uint8_t* vram, uint32_t* palette, uint32_t* outBuffer)
{
SmsVdpState& state = (SmsVdpState&)baseState;
if(options.Layer == 1) {
//Mem accesses
DebugTilemapInfo result = {};
result.TileHeight = 1;
result.TileWidth = 1;
result.RowCount = _scanlineCount;
result.ColumnCount = 342;
result.Bpp = 8;
result.Format = TileFormat::DirectColor;
for(int i = 0, len = (int)(result.RowCount * result.ColumnCount); i < len; i+=2) {
switch(_memAccess[i]) {
case SmsVdpMemAccess::None: outBuffer[i] = 0xFF606060; break;
case SmsVdpMemAccess::BgLoadTable: outBuffer[i] = 0xFF00FF00; break;
case SmsVdpMemAccess::BgLoadTile: outBuffer[i] = 0xFF0000FF; break;
case SmsVdpMemAccess::SpriteEval: outBuffer[i] = 0xFFFF0000; break;
case SmsVdpMemAccess::SpriteLoadTable: outBuffer[i] = 0xFF00E080; break;
case SmsVdpMemAccess::SpriteLoadTile: outBuffer[i] = 0xFF0080E0; break;
case SmsVdpMemAccess::CpuSlot: outBuffer[i] = 0xFFA0A0A0; break;
}
outBuffer[i + 1] = outBuffer[i];
}
return result;
}
bool isGameGear = _console->GetModel() == SmsModel::GameGear;
bool isTextMode = !state.UseMode4 && state.M1_Use224LineMode;
DebugTilemapInfo result = {};
result.Bpp = state.UseMode4 ? 4 : 1;
result.Format = state.UseMode4 ? TileFormat::SmsBpp4 : TileFormat::SmsSgBpp1;
result.TileWidth = isTextMode ? 6 : 8;
result.TileHeight = 8;
result.ColumnCount = isTextMode ? 40 : 32;
result.RowCount = state.NametableHeight / 8;
result.TilesetAddress = 0;
result.ScrollWidth = isGameGear ? 160 : 256;
result.ScrollHeight = isGameGear ? 144 : state.VisibleScanlineCount;
uint8_t colorMask = 0xFF;
if(options.DisplayMode == TilemapDisplayMode::Grayscale) {
palette = (uint32_t*)_grayscaleColorsBpp4;
colorMask = 0x0F;
}
if(state.UseMode4) {
result.ScrollX = 256 - state.HorizontalScroll + (isGameGear ? 48 : 0);
result.ScrollY = state.VerticalScroll + (isGameGear ? 24 : 0);
result.TilemapAddress = state.EffectiveNametableAddress;
for(uint8_t row = 0; row < result.RowCount; row++) {
for(uint8_t column = 0; column < 32; column++) {
uint16_t entryAddr = state.EffectiveNametableAddress + ((row * 32 + column) * 2);
uint16_t ntData = vram[entryAddr] | (vram[entryAddr + 1] << 8);
uint8_t paletteOffset = ntData & 0x800 ? 0x10 : 0;
bool hMirror = ntData & 0x200;
bool vMirror = ntData & 0x400;
uint16_t tileIndex = ntData & 0x1FF;
for(int y = 0; y < 8; y++) {
uint8_t tileRow = vMirror ? 7 - (y & 0x07) : (y & 0x07);
uint16_t tileAddr = tileIndex * 32 + tileRow * 4;
for(int x = 0; x < 8; x++) {
uint8_t tileColumn = hMirror ? 7 - (x & 0x07) : (x & 0x07);
uint8_t color = GetTilePixelColor<TileFormat::SmsBpp4>(vram, 0x3FFF, tileAddr, tileColumn);
uint16_t palAddr = color == 0 ? 0 : (paletteOffset + color);
uint32_t outPos = (row * 8 + y) * 32 * 8 + column * 8 + x;
outBuffer[outPos] = palette[palAddr & colorMask];
}
}
}
}
} else if(state.M1_Use224LineMode) {
//Text mode (mode 1)
result.TilemapAddress = state.NametableAddress;
for(uint8_t row = 0; row < result.RowCount; row++) {
for(uint8_t column = 0; column < result.ColumnCount; column++) {
uint16_t ntAddr = state.NametableAddress + (column + row * 40);
uint16_t tileIndex = vram[ntAddr];
uint16_t tileAddr = (state.BgPatternTableAddress & 0x3800) + (tileIndex * 8);
for(int y = 0; y < 8; y++) {
for(int x = 0; x < 6; x++) {
uint32_t outPos = (row * 8 + y) * 40 * 6 + column * 6 + x;
uint8_t colorBit = GetTilePixelColor<TileFormat::SmsSgBpp1>(vram, 0x3FFF, tileAddr + y, x);
outBuffer[outPos] = palette[colorBit ? state.TextColorIndex : state.BackgroundColorIndex];
}
}
}
}
} else {
//Graphic 1 / Graphic 2 / Multicolor
result.TilemapAddress = state.NametableAddress;
for(uint8_t row = 0; row < result.RowCount; row++) {
for(uint8_t column = 0; column < result.ColumnCount; column++) {
uint16_t ntAddr = state.NametableAddress + (column + row * 32);
uint16_t tileIndex = vram[ntAddr];
uint16_t tileAddr;
if(state.M3_Use240LineMode) {
tileAddr = (state.BgPatternTableAddress & 0x3800) + (tileIndex * 8) + (row & 0x03) * 2;
} else if(state.M2_AllowHeightChange) {
//Move to the next 256 tiles after every 8 tile rows
tileIndex += (row & 0x18) << 5;
uint16_t mask = ((state.BgPatternTableAddress >> 3) | 0xFF) & 0x3FF;
tileAddr = (state.BgPatternTableAddress & 0x2000) + ((tileIndex & mask) * 8);
} else {
tileAddr = (state.BgPatternTableAddress & 0x3800) + (tileIndex * 8);
}
uint16_t colorTableAddr;
if(state.M2_AllowHeightChange) {
uint16_t mask = ((state.ColorTableAddress >> 3) | 0x07) & 0x3FF;
colorTableAddr = (state.ColorTableAddress & 0x2000) | ((tileIndex & mask) << 3);
} else {
colorTableAddr = (state.ColorTableAddress & 0x3FC0) | ((tileIndex >> 3) & 0x1F);
}
for(int y = 0; y < 8; y++) {
uint8_t color;
if(state.M3_Use240LineMode) {
color = vram[tileAddr + (y >= 4 ? 1 : 0)];
} else if(state.M2_AllowHeightChange) {
color = vram[colorTableAddr + y];
} else {
color = vram[colorTableAddr];
}
for(int x = 0; x < 8; x++) {
uint32_t outPos = (row * 8 + y) * 32 * 8 + column * 8 + x;
uint8_t pixelColor;
if(state.M3_Use240LineMode) {
pixelColor = x < 4 ? (color >> 4) : (color & 0xF);
} else {
uint8_t colorBit = GetTilePixelColor<TileFormat::SmsSgBpp1>(vram, 0x3FFF, tileAddr + y, x);
pixelColor = colorBit ? (color >> 4) : (color & 0xF);
}
outBuffer[outPos] = palette[pixelColor];
}
}
}
}
}
return result;
}
DebugTilemapTileInfo SmsVdpTools::GetTilemapTileInfo(uint32_t x, uint32_t y, uint8_t* vram, GetTilemapOptions options, BaseState& baseState, BaseState& ppuToolsState)
{
DebugTilemapTileInfo result = {};
FrameInfo size = GetTilemapSize(options, baseState);
if(x >= size.Width || y >= size.Height || options.Layer == 1) {
return result;
}
SmsVdpState& state = (SmsVdpState&)baseState;
bool isTextMode = !state.UseMode4 && state.M1_Use224LineMode;
result.Width = isTextMode ? 6 : 8;
result.Height = 8;
uint8_t row = y / 8;
uint8_t column = x / result.Width;
result.Row = row;
result.Column = column;
if(state.UseMode4) {
uint16_t ntIndex = (row << 5) + column;
uint16_t entryAddr = state.EffectiveNametableAddress + (ntIndex * 2);
uint16_t ntData = vram[entryAddr] | (vram[entryAddr + 1] << 8);
uint8_t paletteOffset = ntData & 0x800 ? 0x10 : 0;
uint16_t tileIndex = ntData & 0x1FF;
result.TileMapAddress = entryAddr;
result.TileIndex = tileIndex;
result.TileAddress = tileIndex * 32;
result.PaletteIndex = paletteOffset >> 4;
result.PaletteAddress = paletteOffset;
result.HighPriority = (ntData & 0x1000) ? NullableBoolean::True : NullableBoolean::False;
result.VerticalMirroring = (ntData & 0x400) ? NullableBoolean::True : NullableBoolean::False;
result.HorizontalMirroring = (ntData & 0x200) ? NullableBoolean::True : NullableBoolean::False;
} else if(state.M1_Use224LineMode) {
//Text mode (mode 1)
uint16_t ntAddr = state.NametableAddress + (column + row * 40);
int32_t tileIndex = vram[ntAddr];
uint16_t tileAddr = (state.BgPatternTableAddress & 0x3800) + (tileIndex * 8);
result.TileMapAddress = ntAddr;
result.TileIndex = tileIndex;
result.TileAddress = tileAddr;
} else {
//Graphic 1 / Graphic 2 / Multicolor
uint16_t ntAddr = state.NametableAddress + (column + row * 32);
int32_t tileIndex = vram[ntAddr];
uint16_t tileAddr;
if(state.M3_Use240LineMode) {
tileAddr = (state.BgPatternTableAddress & 0x3800) + (tileIndex * 8) + (row & 0x03) * 2;
tileIndex = -1;
} else if(state.M2_AllowHeightChange) {
//Move to the next 256 tiles after every 8 tile rows
tileIndex += (row & 0x18) << 5;
uint16_t mask = ((state.BgPatternTableAddress >> 3) | 0xFF) & 0x3FF;
tileAddr = (state.BgPatternTableAddress & 0x2000) + ((tileIndex & mask) * 8);
} else {
tileAddr = (state.BgPatternTableAddress & 0x3800) + (tileIndex * 8);
}
result.TileMapAddress = ntAddr;
result.TileIndex = tileIndex;
result.TileAddress = tileAddr;
int32_t colorTableAddr;
if(state.M3_Use240LineMode) {
colorTableAddr = -1;
} else if(state.M2_AllowHeightChange) {
uint16_t mask = ((state.ColorTableAddress >> 3) | 0x07) & 0x3FF;
colorTableAddr = (state.ColorTableAddress & 0x2000) | ((tileIndex & mask) << 3);
} else {
colorTableAddr = (state.ColorTableAddress & 0x3FC0) | ((tileIndex >> 3) & 0x1F);
}
result.PaletteAddress = colorTableAddr;
}
return result;
}
void SmsVdpTools::GetSpritePreview(GetSpritePreviewOptions options, BaseState& baseState, DebugSpriteInfo* sprites, uint32_t* spritePreviews, uint32_t* palette, uint32_t* outBuffer)
{
SmsVdpState& state = (SmsVdpState&)baseState;
uint32_t bgColor = GetSpriteBackgroundColor(options.Background, palette, false);
std::fill(outBuffer, outBuffer + 256 * state.VisibleScanlineCount, bgColor);
std::fill(outBuffer + 256 * state.VisibleScanlineCount, outBuffer + 256 * 256, GetSpriteBackgroundColor(options.Background, palette, true));
if(_console->GetModel() == SmsModel::GameGear) {
std::fill(outBuffer, outBuffer + 24 * 256, GetSpriteBackgroundColor(options.Background, palette, true));
std::fill(outBuffer + 168 * 256, outBuffer + state.VisibleScanlineCount * 256, GetSpriteBackgroundColor(options.Background, palette, true));
for(int i = 0; i < state.VisibleScanlineCount; i++) {
std::fill(outBuffer + i * 256, outBuffer + i * 256 + 48, GetSpriteBackgroundColor(options.Background, palette, true));
std::fill(outBuffer + i * 256 + 208, outBuffer + i * 256 + 256, GetSpriteBackgroundColor(options.Background, palette, true));
}
}
int spriteCount = state.UseMode4 ? 64 : 32;
for(int i = spriteCount - 1; i >= 0; i--) {
DebugSpriteInfo& sprite = sprites[i];
uint32_t* spritePreview = spritePreviews + i * _spritePreviewSize;
int spritePosY = sprite.Y + 1;
for(int y = 0; y < sprite.Height; y++) {
for(int x = 0; x < sprite.Width; x++) {
if(spritePosY + y >= 256) {
spritePosY -= 256;
}
uint32_t color = spritePreview[y * sprite.Width + x];
if(color != 0) {
if(sprite.X + x >= 256 || sprite.Visibility == SpriteVisibility::Disabled) {
continue;
}
//TODOSMS zoomed sprites support
outBuffer[((spritePosY + y) * 256) + sprite.X + x] = color;
} else {
spritePreview[y * sprite.Width + x] = bgColor;
}
}
}
}
}
DebugSpritePreviewInfo SmsVdpTools::GetSpritePreviewInfo(GetSpritePreviewOptions options, BaseState& baseState, BaseState& ppuToolsState)
{
SmsVdpState& state = (SmsVdpState&)baseState;
DebugSpritePreviewInfo info = {};
info.Height = 256;
info.Width = 256;
info.SpriteCount = state.UseMode4 ? 64 : 32;
info.CoordOffsetX = 0;
info.CoordOffsetY = 1;
if(_console->GetModel() == SmsModel::GameGear) {
info.VisibleX = 48;
info.VisibleY = 24;
info.VisibleWidth = 160;
info.VisibleHeight = 144;
} else {
info.VisibleX = 0;
info.VisibleY = 0;
info.VisibleWidth = 256;
info.VisibleHeight = state.VisibleScanlineCount;
}
info.WrapBottomToTop = true;
return info;
}
void SmsVdpTools::GetSpriteInfo(DebugSpriteInfo& sprite, uint32_t* spritePreview, uint16_t i, GetSpritePreviewOptions& options, SmsVdpState& state, uint8_t* vram, uint8_t* oamRam, uint32_t* palette)
{
uint8_t* oam = oamRam ? oamRam : (vram + (state.SpriteTableAddress & (state.UseMode4 ? 0x3F00 : 0x3FFF)));
sprite.Bpp = state.UseMode4 ? 4 : 1;
sprite.Format = state.UseMode4 ? TileFormat::SmsBpp4 : TileFormat::SmsSgBpp1;
sprite.SpriteIndex = i;
sprite.UseExtendedVram = false;
sprite.RawY = state.UseMode4 ? oam[i] : oam[i*4];
sprite.RawX = state.UseMode4 ? oam[0x80+i*2] : oam[i*4+1];
sprite.Y = sprite.RawY;
sprite.X = sprite.RawX;
if(state.UseMode4) {
if(state.ShiftSpritesLeft) {
sprite.X -= 8;
}
} else {
if(oam[i * 4 + 3] & 0x80) {
sprite.X -= 32;
}
}
sprite.TileIndex = state.UseMode4 ? oam[0x80 + i * 2 + 1] : oam[i*4+2];
sprite.UseSecondTable = NullableBoolean::Undefined;
sprite.Palette = state.UseMode4 ? 0 : (oam[i*4+3] & 0x0F);
sprite.PaletteAddress = state.UseMode4 ? 0x10 : -1;
sprite.Priority = DebugSpritePriority::Undefined;
bool largeSprites = state.UseLargeSprites;
sprite.Width = 8;
sprite.Height = largeSprites ? 16 : 8;
if(!state.UseMode4) {
sprite.Width = sprite.Height;
}
if(!state.UseMode4 && sprite.Palette == 0) {
sprite.Visibility = SpriteVisibility::Disabled;
} else if(sprite.Y < state.VisibleScanlineCount || (sprite.Y > state.VisibleScanlineCount && (uint8_t)(sprite.Y + sprite.Height) >= 0)) {
sprite.Visibility = SpriteVisibility::Visible;
} else {
sprite.Visibility = SpriteVisibility::Offscreen;
}
uint16_t tileIndex = sprite.TileIndex;
if(largeSprites) {
tileIndex &= state.UseMode4 ? ~0x01 : ~0x03;
}
uint16_t tileStart = (tileIndex * (state.UseMode4 ? 32 : 8)) | (state.SpritePatternSelector & (state.UseMode4 ? 0x2000 : ~0));
sprite.TileAddresses[0] = tileStart;
if(largeSprites) {
if(state.UseMode4) {
sprite.TileAddresses[1] = tileStart + 32;
sprite.TileCount = 2;
} else {
sprite.TileAddresses[1] = tileStart + 8;
sprite.TileAddresses[2] = tileStart + 16;
sprite.TileAddresses[3] = tileStart + 24;
sprite.TileCount = 4;
}
} else {
sprite.TileCount = 1;
}
sprite.TileAddress = tileStart;
for(int y = 0; y < sprite.Height; y++) {
uint16_t pixelStart = tileStart + y * (state.UseMode4 ? 4 : 1);
for(int x = 0; x < sprite.Width; x++) {
uint8_t color;
if(state.UseMode4) {
color = GetTilePixelColor<TileFormat::SmsBpp4>(vram, 0x3FFF, pixelStart, x);
} else {
color = GetTilePixelColor<TileFormat::SmsSgBpp1>(vram, 0x3FFF, pixelStart + (x >= 8 ? 16 : 0), x & 0x07);
}
uint32_t outOffset = (y * sprite.Width) + x;
if(color > 0) {
spritePreview[outOffset] = state.UseMode4 ? palette[0x10 + color] : (sprite.Palette ? palette[sprite.Palette] : 0);
} else {
spritePreview[outOffset] = 0;
}
}
}
}
void SmsVdpTools::GetSpriteList(GetSpritePreviewOptions options, BaseState& baseState, BaseState& ppuToolsState, uint8_t* vram, uint8_t* oamRam, uint32_t* palette, DebugSpriteInfo outBuffer[], uint32_t* spritePreviews, uint32_t* screenPreview)
{
SmsVdpState& state = (SmsVdpState&)baseState;
int spriteCount = state.UseMode4 ? 64 : 32;
bool disableSprites = false;
for(int i = 0; i < spriteCount; i++) {
outBuffer[i].Init();
GetSpriteInfo(outBuffer[i], spritePreviews + (i * _spritePreviewSize), i, options, state, vram, oamRam, palette);
disableSprites |= outBuffer[i].RawY == 0xD0;
if(disableSprites) {
outBuffer[i].Visibility = SpriteVisibility::Disabled;
}
}
GetSpritePreview(options, baseState, outBuffer, spritePreviews, palette, screenPreview);
}
DebugPaletteInfo SmsVdpTools::GetPaletteInfo(GetPaletteInfoOptions options)
{
DebugPaletteInfo info = {};
SmsVdpState state;
_debugger->GetPpuState(state, CpuType::Sms);
info.ColorCount = 16;
info.BgColorCount = 16;
info.SpriteColorCount = 16;
info.ColorsPerPalette = 16;
if(state.UseMode4) {
info.ColorCount = 32;
info.SpritePaletteOffset = info.BgColorCount;
info.HasMemType = true;
info.PaletteMemType = MemoryType::SmsPaletteRam;
info.RawFormat = _console->GetModel() == SmsModel::GameGear ? RawPaletteFormat::Rgb444 : RawPaletteFormat::Rgb222;
uint8_t* paletteRam = _debugger->GetMemoryDumper()->GetMemoryBuffer(MemoryType::SmsPaletteRam);
if(info.RawFormat == RawPaletteFormat::Rgb222) {
for(int i = 0; i < 32; i++) {
info.RawPalette[i] = paletteRam[i];
info.RgbPalette[i] = ColorUtilities::Rgb222ToArgb(paletteRam[i]);
}
} else {
for(int i = 0; i < 32; i++) {
info.RawPalette[i] = paletteRam[i * 2] | (paletteRam[i * 2 + 1] << 8);
info.RgbPalette[i] = ColorUtilities::Rgb444ToArgb(info.RawPalette[i]);
}
}
} else {
info.ColorCount = 16;
info.SpritePaletteOffset = 0;
info.RawFormat = RawPaletteFormat::Indexed;
for(int i = 0; i < 16; i++) {
info.RawPalette[i] = _console->GetVdp()->GetSmsSgPalette()[i];
info.RgbPalette[i] = ColorUtilities::Rgb555ToArgb(info.RawPalette[i]);
}
}
return info;
}
void SmsVdpTools::SetPaletteColor(int32_t colorIndex, uint32_t color)
{
if(_console->GetModel() == SmsModel::GameGear) {
uint8_t r = (color >> 20) & 0x0F;
uint8_t g = (color >> 12) & 0x0F;
uint8_t b = (color >> 4) & 0x0F;
uint16_t rgb444 = (b << 8) | (g << 4) | r;
_debugger->GetMemoryDumper()->SetMemoryValue(MemoryType::SmsPaletteRam, colorIndex * 2, rgb444 & 0xFF);
_debugger->GetMemoryDumper()->SetMemoryValue(MemoryType::SmsPaletteRam, colorIndex * 2 + 1, (rgb444 >> 8));
} else {
if(color < 0x3F) {
_debugger->GetMemoryDumper()->SetMemoryValue(MemoryType::SmsPaletteRam, colorIndex, (uint8_t)color);
}
}
}