From c9c924fe355a1c0bf74e311cab1b2770799a9d13 Mon Sep 17 00:00:00 2001 From: "Jake.Stine" Date: Wed, 16 Sep 2009 17:23:02 +0000 Subject: [PATCH] Yay, craploads of fixups for the new gui: * Lots of crashfixes and threading rules compliance (like using wxYield instead of ProcessPendingEvents) * Killed off some memory corruption * Better error handling and reporting * Much speedier suspend/resume during emulation * Revamped entire savestate system to use a RIFF-style file format (untested, will work on it soon) git-svn-id: http://pcsx2.googlecode.com/svn/trunk@1832 96395faa-99c1-11dd-bbfe-3dabce05a288 --- common/include/Utilities/Exceptions.h | 34 +- common/include/Utilities/HashMap.h | 7 +- common/include/Utilities/SafeArray.h | 11 +- common/include/Utilities/StringHelpers.h | 3 + common/include/Utilities/Threading.h | 62 +-- common/src/Utilities/Console.cpp | 3 +- common/src/Utilities/Exceptions.cpp | 20 +- common/src/Utilities/StringHelpers.cpp | 32 +- common/src/Utilities/ThreadTools.cpp | 132 +++++-- pcsx2/CDVD/CDVD.cpp | 2 +- pcsx2/CDVD/CdRom.cpp | 2 +- pcsx2/Common.h | 1 - pcsx2/Config.h | 20 +- pcsx2/Counters.cpp | 2 +- pcsx2/GS.cpp | 53 +-- pcsx2/GS.h | 22 +- pcsx2/GSState.cpp | 167 ++++++-- pcsx2/Gif.cpp | 11 +- pcsx2/IPU/IPU.cpp | 2 +- pcsx2/IopCounters.cpp | 2 +- pcsx2/IopSio2.cpp | 2 +- pcsx2/MTGS.cpp | 33 +- pcsx2/PluginManager.cpp | 125 ++++-- pcsx2/Plugins.h | 53 ++- pcsx2/PrecompiledHeader.h | 1 + pcsx2/RecoverySystem.cpp | 229 +---------- pcsx2/SPR.cpp | 2 +- pcsx2/SaveState.cpp | 425 +++++++++++---------- pcsx2/SaveState.h | 141 ++++--- pcsx2/Sif.cpp | 2 +- pcsx2/Sio.cpp | 2 +- pcsx2/System.cpp | 365 ++++++------------ pcsx2/System.h | 48 ++- pcsx2/VUmicroMem.cpp | 2 +- pcsx2/VifDma.cpp | 4 +- pcsx2/gui/App.h | 160 ++++++-- pcsx2/gui/AppAssert.cpp | 8 +- pcsx2/gui/AppConfig.cpp | 79 ++-- pcsx2/gui/AppConfig.h | 26 +- pcsx2/gui/AppMain.cpp | 309 +++++++++++---- pcsx2/gui/AppRes.cpp | 4 +- pcsx2/gui/ConsoleLogger.cpp | 11 - pcsx2/gui/ConsoleLogger.h | 2 +- pcsx2/gui/Dialogs/AboutBoxDialog.cpp | 32 +- pcsx2/gui/Dialogs/ConfigurationDialog.cpp | 4 +- pcsx2/gui/Dialogs/ConfigurationDialog.h | 4 +- pcsx2/gui/Dialogs/FirstTimeWizard.cpp | 7 +- pcsx2/gui/Dialogs/LogOptionsDialog.cpp | 4 +- pcsx2/gui/Dialogs/LogOptionsDialog.h | 7 +- pcsx2/gui/Dialogs/ModalPopups.h | 3 +- pcsx2/gui/HostGui.cpp | 20 +- pcsx2/gui/MainFrame.cpp | 6 +- pcsx2/gui/MainMenuClicks.cpp | 66 +--- pcsx2/gui/MemoryCardFile.cpp | 6 - pcsx2/gui/Panels/BiosSelectorPanel.cpp | 13 +- pcsx2/gui/Panels/ConfigurationPanels.h | 44 +-- pcsx2/gui/Panels/CpuPanel.cpp | 4 +- pcsx2/gui/Panels/DirPickerPanel.cpp | 7 +- pcsx2/gui/Panels/GameFixesPanel.cpp | 4 +- pcsx2/gui/Panels/MiscPanelStuff.cpp | 42 +- pcsx2/gui/Panels/PluginSelectorPanel.cpp | 180 +++++---- pcsx2/gui/Panels/SpeedhacksPanel.cpp | 4 +- pcsx2/gui/Panels/VideoPanel.cpp | 2 +- pcsx2/gui/Plugins.cpp | 120 +++++- pcsx2/gui/Saveslots.cpp | 82 +--- pcsx2/gui/wxHelpers.cpp | 20 +- pcsx2/gui/wxHelpers.h | 4 + pcsx2/pcsxAbout.bmp | Bin 251192 -> 0 bytes pcsx2/ps2/BiosTools.cpp | 2 +- pcsx2/ps2/CoreEmuThread.cpp | 51 ++- pcsx2/ps2/CoreEmuThread.h | 27 +- pcsx2/windows/VCprojects/pcsx2_2008.vcproj | 1 - pcsx2_suite_2008.sln | 4 +- plugins/GSdx/GS.h | 5 +- plugins/GSdx/GSdx_vs2008.vcproj | 6 +- 75 files changed, 1736 insertions(+), 1666 deletions(-) delete mode 100644 pcsx2/pcsxAbout.bmp diff --git a/common/include/Utilities/Exceptions.h b/common/include/Utilities/Exceptions.h index cb82652473..6d9b0e39bf 100644 --- a/common/include/Utilities/Exceptions.h +++ b/common/include/Utilities/Exceptions.h @@ -300,7 +300,7 @@ namespace Exception class BadStream : public virtual Stream { public: - DEFINE_STREAM_EXCEPTION( BadStream, wxLt("File data is corrupted or incomplete, or the stream connection closed unexpectedly") ) + DEFINE_STREAM_EXCEPTION( BadStream, wxLt("File data is corrupted or incomplete, or the stream connection closed unexpectedly.") ) }; // A generic exception for odd-ball stream creation errors. @@ -308,7 +308,7 @@ namespace Exception class CreateStream : public virtual Stream { public: - DEFINE_STREAM_EXCEPTION( CreateStream, wxLt("File could not be created or opened") ) + DEFINE_STREAM_EXCEPTION( CreateStream, wxLt("File could not be created or opened.") ) }; // Exception thrown when an attempt to open a non-existent file is made. @@ -317,13 +317,13 @@ namespace Exception class FileNotFound : public virtual CreateStream { public: - DEFINE_STREAM_EXCEPTION( FileNotFound, wxLt("File not found") ) + DEFINE_STREAM_EXCEPTION( FileNotFound, wxLt("File not found.") ) }; class AccessDenied : public virtual CreateStream { public: - DEFINE_STREAM_EXCEPTION( AccessDenied, wxLt("Permission denied to file") ) + DEFINE_STREAM_EXCEPTION( AccessDenied, wxLt("Permission denied to file.") ) }; // EndOfStream can be used either as an error, or used just as a shortcut for manual @@ -332,7 +332,7 @@ namespace Exception class EndOfStream : public virtual Stream { public: - DEFINE_STREAM_EXCEPTION( EndOfStream, wxLt("Unexpected end of file") ); + DEFINE_STREAM_EXCEPTION( EndOfStream, wxLt("Unexpected end of file or stream.") ); }; // --------------------------------------------------------------------------------------- @@ -346,29 +346,7 @@ namespace Exception class BadSavedState : public virtual BadStream { public: - DEFINE_STREAM_EXCEPTION( BadSavedState, wxLt("Savestate data is corrupted or incomplete") ) - }; - - // Exception thrown by SaveState class when a plugin returns an error during state - // load or save. - // - class FreezePluginFailure : public virtual RuntimeError - { - public: - wxString PluginName; // name of the plugin - wxString FreezeAction; - - public: - DEFINE_EXCEPTION_COPYTORS( FreezePluginFailure ) - - explicit FreezePluginFailure( const char* plugin, const char* action ) - { - PluginName = wxString::FromUTF8( plugin ); - FreezeAction = wxString::FromUTF8( action ); - } - - virtual wxString FormatDiagnosticMessage() const; - virtual wxString FormatDisplayMessage() const; + DEFINE_STREAM_EXCEPTION( BadSavedState, wxLt("Savestate data is corrupted or incomplete.") ) }; // thrown when the savestate being loaded isn't supported. diff --git a/common/include/Utilities/HashMap.h b/common/include/Utilities/HashMap.h index 544bd84498..dd1aa22ef5 100644 --- a/common/include/Utilities/HashMap.h +++ b/common/include/Utilities/HashMap.h @@ -550,6 +550,9 @@ template< class Key, class T > class HashMap : public google::dense_hash_map { public: + using dense_hash_map::operator[]; + using dense_hash_map::const_iterator; + virtual ~HashMap() {} /// @@ -574,13 +577,13 @@ public: /// parameter. This is a more favorable alternative to the indexer operator since the /// indexer implementation can and will create new entries for every request that /// - /*void TryGetValue( const Key& key, T& outval ) const + void TryGetValue( const Key& key, T& outval ) const { // See above class for notes on why this is commented out. const_iterator iter = find( key ); if( iter != end() ) outval = iter->second; - }*/ + } const T& GetValue( Key key ) const { diff --git a/common/include/Utilities/SafeArray.h b/common/include/Utilities/SafeArray.h index 68ffe44f6e..27cf276436 100644 --- a/common/include/Utilities/SafeArray.h +++ b/common/include/Utilities/SafeArray.h @@ -248,7 +248,7 @@ protected: } public: - virtual ~SafeList() + virtual ~SafeList() throw() { safe_free( m_ptr ); } @@ -287,6 +287,11 @@ public: // The actual size of the allocation may differ. int GetSizeInBytes() const { return m_length * sizeof(T); } + void MatchLengthToAllocatedSize() + { + m_length = m_allocsize; + } + // Ensures that the allocation is large enough to fit data of the // amount requested. The memory allocation is not resized smaller. void MakeRoomFor( int blockSize ) @@ -398,7 +403,9 @@ protected: } }; -////////////////////////////////////////////////////////////////////////////////////////// +// -------------------------------------------------------------------------------------- +// SafeAlignedArray class +// -------------------------------------------------------------------------------------- // Handy little class for allocating a resizable memory block, complete with // exception-based error handling and automatic cleanup. // This one supports aligned data allocations too! diff --git a/common/include/Utilities/StringHelpers.h b/common/include/Utilities/StringHelpers.h index 9a0a6d52d8..0e60e8f79e 100644 --- a/common/include/Utilities/StringHelpers.h +++ b/common/include/Utilities/StringHelpers.h @@ -29,6 +29,9 @@ extern const wxRect wxDefaultRect; // This should prove useful.... #define wxsFormat wxString::Format +extern void SplitString( wxArrayString& dest, const wxString& src, const wxString& delims ); +extern void JoinString( wxString& dest, const wxArrayString& src, const wxString& separator ); + extern wxString ToString( const wxPoint& src, const wxString& separator=L"," ); extern wxString ToString( const wxSize& src, const wxString& separator=L"," ); extern wxString ToString( const wxRect& src, const wxString& separator=L"," ); diff --git a/common/include/Utilities/Threading.h b/common/include/Utilities/Threading.h index 0717a01217..4d64b104ba 100644 --- a/common/include/Utilities/Threading.h +++ b/common/include/Utilities/Threading.h @@ -130,7 +130,7 @@ namespace Threading volatile long m_running; // set true by Start(), and set false by Cancel(), Block(), etc. public: - virtual ~PersistentThread(); + virtual ~PersistentThread() throw(); PersistentThread(); virtual void Start(); @@ -172,7 +172,7 @@ namespace Threading bool m_IsLocked; public: - virtual ~ScopedLock() + virtual ~ScopedLock() throw() { if( m_IsLocked ) m_lock.Unlock(); @@ -239,77 +239,35 @@ namespace Threading { protected: volatile bool m_Done; - volatile bool m_TaskComplete; + volatile bool m_TaskPending; Semaphore m_post_TaskComplete; + MutexLock m_lock_TaskComplete; public: - virtual ~BaseTaskThread() {} + virtual ~BaseTaskThread() throw() {} BaseTaskThread() : m_Done( false ) - , m_TaskComplete( false ) + , m_TaskPending( false ) , m_post_TaskComplete() { } - // Tells the thread to exit and then waits for thread termination. - sptr Block() - { - if( !m_running ) return m_returncode; - m_Done = true; - m_sem_event.Post(); - return PersistentThread::Block(); - } - - // Initiates the new task. This should be called after your own StartTask has - // initialized internal variables / preparations for task execution. - void PostTask() - { - jASSUME( m_running ); - m_TaskComplete = false; - m_post_TaskComplete.Reset(); - m_sem_event.Post(); - } - - // Blocks current thread execution pending the completion of the parallel task. - void WaitForResult() - { - if( !m_running ) return; - if( !m_TaskComplete ) - m_post_TaskComplete.Wait(); - else - m_post_TaskComplete.Reset(); - } + sptr Block(); + void PostTask(); + void WaitForResult(); protected: // Abstract method run when a task has been posted. Implementing classes should do // all your necessary processing work here. virtual void Task()=0; - sptr ExecuteTask() - { - do - { - // Wait for a job! - m_sem_event.Wait(); - - if( m_Done ) break; - Task(); - m_TaskComplete = true; - m_post_TaskComplete.Post(); - } while( !m_Done ); - - return 0; - } + sptr ExecuteTask(); }; ////////////////////////////////////////////////////////////////////////////////////////// // Our fundamental interlocking functions. All other useful interlocks can be derived // from these little beasties! - extern long pcsx2_InterlockedExchange(volatile long* Target, long srcval); - extern long pcsx2_InterlockedCompareExchange( volatile long* target, long srcval, long comp ); - extern long pcsx2_InterlockedExchangeAdd( volatile long* target, long addval ); - extern void AtomicExchange( volatile u32& Target, u32 value ); extern void AtomicExchangeAdd( volatile u32& Target, u32 value ); extern void AtomicIncrement( volatile u32& Target ); diff --git a/common/src/Utilities/Console.cpp b/common/src/Utilities/Console.cpp index 177fe586e1..b1afd1ca14 100644 --- a/common/src/Utilities/Console.cpp +++ b/common/src/Utilities/Console.cpp @@ -52,8 +52,7 @@ namespace Console { std::string m_format_buffer; vssprintf( m_format_buffer, fmt, args ); - m_format_buffer += "\n"; - Write( wxString::FromUTF8( m_format_buffer.c_str() ) ); + WriteLn( wxString::FromUTF8( m_format_buffer.c_str() ) ); } __forceinline void __fastcall _WriteLn( Colors color, const char* fmt, va_list args ) diff --git a/common/src/Utilities/Exceptions.cpp b/common/src/Utilities/Exceptions.cpp index 0a1b6b819a..bbfe5d0d2d 100644 --- a/common/src/Utilities/Exceptions.cpp +++ b/common/src/Utilities/Exceptions.cpp @@ -104,29 +104,15 @@ namespace Exception wxString Stream::FormatDiagnosticMessage() const { return wxsFormat( - L"Stream exception: %s\n\tObject name: %s", + L"Stream exception: %s\n\tFile/Object: %s", m_message_diag.c_str(), StreamName.c_str() ) + m_stacktrace; } wxString Stream::FormatDisplayMessage() const { - return m_message_user + L"\n\n" + StreamName; - } - - // ------------------------------------------------------------------------ - wxString FreezePluginFailure::FormatDiagnosticMessage() const - { - return wxsFormat( - L"%s plugin returned an error while %s the state.\n\n", - PluginName.c_str(), - FreezeAction.c_str() - ) + m_stacktrace; - } - - wxString FreezePluginFailure::FormatDisplayMessage() const - { - return m_message_user; + return m_message_user + L"\n\n" + + wxsFormat( _("Name: %s"), StreamName.c_str() ); } // ------------------------------------------------------------------------ diff --git a/common/src/Utilities/StringHelpers.cpp b/common/src/Utilities/StringHelpers.cpp index 80084e4d71..a6eb8b01c2 100644 --- a/common/src/Utilities/StringHelpers.cpp +++ b/common/src/Utilities/StringHelpers.cpp @@ -17,7 +17,6 @@ const wxRect wxDefaultRect( wxDefaultCoord, wxDefaultCoord, wxDefaultCoord, wxDefaultCoord ); -////////////////////////////////////////////////////////////////////////////////////////// // Splits a string into parts and adds the parts into the given SafeList. // This list is not cleared, so concatenating many splits into a single large list is // the 'default' behavior, unless you manually clear the SafeList prior to subsequent calls. @@ -31,7 +30,13 @@ void SplitString( SafeList& dest, const wxString& src, const wxString& dest.Add( parts.GetNextToken() ); } -////////////////////////////////////////////////////////////////////////////////////////// +void SplitString( wxArrayString& dest, const wxString& src, const wxString& delims ) +{ + wxStringTokenizer parts( src, delims ); + while( parts.HasMoreTokens() ) + dest.Add( parts.GetNextToken() ); +} + // Joins a list of strings into one larger string, using the given string concatenation // character as a separator. If you want to be able to split the string later then the // concatenation string needs to be a single character. @@ -48,7 +53,16 @@ void JoinString( wxString& dest, const SafeList& src, const wxString& } } -////////////////////////////////////////////////////////////////////////////////////////// +void JoinString( wxString& dest, const wxArrayString& src, const wxString& separator ) +{ + for( int i=0, len=src.GetCount(); i 0 ) + { + multiple--; + sem_post( &sema ); + } #endif + } #if wxUSE_GUI // This is a wxApp-safe implementation of Wait, which makes sure and executes the App's @@ -278,7 +348,7 @@ namespace Threading // to handle messages. do { - wxTheApp->ProcessPendingEvents(); + wxTheApp->Yield(); } while( !Wait( ts_msec_250 ) ); } } @@ -297,7 +367,7 @@ namespace Threading // to handle messages. do { - wxTheApp->ProcessPendingEvents(); + wxTheApp->Yield(); if( Wait( ts_msec_250 ) ) break; countdown -= ts_msec_250; } while( countdown.GetMilliseconds() > 0 ); @@ -342,6 +412,10 @@ namespace Threading return retval; } +// -------------------------------------------------------------------------------------- +// MutexLock Implementations +// -------------------------------------------------------------------------------------- + MutexLock::MutexLock() { int err = 0; @@ -383,74 +457,58 @@ namespace Threading pthread_mutex_unlock( &mutex ); } - ////////////////////////////////////////////////////////////////////// - // define some overloads for InterlockedExchanges - // for commonly used types, like u32 and s32. - - __forceinline long pcsx2_InterlockedExchange( volatile long* target, long srcval ) - { - return _InterlockedExchange( target, srcval ); - } - - __forceinline long pcsx2_InterlockedCompareExchange( volatile long* target, long srcval, long comp ) - { - // Use the pthreads-win32 implementation... - return _InterlockedCompareExchange( target, srcval, comp ); - } - - __forceinline long pcsx2_InterlockedExchangeAdd( volatile long* target, long srcval ) - { - return _InterlockedExchangeAdd( target, srcval ); - } +// -------------------------------------------------------------------------------------- +// InterlockedExchanges / AtomicExchanges (PCSX2's Helper versions) +// -------------------------------------------------------------------------------------- +// define some overloads for InterlockedExchanges for commonly used types, like u32 and s32. __forceinline void AtomicExchange( volatile u32& Target, u32 value ) { - pcsx2_InterlockedExchange( (volatile long*)&Target, value ); + _InterlockedExchange( (volatile long*)&Target, value ); } __forceinline void AtomicExchangeAdd( volatile u32& Target, u32 value ) { - pcsx2_InterlockedExchangeAdd( (volatile long*)&Target, value ); + _InterlockedExchangeAdd( (volatile long*)&Target, value ); } __forceinline void AtomicIncrement( volatile u32& Target ) { - pcsx2_InterlockedExchangeAdd( (volatile long*)&Target, 1 ); + _InterlockedExchangeAdd( (volatile long*)&Target, 1 ); } __forceinline void AtomicDecrement( volatile u32& Target ) { - pcsx2_InterlockedExchangeAdd( (volatile long*)&Target, -1 ); + _InterlockedExchangeAdd( (volatile long*)&Target, -1 ); } __forceinline void AtomicExchange( volatile s32& Target, s32 value ) { - pcsx2_InterlockedExchange( (volatile long*)&Target, value ); + _InterlockedExchange( (volatile long*)&Target, value ); } __forceinline void AtomicExchangeAdd( s32& Target, u32 value ) { - pcsx2_InterlockedExchangeAdd( (volatile long*)&Target, value ); + _InterlockedExchangeAdd( (volatile long*)&Target, value ); } __forceinline void AtomicIncrement( volatile s32& Target ) { - pcsx2_InterlockedExchangeAdd( (volatile long*)&Target, 1 ); + _InterlockedExchangeAdd( (volatile long*)&Target, 1 ); } __forceinline void AtomicDecrement( volatile s32& Target ) { - pcsx2_InterlockedExchangeAdd( (volatile long*)&Target, -1 ); + _InterlockedExchangeAdd( (volatile long*)&Target, -1 ); } __forceinline void _AtomicExchangePointer( const void ** target, const void* value ) { - pcsx2_InterlockedExchange( (volatile long*)target, (long)value ); + _InterlockedExchange( (volatile long*)target, (long)value ); } __forceinline void _AtomicCompareExchangePointer( const void ** target, const void* value, const void* comparand ) { - pcsx2_InterlockedCompareExchange( (volatile long*)target, (long)value, (long)comparand ); + _InterlockedCompareExchange( (volatile long*)target, (long)value, (long)comparand ); } - } diff --git a/pcsx2/CDVD/CDVD.cpp b/pcsx2/CDVD/CDVD.cpp index 9b14f0934f..9a0dc887c2 100644 --- a/pcsx2/CDVD/CDVD.cpp +++ b/pcsx2/CDVD/CDVD.cpp @@ -483,7 +483,7 @@ struct Freeze_v10Compat bool Spinning; }; -void SaveState::cdvdFreeze() +void SaveStateBase::cdvdFreeze() { FreezeTag( "cdvd" ); Freeze( cdvd ); diff --git a/pcsx2/CDVD/CdRom.cpp b/pcsx2/CDVD/CdRom.cpp index b05817e12c..af671ee5f8 100644 --- a/pcsx2/CDVD/CdRom.cpp +++ b/pcsx2/CDVD/CdRom.cpp @@ -961,7 +961,7 @@ void cdrReset() { cdReadTime = (PSXCLK / 1757) * BIAS; } -void SaveState::cdrFreeze() +void SaveStateBase::cdrFreeze() { FreezeTag( "cdrom" ); Freeze(cdr); diff --git a/pcsx2/Common.h b/pcsx2/Common.h index 9494ddafee..3179b9dced 100644 --- a/pcsx2/Common.h +++ b/pcsx2/Common.h @@ -40,6 +40,5 @@ // Some homeless externs. This is as good a spot as any for now... -extern bool EmulationInProgress(); extern void SetCPUState(u32 sseMXCSR, u32 sseVUMXCSR); extern u32 g_sseVUMXCSR, g_sseMXCSR; diff --git a/pcsx2/Config.h b/pcsx2/Config.h index 8a10ad1338..69f89b5112 100644 --- a/pcsx2/Config.h +++ b/pcsx2/Config.h @@ -46,13 +46,15 @@ enum PluginsEnum_t u32 bitset; \ struct { +#define BITFIELD_END }; }; + //------------ DEFAULT sseMXCSR VALUES --------------- #define DEFAULT_sseMXCSR 0xffc0 //FPU rounding > DaZ, FtZ, "chop" #define DEFAULT_sseVUMXCSR 0xffc0 //VU rounding > DaZ, FtZ, "chop" -///////////////////////////////////////////////////////////////////////////////////////// -// Pcsx2Config -// +// -------------------------------------------------------------------------------------- +// Pcsx2Config class +// -------------------------------------------------------------------------------------- // This is intended to be a public class library between the core emulator and GUI only. // It is *not* meant to be shared data between core emulation and plugins, due to issues // with version incompatibilities if the structure formats are changed. @@ -75,7 +77,7 @@ public: RecBlocks_IOP:1, // Enables per-block profiling for the IOP recompiler [unimplemented] RecBlocks_VU0:1, // Enables per-block profiling for the VU0 recompiler [unimplemented] RecBlocks_VU1:1; // Enables per-block profiling for the VU1 recompiler [unimplemented] - }; }; + BITFIELD_END // Default is Disabled, with all recs enabled underneath. ProfilerOptions() : bitset( 0xfffffffe ) {} @@ -105,7 +107,7 @@ public: bool UseMicroVU0:1, UseMicroVU1:1; - }; }; + BITFIELD_END // All recs are enabled by default. RecompilerOptions() : bitset( 0xffffffff ) { } @@ -142,7 +144,7 @@ public: fpuOverflow:1, fpuExtraOverflow:1, fpuFullMode:1; - }; }; + BITFIELD_END CpuOptions(); void LoadSave( IniInterface& conf ); @@ -213,7 +215,7 @@ public: DMAExeHack:1, // Fix for Fatal Frame; breaks Gust and Tri-Ace games. XgKickHack:1, // Fix for Erementar Gerad, adds more delay to VU XGkick instructions. Corrects the color of some graphics, but breaks Tri-ace games and others. MpegHack:1; // Fix for Mana Khemia 1, breaks Digital Devil Saga. - }; }; + BITFIELD_END // all gamefixes are disabled by default. GamefixOptions() : bitset( 0 ) {} @@ -240,7 +242,7 @@ public: BIFC0:1, // enables BIFC0 detection and fast-forwarding vuFlagHack:1, // microVU specific flag hack; Can cause Infinite loops, SPS, etc... vuMinMax:1; // microVU specific MinMax hack; Can cause SPS, Black Screens, etc... - }; }; + BITFIELD_END u8 EECycleRate; // EE cycle rate selector (1.0, 1.5, 2.0) u8 VUCycleSteal; // VU Cycle Stealer factor (0, 1, 2, or 3) @@ -275,7 +277,7 @@ public: MultitapPort0_Enabled:1, MultitapPort1_Enabled:1; - }; }; + BITFIELD_END CpuOptions Cpu; VideoOptions Video; diff --git a/pcsx2/Counters.cpp b/pcsx2/Counters.cpp index 2c3b3eb690..6ed274e5d4 100644 --- a/pcsx2/Counters.cpp +++ b/pcsx2/Counters.cpp @@ -798,7 +798,7 @@ __forceinline u32 rcntCycle(int index) return counters[index].count; } -void SaveState::rcntFreeze() +void SaveStateBase::rcntFreeze() { Freeze( counters ); Freeze( hsyncCounter ); diff --git a/pcsx2/GS.cpp b/pcsx2/GS.cpp index 7a0013031f..eda419d6fb 100644 --- a/pcsx2/GS.cpp +++ b/pcsx2/GS.cpp @@ -40,57 +40,6 @@ static s64 m_iSlowTicks=0; static bool m_justSkipped = false; static bool m_StrictSkipping = false; -#ifdef PCSX2_DEVBUILD - -// GS Playback -int g_SaveGSStream = 0; // save GS stream; 1 - prepare, 2 - save -int g_nLeftGSFrames = 0; // when saving, number of frames left -gzSavingState* g_fGSSave; - -// fixme - need to take this concept and make it MTGS friendly. -#ifdef _STGS_GSSTATE_CODE -void GSGIFTRANSFER1(u32 *pMem, u32 addr) { - if( g_SaveGSStream == 2) { - u32 type = GSRUN_TRANS1; - u32 size = (0x4000-(addr))/16; - g_fGSSave->Freeze( type ); - g_fGSSave->Freeze( size ); - g_fGSSave->FreezeMem( ((u8*)pMem)+(addr), size*16 ); - } - GSgifTransfer1(pMem, addr); -} - -void GSGIFTRANSFER2(u32 *pMem, u32 size) { - if( g_SaveGSStream == 2) { - u32 type = GSRUN_TRANS2; - u32 _size = size; - g_fGSSave->Freeze( type ); - g_fGSSave->Freeze( size ); - g_fGSSave->FreezeMem( pMem, _size*16 ); - } - GSgifTransfer2(pMem, size); -} - -void GSGIFTRANSFER3(u32 *pMem, u32 size) { - if( g_SaveGSStream == 2 ) { - u32 type = GSRUN_TRANS3; - u32 _size = size; - g_fGSSave->Freeze( type ); - g_fGSSave->Freeze( size ); - g_fGSSave->FreezeMem( pMem, _size*16 ); - } - GSgifTransfer3(pMem, size); -} - -__forceinline void GSVSYNC(void) { - if( g_SaveGSStream == 2 ) { - u32 type = GSRUN_VSYNC; - g_fGSSave->Freeze( type ); - } -} -#endif -#endif - void _gs_ChangeTimings( u32 framerate, u32 iTicks ) { m_iSlowStart = GetCPUTicks(); @@ -615,7 +564,7 @@ void gsDynamicSkipEnable() frameLimitReset(); } -void SaveState::gsFreeze() +void SaveStateBase::gsFreeze() { FreezeMem(PS2MEM_GS, 0x2000); Freeze(CSRw); diff --git a/pcsx2/GS.h b/pcsx2/GS.h index 23a36609e5..b9a78e1e19 100644 --- a/pcsx2/GS.h +++ b/pcsx2/GS.h @@ -135,9 +135,16 @@ enum GS_RINGTYPE , GS_RINGTYPE_QUIT }; + +struct MTGS_FreezeData +{ + freezeData* fdata; + s32 retval; // value returned from the call, valid only after an mtgsWaitGS() +}; + class mtgsThreadObject : public Threading::PersistentThread { - friend class SaveState; + friend class SaveStateBase; protected: // Size of the ringbuffer as a power of 2 -- size is a multiple of simd128s. @@ -192,6 +199,10 @@ protected: #endif // the MTGS "dummy" GIFtag info! + // fixme: The real PS2 has a single internal PATH and 3 logical sources, not 3 entirely + // separate paths. But for that to work properly we need also interlocked path sources. + // That is, when the GIF selects a source, it sticks to that source until an EOP. Currently + // this is not emulated! GIFPath m_path[3]; // contains aligned memory allocations for gs and Ringbuffer. @@ -203,7 +214,7 @@ protected: public: mtgsThreadObject(); - virtual ~mtgsThreadObject(); + virtual ~mtgsThreadObject() throw(); void Start(); void Cancel(); @@ -222,13 +233,11 @@ public: void SendPointerPacket( GS_RINGTYPE type, u32 data0, void* data1 ); u8* GetDataPacketPtr() const; - void Freeze( SaveState& state ); + void Freeze( SaveStateBase& state ); void SetEvent(); void PostVsyncEnd( bool updategs ); - void _close_gs(); - protected: // Saves MMX/XMM regs, posts an event to the mtgsThread flag and releases a timeslice. // For use in surrounding loops that wait on the mtgs. @@ -331,11 +340,10 @@ enum gsrun extern int g_SaveGSStream; extern int g_nLeftGSFrames; -extern gzSavingState* g_fGSSave; #endif extern void SaveGSState(const wxString& file); extern void LoadGSState(const wxString& file); -extern void RunGSState(gzLoadingState& f); +extern void RunGSState( memLoadingState& f ); diff --git a/pcsx2/GSState.cpp b/pcsx2/GSState.cpp index 6236e78748..e4be0e704a 100644 --- a/pcsx2/GSState.cpp +++ b/pcsx2/GSState.cpp @@ -19,6 +19,55 @@ #include #ifdef PCSX2_DEVBUILD + +// GS Playback +int g_SaveGSStream = 0; // save GS stream; 1 - prepare, 2 - save +int g_nLeftGSFrames = 0; // when saving, number of frames left +static wxScopedPtr g_fGSSave; + +// fixme - need to take this concept and make it MTGS friendly. +#ifdef _STGS_GSSTATE_CODE +void GSGIFTRANSFER1(u32 *pMem, u32 addr) { + if( g_SaveGSStream == 2) { + u32 type = GSRUN_TRANS1; + u32 size = (0x4000-(addr))/16; + g_fGSSave->Freeze( type ); + g_fGSSave->Freeze( size ); + g_fGSSave->FreezeMem( ((u8*)pMem)+(addr), size*16 ); + } + GSgifTransfer1(pMem, addr); +} + +void GSGIFTRANSFER2(u32 *pMem, u32 size) { + if( g_SaveGSStream == 2) { + u32 type = GSRUN_TRANS2; + u32 _size = size; + g_fGSSave->Freeze( type ); + g_fGSSave->Freeze( size ); + g_fGSSave->FreezeMem( pMem, _size*16 ); + } + GSgifTransfer2(pMem, size); +} + +void GSGIFTRANSFER3(u32 *pMem, u32 size) { + if( g_SaveGSStream == 2 ) { + u32 type = GSRUN_TRANS3; + u32 _size = size; + g_fGSSave->Freeze( type ); + g_fGSSave->Freeze( size ); + g_fGSSave->FreezeMem( pMem, _size*16 ); + } + GSgifTransfer3(pMem, size); +} + +__forceinline void GSVSYNC(void) { + if( g_SaveGSStream == 2 ) { + u32 type = GSRUN_VSYNC; + g_fGSSave->Freeze( type ); + } +} +#endif + void SaveGSState(const wxString& file) { if( g_SaveGSStream ) return; @@ -26,7 +75,8 @@ void SaveGSState(const wxString& file) Console::WriteLn( "Saving GS State..." ); Console::WriteLn( wxsFormat( L"\t%s", file.c_str() ) ); - g_fGSSave = new gzSavingState( file ); + SafeArray buf; + g_fGSSave.reset( new memSavingState( buf ) ); g_SaveGSStream = 1; g_nLeftGSFrames = 2; @@ -37,46 +87,35 @@ void SaveGSState(const wxString& file) void LoadGSState(const wxString& file) { int ret; - gzLoadingState* f; Console::Status( "Loading GS State..." ); - try - { - f = new gzLoadingState( file ); - } - catch( Exception::FileNotFound& ) - { - // file not found? try prefixing with sstates folder: - if( !Path::IsRelative( file ) ) - { - //f = new gzLoadingState( Path::Combine( g_Conf->Folders.Savestates, file ) ); + wxString src( file ); - // If this load attempt fails, then let the exception bubble up to - // the caller to deal with... - } - } + /*if( !wxFileName::FileExists( src ) ) + src = Path::Combine( g_Conf->Folders.Savestates, src );*/ + + if( !wxFileName::FileExists( src ) ) + return; + + SafeArray buf; + memLoadingState f( buf ); // Always set gsIrq callback -- GS States are always exclusionary of MTGS mode GSirqCallback( gsIrq ); ret = GSopen(&pDsp, "PCSX2", 0); if (ret != 0) - { - delete f; throw Exception::PluginOpenError( PluginId_GS ); - } ret = PADopen((void *)&pDsp); - f->Freeze(g_nLeftGSFrames); - f->gsFreeze(); + f.Freeze(g_nLeftGSFrames); + f.gsFreeze(); - f->FreezePlugin( "GS", gsSafeFreeze ); + GetPluginManager().Freeze( PluginId_GS, f ); - RunGSState( *f ); - - delete( f ); + RunGSState( f ); g_plugins->Close( PluginId_GS ); g_plugins->Close( PluginId_PAD ); @@ -90,12 +129,12 @@ struct GSStatePacket // runs the GS // (this should really be part of the AppGui) -void RunGSState( gzLoadingState& f ) +void RunGSState( memLoadingState& f ) { u32 newfield; std::list< GSStatePacket > packets; - while( !f.Finished() ) + while( !f.IsFinished() ) { int type, size; f.Freeze( type ); @@ -152,3 +191,77 @@ void RunGSState( gzLoadingState& f ) } } #endif + +////////////////////////////////////////////////////////////////////////////////////////// +// +void vSyncDebugStuff( uint frame ) +{ +#ifdef OLD_TESTBUILD_STUFF + if( g_TestRun.enabled && g_TestRun.frame > 0 ) { + if( frame > g_TestRun.frame ) { + // take a snapshot + if( g_TestRun.pimagename != NULL && GSmakeSnapshot2 != NULL ) { + if( g_TestRun.snapdone ) { + g_TestRun.curimage++; + g_TestRun.snapdone = 0; + g_TestRun.frame += 20; + if( g_TestRun.curimage >= g_TestRun.numimages ) { + // exit + g_EmuThread->Cancel(); + } + } + else { + // query for the image + GSmakeSnapshot2(g_TestRun.pimagename, &g_TestRun.snapdone, g_TestRun.jpgcapture); + } + } + else { + // exit + g_EmuThread->Cancel(); + } + } + } + + GSVSYNC(); + + if( g_SaveGSStream == 1 ) { + freezeData fP; + + g_SaveGSStream = 2; + g_fGSSave->gsFreeze(); + + if (GSfreeze(FREEZE_SIZE, &fP) == -1) { + safe_delete( g_fGSSave ); + g_SaveGSStream = 0; + } + else { + fP.data = (s8*)malloc(fP.size); + if (fP.data == NULL) { + safe_delete( g_fGSSave ); + g_SaveGSStream = 0; + } + else { + if (GSfreeze(FREEZE_SAVE, &fP) == -1) { + safe_delete( g_fGSSave ); + g_SaveGSStream = 0; + } + else { + g_fGSSave->Freeze( fP.size ); + if (fP.size) { + g_fGSSave->FreezeMem( fP.data, fP.size ); + free(fP.data); + } + } + } + } + } + else if( g_SaveGSStream == 2 ) { + + if( --g_nLeftGSFrames <= 0 ) { + safe_delete( g_fGSSave ); + g_SaveGSStream = 0; + Console::WriteLn("Done saving GS stream"); + } + } +#endif +} diff --git a/pcsx2/Gif.cpp b/pcsx2/Gif.cpp index 91711176ff..3498b088f4 100644 --- a/pcsx2/Gif.cpp +++ b/pcsx2/Gif.cpp @@ -101,15 +101,6 @@ static u32 WRITERING_DMA(u32 *pMem, u32 qwc) int size = mtgsThread->PrepDataPacket(GIF_PATH_3, pMem, qwc); u8* pgsmem = mtgsThread->GetDataPacketPtr(); - /* check if page of endmem is valid (dark cloud2) */ - // fixme: this hack makes no sense, because the giftagDummy will - // process the full length of bytes regardess of how much we copy. - // So you'd think if we're truncating the copy to prevent DEPs, we - // should truncate the gif packet size too.. (air) - - // fixed? PrepDataPacket now returns the actual size of the packet. - // VIF handles scratchpad wrapping also, so this code shouldn't be needed anymore. - memcpy_aligned(pgsmem, pMem, size<<4); mtgsThread->SendDataPacket(); @@ -572,7 +563,7 @@ void gifMFIFOInterrupt() clearFIFOstuff(false); } -void SaveState::gifFreeze() +void SaveStateBase::gifFreeze() { FreezeTag( "GIFdma" ); diff --git a/pcsx2/IPU/IPU.cpp b/pcsx2/IPU/IPU.cpp index 7a4b4c8ca7..42d4dc0c12 100644 --- a/pcsx2/IPU/IPU.cpp +++ b/pcsx2/IPU/IPU.cpp @@ -195,7 +195,7 @@ void ReportIPU() } // fixme - ipuFreeze looks fairly broken. Should probably take a closer look at some point. -void SaveState::ipuFreeze() +void SaveStateBase::ipuFreeze() { IPUProcessInterrupt(); diff --git a/pcsx2/IopCounters.cpp b/pcsx2/IopCounters.cpp index f3520ca448..33a194dae1 100644 --- a/pcsx2/IopCounters.cpp +++ b/pcsx2/IopCounters.cpp @@ -748,7 +748,7 @@ void psxRcntSetGates() psxvblankgate &= ~(1<<3); } -void SaveState::psxRcntFreeze() +void SaveStateBase::psxRcntFreeze() { FreezeTag( "iopCounters" ); diff --git a/pcsx2/IopSio2.cpp b/pcsx2/IopSio2.cpp index 1c28f259ca..89328a07cc 100644 --- a/pcsx2/IopSio2.cpp +++ b/pcsx2/IopSio2.cpp @@ -199,7 +199,7 @@ u8 sio2_fifoOut(){ return 0; // No Data } -void SaveState::sio2Freeze() +void SaveStateBase::sio2Freeze() { FreezeTag( "sio2" ); Freeze(sio2); diff --git a/pcsx2/MTGS.cpp b/pcsx2/MTGS.cpp index 09b9d08542..d2e53cadf7 100644 --- a/pcsx2/MTGS.cpp +++ b/pcsx2/MTGS.cpp @@ -78,7 +78,7 @@ __forceinline void GIFPath::SetTag(const void* mem) curreg = 0; } -static void _mtgsFreezeGIF( SaveState& state, GIFPath (&paths)[3] ) +static void _mtgsFreezeGIF( SaveStateBase& state, GIFPath (&paths)[3] ) { for(int i=0; i<3; i++ ) { @@ -92,7 +92,7 @@ static void _mtgsFreezeGIF( SaveState& state, GIFPath (&paths)[3] ) } } -void SaveState::mtgsFreeze() +void SaveStateBase::mtgsFreeze() { FreezeTag( "mtgs" ); mtgsThread->Freeze( *this ); @@ -388,17 +388,12 @@ struct PacketTagType extern bool renderswitch; -void mtgsThreadObject::_close_gs() +static void _clean_close_gs( void* obj ) { if( g_plugins != NULL ) g_plugins->m_info[PluginId_GS].CommonBindings.Close(); } -static void _clean_close_gs( void* obj ) -{ - ((mtgsThreadObject*)obj)->_close_gs(); -} - void mtgsThreadObject::_RingbufferLoop() { pthread_cleanup_push( _clean_close_gs, this ); @@ -510,9 +505,9 @@ void mtgsThreadObject::_RingbufferLoop() case GS_RINGTYPE_FREEZE: { - freezeData* data = (freezeData*)(*(uptr*)&tag.data[1]); + MTGS_FreezeData* data = (MTGS_FreezeData*)(*(uptr*)&tag.data[1]); int mode = tag.data[0]; - GetPluginManager().Freeze( PluginId_GS, mode, data ); + data->retval = GetPluginManager().DoFreeze( PluginId_GS, mode, data->fdata ); break; } @@ -576,11 +571,17 @@ void mtgsThreadObject::_RingbufferLoop() pthread_cleanup_pop( true ); } +static void dummyIrqCallback() +{ + // dummy, because MTGS doesn't need this mess! + // (and zerogs does >_<) +} + sptr mtgsThreadObject::ExecuteTask() { memcpy_aligned( m_gsMem, PS2MEM_GS, sizeof(PS2MEM_GS) ); GSsetBaseMem( m_gsMem ); - GSirqCallback( NULL ); + GSirqCallback( dummyIrqCallback ); Console::WriteLn( (wxString)L"\t\tForced software switch: " + (renderswitch ? L"Enabled" : L"Disabled") ); m_returncode = GSopen( (void*)&pDsp, "PCSX2", renderswitch ? 2 : 1 ); @@ -781,14 +782,6 @@ int mtgsThreadObject::PrepDataPacket( GIF_PATH pathidx, const u8* srcdata, u32 s jASSUME( size < m_RingBufferSize ); jASSUME( writepos < m_RingBufferSize ); - //fixme: Vif sometimes screws up and size is unaligned, try this then (rama) - //Is this still a problem? It should be fixed on the specific VIF command now. (air) - //It seems to be fixed in Fatal Frame, leaving the code here still in case we get that again (rama) - /*if( (size&15) != 0){ - Console::Error( "MTGS problem, size unaligned"); - size = (size+15)&(~15); - }*/ - m_packet_size = gifTransferDummy(pathidx, srcdata, size); size = m_packet_size + 1; // takes into account our command qword. @@ -1009,7 +1002,7 @@ void mtgsThreadObject::GIFSoftReset( int mask ) mtgsThread->SendSimplePacket( GS_RINGTYPE_SOFTRESET, mask, 0, 0 ); } -void mtgsThreadObject::Freeze( SaveState& state ) +void mtgsThreadObject::Freeze( SaveStateBase& state ) { _mtgsFreezeGIF( state, this->m_path ); } diff --git a/pcsx2/PluginManager.cpp b/pcsx2/PluginManager.cpp index 88ab7dcb61..74693e5e39 100644 --- a/pcsx2/PluginManager.cpp +++ b/pcsx2/PluginManager.cpp @@ -593,6 +593,34 @@ wxString Exception::PluginError::FormatDisplayMessage() const return wxsFormat( m_message_user, tbl_PluginInfo[PluginId].GetShortname().c_str() ); } +wxString Exception::FreezePluginFailure::FormatDiagnosticMessage() const +{ + return wxsFormat( + L"%s plugin returned an error while saving the state.\n\n", + tbl_PluginInfo[PluginId].shortname + ) + m_stacktrace; +} + +wxString Exception::FreezePluginFailure::FormatDisplayMessage() const +{ + // [TODO] + return m_message_user; +} + +wxString Exception::ThawPluginFailure::FormatDiagnosticMessage() const +{ + return wxsFormat( + L"%s plugin returned an error while loading the state.\n\n", + tbl_PluginInfo[PluginId].shortname + ) + m_stacktrace; +} + +wxString Exception::ThawPluginFailure::FormatDisplayMessage() const +{ + // [TODO] + return m_message_user; +} + // -------------------------------------------------------------------------------------- // PCSX2 Callbacks passed to Plugins // -------------------------------------------------------------------------------------- @@ -702,6 +730,8 @@ PluginManager::PluginManager( const wxString (&folders)[PluginId_Count] ) // fixme: use plugin's GetLastError (not implemented yet!) throw Exception::PluginLoadError( PluginId_Mcd, wxEmptyString, "Internal Memorycard Plugin failed to load." ); } + + g_plugins = this; } PluginManager::~PluginManager() @@ -713,6 +743,9 @@ PluginManager::~PluginManager() } DESTRUCTOR_CATCHALL // All library unloading done automatically. + + if( g_plugins == this ) + g_plugins = NULL; } void PluginManager::BindCommon( PluginsEnum_t pid ) @@ -890,7 +923,7 @@ void PluginManager::Open() void PluginManager::Close( PluginsEnum_t pid ) { if( !m_info[pid].IsOpened ) return; - DevCon::Status( "\tClosing %s", tbl_PluginInfo[pid].shortname ); + Console::Status( "\tClosing %s", tbl_PluginInfo[pid].shortname ); if( pid == PluginId_GS ) { @@ -910,7 +943,7 @@ void PluginManager::Close( PluginsEnum_t pid ) void PluginManager::Close( bool closegs ) { - Console::Status( "Closing plugins..." ); + DbgCon::Status( "Closing plugins..." ); // Close plugins in reverse order of the initialization procedure. @@ -920,7 +953,7 @@ void PluginManager::Close( bool closegs ) Close( tbl_PluginInfo[i].id ); } - Console::Status( "Plugins closed successfully." ); + DbgCon::Status( "Plugins closed successfully." ); } // Initializes all plugins. Plugin initialization should be done once for every new emulation @@ -974,8 +1007,8 @@ void PluginManager::Init() void PluginManager::Shutdown() { Close(); + DbgCon::Status( "Shutting down plugins..." ); - Console::Status( "Shutting down plugins..." ); // Shutdown plugins in reverse order (probably doesn't matter... // ... but what the heck, right?) @@ -990,52 +1023,82 @@ void PluginManager::Shutdown() // More memorycard hacks!! - if( EmuPlugins.Mcd != NULL && m_mcdPlugin != NULL ) + if( (EmuPlugins.Mcd != NULL) && (m_mcdPlugin != NULL) ) { m_mcdPlugin->DeleteComponentInstance( (PS2E_THISPTR)EmuPlugins.Mcd ); EmuPlugins.Mcd = NULL; } - Console::Status( "Plugins shutdown successfully." ); + DbgCon::Status( "Plugins shutdown successfully." ); } -void PluginManager::Freeze( PluginsEnum_t pid, int mode, freezeData* data ) +// For internal use only, unless you're the MTGS. Then it's for you too! +// Returns false if the plugin returned an error. +bool PluginManager::DoFreeze( PluginsEnum_t pid, int mode, freezeData* data ) { - m_info[pid].CommonBindings.Freeze( mode, data ); -} - -// ---------------------------------------------------------------------------- -// Thread Safety: -// This function should only be called by the Main GUI thread and the GS thread (for GS states only), -// as it has special handlers to ensure that GS freeze commands are executed appropriately on the -// GS thread. -// -void PluginManager::Freeze( PluginsEnum_t pid, SaveState& state ) -{ - if( pid == PluginId_GS && wxThread::IsMain() ) + if( (pid == PluginId_GS) && wxThread::IsMain() ) { - // Need to send the GS freeze request on the GS thread. + MTGS_FreezeData woot = { data, 0 }; + // GS needs some thread safety love... + mtgsThread->SendPointerPacket( GS_RINGTYPE_FREEZE, mode, &woot ); + mtgsWaitGS(); + return woot.retval != -1; } else { - state.FreezePlugin( tbl_PluginInfo[pid].shortname, m_info[pid].CommonBindings.Freeze ); + return m_info[pid].CommonBindings.Freeze( mode, data ) != -1; } } -// ---------------------------------------------------------------------------- -// This overload of Freeze performs savestate freeze operation on *all* plugins, -// as according to the order in PluignsEnum_t. -// // Thread Safety: // This function should only be called by the Main GUI thread and the GS thread (for GS states only), // as it has special handlers to ensure that GS freeze commands are executed appropriately on the // GS thread. // -void PluginManager::Freeze( SaveState& state ) +void PluginManager::Freeze( PluginsEnum_t pid, SaveStateBase& state ) { - const PluginInfo* pi = tbl_PluginInfo-1; - while( ++pi, pi->shortname != NULL ) - Freeze( pi->id, state ); + Console::WriteLn( "\t%s %s", state.IsSaving() ? "Saving" : "Loading", + tbl_PluginInfo[pid].shortname ); + + freezeData fP = { 0, NULL }; + if( !DoFreeze( pid, FREEZE_SIZE, &fP ) ) + fP.size = 0; + + int fsize = fP.size; + state.Freeze( fsize ); + + if( state.IsLoading() && (fsize == 0) ) + { + // no state data to read, but the plugin expects some state data. + // Issue a warning to console... + if( fP.size != 0 ) + Console::Notice( "\tWarning: No data for this plugin was found. Plugin status may be unpredictable." ); + return; + + // Note: Size mismatch check could also be done here on loading, but + // some plugins may have built-in version support for non-native formats or + // older versions of a different size... or could give different sizes depending + // on the status of the plugin when loading, so let's ignore it. + } + + fP.size = fsize; + if( fP.size == 0 ) return; + + state.PrepBlock( fP.size ); + fP.data = (s8*)state.GetBlockPtr(); + + if( state.IsSaving() ) + { + if( !DoFreeze(pid, FREEZE_SAVE, &fP) ) + throw Exception::FreezePluginFailure( pid ); + } + else + { + if( !DoFreeze(pid, FREEZE_LOAD, &fP) ) + throw Exception::ThawPluginFailure( pid ); + } + + state.CommitBlock( fP.size ); } bool PluginManager::KeyEvent( const keyEvent& evt ) @@ -1062,9 +1125,7 @@ void PluginManager::Configure( PluginsEnum_t pid ) // PluginManager* PluginManager_Create( const wxString (&folders)[PluginId_Count] ) { - PluginManager* retval = new PluginManager( folders ); - retval->Init(); - return retval; + return new PluginManager( folders ); } PluginManager* PluginManager_Create( const wxChar* (&folders)[PluginId_Count] ) diff --git a/pcsx2/Plugins.h b/pcsx2/Plugins.h index 3d28dca955..d07682d1f4 100644 --- a/pcsx2/Plugins.h +++ b/pcsx2/Plugins.h @@ -106,6 +106,39 @@ namespace Exception PluginId = pid; } }; + + // This exception is thrown when a plugin returns an error while trying to save itself. + // Typically this should be a very rare occurance since a plugin typically shoudn't + // be doing memory allocations or file access during state saving. + // + class FreezePluginFailure : public virtual PluginError + { + public: + DEFINE_EXCEPTION_COPYTORS( FreezePluginFailure ) + + explicit FreezePluginFailure( PluginsEnum_t pid) + { + PluginId = pid; + } + + virtual wxString FormatDiagnosticMessage() const; + virtual wxString FormatDisplayMessage() const; + }; + + class ThawPluginFailure : public virtual PluginError, public virtual BadSavedState + { + public: + DEFINE_EXCEPTION_COPYTORS( ThawPluginFailure ) + + explicit ThawPluginFailure( PluginsEnum_t pid ) + { + PluginId = pid; + } + + virtual wxString FormatDiagnosticMessage() const; + virtual wxString FormatDisplayMessage() const; + }; + }; // -------------------------------------------------------------------------------------- @@ -140,7 +173,7 @@ struct LegacyPluginAPI_Common } }; -class SaveState; +class SaveStateBase; class mtgsThreadObject; // -------------------------------------------------------------------------------------- @@ -201,9 +234,12 @@ public: virtual void Close( PluginsEnum_t pid ) {} virtual void Close( bool closegs=true ) {} - virtual void Freeze( PluginsEnum_t pid, int mode, freezeData* data ) { wxASSERT_MSG( false, L"Null PluginManager!" ); } - virtual void Freeze( PluginsEnum_t pid, SaveState& state ) { wxASSERT_MSG( false, L"Null PluginManager!" ); } - virtual void Freeze( SaveState& state ) { wxASSERT_MSG( false, L"Null PluginManager!" ); } + virtual void Freeze( PluginsEnum_t pid, SaveStateBase& state ) { wxASSERT_MSG( false, L"Null PluginManager!" ); } + virtual bool DoFreeze( PluginsEnum_t pid, int mode, freezeData* data ) + { + wxASSERT_MSG( false, L"Null PluginManager!" ); + return false; + } virtual bool KeyEvent( const keyEvent& evt ) { return false; } }; @@ -238,9 +274,11 @@ protected: bool m_initialized; - PluginStatus_t m_info[PluginId_Count]; const PS2E_LibraryAPI* m_mcdPlugin; +public: // hack until we unsuck plugins... + PluginStatus_t m_info[PluginId_Count]; + public: virtual ~PluginManager(); @@ -251,9 +289,8 @@ public: void Close( PluginsEnum_t pid ); void Close( bool closegs=true ); - void Freeze( PluginsEnum_t pid, int mode, freezeData* data ); - void Freeze( PluginsEnum_t pid, SaveState& state ); - void Freeze( SaveState& state ); + void Freeze( PluginsEnum_t pid, SaveStateBase& state ); + bool DoFreeze( PluginsEnum_t pid, int mode, freezeData* data ); bool KeyEvent( const keyEvent& evt ); void Configure( PluginsEnum_t pid ); diff --git a/pcsx2/PrecompiledHeader.h b/pcsx2/PrecompiledHeader.h index 8241c83ea2..a6cc27a5f9 100644 --- a/pcsx2/PrecompiledHeader.h +++ b/pcsx2/PrecompiledHeader.h @@ -83,6 +83,7 @@ typedef int BOOL; #include "i18n.h" #include "Config.h" #include "Utilities/wxBaseTools.h" +#include "Utilities/ScopedPtr.h" #include "Utilities/Path.h" #include "Utilities/Console.h" #include "Utilities/Exceptions.h" diff --git a/pcsx2/RecoverySystem.cpp b/pcsx2/RecoverySystem.cpp index 0139988600..34fbce2aef 100644 --- a/pcsx2/RecoverySystem.cpp +++ b/pcsx2/RecoverySystem.cpp @@ -16,128 +16,33 @@ #include "PrecompiledHeader.h" -#include "Common.h" -#include "ps2/CoreEmuThread.h" +#include "App.h" #include "HostGui.h" - -////////////////////////////////////////////////////////////////////////////////////////// -// RecoverySystem.cpp -- houses code for recovering from on-the-fly changes to the emu -// configuration, and for saving/restoring the GS state (for more seamless exiting of -// fullscreen GS operation). -// -// The following handful of local classes are implemented att he bottom of this file. - -static SafeArray* g_RecoveryState = NULL; -static SafeArray* g_gsRecoveryState = NULL; - -// This class type creates a memory savestate using the existing Recovery information -// (if present) to generate the savestate material. If no recovery data is present, -// the current emulation state is used instead. -class RecoveryMemSavingState : public memSavingState, Sealed -{ -public: - virtual ~RecoveryMemSavingState() { } - RecoveryMemSavingState(); - - void gsFreeze(); - void FreezePlugin( const char* name, s32 (CALLBACK* freezer)(int mode, freezeData *data) ); -}; - -// This class type creates an on-disk (zipped) savestate using the existing Recovery -// information (if present) to generate the savestate material. If no recovery data is -// present, the current emulation state is used instead. -class RecoveryZipSavingState : public gzSavingState, Sealed -{ -public: - virtual ~RecoveryZipSavingState() { } - RecoveryZipSavingState( const wxString& filename ); - - void gsFreeze(); - void FreezePlugin( const char* name, s32 (CALLBACK* freezer)(int mode, freezeData *data) ); -}; - -// Special helper class used to save *just* the GS-relevant state information. -class JustGsSavingState : public memSavingState, Sealed -{ -public: - virtual ~JustGsSavingState() { } - JustGsSavingState(); - - // This special override saves the gs info to m_idx+4, and then goes back and - // writes in the length of data saved. - void gsFreeze(); -}; +static wxScopedPtr> g_RecoveryState; namespace StateRecovery { bool HasState() { - return g_RecoveryState != NULL || g_gsRecoveryState != NULL; + return g_RecoveryState; } void Recover() { - wxASSERT( g_EmuThread != NULL ); - wxASSERT( g_EmuThread->IsSelf() ); + if( !g_RecoveryState ) return; - if( g_RecoveryState != NULL ) - { - Console::Status( "Resuming execution from full memory state..." ); - memLoadingState( *g_RecoveryState ).FreezeAll(); - } - else if( g_gsRecoveryState != NULL ) - { - s32 dummylen; - - Console::Status( "Resuming execution from gsState..." ); - memLoadingState eddie( *g_gsRecoveryState ); - eddie.FreezePlugin( "GS", gsSafeFreeze ); - eddie.Freeze( dummylen ); // reads the length value recorded earlier. - eddie.gsFreeze(); - } + Console::Status( "Resuming execution from full memory state..." ); + memLoadingState( *g_RecoveryState ).FreezeAll(); StateRecovery::Clear(); SysClearExecutionCache(); - - if( GSsetGameCRC != NULL ) - GSsetGameCRC(ElfCRC, g_ZeroGSOptions); } - // Saves recovery state info to the given filename, 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. :) - // - // Threading Notes: - // This function can be invoked by any thread. However, if it is run outside the context - // of a CoreEmuThred then it void SaveToFile( const wxString& file ) { - SysSuspend(); - if( g_RecoveryState == NULL ) - { - RecoveryMemSavingState().FreezeAll(); - } - - SysResume(); - - if( g_RecoveryState != NULL ) - { - gzFile fileptr = gzopen( file.ToAscii().data(), "wb" ); - if( fileptr == NULL ) - throw Exception::CreateStream( file, "General savestate file creation error." ); - - gzwrite( fileptr, &g_SaveVersion, sizeof( u32 ) ); - gzwrite( fileptr, g_RecoveryState->GetPtr(), g_RecoveryState->GetSizeInBytes() ); - gzclose( fileptr ); - } - else - { - if( !EmulationInProgress() ) return; - - RecoveryZipSavingState( file ).FreezeAll(); - } + SafeArray buf; + memSavingState( buf ).FreezeAll(); } // Saves recovery state info to the given saveslot, or saves the active emulation state @@ -146,40 +51,22 @@ namespace StateRecovery { // the one in the memory save. :) void SaveToSlot( uint num ) { - SaveToFile( SaveState::GetFilename( num ) ); + SaveToFile( SaveStateBase::GetFilename( num ) ); } - // This method will override any existing recovery states, so call it with caution, if you - // think that there could be existing important state info in the recovery buffers (but - // really there shouldn't be, unless you're calling this function when it's not intended - // to be called). - void MakeGsOnly() - { - StateRecovery::Clear(); - if( !EmulationInProgress() ) return; - - g_gsRecoveryState = new SafeArray(); - JustGsSavingState eddie; - eddie.FreezePlugin( "GS", gsSafeFreeze ) ; - eddie.gsFreeze(); - } - // Creates a full recovery of the entire emulation state (CPU and all plugins). // If a current recovery state is already present, then nothing is done (the // existing recovery state takes precedence since if it were out-of-date it'd be // deleted!). void MakeFull() { - if( g_RecoveryState != NULL ) return; + if( g_RecoveryState ) return; if( !EmulationInProgress() ) return; - SysSuspend(); - try { - g_RecoveryState = new SafeArray( L"Memory Savestate Recovery" ); - RecoveryMemSavingState().FreezeAll(); - safe_delete( g_gsRecoveryState ); + g_RecoveryState.reset( new SafeArray( L"Memory Savestate Recovery" ) ); + memSavingState( *g_RecoveryState ).FreezeAll(); } catch( Exception::RuntimeError& ex ) { @@ -189,101 +76,13 @@ namespace StateRecovery { L"able to recover if you make changes to your PCSX2 configuration.\n\n" L"Details: %s", ex.FormatDisplayMessage().c_str() ) ); - safe_delete( g_RecoveryState ); + g_RecoveryState.reset(); } } // Clears and deallocates any recovery states. void Clear() { - safe_delete( g_RecoveryState ); - safe_delete( g_gsRecoveryState ); - } -} - -RecoveryMemSavingState::RecoveryMemSavingState() : memSavingState( *g_RecoveryState ) -{ -} - -void RecoveryMemSavingState::gsFreeze() -{ - if( g_gsRecoveryState != NULL ) - { - // just copy the data from src to dst. - // the normal savestate doesn't expect a length prefix for internal structures, - // so don't copy that part. - const u32 pluginlen = *((u32*)g_gsRecoveryState->GetPtr()); - const u32 gslen = *((u32*)g_gsRecoveryState->GetPtr(pluginlen+4)); - memcpy( m_memory.GetPtr(m_idx), g_gsRecoveryState->GetPtr(pluginlen+8), gslen ); - m_idx += gslen; - } - else - memSavingState::gsFreeze(); -} - -void RecoveryMemSavingState::FreezePlugin( const char* name, s32 (CALLBACK* freezer)(int mode, freezeData *data) ) -{ - if( (freezer == gsSafeFreeze) && (g_gsRecoveryState != NULL) ) - { - // Gs data is already in memory, so just copy from src to dest: - // length of the GS data is stored as the first u32, so use that to run the copy: - const u32 len = *((u32*)g_gsRecoveryState->GetPtr()); - memcpy( m_memory.GetPtr(m_idx), g_gsRecoveryState->GetPtr(), len+4 ); - m_idx += len+4; - } - else - memSavingState::FreezePlugin( name, freezer ); -} - -RecoveryZipSavingState::RecoveryZipSavingState( const wxString& filename ) : gzSavingState( filename ) -{ -} - -void RecoveryZipSavingState::gsFreeze() -{ - if( g_gsRecoveryState != NULL ) - { - // read data from the gsRecoveryState allocation instead of the GS, since the gs - // info was invalidated when the plugin was shut down. - - // the normal savestate doesn't expect a length prefix for internal structures, - // so don't copy that part. - - u32& pluginlen = *((u32*)g_gsRecoveryState->GetPtr(0)); - u32& gslen = *((u32*)g_gsRecoveryState->GetPtr(pluginlen+4)); - gzwrite( m_file, g_gsRecoveryState->GetPtr(pluginlen+4), gslen ); - } - else - gzSavingState::gsFreeze(); -} - -void RecoveryZipSavingState::FreezePlugin( const char* name, s32 (CALLBACK* freezer)(int mode, freezeData *data) ) -{ - if( (freezer == gsSafeFreeze) && (g_gsRecoveryState != NULL) ) - { - // Gs data is already in memory, so just copy from there into the gzip file. - // length of the GS data is stored as the first u32, so use that to run the copy: - u32& len = *((u32*)g_gsRecoveryState->GetPtr()); - gzwrite( m_file, g_gsRecoveryState->GetPtr(), len+4 ); - } - else - gzSavingState::FreezePlugin( name, freezer ); -} - -JustGsSavingState::JustGsSavingState() : memSavingState( *g_gsRecoveryState ) -{ -} - -// This special override saves the gs info to m_idx+4, and then goes back and -// writes in the length of data saved. -void JustGsSavingState::gsFreeze() -{ - int oldmidx = m_idx; - m_idx += 4; - memSavingState::gsFreeze(); - if( IsSaving() ) - { - s32& len = *((s32*)m_memory.GetPtr( oldmidx )); - len = (m_idx - oldmidx)-4; + g_RecoveryState.reset(); } } diff --git a/pcsx2/SPR.cpp b/pcsx2/SPR.cpp index 88898a363f..91fbc5887e 100644 --- a/pcsx2/SPR.cpp +++ b/pcsx2/SPR.cpp @@ -437,7 +437,7 @@ void SPRTOinterrupt() hwDmacIrq(DMAC_TO_SPR); } -void SaveState::sprFreeze() +void SaveStateBase::sprFreeze() { FreezeTag("SPRdma"); diff --git a/pcsx2/SaveState.cpp b/pcsx2/SaveState.cpp index aa7b5e6335..2c5581053e 100644 --- a/pcsx2/SaveState.cpp +++ b/pcsx2/SaveState.cpp @@ -34,9 +34,6 @@ using namespace R5900; -extern void recResetEE(); -extern void recResetIOP(); - static void PreLoadPrep() { SysClearExecutionCache(); @@ -49,38 +46,45 @@ static void PostLoadPrep() for(int i=0; i<48; i++) MapTLB(i); } -wxString SaveState::GetFilename( int slot ) +wxString SaveStateBase::GetFilename( int slot ) { return (g_Conf->Folders.Savestates + wxsFormat( L"%8.8X.%3.3d", ElfCRC, slot )).GetFullPath(); } -SaveState::SaveState( const char* msg, const wxString& destination ) : - m_version( g_SaveVersion ) -, m_tagspace( 128 ) +SaveStateBase::SaveStateBase( SafeArray& memblock ) : + m_memory( memblock ) +, m_version( g_SaveVersion ) +, m_idx( 0 ) +, m_sectid( FreezeId_Unknown ) +, m_pid( PluginId_GS ) +, m_DidBios( false ) { - Console::WriteLn( "%s %s", msg, destination.ToAscii().data() ); } -s32 CALLBACK gsSafeFreeze( int mode, freezeData *data ) +void SaveStateBase::PrepBlock( int size ) { - // have to call in the GS thread, otherwise weird stuff will start happening - mtgsThread->SendPointerPacket( GS_RINGTYPE_FREEZE, mode, data ); - mtgsWaitGS(); - return 0; -} - -void SaveState::FreezeTag( const char* src ) -{ - const int length = strlen( src ); - m_tagspace.MakeRoomFor( length+1 ); - - strcpy( m_tagspace.GetPtr(), src ); - FreezeMem( m_tagspace.GetPtr(), length ); - - if( strcmp( m_tagspace.GetPtr(), src ) != 0 ) + const int end = m_idx+size; + if( IsSaving() ) + m_memory.MakeRoomFor( end ); + else { - assert( 0 ); + if( m_memory.GetSizeInBytes() <= end ) + throw Exception::BadSavedState(); + } +} + +void SaveStateBase::FreezeTag( const char* src ) +{ + wxASSERT( strlen(src) < (sizeof( m_tagspace )-1) ); + + memzero_obj( m_tagspace ); + strcpy( m_tagspace, src ); + Freeze( m_tagspace ); + + if( strcmp( m_tagspace, src ) != 0 ) + { + wxASSERT_MSG( false, L"Savestate data corruption detected while reading tag" ); throw Exception::BadSavedState( // Untranslated diagnostic msg (use default msg for translation) L"Savestate data corruption detected while reading tag: " + wxString::FromAscii(src) @@ -88,48 +92,68 @@ void SaveState::FreezeTag( const char* src ) } } -void SaveState::FreezeAll() +void SaveStateBase::FreezeBios() { - if( IsLoading() ) - PreLoadPrep(); - // Check the BIOS, and issue a warning if the bios for this state // doesn't match the bios currently being used (chances are it'll still // work fine, but some games are very picky). - + char descin[128]; wxString descout; IsBIOS( g_Conf->FullpathToBios(), descout ); memcpy_fast( descin, descout.ToAscii().data(), 128 ); - Freeze( descin ); - - if( memcmp( descin, descout, 128 ) != 0 ) - { - Console::Error( - "\n\tWarning: BIOS Version Mismatch, savestate may be unstable!\n" - "\t\tCurrent BIOS: %s\n" - "\t\tSavestate BIOS: %s\n", - descout.ToAscii().data(), descin - ); - } + // ... and only freeze bios info once per state, since the user msg could + // become really annoying on a corrupted state or something. (have to always + // load though, so that we advance past the duplicated info, if present) + + if( IsLoading() || !m_DidBios ) + Freeze( descin ); + + if( !m_DidBios ) + { + if( memcmp( descin, descout, 128 ) != 0 ) + { + Console::Error( + "\n\tWarning: BIOS Version Mismatch, savestate may be unstable!\n" + "\t\tCurrent Version: %s\n" + "\t\tSavestate Version: %s\n", + descout.ToAscii().data(), descin + ); + } + } + m_DidBios = true; +} + +static const int MainMemorySizeInBytes = + Ps2MemSize::Base + Ps2MemSize::Scratch + Ps2MemSize::Hardware + + Ps2MemSize::IopRam + Ps2MemSize::IopHardware + 0x0100; + +void SaveStateBase::FreezeMainMemory() +{ // First Block - Memory Dumps // --------------------------- - FreezeMem(PS2MEM_BASE, Ps2MemSize::Base); // 32 MB main memory - FreezeMem(PS2MEM_SCRATCH, Ps2MemSize::Scratch); // scratch pad - FreezeMem(PS2MEM_HW, Ps2MemSize::Hardware); // hardware memory + FreezeMem(PS2MEM_BASE, Ps2MemSize::Base); // 32 MB main memory + FreezeMem(PS2MEM_SCRATCH, Ps2MemSize::Scratch); // scratch pad + FreezeMem(PS2MEM_HW, Ps2MemSize::Hardware); // hardware memory FreezeMem(psxM, Ps2MemSize::IopRam); // 2 MB main memory FreezeMem(psxH, Ps2MemSize::IopHardware); // hardware memory FreezeMem(psxS, 0x000100); // iop's sif memory +} + +void SaveStateBase::FreezeRegisters() +{ + if( IsLoading() ) + PreLoadPrep(); // Second Block - Various CPU Registers and States // ----------------------------------------------- FreezeTag( "cpuRegs" ); - Freeze(cpuRegs); // cpu regs + COP0 - Freeze(psxRegs); // iop regs + Freeze(cpuRegs); // cpu regs + COP0 + Freeze(psxRegs); // iop regs Freeze(fpuRegs); - Freeze(tlb); // tlbs + Freeze(tlb); // tlbs // Third Block - Cycle Timers and Events // ------------------------------------- @@ -143,6 +167,7 @@ void SaveState::FreezeAll() // Fourth Block - EE-related systems // --------------------------------- + FreezeTag( "EE-Subsystems" ); rcntFreeze(); gsFreeze(); vuMicroFreeze(); @@ -155,154 +180,172 @@ void SaveState::FreezeAll() // Fifth Block - iop-related systems // --------------------------------- + FreezeTag( "IOP-Subsystems" ); psxRcntFreeze(); sioFreeze(); sio2Freeze(); cdrFreeze(); cdvdFreeze(); - // Sixth Block - Plugins Galore! - // ----------------------------- - FreezePlugin( "GS", gsSafeFreeze ); - - g_plugins->Freeze( *this ); - if( IsLoading() ) PostLoadPrep(); } -///////////////////////////////////////////////////////////////////////////// -// gzipped to/from disk state saves implementation - -gzBaseStateInfo::gzBaseStateInfo( const char* msg, const wxString& filename ) : - SaveState( msg, filename ) -, m_filename( filename ) -, m_file( NULL ) +bool SaveStateBase::FreezeSection() { -} + Freeze( m_sectid ); -gzBaseStateInfo::~gzBaseStateInfo() -{ - if( m_file != NULL ) + switch( m_sectid ) { - gzclose( m_file ); - m_file = NULL; + case FreezeId_End: + return false; + + case FreezeId_Bios: + { + int sectlen = 128; + FreezeTag( "BiosVersion" ); + Freeze( sectlen ); + + if( sectlen != MainMemorySizeInBytes ) + { + throw Exception::BadSavedState( wxEmptyString, + L"Invalid size encountered on BiosVersion section.", + _("The savestate data is invalid or corrupted.") + ); + } + + FreezeBios(); + m_sectid++; + } + break; + + case FreezeId_Memory: + { + FreezeTag( "MainMemory" ); + + int sectlen = MainMemorySizeInBytes; + Freeze( sectlen ); + if( sectlen != MainMemorySizeInBytes ) + { + throw Exception::BadSavedState( wxEmptyString, + L"Invalid size encountered on MainMemory section.", + _("The savestate data is invalid or corrupted.") + ); + } + + FreezeMainMemory(); + m_sectid++; + } + break; + + case FreezeId_Registers: + { + FreezeTag( "HardwareRegisters" ); + int seekpos = m_idx; + int sectsize; + Freeze( sectsize ); + + FreezeRegisters(); + + int realsectsize = m_idx - seekpos; + if( IsSaving() ) + { + // write back the section length... + *((u32*)m_memory.GetPtr(seekpos)) = realsectsize - 4; + } + else // IsLoading!! + { + if( sectsize != realsectsize ) // if they don't match then we have a problem, jim. + { + throw Exception::BadSavedState( wxEmptyString, + L"Invalid size encountered on HardwareRegisters section.", + _("The savestate data is invalid or corrupted.") + ); + } + } + } + break; + + case FreezeId_Plugin: + { + FreezeTag( "Plugin" ); + int seekpos = m_idx; + int sectsize; + Freeze( sectsize ); + + Freeze( m_pid ); + g_plugins->Freeze( (PluginsEnum_t)m_pid, *this ); + + int realsectsize = m_idx - seekpos; + if( IsSaving() ) + { + // write back the section length... + *((u32*)m_memory.GetPtr(seekpos)) = realsectsize - 4; + } + else + { + if( sectsize != realsectsize ) // if they don't match then we have a problem, jim. + { + throw Exception::BadSavedState( wxEmptyString, + L"Invalid size encountered on Plugin section.", + _("The savestate data is invalid or corrupted.") + ); + } + } + + + // following increments only affect Saving mode, are ignored by Loading mode. + m_pid++; + if( m_pid > PluginId_Count ) + m_sectid++; + } + break; + + case FreezeId_Unknown: + default: + if( IsSaving() ) + m_sectid = FreezeId_End; + else + { + // Skip unknown sections with a warning log. + // Maybe it'll work! (haha?) + + int size; + Freeze( m_tagspace ); + Freeze( size ); + m_tagspace[sizeof(m_tagspace)-1] = 0; + + Console::Notice( + "Warning: Unknown tag encountered while loading savestate; going to ignore it!\n" + "\tTagname: %s, Size: %d", m_tagspace, size + ); + m_idx += size; + } + break; } + + return true; } - -gzSavingState::gzSavingState( const wxString& filename ) : - gzBaseStateInfo( "Saving state to: ", filename ) +void SaveStateBase::FreezeAll() { - m_file = gzopen(filename.ToAscii().data(), "wb"); - if( m_file == NULL ) - throw Exception::FileNotFound(); + m_sectid = (int)FreezeId_End+1; + m_pid = PluginId_GS; - gzsetparams( m_file, Z_BEST_SPEED, Z_DEFAULT_STRATEGY ); - Freeze( m_version ); -} - - -gzLoadingState::gzLoadingState( const wxString& filename ) : - gzBaseStateInfo( "Loading state from: ", filename ) -{ - m_file = gzopen(filename.ToAscii().data(), "rb"); - if( m_file == NULL ) - throw Exception::FileNotFound(); - - gzread( m_file, &m_version, 4 ); - - if( (m_version >> 16) != (g_SaveVersion >> 16) ) - { - Console::Error( - "Savestate load aborted:\n" - "\tUnknown or invalid savestate identifier, either from a (very!) old version of\n" - "\tPcsx2, or the file is corrupted" - ); - throw Exception::UnsupportedStateVersion( m_version ); - } - else if( m_version > g_SaveVersion ) - { - Console::Error( - "Savestate load aborted:\n" - "\tThe savestate was created with a newer version of Pcsx2. I don't know how to load it!" ); - throw Exception::UnsupportedStateVersion( m_version ); - } -} - -gzLoadingState::~gzLoadingState() { } - - -void gzSavingState::FreezeMem( void* data, int size ) -{ - gzwrite( m_file, data, size ); -} - -void gzLoadingState::FreezeMem( void* data, int size ) -{ - if( gzread( m_file, data, size ) != size ) - throw Exception::BadSavedState( m_filename ); -} - -void gzSavingState::FreezePlugin( const char* name, s32 (CALLBACK *freezer)(int mode, freezeData *data) ) -{ - freezeData fP = { 0, NULL }; - Console::WriteLn( "\tSaving %s", name ); - - FreezeTag( name ); - - if (freezer(FREEZE_SIZE, &fP) == -1) - throw Exception::FreezePluginFailure( name, "saving" ); - - Freeze( fP.size ); - if( fP.size == 0 ) return; - - SafeArray buffer( fP.size ); - fP.data = buffer.GetPtr(); - - if(freezer(FREEZE_SAVE, &fP) == -1) - throw Exception::FreezePluginFailure( name, "saving" ); - - FreezeMem( fP.data, fP.size ); -} - -void gzLoadingState::FreezePlugin( const char* name, s32 (CALLBACK *freezer)(int mode, freezeData *data) ) -{ - freezeData fP = { 0, NULL }; - Console::WriteLn( "\tLoading %s", name ); - - FreezeTag( name ); - Freeze( fP.size ); - if( fP.size == 0 ) return; - - SafeArray buffer( fP.size ); - fP.data = buffer.GetPtr(); - - FreezeMem( fP.data, fP.size ); - - if(freezer(FREEZE_LOAD, &fP) == -1) - throw Exception::FreezePluginFailure( name, "loading" ); + while( FreezeSection() ) ; } ////////////////////////////////////////////////////////////////////////////////// // uncompressed to/from memory state saves implementation -memBaseStateInfo::memBaseStateInfo( SafeArray& memblock, const char* msg ) : - SaveState( msg, L"Memory") -, m_memory( memblock ) -, m_idx( 0 ) -{ - // Always clear the MTGS thread state. - mtgsWaitGS(); -} - -memSavingState::memSavingState( SafeArray& save_to ) : memBaseStateInfo( save_to, "Saving state to: " ) +memSavingState::memSavingState( SafeArray& save_to ) : + SaveStateBase( save_to ) { save_to.ChunkSize = ReallocThreshold; save_to.MakeRoomFor( MemoryBaseAllocSize ); } -// Saving of state data to a memory buffer +// Saving of state data void memSavingState::FreezeMem( void* data, int size ) { const int end = m_idx+size; @@ -315,14 +358,14 @@ void memSavingState::FreezeMem( void* data, int size ) dest[m_idx] = *src; } -memLoadingState::memLoadingState(SafeArray& load_from ) : - memBaseStateInfo( load_from, "Loading state from: " ) +memLoadingState::memLoadingState( const SafeArray& load_from ) : + SaveStateBase( const_cast&>(load_from) ) { } memLoadingState::~memLoadingState() { } -// Loading of state data from a memory buffer... +// Loading of state data void memLoadingState::FreezeMem( void* data, int size ) { const int end = m_idx+size; @@ -332,45 +375,3 @@ void memLoadingState::FreezeMem( void* data, int size ) for( ; m_idx m_memory.GetSizeInBytes() ) - { - assert(0); - throw Exception::BadSavedState( L"memory"); - } - - fP.data = ((s8*)m_memory.GetPtr()) + m_idx; - if(freezer(FREEZE_LOAD, &fP) == -1) - throw Exception::FreezePluginFailure( name, "loading" ); - - m_idx += fP.size; -} diff --git a/pcsx2/SaveState.h b/pcsx2/SaveState.h index 1da2ff747d..22eb3e8100 100644 --- a/pcsx2/SaveState.h +++ b/pcsx2/SaveState.h @@ -13,8 +13,7 @@ * If not, see . */ -#ifndef _SAVESTATE_H_ -#define _SAVESTATE_H_ +#pragma once // This shouldn't break Win compiles, but it does. #ifdef __LINUX__ @@ -28,25 +27,53 @@ // the lower 16 bit value. IF the change is breaking of all compatibility with old // states, increment the upper 16 bit value, and clear the lower 16 bits to 0. -static const u32 g_SaveVersion = 0x8b410001; +static const u32 g_SaveVersion = 0x8b410002; // this function is meant to be used in the place of GSfreeze, and provides a safe layer // between the GS saving function and the MTGS's needs. :) extern s32 CALLBACK gsSafeFreeze( int mode, freezeData *data ); -// This class provides the base API for both loading and saving savestates. -// Normally you'll want to use one of the four "functional" derived classes rather -// than this class directly: gzLoadingState, gzSavingState (gzipped disk-saved + +enum FreezeSectionId +{ + FreezeId_End, + + FreezeId_Memory, + FreezeId_Registers, + + // A BIOS tag should always be saved in conjunction with Memory or Registers tags, + // but can be skipped if the savestate has only plugins. + FreezeId_Bios, + + FreezeId_Plugin, + + // anything here and beyond we can skip, with a warning + FreezeId_Unknown, +}; + +// -------------------------------------------------------------------------------------- +// SaveStateBase class +// -------------------------------------------------------------------------------------- +// Provides the base API for both loading and saving savestates. Normally you'll want to +// use one of the four "functional" derived classes rather than this class directly: gzLoadingState, gzSavingState (gzipped disk-saved // states), and memLoadingState, memSavingState (uncompressed memory states). -class SaveState +class SaveStateBase { protected: + SafeArray& m_memory; + char m_tagspace[32]; + u32 m_version; // version of the savestate being loaded. - SafeArray m_tagspace; + + int m_idx; // current read/write index of the allocation + int m_sectid; + int m_pid; + + bool m_DidBios; public: - SaveState( const char* msg, const wxString& destination ); - virtual ~SaveState() { } + SaveStateBase( SafeArray& memblock ); + virtual ~SaveStateBase() { } static wxString GetFilename( int slot ); @@ -78,14 +105,28 @@ public: FreezeMem( &data, sizeof( T ) - sizeOfNewStuff ); } + void PrepBlock( int size ); + + u8* GetBlockPtr() + { + return &m_memory[m_idx]; + } + + void CommitBlock( int size ) + { + m_idx += size; + } + + bool FreezeSection(); + // Freezes an identifier value into the savestate for troubleshooting purposes. // Identifiers can be used to determine where in a savestate that data has become // skewed (if the value does not match then the error occurs somewhere prior to that // position). void FreezeTag( const char* src ); - // Loads or saves a plugin. Plugin name is for console logging purposes. - virtual void FreezePlugin( const char* name, s32 (CALLBACK* freezer)(int mode, freezeData *data) )=0; + // Returns true if this object is a StateLoading type object. + bool IsLoading() const { return !IsSaving(); } // Loads or saves a memory block. virtual void FreezeMem( void* data, int size )=0; @@ -93,18 +134,18 @@ public: // Returns true if this object is a StateSaving type object. virtual bool IsSaving() const=0; - // Returns true if this object is a StateLoading type object. - bool IsLoading() const { return !IsSaving(); } - - // note: gsFreeze() needs to be public because of the GSState recorder. - public: - virtual void gsFreeze(); + // note: gsFreeze() needs to be public because of the GSState recorder. + void gsFreeze(); protected: // Load/Save functions for the various components of our glorious emulator! + void FreezeBios(); + void FreezeMainMemory(); + void FreezeRegisters(); + void rcntFreeze(); void vuMicroFreeze(); void vif0Freeze(); @@ -125,57 +166,11 @@ protected: }; -///////////////////////////////////////////////////////////////////////////////// -// Class Declarations for Savestates using zlib +// -------------------------------------------------------------------------------------- +// Saving and Loading Specialized Implementations... +// -------------------------------------------------------------------------------------- -class gzBaseStateInfo : public SaveState -{ -protected: - const wxString m_filename; - gzFile m_file; // used for reading/writing disk saves - -public: - gzBaseStateInfo( const char* msg, const wxString& filename ); - - virtual ~gzBaseStateInfo(); -}; - -class gzSavingState : public gzBaseStateInfo -{ -public: - virtual ~gzSavingState() {} - gzSavingState( const wxString& filename ) ; - void FreezePlugin( const char* name, s32(CALLBACK *freezer)(int mode, freezeData *data) ); - void FreezeMem( void* data, int size ); - bool IsSaving() const { return true; } -}; - -class gzLoadingState : public gzBaseStateInfo -{ -public: - virtual ~gzLoadingState(); - gzLoadingState( const wxString& filename ); - - void FreezePlugin( const char* name, s32(CALLBACK *freezer)(int mode, freezeData *data) ); - void FreezeMem( void* data, int size ); - bool IsSaving() const { return false; } - bool Finished() const { return !!gzeof( m_file ); } -}; - -////////////////////////////////////////////////////////////////////////////////// - -class memBaseStateInfo : public SaveState -{ -protected: - SafeArray& m_memory; - int m_idx; // current read/write index of the allocation - -public: - virtual ~memBaseStateInfo() { } - memBaseStateInfo( SafeArray& memblock, const char* msg ); -}; - -class memSavingState : public memBaseStateInfo +class memSavingState : public SaveStateBase { protected: static const int ReallocThreshold = 0x200000; // 256k reallocation block size. @@ -185,22 +180,21 @@ public: virtual ~memSavingState() { } memSavingState( SafeArray& save_to ); - void FreezePlugin( const char* name, s32(CALLBACK *freezer)(int mode, freezeData *data) ); // Saving of state data to a memory buffer void FreezeMem( void* data, int size ); bool IsSaving() const { return true; } }; -class memLoadingState : public memBaseStateInfo +class memLoadingState : public SaveStateBase { public: virtual ~memLoadingState(); - memLoadingState(SafeArray& load_from ); + memLoadingState( const SafeArray& load_from ); - void FreezePlugin( const char* name, s32(CALLBACK *freezer)(int mode, freezeData *data) ); // Loading of state data from a memory buffer... void FreezeMem( void* data, int size ); bool IsSaving() const { return false; } + bool IsFinished() const { return m_idx >= m_memory.GetSizeInBytes(); } }; namespace StateRecovery @@ -209,10 +203,7 @@ namespace StateRecovery extern void Recover(); extern void SaveToFile( const wxString& file ); extern void SaveToSlot( uint num ); - extern void MakeGsOnly(); extern void MakeFull(); extern void Clear(); } -#endif - diff --git a/pcsx2/Sif.cpp b/pcsx2/Sif.cpp index 98b4aa7f49..e43fa45fc8 100644 --- a/pcsx2/Sif.cpp +++ b/pcsx2/Sif.cpp @@ -499,7 +499,7 @@ __forceinline void dmaSIF2() } -void SaveState::sifFreeze() +void SaveStateBase::sifFreeze() { FreezeTag("SIFdma"); diff --git a/pcsx2/Sio.cpp b/pcsx2/Sio.cpp index d0c11948bd..bc62fa32b7 100644 --- a/pcsx2/Sio.cpp +++ b/pcsx2/Sio.cpp @@ -645,7 +645,7 @@ void SIO_FORCEINLINE sioInterrupt() { psxHu32(0x1070)|=0x80; } -void SaveState::sioFreeze() +void SaveStateBase::sioFreeze() { // CRCs for memory cards. u64 m_mcdCRCs[2][8]; diff --git a/pcsx2/System.cpp b/pcsx2/System.cpp index 7ef9d4b4c6..496a2909e5 100644 --- a/pcsx2/System.cpp +++ b/pcsx2/System.cpp @@ -39,25 +39,16 @@ Pcsx2Config EmuConfig; // disable all session overrides by default... SessionOverrideFlags g_Session = {false}; -CoreEmuThread* g_EmuThread; -bool sysInitialized = false; - -// ----------------------------------------------------------------------- // This function should be called once during program execution. -// void SysDetect() { using namespace Console; -#ifdef __LINUX__ - // Haven't rigged up getting the svn version yet... --arcum42 - Notice("PCSX2 %d.%d.%d - compiled on " __DATE__, PCSX2_VersionHi, PCSX2_VersionMid, PCSX2_VersionLo); -#else Notice("PCSX2 %d.%d.%d.r%d %s - compiled on " __DATE__, PCSX2_VersionHi, PCSX2_VersionMid, PCSX2_VersionLo, SVN_REV, SVN_MODS ? "(modded)" : "" ); -#endif + Notice("Savestate version: %x", g_SaveVersion); cpudetectInit(); @@ -82,49 +73,51 @@ void SysDetect() x86caps.Flags, x86caps.Flags2, x86caps.EFlags ) ); + + wxArrayString features[2]; // 2 lines, for readability! + + if( x86caps.hasMultimediaExtensions ) features[0].Add( L"MMX" ); + if( x86caps.hasStreamingSIMDExtensions ) features[0].Add( L"SSE" ); + if( x86caps.hasStreamingSIMD2Extensions ) features[0].Add( L"SSE2" ); + if( x86caps.hasStreamingSIMD3Extensions ) features[0].Add( L"SSE3" ); + if( x86caps.hasSupplementalStreamingSIMD3Extensions ) features[0].Add( L"SSSE3" ); + if( x86caps.hasStreamingSIMD4Extensions ) features[0].Add( L"SSE4.1" ); + if( x86caps.hasStreamingSIMD4Extensions2 ) features[0].Add( L"SSE4.2" ); - WriteLn( "Features:" ); - WriteLn( - "\t%sDetected MMX\n" - "\t%sDetected SSE\n" - "\t%sDetected SSE2\n" - "\t%sDetected SSE3\n" - "\t%sDetected SSSE3\n" - "\t%sDetected SSE4.1\n" - "\t%sDetected SSE4.2\n", - x86caps.hasMultimediaExtensions ? "" : "Not ", - x86caps.hasStreamingSIMDExtensions ? "" : "Not ", - x86caps.hasStreamingSIMD2Extensions ? "" : "Not ", - x86caps.hasStreamingSIMD3Extensions ? "" : "Not ", - x86caps.hasSupplementalStreamingSIMD3Extensions ? "" : "Not ", - x86caps.hasStreamingSIMD4Extensions ? "" : "Not ", - x86caps.hasStreamingSIMD4Extensions2 ? "" : "Not " - ); + if( x86caps.hasMultimediaExtensionsExt ) features[1].Add( L"MMX2 " ); + if( x86caps.has3DNOWInstructionExtensions ) features[1].Add( L"3DNOW " ); + if( x86caps.has3DNOWInstructionExtensionsExt ) features[1].Add( L"3DNOW2" ); + if( x86caps.hasStreamingSIMD4ExtensionsA ) features[1].Add( L"SSE4a " ); - if ( x86caps.VendorName[0] == 'A' ) //AMD cpu - { - WriteLn( " Extended AMD Features:" ); - WriteLn( - "\t%sDetected MMX2\n" - "\t%sDetected 3DNOW\n" - "\t%sDetected 3DNOW2\n" - "\t%sDetected SSE4a\n", - x86caps.hasMultimediaExtensionsExt ? "" : "Not ", - x86caps.has3DNOWInstructionExtensions ? "" : "Not ", - x86caps.has3DNOWInstructionExtensionsExt ? "" : "Not ", - x86caps.hasStreamingSIMD4ExtensionsA ? "" : "Not " - ); - } + wxString result[2]; + JoinString( result[0], features[0], L".. " ); + JoinString( result[1], features[1], L".. " ); + + WriteLn( L"Features Detected:\n\t" + result[0] + (result[1].IsEmpty() ? wxEmptyString : (L"\n\t" + result[1])) + L"\n" ); + + //if ( x86caps.VendorName[0] == 'A' ) //AMD cpu Console::ClearColor(); } -////////////////////////////////////////////////////////////////////////////////////////// -// Allocates memory for all PS2 systems. -bool SysAllocateMem() +// returns the translated error message for the Virtual Machine failing to allocate! +static wxString GetMemoryErrorVM() { - // Allocate PS2 system ram space (required by interpreters and recompilers both) + return pxE( ".Popup Error:EmuCore::MemoryForVM", + L"PCSX2 is unable to allocate memory needed for the PS2 virtual machine. " + L"Close out some memory hogging background tasks and try again." + ); +} +EmuCoreAllocations::EmuCoreAllocations() +{ + Console::Status( "Initializing PS2 virtual machine..." ); + + RecSuccess_EE = false; + RecSuccess_IOP = false; + RecSuccess_VU0 = false; + RecSuccess_VU1 = false; + try { vtlb_Core_Alloc(); @@ -132,147 +125,116 @@ bool SysAllocateMem() psxMemAlloc(); vuMicroMemAlloc(); } - catch( Exception::OutOfMemory& ) + // ---------------------------------------------------------------------------- + catch( Exception::OutOfMemory& ex ) { - // TODO : Should this error be handled here or allowed to be handled by the main - // exception handler? + wxString newmsg( ex.UserMsg() + L"\n\n" + GetMemoryErrorVM() ); + ex.UserMsg() = newmsg; + CleanupMess(); + throw; + } + catch( std::bad_alloc& ex ) + { + CleanupMess(); - // Failures on the core initialization of memory is bad, since it means the emulator is - // completely non-functional. + // re-throw std::bad_alloc as something more friendly. - //Msgbox::Alert( "Failed to allocate memory needed to run pcsx2.\n\nError: %s", ex.cMessage() ); - SysShutdownMem(); - return false; + throw Exception::OutOfMemory( + wxsFormat( // Diagnostic (english) + L"std::bad_alloc caught while trying to allocate memory for the PS2 Virtual Machine.\n" + L"Error Details: " + wxString::FromUTF8( ex.what() ) + ), + + GetMemoryErrorVM() // translated + ); } - return true; -} - -////////////////////////////////////////////////////////////////////////////////////////// -// Allocates memory for all recompilers, and force-disables any recs that fail to initialize. -// This should be done asap, since the recompilers tend to demand a lot of system resources, -// and prefer to have those resources at specific address ranges. The sooner memory is -// allocated, the better. -// -// Returns FALSE on *critical* failure (GUI should issue a msg and exit). -void SysAllocateDynarecs() -{ - // Attempt to initialize the recompilers. - // Most users want to use recs anyway, and if they are using interpreters I don't think the - // extra few megs of allocation is going to be an issue. + Console::Status( "Allocating memory for recompilers..." ); try { - // R5900 and R3000a must be rec-enabled together for now so if either fails they both fail. recCpu.Allocate(); - psxRec.Allocate(); + RecSuccess_EE = true; } - catch( Exception::BaseException& ) + catch( Exception::BaseException& ex ) { - // TODO : Fix this message. It should respond according to the user's - // currently configured recompiler.interpreter options, for example. - - /*Msgbox::Alert( - "The EE/IOP recompiler failed to initialize with the following error:\n\n" - "%s" - "\n\nThe EE/IOP interpreter will be used instead (slow!).", params - ex.cMessage() - );*/ - - g_Session.ForceDisableEErec = true; - + Console::Error( L"EE Recompiler Allocation Failed:\n" + ex.FormatDiagnosticMessage() ); recCpu.Shutdown(); + } + + try + { + psxRec.Allocate(); + RecSuccess_IOP = true; + } + catch( Exception::BaseException& ex ) + { + Console::Error( L"IOP Recompiler Allocation Failed:\n" + ex.FormatDiagnosticMessage() ); psxRec.Shutdown(); } + // hmm! : VU0 and VU1 pre-allocations should do sVU and mVU separately? Sounds complicated. :( + try { VU0micro::recAlloc(); + RecSuccess_VU0 = true; } - catch( Exception::BaseException& ) + catch( Exception::BaseException& ex ) { - - // TODO : Fix this message. It should respond according to the user's - // currently configured recompiler.interpreter options, for example. -/* - Msgbox::Alert( - "The VU0 recompiler failed to initialize with the following error:\n\n" - "%s" - "\n\nThe VU0 interpreter will be used for this session (may slow down some games).", params - ex.cMessage() - ); -*/ - - g_Session.ForceDisableVU0rec = true; + Console::Error( L"VU0 Recompiler Allocation Failed:\n" + ex.FormatDiagnosticMessage() ); VU0micro::recShutdown(); } try { VU1micro::recAlloc(); + RecSuccess_VU1 = true; } - catch( Exception::BaseException& ) + catch( Exception::BaseException& ex ) { - - // TODO : Fix this message. It should respond according to the user's - // currently configured recompiler.interpreter options, for example. -/* - Msgbox::Alert( - "The VU1 recompiler failed to initialize with the following error:\n\n" - "%s" - "\n\nThe VU1 interpreter will be used for this session (will slow down most games).", params - ex.cMessage() - ); -*/ - - g_Session.ForceDisableVU1rec = true; + Console::Error( L"VU1 Recompiler Allocation Failed:\n" + ex.FormatDiagnosticMessage() ); VU1micro::recShutdown(); } - // If both VUrecs failed, then make sure the SuperVU is totally closed out: - if( !CHECK_VU0REC && !CHECK_VU1REC) + // If both VUrecs failed, then make sure the SuperVU is totally closed out, because it + // actually initializes everything once and then shares it between both VU recs. + if( !RecSuccess_VU0 && !RecSuccess_VU1 ) + SuperVUDestroy( -1 ); +} + +void EmuCoreAllocations::CleanupMess() throw() +{ + try + { + // Special SuperVU "complete" terminator. SuperVUDestroy( -1 ); + VU1micro::recShutdown(); + VU0micro::recShutdown(); + + psxRec.Shutdown(); + recCpu.Shutdown(); + + vuMicroMemShutdown(); + psxMemShutdown(); + memShutdown(); + vtlb_Core_Shutdown(); + } + DESTRUCTOR_CATCHALL } -// This should be called last thing before PCSX2 exits. -// -void SysShutdownMem() +EmuCoreAllocations::~EmuCoreAllocations() throw() { - if( sysInitialized ) - SysShutdown(); - - vuMicroMemShutdown(); - psxMemShutdown(); - memShutdown(); - vtlb_Core_Shutdown(); + CleanupMess(); } -// This should generally be called right before calling SysShutdownMem(), although you can optionally -// use it in conjunction with SysAllocDynarecs to allocate/free the dynarec resources on the fly (as -// risky as it might be, since dynarecs could very well fail on the second attempt). -void SysShutdownDynarecs() +bool EmuCoreAllocations::HadSomeFailures( const Pcsx2Config::RecompilerOptions& recOpts ) const { - // Special SuperVU "complete" terminator. - SuperVUDestroy( -1 ); - - VU0micro::recShutdown(); - VU1micro::recShutdown(); - - psxRec.Shutdown(); - recCpu.Shutdown(); -} - -void SysShutdown() -{ - sysInitialized = false; - - Console::Status( "Shutting down PS2 virtual machine..." ); - SysEndExecution(); - safe_delete( g_plugins ); - - SysShutdownDynarecs(); - SysShutdownMem(); + return (recOpts.EnableEE && !RecSuccess_EE) || + (recOpts.EnableIOP && !RecSuccess_IOP) || + (recOpts.EnableVU0 && !RecSuccess_VU0) || + (recOpts.EnableVU1 && !RecSuccess_VU1); } // Resets all PS2 cpu execution caches, which does not affect that actual PS2 state/condition. @@ -291,63 +253,6 @@ void SysClearExecutionCache() vuMicroCpuReset(); } -bool EmulationInProgress() -{ - return (g_EmuThread != NULL) && g_EmuThread->IsRunning(); -} - -// Executes the specified cdvd source and optional elf file. This command performs a -// full closure of any existing VM state and starts a fresh VM with the requested -// sources. -void SysExecute( CoreEmuThread* newThread, CDVD_SourceType cdvdsrc ) -{ - wxASSERT( newThread != NULL ); - safe_delete( g_EmuThread ); - - CDVDsys_ChangeSource( cdvdsrc ); - g_EmuThread = newThread; - g_EmuThread->Resume(); -} - -// Executes the emulator using a saved/existing virtual machine state and currently -// configured CDVD source device. -// Debug assertions: -void SysExecute( CoreEmuThread* newThread ) -{ - wxASSERT( newThread != NULL ); - safe_delete( g_EmuThread ); - - g_EmuThread = newThread; - g_EmuThread->Resume(); -} - -// Once execution has been ended no action can be taken on the Virtual Machine (such as -// saving states). No assertions or exceptions. -void SysEndExecution() -{ - safe_delete( g_EmuThread ); - GetPluginManager().Shutdown(); -} - -void SysSuspend() -{ - if( g_EmuThread != NULL ) - g_EmuThread->Suspend(); -} - -void SysResume() -{ - if( g_EmuThread != NULL ) - g_EmuThread->Resume(); -} - - -void SysRestorableReset() -{ - if( !EmulationInProgress() ) return; - StateRecovery::MakeFull(); -} - // The calling function should trap and handle exceptions as needed. // Exceptions: // Exception::StateLoadError - thrown when a fully recoverable exception ocurred. The @@ -355,35 +260,19 @@ void SysRestorableReset() // // Any other exception means the Virtual Memory state is indeterminate and probably // invalid. -void SysLoadState( const wxString& file ) +void SysLoadState( const wxString& srcfile ) { + SafeArray buf; + memLoadingState joe( buf ); // this could throw n StateLoadError. + // we perform a full backup to memory first so that we can restore later if the // load fails. fixme: should this be made optional? It could have significant // speed impact on state loads on slower machines with low ram. >_< StateRecovery::MakeFull(); - gzLoadingState joe( file ); // this'll throw an StateLoadError. - - GetPluginManager().Open(); - cpuReset(); SysClearExecutionCache(); - + cpuReset(); joe.FreezeAll(); - - if( GSsetGameCRC != NULL ) - GSsetGameCRC(ElfCRC, g_ZeroGSOptions); -} - -void SysReset() -{ - Console::Status( "Resetting PS2 virtual machine..." ); - - SysEndExecution(); - StateRecovery::Clear(); - ElfCRC = 0; - - // Note : No need to call cpuReset() here. It gets called automatically before the - // emulator resumes execution. } // Maps a block of memory for use as a recompiled code buffer, and ensures that the @@ -414,29 +303,3 @@ u8 *SysMmapEx(uptr base, u32 size, uptr bounds, const char *caller) } return Mem; } - -// Ensures existence of necessary folders, and performs error handling if the -// folders fail to create. -static void InitFolderStructure() -{ - -} - -// Returns FALSE if the core/recompiler memory allocations failed. -bool SysInit() -{ - if( sysInitialized ) return true; - sysInitialized = true; - - SysDetect(); - - PCSX2_MEM_PROTECT_BEGIN(); - Console::Status( "Initializing PS2 virtual machine..." ); - if( !SysAllocateMem() ) - return false; // critical memory allocation failure; - - SysAllocateDynarecs(); - PCSX2_MEM_PROTECT_END(); - - return true; -} diff --git a/pcsx2/System.h b/pcsx2/System.h index 7301bffbef..96127805be 100644 --- a/pcsx2/System.h +++ b/pcsx2/System.h @@ -23,36 +23,46 @@ static const int PCSX2_VersionHi = 0; static const int PCSX2_VersionMid = 9; static const int PCSX2_VersionLo = 7; - class CoreEmuThread; -extern bool SysInit(); +// -------------------------------------------------------------------------------------- +// EmuCoreAllocations class +// -------------------------------------------------------------------------------------- +class EmuCoreAllocations +{ +public: + // This set of booleans defaults to false and are only set TRUE if the corresponding + // recompilers succeeded to initialize/allocate. The host application should honor + // these booleans when selecting between recompiler or interpreter, since recompilers + // will fail to operate if these are "false." + + bool RecSuccess_EE:1, + RecSuccess_IOP:1, + RecSuccess_VU0:1, + RecSuccess_VU1:1; + +protected: + +public: + EmuCoreAllocations(); + virtual ~EmuCoreAllocations() throw(); + + bool HadSomeFailures( const Pcsx2Config::RecompilerOptions& recOpts ) const; + +protected: + void CleanupMess() throw(); +}; + + extern void SysDetect(); // Detects cpu type and fills cpuInfo structs. -extern void SysReset(); // Resets the various PS2 cpus, sub-systems, and recompilers. - -extern void SysExecute( CoreEmuThread* newThread ); -extern void SysExecute( CoreEmuThread* newThread, CDVD_SourceType cdvdsrc ); -extern void SysEndExecution(); - -extern void SysSuspend(); -extern void SysResume(); - -extern bool SysAllocateMem(); // allocates memory for all PS2 systems; returns FALSe on critical error. -extern void SysAllocateDynarecs(); // allocates memory for all dynarecs, and force-disables any failures. -extern void SysShutdownDynarecs(); -extern void SysShutdownMem(); -extern void SysShutdown(); extern void SysLoadState( const wxString& file ); -extern void SysRestorableReset(); // Saves the current emulation state prior to spu reset. extern void SysClearExecutionCache(); // clears recompiled execution caches! extern u8 *SysMmapEx(uptr base, u32 size, uptr bounds, const char *caller="Unnamed"); extern void vSyncDebugStuff( uint frame ); -extern CoreEmuThread* g_EmuThread; - ////////////////////////////////////////////////////////////////////////////////////////// // #ifdef __LINUX__ diff --git a/pcsx2/VUmicroMem.cpp b/pcsx2/VUmicroMem.cpp index fc76153743..1d9aa6f3a2 100644 --- a/pcsx2/VUmicroMem.cpp +++ b/pcsx2/VUmicroMem.cpp @@ -138,7 +138,7 @@ void vuMicroMemReset() VU1.vifRegs = vif1Regs; } -void SaveState::vuMicroFreeze() +void SaveStateBase::vuMicroFreeze() { FreezeTag( "vuMicro" ); diff --git a/pcsx2/VifDma.cpp b/pcsx2/VifDma.cpp index c2891e6011..a545e9db69 100644 --- a/pcsx2/VifDma.cpp +++ b/pcsx2/VifDma.cpp @@ -1658,7 +1658,7 @@ void vif0Reset() vif0Regs->stat &= ~VIF0_STAT_FQC; // FQC=0 } -void SaveState::vif0Freeze() +void SaveStateBase::vif0Freeze() { FreezeTag("VIFdma"); @@ -2803,7 +2803,7 @@ void vif1Reset() vif1Regs->stat &= ~VIF1_STAT_FQC; // FQC=0 } -void SaveState::vif1Freeze() +void SaveStateBase::vif1Freeze() { Freeze(vif1); diff --git a/pcsx2/gui/App.h b/pcsx2/gui/App.h index 0b99523804..f68bc871bc 100644 --- a/pcsx2/gui/App.h +++ b/pcsx2/gui/App.h @@ -30,6 +30,12 @@ class IniInterface; +BEGIN_DECLARE_EVENT_TYPES() + DECLARE_EVENT_TYPE( pxEVT_SemaphorePing, -1 ) + DECLARE_EVENT_TYPE( pxEVT_OpenModalDialog, -1 ) + DECLARE_EVENT_TYPE( pxEVT_ReloadPlugins, -1 ) +END_DECLARE_EVENT_TYPES() + // ------------------------------------------------------------------------ // All Menu Options for the Main Window! :D // ------------------------------------------------------------------------ @@ -119,6 +125,14 @@ enum MenuIdentifiers MenuId_Debug_Usermode, }; +enum DialogIdentifiers +{ + DialogId_CoreSettings = 0x800, + DialogId_BiosSelector, + DialogId_LogOptions, + DialogId_About, +}; + ////////////////////////////////////////////////////////////////////////////////////////// // ScopedWindowDisable // @@ -197,53 +211,89 @@ struct AppImageIds } Toolbars; }; -////////////////////////////////////////////////////////////////////////////////////////// -// -class pxAppTraits : public wxGUIAppTraits + +struct MsgboxEventResult { -#ifdef __WXDEBUG__ -public: - virtual bool ShowAssertDialog(const wxString& msg); - -protected: - virtual wxString GetAssertStackTrace(); -#endif + Semaphore WaitForMe; + int result; + MsgboxEventResult() : + WaitForMe(), result( 0 ) + { + } }; -////////////////////////////////////////////////////////////////////////////////////////// -// +// -------------------------------------------------------------------------------------- +// Pcsx2App - main wxApp class +// -------------------------------------------------------------------------------------- + class Pcsx2App : public wxApp { protected: + wxImageList m_ConfigImages; + + wxScopedPtr m_ToolbarImages; + wxScopedPtr m_Bitmap_Logo; + + wxScopedPtr m_CoreAllocs; + +public: + wxScopedPtr m_CorePlugins; + wxScopedPtr m_CoreThread; + +protected: + // Note: Pointers to frames should not be scoped because wxWidgets handles deletion + // of these objects internally. MainEmuFrame* m_MainFrame; ConsoleLogFrame* m_ProgramLogBox; - wxBitmap* m_Bitmap_Logo; - wxImageList m_ConfigImages; - bool m_ConfigImagesAreLoaded; - - wxImageList* m_ToolbarImages; // dynamic (pointer) to allow for large/small redefinition. - AppImageIds m_ImageId; + bool m_ConfigImagesAreLoaded; + AppImageIds m_ImageId; public: Pcsx2App(); virtual ~Pcsx2App(); - wxFrame* GetMainWindow() const; + void ReloadPlugins(); - bool OnInit(); - int OnExit(); - void CleanUp(); + void ApplySettings( const AppConfig* oldconf = NULL ); + void LoadSettings(); + void SaveSettings(); + + void PostMenuAction( MenuIdentifiers menu_id ) const; + int ThreadedModalDialog( DialogIdentifiers dialogId ); + void Ping() const; - void OnInitCmdLine( wxCmdLineParser& parser ); - bool OnCmdLineParsed( wxCmdLineParser& parser ); - bool OnCmdLineError( wxCmdLineParser& parser ); bool PrepForExit(); + + // Executes the emulator using a saved/existing virtual machine state and currently + // configured CDVD source device. + // Debug assertions: + void SysExecute(); + void SysExecute( CDVD_SourceType cdvdsrc ); -#ifdef __WXDEBUG__ - void OnAssertFailure( const wxChar *file, int line, const wxChar *func, const wxChar *cond, const wxChar *msg ); -#endif + void SysResume() + { + if( !m_CoreThread ) return; + m_CoreThread->Resume(); + } + + void SysSuspend() + { + if( !m_CoreThread ) return; + m_CoreThread->Suspend(); + } + + void SysReset() + { + m_CoreThread.reset(); + m_CorePlugins.reset(); + } + + bool EmuInProgress() const + { + return m_CoreThread && m_CoreThread->IsRunning(); + } const wxBitmap& GetLogoBitmap(); wxImageList& GetImgList_Config(); @@ -253,16 +303,24 @@ public: MainEmuFrame& GetMainFrame() const { + wxASSERT( ((uptr)GetTopWindow()) == ((uptr)m_MainFrame) ); wxASSERT( m_MainFrame != NULL ); return *m_MainFrame; } - void PostMenuAction( MenuIdentifiers menu_id ) const; - void Ping() const; + // -------------------------------------------------------------------------- + // Overrides of wxApp virtuals: + // -------------------------------------------------------------------------- + bool OnInit(); + int OnExit(); - void ApplySettings( const AppConfig& newconf ); - void LoadSettings(); - void SaveSettings(); + void OnInitCmdLine( wxCmdLineParser& parser ); + bool OnCmdLineParsed( wxCmdLineParser& parser ); + bool OnCmdLineError( wxCmdLineParser& parser ); + +#ifdef __WXDEBUG__ + void OnAssertFailure( const wxChar *file, int line, const wxChar *func, const wxChar *cond, const wxChar *msg ); +#endif // ---------------------------------------------------------------------------- // Console / Program Logging Helpers @@ -303,7 +361,9 @@ protected: void HandleEvent(wxEvtHandler *handler, wxEventFunction func, wxEvent& event) const; + void OnReloadPlugins( wxCommandEvent& evt ); void OnSemaphorePing( wxCommandEvent& evt ); + void OnOpenModalDialog( wxCommandEvent& evt ); void OnMessageBox( pxMessageBoxEvent& evt ); void OnEmuKeyDown( wxKeyEvent& evt ); @@ -322,15 +382,18 @@ protected: void OnUnhandledException() { throw; } }; -////////////////////////////////////////////////////////////////////////////////////////// -// + +// -------------------------------------------------------------------------------------- +// AppEmuThread class +// -------------------------------------------------------------------------------------- + class AppEmuThread : public CoreEmuThread { protected: wxKeyEvent m_kevt; public: - AppEmuThread(); + AppEmuThread( PluginManager& plugins ); virtual ~AppEmuThread() { } virtual void Resume(); @@ -340,14 +403,29 @@ protected: sptr ExecuteTask(); }; - - DECLARE_APP(Pcsx2App) +class EntryGuard +{ +public: + int& Counter; + EntryGuard( int& counter ) : Counter( counter ) + { ++Counter; } + + virtual ~EntryGuard() throw() + { --Counter; } + + bool IsReentrant() const { return Counter > 1; } +}; + extern int EnumeratePluginsInFolder( const wxDirName& searchPath, wxArrayString* dest ); -extern void LoadPlugins(); -extern void InitPlugins(); -extern void OpenPlugins(); +extern void LoadPluginsPassive(); +extern void LoadPluginsImmediate(); +extern void UnloadPlugins(); extern wxRect wxGetDisplayArea(); extern bool pxIsValidWindowPosition( const wxWindow& window, const wxPoint& windowPos ); + +extern bool HandlePluginError( Exception::PluginError& ex ); +extern bool EmulationInProgress(); + diff --git a/pcsx2/gui/AppAssert.cpp b/pcsx2/gui/AppAssert.cpp index 93c7409413..618c917f32 100644 --- a/pcsx2/gui/AppAssert.cpp +++ b/pcsx2/gui/AppAssert.cpp @@ -68,10 +68,6 @@ static wxString pxGetStackTrace() static __threadlocal bool _reentrant_lock = false; #ifdef __WXDEBUG__ -wxString pxAppTraits::GetAssertStackTrace() -{ - return pxGetStackTrace(); -} // This override of wx's implementation provides thread safe assertion message reporting. If we aren't // on the main gui thread then the assertion message box needs to be passed off to the main gui thread @@ -101,8 +97,8 @@ void Pcsx2App::OnAssertFailure( const wxChar *file, int line, const wxChar *func // make life easier for people using VC++ IDE by using this format, which allows double-click // response times from the Output window... dbgmsg.Printf( L"%s(%d) : assertion failed%s%s: %s", file, line, - (func==NULL) ? L"" : L" in ", - (func==NULL) ? L"" : func, + (func==NULL) ? wxEmptyString : L" in ", + (func==NULL) ? wxEmptyString : func, message.c_str() ); diff --git a/pcsx2/gui/AppConfig.cpp b/pcsx2/gui/AppConfig.cpp index d5ed445936..eeaea5c106 100644 --- a/pcsx2/gui/AppConfig.cpp +++ b/pcsx2/gui/AppConfig.cpp @@ -94,7 +94,10 @@ namespace PathDefs // share with other programs: screenshots, memory cards, and savestates. wxDirName GetDocuments() { - return (wxDirName)g_Conf->GetDefaultDocumentsFolder(); + if( UseAdminMode ) + return (wxDirName)wxGetCwd(); + else + return (wxDirName)Path::Combine( wxStandardPaths::Get().GetDocumentsDir(), wxGetApp().GetAppName() ); } wxDirName GetSnapshots() @@ -155,20 +158,12 @@ namespace PathDefs } }; -wxString AppConfig::GetDefaultDocumentsFolder() -{ - if( UseAdminMode ) - return wxGetCwd(); - else - return Path::Combine( wxStandardPaths::Get().GetDocumentsDir(), wxGetApp().GetAppName() ); -} - const wxDirName& AppConfig::FolderOptions::operator[]( FoldersEnum_t folderidx ) const { switch( folderidx ) { case FolderId_Plugins: return Plugins; - case FolderId_Settings: return Settings; + case FolderId_Settings: return SettingsFolder; case FolderId_Bios: return Bios; case FolderId_Snapshots: return Snapshots; case FolderId_Savestates: return Savestates; @@ -185,7 +180,7 @@ const bool AppConfig::FolderOptions::IsDefault( FoldersEnum_t folderidx ) const switch( folderidx ) { case FolderId_Plugins: return UseDefaultPlugins; - case FolderId_Settings: return UseDefaultSettings; + case FolderId_Settings: return UseDefaultSettingsFolder; case FolderId_Bios: return UseDefaultBios; case FolderId_Snapshots: return UseDefaultSnapshots; case FolderId_Savestates: return UseDefaultSavestates; @@ -207,8 +202,8 @@ void AppConfig::FolderOptions::Set( FoldersEnum_t folderidx, const wxString& src break; case FolderId_Settings: - Settings = src; - UseDefaultSettings = useDefault; + SettingsFolder = src; + UseDefaultSettingsFolder = useDefault; break; case FolderId_Bios: @@ -240,8 +235,9 @@ void AppConfig::FolderOptions::Set( FoldersEnum_t folderidx, const wxString& src } } -////////////////////////////////////////////////////////////////////////////////////////// -// +// -------------------------------------------------------------------------------------- +// Default Filenames +// -------------------------------------------------------------------------------------- namespace FilenameDefs { wxFileName GetConfig() @@ -290,9 +286,14 @@ wxString AppConfig::FullpathTo( PluginsEnum_t pluginidx ) const return Path::Combine( Folders.Plugins, BaseFilenames[pluginidx] ); } -wxString AppConfig::FullPathToConfig() const +wxDirName GetSettingsFolder() { - return g_Conf->Folders.Settings.Combine( FilenameDefs::GetConfig() ).GetFullPath(); + return UseDefaultSettingsFolder ? PathDefs::GetSettings() : SettingsFolder; +} + +wxString GetSettingsFilename() +{ + return GetSettingsFolder().Combine( FilenameDefs::GetConfig() ).GetFullPath(); } @@ -349,7 +350,8 @@ void AppConfig::LoadSaveUserMode( IniInterface& ini, const wxString& cwdhash ) ini.GetConfig().Write( L"Timestamp", timestamp_now );*/ ini.Entry( L"UseAdminMode", UseAdminMode, false ); - ini.Entry( L"SettingsPath", Folders.Settings, PathDefs::GetSettings() ); + ini.Entry( L"UseDefaultSettingsFolder", UseDefaultSettingsFolder, true ); + ini.Entry( L"SettingsFolder", SettingsFolder, PathDefs::GetSettings() ); ini.Flush(); } @@ -404,40 +406,6 @@ void AppConfig::LoadSave( IniInterface& ini ) ini.Flush(); } -// ------------------------------------------------------------------------ -// Performs necessary operations to ensure that the current g_Conf settings (and other config-stored -// globals) are applied to the pcsx2 main window and primary emulation subsystems (if active). -// -void AppConfig::Apply() -{ - Folders.ApplyDefaults(); - - // Ensure existence of necessary documents folders. Plugins and other parts - // of PCSX2 rely on them. - - Folders.MemoryCards.Mkdir(); - Folders.Savestates.Mkdir(); - Folders.Snapshots.Mkdir(); - - EmuOptions.BiosFilename = FullpathToBios(); - - // Update the compression attribute on the Memcards folder. - // Memcards generally compress very well via NTFS compression. - - NTFS_CompressFile( Folders.MemoryCards.ToString(), McdEnableNTFS ); - - { - wxDoNotLogInThisScope please; - if( !i18n_SetLanguage( LanguageId ) ) - { - if( !i18n_SetLanguage( wxLANGUAGE_DEFAULT ) ) - { - i18n_SetLanguage( wxLANGUAGE_ENGLISH ); - } - } - } -} - // ------------------------------------------------------------------------ AppConfig::ConsoleLogOptions::ConsoleLogOptions() : Visible( false ) @@ -463,7 +431,6 @@ void AppConfig::ConsoleLogOptions::LoadSave( IniInterface& ini, const wxChar* lo void AppConfig::FolderOptions::ApplyDefaults() { if( UseDefaultPlugins ) Plugins = PathDefs::GetPlugins(); - if( UseDefaultSettings ) Settings = PathDefs::GetSettings(); if( UseDefaultBios ) Bios = PathDefs::GetBios(); if( UseDefaultSnapshots ) Snapshots = PathDefs::GetSnapshots(); if( UseDefaultSavestates ) Savestates = PathDefs::GetSavestates(); @@ -475,7 +442,6 @@ void AppConfig::FolderOptions::ApplyDefaults() AppConfig::FolderOptions::FolderOptions() : bitset( 0xffffffff ) , Plugins( PathDefs::GetPlugins() ) -, Settings( PathDefs::GetSettings() ) , Bios( PathDefs::GetBios() ) , Snapshots( PathDefs::GetSnapshots() ) , Savestates( PathDefs::GetSavestates() ) @@ -503,7 +469,6 @@ void AppConfig::FolderOptions::LoadSave( IniInterface& ini ) IniBitBool( UseDefaultLogs ); IniEntry( Plugins ); - IniEntry( Settings ); IniEntry( Bios ); IniEntry( Snapshots ); IniEntry( Savestates ); @@ -552,13 +517,13 @@ void AppConfig_ReloadGlobalSettings( bool overwrite ) PathDefs::GetSettings().Mkdir(); // Allow wx to use our config, and enforces auto-cleanup as well - delete wxConfigBase::Set( OpenFileConfig( g_Conf->FullPathToConfig() ) ); + delete wxConfigBase::Set( OpenFileConfig( GetSettingsFilename() ) ); wxConfigBase::Get()->SetRecordDefaults(); if( !overwrite ) wxGetApp().LoadSettings(); - wxGetApp().ApplySettings( *g_Conf ); + wxGetApp().ApplySettings(); g_Conf->Folders.Logs.Mkdir(); wxString newlogname( Path::Combine( g_Conf->Folders.Logs.ToString(), L"emuLog.txt" ) ); diff --git a/pcsx2/gui/AppConfig.h b/pcsx2/gui/AppConfig.h index 87abb9b7cc..30776fc07a 100644 --- a/pcsx2/gui/AppConfig.h +++ b/pcsx2/gui/AppConfig.h @@ -21,7 +21,12 @@ class IniInterface; class wxFileConfig; -extern bool UseAdminMode; // dictates if the program uses /home/user or /cwd for the program data +extern bool UseAdminMode; // dictates if the program uses /home/user or /cwd for the program data +extern wxDirName SettingsFolder; // dictates where the settings folder comes from, *if* UseDefaultSettingsFolder is FALSE. +extern bool UseDefaultSettingsFolder; // when TRUE, pcsx2 derives the settings folder from the UseAdminMode + +wxDirName GetSettingsFolder(); +wxString GetSettingsFilename(); ////////////////////////////////////////////////////////////////////////////////////////// // Pcsx2 Application Configuration. @@ -33,15 +38,15 @@ public: // ------------------------------------------------------------------------ struct ConsoleLogOptions { - bool Visible; + bool Visible; // if true, DisplayPos is ignored and the console is automatically docked to the main window. - bool AutoDock; + bool AutoDock; // Display position used if AutoDock is false (ignored otherwise) - wxPoint DisplayPosition; - wxSize DisplaySize; + wxPoint DisplayPosition; + wxSize DisplaySize; // Size of the font in points. - int FontSize; + int FontSize; ConsoleLogOptions(); void LoadSave( IniInterface& conf, const wxChar* title ); @@ -59,11 +64,10 @@ public: UseDefaultSavestates:1, UseDefaultMemoryCards:1, UseDefaultLogs:1; - }; }; + BITFIELD_END wxDirName Plugins, - Settings, Bios, Snapshots, Savestates, @@ -156,14 +160,10 @@ public: wxString FullpathToBios() const; wxString FullpathToMcd( uint port, uint slot ) const; wxString FullpathTo( PluginsEnum_t pluginId ) const; - wxString FullPathToConfig() const; void LoadSaveUserMode( IniInterface& ini, const wxString& cwdhash ); - wxString GetDefaultDocumentsFolder(); - protected: - void Apply(); void LoadSave( IniInterface& ini ); void LoadSaveMemcards( IniInterface& ini ); @@ -181,4 +181,4 @@ extern ConfigOverrides OverrideOptions; extern wxFileConfig* OpenFileConfig( const wxString& filename ); extern void AppConfig_ReloadGlobalSettings( bool overwrite = false ); -extern AppConfig* g_Conf; +extern wxScopedPtr g_Conf; diff --git a/pcsx2/gui/AppMain.cpp b/pcsx2/gui/AppMain.cpp index 89d7429ad6..d2a0bf9f46 100644 --- a/pcsx2/gui/AppMain.cpp +++ b/pcsx2/gui/AppMain.cpp @@ -19,6 +19,9 @@ #include "Plugins.h" #include "Dialogs/ModalPopups.h" +#include "Dialogs/ConfigurationDialog.h" +#include "Dialogs/LogOptionsDialog.h" + #include "Utilities/ScopedPtr.h" #include "Utilities/HashMap.h" @@ -28,15 +31,16 @@ IMPLEMENT_APP(Pcsx2App) -BEGIN_DECLARE_EVENT_TYPES() - DECLARE_EVENT_TYPE( pxEVT_SemaphorePing, -1 ) -END_DECLARE_EVENT_TYPES() +DEFINE_EVENT_TYPE( pxEVT_SemaphorePing ); +DEFINE_EVENT_TYPE( pxEVT_OpenModalDialog ); +DEFINE_EVENT_TYPE( pxEVT_ReloadPlugins ); -DEFINE_EVENT_TYPE( pxEVT_SemaphorePing ) +bool UseAdminMode = false; +wxDirName SettingsFolder; +bool UseDefaultSettingsFolder = true; -bool UseAdminMode = false; -AppConfig* g_Conf = NULL; -ConfigOverrides OverrideOptions; +wxScopedPtr g_Conf; +ConfigOverrides OverrideOptions; namespace Exception { @@ -57,8 +61,8 @@ namespace Exception }; } -AppEmuThread::AppEmuThread() : - CoreEmuThread() +AppEmuThread::AppEmuThread( PluginManager& plugins ) : + CoreEmuThread( plugins ) , m_kevt() { } @@ -127,19 +131,50 @@ void AppEmuThread::StateCheck() } } -static bool HandlePluginError( Exception::PluginError& ex ) +// Executes the emulator using a saved/existing virtual machine state and currently +// configured CDVD source device. +void Pcsx2App::SysExecute() { + SysReset(); + LoadPluginsImmediate(); + m_CoreThread.reset( new AppEmuThread( *m_CorePlugins ) ); + m_CoreThread->Resume(); +} + +// Executes the specified cdvd source and optional elf file. This command performs a +// full closure of any existing VM state and starts a fresh VM with the requested +// sources. +void Pcsx2App::SysExecute( CDVD_SourceType cdvdsrc ) +{ + SysReset(); + LoadPluginsImmediate(); + CDVDsys_SetFile( CDVDsrc_Iso, g_Conf->CurrentIso ); + CDVDsys_ChangeSource( cdvdsrc ); + m_CoreThread.reset( new AppEmuThread( *m_CorePlugins ) ); + m_CoreThread->Resume(); +} + +__forceinline bool EmulationInProgress() +{ + return wxGetApp().EmuInProgress(); +} + + +bool HandlePluginError( Exception::PluginError& ex ) +{ + if( pxDialogExists( DialogId_CoreSettings ) ) return true; + bool result = Msgbox::OkCancel( ex.FormatDisplayMessage() + - _("\n\nPress Ok to go to the Plugin Configuration Panel.") ); + _("\n\nPress Ok to go to the Plugin Configuration Panel.") + ); if( result ) { g_Conf->SettingsTabName = L"Plugins"; - wxGetApp().PostMenuAction( MenuId_Config_Settings ); - wxGetApp().Ping(); // fixme: Send a message to the panel to select the failed plugin. - // fixme: handle case where user cancels the settings dialog. (should return FALSE). + if( Dialogs::ConfigurationDialog().ShowModal() == wxID_CANCEL ) + return false; } return result; } @@ -162,11 +197,14 @@ sptr AppEmuThread::ExecuteTask() if( result ) { - wxGetApp().PostMenuAction( MenuId_Config_BIOS ); - wxGetApp().Ping(); - - // fixme: handle case where user cancels the settings dialog. (should return FALSE). - // fixme: automatically re-try emu startup here... + if( wxGetApp().ThreadedModalDialog( DialogId_BiosSelector ) == wxID_CANCEL ) + { + // fixme: handle case where user cancels the settings dialog. (should return FALSE). + } + else + { + // fixme: automatically re-try emu startup here... + } } } } @@ -174,10 +212,13 @@ sptr AppEmuThread::ExecuteTask() catch( Exception::PluginError& ex ) { GetPluginManager().Close(); - if( HandlePluginError( ex ) ) + Console::Error( ex.FormatDiagnosticMessage() ); + Msgbox::Alert( ex.FormatDisplayMessage(), _("Plugin Open Error") ); + + /*if( HandlePluginError( ex ) ) { // fixme: automatically re-try emu startup here... - } + }*/ } // ---------------------------------------------------------------------------- // [TODO] : Add exception handling here for debuggable PS2 exceptions that allows @@ -194,8 +235,6 @@ sptr AppEmuThread::ExecuteTask() } -wxFrame* Pcsx2App::GetMainWindow() const { return m_MainFrame; } - void Pcsx2App::OpenWizardConsole() { if( !IsDebugBuild ) return; @@ -254,7 +293,7 @@ void Pcsx2App::ReadUserModeSettings() IniLoader loader( *conf_usermode ); g_Conf->LoadSaveUserMode( loader, groupname ); - if( !wxFile::Exists( g_Conf->FullPathToConfig() ) ) + if( !wxFile::Exists( GetSettingsFilename() ) ) { // user wiped their pcsx2.ini -- needs a reconfiguration via wizard! // (we skip the first page since it's a usermode.ini thing) @@ -361,7 +400,7 @@ bool Pcsx2App::OnInit() wxInitAllImageHandlers(); if( !wxApp::OnInit() ) return false; - g_Conf = new AppConfig(); + g_Conf.reset( new AppConfig() ); wxLocale::AddCatalogLookupPathPrefix( wxGetCwd() ); @@ -371,6 +410,8 @@ bool Pcsx2App::OnInit() Connect( pxEVT_MSGBOX, pxMessageBoxEventThing( Pcsx2App::OnMessageBox ) ); Connect( pxEVT_CallStackBox, pxMessageBoxEventThing( Pcsx2App::OnMessageBox ) ); Connect( pxEVT_SemaphorePing, wxCommandEventHandler( Pcsx2App::OnSemaphorePing ) ); + Connect( pxEVT_OpenModalDialog, wxCommandEventHandler( Pcsx2App::OnOpenModalDialog ) ); + Connect( pxEVT_ReloadPlugins, wxCommandEventHandler( Pcsx2App::OnReloadPlugins ) ); Connect( pxID_Window_GS, wxEVT_KEY_DOWN, wxKeyEventHandler( Pcsx2App::OnEmuKeyDown ) ); @@ -406,9 +447,51 @@ bool Pcsx2App::OnInit() SetExitOnFrameDelete( true ); // but being explicit doesn't hurt... m_MainFrame->Show(); - SysInit(); - ApplySettings( *g_Conf ); - InitPlugins(); + SysDetect(); + ApplySettings(); + + m_CoreAllocs.reset( new EmuCoreAllocations() ); + + if( m_CoreAllocs->HadSomeFailures( g_Conf->EmuOptions.Cpu.Recompiler ) ) + { + wxString message( _("The following cpu recompilers failed to initialize and will not be available:\n\n") ); + + if( !m_CoreAllocs->RecSuccess_EE ) + { + message += L"\t* R5900 (EE)\n"; + g_Session.ForceDisableEErec = true; + } + + if( !m_CoreAllocs->RecSuccess_IOP ) + { + message += L"\t* R3000A (IOP)\n"; + g_Session.ForceDisableIOPrec = true; + } + + if( !m_CoreAllocs->RecSuccess_VU0 ) + { + message += L"\t* VU0\n"; + g_Session.ForceDisableVU0rec = true; + } + + if( !m_CoreAllocs->RecSuccess_VU1 ) + { + message += L"\t* VU1\n"; + g_Session.ForceDisableVU1rec = true; + } + + message += pxE( ".Popup Error:EmuCore:MemoryForRecs", + L"These errors are the result of memory allocation failures (see the program log for details). " + L"Closing out some memory hogging background tasks may resolve this error.\n\n" + L"These recompilers have been disabled and interpreters will be used in their place. " + L"Interpreters can be very slow, so don't get too excited. Press OK to continue or CANCEL to close PCSX2." + ); + + if( !Msgbox::OkCancel( message, _("PCSX2 Initialization Error"), wxICON_ERROR ) ) + return false; + } + + LoadPluginsPassive(); } // ---------------------------------------------------------------------------- catch( Exception::StartupAborted& ex ) @@ -417,10 +500,16 @@ bool Pcsx2App::OnInit() return false; } // ---------------------------------------------------------------------------- - catch( Exception::PluginError& ex ) + // Failures on the core initialization procedure (typically OutOfMemory errors) are bad, + // since it means the emulator is completely non-functional. Let's pop up an error and + // exit gracefully-ish. + // + catch( Exception::RuntimeError& ex ) { - if( !HandlePluginError( ex ) ) - return false; + Console::Error( ex.FormatDiagnosticMessage() ); + Msgbox::Alert( ex.FormatDisplayMessage() + L"\n\nPress OK to close PCSX2.", + _("PCSX2 Critical Error"), wxICON_ERROR ); + return false; } return true; } @@ -433,11 +522,27 @@ void Pcsx2App::PostMenuAction( MenuIdentifiers menu_id ) const if( m_MainFrame == NULL ) return; wxCommandEvent joe( wxEVT_COMMAND_MENU_SELECTED, menu_id ); - m_MainFrame->GetEventHandler()->AddPendingEvent( joe ); + if( wxThread::IsMain() ) + m_MainFrame->GetEventHandler()->ProcessEvent( joe ); + else + m_MainFrame->GetEventHandler()->AddPendingEvent( joe ); +} + +int Pcsx2App::ThreadedModalDialog( DialogIdentifiers dialogId ) +{ + wxASSERT( !wxThread::IsMain() ); // don't call me from MainThread! + + MsgboxEventResult result; + wxCommandEvent joe( pxEVT_OpenModalDialog, dialogId ); + joe.SetClientData( &result ); + AddPendingEvent( joe ); + result.WaitForMe.WaitNoCancel(); + return result.result; } // Waits for the main GUI thread to respond. If run from the main GUI thread, returns -// immediately without error. +// immediately without error. Use this on non-GUI threads to have them sleep until +// the GUI has processed all its pending messages. void Pcsx2App::Ping() const { if( wxThread::IsMain() ) return; @@ -458,6 +563,53 @@ void Pcsx2App::OnSemaphorePing( wxCommandEvent& evt ) ((Semaphore*)evt.GetClientData())->Post(); } +void Pcsx2App::OnOpenModalDialog( wxCommandEvent& evt ) +{ + using namespace Dialogs; + + MsgboxEventResult& evtres( *((MsgboxEventResult*)evt.GetClientData()) ); + switch( evt.GetId() ) + { + case DialogId_CoreSettings: + { + static int _guard = 0; + EntryGuard guard( _guard ); + if( guard.IsReentrant() ) return; + evtres.result = ConfigurationDialog().ShowModal(); + } + break; + + case DialogId_BiosSelector: + { + static int _guard = 0; + EntryGuard guard( _guard ); + if( guard.IsReentrant() ) return; + evtres.result = BiosSelectorDialog().ShowModal(); + } + break; + + case DialogId_LogOptions: + { + static int _guard = 0; + EntryGuard guard( _guard ); + if( guard.IsReentrant() ) return; + evtres.result = LogOptionsDialog().ShowModal(); + } + break; + + case DialogId_About: + { + static int _guard = 0; + EntryGuard guard( _guard ); + if( guard.IsReentrant() ) return; + evtres.result = AboutBoxDialog().ShowModal(); + } + break; + } + + evtres.WaitForMe.Post(); +} + void Pcsx2App::OnMessageBox( pxMessageBoxEvent& evt ) { Msgbox::OnEvent( evt ); @@ -467,16 +619,9 @@ void Pcsx2App::OnMessageBox( pxMessageBoxEvent& evt ) void Pcsx2App::CleanupMess() { - safe_delete( m_Bitmap_Logo ); - safe_delete( g_Conf ); -} - -// This cleanup procedure is issued by wxWidgets prior to destroying base windows and window -// classes. -void Pcsx2App::CleanUp() -{ - SysShutdown(); - wxApp::CleanUp(); + m_CorePlugins.reset(); + m_ProgramLogBox = NULL; + m_MainFrame = NULL; } void Pcsx2App::HandleEvent(wxEvtHandler *handler, wxEventFunction func, wxEvent& event) const @@ -489,7 +634,11 @@ void Pcsx2App::HandleEvent(wxEvtHandler *handler, wxEventFunction func, wxEvent& catch( Exception::PluginError& ex ) { Console::Error( ex.FormatDiagnosticMessage() ); - Msgbox::Alert( ex.FormatDisplayMessage() ); + if( !HandlePluginError( ex ) ) + { + Console::Error( L"User-canceled plugin configuration after load failure. Plugins not loaded!" ); + Msgbox::Alert( _("Warning! Plugins have not been loaded. PCSX2 will be inoperable.") ); + } } // ---------------------------------------------------------------------------- catch( Exception::RuntimeError& ex ) @@ -505,15 +654,15 @@ void Pcsx2App::HandleEvent(wxEvtHandler *handler, wxEventFunction func, wxEvent& // Common exit handler which can be called from any event (though really it should // be called only from CloseWindow handlers since that's the more appropriate way -// to handle window closures) +// to handle cancelable window closures) +// +// returns true if the app can close, or false if the close event was canceled by +// the glorious user, whomever (s)he-it might be. bool Pcsx2App::PrepForExit() { - SysShutdown(); + m_CoreThread.reset(); CleanupMess(); - m_ProgramLogBox = NULL; - m_MainFrame = NULL; - return true; } @@ -521,9 +670,12 @@ int Pcsx2App::OnExit() { PrepForExit(); - if( g_Conf != NULL ) + if( g_Conf ) SaveSettings(); + while( wxGetLocale() != NULL ) + delete wxGetLocale(); + return wxApp::OnExit(); } @@ -540,47 +692,48 @@ Pcsx2App::Pcsx2App() : Pcsx2App::~Pcsx2App() { + // Typically OnExit cleans everything up before we get here, *unless* we cancel + // out of program startup in OnInit (return false) -- then remaning cleanup needs + // to happen here in the destructor. + CleanupMess(); - while( wxGetLocale() != NULL ) - delete wxGetLocale(); } - -void Pcsx2App::ApplySettings( const AppConfig& newconf ) +void Pcsx2App::ApplySettings( const AppConfig* oldconf ) { + DevAssert( wxThread::IsMain(), "ApplySettings valid from the GUI thread only." ); - if( &newconf != g_Conf ) + // Ensure existence of necessary documents folders. Plugins and other parts + // of PCSX2 rely on them. + + g_Conf->Folders.MemoryCards.Mkdir(); + g_Conf->Folders.Savestates.Mkdir(); + g_Conf->Folders.Snapshots.Mkdir(); + + g_Conf->EmuOptions.BiosFilename = g_Conf->FullpathToBios(); + + // Update the compression attribute on the Memcards folder. + // Memcards generally compress very well via NTFS compression. + + NTFS_CompressFile( g_Conf->Folders.MemoryCards.ToString(), g_Conf->McdEnableNTFS ); + + if( (oldconf == NULL) || (oldconf->LanguageId != g_Conf->LanguageId) ) { - // Need to unload the current emulation state if the user changed plugins, because - // the whole plugin system needs to be re-loaded. - - const PluginInfo* pi = tbl_PluginInfo-1; - while( ++pi, pi->shortname != NULL ) + wxDoNotLogInThisScope please; + if( !i18n_SetLanguage( g_Conf->LanguageId ) ) { - if( newconf.FullpathTo( pi->id ) != g_Conf->FullpathTo( pi->id ) ) - break; - } - if( pi->shortname != NULL ) - { - // [TODO] : Post notice that this shuts down existing emulation. - SysEndExecution(); - safe_delete( g_plugins ); - // Think safe to do this earlier, but not positive. - // Have to update the config *before* loading the new plugins. - *g_Conf = newconf; - LoadPlugins(); - } - else { - *g_Conf = newconf; + if( !i18n_SetLanguage( wxLANGUAGE_DEFAULT ) ) + { + i18n_SetLanguage( wxLANGUAGE_ENGLISH ); + } } } - g_Conf->Apply(); if( m_MainFrame != NULL ) m_MainFrame->ApplySettings(); - if( g_EmuThread != NULL ) - g_EmuThread->ApplySettings( g_Conf->EmuOptions ); + if( m_CoreThread ) + m_CoreThread->ApplySettings( g_Conf->EmuOptions ); } void Pcsx2App::LoadSettings() diff --git a/pcsx2/gui/AppRes.cpp b/pcsx2/gui/AppRes.cpp index aee5260a2b..67773c3c45 100644 --- a/pcsx2/gui/AppRes.cpp +++ b/pcsx2/gui/AppRes.cpp @@ -90,7 +90,7 @@ const wxBitmap& Pcsx2App::GetLogoBitmap() wxImage img; EmbeddedImage temp; // because gcc can't allow non-const temporaries. LoadImageAny( img, useTheme, mess, L"BackgroundLogo", temp ); - m_Bitmap_Logo = new wxBitmap( img ); + m_Bitmap_Logo.reset( new wxBitmap( img ) ); return *m_Bitmap_Logo; } @@ -140,7 +140,7 @@ wxImageList& Pcsx2App::GetImgList_Toolbars() if( m_ToolbarImages == NULL ) { const int imgSize = g_Conf->Toolbar_ImageSize ? 64 : 32; - m_ToolbarImages = new wxImageList( imgSize, imgSize ); + m_ToolbarImages.reset( new wxImageList( imgSize, imgSize ) ); wxFileName mess; bool useTheme = (g_Conf->DeskTheme != L"default"); diff --git a/pcsx2/gui/ConsoleLogger.cpp b/pcsx2/gui/ConsoleLogger.cpp index c62c0b62da..667cb2d9da 100644 --- a/pcsx2/gui/ConsoleLogger.cpp +++ b/pcsx2/gui/ConsoleLogger.cpp @@ -686,17 +686,6 @@ static int pxCallstackDialog( const wxString& content, const wxString& caption, return wxMessageDialog( NULL, content, caption, flags ).ShowModal(); } -struct MsgboxEventResult -{ - Semaphore WaitForMe; - int result; - - MsgboxEventResult() : - WaitForMe(), result( 0 ) - { - } -}; - class pxMessageBoxEvent : public wxEvent { protected: diff --git a/pcsx2/gui/ConsoleLogger.h b/pcsx2/gui/ConsoleLogger.h index 9cf90e2844..db9012fe23 100644 --- a/pcsx2/gui/ConsoleLogger.h +++ b/pcsx2/gui/ConsoleLogger.h @@ -58,7 +58,7 @@ public: { } - ~ConsoleTestThread() + ~ConsoleTestThread() throw() { m_done = true; } diff --git a/pcsx2/gui/Dialogs/AboutBoxDialog.cpp b/pcsx2/gui/Dialogs/AboutBoxDialog.cpp index 4001c9527d..36742ca277 100644 --- a/pcsx2/gui/Dialogs/AboutBoxDialog.cpp +++ b/pcsx2/gui/Dialogs/AboutBoxDialog.cpp @@ -26,23 +26,27 @@ using namespace wxHelpers; -namespace Dialogs { - -////////////////////////////////////////////////////////////////////////////////////////// -// Helper class for creating wxStaticText labels which are aligned to center. -// (automates the passing of wxDefaultSize and wxDefaultPosition) -// -class StaticTextCentered : public wxStaticText +namespace Dialogs { -public: - StaticTextCentered( wxWindow* parent, const wxString& text, int id=wxID_ANY ) : - wxStaticText( parent, id, text, wxDefaultPosition, wxDefaultSize, wxALIGN_CENTER ) + // Helper class for creating wxStaticText labels which are aligned to center. + // (automates the passing of wxDefaultSize and wxDefaultPosition) + // + class StaticTextCentered : public wxStaticText { - } -}; + public: + StaticTextCentered( wxWindow* parent, const wxString& text, int id=wxID_ANY ) : + wxStaticText( parent, id, text, wxDefaultPosition, wxDefaultSize, wxALIGN_CENTER ) + { + } + }; +} -AboutBoxDialog::AboutBoxDialog( wxWindow* parent, int id ): +// -------------------------------------------------------------------------------------- +// AboutBoxDialog Implementation +// -------------------------------------------------------------------------------------- + +Dialogs::AboutBoxDialog::AboutBoxDialog( wxWindow* parent, int id ): wxDialog( parent, id, _("About PCSX2"), parent->GetPosition()-wxSize( 32, 32 ) ), m_bitmap_logo( this, wxID_ANY, wxGetApp().GetLogoBitmap(), wxDefaultPosition, wxDefaultSize, wxBORDER_SUNKEN ), @@ -120,5 +124,3 @@ AboutBoxDialog::AboutBoxDialog( wxWindow* parent, int id ): mainSizer.Add( new wxButton( this, wxID_OK, L"I've seen enough"), SizerFlags::StdCenter() ); SetSizerAndFit( &mainSizer ); } - -} // end namespace Dialogs diff --git a/pcsx2/gui/Dialogs/ConfigurationDialog.cpp b/pcsx2/gui/Dialogs/ConfigurationDialog.cpp index 3421880418..0bd6a9596c 100644 --- a/pcsx2/gui/Dialogs/ConfigurationDialog.cpp +++ b/pcsx2/gui/Dialogs/ConfigurationDialog.cpp @@ -144,8 +144,8 @@ void Dialogs::ConfigurationDialog::OnApply_Click( wxCommandEvent& evt ) // ---------------------------------------------------------------------------- -Dialogs::BiosSelectorDialog::BiosSelectorDialog( wxWindow* parent ) : - wxDialogWithHelpers( parent, wxID_ANY, _("BIOS Selector"), false ) +Dialogs::BiosSelectorDialog::BiosSelectorDialog( wxWindow* parent, int id ) : + wxDialogWithHelpers( parent, id, _("BIOS Selector"), false ) { wxBoxSizer& bleh( *new wxBoxSizer( wxVERTICAL ) ); diff --git a/pcsx2/gui/Dialogs/ConfigurationDialog.h b/pcsx2/gui/Dialogs/ConfigurationDialog.h index 83c7ba097a..0c8830f5e8 100644 --- a/pcsx2/gui/Dialogs/ConfigurationDialog.h +++ b/pcsx2/gui/Dialogs/ConfigurationDialog.h @@ -33,7 +33,7 @@ namespace Dialogs public: virtual ~ConfigurationDialog(); - ConfigurationDialog(wxWindow* parent, int id=wxID_ANY); + ConfigurationDialog(wxWindow* parent=NULL, int id=DialogId_CoreSettings); protected: template< typename T > @@ -60,7 +60,7 @@ namespace Dialogs public: virtual ~BiosSelectorDialog() {} - BiosSelectorDialog(wxWindow* parent ); + BiosSelectorDialog( wxWindow* parent=NULL, int id=DialogId_BiosSelector ); protected: void OnOk_Click( wxCommandEvent& evt ); diff --git a/pcsx2/gui/Dialogs/FirstTimeWizard.cpp b/pcsx2/gui/Dialogs/FirstTimeWizard.cpp index 6fdd047a5d..7e355fe2b9 100644 --- a/pcsx2/gui/Dialogs/FirstTimeWizard.cpp +++ b/pcsx2/gui/Dialogs/FirstTimeWizard.cpp @@ -76,10 +76,7 @@ FirstTimeWizard::UsermodePage::UsermodePage( wxWizard* parent ) : void FirstTimeWizard::UsermodePage::OnUsermodeChanged( wxCommandEvent& evt ) { - m_panel_UserSel.Apply( *g_Conf ); // this assigns the current user mode to g_ApplyState.UseAdminMode - if( g_ApplyState.UseAdminMode == UseAdminMode ) return; - - UseAdminMode = g_ApplyState.UseAdminMode; + m_panel_UserSel.Apply(); g_Conf->Folders.ApplyDefaults(); m_dirpick_settings.Reset(); } @@ -161,7 +158,7 @@ void FirstTimeWizard::OnPageChanging( wxWizardEvent& evt ) if( page == 0 ) { - if( wxFile::Exists( g_Conf->FullPathToConfig() ) ) + if( wxFile::Exists( GetSettingsFilename() ) ) { // Asks the user if they want to import or overwrite the existing settings. diff --git a/pcsx2/gui/Dialogs/LogOptionsDialog.cpp b/pcsx2/gui/Dialogs/LogOptionsDialog.cpp index 6f71f556ca..c4267a40a8 100644 --- a/pcsx2/gui/Dialogs/LogOptionsDialog.cpp +++ b/pcsx2/gui/Dialogs/LogOptionsDialog.cpp @@ -112,8 +112,8 @@ LogOptionsDialog::iopLogOptionsPanel::iopLogOptionsPanel( wxWindow* parent ) : ////////////////////////////////////////////////////////////////////////////////////////// // -LogOptionsDialog::LogOptionsDialog(wxWindow* parent, int id, const wxPoint& pos, const wxSize& size): - wxDialogWithHelpers( parent, id, _("Logging"), true, pos, size ) +LogOptionsDialog::LogOptionsDialog(wxWindow* parent, int id): + wxDialogWithHelpers( parent, id, _("Logging"), true ) { eeLogOptionsPanel& eeBox = *new eeLogOptionsPanel( this ); iopLogOptionsPanel& iopSizer = *new iopLogOptionsPanel( this ); diff --git a/pcsx2/gui/Dialogs/LogOptionsDialog.h b/pcsx2/gui/Dialogs/LogOptionsDialog.h index 749bbad13b..85df195937 100644 --- a/pcsx2/gui/Dialogs/LogOptionsDialog.h +++ b/pcsx2/gui/Dialogs/LogOptionsDialog.h @@ -14,9 +14,8 @@ */ #pragma once - -#include -#include + +#include "App.h" #include "wxHelpers.h" #include "CheckedStaticBox.h" @@ -26,7 +25,7 @@ namespace Dialogs { class LogOptionsDialog: public wxDialogWithHelpers { public: - LogOptionsDialog( wxWindow* parent, int id, const wxPoint& pos=wxDefaultPosition, const wxSize& size=wxDefaultSize ); + LogOptionsDialog( wxWindow* parent=NULL, int id=DialogId_LogOptions ); protected: diff --git a/pcsx2/gui/Dialogs/ModalPopups.h b/pcsx2/gui/Dialogs/ModalPopups.h index 4c8f2823fe..b741ee84d9 100644 --- a/pcsx2/gui/Dialogs/ModalPopups.h +++ b/pcsx2/gui/Dialogs/ModalPopups.h @@ -15,6 +15,7 @@ #pragma once +#include "App.h" #include "Panels/ConfigurationPanels.h" #include @@ -68,7 +69,7 @@ namespace Dialogs class AboutBoxDialog: public wxDialog { public: - AboutBoxDialog( wxWindow* parent, int id ); + AboutBoxDialog( wxWindow* parent=NULL, int id=DialogId_About ); protected: wxStaticBitmap m_bitmap_logo; diff --git a/pcsx2/gui/HostGui.cpp b/pcsx2/gui/HostGui.cpp index 416ed95325..c85398e6bd 100644 --- a/pcsx2/gui/HostGui.cpp +++ b/pcsx2/gui/HostGui.cpp @@ -33,7 +33,7 @@ namespace HostGui // Sets the status bar message without mirroring the output to the console. void SetStatusMsg( const wxString& text ) { - wxGetApp().GetMainWindow()->SetStatusText( text ); + wxGetApp().GetMainFrame().SetStatusText( text ); } void Notice( const wxString& text ) @@ -63,10 +63,7 @@ void Pcsx2App::OnEmuKeyDown( wxKeyEvent& evt ) switch( evt.GetKeyCode() ) { case WXK_ESCAPE: - if( g_Conf->CloseGSonEsc ) - StateRecovery::MakeGsOnly(); - - SysEndExecution(); + m_CoreThread->Suspend(); break; case WXK_F1: @@ -82,7 +79,7 @@ void Pcsx2App::OnEmuKeyDown( wxKeyEvent& evt ) Console::Notice( " > Selected savestate slot %d", StatesC); if( GSchangeSaveState != NULL ) - GSchangeSaveState(StatesC, SaveState::GetFilename(StatesC).mb_str()); + GSchangeSaveState(StatesC, SaveStateBase::GetFilename(StatesC).mb_str()); break; case WXK_F3: @@ -100,15 +97,10 @@ void Pcsx2App::OnEmuKeyDown( wxKeyEvent& evt ) break; case WXK_F9: //gsdx "on the fly" renderer switching - - SysSuspend(); - StateRecovery::MakeGsOnly(); - - GetPluginManager().Close( PluginId_GS ); + m_CoreThread->Suspend(); + m_CorePlugins->Close( PluginId_GS ); renderswitch = !renderswitch; - - StateRecovery::Recover(); - SysResume(); + m_CoreThread->Resume(); break; #ifdef PCSX2_DEVBUILD diff --git a/pcsx2/gui/MainFrame.cpp b/pcsx2/gui/MainFrame.cpp index c91b2910d0..99d38ecd0a 100644 --- a/pcsx2/gui/MainFrame.cpp +++ b/pcsx2/gui/MainFrame.cpp @@ -126,10 +126,12 @@ void MainEmuFrame::PopulatePadMenu() // void MainEmuFrame::OnCloseWindow(wxCloseEvent& evt) { - // Note Closure Veting would be handled here (user prompt confirmation + // Note Closure Vetoing would be handled here (user prompt confirmation // of closing the app) - wxGetApp().PrepForExit(); + if( !wxGetApp().PrepForExit() ) + evt.Veto( true ); + evt.Skip(); } diff --git a/pcsx2/gui/MainMenuClicks.cpp b/pcsx2/gui/MainMenuClicks.cpp index 75f0c884fb..260c3e92c0 100644 --- a/pcsx2/gui/MainMenuClicks.cpp +++ b/pcsx2/gui/MainMenuClicks.cpp @@ -82,13 +82,13 @@ bool MainEmuFrame::_DoSelectIsoBrowser() void MainEmuFrame::Menu_BootCdvd_Click( wxCommandEvent &event ) { - SysSuspend(); + wxGetApp().SysSuspend(); if( !wxFileExists( g_Conf->CurrentIso ) ) { if( !_DoSelectIsoBrowser() ) { - SysResume(); + wxGetApp().SysResume(); return; } } @@ -100,66 +100,37 @@ void MainEmuFrame::Menu_BootCdvd_Click( wxCommandEvent &event ) if( !result ) { - SysResume(); + wxGetApp().SysResume(); return; } } - SysEndExecution(); - g_Conf->EmuOptions.SkipBiosSplash = GetMenuBar()->IsChecked( MenuId_SkipBiosToggle ); wxGetApp().SaveSettings(); - InitPlugins(); - - CDVDsys_SetFile( CDVDsrc_Iso, g_Conf->CurrentIso ); - SysExecute( new AppEmuThread(), g_Conf->CdvdSource ); + wxGetApp().SysExecute( g_Conf->CdvdSource ); } void MainEmuFrame::Menu_IsoBrowse_Click( wxCommandEvent &event ) { - SysSuspend(); + wxGetApp().SysSuspend(); _DoSelectIsoBrowser(); - SysResume(); + wxGetApp().SysResume(); } void MainEmuFrame::Menu_RunIso_Click( wxCommandEvent &event ) { - SysSuspend(); + wxGetApp().SysSuspend(); if( !_DoSelectIsoBrowser() ) { - SysResume(); + wxGetApp().SysResume(); return; } - SysEndExecution(); - - InitPlugins(); - SysExecute( new AppEmuThread(), CDVDsrc_Iso ); + wxGetApp().SysExecute( CDVDsrc_Iso ); } -/*void MainEmuFrame::Menu_RunWithoutDisc_Click(wxCommandEvent &event) -{ - if( EmulationInProgress() ) - { - SysSuspend(); - - // [TODO] : Add one of 'dems checkboxes that read like "[x] don't show this stupid shit again, kthx." - bool result = Msgbox::OkCancel( pxE( ".Popup:ConfirmEmuReset", L"This will reset the emulator and your current emulation session will be lost. Are you sure?") ); - - if( !result ) - { - SysResume(); - return; - } - } - - SysEndExecution(); - InitPlugins(); - SysExecute( new AppEmuThread(), CDVDsrc_NoDisc ); -}*/ - void MainEmuFrame::Menu_IsoRecent_Click(wxCommandEvent &event) { //Console::Status( "%d", event.GetId() - g_RecentIsoList->GetBaseId() ); @@ -194,34 +165,32 @@ void MainEmuFrame::Menu_SaveStateOther_Click(wxCommandEvent &event) void MainEmuFrame::Menu_Exit_Click(wxCommandEvent &event) { - SysReset(); Close(); } void MainEmuFrame::Menu_EmuClose_Click(wxCommandEvent &event) { - SysEndExecution(); + //wxGetApp()-> GetMenuBar()->Check( MenuId_Emu_Pause, false ); } void MainEmuFrame::Menu_EmuPause_Click(wxCommandEvent &event) { if( event.IsChecked() ) - SysSuspend(); + wxGetApp().SysSuspend(); else - SysResume(); + wxGetApp().SysResume(); } void MainEmuFrame::Menu_EmuReset_Click(wxCommandEvent &event) { bool wasRunning = EmulationInProgress(); - SysReset(); + wxGetApp().SysReset(); GetMenuBar()->Check( MenuId_Emu_Pause, false ); if( !wasRunning ) return; - InitPlugins(); - SysExecute( new AppEmuThread() ); + wxGetApp().SysExecute(); } void MainEmuFrame::Menu_ConfigPlugin_Click(wxCommandEvent &event) @@ -229,8 +198,11 @@ void MainEmuFrame::Menu_ConfigPlugin_Click(wxCommandEvent &event) typedef void (CALLBACK* PluginConfigureFnptr)(); const PluginsEnum_t pid = (PluginsEnum_t)( event.GetId() - MenuId_Config_GS ); - LoadPlugins(); - ScopedWindowDisable disabler( this ); + LoadPluginsImmediate(); + if( g_plugins == NULL ) return; + + wxWindowDisabler disabler; + //ScopedWindowDisable disabler( this ); g_plugins->Configure( pid ); } diff --git a/pcsx2/gui/MemoryCardFile.cpp b/pcsx2/gui/MemoryCardFile.cpp index c334de445f..00cccc61d0 100644 --- a/pcsx2/gui/MemoryCardFile.cpp +++ b/pcsx2/gui/MemoryCardFile.cpp @@ -32,10 +32,6 @@ struct Component_FileMcd; #include -#ifdef _WIN32 - extern void NTFS_CompressFile( const wxString& file, bool compressMode ); -#endif - static const int MCD_SIZE = 1024 * 8 * 16; static const int MC2_SIZE = 1024 * 528 * 16; @@ -106,9 +102,7 @@ FileMemoryCard::FileMemoryCard() // [TODO] : Add memcard size detection and report it to the console log. // (8MB, 256Mb, whatever) - #ifdef _WIN32 NTFS_CompressFile( str, g_Conf->McdEnableNTFS ); - #endif if( !m_file[port][slot].Open( str.c_str(), wxFile::read_write ) ) { diff --git a/pcsx2/gui/Panels/BiosSelectorPanel.cpp b/pcsx2/gui/Panels/BiosSelectorPanel.cpp index 53d22fa61a..09962e50df 100644 --- a/pcsx2/gui/Panels/BiosSelectorPanel.cpp +++ b/pcsx2/gui/Panels/BiosSelectorPanel.cpp @@ -70,7 +70,6 @@ Panels::BiosSelectorPanel::BiosSelectorPanel( wxWindow& parent, int idealWidth ) _("BIOS Search Path:"), // static box label _("Select folder with PS2 BIOS roms") // dir picker popup label ) ) -, m_BiosList( NULL ) { m_ComboBox.SetFont( wxFont( m_ComboBox.GetFont().GetPointSize()+1, wxFONTFAMILY_MODERN, wxNORMAL, wxNORMAL, false, L"Lucida Console" ) ); m_ComboBox.SetMinSize( wxSize( wxDefaultCoord, std::max( m_ComboBox.GetMinSize().GetHeight(), 96 ) ) ); @@ -88,7 +87,6 @@ Panels::BiosSelectorPanel::BiosSelectorPanel( wxWindow& parent, int idealWidth ) Panels::BiosSelectorPanel::~BiosSelectorPanel() { - safe_delete( m_BiosList ); } bool Panels::BiosSelectorPanel::ValidateEnumerationStatus() @@ -102,11 +100,10 @@ bool Panels::BiosSelectorPanel::ValidateEnumerationStatus() if( m_FolderPicker.GetPath().Exists() ) wxDir::GetAllFiles( m_FolderPicker.GetPath().ToString(), bioslist.get(), L"*.bin", wxDIR_FILES ); - if( (m_BiosList == NULL) || (*bioslist != *m_BiosList) ) + if( !m_BiosList || (*bioslist != *m_BiosList) ) validated = false; - delete m_BiosList; - m_BiosList = bioslist.release(); + m_BiosList.swap( bioslist ); return validated; } @@ -116,7 +113,7 @@ void Panels::BiosSelectorPanel::ReloadSettings() m_FolderPicker.Reset(); } -void Panels::BiosSelectorPanel::Apply( AppConfig& conf ) +void Panels::BiosSelectorPanel::Apply() { int sel = m_ComboBox.GetSelection(); if( sel == wxNOT_FOUND ) @@ -133,11 +130,13 @@ void Panels::BiosSelectorPanel::Apply( AppConfig& conf ) ); } - conf.BaseFilenames.Bios = (*m_BiosList)[(int)m_ComboBox.GetClientData(sel)]; + g_Conf->BaseFilenames.Bios = (*m_BiosList)[(int)m_ComboBox.GetClientData(sel)]; } void Panels::BiosSelectorPanel::DoRefresh() { + if( !m_BiosList ) return; + wxFileName right( g_Conf->FullpathToBios() ); right.MakeAbsolute(); diff --git a/pcsx2/gui/Panels/ConfigurationPanels.h b/pcsx2/gui/Panels/ConfigurationPanels.h index f923f72c78..eb8ec32f40 100644 --- a/pcsx2/gui/Panels/ConfigurationPanels.h +++ b/pcsx2/gui/Panels/ConfigurationPanels.h @@ -93,10 +93,6 @@ namespace Panels // TODO : Rename me to CurOwnerBook, or rename the one above to ParentPage. wxBookCtrlBase* ParentBook; - // Crappy hack to handle the UseAdminMode option, which can't be part of AppConfig - // because AppConfig depends on this value to initialize itself. - bool UseAdminMode; - StaticApplyState() : PanelList() , CurOwnerPage( wxID_NONE ) @@ -168,7 +164,7 @@ namespace Panels // configuration structure (which is typically a copy of g_Conf). If validation // of form contents fails, the function should throw Exception::CannotApplySettings. // If no exceptions are thrown, then the operation is assumed a success. :) - virtual void Apply( AppConfig& conf )=0; + virtual void Apply()=0; }; ////////////////////////////////////////////////////////////////////////////////////////// @@ -183,7 +179,7 @@ namespace Panels virtual ~UsermodeSelectionPanel() { } UsermodeSelectionPanel( wxWindow& parent, int idealWidth=wxDefaultCoord, bool isFirstTime = true ); - void Apply( AppConfig& conf ); + void Apply(); }; ////////////////////////////////////////////////////////////////////////////////////////// @@ -198,7 +194,7 @@ namespace Panels virtual ~LanguageSelectionPanel() { } LanguageSelectionPanel( wxWindow& parent, int idealWidth=wxDefaultCoord ); - void Apply( AppConfig& conf ); + void Apply(); }; ////////////////////////////////////////////////////////////////////////////////////////// @@ -216,7 +212,7 @@ namespace Panels public: CpuPanel( wxWindow& parent, int idealWidth ); - void Apply( AppConfig& conf ); + void Apply(); }; ////////////////////////////////////////////////////////////////////////////////////////// @@ -227,7 +223,7 @@ namespace Panels public: VideoPanel( wxWindow& parent, int idealWidth ); - void Apply( AppConfig& conf ); + void Apply(); }; ////////////////////////////////////////////////////////////////////////////////////////// @@ -248,7 +244,7 @@ namespace Panels public: SpeedHacksPanel( wxWindow& parent, int idealWidth ); - void Apply( AppConfig& conf ); + void Apply(); protected: const wxChar* GetEEcycleSliderMsg( int val ); @@ -267,7 +263,7 @@ namespace Panels public: GameFixesPanel( wxWindow& parent, int idealWidth ); - void Apply( AppConfig& conf ); + void Apply(); }; ////////////////////////////////////////////////////////////////////////////////////////// @@ -286,7 +282,7 @@ namespace Panels DirPickerPanel( wxWindow* parent, FoldersEnum_t folderid, const wxString& label, const wxString& dialogLabel ); virtual ~DirPickerPanel() { } - void Apply( AppConfig& conf ); + void Apply(); void Reset(); wxDirName GetPath() const { return wxDirName( m_pickerCtrl->GetPath() ); } @@ -354,9 +350,9 @@ namespace Panels class BiosSelectorPanel : public BaseSelectorPanel { protected: + wxScopedPtr m_BiosList; wxListBox& m_ComboBox; DirPickerPanel& m_FolderPicker; - wxArrayString* m_BiosList; public: BiosSelectorPanel( wxWindow& parent, int idealWidth ); @@ -364,7 +360,7 @@ namespace Panels void ReloadSettings(); protected: - virtual void Apply( AppConfig& conf ); + virtual void Apply(); virtual void DoRefresh(); virtual bool ValidateEnumerationStatus(); }; @@ -399,16 +395,18 @@ namespace Panels class EnumThread : public Threading::PersistentThread { public: - EnumeratedPluginInfo* Results; // array of plugin results. + SafeList Results; // array of plugin results. protected: PluginSelectorPanel& m_master; - volatile bool m_cancel; public: - virtual ~EnumThread(); + virtual ~EnumThread() throw() + { + PersistentThread::Cancel(); + } + EnumThread( PluginSelectorPanel& master ); - void Cancel(); void DoNextPlugin( int evtidx ); protected: @@ -447,25 +445,27 @@ namespace Panels }; // ------------------------------------------------------------------------ - // PluginSelectorPanel Members + // PluginSelectorPanel Members // ------------------------------------------------------------------------ protected: - wxArrayString* m_FileList; // list of potential plugin files StatusPanel& m_StatusPanel; ComboBoxPanel& m_ComponentBoxes; - EnumThread* m_EnumeratorThread; + + wxScopedPtr m_FileList; // list of potential plugin files + wxScopedPtr m_EnumeratorThread; public: virtual ~PluginSelectorPanel(); PluginSelectorPanel( wxWindow& parent, int idealWidth ); void CancelRefresh(); // used from destructor, stays non-virtual - void Apply( AppConfig& conf ); + void Apply(); void ReloadSettings(); protected: void OnConfigure_Clicked( wxCommandEvent& evt ); + void OnShowStatusBar( wxCommandEvent& evt ); virtual void OnProgress( wxCommandEvent& evt ); virtual void OnEnumComplete( wxCommandEvent& evt ); diff --git a/pcsx2/gui/Panels/CpuPanel.cpp b/pcsx2/gui/Panels/CpuPanel.cpp index 804c470ac9..86a2380517 100644 --- a/pcsx2/gui/Panels/CpuPanel.cpp +++ b/pcsx2/gui/Panels/CpuPanel.cpp @@ -78,9 +78,9 @@ Panels::CpuPanel::CpuPanel( wxWindow& parent, int idealWidth ) : m_Option_sVU1->SetValue( recOps.EnableVU1 ); } -void Panels::CpuPanel::Apply( AppConfig& conf ) +void Panels::CpuPanel::Apply() { - Pcsx2Config::RecompilerOptions& recOps( conf.EmuOptions.Cpu.Recompiler ); + Pcsx2Config::RecompilerOptions& recOps( g_Conf->EmuOptions.Cpu.Recompiler ); recOps.EnableEE = m_Option_RecEE->GetValue(); recOps.EnableIOP = m_Option_RecIOP->GetValue(); recOps.EnableVU0 = m_Option_mVU0->GetValue() || m_Option_sVU0->GetValue(); diff --git a/pcsx2/gui/Panels/DirPickerPanel.cpp b/pcsx2/gui/Panels/DirPickerPanel.cpp index 21deb6bf37..8471c93459 100644 --- a/pcsx2/gui/Panels/DirPickerPanel.cpp +++ b/pcsx2/gui/Panels/DirPickerPanel.cpp @@ -93,8 +93,7 @@ Panels::DirPickerPanel::DirPickerPanel( wxWindow* parent, FoldersEnum_t folderid m_checkCtrl = &AddCheckBox( s_lower, _("Use default setting") ); m_checkCtrl->SetToolTip( pxE( ".Tooltip:DirPicker:UseDefault", - L"When checked this folder will automatically reflect the default associated with PCSX2's current usermode setting. " - L"" ) + L"When checked this folder will automatically reflect the default associated with PCSX2's current usermode setting. " ) ); #ifndef __WXGTK__ @@ -134,7 +133,7 @@ void Panels::DirPickerPanel::Reset() m_pickerCtrl->SetPath( GetNormalizedConfigFolder( m_FolderId ) ); } -void Panels::DirPickerPanel::Apply( AppConfig& conf ) +void Panels::DirPickerPanel::Apply() { - conf.Folders.Set( m_FolderId, m_pickerCtrl->GetPath(), m_checkCtrl->GetValue() ); + g_Conf->Folders.Set( m_FolderId, m_pickerCtrl->GetPath(), m_checkCtrl->GetValue() ); } diff --git a/pcsx2/gui/Panels/GameFixesPanel.cpp b/pcsx2/gui/Panels/GameFixesPanel.cpp index 4129fcd30a..9964eb5512 100644 --- a/pcsx2/gui/Panels/GameFixesPanel.cpp +++ b/pcsx2/gui/Panels/GameFixesPanel.cpp @@ -85,9 +85,9 @@ Panels::GameFixesPanel::GameFixesPanel( wxWindow& parent, int idealWidth ) : } // I could still probably get rid of the for loop, but I think this is clearer. -void Panels::GameFixesPanel::Apply( AppConfig& conf ) +void Panels::GameFixesPanel::Apply() { - Pcsx2Config::GamefixOptions& opts( conf.EmuOptions.Gamefixes ); + Pcsx2Config::GamefixOptions& opts( g_Conf->EmuOptions.Gamefixes ); for (int i = 0; i < NUM_OF_GAME_FIXES; i++) { if (m_checkbox[i]->GetValue()) diff --git a/pcsx2/gui/Panels/MiscPanelStuff.cpp b/pcsx2/gui/Panels/MiscPanelStuff.cpp index 4ba040e80a..d3e06149dd 100644 --- a/pcsx2/gui/Panels/MiscPanelStuff.cpp +++ b/pcsx2/gui/Panels/MiscPanelStuff.cpp @@ -60,31 +60,40 @@ void Panels::StaticApplyState::StartWizard() bool Panels::StaticApplyState::ApplyPage( int pageid, bool saveOnSuccess ) { bool retval = true; + + // Save these settings so we can restore them if the Apply fails. + + bool oldAdminMode = UseAdminMode; + wxDirName oldSettingsFolder = SettingsFolder; + bool oldUseDefSet = UseDefaultSettingsFolder; + + AppConfig confcopy( *g_Conf ); + try { - AppConfig confcopy( *g_Conf ); - - g_ApplyState.UseAdminMode = UseAdminMode; - PanelApplyList_t::iterator yay = PanelList.begin(); while( yay != PanelList.end() ) { //DbgCon::Status( L"Writing settings for: " + (*yay)->GetLabel() ); if( (pageid < 0) || (*yay)->IsOnPage( pageid ) ) - (*yay)->Apply( confcopy ); + (*yay)->Apply(); yay++; } // If an exception is thrown above, this code below won't get run. // (conveniently skipping any option application! :D) - UseAdminMode = g_ApplyState.UseAdminMode; - wxGetApp().ApplySettings( confcopy ); + wxGetApp().ApplySettings( &confcopy ); if( saveOnSuccess ) wxGetApp().SaveSettings(); } catch( Exception::CannotApplySettings& ex ) { + UseAdminMode = oldAdminMode; + SettingsFolder = oldSettingsFolder; + UseDefaultSettingsFolder = oldUseDefSet; + *g_Conf = confcopy; + wxMessageBox( ex.FormatDisplayMessage(), _("Cannot apply settings...") ); if( ex.GetPanel() != NULL ) @@ -92,6 +101,15 @@ bool Panels::StaticApplyState::ApplyPage( int pageid, bool saveOnSuccess ) retval = false; } + catch( ... ) + { + UseAdminMode = oldAdminMode; + SettingsFolder = oldSettingsFolder; + UseDefaultSettingsFolder = oldUseDefSet; + *g_Conf = confcopy; + + throw; + } return retval; } @@ -133,12 +151,12 @@ Panels::UsermodeSelectionPanel::UsermodeSelectionPanel( wxWindow& parent, int id SetSizer( &s_boxer ); } -void Panels::UsermodeSelectionPanel::Apply( AppConfig& conf ) +void Panels::UsermodeSelectionPanel::Apply() { if( !m_radio_cwd->GetValue() && !m_radio_user->GetValue() ) throw Exception::CannotApplySettings( this, wxLt( "You must select one of the available user modes before proceeding." ) ); - g_ApplyState.UseAdminMode = m_radio_cwd->GetValue(); + UseAdminMode = m_radio_cwd->GetValue(); } // ----------------------------------------------------------------------- @@ -173,20 +191,20 @@ Panels::LanguageSelectionPanel::LanguageSelectionPanel( wxWindow& parent, int id SetSizer( &s_lang ); } -void Panels::LanguageSelectionPanel::Apply( AppConfig& conf ) +void Panels::LanguageSelectionPanel::Apply() { // The combo box's order is sorted and may not match our m_langs order, so // we have to do a string comparison to find a match: wxString sel( m_picker->GetString( m_picker->GetSelection() ) ); - conf.LanguageId = wxLANGUAGE_DEFAULT; // use this if no matches found + g_Conf->LanguageId = wxLANGUAGE_DEFAULT; // use this if no matches found int size = m_langs.size(); for( int i=0; iLanguageId = m_langs[i].wxLangId; break; } } diff --git a/pcsx2/gui/Panels/PluginSelectorPanel.cpp b/pcsx2/gui/Panels/PluginSelectorPanel.cpp index 28875379da..53a733c4ae 100644 --- a/pcsx2/gui/Panels/PluginSelectorPanel.cpp +++ b/pcsx2/gui/Panels/PluginSelectorPanel.cpp @@ -34,12 +34,14 @@ using namespace wxHelpers; using namespace Threading; BEGIN_DECLARE_EVENT_TYPES() - DECLARE_EVENT_TYPE(wxEVT_EnumeratedNext, -1) - DECLARE_EVENT_TYPE(wxEVT_EnumerationFinished, -1) + DECLARE_EVENT_TYPE(pxEVT_EnumeratedNext, -1) + DECLARE_EVENT_TYPE(pxEVT_EnumerationFinished, -1) + DECLARE_EVENT_TYPE(pxEVT_ShowStatusBar, -1) END_DECLARE_EVENT_TYPES() -DEFINE_EVENT_TYPE(wxEVT_EnumeratedNext) -DEFINE_EVENT_TYPE(wxEVT_EnumerationFinished); +DEFINE_EVENT_TYPE(pxEVT_EnumeratedNext) +DEFINE_EVENT_TYPE(pxEVT_EnumerationFinished); +DEFINE_EVENT_TYPE(pxEVT_ShowStatusBar); typedef s32 (CALLBACK* PluginTestFnptr)(); typedef void (CALLBACK* PluginConfigureFnptr)(); @@ -55,8 +57,10 @@ namespace Exception }; } -////////////////////////////////////////////////////////////////////////////////////////// -// +// -------------------------------------------------------------------------------------- +// PluginEnumerator class +// -------------------------------------------------------------------------------------- + class PluginEnumerator { protected: @@ -74,15 +78,15 @@ public: // Constructor! // // Possible Exceptions: - // BadStream - thrown if the provided file is simply not a loadable DLL. - // NotPcsxPlugin - thrown if the DLL is not a PCSX2 plugin, or if it's of an unsupported version. + // BadStream - thrown if the provided file is simply not a loadable DLL. + // NotEnumerablePlugin - thrown if the DLL is not a PCSX2 plugin, or if it's of an unsupported version. // PluginEnumerator( const wxString& plugpath ) : m_plugpath( plugpath ) , m_plugin() { if( !m_plugin.Load( m_plugpath ) ) - throw Exception::BadStream( m_plugpath, "File is not a valid dynamic library" ); + throw Exception::BadStream( m_plugpath, "File is not a valid dynamic library." ); wxDoNotLogInThisScope please; m_GetLibType = (_PS2EgetLibType)m_plugin.GetSymbol( L"PS2EgetLibType" ); @@ -90,9 +94,8 @@ public: m_GetLibVersion2 = (_PS2EgetLibVersion2)m_plugin.GetSymbol( L"PS2EgetLibVersion2" ); if( m_GetLibType == NULL || m_GetLibName == NULL || m_GetLibVersion2 == NULL ) - { throw Exception::NotEnumerablePlugin( m_plugpath ); - } + m_type = m_GetLibType(); } @@ -134,7 +137,10 @@ public: static const wxString failed_separator( L"-------- Unsupported Plugins --------" ); -// ------------------------------------------------------------------------ +// -------------------------------------------------------------------------------------- +// PluginSelectorPanel implementations +// -------------------------------------------------------------------------------------- + Panels::PluginSelectorPanel::StatusPanel::StatusPanel( wxWindow* parent ) : wxPanelWithHelpers( parent ) , m_gauge( *new wxGauge( this, wxID_ANY, 10 ) ) @@ -221,10 +227,8 @@ void Panels::PluginSelectorPanel::ComboBoxPanel::Reset() // ------------------------------------------------------------------------ Panels::PluginSelectorPanel::PluginSelectorPanel( wxWindow& parent, int idealWidth ) : BaseSelectorPanel( parent, idealWidth ) -, m_FileList( NULL ) , m_StatusPanel( *new StatusPanel( this ) ) , m_ComponentBoxes( *new ComboBoxPanel( this ) ) -, m_EnumeratorThread( NULL ) { // note: the status panel is a floating window, so that it can be positioned in the // center of the dialog after it's been fitted to the contents. @@ -242,8 +246,9 @@ Panels::PluginSelectorPanel::PluginSelectorPanel( wxWindow& parent, int idealWid SetSizer( &s_main ); - Connect( wxEVT_EnumeratedNext, wxCommandEventHandler( PluginSelectorPanel::OnProgress ) ); - Connect( wxEVT_EnumerationFinished, wxCommandEventHandler( PluginSelectorPanel::OnEnumComplete ) ); + Connect( pxEVT_EnumeratedNext, wxCommandEventHandler( PluginSelectorPanel::OnProgress ) ); + Connect( pxEVT_EnumerationFinished, wxCommandEventHandler( PluginSelectorPanel::OnEnumComplete ) ); + Connect( pxEVT_ShowStatusBar, wxCommandEventHandler( PluginSelectorPanel::OnShowStatusBar ) ); Connect( ButtonId_Configure, wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( PluginSelectorPanel::OnConfigure_Clicked ) ); } @@ -257,10 +262,19 @@ void Panels::PluginSelectorPanel::ReloadSettings() m_ComponentBoxes.GetDirPicker().Reset(); } -void Panels::PluginSelectorPanel::Apply( AppConfig& conf ) +static wxString GetApplyFailedMsg() +{ + return pxE( ".Popup Error:PluginSelector:ApplyFailed", + L"All plugins must have valid selections for PCSX2 to run. If you are unable to make\n" + L"a valid selection due to missing plugins or an incomplete install of PCSX2, then\n" + L"press cancel to close the Configuration panel." + ); +} + +void Panels::PluginSelectorPanel::Apply() { // user never entered plugins panel? Skip application since combo boxes are invalid/uninitialized. - if( m_FileList == NULL ) return; + if( !m_FileList ) return; for( int i=0; iBaseFilenames.Plugins[tbl_PluginInfo[i].id] = GetFilename((int)m_ComponentBoxes.Get(i).GetClientData(sel)); } + + // ---------------------------------------------------------------------------- + // Make sure folders are up to date, and try to load/reload plugins if needed... + + g_Conf->Folders.ApplyDefaults(); + + // Need to unload the current emulation state if the user changed plugins, because + // the whole plugin system needs to be re-loaded. + + const PluginInfo* pi = tbl_PluginInfo-1; + while( ++pi, pi->shortname != NULL ) + { + if( g_Conf->FullpathTo( pi->id ) != g_Conf->FullpathTo( pi->id ) ) + break; + } + + if( pi->shortname != NULL ) + { + if( wxGetApp().m_CoreThread ) + { + // [TODO] : Post notice that this shuts down existing emulation, and may not safely recover. + } + + wxGetApp().SysReset(); + } + + if( !wxGetApp().m_CorePlugins ) + { + try + { + LoadPluginsImmediate(); + } + catch( Exception::PluginError& ex ) + { + // Rethrow PluginLoadErrors as a failure to Apply... + + wxString plugname( tbl_PluginInfo[ex.PluginId].GetShortname() ); + + throw Exception::CannotApplySettings( this, + // English Log + ex.FormatDiagnosticMessage(), + + // Translated + wxsFormat( L"The selected %s plugin failed to load.", plugname.c_str() ) + L"\n\n" + GetApplyFailedMsg() + ); + } + } } void Panels::PluginSelectorPanel::CancelRefresh() { - safe_delete( m_EnumeratorThread ); - safe_delete( m_FileList ); } void Panels::PluginSelectorPanel::DoRefresh() { m_ComponentBoxes.Reset(); - if( m_FileList == NULL ) + if( !m_FileList ) { wxCommandEvent evt; OnEnumComplete( evt ); @@ -304,21 +358,17 @@ void Panels::PluginSelectorPanel::DoRefresh() } // Disable all controls until enumeration is complete. - // Show status bar for plugin enumeration. - - // fixme: the status bar doesn't always fit itself to the window correctly because - // sometimes this gets called before the parent window has been fully created. I'm - // not quite sure how to fix (a delayed event/message might work tho implementing it - // may not be trivial) -- air + // Show status bar for plugin enumeration. Use a pending event so that + // the window's size can get initialized properly before trying to custom- + // fit the status panel to it. m_ComponentBoxes.Hide(); - m_StatusPanel.SetSize( m_ComponentBoxes.GetSize().GetWidth() - 8, wxDefaultCoord ); - m_StatusPanel.CentreOnParent(); - m_StatusPanel.Show(); + wxCommandEvent evt( pxEVT_ShowStatusBar ); + GetEventHandler()->AddPendingEvent( evt ); // Use a thread to load plugins. - safe_delete( m_EnumeratorThread ); - m_EnumeratorThread = new EnumThread( *this ); + m_EnumeratorThread.reset( NULL ); + m_EnumeratorThread.reset( new EnumThread( *this ) ); if( DisableThreading ) m_EnumeratorThread->DoNextPlugin( 0 ); @@ -328,6 +378,8 @@ void Panels::PluginSelectorPanel::DoRefresh() bool Panels::PluginSelectorPanel::ValidateEnumerationStatus() { + m_EnumeratorThread.reset(); // make sure the thread is STOPPED, just in case... + bool validated = true; // re-enumerate plugins, and if anything changed then we need to wipe @@ -339,17 +391,16 @@ bool Panels::PluginSelectorPanel::ValidateEnumerationStatus() int pluggers = EnumeratePluginsInFolder( m_ComponentBoxes.GetPluginsPath(), pluginlist.get() ); - if( (m_FileList == NULL) || (*pluginlist != *m_FileList) ) + if( !m_FileList || (*pluginlist != *m_FileList) ) validated = false; if( pluggers == 0 ) { - safe_delete( m_FileList ); + m_FileList.reset(); return validated; } - delete m_FileList; - m_FileList = pluginlist.release(); + m_FileList.swap( pluginlist ); m_StatusPanel.SetGaugeLength( pluggers ); @@ -370,9 +421,16 @@ void Panels::PluginSelectorPanel::OnConfigure_Clicked( wxCommandEvent& evt ) } } +void Panels::PluginSelectorPanel::OnShowStatusBar( wxCommandEvent& evt ) +{ + m_StatusPanel.SetSize( m_ComponentBoxes.GetSize().GetWidth() - 8, wxDefaultCoord ); + m_StatusPanel.CentreOnParent(); + m_StatusPanel.Show(); +} + void Panels::PluginSelectorPanel::OnEnumComplete( wxCommandEvent& evt ) { - safe_delete( m_EnumeratorThread ); + m_EnumeratorThread.reset(); // fixme: Default plugins should be picked based on the timestamp of the DLL or something? // (for now we just force it to selection zero if nothing's selected) @@ -395,7 +453,7 @@ void Panels::PluginSelectorPanel::OnEnumComplete( wxCommandEvent& evt ) void Panels::PluginSelectorPanel::OnProgress( wxCommandEvent& evt ) { - if( m_FileList == NULL ) return; + if( !m_FileList ) return; const size_t evtidx = evt.GetExtraLong(); @@ -404,7 +462,7 @@ void Panels::PluginSelectorPanel::OnProgress( wxCommandEvent& evt ) const int nextidx = evtidx+1; if( nextidx == m_FileList->Count() ) { - wxCommandEvent done( wxEVT_EnumerationFinished ); + wxCommandEvent done( pxEVT_EnumerationFinished ); GetEventHandler()->AddPendingEvent( done ); } else @@ -447,28 +505,16 @@ void Panels::PluginSelectorPanel::OnProgress( wxCommandEvent& evt ) } -////////////////////////////////////////////////////////////////////////////////////////// -// EnumThread Method Implementations +// -------------------------------------------------------------------------------------- +// EnumThread method implementations +// -------------------------------------------------------------------------------------- Panels::PluginSelectorPanel::EnumThread::EnumThread( PluginSelectorPanel& master ) : PersistentThread() -, Results( new EnumeratedPluginInfo[master.FileCount()] ) +, Results( master.FileCount(), L"PluginSelectorResults" ) , m_master( master ) -, m_cancel( false ) { -} - -Panels::PluginSelectorPanel::EnumThread::~EnumThread() -{ - EnumThread::Cancel(); - safe_delete_array( Results ); -} - -void Panels::PluginSelectorPanel::EnumThread::Cancel() -{ - m_cancel = true; - Sleep( 2 ); - PersistentThread::Cancel(); + Results.MatchLengthToAllocatedSize(); } void Panels::PluginSelectorPanel::EnumThread::DoNextPlugin( int curidx ) @@ -499,7 +545,7 @@ void Panels::PluginSelectorPanel::EnumThread::DoNextPlugin( int curidx ) Console::Status( ex.FormatDiagnosticMessage() ); } - wxCommandEvent yay( wxEVT_EnumeratedNext ); + wxCommandEvent yay( pxEVT_EnumeratedNext ); yay.SetExtraLong( curidx ); m_master.GetEventHandler()->AddPendingEvent( yay ); } @@ -509,15 +555,15 @@ sptr Panels::PluginSelectorPanel::EnumThread::ExecuteTask() DevCon::Status( "Plugin Enumeration Thread started..." ); wxGetApp().Ping(); // gives the gui thread some time to refresh + Sleep( 3 ); for( int curidx=0; curidx < m_master.FileCount(); ++curidx ) { - if( m_cancel ) return 0; DoNextPlugin( curidx ); pthread_testcancel(); } - wxCommandEvent done( wxEVT_EnumerationFinished ); + wxCommandEvent done( pxEVT_EnumerationFinished ); m_master.GetEventHandler()->AddPendingEvent( done ); DevCon::Status( "Plugin Enumeration Thread complete!" ); diff --git a/pcsx2/gui/Panels/SpeedhacksPanel.cpp b/pcsx2/gui/Panels/SpeedhacksPanel.cpp index af2d2b3a04..22e8052651 100644 --- a/pcsx2/gui/Panels/SpeedhacksPanel.cpp +++ b/pcsx2/gui/Panels/SpeedhacksPanel.cpp @@ -219,9 +219,9 @@ Panels::SpeedHacksPanel::SpeedHacksPanel( wxWindow& parent, int idealWidth ) : Connect( m_slider_vustealer->GetId(), wxEVT_SCROLL_CHANGED, wxScrollEventHandler( SpeedHacksPanel::VUCycleRate_Scroll ) ); } -void Panels::SpeedHacksPanel::Apply( AppConfig& conf ) +void Panels::SpeedHacksPanel::Apply() { - Pcsx2Config::SpeedhackOptions& opts( conf.EmuOptions.Speedhacks ); + Pcsx2Config::SpeedhackOptions& opts( g_Conf->EmuOptions.Speedhacks ); opts.EECycleRate = m_slider_eecycle->GetValue()-1; opts.VUCycleSteal = m_slider_vustealer->GetValue(); diff --git a/pcsx2/gui/Panels/VideoPanel.cpp b/pcsx2/gui/Panels/VideoPanel.cpp index 82b9caab8e..72b1dc7cbd 100644 --- a/pcsx2/gui/Panels/VideoPanel.cpp +++ b/pcsx2/gui/Panels/VideoPanel.cpp @@ -25,6 +25,6 @@ Panels::VideoPanel::VideoPanel( wxWindow& parent, int idealWidth ) : // MTGS Forced Synchronization } -void Panels::VideoPanel::Apply( AppConfig& conf ) +void Panels::VideoPanel::Apply() { } diff --git a/pcsx2/gui/Plugins.cpp b/pcsx2/gui/Plugins.cpp index e86e56795a..65da250122 100644 --- a/pcsx2/gui/Plugins.cpp +++ b/pcsx2/gui/Plugins.cpp @@ -14,7 +14,7 @@ */ #include "PrecompiledHeader.h" -#include "Utilities/ScopedPtr.h" +#include "App.h" #include #include @@ -44,9 +44,73 @@ int EnumeratePluginsInFolder( const wxDirName& searchpath, wxArrayString* dest ) wxDir::GetAllFiles( searchpath.ToString(), realdest, wxsFormat( pattern, wxDynamicLibrary::GetDllExt()), wxDIR_FILES ) : 0; } -void LoadPlugins() +using namespace Threading; + +void Pcsx2App::OnReloadPlugins( wxCommandEvent& evt ) { - if( g_plugins != NULL ) return; + ReloadPlugins(); +} + +void Pcsx2App::ReloadPlugins() +{ + class LoadPluginsTask : public Threading::BaseTaskThread + { + public: + PluginManager* Result; + Exception::PluginError* Ex_PluginError; + Exception::RuntimeError* Ex_RuntimeError; + + protected: + const wxString (&m_folders)[PluginId_Count]; + + public: + LoadPluginsTask( const wxString (&folders)[PluginId_Count] ) : + Result( NULL ) + , Ex_PluginError( NULL ) + , Ex_RuntimeError( NULL ) + , m_folders( folders ) + { + } + + virtual ~LoadPluginsTask() throw() + { + BaseTaskThread::Cancel(); + } + + protected: + void Task() + { + wxGetApp().Ping(); + Sleep(3); + + try + { + //throw Exception::PluginError( PluginId_PAD, "This one is for testing the error handler!" ); + Result = PluginManager_Create( m_folders ); + } + catch( Exception::PluginError& ex ) + { + Result = NULL; + Ex_PluginError = new Exception::PluginError( ex ); + } + catch( Exception::RuntimeError& innerEx ) + { + // Runtime errors are typically recoverable, so handle them here + // and prep them for re-throw on the main thread. + Result = NULL; + Ex_RuntimeError = new Exception::RuntimeError( + L"A runtime error occurred on the LoadPlugins thread.\n" + innerEx.FormatDiagnosticMessage(), + innerEx.FormatDisplayMessage() + ); + } + + // anything else leave unhandled so that the debugger catches it! + } + }; + + m_CoreThread.reset(); + m_CorePlugins.reset(); + wxString passins[PluginId_Count]; const PluginInfo* pi = tbl_PluginInfo-1; @@ -57,29 +121,45 @@ void LoadPlugins() if( passins[pi->id].IsEmpty() || !wxFileExists( passins[pi->id] ) ) passins[pi->id] = g_Conf->FullpathTo( pi->id ); } + + LoadPluginsTask task( passins ); + task.Start(); + task.PostTask(); + task.WaitForResult(); + + if( task.Result == NULL ) + { + if( task.Ex_PluginError != NULL ) + throw *task.Ex_PluginError; + if( task.Ex_RuntimeError != NULL ) + throw *task.Ex_RuntimeError; // Re-Throws generic threaded errors + } - g_plugins = PluginManager_Create( passins ); + m_CorePlugins.reset( task.Result ); } -void InitPlugins() +// Posts a message to the App to reload plugins. Plugins are loaded via a background thread +// which is started on a pending event, so don't expect them to be ready "right now." +void LoadPluginsPassive() { - if( g_plugins == NULL ) - LoadPlugins(); - - GetPluginManager().Init(); + wxCommandEvent evt( pxEVT_ReloadPlugins ); + wxGetApp().AddPendingEvent( evt ); } -// All plugins except the CDVD are opened here. The CDVD must be opened manually by -// the GUI, depending on the user's menu/config in use. -// -// Exceptions: -// PluginFailure - if any plugin fails to initialize or open. -// ThreadCreationError - If the MTGS thread fails to be created. -// -void OpenPlugins() +// Blocks until plugins have been successfully loaded, or throws an exception if +// the user cancels the loading procedure after error. +void LoadPluginsImmediate() { - if( g_plugins == NULL ) - InitPlugins(); + wxASSERT( wxThread::IsMain() ); - GetPluginManager().Open(); + static int _reentrant = 0; + EntryGuard guard( _reentrant ); + + wxASSERT( !guard.IsReentrant() ); + wxGetApp().ReloadPlugins(); +} + +void UnloadPlugins() +{ + wxGetApp().m_CorePlugins.reset(); } diff --git a/pcsx2/gui/Saveslots.cpp b/pcsx2/gui/Saveslots.cpp index 07a3ad5303..9c46dbaaec 100644 --- a/pcsx2/gui/Saveslots.cpp +++ b/pcsx2/gui/Saveslots.cpp @@ -33,7 +33,7 @@ bool States_isSlotUsed(int num) if (ElfCRC == 0) return false; else - return wxFileExists( SaveState::GetFilename( num ) ); + return wxFileExists( SaveStateBase::GetFilename( num ) ); } ////////////////////////////////////////////////////////////////////////////////////////// @@ -62,12 +62,12 @@ void States_Load( const wxString& file ) StateRecovery::Recover(); } - SysExecute( new AppEmuThread() ); + wxGetApp().SysExecute(); } void States_Load(int num) { - wxString file( SaveState::GetFilename( num ) ); + wxString file( SaveStateBase::GetFilename( num ) ); if( !wxFileExists( file ) ) { @@ -120,79 +120,5 @@ void States_Save( const wxString& file ) void States_Save(int num) { Console::Status( "Saving savestate to slot %d...", num ); - States_Save( SaveState::GetFilename( num ) ); -} - -////////////////////////////////////////////////////////////////////////////////////////// -// -void vSyncDebugStuff( uint frame ) -{ -#ifdef OLD_TESTBUILD_STUFF - if( g_TestRun.enabled && g_TestRun.frame > 0 ) { - if( frame > g_TestRun.frame ) { - // take a snapshot - if( g_TestRun.pimagename != NULL && GSmakeSnapshot2 != NULL ) { - if( g_TestRun.snapdone ) { - g_TestRun.curimage++; - g_TestRun.snapdone = 0; - g_TestRun.frame += 20; - if( g_TestRun.curimage >= g_TestRun.numimages ) { - // exit - g_EmuThread->Cancel(); - } - } - else { - // query for the image - GSmakeSnapshot2(g_TestRun.pimagename, &g_TestRun.snapdone, g_TestRun.jpgcapture); - } - } - else { - // exit - g_EmuThread->Cancel(); - } - } - } - - GSVSYNC(); - - if( g_SaveGSStream == 1 ) { - freezeData fP; - - g_SaveGSStream = 2; - g_fGSSave->gsFreeze(); - - if (GSfreeze(FREEZE_SIZE, &fP) == -1) { - safe_delete( g_fGSSave ); - g_SaveGSStream = 0; - } - else { - fP.data = (s8*)malloc(fP.size); - if (fP.data == NULL) { - safe_delete( g_fGSSave ); - g_SaveGSStream = 0; - } - else { - if (GSfreeze(FREEZE_SAVE, &fP) == -1) { - safe_delete( g_fGSSave ); - g_SaveGSStream = 0; - } - else { - g_fGSSave->Freeze( fP.size ); - if (fP.size) { - g_fGSSave->FreezeMem( fP.data, fP.size ); - free(fP.data); - } - } - } - } - } - else if( g_SaveGSStream == 2 ) { - - if( --g_nLeftGSFrames <= 0 ) { - safe_delete( g_fGSSave ); - g_SaveGSStream = 0; - Console::WriteLn("Done saving GS stream"); - } - } -#endif + States_Save( SaveStateBase::GetFilename( num ) ); } diff --git a/pcsx2/gui/wxHelpers.cpp b/pcsx2/gui/wxHelpers.cpp index fa410f1327..9fac1d5ded 100644 --- a/pcsx2/gui/wxHelpers.cpp +++ b/pcsx2/gui/wxHelpers.cpp @@ -14,6 +14,7 @@ */ #include "PrecompiledHeader.h" +#include "Utilities/HashMap.h" #include "wxHelpers.h" #include @@ -276,12 +277,21 @@ void pxTextWrapperBase::Wrap( const wxWindow *win, const wxString& text, int wid } } -////////////////////////////////////////////////////////////////////////////////////////// -// +HashTools::HashMap< wxWindowID, int > m_DialogIdents( 0, wxID_ANY ); + +bool pxDialogExists( wxWindowID id ) +{ + int dest = 0; + m_DialogIdents.TryGetValue( id, dest ); + return (dest > 0); +} + wxDialogWithHelpers::wxDialogWithHelpers( wxWindow* parent, int id, const wxString& title, bool hasContextHelp, const wxPoint& pos, const wxSize& size ) : wxDialog( parent, id, title, pos, size , wxDEFAULT_DIALOG_STYLE), //, (wxCAPTION | wxMAXIMIZE | wxCLOSE_BOX | wxRESIZE_BORDER) ), // flags for resizable dialogs, currently unused. m_hasContextHelp( hasContextHelp ) { + ++m_DialogIdents[GetId()]; + if( hasContextHelp ) wxHelpProvider::Set( new wxSimpleHelpProvider() ); @@ -291,6 +301,12 @@ wxDialogWithHelpers::wxDialogWithHelpers( wxWindow* parent, int id, const wxStr // any good. } +wxDialogWithHelpers::~wxDialogWithHelpers() +{ + --m_DialogIdents[GetId()]; + wxASSERT( m_DialogIdents[GetId()] >= 0 ); +} + // ------------------------------------------------------------------------ // Creates a new checkbox and adds it to the specified sizer/parent combo, with optional tooltip. // Uses the default spacer setting for adding checkboxes, and the tooltip (if specified) is applied diff --git a/pcsx2/gui/wxHelpers.h b/pcsx2/gui/wxHelpers.h index befdc76524..a9f92f579c 100644 --- a/pcsx2/gui/wxHelpers.h +++ b/pcsx2/gui/wxHelpers.h @@ -55,6 +55,7 @@ protected: public: wxDialogWithHelpers(wxWindow* parent, int id, const wxString& title, bool hasContextHelp, const wxPoint& pos=wxDefaultPosition, const wxSize& size=wxDefaultSize ); + virtual ~wxDialogWithHelpers() throw(); wxCheckBox& AddCheckBox( wxSizer& sizer, const wxString& label, const wxString& subtext=wxEmptyString, const wxString& tooltip=wxEmptyString ); wxStaticText& AddStaticText(wxSizer& sizer, const wxString& label, int alignFlags=wxALIGN_CENTRE, int size=wxDefaultCoord ); @@ -88,3 +89,6 @@ protected: m_StartNewRadioGroup = true; } }; + +extern bool pxDialogExists( wxWindowID id ); + diff --git a/pcsx2/pcsxAbout.bmp b/pcsx2/pcsxAbout.bmp deleted file mode 100644 index 3d6accb181042ac003e4d40d7bab3ec29686deef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 251192 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 @@ -119,7 +120,6 @@ /> @@ -245,7 +245,7 @@ /> @@ -308,6 +308,7 @@ /> @@ -371,7 +372,6 @@ />