/* * HalfNES by Andrew Hoffman * Licensed under the GNU GPL Version 3. See LICENSE file */ package com.grapeshot.halfnes; import static com.grapeshot.halfnes.PrefsSingleton.get; import com.grapeshot.halfnes.mappers.Mapper; import com.grapeshot.halfnes.ui.DebugUI; import com.grapeshot.halfnes.ui.GUIInterface; import static com.grapeshot.halfnes.utils.reverseByte; import java.awt.image.BufferedImage; import static java.awt.image.BufferedImage.TYPE_INT_BGR; import java.util.Arrays; import static java.util.Arrays.fill; import static java.util.Arrays.fill; public class PPU { public Mapper mapper; private int oamaddr, oamstart, readbuffer = 0; private int loopyV = 0x0;//ppu memory pointer private int loopyT = 0x0;//temp pointer private int loopyX = 0;//fine x scroll public int scanline = 0; public int cycles = 0; private int framecount = 0; private int div = 2; private final int[] OAM = new int[256], secOAM = new int[32], spriteshiftregH = new int[8], spriteshiftregL = new int[8], spriteXlatch = new int[8], spritepals = new int[8], bitmap = new int[240 * 256]; private int found, bgShiftRegH, bgShiftRegL, bgAttrShiftRegH, bgAttrShiftRegL; private final boolean[] spritebgflags = new boolean[8]; private boolean even = true, bgpattern = true, sprpattern, spritesize, nmicontrol, grayscale, bgClip, spriteClip, bgOn, spritesOn, vblankflag, sprite0hit, spriteoverflow; private int emph; public final int[] pal; private DebugUI debuggui; private int vraminc = 1; private final static boolean PPUDEBUG = get().getBoolean("ntView", false); private BufferedImage nametableView; private final int[] bgcolors = new int[256]; private int openbus = 0; //the last value written to the PPU private int nextattr; private int linelowbits; private int linehighbits; private int penultimateattr; private int numscanlines; private int vblankline; private final int[] cpudivider = {3, 3, 3, 3, 3}; public PPU(final Mapper mapper) { this.pal = new int[]{0x09, 0x01, 0x00, 0x01, 0x00, 0x02, 0x02, 0x0D, 0x08, 0x10, 0x08, 0x24, 0x00, 0x00, 0x04, 0x2C, 0x09, 0x01, 0x34, 0x03, 0x00, 0x04, 0x00, 0x14, 0x08, 0x3A, 0x00, 0x02, 0x00, 0x20, 0x2C, 0x08}; /* power-up pallette checked by Blargg's power_up_palette test. Different revs of NES PPU might give different initial results but there's a test expecting this set of values and nesemu1, BizHawk, RockNES, MyNes use it */ this.mapper = mapper; fill(OAM, 0xff); if (PPUDEBUG) { nametableView = new BufferedImage(512, 480, TYPE_INT_BGR); debuggui = new DebugUI(512, 480); debuggui.run(); } setParameters(); } final void setParameters() { //set stuff to NTSC or PAL or Dendy values switch (mapper.getTVType()) { case NTSC: default: numscanlines = 262; vblankline = 241; cpudivider[0] = 3; break; case PAL: numscanlines = 312; vblankline = 241; cpudivider[0] = 4; break; case DENDY: numscanlines = 312; vblankline = 291; cpudivider[0] = 3; break; } } public void runFrame() { for (int line = 0; line < numscanlines; ++line) { clockLine(line); } } /** * Performs a read from a PPU register, as well as causes any side effects * of reading that specific register. * * @param regnum register to read (address with 0x2000 already subtracted) * @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; } /** * 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 if ((loopyV & 0x7000) == 0x7000) { int YScroll = loopyV & 0x3E0; loopyV &= 0xFFF; switch (YScroll) { case 0x3A0: loopyV ^= 0xBA0; break; case 0x3E0: loopyV ^= 0x3E0; break; default: loopyV += 0x20; break; } } else { // while rendering, it seems to drop by 1 line, regardless of increment mode loopyV += 0x1000; } break; default: break; } } /** * PPU is on if either background or sprites are enabled * * @return true */ public boolean renderingOn() { return bgOn || spritesOn; } /** * MMC3 scan line counter isn't clocked if background and sprites are using * the same half of the pattern table * * @return true if PPU is rendering and BG and sprites are using different * pattern tables */ public final boolean mmc3CounterClocking() { return (bgpattern != sprpattern) && renderingOn(); } /** * Runs the PPU emulation for one NES scan line. */ public final void clockLine(int scanline) { //skip a PPU clock on line 0 of odd frames when rendering is on //and we are in NTSC mode (pal has no skip) int skip = (numscanlines == 262 && scanline == 0 && renderingOn() && !((framecount & (utils.BIT1)) != 0)) ? 1 : 0; for (cycles = skip; cycles < 341; ++cycles) { clock(); } } private int tileAddr = 0; private int cpudividerctr = 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 scanline 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 && cycles >= 1 && cycles <= 256) { int bufferoffset = (scanline << 8) + (cycles - 1); //bg drawing if (bgOn) { //if background is on, draw a dot of that first final boolean isBG = drawBGPixel(bufferoffset); //sprite drawing drawSprites(scanline, 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, 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; } } } 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(); } } private void incLoopyVVert() { //increment loopy_v to next row of tiles if ((loopyV & 0x7000) == 0x7000) { //reset the fine scroll bits and increment tile address to next row loopyV &= ~0x7000; int y = (loopyV & 0x03E0) >> 5; if (y == 29) { //if row is 29 zero fine scroll and bump to next nametable y = 0; loopyV ^= 0x0800; } else { //increment (wrap to 5 bits) but if row is already over 29 //we don't bump loopyV to next nt. y = (y + 1) & 31; } loopyV = (loopyV & ~0x03E0) | (y << 5); } else { //increment the fine scroll loopyV += 0x1000; } } private void incLoopyVHoriz() { //increment horizontal part of loopyv if ((loopyV & 0x001F) == 31) // if coarse X == 31 { loopyV &= ~0x001F; // coarse X = 0 loopyV ^= 0x0400;// switch horizontal nametable } else { loopyV += 1;// increment coarse X } } private void fetchNTByte() { //fetch nt byte tileAddr = mapper.ppuRead( ((loopyV & 0xc00) | 0x2000) + (loopyV & 0x3ff)) * 16 + (bgpattern ? 0x1000 : 0); } private boolean drawBGPixel(int bufferoffset) { //background drawing //loopyX picks bits final boolean isBG; if (bgClip && (bufferoffset & 0xff) < 8) { //left hand of screen clipping //(needs to be marked as BG and not cause a sprite hit) bitmap[bufferoffset] = pal[0]; isBG = true; } else { final int bgPix = (((bgShiftRegH >> -loopyX + 16) & 1) << 1) + ((bgShiftRegL >> -loopyX + 16) & 1); final int bgPal = (((bgAttrShiftRegH >> -loopyX + 8) & 1) << 1) + ((bgAttrShiftRegL >> -loopyX + 8) & 1); isBG = (bgPix == 0); bitmap[bufferoffset] = isBG ? pal[0] : pal[(bgPal << 2) + bgPix]; } bgShiftClock(); return isBG; } private void bgShiftClock() { bgShiftRegH <<= 1; bgShiftRegL <<= 1; bgAttrShiftRegH <<= 1; bgAttrShiftRegL <<= 1; } boolean dotcrawl = true; private boolean sprite0here = false; /** * evaluates PPU sprites for the NEXT scanline */ private void evalSprites() { sprite0here = false; int ypos, offset; found = 0; Arrays.fill(secOAM, 0xff); //primary evaluation //need to emulate behavior when OAM address is set to nonzero here for (int spritestart = oamstart; spritestart < 255; spritestart += 4) { //for each sprite, first we cull the non-visible ones ypos = OAM[spritestart]; offset = scanline - ypos; if (ypos > scanline || offset > (spritesize ? 15 : 7)) { //sprite is out of range vertically continue; } //if we're here it's a valid renderable sprite if (spritestart == 0) { sprite0here = true; } //actually which sprite is flagged for sprite 0 depends on the starting //oam address which is, on the real thing, not necessarily zero. if (found >= 8) { //if more than 8 sprites, set overflow bit and STOP looking //todo: add "no sprite limit" option back spriteoverflow = true; break; //also the real PPU does strange stuff on sprite overflow //todo: emulate register trashing that happens when overflow } else { //set up ye sprite for rendering secOAM[found * 4] = OAM[spritestart]; // secOAM[found * 4 + 1] = OAM[spritestart + 1]; // secOAM[found * 4 + 2] = OAM[spritestart + 2]; // secOAM[found * 4 + 3] = OAM[spritestart + 3]; final int oamextra = OAM[spritestart + 2]; //bg flag spritebgflags[found] = ((oamextra & (utils.BIT5)) != 0); //x value spriteXlatch[found] = OAM[spritestart + 3]; spritepals[found] = ((oamextra & 3) + 4) * 4; if (((oamextra & (utils.BIT7)) != 0)) { //if sprite is flipped vertically, reverse the offset offset = (spritesize ? 15 : 7) - offset; } //now correction for the fact that 8x16 tiles are 2 separate tiles if (offset > 7) { offset += 8; } //get tile address (8x16 sprites can use both pattern tbl pages but only the even tiles) final int tilenum = OAM[spritestart + 1]; spriteFetch(spritesize, tilenum, offset, oamextra); ++found; } } for (int i = found; i < 8; ++i) { //fill unused sprite registers with zeros spriteshiftregL[found] = 0; spriteshiftregH[found] = 0; //also, we need to do 8 reads no matter how many sprites we found //dummy reads are to sprite 0xff spriteFetch(spritesize, 0xff, 0, 0); } } 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); } } /** * draws appropriate pixel of the sprites selected by sprite evaluation */ private void drawSprites(int line, int x, boolean bgflag) { final int startdraw = !spriteClip ? 0 : 8;//sprite left 8 pixels clip int sprpxl = 0; int index = 7; //check all the used sprite slots to see if any sprite covers this pixel for (int y = found - 1; y >= 0; --y) { int off = x - spriteXlatch[y]; if (off >= 0 && off <= 8) { if ((spriteshiftregH[y] & 1) + (spriteshiftregL[y] & 1) != 0) { index = y; sprpxl = 2 * (spriteshiftregH[y] & 1) + (spriteshiftregL[y] & 1); } spriteshiftregH[y] >>= 1; spriteshiftregL[y] >>= 1; } } if (sprpxl == 0 || x < startdraw || !spritesOn) { //no opaque sprite pixel here return; } if (sprite0here && (index == 0) && !bgflag && x < 255) { //sprite 0 hit! sprite0hit = true; } //now, FINALLY, drawing. if (!spritebgflags[index] || bgflag) { bitmap[(line << 8) + x] = pal[spritepals[index] + sprpxl]; } } /** * 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; } } /** * 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(); } /** * 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. * * @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; } /** * Sends off a frame of NES video to be rendered by the GUI. also includes * dot crawl flag and BG color to be displayed around edges which are needed * for the NTSC renderer. * * @param gui the GUI window to render to */ public final void renderFrame(GUIInterface gui) { if (PPUDEBUG) { debugDraw(); } if (gui != null) { gui.setFrame(bitmap, bgcolors, dotcrawl); } } }