BizHawk/BizHawk.Client.EmuHawk/tools/NES/NESNameTableViewer.cs

351 lines
8.3 KiB
C#

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Windows.Forms;
using BizHawk.Emulation.Cores.Nintendo.NES;
using BizHawk.Emulation.Common;
namespace BizHawk.Client.EmuHawk
{
public partial class NESNameTableViewer : Form, IToolFormAutoConfig
{
// TODO:
// Show Scroll Lines + UI Toggle
[RequiredService]
private INESPPUViewable _ppu { get; set; }
[RequiredService]
private IEmulator _emu { get; set; }
[ConfigPersist]
private int RefreshRateConfig
{
get => RefreshRate.Value;
set => RefreshRate.Value = value;
}
private int _scanline;
public NESNameTableViewer()
{
InitializeComponent();
}
private void NESNameTableViewer_Load(object sender, EventArgs e)
{
Generate(true);
}
#region Public API
public bool AskSaveChanges() => true;
public bool UpdateBefore => true;
public void Restart()
{
Generate(true);
}
public void NewUpdate(ToolFormUpdateType type) { }
public void UpdateValues()
{
_ppu.InstallCallback1(() => Generate(), _scanline);
}
public void FastUpdate()
{
// Do nothing
}
#endregion
private unsafe void DrawTile(int* dst, int pitch, byte* pal, byte* tile, int* finalPal)
{
dst += 7;
int verticalInc = pitch + 8;
for (int j = 0; j < 8; j++)
{
int lo = tile[0];
int hi = tile[8] << 1;
for (int i = 0; i < 8; i++)
{
*dst-- = finalPal[pal[lo & 1 | hi & 2]];
lo >>= 1;
hi >>= 1;
}
dst += verticalInc;
tile++;
}
}
private unsafe void GenerateExAttr(int* dst, int pitch, byte[] palRam, byte[] ppuMem, byte[] exRam)
{
byte[] chr = _ppu.GetExTiles();
int chrMask = chr.Length - 1;
fixed (byte* chrPtr = chr, palPtr = palRam, ppuPtr = ppuMem, exPtr = exRam)
fixed (int* finalPal = _ppu.GetPalette())
{
DrawExNT(dst, pitch, palPtr, ppuPtr + 0x2000, exPtr, chrPtr, chrMask, finalPal);
DrawExNT(dst + 256, pitch, palPtr, ppuPtr + 0x2400, exPtr, chrPtr, chrMask, finalPal);
dst += pitch * 240;
DrawExNT(dst, pitch, palPtr, ppuPtr + 0x2800, exPtr, chrPtr, chrMask, finalPal);
DrawExNT(dst + 256, pitch, palPtr, ppuPtr + 0x2c00, exPtr, chrPtr, chrMask, finalPal);
}
}
private unsafe void GenerateAttr(int* dst, int pitch, byte[] palRam, byte[] ppuMem)
{
fixed (byte* palPtr = palRam, ppuPtr = ppuMem)
fixed (int* finalPal = _ppu.GetPalette())
{
byte* chrPtr = ppuPtr + (_ppu.BGBaseHigh ? 0x1000 : 0);
DrawNT(dst, pitch, palPtr, ppuPtr + 0x2000, chrPtr, finalPal);
DrawNT(dst + 256, pitch, palPtr, ppuPtr + 0x2400, chrPtr, finalPal);
dst += pitch * 240;
DrawNT(dst, pitch, palPtr, ppuPtr + 0x2800, chrPtr, finalPal);
DrawNT(dst + 256, pitch, palPtr, ppuPtr + 0x2c00, chrPtr, finalPal);
}
}
private unsafe void DrawNT(int* dst, int pitch, byte* palRam, byte* nt, byte* chr, int* finalPal)
{
byte* at = nt + 0x3c0;
for (int ty = 0; ty < 30; ty++)
{
for (int tx = 0; tx < 32; tx++)
{
byte t = *nt++;
byte a = at[ty >> 2 << 3 | tx >> 2];
a >>= tx & 2;
a >>= (ty & 2) << 1;
int palNum = a & 3;
int tileAddr = t << 4;
DrawTile(dst, pitch, palRam + palNum * 4, chr + tileAddr, finalPal);
dst += 8;
}
dst -= 256;
dst += pitch * 8;
}
}
private unsafe void DrawExNT(int* dst, int pitch, byte* palRam, byte* nt, byte* exNt, byte* chr, int chrMask, int* finalPal)
{
for (int ty = 0; ty < 30; ty++)
{
for (int tx = 0; tx < 32; tx++)
{
byte t = *nt++;
byte ex = *exNt++;
int tileNum = t | (ex & 0x3f) << 8;
int palNum = ex >> 6;
int tileAddr = tileNum << 4 & chrMask;
DrawTile(dst, pitch, palRam + palNum * 4, chr + tileAddr, finalPal);
dst += 8;
}
dst -= 256;
dst += pitch * 8;
}
}
private unsafe void Generate(bool now = false)
{
if (!IsHandleCreated || IsDisposed)
{
return;
}
if (now == false && _emu.Frame % RefreshRate.Value != 0)
{
return;
}
var bmpData = NameTableView.Nametables.LockBits(
new Rectangle(0, 0, 512, 480),
ImageLockMode.WriteOnly,
PixelFormat.Format32bppArgb);
var dPtr = (int*)bmpData.Scan0.ToPointer();
var pitch = bmpData.Stride / 4;
// Buffer all the data from the ppu, because it will be read multiple times and that is slow
var ppuBuffer = _ppu.GetPPUBus();
var palRam = _ppu.GetPalRam();
if (_ppu.ExActive)
{
byte[] exRam = _ppu.GetExRam();
GenerateExAttr(dPtr, pitch, palRam, ppuBuffer, exRam);
}
else
{
GenerateAttr(dPtr, pitch, palRam, ppuBuffer);
}
NameTableView.Nametables.UnlockBits(bmpData);
NameTableView.Refresh();
}
#region Events
#region Menu and Context Menu
private void ScreenshotMenuItem_Click(object sender, EventArgs e)
{
NameTableView.Screenshot();
}
private void ScreenshotToClipboardMenuItem_Click(object sender, EventArgs e)
{
NameTableView.ScreenshotToClipboard();
}
private void ExitMenuItem_Click(object sender, EventArgs e)
{
Close();
}
private void RefreshImageContextMenuItem_Click(object sender, EventArgs e)
{
UpdateValues();
NameTableView.Refresh();
}
#endregion
#region Dialog and Controls
private void NesNameTableViewer_KeyDown(object sender, KeyEventArgs e)
{
switch (e.KeyCode)
{
case Keys.C:
if (e.Modifiers == Keys.Control)
{
NameTableView.ScreenshotToClipboard();
}
break;
}
}
private void NESNameTableViewer_FormClosed(object sender, FormClosedEventArgs e)
{
_ppu?.RemoveCallback1();
}
private void ScanlineTextBox_TextChanged(object sender, EventArgs e)
{
if (int.TryParse(txtScanline.Text, out _scanline))
{
_ppu.InstallCallback1(() => Generate(), _scanline);
}
}
private void NametableRadio_CheckedChanged(object sender, EventArgs e)
{
if (rbNametableNW.Checked)
{
NameTableView.Which = NameTableViewer.WhichNametable.NT_2000;
}
if (rbNametableNE.Checked)
{
NameTableView.Which = NameTableViewer.WhichNametable.NT_2400;
}
if (rbNametableSW.Checked)
{
NameTableView.Which = NameTableViewer.WhichNametable.NT_2800;
}
if (rbNametableSE.Checked)
{
NameTableView.Which = NameTableViewer.WhichNametable.NT_2C00;
}
if (rbNametableAll.Checked)
{
NameTableView.Which = NameTableViewer.WhichNametable.NT_ALL;
}
}
private void NameTableView_MouseMove(object sender, MouseEventArgs e)
{
int tileX, tileY, nameTable;
if (NameTableView.Which == NameTableViewer.WhichNametable.NT_ALL)
{
tileX = e.X / 8;
tileY = e.Y / 8;
nameTable = (tileX / 32) + ((tileY / 30) * 2);
}
else
{
switch (NameTableView.Which)
{
default:
case NameTableViewer.WhichNametable.NT_2000:
nameTable = 0;
break;
case NameTableViewer.WhichNametable.NT_2400:
nameTable = 1;
break;
case NameTableViewer.WhichNametable.NT_2800:
nameTable = 2;
break;
case NameTableViewer.WhichNametable.NT_2C00:
nameTable = 3;
break;
}
tileX = e.X / 16;
tileY = e.Y / 16;
}
XYLabel.Text = $"{tileX} : {tileY}";
int ppuAddress = 0x2000 + (nameTable * 0x400) + ((tileY % 30) * 32) + (tileX % 32);
PPUAddressLabel.Text = $"{ppuAddress:X4}";
int tileID = _ppu.PeekPPU(ppuAddress);
TileIDLabel.Text = $"{tileID:X2}";
TableLabel.Text = nameTable.ToString();
int yTable = 0, yLine = 0;
if (e.Y >= 240)
{
yTable += 2;
yLine = 240;
}
int table = (e.X >> 8) + yTable;
int ntAddr = (table << 10);
int px = e.X & 255;
int py = e.Y - yLine;
int tx = px >> 3;
int ty = py >> 3;
int atBytePtr = ntAddr + 0x3C0 + ((ty >> 2) << 3) + (tx >> 2);
int at = _ppu.PeekPPU(atBytePtr + 0x2000);
if ((ty & 2) != 0) at >>= 4;
if ((tx & 2) != 0) at >>= 2;
at &= 0x03;
PaletteLabel.Text = at.ToString();
}
private void NameTableView_MouseLeave(object sender, EventArgs e)
{
XYLabel.Text = "";
PPUAddressLabel.Text = "";
TileIDLabel.Text = "";
TableLabel.Text = "";
PaletteLabel.Text = "";
}
#endregion
#endregion
}
}