#include #include #include #include "Common/System/NativeApp.h" #include "Common/System/System.h" #include "Common/System/Request.h" #include "Common/Data/Text/I18n.h" #include "Common/Input/InputState.h" #include "Common/Data/Encoding/Utf8.h" #include "Common/Log.h" #include "Common/StringUtils.h" #include "Common/GraphicsContext.h" #include "Common/Thread/ThreadUtil.h" #include "Windows/EmuThread.h" #include "Windows/W32Util/Misc.h" #include "Windows/MainWindow.h" #include "Core/Reporting.h" #include "Core/Core.h" #include "Core/System.h" #include "Core/Config.h" #include "Core/ConfigValues.h" #if PPSSPP_API(ANY_GL) #include "Windows/GPU/WindowsGLContext.h" #endif #include "Windows/GPU/WindowsVulkanContext.h" #include "Windows/GPU/D3D11Context.h" enum class EmuThreadState { DISABLED, START_REQUESTED, RUNNING, QUIT_REQUESTED, STOPPED, }; static std::thread emuThread; static std::atomic g_emuThreadState(EmuThreadState::DISABLED); static std::thread mainThread; static std::string g_error_message; static bool g_inLoop; class GraphicsContext; static GraphicsContext *g_graphicsContext; void MainThreadFunc(); void MainThread_Start() { mainThread = std::thread([]() { MainThreadFunc(); }); } void MainThread_Stop() { // Already stopped? UpdateUIState(UISTATE_EXIT); _dbg_assert_(mainThread.joinable()); mainThread.join(); } bool MainThread_Ready() { if (g_inLoop) { _dbg_assert_(mainThread.joinable()); } return g_inLoop; } static void EmuThreadFunc(GraphicsContext *graphicsContext) { SetCurrentThreadName("EmuThread"); // There's no real requirement that NativeInit happen on this thread. // We just call the update/render loop here. g_emuThreadState = EmuThreadState::RUNNING; NativeInitGraphics(graphicsContext); while (g_emuThreadState != EmuThreadState::QUIT_REQUESTED) { // We're here again, so the game quit. Restart Run() which controls the UI. // This way they can load a new game. if (!Core_IsActive()) { UpdateUIState(UISTATE_MENU); } Core_StateProcessed(); NativeFrame(graphicsContext); if (GetUIState() == UISTATE_EXIT) { g_emuThreadState = EmuThreadState::QUIT_REQUESTED; } } g_emuThreadState = EmuThreadState::STOPPED; NativeShutdownGraphics(); } static void EmuThreadStart(GraphicsContext *graphicsContext) { g_emuThreadState = EmuThreadState::START_REQUESTED; emuThread = std::thread(&EmuThreadFunc, graphicsContext); } static void EmuThreadStop() { const EmuThreadState state = g_emuThreadState; if (state != EmuThreadState::QUIT_REQUESTED && state != EmuThreadState::STOPPED) { g_emuThreadState = EmuThreadState::QUIT_REQUESTED; } } static void EmuThreadJoin() { emuThread.join(); INFO_LOG(Log::System, "EmuThreadJoin - joined"); } bool CreateGraphicsBackend(std::string *error_message, GraphicsContext **ctx) { WindowsGraphicsContext *graphicsContext = nullptr; switch (g_Config.iGPUBackend) { #if PPSSPP_API(ANY_GL) case (int)GPUBackend::OPENGL: graphicsContext = new WindowsGLContext(); break; #endif case (int)GPUBackend::DIRECT3D11: graphicsContext = new D3D11Context(); break; case (int)GPUBackend::VULKAN: graphicsContext = new WindowsVulkanContext(); break; default: return false; } if (graphicsContext->Init(MainWindow::GetHInstance(), MainWindow::GetHWND(), error_message)) { *ctx = graphicsContext; return true; } else { delete graphicsContext; *ctx = nullptr; return false; } } void MainThreadFunc() { const bool useEmuThread = g_Config.iGPUBackend == (int)GPUBackend::OPENGL; SetCurrentThreadName(useEmuThread ? "RenderThread" : "EmuThread"); System_SetWindowTitle(""); const bool performingRestart = NativeIsRestarting(); if (g_Config.sFailedGPUBackends.find("ALL") != std::string::npos) { Reporting::ReportMessage("Graphics init error: %s", "ALL"); auto err = GetI18NCategory(I18NCat::ERRORS); const char *defaultErrorAll = "PPSSPP failed to startup with any graphics backend. Try upgrading your graphics and other drivers."; std::string_view genericError = err->T("GenericAllStartupError", defaultErrorAll); std::wstring title = ConvertUTF8ToWString(err->T("GenericGraphicsError", "Graphics Error")); MessageBox(0, ConvertUTF8ToWString(genericError).c_str(), title.c_str(), MB_OK); // Let's continue (and probably crash) just so they have a way to keep trying. } System_Notify(SystemNotification::UI); std::string error_string; bool success = CreateGraphicsBackend(&error_string, &g_graphicsContext); if (success) { // Main thread is the render thread. success = g_graphicsContext->InitFromRenderThread(&error_string); } if (!success) { // Before anything: are we restarting right now? if (performingRestart) { // Okay, switching graphics didn't work out. Probably a driver bug - fallback to restart. // This happens on NVIDIA when switching OpenGL -> Vulkan. g_Config.Save("switch_graphics_failed"); W32Util::ExitAndRestart(); } auto err = GetI18NCategory(I18NCat::ERRORS); Reporting::ReportMessage("Graphics init error: %s", error_string.c_str()); const char *defaultErrorVulkan = "Failed initializing graphics. Try upgrading your graphics drivers.\n\nWould you like to try switching to OpenGL?\n\nError message:"; const char *defaultErrorOpenGL = "Failed initializing graphics. Try upgrading your graphics drivers.\n\nWould you like to try switching to DirectX 9?\n\nError message:"; const char *defaultErrorDirect3D9 = "Failed initializing graphics. Try upgrading your graphics drivers and directx 9 runtime.\n\nWould you like to try switching to OpenGL?\n\nError message:"; std::string_view genericError; GPUBackend nextBackend = GPUBackend::VULKAN; switch (g_Config.iGPUBackend) { case (int)GPUBackend::VULKAN: nextBackend = GPUBackend::OPENGL; genericError = err->T("GenericVulkanError", defaultErrorVulkan); break; case (int)GPUBackend::OPENGL: default: nextBackend = GPUBackend::DIRECT3D11; genericError = err->T("GenericOpenGLError", defaultErrorOpenGL); break; } std::string full_error = StringFromFormat("%.*s\n\n%s", (int)genericError.size(), genericError.data(), error_string.c_str()); std::wstring title = ConvertUTF8ToWString(err->T("GenericGraphicsError", "Graphics Error")); bool yes = IDYES == MessageBox(0, ConvertUTF8ToWString(full_error).c_str(), title.c_str(), MB_ICONERROR | MB_YESNO); ERROR_LOG(Log::Boot, "%s", full_error.c_str()); if (yes) { // Change the config to the alternative and restart. g_Config.iGPUBackend = (int)nextBackend; // Clear this to ensure we try their selection. g_Config.sFailedGPUBackends.clear(); g_Config.Save("save_graphics_fallback"); W32Util::ExitAndRestart(); } // No safe way out without graphics. ExitProcess(1); } GraphicsContext *graphicsContext = g_graphicsContext; if (!useEmuThread) { NativeInitGraphics(graphicsContext); NativeResized(); } DEBUG_LOG(Log::Boot, "Done."); g_inLoop = true; if (useEmuThread) { EmuThreadStart(graphicsContext); } graphicsContext->ThreadStart(); if (useEmuThread) { // Again, this thread becomes the render thread. while (true) { if (equals_any(g_emuThreadState, EmuThreadState::QUIT_REQUESTED, EmuThreadState::STOPPED)) { break; } graphicsContext->ThreadFrame(true); if (GetUIState() == UISTATE_EXIT) { break; } } } else { // Same contents as EmuThread. while (GetUIState() != UISTATE_EXIT) { // && GetUIState() != UISTATE_EXCEPTION // We're here again, so the game quit. Restart Run() which controls the UI. // This way they can load a new game. if (!(Core_IsActive() || Core_IsStepping())) UpdateUIState(UISTATE_MENU); Core_StateProcessed(); NativeFrame(graphicsContext); } } Core_Stop(); if (!useEmuThread) { // Process the shutdown. Without this, non-GL delays 800ms on shutdown. Core_StateProcessed(); NativeFrame(graphicsContext); } g_inLoop = false; if (useEmuThread) { EmuThreadStop(); graphicsContext->ThreadFrameUntilCondition([] { // Need to keep eating frames to allow the EmuThread to exit correctly. return g_emuThreadState == EmuThreadState::STOPPED; }); EmuThreadJoin(); } if (!useEmuThread) { NativeShutdownGraphics(); } g_graphicsContext->ThreadEnd(); g_graphicsContext->ShutdownFromRenderThread(); g_graphicsContext->Shutdown(); delete g_graphicsContext; g_graphicsContext = nullptr; PostMessage(MainWindow::GetHWND(), MainWindow::WM_USER_UPDATE_UI, 0, 0); }