/* PCSX2 - PS2 Emulator for PCs * Copyright (C) 2002-2009 PCSX2 Dev Team * * PCSX2 is free software: you can redistribute it and/or modify it under the terms * of the GNU Lesser General Public License as published by the Free Software Found- * ation, either version 3 of te License, or (at your option) any later version. * * PCSX2 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 PCSX2. * If not, see . */ #include "PrecompiledHeader.h" #include "zlib/zlib.h" #include "App.h" #include "HostGui.h" class _BaseStateThread; // Used to hold the current state backup (fullcopy of PS2 memory and plugin states). static SafeArray state_buffer; _BaseStateThread* current_state_thread = NULL; // Simple lock boolean for the state buffer being in use by a thread. static NonblockingMutex state_buffer_lock; // This boolean is to keep the system from resuming emulation until the current state has completely // uploaded or downloaded itself. It is only modified from the main thread, and should only be read // from the main thread. int sys_resume_lock = 0; static FnType_OnThreadComplete* Callback_FreezeFinished = NULL; static void __evt_fastcall StateThread_OnAppStatus( void* thr, AppEventType& stat ) { if( (thr == NULL) || (stat != AppStatus_Exiting) ) return; ((PersistentThread*)thr)->Cancel(); } enum { StateThreadAction_None = 0, StateThreadAction_Create, StateThreadAction_Restore, StateThreadAction_ZipToDisk, StateThreadAction_UnzipFromDisk, }; class _BaseStateThread : public PersistentThread { typedef PersistentThread _parent; protected: EventListenerBinding m_bind_OnExit; bool m_isStarted; // Holds the pause/suspend state of the emulator when the state load/stave chain of action is started, // so that the proper state can be restoed automatically on completion. bool m_resume_when_done; public: virtual ~_BaseStateThread() throw() { if( !m_isStarted ) return; // Assertion fails because C++ changes the 'this' pointer to the base class since // derived classes have been deallocated at this point the destructor! //pxAssumeDev( current_state_thread == this, wxCharNull ); current_state_thread = NULL; state_buffer_lock.Release(); // just in case; } protected: _BaseStateThread( const char* name, FnType_OnThreadComplete* onFinished ) : m_bind_OnExit( wxGetApp().Source_AppStatus(), EventListener( this, StateThread_OnAppStatus ) ) { Callback_FreezeFinished = onFinished; m_name = L"StateThread::" + fromUTF8(name); m_isStarted = false; m_resume_when_done = false; } void OnStart() { if( !state_buffer_lock.TryAcquire() ) throw Exception::CancelEvent( m_name + L"request ignored: state copy buffer is already locked!" ); current_state_thread = this; m_isStarted = true; _parent::OnStart(); } void SendFinishEvent( int type ) { wxCommandEvent evt( pxEVT_FreezeThreadFinished ); evt.SetClientData( this ); evt.SetInt( type ); evt.SetExtraLong( m_resume_when_done ); wxGetApp().AddPendingEvent( evt ); } }; // -------------------------------------------------------------------------------------- // StateThread_Freeze // -------------------------------------------------------------------------------------- class StateThread_Freeze : public _BaseStateThread { typedef _BaseStateThread _parent; public: StateThread_Freeze( FnType_OnThreadComplete* onFinished ) : _BaseStateThread( "Freeze", onFinished ) { if( !SysHasValidState() ) throw Exception::RuntimeError( L"Cannot complete state freeze request; the virtual machine state is reset.", _("You'll need to start a new virtual machine before you can save its state.") ); } protected: void OnStart() { _parent::OnStart(); ++sys_resume_lock; m_resume_when_done = CoreThread.Pause(); } void ExecuteTaskInThread() { memSavingState( state_buffer ).FreezeAll(); } void OnCleanupInThread() { SendFinishEvent( StateThreadAction_Create ); _parent::OnCleanupInThread(); } }; // -------------------------------------------------------------------------------------- // StateThread_ZipToDisk // -------------------------------------------------------------------------------------- class StateThread_ZipToDisk : public _BaseStateThread { typedef _BaseStateThread _parent; protected: const wxString m_filename; gzFile m_gzfp; public: StateThread_ZipToDisk( FnType_OnThreadComplete* onFinished, bool resume_done, const wxString& file ) : _BaseStateThread( "ZipToDisk", onFinished ) , m_filename( file ) { m_gzfp = NULL; m_resume_when_done = resume_done; } virtual ~StateThread_ZipToDisk() throw() { if( m_gzfp != NULL ) gzclose( m_gzfp ); } protected: void OnStart() { _parent::OnStart(); m_gzfp = gzopen( m_filename.ToUTF8(), "wb" ); if( m_gzfp == NULL ) throw Exception::CreateStream( m_filename, "Cannot create savestate file for writing." ); } void ExecuteTaskInThread() { Yield( 3 ); static const int BlockSize = 0x10000; int curidx = 0; do { int thisBlockSize = std::min( BlockSize, state_buffer.GetSizeInBytes() - curidx ); if( gzwrite( m_gzfp, state_buffer.GetPtr(curidx), thisBlockSize ) < thisBlockSize ) throw Exception::BadStream( m_filename ); curidx += thisBlockSize; Yield( 1 ); } while( curidx < state_buffer.GetSizeInBytes() ); } void OnCleanupInThread() { SendFinishEvent( StateThreadAction_ZipToDisk ); _parent::OnCleanupInThread(); } }; // -------------------------------------------------------------------------------------- // StateThread_UnzipFromDisk // -------------------------------------------------------------------------------------- class StateThread_UnzipFromDisk : public _BaseStateThread { typedef _BaseStateThread _parent; protected: const wxString m_filename; gzFile m_gzfp; // set true only once the whole file has finished loading. IF the thread is canceled or // an error occurs, this will remain false. bool m_finished; public: StateThread_UnzipFromDisk( FnType_OnThreadComplete* onFinished, bool resume_done, const wxString& file ) : _BaseStateThread( "UnzipFromDisk", onFinished ) , m_filename( file ) { m_gzfp = NULL; m_finished = false; m_resume_when_done = resume_done; } virtual ~StateThread_UnzipFromDisk() throw() { if( m_gzfp != NULL ) gzclose( m_gzfp ); } protected: void OnStart() { _parent::OnStart(); m_gzfp = gzopen( m_filename.ToUTF8(), "rb" ); if( m_gzfp == NULL ) throw Exception::CreateStream( m_filename, "Cannot open savestate file for reading." ); } void ExecuteTaskInThread() { // fixme: should start initially with the file size, and then grow from there. static const int BlockSize = 0x100000; state_buffer.MakeRoomFor( 0x800000 ); // start with an 8 meg buffer to avoid frequent reallocation. int curidx = 0; do { state_buffer.MakeRoomFor( curidx+BlockSize ); gzread( m_gzfp, state_buffer.GetPtr(curidx), BlockSize ); curidx += BlockSize; TestCancel(); } while( !gzeof(m_gzfp) ); m_finished = true; } void OnCleanupInThread() { SendFinishEvent( StateThreadAction_UnzipFromDisk ); _parent::OnCleanupInThread(); } }; void Pcsx2App::OnFreezeThreadFinished( wxCommandEvent& evt ) { // clear the OnFreezeFinished to NULL now, in case of error. // (but only actually run it if no errors occur) FnType_OnThreadComplete* fn_tmp = Callback_FreezeFinished; Callback_FreezeFinished = NULL; { ScopedPtr thr( (PersistentThread*)evt.GetClientData() ); if( !pxAssertDev( thr != NULL, "NULL thread handle on freeze finished?" ) ) return; state_buffer_lock.Release(); --sys_resume_lock; thr->RethrowException(); } if( fn_tmp != NULL ) fn_tmp( evt ); } static void OnFinished_Resume( const wxCommandEvent& evt ) { CoreThread.RecoverState(); if( evt.GetExtraLong() ) CoreThread.Resume(); } static wxString zip_dest_filename; static void OnFinished_ZipToDisk( const wxCommandEvent& evt ) { if( !pxAssertDev( evt.GetInt() == StateThreadAction_Create, "Unexpected StateThreadAction value, aborting save." ) ) return; if( zip_dest_filename.IsEmpty() ) { Console.Warning( "Cannot save state to disk: empty filename specified." ); return; } // Phase 2: Record to disk!! (new StateThread_ZipToDisk( NULL, !!evt.GetExtraLong(), zip_dest_filename ))->Start(); CoreThread.Resume(); } // ===================================================================================================== // StateCopy Public Interface // ===================================================================================================== void StateCopy_SaveToFile( const wxString& file ) { if( state_buffer_lock.IsLocked() ) return; zip_dest_filename = file; (new StateThread_Freeze( OnFinished_ZipToDisk ))->Start(); Console.WriteLn( Color_StrongGreen, L"Saving savestate to file: %s", zip_dest_filename.c_str() ); } void StateCopy_LoadFromFile( const wxString& file ) { if( state_buffer_lock.IsLocked() ) return; bool resume_when_done = CoreThread.Pause(); (new StateThread_UnzipFromDisk( OnFinished_Resume, resume_when_done, file ))->Start(); } // Saves recovery state info to the given saveslot, or saves the active emulation state // (if one exists) and no recovery data was found. This is needed because when a recovery // state is made, the emulation state is usually reset so the only persisting state is // the one in the memory save. :) void StateCopy_SaveToSlot( uint num ) { if( state_buffer_lock.IsLocked() ) return; zip_dest_filename = SaveStateBase::GetFilename( num ); (new StateThread_Freeze( OnFinished_ZipToDisk ))->Start(); Console.WriteLn( Color_StrongGreen, "Saving savestate to slot %d...", num ); Console.Indent().WriteLn( Color_StrongGreen, L"filename: %s", zip_dest_filename.c_str() ); } void StateCopy_LoadFromSlot( uint slot ) { if( state_buffer_lock.IsLocked() ) return; wxString file( SaveStateBase::GetFilename( slot ) ); if( !wxFileExists( file ) ) { Console.Warning( "Savestate slot %d is empty.", slot ); return; } Console.WriteLn( Color_StrongGreen, "Loading savestate from slot %d...", slot ); Console.Indent().WriteLn( Color_StrongGreen, L"filename: %s", file.c_str() ); bool resume_when_done = CoreThread.Pause(); (new StateThread_UnzipFromDisk( OnFinished_Resume, resume_when_done, file ))->Start(); } bool StateCopy_IsValid() { return !state_buffer.IsDisposed(); } const SafeArray* StateCopy_GetBuffer() { if( state_buffer_lock.IsLocked() || state_buffer.IsDisposed() ) return NULL; return &state_buffer; } void StateCopy_FreezeToMem() { if( state_buffer_lock.IsLocked() ) return; (new StateThread_Freeze( OnFinished_Resume ))->Start(); } static void _acquire_and_block() { if( state_buffer_lock.TryAcquire() ) return; /* // If the state buffer is locked and we're being called from the main thread then we need // to cancel the current action. This is needed because state_buffer_lock is only updated // from events handled on the main thread. if( wxThread::IsMain() ) throw Exception::CancelEvent( "Blocking ThawFromMem canceled due to existing state buffer lock." ); else*/ { pxAssume( current_state_thread != NULL ); do { current_state_thread->Block(); wxGetApp().ProcessPendingEvents(); // Trying this for now, may or may not work due to recursive pitfalls (see above) } while ( !state_buffer_lock.TryAcquire() ); } } void StateCopy_FreezeToMem_Blocking() { _acquire_and_block(); memSavingState( state_buffer ).FreezeAll(); state_buffer_lock.Release(); } // Copies the saved state into the active VM, and automatically free's the saved state data. void StateCopy_ThawFromMem_Blocking() { _acquire_and_block(); memLoadingState( state_buffer ).FreezeAll(); state_buffer.Dispose(); state_buffer_lock.Release(); } void StateCopy_Clear() { if( state_buffer_lock.IsLocked() ) return; state_buffer.Dispose(); } bool StateCopy_IsBusy() { return state_buffer_lock.IsLocked(); }