//////////////////////////////////////////////////////////////////////////////////////// // // 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 #include #include #include #include "NstMachine.hpp" #include "NstState.hpp" #include "NstTrackerRewinder.hpp" #include "api/NstApiRewinder.hpp" #include "NstZlib.hpp" namespace Nes { namespace Core { class Tracker::Rewinder::ReverseVideo::Mutex { Ppu& ppu; Video::Screen::Pixel* const pixels; public: explicit Mutex(const ReverseVideo& r) : ppu(r.ppu), pixels(r.ppu.GetOutputPixels()) {} void Flush(Video::Screen::Pixel* src) const { std::memcpy( pixels, src, Video::Screen::PIXELS * sizeof(Video::Screen::Pixel) ); } ~Mutex() { ppu.SetOutputPixels( pixels ); } }; class Tracker::Rewinder::ReverseVideo::Buffer { typedef Video::Screen::Pixel Pixel; enum { PIXELS = Video::Screen::PIXELS, SIZE = PIXELS * dword(NUM_FRAMES), FULL_SIZE = dword(SIZE) + Video::Screen::PIXELS_PADDING }; Pixel pixels[FULL_SIZE]; public: Buffer() { std::fill( pixels + SIZE, pixels + FULL_SIZE, Pixel(0) ); } Pixel* operator [] (dword i) { NST_ASSERT( i < NUM_FRAMES ); return pixels + (PIXELS * i); } }; class Tracker::Rewinder::ReverseSound::Mutex { Output::LockCallback funcLock; void* userLock; Output::UnlockCallback funcUnlock; void* userUnlock; public: Mutex() { Output::lockCallback.Get( funcLock, userLock ); Output::unlockCallback.Get( funcUnlock, userUnlock ); Output::lockCallback.Set( NULL, NULL ); Output::unlockCallback.Set( NULL, NULL ); } bool Lock(Output& output) const { return funcLock ? funcLock( userLock, output ) : true; } void Unlock(Output& output) const { if (funcUnlock) funcUnlock( userUnlock, output ); } ~Mutex() { Output::lockCallback.Set( funcLock, userLock ); Output::unlockCallback.Set( funcUnlock, userUnlock ); } }; #ifdef NST_MSVC_OPTIMIZE #pragma optimize("s", on) #endif Tracker::Rewinder::ReverseVideo::ReverseVideo(Ppu& p) : pingpong (1), frame (0), ppu (p), buffer (NULL) {} Tracker::Rewinder::ReverseSound::ReverseSound(const Apu& a,bool e) : enabled (e), good (false), stereo (false), rate (0), index (0), buffer (NULL), size (0), input (NULL), apu (a) {} Tracker::Rewinder::Rewinder(Machine& e,EmuExecute x,EmuLoadState l,EmuSaveState s,Cpu& c,const Apu& a,Ppu& p,bool b) : rewinding (false), sound (a,b), video (p), emulator (e), emuExecute (x), emuLoadState (l), emuSaveState (s), cpu (c), ppu (p) { Reset( true ); } Tracker::Rewinder::ReverseVideo::~ReverseVideo() { End(); } Tracker::Rewinder::ReverseSound::~ReverseSound() { End(); } Tracker::Rewinder::~Rewinder() { LinkPorts( false ); } void Tracker::Rewinder::LinkPorts(bool on) { for (uint i=0; i < 2; ++i) { cpu.Unlink( 0x4016+i, this, &Rewinder::Peek_Port_Get, &Rewinder::Poke_Port ); cpu.Unlink( 0x4016+i, this, &Rewinder::Peek_Port_Put, &Rewinder::Poke_Port ); } if (on) { for (uint i=0; i < 2; ++i) ports[i] = cpu.Link( 0x4016+i, Cpu::LEVEL_HIGHEST, this, rewinding ? &Rewinder::Peek_Port_Get : &Rewinder::Peek_Port_Put, &Rewinder::Poke_Port ); } } Tracker::Rewinder::Key::Key() { } Tracker::Rewinder::Key::~Key() { } void Tracker::Rewinder::Key::Input::Reset() { pos = BAD_POS; buffer.Destroy(); } void Tracker::Rewinder::Key::Reset() { stream.str( std::string() ); input.Reset(); } void Tracker::Rewinder::Reset(bool on) { video.End(); sound.End(); if (rewinding) { rewinding = false; Api::Rewinder::stateCallback( Api::Rewinder::STOPPED ); } uturn = false; frame = LAST_FRAME; key = keys + LAST_KEY; for (uint i=0; i < NUM_FRAMES; ++i) keys[i].Reset(); LinkPorts( on ); } void Tracker::Rewinder::ReverseVideo::Begin() { pingpong = 1; frame = 0; if (buffer == NULL) buffer = new Buffer; } void Tracker::Rewinder::ReverseVideo::End() { delete buffer; buffer = NULL; } void Tracker::Rewinder::ReverseSound::Begin() { good = true; index = 0; } void Tracker::Rewinder::ReverseSound::End() { std::free( buffer ); buffer = NULL; } void Tracker::Rewinder::ReverseSound::Enable(bool state) { enabled = state; if (!state) End(); } bool Tracker::Rewinder::ReverseSound::Update() { const dword old = size * sizeof(iword); rate = apu.GetSampleRate(); stereo = apu.InStereo(); size = rate << (stereo+1); const dword total = size * sizeof(iword); NST_ASSERT( total ); if (!buffer || total != old) { if (void* const next = std::realloc( buffer, total )) { buffer = next; } else { End(); good = false; return false; } } good = true; index = 0; std::fill( static_cast(buffer), static_cast(buffer) + size, iword(0) ); return true; } #ifdef NST_MSVC_OPTIMIZE #pragma optimize("", on) #endif inline void Tracker::Rewinder::Key::Input::Invalidate() { pos = BAD_POS; } inline void Tracker::Rewinder::Key::Input::BeginForward() { const dword hint = pos; pos = 0; buffer.Clear(); if (hint != BAD_POS) buffer.Reserve( hint ); } bool Tracker::Rewinder::Key::Input::EndForward() { if (pos == 0) { pos = buffer.Size(); if (Zlib::AVAILABLE && pos >= MIN_COMPRESSION_SIZE) { Buffer tmp( pos - 1 ); if (const dword size = Zlib::Compress( buffer.Begin(), buffer.Size(), tmp.Begin(), tmp.Size(), Zlib::NORMAL_COMPRESSION )) { NST_ASSERT( size < pos ); tmp.SetTo( size ); Buffer::Swap( tmp, buffer ); } else { NST_DEBUG_MSG("compress() in Tracker::Rewinder::Key::Input failed!"); } buffer.Defrag(); } return true; } return false; } void Tracker::Rewinder::Key::Input::BeginBackward() { dword size = pos; pos = 0; if (Zlib::AVAILABLE && size > buffer.Size()) { Buffer tmp( size ); size = Zlib::Uncompress( buffer.Begin(), buffer.Size(), tmp.Begin(), tmp.Size() ); if (!size) throw RESULT_ERR_CORRUPT_FILE; NST_VERIFY( size == tmp.Size() ); Buffer::Swap( tmp, buffer ); } } inline void Tracker::Rewinder::Key::Input::EndBackward() { pos = 0; } inline void Tracker::Rewinder::Key::Input::ResumeForward() { NST_VERIFY( pos != BAD_POS ); dword size = pos; pos = 0; buffer.Resize( size != BAD_POS ? size : 0 ); } inline bool Tracker::Rewinder::Key::Input::CanRewind() const { return pos != BAD_POS; } inline uint Tracker::Rewinder::Key::Input::Put(const uint data) { if (pos != BAD_POS) { try { buffer.Append( data ); } catch (...) { NST_DEBUG_MSG("buffer << data failed!"); pos = BAD_POS; } } return data; } inline uint Tracker::Rewinder::Key::Input::Get() { if (pos < buffer.Size()) { return buffer[pos++]; } else { NST_DEBUG_MSG("buffer >> data failed!"); pos = BAD_POS; return OPEN_BUS; } } inline void Tracker::Rewinder::Key::Invalidate() { input.Invalidate(); } inline bool Tracker::Rewinder::Key::CanRewind() const { return input.CanRewind(); } inline void Tracker::Rewinder::Key::ResumeForward() { input.ResumeForward(); } void Tracker::Rewinder::Key::BeginForward(Machine& emulator,EmuSaveState saveState,EmuLoadState loadState) { NST_ASSERT( !saveState || !loadState ); input.BeginForward(); if (saveState) { stream.clear(); stream.seekp( 0, std::stringstream::beg ); stream.clear(); State::Saver saver( &static_cast(stream), false, true ); (emulator.*saveState)( saver ); } else if (loadState) { TurnForward( emulator, loadState ); } } void Tracker::Rewinder::Key::EndForward() { if (!input.EndForward()) Reset(); } void Tracker::Rewinder::Key::TurnForward(Machine& emulator,EmuLoadState loadState) { stream.clear(); stream.seekg( 0, std::stringstream::beg ); stream.clear(); State::Loader loader( &static_cast(stream), false ); (emulator.*loadState)( loader, true ); } void Tracker::Rewinder::Key::BeginBackward(Machine& emulator,EmuLoadState loadState) { NST_VERIFY( CanRewind() ); TurnForward( emulator, loadState ); input.BeginBackward(); } inline void Tracker::Rewinder::Key::EndBackward() { input.EndBackward(); } inline uint Tracker::Rewinder::Key::Put(uint data) { return input.Put( data ); } inline uint Tracker::Rewinder::Key::Get() { return input.Get(); } inline Tracker::Rewinder::Key* Tracker::Rewinder::PrevKey(Key* k) { return (k != keys ? k-1 : keys+LAST_KEY); } inline Tracker::Rewinder::Key* Tracker::Rewinder::PrevKey() { return PrevKey( key ); } inline Tracker::Rewinder::Key* Tracker::Rewinder::NextKey(Key* k) { return (k != keys+LAST_KEY ? k+1 : keys); } inline Tracker::Rewinder::Key* Tracker::Rewinder::NextKey() { return NextKey( key ); } inline void Tracker::Rewinder::ReverseVideo::Flush(const Mutex& mutex) { mutex.Flush( (*buffer)[frame] ); } void Tracker::Rewinder::ReverseVideo::Store() { NST_ASSERT( frame < NUM_FRAMES && (pingpong == 1U-0U || pingpong == 0U-1U) ); ppu.SetOutputPixels( (*buffer)[frame] ); frame += pingpong; if (frame == NUM_FRAMES) { frame = LAST_FRAME; pingpong = 0U-1U; } else if (frame == 0U-1U) { frame = 0; pingpong = 1U-0U; } } template NST_FORCE_INLINE Sound::Output* Tracker::Rewinder::ReverseSound::StoreType() { NST_ASSERT( index <= NUM_FRAMES+LAST_FRAME ); switch (index++) { case 0: *output.length = rate / NUM_FRAMES; *output.samples = buffer; input = static_cast(buffer) + (size / 1); break; case LAST_FRAME: *output.samples = static_cast(*output.samples) + (*output.length << stereo); *output.length = dword(static_cast(buffer) + (size / 2) - static_cast(*output.samples)) >> stereo; break; case NUM_FRAMES: *output.length = rate / NUM_FRAMES; *output.samples = static_cast(buffer) + (size / 2); input = *output.samples; break; case NUM_FRAMES+LAST_FRAME: index = 0; *output.samples = static_cast(*output.samples) + (*output.length << stereo); *output.length = dword(static_cast(buffer) + (size / 1) - static_cast(*output.samples)) >> stereo; break; default: *output.samples = static_cast(*output.samples) + (*output.length << stereo); break; } return &output; } Sound::Output* Tracker::Rewinder::ReverseSound::Store() { NST_COMPILE_ASSERT( NUM_FRAMES % 2 == 0 ); if (!buffer || (rate ^ apu.GetSampleRate()) | (stereo ^ uint(bool(apu.InStereo())))) { if (!good || !Update() || !enabled) return NULL; } return StoreType(); } template void Tracker::Rewinder::ReverseSound::ReverseSilence(const Output& target) const { for (uint i=0; i < 2; ++i) std::fill( static_cast(target.samples[i]), static_cast(target.samples[i]) + (target.length[i] << stereo), SILENCE ); } template const void* Tracker::Rewinder::ReverseSound::ReverseCopy(const Output& target) const { const T* NST_RESTRICT src = static_cast(input); for (uint i=0; i < 2; ++i) { if (const dword length = (target.length[i] << stereo)) { T* NST_RESTRICT dst = static_cast(target.samples[i]); T* const dstEnd = dst + length; for (const T* const srcEnd = dword(src - static_cast(buffer)) >= length ? src - length : static_cast(buffer); src != srcEnd; ) *dst++ = *--src; const T last( *src ); std::fill( dst, dstEnd, last ); } } return src; } void Tracker::Rewinder::ReverseSound::Flush(Output* const target,const Mutex& mutex) { if (target && mutex.Lock( *target )) { if (enabled & good) { input = ReverseCopy( *target ); } else { ReverseSilence( *target ); } mutex.Unlock( *target ); } } void Tracker::Rewinder::Execute(Video::Output* videoOut,Sound::Output* soundOut,Input::Controllers* inputOut) { try { if (uturn) ChangeDirection(); NST_ASSERT( frame < NUM_FRAMES ); if (!rewinding) { if (++frame == NUM_FRAMES) { frame = 0; key->EndForward(); key = NextKey(); key->BeginForward( emulator, emuSaveState, NULL ); } } else { if (++frame == NUM_FRAMES) { frame = 0; key->EndBackward(); Key* const prev = PrevKey(); if (prev->CanRewind()) { prev->BeginBackward( emulator, emuLoadState ); key = prev; } else { rewinding = false; key->Invalidate(); key = NextKey(); key->BeginForward( emulator, NULL, emuLoadState ); Api::Rewinder::stateCallback( Api::Rewinder::STOPPED ); LinkPorts(); } } if (rewinding) { const ReverseVideo::Mutex videoMutex( video ); video.Flush( videoMutex ); video.Store(); const ReverseSound::Mutex soundMutex; sound.Flush( soundOut, soundMutex ); soundOut = sound.Store(); (emulator.*emuExecute)( videoOut, soundOut, inputOut ); return; } } } catch (...) { Reset(); throw; } (emulator.*emuExecute)( videoOut, soundOut, inputOut ); } void Tracker::Rewinder::ChangeDirection() { Api::Rewinder::stateCallback( Api::Rewinder::PREPARING ); uturn = false; if (rewinding) { for (uint i=frame; i < LAST_FRAME; ++i) (emulator.*emuExecute)( NULL, NULL, NULL ); NextKey()->Invalidate(); video.Begin(); sound.Begin(); key->BeginBackward( emulator, emuLoadState ); LinkPorts(); { const ReverseVideo::Mutex videoMutex( video ); const ReverseSound::Mutex soundMutex; for (uint i=0; i < NUM_FRAMES; ++i) { video.Store(); (emulator.*emuExecute)( NULL, sound.Store(), NULL ); } } uint align = LAST_FRAME - frame; frame = LAST_FRAME; while (align--) { Execute( NULL, NULL, NULL ); if (!rewinding) throw RESULT_ERR_CORRUPT_FILE; } Api::Rewinder::stateCallback( Api::Rewinder::REWINDING ); } else { for (uint i=NUM_FRAMES+LAST_FRAME-frame*2; i; --i) { if (++frame == NUM_FRAMES) { frame = 0; key = NextKey(); key->TurnForward( emulator, emuLoadState ); } (emulator.*emuExecute)( NULL, NULL, NULL ); } key->ResumeForward(); LinkPorts(); video.End(); sound.End(); Api::Rewinder::stateCallback( Api::Rewinder::STOPPED ); } } Result Tracker::Rewinder::Start() { if (rewinding) return RESULT_NOP; if (uturn || !PrevKey()->CanRewind()) return RESULT_ERR_NOT_READY; uturn = true; rewinding = true; return RESULT_OK; } Result Tracker::Rewinder::Stop() { if (!rewinding) return RESULT_NOP; if (uturn) return RESULT_ERR_NOT_READY; uturn = true; rewinding = false; return RESULT_OK; } NES_PEEK_A(Tracker::Rewinder,Port_Put) { return key->Put( ports[address-0x4016]->Peek( address ) ); } NES_PEEK(Tracker::Rewinder,Port_Get) { return key->Get(); } NES_POKE_AD(Tracker::Rewinder,Port) { ports[address-0x4016]->Poke( address, data ); } } }