Beispiel #1
0
class Video(iMemory):

    def __init__(self, video_driver, interrupt, memory):
        assert isinstance(video_driver, VideoDriver)
        self.driver                 = video_driver
        self.v_blank_interrupt_flag = interrupt.v_blank
        self.lcd_interrupt_flag     = interrupt.lcd
        self.create_tile_maps()
        self.window                 = Window(self.tile_maps)
        self.background             = Background(self.tile_maps)
        self.status                 = StatusRegister(self)
        self.control                = ControlRegister(self, self.window, 
                                                      self.background)
        self.memory                 = memory
        self.create_tiles()
        self.create_sprites()
        self.reset()
    
    # -----------------------------------------------------------------------
    
    def create_tile_maps(self):
        # create the maximal possible sprites
        self.tile_map_0 = self.create_tile_map()
        self.tile_map_1 = self.create_tile_map()
        self.tile_maps = [self.tile_map_0, self.tile_map_1]
    
    def create_tile_map(self):
        return [self.create_tile_group() for i in range(TILE_MAP_SIZE)]

    def create_tile_group(self):
        return [0x00 for i in range(TILE_GROUP_SIZE)]

    def create_tiles(self):
        tile_data_overlap = self.create_tile_data()
        self.tile_data_0 = self.create_tile_data() + tile_data_overlap
        self.tile_data_1 = tile_data_overlap + self.create_tile_data()
        self.tile_data = [self.tile_data_0, self.tile_data_1]

    def create_tile_data(self):
        return [Tile() for i in range(TILE_DATA_SIZE / 2)]
        
    def update_tile(self, address, data):
        self.get_tile(address).set_data_at(address, data);

    def get_tile_at(self, tile_index):
        if tile_index < TILE_DATA_SIZE:
            return self.tile_data_0[tile_index]
        else:
            return self.tile_data_1[tile_index - TILE_DATA_SIZE / 2]
    
    def get_tile(self, address):
        tile_index = (address - TILE_DATA_ADDR) >> 4
        return self.get_tile_at(tile_index)

    def select_tile_group_for(self, address):
        tile_map_index = address - TILE_MAP_ADDR #) >> 1
        if tile_map_index < TILE_MAP_SIZE * TILE_GROUP_SIZE:
            map = self.tile_map_0
        else:
            map = self.tile_map_1
            tile_map_index -= TILE_MAP_SIZE * TILE_GROUP_SIZE
        tile_group = map[tile_map_index >> 5]
        return tile_group, tile_map_index & 0x1F

    def get_selected_tile_data_space(self):
        return self.tile_data[not self.control.lower_tile_data_selected]

    def get_tile_map(self, address):
        tile_group, group_index = self.select_tile_group_for(address)
        return tile_group[group_index]

    def update_tile_map(self, address, data):
        tile_group, group_index = self.select_tile_group_for(address)
        tile_group[group_index] = data
    
    # -----------------------------------------------------------------------
    def create_sprites(self):
        self.sprites = [None] * MAX_SPRITES
        for i in range(MAX_SPRITES):
            self.sprites[i] = Sprite(self)

    def update_all_sprites(self):
        # TODO: TEST!
        for i in range(MAX_SPRITES):
            address = i * 4
            self.sprites[i].set_data(self.oam[address + 0],
                                     self.oam[address + 1],
                                     self.oam[address + 2],
                                     self.oam[address + 3])
            
    def update_sprite(self, address, data):
        self.get_sprite(address).set_data_at(address, data)
        
    def update_sprite_size(self):
        for sprite in self.sprites:
            sprite.big_size = self.control.big_sprites 

    def get_sprite_at(self, sprite_index):
        return self.sprites[sprite_index]       

    def get_sprite(self, address):
        address -= OAM_ADDR
        # address divided by 4 gives the correct sprite, each sprite has 4
        # bytes of attributes
        return self.get_sprite_at(address / 4)
        
    # -----------------------------------------------------------------------
         
    def reset(self):
        self.control.reset()
        self.status.reset()
        self.background.reset()
        self.window.reset()
        self.cycles     = MODE_2_TICKS
        self.line_y     = 0
        self.line_y_compare = 0
        self.dma        = 0xFF
        # window position
        self.background_palette = 0xFC
        self.object_palette_0   = 0xFF 
        self.object_palette_1   = 0xFF

        self.transfer   = True
        self.display    = True
        self.v_blank    = True
        self.dirty      = True

        # self.vram       = [0] * VRAM_SIZE
        # Object Attribute Memory
        self.oam        = [0] * OAM_SIZE
        
        #XXX remove those dumb helper "shown_sprites"
        self.line       = [0] * (SPRITE_SIZE + GAMEBOY_SCREEN_WIDTH + SPRITE_SIZE)
        self.shown_sprites    = [None] * SPRITES_PER_LINE
        self.palette    = [0] * 1024
        
        self.frames     = 0
        self.frame_skip = 0
    
    # Read Write shared memory -------------------------------------------------
    
    def write(self, address, data):
        address = int(address)
        # assert data >= 0x00 and data <= 0xFF
        if address == LCDC :
            self.set_control(data)
        elif address == STAT:
            self.set_status(data)
        elif address == SCY:
            self.set_scroll_y(data)
        elif address == SCX:
            self.set_scroll_x(data)
        #elif address == LY:
        #    Read Only: line_y
        #    pass
        elif address == LYC:
            self.set_line_y_compare(data)
        elif address == DMA:
            self.set_dma(data)
        elif address == BGP:
            self.set_background_palette(data)
        elif address == OBP0:
            self.set_object_palette_0(data)
        elif address == OBP1:
            self.set_object_palette_1(data)
        elif address == WY:
            self.set_window_y(data)
        elif address == WX:
            self.set_window_x(data)
        elif OAM_ADDR <= address < \
        OAM_ADDR + OAM_SIZE:
            self.set_oam(address, data)
        elif VRAM_ADDR <= address < \
        VRAM_ADDR + VRAM_SIZE:
            self.set_vram(address, data)
            
    def read(self, address):
        if address == LCDC:
            return self.get_control()
        elif address == STAT:
            return self.get_status()
        elif address == SCY:
            return self.get_scroll_y()
        elif address == SCX:
            return self.get_scroll_x()
        elif address == LY:
            return self.get_line_y()
        elif address == LYC:
            return self.get_line_y_compare()
        elif address == DMA:
            return self.get_dma()
        elif address == BGP:
            return self.get_background_palette()
        elif address == OBP0:
            return self.get_object_palette_0()
        elif address == OBP1:
            return self.get_object_palette_1()
        elif address == WY:
            return self.get_window_y()
        elif address == WX:
            return self.get_window_x()
        elif OAM_ADDR <= address < \
        OAM_ADDR + OAM_SIZE:
            return self.get_oam(address)
        elif VRAM_ADDR <= address < \
        VRAM_ADDR + VRAM_SIZE:
            return self.get_vram(address)
        return 0xFF

    # Getters and Setters ------------------------------------------------------
    
    def get_frame_skip(self):
        return self.frame_skip

    def set_frame_skip(self, frame_skip):
        self.frame_skip = frame_skip
        
    def get_cycles(self):
        return self.cycles

    def get_control(self):
        return self.control.read()

    def set_control(self, data):
        self.control.write(data)

    def get_status(self):
        return self.status.read(extend=True)

    def set_status(self, data):
        self.status.write(data)
        self.set_status_bug()
        
    def set_status_bug(self) :
        # Gameboy Bug
        if self.control.lcd_enabled and \
           self.status.get_mode() == 1 and \
           self.status.line_y_compare_check():
                self.lcd_interrupt_flag.set_pending()
        
    def get_scroll_x(self):
        """ see set_scroll_x """
        return self.background.scroll_x

    def set_scroll_x(self, data):
        """
        Specifies the position in the 256x256 pixels BG map (32x32 tiles) which 
        is to be displayed at the upper/left LCD display position.
        Values in range from 0-255 may be used for X/Y each, the video 
        controller automatically wraps back to the upper (left) position in BG
        map when drawing exceeds the lower (right) border of the BG map area.
        """
        self.background.scroll_x = data
        
    def get_scroll_y(self):
        """ see set_scroll_x """
        return self.background.scroll_y
                
    def set_scroll_y(self, data):
        """ see set_scroll_x """
        self.background.scroll_y = data
        
    def get_line_y(self):
        """ see set_line_y """
        return self.line_y
    
    def set_line_y(self):
        """
        The LY indicates the vertical line to which the present data is 
        transferred to the LCD Driver. The LY can take on any value between 0 
        through 153. The values between 144 and 153 indicate the V-Blank period.
        Writing will reset the counter.
        """
        pass

    def get_line_y_compare(self):
        """ see set_line_y_compare"""
        return self.line_y_compare

    def set_line_y_compare(self, data):
        """
        The gameboy permanently compares the value of the LYC and LY registers.
        When both values are identical, the coincident bit in the STAT register
        becomes set, and (if enabled) a STAT interrupt is requested.
        """
        self.line_y_compare = data
        if self.control.lcd_enabled:
            self.status.mode0.emulate_hblank_line_y_compare(stat_check=True)
                
    def get_dma(self):
        return self.dma

    def set_dma(self, data):
        """
        Writing to this register launches a DMA transfer from ROM or RAM to OAM
        memory (sprite attribute table). The written value specifies the
        transfer source address divided by 100h, ie. source & destination are:
            Source:      XX00-XX9F   ;XX in range from 00-F1h
            Destination: FE00-FE9F
        It takes 160 microseconds until the transfer has completed, during this
        time the CPU can access only HRAM (memory at FF80-FFFE). For this
        reason, the programmer must copy a short procedure into HRAM, and use
        this procedure to start the transfer from inside HRAM, and wait until
        the transfer has finished:
            ld  (0FF46h),a ;start DMA transfer, a=start address/100h
            ld  a,28h      ;delay...
            wait:          ;total 5x40 cycles, approx 200ms
            dec a          ;1 cycle
            jr  nz,wait    ;4 cycles
        Most programs are executing this procedure from inside of their VBlank
        procedure, but it is possible to execute it during display redraw also,
        allowing to display more than 40 sprites on the screen (ie. for example 
        40 sprites in upper half, and other 40 sprites in lower half of the
        screen).
        """
        self.dma = data
        # copy the memory region
        for index in range(OAM_SIZE):
            self.oam[index] = self.memory.read((self.dma << 8) + index)
        self.update_all_sprites()

    def get_background_palette(self):
        """ see set_background_palette"""
        return self.background_palette

    def set_background_palette(self, data):
        """
        This register assigns gray shades to the color numbers of the BG and 
        Window tiles.
          Bit 7-6 - Shade for Color Number 3
          Bit 5-4 - Shade for Color Number 2
          Bit 3-2 - Shade for Color Number 1
          Bit 1-0 - Shade for Color Number 0
        The four possible gray shades are:
          0  White
          1  Light gray
          2  Dark gray
          3  Black
        """
        if self.background_palette != data:
            self.background_palette = data
            self.dirty              = True

    def get_object_palette_0(self):
        return self.object_palette_0

    def set_object_palette_0(self, data):
        """
        This register assigns gray shades for sprite palette 0. It works exactly
        as BGP (FF47), except that the lower two bits aren't used because sprite
        data 00 is transparent.
        """
        if self.object_palette_0 != data:
            self.object_palette_0 = data
            self.dirty            = True

    def get_object_palette_1(self):
        return self.object_palette_1

    def set_object_palette_1(self, data):
        """
        This register assigns gray shades for sprite palette 1. It works exactly
        as BGP (FF47), except that the lower two bits aren't used because sprite
        data 00 is transparent.
        """
        if self.object_palette_1 != data:
            self.object_palette_1 = data
            self.dirty            = True

    def get_window_y(self):
        """ see set_window.y """
        return self.window.y

    def set_window_y(self, data):
        """
        Specifies the upper/left positions of the Window area. (The window is an
        alternate background area which can be displayed above of the normal
        background. OBJs (sprites) may be still displayed above or behind the 
        window, just as for normal BG.)
        The window becomes visible (if enabled) when positions are set in range
        WX=0..166, WY=0..143. A postion of WX=7, WY=0 locates the window at
        upper left, it is then completely covering normal background.
        """
        self.window.y = data
        
    def get_window_x(self):
        return self.window.x

    def set_window_x(self, data):
        self.window.x = data

    def set_oam(self, address, data):
        """
        sets one byte of the object attribute memory.
        The object attribute memory stores the position and seme other
        attributes of the sprites, this works only during the v-blank and
        the h-blank period.
        """
        self.oam[address - OAM_ADDR] = data & 0xFF
        self.update_sprite(address, data)
        
    def get_oam(self, address):
        return self.get_sprite(address).get_data_at(address);
        
    def set_vram(self, address, data):
        """
        sets one byte of the video memory.
        The video memory contains the tiles used to display.
        """
        if address < TILE_MAP_ADDR:
            self.update_tile(address, data)
        else:
            self.update_tile_map(address, data)
    
    def get_vram(self, address):
        if address < TILE_MAP_ADDR:
            return self.get_tile(address).get_data_at(address)
        else:
            return self.get_tile_map(address)
    
    # emulation ----------------------------------------------------------------

    def emulate(self, ticks):
        if self.control.lcd_enabled:
            self.cycles -= int(ticks)
            while self.cycles <= 0:
                self.current_mode().emulate()

    def current_mode(self):
        return self.status.current_mode

    # graphics handling --------------------------------------------------------
    
    def draw_frame(self):
        self.driver.update_gb_display()

    def clear_frame(self):
        self.driver.clear_gb_pixels()
        self.driver.update_gb_display()

    def tile_index_flip(self):
        if self.control.lower_tile_data_selected:
            return 0
        else:
            return 1 << 7 # First and last 128 tiles are swapped.

    def draw_window(self, window, line_y, line):
        if window.enabled:
            tile_data = self.get_selected_tile_data_space()
            tile_index_flip = self.tile_index_flip()
            window.draw_line(line_y, tile_data, tile_index_flip, line) 
        else:
            window.draw_clean_line(self.line)

    def draw_line(self):
        # XXX We should check if this is necessary for each line.
        self.update_palette()
        self.draw_window(self.background, self.line_y, self.line)
        self.draw_window(self.window, self.line_y, self.line)
        self.draw_sprites(self.line_y, self.line)
        self.send_pixels_line_to_driver()
    
    def draw_sprites(self, line_y, line):
        if not self.control.sprites_enabled: return
        count = self.scan_sprites(line_y)
        lastx = SPRITE_SIZE + GAMEBOY_SCREEN_WIDTH + SPRITE_SIZE
        for index in range(count):
            sprite = self.shown_sprites[index]
            sprite.draw(line, line_y, lastx)
            lastx = sprite.x
            
    def scan_sprites(self, line_y):
        # search active shown_sprites
        count = 0
        for sprite in self.sprites:
            if sprite.is_shown_on_line(line_y):
                self.shown_sprites[count] = sprite
                count += 1
                if count >= SPRITES_PER_LINE:
                    break
        self.sort_scan_sprite(count)
        return count

    def sort_scan_sprite(self, count):
        # TODO: optimize :)
        # sort shown_sprites from high to low priority using the real tile_address
        for index in range(count):
            highest = index
            for right in range(index+1, count):
                if self.shown_sprites[right].x > self.shown_sprites[highest].x:
                    highest = right
            self.shown_sprites[index], self.shown_sprites[highest] = \
                    self.shown_sprites[highest], self.shown_sprites[index]

    def send_pixels_line_to_driver(self):
        for x in range(0, GAMEBOY_SCREEN_WIDTH):
            color = self.palette[self.line[SPRITE_SIZE + x]]
            self.driver.draw_gb_pixel(x, self.line_y, color)

    def update_palette(self):
        if not self.dirty: return
        # bit 4/0 = BG color, 
        # bit 5/1 = OBJ color, 
        # bit 2   = OBJ palette, 
        # bit 3   = OBJ priority
        for pattern in range(0, 64):
            #color
            if (pattern & 0x22) == 0 or \
               ((pattern & 0x08) != 0 and (pattern & 0x11) != 0):
                # OBJ behind BG color 1-3
                color = (self.background_palette >> ((((pattern >> 3) & 0x02) +\
                        (pattern & 0x01)) << 1)) & 0x03
             # OBJ above BG
            elif ((pattern & 0x04) == 0):
                color = (self.object_palette_0 >> ((((pattern >> 4) & 0x02) + \
                        ((pattern >> 1) & 0x01)) << 1)) & 0x03
            else:
                color = (self.object_palette_1 >> ((((pattern >> 4) & 0x02) +\
                        ((pattern >> 1) & 0x01)) << 1)) & 0x03
            index = ((pattern & 0x30) << 4) + (pattern & 0x0F)
            #self.palette[index] = COLOR_MAP[color]
            self.palette[index] = color
        self.dirty = False