/****************************************************************************** Nintendo 2C0x PPU emulation. Written by Ernesto Corvi. This code is heavily based on Brad Oliver's MESS implementation. ******************************************************************************/ /****************************************************************************** Current known bugs General: * PPU timing is imprecise for updates that happen mid-scanline. Some games may demand more precision. NES-specific: * Micro Machines has minor rendering glitches (needs better timing). * Mach Rider has minor road rendering glitches (needs better timing). ******************************************************************************/ #include #include "driver.h" #include "profiler.h" #include "video/ppu2c0x.h" /* constant definitions */ #define VISIBLE_SCREEN_WIDTH (32*8) /* Visible screen width */ #define VISIBLE_SCREEN_HEIGHT (30*8) /* Visible screen height */ #define VIDEORAM_SIZE 0x4000 /* videoram size */ #define SPRITERAM_SIZE 0x100 /* spriteram size */ #define SPRITERAM_MASK (0x100-1) /* spriteram size */ #define CHARGEN_NUM_CHARS 512 /* max number of characters handled by the chargen */ /* default monochromatic colortable */ static const pen_t default_colortable_mono[] = { 0,1,2,3, 0,1,2,3, 0,1,2,3, 0,1,2,3, 0,1,2,3, 0,1,2,3, 0,1,2,3, 0,1,2,3, }; /* default colortable */ static const pen_t default_colortable[] = { 0,1,2,3, 0,5,6,7, 0,9,10,11, 0,13,14,15, 0,17,18,19, 0,21,22,23, 0,25,26,27, 0,29,30,31, }; /* our chip state */ typedef struct { running_machine *machine; /* execution context */ mame_bitmap *bitmap; /* target bitmap */ UINT8 *videoram; /* video ram */ UINT8 *spriteram; /* sprite ram */ pen_t *colortable; /* color table modified at run time */ pen_t *colortable_mono; /* monochromatic color table modified at run time */ UINT8 *dirtychar; /* an array flagging dirty characters */ int chars_are_dirty; /* master flag to check if theres any dirty character */ emu_timer *scanline_timer; /* scanline timer */ emu_timer *hblank_timer; /* hblank period at end of each scanline */ emu_timer *nmi_timer; /* NMI timer */ int scanline; /* scanline count */ ppu2c0x_scanline_cb scanline_callback_proc; /* optional scanline callback */ ppu2c0x_hblank_cb hblank_callback_proc; /* optional hblank callback */ ppu2c0x_vidaccess_cb vidaccess_callback_proc;/* optional video access callback */ int has_videorom; /* whether we access a video rom or not */ int videorom_banks; /* number of banks in the videorom (if available) */ int regs[PPU_MAX_REG]; /* registers */ int refresh_data; /* refresh-related */ int refresh_latch; /* refresh-related */ int x_fine; /* refresh-related */ int toggle; /* used to latch hi-lo scroll */ int add; /* vram increment amount */ int videoram_addr; /* videoram address pointer */ int addr_latch; /* videoram address latch */ int data_latch; /* latched videoram data */ int buffered_data; int tile_page; /* current tile page */ int sprite_page; /* current sprite page */ int back_color; /* background color */ UINT8 *ppu_page[4]; /* ppu pages */ int nes_vram[8]; /* keep track of 8 .5k vram pages to speed things up */ int scan_scale; /* scan scale */ int scanlines_per_frame; /* number of scanlines per frame */ int mirror_state; rgb_t palette[64*4]; /* palette for this chip */ } ppu2c0x_chip; /* our local copy of the interface */ static ppu2c0x_interface *intf; /* chips state - allocated at init time */ static ppu2c0x_chip *chips = 0; static void update_scanline(int num ); static TIMER_CALLBACK( scanline_callback ); static TIMER_CALLBACK( hblank_callback ); static TIMER_CALLBACK( nmi_callback ); void (*ppu_latch)( offs_t offset ); /************************************* * * PPU Palette Initialization * *************************************/ void ppu2c0x_init_palette(running_machine *machine, int first_entry ) { /* This routine builds a palette using a transformation from */ /* the YUV (Y, B-Y, R-Y) to the RGB color space */ /* The NES has a 64 color palette */ /* 16 colors, with 4 luminance levels for each color */ /* The 16 colors circle around the YUV color space, */ int colorIntensity, colorNum, colorEmphasis; double R, G, B; double tint = 0.22; /* adjust to taste */ double hue = 287.0; double Kr = 0.2989; double Kb = 0.1145; double Ku = 2.029; double Kv = 1.140; double brightness[3][4] = { { 0.50, 0.75, 1.0, 1.0 }, { 0.29, 0.45, 0.73, 0.9 }, { 0, 0.24, 0.47, 0.77 } }; /* Loop through the emphasis modes (8 total) */ for (colorEmphasis = 0; colorEmphasis < 8; colorEmphasis ++) { double r_mod = 0.0; double g_mod = 0.0; double b_mod = 0.0; switch (colorEmphasis) { case 0: r_mod = 1.0; g_mod = 1.0; b_mod = 1.0; break; case 1: r_mod = 1.24; g_mod = .915; b_mod = .743; break; case 2: r_mod = .794; g_mod = 1.09; b_mod = .882; break; case 3: r_mod = .905; g_mod = 1.03; b_mod = 1.28; break; case 4: r_mod = .741; g_mod = .987; b_mod = 1.0; break; case 5: r_mod = 1.02; g_mod = .908; b_mod = .979; break; case 6: r_mod = 1.02; g_mod = .98; b_mod = .653; break; case 7: r_mod = .75; g_mod = .75; b_mod = .75; break; } /* loop through the 4 intensities */ for (colorIntensity = 0; colorIntensity < 4; colorIntensity++) { /* loop through the 16 colors */ for (colorNum = 0; colorNum < 16; colorNum++) { double sat; double y, u, v; double rad; switch (colorNum) { case 0: sat = 0; rad = 0; y = brightness[0][colorIntensity]; break; case 13: sat = 0; rad = 0; y = brightness[2][colorIntensity]; break; case 14: case 15: sat = 0; rad = 0; y = 0; break; default: sat = tint; rad = M_PI * ((colorNum * 30 + hue) / 180.0); y = brightness[1][colorIntensity]; break; } u = sat * cos( rad ); v = sat * sin( rad ); /* Transform to RGB */ R = ( y + Kv * v ) * 255.0; G = ( y - ( Kb * Ku * u + Kr * Kv * v) / (1 - Kb - Kr) ) * 255.0; B = ( y + Ku * u ) * 255.0; /* Clipping, in case of saturation */ if ( R < 0 ) R = 0; if ( R > 255 ) R = 255; if ( G < 0 ) G = 0; if ( G > 255 ) G = 255; if ( B < 0 ) B = 0; if ( B > 255 ) B = 255; /* Round, and set the value */ palette_set_color_rgb(machine, first_entry++, floor(R+.5), floor(G+.5), floor(B+.5)); } } } /* color tables are modified at run-time, and are initialized on 'ppu2c0x_reset' */ } /* the charlayout we use for the chargen */ static gfx_layout ppu_charlayout = { 8,8, /* 8*8 characters */ 512, /* 512 characters - modified at runtime */ 2, /* 2 bits per pixel */ { 8*8, 0 }, /* the two bitplanes are separated */ { 0, 1, 2, 3, 4, 5, 6, 7 }, { 0*8, 1*8, 2*8, 3*8, 4*8, 5*8, 6*8, 7*8 }, 16*8 /* every char takes 16 consecutive bytes */ }; /************************************* * * PPU Initialization and Disposal * *************************************/ void ppu2c0x_init(running_machine *machine, const ppu2c0x_interface *interface ) { int i; /* keep a local copy of the interface */ intf = auto_malloc(sizeof(*interface)); memcpy(intf, interface, sizeof(*interface)); /* safety check */ assert_always ( intf->num > 0, "Invalid intf->num" ); chips = auto_malloc( intf->num * sizeof( ppu2c0x_chip ) ); memset(chips, 0, intf->num * sizeof( ppu2c0x_chip )); /* intialize our virtual chips */ for( i = 0; i < intf->num; i++ ) { chips[i].machine = machine; switch (intf->type) { case PPU_2C02: chips[i].scanlines_per_frame = PPU_NTSC_SCANLINES_PER_FRAME; break; case PPU_2C03B: chips[i].scanlines_per_frame = PPU_NTSC_SCANLINES_PER_FRAME; break; case PPU_2C04: chips[i].scanlines_per_frame = PPU_NTSC_SCANLINES_PER_FRAME; break; case PPU_2C05: chips[i].scanlines_per_frame = PPU_NTSC_SCANLINES_PER_FRAME; break; case PPU_2C07: chips[i].scanlines_per_frame = PPU_PAL_SCANLINES_PER_FRAME; break; default: chips[i].scanlines_per_frame = PPU_NTSC_SCANLINES_PER_FRAME; break; } /* initialize the scanline handling portion */ chips[i].scanline_timer = timer_alloc(scanline_callback); chips[i].hblank_timer = timer_alloc(hblank_callback); chips[i].nmi_timer = timer_alloc(nmi_callback); chips[i].scanline = 0; chips[i].scan_scale = 1; /* allocate a screen bitmap, videoram and spriteram, a dirtychar array and the monochromatic colortable */ chips[i].bitmap = auto_bitmap_alloc( VISIBLE_SCREEN_WIDTH, VISIBLE_SCREEN_HEIGHT, machine->screen[0].format ); chips[i].videoram = auto_malloc( VIDEORAM_SIZE ); chips[i].spriteram = auto_malloc( SPRITERAM_SIZE ); chips[i].dirtychar = auto_malloc( CHARGEN_NUM_CHARS ); chips[i].colortable = auto_malloc( sizeof( default_colortable ) ); chips[i].colortable_mono = auto_malloc( sizeof( default_colortable_mono ) ); /* clear videoram & spriteram */ memset( chips[i].videoram, 0, VIDEORAM_SIZE ); memset( chips[i].spriteram, 0, SPRITERAM_SIZE ); /* set all characters dirty */ memset( chips[i].dirtychar, 1, CHARGEN_NUM_CHARS ); /* initialize the video ROM portion, if available */ if ( ( intf->vrom_region[i] != REGION_INVALID ) && ( memory_region( intf->vrom_region[i] ) != 0 ) ) { /* mark that we have a videorom */ chips[i].has_videorom = 1; /* find out how many banks */ chips[i].videorom_banks = memory_region_length( intf->vrom_region[i] ) / 0x2000; /* tweak the layout accordingly */ ppu_charlayout.total = chips[i].videorom_banks * CHARGEN_NUM_CHARS; } else { chips[i].has_videorom = chips[i].videorom_banks = 0; /* we need to reset this in case of mame running multisession */ ppu_charlayout.total = CHARGEN_NUM_CHARS; } /* now create the gfx region */ { UINT8 *src = chips[i].has_videorom ? memory_region( intf->vrom_region[i] ) : chips[i].videoram; machine->gfx[intf->gfx_layout_number[i]] = allocgfx( &ppu_charlayout ); decodegfx( machine->gfx[intf->gfx_layout_number[i]], src, 0, machine->gfx[intf->gfx_layout_number[i]]->total_elements ); machine->gfx[intf->gfx_layout_number[i]]->total_colors = 8; } /* setup our videoram handlers based on mirroring */ ppu2c0x_set_mirroring( i, intf->mirroring[i] ); } } static TIMER_CALLBACK( hblank_callback ) { int num = param; ppu2c0x_chip* this_ppu = &chips[num]; int *ppu_regs = &chips[num].regs[0]; int blanked = ( ppu_regs[PPU_CONTROL1] & ( PPU_CONTROL1_BACKGROUND | PPU_CONTROL1_SPRITES ) ) == 0; int vblank = ((this_ppu->scanline >= PPU_VBLANK_FIRST_SCANLINE-1) && (this_ppu->scanline < this_ppu->scanlines_per_frame-1)) ? 1 : 0; // update_scanline (num); if (this_ppu->hblank_callback_proc) (*this_ppu->hblank_callback_proc) (num, this_ppu->scanline, vblank, blanked); timer_adjust(chips[num].hblank_timer, attotime_never, num, attotime_never); } static TIMER_CALLBACK( nmi_callback ) { int num = param; int *ppu_regs = &chips[num].regs[0]; // Actually fire the VMI if (intf->nmi_handler[num]) (*intf->nmi_handler[num]) (num, ppu_regs); timer_adjust(chips[num].nmi_timer, attotime_never, num, attotime_never); } static void draw_background(const int num, UINT8 *line_priority ) { /* cache some values locally */ mame_bitmap *bitmap = chips[num].bitmap; const int *ppu_regs = &chips[num].regs[0]; const int scanline = chips[num].scanline; const int refresh_data = chips[num].refresh_data; const int gfx_bank = intf->gfx_layout_number[num]; const int total_elements = chips[num].machine->gfx[gfx_bank]->total_elements; const int *nes_vram = &chips[num].nes_vram[0]; const int tile_page = chips[num].tile_page; const int char_modulo = chips[num].machine->gfx[gfx_bank]->char_modulo; const int line_modulo = chips[num].machine->gfx[gfx_bank]->line_modulo; UINT8 *gfx_data = chips[num].machine->gfx[gfx_bank]->gfxdata; UINT8 **ppu_page = chips[num].ppu_page; int start_x = ( chips[num].x_fine ^ 0x07 ) - 7; UINT16 back_pen; UINT16 *dest; UINT8 scroll_x_coarse, scroll_y_coarse, scroll_y_fine, color_mask; int x, tile_index, start, i; const pen_t *color_table; const pen_t *paldata; const UINT8 *sd; int tilecount=0; /* setup the color mask and colortable to use */ if ( ppu_regs[PPU_CONTROL1] & PPU_CONTROL1_DISPLAY_MONO ) { color_mask = 0xf0; color_table = chips[num].colortable_mono; } else { color_mask = 0xff; color_table = chips[num].colortable; } /* cache the background pen */ back_pen = chips[num].machine->pens[(chips[num].back_color & color_mask)+intf->color_base[num]]; /* determine where in the nametable to start drawing from */ /* based on the current scanline and scroll regs */ scroll_x_coarse = refresh_data & 0x1f; scroll_y_coarse = ( refresh_data & 0x3e0 ) >> 5; scroll_y_fine = ( refresh_data & 0x7000 ) >> 12; x = scroll_x_coarse; /* get the tile index */ tile_index = ( ( refresh_data & 0xc00 ) | 0x2000 ) + scroll_y_coarse * 32; /* set up dest */ dest = ((UINT16 *) bitmap->base) + (bitmap->rowpixels * scanline) + start_x; /* draw the 32 or 33 tiles that make up a line */ while ( tilecount <34) { int color_byte; int color_bits; int pos; int index1; int page, page2, address; int index2; UINT16 pen; index1 = tile_index + x; /* Figure out which byte in the color table to use */ pos = ( ( index1 & 0x380 ) >> 4 ) | ( ( index1 & 0x1f ) >> 2 ); page = (index1 & 0x0c00) >> 10; address = 0x3c0 + pos; color_byte = ppu_page[page][address]; /* figure out which bits in the color table to use */ color_bits = ( ( index1 & 0x40 ) >> 4 ) + ( index1 & 0x02 ); address = index1 & 0x3ff; page2 = ppu_page[page][address]; index2 = nes_vram[ ( page2 >> 6 ) | tile_page ] + ( page2 & 0x3f ); //27/12/2002 if( ppu_latch ) { (*ppu_latch)(( tile_page << 10 ) | ( page2 << 4 )); } if(start_x < VISIBLE_SCREEN_WIDTH ) { paldata = &color_table[ 4 * ( ( ( color_byte >> color_bits ) & 0x03 ) ) ]; start = ( index2 % total_elements ) * char_modulo + scroll_y_fine * line_modulo; sd = &gfx_data[start]; /* render the pixel */ for( i = 0; i < 8; i++ ) { if ( ( start_x+i ) >= 0 && ( start_x+i ) < VISIBLE_SCREEN_WIDTH ) { if ( sd[i] ) { pen = paldata[sd[i]]; line_priority[ start_x+i ] |= 0x02; } else { pen = back_pen; } *dest = pen; } dest++; } start_x += 8; /* move to next tile over and toggle the horizontal name table if necessary */ x++; if ( x > 31 ) { x = 0; tile_index ^= 0x400; } } tilecount++; } /* if the left 8 pixels for the background are off, blank 'em */ if ( !( ppu_regs[PPU_CONTROL1] & PPU_CONTROL1_BACKGROUND_L8 ) ) { dest = ((UINT16 *) bitmap->base) + (bitmap->rowpixels * scanline); for( i = 0; i < 8; i++ ) { *(dest++) = back_pen; line_priority[ i ] ^= 0x02; } } } static void draw_sprites(const int num, UINT8 *line_priority ) { /* cache some values locally */ mame_bitmap *bitmap = chips[num].bitmap; const int scanline = chips[num].scanline; const int gfx_bank = intf->gfx_layout_number[num]; const int total_elements = chips[num].machine->gfx[gfx_bank]->total_elements; const int sprite_page = chips[num].sprite_page; const int char_modulo = chips[num].machine->gfx[gfx_bank]->char_modulo; const int line_modulo = chips[num].machine->gfx[gfx_bank]->line_modulo; const UINT8 *sprite_ram = chips[num].spriteram; pen_t *color_table = chips[num].colortable; UINT8 *gfx_data = chips[num].machine->gfx[gfx_bank]->gfxdata; int *ppu_regs = &chips[num].regs[0]; int spriteXPos, spriteYPos, spriteIndex; int tile, index1, page; int pri; int flipx, flipy, color; int size; int spriteCount = 0; int sprite_line; int drawn; int start; int first_pixel; const pen_t *paldata; const UINT8 *sd; int pixel; /* determine if the sprites are 8x8 or 8x16 */ size = ( ppu_regs[PPU_CONTROL0] & PPU_CONTROL0_SPRITE_SIZE ) ? 16 : 8; first_pixel = (ppu_regs[PPU_CONTROL1] & PPU_CONTROL1_SPRITES_L8)? 0: 8; for( spriteIndex = 0; spriteIndex < SPRITERAM_SIZE; spriteIndex += 4 ) { spriteYPos = sprite_ram[spriteIndex] + 1; spriteXPos = sprite_ram[spriteIndex+3]; // The sprite collision acts funny on the last pixel of a scanline. // The various scanline latches update while the last few pixels // are being drawn. Since we don't do cycle-by-cycle PPU emulation, // we fudge it a bit here so that sprite 0 collisions are detected // when, e.g., sprite x is 254, sprite y is 29 and we're rendering // at the end of scanline 28. // Battletoads needs this level of precision to be playable. if ((spriteIndex == 0) && (spriteXPos == 254)) { spriteYPos --; /* set the "sprite 0 hit" flag if appropriate */ if (line_priority[spriteXPos] & 0x02) ppu_regs[PPU_STATUS] |= PPU_STATUS_SPRITE0_HIT; } /* if the sprite isn't visible, skip it */ if ( ( spriteYPos + size <= scanline ) || ( spriteYPos > scanline ) ) continue; /* clear our drawn flag */ drawn = 0; tile = sprite_ram[spriteIndex+1]; color = ( sprite_ram[spriteIndex+2] & 0x03 ) + 4; pri = sprite_ram[spriteIndex+2] & 0x20; flipx = sprite_ram[spriteIndex+2] & 0x40; flipy = sprite_ram[spriteIndex+2] & 0x80; if ( size == 16 ) { /* if it's 8x16 and odd-numbered, draw the other half instead */ if ( tile & 0x01 ) { tile &= ~0x01; tile |= 0x100; } /* note that the sprite page value has no effect on 8x16 sprites */ page = tile >> 6; } else page = ( tile >> 6 ) | sprite_page; index1 = chips[num].nes_vram[page] + ( tile & 0x3f ); if ( ppu_latch ) (*ppu_latch)(( sprite_page << 10 ) | ( (tile & 0xff) << 4 )); /* compute the character's line to draw */ sprite_line = scanline - spriteYPos; if ( flipy ) sprite_line = ( size - 1 ) - sprite_line; paldata = &color_table[4 * color]; start = ( index1 % total_elements ) * char_modulo + sprite_line * line_modulo; sd = &gfx_data[start]; if ( pri ) { /* draw the low-priority sprites */ for ( pixel = 0; pixel < 8; pixel++ ) { UINT8 pixelData = flipx ? sd[7-pixel] : sd[pixel]; /* is this pixel non-transparent? */ if ( spriteXPos + pixel >= first_pixel) { if (pixelData) { /* has the background (or another sprite) already been drawn here? */ if ( !line_priority[ spriteXPos + pixel ] ) { /* no, draw */ if ( ( spriteXPos + pixel ) < VISIBLE_SCREEN_WIDTH ) *BITMAP_ADDR16(bitmap, scanline, spriteXPos + pixel) = paldata[pixelData]; drawn = 1; } /* indicate that a sprite was drawn at this location, even if it's not seen */ if ( ( spriteXPos + pixel ) < VISIBLE_SCREEN_WIDTH ) line_priority[ spriteXPos + pixel ] |= 0x01; } /* set the "sprite 0 hit" flag if appropriate */ if ( spriteIndex == 0 && (pixelData & 0x03) && ((spriteXPos + pixel) < 255) && ( line_priority[ spriteXPos + pixel ] & 0x02 )) ppu_regs[PPU_STATUS] |= PPU_STATUS_SPRITE0_HIT; } } } else { /* draw the high-priority sprites */ for ( pixel = 0; pixel < 8; pixel++ ) { UINT8 pixelData = flipx ? sd[7-pixel] : sd[pixel]; /* is this pixel non-transparent? */ if ( spriteXPos + pixel >= first_pixel) { if (pixelData) { /* has another sprite been drawn here? */ if ( !( line_priority[ spriteXPos + pixel ] & 0x01 ) ) { /* no, draw */ if ( ( spriteXPos + pixel ) < VISIBLE_SCREEN_WIDTH ) { *BITMAP_ADDR16(bitmap, scanline, spriteXPos + pixel) = paldata[pixelData]; line_priority[ spriteXPos + pixel ] |= 0x01; } drawn = 1; } } /* set the "sprite 0 hit" flag if appropriate */ if ( spriteIndex == 0 && (pixelData & 0x03) && ((spriteXPos + pixel) < 255) && ( line_priority[ spriteXPos + pixel ] & 0x02 )) ppu_regs[PPU_STATUS] |= PPU_STATUS_SPRITE0_HIT; } } } if ( drawn ) { /* if there are more than 8 sprites on this line, set the flag */ spriteCount++; if ( spriteCount == 8 ) { ppu_regs[PPU_STATUS] |= PPU_STATUS_8SPRITES; // logerror ("> 8 sprites, scanline: %d\n", scanline); /* the real NES only draws up to 8 sprites - the rest should be invisible */ break; } } } } /************************************* * * Scanline Rendering and Update * *************************************/ static void render_scanline(int num) { UINT8 line_priority[VISIBLE_SCREEN_WIDTH]; int *ppu_regs = &chips[num].regs[0]; /* lets see how long it takes */ profiler_mark(PROFILER_USER1+num); /* clear the line priority for this scanline */ memset( line_priority, 0, VISIBLE_SCREEN_WIDTH ); /* clear the sprite count for this line */ ppu_regs[PPU_STATUS] &= ~PPU_STATUS_8SPRITES; /* see if we need to render the background */ if ( ppu_regs[PPU_CONTROL1] & PPU_CONTROL1_BACKGROUND ) draw_background(num, line_priority ); else { mame_bitmap *bitmap = chips[num].bitmap; const int scanline = chips[num].scanline; UINT8 color_mask; UINT16 back_pen; int i; /* setup the color mask and colortable to use */ if ( ppu_regs[PPU_CONTROL1] & PPU_CONTROL1_DISPLAY_MONO ) color_mask = 0xf0; else color_mask = 0xff; /* cache the background pen */ back_pen = chips[num].machine->pens[(chips[num].back_color & color_mask)+intf->color_base[num]]; // Fill this scanline with the background pen. for (i = 0; i < bitmap->width; i ++) *BITMAP_ADDR16(bitmap, scanline, i) = back_pen; } /* if sprites are on, draw them */ if ( ppu_regs[PPU_CONTROL1] & PPU_CONTROL1_SPRITES ) draw_sprites( num, line_priority ); /* done updating, whew */ profiler_mark(PROFILER_END); } static void update_scanline(int num ) { ppu2c0x_chip* this_ppu; int scanline = chips[num].scanline; int *ppu_regs = &chips[num].regs[0]; this_ppu = &chips[num]; if ( scanline <= PPU_BOTTOM_VISIBLE_SCANLINE ) { /* Render this scanline if appropriate */ if ( ppu_regs[PPU_CONTROL1] & ( PPU_CONTROL1_BACKGROUND | PPU_CONTROL1_SPRITES ) ) { /* If background or sprites are enabled, copy the ppu address latch */ /* Copy only the scroll x-coarse and the x-overflow bit */ this_ppu->refresh_data &= ~0x041f; this_ppu->refresh_data |= ( this_ppu->refresh_latch & 0x041f ); //logerror(" updating refresh_data: %04x (scanline: %d)\n", this_ppu->refresh_data, this_ppu->scanline); render_scanline( num ); } else { mame_bitmap *bitmap = this_ppu->bitmap; const int scanline = this_ppu->scanline; UINT8 color_mask; UINT16 back_pen; int i; /* setup the color mask and colortable to use */ if ( ppu_regs[PPU_CONTROL1] & PPU_CONTROL1_DISPLAY_MONO ) color_mask = 0xf0; else color_mask = 0xff; /* cache the background pen */ if (this_ppu->videoram_addr >= 0x3f00) { // If the PPU's VRAM address happens to point into palette ram space while // both the sprites and background are disabled, the PPU paints the scanline // with the palette entry at the VRAM address instead of the usual background // pen. Micro Machines makes use of this feature. int penNum; if (this_ppu->videoram_addr & 0x03) { penNum = this_ppu->videoram[this_ppu->videoram_addr & 0x3f1f] & 0x3f; } else { penNum = this_ppu->videoram[this_ppu->videoram_addr & 0x3f00] & 0x3f; } back_pen = chips[num].machine->pens[penNum + intf->color_base[num]]; } else back_pen = chips[num].machine->pens[(this_ppu->back_color & color_mask)+intf->color_base[num]]; // Fill this scanline with the background pen. for (i = 0; i < bitmap->width; i ++) *BITMAP_ADDR16(bitmap, scanline, i) = back_pen; } /* increment the fine y-scroll */ this_ppu->refresh_data += 0x1000; /* if it's rolled, increment the coarse y-scroll */ if ( this_ppu->refresh_data & 0x8000 ) { UINT16 tmp; tmp = ( this_ppu->refresh_data & 0x03e0 ) + 0x20; this_ppu->refresh_data &= 0x7c1f; /* handle bizarro scrolling rollover at the 30th (not 32nd) vertical tile */ if ( tmp == 0x03c0 ) { this_ppu->refresh_data ^= 0x0800; } else { this_ppu->refresh_data |= ( tmp & 0x03e0 ); } //logerror("updating refresh_data: %04x\n", this_ppu->refresh_data); } } } static TIMER_CALLBACK( scanline_callback ) { int num = param; ppu2c0x_chip* this_ppu = &chips[num]; int *ppu_regs = &chips[num].regs[0]; int i; int blanked = ( ppu_regs[PPU_CONTROL1] & ( PPU_CONTROL1_BACKGROUND | PPU_CONTROL1_SPRITES ) ) == 0; int vblank = ((this_ppu->scanline >= PPU_VBLANK_FIRST_SCANLINE-1) && (this_ppu->scanline < this_ppu->scanlines_per_frame-1)) ? 1 : 0; int next_scanline; /* if a callback is available, call it */ if ( this_ppu->scanline_callback_proc ) (*this_ppu->scanline_callback_proc)( num, this_ppu->scanline, vblank, blanked ); /* update the scanline that just went by */ update_scanline( num ); /* increment our scanline count */ this_ppu->scanline++; //logerror("starting scanline %d (MAME %d, beam %d)\n", this_ppu->scanline, video_screen_get_vpos(0), video_screen_get_hpos(0)); /* Note: this is called at the _end_ of each scanline */ if (this_ppu->scanline == PPU_VBLANK_FIRST_SCANLINE) { logerror("vlbank starting\n"); /* We just entered VBLANK */ ppu_regs[PPU_STATUS] |= PPU_STATUS_VBLANK; /* If NMI's are set to be triggered, go for it */ if (ppu_regs[PPU_CONTROL0] & PPU_CONTROL0_NMI) { // We need an ever-so-slight delay between entering vblank and firing an NMI - enough so that // a game can read the high bit of $2002 before the NMI is called (potentially resetting the bit // via a read from $2002 in the NMI handler). // B-Wings is an example game that needs this. timer_adjust(this_ppu->nmi_timer, ATTOTIME_IN_CYCLES(4, 0), num, attotime_never); } } /* decode any dirty chars if we're using vram */ /* first, check the master dirty char flag */ if ( !this_ppu->has_videorom && this_ppu->chars_are_dirty ) { /* cache some values */ UINT8 *dirtyarray = this_ppu->dirtychar; UINT8 *vram = this_ppu->videoram; gfx_element *gfx = chips[num].machine->gfx[intf->gfx_layout_number[num]]; /* then iterate and decode */ for( i = 0; i < CHARGEN_NUM_CHARS; i++ ) { if ( dirtyarray[i] ) { decodechar( gfx, i, vram, &ppu_charlayout); dirtyarray[i] = 0; } } this_ppu->chars_are_dirty = 0; } if ( this_ppu->scanline == this_ppu->scanlines_per_frame - 1 ) { logerror("vlbank ending\n"); /* clear the vblank & sprite hit flag */ ppu_regs[PPU_STATUS] &= ~( PPU_STATUS_VBLANK | PPU_STATUS_SPRITE0_HIT ); } /* see if we rolled */ else if ( this_ppu->scanline == this_ppu->scanlines_per_frame ) { /* if background or sprites are enabled, copy the ppu address latch */ if ( !blanked ) this_ppu->refresh_data = this_ppu->refresh_latch; /* reset the scanline count */ this_ppu->scanline = 0; //logerror(" sprite 0 x: %d y: %d num: %d\n", this_ppu->spriteram[3], this_ppu->spriteram[0]+1, this_ppu->spriteram[1]); } next_scanline = this_ppu->scanline+1; if (next_scanline == this_ppu->scanlines_per_frame) next_scanline = 0; // Call us back when the hblank starts for this scanline timer_adjust(this_ppu->hblank_timer, ATTOTIME_IN_CYCLES(86.67, 0), num, attotime_never); // ??? FIXME - hardcoding NTSC, need better calculation // trigger again at the start of the next scanline timer_adjust(this_ppu->scanline_timer, video_screen_get_time_until_pos(0, next_scanline * this_ppu->scan_scale, 0), num, attotime_zero); } /************************************* * * PPU Reset * *************************************/ void ppu2c0x_reset(int num, int scan_scale ) { int i; /* check bounds */ if ( num >= intf->num ) { logerror( "PPU(reset): Attempting to access an unmapped chip\n" ); return; } /* reset the scanline count */ chips[num].scanline = 0; /* set the scan scale (this is for dual monitor vertical setups) */ chips[num].scan_scale = scan_scale; timer_adjust(chips[num].nmi_timer, attotime_never, num, attotime_never); // Call us back when the hblank starts for this scanline timer_adjust(chips[num].hblank_timer, ATTOTIME_IN_CYCLES(86.67, 0), num, attotime_never); // ??? FIXME - hardcoding NTSC, need better calculation // Call us back at the start of the next scanline timer_adjust(chips[num].scanline_timer, video_screen_get_time_until_pos(0, 1, 0), num, attotime_zero); /* reset the callbacks */ chips[num].scanline_callback_proc = 0; chips[num].vidaccess_callback_proc = 0; for( i = 0; i < PPU_MAX_REG; i++ ) chips[num].regs[i] = 0; /* initialize the rest of the members */ chips[num].refresh_data = 0; chips[num].refresh_latch = 0; chips[num].x_fine = 0; chips[num].toggle = 0; chips[num].add = 1; chips[num].videoram_addr = 0; chips[num].addr_latch = 0; chips[num].data_latch = 0; chips[num].tile_page = 0; chips[num].sprite_page = 0; chips[num].back_color = 0; chips[num].chars_are_dirty = 1; /* initialize the color tables */ { int color_base = intf->color_base[num]; for( i = 0; i < ARRAY_LENGTH( default_colortable_mono ); i++ ) { /* monochromatic table */ chips[num].colortable_mono[i] = chips[num].machine->pens[default_colortable_mono[i] + color_base]; /* color table */ chips[num].colortable[i] = chips[num].machine->pens[default_colortable[i] + color_base]; } } /* set the vram bank-switch values to the default */ for( i = 0; i < 8; i++ ) chips[num].nes_vram[i] = i * 64; if ( chips[num].has_videorom ) ppu2c0x_set_videorom_bank( num, 0, 8, 0, 512 ); } /************************************* * * PPU Registers Read * *************************************/ int ppu2c0x_r( int num, offs_t offset ) { ppu2c0x_chip* this_ppu; /* check bounds */ if ( num >= intf->num ) { logerror( "PPU %d(r): Attempting to access an unmapped chip\n", num ); return 0; } this_ppu = &chips[num]; if ( offset >= PPU_MAX_REG ) { logerror( "PPU %d(r): Attempting to read past the chip\n", num ); offset &= PPU_MAX_REG - 1; } // see which register to read switch( offset & 7 ) { case PPU_STATUS: // The top 3 bits of the status register are the only ones that report data. The // remainder contain whatever was last in the PPU data latch. this_ppu->data_latch = this_ppu->regs[PPU_STATUS] | (this_ppu->data_latch & 0x1f); // Reset hi/lo scroll toggle this_ppu->toggle = 0; // If the vblank bit is set, clear all status bits but the 2 sprite flags if (this_ppu->data_latch & PPU_STATUS_VBLANK) this_ppu->regs[PPU_STATUS] &= 0x60; break; case PPU_SPRITE_DATA: this_ppu->data_latch = this_ppu->spriteram[this_ppu->regs[PPU_SPRITE_ADDRESS]]; break; case PPU_DATA: if ( this_ppu->videoram_addr >= 0x3f00 ) { this_ppu->data_latch = this_ppu->videoram[this_ppu->videoram_addr & 0x3F1F]; if (this_ppu->regs[PPU_CONTROL1] & PPU_CONTROL1_DISPLAY_MONO) this_ppu->data_latch &= 0x30; } else this_ppu->data_latch = this_ppu->buffered_data; if ( ppu_latch ) (*ppu_latch)( this_ppu->videoram_addr & 0x3fff ); if ( ( this_ppu->videoram_addr >= 0x2000 ) && ( this_ppu->videoram_addr <= 0x3fff ) ) this_ppu->buffered_data = this_ppu->ppu_page[ ( this_ppu->videoram_addr & 0xc00) >> 10][ this_ppu->videoram_addr & 0x3ff ]; else this_ppu->buffered_data = this_ppu->videoram[ this_ppu->videoram_addr & 0x3fff ]; this_ppu->videoram_addr += this_ppu->add; break; default: break; } return this_ppu->data_latch; } /************************************* * * PPU Registers Write * *************************************/ void ppu2c0x_w( int num, offs_t offset, UINT8 data ) { ppu2c0x_chip* this_ppu; int color_base; /* check bounds */ if ( num >= intf->num ) { logerror( "PPU(w): Attempting to access an unmapped chip\n" ); return; } this_ppu = &chips[num]; color_base = intf->color_base[num]; if ( offset >= PPU_MAX_REG ) { logerror( "PPU: Attempting to write past the chip\n" ); offset &= PPU_MAX_REG - 1; } #ifdef MAME_DEBUG if (this_ppu->scanline <= PPU_BOTTOM_VISIBLE_SCANLINE) logerror(" PPU register %d write %02x during non-vblank scanline %d (MAME %d, beam pos: %d)\n", offset, data, this_ppu->scanline, video_screen_get_vpos(0), video_screen_get_hpos(0)); #endif switch( offset & 7 ) { case PPU_CONTROL0: this_ppu->regs[PPU_CONTROL0] = data; /* update the name table number on our refresh latches */ this_ppu->refresh_latch &= 0x73ff; this_ppu->refresh_latch |= ( data & 3 ) << 10; /* the char ram bank points either 0x0000 or 0x1000 (page 0 or page 4) */ this_ppu->tile_page = ( data & PPU_CONTROL0_CHR_SELECT ) >> 2; this_ppu->sprite_page = ( data & PPU_CONTROL0_SPR_SELECT ) >> 1; this_ppu->add = ( data & PPU_CONTROL0_INC ) ? 32 : 1; //logerror(" control0 write: %02x (scanline: %d)\n", data, this_ppu->scanline); break; case PPU_CONTROL1: /* if color intensity has changed, change all the color tables to reflect them */ if ( ( data & PPU_CONTROL1_COLOR_EMPHASIS ) != ( this_ppu->regs[PPU_CONTROL1] & PPU_CONTROL1_COLOR_EMPHASIS ) ) { int i; for (i = 0; i <= 0x1f; i ++) { UINT8 oldColor = this_ppu->videoram[i+0x3f00]; this_ppu->colortable[i] = chips[num].machine->pens[color_base + oldColor + (data & PPU_CONTROL1_COLOR_EMPHASIS)*2]; } } //logerror(" control1 write: %02x (scanline: %d)\n", data, this_ppu->scanline); this_ppu->regs[PPU_CONTROL1] = data; break; case PPU_SPRITE_ADDRESS: this_ppu->regs[PPU_SPRITE_ADDRESS] = data; break; case PPU_SPRITE_DATA: // If the PPU is currently rendering the screen, 0xff is written instead of the desired data. if (this_ppu->scanline <= PPU_BOTTOM_VISIBLE_SCANLINE) data = 0xff; this_ppu->spriteram[this_ppu->regs[PPU_SPRITE_ADDRESS]] = data; this_ppu->regs[PPU_SPRITE_ADDRESS] = ( this_ppu->regs[PPU_SPRITE_ADDRESS] + 1 ) & 0xff; break; case PPU_SCROLL: if ( this_ppu->toggle ) { /* second write */ this_ppu->refresh_latch &= 0x0c1f; this_ppu->refresh_latch |= ( data & 0xf8 ) << 2; this_ppu->refresh_latch |= ( data & 0x07 ) << 12; //logerror(" scroll write 2: %d, %04x (scanline: %d)\n", data, this_ppu->refresh_latch, this_ppu->scanline); } else { /* first write */ this_ppu->refresh_latch &= 0x7fe0; this_ppu->refresh_latch |= (data & 0xf8) >> 3; this_ppu->x_fine = data & 7; //logerror(" scroll write 1: %d, %04x (scanline: %d)\n", data, this_ppu->refresh_latch, this_ppu->scanline); } this_ppu->toggle ^= 1; break; case PPU_ADDRESS: if ( this_ppu->toggle ) { /* second write */ this_ppu->refresh_latch &= 0x7f00; this_ppu->refresh_latch |= data; this_ppu->refresh_data = this_ppu->refresh_latch; this_ppu->videoram_addr = this_ppu->refresh_latch; //logerror(" vram addr write 2: %02x, %04x (scanline: %d)\n", data, this_ppu->refresh_latch, this_ppu->scanline); } else { /* first write */ this_ppu->refresh_latch &= 0x00ff; this_ppu->refresh_latch |= ( data & 0x3f ) << 8; //logerror(" vram addr write 1: %02x, %04x (scanline: %d)\n", data, this_ppu->refresh_latch, this_ppu->scanline); } this_ppu->toggle ^= 1; break; case PPU_DATA: { int tempAddr = this_ppu->videoram_addr & 0x3fff; if ( ppu_latch ) (*ppu_latch)( tempAddr ); /* if there's a callback, call it now */ if ( this_ppu->vidaccess_callback_proc ) data = (*this_ppu->vidaccess_callback_proc)( num, tempAddr, data ); /* see if it's on the chargen portion */ if ( tempAddr < 0x2000 ) { /* if we have a videorom mapped there, dont write and log the problem */ if ( this_ppu->has_videorom ) { /* if there is a vidaccess callback, assume it coped with it */ if ( this_ppu->vidaccess_callback_proc == NULL ) logerror( "PPU: Attempting to write to the chargen when there's a ROM there!\n" ); } else { /* store the data */ this_ppu->videoram[tempAddr] = data; /* setup the master dirty switch */ this_ppu->chars_are_dirty = 1; /* mark the char dirty */ this_ppu->dirtychar[tempAddr >> 4] = 1; } } else if ( tempAddr >= 0x3f00 ) { int colorEmphasis = (this_ppu->regs[PPU_CONTROL1] & PPU_CONTROL1_COLOR_EMPHASIS) * 2; /* store the data */ if (tempAddr & 0x03) this_ppu->videoram[tempAddr & 0x3F1F] = data; else { this_ppu->videoram[0x3F10+(tempAddr&0xF)] = data; this_ppu->videoram[0x3F00+(tempAddr&0xF)] = data; } /* As usual, some games attempt to write values > the number of colors so we must mask the data. */ data &= 0x3f; if ( tempAddr & 0x03 ) { this_ppu->colortable[ tempAddr & 0x1f ] = chips[num].machine->pens[color_base + data + colorEmphasis]; this_ppu->colortable_mono[tempAddr & 0x1f] = chips[num].machine->pens[color_base + (data & 0xf0) + colorEmphasis]; } /* The only valid background colors are writes to 0x3f00 and 0x3f10 */ /* and even then, they are mirrors of each other. */ if ( ( tempAddr & 0x0f ) == 0 ) { int i; this_ppu->back_color = data; for( i = 0; i < 32; i += 4 ) { this_ppu->colortable[ i ] = chips[num].machine->pens[color_base + data + colorEmphasis]; this_ppu->colortable_mono[i] = chips[num].machine->pens[color_base + (data & 0xf0) + colorEmphasis]; } } } /* everything else */ /* writes to $3000-$3eff are mirrors of $2000-$2eff */ else { int page = ( tempAddr & 0x0c00) >> 10; int address = tempAddr & 0x3ff; this_ppu->ppu_page[page][address] = data; } /* increment the address */ this_ppu->videoram_addr += this_ppu->add; } break; default: /* ignore other registers writes */ break; } } /************************************* * * Sprite DMA * *************************************/ void ppu2c0x_spriteram_dma (int num, const UINT8 page) { int i; int address = page << 8; /* check bounds */ if ( num >= intf->num ) { logerror( "PPU(w): Attempting to access an unmapped chip\n" ); return; } //logerror(" sprite DMA: %d (scanline: %d)\n", page, chips[num].scanline); for (i = 0; i < SPRITERAM_SIZE; i++) { UINT8 spriteData = program_read_byte_8 (address + i); ppu2c0x_w (num, PPU_SPRITE_DATA, spriteData); } // should last 513 CPU cycles. activecpu_adjust_icount(-513); // ????TODO : need to account for PPU rendering - this is roughly 4.5 scanlines eaten up. // Because the DMA is only useful during vblank, this may not be strictly necessary since // the scanline timers should catch us up before drawing actually happens. #if 0 scanline_callback(Machine, num); scanline_callback(Machine, num); scanline_callback(Machine, num); scanline_callback(Machine, num); #endif } /************************************* * * PPU Rendering * *************************************/ void ppu2c0x_render( int num, mame_bitmap *bitmap, int flipx, int flipy, int sx, int sy ) { /* check bounds */ if ( num >= intf->num ) { logerror( "PPU(render): Attempting to access an unmapped chip\n" ); return; } copybitmap( bitmap, chips[num].bitmap, flipx, flipy, sx, sy, 0, TRANSPARENCY_NONE, 0 ); } /************************************* * * PPU VideoROM banking * *************************************/ void ppu2c0x_set_videorom_bank( int num, int start_page, int num_pages, int bank, int bank_size ) { int i; /* check bounds */ if ( num >= intf->num ) { logerror( "PPU(set vrom bank): Attempting to access an unmapped chip\n" ); return; } if ( !chips[num].has_videorom ) { logerror( "PPU(set vrom bank): Attempting to switch videorom banks and no rom is mapped\n" ); return; } bank &= ( chips[num].videorom_banks * ( CHARGEN_NUM_CHARS / bank_size ) ) - 1; for( i = start_page; i < ( start_page + num_pages ); i++ ) chips[num].nes_vram[i] = bank * bank_size + 64 * ( i - start_page ); { int vram_start = start_page * 0x400; int count = num_pages * 0x400; int rom_start = bank * bank_size * 16; memcpy( &chips[num].videoram[vram_start], &memory_region( intf->vrom_region[num] )[rom_start], count ); } } /************************************* * * Utility functions * *************************************/ int ppu2c0x_get_pixel( int num, int x, int y ) { /* check bounds */ if ( num >= intf->num ) { logerror( "PPU(get_pixel): Attempting to access an unmapped chip\n" ); return 0; } if ( x >= VISIBLE_SCREEN_WIDTH ) x = VISIBLE_SCREEN_WIDTH - 1; if ( y >= VISIBLE_SCREEN_HEIGHT ) y = VISIBLE_SCREEN_HEIGHT - 1; return *BITMAP_ADDR16(chips[num].bitmap, y, x); } int ppu2c0x_get_colorbase( int num ) { /* check bounds */ if ( num >= intf->num ) { logerror( "PPU(get_colorbase): Attempting to access an unmapped chip\n" ); return 0; } return intf->color_base[num]; } int ppu2c0x_get_current_scanline( int num ) { /* check bounds */ if ( num >= intf->num ) { logerror( "PPU(get_colorbase): Attempting to access an unmapped chip\n" ); return 0; } return chips[num].scanline; } void ppu2c0x_set_mirroring( int num, int mirroring ) { ppu2c0x_chip* this_ppu; /* check bounds */ if ( num >= intf->num ) { logerror( "PPU %d: Attempting to access an unmapped chip\n", num ); return; } this_ppu = &chips[num]; // Once we've set 4-screen mirroring, do not change. Some games // (notably Gauntlet) use mappers that can change the mirroring // state, but are also hard-coded for 4-screen VRAM. if (this_ppu->mirror_state == PPU_MIRROR_4SCREEN) return; /* setup our videoram handlers based on mirroring */ switch( mirroring ) { case PPU_MIRROR_VERT: this_ppu->ppu_page[0] = &(this_ppu->videoram[0x2000]); this_ppu->ppu_page[1] = &(this_ppu->videoram[0x2400]); this_ppu->ppu_page[2] = &(this_ppu->videoram[0x2000]); this_ppu->ppu_page[3] = &(this_ppu->videoram[0x2400]); break; case PPU_MIRROR_HORZ: this_ppu->ppu_page[0] = &(this_ppu->videoram[0x2000]); this_ppu->ppu_page[1] = &(this_ppu->videoram[0x2000]); this_ppu->ppu_page[2] = &(this_ppu->videoram[0x2400]); this_ppu->ppu_page[3] = &(this_ppu->videoram[0x2400]); break; case PPU_MIRROR_HIGH: this_ppu->ppu_page[0] = &(this_ppu->videoram[0x2400]); this_ppu->ppu_page[1] = &(this_ppu->videoram[0x2400]); this_ppu->ppu_page[2] = &(this_ppu->videoram[0x2400]); this_ppu->ppu_page[3] = &(this_ppu->videoram[0x2400]); break; case PPU_MIRROR_LOW: this_ppu->ppu_page[0] = &(this_ppu->videoram[0x2000]); this_ppu->ppu_page[1] = &(this_ppu->videoram[0x2000]); this_ppu->ppu_page[2] = &(this_ppu->videoram[0x2000]); this_ppu->ppu_page[3] = &(this_ppu->videoram[0x2000]); break; case PPU_MIRROR_NONE: case PPU_MIRROR_4SCREEN: default: this_ppu->ppu_page[0] = &(this_ppu->videoram[0x2000]); this_ppu->ppu_page[1] = &(this_ppu->videoram[0x2400]); this_ppu->ppu_page[2] = &(this_ppu->videoram[0x2800]); this_ppu->ppu_page[3] = &(this_ppu->videoram[0x2c00]); break; } this_ppu->mirror_state = mirroring; } #ifdef UNUSED_FUNCTION void ppu2c0x_set_nmi_callback( int num, ppu2c0x_nmi_cb cb ) { /* check bounds */ if ( num >= intf->num ) { logerror( "PPU(set_nmi_callback): Attempting to access an unmapped chip\n" ); return; } intf->nmi_handler[num] = cb; } #endif void ppu2c0x_set_scanline_callback( int num, ppu2c0x_scanline_cb cb ) { /* check bounds */ if ( num >= intf->num ) { logerror( "PPU(set_scanline_callback): Attempting to access an unmapped chip\n" ); return; } chips[num].scanline_callback_proc = cb; } void ppu2c0x_set_hblank_callback( int num, ppu2c0x_hblank_cb cb ) { /* check bounds */ if ( num >= intf->num ) { logerror( "PPU(set_scanline_callback): Attempting to access an unmapped chip\n" ); return; } chips[num].hblank_callback_proc = cb; } void ppu2c0x_set_vidaccess_callback( int num, ppu2c0x_vidaccess_cb cb ) { /* check bounds */ if ( num >= intf->num ) { logerror( "PPU(set_vidaccess_callback): Attempting to access an unmapped chip\n" ); return; } chips[num].vidaccess_callback_proc = cb; } void ppu2c0x_set_scanlines_per_frame( int num, int scanlines ) { /* check bounds */ if ( num >= intf->num ) { logerror( "PPU(set_scanlines_per_frame): Attempting to access an unmapped chip\n" ); return; } chips[num].scanlines_per_frame = scanlines; } /************************************* * * Accesors * *************************************/ READ8_HANDLER( ppu2c0x_0_r ) { return ppu2c0x_r( 0, offset ); } READ8_HANDLER( ppu2c0x_1_r ) { return ppu2c0x_r( 1, offset ); } WRITE8_HANDLER( ppu2c0x_0_w ) { ppu2c0x_w( 0, offset, data ); } WRITE8_HANDLER( ppu2c0x_1_w ) { ppu2c0x_w( 1, offset, data ); }