What is WRONG with my PPU???
Posted: Sun Sep 16, 2007 10:49 am
I'm the guy working on NES.app, a Nintendo emulator for iPhone. I've recently adopted the old InfoNES core, which I've spent considerable time fixing up and have forked into a new project called NESCore, which I hope will eventually become a viable multi-platform NES core.
Anyway, I've gotten a lot working so far, but am struggling to figure out the PPU. I must say I'm confused, even after reading all the docs I could find on the subject. Rad Racer is still mangled, and mid-frame scrolling appears to be completely hosed. I've posted the code here:
http://svn.natetrue.com/nesapp/
If anyone would like to have a look at it, I would be glad to share some of the cash that my users have donated toward working on the project. I'll gladly send $250 to anyone who can help me get the PPU working the way it's supposed to. I've included relevant portions of my code below, but feel free to look in NESCore/M6502_rw.h for read/write functions and NESCore/NESCore.c for rendering (NESCore_DrawScanline).
Any help you guys can offer would be appreciated. I've been taking a crash course in emulation to try and make this useful for my users, would love to be able to fix this.
NG
Read Functions:
case 0x2000: /* PPU */
switch( wAddr )
{
case (0x2007): /* VRAM Read */
if (S.vAddr <0x3F00) {
wScratch = S.vAddr;
wScratch &= 0x3FFF;
bScratch = S.PPU_R7;
S.PPU_R7 = W.PPUBANK[ wScratch >> 10 ][ wScratch & 0x3FF ];
} else {
bScratch = W.PPUBANK[ wScratch >> 10 ] [ wScratch & 0x3FF ];
}
S.vAddr += (S.PPU_R0 & R0_INC_ADDR) ? 0x20 : 0x01;
S.vAddr &= 0x3FFF;
/* Mid-HBlank Update. If an address is written into $2006
followed by reads of $2007 during refresh, then the
address is loaded into the PPU and used as the start address
of the next scanline. This is used to scroll the background
vertically on a portion of the screen. This code converts
the scanline address into X/Y offsets. */
if (!(S.PPU_R2 & 0x80) && (S.PPU_R1 & 0x08)!=0) {
S.PPU_SCROLL_X = (S.PPU_SCROLL_X & 0xFF)
| ((S.vAddr & 0x400) >> 2);
S.PPU_SCROLL_Y = ( 480 + 480
+ (((S.vAddr & 0x800) >> 11) * 240)
+ ((S.vAddr & 0x3e0) >> 2)
- (S.PPU_Scanline + 1) ) % 480;
if (S.PPU_SCROLL_Y > 240)
S.PPU_SCROLL_Y -= 240;
NESCore_Develop_Scroll_Values();
}
return bScratch;
break;
case (0x2004): /* SPR-RAM Read */
return S.SPRRAM[ S.PPU_R3 ];
break;
case (0x2002):
S.PPU_Latch_Flag = 0;
if ((S.PPU_R0 & 0x80) && (S.PPU_R2 & 0x80)) {
S.PPU_R0 &= 0xFE;
S.PPU_SCROLL_X = S.PPU_SCROLL_X & 255;
}
return S.PPU_R2;
break;
default: /* $2000, $2001, $2003, $2005, $2006 */
return S.PPU_R7;
break;
}
break;
Write Functions:
case 0x2000: /* PPU */
switch ( wAddr )
{
case 0x2000: /* PPU Control Register 1 */
S.PPU_R0 = byData;
S.PPU_SP_Height = (S.PPU_R0 & R0_SP_SIZE) ? 0x10 : 0x08;
W.PPU_BG = (S.PPU_R0 & R0_BG_ADDR) ? S.ChrBuf + 0x4000 : S.ChrBuf;
W.PPU_SP = (S.PPU_R0 & R0_SP_ADDR) ? S.ChrBuf + 0x4000 : S.ChrBuf;
break;
case 0x2001: /* PPU Control Register 2 */
S.PPU_R1 = byData;
break;
case 0x2002: /* PPU Status - NOT WRITABLE */
break;
case 0x2003: /* Sprite RAM ADDR */
S.PPU_R3 = byData;
break;
case 0x2004: /* Sprite RAM DATA */
S.SPRRAM[ S.PPU_R3++ ] = byData;
break;
case 0x2005: /* Scroll Register */
if (S.PPU_Latch_Flag ^= 1) {
S.PPU_R5A = byData;
S.PPU_SCROLL_X = S.PPU_R5A;
} else {
S.PPU_R5B = byData;
S.PPU_SCROLL_Y = S.PPU_R5B;
if (S.PPU_SCROLL_Y > 240)
S.PPU_SCROLL_Y -= 240;
}
break;
case 0x2006: /* VRAM Address Register */
if (S.PPU_Latch_Flag ^= 1) {
S.PPU_R6A = byData;
if ( S.PPU_Scanline < 240 && (S.PPU_R1 & R1_SHOW_SCR) )
{
BITCOPY(S.PPU_R6RA, 0x01, S.PPU_R5B, 0x40)
BITCOPY(S.PPU_R6RA, 0x02, S.PPU_R5B, 0x80)
BITCOPY(S.PPU_R6RA, 0x08, S.PPU_R0, 0x01)
BITCOPY(S.PPU_R6RA, 0x10, S.PPU_R5B, 0x01)
BITCOPY(S.PPU_R6RA, 0x20, S.PPU_R5B, 0x02)
BITCOPY(S.PPU_R6RA, 0x40, S.PPU_R5B, 0x04)
BITCOPY(S.PPU_R6RB, 0x20, S.PPU_R5B, 0x08)
BITCOPY(S.PPU_R6RB, 0x40, S.PPU_R5B, 0x10)
BITCOPY(S.PPU_R6RB, 0x80, S.PPU_R5B, 0x20)
S.PPU_SCROLL_Y = S.PPU_R5B;
if (S.PPU_SCROLL_Y > 240)
S.PPU_SCROLL_Y -= 240;
}
S.vAddr_Latch = (S.vAddr_Latch & 0xFF)
| ((word) (byData & 0xFF) << 8);
}
else {
S.PPU_R6B = byData;
if ( S.PPU_Scanline < 240 && (S.PPU_R1 & R1_SHOW_SCR) )
{
BITCOPY(S.PPU_R6RA, 0x01, S.PPU_R5B, 0x40)
BITCOPY(S.PPU_R6RA, 0x02, S.PPU_R5B, 0x80)
BITCOPY(S.PPU_R6RA, 0x08, S.PPU_R0, 0x01)
BITCOPY(S.PPU_R6RA, 0x10, S.PPU_R5B, 0x01)
BITCOPY(S.PPU_R6RA, 0x20, S.PPU_R5B, 0x02)
BITCOPY(S.PPU_R6RA, 0x40, S.PPU_R5B, 0x04)
BITCOPY(S.PPU_R6RB, 0x20, S.PPU_R5B, 0x08)
BITCOPY(S.PPU_R6RB, 0x40, S.PPU_R5B, 0x10)
BITCOPY(S.PPU_R6RB, 0x80, S.PPU_R5B, 0x20)
S.PPU_SCROLL_X = S.PPU_R5A;
}
S.vAddr_Latch = (S.vAddr_Latch & 0xFF00)
| ((word) byData & 0xFF);
}
S.vAddr = S.vAddr_Latch & 0x3FFF;
break;
case 0x2007: /* VRAM Data */
{
wScratch = S.vAddr;
wScratch &= 0x3FFF;
S.vAddr += (S.PPU_R0 & R0_INC_ADDR) ? 0x20 : 0x01;
if (S.vAddr > 0x3FFF)
S.vAddr &= 0x3FFF;
if (wScratch < 0x2000 && S.VRAMWriteEnable)
{
/* Pattern Data */
S.ChrBufUpdate |= ( 1 << ( wScratch >> 10 ) );
W.PPUBANK[ wScratch >> 10 ][ wScratch & 0x3FF ] = byData;
}
else if (wScratch < 0x3F00 ) /* 0x2000 - 0x3EFF */
{
/* Name Table and Mirror */
W.PPUBANK[ (wScratch) >> 10 ][ wScratch & 0x3ff ] = byData;
W.PPUBANK[ (wScratch ^ 0x1000) >> 10][ wScratch & 0x3FF ] = byData;
}
else if (!(wScratch & 0xF)) /* 0x3F00 or 0x3F10 */
{
/* Palette Mirror */
S.PPURAM[ 0x3f10 ] = S.PPURAM[ 0x3f14 ] = S.PPURAM[ 0x3f18 ]
= S.PPURAM[ 0x3f1c ] = S.PPURAM[ 0x3f00 ] = S.PPURAM[ 0x3f04 ]
= S.PPURAM[ 0x3f08 ] = S.PPURAM[ 0x3f0c ] = byData;
S.PalTable[ 0x00 ] = S.PalTable[ 0x04 ] = S.PalTable[ 0x08 ]
= S.PalTable[ 0x0c ] = S.PalTable[ 0x10 ] = S.PalTable[ 0x14 ]
= S.PalTable[ 0x18 ] = S.PalTable[ 0x1c ]
= NesPalette[ byData ] | 0x8000;
}
else if (wScratch & 0x03)
{
/* Palette */
S.PPURAM[ wScratch ] = byData;
S.PalTable[ wScratch & 0x1f ] = NesPalette[ byData ];
}
}
break;
}
break;
Relevant portions of DrawScanline:
Exec6502(&S.m6502_state, STEP_PER_HBLANK);
/* Reload horizontal scroll bits at beginning of each scanline */
if ( S.PPU_Scanline < 240 )
{
BITCOPY_BACK(S.PPU_R6RA, 0x04, S.PPU_R0, 0x01)
BITCOPY_BACK(S.PPU_R6RB, 0x01, S.PPU_R5A, 0x08)
BITCOPY_BACK(S.PPU_R6RB, 0x02, S.PPU_R5A, 0x10)
BITCOPY_BACK(S.PPU_R6RB, 0x04, S.PPU_R5A, 0x20)
BITCOPY_BACK(S.PPU_R6RB, 0x08, S.PPU_R5A, 0x40)
BITCOPY_BACK(S.PPU_R6RB, 0x10, S.PPU_R5A, 0x80)
}
/* Reload vertical scroll bits at end of VBLANK */
if ( S.PPU_Scanline == 0 )
{
BITCOPY_BACK(S.PPU_R6RA, 0x01, S.PPU_R5B, 0x40)
BITCOPY_BACK(S.PPU_R6RA, 0x02, S.PPU_R5B, 0x80)
BITCOPY_BACK(S.PPU_R6RA, 0x08, S.PPU_R0, 0x01)
BITCOPY_BACK(S.PPU_R6RA, 0x10, S.PPU_R5B, 0x01)
BITCOPY_BACK(S.PPU_R6RA, 0x20, S.PPU_R5B, 0x02)
BITCOPY_BACK(S.PPU_R6RA, 0x40, S.PPU_R5B, 0x04)
BITCOPY_BACK(S.PPU_R6RB, 0x20, S.PPU_R5B, 0x08)
BITCOPY_BACK(S.PPU_R6RB, 0x40, S.PPU_R5B, 0x10)
BITCOPY_BACK(S.PPU_R6RB, 0x80, S.PPU_R5B, 0x20)
}
NESCore_Develop_Scroll_Values();
/* Render Background */
/* ================= */
/* MMC5 VROM Switch */
MapperRenderScreen(1);
pPoint = &S.WorkFrame[ S.PPU_Scanline * NES_DISP_WIDTH ];
if (!( S.PPU_R1 & R1_SHOW_SCR ))
{
/* Clear scanline if display is off */
memset( pPoint, 0, NES_DISP_WIDTH << 1 ); /* Assumes 16-Bit buffer! */
}
else
{
nY = S.PPU_SCROLL_Y_BYTE + (S.PPU_Scanline >> 3);
nYBit = S.PPU_SCROLL_Y_BIT + (S.PPU_Scanline & 7);
if ( nYBit > 7 )
{
nYBit &= 7;
nY++;
}
nYBit <<= 3;
nNameTable = NAME_TABLE0 + (S.PPU_R0 & 0x03);
/* Determine which Vertical Name Table we're in */
if (nY > 29)
{
nY -= 30;
nNameTable ^= NAME_TABLE_V_MASK;
}
nX = S.PPU_SCROLL_X_BYTE;
nY4 = ( ( nY & 0x02 ) << 1 );
Anyway, I've gotten a lot working so far, but am struggling to figure out the PPU. I must say I'm confused, even after reading all the docs I could find on the subject. Rad Racer is still mangled, and mid-frame scrolling appears to be completely hosed. I've posted the code here:
http://svn.natetrue.com/nesapp/
If anyone would like to have a look at it, I would be glad to share some of the cash that my users have donated toward working on the project. I'll gladly send $250 to anyone who can help me get the PPU working the way it's supposed to. I've included relevant portions of my code below, but feel free to look in NESCore/M6502_rw.h for read/write functions and NESCore/NESCore.c for rendering (NESCore_DrawScanline).
Any help you guys can offer would be appreciated. I've been taking a crash course in emulation to try and make this useful for my users, would love to be able to fix this.
NG
Read Functions:
case 0x2000: /* PPU */
switch( wAddr )
{
case (0x2007): /* VRAM Read */
if (S.vAddr <0x3F00) {
wScratch = S.vAddr;
wScratch &= 0x3FFF;
bScratch = S.PPU_R7;
S.PPU_R7 = W.PPUBANK[ wScratch >> 10 ][ wScratch & 0x3FF ];
} else {
bScratch = W.PPUBANK[ wScratch >> 10 ] [ wScratch & 0x3FF ];
}
S.vAddr += (S.PPU_R0 & R0_INC_ADDR) ? 0x20 : 0x01;
S.vAddr &= 0x3FFF;
/* Mid-HBlank Update. If an address is written into $2006
followed by reads of $2007 during refresh, then the
address is loaded into the PPU and used as the start address
of the next scanline. This is used to scroll the background
vertically on a portion of the screen. This code converts
the scanline address into X/Y offsets. */
if (!(S.PPU_R2 & 0x80) && (S.PPU_R1 & 0x08)!=0) {
S.PPU_SCROLL_X = (S.PPU_SCROLL_X & 0xFF)
| ((S.vAddr & 0x400) >> 2);
S.PPU_SCROLL_Y = ( 480 + 480
+ (((S.vAddr & 0x800) >> 11) * 240)
+ ((S.vAddr & 0x3e0) >> 2)
- (S.PPU_Scanline + 1) ) % 480;
if (S.PPU_SCROLL_Y > 240)
S.PPU_SCROLL_Y -= 240;
NESCore_Develop_Scroll_Values();
}
return bScratch;
break;
case (0x2004): /* SPR-RAM Read */
return S.SPRRAM[ S.PPU_R3 ];
break;
case (0x2002):
S.PPU_Latch_Flag = 0;
if ((S.PPU_R0 & 0x80) && (S.PPU_R2 & 0x80)) {
S.PPU_R0 &= 0xFE;
S.PPU_SCROLL_X = S.PPU_SCROLL_X & 255;
}
return S.PPU_R2;
break;
default: /* $2000, $2001, $2003, $2005, $2006 */
return S.PPU_R7;
break;
}
break;
Write Functions:
case 0x2000: /* PPU */
switch ( wAddr )
{
case 0x2000: /* PPU Control Register 1 */
S.PPU_R0 = byData;
S.PPU_SP_Height = (S.PPU_R0 & R0_SP_SIZE) ? 0x10 : 0x08;
W.PPU_BG = (S.PPU_R0 & R0_BG_ADDR) ? S.ChrBuf + 0x4000 : S.ChrBuf;
W.PPU_SP = (S.PPU_R0 & R0_SP_ADDR) ? S.ChrBuf + 0x4000 : S.ChrBuf;
break;
case 0x2001: /* PPU Control Register 2 */
S.PPU_R1 = byData;
break;
case 0x2002: /* PPU Status - NOT WRITABLE */
break;
case 0x2003: /* Sprite RAM ADDR */
S.PPU_R3 = byData;
break;
case 0x2004: /* Sprite RAM DATA */
S.SPRRAM[ S.PPU_R3++ ] = byData;
break;
case 0x2005: /* Scroll Register */
if (S.PPU_Latch_Flag ^= 1) {
S.PPU_R5A = byData;
S.PPU_SCROLL_X = S.PPU_R5A;
} else {
S.PPU_R5B = byData;
S.PPU_SCROLL_Y = S.PPU_R5B;
if (S.PPU_SCROLL_Y > 240)
S.PPU_SCROLL_Y -= 240;
}
break;
case 0x2006: /* VRAM Address Register */
if (S.PPU_Latch_Flag ^= 1) {
S.PPU_R6A = byData;
if ( S.PPU_Scanline < 240 && (S.PPU_R1 & R1_SHOW_SCR) )
{
BITCOPY(S.PPU_R6RA, 0x01, S.PPU_R5B, 0x40)
BITCOPY(S.PPU_R6RA, 0x02, S.PPU_R5B, 0x80)
BITCOPY(S.PPU_R6RA, 0x08, S.PPU_R0, 0x01)
BITCOPY(S.PPU_R6RA, 0x10, S.PPU_R5B, 0x01)
BITCOPY(S.PPU_R6RA, 0x20, S.PPU_R5B, 0x02)
BITCOPY(S.PPU_R6RA, 0x40, S.PPU_R5B, 0x04)
BITCOPY(S.PPU_R6RB, 0x20, S.PPU_R5B, 0x08)
BITCOPY(S.PPU_R6RB, 0x40, S.PPU_R5B, 0x10)
BITCOPY(S.PPU_R6RB, 0x80, S.PPU_R5B, 0x20)
S.PPU_SCROLL_Y = S.PPU_R5B;
if (S.PPU_SCROLL_Y > 240)
S.PPU_SCROLL_Y -= 240;
}
S.vAddr_Latch = (S.vAddr_Latch & 0xFF)
| ((word) (byData & 0xFF) << 8);
}
else {
S.PPU_R6B = byData;
if ( S.PPU_Scanline < 240 && (S.PPU_R1 & R1_SHOW_SCR) )
{
BITCOPY(S.PPU_R6RA, 0x01, S.PPU_R5B, 0x40)
BITCOPY(S.PPU_R6RA, 0x02, S.PPU_R5B, 0x80)
BITCOPY(S.PPU_R6RA, 0x08, S.PPU_R0, 0x01)
BITCOPY(S.PPU_R6RA, 0x10, S.PPU_R5B, 0x01)
BITCOPY(S.PPU_R6RA, 0x20, S.PPU_R5B, 0x02)
BITCOPY(S.PPU_R6RA, 0x40, S.PPU_R5B, 0x04)
BITCOPY(S.PPU_R6RB, 0x20, S.PPU_R5B, 0x08)
BITCOPY(S.PPU_R6RB, 0x40, S.PPU_R5B, 0x10)
BITCOPY(S.PPU_R6RB, 0x80, S.PPU_R5B, 0x20)
S.PPU_SCROLL_X = S.PPU_R5A;
}
S.vAddr_Latch = (S.vAddr_Latch & 0xFF00)
| ((word) byData & 0xFF);
}
S.vAddr = S.vAddr_Latch & 0x3FFF;
break;
case 0x2007: /* VRAM Data */
{
wScratch = S.vAddr;
wScratch &= 0x3FFF;
S.vAddr += (S.PPU_R0 & R0_INC_ADDR) ? 0x20 : 0x01;
if (S.vAddr > 0x3FFF)
S.vAddr &= 0x3FFF;
if (wScratch < 0x2000 && S.VRAMWriteEnable)
{
/* Pattern Data */
S.ChrBufUpdate |= ( 1 << ( wScratch >> 10 ) );
W.PPUBANK[ wScratch >> 10 ][ wScratch & 0x3FF ] = byData;
}
else if (wScratch < 0x3F00 ) /* 0x2000 - 0x3EFF */
{
/* Name Table and Mirror */
W.PPUBANK[ (wScratch) >> 10 ][ wScratch & 0x3ff ] = byData;
W.PPUBANK[ (wScratch ^ 0x1000) >> 10][ wScratch & 0x3FF ] = byData;
}
else if (!(wScratch & 0xF)) /* 0x3F00 or 0x3F10 */
{
/* Palette Mirror */
S.PPURAM[ 0x3f10 ] = S.PPURAM[ 0x3f14 ] = S.PPURAM[ 0x3f18 ]
= S.PPURAM[ 0x3f1c ] = S.PPURAM[ 0x3f00 ] = S.PPURAM[ 0x3f04 ]
= S.PPURAM[ 0x3f08 ] = S.PPURAM[ 0x3f0c ] = byData;
S.PalTable[ 0x00 ] = S.PalTable[ 0x04 ] = S.PalTable[ 0x08 ]
= S.PalTable[ 0x0c ] = S.PalTable[ 0x10 ] = S.PalTable[ 0x14 ]
= S.PalTable[ 0x18 ] = S.PalTable[ 0x1c ]
= NesPalette[ byData ] | 0x8000;
}
else if (wScratch & 0x03)
{
/* Palette */
S.PPURAM[ wScratch ] = byData;
S.PalTable[ wScratch & 0x1f ] = NesPalette[ byData ];
}
}
break;
}
break;
Relevant portions of DrawScanline:
Exec6502(&S.m6502_state, STEP_PER_HBLANK);
/* Reload horizontal scroll bits at beginning of each scanline */
if ( S.PPU_Scanline < 240 )
{
BITCOPY_BACK(S.PPU_R6RA, 0x04, S.PPU_R0, 0x01)
BITCOPY_BACK(S.PPU_R6RB, 0x01, S.PPU_R5A, 0x08)
BITCOPY_BACK(S.PPU_R6RB, 0x02, S.PPU_R5A, 0x10)
BITCOPY_BACK(S.PPU_R6RB, 0x04, S.PPU_R5A, 0x20)
BITCOPY_BACK(S.PPU_R6RB, 0x08, S.PPU_R5A, 0x40)
BITCOPY_BACK(S.PPU_R6RB, 0x10, S.PPU_R5A, 0x80)
}
/* Reload vertical scroll bits at end of VBLANK */
if ( S.PPU_Scanline == 0 )
{
BITCOPY_BACK(S.PPU_R6RA, 0x01, S.PPU_R5B, 0x40)
BITCOPY_BACK(S.PPU_R6RA, 0x02, S.PPU_R5B, 0x80)
BITCOPY_BACK(S.PPU_R6RA, 0x08, S.PPU_R0, 0x01)
BITCOPY_BACK(S.PPU_R6RA, 0x10, S.PPU_R5B, 0x01)
BITCOPY_BACK(S.PPU_R6RA, 0x20, S.PPU_R5B, 0x02)
BITCOPY_BACK(S.PPU_R6RA, 0x40, S.PPU_R5B, 0x04)
BITCOPY_BACK(S.PPU_R6RB, 0x20, S.PPU_R5B, 0x08)
BITCOPY_BACK(S.PPU_R6RB, 0x40, S.PPU_R5B, 0x10)
BITCOPY_BACK(S.PPU_R6RB, 0x80, S.PPU_R5B, 0x20)
}
NESCore_Develop_Scroll_Values();
/* Render Background */
/* ================= */
/* MMC5 VROM Switch */
MapperRenderScreen(1);
pPoint = &S.WorkFrame[ S.PPU_Scanline * NES_DISP_WIDTH ];
if (!( S.PPU_R1 & R1_SHOW_SCR ))
{
/* Clear scanline if display is off */
memset( pPoint, 0, NES_DISP_WIDTH << 1 ); /* Assumes 16-Bit buffer! */
}
else
{
nY = S.PPU_SCROLL_Y_BYTE + (S.PPU_Scanline >> 3);
nYBit = S.PPU_SCROLL_Y_BIT + (S.PPU_Scanline & 7);
if ( nYBit > 7 )
{
nYBit &= 7;
nY++;
}
nYBit <<= 3;
nNameTable = NAME_TABLE0 + (S.PPU_R0 & 0x03);
/* Determine which Vertical Name Table we're in */
if (nY > 29)
{
nY -= 30;
nNameTable ^= NAME_TABLE_V_MASK;
}
nX = S.PPU_SCROLL_X_BYTE;
nY4 = ( ( nY & 0x02 ) << 1 );