quickerNES/source/core/Nes_Ppu.cpp

658 lines
16 KiB
C++

// Timing and behavior of PPU
// Nes_Emu 0.7.0. http://www.slack.net/~ant/
#include "Nes_Ppu.h"
#include <string.h>
#include "Nes_State.h"
#include "Nes_Mapper.h"
#include "Nes_Core.h"
/* 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 */
// to do: remove unnecessary run_until() calls
#include "blargg_source.h"
// Timing
ppu_time_t const scanline_len = Nes_Ppu::scanline_len;
// if non-zero, report sprite max at fixed time rather than calculating it
nes_time_t const fixed_sprite_max_time = 0; // 1 * ((21 + 164) * scanline_len + 100) / ppu_overclock;
int const sprite_max_cpu_offset = 2420 + 3;
ppu_time_t const t_to_v_time = 20 * scanline_len + 302;
ppu_time_t const even_odd_time = 20 * scanline_len + 328;
ppu_time_t const first_scanline_time = 21 * scanline_len + 60; // this can be varied
ppu_time_t const first_hblank_time = 21 * scanline_len + 252;
ppu_time_t const earliest_sprite_max = sprite_max_cpu_offset * ppu_overclock;
ppu_time_t const earliest_sprite_hit = 21 * scanline_len + 339; // needs to be 22 * scanline_len when fixed_sprite_max_time is set
nes_time_t const vbl_end_time = 2272;
ppu_time_t const max_frame_length = 262 * scanline_len;
//ppu_time_t const max_frame_length = 320 * scanline_len; // longer frame for testing movie resync
nes_time_t const earliest_vbl_end_time = max_frame_length / ppu_overclock - 10;
// Scanline rendering
void Nes_Ppu::render_bg_until_( nes_time_t cpu_time )
{
ppu_time_t time = ppu_time( cpu_time );
ppu_time_t const frame_duration = scanline_len * 261;
if ( time > frame_duration )
time = frame_duration;
// one-time events
if ( frame_phase <= 1 )
{
if ( frame_phase < 1 )
{
// vtemp->vaddr
frame_phase = 1;
if ( w2001 & 0x08 )
vram_addr = vram_temp;
}
// variable-length scanline
if ( time <= even_odd_time )
{
next_bg_time = nes_time( even_odd_time );
return;
}
frame_phase = 2;
if ( !(w2001 & 0x08) || emu.nes.frame_count & 1 )
{
if ( --frame_length_extra < 0 )
{
frame_length_extra = 2;
frame_length_++;
}
burst_phase--;
}
burst_phase = (burst_phase + 2) % 3;
}
// scanlines
if ( scanline_time < time )
{
int count = (time - scanline_time + scanline_len) / scanline_len;
// hblank before next scanline
if ( hblank_time < scanline_time )
{
hblank_time += scanline_len;
run_hblank( 1 );
}
scanline_time += count * scanline_len;
hblank_time += scanline_len * (count - 1);
int saved_vaddr = vram_addr;
int start = scanline_count;
scanline_count += count;
draw_background( start, count );
vram_addr = saved_vaddr; // to do: this is cheap
run_hblank( count - 1 );
}
// hblank after current scanline
ppu_time_t next_ppu_time = hblank_time;
if ( hblank_time < time )
{
hblank_time += scanline_len;
run_hblank( 1 );
next_ppu_time = scanline_time; // scanline will run next
}
// either hblank or scanline comes next
next_bg_time = nes_time( next_ppu_time );
}
void Nes_Ppu::render_until_( nes_time_t time )
{
// render bg scanlines then render sprite scanlines up to wherever bg was rendered to
render_bg_until( time );
next_sprites_time = nes_time( scanline_time );
if ( host_pixels )
{
int start = next_sprites_scanline;
int count = scanline_count - start;
if ( count > 0 )
{
next_sprites_scanline += count;
draw_sprites( start, count );
}
}
}
// Frame events
inline void Nes_Ppu::end_vblank()
{
// clear VBL, sprite hit, and max sprites flags first time after 20 scanlines
r2002 &= end_vbl_mask;
end_vbl_mask = ~0;
}
inline void Nes_Ppu::run_end_frame( nes_time_t time )
{
if ( !frame_ended )
{
// update frame_length
render_bg_until( time );
// set VBL when end of frame is reached
nes_time_t len = frame_length();
if ( time >= len )
{
r2002 |= 0x80;
frame_ended = true;
if ( w2000 & 0x80 )
nmi_time_ = len + 2 - (frame_length_extra >> 1);
}
}
}
// Sprite max
inline void Nes_Ppu::invalidate_sprite_max_()
{
next_sprite_max_run = earliest_sprite_max / ppu_overclock;
sprite_max_set_time = 0;
}
void Nes_Ppu::run_sprite_max_( nes_time_t cpu_time )
{
end_vblank(); // might get run outside $2002 handler
// 577.0 / 0x10000 ~= 1.0 / 113.581, close enough to accurately calculate which scanline it is
int start_scanline = next_sprite_max_scanline;
next_sprite_max_scanline = unsigned ((cpu_time - sprite_max_cpu_offset) * 577) / 0x10000u;
if ( !sprite_max_set_time )
{
if ( !(w2001 & 0x18) )
return;
long t = recalc_sprite_max( start_scanline );
sprite_max_set_time = indefinite_time;
if ( t > 0 )
sprite_max_set_time = t / 3 + sprite_max_cpu_offset;
next_sprite_max_run = sprite_max_set_time;
//dprintf( "sprite_max_set_time: %d\n", sprite_max_set_time );
}
if ( cpu_time > sprite_max_set_time )
{
r2002 |= 0x20;
//dprintf( "Sprite max flag set: %d\n", sprite_max_set_time );
next_sprite_max_run = indefinite_time;
}
}
inline void Nes_Ppu::run_sprite_max( nes_time_t t )
{
if ( !fixed_sprite_max_time && t > next_sprite_max_run )
run_sprite_max_( t );
}
inline void Nes_Ppu::invalidate_sprite_max( nes_time_t t )
{
if ( !fixed_sprite_max_time && !(r2002 & 0x20) )
{
run_sprite_max( t );
invalidate_sprite_max_();
}
}
// Sprite 0 hit
inline int Nes_Ppu_Impl::first_opaque_sprite_line()
{
// advance earliest time if sprite has blank lines at beginning
uint8_t const* p = map_chr( sprite_tile_index( spr_ram ) * 16 );
int twice = w2000 >> 5 & 1; // loop twice if double height is set
int line = 0;
do
{
for ( int n = 8; n--; p++ )
{
if ( p [0] | p [8] )
return line;
line++;
}
p += 8;
}
while ( !--twice );
return line;
}
void Nes_Ppu::update_sprite_hit( nes_time_t cpu_time )
{
ppu_time_t earliest = earliest_sprite_hit + spr_ram [0] * scanline_len + spr_ram [3];
//ppu_time_t latest = earliest + sprite_height() * scanline_len;
earliest += first_opaque_sprite_line() * scanline_len;
ppu_time_t time = ppu_time( cpu_time );
next_sprite_hit_check = indefinite_time;
if ( false )
if ( earliest < time )
{
r2002 |= 0x40;
return;
}
if ( time < earliest )
{
next_sprite_hit_check = nes_time( earliest );
return;
}
// within possible range; render scanline and compare pixels
int count_needed = 2 + (time - earliest_sprite_hit - spr_ram [3]) / scanline_len;
if ( count_needed > 240 )
count_needed = 240;
while ( scanline_count < count_needed )
render_bg_until( max( cpu_time, next_bg_time + 1 ) );
if ( sprite_hit_found < 0 )
return; // sprite won't hit
if ( !sprite_hit_found )
{
// check again next scanline
next_sprite_hit_check = nes_time( earliest_sprite_hit + spr_ram [3] +
(scanline_count - 1) * scanline_len );
}
else
{
// hit found
ppu_time_t hit_time = earliest_sprite_hit + sprite_hit_found - scanline_len;
if ( time < hit_time )
{
next_sprite_hit_check = nes_time( hit_time );
return;
}
//dprintf( "Sprite hit x: %d, y: %d, scanline_count: %d\n",
// sprite_hit_found % 341, sprite_hit_found / 341, scanline_count );
r2002 |= 0x40;
}
}
// $2002
inline void Nes_Ppu::query_until( nes_time_t time )
{
end_vblank();
// sprite hit
if ( time > next_sprite_hit_check )
update_sprite_hit( time );
// sprite max
if ( !fixed_sprite_max_time )
run_sprite_max( time );
else if ( time >= fixed_sprite_max_time )
r2002 |= (w2001 << 1 & 0x20) | (w2001 << 2 & 0x20);
}
int Nes_Ppu::read_2002( nes_time_t time )
{
nes_time_t next = next_status_event;
next_status_event = vbl_end_time;
int extra_clock = extra_clocks ? (extra_clocks - 1) >> 2 & 1 : 0;
if ( time > next && time > vbl_end_time + extra_clock )
{
query_until( time );
next_status_event = next_sprite_hit_check;
nes_time_t const next_max = fixed_sprite_max_time ?
fixed_sprite_max_time : next_sprite_max_run;
if ( next_status_event > next_max )
next_status_event = next_max;
if ( time > earliest_open_bus_decay() )
{
next_status_event = earliest_open_bus_decay();
update_open_bus( time );
}
if ( time > earliest_vbl_end_time )
{
if ( next_status_event > earliest_vbl_end_time )
next_status_event = earliest_vbl_end_time;
run_end_frame( time );
// special vbl behavior when read is just before or at clock when it's set
if ( extra_clocks != 1 )
{
if ( time == frame_length() )
{
nmi_time_ = indefinite_time;
//dprintf( "Suppressed NMI\n" );
}
}
else if ( time == frame_length() - 1 )
{
r2002 &= ~0x80;
frame_ended = true;
nmi_time_ = indefinite_time;
//dprintf( "Suppressed NMI\n" );
}
}
}
emu.set_ppu_2002_time( next_status_event );
int result = r2002;
second_write = false;
r2002 = result & ~0x80;
poke_open_bus( time, result, 0xE0 );
update_open_bus( time );
return ( result & 0xE0 ) | ( open_bus & 0x1F );
}
void Nes_Ppu::dma_sprites( nes_time_t time, void const* in )
{
//dprintf( "%d sprites written\n", time );
render_until( time );
invalidate_sprite_max( time );
memcpy( spr_ram + w2003, in, 0x100 - w2003 );
memcpy( spr_ram, (char*) in + 0x100 - w2003, w2003 );
}
// Read
inline int Nes_Ppu_Impl::read_2007( int addr )
{
int result = r2007;
if ( addr < 0x2000 )
{
r2007 = *map_chr( addr );
}
else
{
r2007 = get_nametable( addr ) [addr & 0x3ff];
if ( addr >= 0x3f00 )
{
return palette [map_palette( addr )] | ( open_bus & 0xC0 );
}
}
return result;
}
int Nes_Ppu::read( unsigned addr, nes_time_t time )
{
switch ( addr & 7 )
{
// status
case 2: // handled inline
return read_2002( time );
// sprite ram
case 4: {
int result = spr_ram [w2003];
if ( (w2003 & 3) == 2 )
result &= 0xe3;
poke_open_bus( time, result, ~0 );
return result;
}
// video ram
case 7: {
render_bg_until( time );
int addr = vram_addr;
int new_addr = addr + addr_inc;
vram_addr = new_addr;
if ( ~addr & new_addr & vaddr_clock_mask )
{
emu.mapper->a12_clocked();
addr = vram_addr - addr_inc; // avoid having to save across func call
}
int result = read_2007( addr & 0x3fff );
poke_open_bus( time, result, ( ( addr & 0x3fff ) >= 0x3f00 ) ? 0x3F : ~0 );
return result;
}
default:
/* Read from unimplemented PPU register */
break;
}
update_open_bus( time );
return open_bus;
}
// Write
void Nes_Ppu::write( nes_time_t time, unsigned addr, int data )
{
switch ( addr & 7 )
{
case 0:{// control
int changed = w2000 ^ data;
if ( changed & 0x28 )
render_until( time ); // obj height or pattern addr changed
else if ( changed & 0x10 )
render_bg_until( time ); // bg pattern addr changed
else if ( ((data << 10) ^ vram_temp) & 0x0C00 )
render_bg_until( time ); // nametable changed
if ( changed & 0x80 )
{
if ( time > vbl_end_time + ((extra_clocks - 1) >> 2 & 1) )
end_vblank(); // to do: clean this up
if ( data & 0x80 & r2002 )
{
nmi_time_ = time + 2;
emu.event_changed();
}
if ( time >= earliest_vbl_end_time )
run_end_frame( time - 1 + (extra_clocks & 1) );
}
// nametable select
vram_temp = (vram_temp & ~0x0C00) | ((data & 3) * 0x400);
if ( changed & 0x20 ) // sprite height changed
invalidate_sprite_max( time );
w2000 = data;
addr_inc = data & 4 ? 32 : 1;
break;
}
case 1:{// sprites, bg enable
int changed = w2001 ^ data;
if ( changed & 0xE1 )
{
render_until( time + 1 ); // emphasis/monochrome bits changed
palette_changed = 0x18;
}
if ( changed & 0x14 )
render_until( time + 1 ); // sprite enable/clipping changed
else if ( changed & 0x0A )
render_bg_until( time + 1 ); // bg enable/clipping changed
if ( changed & 0x08 ) // bg enabled changed
emu.mapper->run_until( time );
if ( !(w2001 & 0x18) != !(data & 0x18) )
invalidate_sprite_max( time ); // all rendering just turned on or off
w2001 = data;
if ( changed & 0x08 )
emu.irq_changed();
break;
}
case 3: // spr addr
w2003 = data;
poke_open_bus( time, w2003, ~0 );
break;
case 4:
//dprintf( "%d sprites written\n", time );
if ( time > first_scanline_time / ppu_overclock )
{
render_until( time );
invalidate_sprite_max( time );
}
spr_ram [w2003] = data;
w2003 = (w2003 + 1) & 0xff;
break;
case 5:
render_bg_until( time );
if ( (second_write ^= 1) )
{
pixel_x = data & 7;
vram_temp = (vram_temp & ~0x1f) | (data >> 3);
}
else
{
vram_temp = (vram_temp & ~0x73e0) |
(data << 12 & 0x7000) | (data << 2 & 0x03e0);
}
break;
case 6:
render_bg_until( time );
if ( (second_write ^= 1) )
{
vram_temp = (vram_temp & 0xff) | (data << 8 & 0x3f00);
}
else
{
int changed = ~vram_addr & vram_temp;
vram_addr = vram_temp = (vram_temp & 0xff00) | data;
if ( changed & vaddr_clock_mask )
emu.mapper->a12_clocked();
}
break;
default:
/* Wrote to unimplemented PPU register */
break;
}
poke_open_bus( time, data, ~0 );
}
// Frame begin/end
nes_time_t Nes_Ppu::begin_frame( ppu_time_t timestamp )
{
// current time
int cpu_timestamp = timestamp / ppu_overclock;
extra_clocks = timestamp - cpu_timestamp * ppu_overclock;
// frame end
ppu_time_t const frame_end = max_frame_length - 1 - extra_clocks;
frame_length_ = (frame_end + (ppu_overclock - 1)) / ppu_overclock;
frame_length_extra = frame_length_ * ppu_overclock - frame_end;
// nmi
nmi_time_ = indefinite_time;
if ( w2000 & 0x80 & r2002 )
nmi_time_ = 2 - (extra_clocks >> 1);
// bg rendering
frame_phase = 0;
scanline_count = 0;
hblank_time = first_hblank_time;
scanline_time = first_scanline_time;
next_bg_time = nes_time( t_to_v_time );
// sprite rendering
next_sprites_scanline = 0;
next_sprites_time = 0;
// status register
frame_ended = false;
end_vbl_mask = ~0xE0;
next_status_event = 0;
sprite_hit_found = 0;
next_sprite_hit_check = 0;
next_sprite_max_scanline = 0;
invalidate_sprite_max_();
decay_low += cpu_timestamp;
decay_high += cpu_timestamp;
base::begin_frame();
//dprintf( "cpu_timestamp: %d\n", cpu_timestamp );
return cpu_timestamp;
}
ppu_time_t Nes_Ppu::end_frame( nes_time_t end_time )
{
render_bg_until( end_time );
render_until( end_time );
query_until( end_time );
run_end_frame( end_time );
update_open_bus( end_time );
decay_low -= end_time;
decay_high -= end_time;
// to do: do more PPU RE to get exact behavior
if ( w2001 & 0x08 )
{
unsigned a = vram_addr + 2;
if ( (vram_addr & 0xff) >= 0xfe )
a = (vram_addr ^ 0x400) - 0x1e;
vram_addr = a;
}
if ( w2001 & 0x10 )
w2003 = 0;
suspend_rendering();
return (end_time - frame_length_) * ppu_overclock + frame_length_extra;
}
void Nes_Ppu::poke_open_bus( nes_time_t time, int data, int mask )
{
open_bus = ( open_bus & ~mask ) | ( data & mask );
if ( mask & 0x1F ) decay_low = time + scanline_len * 100 / ppu_overclock;
if ( mask & 0xE0 ) decay_high = time + scanline_len * 100 / ppu_overclock;
}
nes_time_t Nes_Ppu::earliest_open_bus_decay()
{
return ( decay_low < decay_high ) ? decay_low : decay_high;
}