AxibugEmuOnline/AxibugEmuOnline.Client/Assets/Runtime/Core/PPU.Core.cs
2024-06-28 18:08:25 +08:00

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