diff --git a/Source/Android/jni/MainAndroid.cpp b/Source/Android/jni/MainAndroid.cpp index cd96fe46a2..6b30bc5409 100644 --- a/Source/Android/jni/MainAndroid.cpp +++ b/Source/Android/jni/MainAndroid.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -65,13 +66,23 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) void Host_NotifyMapLoaded() {} void Host_RefreshDSPDebuggerWindow() {} +// The Core only supports using a single Host thread. +// If multiple threads want to call host functions then they need to queue +// sequentially for access. +static std::mutex s_host_identity_lock; Common::Event updateMainFrameEvent; static bool s_have_wm_user_stop = false; void Host_Message(int Id) { - if (Id == WM_USER_STOP) + if (Id == WM_USER_JOB_DISPATCH) + { + updateMainFrameEvent.Set(); + } + else if (Id == WM_USER_STOP) { s_have_wm_user_stop = true; + if (Core::IsRunning()) + Core::QueueHostJob(&Core::Stop); } } @@ -393,15 +404,18 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_SurfaceDestr JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_UnPauseEmulation(JNIEnv *env, jobject obj) { + std::lock_guard guard(s_host_identity_lock); Core::SetState(Core::CORE_RUN); } JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_PauseEmulation(JNIEnv *env, jobject obj) { + std::lock_guard guard(s_host_identity_lock); Core::SetState(Core::CORE_PAUSE); } JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_StopEmulation(JNIEnv *env, jobject obj) { + std::lock_guard guard(s_host_identity_lock); Core::SaveScreenShot("thumb"); Renderer::s_screenshotCompleted.WaitFor(std::chrono::seconds(2)); Core::Stop(); @@ -490,6 +504,7 @@ JNIEXPORT jboolean JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_Supports JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_SaveScreenShot(JNIEnv *env, jobject obj) { + std::lock_guard guard(s_host_identity_lock); Core::SaveScreenShot(); } @@ -534,11 +549,13 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_SetFilename( JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_SaveState(JNIEnv *env, jobject obj, jint slot) { + std::lock_guard guard(s_host_identity_lock); State::Save(slot); } JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_LoadState(JNIEnv *env, jobject obj, jint slot) { + std::lock_guard guard(s_host_identity_lock); State::Load(slot); } @@ -565,6 +582,7 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_CreateUserFo JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_SetUserDirectory(JNIEnv *env, jobject obj, jstring jDirectory) { + std::lock_guard guard(s_host_identity_lock); std::string directory = GetJString(env, jDirectory); g_set_userpath = directory; UICommon::SetUserDirectory(directory); @@ -577,6 +595,7 @@ JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_GetUserDi JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_SetProfiling(JNIEnv *env, jobject obj, jboolean enable) { + std::lock_guard guard(s_host_identity_lock); Core::SetState(Core::CORE_PAUSE); JitInterface::ClearCache(); Profiler::g_ProfileBlocks = enable; @@ -585,6 +604,7 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_SetProfiling JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_WriteProfileResults(JNIEnv *env, jobject obj) { + std::lock_guard guard(s_host_identity_lock); std::string filename = File::GetUserPath(D_DUMP_IDX) + "Debug/profiler.txt"; File::CreateFullPath(filename); JitInterface::WriteProfileResults(filename); @@ -643,6 +663,7 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_SurfaceDestr } JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_RefreshWiimotes(JNIEnv *env, jobject obj) { + std::lock_guard guard(s_host_identity_lock); WiimoteReal::Refresh(); } @@ -656,6 +677,7 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_Run(JNIEnv * RegisterMsgAlertHandler(&MsgAlert); + std::unique_lock guard(s_host_identity_lock); UICommon::SetUserDirectory(g_set_userpath); UICommon::Init(); @@ -676,12 +698,16 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_Run(JNIEnv * } while (Core::IsRunning()) { + guard.unlock(); updateMainFrameEvent.Wait(); + guard.lock(); + Core::HostDispatchJobs(); } } Core::Shutdown(); UICommon::Shutdown(); + guard.unlock(); if (surf) { diff --git a/Source/Core/Common/Common.h b/Source/Core/Common/Common.h index 36a1bc13bf..7f8e6ce639 100644 --- a/Source/Core/Common/Common.h +++ b/Source/Core/Common/Common.h @@ -83,6 +83,7 @@ enum HOST_COMM WM_USER_STOP = 10, WM_USER_CREATE, WM_USER_SETCURSOR, + WM_USER_JOB_DISPATCH, }; // Used for notification on emulation state diff --git a/Source/Core/Core/Core.cpp b/Source/Core/Core/Core.cpp index b5fbf14663..ab888de322 100644 --- a/Source/Core/Core/Core.cpp +++ b/Source/Core/Core/Core.cpp @@ -5,6 +5,9 @@ #include #include #include +#include +#include +#include #ifdef _WIN32 #include @@ -102,6 +105,7 @@ void EmuThread(); static bool s_is_stopping = false; static bool s_hardware_initialized = false; static bool s_is_started = false; +static std::atomic s_is_booting{ false }; static void* s_window_handle = nullptr; static std::string s_state_filename; static std::thread s_emu_thread; @@ -112,6 +116,14 @@ static bool s_request_refresh_info = false; static int s_pause_and_lock_depth = 0; static bool s_is_throttler_temp_disabled = false; +struct HostJob +{ + std::function job; + bool run_after_stop; +}; +static std::mutex s_host_jobs_lock; +static std::queue s_host_jobs_queue; + #ifdef ThreadLocalStorage static ThreadLocalStorage bool tls_is_cpu_thread = false; #else @@ -225,6 +237,9 @@ bool Init() s_emu_thread.join(); } + // Drain any left over jobs + HostDispatchJobs(); + Core::UpdateWantDeterminism(/*initial*/ true); INFO_LOG(OSREPORT, "Starting core = %s mode", @@ -260,6 +275,9 @@ void Stop() // - Hammertime! s_is_stopping = true; + // Dump left over jobs + HostDispatchJobs(); + Fifo::EmulatorState(false); INFO_LOG(CONSOLE, "Stop [Main Thread]\t\t---- Shutting down ----"); @@ -310,6 +328,16 @@ void UndeclareAsCPUThread() #endif } +// For the CPU Thread only. +static void CPUSetInitialExecutionState() +{ + QueueHostJob([] + { + SetState(SConfig::GetInstance().bBootToPause ? CORE_PAUSE : CORE_RUN); + Host_UpdateMainFrame(); + }); +} + // Create the CPU thread, which is a CPU + Video thread in Single Core mode. static void CpuThread() { @@ -331,10 +359,20 @@ static void CpuThread() EMM::InstallExceptionHandler(); // Let's run under memory watch if (!s_state_filename.empty()) - State::LoadAs(s_state_filename); + { + // Needs to PauseAndLock the Core + // NOTE: EmuThread should have left us in CPU_STEPPING so nothing will happen + // until after the job is serviced. + QueueHostJob([] + { + // Recheck in case Movie cleared it since. + if (!s_state_filename.empty()) + State::LoadAs(s_state_filename); + }); + } s_is_started = true; - + CPUSetInitialExecutionState(); #ifdef USE_GDBSTUB #ifndef _WIN32 @@ -393,7 +431,10 @@ static void FifoPlayerThread() { PowerPC::InjectExternalCPUCore(cpu_core.get()); s_is_started = true; + + CPUSetInitialExecutionState(); CPU::Run(); + s_is_started = false; PowerPC::InjectExternalCPUCore(nullptr); } @@ -427,6 +468,7 @@ static void FifoPlayerThread() void EmuThread() { const SConfig& core_parameter = SConfig::GetInstance(); + s_is_booting.store(true); Common::SetCurrentThreadName("Emuthread - Starting"); @@ -445,6 +487,7 @@ void EmuThread() if (!g_video_backend->Initialize(s_window_handle)) { + s_is_booting.store(false); PanicAlert("Failed to initialize video backend!"); Host_Message(WM_USER_STOP); return; @@ -459,6 +502,7 @@ void EmuThread() if (!DSP::GetDSPEmulator()->Initialize(core_parameter.bWii, core_parameter.bDSPThread)) { + s_is_booting.store(false); HW::Shutdown(); g_video_backend->Shutdown(); PanicAlert("Failed to initialize DSP emulation!"); @@ -499,12 +543,10 @@ void EmuThread() // The hardware is initialized. s_hardware_initialized = true; + s_is_booting.store(false); - // Boot to pause or not - // NOTE: This violates the Host Thread requirement for SetState but we should - // not race the Host because the UI should have the buttons disabled until - // Host_UpdateMainFrame enables them. - Core::SetState(core_parameter.bBootToPause ? Core::CORE_PAUSE : Core::CORE_RUN); + // Set execution state to known values (CPU/FIFO/Audio Paused) + CPU::Break(); // Load GCM/DOL/ELF whatever ... we boot with the interpreter core PowerPC::SetMode(PowerPC::MODE_INTERPRETER); @@ -640,6 +682,10 @@ void EmuThread() void SetState(EState state) { + // State cannot be controlled until the CPU Thread is operational + if (!IsRunningAndStarted()) + return; + switch (state) { case CORE_PAUSE: @@ -904,6 +950,9 @@ void Shutdown() // on MSDN. if (s_emu_thread.joinable()) s_emu_thread.join(); + + // Make sure there's nothing left over in case we're about to exit. + HostDispatchJobs(); } void SetOnStoppedCallback(StoppedCallbackFunc callback) @@ -937,4 +986,45 @@ void UpdateWantDeterminism(bool initial) } } +void QueueHostJob(std::function job, bool run_during_stop) +{ + if (!job) + return; + + bool send_message = false; + { + std::lock_guard guard(s_host_jobs_lock); + send_message = s_host_jobs_queue.empty(); + s_host_jobs_queue.emplace(HostJob{ std::move(job), run_during_stop }); + } + // If the the queue was empty then kick the Host to come and get this job. + if (send_message) + Host_Message(WM_USER_JOB_DISPATCH); +} + +void HostDispatchJobs() +{ + // WARNING: This should only run on the Host Thread. + // NOTE: This function is potentially re-entrant. If a job calls + // Core::Stop for instance then we'll enter this a second time. + std::unique_lock guard(s_host_jobs_lock); + while (!s_host_jobs_queue.empty()) + { + HostJob job = std::move(s_host_jobs_queue.front()); + s_host_jobs_queue.pop(); + + // NOTE: Memory ordering is important. The booting flag needs to be + // checked first because the state transition is: + // CORE_UNINITIALIZED: s_is_booting -> s_hardware_initialized + // We need to check variables in the same order as the state + // transition, otherwise we race and get transient failures. + if (!job.run_after_stop && !s_is_booting.load() && !IsRunning()) + continue; + + guard.unlock(); + job.job(); + guard.lock(); + } +} + } // Core diff --git a/Source/Core/Core/Core.h b/Source/Core/Core/Core.h index b5de60fd5e..f09f89bce2 100644 --- a/Source/Core/Core/Core.h +++ b/Source/Core/Core/Core.h @@ -11,6 +11,7 @@ #pragma once +#include #include #include @@ -91,4 +92,19 @@ void SetOnStoppedCallback(StoppedCallbackFunc callback); // Run on the Host thread when the factors change. [NOT THREADSAFE] void UpdateWantDeterminism(bool initial = false); +// Queue an arbitrary function to asynchronously run once on the Host thread later. +// Threadsafe. Can be called by any thread, including the Host itself. +// Jobs will be executed in RELATIVE order. If you queue 2 jobs from the same thread +// then they will be executed in the order they were queued; however, there is no +// global order guarantee across threads - jobs from other threads may execute in +// between. +// NOTE: Make sure the jobs check the global state instead of assuming everything is +// still the same as when the job was queued. +// NOTE: Jobs that are not set to run during stop will be discarded instead. +void QueueHostJob(std::function job, bool run_during_stop = false); + +// Should be called periodically by the Host to run pending jobs. +// WM_USER_JOB_DISPATCH will be sent when something is added to the queue. +void HostDispatchJobs(); + } // namespace diff --git a/Source/Core/Core/HW/DVDInterface.cpp b/Source/Core/Core/HW/DVDInterface.cpp index 2276150d27..c3626cd428 100644 --- a/Source/Core/Core/HW/DVDInterface.cpp +++ b/Source/Core/Core/HW/DVDInterface.cpp @@ -478,8 +478,8 @@ static void InsertDiscCallback(u64 userdata, s64 cyclesLate) void ChangeDisc(const std::string& newFileName) { - bool is_cpu = Core::IsCPUThread(); - bool was_unpaused = is_cpu ? false : Core::PauseAndLock(true); + // WARNING: Can only run on Host Thread + bool was_unpaused = Core::PauseAndLock(true); std::string* _FileName = new std::string(newFileName); CoreTiming::ScheduleEvent(0, s_eject_disc); CoreTiming::ScheduleEvent(500000000, s_insert_disc, (u64)_FileName); @@ -495,8 +495,7 @@ void ChangeDisc(const std::string& newFileName) } Movie::g_discChange = fileName.substr(sizeofpath); } - if (!is_cpu) - Core::PauseAndLock(false, was_unpaused); + Core::PauseAndLock(false, was_unpaused); } void SetLidOpen(bool open) diff --git a/Source/Core/Core/HW/DVDInterface.h b/Source/Core/Core/HW/DVDInterface.h index 8c26b3b7c8..96b065132e 100644 --- a/Source/Core/Core/HW/DVDInterface.h +++ b/Source/Core/Core/HW/DVDInterface.h @@ -102,7 +102,7 @@ bool VolumeIsValid(); // Disc detection and swapping void SetDiscInside(bool _DiscInside); bool IsDiscInside(); -void ChangeDisc(const std::string& fileName); +void ChangeDisc(const std::string& fileName); // [NOT THREADSAFE] Host only // DVD Access Functions bool ChangePartition(u64 offset); diff --git a/Source/Core/Core/Movie.cpp b/Source/Core/Core/Movie.cpp index 35bbae2762..176ef5b445 100644 --- a/Source/Core/Core/Movie.cpp +++ b/Source/Core/Core/Movie.cpp @@ -415,7 +415,7 @@ bool IsNetPlayRecording() return s_bNetPlay; } -// NOTE: Host / CPU Thread +// NOTE: Host Thread void ChangePads(bool instantly) { if (!Core::IsRunning()) @@ -824,7 +824,7 @@ void RecordWiimote(int wiimote, u8 *data, u8 size) s_totalBytes = s_currentByte; } -// NOTE: CPU / EmuThread / Host Thread +// NOTE: EmuThread / Host Thread void ReadHeader() { s_numPads = tmpHeader.numControllers; @@ -934,7 +934,7 @@ void DoState(PointerWrap &p) // other variables (such as s_totalBytes and g_totalFrames) are set in LoadInput } -// NOTE: Host / CPU Thread +// NOTE: Host Thread void LoadInput(const std::string& filename) { File::IOFile t_record; @@ -1152,7 +1152,7 @@ void PlayController(GCPadStatus* PadStatus, int controllerID) { // This implementation assumes the disc change will only happen once. Trying to change more than that will cause // it to load the last disc every time. As far as i know though, there are no 3+ disc games, so this should be fine. - Core::SetState(Core::CORE_PAUSE); + CPU::Break(); bool found = false; std::string path; for (size_t i = 0; i < SConfig::GetInstance().m_ISOFolder.size(); ++i) @@ -1166,8 +1166,16 @@ void PlayController(GCPadStatus* PadStatus, int controllerID) } if (found) { - DVDInterface::ChangeDisc(path + '/' + g_discChange); - Core::SetState(Core::CORE_RUN); + path += '/' + g_discChange; + + Core::QueueHostJob([=] + { + if (!Movie::IsPlayingInput()) + return; + + DVDInterface::ChangeDisc(path); + CPU::EnableStepping(false); + }); } else { @@ -1235,10 +1243,13 @@ void EndPlayInput(bool cont) } else if (s_playMode != MODE_NONE) { + // We can be called by EmuThread during boot (CPU_POWERDOWN) + bool was_running = Core::IsRunningAndStarted() && !CPU::IsStepping(); + if (was_running) + CPU::Break(); s_rerecords = 0; s_currentByte = 0; s_playMode = MODE_NONE; - Core::UpdateWantDeterminism(); Core::DisplayMessage("Movie End.", 2000); s_bRecordingFromSaveState = false; // we don't clear these things because otherwise we can't resume playback if we load a movie state later @@ -1246,8 +1257,12 @@ void EndPlayInput(bool cont) //delete tmpInput; //tmpInput = nullptr; - if (SConfig::GetInstance().m_PauseMovie) - Core::SetState(Core::CORE_PAUSE); + Core::QueueHostJob([=] + { + Core::UpdateWantDeterminism(); + if (was_running && !SConfig::GetInstance().m_PauseMovie) + CPU::EnableStepping(false); + }); } } @@ -1353,7 +1368,7 @@ void SetGraphicsConfig() g_Config.bUseRealXFB = tmpHeader.bUseRealXFB; } -// NOTE: CPU / EmuThread / Host Thread +// NOTE: EmuThread / Host Thread void GetSettings() { s_bSaveConfig = true; diff --git a/Source/Core/DolphinQt2/Host.cpp b/Source/Core/DolphinQt2/Host.cpp index 389e0508b8..db74d88b39 100644 --- a/Source/Core/DolphinQt2/Host.cpp +++ b/Source/Core/DolphinQt2/Host.cpp @@ -2,6 +2,8 @@ // Licensed under GPLv2+ // Refer to the license.txt file included. +#include +#include #include #include "Common/Common.h" @@ -57,7 +59,15 @@ void Host::SetRenderFullscreen(bool fullscreen) void Host_Message(int id) { if (id == WM_USER_STOP) + { emit Host::GetInstance()->RequestStop(); + } + else if (id == WM_USER_JOB_DISPATCH) + { + // Just poke the main thread to get it to wake up, job dispatch + // will happen automatically before it goes back to sleep again. + QAbstractEventDispatcher::instance(qApp->thread())->wakeUp(); + } } void Host_UpdateTitle(const std::string& title) diff --git a/Source/Core/DolphinQt2/Main.cpp b/Source/Core/DolphinQt2/Main.cpp index 639f68a4b4..12963caf59 100644 --- a/Source/Core/DolphinQt2/Main.cpp +++ b/Source/Core/DolphinQt2/Main.cpp @@ -2,6 +2,7 @@ // Licensed under GPLv2+ // Refer to the license.txt file included. +#include #include #include "Core/BootManager.h" @@ -20,6 +21,12 @@ int main(int argc, char* argv[]) UICommon::Init(); Resources::Init(); + // Whenever the event loop is about to go to sleep, dispatch the jobs + // queued in the Core first. + QObject::connect(QAbstractEventDispatcher::instance(), + &QAbstractEventDispatcher::aboutToBlock, + &app, &Core::HostDispatchJobs); + MainWindow win; win.show(); int retval = app.exec(); diff --git a/Source/Core/DolphinWX/Main.cpp b/Source/Core/DolphinWX/Main.cpp index e085a94bb2..5398bf244c 100644 --- a/Source/Core/DolphinWX/Main.cpp +++ b/Source/Core/DolphinWX/Main.cpp @@ -101,6 +101,7 @@ bool DolphinApp::OnInit() Bind(wxEVT_QUERY_END_SESSION, &DolphinApp::OnEndSession, this); Bind(wxEVT_END_SESSION, &DolphinApp::OnEndSession, this); + Bind(wxEVT_IDLE, &DolphinApp::OnIdle, this); // Register message box and translation handlers RegisterMsgAlertHandler(&wxMsgAlert); @@ -359,6 +360,12 @@ void DolphinApp::OnFatalException() WiimoteReal::Shutdown(); } +void DolphinApp::OnIdle(wxIdleEvent& ev) +{ + ev.Skip(); + Core::HostDispatchJobs(); +} + // ------------ // Talk to GUI @@ -395,6 +402,12 @@ CFrame* DolphinApp::GetCFrame() void Host_Message(int Id) { + if (Id == WM_USER_JOB_DISPATCH) + { + // Trigger a wxEVT_IDLE + wxWakeUpIdle(); + return; + } wxCommandEvent event(wxEVT_HOST_COMMAND, Id); main_frame->GetEventHandler()->AddPendingEvent(event); } diff --git a/Source/Core/DolphinWX/Main.h b/Source/Core/DolphinWX/Main.h index 9b3bf2fdd0..7e9e3f56ff 100644 --- a/Source/Core/DolphinWX/Main.h +++ b/Source/Core/DolphinWX/Main.h @@ -33,6 +33,7 @@ private: void OnEndSession(wxCloseEvent& event); void InitLanguageSupport(); void AfterInit(); + void OnIdle(wxIdleEvent&); bool m_batch_mode = false; bool m_confirm_stop = false; diff --git a/Source/Core/DolphinWX/MainNoGUI.cpp b/Source/Core/DolphinWX/MainNoGUI.cpp index bf2089a5c8..aaf207ade3 100644 --- a/Source/Core/DolphinWX/MainNoGUI.cpp +++ b/Source/Core/DolphinWX/MainNoGUI.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include "Common/CommonTypes.h" @@ -36,7 +37,14 @@ class Platform public: virtual void Init() {} virtual void SetTitle(const std::string &title) {} - virtual void MainLoop() { while(running) {} } + virtual void MainLoop() + { + while (running) + { + Core::HostDispatchJobs(); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + } virtual void Shutdown() {} virtual ~Platform() {} }; @@ -50,7 +58,10 @@ static Common::Event updateMainFrameEvent; void Host_Message(int Id) { if (Id == WM_USER_STOP) + { running = false; + updateMainFrameEvent.Set(); + } } static void* s_window_handle = nullptr; @@ -101,10 +112,13 @@ void Host_ConnectWiimote(int wm_idx, bool connect) { if (Core::IsRunning() && SConfig::GetInstance().bWii) { - bool was_unpaused = Core::PauseAndLock(true); - GetUsbPointer()->AccessWiiMote(wm_idx | 0x100)->Activate(connect); - Host_UpdateMainFrame(); - Core::PauseAndLock(false, was_unpaused); + Core::QueueHostJob([=] + { + bool was_unpaused = Core::PauseAndLock(true); + GetUsbPointer()->AccessWiiMote(wm_idx | 0x100)->Activate(connect); + Host_UpdateMainFrame(); + Core::PauseAndLock(false, was_unpaused); + }); } } @@ -270,6 +284,7 @@ class PlatformX11 : public Platform &borderDummy, &depthDummy); rendererIsFullscreen = false; } + Core::HostDispatchJobs(); usleep(100000); } } @@ -353,10 +368,14 @@ int main(int argc, char* argv[]) return 1; } - while (!Core::IsRunning()) + while (!Core::IsRunning() && running) + { + Core::HostDispatchJobs(); updateMainFrameEvent.Wait(); + } - platform->MainLoop(); + if (running) + platform->MainLoop(); Core::Stop(); Core::Shutdown();