forked from sin365/AxibugEmuOnline
395 lines
15 KiB
C#
395 lines
15 KiB
C#
using System;
|
|
|
|
namespace AxibugEmuOnline.Client.UNES
|
|
{
|
|
partial class PPU
|
|
{
|
|
private const int GameWidth = 256, GameHeight = 240;
|
|
|
|
private uint _bufferPos;
|
|
public readonly uint[] RawBitmap = new uint[GameWidth * GameHeight];
|
|
//查找表的Idx网络缓存
|
|
public readonly byte[] RawBitmap_paletteIdxCache = new byte[GameWidth * GameHeight];
|
|
private readonly uint[] _priority = new uint[GameWidth * GameHeight];
|
|
|
|
// TODO: use real chroma/luma decoding
|
|
private readonly uint[] _palette = {
|
|
0x7C7C7C, 0x0000FC, 0x0000BC, 0x4428BC, 0x940084, 0xA80020, 0xA81000, 0x881400,
|
|
0x503000, 0x007800, 0x006800, 0x005800, 0x004058, 0x000000, 0x000000, 0x000000,
|
|
0xBCBCBC, 0x0078F8, 0x0058F8, 0x6844FC, 0xD800CC, 0xE40058, 0xF83800, 0xE45C10,
|
|
0xAC7C00, 0x00B800, 0x00A800, 0x00A844, 0x008888, 0x000000, 0x000000, 0x000000,
|
|
0xF8F8F8, 0x3CBCFC, 0x6888FC, 0x9878F8, 0xF878F8, 0xF85898, 0xF87858, 0xFCA044,
|
|
0xF8B800, 0xB8F818, 0x58D854, 0x58F898, 0x00E8D8, 0x787878, 0x000000, 0x000000,
|
|
0xFCFCFC, 0xA4E4FC, 0xB8B8F8, 0xD8B8F8, 0xF8B8F8, 0xF8A4C0, 0xF0D0B0, 0xFCE0A8,
|
|
0xF8D878, 0xD8F878, 0xB8F8B8, 0xB8F8D8, 0x00FCFC, 0xF8D8F8, 0x000000, 0x000000
|
|
};
|
|
|
|
private int _scanLineCount = 261;
|
|
private int _cyclesPerLine = 341;
|
|
private int _cpuSyncCounter;
|
|
private readonly uint[] _scanLineOAM = new uint[8 * 4];
|
|
private readonly bool[] _isSprite0 = new bool[8];
|
|
private int _spriteCount;
|
|
|
|
private long _tileShiftRegister;
|
|
private uint _currentNameTableByte;
|
|
private uint _currentHighTile, _currentLowTile;
|
|
private uint _currentColor;
|
|
|
|
public void ProcessPixel(int x, int y)
|
|
{
|
|
ProcessBackgroundForPixel(x, y);
|
|
if (F.DrawSprites)
|
|
{
|
|
ProcessSpritesForPixel(x, y);
|
|
}
|
|
|
|
if (y != -1)
|
|
{
|
|
_bufferPos++;
|
|
}
|
|
}
|
|
|
|
private void CountSpritesOnLine(int scanLine)
|
|
{
|
|
_spriteCount = 0;
|
|
var height = F.TallSpritesEnabled ? 16 : 8;
|
|
|
|
for (var idx = 0; idx < _oam.Length; idx += 4)
|
|
{
|
|
var y = _oam[idx] + 1;
|
|
if (scanLine >= y && scanLine < y + height)
|
|
{
|
|
_isSprite0[_spriteCount] = idx == 0;
|
|
_scanLineOAM[_spriteCount * 4 + 0] = _oam[idx + 0];
|
|
_scanLineOAM[_spriteCount * 4 + 1] = _oam[idx + 1];
|
|
_scanLineOAM[_spriteCount * 4 + 2] = _oam[idx + 2];
|
|
_scanLineOAM[_spriteCount * 4 + 3] = _oam[idx + 3];
|
|
_spriteCount++;
|
|
}
|
|
|
|
if (_spriteCount == 8)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void NextNameTableByte()
|
|
{
|
|
_currentNameTableByte = ReadByte(0x2000 | (V & 0x0FFF));
|
|
}
|
|
|
|
private void NextTileByte(bool hi)
|
|
{
|
|
var tileIdx = _currentNameTableByte * 16;
|
|
var address = F.PatternTableAddress + tileIdx + FineY;
|
|
|
|
if (hi)
|
|
{
|
|
_currentHighTile = ReadByte(address + 8);
|
|
}
|
|
else
|
|
{
|
|
_currentLowTile = ReadByte(address);
|
|
}
|
|
}
|
|
|
|
private void NextAttributeByte()
|
|
{
|
|
// Bless nesdev
|
|
var address = 0x23C0 | (V & 0x0C00) | ((V >> 4) & 0x38) | ((V >> 2) & 0x07);
|
|
_currentColor = (ReadByte(address) >> (int)((CoarseX & 2) | ((CoarseY & 2) << 1))) & 0x3;
|
|
}
|
|
|
|
private void ShiftTileRegister()
|
|
{
|
|
for (var x = 0; x < 8; x++)
|
|
{
|
|
uint palette = ((_currentHighTile & 0x80) >> 6) | ((_currentLowTile & 0x80) >> 7);
|
|
_tileShiftRegister |= (palette + _currentColor * 4) << ((7 - x) * 4);
|
|
_currentLowTile <<= 1;
|
|
_currentHighTile <<= 1;
|
|
}
|
|
}
|
|
|
|
private void ProcessBackgroundForPixel(int cycle, int scanLine)
|
|
{
|
|
if (cycle < 8 && !F.DrawLeftBackground || !F.DrawBackground && scanLine != -1)
|
|
{
|
|
// Maximally sketchy: if current address is in the PPU palette, then it draws that palette entry if rendering is disabled
|
|
// Otherwise, it draws $3F00 (universal bg color)
|
|
// https://www.romhacking.net/forum/index.php?topic=20554.0
|
|
// Don't know if any game actually uses it, but a test ROM I wrote unexpectedly showed this
|
|
// corner case
|
|
|
|
//RawBitmap[_bufferPos] = _palette[ReadByte(0x3F00 + ((F.BusAddress & 0x3F00) == 0x3F00 ? F.BusAddress & 0x001F : 0)) & 0x3F];
|
|
byte pIdx = (byte)(ReadByte(0x3F00 + ((F.BusAddress & 0x3F00) == 0x3F00 ? F.BusAddress & 0x001F : 0)) & 0x3F);
|
|
//RawBitmap[_bufferPos] = _palette[pIdx];
|
|
RawBitmap_paletteIdxCache[_bufferPos] = pIdx;
|
|
return;
|
|
}
|
|
|
|
var paletteEntry = (uint)(_tileShiftRegister >> 32 >> (int)((7 - X) * 4)) & 0x0F;
|
|
if (paletteEntry % 4 == 0) paletteEntry = 0;
|
|
|
|
if (scanLine != -1)
|
|
{
|
|
_priority[_bufferPos] = paletteEntry;
|
|
//RawBitmap[_bufferPos] = _palette[ReadByte(0x3F00u + paletteEntry) & 0x3F];
|
|
byte pIdx = (byte)(ReadByte(0x3F00u + paletteEntry) & 0x3F);
|
|
//RawBitmap[_bufferPos] = _palette[pIdx];
|
|
RawBitmap_paletteIdxCache[_bufferPos] = pIdx;
|
|
}
|
|
}
|
|
|
|
private void ProcessSpritesForPixel(int x, int scanLine)
|
|
{
|
|
for (var idx = _spriteCount * 4 - 4; idx >= 0; idx -= 4)
|
|
{
|
|
var spriteX = _scanLineOAM[idx + 3];
|
|
var spriteY = _scanLineOAM[idx] + 1;
|
|
|
|
// Don't draw this sprite if...
|
|
if (spriteY == 0 || // it's located at y = 0
|
|
spriteY > 239 || // it's located past y = 239 ($EF)
|
|
x >= spriteX + 8 || // it's behind the current dot
|
|
x < spriteX || // it's ahead of the current dot
|
|
x < 8 && !F.DrawLeftSprites) // it's in the clip area, and clipping is enabled
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// amusingly enough, the PPU's palette handling is basically identical
|
|
// to that of the Gameboy / Gameboy Color, so I've sort of just copy/pasted
|
|
// handling code wholesale from my GBC emulator at
|
|
// https://github.com/Xyene/Nitrous-Emulator/blob/master/src/main/java/nitrous/lcd/LCD.java#L642
|
|
var tileIdx = _scanLineOAM[idx + 1];
|
|
if (F.TallSpritesEnabled)
|
|
{
|
|
tileIdx &= ~0x1u;
|
|
}
|
|
|
|
tileIdx *= 16;
|
|
|
|
var attribute = _scanLineOAM[idx + 2] & 0xE3;
|
|
|
|
var palette = attribute & 0x3;
|
|
var front = (attribute & 0x20) == 0;
|
|
var flipX = (attribute & 0x40) > 0;
|
|
var flipY = (attribute & 0x80) > 0;
|
|
|
|
var px = (int) (x - spriteX);
|
|
var line = (int) (scanLine - spriteY);
|
|
|
|
var tableBase = F.TallSpritesEnabled ? (_scanLineOAM[idx + 1] & 1) * 0x1000 : F.SpriteTableAddress;
|
|
|
|
if (F.TallSpritesEnabled)
|
|
{
|
|
if (line >= 8)
|
|
{
|
|
line -= 8;
|
|
if (!flipY)
|
|
{
|
|
tileIdx += 16;
|
|
}
|
|
|
|
flipY = false;
|
|
}
|
|
|
|
if (flipY)
|
|
{
|
|
tileIdx += 16;
|
|
}
|
|
}
|
|
|
|
// here we handle the x and y flipping by tweaking the indices we are accessing
|
|
var logicalX = flipX ? 7 - px : px;
|
|
var logicalLine = flipY ? 7 - line : line;
|
|
|
|
var address = (uint) (tableBase + tileIdx + logicalLine);
|
|
|
|
// this looks bad, but it's about as readable as it's going to get
|
|
var color = (uint) (
|
|
(
|
|
(
|
|
(
|
|
// fetch upper bit from 2nd bit plane
|
|
ReadByte(address + 8) & (0x80 >> logicalX)
|
|
) >> (7 - logicalX)
|
|
) << 1 // this is the upper bit of the color number
|
|
) |
|
|
(
|
|
(
|
|
ReadByte(address) & (0x80 >> logicalX)
|
|
) >> (7 - logicalX)
|
|
)); // << 0, this is the lower bit of the color number
|
|
|
|
if (color > 0)
|
|
{
|
|
var backgroundPixel = _priority[_bufferPos];
|
|
// Sprite 0 hits...
|
|
if (!(!_isSprite0[idx / 4] || // do not occur on not-0 sprite
|
|
x < 8 && !F.DrawLeftSprites || // or if left clipping is enabled
|
|
backgroundPixel == 0 || // or if bg pixel is transparent
|
|
F.Sprite0Hit || // or if it fired this frame already
|
|
x == 255)) // or if x is 255, "for an obscure reason related to the pixel pipeline"
|
|
{
|
|
F.Sprite0Hit = true;
|
|
}
|
|
|
|
if (F.DrawBackground && (front || backgroundPixel == 0))
|
|
{
|
|
if (scanLine != -1)
|
|
{
|
|
//RawBitmap[_bufferPos] = _palette[ReadByte(0x3F10 + palette * 4 + color) & 0x3F];
|
|
|
|
byte pIdx = (byte)(ReadByte(0x3F10 + palette * 4 + color) & 0x3F);
|
|
RawBitmap[_bufferPos] = _palette[pIdx];
|
|
RawBitmap_paletteIdxCache[_bufferPos] = pIdx;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void ProcessFrame()
|
|
{
|
|
RawBitmap.Fill(0u);
|
|
RawBitmap_paletteIdxCache.Fill((byte)0);
|
|
_priority.Fill(0u);
|
|
_bufferPos = 0;
|
|
|
|
for (var i = -1; i < _scanLineCount; i++)
|
|
{
|
|
ProcessScanLine(i);
|
|
}
|
|
}
|
|
|
|
public void ProcessScanLine(int line)
|
|
{
|
|
for (var i = 0; i < _cyclesPerLine; i++)
|
|
{
|
|
ProcessCycle(line, i);
|
|
}
|
|
}
|
|
|
|
private int _cpuClocksSinceVBL;
|
|
private int _ppuClocksSinceVBL;
|
|
|
|
public void ProcessCycle(int scanLine, int cycle)
|
|
{
|
|
var visibleCycle = 1 <= cycle && cycle <= 256;
|
|
var prefetchCycle = 321 <= cycle && cycle <= 336;
|
|
var fetchCycle = visibleCycle || prefetchCycle;
|
|
|
|
if (F.VBlankStarted)
|
|
{
|
|
_ppuClocksSinceVBL++;
|
|
}
|
|
|
|
if (0 <= scanLine && scanLine < 240 || scanLine == -1)
|
|
{
|
|
if (visibleCycle)
|
|
{
|
|
ProcessPixel(cycle - 1, scanLine);
|
|
}
|
|
|
|
// During pixels 280 through 304 of this scanline, the vertical scroll bits are reloaded TODO: if rendering is enabled.
|
|
if (scanLine == -1 && 280 <= cycle && cycle <= 304)
|
|
{
|
|
ReloadScrollY();
|
|
}
|
|
|
|
if (fetchCycle)
|
|
{
|
|
_tileShiftRegister <<= 4;
|
|
|
|
// See https://wiki.nesdev.com/w/images/d/d1/Ntsc_timing.png
|
|
// Takes 8 cycles for tile to be read, 2 per "step"
|
|
switch (cycle & 7)
|
|
{
|
|
case 1: // NT
|
|
NextNameTableByte();
|
|
break;
|
|
case 3: // AT
|
|
NextAttributeByte();
|
|
break;
|
|
case 5: // Tile low
|
|
NextTileByte(false);
|
|
break;
|
|
case 7: // Tile high
|
|
NextTileByte(true);
|
|
break;
|
|
case 0: // 2nd cycle of tile high fetch
|
|
if (cycle == 256)
|
|
IncrementScrollY();
|
|
else
|
|
IncrementScrollX();
|
|
// Begin rendering a brand new tile
|
|
ShiftTileRegister();
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (cycle == 257)
|
|
{
|
|
ReloadScrollX();
|
|
// 257 - 320
|
|
// The tile data for the sprites on the next scanline are fetched here.
|
|
// TODO: stagger this over all the cycles as opposed to only on 257
|
|
CountSpritesOnLine(scanLine + 1);
|
|
}
|
|
}
|
|
|
|
// TODO: this is a hack; VBlank should be cleared on dot 1 of the pre-render line,
|
|
// but for some reason we're at 2272-2273 CPU clocks at that time
|
|
// (i.e., our PPU timing is off somewhere by 6-9 PPU cycles per frame)
|
|
if (F.VBlankStarted && _cpuClocksSinceVBL == 2270)
|
|
{
|
|
F.VBlankStarted = false;
|
|
_cpuClocksSinceVBL = 0;
|
|
}
|
|
|
|
if (cycle == 1)
|
|
{
|
|
if (scanLine == 241)
|
|
{
|
|
F.VBlankStarted = true;
|
|
if (F.NMIEnabled)
|
|
{
|
|
_emulator.CPU.TriggerInterrupt(CPU.InterruptType.NMI);
|
|
}
|
|
}
|
|
|
|
// Happens at the same time as 1st cycle of NT byte fetch
|
|
if (scanLine == -1)
|
|
{
|
|
// Console.WriteLine(_ppuClocksSinceVBL);
|
|
_ppuClocksSinceVBL = 0;
|
|
F.VBlankStarted = false;
|
|
F.Sprite0Hit = false;
|
|
F.SpriteOverflow = false;
|
|
}
|
|
}
|
|
|
|
_emulator.Mapper.ProcessCycle(scanLine, cycle);
|
|
|
|
if (_cpuSyncCounter + 1 == 3)
|
|
{
|
|
if (F.VBlankStarted)
|
|
{
|
|
_cpuClocksSinceVBL++;
|
|
}
|
|
|
|
_emulator.CPU.TickFromPPU();
|
|
_cpuSyncCounter = 0;
|
|
}
|
|
else
|
|
{
|
|
_cpuSyncCounter++;
|
|
}
|
|
}
|
|
}
|
|
}
|