mirror of
https://github.com/holub/mame
synced 2025-06-02 02:49:44 +03:00

machines promoted to WORKING ----- Golden Nugget Casino [David Haywood, Sean Riddle] Sudoku Plug & Play TV Game '6 Intelligent Games' [David Haywood, Sean Riddle] Frogger (MSI Plug & Play, white joystick) [David Haywood, Sean Riddle] 35 in 1 Super Twins [David Haywood, Sean Riddle] Vs. Maxx 17-in-1 [David Haywood, Sean Riddle] made use of the code that was already partially present in the NES PPU code to handle emphasis modes, used to highlight tally bars etc. in 'nes rampart', apply a red tint to 'fds bublbobl' and dim the screen in a number of other games. fixed a number of issues with VT palette modes that was causing black screens in many games, allowing them to be promoted added a timing kludge for interrupts in the NES Frogger titles, until timings are better overall,makes playfield stable here and in a number of other places so that other things can be improved note "Vs. Maxx 17-in-1" was added as NOT WORKING in this same cycle so if this gets merged before freeze they'll need to be moved to the 'new working' section. This is a checkpoint, further code improvements and refactors will come later.
1388 lines
38 KiB
C++
1388 lines
38 KiB
C++
// license:BSD-3-Clause
|
|
// copyright-holders:Ernesto Corvi, Brad Oliver, Fabio Priuli
|
|
/******************************************************************************
|
|
|
|
Nintendo 2C0x PPU emulation.
|
|
|
|
Written by Ernesto Corvi.
|
|
This code is heavily based on Brad Oliver's MESS implementation.
|
|
|
|
2009-04: Changed NES PPU to be a device (Nathan Woods)
|
|
2009-07: Changed NES PPU to use a device memory map (Robert Bohms)
|
|
|
|
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).
|
|
* Rad Racer demonstrates road glitches: it changes horizontal scrolling mid-line.
|
|
|
|
******************************************************************************/
|
|
|
|
#include "emu.h"
|
|
#include "video/ppu2c0x.h"
|
|
|
|
#include "screen.h"
|
|
|
|
//**************************************************************************
|
|
// GLOBAL VARIABLES
|
|
//**************************************************************************
|
|
|
|
// devices
|
|
DEFINE_DEVICE_TYPE(PPU_2C02, ppu2c02_device, "ppu2c02", "2C02 PPU")
|
|
DEFINE_DEVICE_TYPE(PPU_2C03B, ppu2c03b_device, "ppu2c03b", "2C03B PPC")
|
|
DEFINE_DEVICE_TYPE(PPU_2C04, ppu2c04_device, "ppu2c04", "2C04 PPU")
|
|
DEFINE_DEVICE_TYPE(PPU_2C07, ppu2c07_device, "ppu2c07", "2C07 PPU")
|
|
DEFINE_DEVICE_TYPE(PPU_PALC, ppupalc_device, "ppupalc", "Generic PAL Clone PPU")
|
|
DEFINE_DEVICE_TYPE(PPU_2C05_01, ppu2c05_01_device, "ppu2c05_01", "2C05_01 PPU")
|
|
DEFINE_DEVICE_TYPE(PPU_2C05_02, ppu2c05_02_device, "ppu2c05_02", "2C05_02 PPU")
|
|
DEFINE_DEVICE_TYPE(PPU_2C05_03, ppu2c05_03_device, "ppu2c05_03", "2C05_03 PPU")
|
|
DEFINE_DEVICE_TYPE(PPU_2C05_04, ppu2c05_04_device, "ppu2c05_04", "2C05_04 PPU")
|
|
|
|
|
|
// default address map
|
|
void ppu2c0x_device::ppu2c0x(address_map& map)
|
|
{
|
|
if (!has_configured_map(0))
|
|
{
|
|
map(0x0000, 0x3eff).ram();
|
|
map(0x3f00, 0x3fff).rw(FUNC(ppu2c0x_device::palette_read), FUNC(ppu2c0x_device::palette_write));
|
|
//map(0x0000, 0x3fff).ram();
|
|
}
|
|
}
|
|
|
|
//-------------------------------------------------
|
|
// memory_space_config - return a description of
|
|
// any address spaces owned by this device
|
|
//-------------------------------------------------
|
|
|
|
device_memory_interface::space_config_vector ppu2c0x_device::memory_space_config() const
|
|
{
|
|
return space_config_vector
|
|
{
|
|
std::make_pair(0, &m_space_config)
|
|
};
|
|
}
|
|
|
|
|
|
//-------------------------------------------------
|
|
// ppu2c0x_device - constructor
|
|
//-------------------------------------------------
|
|
|
|
void ppu2c0x_device::device_config_complete()
|
|
{
|
|
/* reset the callbacks */
|
|
m_scanline_callback_proc.set(nullptr);
|
|
m_hblank_callback_proc.set(nullptr);
|
|
m_vidaccess_callback_proc.set(nullptr);
|
|
m_latch.set(nullptr);
|
|
}
|
|
|
|
ppu2c0x_device::ppu2c0x_device(const machine_config& mconfig, device_type type, const char* tag, device_t* owner, uint32_t clock, address_map_constructor internal_map) :
|
|
device_t(mconfig, type, tag, owner, clock),
|
|
device_memory_interface(mconfig, *this),
|
|
device_video_interface(mconfig, *this),
|
|
m_space_config("videoram", ENDIANNESS_LITTLE, 8, 17, 0, internal_map),
|
|
m_cpu(*this, finder_base::DUMMY_TAG),
|
|
m_scanline(0), // reset the scanline count
|
|
m_videoram_addr_mask(0x3fff),
|
|
m_global_refresh_mask(0x7fff),
|
|
m_line_write_increment_large(32),
|
|
m_paletteram_in_ppuspace(false),
|
|
m_tile_page(0),
|
|
m_back_color(0),
|
|
m_refresh_data(0),
|
|
m_x_fine(0),
|
|
m_tilecount(0),
|
|
m_latch(*this),
|
|
m_scanline_callback_proc(*this),
|
|
m_hblank_callback_proc(*this),
|
|
m_vidaccess_callback_proc(*this),
|
|
m_int_callback(*this),
|
|
m_refresh_latch(0),
|
|
m_toggle(0),
|
|
m_add(1),
|
|
m_videomem_addr(0),
|
|
m_data_latch(0),
|
|
m_buffered_data(0),
|
|
m_sprite_page(0),
|
|
m_scan_scale(1), // set the scan scale (this is for dual monitor vertical setups)
|
|
m_draw_phase(0),
|
|
m_use_sprite_write_limitation(true)
|
|
{
|
|
for (auto& elem : m_regs)
|
|
elem = 0;
|
|
|
|
m_scanlines_per_frame = NTSC_SCANLINES_PER_FRAME;
|
|
m_vblank_first_scanline = VBLANK_FIRST_SCANLINE;
|
|
|
|
/* usually, no security value... */
|
|
m_security_value = 0;
|
|
}
|
|
|
|
ppu2c0x_device::ppu2c0x_device(const machine_config& mconfig, device_type type, const char* tag, device_t* owner, uint32_t clock) :
|
|
ppu2c0x_device(mconfig, type, tag, owner, clock, address_map_constructor(FUNC(ppu2c0x_device::ppu2c0x), this))
|
|
{
|
|
m_paletteram_in_ppuspace = true;
|
|
}
|
|
|
|
ppu2c0x_rgb_device::ppu2c0x_rgb_device(const machine_config& mconfig, device_type type, const char* tag, device_t* owner, uint32_t clock) :
|
|
ppu2c0x_device(mconfig, type, tag, owner, clock),
|
|
m_palette_data(*this, "palette", 0xc0)
|
|
{
|
|
}
|
|
|
|
// NTSC NES
|
|
ppu2c02_device::ppu2c02_device(const machine_config& mconfig, const char* tag, device_t* owner, uint32_t clock) :
|
|
ppu2c0x_device(mconfig, PPU_2C02, tag, owner, clock)
|
|
{
|
|
}
|
|
|
|
// Playchoice 10
|
|
ppu2c03b_device::ppu2c03b_device(const machine_config& mconfig, const char* tag, device_t* owner, uint32_t clock) :
|
|
ppu2c0x_rgb_device(mconfig, PPU_2C03B, tag, owner, clock)
|
|
{
|
|
}
|
|
|
|
// Vs. Unisystem
|
|
ppu2c04_device::ppu2c04_device(const machine_config& mconfig, const char* tag, device_t* owner, uint32_t clock) :
|
|
ppu2c0x_rgb_device(mconfig, PPU_2C04, tag, owner, clock)
|
|
{
|
|
}
|
|
|
|
// PAL NES
|
|
ppu2c07_device::ppu2c07_device(const machine_config& mconfig, const char* tag, device_t* owner, uint32_t clock) :
|
|
ppu2c0x_device(mconfig, PPU_2C07, tag, owner, clock)
|
|
{
|
|
m_scanlines_per_frame = PAL_SCANLINES_PER_FRAME;
|
|
}
|
|
|
|
// PAL clones
|
|
ppupalc_device::ppupalc_device(const machine_config& mconfig, const char* tag, device_t* owner, uint32_t clock) :
|
|
ppu2c0x_device(mconfig, PPU_PALC, tag, owner, clock)
|
|
{
|
|
m_scanlines_per_frame = PAL_SCANLINES_PER_FRAME;
|
|
m_vblank_first_scanline = VBLANK_FIRST_SCANLINE_PALC;
|
|
}
|
|
|
|
// The PPU_2C05 variants have different protection value, set at device start, but otherwise are all the same...
|
|
// Vs. Unisystem (Ninja Jajamaru Kun)
|
|
ppu2c05_01_device::ppu2c05_01_device(const machine_config& mconfig, const char* tag, device_t* owner, uint32_t clock) :
|
|
ppu2c0x_rgb_device(mconfig, PPU_2C05_01, tag, owner, clock)
|
|
{
|
|
m_security_value = 0x1b; // game (jajamaru) doesn't seem to ever actually check it
|
|
}
|
|
// Vs. Unisystem (Mighty Bomb Jack)
|
|
ppu2c05_02_device::ppu2c05_02_device(const machine_config& mconfig, const char* tag, device_t* owner, uint32_t clock) :
|
|
ppu2c0x_rgb_device(mconfig, PPU_2C05_02, tag, owner, clock)
|
|
{
|
|
m_security_value = 0x3d;
|
|
}
|
|
// Vs. Unisystem (Gumshoe)
|
|
ppu2c05_03_device::ppu2c05_03_device(const machine_config& mconfig, const char* tag, device_t* owner, uint32_t clock) :
|
|
ppu2c0x_rgb_device(mconfig, PPU_2C05_03, tag, owner, clock)
|
|
{
|
|
m_security_value = 0x1c;
|
|
}
|
|
// Vs. Unisystem (Top Gun)
|
|
ppu2c05_04_device::ppu2c05_04_device(const machine_config& mconfig, const char* tag, device_t* owner, uint32_t clock) :
|
|
ppu2c0x_rgb_device(mconfig, PPU_2C05_04, tag, owner, clock)
|
|
{
|
|
m_security_value = 0x1b;
|
|
}
|
|
|
|
|
|
//-------------------------------------------------
|
|
// device_start - device-specific startup
|
|
//-------------------------------------------------
|
|
|
|
void ppu2c0x_device::start_nopalram()
|
|
{
|
|
// bind our handler
|
|
m_int_callback.resolve_safe();
|
|
|
|
// allocate timers
|
|
m_hblank_timer = timer_alloc(TIMER_HBLANK);
|
|
m_nmi_timer = timer_alloc(TIMER_NMI);
|
|
m_scanline_timer = timer_alloc(TIMER_SCANLINE);
|
|
|
|
/* initialize the scanline handling portion */
|
|
m_scanline_timer->adjust(screen().time_until_pos(1));
|
|
m_hblank_timer->adjust(m_cpu->cycles_to_attotime(260) / 3); // ??? FIXME - hardcoding NTSC, need better calculation
|
|
m_nmi_timer->adjust(attotime::never);
|
|
|
|
/* allocate a screen bitmap, videomem and spriteram, a dirtychar array and the monochromatic colortable */
|
|
m_bitmap = std::make_unique<bitmap_rgb32>(VISIBLE_SCREEN_WIDTH, VISIBLE_SCREEN_HEIGHT);
|
|
m_spriteram = make_unique_clear<uint8_t[]>(SPRITERAM_SIZE);
|
|
|
|
init_palette_tables();
|
|
|
|
// register for state saving
|
|
save_item(NAME(m_scanline));
|
|
save_item(NAME(m_refresh_data));
|
|
save_item(NAME(m_refresh_latch));
|
|
save_item(NAME(m_x_fine));
|
|
save_item(NAME(m_toggle));
|
|
save_item(NAME(m_add));
|
|
save_item(NAME(m_videomem_addr));
|
|
save_item(NAME(m_data_latch));
|
|
save_item(NAME(m_buffered_data));
|
|
save_item(NAME(m_tile_page));
|
|
save_item(NAME(m_sprite_page));
|
|
save_item(NAME(m_back_color));
|
|
save_item(NAME(m_scan_scale));
|
|
save_item(NAME(m_scanlines_per_frame));
|
|
save_item(NAME(m_vblank_first_scanline));
|
|
save_item(NAME(m_regs));
|
|
save_item(NAME(m_draw_phase));
|
|
save_item(NAME(m_tilecount));
|
|
save_pointer(NAME(m_spriteram), SPRITERAM_SIZE);
|
|
|
|
save_item(NAME(*m_bitmap));
|
|
}
|
|
|
|
void ppu2c0x_device::device_start()
|
|
{
|
|
start_nopalram();
|
|
|
|
m_palette_ram.resize(0x20);
|
|
|
|
for (int i = 0; i < 0x20; i++)
|
|
m_palette_ram[i] = 0x00;
|
|
|
|
save_item(NAME(m_palette_ram));
|
|
}
|
|
|
|
//**************************************************************************
|
|
// INLINE HELPERS
|
|
//**************************************************************************
|
|
|
|
//-------------------------------------------------
|
|
// readbyte - read a byte at the given address
|
|
//-------------------------------------------------
|
|
|
|
uint8_t ppu2c0x_device::readbyte(offs_t address)
|
|
{
|
|
return space().read_byte(address);
|
|
}
|
|
|
|
|
|
//-------------------------------------------------
|
|
// writebyte - write a byte at the given address
|
|
//-------------------------------------------------
|
|
|
|
inline void ppu2c0x_device::writebyte(offs_t address, uint8_t data)
|
|
{
|
|
space().write_byte(address, data);
|
|
}
|
|
|
|
|
|
/***************************************************************************
|
|
IMPLEMENTATION
|
|
***************************************************************************/
|
|
|
|
/*************************************
|
|
*
|
|
* PPU Palette Initialization
|
|
*
|
|
*************************************/
|
|
|
|
void ppu2c0x_device::apply_color_emphasis_and_clamp(bool is_pal_or_dendy, int color_emphasis, double& R, double& G, double& B)
|
|
{
|
|
if (is_pal_or_dendy) // PAL machines swap the colour emphasis bits, this means the red/blue highlighting on rampart tally bar doesn't look as good
|
|
{
|
|
color_emphasis = bitswap<3>(color_emphasis, 2, 0, 1);
|
|
}
|
|
|
|
double r_mod = 0.0;
|
|
double g_mod = 0.0;
|
|
double b_mod = 0.0;
|
|
|
|
switch (color_emphasis)
|
|
{
|
|
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;
|
|
}
|
|
|
|
R = R * r_mod;
|
|
G = G * g_mod;
|
|
B = B * b_mod;
|
|
|
|
/* 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;
|
|
}
|
|
|
|
rgb_t ppu2c0x_device::nespal_to_RGB(int color_intensity, int color_num, int color_emphasis, bool is_pal_or_dendy)
|
|
{
|
|
const double tint = 0.22; /* adjust to taste */
|
|
const double hue = 287.0;
|
|
|
|
const double Kr = 0.2989;
|
|
const double Kb = 0.1145;
|
|
const double Ku = 2.029;
|
|
const double Kv = 1.140;
|
|
|
|
static const 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 }
|
|
};
|
|
|
|
double sat;
|
|
double y, u, v;
|
|
double rad;
|
|
|
|
switch (color_num)
|
|
{
|
|
case 0:
|
|
sat = 0; rad = 0;
|
|
y = brightness[0][color_intensity];
|
|
break;
|
|
|
|
case 13:
|
|
sat = 0; rad = 0;
|
|
y = brightness[2][color_intensity];
|
|
break;
|
|
|
|
case 14:
|
|
case 15:
|
|
sat = 0; rad = 0; y = 0;
|
|
break;
|
|
|
|
default:
|
|
sat = tint;
|
|
rad = M_PI * ((color_num * 30 + hue) / 180.0);
|
|
y = brightness[1][color_intensity];
|
|
break;
|
|
}
|
|
|
|
u = sat * cos(rad);
|
|
v = sat * sin(rad);
|
|
|
|
/* Transform to RGB */
|
|
double R = (y + Kv * v) * 255.0;
|
|
double G = (y - (Kb * Ku * u + Kr * Kv * v) / (1 - Kb - Kr)) * 255.0;
|
|
double B = (y + Ku * u) * 255.0;
|
|
|
|
apply_color_emphasis_and_clamp(is_pal_or_dendy, color_emphasis, R, G, B);
|
|
|
|
return rgb_t(floor(R + .5), floor(G + .5), floor(B + .5));
|
|
}
|
|
|
|
void ppu2c0x_device::init_palette_tables()
|
|
{
|
|
bool is_pal = m_scanlines_per_frame != NTSC_SCANLINES_PER_FRAME;
|
|
|
|
/* 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 entry = 0;
|
|
|
|
/* Loop through the emphasis modes (8 total) */
|
|
for (int color_emphasis = 0; color_emphasis < 8; color_emphasis++)
|
|
{
|
|
/* loop through the 4 intensities */
|
|
for (int color_intensity = 0; color_intensity < 4; color_intensity++)
|
|
{
|
|
/* loop through the 16 colors */
|
|
for (int color_num = 0; color_num < 16; color_num++)
|
|
{
|
|
rgb_t col = nespal_to_RGB(color_intensity, color_num, color_emphasis, is_pal);
|
|
|
|
m_nespens[entry] = (uint32_t)col;
|
|
entry++;
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ppu2c0x_rgb_device::init_palette_tables()
|
|
{
|
|
/* Loop through the emphasis modes (8 total) */
|
|
int entry = 0;
|
|
for (int color_emphasis = 0; color_emphasis < 8; color_emphasis++)
|
|
{
|
|
for (int color_num = 0; color_num < 64; color_num++)
|
|
{
|
|
int R = ((color_emphasis & 1) ? 7 : m_palette_data[color_num * 3]);
|
|
int G = ((color_emphasis & 2) ? 7 : m_palette_data[color_num * 3 + 1]);
|
|
int B = ((color_emphasis & 4) ? 7 : m_palette_data[color_num * 3 + 2]);
|
|
|
|
m_nespens[entry] = (pal3bit(R)<<16) | (pal3bit(G)<<8) | pal3bit(B);
|
|
|
|
//set_pen_color(entry++, pal3bit(R), pal3bit(G), pal3bit(B));
|
|
entry++;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*************************************
|
|
*
|
|
* PPU Initialization and Disposal
|
|
*
|
|
*************************************/
|
|
|
|
//-------------------------------------------------
|
|
// device_timer - handle timer events
|
|
//-------------------------------------------------
|
|
|
|
void ppu2c0x_device::device_timer(emu_timer& timer, device_timer_id id, int param, void* ptr)
|
|
{
|
|
int blanked, vblank;
|
|
|
|
switch (id)
|
|
{
|
|
case TIMER_HBLANK:
|
|
blanked = (m_regs[PPU_CONTROL1] & (PPU_CONTROL1_BACKGROUND | PPU_CONTROL1_SPRITES)) == 0;
|
|
vblank = ((m_scanline >= m_vblank_first_scanline - 1) && (m_scanline < m_scanlines_per_frame - 1)) ? 1 : 0;
|
|
|
|
//update_scanline();
|
|
|
|
if (!m_hblank_callback_proc.isnull())
|
|
m_hblank_callback_proc(m_scanline, vblank, blanked);
|
|
|
|
m_hblank_timer->adjust(attotime::never);
|
|
break;
|
|
|
|
case TIMER_NMI:
|
|
// Actually fire the VMI
|
|
m_int_callback(ASSERT_LINE);
|
|
m_int_callback(CLEAR_LINE);
|
|
|
|
m_nmi_timer->adjust(attotime::never);
|
|
break;
|
|
|
|
case TIMER_SCANLINE:
|
|
blanked = (m_regs[PPU_CONTROL1] & (PPU_CONTROL1_BACKGROUND | PPU_CONTROL1_SPRITES)) == 0;
|
|
vblank = ((m_scanline >= m_vblank_first_scanline - 1) && (m_scanline < m_scanlines_per_frame - 1)) ? 1 : 0;
|
|
int next_scanline;
|
|
|
|
/* if a callback is available, call it */
|
|
if (!m_scanline_callback_proc.isnull())
|
|
m_scanline_callback_proc(m_scanline, vblank, blanked);
|
|
|
|
/* update the scanline that just went by */
|
|
update_scanline();
|
|
|
|
/* increment our scanline count */
|
|
m_scanline++;
|
|
|
|
//logerror("starting scanline %d (MAME %d, beam %d)\n", m_scanline, device->screen().vpos(), device->screen().hpos());
|
|
|
|
/* Note: this is called at the _end_ of each scanline */
|
|
if (m_scanline == m_vblank_first_scanline)
|
|
{
|
|
// logerror("vblank starting\n");
|
|
/* We just entered VBLANK */
|
|
m_regs[PPU_STATUS] |= PPU_STATUS_VBLANK;
|
|
|
|
/* If NMI's are set to be triggered, go for it */
|
|
if (m_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.
|
|
m_nmi_timer->adjust(m_cpu->cycles_to_attotime(4));
|
|
}
|
|
}
|
|
|
|
if (m_scanline == m_scanlines_per_frame - 1)
|
|
{
|
|
//logerror("vblank ending\n");
|
|
/* clear the vblank & sprite hit flag */
|
|
m_regs[PPU_STATUS] &= ~(PPU_STATUS_VBLANK | PPU_STATUS_SPRITE0_HIT | PPU_STATUS_8SPRITES);
|
|
}
|
|
|
|
/* see if we rolled */
|
|
else if (m_scanline == m_scanlines_per_frame)
|
|
{
|
|
/* if background or sprites are enabled, copy the ppu address latch */
|
|
if (!blanked)
|
|
m_refresh_data = m_refresh_latch;
|
|
|
|
/* reset the scanline count */
|
|
m_scanline = 0;
|
|
//logerror("sprite 0 x: %d y: %d num: %d\n", m_spriteram[3], m_spriteram[0] + 1, m_spriteram[1]);
|
|
}
|
|
|
|
next_scanline = m_scanline + 1;
|
|
if (next_scanline == m_scanlines_per_frame)
|
|
next_scanline = 0;
|
|
|
|
// Call us back when the hblank starts for this scanline
|
|
m_hblank_timer->adjust(m_cpu->cycles_to_attotime(260) / 3); // ??? FIXME - hardcoding NTSC, need better calculation
|
|
|
|
// trigger again at the start of the next scanline
|
|
m_scanline_timer->adjust(screen().time_until_pos(next_scanline * m_scan_scale));
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
void ppu2c0x_device::read_tile_plane_data(int address, int color)
|
|
{
|
|
m_planebuf[0] = readbyte((address & 0x1fff));
|
|
m_planebuf[1] = readbyte((address + 8) & 0x1fff);
|
|
}
|
|
|
|
void ppu2c0x_device::shift_tile_plane_data(uint8_t& pix)
|
|
{
|
|
pix = ((m_planebuf[0] >> 7) & 1) | (((m_planebuf[1] >> 7) & 1) << 1);
|
|
m_planebuf[0] = m_planebuf[0] << 1;
|
|
m_planebuf[1] = m_planebuf[1] << 1;
|
|
}
|
|
|
|
void ppu2c0x_device::draw_tile_pixel(uint8_t pix, int color, uint32_t back_pen, uint32_t*& dest)
|
|
{
|
|
uint32_t usepen;
|
|
|
|
if (pix)
|
|
{
|
|
uint8_t pen = ((4 * color) + pix) & 0x1f;
|
|
uint16_t palval = m_palette_ram[pen];
|
|
|
|
if (m_regs[PPU_CONTROL1] & PPU_CONTROL1_DISPLAY_MONO)
|
|
palval &= 0x30;
|
|
|
|
// apply colour emphasis
|
|
palval |= ((m_regs[PPU_CONTROL1] & PPU_CONTROL1_COLOR_EMPHASIS) << 1);
|
|
|
|
usepen = m_nespens[palval];
|
|
}
|
|
else
|
|
{
|
|
usepen = m_nespens[back_pen];
|
|
}
|
|
|
|
|
|
|
|
*dest = usepen;
|
|
}
|
|
|
|
void ppu2c0x_device::draw_tile(uint8_t* line_priority, int color_byte, int color_bits, int address, int start_x, uint32_t back_pen, uint32_t*& dest)
|
|
{
|
|
int color = (((color_byte >> color_bits) & 0x03));
|
|
|
|
read_tile_plane_data(address, color);
|
|
|
|
/* render the pixel */
|
|
for (int i = 0; i < 8; i++)
|
|
{
|
|
uint8_t pix;
|
|
shift_tile_plane_data(pix);
|
|
|
|
if ((start_x + i) >= 0 && (start_x + i) < VISIBLE_SCREEN_WIDTH)
|
|
{
|
|
draw_tile_pixel(pix, color, back_pen, dest);
|
|
|
|
// priority marking
|
|
if (pix)
|
|
line_priority[start_x + i] |= 0x02;
|
|
}
|
|
dest++;
|
|
}
|
|
}
|
|
|
|
|
|
// m_refresh_data is important as it is updated during rendering, and overwritten when you write new scroll values
|
|
// making raster effects more complex than on other systems
|
|
// https://retrocomputing.stackexchange.com/questions/1898/how-can-i-create-a-split-scroll-effect-in-an-nes-game
|
|
|
|
void ppu2c0x_device::draw_background(uint8_t* line_priority)
|
|
{
|
|
bitmap_rgb32& bitmap = *m_bitmap;
|
|
|
|
uint16_t palval = m_back_color;
|
|
|
|
/* cache the background pen */
|
|
uint32_t back_pen = palval;
|
|
|
|
/* determine where in the nametable to start drawing from */
|
|
/* based on the current scanline and scroll regs */
|
|
uint8_t scroll_x_coarse = m_refresh_data & 0x001f;
|
|
uint8_t scroll_y_coarse = (m_refresh_data & 0x03e0) >> 5;
|
|
uint16_t nametable = (m_refresh_data & 0x0c00);
|
|
uint8_t scroll_y_fine = (m_refresh_data & 0x7000) >> 12;
|
|
|
|
int x = scroll_x_coarse;
|
|
|
|
/* get the tile index */
|
|
int tile_index = (nametable | 0x2000) + scroll_y_coarse * 32;
|
|
|
|
/* set up dest */
|
|
int start_x = (m_x_fine ^ 0x07) - 7;
|
|
uint32_t* dest = &bitmap.pix32(m_scanline, start_x);
|
|
|
|
m_tilecount = 0;
|
|
|
|
/* draw the 32 or 33 tiles that make up a line */
|
|
while (m_tilecount < 34)
|
|
{
|
|
int color_byte;
|
|
int color_bits;
|
|
int pos;
|
|
int index1;
|
|
int page, page2, address;
|
|
|
|
index1 = tile_index + x;
|
|
|
|
// page2 is the output of the nametable read (this section is the FIRST read per tile!)
|
|
page2 = readbyte(index1);
|
|
|
|
// this is attribute table stuff! (actually read 2 in PPUspeak)!
|
|
/* 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 = readbyte((((page * 0x400) + address) & 0xfff) + 0x2000);
|
|
|
|
/* figure out which bits in the color table to use */
|
|
color_bits = ((index1 & 0x40) >> 4) + (index1 & 0x02);
|
|
|
|
// 27/12/2002
|
|
if (!m_latch.isnull())
|
|
m_latch((m_tile_page << 10) | (page2 << 4));
|
|
|
|
if (start_x < VISIBLE_SCREEN_WIDTH)
|
|
{
|
|
// need to read 0x0000 or 0x1000 + 16*nametable data
|
|
address = ((m_tile_page) ? 0x1000 : 0) + (page2 * 16);
|
|
// plus something that accounts for y
|
|
address += scroll_y_fine;
|
|
|
|
draw_tile(line_priority, color_byte, color_bits, address, start_x, back_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;
|
|
}
|
|
}
|
|
m_tilecount++;
|
|
}
|
|
|
|
/* if the left 8 pixels for the background are off, blank 'em */
|
|
if (!(m_regs[PPU_CONTROL1] & PPU_CONTROL1_BACKGROUND_L8))
|
|
{
|
|
dest = &bitmap.pix32(m_scanline);
|
|
for (int i = 0; i < 8; i++)
|
|
{
|
|
draw_back_pen(dest, back_pen);
|
|
dest++;
|
|
|
|
line_priority[i] ^= 0x02;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ppu2c0x_device::draw_back_pen(uint32_t* dst, int back_pen)
|
|
{
|
|
*dst = m_nespens[back_pen];
|
|
}
|
|
|
|
void ppu2c0x_device::draw_background_pen()
|
|
{
|
|
bitmap_rgb32& bitmap = *m_bitmap;
|
|
|
|
/* setup the color mask and colortable to use */
|
|
uint8_t color_mask = (m_regs[PPU_CONTROL1] & PPU_CONTROL1_DISPLAY_MONO) ? 0x30 : 0x3f;
|
|
uint16_t palval = m_back_color & color_mask;
|
|
|
|
/* cache the background pen */
|
|
uint32_t back_pen = palval;
|
|
|
|
// Fill this scanline with the background pen.
|
|
for (int i = 0; i < bitmap.width(); i++)
|
|
draw_back_pen(&bitmap.pix32(m_scanline, i), back_pen);
|
|
}
|
|
|
|
void ppu2c0x_device::read_sprite_plane_data(int address)
|
|
{
|
|
m_planebuf[0] = readbyte((address + 0) & 0x1fff);
|
|
m_planebuf[1] = readbyte((address + 8) & 0x1fff);
|
|
}
|
|
|
|
void ppu2c0x_device::make_sprite_pixel_data(uint8_t& pixel_data, int flipx)
|
|
{
|
|
if (flipx)
|
|
{
|
|
pixel_data = (m_planebuf[0] & 1) + ((m_planebuf[1] & 1) << 1);
|
|
m_planebuf[0] = m_planebuf[0] >> 1;
|
|
m_planebuf[1] = m_planebuf[1] >> 1;
|
|
}
|
|
else
|
|
{
|
|
pixel_data = ((m_planebuf[0] >> 7) & 1) | (((m_planebuf[1] >> 7) & 1) << 1);
|
|
m_planebuf[0] = m_planebuf[0] << 1;
|
|
m_planebuf[1] = m_planebuf[1] << 1;
|
|
}
|
|
}
|
|
|
|
void ppu2c0x_device::draw_sprite_pixel(int sprite_xpos, int color, int pixel, uint8_t pixel_data, bitmap_rgb32& bitmap)
|
|
{
|
|
uint16_t palval = m_palette_ram[((4 * color) | pixel_data) & 0x1f];
|
|
|
|
if (m_regs[PPU_CONTROL1] & PPU_CONTROL1_DISPLAY_MONO)
|
|
palval &= 0x30;
|
|
|
|
// apply colour emphasis
|
|
palval |= ((m_regs[PPU_CONTROL1] & PPU_CONTROL1_COLOR_EMPHASIS) << 1);
|
|
|
|
uint32_t pix = m_nespens[palval];
|
|
bitmap.pix32(m_scanline, sprite_xpos + pixel) = pix;
|
|
}
|
|
|
|
void ppu2c0x_device::read_extra_sprite_bits(int sprite_index)
|
|
{
|
|
// needed for some clones
|
|
}
|
|
|
|
bool ppu2c0x_device::is_spritepixel_opaque(int pixel_data, int color)
|
|
{
|
|
if (pixel_data)
|
|
return true;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
void ppu2c0x_device::draw_sprite_pixel_low(bitmap_rgb32& bitmap, int pixel_data, int pixel, int sprite_xpos, int color, int sprite_index, uint8_t* line_priority)
|
|
{
|
|
if (is_spritepixel_opaque(pixel_data, color))
|
|
{
|
|
/* has the background (or another sprite) already been drawn here? */
|
|
if ((sprite_xpos + pixel) < VISIBLE_SCREEN_WIDTH)
|
|
{
|
|
if (!line_priority[sprite_xpos + pixel])
|
|
{
|
|
/* no, draw */
|
|
draw_sprite_pixel(sprite_xpos, color, pixel, pixel_data, bitmap);
|
|
}
|
|
/* indicate that a sprite was drawn at this location, even if it's not seen */
|
|
line_priority[sprite_xpos + pixel] |= 0x01;
|
|
}
|
|
}
|
|
|
|
/* set the "sprite 0 hit" flag if appropriate */
|
|
if (sprite_index == 0 && (pixel_data & 0x03) && ((sprite_xpos + pixel) < 255) && (line_priority[sprite_xpos + pixel] & 0x02))
|
|
m_regs[PPU_STATUS] |= PPU_STATUS_SPRITE0_HIT;
|
|
}
|
|
|
|
void ppu2c0x_device::draw_sprite_pixel_high(bitmap_rgb32& bitmap, int pixel_data, int pixel, int sprite_xpos, int color, int sprite_index, uint8_t* line_priority)
|
|
{
|
|
if (is_spritepixel_opaque(pixel_data, color))
|
|
{
|
|
if ((sprite_xpos + pixel) < VISIBLE_SCREEN_WIDTH)
|
|
{
|
|
/* has another sprite been drawn here? */
|
|
if (!(line_priority[sprite_xpos + pixel] & 0x01))
|
|
{
|
|
/* no, draw */
|
|
draw_sprite_pixel(sprite_xpos, color, pixel, pixel_data, bitmap);
|
|
line_priority[sprite_xpos + pixel] |= 0x01;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* set the "sprite 0 hit" flag if appropriate */
|
|
if (sprite_index == 0 && (pixel_data & 0x03) && ((sprite_xpos + pixel) < 255) && (line_priority[sprite_xpos + pixel] & 0x02))
|
|
m_regs[PPU_STATUS] |= PPU_STATUS_SPRITE0_HIT;
|
|
}
|
|
|
|
int ppu2c0x_device::apply_sprite_pattern_page(int index1, int size)
|
|
{
|
|
if (size == 8)
|
|
index1 += ((m_sprite_page == 0) ? 0 : 0x1000);
|
|
|
|
return index1;
|
|
}
|
|
|
|
void ppu2c0x_device::draw_sprites(uint8_t* line_priority)
|
|
{
|
|
bitmap_rgb32& bitmap = *m_bitmap;
|
|
|
|
int sprite_xpos, sprite_ypos, sprite_index;
|
|
int tile, index1;
|
|
int pri;
|
|
|
|
int flipx, flipy, color;
|
|
int size;
|
|
int sprite_count = 0;
|
|
int sprite_line;
|
|
|
|
int first_pixel;
|
|
int pixel;
|
|
|
|
/* determine if the sprites are 8x8 or 8x16 */
|
|
size = (m_regs[PPU_CONTROL0] & PPU_CONTROL0_SPRITE_SIZE) ? 16 : 8;
|
|
|
|
first_pixel = (m_regs[PPU_CONTROL1] & PPU_CONTROL1_SPRITES_L8) ? 0 : 8;
|
|
|
|
for (sprite_index = 0; sprite_index < SPRITERAM_SIZE; sprite_index += 4)
|
|
{
|
|
sprite_ypos = m_spriteram[sprite_index] + 1;
|
|
sprite_xpos = m_spriteram[sprite_index + 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 ((sprite_index == 0) && (sprite_xpos == 254))
|
|
{
|
|
sprite_ypos--;
|
|
/* set the "sprite 0 hit" flag if appropriate */
|
|
if (line_priority[sprite_xpos] & 0x02)
|
|
m_regs[PPU_STATUS] |= PPU_STATUS_SPRITE0_HIT;
|
|
}
|
|
|
|
/* if the sprite isn't visible, skip it */
|
|
if ((sprite_ypos + size <= m_scanline) || (sprite_ypos > m_scanline))
|
|
continue;
|
|
|
|
tile = m_spriteram[sprite_index + 1];
|
|
color = (m_spriteram[sprite_index + 2] & 0x03) + 4;
|
|
pri = m_spriteram[sprite_index + 2] & 0x20;
|
|
flipx = m_spriteram[sprite_index + 2] & 0x40;
|
|
flipy = m_spriteram[sprite_index + 2] & 0x80;
|
|
read_extra_sprite_bits(sprite_index);
|
|
|
|
if (size == 16)
|
|
{
|
|
/* if it's 8x16 and odd-numbered, draw the other half instead */
|
|
if (tile & 0x01)
|
|
{
|
|
tile &= ~0x01;
|
|
tile |= 0x100;
|
|
}
|
|
}
|
|
|
|
if (!m_latch.isnull())
|
|
m_latch((m_sprite_page << 10) | ((tile & 0xff) << 4));
|
|
|
|
/* compute the character's line to draw */
|
|
sprite_line = m_scanline - sprite_ypos;
|
|
|
|
if (flipy)
|
|
sprite_line = (size - 1) - sprite_line;
|
|
|
|
if (size == 16 && sprite_line > 7)
|
|
{
|
|
tile++;
|
|
sprite_line -= 8;
|
|
}
|
|
|
|
index1 = tile * 16;
|
|
|
|
index1 = apply_sprite_pattern_page(index1, size);
|
|
|
|
read_sprite_plane_data(index1 + sprite_line);
|
|
|
|
/* if there are more than 8 sprites on this line, set the flag */
|
|
if (sprite_count == 8)
|
|
{
|
|
m_regs[PPU_STATUS] |= PPU_STATUS_8SPRITES;
|
|
//logerror ("> 8 sprites, scanline: %d\n", m_scanline);
|
|
|
|
/* the real NES only draws up to 8 sprites - the rest should be invisible */
|
|
break;
|
|
}
|
|
|
|
sprite_count++;
|
|
|
|
/* abort drawing if sprites aren't rendered */
|
|
if (!(m_regs[PPU_CONTROL1] & PPU_CONTROL1_SPRITES))
|
|
continue;
|
|
|
|
if (pri)
|
|
{
|
|
/* draw the low-priority sprites */
|
|
for (pixel = 0; pixel < 8; pixel++)
|
|
{
|
|
uint8_t pixel_data;
|
|
make_sprite_pixel_data(pixel_data, flipx);
|
|
|
|
/* is this pixel non-transparent? */
|
|
if (sprite_xpos + pixel >= first_pixel)
|
|
{
|
|
draw_sprite_pixel_low(bitmap, pixel_data, pixel, sprite_xpos, color, sprite_index, line_priority);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* draw the high-priority sprites */
|
|
for (pixel = 0; pixel < 8; pixel++)
|
|
{
|
|
uint8_t pixel_data;
|
|
make_sprite_pixel_data(pixel_data, flipx);
|
|
|
|
/* is this pixel non-transparent? */
|
|
if (sprite_xpos + pixel >= first_pixel)
|
|
{
|
|
draw_sprite_pixel_high(bitmap, pixel_data, pixel, sprite_xpos, color, sprite_index, line_priority);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*************************************
|
|
*
|
|
* Scanline Rendering and Update
|
|
*
|
|
*************************************/
|
|
|
|
void ppu2c0x_device::render_scanline()
|
|
{
|
|
uint8_t line_priority[VISIBLE_SCREEN_WIDTH];
|
|
|
|
/* lets see how long it takes */
|
|
g_profiler.start(PROFILER_USER1);
|
|
|
|
/* clear the line priority for this scanline */
|
|
memset(line_priority, 0, VISIBLE_SCREEN_WIDTH);
|
|
|
|
m_draw_phase = PPU_DRAW_BG;
|
|
|
|
/* see if we need to render the background */
|
|
if (m_regs[PPU_CONTROL1] & PPU_CONTROL1_BACKGROUND)
|
|
{
|
|
draw_background(line_priority);
|
|
}
|
|
else
|
|
{
|
|
draw_background_pen();
|
|
}
|
|
|
|
m_draw_phase = PPU_DRAW_OAM;
|
|
|
|
/* if sprites are on, draw them, but we call always to process them */
|
|
draw_sprites(line_priority);
|
|
|
|
m_draw_phase = PPU_DRAW_BG;
|
|
|
|
/* done updating, whew */
|
|
g_profiler.stop();
|
|
}
|
|
|
|
void ppu2c0x_device::scanline_increment_fine_ycounter()
|
|
{
|
|
/* increment the fine y-scroll */
|
|
m_refresh_data += 0x1000;
|
|
|
|
/* if it's rolled, increment the coarse y-scroll */
|
|
if (m_refresh_data & 0x8000)
|
|
{
|
|
uint16_t tmp;
|
|
tmp = (m_refresh_data & 0x03e0) + 0x20;
|
|
m_refresh_data &= 0x7c1f;
|
|
|
|
/* handle bizarro scrolling rollover at the 30th (not 32nd) vertical tile */
|
|
if (tmp == 0x03c0)
|
|
m_refresh_data ^= 0x0800;
|
|
else
|
|
m_refresh_data |= (tmp & 0x03e0);
|
|
|
|
//logerror("updating refresh_data: %04x\n", m_refresh_data);
|
|
}
|
|
}
|
|
|
|
void ppu2c0x_device::update_visible_enabled_scanline()
|
|
{
|
|
if (m_scanline_timer->remaining() == attotime::zero)
|
|
{
|
|
/* If background or sprites are enabled, copy the ppu address latch */
|
|
/* Copy only the scroll x-coarse and the x-overflow bit */
|
|
m_refresh_data &= ~0x041f;
|
|
m_refresh_data |= (m_refresh_latch & 0x041f);
|
|
}
|
|
|
|
//logerror("updating refresh_data: %04x (scanline: %d)\n", m_refresh_data, m_scanline);
|
|
render_scanline();
|
|
}
|
|
|
|
void ppu2c0x_device::update_visible_disabled_scanline()
|
|
{
|
|
bitmap_rgb32& bitmap = *m_bitmap;
|
|
uint32_t back_pen;
|
|
|
|
/* setup the color mask and colortable to use */
|
|
uint8_t color_mask = (m_regs[PPU_CONTROL1] & PPU_CONTROL1_DISPLAY_MONO) ? 0x30 : 0x3f;
|
|
|
|
uint16_t palval = m_back_color & color_mask;
|
|
|
|
back_pen = palval;
|
|
|
|
if (m_paletteram_in_ppuspace)
|
|
{
|
|
/* cache the background pen */
|
|
if (m_videomem_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 pen_num = m_palette_ram[(m_videomem_addr & 0x03) ? (m_videomem_addr & 0x1f) : 0];
|
|
|
|
back_pen = pen_num;
|
|
}
|
|
}
|
|
|
|
// Fill this scanline with the background pen.
|
|
for (int i = 0; i < bitmap.width(); i++)
|
|
draw_back_pen(&bitmap.pix32(m_scanline, i), back_pen);
|
|
}
|
|
|
|
void ppu2c0x_device::update_visible_scanline()
|
|
{
|
|
/* Render this scanline if appropriate */
|
|
if (m_regs[PPU_CONTROL1] & (PPU_CONTROL1_BACKGROUND | PPU_CONTROL1_SPRITES))
|
|
{
|
|
update_visible_enabled_scanline();
|
|
}
|
|
else
|
|
{
|
|
update_visible_disabled_scanline();
|
|
}
|
|
|
|
if (m_scanline_timer->remaining() == attotime::zero)
|
|
{
|
|
scanline_increment_fine_ycounter();
|
|
}
|
|
}
|
|
|
|
void ppu2c0x_device::update_scanline()
|
|
{
|
|
if (m_scanline <= BOTTOM_VISIBLE_SCANLINE)
|
|
{
|
|
update_visible_scanline();
|
|
}
|
|
}
|
|
|
|
/*************************************
|
|
*
|
|
* PPU Memory functions
|
|
*
|
|
*************************************/
|
|
|
|
void ppu2c0x_device::palette_write(offs_t offset, uint8_t data)
|
|
{
|
|
// palette RAM is only 6 bits wide
|
|
data &= 0x3f;
|
|
|
|
if (offset & 0x3)
|
|
{
|
|
// regular pens, no mirroring
|
|
m_palette_ram[offset & 0x1f] = data;
|
|
}
|
|
else
|
|
{
|
|
// transparent pens are mirrored!
|
|
if (0 == (offset & 0xf))
|
|
{
|
|
m_back_color = data;
|
|
}
|
|
m_palette_ram[offset & 0xf] = m_palette_ram[(offset & 0xf) + 0x10] = data;
|
|
}
|
|
}
|
|
|
|
uint8_t ppu2c0x_device::palette_read(offs_t offset)
|
|
{
|
|
if (m_regs[PPU_CONTROL1] & PPU_CONTROL1_DISPLAY_MONO)
|
|
return (m_palette_ram[offset & 0x1f] & 0x30);
|
|
|
|
else
|
|
return (m_palette_ram[offset & 0x1f]);
|
|
}
|
|
|
|
/*************************************
|
|
*
|
|
* PPU Registers Read
|
|
*
|
|
*************************************/
|
|
|
|
uint8_t ppu2c0x_device::read(offs_t offset)
|
|
{
|
|
if (offset >= PPU_MAX_REG)
|
|
{
|
|
logerror("PPU %s: Attempting to read past the chip: offset %x\n", this->tag(), offset);
|
|
offset &= PPU_MAX_REG - 1;
|
|
}
|
|
|
|
// see which register to read
|
|
switch (offset & 7)
|
|
{
|
|
case PPU_STATUS: /* 2 */
|
|
// 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, except on the RC2C05 (protection)
|
|
if (m_security_value)
|
|
m_data_latch = (m_regs[PPU_STATUS] & 0xc0) | m_security_value;
|
|
else
|
|
m_data_latch = m_regs[PPU_STATUS] | (m_data_latch & 0x1f);
|
|
|
|
// Reset hi/lo scroll toggle
|
|
m_toggle = 0;
|
|
|
|
// If the vblank bit is set, clear all status bits but the 2 sprite flags
|
|
if (m_data_latch & PPU_STATUS_VBLANK)
|
|
m_regs[PPU_STATUS] &= 0x60;
|
|
break;
|
|
|
|
case PPU_SPRITE_DATA: /* 4 */
|
|
m_data_latch = m_spriteram[m_regs[PPU_SPRITE_ADDRESS]];
|
|
break;
|
|
|
|
case PPU_DATA: /* 7 */
|
|
if (!m_latch.isnull())
|
|
m_latch(m_videomem_addr & 0x3fff);
|
|
|
|
if ((m_videomem_addr >= 0x3f00) && m_paletteram_in_ppuspace)
|
|
{
|
|
m_data_latch = readbyte(m_videomem_addr);
|
|
// buffer the mirrored NT data
|
|
m_buffered_data = readbyte(m_videomem_addr & 0x2fff);
|
|
}
|
|
else
|
|
{
|
|
m_data_latch = m_buffered_data;
|
|
m_buffered_data = readbyte(m_videomem_addr);
|
|
}
|
|
|
|
m_videomem_addr += m_add;
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return m_data_latch;
|
|
}
|
|
|
|
|
|
/*************************************
|
|
*
|
|
* PPU Registers Write
|
|
*
|
|
*************************************/
|
|
|
|
void ppu2c0x_device::write(offs_t offset, uint8_t data)
|
|
{
|
|
if (offset >= PPU_MAX_REG)
|
|
{
|
|
logerror("PPU %s: Attempting to write past the chip: offset %x, data %x\n", this->tag(), offset, data);
|
|
offset &= PPU_MAX_REG - 1;
|
|
}
|
|
|
|
#ifdef MAME_DEBUG
|
|
if (m_scanline <= BOTTOM_VISIBLE_SCANLINE)
|
|
{
|
|
logerror("PPU register %d write %02x during non-vblank scanline %d (MAME %d, beam pos: %d)\n", offset, data, m_scanline, screen().vpos(), screen().hpos());
|
|
}
|
|
#endif
|
|
|
|
/* on the RC2C05, PPU_CONTROL0 and PPU_CONTROL1 are swapped (protection) */
|
|
if ((m_security_value) && !(offset & 6))
|
|
offset ^= 1;
|
|
|
|
switch (offset & 7)
|
|
{
|
|
case PPU_CONTROL0: /* 0 */
|
|
m_regs[PPU_CONTROL0] = data;
|
|
|
|
/* update the name table number on our refresh latches */
|
|
m_refresh_latch &= 0x73ff;
|
|
m_refresh_latch |= (data & 3) << 10;
|
|
|
|
/* the char ram bank points either 0x0000 or 0x1000 (page 0 or page 4) */
|
|
m_tile_page = (data & PPU_CONTROL0_CHR_SELECT) >> 2;
|
|
m_sprite_page = (data & PPU_CONTROL0_SPR_SELECT) >> 1;
|
|
|
|
m_add = (data & PPU_CONTROL0_INC) ? m_line_write_increment_large : 1;
|
|
//logerror("control0 write: %02x (scanline: %d)\n", data, m_scanline);
|
|
break;
|
|
|
|
case PPU_CONTROL1: /* 1 */
|
|
//logerror("control1 write: %02x (scanline: %d)\n", data, m_scanline);
|
|
m_regs[PPU_CONTROL1] = data;
|
|
break;
|
|
|
|
case PPU_SPRITE_ADDRESS: /* 3 */
|
|
m_regs[PPU_SPRITE_ADDRESS] = data;
|
|
break;
|
|
|
|
case PPU_SPRITE_DATA: /* 4 */
|
|
// If the PPU is currently rendering the screen, 0xff is written instead of the desired data.
|
|
if (m_use_sprite_write_limitation)
|
|
if (m_scanline <= BOTTOM_VISIBLE_SCANLINE)
|
|
data = 0xff;
|
|
m_spriteram[m_regs[PPU_SPRITE_ADDRESS]] = data;
|
|
m_regs[PPU_SPRITE_ADDRESS] = (m_regs[PPU_SPRITE_ADDRESS] + 1) & 0xff;
|
|
break;
|
|
|
|
case PPU_SCROLL: /* 5 */
|
|
if (m_toggle)
|
|
{
|
|
/* second write */
|
|
m_refresh_latch &= 0x0c1f;
|
|
m_refresh_latch |= (data & 0xf8) << 2;
|
|
m_refresh_latch |= (data & 0x07) << 12;
|
|
//logerror("scroll write 2: %d, %04x (scanline: %d)\n", data, m_refresh_latch, m_scanline);
|
|
}
|
|
else
|
|
{
|
|
/* first write */
|
|
m_refresh_latch &= (0xffe0 & m_global_refresh_mask);
|
|
m_refresh_latch |= (data & 0xf8) >> 3;
|
|
|
|
m_x_fine = data & 7;
|
|
//logerror("scroll write 1: %d, %04x (scanline: %d)\n", data, m_refresh_latch, m_scanline);
|
|
}
|
|
|
|
m_toggle ^= 1;
|
|
break;
|
|
|
|
case PPU_ADDRESS: /* 6 */
|
|
if (m_toggle)
|
|
{
|
|
/* second write */
|
|
m_refresh_latch &= (0xff00 & m_global_refresh_mask);
|
|
m_refresh_latch |= data;
|
|
m_refresh_data = m_refresh_latch;
|
|
|
|
m_videomem_addr = m_refresh_latch;
|
|
//logerror("vram addr write 2: %02x, %04x (scanline: %d)\n", data, m_refresh_latch, m_scanline);
|
|
}
|
|
else
|
|
{
|
|
/* first write */
|
|
m_refresh_latch &= 0x00ff;
|
|
m_refresh_latch |= (data & (m_videoram_addr_mask >>8) ) << 8;
|
|
//logerror("vram addr write 1: %02x, %04x (scanline: %d)\n", data, m_refresh_latch, m_scanline);
|
|
}
|
|
|
|
m_toggle ^= 1;
|
|
break;
|
|
|
|
case PPU_DATA: /* 7 */
|
|
{
|
|
int tempAddr = m_videomem_addr & m_videoram_addr_mask;
|
|
|
|
if (!m_latch.isnull())
|
|
m_latch(tempAddr);
|
|
|
|
/* if there's a callback, call it now */
|
|
if (!m_vidaccess_callback_proc.isnull())
|
|
data = m_vidaccess_callback_proc(tempAddr, data);
|
|
|
|
/* see if it's on the chargen portion */
|
|
if (tempAddr < 0x2000)
|
|
{
|
|
/* store the data */
|
|
writebyte(tempAddr, data);
|
|
}
|
|
else // this codepath is identical?
|
|
{
|
|
writebyte(tempAddr, data);
|
|
}
|
|
/* increment the address */
|
|
m_videomem_addr += m_add;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
/* ignore other registers writes */
|
|
break;
|
|
}
|
|
|
|
m_data_latch = data;
|
|
}
|
|
|
|
uint16_t ppu2c0x_device::get_vram_dest()
|
|
{
|
|
return m_videomem_addr;
|
|
}
|
|
|
|
void ppu2c0x_device::set_vram_dest(uint16_t dest)
|
|
{
|
|
m_videomem_addr = dest;
|
|
}
|
|
|
|
/*************************************
|
|
*
|
|
* Sprite DMA
|
|
*
|
|
*************************************/
|
|
|
|
void ppu2c0x_device::spriteram_dma(address_space& space, const uint8_t page)
|
|
{
|
|
int i;
|
|
int address = page << 8;
|
|
|
|
for (i = 0; i < SPRITERAM_SIZE; i++)
|
|
{
|
|
uint8_t spriteData = space.read_byte(address + i);
|
|
space.write_byte(0x2004, spriteData);
|
|
}
|
|
|
|
// should last 513 CPU cycles.
|
|
space.device().execute().adjust_icount(-513);
|
|
}
|
|
|
|
/*************************************
|
|
*
|
|
* PPU Rendering
|
|
*
|
|
*************************************/
|
|
|
|
void ppu2c0x_device::render(bitmap_rgb32& bitmap, int flipx, int flipy, int sx, int sy, const rectangle& cliprect)
|
|
{
|
|
if (m_scanline_timer->remaining() != attotime::zero)
|
|
{
|
|
// Partial line update, need to render first (especially for light gun emulation).
|
|
update_scanline();
|
|
}
|
|
copybitmap(bitmap, *m_bitmap, flipx, flipy, sx, sy, cliprect);
|
|
}
|
|
|
|
uint32_t ppu2c0x_device::screen_update(screen_device& screen, bitmap_rgb32& bitmap, const rectangle& cliprect)
|
|
{
|
|
render(bitmap, 0, 0, 0, 0, cliprect);
|
|
return 0;
|
|
}
|