class PPU: # private classes of PPU class NameTable: def __init__(self, width, height, name): self.width = width self.height = height self.name = name self.tile = [None]*(width*height) self.attrib = [None]*(width*height) def getTileIndex(self, x, y): return self.tile[y * self.width + x] def getAttrib(self, x, y): return self.attrib[y * self.width + x] def writeAttrib(self, index, value): basex = (index % 8) * 4 basey = (index / 8) * 4 for sqy in range (0, 2): for sqx in range (0, 2): add = (value>>(2*(sqy*2+sqx)))&3 for y in range(0, 2): for x in range(0, 2): tx = basex + sqx*2 + x ty = basey + sqy*2 + y attindex = ty*self.width+tx self.attrib[ty*self.width+tx] = (add<<2)&12 class PaletteTable: def __init__(self): self.curTable = [None]*64 self.emphTable = [[None]*64 for i in range(8)] self.currentEmph = -1 def reset(self): self.setEmphasis(0) def loadNTSCPalette(self): self.curTable = [0x525252, 0xB40000, 0xA00000, 0xB1003D, 0x740069, 0x00005B, 0x00005F, 0x001840, 0x002F10, 0x084A08, 0x006700, 0x124200, 0x6D2800, 0x000000, 0x000000, 0x000000, 0xC4D5E7, 0xFF4000, 0xDC0E22, 0xFF476B, 0xD7009F, 0x680AD7, 0x0019BC, 0x0054B1, 0x006A5B, 0x008C03, 0x00AB00, 0x2C8800, 0xA47200, 0x000000, 0x000000, 0x000000, 0xF8F8F8, 0xFFAB3C, 0xFF7981, 0xFF5BC5, 0xFF48F2, 0xDF49FF, 0x476DFF, 0x00B4F7, 0x00E0FF, 0x00E375, 0x03F42B, 0x78B82E, 0xE5E218, 0x787878, 0x000000, 0x000000, 0xFFFFFF, 0xFFF2BE, 0xF8B8B8, 0xF8B8D8, 0xFFB6FF, 0xFFC3FF, 0xC7D1FF, 0x9ADAFF, 0x88EDF8, 0x83FFDD, 0xB8F8B8, 0xF5F8AC, 0xFFFFB0, 0xF8D8F8, 0x000000, 0x000000] self.makeTables() self.setEmphasis(0) def loadPALPalette(self): self.curTable = [0x525252, 0xB40000, 0xA00000, 0xB1003D, 0x740069, 0x00005B, 0x00005F, 0x001840, 0x002F10, 0x084A08, 0x006700, 0x124200, 0x6D2800, 0x000000, 0x000000, 0x000000, 0xC4D5E7, 0xFF4000, 0xDC0E22, 0xFF476B, 0xD7009F, 0x680AD7, 0x0019BC, 0x0054B1, 0x006A5B, 0x008C03, 0x00AB00, 0x2C8800, 0xA47200, 0x000000, 0x000000, 0x000000, 0xF8F8F8, 0xFFAB3C, 0xFF7981, 0xFF5BC5, 0xFF48F2, 0xDF49FF, 0x476DFF, 0x00B4F7, 0x00E0FF, 0x00E375, 0x03F42B, 0x78B82E, 0xE5E218, 0x787878, 0x000000, 0x000000, 0xFFFFFF, 0xFFF2BE, 0xF8B8B8, 0xF8B8D8, 0xFFB6FF, 0xFFC3FF, 0xC7D1FF, 0x9ADAFF, 0x88EDF8, 0x83FFDD, 0xB8F8B8, 0xF5F8AC, 0xFFFFB0, 0xF8D8F8, 0x000000, 0x000000] self.makeTables() self.setEmphasis(0) def makeTables(self): # Calculate a table for each possible emphasis setting for emph in range(0, 8): # Color component factors rFactor, gFactor, bFactor = 1.0, 1.0, 1.0 if not emph&1 == 0: rFactor, bFactor = 0.75, 0.75 if not emph&2 == 0: rFactor, gFactor = 0.75, 0.75 if not emph&4 == 0: gFactor, bFactor = 0.75, 0.75 self.emphTable[emph] = [None]*64 for i in range(64): col = self.curTable[i] r = int(self.getRed(col) * rFactor) g = int(self.getGreen(col) * gFactor) b = int(self.getBlue(col) * bFactor) self.emphTable[emph][i] = self.getRgb(r,g,b) def setEmphasis(self, emph): if not emph == self.currentEmph: self.currentEmph = emph self.curTable = self.emphTable[emph][:] def getEntry(self, yiq): return self.curTable[yiq] def getRed(self, rgb): return (rgb>>16)&0xFF def getGreen(self, rgb): return (rgb>>8)&0xFF def getBlue(self, rgb): return (rgb)&0xFF def getRgb(self, r, g, b): return ((r<<16)|(g<<8)|(b)) def loadDefaultPalette(self): self.curTable[ 0] = self.getRgb(117,117,117) self.curTable[ 1] = self.getRgb( 39, 27,143) self.curTable[ 2] = self.getRgb( 0, 0,171) self.curTable[ 3] = self.getRgb( 71, 0,159) self.curTable[ 4] = self.getRgb(143, 0,119) self.curTable[ 5] = self.getRgb(171, 0, 19) self.curTable[ 6] = self.getRgb(167, 0, 0) self.curTable[ 7] = self.getRgb(127, 11, 0) self.curTable[ 8] = self.getRgb( 67, 47, 0) self.curTable[ 9] = self.getRgb( 0, 71, 0) self.curTable[10] = self.getRgb( 0, 81, 0) self.curTable[11] = self.getRgb( 0, 63, 23) self.curTable[12] = self.getRgb( 27, 63, 95) self.curTable[13] = self.getRgb( 0, 0, 0) self.curTable[14] = self.getRgb( 0, 0, 0) self.curTable[15] = self.getRgb( 0, 0, 0) self.curTable[16] = self.getRgb(188,188,188) self.curTable[17] = self.getRgb( 0,115,239) self.curTable[18] = self.getRgb( 35, 59,239) self.curTable[19] = self.getRgb(131, 0,243) self.curTable[20] = self.getRgb(191, 0,191) self.curTable[21] = self.getRgb(231, 0, 91) self.curTable[22] = self.getRgb(219, 43, 0) self.curTable[23] = self.getRgb(203, 79, 15) self.curTable[24] = self.getRgb(139,115, 0) self.curTable[25] = self.getRgb( 0,151, 0) self.curTable[26] = self.getRgb( 0,171, 0) self.curTable[27] = self.getRgb( 0,147, 59) self.curTable[28] = self.getRgb( 0,131,139) self.curTable[29] = self.getRgb( 0, 0, 0) self.curTable[30] = self.getRgb( 0, 0, 0) self.curTable[31] = self.getRgb( 0, 0, 0) self.curTable[32] = self.getRgb(255,255,255) self.curTable[33] = self.getRgb( 63,191,255) self.curTable[34] = self.getRgb( 95,151,255) self.curTable[35] = self.getRgb(167,139,253) self.curTable[36] = self.getRgb(247,123,255) self.curTable[37] = self.getRgb(255,119,183) self.curTable[38] = self.getRgb(255,119, 99) self.curTable[39] = self.getRgb(255,155, 59) self.curTable[40] = self.getRgb(243,191, 63) self.curTable[41] = self.getRgb(131,211, 19) self.curTable[42] = self.getRgb( 79,223, 75) self.curTable[43] = self.getRgb( 88,248,152) self.curTable[44] = self.getRgb( 0,235,219) self.curTable[45] = self.getRgb( 0, 0, 0) self.curTable[46] = self.getRgb( 0, 0, 0) self.curTable[47] = self.getRgb( 0, 0, 0) self.curTable[48] = self.getRgb(255,255,255) self.curTable[49] = self.getRgb(171,231,255) self.curTable[50] = self.getRgb(199,215,255) self.curTable[51] = self.getRgb(215,203,255) self.curTable[52] = self.getRgb(255,199,255) self.curTable[53] = self.getRgb(255,199,219) self.curTable[54] = self.getRgb(255,191,179) self.curTable[55] = self.getRgb(255,219,171) self.curTable[56] = self.getRgb(255,231,163) self.curTable[57] = self.getRgb(227,255,163) self.curTable[58] = self.getRgb(171,243,191) self.curTable[59] = self.getRgb(179,255,207) self.curTable[60] = self.getRgb(159,255,243) self.curTable[61] = self.getRgb( 0, 0, 0) self.curTable[62] = self.getRgb( 0, 0, 0) self.curTable[63] = self.getRgb( 0, 0, 0) self.makeTables() self.setEmphasis(0) # PPU class variables STATUS_VRAMWRITE= 4 STATUS_SLSPRITECOUNT= 5 STATUS_SPRITE0HIT= 6 STATUS_VBLANK= 7 def __init__(self, nes): self.nes = nes self.screen = Screen(nes) def reset(self): self.screen.reset() self.cycles = 0 # memory self.vramMem = bytearray('\x00'*0x8000) self.spriteMem = bytearray('\x00'*0x100) # VRAM I/O self.vramAddress = 0 self.vramTmpAddress = 0 self.vramBufferedReadValue = 0 self.firstWrite = True # SPR-RAM I/O self.sramAddress = 0 # 8-bit only self.mapperIrqCounter = 0 self.currentMirroring = -1 self.requestEndFrame = False self.nmiOk = False self.dummyCycleToggle = False self.validTileData = False self.nmiCounter = 0 self.scanlineAlreadyRendered = False # Control Flags Register 1: self.f_nmiOnVblank = 0 # NMI on VBlank. 0=disable, 1=enable self.f_spriteSize = 0 # Sprite size. 0=8x8, 1=8x16 self.f_bgPatternTable = 0 # Background Pattern Table address. 0=0x0000,1=0x1000 self.f_spPatternTable = 0 # Sprite Pattern Table address. 0=0x0000,1=0x1000 self.f_addrInc = 0 # PPU Address Increment. 0=1,1=32 self.f_nTblAddress = 0 # Name Table Address. 0=0x2000,1=0x2400,2=0x2800,3=0x2C00 # Control Flags Register 2: self.f_color = 0 # Background color. 0=black, 1=blue, 2=green, 4=red self.f_spVisibility = 0 # Sprite visibility. 0=not displayed,1=displayed self.f_bgVisibility = 0 # Background visibility. 0=Not Displayed,1=displayed self.f_spClipping = 0 # Sprite clipping. 0=Sprites invisible in left 8-pixel column,1=No clipping self.f_bgClipping = 0 # Background clipping. 0=BG invisible in left 8-pixel column, 1=No clipping self.f_dispType = 0 # Display type. 0=color, 1=monochrome # Counters: self.cntFV = 0 self.cntV = 0 self.cntH = 0 self.cntVT = 0 self.cntHT = 0 # Registers: self.regFV = 0 self.regV = 0 self.regH = 0 self.regVT = 0 self.regHT = 0 self.regFH = 0 self.regS = 0 self.scanlineChanged = [True]*240 # These are temporary variables used in rendering and sound procedures. # Their states outside of those procedures can be ignored. # TODO: the use of self is a bit weird, investigate self.curNt = 0 # Variables used when rendering: self.attrib = [None]*32; self.buffer = [None]*(256*240) self.prevBuffer = [None]*(256*240) self.bgbuffer = [None]*(256*240) self.pixrendered = [None]*(256*240) self.spr0dummybuffer = [None]*(256*240) self.dummyPixPriTable = [None]*(256*240) self.validTileData = False; self.scantile = [None]*32 # Initialize misc vars: self.scanline = 0 self.lastRenderedScanline = -1 self.curX = 0 # Sprite data: self.sprX = [0]*(64) # X coordinate self.sprY = [0]*(64) # Y coordinate self.sprTile = [0]*(64) # Tile Index (into pattern table) self.sprCol = [0]*(64) # Upper two bits of color self.vertFlip = [False]*(64) # Vertical Flip self.horiFlip = [False]*(64) # Horizontal Flip self.bgPriority = [False]*(64) # Background priority self.spr0HitX = 0 # Sprite #0 hit X coordinate self.spr0HitY = 0 # Sprite #0 hit Y coordinate self.hitSpr0 = False # Palette data: self.sprPalette = [0]*(16) self.imgPalette = [0]*(16) # Create pattern table tile buffers: self.ptTile = [Tile() for i in range(512)] # Create nametable buffers: # Name table data: self.ntable1 = [None]*(4) self.currentMirroring = -1 self.nameTable = [PPU.NameTable(32,32, "Nt{0}".format(i)) for i in range(4)] # Initialize mirroring lookup table: self.vramMirrorTable = [i for i in range(0x8000)] self.showSpr0Hit = False self.clipToTvSize = True self.palTable = PPU.PaletteTable() self.palTable.loadNTSCPalette() self.palTable.loadDefaultPalette() self.updateControlReg1(0) self.updateControlReg2(0) self.oldFrame = [-1]*(256*240) def setMirroring(self, mirroring): if(mirroring == self.currentMirroring): return self.currentMirroring = mirroring self.triggerRendering() # Remove mirroring: if(not self.vramMirrorTable): self.vramMirrorTable = [i for i in range(0x8000)] # Palette mirroring: self.defineMirrorRegion(0x3f20,0x3f00,0x20) self.defineMirrorRegion(0x3f40,0x3f00,0x20) self.defineMirrorRegion(0x3f80,0x3f00,0x20) self.defineMirrorRegion(0x3fc0,0x3f00,0x20) # Additional mirroring: self.defineMirrorRegion(0x3000,0x2000,0xf00) self.defineMirrorRegion(0x4000,0x0000,0x4000) if(mirroring == self.nes.rom.HORIZONTAL_MIRRORING): # Horizontal mirroring. self.ntable1[0] = 0 self.ntable1[1] = 0 self.ntable1[2] = 1 self.ntable1[3] = 1 self.defineMirrorRegion(0x2400,0x2000,0x400) self.defineMirrorRegion(0x2c00,0x2800,0x400) elif(mirroring == self.nes.rom.VERTICAL_MIRRORING): # Vertical mirroring. self.ntable1[0] = 0 self.ntable1[1] = 1 self.ntable1[2] = 0 self.ntable1[3] = 1 self.defineMirrorRegion(0x2800,0x2000,0x400) self.defineMirrorRegion(0x2c00,0x2400,0x400) elif(mirroring == self.nes.rom.SINGLESCREEN_MIRRORING): # Single Screen mirroring self.ntable1[0] = 0 self.ntable1[1] = 0 self.ntable1[2] = 0 self.ntable1[3] = 0 self.defineMirrorRegion(0x2400,0x2000,0x400) self.defineMirrorRegion(0x2800,0x2000,0x400) self.defineMirrorRegion(0x2c00,0x2000,0x400) elif(mirroring == self.nes.rom.SINGLESCREEN_MIRRORING2): self.ntable1[0] = 1 self.ntable1[1] = 1 self.ntable1[2] = 1 self.ntable1[3] = 1 self.defineMirrorRegion(0x2400,0x2400,0x400) self.defineMirrorRegion(0x2800,0x2400,0x400) self.defineMirrorRegion(0x2c00,0x2400,0x400) else: # Assume Four-screen mirroring. self.ntable1[0] = 0 self.ntable1[1] = 1 self.ntable1[2] = 2 self.ntable1[3] = 3 def defineMirrorRegion(self, fromStart, toStart, size): """ Define a mirrored area in the address lookup table. Assumes the regions don't overlap. The 'to' region is the region that is physically in memory. """ for i in range(size): self.vramMirrorTable[fromStart+i] = toStart+i def startVBlank(self): # Do NMI: self.nes.cpu.requestIrq(self.nes.cpu.IRQ_NMI) # Make sure everything is rendered: if(self.lastRenderedScanline < 239): self.renderFramePartially( self.lastRenderedScanline+1,240-self.lastRenderedScanline ) # End frame: self.endFrame() # Reset scanline counter: self.lastRenderedScanline = -1 self.startFrame() def endScanline(self): if self.scanline == 19: # Dummy scanline. # May be variable length: if(self.dummyCycleToggle): # Remove dead cycle at end of scanline, # for next scanline: self.curX = 1 self.dummyCycleToggle = not self.dummyCycleToggle elif self.scanline == 20: # Clear VBlank flag: self.setStatusFlag(self.STATUS_VBLANK,False) # Clear Sprite #0 hit flag: self.setStatusFlag(self.STATUS_SPRITE0HIT,False) self.hitSpr0 = False self.spr0HitX = -1 self.spr0HitY = -1 if(self.f_bgVisibility == 1 or self.f_spVisibility==1): # Update counters: self.cntFV = self.regFV self.cntV = self.regV self.cntH = self.regH self.cntVT = self.regVT self.cntHT = self.regHT if(self.f_bgVisibility==1): # Render dummy scanline: self.renderBgScanline(False,0) if(self.f_bgVisibility==1 and self.f_spVisibility==1): # Check sprite 0 hit for first scanline: self.checkSprite0(0) if(self.f_bgVisibility==1 or self.f_spVisibility==1): # Clock mapper IRQ Counter: self.nes.mmap.clockIrqCounter() elif self.scanline == 261: # Dead scanline, no rendering. # Set VINT: self.setStatusFlag(self.STATUS_VBLANK,True) self.requestEndFrame = True self.nmiCounter = 9 # Wrap around: self.scanline = -1 # will be incremented to 0 else: if(self.scanline>=21 and self.scanline<=260): # Render normally: if(self.f_bgVisibility == 1): if(not self.scanlineAlreadyRendered): # update scroll: self.cntHT = self.regHT self.cntH = self.regH self.renderBgScanline(True,self.scanline+1-21) self.scanlineAlreadyRendered=False # Check for sprite 0 (next scanline): if((not self.hitSpr0) and self.f_spVisibility==1): if(self.sprX[0]>=-7 and self.sprX[0]<256 and self.sprY[0]+1<=(self.scanline-20) and (self.sprY[0]+1+(8 if self.f_spriteSize==0 else 16))>=(self.scanline-20)): if(self.checkSprite0(self.scanline-20)): #console.log("found spr0. curscan="+self.scanline+" hitscan="+self.spr0HitY) self.hitSpr0 = True if(self.f_bgVisibility==1 or self.f_spVisibility==1): # Clock mapper IRQ Counter: self.nes.mmap.clockIrqCounter() self.scanline+=1 self.regsToAddress() self.cntsToAddress() def startFrame(self): # Set background color: bgColor=0 if(self.f_dispType == 0): # Color display. # f_color determines color emphasis. # Use first entry of image palette as BG color. bgColor = self.imgPalette[0] else: # Monochrome display. # f_color determines the bg color. if self.f_color == 0: # Black bgColor = 0x000000 elif self.f_color == 1: # Green bgColor = 0x00FF00 elif self.f_color == 2: # Blue bgColor = 0xFF0000 elif self.f_color == 3: # Invalid. Use black. bgColor = 0x000000 elif self.f_color == 4: # Red bgColor = 0x0000FF else: # Invalid. Use black. bgColor = 0x000000 buffer = self.buffer for i in range(256*240): buffer[i] = bgColor pixrendered = self.pixrendered for i in range(len(pixrendered)): pixrendered[i]=65 def endFrame(self): buffer = self.buffer; # Draw spr#0 hit coordinates: if(self.showSpr0Hit): # Spr 0 position: if(self.sprX[0]>=0 and self.sprX[0]<256 and self.sprY[0]>=0 and self.sprY[0]<240): for i in range(256): buffer[(self.sprY[0]<<8)+i] = 0xFF5555; for i in range(240): buffer[(i<<8)+self.sprX[0]] = 0xFF5555; # Hit position: if(self.spr0HitX>=0 and self.spr0HitX<256 and self.spr0HitY>=0 and self.spr0HitY<240): for i in range(256): buffer[(self.spr0HitY<<8)+i] = 0x55FF55; for i in range(240): buffer[(i<<8)+self.spr0HitX] = 0x55FF55; # This is a bit lazy.. # if either the sprites or the background should be clipped, # both are clipped after rendering is finished. if(self.clipToTvSize or self.f_bgClipping==0 or self.f_spClipping==0): # Clip left 8-pixels column: for y in range(240): for x in range(8): buffer[(y<<8)+x] = 0; if(self.clipToTvSize): # Clip right 8-pixels column too: for y in range(240): for x in range(8): buffer[(y<<8)+255-x] = 0; # Clip top and bottom 8 pixels: if(self.clipToTvSize): for y in range(8): for x in range(256): buffer[(y<<8)+x] = 0; buffer[((239-y)<<8)+x] = 0; # print buffer if (self.nes.opts.showDisplay): #imageData = self.canvasImageData.data; #prevBuffer = self.prevBuffer; # #for i in range(256*240): # pixel = buffer[i]; # if (pixel != prevBuffer[i]) : # j = i*4; # imageData[j] = pixel&0xFF; # imageData[j+1] = (pixel>>8)&0xFF; # imageData[j+2] = (pixel>>16)&0xFF; # prevBuffer[i] = pixel; self.screen.putImageData(buffer, 0, 0); def updatePalettes(self): """ Reads data from $3f00 to $f20 into the two buffered palettes. """ for i in range(16): if self.f_dispType == 0: self.imgPalette[i] = self.palTable.getEntry( self.vramMem[0x3f00+i] & 63 ) else: self.imgPalette[i] = self.palTable.getEntry( self.vramMem[0x3f00+i] & 32 ) for i in range(16): if self.f_dispType == 0: self.sprPalette[i] = self.palTable.getEntry( self.vramMem[0x3f10+i] & 63 ) else: self.sprPalette[i] = self.palTable.getEntry( self.vramMem[0x3f10+i] & 32 ) def updateControlReg1(self, value): self.triggerRendering() self.f_nmiOnVblank = (value>>7)&1 self.f_spriteSize = (value>>5)&1 self.f_bgPatternTable = (value>>4)&1 self.f_spPatternTable = (value>>3)&1 self.f_addrInc = (value>>2)&1 self.f_nTblAddress = value&3 self.regV = (value>>1)&1 self.regH = value&1 self.regS = (value>>4)&1 def updateControlReg2(self, value): self.triggerRendering() self.f_color = (value>>5)&7 self.f_spVisibility = (value>>4)&1 self.f_bgVisibility = (value>>3)&1 self.f_spClipping = (value>>2)&1 self.f_bgClipping = (value>>1)&1 self.f_dispType = value&1 if self.f_dispType == 0: self.palTable.setEmphasis(self.f_color) self.updatePalettes() def setStatusFlag(self, flag, value): n = 1<<flag self.nes.cpu.mem[0x2002] = ((self.nes.cpu.mem[0x2002] & (255-n)) | (n if value else 0)) def readStatusRegister(self): """ CPU Register $2002: Read the Status Register. """ rv = self.nes.cpu.mem[0x2002] # Reset scroll & VRAM Address toggle: self.firstWrite = True # Clear VBlank flag: self.setStatusFlag(self.STATUS_VBLANK,False) # Fetch status data: return rv def writeSRAMAddress(self, address): """ CPU Register $2003: Write the SPR-RAM address that is used for sramWrite (Register 0x2004 in CPU memory map) """ self.sramAddress = address def sramLoad(self): """ CPU Register $2004 (R): Read from SPR-RAM (Sprite RAM). The address should be set first. """ return self.spriteMem[self.sramAddress] def sramWrite(self, value): """ CPU Register $2004 (W): Write to SPR-RAM (Sprite RAM). The address should be set first. """ self.spriteMem[self.sramAddress] = value self.spriteRamWriteUpdate(self.sramAddress,value) self.sramAddress+=1 self.sramAddress %= 0x100 def scrollWrite(self, value): """ CPU Register $2005: Write to scroll registers. The first write is the vertical offset, the second is the horizontal offset: """ self.triggerRendering() if(self.firstWrite): # First write, horizontal scroll: self.regHT = (value>>3)&31 self.regFH = value&7 else: # Second write, vertical scroll: self.regFV = value&7 self.regVT = (value>>3)&31 self.firstWrite = not self.firstWrite def writeVRAMAddress(self, address): """ CPU Register $2006: Sets the adress used when reading/writing from/to VRAM. The first write sets the high byte, the second the low byte. """ if(self.firstWrite): self.regFV = (address>>4)&3 self.regV = (address>>3)&1 self.regH = (address>>2)&1 self.regVT = (self.regVT&7) | ((address&3)<<3) else: self.triggerRendering() self.regVT = (self.regVT&24) | ((address>>5)&7) self.regHT = address&31 self.cntFV = self.regFV self.cntV = self.regV self.cntH = self.regH self.cntVT = self.regVT self.cntHT = self.regHT self.checkSprite0(self.scanline-20) self.firstWrite = not self.firstWrite # Invoke mapper latch: self.cntsToAddress() if(self.vramAddress < 0x2000): self.nes.mmap.latchAccess(self.vramAddress) def vramLoad(self): """ CPU Register $2007(R): Read from PPU memory. The address should be set first. """ self.cntsToAddress() self.regsToAddress() # If address is in range 0x0000-0x3EFF, return buffered values: if(self.vramAddress <= 0x3EFF): tmp = self.vramBufferedReadValue # Update buffered value: if(self.vramAddress < 0x2000): self.vramBufferedReadValue = self.vramMem[self.vramAddress] else: self.vramBufferedReadValue = self.mirroredLoad(self.vramAddress) # Mapper latch access: if(self.vramAddress < 0x2000): self.nes.mmap.latchAccess(self.vramAddress) # Increment by either 1 or 32, depending on d2 of Control Register 1: self.vramAddress += (32 if self.f_addrInc==1 else 1) self.cntsFromAddress() self.regsFromAddress() return tmp # Return the previous buffered value. # No buffering in self mem range. Read normally. tmp = self.mirroredLoad(self.vramAddress) # Increment by either 1 or 32, depending on d2 of Control Register 1: self.vramAddress += (32 if self.f_addrInc==1 else 1) self.cntsFromAddress() self.regsFromAddress() return tmp def vramWrite(self, value): """ CPU Register $2007(W): Write to PPU memory. The address should be set first. """ self.triggerRendering() self.cntsToAddress() self.regsToAddress() if(self.vramAddress >= 0x2000): # Mirroring is used. self.mirroredWrite(self.vramAddress,value) else: # Write normally. self.writeMem(self.vramAddress,value) # Invoke mapper latch: self.nes.mmap.latchAccess(self.vramAddress) # Increment by either 1 or 32, depending on d2 of Control Register 1: self.vramAddress += (32 if self.f_addrInc==1 else 1) self.regsFromAddress() self.cntsFromAddress() def sramDMA(self, value): """ CPU Register $4014: Write 256 bytes of main memory into Sprite RAM. """ baseAddress = value * 0x100 for i in range(self.sramAddress, 256): data = self.nes.cpu.mem[baseAddress+i] self.spriteMem[i] = data self.spriteRamWriteUpdate(i, data) self.nes.cpu.haltCycles(513) def regsFromAddress(self): """ Updates the scroll registers from a new VRAM address. """ address = (self.vramTmpAddress>>8)&0xFF self.regFV = (address>>4)&7 self.regV = (address>>3)&1 self.regH = (address>>2)&1 self.regVT = (self.regVT&7) | ((address&3)<<3) address = self.vramTmpAddress&0xFF self.regVT = (self.regVT&24) | ((address>>5)&7) self.regHT = address&31 def cntsFromAddress(self): """ Updates the scroll registers from a new VRAM address. """ address = (self.vramAddress>>8)&0xFF self.cntFV = (address>>4)&3 self.cntV = (address>>3)&1 self.cntH = (address>>2)&1 self.cntVT = (self.cntVT&7) | ((address&3)<<3) address = self.vramAddress&0xFF self.cntVT = (self.cntVT&24) | ((address>>5)&7) self.cntHT = address&31 def regsToAddress(self): b1 = (self.regFV&7)<<4 b1 |= (self.regV&1)<<3 b1 |= (self.regH&1)<<2 b1 |= (self.regVT>>3)&3 b2 = (self.regVT&7)<<5 b2 |= self.regHT&31 self.vramTmpAddress = ((b1<<8) | b2)&0x7FFF def cntsToAddress(self): b1 = (self.cntFV&7)<<4 b1 |= (self.cntV&1)<<3 b1 |= (self.cntH&1)<<2 b1 |= (self.cntVT>>3)&3 b2 = (self.cntVT&7)<<5 b2 |= self.cntHT&31 self.vramAddress = ((b1<<8) | b2)&0x7FFF def incTileCounter(self, count): for i in range(count, -1, -1): self.cntHT+=1 if(self.cntHT==32): self.cntHT=0 self.cntVT+=1 if(self.cntVT>=30): self.cntH+=1 if(self.cntH==2): self.cntH=0 self.cntV+=1 if(self.cntV==2): self.cntV=0 self.cntFV+=1 self.cntFV&=0x7 def mirroredLoad(self, address): """ Reads from memory, taking into account mirroring/mapping of address ranges. """ return self.vramMem[self.vramMirrorTable[address]] def mirroredWrite(self, address, value): """ Writes to memory, taking into account mirroring/mapping of address ranges. """ if(address>=0x3f00 and address<0x3f20): # Palette write mirroring. if(address==0x3F00 or address==0x3F10): self.writeMem(0x3F00,value) self.writeMem(0x3F10,value) elif(address==0x3F04 or address==0x3F14): self.writeMem(0x3F04,value) self.writeMem(0x3F14,value) elif(address==0x3F08 or address==0x3F18): self.writeMem(0x3F08,value) self.writeMem(0x3F18,value) elif(address==0x3F0C or address==0x3F1C): self.writeMem(0x3F0C,value) self.writeMem(0x3F1C,value) else: self.writeMem(address,value) else: # Use lookup table for mirrored address: if(address<len(self.vramMirrorTable)): self.writeMem(self.vramMirrorTable[address],value) else: # FIXME print "Invalid VRAM address: {0}".format(address) def triggerRendering(self): if 260 >= self.scanline >= 21: # Render sprites, and combine self.renderFramePartially( self.lastRenderedScanline + 1, self.scanline - 21 - self.lastRenderedScanline ) self.lastRenderedScanline = self.scanline - 21 def renderFramePartially(self, startScan, scanCount): if self.f_spVisibility == 1: self.renderSpritesPartially(startScan,scanCount,True) if self.f_bgVisibility == 1: si = startScan << 8 ei = (startScan + scanCount) << 8 ei = 0xF000 if ei > 0xF000 else ei buffer = self.buffer bgbuffer = self.bgbuffer pixrendered = self.pixrendered for destIndex in range(si, ei): if pixrendered[destIndex] > 0xFF: buffer[destIndex] = bgbuffer[destIndex] if self.f_spVisibility == 1: self.renderSpritesPartially(startScan, scanCount, False) self.validTileData = False def renderBgScanline(self, bgbuffer, scan): baseTile = 0 if (self.regS == 0) else 256 destIndex = (scan <<8) - self.regFH self.curNt = self.ntable1[self.cntV+self.cntV+self.cntH] self.cntHT = self.regHT self.cntH = self.regH self.curNt = self.ntable1[self.cntV+self.cntV+self.cntH] if (scan<240 and (scan-self.cntFV)>=0): tscanoffset = self.cntFV<<3 scantile = self.scantile attrib = self.attrib ptTile = self.ptTile nameTable = self.nameTable imgPalette = self.imgPalette pixrendered = self.pixrendered targetBuffer = self.bgbuffer if bgbuffer else self.buffer t, tpix, att, col = 0, 0, 0, 0 for tile in range(32): if(scan>=0): # Fetch tile & attrib data: if(self.validTileData): # Get data from array: t = scantile[tile] tpix = t.pix att = attrib[tile] else: # Fetch data: t = ptTile[baseTile+nameTable[self.curNt].getTileIndex(self.cntHT,self.cntVT)] tpix = t.pix att = nameTable[self.curNt].getAttrib(self.cntHT,self.cntVT) scantile[tile] = t attrib[tile] = att # Render tile scanline: sx = 0 x = (tile<<3)-self.regFH if(x>-8): if(x<0): destIndex-=x sx = -x if(t.opaque[self.cntFV]): while sx<8: pix = imgPalette[tpix[tscanoffset+sx]+att] targetBuffer[destIndex] = pix pixrendered[destIndex] |= 256 destIndex+=1 sx+=1 else: while sx<8: col = tpix[tscanoffset+sx] if(col != 0): pix = imgPalette[col+att] targetBuffer[destIndex] = pix pixrendered[destIndex] |= 256 destIndex+=1 sx+=1 # Increase Horizontal Tile Counter: self.cntHT += 1 if(self.cntHT==32): self.cntHT=0 self.cntH+=1 self.cntH%=2 self.curNt = self.ntable1[(self.cntV<<1)+self.cntH] # Tile data for one row should now have been fetched, # so the data in the array is valid. self.validTileData = True # update vertical scroll: self.cntFV+=1 if(self.cntFV==8): self.cntFV = 0 self.cntVT+=1 if(self.cntVT==30): self.cntVT = 0 self.cntV+=1 self.cntV%=2 self.curNt = self.ntable1[(self.cntV<<1)+self.cntH] elif(self.cntVT==32): self.cntVT = 0 # Invalidate fetched data: self.validTileData = False def renderSpritesPartially(self, startscan, scancount, bgPri): if(self.f_spVisibility==1): for i in range(64): if(self.bgPriority[i]==bgPri and self.sprX[i]>=0 and self.sprX[i]<256 and self.sprY[i]+8>=startscan and self.sprY[i]<startscan+scancount): # Show sprite. if(self.f_spriteSize == 0): # 8x8 sprites self.srcy1 = 0 self.srcy2 = 8 if(self.sprY[i]<startscan): self.srcy1 = startscan - self.sprY[i]-1 if(self.sprY[i]+8 > startscan+scancount): self.srcy2 = startscan+scancount-self.sprY[i]+1 if(self.f_spPatternTable==0): self.ptTile[self.sprTile[i]].render(self.buffer, 0, self.srcy1, 8, self.srcy2, self.sprX[i], self.sprY[i]+1, self.sprCol[i], self.sprPalette, self.horiFlip[i], self.vertFlip[i], i, self.pixrendered ) else: self.ptTile[self.sprTile[i]+256].render(self.buffer, 0, self.srcy1, 8, self.srcy2, self.sprX[i], self.sprY[i]+1, self.sprCol[i], self.sprPalette, self.horiFlip[i], self.vertFlip[i], i, self.pixrendered) else: # 8x16 sprites top = self.sprTile[i] if((top&1)!=0): top = self.sprTile[i]-1+256 srcy1 = 0 srcy2 = 8 if(self.sprY[i]<startscan): srcy1 = startscan - self.sprY[i]-1 if(self.sprY[i]+8 > startscan+scancount): srcy2 = startscan+scancount-self.sprY[i] self.ptTile[top+(1 if self.vertFlip[i] else 0)].render( self.buffer, 0, srcy1, 8, srcy2, self.sprX[i], self.sprY[i]+1, self.sprCol[i], self.sprPalette, self.horiFlip[i], self.vertFlip[i], i, self.pixrendered ) srcy1 = 0 srcy2 = 8 if(self.sprY[i]+8<startscan): srcy1 = startscan - (self.sprY[i]+8+1) if(self.sprY[i]+16 > startscan+scancount): srcy2 = startscan+scancount-(self.sprY[i]+8) self.ptTile[top+(0 if self.vertFlip[i] else 1)].render( self.buffer, 0, srcy1, 8, srcy2, self.sprX[i], self.sprY[i]+1+8, self.sprCol[i], self.sprPalette, self.horiFlip[i], self.vertFlip[i], i, self.pixrendered ) def checkSprite0(self, scan): self.spr0HitX = -1 self.spr0HitY = -1 toffset = 0 tIndexAdd = 0 if (self.f_spPatternTable==0) else 256 bufferIndex = 0 col = 0 bgPri = 0 t = 0 x = self.sprX[0] y = self.sprY[0]+1 if(self.f_spriteSize==0): # 8x8 sprites. # Check range: if(y<=scan and y+8>scan and x>=-7 and x<256): # Sprite is in range. # Draw scanline: t = self.ptTile[self.sprTile[0]+tIndexAdd] col = self.sprCol[0] bgPri = self.bgPriority[0] if(self.vertFlip[0]): toffset = 7-(scan-y) else: toffset = scan-y toffset*=8 bufferIndex = scan*256+x if(self.horiFlip[0]): for i in range(7, -1, -1): if(x>=0 and x<256): if(bufferIndex>=0 and bufferIndex<61440 and self.pixrendered[bufferIndex]!=0): if(t.pix[toffset+i] != 0): self.spr0HitX = bufferIndex%256 self.spr0HitY = scan return True x+=1 bufferIndex+=1 else: for i in range(8): if(x>=0 and x<256): if(bufferIndex>=0 and bufferIndex<61440 and self.pixrendered[bufferIndex]!=0): if(t.pix[toffset+i] != 0): self.spr0HitX = bufferIndex%256 self.spr0HitY = scan return True x+=1 bufferIndex+=1 else: # 8x16 sprites: # Check range: if(y<=scan and y+16>scan and x>=-7 and x<256): # Sprite is in range. # Draw scanline: if(self.vertFlip[0]): toffset = 15-(scan-y) else: toffset = scan-y if(toffset<8): # first half of sprite. t = self.ptTile[self.sprTile[0]+(1 if self.vertFlip[0] else 0)+(255 if (self.sprTile[0]&1)!=0 else 0)] else: # second half of sprite. t = self.ptTile[self.sprTile[0]+(0 if self.vertFlip[0] else 1)+(255 if (self.sprTile[0]&1)!=0 else 0)] if(self.vertFlip[0]): toffset = 15-toffset else: toffset -= 8 toffset*=8 col = self.sprCol[0] bgPri = self.bgPriority[0] bufferIndex = scan*256+x if(self.horiFlip[0]): for i in range(7, -1, -1): if(x>=0 and x<256): if(bufferIndex>=0 and bufferIndex<61440 and self.pixrendered[bufferIndex]!=0): if(t.pix[toffset+i] != 0): self.spr0HitX = bufferIndex%256 self.spr0HitY = scan return True x+=1 bufferIndex+=1 else: for i in range(8): if(x>=0 and x<256): if(bufferIndex>=0 and bufferIndex<61440 and self.pixrendered[bufferIndex]!=0): if(t.pix[toffset+i] != 0): self.spr0HitX = bufferIndex%256 self.spr0HitY = scan return True x+=1 bufferIndex+=1 return False def writeMem(self, address, value): """ This will write to PPU memory, and update internally buffered data appropriately. """ self.vramMem[address] = value # Update internally buffered data: if(address < 0x2000): self.vramMem[address] = value self.patternWrite(address,value) elif(0x23c0 > address >=0x2000): self.nameTableWrite(self.ntable1[0],address-0x2000,value) elif(0x2400 > address >=0x23c0): self.attribTableWrite(self.ntable1[0],address-0x23c0,value) elif(0x27c0 > address >=0x2400): self.nameTableWrite(self.ntable1[1],address-0x2400,value) elif(0x2800 > address >=0x27c0): self.attribTableWrite(self.ntable1[1],address-0x27c0,value) elif(0x2bc0 > address >=0x2800): self.nameTableWrite(self.ntable1[2],address-0x2800,value) elif(0x2c00 > address >=0x2bc0): self.attribTableWrite(self.ntable1[2],address-0x2bc0,value) elif(0x2fc0 > address >=0x2c00): self.nameTableWrite(self.ntable1[3],address-0x2c00,value) elif(0x3000 > address >=0x2fc0): self.attribTableWrite(self.ntable1[3],address-0x2fc0,value) elif(0x3f20 > address >=0x3f00): self.updatePalettes() def patternWrite(self, address, value): """ Updates the internal pattern table buffers with this new byte. In vNES, there is a version of this with 4 arguments which isn't used. """ tileIndex = address/16 leftOver = address%16; if (leftOver<8) : self.ptTile[tileIndex].setScanline( leftOver, value, self.vramMem[address+8] ) else: self.ptTile[tileIndex].setScanline( leftOver-8, self.vramMem[address-8], value ) def nameTableWrite(self, index, address, value): """ Updates the internal name table buffers with this new byte. """ self.nameTable[index].tile[address] = value # Update Sprite #0 hit: #updateSpr0Hit(); self.checkSprite0(self.scanline-20) def attribTableWrite(self, index, address, value): """ Updates the internal pattern table buffers with self new attribute table byte. """ self.nameTable[index].writeAttrib(address,value) def spriteRamWriteUpdate(self, address, value): """ Updates the internally buffered sprite data with this new byte of info. """ tIndex = address/4 if(tIndex == 0): #updateSpr0Hit() self.checkSprite0(self.scanline-20) if(address%4 == 0): # Y coordinate self.sprY[tIndex] = value elif(address%4 == 1): # Tile index self.sprTile[tIndex] = value elif(address%4 == 2): # Attributes self.vertFlip[tIndex] = ((value&0x80)!=0) self.horiFlip[tIndex] = ((value&0x40)!=0) self.bgPriority[tIndex] = ((value&0x20)!=0) self.sprCol[tIndex] = (value&3)<<2 elif(address%4 == 3): # X coordinate self.sprX[tIndex] = value def doNMI(self): # Set VBlank flag self.setStatusFlag(self.STATUS_VBLANK,True) self.nes.cpu.requestIrq(self.nes.cpu.IRQ_NMI) def emulateCycles(self): while self.cycles > 0: if (self.scanline - 21 == self.spr0HitY and self.curX == self.spr0HitX and self.f_spVisibility == 1): # Set sprite 0 hit flag: self.setStatusFlag(PPU.STATUS_SPRITE0HIT, True) if (self.requestEndFrame): self.nmiCounter-=1 if (self.nmiCounter == 0): self.requestEndFrame = False self.startVBlank() #break self.curX+=1 if (self.curX == 341): self.curX = 0 self.endScanline() self.cycles -= 1