nintendo/vsnes.cpp: Various cleanups. (#10195)

* Merged vsnes_m.cpp and vsnes.h into vsnes.cpp.
* Split into base class and three separate classes for UniSystem, DualSystem, and SMB bootleg machines.
* Merged DualSystem's main/sub VROM banking functions.
* Added a separate speaker for each screen on VS. DualSystem - sound from the two APUs is not mixed.
This commit is contained in:
0kmg 2022-08-08 09:00:05 -08:00 committed by GitHub
parent 9f182125f1
commit 10a34202ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 877 additions and 870 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,157 +0,0 @@
// license:BSD-3-Clause
// copyright-holders:Pierpaolo Prazzoli
#include "bus/nes_ctrl/zapper_sensor.h"
#include "machine/nvram.h"
#include "sound/sn76496.h"
#include "video/ppu2c0x.h"
class vsnes_state : public driver_device
{
public:
vsnes_state(const machine_config &mconfig, device_type type, const char *tag)
: driver_device(mconfig, type, tag)
, m_maincpu(*this, "maincpu")
, m_subcpu(*this, "sub")
, m_ppu1(*this, "ppu1")
, m_ppu2(*this, "ppu2")
, m_sn1(*this, "sn1")
, m_sn2(*this, "sn2")
, m_sensor(*this, "sensor")
, m_nvram(*this, "nvram")
, m_gfx1_rom(*this, "gfx1")
, m_in(*this, "IN%u", 0U)
, m_coins(*this, "COINS%u", 1U)
, m_dsw(*this, "DSW%u", 0U)
, m_gunx(*this, "GUNX")
, m_guny(*this, "GUNY")
, m_prg_banks(*this, "prg%u", 0U)
, m_prg_view(*this, "prg_view")
, m_chr_banks(*this, "chr%u", 0U)
, m_chr_view(*this, "chr_view")
{
}
void vsdual(machine_config &config);
void vsgshoe(machine_config &config);
void vsnes(machine_config &config);
void vsdual_pi(machine_config &config);
void topgun(machine_config &config);
void mightybj(machine_config &config);
void vsnes_bootleg(machine_config &config);
void jajamaru(machine_config &config);
void init_vskonami();
void init_vsvram();
void init_bnglngby();
void init_drmario();
void init_vs108();
void init_vsfdf();
void init_tkoboxng();
void init_vsgun();
void init_supxevs();
void init_vsgshoe();
void init_vsnormal();
void init_platoon();
void init_rbibb();
void init_vsdual();
void init_bootleg();
protected:
virtual void machine_reset() override;
private:
enum { MAIN = 0, SUB = 1 };
required_device<cpu_device> m_maincpu;
optional_device<cpu_device> m_subcpu;
required_device<ppu2c0x_device> m_ppu1;
optional_device<ppu2c0x_device> m_ppu2;
optional_device<sn76489_device> m_sn1;
optional_device<sn76489_device> m_sn2;
optional_device<nes_zapper_sensor_device> m_sensor;
optional_device<nvram_device> m_nvram;
optional_memory_region m_gfx1_rom;
optional_ioport_array<4> m_in;
optional_ioport_array<2> m_coins;
optional_ioport_array<2> m_dsw;
optional_ioport m_gunx;
optional_ioport m_guny;
template <u8 Side> void sprite_dma_w(address_space &space, u8 data);
template <u8 Side> void vsnes_coin_counter_w(offs_t offset, u8 data);
template <u8 Side> u8 vsnes_coin_counter_r(offs_t offset);
template <u8 Side> void vsnes_in0_w(u8 data);
template <u8 Side> u8 vsnes_in0_r();
template <u8 Side> u8 vsnes_in1_r();
void vsnormal_vrom_banking(uint8_t data);
void vskonami_rom_banking(offs_t offset, uint8_t data);
void vsgshoe_gun_in0_w(uint8_t data);
void drmario_rom_banking(offs_t offset, uint8_t data);
void vsvram_rom_banking(uint8_t data);
void vs108_rom_banking(offs_t offset, uint8_t data);
u8 rbibb_prot_r(offs_t offset);
u8 supxevs_prot_1_r();
u8 supxevs_prot_2_r();
u8 supxevs_prot_3_r();
u8 supxevs_prot_4_r();
u8 tkoboxng_prot_r(offs_t offset);
void sunsoft3_rom_banking(offs_t offset, uint8_t data);
void set_bnglngby_irq_w(uint8_t data);
uint8_t set_bnglngby_irq_r();
void vsdual_vrom_banking_main(uint8_t data);
void vsdual_vrom_banking_sub(uint8_t data);
void vssmbbl_sn_w(offs_t offset, uint8_t data);
DECLARE_MACHINE_START(vsnes);
DECLARE_MACHINE_START(vsdual);
DECLARE_MACHINE_START(bootleg);
void v_set_videorom_bank(int start, int count, int vrom_start_bank);
void bootleg_sound_write(offs_t offset, uint8_t data);
uint8_t vsnes_bootleg_z80_data_r();
uint8_t vsnes_bootleg_z80_address_r(offs_t offset);
void vsnes_bootleg_scanline(int scanline, bool vblank, bool blanked);
uint8_t vsnes_bootleg_ppudata();
void vsnes_bootleg_z80_map(address_map &map);
void vsnes_cpu1_bootleg_map(address_map &map);
void vsnes_ppu_bootleg_map(address_map &map);
void vsnes_cpu1_map(address_map &map);
void vsnes_cpu2_map(address_map &map);
void vsnes_ppu1_map(address_map &map);
void vsnes_ppu2_map(address_map &map);
void init_prg_banking();
void prg32(int bank);
void prg16(int slot, int bank);
void prg8(int slot, int bank);
memory_bank_array_creator<4> m_prg_banks;
memory_view m_prg_view;
int m_prg_chunks = 0;
memory_bank_array_creator<8> m_chr_banks;
memory_view m_chr_view;
int m_chr_chunks = 0;
bool m_has_gun = false;
int m_coin = 0;
int m_input_latch[4]{};
int m_input_strobe[2]{};
int m_mmc1_shiftreg = 0;
int m_mmc1_shiftcount = 0;
int m_mmc1_prg16k = 0;
int m_mmc1_switchlow = 0;
int m_mmc1_chr4k = 0;
int m_108_reg = 0;
int m_prot_index = 0;
int m_ret = 0;
u8 m_bootleg_sound_offset = 0U;
u8 m_bootleg_sound_data = 0U;
int m_bootleg_latched_scanline = 0;
};

View File

@ -1,582 +0,0 @@
// license:BSD-3-Clause
// copyright-holders:Brad Oliver, Fabio Priuli
/***************************************************************************
Nintendo VS UniSystem and DualSystem - (c) 1984 Nintendo of America
Portions of this code are heavily based on
Brad Oliver's MESS implementation of the NES.
***************************************************************************/
#include "emu.h"
#include "screen.h"
#include "video/ppu2c0x.h"
#include "vsnes.h"
/* PPU notes */
/* nametable is, per Lord Nightmare, always 4K per PPU */
/* The vsnes system has relatively few banking options for CHR */
/* Each driver will use ROM or RAM for CHR, never both, and RAM is never banked */
/* This leads to the memory system being an optimal place to perform banking */
/*************************************
*
* Init machine
*
*************************************/
void vsnes_state::machine_reset()
{
m_input_latch[0] = m_input_latch[1] = 0;
m_input_latch[2] = m_input_latch[3] = 0;
m_input_strobe[0] = m_input_strobe[1] = 0;
}
/*************************************
*
* Machine start functions
*
*************************************/
void vsnes_state::init_prg_banking()
{
u8 *base = memregion("prg")->base();
m_prg_chunks = memregion("prg")->bytes() / 0x2000;
for (int i = 0; i < 4; i++)
{
m_prg_banks[i]->configure_entries(0, m_prg_chunks, base, 0x2000);
m_prg_banks[i]->set_entry(m_prg_chunks - 4 + i);
}
m_prg_view.select(0);
}
// safe banking helpers (only work when PRG size is a power of 2, i.e. not Gumshoe)
void vsnes_state::prg32(int bank)
{
bank = (bank << 2) & (m_prg_chunks - 1);
for (int i = 0; i < 4; i++)
m_prg_banks[i]->set_entry(bank + i);
}
void vsnes_state::prg16(int slot, int bank)
{
bank = (bank << 1) & (m_prg_chunks - 1);
slot = (slot & 1) << 1;
for (int i = 0; i < 2; i++)
m_prg_banks[slot + i]->set_entry(bank + i);
}
void vsnes_state::prg8(int slot, int bank)
{
m_prg_banks[slot & 0x03]->set_entry(bank & (m_prg_chunks - 1));
}
void vsnes_state::v_set_videorom_bank(int start, int count, int vrom_start_bank)
{
assert(start + count <= 8);
vrom_start_bank &= m_chr_chunks - 1;
assert(vrom_start_bank + count <= m_chr_chunks);
// count determines the size of the area mapped
for (int i = 0; i < count; i++)
m_chr_banks[i + start]->set_entry(vrom_start_bank + i);
}
MACHINE_START_MEMBER(vsnes_state, vsnes)
{
// establish chr banks
// DRIVER_INIT is called first - means we can handle this different for VRAM games!
if (m_gfx1_rom != nullptr)
{
u8 *base = m_gfx1_rom->base();
m_chr_chunks = m_gfx1_rom->bytes() / 0x400;
for (int i = 0; i < 8; i++)
{
m_ppu1->space(AS_PROGRAM).install_read_bank(0x0400 * i, 0x0400 * i + 0x03ff, m_chr_banks[i]);
m_chr_banks[i]->configure_entries(0, m_chr_chunks, base, 0x400);
}
v_set_videorom_bank(0, 8, 0);
}
else
m_chr_view.select(0);
}
MACHINE_START_MEMBER(vsnes_state, vsdual)
{
for (int i = 0; i < 2; i++)
{
const char *region = i ? "gfx2" : "gfx1";
u8 *base = memregion(region)->base();
int entries = memregion(region)->bytes() / 0x2000;
m_chr_banks[i]->configure_entries(0, entries, base, 0x2000);
m_chr_banks[i]->set_entry(0);
}
}
MACHINE_START_MEMBER(vsnes_state, bootleg)
{
u8 *base = m_gfx1_rom->base();
int entries = m_gfx1_rom->bytes() / 0x2000;
m_chr_banks[0]->configure_entries(0, entries, base, 0x2000);
m_chr_banks[0]->set_entry(0);
}
/**********************************************************************************
*
* Game and Board-specific initialization
*
**********************************************************************************/
//**********************************************************************************
// Most games: VROM Banking in controller 0 write
void vsnes_state::vsnormal_vrom_banking(u8 data)
{
// switch vrom
v_set_videorom_bank(0, 8, (data & 4) ? 8 : 0);
// bit 1 ( data & 2 ) enables writes to extra ram, we ignore it
// move along
vsnes_in0_w<MAIN>(data);
}
void vsnes_state::init_vsnormal()
{
// vrom switching is enabled with bit 2 of $4016
m_maincpu->space(AS_PROGRAM).install_write_handler(0x4016, 0x4016, write8smo_delegate(*this, FUNC(vsnes_state::vsnormal_vrom_banking)));
}
//**********************************************************************************
// Gun games: VROM Banking in controller 0 write
void vsnes_state::init_vsgun()
{
init_vsnormal();
m_has_gun = true;
}
//**********************************************************************************
// Konami VRC1 games: ROM banking at $8000-$ffff
void vsnes_state::vskonami_rom_banking(offs_t offset, u8 data)
{
int reg = BIT(offset, 12, 3);
switch (reg)
{
case 0: // prg bank 0
case 2: // prg bank 1
case 4: // prg bank 2
prg8(reg >> 1, data);
break;
case 6: // vrom bank 0
v_set_videorom_bank(0, 4, data * 4);
break;
case 7: // vrom bank 1
v_set_videorom_bank(4, 4, data * 4);
break;
}
}
void vsnes_state::init_vskonami()
{
// point program banks to last 32K
init_prg_banking();
// banking is done with writes to the $8000-$ffff area
m_maincpu->space(AS_PROGRAM).install_write_handler(0x8000, 0xffff, write8sm_delegate(*this, FUNC(vsnes_state::vskonami_rom_banking)));
}
//**********************************************************************************
// Vs. Gumshoe
void vsnes_state::vsgshoe_gun_in0_w(u8 data)
{
// Gumshoe uniquely has a bankable 16K EPROM in addition to the normal unbanked 8K slots
m_prg_banks[0]->set_entry(BIT(data, 2));
// otherwise do normal CHR banking and IO write
vsnormal_vrom_banking(data);
}
void vsnes_state::init_vsgshoe()
{
// point program banks to last 32K
init_prg_banking();
// vrom switching is enabled with bit 2 of $4016
m_maincpu->space(AS_PROGRAM).install_write_handler(0x4016, 0x4016, write8smo_delegate(*this, FUNC(vsnes_state::vsgshoe_gun_in0_w)));
m_has_gun = true;
}
//**********************************************************************************
// MMC1 (Dr Mario): ROM banking at $8000-$ffff
void vsnes_state::drmario_rom_banking(offs_t offset, u8 data)
{
// reset mapper
if (data & 0x80)
{
m_mmc1_shiftcount = 0;
m_mmc1_prg16k = 1;
m_mmc1_switchlow = 1;
m_mmc1_chr4k = 0;
return;
}
// update shift register
m_mmc1_shiftreg = (m_mmc1_shiftreg >> 1) | (data & 1) << 4;
m_mmc1_shiftcount = (m_mmc1_shiftcount + 1) % 5;
// are we done shifting?
if (!m_mmc1_shiftcount)
{
// apply data to registers
switch (BIT(offset, 13, 2))
{
case 0: // mirroring and options
m_mmc1_chr4k = m_mmc1_shiftreg & 0x10;
m_mmc1_prg16k = m_mmc1_shiftreg & 0x08;
m_mmc1_switchlow = m_mmc1_shiftreg & 0x04;
// 0x03: mirroring bits unused on VS
break;
case 1: // video rom banking - bank 0 - 4k or 8k
if (m_mmc1_chr4k)
v_set_videorom_bank(0, 4, m_mmc1_shiftreg * 4);
else
v_set_videorom_bank(0, 8, (m_mmc1_shiftreg & ~1) * 4);
break;
case 2: // video rom banking - bank 1 - 4k only
if (m_mmc1_chr4k)
v_set_videorom_bank(4, 4, m_mmc1_shiftreg * 4);
break;
case 3: // program banking
if (m_mmc1_prg16k)
prg16(!m_mmc1_switchlow, m_mmc1_shiftreg);
else
prg32(m_mmc1_shiftreg >> 1);
break;
}
}
}
void vsnes_state::init_drmario()
{
// point program banks to last 32K
init_prg_banking();
// MMC1 mapper at $8000-$ffff
m_maincpu->space(AS_PROGRAM).install_write_handler(0x8000, 0xffff, write8sm_delegate(*this, FUNC(vsnes_state::drmario_rom_banking)));
m_mmc1_shiftreg = 0;
m_mmc1_shiftcount = 0;
}
//**********************************************************************************
// (UNROM) Games with VRAM instead of graphics ROMs: ROM banking at $8000-$ffff
void vsnes_state::vsvram_rom_banking(u8 data)
{
prg16(0, data);
}
void vsnes_state::init_vsvram()
{
// point program banks to last 32K
init_prg_banking();
// banking is done with writes to the $8000-$ffff area
m_maincpu->space(AS_PROGRAM).install_write_handler(0x8000, 0xffff, write8smo_delegate(*this, FUNC(vsnes_state::vsvram_rom_banking)));
}
//**********************************************************************************
// (Namco) 108 (MMC3 predecessor) games
void vsnes_state::vs108_rom_banking(offs_t offset, u8 data)
{
switch (offset & 0x6001)
{
case 0x0000: // $8000
m_108_reg = data & 0x07;
break;
case 0x0001: // $8001
switch (m_108_reg)
{
case 0: case 1:
v_set_videorom_bank(m_108_reg * 2, 2, data);
break;
case 2: case 3: case 4: case 5:
v_set_videorom_bank(m_108_reg + 2, 1, data);
break;
case 6: case 7:
prg8(m_108_reg - 6, data);
break;
}
break;
default:
logerror("vs108_rom_banking uncaught: %04x value: %02x\n", offset + 0x8000, data);
break;
}
}
// Common init for (Namco) 108 games
void vsnes_state::init_vs108()
{
// point program banks to last 32K
init_prg_banking();
m_108_reg = 0;
// 108 chip at $8000-$9fff
m_maincpu->space(AS_PROGRAM).install_write_handler(0x8000, 0xffff, write8sm_delegate(*this, FUNC(vsnes_state::vs108_rom_banking)));
}
// Vs. RBI Baseball
// rbibb uses a protection chip labeled '127'
// Tests indicate that '127' uses a LFSR with taps: 23, 18, 0
// It is still unknown how bits are then shuffled for readback at 0x5601/0x5e01
u8 vsnes_state::rbibb_prot_r(offs_t offset)
{
static constexpr u8 prot_data[32] = {
0xff, 0xfd, 0xf5, 0xf4, 0xb4, 0xb4, 0xa6, 0x2e,
0x2f, 0x6f, 0x6f, 0x7d, 0xd5, 0xd4, 0x94, 0x94,
0x86, 0x2e, 0x2f, 0x6f, 0x6b, 0x79, 0xd1, 0xd0,
0x92, 0x92, 0x8d, 0x65, 0x64, 0x34, 0xb0, 0xa2
};
if (offset == 0)
{
m_prot_index = 0;
return 0;
}
return prot_data[m_prot_index++];
}
void vsnes_state::init_rbibb()
{
init_vs108();
// RBI Baseball protection, address fully decoded except A11
m_maincpu->space(AS_PROGRAM).install_read_handler(0x5600, 0x5601, read8sm_delegate(*this, FUNC(vsnes_state::rbibb_prot_r)));
m_maincpu->space(AS_PROGRAM).install_read_handler(0x5e00, 0x5e01, read8sm_delegate(*this, FUNC(vsnes_state::rbibb_prot_r)));
}
// Vs. Super Xevious
u8 vsnes_state::supxevs_prot_1_r()
{
m_prot_index ^= 1;
return 0x05;
}
u8 vsnes_state::supxevs_prot_2_r()
{
return m_prot_index ? 0x01 : 0x00;
}
u8 vsnes_state::supxevs_prot_3_r()
{
return m_prot_index ? 0x89 : 0xd1;
}
u8 vsnes_state::supxevs_prot_4_r()
{
return m_prot_index ? 0x37 : 0x3e;
}
void vsnes_state::init_supxevs()
{
init_vs108();
// Vs. Super Xevious Protection
m_maincpu->space(AS_PROGRAM).install_read_handler(0x54ff, 0x54ff, read8smo_delegate(*this, FUNC(vsnes_state::supxevs_prot_1_r)));
m_maincpu->space(AS_PROGRAM).install_read_handler(0x5678, 0x5678, read8smo_delegate(*this, FUNC(vsnes_state::supxevs_prot_2_r)));
m_maincpu->space(AS_PROGRAM).install_read_handler(0x578f, 0x578f, read8smo_delegate(*this, FUNC(vsnes_state::supxevs_prot_3_r)));
m_maincpu->space(AS_PROGRAM).install_read_handler(0x5567, 0x5567, read8smo_delegate(*this, FUNC(vsnes_state::supxevs_prot_4_r)));
m_prot_index = 0;
}
// Vs. TKO Boxing
// tkoboxng uses a protection chip labeled '128'
u8 vsnes_state::tkoboxng_prot_r(offs_t offset)
{
static constexpr u8 prot_data[32] = {
0xff, 0xbf, 0xb7, 0x97, 0x97, 0x17, 0x57, 0x4f,
0x6f, 0x6b, 0xeb, 0xa9, 0xb1, 0x90, 0x94, 0x14,
0x56, 0x4e, 0x6f, 0x6b, 0xeb, 0xa9, 0xb1, 0x90,
0xd4, 0x5c, 0x3e, 0x26, 0x87, 0x83, 0x13, 0x00
};
if (offset == 0)
{
m_prot_index = 0;
return 0;
}
return prot_data[m_prot_index++];
}
void vsnes_state::init_tkoboxng()
{
init_vs108();
// security device at $5e00-$5e01
m_maincpu->space(AS_PROGRAM).install_read_handler(0x5e00, 0x5e01, read8sm_delegate(*this, FUNC(vsnes_state::tkoboxng_prot_r)));
}
// Vs. Freedom Force
void vsnes_state::init_vsfdf()
{
init_vs108();
m_has_gun = true;
}
//**********************************************************************************
// Sunsoft-3 (Platoon) rom banking
void vsnes_state::sunsoft3_rom_banking(offs_t offset, u8 data)
{
switch (offset & 0x7800)
{
case 0x0800:
case 0x1800:
case 0x2800:
case 0x3800:
v_set_videorom_bank((offset >> 11) & 0x06, 2, data * 2);
break;
case 0x7800:
prg16(0, data);
break;
}
}
void vsnes_state::init_platoon()
{
// point program banks to last 32K
init_prg_banking();
m_maincpu->space(AS_PROGRAM).install_write_handler(0x8000, 0xffff, write8sm_delegate(*this, FUNC(vsnes_state::sunsoft3_rom_banking)));
}
//**********************************************************************************
// Vs. Raid on Bungeling Bay (Japan)
// FIXME: this is a bad hack! The unused 8K ROM in the driver supposedly runs on the main CPU (protection?)
// and does some sort of handshake (with IRQs) with the game running on the sub CPU.
void vsnes_state::set_bnglngby_irq_w(uint8_t data)
{
m_ret = data;
m_maincpu->set_input_line(0, (data & 2) ? ASSERT_LINE : CLEAR_LINE);
/* other values ??? */
/* 0, 4, 84 */
}
uint8_t vsnes_state::set_bnglngby_irq_r()
{
return m_ret;
}
void vsnes_state::init_bnglngby()
{
m_maincpu->space(AS_PROGRAM).install_readwrite_handler(0x0231, 0x0231, read8smo_delegate(*this, FUNC(vsnes_state::set_bnglngby_irq_r)), write8smo_delegate(*this, FUNC(vsnes_state::set_bnglngby_irq_w)));
m_ret = 0;
/* normal banking */
init_vsnormal();
}
//**********************************************************************************
// VS Dualsystem
void vsnes_state::vsdual_vrom_banking_main(u8 data)
{
// switch vrom
m_chr_banks[0]->set_entry(BIT(data, 2));
// bit 1 ( data & 2 ) triggers irq on the other cpu
m_subcpu->set_input_line(0, (data & 2) ? CLEAR_LINE : ASSERT_LINE);
// move along
vsnes_in0_w<MAIN>(data);
}
void vsnes_state::vsdual_vrom_banking_sub(u8 data)
{
// switch vrom
m_chr_banks[1]->set_entry(BIT(data, 2));
// bit 1 ( data & 2 ) triggers irq on the other cpu
m_maincpu->set_input_line(0, (data & 2) ? CLEAR_LINE : ASSERT_LINE);
// move along
vsnes_in0_w<SUB>(data);
}
void vsnes_state::init_vsdual()
{
// vrom switching is enabled with bit 2 of $4016
m_maincpu->space(AS_PROGRAM).install_write_handler(0x4016, 0x4016, write8smo_delegate(*this, FUNC(vsnes_state::vsdual_vrom_banking_main)));
m_subcpu->space(AS_PROGRAM).install_write_handler(0x4016, 0x4016, write8smo_delegate(*this, FUNC(vsnes_state::vsdual_vrom_banking_sub)));
}
//**********************************************************************************
// Vs. Super Mario Bros (Bootleg)
void vsnes_state::vsnes_bootleg_scanline(int scanline, bool vblank, bool blanked)
{
// Z80 IRQ is controlled by two factors:
// - bit 6 of current (next) scanline number
// - bit 6 of latched scanline number from Z80 reading $4000
if (!(m_bootleg_latched_scanline & 0x40))
{
m_subcpu->set_input_line(INPUT_LINE_IRQ0, ((scanline + 1) & 0x40) ? ASSERT_LINE : CLEAR_LINE);
}
}
u8 vsnes_state::vsnes_bootleg_ppudata()
{
// CPU always reads higher CHR ROM banks from $2007, PPU always reads lower ones
m_chr_banks[0]->set_entry(1);
u8 data = m_ppu1->read(0x2007);
m_chr_banks[0]->set_entry(0);
return data;
}
void vsnes_state::init_bootleg()
{
m_bootleg_sound_offset = 0;
m_bootleg_sound_data = 0;
m_bootleg_latched_scanline = 0;
m_ppu1->set_scanline_callback(*this, FUNC(vsnes_state::vsnes_bootleg_scanline));
m_maincpu->space(AS_PROGRAM).install_read_handler(0x2007, 0x2007, read8smo_delegate(*this, FUNC(vsnes_state::vsnes_bootleg_ppudata)));
}