// Nes_Emu 0.7.0. http://www.slack.net/~ant/ #include "Nes_Ppu_Rendering.h" #include #include /* Copyright (C) 2004-2006 Shay Green. This module is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This module is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this module; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "blargg_source.h" #ifdef BLARGG_ENABLE_OPTIMIZER #include BLARGG_ENABLE_OPTIMIZER #endif #ifdef __MWERKS__ static unsigned zero = 0; // helps CodeWarrior optimizer when added to constants #else const unsigned zero = 0; // compile-time constant on other compilers #endif // Nes_Ppu_Impl inline Nes_Ppu_Impl::cached_tile_t const& Nes_Ppu_Impl::get_sprite_tile( byte const* sprite ) /*const*/ { cached_tile_t* tiles = tile_cache; if ( sprite [2] & 0x40 ) tiles = flipped_tiles; int index = sprite_tile_index( sprite ); // use index directly, since cached tile is same size as native tile BOOST_STATIC_ASSERT( sizeof (cached_tile_t) == bytes_per_tile ); return *(Nes_Ppu_Impl::cached_tile_t*) ((byte*) tiles + map_chr_addr( index * bytes_per_tile )); } inline Nes_Ppu_Impl::cached_tile_t const& Nes_Ppu_Impl::get_bg_tile( int index ) /*const*/ { // use index directly, since cached tile is same size as native tile BOOST_STATIC_ASSERT( sizeof (cached_tile_t) == bytes_per_tile ); return *(Nes_Ppu_Impl::cached_tile_t*) ((byte*) tile_cache + map_chr_addr( index * bytes_per_tile )); } // Fill void Nes_Ppu_Rendering::fill_background( int count ) { ptrdiff_t const next_line = scanline_row_bytes - image_width; uint32_t* pixels = (uint32_t*) scanline_pixels; unsigned long fill = palette_offset; if ( (vram_addr & 0x3f00) == 0x3f00 ) { // PPU uses current palette entry if addr is within palette ram int color = vram_addr & 0x1f; if ( !(color & 3) ) color &= 0x0f; fill += color * 0x01010101; } for ( int n = count; n--; ) { for ( int n = image_width / 16; n--; ) { pixels [0] = fill; pixels [1] = fill; pixels [2] = fill; pixels [3] = fill; pixels += 4; } pixels = (uint32_t*) ((byte*) pixels + next_line); } } void Nes_Ppu_Rendering::clip_left( int count ) { ptrdiff_t next_line = scanline_row_bytes; byte* p = scanline_pixels; unsigned long fill = palette_offset; for ( int n = count; n--; ) { ((uint32_t*) p) [0] = fill; ((uint32_t*) p) [1] = fill; p += next_line; } } void Nes_Ppu_Rendering::save_left( int count ) { ptrdiff_t next_line = scanline_row_bytes; byte* in = scanline_pixels; uint32_t* out = impl->clip_buf; for ( int n = count; n--; ) { unsigned long in0 = ((uint32_t*) in) [0]; unsigned long in1 = ((uint32_t*) in) [1]; in += next_line; out [0] = in0; out [1] = in1; out += 2; } } void Nes_Ppu_Rendering::restore_left( int count ) { ptrdiff_t next_line = scanline_row_bytes; byte* out = scanline_pixels; uint32_t* in = impl->clip_buf; for ( int n = count; n--; ) { unsigned long in0 = in [0]; unsigned long in1 = in [1]; in += 2; ((uint32_t*) out) [0] = in0; ((uint32_t*) out) [1] = in1; out += next_line; } } // Background void Nes_Ppu_Rendering::draw_background_( int remain ) { // Draws 'remain' background scanlines. Does not modify vram_addr. int vram_addr = this->vram_addr & 0x7fff; byte* row_pixels = scanline_pixels - pixel_x; int left_clip = (w2001 >> 1 & 1) ^ 1; row_pixels += left_clip * 8; do { // scanlines until next row int height = 8 - (vram_addr >> 12); if ( height > remain ) height = remain; // handle hscroll change before next scanline int hscroll_changed = (vram_addr ^ vram_temp) & 0x41f; int addr = vram_addr; if ( hscroll_changed ) { vram_addr ^= hscroll_changed; height = 1; // hscroll will change after first line } remain -= height; // increment address for next row vram_addr += height << 12; assert( vram_addr < 0x10000 ); if ( vram_addr & 0x8000 ) { int y = (vram_addr + 0x20) & 0x3e0; vram_addr &= 0x7fff & ~0x3e0; if ( y == 30 * 0x20 ) y = 0x800; // toggle vertical nametable vram_addr ^= y; } // nametable change usually occurs in middle of row byte const* nametable = get_nametable( addr ); byte const* nametable2 = get_nametable( addr ^ 0x400 ); int count2 = addr & 31; int count = 32 - count2 - left_clip; // this conditional is commented out because of mmc2\4 // normally, the extra row of pixels is only fetched when pixel_ x is not 0, which makes sense // but here, we need a correct fetch pattern to pick up 0xfd\0xfe tiles off the edge of the display // this doesn't cause any problems with buffer overflow because the framebuffer we're rendering to is // already guarded (width = 272) // this doesn't give us a fully correct ppu fetch pattern, but it's close enough for punch out //if ( pixel_x ) count2++; byte const* attr_table = &nametable [0x3c0 | (addr >> 4 & 0x38)]; int bg_bank = (w2000 << 4) & 0x100; addr += left_clip; // output pixels ptrdiff_t const row_bytes = scanline_row_bytes; byte* pixels = row_pixels; row_pixels += height * row_bytes; unsigned long const mask = 0x03030303 + zero; unsigned long const attrib_factor = 0x04040404 + zero; if ( height == 8 ) { // unclipped assert( (addr >> 12) == 0 ); addr &= 0x03ff; int const fine_y = 0; int const clipped = false; #include "Nes_Ppu_Bg.h" } else { // clipped int const fine_y = addr >> 12; addr &= 0x03ff; height -= fine_y & 1; int const clipped = true; #include "Nes_Ppu_Bg.h" } } while ( remain ); } // Sprites void Nes_Ppu_Rendering::draw_sprites_( int begin, int end ) { // Draws sprites on scanlines begin through end - 1. Handles clipping. int const sprite_height = this->sprite_height(); int end_minus_one = end - 1; int begin_minus_one = begin - 1; int index = 0; do { byte const* sprite = &spr_ram [index]; index += 4; // find if sprite is visible int top_minus_one = sprite [0]; int visible = end_minus_one - top_minus_one; if ( visible <= 0 ) continue; // off bottom // quickly determine whether sprite is unclipped int neg_vis = visible - sprite_height; int neg_skip = top_minus_one - begin_minus_one; if ( (neg_skip | neg_vis) >= 0 ) // neg_skip >= 0 && neg_vis >= 0 { // unclipped #ifndef NDEBUG int top = sprite [0] + 1; assert( (top + sprite_height) > begin && top < end ); assert( begin <= top && top + sprite_height <= end ); #endif int const skip = 0; int visible = sprite_height; #define CLIPPED 0 #include "Nes_Ppu_Sprites.h" } else { // clipped if ( neg_vis > 0 ) visible -= neg_vis; if ( neg_skip > 0 ) neg_skip = 0; visible += neg_skip; if ( visible <= 0 ) continue; // off top // visible and clipped #ifndef NDEBUG int top = sprite [0] + 1; assert( (top + sprite_height) > begin && top < end ); assert( top < begin || top + sprite_height > end ); #endif int skip = -neg_skip; //dprintf( "begin: %d, end: %d, top: %d, skip: %d, visible: %d\n", // begin, end, top_minus_one + 1, skip, visible ); #define CLIPPED 1 #include "Nes_Ppu_Sprites.h" } } while ( index < 0x100 ); } void Nes_Ppu_Rendering::check_sprite_hit( int begin, int end ) { // Checks for sprite 0 hit on scanlines begin through end - 1. // Updates sprite_hit_found. Background (but not sprites) must have // already been rendered for the scanlines. // clip int top = spr_ram [0] + 1; int skip = begin - top; if ( skip < 0 ) skip = 0; top += skip; int visible = end - top; if ( visible <= 0 ) return; // not visible int height = sprite_height(); if ( visible >= height ) { visible = height; sprite_hit_found = -1; // signal that no more hit checking will take place } // pixels ptrdiff_t next_row = this->scanline_row_bytes; byte const* bg = this->scanline_pixels + spr_ram [3] + (top - begin) * next_row; cache_t const* lines = get_sprite_tile( spr_ram ); // left edge clipping int start_x = 0; if ( spr_ram [3] < 8 && (w2001 & 0x01e) != 0x1e ) { if ( spr_ram [3] == 0 ) return; // won't hit start_x = 8 - spr_ram [3]; } // vertical flip int final = skip + visible; if ( spr_ram [2] & 0x80 ) { skip += height - 1; final = skip - visible; } // check each line unsigned long const mask = 0x01010101 + zero; do { // get pixels for line unsigned long line = lines [skip >> 1]; unsigned long hit0 = ((uint32_t*) bg) [0]; unsigned long hit1 = ((uint32_t*) bg) [1]; bg += next_row; line >>= skip << 1 & 2; line |= line >> 1; // check for hits hit0 = ((hit0 >> 1) | hit0) & (line >> 4); hit1 = ((hit1 >> 1) | hit1) & line; if ( (hit0 | hit1) & mask ) { // write to memory to avoid endian issues uint32_t quads [3]; quads [0] = hit0; quads [1] = hit1; // find which pixel hit int x = start_x; do { if ( ((byte*) quads) [x] & 1 ) { x += spr_ram [3]; if ( x >= 255 ) break; // ignore right edge if ( spr_ram [2] & 0x80 ) skip = height - 1 - skip; // vertical flip int y = spr_ram [0] + 1 + skip; sprite_hit_found = y * scanline_len + x; return; } } while ( x++ < 7 ); } if ( skip > final ) skip -= 2; skip++; } while ( skip != final ); } // Draw scanlines inline bool Nes_Ppu_Rendering::sprite_hit_possible( int scanline ) const { return !sprite_hit_found && spr_ram [0] <= scanline && (w2001 & 0x18) == 0x18; } void Nes_Ppu_Rendering::draw_scanlines( int start, int count, byte* pixels, long pitch, int mode ) { assert( start + count <= image_height ); assert( pixels ); scanline_pixels = pixels + image_left; scanline_row_bytes = pitch; int const obj_mask = 2; int const bg_mask = 1; int draw_mode = (w2001 >> 3) & 3; int clip_mode = (~w2001 >> 1) & draw_mode; if ( !(draw_mode & bg_mask) ) { // no background clip_mode |= bg_mask; // avoid unnecessary save/restore if ( mode & bg_mask ) fill_background( count ); } if ( start == 0 && mode & 1 ) memset( sprite_scanlines, max_sprites - sprite_limit, 240 ); if ( (draw_mode &= mode) ) { // sprites and/or background are being rendered if ( any_tiles_modified && chr_is_writable ) { any_tiles_modified = false; update_tiles( 0 ); } if ( draw_mode & bg_mask ) { //dprintf( "bg %3d-%3d\n", start, start + count - 1 ); draw_background_( count ); if ( clip_mode == bg_mask ) clip_left( count ); if ( sprite_hit_possible( start + count ) ) check_sprite_hit( start, start + count ); } if ( draw_mode & obj_mask ) { // when clipping just sprites, save left strip then restore after drawing them if ( clip_mode == obj_mask ) save_left( count ); //dprintf( "obj %3d-%3d\n", start, start + count - 1 ); draw_sprites_( start, start + count ); if ( clip_mode == obj_mask ) restore_left( count ); if ( clip_mode == (obj_mask | bg_mask) ) clip_left( count ); } } scanline_pixels = NULL; } void Nes_Ppu_Rendering::draw_background( int start, int count ) { // always capture palette at least once per frame if ( (start + count >= 240 && !palette_size) || (w2001 & palette_changed) ) { palette_changed = false; capture_palette(); } if ( host_pixels ) { draw_scanlines( start, count, host_pixels + host_row_bytes * start, host_row_bytes, 1 ); } else if ( sprite_hit_possible( start + count ) ) { // not rendering, but still handle sprite hit using mini graphics buffer int y = spr_ram [0] + 1; int skip = min( count, max( y - start, 0 ) ); int visible = min( count - skip, sprite_height() ); assert( skip + visible <= count ); assert( visible <= mini_offscreen_height ); if ( visible > 0 ) { run_hblank( skip ); draw_scanlines( start + skip, visible, impl->mini_offscreen, buffer_width, 3 ); } } }