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

852 lines
20 KiB
C#

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Windows.Forms;
using BizHawk.Client.Common;
using BizHawk.Emulation.Cores.Nintendo.NES;
using BizHawk.Emulation.Common;
using System.Collections.Generic;
namespace BizHawk.Client.EmuHawk
{
public partial class NesPPU : Form, IToolFormAutoConfig
{
// TODO:
// If 8/16 sprite mode, mouse over should put 32x64 version of prite
// Speedups
// Smarter refreshing? only refresh when things of changed, perhaps peek at the ppu to when the pattern table has changed, or sprites have moved
// Maybe 48 individual bitmaps for sprites is faster than the overhead of redrawing all that transparent space
private readonly byte[] _ppuBusprev = new byte[0x3000];
private readonly byte[] _palRamPrev = new byte[0x20];
int scanline;
private Bitmap _zoomBoxDefaultImage = new Bitmap(64, 64);
private bool _forceChange;
[RequiredService]
private INESPPUViewable _ppu { get; set; }
[RequiredService]
private IEmulator _emu { get; set; }
[ConfigPersist]
private int RefreshRateConfig
{
get { return RefreshRate.Value; }
set { RefreshRate.Value = value; }
}
private bool _chrromview = false;
[ConfigPersist]
private bool ChrRomView
{
get { return _chrromview; }
set { _chrromview = value; CalculateFormSize(); }
}
public NesPPU()
{
InitializeComponent();
CalculateFormSize();
}
private void NesPPU_Load(object sender, EventArgs e)
{
ClearDetails();
Generate(true);
CHRROMViewReload();
}
#region Public API
public bool AskSaveChanges() { return true; }
public bool UpdateBefore { get { return true; } }
public void NewUpdate(ToolFormUpdateType type) { }
public void UpdateValues()
{
_ppu.InstallCallback2(() => Generate(), scanline);
}
public void FastUpdate()
{
// Do nothing
}
public void Restart()
{
Generate(true);
CHRROMViewReload();
}
#endregion
private byte GetBit(byte[] PPUBus, int address, int bit)
{
return (byte)((PPUBus[address] >> (7 - bit)) & 1);
}
private bool CheckChange(byte[] PALRAM, byte[] PPUBus)
{
bool changed = false;
for (int i = 0; i < 0x20; i++)
{
if (_palRamPrev[i] != PALRAM[i])
{
changed = true;
break;
}
}
if (!changed)
{
for (int i = 0; i < 0x2000; i++)
{
if (_ppuBusprev[i] != PPUBus[i])
{
changed = true;
break;
}
}
}
Buffer.BlockCopy(PALRAM, 0, _palRamPrev, 0, 0x20);
Buffer.BlockCopy(PPUBus, 0, _ppuBusprev, 0, 0x3000);
if (_forceChange)
{
return true;
}
return changed;
}
private unsafe void DrawPatternView(PatternViewer dest, byte[] src, int[] FinalPalette, byte[] PALRAM)
{
int b0;
int b1;
byte value;
int cvalue;
var bmpdata = dest.pattern.LockBits(
new Rectangle(new Point(0, 0), dest.pattern.Size),
ImageLockMode.WriteOnly,
PixelFormat.Format32bppArgb);
int* framebuf = (int*)bmpdata.Scan0;
for (int z = 0; z < 2; z++)
{
int pal;
pal = z == 0 ? PatternView.Pal0 : PatternView.Pal1; // change?
for (int i = 0; i < 16; i++)
{
for (int j = 0; j < 16; j++)
{
for (int x = 0; x < 8; x++)
{
for (int y = 0; y < 8; y++)
{
int address = (z << 12) + (i << 8) + (j << 4) + y;
b0 = (byte)((src[address] >> (7 - x)) & 1);
b1 = (byte)((src[address + 8] >> (7 - x)) & 1);
value = (byte)(b0 + (b1 << 1));
cvalue = FinalPalette[PALRAM[value + (pal << 2)]];
int adr = (x + (j << 3)) + (y + (i << 3)) * (bmpdata.Stride >> 2);
framebuf[adr + (z << 7)] = cvalue;
}
}
}
}
}
dest.pattern.UnlockBits(bmpdata);
dest.Refresh();
}
private unsafe void Generate(bool now = false)
{
if (!IsHandleCreated || IsDisposed)
{
return;
}
if (_emu.Frame % RefreshRate.Value != 0 && !now)
return;
byte[] PALRAM = _ppu.GetPalRam();
int[] FinalPalette = _ppu.GetPalette();
byte[] OAM = _ppu.GetOam();
byte[] PPUBus = _ppu.GetPPUBus();
int b0;
int b1;
byte value;
int cvalue;
if (CheckChange(PALRAM, PPUBus))
{
_forceChange = false;
// Pattern Viewer
for (var i = 0; i < 16; i++)
{
PaletteView.BgPalettesPrev[i].Value = PaletteView.BgPalettes[i].Value;
PaletteView.SpritePalettesPrev[i].Value = PaletteView.SpritePalettes[i].Value;
PaletteView.BgPalettes[i].Value = FinalPalette[PALRAM[PaletteView.BgPalettes[i].Address]];
PaletteView.SpritePalettes[i].Value = FinalPalette[PALRAM[PaletteView.SpritePalettes[i].Address]];
}
if (PaletteView.HasChanged())
{
PaletteView.Refresh();
}
DrawPatternView(PatternView, PPUBus, FinalPalette, PALRAM);
}
var bmpdata2 = SpriteView.sprites.LockBits(new Rectangle(new Point(0, 0), SpriteView.sprites.Size), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
var framebuf2 = (int*)bmpdata2.Scan0.ToPointer();
int pt_add = _ppu.SPBaseHigh ? 0x1000 : 0;
bool is8x16 = _ppu.SPTall;
// Sprite Viewer
for (int n = 0; n < 4; n++)
{
for (int r = 0; r < 16; r++)
{
int BaseAddr = (r << 2) + (n << 6);
int TileNum = OAM[BaseAddr + 1];
int patternAddr;
if (is8x16)
{
patternAddr = (TileNum >> 1) * 0x20;
patternAddr += 0x1000 * (TileNum & 1);
}
else
{
patternAddr = TileNum * 0x10;
patternAddr += pt_add;
}
int Attributes = OAM[BaseAddr + 2];
int Palette = Attributes & 0x03;
for (int x = 0; x < 8; x++)
{
for (int y = 0; y < 8; y++)
{
int address = patternAddr + y;
b0 = (byte)((PPUBus[address] >> (7 - x)) & 1);
b1 = (byte)((PPUBus[address + 8] >> (7 - x)) & 1);
value = (byte)(b0 + (b1 << 1));
cvalue = FinalPalette[PALRAM[16 + value + (Palette << 2)]];
int adr = (x + (r * 16)) + (y + (n * 24)) * (bmpdata2.Stride >> 2);
framebuf2[adr] = cvalue;
}
if (is8x16)
{
patternAddr += 0x10;
for (int y = 0; y < 8; y++)
{
int address = patternAddr + y;
b0 = (byte)((PPUBus[address] >> (7 - x)) & 1);
b1 = (byte)((PPUBus[address + 8] >> (7 - x)) & 1);
value = (byte)(b0 + (b1 << 1));
cvalue = FinalPalette[PALRAM[16 + value + (Palette << 2)]];
int adr = (x + (r << 4)) + ((y + 8) + (n * 24)) * (bmpdata2.Stride >> 2);
framebuf2[adr] = cvalue;
}
patternAddr -= 0x10;
}
}
}
}
SpriteView.sprites.UnlockBits(bmpdata2);
SpriteView.Refresh();
HandleSpriteViewMouseMove(SpriteView.PointToClient(MousePosition));
HandlePaletteViewMouseMove(PaletteView.PointToClient(MousePosition));
}
private void ClearDetails()
{
DetailsBox.Text = "Details";
AddressLabel.Text = string.Empty;
ValueLabel.Text = string.Empty;
Value2Label.Text = string.Empty;
Value3Label.Text = string.Empty;
Value4Label.Text = string.Empty;
Value5Label.Text = string.Empty;
ZoomBox.Image = _zoomBoxDefaultImage;
}
private void UpdatePaletteSelection()
{
_forceChange = true;
Table0PaletteLabel.Text = "Palette: " + PatternView.Pal0;
Table1PaletteLabel.Text = "Palette: " + PatternView.Pal1;
}
private static Bitmap Section(Image srcBitmap, Rectangle section, bool is8x16)
{
// Create the new bitmap and associated graphics object
var bmp = new Bitmap(64, 64);
var g = Graphics.FromImage(bmp);
// Draw the specified section of the source bitmap to the new one
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
g.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.Half;
var rect = is8x16 ? new Rectangle(0, 0, 32, 64) : new Rectangle(0, 0, 64, 64);
g.DrawImage(srcBitmap, rect, section, GraphicsUnit.Pixel);
g.Dispose();
return bmp;
}
private void HandleDefaultImage()
{
if (ModifierKeys == Keys.Shift)
{
_zoomBoxDefaultImage = ZoomBox.Image as Bitmap;
}
}
#region Events
#region Menu and Context Menu
#region File
private void SavePaletteScreenshotMenuItem_Click(object sender, EventArgs e)
{
PaletteView.Screenshot();
}
private void SavePatternScreenshotMenuItem_Click(object sender, EventArgs e)
{
PatternView.Screenshot();
}
private void SaveSpriteScreenshotMenuItem_Click(object sender, EventArgs e)
{
SpriteView.Screenshot();
}
private void CopyPaletteToClipboardMenuItem_Click(object sender, EventArgs e)
{
PaletteView.ScreenshotToClipboard();
}
private void CopyPatternToClipboardMenuItem_Click(object sender, EventArgs e)
{
PatternView.ScreenshotToClipboard();
}
private void CopySpriteToClipboardMenuItem_Click(object sender, EventArgs e)
{
SpriteView.ScreenshotToClipboard();
}
private void ExitMenuItem_Click(object sender, EventArgs e)
{
Close();
}
#endregion
#region Pattern
private void Table0PaletteSubMenu_DropDownOpened(object sender, EventArgs e)
{
Table0P0MenuItem.Checked = PatternView.Pal0 == 0;
Table0P1MenuItem.Checked = PatternView.Pal0 == 1;
Table0P2MenuItem.Checked = PatternView.Pal0 == 2;
Table0P3MenuItem.Checked = PatternView.Pal0 == 3;
Table0P4MenuItem.Checked = PatternView.Pal0 == 4;
Table0P5MenuItem.Checked = PatternView.Pal0 == 5;
Table0P6MenuItem.Checked = PatternView.Pal0 == 6;
Table0P7MenuItem.Checked = PatternView.Pal0 == 7;
}
private void Table1PaletteSubMenu_DropDownOpened(object sender, EventArgs e)
{
Table1P0MenuItem.Checked = PatternView.Pal1 == 0;
Table1P1MenuItem.Checked = PatternView.Pal1 == 1;
Table1P2MenuItem.Checked = PatternView.Pal1 == 2;
Table1P3MenuItem.Checked = PatternView.Pal1 == 3;
Table1P4MenuItem.Checked = PatternView.Pal1 == 4;
Table1P5MenuItem.Checked = PatternView.Pal1 == 5;
Table1P6MenuItem.Checked = PatternView.Pal1 == 6;
Table1P7MenuItem.Checked = PatternView.Pal1 == 7;
}
private void Palette_Click(object sender, EventArgs e)
{
if (sender == Table0P0MenuItem)
{
PatternView.Pal0 = 0;
}
else if (sender == Table0P1MenuItem)
{
PatternView.Pal0 = 1;
}
else if (sender == Table0P2MenuItem)
{
PatternView.Pal0 = 2;
}
else if (sender == Table0P3MenuItem)
{
PatternView.Pal0 = 3;
}
else if (sender == Table0P4MenuItem)
{
PatternView.Pal0 = 4;
}
else if (sender == Table0P5MenuItem)
{
PatternView.Pal0 = 5;
}
else if (sender == Table0P6MenuItem)
{
PatternView.Pal0 = 6;
}
else if (sender == Table0P7MenuItem)
{
PatternView.Pal0 = 7;
}
else if (sender == Table1P0MenuItem)
{
PatternView.Pal1 = 0;
}
else if (sender == Table1P1MenuItem)
{
PatternView.Pal1 = 1;
}
else if (sender == Table1P2MenuItem)
{
PatternView.Pal1 = 2;
}
else if (sender == Table1P3MenuItem)
{
PatternView.Pal1 = 3;
}
else if (sender == Table1P4MenuItem)
{
PatternView.Pal1 = 4;
}
else if (sender == Table1P5MenuItem)
{
PatternView.Pal1 = 5;
}
else if (sender == Table1P6MenuItem)
{
PatternView.Pal1 = 6;
}
else if (sender == Table1P7MenuItem)
{
PatternView.Pal1 = 7;
}
UpdatePaletteSelection();
}
#endregion
#region Settings
private void SettingsSubMenu_DropDownOpened(object sender, EventArgs e)
{
cHRROMTileViewerToolStripMenuItem.Checked = ChrRomView;
}
#endregion
#region Context Menus
private void PaletteRefreshMenuItem_Click(object sender, EventArgs e)
{
PaletteView.Refresh();
}
private void PatternRefreshMenuItem_Click(object sender, EventArgs e)
{
PatternView.Refresh();
}
private void SpriteRefreshMenuItem_Click(object sender, EventArgs e)
{
SpriteView.Refresh();
}
#endregion
#endregion
#region Dialog and Controls
private void NesPPU_MouseClick(object sender, MouseEventArgs e)
{
ZoomBox.Image = new Bitmap(64, 64);
}
private void NesPPU_KeyDown(object sender, KeyEventArgs e)
{
if (ModifierKeys.HasFlag(Keys.Control) && e.KeyCode == Keys.C)
{
// find the control under the mouse
var m = Cursor.Position;
Control top = this;
Control found;
do
{
found = top.GetChildAtPoint(top.PointToClient(m));
top = found;
}
while (found != null && found.HasChildren);
if (found != null)
{
var meth = found.GetType().GetMethod("ScreenshotToClipboard", Type.EmptyTypes);
if (meth != null)
{
meth.Invoke(found, null);
}
else if (found is PictureBox)
{
Clipboard.SetImage((found as PictureBox).Image);
}
else
{
return;
}
toolStripStatusLabel1.Text = found.Text + " copied to clipboard.";
Messagetimer.Stop();
Messagetimer.Start();
}
}
}
private void MessageTimer_Tick(object sender, EventArgs e)
{
Messagetimer.Stop();
toolStripStatusLabel1.Text = "Use CTRL+C to copy the pane under the mouse to the clipboard.";
}
private void PaletteView_MouseClick(object sender, MouseEventArgs e)
{
HandleDefaultImage();
}
private void SpriteView_MouseClick(object sender, MouseEventArgs e)
{
HandleDefaultImage();
}
private void SpriteView_MouseEnter(object sender, EventArgs e)
{
DetailsBox.Text = "Details - Sprites";
}
private void SpriteView_MouseLeave(object sender, EventArgs e)
{
ClearDetails();
}
private void SpriteView_MouseMove(object sender, MouseEventArgs e)
{
HandleSpriteViewMouseMove(e.Location);
}
private void HandleSpriteViewMouseMove(Point e)
{
if (e.X < SpriteView.ClientRectangle.Left) return;
if (e.Y < SpriteView.ClientRectangle.Top) return;
if (e.X >= SpriteView.ClientRectangle.Right) return;
if (e.Y >= SpriteView.ClientRectangle.Bottom) return;
byte[] OAM = _ppu.GetOam();
byte[] PPUBus = _ppu.GetPPUBus(); // caching is quicker, but not really correct in this case
bool is8x16 = _ppu.SPTall;
var spriteNumber = ((e.Y / 24) * 16) + (e.X / 16);
int x = OAM[(spriteNumber * 4) + 3];
int y = OAM[spriteNumber * 4];
var color = OAM[(spriteNumber * 4) + 2] & 0x03;
var attributes = OAM[(spriteNumber * 4) + 2];
var flags = "Flags: ";
int h = GetBit(PPUBus, attributes, 6);
int v = GetBit(PPUBus, attributes, 7);
int priority = GetBit(PPUBus, attributes, 5);
if (h > 0)
{
flags += "H ";
}
if (v > 0)
{
flags += "V ";
}
if (priority > 0)
{
flags += "Behind";
}
else
{
flags += "Front";
}
int tile = OAM[spriteNumber * 4 + 1];
if (is8x16)
{
if ((tile & 1) != 0)
tile += 256;
tile &= ~1;
}
AddressLabel.Text = "Number: " + string.Format("{0:X2}", spriteNumber);
ValueLabel.Text = "X: " + string.Format("{0:X2}", x);
Value2Label.Text = "Y: " + string.Format("{0:X2}", y);
Value3Label.Text = "Tile: " + string.Format("{0:X2}", tile);
Value4Label.Text = "Color: " + color;
Value5Label.Text = flags;
if (is8x16)
{
ZoomBox.Image = Section(
SpriteView.sprites, new Rectangle(new Point((e.X / 8) * 8, (e.Y / 24) * 24), new Size(8, 16)), true);
}
else
{
ZoomBox.Image = Section(
SpriteView.sprites, new Rectangle(new Point((e.X / 8) * 8, (e.Y / 8) * 8), new Size(8, 8)), false);
}
}
private void PaletteView_MouseLeave(object sender, EventArgs e)
{
ClearDetails();
}
private void PaletteView_MouseEnter(object sender, EventArgs e)
{
DetailsBox.Text = "Details - Palettes";
}
private void PaletteView_MouseMove(object sender, MouseEventArgs e)
{
HandlePaletteViewMouseMove(e.Location);
}
private void HandlePaletteViewMouseMove(Point e)
{
if (e.X < PaletteView.ClientRectangle.Left) return;
if (e.Y < PaletteView.ClientRectangle.Top) return;
if (e.X >= PaletteView.ClientRectangle.Right) return;
if (e.Y >= PaletteView.ClientRectangle.Bottom) return;
int baseAddr = 0x3F00;
if (e.Y > 16)
{
baseAddr += 16;
}
int column = e.X / 16;
int addr = column + baseAddr;
AddressLabel.Text = "Address: 0x" + string.Format("{0:X4}", addr);
int val;
var bmp = new Bitmap(64, 64);
var g = Graphics.FromImage(bmp);
byte[] PALRAM = _ppu.GetPalRam();
if (baseAddr == 0x3F00)
{
val = PALRAM[PaletteView.BgPalettes[column].Address];
ValueLabel.Text = "ID: BG" + (column / 4);
g.FillRectangle(new SolidBrush(PaletteView.BgPalettes[column].Color), 0, 0, 64, 64);
}
else
{
val = PALRAM[PaletteView.SpritePalettes[column].Address];
ValueLabel.Text = "ID: SPR" + (column / 4);
g.FillRectangle(new SolidBrush(PaletteView.SpritePalettes[column].Color), 0, 0, 64, 64);
}
g.Dispose();
Value3Label.Text = "Color: 0x" + string.Format("{0:X2}", val);
Value4Label.Text = "Offset: " + (addr & 0x03);
ZoomBox.Image = bmp;
}
private void PatternView_Click(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
if (e.X < PatternView.Width / 2)
{
PatternView.Pal0++;
if (PatternView.Pal0 > 7)
{
PatternView.Pal0 = 0;
}
}
else
{
PatternView.Pal1++;
if (PatternView.Pal1 > 7)
{
PatternView.Pal1 = 0;
}
}
UpdatePaletteSelection();
}
HandleDefaultImage();
}
private void PatternView_MouseEnter(object sender, EventArgs e)
{
DetailsBox.Text = "Details - Patterns";
}
private void PatternView_MouseLeave(object sender, EventArgs e)
{
ClearDetails();
}
private void PatternView_MouseMove(object sender, MouseEventArgs e)
{
int table = 0;
int address;
int tile;
if (e.X > PatternView.Width / 2)
{
table = 1;
}
if (table == 0)
{
tile = (e.X - 1) / 8;
address = tile * 16;
}
else
{
tile = (e.X - 128) / 8;
address = 0x1000 + (tile * 16);
}
address += (e.Y / 8) * 256;
tile += (e.Y / 8) * 16;
var usage = "Usage: ";
if (_ppu.BGBaseHigh == address >= 0x1000) // bghigh
{
usage = "BG";
}
else if (_ppu.SPBaseHigh == address >= 0x1000) // sphigh
{
usage = "SPR";
}
if (_ppu.SPTall) // spbig
{
usage += " (SPR16)";
}
AddressLabel.Text = "Address: " + string.Format("{0:X4}", address);
ValueLabel.Text = "Table " + table;
Value3Label.Text = "Tile " + string.Format("{0:X2}", tile);
Value4Label.Text = usage;
ZoomBox.Image = Section(PatternView.pattern, new Rectangle(new Point((e.X / 8) * 8, (e.Y / 8) * 8), new Size(8, 8)), false);
}
private void ScanlineTextbox_TextChanged(object sender, EventArgs e)
{
if (int.TryParse(txtScanline.Text, out scanline))
{
_ppu.InstallCallback2(() => Generate(), scanline);
}
}
private void NesPPU_FormClosed(object sender, FormClosedEventArgs e)
{
if (_ppu != null)
{
_ppu.RemoveCallback2();
}
}
#endregion
MemoryDomain CHRROM;
byte[] chrromcache = new byte[8192];
private void cHRROMTileViewerToolStripMenuItem_Click(object sender, EventArgs e)
{
ChrRomView ^= true;
}
private void CalculateFormSize()
{
Width = ChrRomView ? 861 : 580;
}
private void CHRROMViewReload()
{
CHRROM = _ppu.GetCHRROM();
if (CHRROM == null)
{
numericUpDownCHRROMBank.Enabled = false;
Array.Clear(chrromcache, 0, 8192);
}
else
{
numericUpDownCHRROMBank.Enabled = true;
numericUpDownCHRROMBank.Minimum = 0;
numericUpDownCHRROMBank.Maximum = CHRROM.Size / 8192 - 1;
numericUpDownCHRROMBank.Value = Math.Min(numericUpDownCHRROMBank.Value, numericUpDownCHRROMBank.Maximum);
}
CHRROMViewRefresh();
}
private void CHRROMViewRefresh()
{
if (CHRROM != null)
{
int offs = 8192 * (int)numericUpDownCHRROMBank.Value;
for (int i = 0; i < 8192; i++)
chrromcache[i] = CHRROM.PeekByte(offs + i);
DrawPatternView(CHRROMView, chrromcache, _ppu.GetPalette(), _ppu.GetPalRam());
}
}
#endregion
private void numericUpDownCHRROMBank_ValueChanged(object sender, EventArgs e)
{
CHRROMViewRefresh();
}
}
}