/** * Fetches 8x8 NES tile stored at the given offset. This is an artifact of the first renderer I * wrote which drew 8 scanlines at a time. * * @param offset * @return an 8x8 array with colors stored as RGB packed in int */ private int[] debugGetTile(final int offset) { // read one whole tile from nametable and convert from bitplane to packed // only used for debugging int[] dat = new int[64]; for (int i = 0; i < 8; ++i) { // per line of tile ( 1 byte) for (int j = 0; j < 8; ++j) { // per pixel(1 bit) dat[8 * i + j] = ((((mapper.ppuRead(i + offset) & (utils.BIT7 - j)) != 0)) ? 0x555555 : 0) + ((((mapper.ppuRead(i + offset + 8) & (utils.BIT7 - j)) != 0)) ? 0xaaaaaa : 0); } } return dat; }
private void spriteFetch( final boolean spritesize, final int tilenum, int offset, final int oamextra) { int tilefetched; if (spritesize) { tilefetched = ((tilenum & 1) * 0x1000) + (tilenum & 0xfe) * 16; } else { tilefetched = tilenum * 16 + ((sprpattern) ? 0x1000 : 0); } tilefetched += offset; // now load up the shift registers for said sprite final boolean hflip = ((oamextra & (utils.BIT6)) != 0); if (!hflip) { spriteshiftregL[found] = reverseByte(mapper.ppuRead(tilefetched)); spriteshiftregH[found] = reverseByte(mapper.ppuRead(tilefetched + 8)); } else { spriteshiftregL[found] = mapper.ppuRead(tilefetched); spriteshiftregH[found] = mapper.ppuRead(tilefetched + 8); } }
private void bgFetch() { // fetch tiles for background // on real PPU this logic is repurposed for sprite fetches as well // System.err.println(hex(loopyV)); bgAttrShiftRegH |= ((nextattr >> 1) & 1); bgAttrShiftRegL |= (nextattr & 1); // background fetches switch ((cycles - 1) & 7) { case 1: fetchNTByte(); break; case 3: // fetch attribute (FIX MATH) penultimateattr = getAttribute(((loopyV & 0xc00) + 0x23c0), (loopyV) & 0x1f, (((loopyV) & 0x3e0) >> 5)); break; case 5: // fetch low bg byte linelowbits = mapper.ppuRead((tileAddr) + ((loopyV & 0x7000) >> 12)); break; case 7: // fetch high bg byte linehighbits = mapper.ppuRead((tileAddr) + 8 + ((loopyV & 0x7000) >> 12)); bgShiftRegL |= linelowbits; bgShiftRegH |= linehighbits; nextattr = penultimateattr; if (cycles != 256) { incLoopyVHoriz(); } else { incLoopyVVert(); } break; default: break; } if (cycles >= 321 && cycles <= 336) { bgShiftClock(); } }
/** * Read the appropriate color attribute byte for the current tile. this is fetched 2x as often as * it really needs to be, the MMC5 takes advantage of that for ExGrafix mode. * * @param ntstart //start of the current attribute table * @param tileX //x position of tile (0-31) * @param tileY //y position of tile (0-29) * @return attribute table value (0-3) */ private int getAttribute(final int ntstart, final int tileX, final int tileY) { final int base = mapper.ppuRead(ntstart + (tileX >> 2) + 8 * (tileY >> 2)); if (((tileY & (utils.BIT1)) != 0)) { if (((tileX & (utils.BIT1)) != 0)) { return (base >> 6) & 3; } else { return (base >> 4) & 3; } } else { if (((tileX & (utils.BIT1)) != 0)) { return (base >> 2) & 3; } else { return base & 3; } } }
final void setParameters() { // set stuff to NTSC or PAL or Dendy values switch (mapper.getTVType()) { case NTSC: default: numscanlines = 262; vblankline = 241; cpudivider = new int[] {3, 3, 3, 3, 3}; break; case PAL: numscanlines = 312; vblankline = 241; cpudivider = new int[] {4, 3, 3, 3, 3}; break; case DENDY: numscanlines = 312; vblankline = 291; cpudivider = new int[] {3, 3, 3, 3, 3}; break; } }
/** draw all 4 nametables/tileset/pallette to debug window. (for the nametable viewer) */ private void debugDraw() { for (int i = 0; i < 32; ++i) { for (int j = 0; j < 30; ++j) { nametableView.setRGB( i * 8, j * 8, 8, 8, debugGetTile(mapper.ppuRead(0x2000 + i + 32 * j) * 16 + (bgpattern ? 0x1000 : 0)), 0, 8); } } for (int i = 0; i < 32; ++i) { for (int j = 0; j < 30; ++j) { nametableView.setRGB( i * 8 + 255, j * 8, 8, 8, debugGetTile(mapper.ppuRead(0x2400 + i + 32 * j) * 16 + (bgpattern ? 0x1000 : 0)), 0, 8); } } for (int i = 0; i < 32; ++i) { for (int j = 0; j < 30; ++j) { nametableView.setRGB( i * 8, j * 8 + 239, 8, 8, debugGetTile(mapper.ppuRead(0x2800 + i + 32 * j) * 16 + (bgpattern ? 0x1000 : 0)), 0, 8); } } for (int i = 0; i < 32; ++i) { for (int j = 0; j < 30; ++j) { nametableView.setRGB( i * 8 + 255, j * 8 + 239, 8, 8, debugGetTile(mapper.ppuRead(0x2C00 + i + 32 * j) * 16 + (bgpattern ? 0x1000 : 0)), 0, 8); } } // draw the tileset // for (int i = 0; i < 16; ++i) { // for (int j = 0; j < 32; ++j) { // nametableView.setRGB(i * 8, j * 8, 8, 8, // debugGetTile((i + 16 * j) * 16), 0, 8); // } // } // draw the palettes on the bottom. // for (int i = 0; i < 32; ++i) { // for (int j = 0; j < 16; ++j) { // for (int k = 0; k < 16; ++k) { // nametableView.setRGB(j + i * 16, k + 256, nescolor[0][pal[i]]); // } // } // } debuggui.setFrame(nametableView); // debugbuff.clear(); }
private void fetchNTByte() { // fetch nt byte tileAddr = mapper.ppuRead(((loopyV & 0xc00) | 0x2000) + (loopyV & 0x3ff)) * 16 + (bgpattern ? 0x1000 : 0); }
/** runs the emulation for one PPU clock cycle. */ public final void clock() { // cycle based ppu stuff will go here if (cycles == 1) { if (scanline == 0) { dotcrawl = renderingOn(); } if (scanline < 240) { bgcolors[scanline] = pal[0]; } } if (scanline < 240 || scanline == (numscanlines - 1)) { // on all rendering lines if (renderingOn() && ((cycles >= 1 && cycles <= 256) || (cycles >= 321 && cycles <= 336))) { // fetch background tiles, load shift registers bgFetch(); } else if (cycles == 257 && renderingOn()) { // x scroll reset // horizontal bits of loopyV = loopyT loopyV &= ~0x41f; loopyV |= loopyT & 0x41f; } else if (cycles > 257 && cycles <= 341) { // clear the oam address from pxls 257-341 continuously oamaddr = 0; } if ((cycles == 340) && renderingOn()) { // read the same nametable byte twice // this signals the MMC5 to increment the scanline counter fetchNTByte(); fetchNTByte(); } if (cycles == 65 && renderingOn()) { oamstart = oamaddr; } if (cycles == 260 && renderingOn()) { // evaluate sprites for NEXT scanline (as long as either background or sprites are enabled) // this does in fact happen on scanine 261 but it doesn't do anything useful // it's cycle 260 because that's when the first important sprite byte is read // actually sprite overflow should be set by sprite eval somewhat before // so this needs to be split into 2 parts, the eval and the data fetches evalSprites(); } if (scanline == (numscanlines - 1)) { if (cycles == 0) { // turn off vblank, sprite 0, sprite overflow flags vblankflag = false; sprite0hit = false; spriteoverflow = false; } else if (cycles >= 280 && cycles <= 304 && renderingOn()) { // loopyV = (all of)loopyT for each of these cycles loopyV = loopyT; } } } else if (scanline == vblankline && cycles == 1) { // handle vblank on / off vblankflag = true; } if (!renderingOn() || (scanline > 240 && scanline < (numscanlines - 1))) { // HACK ALERT // handle the case of MMC3 mapper watching A12 toggle // even when read or write aren't asserted on the bus // needed to pass Blargg's mmc3 tests mapper.checkA12(loopyV & 0x3fff); } if (scanline < 240) { if (cycles >= 1 && cycles <= 256) { int bufferoffset = (scanline << 8) + (cycles - 1); // bg drawing if (bgOn) { // if background is on, draw a line of that final boolean isBG = drawBGPixel(bufferoffset); // sprite drawing drawSprites(scanline << 8, cycles - 1, isBG); } else if (spritesOn) { // just the sprites then int bgcolor = ((loopyV > 0x3f00 && loopyV < 0x3fff) ? mapper.ppuRead(loopyV) : pal[0]); bitmap[bufferoffset] = bgcolor; drawSprites(scanline << 8, cycles - 1, true); } else { // rendering is off, so draw either the background color OR // if the PPU address points to the palette, draw that color instead. int bgcolor = ((loopyV > 0x3f00 && loopyV < 0x3fff) ? mapper.ppuRead(loopyV) : pal[0]); bitmap[bufferoffset] = bgcolor; } // deal with the grayscale flag if (grayscale) { bitmap[bufferoffset] &= 0x30; } // handle color emphasis bitmap[bufferoffset] = (bitmap[bufferoffset] & 0x3f) | emph; } } // handle nmi if (vblankflag && nmicontrol) { // pull NMI line on when conditions are right mapper.cpu.setNMI(true); } else { mapper.cpu.setNMI(false); } // clock CPU, once every 3 ppu cycles div = (div + 1) % cpudivider[cpudividerctr]; if (div == 0) { mapper.cpu.runcycle(scanline, cycles); mapper.cpucycle(1); cpudividerctr = (cpudividerctr + 1) % cpudivider.length; } if (cycles == 257) { mapper.notifyscanline(scanline); } else if (cycles == 340) { scanline = (scanline + 1) % numscanlines; if (scanline == 0) { ++framecount; } } }
/** * Performs a write to a PPU register * * @param regnum register number from 0 to 7, memory addresses are decoded to these elsewhere * @param data the value to write to the register (0x00 to 0xff valid) */ public final void write(final int regnum, final int data) { // if (regnum != 4 /*&& regnum != 7*/) { // System.err.println("PPU write - wrote " + utils.hex(data) + " to reg " // + utils.hex(regnum + 0x2000) // + " frame " + framecount + " scanline " + scanline); // } // debugdraw(); openbus = data; switch (regnum) { case 0: // PPUCONTROL (2000) // set 2 bits of vram address (nametable select) // bits 0 and 1 affect loopyT to change nametable start by 0x400 loopyT &= ~0xc00; loopyT |= (data & 3) << 10; /* SMB1 writes here at the end of its main loop and if this write lands on one exact PPU clock, the address bits are set to 0. This only happens on one CPU/PPU alignment of real hardware though so it only shows up ~33% of the time. */ vraminc = (((data & (utils.BIT2)) != 0) ? 32 : 1); sprpattern = ((data & (utils.BIT3)) != 0); bgpattern = ((data & (utils.BIT4)) != 0); spritesize = ((data & (utils.BIT5)) != 0); /*bit 6 is kind of a halt and catch fire situation since it outputs ppu color data on the EXT pins that are tied to ground if set and that'll make the PPU get very hot from sourcing the current. Only really useful for the NESRGB interposer board, kind of useless for emulators. I will ignore it. */ nmicontrol = ((data & (utils.BIT7)) != 0); break; case 1: // PPUMASK (2001) grayscale = ((data & (utils.BIT0)) != 0); bgClip = !((data & (utils.BIT1)) != 0); // clip left 8 pixels when its on spriteClip = !((data & (utils.BIT2)) != 0); bgOn = ((data & (utils.BIT3)) != 0); spritesOn = ((data & (utils.BIT4)) != 0); emph = (data & 0xe0) << 1; if (numscanlines == 312) { // if PAL switch position of red and green emphasis bits (6 and 5) // red is bit 6 -> bit 7 // green is bit 7 -> bit 6 int red = (emph >> 6) & 1; int green = (emph >> 7) & 1; emph &= 0xf3f; emph |= (red << 7) | (green << 6); } break; case 3: // PPUOAMADDR (2003) // most games just write zero and use the dma oamaddr = data & 0xff; break; case 4: // PPUOAMDATA(2004) if ((oamaddr & 3) == 2) { OAM[oamaddr++] = (data & 0xE3); } else { OAM[oamaddr++] = data; } oamaddr &= 0xff; // games don't usually write this directly anyway, it's unreliable break; // PPUSCROLL(2005) case 5: if (even) { // update horizontal scroll loopyT &= ~0x1f; loopyX = data & 7; loopyT |= data >> 3; even = false; } else { // update vertical scroll loopyT &= ~0x7000; loopyT |= ((data & 7) << 12); loopyT &= ~0x3e0; loopyT |= (data & 0xf8) << 2; even = true; } break; case 6: // PPUADDR (2006) if (even) { // high byte loopyT &= 0xc0ff; loopyT |= ((data & 0x3f) << 8); loopyT &= 0x3fff; even = false; } else { loopyT &= 0xfff00; loopyT |= data; loopyV = loopyT; even = true; } break; case 7: // PPUDATA mapper.ppuWrite((loopyV & 0x3fff), data); if (!renderingOn() || (scanline > 240 && scanline < (numscanlines - 1))) { loopyV += vraminc; } else { // while rendering, it seems to drop by 1 scanline, regardless of increment mode if ((loopyV & 0x7000) == 0x7000) { int YScroll = loopyV & 0x3E0; loopyV &= 0xFFF; if (YScroll == 0x3A0) { loopyV ^= 0xBA0; } else if (YScroll == 0x3E0) { loopyV ^= 0x3E0; } else { loopyV += 0x20; } } else { loopyV += 0x1000; } } break; default: break; } }
/** * Performs a read from a PPU register, as well as causes any side effects of reading that * specific register. * * @param regnum * @return the data in the PPU register, or open bus (the last value written to a PPU register) if * the register is read only */ public final int read(final int regnum) { switch (regnum) { case 2: even = true; if (scanline == 241) { if (cycles == 1) { // suppress NMI flag if it was just turned on this same cycle vblankflag = false; } // OK, uncommenting this makes blargg's NMI suppression test // work but breaks Antarctic Adventure. // I'm going to need a cycle accurate CPU to fix that... // if (cycles < 4) { // //show vblank flag but cancel pending NMI before the CPU // //can actually do anything with it // //TODO: use proper interface for this // mapper.cpu.nmiNext = false; // } } openbus = (vblankflag ? 0x80 : 0) | (sprite0hit ? 0x40 : 0) | (spriteoverflow ? 0x20 : 0) | (openbus & 0x1f); vblankflag = false; break; case 4: // reading this is NOT reliable but some games do it anyways openbus = OAM[oamaddr]; // System.err.println("codemasters?"); if (renderingOn() && (scanline <= 240)) { if (cycles < 64) { return 0xFF; } else if (cycles <= 256) { return 0x00; } // Micro Machines relies on this: else if (cycles < 320) { return 0xFF; } // and this: else { return secOAM[0]; // is this the right value @ the time? } } break; case 7: // PPUDATA // correct behavior. read is delayed by one // -unless- is a read from sprite pallettes final int temp; if ((loopyV & 0x3fff) < 0x3f00) { temp = readbuffer; readbuffer = mapper.ppuRead(loopyV & 0x3fff); } else { readbuffer = mapper.ppuRead((loopyV & 0x3fff) - 0x1000); temp = mapper.ppuRead(loopyV); } if (!renderingOn() || (scanline > 240 && scanline < (numscanlines - 1))) { loopyV += vraminc; } else { // if 2007 is read during rendering PPU increments both horiz // and vert counters erroneously. incLoopyVHoriz(); incLoopyVVert(); } openbus = temp; break; // and don't increment on read default: return openbus; // last value written to ppu } return openbus; }