From 5763a0d73188b5a6c0a500132c74f44e339e17e0 Mon Sep 17 00:00:00 2001 From: Acts1631 Date: Thu, 23 Jul 2026 14:59:29 -0400 Subject: [PATCH 01/12] Validate ZIM image sizes before loading Check the ZIM header, dimensions, mip count, allocation arithmetic, and uncompressed payload size before allocating or copying image data. This prevents crafted texture files from overflowing the image buffer. --- Common/Data/Format/ZIMLoad.cpp | 61 ++++++++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/Common/Data/Format/ZIMLoad.cpp b/Common/Data/Format/ZIMLoad.cpp index daace4992d..0113a46d71 100644 --- a/Common/Data/Format/ZIMLoad.cpp +++ b/Common/Data/Format/ZIMLoad.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include "zlib.h" @@ -50,6 +51,10 @@ int ezuncompress(unsigned char* pDest, long* pnDestLen, const unsigned char* pSr } int LoadZIMPtr(const uint8_t *zim, size_t datasize, int *width, int *height, int *flags, uint8_t **image) { + if (datasize < 16) { + ERROR_LOG(Log::IO, "ZIM file is too small"); + return 0; + } if (zim[0] != 'Z' || zim[1] != 'I' || zim[2] != 'M' || zim[3] != 'G') { ERROR_LOG(Log::IO, "Not a ZIM file"); return 0; @@ -58,29 +63,53 @@ int LoadZIMPtr(const uint8_t *zim, size_t datasize, int *width, int *height, int memcpy(height, zim + 8, 4); memcpy(flags, zim + 12, 4); + if (*width <= 0 || *height <= 0) { + ERROR_LOG(Log::IO, "Invalid ZIM dimensions %i x %i", *width, *height); + return 0; + } + int num_levels = 1; - int image_data_size[ZIM_MAX_MIP_LEVELS]{}; + size_t image_data_size[ZIM_MAX_MIP_LEVELS]{}; if (*flags & ZIM_HAS_MIPS) { num_levels = log2i(*width < *height ? *width : *height) + 1; } - int total_data_size = 0; + if (num_levels > ZIM_MAX_MIP_LEVELS) { + ERROR_LOG(Log::IO, "Too many ZIM mip levels: %i", num_levels); + return 0; + } + + size_t total_data_size = 0; for (int i = 0; i < num_levels; i++) { if (i > 0) { width[i] = width[i-1] / 2; height[i] = height[i-1] / 2; } + + const size_t level_width = (size_t)width[i]; + const size_t level_height = (size_t)height[i]; + size_t bytes_per_pixel; switch (*flags & ZIM_FORMAT_MASK) { case ZIM_RGBA8888: - image_data_size[i] = width[i] * height[i] * 4; + bytes_per_pixel = 4; break; case ZIM_RGBA4444: case ZIM_RGB565: - image_data_size[i] = width[i] * height[i] * 2; + bytes_per_pixel = 2; break; default: ERROR_LOG(Log::IO, "Invalid ZIM format %i", *flags & ZIM_FORMAT_MASK); return 0; } + if (level_width > std::numeric_limits::max() / level_height || + level_width * level_height > std::numeric_limits::max() / bytes_per_pixel) { + ERROR_LOG(Log::IO, "ZIM image size is too large"); + return 0; + } + image_data_size[i] = level_width * level_height * bytes_per_pixel; + if (total_data_size > std::numeric_limits::max() - image_data_size[i]) { + ERROR_LOG(Log::IO, "ZIM mip data size is too large"); + return 0; + } total_data_size += image_data_size[i]; } @@ -89,14 +118,29 @@ int LoadZIMPtr(const uint8_t *zim, size_t datasize, int *width, int *height, int return 0; } + const size_t payload_size = datasize - 16; + if (!(*flags & (ZIM_ZLIB_COMPRESSED | ZIM_ZSTD_COMPRESSED)) && payload_size != total_data_size) { + ERROR_LOG(Log::IO, "Wrong size data in ZIM: %i vs %i", (int)payload_size, (int)total_data_size); + return 0; + } + if ((*flags & ZIM_ZLIB_COMPRESSED) && + (total_data_size > (size_t)std::numeric_limits::max() || payload_size > (size_t)std::numeric_limits::max())) { + ERROR_LOG(Log::IO, "ZIM zlib data is too large"); + return 0; + } + image[0] = (uint8_t *)malloc(total_data_size); + if (!image[0]) { + ERROR_LOG(Log::IO, "Failed to allocate %lld bytes for ZIM image", (long long)total_data_size); + return 0; + } for (int i = 1; i < num_levels; i++) { image[i] = image[i-1] + image_data_size[i-1]; } if (*flags & ZIM_ZLIB_COMPRESSED) { long outlen = (long)total_data_size; - int retcode = ezuncompress(*image, &outlen, (unsigned char *)(zim + 16), (long)datasize - 16); + int retcode = ezuncompress(*image, &outlen, (unsigned char *)(zim + 16), (long)payload_size); if (Z_OK != retcode) { ERROR_LOG(Log::IO, "ZIM zlib format decompression failed: %d", retcode); free(*image); @@ -108,7 +152,7 @@ int LoadZIMPtr(const uint8_t *zim, size_t datasize, int *width, int *height, int ERROR_LOG(Log::IO, "Wrong size data in ZIM: %i vs %i", (int)outlen, (int)total_data_size); } } else if (*flags & ZIM_ZSTD_COMPRESSED) { - size_t outlen = ZSTD_decompress(*image, total_data_size, zim + 16, datasize - 16); + size_t outlen = ZSTD_decompress(*image, total_data_size, zim + 16, payload_size); if (outlen != (size_t)total_data_size) { ERROR_LOG(Log::IO, "ZIM zstd format decompression failed: %lld", (long long)outlen); free(*image); @@ -116,10 +160,7 @@ int LoadZIMPtr(const uint8_t *zim, size_t datasize, int *width, int *height, int return 0; } } else { - memcpy(*image, zim + 16, datasize - 16); - if (datasize - 16 != (size_t)total_data_size) { - ERROR_LOG(Log::IO, "Wrong size data in ZIM: %i vs %i", (int)(datasize-16), (int)total_data_size); - } + memcpy(*image, zim + 16, payload_size); } return num_levels; } From 86c98c2d23e748f3b837a3b80d0b877ee475c06c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 21 Jul 2026 14:21:54 +0200 Subject: [PATCH 02/12] Prepare EmuThread for other users --- Core/EmuThread.cpp | 57 +++++++++++++++++++++++++++------------------- Core/EmuThread.h | 8 ++++++- UI/NativeApp.cpp | 4 ++++ Windows/main.cpp | 2 +- 4 files changed, 46 insertions(+), 25 deletions(-) diff --git a/Core/EmuThread.cpp b/Core/EmuThread.cpp index a4170b249a..eb9d2c1aaf 100644 --- a/Core/EmuThread.cpp +++ b/Core/EmuThread.cpp @@ -40,7 +40,7 @@ bool MainThread_Ready() { return g_inLoop; } -static void EmuThreadFunc(GraphicsContext *graphicsContext) { +static void EmuThreadFunc(GraphicsContext *graphicsContext, std::function postFrame) { SetCurrentThreadName("EmuThread"); // There's no real requirement that NativeInit happen on this thread. @@ -52,12 +52,10 @@ static void EmuThreadFunc(GraphicsContext *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 (postFrame) { + postFrame(); + } if (GetUIState() == UISTATE_EXIT) { g_emuThreadState = EmuThreadState::QUIT_REQUESTED; @@ -69,7 +67,28 @@ static void EmuThreadFunc(GraphicsContext *graphicsContext) { NativeShutdownGraphics(); } -void MainThreadFunc(GraphicsContext *graphicsContext) { +std::thread EmuThread_Start(GraphicsContext *graphicsContext, std::function postFrame) { + g_emuThreadState = EmuThreadState::START_REQUESTED; + std::thread emuThread = std::thread(&EmuThreadFunc, graphicsContext, postFrame); + return emuThread; +} + +void EmuThread_Join(GraphicsContext *graphicsContext, std::thread &emuThread) { + _dbg_assert_(emuThread.joinable()); + const EmuThreadState state = g_emuThreadState; + if (state != EmuThreadState::QUIT_REQUESTED && + state != EmuThreadState::STOPPED) { + g_emuThreadState = EmuThreadState::QUIT_REQUESTED; + } + graphicsContext->ThreadFrameUntilCondition([] { + // Need to keep eating frames to allow the EmuThread to exit correctly. + return g_emuThreadState == EmuThreadState::STOPPED; + }); + emuThread.join(); + emuThread = std::thread(); +} + +void MainThreadFunc(GraphicsContext *graphicsContext, std::function postFrame) { const bool useEmuThread = g_Config.iGPUBackend == (int)GPUBackend::OPENGL; if (useEmuThread) { SetCurrentThreadName("RenderThread"); @@ -82,8 +101,7 @@ void MainThreadFunc(GraphicsContext *graphicsContext) { g_inLoop = true; - g_emuThreadState = EmuThreadState::START_REQUESTED; - std::thread emuThread = std::thread(&EmuThreadFunc, graphicsContext); + std::thread emuThread = EmuThread_Start(graphicsContext, postFrame); graphicsContext->ThreadStart(); // This thread becomes the render thread. @@ -99,19 +117,13 @@ void MainThreadFunc(GraphicsContext *graphicsContext) { Core_Stop(); g_inLoop = false; - const EmuThreadState state = g_emuThreadState; - if (state != EmuThreadState::QUIT_REQUESTED && - state != EmuThreadState::STOPPED) { - g_emuThreadState = EmuThreadState::QUIT_REQUESTED; - } - graphicsContext->ThreadFrameUntilCondition([] { - // Need to keep eating frames to allow the EmuThread to exit correctly. - return g_emuThreadState == EmuThreadState::STOPPED; - }); - emuThread.join(); - INFO_LOG(Log::System, "EmuThreadJoin - joined"); + EmuThread_Join(graphicsContext, emuThread); graphicsContext->ThreadEnd(); + graphicsContext->ShutdownFromRenderThread(); + + INFO_LOG(Log::System, "EmuThreadJoin - joined"); + } else { SetCurrentThreadName("EmuThread"); // This is the emu thread. the graphics contexts will spawn and handle its own threads if needed. @@ -131,10 +143,8 @@ void MainThreadFunc(GraphicsContext *graphicsContext) { while (GetUIState() != UISTATE_EXIT) { // 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); + postFrame(); } Core_Stop(); @@ -148,6 +158,7 @@ void MainThreadFunc(GraphicsContext *graphicsContext) { graphicsContext->ThreadEnd(); graphicsContext->ShutdownFromRenderThread(); + } graphicsContext->Shutdown(); diff --git a/Core/EmuThread.h b/Core/EmuThread.h index 470ea7366c..2c4cb91dbd 100644 --- a/Core/EmuThread.h +++ b/Core/EmuThread.h @@ -16,6 +16,7 @@ // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. #pragma once +#include // Utilities to manage Emu and Render threads. // TODO: Use across platforms, currently Windows-only. @@ -27,4 +28,9 @@ class GraphicsContext; // Doesn't take ownership of the graphicsContext, you have to delete it. // This should be used by platforms that launch a separate thread and doesn't // need to run a polling loop in it. -void MainThreadFunc(GraphicsContext *graphicsContext); +void MainThreadFunc(GraphicsContext *graphicsContext, std::function postFrame); + + +// If you're not using MainThreadFunc, you can at least use these to manage an emu thread. +std::thread EmuThread_Start(GraphicsContext *graphicsContext, std::function postFrame); +void EmuThread_Join(GraphicsContext *graphicsContext, std::thread &emuThread); diff --git a/UI/NativeApp.cpp b/UI/NativeApp.cpp index f17d882845..0ce72ead88 100644 --- a/UI/NativeApp.cpp +++ b/UI/NativeApp.cpp @@ -1018,6 +1018,10 @@ static void SendMouseDeltaAxis(); void NativeFrame(GraphicsContext *graphicsContext) { PROFILE_END_FRAME(); + if (!(Core_IsActive() || Core_IsStepping())) + UpdateUIState(UISTATE_MENU); + Core_StateProcessed(); + if (System_GetPropertyInt(SYSPROP_DEVICE_TYPE) == DEVICE_TYPE_DESKTOP) { if (g_windowHidden && g_Config.bPauseWhenMinimized) { sleep_ms(16, "window-hidden"); diff --git a/Windows/main.cpp b/Windows/main.cpp index 4eb5ba79cc..7bf2712265 100644 --- a/Windows/main.cpp +++ b/Windows/main.cpp @@ -1267,7 +1267,7 @@ int WINAPI WinMain(HINSTANCE _hInstance, HINSTANCE hPrevInstance, LPSTR szCmdLin _dbg_assert_(false); return; } - MainThreadFunc(graphicsContext.get()); + MainThreadFunc(graphicsContext.get(), []() {}); PostMessage(MainWindow::GetHWND(), MainWindow::WM_USER_UPDATE_UI, 0, 0); }); From af7977267e33428f0875da8ba0923d2090226fe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 21 Jul 2026 14:37:32 +0200 Subject: [PATCH 03/12] Add NeedsRenderThread property to GraphicsContext --- Common/GraphicsContext.h | 5 +++++ Core/EmuThread.cpp | 17 ++++++++--------- Qt/QtMain.h | 2 ++ SDL/SDLGLGraphicsContext.h | 2 ++ Windows/GPU/WindowsGLContext.h | 2 ++ android/jni/AndroidJavaGLContext.h | 2 ++ ios/ViewController.mm | 1 + libretro/LibretroGLContext.h | 2 ++ libretro/LibretroGLCoreContext.h | 4 ++-- 9 files changed, 26 insertions(+), 11 deletions(-) diff --git a/Common/GraphicsContext.h b/Common/GraphicsContext.h index 59b33ae169..4f7008cad9 100644 --- a/Common/GraphicsContext.h +++ b/Common/GraphicsContext.h @@ -29,6 +29,9 @@ public: // Needs casting to the appropriate type, unfortunately. Should find a better solution.. virtual void *GetAPIContext() { return nullptr; } + // OpenGL returns true here. + virtual bool NeedsRenderThread() const { return false; } + // Called from the render thread from threaded backends. virtual void ThreadStart() {} virtual bool ThreadFrame(bool waitIfEmpty) { return true; } // waitIfEmpty should normally be true, except in exit scenarios. @@ -41,6 +44,8 @@ public: virtual Draw::DrawContext *GetDrawContext() = 0; void ThreadFrameUntilCondition(std::function conditionStopped) { + _dbg_assert_(NeedsRenderThread()); + bool exitOnEmpty = false; INFO_LOG(Log::System, "Executing graphicsContext->ThreadFrame to clear buffers"); diff --git a/Core/EmuThread.cpp b/Core/EmuThread.cpp index eb9d2c1aaf..fb52b85963 100644 --- a/Core/EmuThread.cpp +++ b/Core/EmuThread.cpp @@ -74,23 +74,24 @@ std::thread EmuThread_Start(GraphicsContext *graphicsContext, std::functionThreadFrameUntilCondition([] { - // Need to keep eating frames to allow the EmuThread to exit correctly. - return g_emuThreadState == EmuThreadState::STOPPED; - }); + _dbg_assert_(emuThread.joinable()); + if (graphicsContext->NeedsRenderThread()) { + graphicsContext->ThreadFrameUntilCondition([] { + // Need to keep eating frames to allow the EmuThread to exit correctly. + return g_emuThreadState == EmuThreadState::STOPPED; + }); + } emuThread.join(); emuThread = std::thread(); } void MainThreadFunc(GraphicsContext *graphicsContext, std::function postFrame) { - const bool useEmuThread = g_Config.iGPUBackend == (int)GPUBackend::OPENGL; - if (useEmuThread) { + if (graphicsContext->NeedsRenderThread()) { SetCurrentThreadName("RenderThread"); // This is now the render thread, and will spawn the emu thread below. @@ -139,7 +140,6 @@ void MainThreadFunc(GraphicsContext *graphicsContext, std::function post g_inLoop = true; graphicsContext->ThreadStart(); - while (GetUIState() != UISTATE_EXIT) { // We're here again, so the game quit. Restart Run() which controls the UI. // This way they can load a new game. @@ -158,7 +158,6 @@ void MainThreadFunc(GraphicsContext *graphicsContext, std::function post graphicsContext->ThreadEnd(); graphicsContext->ShutdownFromRenderThread(); - } graphicsContext->Shutdown(); diff --git a/Qt/QtMain.h b/Qt/QtMain.h index f5d89608f8..3f80b5c82d 100644 --- a/Qt/QtMain.h +++ b/Qt/QtMain.h @@ -65,6 +65,8 @@ public: renderManager_ = nullptr; } + bool NeedsRenderThread() const override { return true; } + void Shutdown() override {} void Resize() override {} diff --git a/SDL/SDLGLGraphicsContext.h b/SDL/SDLGLGraphicsContext.h index 362aad00eb..b789976c3d 100644 --- a/SDL/SDLGLGraphicsContext.h +++ b/SDL/SDLGLGraphicsContext.h @@ -12,6 +12,8 @@ public: // Returns 0 on success. int Init(SDL_Window *&window, int x, int y, int w, int h, int mode, std::string *error_message, int force_gl_version); + bool NeedsRenderThread() const override { return true; } + bool InitFromRenderThread(std::string *errorMessage) override; void Shutdown() override {} diff --git a/Windows/GPU/WindowsGLContext.h b/Windows/GPU/WindowsGLContext.h index 0d5482995f..9c473a74bb 100644 --- a/Windows/GPU/WindowsGLContext.h +++ b/Windows/GPU/WindowsGLContext.h @@ -13,6 +13,8 @@ class WindowsGLContext : public WindowsGraphicsContext { public: bool Init(HINSTANCE hInst, HWND window, std::string *error_message) override; + bool NeedsRenderThread() const override { return true; } + bool InitFromRenderThread(std::string *errorMessage) override; void ShutdownFromRenderThread() override; diff --git a/android/jni/AndroidJavaGLContext.h b/android/jni/AndroidJavaGLContext.h index 611df72dba..6ae1374448 100644 --- a/android/jni/AndroidJavaGLContext.h +++ b/android/jni/AndroidJavaGLContext.h @@ -9,6 +9,8 @@ public: AndroidJavaEGLGraphicsContext(); ~AndroidJavaEGLGraphicsContext() override { delete draw_; } + bool NeedsRenderThread() const override { return true; } + // This performs the actual initialization, bool InitFromRenderThread(ANativeWindow *wnd, int desiredBackbufferSizeX, int desiredBackbufferSizeY, int backbufferFormat, int androidVersion) override; diff --git a/ios/ViewController.mm b/ios/ViewController.mm index ec3b1698cc..d5c22cbd39 100644 --- a/ios/ViewController.mm +++ b/ios/ViewController.mm @@ -55,6 +55,7 @@ public: ~IOSGLESContext() { delete draw_; } + bool NeedsRenderThread() const override { return true; } Draw::DrawContext *GetDrawContext() override { return draw_; } diff --git a/libretro/LibretroGLContext.h b/libretro/LibretroGLContext.h index f02d847928..6ca8691a28 100644 --- a/libretro/LibretroGLContext.h +++ b/libretro/LibretroGLContext.h @@ -16,6 +16,8 @@ public: hw_render_.bottom_left_origin = true; } + bool NeedsRenderThread() const override { return true; } + bool Init() override; void CreateDrawContext() override; void DestroyDrawContext() override; diff --git a/libretro/LibretroGLCoreContext.h b/libretro/LibretroGLCoreContext.h index d9ee56820f..c84e741aa9 100644 --- a/libretro/LibretroGLCoreContext.h +++ b/libretro/LibretroGLCoreContext.h @@ -7,10 +7,10 @@ class LibretroGLCoreContext : public LibretroHWRenderContext { public: LibretroGLCoreContext() - : LibretroHWRenderContext(RETRO_HW_CONTEXT_OPENGL_CORE, 3, 1) - { + : LibretroHWRenderContext(RETRO_HW_CONTEXT_OPENGL_CORE, 3, 1) { hw_render_.bottom_left_origin = true; } + bool NeedsRenderThread() const override { return true; } bool Init() override; void CreateDrawContext() override; From 2489f5c581c3478fa63db0663a7603494bf1459a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 21 Jul 2026 15:38:05 +0200 Subject: [PATCH 04/12] Some CMakeLists.txt reorganization, use EmuThread from SDL --- AGENTS.md | 8 +++++ CLAUDE.md | 1 + CMakeLists.txt | 1 + Core/EmuThread.h | 4 +-- SDL/SDLMain.cpp | 80 ++++++------------------------------------------ 5 files changed, 22 insertions(+), 72 deletions(-) create mode 100644 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md index 90a909ec5d..4c53e5d3fb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,3 +44,11 @@ when making cross platform changes. New unit tests are added by listing them in availableTests in unittest.cpp. If they are large, put them in separate files in the unittest subdirectory. Remember to update both CMakeLists.txt and the visual studio project. + +## Quick rebuild on Linux + +You don't need to do ./b.sh --debug to verify every single little change, instead use this shortcut: + +```bash +cd build ; make -j32; cd .. +``` \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..43c994c2d3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/CMakeLists.txt b/CMakeLists.txt index 82f33b6abe..46bd91e9c1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1549,6 +1549,7 @@ if(WIN32) target_link_libraries(Common winmm dsound dxguid d2d1 dwrite d3d11 Version) endif() +set(UISource) if(NOT LIBRETRO) list(APPEND UISource UI/AudioCommon.h diff --git a/Core/EmuThread.h b/Core/EmuThread.h index 2c4cb91dbd..6a8e2a1f6f 100644 --- a/Core/EmuThread.h +++ b/Core/EmuThread.h @@ -30,7 +30,7 @@ class GraphicsContext; // need to run a polling loop in it. void MainThreadFunc(GraphicsContext *graphicsContext, std::function postFrame); - -// If you're not using MainThreadFunc, you can at least use these to manage an emu thread. +// If you're not using MainThreadFunc, you can at least use these to manage a spinning EmuThread (that calls NativeFrame), +// whether your graphics context requires multithreading or not. std::thread EmuThread_Start(GraphicsContext *graphicsContext, std::function postFrame); void EmuThread_Join(GraphicsContext *graphicsContext, std::thread &emuThread); diff --git a/SDL/SDLMain.cpp b/SDL/SDLMain.cpp index 9883262a6e..7e17283155 100644 --- a/SDL/SDLMain.cpp +++ b/SDL/SDLMain.cpp @@ -29,6 +29,7 @@ SDLJoystick *joystick = NULL; #include "Common/System/NativeApp.h" #include "Common/Audio/AudioBackend.h" #include "Core/CmdLine.h" +#include "Core/EmuThread.h" #include "ext/glslang/glslang/Public/ShaderLang.h" #include "Common/Data/Format/PNGLoad.h" #include "Common/Net/Resolve.h" @@ -1276,48 +1277,6 @@ void UpdateWindowState(SDL_Window *window) { g_windowState.update = false; } -enum class EmuThreadState { - DISABLED, - START_REQUESTED, - RUNNING, - QUIT_REQUESTED, - STOPPED, -}; - -static std::thread emuThread; -static std::atomic emuThreadState((int)EmuThreadState::DISABLED); - -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. - emuThreadState = (int)EmuThreadState::RUNNING; - - NativeInitGraphics(graphicsContext); - - while (emuThreadState != (int)EmuThreadState::QUIT_REQUESTED) { - NativeFrame(graphicsContext); - } - emuThreadState = (int)EmuThreadState::STOPPED; - - NativeShutdownGraphics(); -} - -static void EmuThreadStart(GraphicsContext *context) { - emuThreadState = (int)EmuThreadState::START_REQUESTED; - emuThread = std::thread(&EmuThreadFunc, context); -} - -static void EmuThreadStop(const char *reason) { - emuThreadState = (int)EmuThreadState::QUIT_REQUESTED; -} - -static void EmuThreadJoin() { - emuThread.join(); - emuThread = std::thread(); -} - struct InputStateTracker { void MouseCaptureControl(SDL_Window *window) { bool captureMouseCondition = g_Config.bMouseControl && ((GetUIState() == UISTATE_INGAME && g_Config.bMouseConfine) || g_IsMappingMouseInput); @@ -2080,8 +2039,6 @@ int main(int argc, char *argv[]) { Native_UpdateScreenScale(w * g_DesktopDPI, h * g_DesktopDPI, UIScaleFactorToMultiplier(g_Config.iUIScaleFactor)); - bool mainThreadIsRender = g_Config.iGPUBackend == (int)GPUBackend::OPENGL; - SDL_SetWindowTitle(window, (app_name_nice + " " + PPSSPP_GIT_VERSION).c_str()); char iconPath[PATH_MAX]; @@ -2139,7 +2096,7 @@ int main(int argc, char *argv[]) { } EnableFZ(); - EmuThreadStart(graphicsContext); + std::thread emuThread = EmuThread_Start(graphicsContext, nullptr); graphicsContext->ThreadStart(); @@ -2150,8 +2107,6 @@ int main(int argc, char *argv[]) { initializeOSXExtras(); #endif - bool waitOnExit = g_Config.iGPUBackend == (int)GPUBackend::OPENGL; - // Check if the path to a directory containing an unpacked ISO is passed as a command line argument for (int i = 1; i < argc; i++) { if (File::IsDirectory(Path(argv[i]))) { @@ -2161,10 +2116,11 @@ int main(int argc, char *argv[]) { } } + const bool mainThreadIsRender = graphicsContext->NeedsRenderThread(); if (!mainThreadIsRender) { // Vulkan mode uses this. // We should only be a message pump. This allows for lower latency - // input events, and so on. + // input events, and so on. The spawned EmuThread runs emulation and rendering. while (true) { SDL_Event event; if (SDL_WaitEventTimeout(&event, 100)) { @@ -2192,17 +2148,13 @@ int main(int argc, char *argv[]) { } } } else while (true) { + // OpenGL mode uses this. { SDL_Event event; while (SDL_PollEvent(&event)) { ProcessSDLEvent(window, event, &inputTracker); } } - if (g_QuitRequested || g_RestartRequested) - break; - if (emuThreadState == (int)EmuThreadState::DISABLED) { - NativeFrame(graphicsContext); - } if (g_QuitRequested || g_RestartRequested) break; @@ -2211,8 +2163,8 @@ int main(int argc, char *argv[]) { inputTracker.MouseCaptureControl(window); - bool renderThreadPaused = Native_IsWindowHidden() && g_Config.bPauseWhenMinimized && emuThreadState != (int)EmuThreadState::DISABLED; - if (emuThreadState != (int)EmuThreadState::DISABLED && !renderThreadPaused) { + bool renderThreadPaused = Native_IsWindowHidden() && g_Config.bPauseWhenMinimized; + if (graphicsContext->NeedsRenderThread() && !renderThreadPaused) { if (!graphicsContext->ThreadFrame(true)) break; } @@ -2227,11 +2179,7 @@ int main(int argc, char *argv[]) { if (g_rebootEmuThread) { fprintf(stderr, "rebooting emu thread"); g_rebootEmuThread = false; - EmuThreadStop("shutdown"); - graphicsContext->ThreadFrameUntilCondition([]() { - return emuThreadState == (int)EmuThreadState::STOPPED || emuThreadState == (int)EmuThreadState::DISABLED; - }); - EmuThreadJoin(); + EmuThread_Join(graphicsContext, emuThread); graphicsContext->ThreadEnd(); graphicsContext->ShutdownFromRenderThread(); @@ -2249,20 +2197,12 @@ int main(int argc, char *argv[]) { return 1; } - EmuThreadStart(graphicsContext); + emuThread = EmuThread_Start(graphicsContext, nullptr); graphicsContext->ThreadStart(); } } - EmuThreadStop("shutdown"); - - if (waitOnExit) { - graphicsContext->ThreadFrameUntilCondition([]() { - return emuThreadState == (int)EmuThreadState::STOPPED || emuThreadState == (int)EmuThreadState::DISABLED; - }); - } - - EmuThreadJoin(); + EmuThread_Join(graphicsContext, emuThread); delete joystick; From 86c8e9ae6864ec519aaaee5aa31b1f56efef44ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 21 Jul 2026 18:12:05 +0200 Subject: [PATCH 05/12] Minor code cleanup --- Core/EmuThread.cpp | 1 + SDL/SDLMain.cpp | 1 - UI/MainScreen.cpp | 1 - android/jni/app-android.cpp | 55 ++++++++++--------- .../src/org/ppsspp/ppsspp/NativeRenderer.java | 1 - 5 files changed, 29 insertions(+), 30 deletions(-) diff --git a/Core/EmuThread.cpp b/Core/EmuThread.cpp index fb52b85963..65cc06153d 100644 --- a/Core/EmuThread.cpp +++ b/Core/EmuThread.cpp @@ -42,6 +42,7 @@ bool MainThread_Ready() { static void EmuThreadFunc(GraphicsContext *graphicsContext, std::function postFrame) { SetCurrentThreadName("EmuThread"); + AndroidJNIThreadContext context; // There's no real requirement that NativeInit happen on this thread. // We just call the update/render loop here. diff --git a/SDL/SDLMain.cpp b/SDL/SDLMain.cpp index 7e17283155..29546ea045 100644 --- a/SDL/SDLMain.cpp +++ b/SDL/SDLMain.cpp @@ -2111,7 +2111,6 @@ int main(int argc, char *argv[]) { for (int i = 1; i < argc; i++) { if (File::IsDirectory(Path(argv[i]))) { // Display the toast warning - System_Toast("Warning: Playing unpacked games may cause issues."); break; } } diff --git a/UI/MainScreen.cpp b/UI/MainScreen.cpp index 785646dfb3..3cadee899b 100644 --- a/UI/MainScreen.cpp +++ b/UI/MainScreen.cpp @@ -320,7 +320,6 @@ void MainScreen::CreateMainButtons(UI::ViewGroup *parent, bool portrait) { if (!g_Config.Save("MainScreen::OnExit")) { System_Toast("Failed to save settings!\nCheck permissions, or try to restart the device."); } - UpdateUIState(UISTATE_EXIT); // Request the framework to exit cleanly. System_ExitApp(); diff --git a/android/jni/app-android.cpp b/android/jni/app-android.cpp index cea0d300bb..efc30133d6 100644 --- a/android/jni/app-android.cpp +++ b/android/jni/app-android.cpp @@ -108,8 +108,6 @@ struct JNIEnv {}; #include "app-android.h" -bool useCPUThread = true; - enum class EmuThreadState { DISABLED, START_REQUESTED, @@ -281,7 +279,8 @@ static void EmuThreadFunc() { // Wait for render loop to get started. INFO_LOG(Log::System, "Runloop: Waiting for displayInit..."); - while (!graphicsContext || graphicsContext->GetState() == GraphicsContextState::PENDING) { + _dbg_assert_(graphicsContext); + while (graphicsContext->GetState() == GraphicsContextState::PENDING) { sleep_ms(5, "graphics-poll"); } @@ -864,7 +863,6 @@ extern "C" void Java_org_ppsspp_ppsspp_NativeApp_init retry: switch (g_Config.iGPUBackend) { case (int)GPUBackend::OPENGL: - useCPUThread = true; INFO_LOG(Log::System, "NativeApp.init() -- creating OpenGL context (JavaGL)"); graphicsContext = new AndroidJavaEGLGraphicsContext(); INFO_LOG(Log::System, "NativeApp.init() - launching emu thread"); @@ -873,7 +871,6 @@ retry: case (int)GPUBackend::VULKAN: { INFO_LOG(Log::System, "NativeApp.init() -- creating Vulkan context"); - useCPUThread = false; // The Vulkan render manager manages its own thread. // We create and destroy the Vulkan graphics context in the app main thread though. AndroidVulkanContext *ctx = new AndroidVulkanContext(); @@ -989,9 +986,8 @@ extern "C" void Java_org_ppsspp_ppsspp_NativeApp_pause(JNIEnv *, jclass) { extern "C" void Java_org_ppsspp_ppsspp_NativeApp_shutdown(JNIEnv *, jclass) { INFO_LOG(Log::System, "NativeApp.shutdown() -- begin"); - if (renderer_inited && useCPUThread && graphicsContext) { + if (renderer_inited && graphicsContext && graphicsContext->NeedsRenderThread()) { // Only used in Java EGL path. - EmuThreadStop("shutdown"); // NOTE: We know that the GLSurfaceView render thread is stopped here, since we now // correctly call GLSurfaceView.onPause/onResume. However, there may still be queued frames. @@ -1042,14 +1038,32 @@ extern "C" void Java_org_ppsspp_ppsspp_NativeApp_shutdown(JNIEnv *, jclass) { // JavaEGL. This doesn't get called on the Vulkan path. // This gets called from onSurfaceCreated. extern "C" jboolean Java_org_ppsspp_ppsspp_NativeRenderer_displayInit(JNIEnv * env, jobject obj) { - _assert_(useCPUThread); + if (!graphicsContext) { + ERROR_LOG(Log::G3D, "NativeApp.displayInit() - graphicsContext is null!"); + return false; + } + + _assert_(graphicsContext->NeedsRenderThread()); INFO_LOG(Log::G3D, "NativeApp.displayInit()"); bool firstStart = !renderer_inited; // We should be running on the render thread here. std::string errorMessage; - if (renderer_inited) { + if (!renderer_inited) { + INFO_LOG(Log::G3D, "NativeApp.displayInit() first time"); + if (!graphicsContext->InitFromRenderThread(nullptr, 0, 0, 0, 0)) { + System_Toast("Graphics initialization failed. Quitting."); + return false; + } + + graphicsContext->GetDrawContext()->SetErrorCallback([](const char *shortDesc, const char *details, void *userdata) { + g_OSD.Show(OSDType::MESSAGE_ERROR, details, 5.0); + }, nullptr); + + graphicsContext->ThreadStart(); + renderer_inited = true; + } else { // Would be really nice if we could get something on the GL thread immediately when shutting down, // but the only mechanism for handling lost devices seems to be that onSurfaceCreated is called again, // which ends up calling displayInit. @@ -1084,19 +1098,6 @@ extern "C" jboolean Java_org_ppsspp_ppsspp_NativeRenderer_displayInit(JNIEnv * e graphicsContext->ThreadStart(); INFO_LOG(Log::G3D, "Restored."); - } else { - INFO_LOG(Log::G3D, "NativeApp.displayInit() first time"); - if (!graphicsContext || !graphicsContext->InitFromRenderThread(nullptr, 0, 0, 0, 0)) { - System_Toast("Graphics initialization failed. Quitting."); - return false; - } - - graphicsContext->GetDrawContext()->SetErrorCallback([](const char *shortDesc, const char *details, void *userdata) { - g_OSD.Show(OSDType::MESSAGE_ERROR, details, 5.0); - }, nullptr); - - graphicsContext->ThreadStart(); - renderer_inited = true; } System_PostUIMessage(UIMessage::RECREATE_VIEWS); @@ -1246,10 +1247,9 @@ extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_sendRequestResult(JNIEn } } +// This doesn't get called on the Vulkan path. +// We don't need a render thread "loop" as this gets called repeatedly by the system. extern "C" void Java_org_ppsspp_ppsspp_NativeRenderer_displayRender(JNIEnv *env, jobject obj) { - // This doesn't get called on the Vulkan path. - _assert_(useCPUThread); - static bool hasSetThreadName = false; if (!hasSetThreadName) { hasSetThreadName = true; @@ -1263,6 +1263,7 @@ extern "C" void Java_org_ppsspp_ppsspp_NativeRenderer_displayRender(JNIEnv *env, if (!graphicsContext || !graphicsContext->ThreadFrame(true)) { return; } + _assert_(graphicsContext->NeedsRenderThread()); if (IsVREnabled()) { UpdateVRInput(g_Config.bHapticFeedback, g_display.dpi_scale_x, g_display.dpi_scale_y); @@ -1718,13 +1719,13 @@ static void VulkanEmuThread(ANativeWindow *wnd); // This runs in Vulkan mode only. // This handles the entire lifecycle of the Vulkan context, init and exit. extern "C" jboolean JNICALL Java_org_ppsspp_ppsspp_PpssppActivity_runVulkanRenderLoop(JNIEnv * env, jobject obj, jobject _surf) { - _assert_(!useCPUThread); - if (!graphicsContext) { ERROR_LOG(Log::G3D, "runVulkanRenderLoop: Tried to enter without a created graphics context."); return false; } + _assert_(!graphicsContext->NeedsRenderThread()); + if (g_renderLoopThread.joinable()) { ERROR_LOG(Log::G3D, "runVulkanRenderLoop: Already running"); return false; diff --git a/android/src/org/ppsspp/ppsspp/NativeRenderer.java b/android/src/org/ppsspp/ppsspp/NativeRenderer.java index 853514787d..c7837d807e 100644 --- a/android/src/org/ppsspp/ppsspp/NativeRenderer.java +++ b/android/src/org/ppsspp/ppsspp/NativeRenderer.java @@ -16,7 +16,6 @@ public class NativeRenderer implements GLSurfaceView.Renderer { public void onDrawFrame(GL10 unused /*use GLES20*/) { displayRender(); } - public void onSurfaceCreated(GL10 gl, EGLConfig config) { Log.i(TAG, "NativeRenderer (OpenGL): onSurfaceCreated"); From 906d8770194c85f8c593fb97e09ad76a171fbf4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 21 Jul 2026 18:19:15 +0200 Subject: [PATCH 06/12] Android: Remove some state and a race condition --- android/jni/AndroidGraphicsContext.h | 12 ------------ android/jni/AndroidJavaGLContext.cpp | 4 ---- android/jni/AndroidVulkanContext.cpp | 3 --- android/jni/app-android.cpp | 26 +++++--------------------- 4 files changed, 5 insertions(+), 40 deletions(-) diff --git a/android/jni/AndroidGraphicsContext.h b/android/jni/AndroidGraphicsContext.h index f51b7e96c7..544b5c2c06 100644 --- a/android/jni/AndroidGraphicsContext.h +++ b/android/jni/AndroidGraphicsContext.h @@ -16,13 +16,6 @@ enum { ANDROID_VERSION_NOUGAT_1 = 25, }; -enum class GraphicsContextState { - PENDING, - INITIALIZED, - FAILED_INIT, - SHUTDOWN, -}; - class AndroidGraphicsContext : public GraphicsContext { public: // This is different than the base class function since on @@ -30,11 +23,6 @@ public: virtual bool InitFromRenderThread(ANativeWindow *wnd, int desiredBackbufferSizeX, int desiredBackbufferSizeY, int backbufferFormat, int androidVersion) = 0; virtual void BeginAndroidShutdown() {} - virtual GraphicsContextState GetState() const { return state_; } - -protected: - GraphicsContextState state_ = GraphicsContextState::PENDING; - private: using GraphicsContext::InitFromRenderThread; }; diff --git a/android/jni/AndroidJavaGLContext.cpp b/android/jni/AndroidJavaGLContext.cpp index 12e4862592..4fc9473b8c 100644 --- a/android/jni/AndroidJavaGLContext.cpp +++ b/android/jni/AndroidJavaGLContext.cpp @@ -14,7 +14,6 @@ bool AndroidJavaEGLGraphicsContext::InitFromRenderThread(ANativeWindow *wnd, int INFO_LOG(Log::G3D, "AndroidJavaEGLGraphicsContext::InitFromRenderThread"); if (!CheckGLExtensions()) { ERROR_LOG(Log::G3D, "CheckGLExtensions failed - not gonna attempt starting up."); - state_ = GraphicsContextState::FAILED_INIT; return false; } @@ -29,10 +28,8 @@ bool AndroidJavaEGLGraphicsContext::InitFromRenderThread(ANativeWindow *wnd, int if (!draw_->CreatePresets()) { // This can't really happen now that compilation is async - they're only really queued for compile here. _assert_msg_(false, "Failed to compile preset shaders"); - state_ = GraphicsContextState::FAILED_INIT; return false; } - state_ = GraphicsContextState::INITIALIZED; return true; } @@ -41,5 +38,4 @@ void AndroidJavaEGLGraphicsContext::ShutdownFromRenderThread() { renderManager_ = nullptr; // owned by draw_. delete draw_; draw_ = nullptr; - state_ = GraphicsContextState::SHUTDOWN; } diff --git a/android/jni/AndroidVulkanContext.cpp b/android/jni/AndroidVulkanContext.cpp index 65773bb08a..0b7df65fe1 100644 --- a/android/jni/AndroidVulkanContext.cpp +++ b/android/jni/AndroidVulkanContext.cpp @@ -32,7 +32,6 @@ bool AndroidVulkanContext::InitAPI() { std::string errorStr; if (!VulkanLoad(&errorStr)) { ERROR_LOG(Log::G3D, "Failed to load Vulkan driver library: %s", errorStr.c_str()); - state_ = GraphicsContextState::FAILED_INIT; return false; } @@ -49,12 +48,10 @@ bool AndroidVulkanContext::InitAPI() { if (!g_Vulkan->CreateInstanceAndDevice(info)) { delete g_Vulkan; g_Vulkan = nullptr; - state_ = GraphicsContextState::FAILED_INIT; return false; } INFO_LOG(Log::G3D, "Vulkan device created!"); - state_ = GraphicsContextState::INITIALIZED; return true; } diff --git a/android/jni/app-android.cpp b/android/jni/app-android.cpp index efc30133d6..58159ba082 100644 --- a/android/jni/app-android.cpp +++ b/android/jni/app-android.cpp @@ -267,35 +267,19 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *pjvm, void *reserved) { static void EmuThreadFunc() { SetCurrentThreadName("Entering EmuThread"); - // Name the thread in the JVM, because why not (might result in better debug output in Play Console). - // TODO: Do something clever with getEnv() and stored names from SetCurrentThreadName? - JNIEnv *env; - JavaVMAttachArgs args{}; - args.version = JNI_VERSION_1_6; - args.name = "EmuThread"; - gJvm->AttachCurrentThread(&env, &args); + AndroidJNIThreadContext jniContext; + JNIEnv *env = getEnv(); // TODO: Make it gettable from AndroidJNIThreadContext INFO_LOG(Log::System, "Entering emu thread"); // Wait for render loop to get started. INFO_LOG(Log::System, "Runloop: Waiting for displayInit..."); _dbg_assert_(graphicsContext); - while (graphicsContext->GetState() == GraphicsContextState::PENDING) { - sleep_ms(5, "graphics-poll"); - } // Check the state of the graphics context before we try to feed it into NativeInitGraphics. - if (graphicsContext->GetState() != GraphicsContextState::INITIALIZED) { - ERROR_LOG(Log::G3D, "Failed to initialize the graphics context! %d", (int)graphicsContext->GetState()); - emuThreadState = (int)EmuThreadState::QUIT_REQUESTED; - gJvm->DetachCurrentThread(); - return; - } - if (!NativeInitGraphics(graphicsContext)) { _assert_msg_(false, "NativeInitGraphics failed, might as well bail"); emuThreadState = (int)EmuThreadState::QUIT_REQUESTED; - gJvm->DetachCurrentThread(); return; } @@ -323,7 +307,6 @@ static void EmuThreadFunc() { NativeShutdownGraphics(); - gJvm->DetachCurrentThread(); INFO_LOG(Log::System, "Leaving EmuThread"); } @@ -865,8 +848,7 @@ retry: case (int)GPUBackend::OPENGL: INFO_LOG(Log::System, "NativeApp.init() -- creating OpenGL context (JavaGL)"); graphicsContext = new AndroidJavaEGLGraphicsContext(); - INFO_LOG(Log::System, "NativeApp.init() - launching emu thread"); - EmuThreadStart(); + INFO_LOG(Log::System, "NativeApp.init() - not yet launching emuthread, waiting to displayInit"); break; case (int)GPUBackend::VULKAN: { @@ -1056,6 +1038,8 @@ extern "C" jboolean Java_org_ppsspp_ppsspp_NativeRenderer_displayInit(JNIEnv * e System_Toast("Graphics initialization failed. Quitting."); return false; } + // This is where we start the emuthread now - after InitFromRenderThread. This eliminates a race condition. + EmuThreadStart(); graphicsContext->GetDrawContext()->SetErrorCallback([](const char *shortDesc, const char *details, void *userdata) { g_OSD.Show(OSDType::MESSAGE_ERROR, details, 5.0); From 8b05f703a4e3913e0ae0521c29a2fc62d3f105a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 21 Jul 2026 18:24:34 +0200 Subject: [PATCH 07/12] Clean up unused parameters --- android/jni/AndroidGraphicsContext.h | 2 +- android/jni/AndroidJavaGLContext.cpp | 2 +- android/jni/AndroidJavaGLContext.h | 2 +- android/jni/AndroidVulkanContext.cpp | 4 ++-- android/jni/AndroidVulkanContext.h | 2 +- android/jni/app-android.cpp | 6 +++--- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/jni/AndroidGraphicsContext.h b/android/jni/AndroidGraphicsContext.h index 544b5c2c06..0c6f963086 100644 --- a/android/jni/AndroidGraphicsContext.h +++ b/android/jni/AndroidGraphicsContext.h @@ -20,7 +20,7 @@ class AndroidGraphicsContext : public GraphicsContext { public: // This is different than the base class function since on // Android (EGL, Vulkan) we do have all this info on the render thread. - virtual bool InitFromRenderThread(ANativeWindow *wnd, int desiredBackbufferSizeX, int desiredBackbufferSizeY, int backbufferFormat, int androidVersion) = 0; + virtual bool InitFromRenderThread(ANativeWindow *wnd) = 0; virtual void BeginAndroidShutdown() {} private: diff --git a/android/jni/AndroidJavaGLContext.cpp b/android/jni/AndroidJavaGLContext.cpp index 4fc9473b8c..69298c7cf0 100644 --- a/android/jni/AndroidJavaGLContext.cpp +++ b/android/jni/AndroidJavaGLContext.cpp @@ -10,7 +10,7 @@ AndroidJavaEGLGraphicsContext::AndroidJavaEGLGraphicsContext() { SetGPUBackend(GPUBackend::OPENGL); } -bool AndroidJavaEGLGraphicsContext::InitFromRenderThread(ANativeWindow *wnd, int desiredBackbufferSizeX, int desiredBackbufferSizeY, int backbufferFormat, int androidVersion) { +bool AndroidJavaEGLGraphicsContext::InitFromRenderThread(ANativeWindow *wnd) { INFO_LOG(Log::G3D, "AndroidJavaEGLGraphicsContext::InitFromRenderThread"); if (!CheckGLExtensions()) { ERROR_LOG(Log::G3D, "CheckGLExtensions failed - not gonna attempt starting up."); diff --git a/android/jni/AndroidJavaGLContext.h b/android/jni/AndroidJavaGLContext.h index 6ae1374448..ad0e67eff2 100644 --- a/android/jni/AndroidJavaGLContext.h +++ b/android/jni/AndroidJavaGLContext.h @@ -12,7 +12,7 @@ public: bool NeedsRenderThread() const override { return true; } // This performs the actual initialization, - bool InitFromRenderThread(ANativeWindow *wnd, int desiredBackbufferSizeX, int desiredBackbufferSizeY, int backbufferFormat, int androidVersion) override; + bool InitFromRenderThread(ANativeWindow *wnd) override; void ShutdownFromRenderThread() override; diff --git a/android/jni/AndroidVulkanContext.cpp b/android/jni/AndroidVulkanContext.cpp index 0b7df65fe1..9036cc1a44 100644 --- a/android/jni/AndroidVulkanContext.cpp +++ b/android/jni/AndroidVulkanContext.cpp @@ -55,8 +55,8 @@ bool AndroidVulkanContext::InitAPI() { return true; } -bool AndroidVulkanContext::InitFromRenderThread(ANativeWindow *wnd, int desiredBackbufferSizeX, int desiredBackbufferSizeY, int backbufferFormat, int androidVersion) { - INFO_LOG(Log::G3D, "AndroidVulkanContext::InitFromRenderThread: desiredwidth=%d desiredheight=%d", desiredBackbufferSizeX, desiredBackbufferSizeY); +bool AndroidVulkanContext::InitFromRenderThread(ANativeWindow *wnd) { + INFO_LOG(Log::G3D, "AndroidVulkanContext::InitFromRenderThread"); if (!g_Vulkan) { ERROR_LOG(Log::G3D, "AndroidVulkanContext::InitFromRenderThread: No Vulkan context"); return false; diff --git a/android/jni/AndroidVulkanContext.h b/android/jni/AndroidVulkanContext.h index 7da96fbd92..bcdf070fe2 100644 --- a/android/jni/AndroidVulkanContext.h +++ b/android/jni/AndroidVulkanContext.h @@ -11,7 +11,7 @@ public: bool InitAPI(); - bool InitFromRenderThread(ANativeWindow *wnd, int desiredBackbufferSizeX, int desiredBackbufferSizeY, int backbufferFormat, int androidVersion) override; + bool InitFromRenderThread(ANativeWindow *wnd) override; void ShutdownFromRenderThread() override; // Inverses InitFromRenderThread. void Shutdown() override; diff --git a/android/jni/app-android.cpp b/android/jni/app-android.cpp index 58159ba082..8572866bc2 100644 --- a/android/jni/app-android.cpp +++ b/android/jni/app-android.cpp @@ -1034,7 +1034,7 @@ extern "C" jboolean Java_org_ppsspp_ppsspp_NativeRenderer_displayInit(JNIEnv * e std::string errorMessage; if (!renderer_inited) { INFO_LOG(Log::G3D, "NativeApp.displayInit() first time"); - if (!graphicsContext->InitFromRenderThread(nullptr, 0, 0, 0, 0)) { + if (!graphicsContext->InitFromRenderThread((ANativeWindow *)nullptr)) { System_Toast("Graphics initialization failed. Quitting."); return false; } @@ -1068,7 +1068,7 @@ extern "C" jboolean Java_org_ppsspp_ppsspp_NativeRenderer_displayInit(JNIEnv * e INFO_LOG(Log::G3D, "Shut down both threads. Now let's bring it up again!"); - if (!graphicsContext->InitFromRenderThread(nullptr, 0, 0, 0, 0)) { + if (!graphicsContext->InitFromRenderThread((ANativeWindow *)nullptr)) { System_Toast("Graphics initialization failed. Quitting."); return false; } @@ -1769,7 +1769,7 @@ static void VulkanEmuThread(ANativeWindow *wnd) { WARN_LOG(Log::G3D, "runVulkanRenderLoop. display_xres=%d display_yres=%d desiredBackbufferSizeX=%d desiredBackbufferSizeY=%d", display_xres, display_yres, desiredBackbufferSizeX, desiredBackbufferSizeY); - if (!graphicsContext->InitFromRenderThread(wnd, desiredBackbufferSizeX, desiredBackbufferSizeY, backbuffer_format, androidVersion)) { + if (!graphicsContext->InitFromRenderThread(wnd)) { // On Android, if we get here, really no point in continuing. // The UI is supposed to render on any device both on OpenGL and Vulkan. If either of those don't work // on a device, we blacklist it. Hopefully we should have already failed in InitAPI anyway and reverted to GL back then. From 02017ad41e4d660392727fa42cef336e8f19d12e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 22 Jul 2026 09:32:48 +0200 Subject: [PATCH 08/12] Fix name collision between functions --- android/jni/AndroidGraphicsContext.h | 5 +---- android/jni/AndroidJavaGLContext.cpp | 3 ++- android/jni/AndroidJavaGLContext.h | 3 +-- android/jni/AndroidVulkanContext.cpp | 9 ++++++--- android/jni/AndroidVulkanContext.h | 2 +- android/jni/app-android.cpp | 22 +++++++++++++++------- 6 files changed, 26 insertions(+), 18 deletions(-) diff --git a/android/jni/AndroidGraphicsContext.h b/android/jni/AndroidGraphicsContext.h index 0c6f963086..f9191aadbd 100644 --- a/android/jni/AndroidGraphicsContext.h +++ b/android/jni/AndroidGraphicsContext.h @@ -20,9 +20,6 @@ class AndroidGraphicsContext : public GraphicsContext { public: // This is different than the base class function since on // Android (EGL, Vulkan) we do have all this info on the render thread. - virtual bool InitFromRenderThread(ANativeWindow *wnd) = 0; + virtual bool Init(ANativeWindow *wnd) { return true; } virtual void BeginAndroidShutdown() {} - -private: - using GraphicsContext::InitFromRenderThread; }; diff --git a/android/jni/AndroidJavaGLContext.cpp b/android/jni/AndroidJavaGLContext.cpp index 69298c7cf0..a23ed5c666 100644 --- a/android/jni/AndroidJavaGLContext.cpp +++ b/android/jni/AndroidJavaGLContext.cpp @@ -10,9 +10,10 @@ AndroidJavaEGLGraphicsContext::AndroidJavaEGLGraphicsContext() { SetGPUBackend(GPUBackend::OPENGL); } -bool AndroidJavaEGLGraphicsContext::InitFromRenderThread(ANativeWindow *wnd) { +bool AndroidJavaEGLGraphicsContext::InitFromRenderThread(std::string *errorMessage) { INFO_LOG(Log::G3D, "AndroidJavaEGLGraphicsContext::InitFromRenderThread"); if (!CheckGLExtensions()) { + *errorMessage = "CheckExtensions failed"; ERROR_LOG(Log::G3D, "CheckGLExtensions failed - not gonna attempt starting up."); return false; } diff --git a/android/jni/AndroidJavaGLContext.h b/android/jni/AndroidJavaGLContext.h index ad0e67eff2..b24ed480e0 100644 --- a/android/jni/AndroidJavaGLContext.h +++ b/android/jni/AndroidJavaGLContext.h @@ -12,8 +12,7 @@ public: bool NeedsRenderThread() const override { return true; } // This performs the actual initialization, - bool InitFromRenderThread(ANativeWindow *wnd) override; - + bool InitFromRenderThread(std::string *errorMessage) override; void ShutdownFromRenderThread() override; void Shutdown() override {} diff --git a/android/jni/AndroidVulkanContext.cpp b/android/jni/AndroidVulkanContext.cpp index 9036cc1a44..957c32d751 100644 --- a/android/jni/AndroidVulkanContext.cpp +++ b/android/jni/AndroidVulkanContext.cpp @@ -55,7 +55,7 @@ bool AndroidVulkanContext::InitAPI() { return true; } -bool AndroidVulkanContext::InitFromRenderThread(ANativeWindow *wnd) { +bool AndroidVulkanContext::Init(ANativeWindow *wnd) { INFO_LOG(Log::G3D, "AndroidVulkanContext::InitFromRenderThread"); if (!g_Vulkan) { ERROR_LOG(Log::G3D, "AndroidVulkanContext::InitFromRenderThread: No Vulkan context"); @@ -101,7 +101,7 @@ bool AndroidVulkanContext::InitFromRenderThread(ANativeWindow *wnd) { } void AndroidVulkanContext::ShutdownFromRenderThread() { - INFO_LOG(Log::G3D, "AndroidVulkanContext::Shutdown"); + INFO_LOG(Log::G3D, "AndroidVulkanContext::ShutdownFromRenderThread"); draw_->HandleEvent(Draw::Event::LOST_BACKBUFFER, g_Vulkan->GetBackbufferWidth(), g_Vulkan->GetBackbufferHeight()); delete draw_; draw_ = nullptr; @@ -113,7 +113,7 @@ void AndroidVulkanContext::ShutdownFromRenderThread() { } void AndroidVulkanContext::Shutdown() { - INFO_LOG(Log::G3D, "Calling NativeShutdownGraphics"); + INFO_LOG(Log::G3D, "AndroidVulkanContext::Shutdown"); g_Vulkan->DestroyDevice(); g_Vulkan->DestroyInstance(); // We keep the g_Vulkan context around to avoid invalidating a ton of pointers around the app. @@ -128,6 +128,9 @@ void AndroidVulkanContext::Resize() { // TODO: We should only destroy the surface here if the window changed. We can track this inside g_Vulkan. g_Vulkan->DestroySurface(); + + // ========================================== + g_Vulkan->ReinitSurface(); VkPresentModeKHR presentMode = ConfigPresentModeToVulkan(draw_); diff --git a/android/jni/AndroidVulkanContext.h b/android/jni/AndroidVulkanContext.h index bcdf070fe2..f83daf9fa7 100644 --- a/android/jni/AndroidVulkanContext.h +++ b/android/jni/AndroidVulkanContext.h @@ -11,7 +11,7 @@ public: bool InitAPI(); - bool InitFromRenderThread(ANativeWindow *wnd) override; + bool Init(ANativeWindow *wnd) override; void ShutdownFromRenderThread() override; // Inverses InitFromRenderThread. void Shutdown() override; diff --git a/android/jni/app-android.cpp b/android/jni/app-android.cpp index 8572866bc2..83489d4fac 100644 --- a/android/jni/app-android.cpp +++ b/android/jni/app-android.cpp @@ -1034,7 +1034,7 @@ extern "C" jboolean Java_org_ppsspp_ppsspp_NativeRenderer_displayInit(JNIEnv * e std::string errorMessage; if (!renderer_inited) { INFO_LOG(Log::G3D, "NativeApp.displayInit() first time"); - if (!graphicsContext->InitFromRenderThread((ANativeWindow *)nullptr)) { + if (!graphicsContext->InitFromRenderThread(&errorMessage)) { System_Toast("Graphics initialization failed. Quitting."); return false; } @@ -1068,8 +1068,9 @@ extern "C" jboolean Java_org_ppsspp_ppsspp_NativeRenderer_displayInit(JNIEnv * e INFO_LOG(Log::G3D, "Shut down both threads. Now let's bring it up again!"); - if (!graphicsContext->InitFromRenderThread((ANativeWindow *)nullptr)) { - System_Toast("Graphics initialization failed. Quitting."); + std::string errorMessage; + if (!graphicsContext->InitFromRenderThread(&errorMessage)) { + System_Toast(("Graphics initialization failed: Quitting: " + errorMessage).c_str()); return false; } @@ -1232,7 +1233,8 @@ extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_sendRequestResult(JNIEn } // This doesn't get called on the Vulkan path. -// We don't need a render thread "loop" as this gets called repeatedly by the system. +// We don't need a render thread "loop" as this gets called repeatedly by the system, by a system +// render thread. extern "C" void Java_org_ppsspp_ppsspp_NativeRenderer_displayRender(JNIEnv *env, jobject obj) { static bool hasSetThreadName = false; if (!hasSetThreadName) { @@ -1240,15 +1242,21 @@ extern "C" void Java_org_ppsspp_ppsspp_NativeRenderer_displayRender(JNIEnv *env, SetCurrentThreadName("AndroidRender"); } - if (IsVREnabled() && !StartVRRender()) + if (IsVREnabled() && !StartVRRender()) { return; + } // This is the "GPU thread". Call ThreadFrame. - if (!graphicsContext || !graphicsContext->ThreadFrame(true)) { + if (!graphicsContext) { return; } _assert_(graphicsContext->NeedsRenderThread()); + if (!graphicsContext->ThreadFrame(true)) { + INFO_LOG(Log::G3D, "ThreadFrame returned false"); + return; + } + if (IsVREnabled()) { UpdateVRInput(g_Config.bHapticFeedback, g_display.dpi_scale_x, g_display.dpi_scale_y); FinishVRRender(); @@ -1769,7 +1777,7 @@ static void VulkanEmuThread(ANativeWindow *wnd) { WARN_LOG(Log::G3D, "runVulkanRenderLoop. display_xres=%d display_yres=%d desiredBackbufferSizeX=%d desiredBackbufferSizeY=%d", display_xres, display_yres, desiredBackbufferSizeX, desiredBackbufferSizeY); - if (!graphicsContext->InitFromRenderThread(wnd)) { + if (!graphicsContext->Init(wnd)) { // On Android, if we get here, really no point in continuing. // The UI is supposed to render on any device both on OpenGL and Vulkan. If either of those don't work // on a device, we blacklist it. Hopefully we should have already failed in InitAPI anyway and reverted to GL back then. From b2c5fdc39f850b01c9a8013756b0a80591c21d0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 22 Jul 2026 09:47:05 +0200 Subject: [PATCH 09/12] Make EmuThreadFunc match Android's EmuThreadFunc --- Core/EmuThread.cpp | 13 ++++++++++++- android/jni/app-android.cpp | 15 ++------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Core/EmuThread.cpp b/Core/EmuThread.cpp index 65cc06153d..4e21db6622 100644 --- a/Core/EmuThread.cpp +++ b/Core/EmuThread.cpp @@ -41,14 +41,22 @@ bool MainThread_Ready() { } static void EmuThreadFunc(GraphicsContext *graphicsContext, std::function postFrame) { + INFO_LOG(Log::G3D, "Entering emu thread"); SetCurrentThreadName("EmuThread"); + AndroidJNIThreadContext context; // 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); + if (!NativeInitGraphics(graphicsContext)) { + _assert_msg_(false, "NativeInitGraphics failed, might as well bail"); + // If this fails, which it normally shouldn't, let's bail. + g_emuThreadState = EmuThreadState::QUIT_REQUESTED; + } else { + INFO_LOG(Log::G3D, "EmuThread: Entering loop"); + } while (g_emuThreadState != EmuThreadState::QUIT_REQUESTED) { // We're here again, so the game quit. Restart Run() which controls the UI. @@ -63,9 +71,12 @@ static void EmuThreadFunc(GraphicsContext *graphicsContext, std::function postFrame) { diff --git a/android/jni/app-android.cpp b/android/jni/app-android.cpp index 83489d4fac..7ae8fc9cef 100644 --- a/android/jni/app-android.cpp +++ b/android/jni/app-android.cpp @@ -272,11 +272,7 @@ static void EmuThreadFunc() { INFO_LOG(Log::System, "Entering emu thread"); - // Wait for render loop to get started. - INFO_LOG(Log::System, "Runloop: Waiting for displayInit..."); - _dbg_assert_(graphicsContext); - - // Check the state of the graphics context before we try to feed it into NativeInitGraphics. + _assert_(graphicsContext); if (!NativeInitGraphics(graphicsContext)) { _assert_msg_(false, "NativeInitGraphics failed, might as well bail"); emuThreadState = (int)EmuThreadState::QUIT_REQUESTED; @@ -1754,14 +1750,7 @@ static void VulkanEmuThread(ANativeWindow *wnd) { AndroidJNIThreadContext ctx; JNIEnv *env = getEnv(); - - if (!graphicsContext) { - ERROR_LOG(Log::G3D, "runVulkanRenderLoop: Tried to enter without a created graphics context."); - renderLoopRunning = false; - exitRenderLoop = false; - ANativeWindow_release(wnd); - return; - } + _assert_(graphicsContext); if (exitRenderLoop) { WARN_LOG(Log::G3D, "runVulkanRenderLoop: ExitRenderLoop requested at start, skipping the whole thing."); From cb1b4067f7b9c372787d05b2232119cbdacce0e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 23 Jul 2026 01:01:03 +0200 Subject: [PATCH 10/12] Small optimizations in app-android.cpp --- UI/EmuScreen.h | 8 ----- android/jni/AndroidVulkanContext.cpp | 9 +++++- android/jni/app-android.cpp | 44 ++++++++++++---------------- 3 files changed, 27 insertions(+), 34 deletions(-) diff --git a/UI/EmuScreen.h b/UI/EmuScreen.h index 82bf1d4eb2..68fb0b13eb 100644 --- a/UI/EmuScreen.h +++ b/UI/EmuScreen.h @@ -143,14 +143,6 @@ private: ImCommand imCmd_{}; // needed to buffer commands in case imgui wasn't created yet. bool imguiInited_ = false; - // For ImGui modifier tracking. It works a bit weirdly and it's easier to track it here - // than to use our global modifier tracking that attaches modifiers to each key event. - bool keyCtrlLeft_ = false; - bool keyCtrlRight_ = false; - bool keyShiftLeft_ = false; - bool keyShiftRight_ = false; - bool keyAltLeft_ = false; - bool keyAltRight_ = false; bool lastImguiEnabled_ = false; diff --git a/android/jni/AndroidVulkanContext.cpp b/android/jni/AndroidVulkanContext.cpp index 957c32d751..2b4763db5a 100644 --- a/android/jni/AndroidVulkanContext.cpp +++ b/android/jni/AndroidVulkanContext.cpp @@ -22,6 +22,7 @@ AndroidVulkanContext::~AndroidVulkanContext() { } bool AndroidVulkanContext::InitAPI() { + _dbg_assert_(!g_Vulkan); INFO_LOG(Log::G3D, "AndroidVulkanContext::Init"); init_glslang(); @@ -117,6 +118,7 @@ void AndroidVulkanContext::Shutdown() { g_Vulkan->DestroyDevice(); g_Vulkan->DestroyInstance(); // We keep the g_Vulkan context around to avoid invalidating a ton of pointers around the app. + // TODO: Actually the above is inaccurate.. finalize_glslang(); INFO_LOG(Log::G3D, "AndroidVulkanContext::Shutdown completed"); } @@ -124,17 +126,22 @@ void AndroidVulkanContext::Shutdown() { void AndroidVulkanContext::Resize() { INFO_LOG(Log::G3D, "AndroidVulkanContext::Resize begin (oldsize: %dx%d)", g_Vulkan->GetBackbufferWidth(), g_Vulkan->GetBackbufferHeight()); draw_->HandleEvent(Draw::Event::LOST_BACKBUFFER, g_Vulkan->GetBackbufferWidth(), g_Vulkan->GetBackbufferHeight()); + + // This stuff does not seem to be needed, as the window doesn't change here. +#if 0 g_Vulkan->DestroySwapchain(); // TODO: We should only destroy the surface here if the window changed. We can track this inside g_Vulkan. + // Actually, the window can't really change, can it? g_Vulkan->DestroySurface(); // ========================================== g_Vulkan->ReinitSurface(); +#endif VkPresentModeKHR presentMode = ConfigPresentModeToVulkan(draw_); - g_Vulkan->InitSwapchain(presentMode); + g_Vulkan->InitSwapchain(presentMode); // This also destroys the old swapchain and makes a nice transition. draw_->HandleEvent(Draw::Event::GOT_BACKBUFFER, g_Vulkan->GetBackbufferWidth(), g_Vulkan->GetBackbufferHeight()); INFO_LOG(Log::G3D, "AndroidVulkanContext::Resize end (final size: %dx%d)", g_Vulkan->GetBackbufferWidth(), g_Vulkan->GetBackbufferHeight()); } diff --git a/android/jni/app-android.cpp b/android/jni/app-android.cpp index 7ae8fc9cef..6d44c16e1e 100644 --- a/android/jni/app-android.cpp +++ b/android/jni/app-android.cpp @@ -130,7 +130,7 @@ struct FrameCommand { }; static std::mutex frameCommandLock; -static std::queue frameCommands; +static std::vector g_frameCommands; static std::string systemName; static std::string langRegion; @@ -286,15 +286,6 @@ static void EmuThreadFunc() { emuThreadState = (int)EmuThreadState::RUNNING; while (emuThreadState != (int)EmuThreadState::QUIT_REQUESTED) { NativeFrame(graphicsContext); - - std::lock_guard guard(frameCommandLock); - if (!ppssppActivity) { - ERROR_LOG(Log::System, "No activity, clearing commands"); - while (!frameCommands.empty()) - frameCommands.pop(); - break; - } - // Still under lock here. ProcessFrameCommands(env); } @@ -328,7 +319,7 @@ static void EmuThreadJoin() { static void PushCommand(std::string_view cmd, std::string_view param) { std::lock_guard guard(frameCommandLock); - frameCommands.emplace(std::string(cmd), std::string(param)); + g_frameCommands.emplace_back(std::string(cmd), std::string(param)); } // Android implementation of callbacks to the Java part of the app @@ -1007,8 +998,7 @@ extern "C" void Java_org_ppsspp_ppsspp_NativeApp_shutdown(JNIEnv *, jclass) { { std::lock_guard guard(frameCommandLock); - while (!frameCommands.empty()) - frameCommands.pop(); + g_frameCommands.clear(); } INFO_LOG(Log::System, "NativeApp.shutdown() -- end"); } @@ -1685,9 +1675,18 @@ extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_pushCameraImageAndroid( // Call this under frameCommandLock. static void ProcessFrameCommands(JNIEnv *env) { - while (!frameCommands.empty()) { - const FrameCommand &frameCmd = frameCommands.front(); - + std::vector frameCommands; + { + std::lock_guard guard(frameCommandLock); + if (!ppssppActivity) { + ERROR_LOG(Log::System, "No activity, clearing commands"); + } else { + frameCommands = std::move(g_frameCommands); + INFO_LOG(Log::System, "Processing %zu frame commands", g_frameCommands.size()); + } + g_frameCommands.clear(); + } + for (const FrameCommand &frameCmd : frameCommands) { DEBUG_LOG(Log::System, "frameCommand '%s' '%s'", frameCmd.command.c_str(), frameCmd.params.c_str()); jstring cmd = env->NewStringUTF(frameCmd.command.c_str()); @@ -1695,8 +1694,6 @@ static void ProcessFrameCommands(JNIEnv *env) { env->CallVoidMethod(ppssppActivity, postCommand, cmd, param); env->DeleteLocalRef(cmd); env->DeleteLocalRef(param); - - frameCommands.pop(); } } @@ -1719,6 +1716,8 @@ extern "C" jboolean JNICALL Java_org_ppsspp_ppsspp_PpssppActivity_runVulkanRende return false; } + _assert_(!exitRenderLoop); + ANativeWindow *wnd = _surf ? ANativeWindow_fromSurface(env, _surf) : nullptr; if (!wnd) { @@ -1789,13 +1788,8 @@ static void VulkanEmuThread(ANativeWindow *wnd) { renderer_inited = true; while (!exitRenderLoop) { - { - NativeFrame(graphicsContext); - } - { - std::lock_guard guard(frameCommandLock); - ProcessFrameCommands(env); - } + NativeFrame(graphicsContext); + ProcessFrameCommands(env); } INFO_LOG(Log::G3D, "Leaving Vulkan main loop."); } else { From 85f87156602a724ad5f9072b630272a2e111652c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 23 Jul 2026 10:14:31 +0200 Subject: [PATCH 11/12] Break the circular dependency caused by putting EmuThread in core using an interface --- CMakeLists.txt | 1 + Common/Common.vcxproj | 1 + Common/Common.vcxproj.filters | 3 +++ Common/System/Application.h | 11 +++++++++++ Common/System/NativeApp.h | 17 ++++++++++++++++- Core/EmuThread.cpp | 28 ++++++++++++++-------------- Core/EmuThread.h | 6 ++++-- Qt/QtMain.cpp | 2 +- SDL/SDLMain.cpp | 5 +++-- UI/NativeApp.cpp | 4 ++-- UWP/PPSSPP_UWPMain.cpp | 2 +- Windows/main.cpp | 3 ++- android/jni/app-android.cpp | 16 ++++++++-------- ios/ViewController.mm | 2 +- ios/ViewControllerMetal.mm | 4 ++-- 15 files changed, 70 insertions(+), 35 deletions(-) create mode 100644 Common/System/Application.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 46bd91e9c1..3251ad12f6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -822,6 +822,7 @@ add_library(Common STATIC Common/Audio/AudioBackend.h Common/System/Display.cpp Common/System/Display.h + Common/System/Application.h Common/System/System.h Common/System/NativeApp.h Common/System/Request.cpp diff --git a/Common/Common.vcxproj b/Common/Common.vcxproj index 41021a051c..6569f886d8 100644 --- a/Common/Common.vcxproj +++ b/Common/Common.vcxproj @@ -536,6 +536,7 @@ + diff --git a/Common/Common.vcxproj.filters b/Common/Common.vcxproj.filters index 048031eb65..3b111d99f1 100644 --- a/Common/Common.vcxproj.filters +++ b/Common/Common.vcxproj.filters @@ -784,6 +784,9 @@ Data\Text + + System + diff --git a/Common/System/Application.h b/Common/System/Application.h new file mode 100644 index 0000000000..75afe29a5f --- /dev/null +++ b/Common/System/Application.h @@ -0,0 +1,11 @@ +#pragma once + +class GraphicsContext; + +class Application { +public: + virtual ~Application() = default; + virtual bool InitGraphics(GraphicsContext *graphicsContext) = 0; + virtual void ShutdownGraphics(GraphicsContext *graphicsContext) = 0; + virtual void Frame(GraphicsContext *graphicsContext) = 0; +}; diff --git a/Common/System/NativeApp.h b/Common/System/NativeApp.h index 513ab571df..6124978766 100644 --- a/Common/System/NativeApp.h +++ b/Common/System/NativeApp.h @@ -4,6 +4,7 @@ #include #include "Common/Common.h" +#include "Common/System/Application.h" // The Native App API. // @@ -91,7 +92,7 @@ void NativeMix(short *audio, int num_samples, int sampleRateHz, void *userdata); // The graphics context should still be active when calling this, as freeing // of graphics resources happens here. // Main thread. -void NativeShutdownGraphics(); +void NativeShutdownGraphics(GraphicsContext *graphicsContext); void NativeShutdown(); void PostLoadConfig(); @@ -112,3 +113,17 @@ bool Native_IsWindowHidden(); bool Native_UpdateScreenScale(int width, int height, float customScale); AudioBackend *System_CreateAudioBackend(); + +class NativeApplication : public Application { +public: + NativeApplication() {} + bool InitGraphics(GraphicsContext *graphicsContext) override { + return NativeInitGraphics(graphicsContext); + } + void ShutdownGraphics(GraphicsContext *graphicsContext) override { + return NativeShutdownGraphics(graphicsContext); + } + void Frame(GraphicsContext *graphicsContext) override { + return NativeFrame(graphicsContext); + } +}; diff --git a/Core/EmuThread.cpp b/Core/EmuThread.cpp index 4e21db6622..6511340810 100644 --- a/Core/EmuThread.cpp +++ b/Core/EmuThread.cpp @@ -4,9 +4,9 @@ #include #include -#include "Common/System/NativeApp.h" #include "Common/System/System.h" #include "Common/System/Request.h" +#include "Common/System/Application.h" #include "Common/Data/Text/I18n.h" #include "Common/Input/InputState.h" #include "Common/Data/Encoding/Utf8.h" @@ -40,7 +40,7 @@ bool MainThread_Ready() { return g_inLoop; } -static void EmuThreadFunc(GraphicsContext *graphicsContext, std::function postFrame) { +static void EmuThreadFunc(GraphicsContext *graphicsContext, Application *application, std::function postFrame) { INFO_LOG(Log::G3D, "Entering emu thread"); SetCurrentThreadName("EmuThread"); @@ -50,7 +50,7 @@ static void EmuThreadFunc(GraphicsContext *graphicsContext, std::functionInitGraphics(graphicsContext)) { _assert_msg_(false, "NativeInitGraphics failed, might as well bail"); // If this fails, which it normally shouldn't, let's bail. g_emuThreadState = EmuThreadState::QUIT_REQUESTED; @@ -61,7 +61,7 @@ static void EmuThreadFunc(GraphicsContext *graphicsContext, std::functionFrame(graphicsContext); if (postFrame) { postFrame(); } @@ -75,13 +75,13 @@ static void EmuThreadFunc(GraphicsContext *graphicsContext, std::functionShutdownGraphics(graphicsContext); INFO_LOG(Log::System, "Leaving emu thread"); } -std::thread EmuThread_Start(GraphicsContext *graphicsContext, std::function postFrame) { +std::thread EmuThread_Start(GraphicsContext *graphicsContext, Application *application, std::function postFrame) { g_emuThreadState = EmuThreadState::START_REQUESTED; - std::thread emuThread = std::thread(&EmuThreadFunc, graphicsContext, postFrame); + std::thread emuThread = std::thread(&EmuThreadFunc, graphicsContext, application, postFrame); return emuThread; } @@ -102,7 +102,7 @@ void EmuThread_Join(GraphicsContext *graphicsContext, std::thread &emuThread) { emuThread = std::thread(); } -void MainThreadFunc(GraphicsContext *graphicsContext, std::function postFrame) { +void MainThreadFunc(GraphicsContext *graphicsContext, Application *application, std::function postFrame) { if (graphicsContext->NeedsRenderThread()) { SetCurrentThreadName("RenderThread"); // This is now the render thread, and will spawn the emu thread below. @@ -114,7 +114,7 @@ void MainThreadFunc(GraphicsContext *graphicsContext, std::function post g_inLoop = true; - std::thread emuThread = EmuThread_Start(graphicsContext, postFrame); + std::thread emuThread = EmuThread_Start(graphicsContext, application, postFrame); graphicsContext->ThreadStart(); // This thread becomes the render thread. @@ -144,8 +144,8 @@ void MainThreadFunc(GraphicsContext *graphicsContext, std::function post std::string error_string; bool success = graphicsContext->InitFromRenderThread(&error_string); - NativeInitGraphics(graphicsContext); - NativeResized(); + application->InitGraphics(graphicsContext); + // NativeResized(); DEBUG_LOG(Log::Boot, "Done."); @@ -155,18 +155,18 @@ void MainThreadFunc(GraphicsContext *graphicsContext, std::function post while (GetUIState() != UISTATE_EXIT) { // We're here again, so the game quit. Restart Run() which controls the UI. // This way they can load a new game. - NativeFrame(graphicsContext); + application->Frame(graphicsContext); postFrame(); } Core_Stop(); // Process the shutdown. Without this, non-GL delays 800ms on shutdown. Core_StateProcessed(); - NativeFrame(graphicsContext); + application->Frame(graphicsContext); g_inLoop = false; - NativeShutdownGraphics(); + application->ShutdownGraphics(graphicsContext); graphicsContext->ThreadEnd(); graphicsContext->ShutdownFromRenderThread(); diff --git a/Core/EmuThread.h b/Core/EmuThread.h index 6a8e2a1f6f..c34a11de23 100644 --- a/Core/EmuThread.h +++ b/Core/EmuThread.h @@ -18,6 +18,8 @@ #pragma once #include +#include "Common/System/Application.h" + // Utilities to manage Emu and Render threads. // TODO: Use across platforms, currently Windows-only. @@ -28,9 +30,9 @@ class GraphicsContext; // Doesn't take ownership of the graphicsContext, you have to delete it. // This should be used by platforms that launch a separate thread and doesn't // need to run a polling loop in it. -void MainThreadFunc(GraphicsContext *graphicsContext, std::function postFrame); +void MainThreadFunc(GraphicsContext * graphicsContext, Application *application, std::function postFrame); // If you're not using MainThreadFunc, you can at least use these to manage a spinning EmuThread (that calls NativeFrame), // whether your graphics context requires multithreading or not. -std::thread EmuThread_Start(GraphicsContext *graphicsContext, std::function postFrame); +std::thread EmuThread_Start(GraphicsContext *graphicsContext, Application *application, std::function postFrame); void EmuThread_Join(GraphicsContext *graphicsContext, std::thread &emuThread); diff --git a/Qt/QtMain.cpp b/Qt/QtMain.cpp index 82ea66e4d3..0fa59d8e75 100644 --- a/Qt/QtMain.cpp +++ b/Qt/QtMain.cpp @@ -676,7 +676,7 @@ void MainUI::EmuThreadFunc() { } emuThreadState = (int)EmuThreadState::STOPPED; - NativeShutdownGraphics(); + NativeShutdownGraphics(graphicsContext); } void MainUI::EmuThreadStart() { diff --git a/SDL/SDLMain.cpp b/SDL/SDLMain.cpp index 29546ea045..6362cca242 100644 --- a/SDL/SDLMain.cpp +++ b/SDL/SDLMain.cpp @@ -2096,7 +2096,8 @@ int main(int argc, char *argv[]) { } EnableFZ(); - std::thread emuThread = EmuThread_Start(graphicsContext, nullptr); + NativeApplication application; + std::thread emuThread = EmuThread_Start(graphicsContext, &application, nullptr); graphicsContext->ThreadStart(); @@ -2196,7 +2197,7 @@ int main(int argc, char *argv[]) { return 1; } - emuThread = EmuThread_Start(graphicsContext, nullptr); + emuThread = EmuThread_Start(graphicsContext, &application, nullptr); graphicsContext->ThreadStart(); } } diff --git a/UI/NativeApp.cpp b/UI/NativeApp.cpp index 0ce72ead88..b81442a543 100644 --- a/UI/NativeApp.cpp +++ b/UI/NativeApp.cpp @@ -953,7 +953,7 @@ bool CreateGlobalPipelines() { return true; } -void NativeShutdownGraphics() { +void NativeShutdownGraphics(GraphicsContext *graphicContext) { INFO_LOG(Log::System, "NativeShutdownGraphics begin"); if (g_screenManager) { @@ -1032,7 +1032,7 @@ void NativeFrame(GraphicsContext *graphicsContext) { // This can only be accessed from Windows currently, and causes linking errors with headless etc. if (g_restartGraphics == 1) { // Used for debugging only. - NativeShutdownGraphics(); + NativeShutdownGraphics(graphicsContext); g_restartGraphics++; return; } diff --git a/UWP/PPSSPP_UWPMain.cpp b/UWP/PPSSPP_UWPMain.cpp index 37efeb8c28..9efbcb0080 100644 --- a/UWP/PPSSPP_UWPMain.cpp +++ b/UWP/PPSSPP_UWPMain.cpp @@ -101,7 +101,7 @@ PPSSPP_UWPMain::~PPSSPP_UWPMain() { g_InputManager.Shutdown(); ctx_->GetDrawContext()->HandleEvent(Draw::Event::LOST_BACKBUFFER, 0, 0, nullptr); - NativeShutdownGraphics(); + NativeShutdownGraphics(ctx_.get()); NativeShutdown(); g_VFS.Clear(); diff --git a/Windows/main.cpp b/Windows/main.cpp index 7bf2712265..21994b52a8 100644 --- a/Windows/main.cpp +++ b/Windows/main.cpp @@ -1261,13 +1261,14 @@ int WINAPI WinMain(HINSTANCE _hInstance, HINSTANCE hPrevInstance, LPSTR szCmdLin System_SetWindowTitle(""); std::thread mainThread = std::thread([]() { + NativeApplication application; std::unique_ptr graphicsContext(CreateGraphicsContextOrExit()); if (!graphicsContext) { // We're screwed. Should never get here. _dbg_assert_(false); return; } - MainThreadFunc(graphicsContext.get(), []() {}); + MainThreadFunc(graphicsContext.get(), &application, []() {}); PostMessage(MainWindow::GetHWND(), MainWindow::WM_USER_UPDATE_UI, 0, 0); }); diff --git a/android/jni/app-android.cpp b/android/jni/app-android.cpp index 6d44c16e1e..512ff66b0b 100644 --- a/android/jni/app-android.cpp +++ b/android/jni/app-android.cpp @@ -203,7 +203,7 @@ int utimensat(int fd, const char *path, const struct timespec times[2]) { } #endif -static void ProcessFrameCommands(JNIEnv *env); +static void ProcessFrameCommands(); JNIEnv* getEnv() { JNIEnv *env; @@ -268,7 +268,6 @@ static void EmuThreadFunc() { SetCurrentThreadName("Entering EmuThread"); AndroidJNIThreadContext jniContext; - JNIEnv *env = getEnv(); // TODO: Make it gettable from AndroidJNIThreadContext INFO_LOG(Log::System, "Entering emu thread"); @@ -286,13 +285,13 @@ static void EmuThreadFunc() { emuThreadState = (int)EmuThreadState::RUNNING; while (emuThreadState != (int)EmuThreadState::QUIT_REQUESTED) { NativeFrame(graphicsContext); - ProcessFrameCommands(env); + ProcessFrameCommands(); } INFO_LOG(Log::System, "emuThreadState was set to QUIT_REQUESTED, left EmuThreadFunc loop. Setting state to STOPPED."); emuThreadState = (int)EmuThreadState::STOPPED; - NativeShutdownGraphics(); + NativeShutdownGraphics(graphicsContext); INFO_LOG(Log::System, "Leaving EmuThread"); } @@ -723,7 +722,7 @@ extern "C" void Java_org_ppsspp_ppsspp_NativeApp_init SetCurrentThreadName("androidInit"); // Makes sure we get early permission grants. - ProcessFrameCommands(env); + ProcessFrameCommands(); EARLY_LOG("NativeApp.init() -- begin"); PROFILE_INIT(); @@ -1674,7 +1673,8 @@ extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_pushCameraImageAndroid( } // Call this under frameCommandLock. -static void ProcessFrameCommands(JNIEnv *env) { +static void ProcessFrameCommands() { + JNIEnv *env = getEnv(); std::vector frameCommands; { std::lock_guard guard(frameCommandLock); @@ -1789,14 +1789,14 @@ static void VulkanEmuThread(ANativeWindow *wnd) { while (!exitRenderLoop) { NativeFrame(graphicsContext); - ProcessFrameCommands(env); + ProcessFrameCommands(); } INFO_LOG(Log::G3D, "Leaving Vulkan main loop."); } else { INFO_LOG(Log::G3D, "Not entering main loop."); } - NativeShutdownGraphics(); + NativeShutdownGraphics(graphicsContext); renderer_inited = false; graphicsContext->ThreadEnd(); diff --git a/ios/ViewController.mm b/ios/ViewController.mm index d5c22cbd39..09bde9e981 100644 --- a/ios/ViewController.mm +++ b/ios/ViewController.mm @@ -134,7 +134,7 @@ void GLRenderLoop(IOSGLESContext *graphicsContext) { } INFO_LOG(Log::System, "Emulation thread shutting down\n"); - NativeShutdownGraphics(); + NativeShutdownGraphics(graphicsContext); // Also ask the main thread to stop, so it doesn't hang waiting for a new frame. INFO_LOG(Log::System, "Emulation thread stopping\n"); diff --git a/ios/ViewControllerMetal.mm b/ios/ViewControllerMetal.mm index 70382a2a62..dfccad3aa2 100644 --- a/ios/ViewControllerMetal.mm +++ b/ios/ViewControllerMetal.mm @@ -117,7 +117,7 @@ void IOSVulkanContext::ShutdownFromRenderThread() { } void IOSVulkanContext::Shutdown() { - INFO_LOG(Log::G3D, "Calling NativeShutdownGraphics"); + INFO_LOG(Log::G3D, "IOSVulkanContext::Shutdown"); g_Vulkan->DestroyDevice(); g_Vulkan->DestroyInstance(); // We keep the g_Vulkan context around to avoid invalidating a ton of pointers around the app. @@ -243,7 +243,7 @@ void VulkanRenderLoop(IOSVulkanContext *graphicsContext, CAMetalLayer *metalLaye INFO_LOG(Log::G3D, "Not entering main loop."); } - NativeShutdownGraphics(); + NativeShutdownGraphics(graphicsContext); graphicsContext->ThreadEnd(); From 6cd5c535d9aa5fa609ab0671cf115e8b7fa37515 Mon Sep 17 00:00:00 2001 From: AgRichard Date: Fri, 24 Jul 2026 14:22:39 +0800 Subject: [PATCH 12/12] Update en_US.ini Add name to translator --- assets/lang/en_US.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/lang/en_US.ini b/assets/lang/en_US.ini index cc20ffebf5..b0a2f513ef 100644 --- a/assets/lang/en_US.ini +++ b/assets/lang/en_US.ini @@ -1149,8 +1149,8 @@ tools = Free tools used: translators1 = MrYadro, mgaver, VIRGINKLM, raven02 translators2 = sum2012, vnctdj, unknownbrackets, ufdada translators3 = The Dax, wuspring, hrydgard, adrian17 -translators4 = papel -translators5 = +translators4 = papel, Richard Ali, Zie Nozie, Eve Ellora +translators5 = Eli Ezekiel translators6 = website = Check out the website: written = Written in C++ for speed and portability