Esempio n. 1
0
class PPU(object):

    def __init__(self, cpu, mirroring, ppu_debug = False):
        self.cpu = cpu
        self.mirroring = mirroring
        self.ppu_debug = ppu_debug or FORCE_PPU_DEBUG

        self.cache = ppucache.PPUCache(self)

        self.scanline = 261
        self.cycle = 0
        self.frame = 0

        # values in the latch decay over time in the actual NES, but I
        # don't think that's worth emulating
        self.latch = 0x0

        self.oam = ['\x00' for i in range(OAM_SIZE)]
        self.paletteRam = ['\x00' for i in range(PALETTE_SIZE)]

        ## PPUCTRL flags

        # base nametable address: 0 = $2000; 1 = $2400; 2 = $2800; 3 = $2C00
        self.nametableBase = 0
        # VRAM address increment per CPU read/write of PPUDATA.
        # 0: add 1, going across; 1: add 32, going down
        self.vramInc = 0
        # Sprite pattern table address for 8x8 sprites.
        # 0: $0000; 1: $1000; ignored in 8x16 mode
        self.spritePatternTableAddr = 0
        # Background pattern table address. 0: $0000; 1: $1000
        self.bgPatternTableAddr = 0
        # Sprite size. 0: 8x8; 1: 8x16.
        self.spriteSize = 0
        # PPU master/slave select. Setting to 1 can damage an
        # unmodified NES, so we'll just throw an error if someone
        # tries to set it.
        self.ppuMasterSlave = 0
        # Whether to generate an NMI at the start of the vertical
        # blanking interval.
        self.vblankNMI = 0

        ## PPUMASK flags
        self.maskState = 0

        ## PPUSTATUS flags
        self.spriteOverflow = 0
        self.sprite0Hit = 0
        self.vblank = 0

        ## OAMADDR
        self.oamaddr = 0x00

        ## PPUSCROLL
        self.fineScrollX = 0
        self.fineScrollY = 0
        self.nextScroll = 0 # 0 for X, 1 for Y

        ## PPUADDR
        self.ppuaddr = 0
        self.addrHigh = 0
        self.addrLow = 0
        self.nextAddr = 0 # 0 for high, 1 for low

        ## PPUDATA
        self.ppuDataBuffer = 0

        ## Sprite-relevant state
        self.spritesThisScanline = [None for i in range(MAX_SPRITES)]
        self.nSpritesThisScanline = 0

        ## Background tile caches
        self.bglowbyte = 0
        self.bghighbyte = 0
        self.bgpalette = [0,0,0] # global palette indexes for numbers 1, 2, and 3
        self.universalBg = 0 # global palette index for number 0

        # Stored value of y scroll offset, only normally updated
        # between frames
        self.tempScrollY = 0

        self.sleepUntil(VBLANK_START, self.vblankStart)

        from screen import Screen # herp derp circular import
        self.pgscreen = Screen(self)

    def readReg(self, register):
        # Set the latch, then return it. Write-only registers just set
        # the latch, but for now they also print an error message.
        if register == REG_PPUCTRL:
            if self.ppu_debug:
                print >> sys.stderr, 'Warning: read from PPUCTRL'
        elif register == REG_PPUMASK:
            if self.ppu_debug:
                print >> sys.stderr, 'Warning: read from PPUMASK'
        elif register == REG_PPUSTATUS:
            # keep first five bits of latch
            self.latch &= 0x1f
            if self.spriteOverflow:
                self.latch |= 0x20
            if self.sprite0Hit:
                self.latch |= 0x40
            if self.vblank:
                self.latch |= 0x80
            # TODO there's some weirdness with reading this register
            # within like a cycle of when vblank begins, but I don't
            # think I care enough to emulate that
            self.vblank = False
            # clear address latch (pretending they're two different things)
            self.nextScroll = 0
            self.nextAddr = 0
        elif register == REG_OAMADDR:
            if self.ppu_debug:
                print >> sys.stderr, 'Warning: read from OAMADDR'
        elif register == REG_OAMDATA:
            # TODO: if (oamaddr % 4) == 3, report that bits 2-4 are 0
            # see http://wiki.nesdev.com/w/index.php/PPU_OAM
            self.latch = ord(self.oam[self.oamaddr])
        elif register == REG_PPUSCROLL:
            if self.ppu_debug:
                print >> sys.stderr, 'Warning: read from PPUSCROLL'
        elif register == REG_PPUADDR:
            if self.ppu_debug:
                print >> sys.stderr, 'Warning: read from PPUADDR'
        elif register == REG_PPUDATA:
            # do not question the PPUDATA post-fetch read buffer
            if self.ppuaddr < 0x3f00:
                self.latch = self.ppuDataBuffer
                self.ppuDataBuffer = ord(self.cpu.mem.ppuRead(self.ppuaddr))
            else:
                self.ppuDataBuffer = ord(self.cpu.mem.ppuRead(self.ppuaddr))
                self.latch = self.ppuDataBuffer
            self.advanceVram()
        else:
            raise RuntimeError("PPU read from bad register %x" % register)
        return chr(self.latch)

    def writeReg(self, register, val):
        if isinstance(val, str): # someday we will want to get rid of this chr/ord weirdness
            val = ord(val)
        self.latch = val
        if register == REG_PPUCTRL:
            # TODO: writing to PPUCTRL during rendering will change
            # the high scroll bits in /t/. This will change the high
            # bit for horizontal scroll on the next scanline, but the
            # high bit for vertical scroll will not be affected until
            # the next frame (unless we write to REG_PPUADDR).
            oldNametableBase = self.nametableBase
            oldBgPatternTableAddr = self.bgPatternTableAddr

            self.nametableBase = val & 0x3 # bits 0,1
            self.vramInc = (val >> 2) & 0x1 # bit 2
            self.spritePatternTableAddr = (val >> 3) & 0x1 # bit 3
            self.bgPatternTableAddr = (val >> 4) & 0x1 # bit 4
            self.spriteSize = (val >> 5) & 0x1 # bit 5
            self.ppuMasterSlave = (val >> 6) & 0x1 # bit 6
            self.vblankNMI = (val >> 7) & 0x1 # bit 7
            # TODO if we just set vblankNMI and we're in vblank, this
            # might be supposed to trigger the NMI

            if (self.nametableBase != oldNametableBase or
                self.bgPatternTableAddr != oldBgPatternTableAddr):
                self.flushBgCache()

            if self.nametableBase != oldNametableBase:
                self.maintainScroll()

            if self.ppu_debug:
                xcycle, ycycle = self.cycleToCoords(self.fineCycle())
                print "PPUCTRL (cycle %d: %d, %d): nametableBase = %d" % (
                    self.fineCycle(),
                    xcycle, ycycle,
                    self.nametableBase)

            if self.ppuMasterSlave:
                raise RuntimeError("We set the PPU master/slave bit! That's bad!")
        elif register == REG_PPUMASK:
            self.maskState = val
            grayscale = val & 0x1 # bit 0
            leftBkg = (val >> 1) & 0x1 # bit 1
            leftSprites = (val >> 2) & 0x1 # bit 2
            showBkg = (val >> 3) & 0x1 # bit 3
            showSprites = (val >> 4) & 0x1 # bit 4
            emphasizeRed = (val >> 5) & 0x1 # bit 5
            emphasizeGreen = (val >> 6) & 0x1 # bit 6
            emphasizeBlue = (val >> 7) & 0x1 # bit 7
            if self.ppu_debug and grayscale:
                print >> sys.stderr, "PPUMASK write: ignoring grayscale"
            if self.ppu_debug and (leftBkg or leftSprites):
                print >> sys.stderr, "PPUMASK write: ignoring left-column hiding"
            if self.ppu_debug and (emphasizeRed or emphasizeGreen or emphasizeBlue):
                print >> sys.stderr, "PPUMASK write: ignoring color emphasis"
        elif register == REG_PPUSTATUS:
            if self.ppu_debug:
                print >> sys.stderr, 'Warning: write to PPUSTATUS'
        elif register == REG_OAMADDR:
            self.oamaddr = val
        elif register == REG_OAMDATA:
            self.oam[self.oamaddr] = chr(val)
            self.oamaddr = (self.oamaddr + 1) % OAM_SIZE
        elif register == REG_PPUSCROLL:
            # TODO: During rendering, the first write to PPUSCROLL
            # will update the coarse x scroll in /t/, to be loaded
            # into /v/ for the next scanline. It will also change the
            # fine x scroll (horizontal position within a tile)
            # immediately.

            if self.nextScroll == 0:
                self.fineScrollX = val
                self.nextScroll = 1
                if self.ppu_debug:
                    xcycle, ycycle = self.cycleToCoords(self.fineCycle())
                    print "PPUSCROLL (cycle %d: %d, %d): x = %d" % (
                        self.fineCycle(), xcycle, ycycle, val)
            else:
                self.fineScrollY = val
                self.nextScroll = 0
                if self.ppu_debug:
                    xcycle, ycycle = self.cycleToCoords(self.fineCycle())
                    print "PPUSCROLL (cycle %d: %d, %d): y = %d" % (
                        self.fineCycle(), xcycle, ycycle, val)
                self.maintainScroll()

        elif register == REG_PPUADDR:
            # TODO: writing to PPUADDR during rendering will cause
            # strange effects, because the address register is also
            # used for the scroll position. The first write sets the
            # top 2 bits of coarse y scroll, the two nametable bits,
            # and all 3 bits of fine y scroll in /t/; however, the top
            # bit of fine y scroll is always set to 0. The second
            # write sets coarse x scroll and the bottom 3 bits of
            # coarse y scroll in /t/; then it immediately sets /v/
            # equal to /t/. This can change both horizontal and
            # vertical scrolling anywhere; it is the only way to
            # change horiz. scrolling during a scanline or vert.
            # scrolling during a frame.
            if self.nextAddr == 0:
                # addresses past $3fff are mirrored down
                self.addrHigh = val & 0x3f
                self.nextAddr = 1
            else:
                self.addrLow = val
                self.nextAddr = 0
                self.ppuaddr = self.addrLow + self.addrHigh * 0x100

                # A hack to represent PPUADDR's effect on the
                # nametable bits of the scroll coordinates. To
                # properly represent this, see big comment above.
                self.nametableBase = (self.addrHigh & 12) >> 2
                self.maintainScroll()

        elif register == REG_PPUDATA:
            self.cpu.mem.ppuWrite(self.ppuaddr, val)
            self.advanceVram()
        else:
            raise RuntimeError("PPU write to bad register %x" % register)

    def advanceVram(self):
        # TODO: advancing vram (on reads or writes to REG_PPUDATA)
        # during rendering does bizarre things to the scroll values.
        # Specifically, it increments the coarse component of X and
        # the fine component of Y (overflowing to coarse if
        # appropriate).
        if self.vramInc == 0:
            self.ppuaddr += 1
        else:
            self.ppuaddr += 32
        self.ppuaddr &= 0xffff

    def readPtab(self, base, finey, tile):
        """Read an entry from a pattern table.
        Arguments:
        - base: 0 or 1 corresponding to the pattern table base
        - finey: Fine y offset. y position within the tile (0-7).
        - tile: One byte determining the tile within the table."""
        # finding pattern table entry:
        #
        # bits 0 through 2 are the "fine y offset", the y position
        # within a tile (y position % 8, I guess)
        #
        # bit 3 is the bitplane: we'll need to read once with it
        # set and once with it unset to get two bits of color
        #
        # bits 4 through 7 are the column of the tile (column / 8)
        #
        # bits 8 through b are the tile row (row / 8)
        #
        # bit c is the same as self.bgPatternTableAddr (or spritePatternTableAddr)
        #
        # bits d through f are 0 (pattern tables go from 0000 to 1fff)

        lowplane = (
            (finey) + # 0-2: fine y offset
            (0 << 3) + # 3: dataplane
            (tile << 4) + # 4-11: column and row
            (base << 12)) # 12: pattern table base
        # bits d through f are 0 (pattern tables go from 0000 to 1fff)
        assert ((lowplane & 8) == 0)
        highplane = lowplane | 8 # set bit 3 for high dataplane

        lowbyte = ord(self.cpu.mem.ppuRead(lowplane))
        highbyte = ord(self.cpu.mem.ppuRead(highplane))

        return (lowbyte,highbyte)

    def tileRows(self):
        if self.mirroring == MirrorMode.horizontalMirroring:
            # horizontal mirroring means vertical scrolling
            return (VISIBLE_SCANLINES / 8) * 2
        elif self.mirroring == MirrorMode.verticalMirroring:
            return VISIBLE_SCANLINES / 8
        else:
            raise NotImplementedError("Unimplemented mirroring mode")

    def tileColumns(self):
        if self.mirroring == MirrorMode.horizontalMirroring:
            # horizontal mirroring means vertical scrolling
            return VISIBLE_COLUMNS / 8
        elif self.mirroring == MirrorMode.verticalMirroring:
            return (VISIBLE_COLUMNS / 8) * 2
        else:
            raise NotImplementedError("Unimplemented mirroring mode")

    def scrollX(self):
        coarseScrollX = (self.nametableBase % 2) * 256
        return coarseScrollX + self.fineScrollX

    def scrollY(self):
        coarseScrollY = (self.nametableBase // 2) * 240
        return coarseScrollY + self.fineScrollY

    def updateBgTiles(self):
        for tilecolumn in xrange(self.tileColumns()):
            for tilerow in xrange(self.tileRows()):
                ## BACKGROUND

                # Grab data from nametable to find pattern table
                # entry. There are 30*32 bytes in a pattern table, and
                # each byte corresponds to an 8*8-pixel tile.

                # We can pull 8 horizontally-continguous pixels at
                # once: we have a low-plane byte with the low-plane
                # bits for 8 pixels, and a high-plane byte with the
                # high-plane bits for the same 8 pixels. Finally, we
                # need to grab the background palette from the
                # attribute table.

                # This function copies over both background
                # nametables, ignoring the scroll data (both the low
                # bits from PPUSCROLL and the high bits from PPUCTRL).

                # TODO: remove assumption that there are exactly two
                # background nametables

                # Ignore the nametableBase setting from PPUCTRL here:
                # it'll become the high bits for scrolling.
                nametableBase = 0
                if (tilecolumn >= (VISIBLE_COLUMNS / 8)):
                    nametableBase += 1
                if (tilerow >= (VISIBLE_SCANLINES / 8)):
                    nametableBase += 2

                nametable = 0x2000 + 0x400 * nametableBase # TODO don't use magic numbers
                wrappedColumn = tilecolumn % (VISIBLE_COLUMNS / 8)
                wrappedRow = tilerow % (VISIBLE_SCANLINES / 8)
                nametableEntry = nametable + wrappedColumn + wrappedRow * 32
                ptabTile = ord(self.cpu.mem.ppuRead(nametableEntry))

                # TODO don't use magic numbers
                attributeRow = wrappedRow // 4
                attributeColumn = wrappedColumn // 4
                attributeTable = nametable + 0x3C0
                attributeTableEntry = attributeTable + attributeColumn + attributeRow * 8
                attributeTile = ord(self.cpu.mem.ppuRead(attributeTableEntry))

                # The attributeTile byte divides the 32x32 tile into
                # four 16x16 quarter-tiles. Bits 0-1 specify the
                # palette for the top-left quarter-tile, bits 2-3 are
                # the top-right, bits 4-5 are the bottom-left, and
                # bits 6-7 are the bottom-right.
                paletteOffset = 0
                if (wrappedColumn % 4) >= 2:
                    paletteOffset += 1*2
                if (wrappedRow % 4) >= 2:
                    paletteOffset += 2*2

                paletteNumber = (attributeTile >> paletteOffset) & 0x3

                self.pgscreen.tileIndices[tilecolumn][tilerow] = ptabTile
                self.pgscreen.paletteIndices[tilecolumn][tilerow] = paletteNumber

    def vblankStart(self):
        if self.ppu_debug:
            print "Starting vblank"
        self.draw()
        self.vblank = 1
        if self.vblankNMI:
            # signal NMI
            self.cpu.nmiPending = True
        self.sleepUntil(VBLANK_END, self.vblankEnd)

    def vblankEnd(self):
        if self.ppu_debug:
            print "Ending vblank"
        self.vblank = 0
        self.updateBgTiles()
        # It's possible that we're supposed to reset sprite 0 one
        # frame earlier, but I don't want to look up the details right
        # now
        self.sprite0Hit = 0
        self.tempScrollY = self.scrollY()
        if self.ppu_debug:
            print "Initializing frame with scroll offset (%d, %d)" % (self.scrollX(), self.scrollY())
        self.pgscreen.initFrame()
        self.sleepUntil(FRAME_END, self.frameEnd)

    def frameEnd(self):
        self.frame += 1
        self.cycle = -1
        if self.ppu_debug:
            print "BEGIN PPU FRAME %d" % self.frame
        sprite0hit = self.findSprite0Hit()
        if sprite0hit < 0:
            self.sleepUntil(VBLANK_START, self.vblankStart)
        else:
            self.sleepUntil(sprite0hit, self.flagSprite0Hit)


    def draw(self):
        # TODO: if the VRAM address points to something in
        # $3f00-$3fff, set universalBg to that instead of
        # $3f00 (though for exact behavior, this should
        # actually be checked repeatedly during the frame - I
        # don't know exactly how often). This is the
        # "background hack".
        self.universalBg = ord(self.cpu.mem.ppuRead(0x3F00)) # TODO no magic numbers

        # TODO check the frame count for off-by-one errors
        self.pgscreen.tick(self.frame)

    def flagSprite0Hit(self):
        if self.ppu_debug:
            print "Setting sprite 0 hit flag"
        self.sprite0Hit = 1
        self.sleepUntil(VBLANK_START, self.vblankStart)

    def ppuTick(self, ticks):
        # streamline calls to this as follows:
        #
        # - only do anything on certain ticks (if we can track where
        #   sprites are, we can avoid redrawing most of the screen)
        # - keep track of when we next have to do things
        # - don't call ppuTick until then (but act gracefully if that does happen)

        if self.cycle + ticks >= self.nextActionCycle:
            nextTicks = self.cycle + ticks - self.nextActionCycle
            # set this now, because calling the next action will change nextActionCycle
            self.cycle = self.nextActionCycle % CYCLES_PER_FRAME
            self.nextActionF()
            assert (self.cycle < self.nextActionCycle)
            if nextTicks > 0:
                self.ppuTick(nextTicks)
        else:
            self.cycle = (self.cycle + ticks) % CYCLES_PER_FRAME

        # TODO skip cycle 340 on scanline 239 on odd
        # frames... hahahaha no I don't care

    def sleepUntil(self, cycle, f):
        self.nextActionCycle = cycle
        self.nextActionF = f
        self.cpu.ppuCyclesUntilAction = cycle - self.cycle
        assert (self.cpu.ppuCyclesUntilAction >= 0)

    def fineCycle(self):
        # Return the current cycle number, taking into account the
        # number of cycles tracked by the CPU.
        return self.cycle + self.cpu.ppuStoredCycles

    def cycleToCoords(self, cycle):
        return (cycle % CYCLES_PER_SCANLINE, cycle // CYCLES_PER_SCANLINE)

    def flushBgCache(self):
        # Clear our background tile cache: we'll redraw the whole
        # background next frame. Currently this does nothing.
        pass

    def flushBgTile(self, tileX, tileY):
        # Currently this does nothing.

        # Clear our background tile cache for a single tile. This
        # should be called when we write to the PPU nametable address
        # corresponding to our active nametable. For now I'm going to
        # have the memory management code deal with that - that's
        # going to involve some weird interplay between the two
        # modules, so I might want to reorganize that eventually.

        # first make sure we're not actually writing to the attribute table
        if tileY < VISIBLE_SCANLINES / 8:
            pass
        else:
            # if we are, just flush everything to be safe
            self.flushBgCache()
            pass

    def dumpPtab(self, base):
        """Returns a string of bytes representing the specified half of the pattern table. The bytes are stored in a large atlas texture of dimension 8*256 by 8."""
        # this might be a bit slow for now, but it shouldn't be called
        # much, at least for early games. If I want to figure out the
        # details, it should be possible to directly dump the memory
        # into the string, maybe given some sort of transformation.
        out = bytearray(256*8*8)
        for tile in xrange(256):
            for y in xrange(8):
                (lowbyte, highbyte) = self.readPtab(base, y, tile)
                for x in xrange(8):
                    lowbit = (lowbyte >> (7-x)) & 1
                    highbit = (highbyte >> (7-x)) & 1
                    pixel = lowbit + 2 * highbit
                    out[x + 8*256*y + 8*tile] = pixel
        return str(out)

    def dumpLocalPalettes(self, base):
        """Returns a list of floats representing the local palette starting at the base."""
        out = [0.0 for i in range(16*4)]
        for i in range(16):
            if (i % 4) == 0:
                continue
            paletteIndex = ord(self.cpu.mem.ppuRead(base + i))
            out[4*i:(4*i)+3] = [float(x)/255.0 for x in palette.palette(paletteIndex)] # rgb
            out[(4*i)+3] = 1.0 # alpha
        return out


    # Find the cycle where a sprite 0 hit occurs this frame. If there
    # will not be a sprite 0 hit, return -1.
    def findSprite0Hit(self):
        if FORCE_SPRITE0_CYCLE is not None:
            return FORCE_SPRITE0_CYCLE

        # Sprite 0 hits can only happen if both background and sprites
        # are being rendered.
        if not (self.maskState & ((1 << 3) | (1 << 4))):
            if self.ppu_debug:
                print "No sprite 0 hit"
            return -1

        spritetop = ord(self.oam[0]) + 1
        # TODO account for 8x16 sprites

        if spritetop >= 0xf0:
            # Sprite 0 is wholly off the screen; no sprite 0 hit
            if self.ppu_debug:
                print "No sprite 0 hit"
            return -1

        tileIndex = ord(self.oam[1])
        attributes = ord(self.oam[2])
        spriteX = ord(self.oam[3])
        horizontalMirror = bool(attributes & 0x40)
        verticalMirror = bool(attributes & 0x80)
        # Note: palette is irrelevant for sprite 0 hits

        # fetch opacity patterns for sprite 0 and relevant bkg tiles

        # Lazily load the sprite opacity: we need to read two bytes per row
        spriteOpacity = [None for _ in xrange(8)]

        def spriteOpaque(xoffset, yoffset):
            # Load sprite opacity if necessary
            if spriteOpacity[yoffset] is None:
                lowbyte, highbyte = self.readPtab(self.spritePatternTableAddr,
                                                  yoffset, tileIndex)
                # We have two bits per pixel to represent color, but
                # the pixel is opaque if either is set. We don't care
                # which one here.
                spriteOpacity[yoffset] = lowbyte | highbyte
            # Now spriteOpacity[yoffset] is a byte whose bits
            # represent pixel opacity: the most-significant bit
            # represents the leftmost pixel in the row.
            return bool(spriteOpacity[yoffset] & (0x80 >> xoffset))

        # Same for background opacity, except that we potentially have
        # four background tiles to read. This list is indexed first by
        # horizontal tile (0 or 1) and then by vertical row.
        bkgOpacity = [[None for _ in xrange(8*2)] for _ in xrange(2)]
        bkgTopTile = spritetop // 8
        bkgLeftTile = spriteX // 8
        bkgTopPixel = bkgTopTile * 8
        bkgLeftPixel = bkgLeftTile * 8

        def bkgOpaque(x, y):
            xoffset = x - bkgLeftPixel
            yoffset = y - bkgTopPixel
            if xoffset < 8:
                tileColumnOffset = 0
            else:
                tileColumnOffset = 1

            if bkgOpacity[tileColumnOffset][yoffset] is None:
                tileColumn = bkgLeftTile + tileColumnOffset
                if yoffset < 8:
                    tileRow = bkgTopTile
                else:
                    tileRow = bkgTopTile + 1

                nametable = 0x2000 + 0x400 * self.nametableBase # TODO don't use magic numbers
                nametableEntry = nametable + tileColumn + tileRow * 32
                # We could cache this read, but it probably doesn't matter much.
                bkgTile = ord(self.cpu.mem.ppuRead(nametableEntry))

                lowbyte, highbyte = self.readPtab(self.bgPatternTableAddr,
                                                  yoffset % 8, bkgTile)
                bkgOpacity[tileColumnOffset][yoffset] = lowbyte | highbyte
            return bool(bkgOpacity[tileColumnOffset][yoffset]
                        & (0x80 >> (xoffset % 8)))

        # TODO account for 8x16 sprites
        for xoffset in xrange(8):
            x = xoffset + spriteX
            for yoffset in xrange(8):
                y = yoffset + spritetop
                if (spriteOpaque(xoffset,yoffset) and
                    bkgOpaque(x,y)):
                    out = (x
                           + (y * CYCLES_PER_SCANLINE)
                           + SPRITE0_CYCLE_OFFSET)
                    if self.ppu_debug:
                        print ("Sprite 0 hit at (%d,%d): PPU cycle %d" %
                               (x, y, out))
                    return out

        # Didn't find a hit.
        if self.ppu_debug:
            print "No sprite 0 hit"
        return -1

    def maintainScroll(self):
        """Maintains scroll coordinates during rendering by pushing a scroll
region to the screen module. Does nothing outside of rendering. Can
safely be called multiple times during the same render frame.

        """

        # Handle scroll updates during rendering. This is not
        # completely accurate, but it'll work for simple cases. (For
        # one thing, we should write on a write to the x scroll, not
        # the y scroll. For another, writes to the x scroll should
        # modify the low 3 bits immediately but the upper bits at the
        # end of the line. (But in practice, that last one isn't easy
        # to control for game-writers.))

        (xStart, yTop) = self.cycleToCoords(self.fineCycle())
        if xStart > 0:
            yTop += 1
            xStart = 0
        # Check yTop instead of vblank, because we go off the
        # screen a bit before the vblank flag is actually set.
        if yTop < VISIBLE_SCANLINES:
            self.pgscreen.recordScroll(self.scrollX(), self.tempScrollY,
                                       xStart, yTop)




    def printTile(self, base, tile):

        """Print a representation of a tile to stdout. For debug purposes."""
        import sys
        for finey in xrange(8):
            (lowbyte, highbyte) = self.readPtab(base, finey, tile)
            for finex in xrange(8):
                pattern = 0
                if (lowbyte & (1 << (7-finex))):
                    pattern += 1
                if (highbyte & (1 << (7-finex))):
                    pattern += 2
                if pattern:
                    sys.stdout.write(str(pattern))
                else:
                    sys.stdout.write(".")
            sys.stdout.write("\n")