Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
1735 lines (1416 sloc) 40.5 KB
////////////////////////////////////////////////////////////////////////////////////////
//
// Nestopia - NES/Famicom emulator written in C++
//
// Copyright (C) 2003-2008 Martin Freij
//
// This file is part of Nestopia.
//
// Nestopia is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// Nestopia 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Nestopia; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////////////
#include <cstring>
#include "NstBoard.hpp"
#include "NstBoardMmc5.hpp"
namespace Nes
{
namespace Core
{
namespace Boards
{
#ifdef NST_MSVC_OPTIMIZE
#pragma optimize("s", on)
#endif
const byte Mmc5::Filler::squared[4] = {0x00,0x55,0xAA,0xFF};
Mmc5::Sound::Sound(Apu& a,bool connect)
: Channel(a)
{
Reset();
bool audible = UpdateSettings();
if (connect)
Connect( audible );
}
Mmc5::Banks::Wrk::Wrk(dword s)
{
enum {X = INVALID};
static const byte access[6][8] =
{
{X,X,X,X,X,X,X,X},
{0,0,0,0,X,X,X,X},
{0,0,0,0,1,1,1,1},
{0,1,2,3,X,X,X,X},
{0,1,2,3,4,4,4,4},
{0,1,2,3,4,5,6,7}
};
std::memcpy( banks, access[s==SIZE_16K ? 2 : s==SIZE_32K ? 3 : s==SIZE_40K ? 4 : s==SIZE_64K ? 5 : s ? 1 : 0], 8 );
}
Mmc5::Banks::Banks(uint wrkSize)
: wrk(wrkSize) {}
Mmc5::Mmc5(const Context& c)
:
Board (c),
banks (board.GetWram()),
sound (*c.apu)
{
nmt.Source(1).Set( Ram::RAM, true, true, sizeof(exRam.mem), exRam.mem );
}
void Mmc5::Flow::Reset()
{
cycles = 0;
phase = &Mmc5::VBlank;
scanline = 240;
}
void Mmc5::Irq::Reset()
{
state = 0;
count = 0U-2U;
target = 0;
}
void Mmc5::Regs::Reset()
{
prgMode = PRG_MODE_8K;
chrMode = CHR_MODE_8K;
exRamMode = EXRAM_MODE_PPU_NT;
mul[0] = 0;
mul[1] = 0;
}
void Mmc5::Banks::Reset()
{
nmt = 0;
for (uint i=0; i < 8; ++i)
chrA[i] = i;
for (uint i=0; i < 4; ++i)
chrB[i] = i;
chrHigh = 0;
lastChr = LAST_CHR_A;
fetchMode = FETCH_MODE_NONE;
security = 0;
for (uint i=0; i < 4; ++i)
prg[i] = 0xFF;
}
void Mmc5::Filler::Reset()
{
tile = 0;
attribute = 0;
}
void Mmc5::Spliter::Reset()
{
ctrl = 0;
tile = 0;
inside = false;
yStart = 0;
chrBank = 0;
x = 0;
y = 0;
}
void Mmc5::ExRam::Reset(bool hard)
{
tile = 0;
if (hard)
std::memset( mem, 0x00, sizeof(mem) );
}
void Mmc5::Sound::Square::Reset()
{
waveLength = 0;
active = false;
frequency = 0;
timer = 0;
step = 0;
duty = 0;
lengthCounter.Reset();
envelope.Reset();
}
void Mmc5::Sound::Pcm::Reset()
{
sample = 0;
enabled = false;
amp = 0;
}
void Mmc5::Sound::Reset()
{
atHalfClock = 0;
for (uint i=0; i < NUM_SQUARES; ++i)
square[i].Reset();
pcm.Reset();
dcBlocker.Reset();
}
void Mmc5::SubReset(const bool hard)
{
cpu.AddHook( Hook(this,&Mmc5::Hook_Cpu) );
ppu.SetHActiveHook( Hook(this,&Mmc5::Hook_HActive) );
ppu.SetHBlankHook( Hook(this,&Mmc5::Hook_HBlank) );
Map( 0x5000U, &Mmc5::Poke_5000 );
Map( 0x5002U, &Mmc5::Poke_5002 );
Map( 0x5003U, &Mmc5::Poke_5003 );
Map( 0x5004U, &Mmc5::Poke_5004 );
Map( 0x5006U, &Mmc5::Poke_5006 );
Map( 0x5007U, &Mmc5::Poke_5007 );
Map( 0x5010U, &Mmc5::Poke_5010 );
Map( 0x5011U, &Mmc5::Poke_5011 );
Map( 0x5015U, &Mmc5::Peek_5015, &Mmc5::Poke_5015 );
Map( 0x5100U, &Mmc5::Poke_5100 );
Map( 0x5101U, &Mmc5::Poke_5101 );
Map( 0x5102U, &Mmc5::Poke_5102 );
Map( 0x5103U, &Mmc5::Poke_5103 );
Map( 0x5104U, &Mmc5::Poke_5104 );
Map( 0x5105U, &Mmc5::Poke_5105 );
Map( 0x5106U, &Mmc5::Poke_5106 );
Map( 0x5107U, &Mmc5::Poke_5107 );
Map( 0x5113U, &Mmc5::Poke_5113 );
Map( 0x5114U, 0x5117U, &Mmc5::Poke_5114 );
Map( 0x5120U, 0x5127U, &Mmc5::Poke_5120 );
Map( 0x5128U, 0x512BU, &Mmc5::Poke_5128 );
Map( 0x5130U, &Mmc5::Poke_5130 );
Map( 0x5200U, &Mmc5::Poke_5200 );
Map( 0x5201U, &Mmc5::Poke_5201 );
Map( 0x5202U, &Mmc5::Poke_5202 );
Map( 0x5203U, &Mmc5::Poke_5203 );
Map( 0x5204U, &Mmc5::Peek_5204, &Mmc5::Poke_5204 );
Map( 0x5205U, &Mmc5::Peek_5205, &Mmc5::Poke_5205 );
Map( 0x5206U, &Mmc5::Peek_5206, &Mmc5::Poke_5206 );
Map( 0x5C00U, 0x5FFFU, &Mmc5::Peek_5C00, &Mmc5::Poke_5C00 );
Map( 0x6000U, 0x7FFFU, &Mmc5::Peek_6000, &Mmc5::Poke_6000 );
Map( 0x8000U, 0x9FFFU, &Mmc5::Peek_8000, &Mmc5::Poke_8000 );
Map( 0xA000U, 0xBFFFU, &Mmc5::Peek_A000, &Mmc5::Poke_A000 );
Map( 0xC000U, 0xDFFFU, &Mmc5::Peek_C000, &Mmc5::Poke_C000 );
p2001 = cpu.Map( 0x2001 );
for (uint i=0x2001; i < 0x4000; i += 0x8)
cpu.Map( i ).Set( this, &Mmc5::Peek_2001, &Mmc5::Poke_2001 );
for (uint i=0; i < 2; ++i)
ciRam[i] = nmt.Source().Mem(SIZE_1K * i);
exRam.Reset( hard );
flow.Reset();
banks.Reset();
regs.Reset();
irq.Reset();
filler.Reset();
spliter.Reset();
UpdatePrg();
UpdateChrA();
UpdateRenderMethod();
}
inline bool Mmc5::Sound::Square::CanOutput() const
{
return lengthCounter.GetCount() && waveLength >= MIN_FRQ;
}
void Mmc5::Sound::Square::UpdateSettings(const uint fixed)
{
active = CanOutput();
frequency = (waveLength + 1UL) * fixed * 2;
}
bool Mmc5::Sound::UpdateSettings()
{
uint volume = GetVolume(EXT_MMC5);
output = IsMuted() ? 0 : volume;
GetOscillatorClock( rate, fixed );
for (uint i=0; i < NUM_SQUARES; ++i)
square[i].UpdateSettings( fixed );
quarterClock = GetCpuClockBase() / (240UL * GetCpuClockDivider() * GetCpuClock()) * GetCpuClock();
dcBlocker.Reset();
return volume;
}
void Mmc5::SubSave(State::Saver& state) const
{
state.Begin( AsciiId<'M','M','5'>::V );
{
const byte data[32] =
{
regs.prgMode | (regs.chrMode << 2) | (regs.exRamMode << 4),
banks.prg[0],
banks.prg[1],
banks.prg[2],
banks.prg[3],
banks.security & (Banks::READABLE_6|Banks::WRITABLE_6|Regs::WRK_WRITABLE_A|Regs::WRK_WRITABLE_B),
banks.nmt,
banks.chrA[0] & 0xFFU,
banks.chrA[1] & 0xFFU,
banks.chrA[2] & 0xFFU,
banks.chrA[3] & 0xFFU,
banks.chrA[4] & 0xFFU,
banks.chrA[5] & 0xFFU,
banks.chrA[6] & 0xFFU,
banks.chrA[7] & 0xFFU,
banks.chrB[0] & 0xFFU,
banks.chrB[1] & 0xFFU,
banks.chrB[2] & 0xFFU,
banks.chrB[3] & 0xFFU,
uint(banks.chrA[0]) >> 8 | uint(banks.chrA[1]) >> 8 << 2 | uint(banks.chrA[2]) >> 8 << 4 | uint(banks.chrA[3]) >> 8 << 6,
uint(banks.chrA[4]) >> 8 | uint(banks.chrA[5]) >> 8 << 2 | uint(banks.chrA[6]) >> 8 << 4 | uint(banks.chrA[7]) >> 8 << 6,
uint(banks.chrB[0]) >> 8 | uint(banks.chrB[1]) >> 8 << 2 | uint(banks.chrB[2]) >> 8 << 4 | uint(banks.chrB[3]) >> 8 << 6,
(banks.chrHigh >> 6) | (banks.lastChr != Banks::LAST_CHR_A ? 0x80 : 0x00),
filler.tile,
(filler.attribute & 0x3) | (spliter.tile >> 2 & 0xF8),
exRam.tile,
spliter.ctrl,
spliter.yStart,
spliter.chrBank >> 12,
spliter.tile & 0x1F,
spliter.x,
spliter.y
};
state.Begin( AsciiId<'R','E','G'>::V ).Write( data ).End();
}
{
const byte data[2] = { regs.mul[0], regs.mul[1] };
state.Begin( AsciiId<'M','U','L'>::V ).Write( data ).End();
}
{
const byte data[2] =
{
irq.state,
irq.target
};
state.Begin( AsciiId<'I','R','Q'>::V ).Write( data ).End();
}
state.Begin( AsciiId<'R','A','M'>::V ).Compress( exRam.mem ).End();
sound.SaveState( state, AsciiId<'S','N','D'>::V );
state.End();
}
void Mmc5::SubLoad(State::Loader& state,const dword baseChunk)
{
NST_VERIFY( baseChunk == (AsciiId<'M','M','5'>::V) );
if (baseChunk == AsciiId<'M','M','5'>::V)
{
while (const dword chunk = state.Begin())
{
switch (chunk)
{
case AsciiId<'R','E','G'>::V:
{
State::Loader::Data<32> data( state );
regs.prgMode = data[0] >> 0 & Regs::PRG_MODE;
regs.chrMode = data[0] >> 2 & Regs::CHR_MODE;
regs.exRamMode = data[0] >> 4 & Regs::EXRAM_MODE;
for (uint i=0; i < 4; ++i)
banks.prg[i] = data[1+i];
banks.security = data[5] & (Banks::READABLE_6|Banks::WRITABLE_6|Regs::WRK_WRITABLE_A|Regs::WRK_WRITABLE_B);
banks.nmt = data[6];
for (uint i=0; i < 8; ++i)
banks.chrA[i] = data[7+i] | (data[19+(i/4)] & Regs::CHR_HIGH) << 8;
for (uint i=0; i < 4; ++i)
banks.chrB[i] = data[15+i] | (data[21+(i/4)] & Regs::CHR_HIGH) << 8;
banks.chrHigh = (data[22] & Regs::CHR_HIGH) << 6;
banks.lastChr = (data[22] & 0x80) ? Banks::LAST_CHR_B : Banks::LAST_CHR_A;
filler.tile = data[23];
filler.attribute = Filler::squared[data[24] & 0x3];
exRam.tile = data[25];
spliter.ctrl = data[26];
spliter.yStart = NST_MIN(data[27],239);
spliter.chrBank = data[28] << 12;
spliter.tile = (data[29] & 0x1F) | (data[24] << 2 & 0x3E0);
spliter.x = data[30] & 0x1F;
spliter.y = NST_MIN(data[31],239);
UpdatePrg();
if (banks.lastChr == Banks::LAST_CHR_A)
UpdateChrA();
else
UpdateChrB();
UpdateRenderMethod();
break;
}
case AsciiId<'M','U','L'>::V:
{
State::Loader::Data<2> data( state );
regs.mul[0] = data[0];
regs.mul[1] = data[1];
break;
}
case AsciiId<'I','R','Q'>::V:
{
State::Loader::Data<2> data( state );
NST_VERIFY( !(data[0] & Irq::FRAME) );
irq.state = data[0] & (Irq::HIT|Irq::ENABLED);
irq.target = data[1];
break;
}
case AsciiId<'R','A','M'>::V:
state.Uncompress( exRam.mem );
break;
case AsciiId<'S','N','D'>::V:
sound.LoadState( state );
break;
}
state.End();
}
}
}
void Mmc5::Sound::SaveState(State::Saver& state,const dword baseChunk) const
{
state.Begin( baseChunk );
state.Begin( AsciiId<'C','L','K'>::V ).Write8( atHalfClock ).End();
square[0].SaveState( state, AsciiId<'S','Q','0'>::V );
square[1].SaveState( state, AsciiId<'S','Q','1'>::V );
pcm.SaveState( state, AsciiId<'P','C','M'>::V );
state.End();
}
void Mmc5::Sound::LoadState(State::Loader& state)
{
while (const dword chunk = state.Begin())
{
switch (chunk)
{
case AsciiId<'C','L','K'>::V:
atHalfClock = state.Read8() & 0x1;
break;
case AsciiId<'S','Q','0'>::V:
square[0].LoadState( state, fixed );
break;
case AsciiId<'S','Q','1'>::V:
square[1].LoadState( state, fixed );
break;
case AsciiId<'P','C','M'>::V:
pcm.LoadState( state );
break;
}
state.End();
}
}
void Mmc5::Sound::Square::SaveState(State::Saver& state,const dword chunk) const
{
state.Begin( chunk );
{
const byte data[3] =
{
waveLength & 0xFF,
waveLength >> 8,
duty
};
state.Begin( AsciiId<'R','E','G'>::V ).Write( data ).End();
}
lengthCounter.SaveState( state, AsciiId<'L','E','N'>::V );
envelope.SaveState( state, AsciiId<'E','N','V'>::V );
state.End();
}
void Mmc5::Sound::Square::LoadState(State::Loader& state,const dword fixed)
{
while (const dword chunk = state.Begin())
{
switch (chunk)
{
case AsciiId<'R','E','G'>::V:
waveLength = state.Read16() & 0x7FF;
duty = state.Read8() & 0x3;
break;
case AsciiId<'L','E','N'>::V:
lengthCounter.LoadState( state );
break;
case AsciiId<'E','N','V'>::V:
envelope.LoadState( state );
break;
}
state.End();
}
step = 0;
timer = 0;
frequency = (waveLength + 1UL) * fixed * 2;
active = CanOutput();
}
void Mmc5::Sound::Pcm::SaveState(State::Saver& state,const dword chunk) const
{
state.Begin( chunk ).Write16( (enabled != 0) | dword(amp / VOLUME) << 8 ).End();
}
void Mmc5::Sound::Pcm::LoadState(State::Loader& state)
{
const uint data = state.Read16();
enabled = data & 0x1;
amp = (data >> 8) * VOLUME;
sample = enabled ? amp : 0;
}
#ifdef NST_MSVC_OPTIMIZE
#pragma optimize("", on)
#endif
inline ibool Mmc5::IsPpuSprite8x16() const
{
return ppu.GetCtrl(0) & Regs::PPU_CTRL0_SP8X16;
}
void Mmc5::VBlank()
{
NST_ASSERT( flow.cycles == 0 );
flow.cycles = ppu.GetHVIntClock();
if (flow.cycles <= cpu.GetCycles())
HDummy();
else
flow.phase = &Mmc5::HDummy;
}
void Mmc5::HDummy()
{
if (ppu.IsEnabled())
irq.count++;
flow.cycles += ppu.GetHSyncClock() - (ppu.IsShortFrame() ? ppu.GetClock() : 0);
if (flow.cycles <= cpu.GetCycles())
HActive0();
else
flow.phase = &Mmc5::HActive0;
}
void Mmc5::HActive0()
{
if (ppu.IsEnabled())
{
irq.count++;
irq.state = (irq.state & Irq::ENABLED) | Irq::FRAME;
cpu.ClearIRQ();
}
flow.cycles += ppu.GetHSyncClock();
flow.scanline = 0;
if (flow.cycles <= cpu.GetCycles())
HActiveX();
else
flow.phase = &Mmc5::HActiveX;
}
void Mmc5::HActiveX()
{
for (;;)
{
flow.scanline++;
if (ppu.IsEnabled())
{
if (++irq.count == irq.target && irq.target)
irq.state |= Irq::HIT;
if ((irq.state & Irq::SIGNAL_HIT) == Irq::SIGNAL_HIT)
cpu.DoIRQ( Cpu::IRQ_EXT, flow.cycles );
}
flow.cycles += ppu.GetHSyncClock();
if (flow.scanline < 240)
{
if (flow.cycles > cpu.GetCycles())
break;
}
else
{
irq.count = 0U-2U;
flow.cycles = Cpu::CYCLE_MAX;
irq.state &= (Irq::ENABLED|Irq::HIT);
ppu.Update();
banks.fetchMode = Banks::FETCH_MODE_NONE;
spliter.inside = false;
if (banks.lastChr == Banks::LAST_CHR_A)
UpdateChrA();
else
UpdateChrB();
break;
}
}
}
inline void Mmc5::Update()
{
if (flow.cycles <= cpu.GetCycles())
(*this.*flow.phase)();
}
void Mmc5::Sync(Event event,Input::Controllers* controllers)
{
if (event == EVENT_END_FRAME)
{
Update();
flow.cycles = 0;
flow.phase = &Mmc5::VBlank;
}
Board::Sync( event, controllers );
}
NES_HOOK(Mmc5,Cpu)
{
Update();
}
NES_HOOK(Mmc5,HActive)
{
banks.fetchMode = Banks::FETCH_MODE_BG;
if (ppu.IsEnabled())
{
spliter.x = 0x1F;
if (ppu.GetPixelCycles() != ~0U)
{
if (spliter.y < 239)
spliter.y++;
else
spliter.y = 0;
}
else
{
spliter.y = spliter.yStart;
}
if (banks.lastChr != Banks::LAST_CHR_A || IsPpuSprite8x16())
UpdateChrB();
else
UpdateChrA();
}
}
NES_HOOK(Mmc5,HBlank)
{
banks.fetchMode = Banks::FETCH_MODE_SP;
spliter.inside = false;
if (ppu.IsEnabled())
{
if (banks.lastChr == Banks::LAST_CHR_A || IsPpuSprite8x16())
UpdateChrA();
else
UpdateChrB();
}
}
inline uint Mmc5::Banks::Wrk::operator [] (uint i) const
{
NST_ASSERT( i < 8 );
return banks[i];
}
template<uint ADDRESS>
void Mmc5::SwapPrg8Ex(uint bank)
{
enum
{
ROM = uint(Banks::READABLE_8) << (ADDRESS / SIZE_8K),
RAM = uint(Banks::WRITABLE_8) << (ADDRESS / SIZE_8K) | ROM
};
// GCC goes banana without the explicit cast
if (bank & Regs::PRG_ROM_SELECT)
{
banks.security = (banks.security & ~uint(RAM)) | ROM;
static_cast<Prg::SourceProxy>(prg.Source(0)).SwapBank<SIZE_8K,ADDRESS>( bank & Regs::PRG_ROM_BANK );
}
else if (Banks::Wrk::INVALID != (bank = banks.wrk[bank & Regs::PRG_RAM_BANK]))
{
banks.security |= RAM;
static_cast<Prg::SourceProxy>(prg.Source(1)).SwapBank<SIZE_8K,ADDRESS>( bank );
}
else
{
banks.security &= ~uint(RAM);
NST_DEBUG_MSG("MMC5 open bus PRG bankswitch!");
}
}
void Mmc5::UpdatePrg()
{
enum
{
ROM_8_A_C = Banks::READABLE_8|Banks::READABLE_A|Banks::READABLE_C,
ROM_C = Banks::READABLE_C,
RAM_8_A_C = Banks::WRITABLE_8|Banks::WRITABLE_A|Banks::WRITABLE_C|ROM_8_A_C,
RAM_C = Banks::WRITABLE_C|ROM_C
};
switch (regs.prgMode & Regs::PRG_MODE)
{
case Regs::PRG_MODE_32K:
banks.security = (banks.security & ~uint(RAM_8_A_C)) | ROM_8_A_C;
prg.SwapBank<SIZE_32K,0x0000>( banks.prg[3] >> 2 );
break;
case Regs::PRG_MODE_16K:
banks.security = (banks.security & ~uint(RAM_C)) | ROM_C;
SwapPrg8Ex<0x0000>( banks.prg[1] & 0xFEU );
SwapPrg8Ex<0x2000>( banks.prg[1] | 0x01U );
prg.SwapBank<SIZE_16K,0x4000>( banks.prg[3] >> 1 );
break;
case Regs::PRG_MODE_16K_8K:
SwapPrg8Ex<0x0000>( banks.prg[1] & 0xFEU );
SwapPrg8Ex<0x2000>( banks.prg[1] | 0x01U );
SwapPrg8Ex<0x4000>( banks.prg[2] );
prg.SwapBank<SIZE_8K,0x6000>( banks.prg[3] );
break;
case Regs::PRG_MODE_8K:
SwapPrg8Ex<0x0000>( banks.prg[0] );
SwapPrg8Ex<0x2000>( banks.prg[1] );
SwapPrg8Ex<0x4000>( banks.prg[2] );
prg.SwapBank<SIZE_8K,0x6000>( banks.prg[3] );
break;
}
}
void Mmc5::UpdateChrA() const
{
switch (regs.chrMode)
{
case Regs::CHR_MODE_8K:
chr.SwapBank<SIZE_8K,0x0000>( banks.chrA[7] );
break;
case Regs::CHR_MODE_4K:
chr.SwapBanks<SIZE_4K,0x0000>( banks.chrA[3], banks.chrA[7] );
break;
case Regs::CHR_MODE_2K:
chr.SwapBanks<SIZE_2K,0x0000>( banks.chrA[1], banks.chrA[3], banks.chrA[5], banks.chrA[7] );
break;
case Regs::CHR_MODE_1K:
chr.SwapBanks<SIZE_1K,0x0000>( banks.chrA[0], banks.chrA[1], banks.chrA[2], banks.chrA[3], banks.chrA[4], banks.chrA[5], banks.chrA[6], banks.chrA[7] );
break;
}
}
void Mmc5::UpdateChrB() const
{
switch (regs.chrMode)
{
case Regs::CHR_MODE_8K:
chr.SwapBank<SIZE_8K,0x0000>( banks.chrB[3] );
break;
case Regs::CHR_MODE_4K:
chr.SwapBanks<SIZE_4K,0x0000>( banks.chrB[3], banks.chrB[3] );
break;
case Regs::CHR_MODE_2K:
chr.SwapBanks<SIZE_2K,0x0000>( banks.chrB[1], banks.chrB[3], banks.chrB[1], banks.chrB[3] );
break;
case Regs::CHR_MODE_1K:
chr.SwapBanks<SIZE_1K,0x0000>( banks.chrB[0], banks.chrB[1], banks.chrB[2], banks.chrB[3], banks.chrB[0], banks.chrB[1], banks.chrB[2], banks.chrB[3] );
break;
}
}
bool Mmc5::ClockSpliter()
{
NST_ASSERT( spliter.ctrl & Spliter::CTRL_ENABLED );
if (banks.fetchMode == Banks::FETCH_MODE_BG)
{
spliter.x = (spliter.x + 1) & 0x1F;
if
(
(spliter.ctrl & Spliter::CTRL_RIGHT_SIDE) ? (spliter.x >= (spliter.ctrl & Spliter::CTRL_START)) :
(spliter.x < (spliter.ctrl & Spliter::CTRL_START))
)
{
spliter.tile = ((spliter.y & 0xF8) << 2) | spliter.x;
spliter.inside = true;
return true;
}
spliter.inside = false;
}
return false;
}
template<>
NST_FORCE_INLINE uint Mmc5::FetchByte<Mmc5::NT_CIRAM_0>(uint address) const
{
return ciRam[0][address];
}
template<>
NST_FORCE_INLINE uint Mmc5::FetchByte<Mmc5::NT_CIRAM_1>(uint address) const
{
return ciRam[1][address];
}
template<>
NST_FORCE_INLINE uint Mmc5::FetchByte<Mmc5::NT_EXRAM>(uint address) const
{
return exRam.mem[address];
}
template<>
NST_FORCE_INLINE uint Mmc5::FetchByte<Mmc5::NT_FILL>(uint address) const
{
return (address & 0x3FF) < 0x3C0 ? filler.tile : filler.attribute;
}
template<>
NST_FORCE_INLINE uint Mmc5::FetchByte<Mmc5::NT_ZERO>(uint) const
{
return 0;
}
template<Mmc5::FetchType NT>
NST_FORCE_INLINE uint Mmc5::FetchNt(uint address) const
{
return FetchByte<NT>( address );
}
template<Mmc5::FetchType NT>
NST_FORCE_INLINE uint Mmc5::FetchNtExt(uint address)
{
if ((address & 0x3FF) < 0x3C0)
{
exRam.tile = exRam.mem[address];
return FetchByte<NT>( address );
}
else
{
return Filler::squared[exRam.tile >> 6];
}
}
template<Mmc5::FetchType NT>
NST_FORCE_INLINE uint Mmc5::FetchNtSplit(uint address)
{
if ((address & 0x3FF) < 0x3C0)
{
if (ClockSpliter())
return exRam.mem[spliter.tile];
else
return FetchByte<NT>( address );
}
else
{
if (spliter.inside)
return GetSpliterAttribute();
else
return FetchByte<NT>( address );
}
}
template<Mmc5::FetchType NT>
NST_FORCE_INLINE uint Mmc5::FetchNtExtSplit(uint address)
{
if ((address & 0x3FF) < 0x3C0)
{
if (ClockSpliter())
{
return exRam.mem[spliter.tile];
}
else
{
exRam.tile = exRam.mem[address];
return FetchByte<NT>( address );
}
}
else
{
if (spliter.inside)
return GetSpliterAttribute();
else
return Filler::squared[exRam.tile >> 6];
}
}
uint Mmc5::GetSpliterAttribute() const
{
return Filler::squared[(exRam.mem[0x3C0 | (spliter.tile >> 4 & 0x38) | (spliter.tile >> 2 & 0x7)] >> ((spliter.tile >> 4 & 0x4) | (spliter.tile & 0x2))) & 0x3];
}
NES_ACCESSOR( Mmc5, Nt_CiRam_0 ) { return FetchNt < NT_CIRAM_0 >( address ); }
NES_ACCESSOR( Mmc5, Nt_CiRam_1 ) { return FetchNt < NT_CIRAM_1 >( address ); }
NES_ACCESSOR( Mmc5, Nt_ExRam ) { return FetchNt < NT_EXRAM >( address ); }
NES_ACCESSOR( Mmc5, Nt_Fill ) { return FetchNt < NT_FILL >( address ); }
NES_ACCESSOR( Mmc5, Nt_Zero ) { return FetchNt < NT_ZERO >( address ); }
NES_ACCESSOR( Mmc5, NtExt_CiRam_0 ) { return FetchNtExt < NT_CIRAM_0 >( address ); }
NES_ACCESSOR( Mmc5, NtExt_CiRam_1 ) { return FetchNtExt < NT_CIRAM_1 >( address ); }
NES_ACCESSOR( Mmc5, NtExt_ExRam ) { return FetchNtExt < NT_EXRAM >( address ); }
NES_ACCESSOR( Mmc5, NtExt_Fill ) { return FetchNtExt < NT_FILL >( address ); }
NES_ACCESSOR( Mmc5, NtSplit_CiRam_0 ) { return FetchNtSplit < NT_CIRAM_0 >( address ); }
NES_ACCESSOR( Mmc5, NtSplit_CiRam_1 ) { return FetchNtSplit < NT_CIRAM_1 >( address ); }
NES_ACCESSOR( Mmc5, NtSplit_ExRam ) { return FetchNtSplit < NT_EXRAM >( address ); }
NES_ACCESSOR( Mmc5, NtSplit_Fill ) { return FetchNtSplit < NT_FILL >( address ); }
NES_ACCESSOR( Mmc5, NtExtSplit_CiRam_0 ) { return FetchNtExtSplit < NT_CIRAM_0 >( address ); }
NES_ACCESSOR( Mmc5, NtExtSplit_CiRam_1 ) { return FetchNtExtSplit < NT_CIRAM_1 >( address ); }
NES_ACCESSOR( Mmc5, NtExtSplit_ExRam ) { return FetchNtExtSplit < NT_EXRAM >( address ); }
NES_ACCESSOR( Mmc5, NtExtSplit_Fill ) { return FetchNtExtSplit < NT_FILL >( address ); }
NES_ACCESSOR(Mmc5,CRom)
{
return chr.Peek( address );
}
uint Mmc5::GetSpliterPattern(uint address) const
{
return *chr.Source().Mem( spliter.chrBank + (address & 0xFFF) );
}
uint Mmc5::GetExtPattern(uint address) const
{
return *chr.Source().Mem( (((exRam.tile & Regs::EXRAM_EXT_CHR_BANK) + banks.chrHigh) << 12) + (address & 0xFFF) );
}
NES_ACCESSOR(Mmc5,CRomExt)
{
if (banks.fetchMode == Banks::FETCH_MODE_BG)
return GetExtPattern( address );
else
return chr.Peek( address );
}
NES_ACCESSOR(Mmc5,CRomSplit)
{
if (spliter.inside)
return GetSpliterPattern( address );
else
return chr.Peek( address );
}
NES_ACCESSOR(Mmc5,CRomExtSplit)
{
if (spliter.inside)
{
return GetSpliterPattern( address );
}
else if (banks.fetchMode == Banks::FETCH_MODE_BG)
{
return GetExtPattern( address );
}
else
{
return chr.Peek( address );
}
}
void Mmc5::UpdateRenderMethod()
{
ppu.Update();
const uint method = regs.exRamMode | (spliter.ctrl >> 5 & 0x4);
{
static const Io::Accessor::Type<Mmc5>::Function chrMethods[8] =
{
&Mmc5::Access_CRom, // PPU NT
&Mmc5::Access_CRomExt, // PPU EXT
&Mmc5::Access_CRom, // CPU EXRAM
&Mmc5::Access_CRom, // CPU EXROM
&Mmc5::Access_CRomSplit, // PPU NT + SPLIT
&Mmc5::Access_CRomExtSplit, // PPU EXT + SPLIT
&Mmc5::Access_CRom, // CPU EXRAM + denied SPLIT
&Mmc5::Access_CRom // CPU EXROM + denied SPLIT
};
chr.SetAccessor( this, chrMethods[method] );
}
uint bank = banks.nmt;
{
static const Io::Accessor::Type<Mmc5>::Function nmtMethods[8][4] =
{
{ // PPU NT
&Mmc5::Access_Nt_CiRam_0,
&Mmc5::Access_Nt_CiRam_1,
&Mmc5::Access_Nt_ExRam,
&Mmc5::Access_Nt_Fill
},
{ // PPU EXT
&Mmc5::Access_NtExt_CiRam_0,
&Mmc5::Access_NtExt_CiRam_1,
&Mmc5::Access_NtExt_ExRam,
&Mmc5::Access_NtExt_Fill
},
{ // CPU EXRAM
&Mmc5::Access_Nt_CiRam_0,
&Mmc5::Access_Nt_CiRam_1,
&Mmc5::Access_Nt_Zero,
&Mmc5::Access_Nt_Fill
},
{ // CPU EXROM
&Mmc5::Access_Nt_CiRam_0,
&Mmc5::Access_Nt_CiRam_1,
&Mmc5::Access_Nt_Zero,
&Mmc5::Access_Nt_Fill
},
{ // PPU NT + SPLIT
&Mmc5::Access_NtSplit_CiRam_0,
&Mmc5::Access_NtSplit_CiRam_1,
&Mmc5::Access_NtSplit_ExRam,
&Mmc5::Access_NtSplit_Fill
},
{ // PPU EXT + SPLIT
&Mmc5::Access_NtExtSplit_CiRam_0,
&Mmc5::Access_NtExtSplit_CiRam_1,
&Mmc5::Access_NtExtSplit_ExRam,
&Mmc5::Access_NtExtSplit_Fill
},
{ // CPU EXRAM + denied SPLIT
&Mmc5::Access_Nt_CiRam_0,
&Mmc5::Access_Nt_CiRam_1,
&Mmc5::Access_Nt_Zero,
&Mmc5::Access_Nt_Fill
},
{ // CPU EXROM + denied SPLIT
&Mmc5::Access_Nt_CiRam_0,
&Mmc5::Access_Nt_CiRam_1,
&Mmc5::Access_Nt_Zero,
&Mmc5::Access_Nt_Fill
}
};
nmt.SetAccessors
(
this,
nmtMethods[method][bank >> 0 & Regs::NMT_MODE],
nmtMethods[method][bank >> 2 & Regs::NMT_MODE],
nmtMethods[method][bank >> 4 & Regs::NMT_MODE],
nmtMethods[method][bank >> 6 & Regs::NMT_MODE]
);
}
for (uint address=0; address < SIZE_4K; address += SIZE_1K, bank >>= 2)
{
static const byte securities[4][4][2] =
{
{ {0,0}, {0,1}, {1,0}, {0,0} },
{ {0,0}, {0,1}, {1,0}, {0,0} },
{ {0,0}, {0,1}, {0,0}, {0,0} },
{ {0,0}, {0,1}, {0,0}, {0,0} }
};
nmt.Source( securities[regs.exRamMode][bank & Regs::NMT_MODE][0] ).SwapBank<SIZE_1K>
(
address,
securities[regs.exRamMode][bank & Regs::NMT_MODE][1]
);
}
}
NES_POKE_AD(Mmc5,2001)
{
Update();
if (!(data & Regs::PPU_CTRL1_ENABLED))
{
irq.count = 0U-2U;
irq.state &= (Irq::HIT|Irq::ENABLED);
banks.fetchMode = Banks::FETCH_MODE_NONE;
spliter.inside = false;
}
p2001.Poke( address, data );
}
NES_PEEK_A(Mmc5,2001)
{
return p2001.Peek( address );
}
NES_POKE_D(Mmc5,5000)
{
sound.WriteSquareReg0( 0, data );
}
NES_POKE_D(Mmc5,5002)
{
sound.WriteSquareReg1( 0, data );
}
NES_POKE_D(Mmc5,5003)
{
sound.WriteSquareReg2( 0, data );
}
NES_POKE_D(Mmc5,5004)
{
sound.WriteSquareReg0( 1, data );
}
NES_POKE_D(Mmc5,5006)
{
sound.WriteSquareReg1( 1, data );
}
NES_POKE_D(Mmc5,5007)
{
sound.WriteSquareReg2( 1, data );
}
NES_POKE_D(Mmc5,5010)
{
sound.WritePcmReg0( data );
}
NES_POKE_D(Mmc5,5011)
{
sound.WritePcmReg1( data );
}
NES_PEEK(Mmc5,5015)
{
return sound.ReadCtrl();
}
NES_POKE_D(Mmc5,5015)
{
sound.WriteCtrl( data );
}
NES_POKE_D(Mmc5,5100)
{
data &= Regs::PRG_MODE;
if (regs.prgMode != data)
{
regs.prgMode = data;
UpdatePrg();
}
}
NES_POKE_D(Mmc5,5101)
{
data &= Regs::CHR_MODE;
if (regs.chrMode != data)
{
ppu.Update();
regs.chrMode = data;
if (!IsPpuSprite8x16() || !ppu.IsEnabled() || ppu.GetScanline() == Ppu::SCANLINE_VBLANK)
{
if (banks.lastChr == Banks::LAST_CHR_A)
UpdateChrA();
else
UpdateChrB();
}
}
}
NES_POKE_D(Mmc5,5102)
{
if (data == Regs::WRK_WRITABLE_A)
banks.security |= Regs::WRK_WRITABLE_A;
else
banks.security &= ~uint(Regs::WRK_WRITABLE_A);
}
NES_POKE_D(Mmc5,5103)
{
if (data == Regs::WRK_WRITABLE_B)
banks.security |= Regs::WRK_WRITABLE_B;
else
banks.security &= ~uint(Regs::WRK_WRITABLE_B);
}
NES_POKE_D(Mmc5,5104)
{
data &= Regs::EXRAM_MODE;
if (regs.exRamMode != data)
{
regs.exRamMode = data;
UpdateRenderMethod();
}
}
NES_POKE_D(Mmc5,5105)
{
if (banks.nmt != data)
{
banks.nmt = data;
UpdateRenderMethod();
}
}
NES_POKE_D(Mmc5,5106)
{
if (banks.nmt & (banks.nmt << 1))
ppu.Update();
filler.tile = data;
}
NES_POKE_D(Mmc5,5107)
{
if (banks.nmt & (banks.nmt << 1))
ppu.Update();
filler.attribute = Filler::squared[data & 0x3];
}
NES_POKE_D(Mmc5,5113)
{
data = banks.wrk[data & Regs::PRG_RAM_BANK];
if (data != Banks::Wrk::INVALID)
{
banks.security |= Banks::READABLE_6|Banks::WRITABLE_6;
wrk.SwapBank<SIZE_8K,0x0000>( data );
}
else
{
banks.security &= ~uint(Banks::READABLE_6|Banks::WRITABLE_6);
}
}
NES_POKE_AD(Mmc5,5114)
{
if (banks.prg[address - 0x5114] != data)
{
banks.prg[address - 0x5114] = data;
UpdatePrg();
}
}
NES_POKE_AD(Mmc5,5120)
{
data |= banks.chrHigh << 2;
address &= 0x7;
if (banks.lastChr != Banks::LAST_CHR_A || banks.chrA[address] != data)
{
ppu.Update();
banks.chrA[address] = data;
banks.lastChr = Banks::LAST_CHR_A;
if (!IsPpuSprite8x16() || !ppu.IsEnabled() || ppu.GetScanline() == Ppu::SCANLINE_VBLANK)
UpdateChrA();
}
}
NES_POKE_AD(Mmc5,5128)
{
data |= banks.chrHigh << 2;
address &= 0x3;
if (banks.lastChr == Banks::LAST_CHR_A || banks.chrB[address] != data)
{
ppu.Update();
banks.chrB[address] = data;
banks.lastChr = Banks::LAST_CHR_B;
if (!IsPpuSprite8x16() || !ppu.IsEnabled() || ppu.GetScanline() == Ppu::SCANLINE_VBLANK)
UpdateChrB();
}
}
NES_POKE_D(Mmc5,5130)
{
data = (data & Regs::CHR_HIGH) << 6;
if (banks.chrHigh != data)
{
ppu.Update();
banks.chrHigh = data;
}
}
NES_POKE_D(Mmc5,5200)
{
if (spliter.ctrl != data)
{
ppu.Update();
spliter.ctrl = data;
UpdateRenderMethod();
}
}
NES_POKE_D(Mmc5,5201)
{
if (data >= 240)
data -= 16;
if (spliter.yStart != data)
{
ppu.Update();
spliter.yStart = data;
}
}
NES_POKE_D(Mmc5,5202)
{
const dword chrBank = dword(data) << 12;
if (spliter.chrBank != chrBank)
{
ppu.Update();
spliter.chrBank = chrBank;
}
}
NES_POKE_D(Mmc5,5203)
{
Update();
irq.target = data;
}
NES_PEEK(Mmc5,5204)
{
Update();
const uint status = irq.state & (Irq::FRAME|Irq::HIT);
irq.state &= (Irq::FRAME|Irq::ENABLED);
cpu.ClearIRQ();
return status;
}
NES_POKE_D(Mmc5,5204)
{
Update();
if (data & 0x80)
{
irq.state |= Irq::ENABLED;
if (irq.state & Irq::HIT)
cpu.DoIRQ();
}
else
{
irq.state &= (Irq::HIT|Irq::FRAME);
cpu.ClearIRQ();
}
}
NES_PEEK(Mmc5,5205)
{
return (regs.mul[0] * regs.mul[1]) >> 0 & 0xFF;
}
NES_PEEK(Mmc5,5206)
{
return (regs.mul[0] * regs.mul[1]) >> 8 & 0xFF;
}
NES_POKE_D(Mmc5,5205)
{
regs.mul[0] = data;
}
NES_POKE_D(Mmc5,5206)
{
regs.mul[1] = data;
}
NES_PEEK_A(Mmc5,5C00)
{
if (regs.exRamMode & Regs::EXRAM_MODE_CPU_RAM)
return exRam.mem[address - 0x5C00];
else
return address >> 8;
}
NES_POKE_AD(Mmc5,5C00)
{
switch (regs.exRamMode)
{
default:
ppu.Update();
Update();
NST_VERIFY( irq.state & Irq::FRAME );
if (!(irq.state & Irq::FRAME))
data = 0;
case Regs::EXRAM_MODE_CPU_RAM:
exRam.mem[address - 0x5C00] = data;
case Regs::EXRAM_MODE_CPU_ROM:
break;
}
}
NES_POKE_AD(Mmc5,6000)
{
NST_VERIFY( (banks.security & Banks::CAN_WRITE_6) == Banks::CAN_WRITE_6 );
if ((banks.security & Banks::CAN_WRITE_6) == Banks::CAN_WRITE_6)
wrk[0][address - 0x6000] = data;
}
NES_POKE_AD(Mmc5,8000)
{
NST_VERIFY( (banks.security & Banks::CAN_WRITE_8) == Banks::CAN_WRITE_8 );
if ((banks.security & Banks::CAN_WRITE_8) == Banks::CAN_WRITE_8)
prg[0][address - 0x8000] = data;
}
NES_POKE_AD(Mmc5,A000)
{
NST_VERIFY( (banks.security & Banks::CAN_WRITE_A) == Banks::CAN_WRITE_A );
if ((banks.security & Banks::CAN_WRITE_A) == Banks::CAN_WRITE_A)
prg[1][address - 0xA000] = data;
}
NES_POKE_AD(Mmc5,C000)
{
NST_VERIFY( (banks.security & Banks::CAN_WRITE_C) == Banks::CAN_WRITE_C );
if ((banks.security & Banks::CAN_WRITE_C) == Banks::CAN_WRITE_C)
prg[2][address - 0xC000] = data;
}
NES_PEEK_A(Mmc5,6000)
{
NST_VERIFY( banks.security & Banks::READABLE_6 );
return (banks.security & Banks::READABLE_6) ? wrk[0][address - 0x6000] : (address >> 8);
}
NES_PEEK_A(Mmc5,8000)
{
NST_VERIFY( banks.security & Banks::READABLE_8 );
return (banks.security & Banks::READABLE_8) ? prg[0][address - 0x8000] : (address >> 8);
}
NES_PEEK_A(Mmc5,A000)
{
NST_VERIFY( banks.security & Banks::READABLE_A );
return (banks.security & Banks::READABLE_A) ? prg[1][address - 0xA000] : (address >> 8);
}
NES_PEEK_A(Mmc5,C000)
{
NST_VERIFY( banks.security & Banks::READABLE_C );
return (banks.security & Banks::READABLE_C) ? prg[2][address - 0xC000] : (address >> 8);
}
NST_SINGLE_CALL void Mmc5::Sound::Square::Disable(const bool disable)
{
if (disable)
{
timer = 0;
active = 0;
}
lengthCounter.Disable( disable );
}
void Mmc5::Sound::WriteCtrl(uint data)
{
Update();
data = ~data;
for (uint i=0; i < NUM_SQUARES; ++i)
square[i].Disable( data >> i & 0x1 );
}
inline uint Mmc5::Sound::Square::GetLengthCounter() const
{
return lengthCounter.GetCount();
}
uint Mmc5::Sound::ReadCtrl() const
{
Update();
uint data = 0;
for (uint i=0; i < NUM_SQUARES; ++i)
{
if (square[i].GetLengthCounter())
data |= 0x1U << i;
}
return data;
}
NST_SINGLE_CALL void Mmc5::Sound::Square::WriteReg0(const uint data)
{
envelope.Write( data );
duty = data >> DUTY_SHIFT;
}
NST_SINGLE_CALL void Mmc5::Sound::Square::WriteReg1(const uint data,const uint fixed)
{
waveLength &= uint(REG2_WAVELENGTH_HIGH) << 8;
waveLength |= data;
frequency = (waveLength + 1UL) * fixed * 2;
active = CanOutput();
}
NST_SINGLE_CALL void Mmc5::Sound::Square::WriteReg2(const uint data,const uint fixed)
{
step = 0;
envelope.ResetClock();
lengthCounter.Write( data );
waveLength &= REG1_WAVELENGTH_LOW;
waveLength |= (data & REG2_WAVELENGTH_HIGH) << 8;
frequency = (waveLength + 1UL) * fixed * 2;
active = CanOutput();
}
void Mmc5::Sound::WriteSquareReg0(uint index,uint data)
{
Update();
NST_ASSERT( index < NUM_SQUARES );
square[index].WriteReg0( data );
}
void Mmc5::Sound::WriteSquareReg1(uint index,uint data)
{
Update();
NST_ASSERT( index < NUM_SQUARES );
square[index].WriteReg1( data, fixed );
}
void Mmc5::Sound::WriteSquareReg2(uint index,uint data)
{
Update();
NST_ASSERT( index < NUM_SQUARES );
square[index].WriteReg2( data, fixed );
}
NST_SINGLE_CALL void Mmc5::Sound::Pcm::WriteReg0(const uint data)
{
enabled = ~data & PCM_DISABLE;
sample = enabled ? amp : 0;
}
NST_SINGLE_CALL void Mmc5::Sound::Pcm::WriteReg1(const uint data)
{
amp = data * VOLUME;
sample = enabled ? amp : 0;
}
void Mmc5::Sound::WritePcmReg0(uint data)
{
Update();
pcm.WriteReg0( data );
}
void Mmc5::Sound::WritePcmReg1(uint data)
{
Update();
pcm.WriteReg1( data );
}
NST_SINGLE_CALL dword Mmc5::Sound::Square::GetSample(const Cycle rate)
{
NST_VERIFY( bool(active) == CanOutput() && timer >= 0 );
if (active)
{
dword sum = timer;
timer -= idword(rate);
static const byte duties[4][8] =
{
{0x1F,0x00,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F},
{0x1F,0x00,0x00,0x1F,0x1F,0x1F,0x1F,0x1F},
{0x1F,0x00,0x00,0x00,0x00,0x1F,0x1F,0x1F},
{0x00,0x1F,0x1F,0x00,0x00,0x00,0x00,0x00}
};
if (timer >= 0)
{
return dword(envelope.Volume()) >> duties[duty][step];
}
else
{
sum >>= duties[duty][step];
do
{
sum += NST_MIN(dword(-timer),frequency) >> duties[duty][step = (step + 1) & 0x7];
timer += idword(frequency);
}
while (timer < 0);
return (sum * envelope.Volume() + rate/2) / rate;
}
}
else
{
return 0;
}
}
inline Mmc5::Sound::Sample Mmc5::Sound::Pcm::GetSample() const
{
return sample;
}
Mmc5::Sound::Sample Mmc5::Sound::GetSample()
{
if (output)
{
dword sample = 0;
for (uint i=0; i < NUM_SQUARES; ++i)
sample += square[i].GetSample( rate );
sample += pcm.GetSample();
return dcBlocker.Apply( sample * 2 * output / DEFAULT_VOLUME );
}
else
{
return 0;
}
}
NST_SINGLE_CALL void Mmc5::Sound::Square::ClockQuarter()
{
envelope.Clock();
}
NST_SINGLE_CALL void Mmc5::Sound::Square::ClockHalf()
{
if (!envelope.Looping() && lengthCounter.Clock())
active = false;
}
Cycle Mmc5::Sound::Clock(Cycle rateCycles,Cycle rateClock,const Cycle targetCycles)
{
rateClock *= quarterClock;
do
{
for (uint i=0; i < NUM_SQUARES; ++i)
square[i].ClockQuarter();
if (atHalfClock)
{
for (uint i=0; i < NUM_SQUARES; ++i)
square[i].ClockHalf();
}
atHalfClock ^= 1;
rateCycles += rateClock;
}
while (rateCycles <= targetCycles);
return rateCycles;
}
}
}
}
You can’t perform that action at this time.