// This is generic code that is included in all Android apps that use the // Native framework by Henrik Rydgard (https://github.com/hrydgard/native). // It calls a set of methods defined in NativeApp.h. These should be implemented // by your game or app. #include #include #include #include #include #include #include #ifndef _MSC_VER #include #include #include #elif !defined(JNIEXPORT) // Just for better highlighting in MSVC if opening this file. // Not having types makes it get confused and say everything is wrong. struct JavaVM; typedef void *jmethodID; typedef void *jfieldID; typedef uint8_t jboolean; typedef int8_t jbyte; typedef int16_t jshort; typedef int32_t jint; typedef int64_t jlong; typedef jint jsize; typedef float jfloat; typedef double jdouble; class _jobject {}; class _jclass : public _jobject {}; typedef _jobject *jobject; typedef _jclass *jclass; typedef jobject jstring; typedef jobject jbyteArray; struct JNIEnv {}; #define JNIEXPORT #define JNICALL // Just a random value to make MSVC highlighting happy. #define JNI_VERSION_1_6 16 #endif #include "Common/Log.h" #include "Common/LogReporting.h" #include "Common/Net/Resolve.h" #include "android/jni/AndroidAudio.h" #include "Common/GPU/OpenGL/GLCommon.h" #include "Common/GPU/OpenGL/GLFeatures.h" #include "Common/System/Display.h" #include "Common/System/NativeApp.h" #include "Common/System/System.h" #include "Common/System/Request.h" #include "Common/Thread/ThreadUtil.h" #include "Common/File/Path.h" #include "Common/File/DirListing.h" #include "Common/File/VFS/VFS.h" #include "Common/File/VFS/DirectoryReader.h" #include "Common/File/VFS/ZipFileReader.h" #include "Common/File/AndroidStorage.h" #include "Common/Input/InputState.h" #include "Common/Input/KeyCodes.h" #include "Common/Profiler/Profiler.h" #include "Common/Math/math_util.h" #include "Common/Data/Text/Parsers.h" #include "Common/VR/PPSSPPVR.h" #include "Common/GPU/Vulkan/VulkanLoader.h" #include "Common/GraphicsContext.h" #include "Common/StringUtils.h" #include "Common/TimeUtil.h" #include "AndroidGraphicsContext.h" #include "AndroidVulkanContext.h" #include "AndroidJavaGLContext.h" #include "Core/Config.h" #include "Core/ConfigValues.h" #include "Core/Loaders.h" #include "Core/FileLoaders/LocalFileLoader.h" #include "Core/System.h" #include "Core/HLE/sceUsbCam.h" #include "Core/HLE/sceUsbGps.h" #include "Common/CPUDetect.h" #include "Common/Log.h" #include "UI/GameInfoCache.h" #include "app-android.h" bool useCPUThread = true; enum class EmuThreadState { DISABLED, START_REQUESTED, RUNNING, QUIT_REQUESTED, STOPPED, }; static std::thread emuThread; static std::atomic emuThreadState((int)EmuThreadState::DISABLED); void UpdateRunLoopAndroid(JNIEnv *env); AndroidAudioState *g_audioState; struct FrameCommand { FrameCommand() {} FrameCommand(std::string cmd, std::string prm) : command(cmd), params(prm) {} std::string command; std::string params; }; static std::mutex frameCommandLock; static std::queue frameCommands; static std::string systemName; static std::string langRegion; static std::string mogaVersion; static std::string boardName; std::string g_externalDir; // Original external dir (root of Android storage). std::string g_extFilesDir; // App private external dir. static std::vector g_additionalStorageDirs; static int optimalFramesPerBuffer = 0; static int optimalSampleRate = 0; static int sampleRate = 0; static int framesPerBuffer = 0; static int androidVersion; static int deviceType; // Should only be used for display detection during startup (for config defaults etc) // This is the ACTUAL display size, not the hardware scaled display size. // Exposed so it can be displayed on the touchscreen test. int display_xres; int display_yres; static int display_dpi_x; static int display_dpi_y; static int backbuffer_format; // Android PixelFormat enum static int desiredBackbufferSizeX; static int desiredBackbufferSizeY; // Cache the class loader so we can use it from native threads. Required for TextAndroid. static JavaVM* gJvm = nullptr; static jobject gClassLoader; static jmethodID gFindClassMethod; static float g_safeInsetLeft = 0.0; static float g_safeInsetRight = 0.0; static float g_safeInsetTop = 0.0; static float g_safeInsetBottom = 0.0; static jmethodID postCommand; static jmethodID getDebugString; static jobject nativeActivity; static std::atomic exitRenderLoop; static std::atomic renderLoopRunning; static bool renderer_inited = false; static std::mutex renderLock; static bool sustainedPerfSupported = false; static std::map permissions; static AndroidGraphicsContext *graphicsContext; #ifndef LOG_APP_NAME #define LOG_APP_NAME "PPSSPP" #endif #define MessageBox(a, b, c, d) __android_log_print(ANDROID_LOG_INFO, APP_NAME, "%s %s", (b), (c)); #if PPSSPP_ARCH(ARMV7) // Old Android workaround extern "C" { int utimensat(int fd, const char *path, const struct timespec times[2]) { return -1; } } #endif void AndroidLogger::Log(const LogMessage &message) { int mode; switch (message.level) { case LogTypes::LWARNING: mode = ANDROID_LOG_WARN; break; case LogTypes::LERROR: mode = ANDROID_LOG_ERROR; break; default: mode = ANDROID_LOG_INFO; break; } // Long log messages need splitting up. // Not sure what the actual limit is (seems to vary), but let's be conservative. const size_t maxLogLength = 512; if (message.msg.length() < maxLogLength) { // Log with simplified headers as Android already provides timestamp etc. __android_log_print(mode, LOG_APP_NAME, "[%s] %s", message.log, message.msg.c_str()); } else { std::string msg = message.msg; // Ideally we should split at line breaks, but it's at least fairly usable anyway. std::string first_part = msg.substr(0, maxLogLength); __android_log_print(mode, LOG_APP_NAME, "[%s] %s", message.log, first_part.c_str()); msg = msg.substr(maxLogLength); while (msg.length() > maxLogLength) { std::string first_part = msg.substr(0, maxLogLength); __android_log_print(mode, LOG_APP_NAME, "%s", first_part.c_str()); msg = msg.substr(maxLogLength); } // Print the final part. __android_log_print(mode, LOG_APP_NAME, "%s", msg.c_str()); } } JNIEnv* getEnv() { JNIEnv *env; int status = gJvm->GetEnv((void**)&env, JNI_VERSION_1_6); _assert_msg_(status >= 0, "'%s': Can only call getEnv if you've attached the thread already!", GetCurrentThreadName()); return env; } jclass findClass(const char* name) { return static_cast(getEnv()->CallObjectMethod(gClassLoader, gFindClassMethod, getEnv()->NewStringUTF(name))); } void Android_AttachThreadToJNI() { JNIEnv *env; int status = gJvm->GetEnv((void **)&env, JNI_VERSION_1_6); if (status < 0) { INFO_LOG(SYSTEM, "Attaching thread '%s' (not already attached) to JNI.", GetCurrentThreadName()); JavaVMAttachArgs args{}; args.version = JNI_VERSION_1_6; args.name = GetCurrentThreadName(); status = gJvm->AttachCurrentThread(&env, &args); if (status < 0) { // bad, but what can we do other than report.. ERROR_LOG_REPORT_ONCE(threadAttachFail, SYSTEM, "Failed to attach thread %s to JNI.", GetCurrentThreadName()); } } else { WARN_LOG(SYSTEM, "Thread %s was already attached to JNI.", GetCurrentThreadName()); } } void Android_DetachThreadFromJNI() { if (gJvm->DetachCurrentThread() == JNI_OK) { INFO_LOG(SYSTEM, "Detached thread from JNI: '%s'", GetCurrentThreadName()); } else { WARN_LOG(SYSTEM, "Failed to detach thread '%s' from JNI - never attached?", GetCurrentThreadName()); } } JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *pjvm, void *reserved) { INFO_LOG(SYSTEM, "JNI_OnLoad"); gJvm = pjvm; // cache the JavaVM pointer auto env = getEnv(); //replace with one of your classes in the line below auto randomClass = env->FindClass("org/ppsspp/ppsspp/NativeActivity"); jclass classClass = env->GetObjectClass(randomClass); auto classLoaderClass = env->FindClass("java/lang/ClassLoader"); auto getClassLoaderMethod = env->GetMethodID(classClass, "getClassLoader", "()Ljava/lang/ClassLoader;"); gClassLoader = env->NewGlobalRef(env->CallObjectMethod(randomClass, getClassLoaderMethod)); gFindClassMethod = env->GetMethodID(classLoaderClass, "findClass", "(Ljava/lang/String;)Ljava/lang/Class;"); RegisterAttachDetach(&Android_AttachThreadToJNI, &Android_DetachThreadFromJNI); return JNI_VERSION_1_6; } // Only used in OpenGL mode. static void EmuThreadFunc() { JNIEnv *env; SetCurrentThreadName("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? JavaVMAttachArgs args{}; args.version = JNI_VERSION_1_6; args.name = "EmuThread"; gJvm->AttachCurrentThread(&env, &args); INFO_LOG(SYSTEM, "Entering emu thread"); // Wait for render loop to get started. INFO_LOG(SYSTEM, "Runloop: Waiting for displayInit..."); while (!graphicsContext || graphicsContext->GetState() == GraphicsContextState::PENDING) { sleep_ms(20); } // Check the state of the graphics context before we try to feed it into NativeInitGraphics. if (graphicsContext->GetState() != GraphicsContextState::INITIALIZED) { ERROR_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; } INFO_LOG(SYSTEM, "Graphics initialized. Entering loop."); // There's no real requirement that NativeInit happen on this thread. // We just call the update/render loop here. emuThreadState = (int)EmuThreadState::RUNNING; while (emuThreadState != (int)EmuThreadState::QUIT_REQUESTED) { UpdateRunLoopAndroid(env); } INFO_LOG(SYSTEM, "QUIT_REQUESTED found, left EmuThreadFunc loop. Setting state to STOPPED."); emuThreadState = (int)EmuThreadState::STOPPED; NativeShutdownGraphics(); // Also ask the main thread to stop, so it doesn't hang waiting for a new frame. graphicsContext->StopThread(); gJvm->DetachCurrentThread(); INFO_LOG(SYSTEM, "Leaving emu thread"); } static void EmuThreadStart() { INFO_LOG(SYSTEM, "EmuThreadStart"); emuThreadState = (int)EmuThreadState::START_REQUESTED; emuThread = std::thread(&EmuThreadFunc); } // Call EmuThreadStop first, then keep running the GPU (or eat commands) // as long as emuThreadState isn't STOPPED and/or there are still things queued up. // Only after that, call EmuThreadJoin. static void EmuThreadStop(const char *caller) { INFO_LOG(SYSTEM, "EmuThreadStop - stopping (%s)...", caller); emuThreadState = (int)EmuThreadState::QUIT_REQUESTED; } static void EmuThreadJoin() { emuThread.join(); emuThread = std::thread(); INFO_LOG(SYSTEM, "EmuThreadJoin - joined"); } static void ProcessFrameCommands(JNIEnv *env); void PushCommand(std::string cmd, std::string param) { std::lock_guard guard(frameCommandLock); frameCommands.push(FrameCommand(cmd, param)); } // Android implementation of callbacks to the Java part of the app void System_Toast(const char *text) { PushCommand("toast", text); } void System_ShowKeyboard() { PushCommand("showKeyboard", ""); } void System_Vibrate(int length_ms) { char temp[32]; sprintf(temp, "%i", length_ms); PushCommand("vibrate", temp); } void System_ShowFileInFolder(const char *path) { // Unsupported } void System_LaunchUrl(LaunchUrlType urlType, const char *url) { switch (urlType) { case LaunchUrlType::BROWSER_URL: PushCommand("launchBrowser", url); break; case LaunchUrlType::MARKET_URL: PushCommand("launchMarket", url); break; case LaunchUrlType::EMAIL_ADDRESS: PushCommand("launchEmail", url); break; } } std::string System_GetProperty(SystemProperty prop) { switch (prop) { case SYSPROP_NAME: return systemName; case SYSPROP_LANGREGION: // "en_US" return langRegion; case SYSPROP_MOGA_VERSION: return mogaVersion; case SYSPROP_BOARDNAME: return boardName; default: return ""; } } std::vector System_GetPropertyStringVec(SystemProperty prop) { switch (prop) { case SYSPROP_ADDITIONAL_STORAGE_DIRS: return g_additionalStorageDirs; case SYSPROP_TEMP_DIRS: default: return std::vector(); } } int System_GetPropertyInt(SystemProperty prop) { switch (prop) { case SYSPROP_SYSTEMVERSION: return androidVersion; case SYSPROP_DEVICE_TYPE: return deviceType; case SYSPROP_DISPLAY_XRES: return display_xres; case SYSPROP_DISPLAY_YRES: return display_yres; case SYSPROP_AUDIO_SAMPLE_RATE: return sampleRate; case SYSPROP_AUDIO_FRAMES_PER_BUFFER: return framesPerBuffer; case SYSPROP_AUDIO_OPTIMAL_SAMPLE_RATE: return optimalSampleRate; case SYSPROP_AUDIO_OPTIMAL_FRAMES_PER_BUFFER: return optimalFramesPerBuffer; default: return -1; } } float System_GetPropertyFloat(SystemProperty prop) { switch (prop) { case SYSPROP_DISPLAY_REFRESH_RATE: return g_display.display_hz; case SYSPROP_DISPLAY_SAFE_INSET_LEFT: return g_safeInsetLeft; case SYSPROP_DISPLAY_SAFE_INSET_RIGHT: return g_safeInsetRight; case SYSPROP_DISPLAY_SAFE_INSET_TOP: return g_safeInsetTop; case SYSPROP_DISPLAY_SAFE_INSET_BOTTOM: return g_safeInsetBottom; default: return -1; } } bool System_GetPropertyBool(SystemProperty prop) { switch (prop) { case SYSPROP_SUPPORTS_PERMISSIONS: if (androidVersion < 23) { // 6.0 Marshmallow introduced run time permissions. return false; } else { // It gets a bit complicated here. If scoped storage enforcement is on, // we also don't need to request permissions. We'll have the access we request // on a per-folder basis. return !System_GetPropertyBool(SYSPROP_ANDROID_SCOPED_STORAGE); } case SYSPROP_SUPPORTS_SUSTAINED_PERF_MODE: return sustainedPerfSupported; // 7.0 introduced sustained performance mode as an optional feature. case SYSPROP_HAS_OPEN_DIRECTORY: return false; case SYSPROP_HAS_ADDITIONAL_STORAGE: return !g_additionalStorageDirs.empty(); case SYSPROP_HAS_BACK_BUTTON: return true; case SYSPROP_HAS_IMAGE_BROWSER: return true; case SYSPROP_HAS_FILE_BROWSER: // It's only really needed with scoped storage, but why not make it available // as far back as possible - works just fine. return (androidVersion >= 19) && (deviceType != DEVICE_TYPE_VR); // when ACTION_OPEN_DOCUMENT was added case SYSPROP_HAS_FOLDER_BROWSER: // Uses OPEN_DOCUMENT_TREE to let you select a folder. // Doesn't actually mean it's usable though, in many early versions of Android // this dialog is complete garbage and only lets you select subfolders of the Downloads folder. return androidVersion >= 21; // when ACTION_OPEN_DOCUMENT_TREE was added case SYSPROP_SUPPORTS_OPEN_FILE_IN_EDITOR: return false; // Update if we add support in FileUtil.cpp: OpenFileInEditor case SYSPROP_APP_GOLD: #ifdef GOLD return true; #else return false; #endif case SYSPROP_CAN_JIT: return true; case SYSPROP_ANDROID_SCOPED_STORAGE: // We turn this on for Android 30+ (11) now that when we target Android 11+. // Along with adding: // android:preserveLegacyExternalStorage="true" // To the already requested: // android:requestLegacyExternalStorage="true" // // This will cause Android 11+ to still behave like Android 10 until the app // is manually uninstalled. We can detect this state with // Android_IsExternalStoragePreservedLegacy(), but most of the app will just see // that scoped storage enforcement is disabled in this case. if (androidVersion >= 30) { // Here we do a check to see if we ended up in the preserveLegacyExternalStorage path. // That won't last if the user uninstalls/reinstalls though, but would preserve the user // experience for simple upgrades so maybe let's support it. return !Android_IsExternalStoragePreservedLegacy(); } else { return false; } case SYSPROP_HAS_KEYBOARD: return true; default: return false; } } std::string Android_GetInputDeviceDebugString() { if (!nativeActivity) { return "(N/A)"; } auto env = getEnv(); jstring jparam = env->NewStringUTF("InputDevice"); jstring jstr = (jstring)env->CallObjectMethod(nativeActivity, getDebugString, jparam); if (!jstr) { env->DeleteLocalRef(jparam); return "(N/A)"; } const char *charArray = env->GetStringUTFChars(jstr, 0); std::string retVal = charArray; env->ReleaseStringUTFChars(jstr, charArray); env->DeleteLocalRef(jstr); env->DeleteLocalRef(jparam); return retVal; } std::string GetJavaString(JNIEnv *env, jstring jstr) { if (!jstr) return ""; const char *str = env->GetStringUTFChars(jstr, 0); std::string cpp_string = std::string(str); env->ReleaseStringUTFChars(jstr, str); return cpp_string; } extern "C" void Java_org_ppsspp_ppsspp_NativeActivity_registerCallbacks(JNIEnv *env, jobject obj) { nativeActivity = env->NewGlobalRef(obj); postCommand = env->GetMethodID(env->GetObjectClass(obj), "postCommand", "(Ljava/lang/String;Ljava/lang/String;)V"); getDebugString = env->GetMethodID(env->GetObjectClass(obj), "getDebugString", "(Ljava/lang/String;)Ljava/lang/String;"); _dbg_assert_(postCommand); _dbg_assert_(getDebugString); Android_RegisterStorageCallbacks(env, obj); Android_StorageSetNativeActivity(nativeActivity); } extern "C" void Java_org_ppsspp_ppsspp_NativeActivity_unregisterCallbacks(JNIEnv *env, jobject obj) { Android_StorageSetNativeActivity(nullptr); env->DeleteGlobalRef(nativeActivity); nativeActivity = nullptr; } // This is now only used as a trigger for GetAppInfo as a function to all before Init. // On Android we don't use any of the values it returns. extern "C" jboolean Java_org_ppsspp_ppsspp_NativeApp_isLandscape(JNIEnv *env, jclass) { std::string app_name, app_nice_name, version; bool landscape; NativeGetAppInfo(&app_name, &app_nice_name, &landscape, &version); return landscape; } // Allow the app to intercept the back button. extern "C" jboolean Java_org_ppsspp_ppsspp_NativeApp_isAtTopLevel(JNIEnv *env, jclass) { return NativeIsAtTopLevel(); } extern "C" void Java_org_ppsspp_ppsspp_NativeApp_audioConfig (JNIEnv *env, jclass, jint optimalFPB, jint optimalSR) { optimalFramesPerBuffer = optimalFPB; optimalSampleRate = optimalSR; } extern "C" jstring Java_org_ppsspp_ppsspp_NativeApp_queryConfig (JNIEnv *env, jclass, jstring jquery) { std::string query = GetJavaString(env, jquery); std::string result = NativeQueryConfig(query); jstring jresult = env->NewStringUTF(result.c_str()); return jresult; } static void parse_args(std::vector &args, const std::string value) { // Simple argument parser so we can take args from extra params. const char *p = value.c_str(); while (*p != '\0') { while (isspace(*p)) { p++; } if (*p == '\0') { break; } bool done = false; bool quote = false; std::string arg; while (!done) { size_t sz = strcspn(p, "\"\\ \r\n\t"); arg += std::string(p, sz); p += sz; switch (*p) { case '"': quote = !quote; p++; break; case '\\': p++; arg += std::string(p, 1); p++; break; case '\0': done = true; break; default: // If it's not the above, it's whitespace. if (!quote) { done = true; } else { sz = strspn(p, " \r\n\t"); arg += std::string(p, sz); p += sz; } break; } } args.push_back(arg); while (isspace(*p)) { p++; } } } // Need to use raw Android logging before NativeInit. #define EARLY_LOG(...) __android_log_print(ANDROID_LOG_INFO, "PPSSPP", __VA_ARGS__) extern "C" void Java_org_ppsspp_ppsspp_NativeApp_init (JNIEnv * env, jclass, jstring jmodel, jint jdeviceType, jstring jlangRegion, jstring japkpath, jstring jdataDir, jstring jexternalStorageDir, jstring jexternalFilesDir, jstring jadditionalStorageDirs, jstring jcacheDir, jstring jshortcutParam, jint jAndroidVersion, jstring jboard) { SetCurrentThreadName("androidInit"); // Makes sure we get early permission grants. ProcessFrameCommands(env); EARLY_LOG("NativeApp.init() -- begin"); PROFILE_INIT(); std::lock_guard guard(renderLock); // Note: This is held for the rest of this function - intended? renderer_inited = false; androidVersion = jAndroidVersion; deviceType = jdeviceType; Path apkPath(GetJavaString(env, japkpath)); g_VFS.Register("", ZipFileReader::Create(apkPath, "assets/")); systemName = GetJavaString(env, jmodel); langRegion = GetJavaString(env, jlangRegion); EARLY_LOG("NativeApp.init(): device name: '%s'", systemName.c_str()); std::string externalStorageDir = GetJavaString(env, jexternalStorageDir); std::string additionalStorageDirsString = GetJavaString(env, jadditionalStorageDirs); std::string externalFilesDir = GetJavaString(env, jexternalFilesDir); g_externalDir = externalStorageDir; g_extFilesDir = externalFilesDir; if (!additionalStorageDirsString.empty()) { SplitString(additionalStorageDirsString, ':', g_additionalStorageDirs); for (auto &str : g_additionalStorageDirs) { EARLY_LOG("Additional storage: %s", str.c_str()); } } std::string user_data_path = GetJavaString(env, jdataDir); if (user_data_path.size() > 0) user_data_path += "/"; std::string shortcut_param = GetJavaString(env, jshortcutParam); std::string cacheDir = GetJavaString(env, jcacheDir); std::string buildBoard = GetJavaString(env, jboard); boardName = buildBoard; EARLY_LOG("NativeApp.init(): External storage path: %s", externalStorageDir.c_str()); EARLY_LOG("NativeApp.init(): Launch shortcut parameter: %s", shortcut_param.c_str()); std::string app_name; std::string app_nice_name; std::string version; bool landscape; // Unfortunately, on the Samsung Galaxy S7, this isn't in /proc/cpuinfo. // We also can't read it from __system_property_get. if (buildBoard == "universal8890") { cpu_info.sQuirks.bExynos8890DifferingCachelineSizes = true; } NativeGetAppInfo(&app_name, &app_nice_name, &landscape, &version); // If shortcut_param is not empty, pass it as additional arguments to the NativeInit() method. // NativeInit() is expected to treat extra argument as boot_filename, which in turn will start game immediately. // NOTE: Will only work if ppsspp started from Activity.onCreate(). Won't work if ppsspp app start from onResume(). std::vector args; std::vector temp; args.push_back(app_name.c_str()); if (!shortcut_param.empty()) { parse_args(temp, shortcut_param); for (const auto &arg : temp) { args.push_back(arg.c_str()); } } NativeInit((int)args.size(), &args[0], user_data_path.c_str(), externalStorageDir.c_str(), cacheDir.c_str()); // In debug mode, don't allow creating software Vulkan devices (reject by VulkaMaybeAvailable). // Needed for #16931. #ifdef NDEBUG if (!VulkanMayBeAvailable()) { // If VulkanLoader decided on no viable backend, let's force Vulkan off in release builds at least. g_Config.iGPUBackend = 0; } #endif // No need to use EARLY_LOG anymore. retry: switch (g_Config.iGPUBackend) { case (int)GPUBackend::OPENGL: useCPUThread = true; INFO_LOG(SYSTEM, "NativeApp.init() -- creating OpenGL context (JavaGL)"); graphicsContext = new AndroidJavaEGLGraphicsContext(); INFO_LOG(SYSTEM, "NativeApp.init() - launching emu thread"); EmuThreadStart(); break; case (int)GPUBackend::VULKAN: { INFO_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(); if (!ctx->InitAPI()) { INFO_LOG(SYSTEM, "Failed to initialize Vulkan, switching to OpenGL"); g_Config.iGPUBackend = (int)GPUBackend::OPENGL; SetGPUBackend(GPUBackend::OPENGL); goto retry; } else { graphicsContext = ctx; } break; } default: ERROR_LOG(SYSTEM, "NativeApp.init(): iGPUBackend %d not supported. Switching to OpenGL.", (int)g_Config.iGPUBackend); g_Config.iGPUBackend = (int)GPUBackend::OPENGL; goto retry; } if (IsVREnabled()) { Version gitVer(PPSSPP_GIT_VERSION); InitVROnAndroid(gJvm, nativeActivity, systemName.c_str(), gitVer.ToInteger(), "PPSSPP"); SetVRCallbacks(NativeAxis, NativeKey, NativeTouch); } } extern "C" void Java_org_ppsspp_ppsspp_NativeApp_audioInit(JNIEnv *, jclass) { sampleRate = optimalSampleRate; if (optimalSampleRate == 0) { sampleRate = 44100; } if (optimalFramesPerBuffer > 0) { framesPerBuffer = optimalFramesPerBuffer; } else { framesPerBuffer = 512; } // Some devices have totally bonkers buffer sizes like 8192. They will have terrible latency anyway, so to avoid having to // create extra smart buffering code, we'll just let their regular mixer deal with it, missing the fast path (as if they had one...) if (framesPerBuffer > 512) { framesPerBuffer = 512; sampleRate = 44100; } INFO_LOG(AUDIO, "NativeApp.audioInit() -- Using OpenSL audio! frames/buffer: %i optimal sr: %i actual sr: %i", optimalFramesPerBuffer, optimalSampleRate, sampleRate); if (!g_audioState) { g_audioState = AndroidAudio_Init(&NativeMix, framesPerBuffer, sampleRate); } else { ERROR_LOG(AUDIO, "Audio state already initialized"); } } extern "C" void Java_org_ppsspp_ppsspp_NativeApp_audioShutdown(JNIEnv *, jclass) { if (g_audioState) { AndroidAudio_Shutdown(g_audioState); g_audioState = nullptr; } else { ERROR_LOG(AUDIO, "Audio state already shutdown!"); } } extern "C" void Java_org_ppsspp_ppsspp_NativeApp_audioRecording_1SetSampleRate(JNIEnv *, jclass, jint sampleRate) { AndroidAudio_Recording_SetSampleRate(g_audioState, sampleRate); } extern "C" void Java_org_ppsspp_ppsspp_NativeApp_audioRecording_1Start(JNIEnv *, jclass) { AndroidAudio_Recording_Start(g_audioState); } extern "C" void Java_org_ppsspp_ppsspp_NativeApp_audioRecording_1Stop(JNIEnv *, jclass) { AndroidAudio_Recording_Stop(g_audioState); } bool System_AudioRecordingIsAvailable() { return true; } bool System_AudioRecordingState() { return AndroidAudio_Recording_State(g_audioState); } extern "C" void Java_org_ppsspp_ppsspp_NativeApp_resume(JNIEnv *, jclass) { INFO_LOG(SYSTEM, "NativeApp.resume() - resuming audio"); AndroidAudio_Resume(g_audioState); NativeMessageReceived("app_resumed", ""); } extern "C" void Java_org_ppsspp_ppsspp_NativeApp_pause(JNIEnv *, jclass) { INFO_LOG(SYSTEM, "NativeApp.pause() - pausing audio"); AndroidAudio_Pause(g_audioState); } extern "C" void Java_org_ppsspp_ppsspp_NativeApp_shutdown(JNIEnv *, jclass) { if (renderer_inited && useCPUThread && graphicsContext) { // Only used in Java EGL path. EmuThreadStop("shutdown"); INFO_LOG(SYSTEM, "BeginAndroidShutdown"); graphicsContext->BeginAndroidShutdown(); // Now, it could be that we had some frames queued up. Get through them. // We're on the render thread, so this is synchronous. do { INFO_LOG(SYSTEM, "Executing graphicsContext->ThreadFrame to clear buffers"); } while (graphicsContext->ThreadFrame()); graphicsContext->ThreadEnd(); INFO_LOG(SYSTEM, "ThreadEnd called."); graphicsContext->ShutdownFromRenderThread(); INFO_LOG(SYSTEM, "Graphics context now shut down from NativeApp_shutdown"); INFO_LOG(SYSTEM, "Joining emuthread"); EmuThreadJoin(); } INFO_LOG(SYSTEM, "NativeApp.shutdown() -- begin"); if (renderer_inited) { INFO_LOG(G3D, "Shutting down renderer"); graphicsContext->Shutdown(); delete graphicsContext; graphicsContext = nullptr; renderer_inited = false; } else { INFO_LOG(G3D, "Not shutting down renderer - not initialized"); } { std::lock_guard guard(renderLock); NativeShutdown(); g_VFS.Clear(); } { std::lock_guard guard(frameCommandLock); while (frameCommands.size()) frameCommands.pop(); } INFO_LOG(SYSTEM, "NativeApp.shutdown() -- end"); } // JavaEGL. This doesn't get called on the Vulkan path. // This gets called from onSurfaceCreated. extern "C" bool Java_org_ppsspp_ppsspp_NativeRenderer_displayInit(JNIEnv * env, jobject obj) { _assert_(useCPUThread); INFO_LOG(G3D, "NativeApp.displayInit()"); bool firstStart = !renderer_inited; // We should be running on the render thread here. std::string errorMessage; if (renderer_inited) { // 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. INFO_LOG(G3D, "NativeApp.displayInit() restoring"); EmuThreadStop("displayInit"); graphicsContext->BeginAndroidShutdown(); INFO_LOG(G3D, "BeginAndroidShutdown. Looping until emu thread done..."); // Skipping GL calls here because the old context is lost. while (graphicsContext->ThreadFrame()) { continue; } INFO_LOG(G3D, "Joining emu thread"); EmuThreadJoin(); graphicsContext->ThreadEnd(); graphicsContext->ShutdownFromRenderThread(); INFO_LOG(G3D, "Shut down both threads. Now let's bring it up again!"); 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) { System_NotifyUserMessage(details, 5.0, 0xFFFFFFFF, "error_callback"); }, nullptr); EmuThreadStart(); graphicsContext->ThreadStart(); INFO_LOG(G3D, "Restored."); } else { INFO_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) { System_NotifyUserMessage(details, 5.0, 0xFFFFFFFF, "error_callback"); }, nullptr); graphicsContext->ThreadStart(); renderer_inited = true; } NativeMessageReceived("recreateviews", ""); if (IsVREnabled()) { EnterVR(firstStart, graphicsContext->GetAPIContext()); } return true; } static void recalculateDpi() { g_display.dpi = display_dpi_x; g_display.dpi_scale_x = 240.0f / display_dpi_x; g_display.dpi_scale_y = 240.0f / display_dpi_y; g_display.dpi_scale_real_x = g_display.dpi_scale_x; g_display.dpi_scale_real_y = g_display.dpi_scale_y; g_display.dp_xres = display_xres * g_display.dpi_scale_x; g_display.dp_yres = display_yres * g_display.dpi_scale_y; g_display.pixel_in_dps_x = (float)g_display.pixel_xres / g_display.dp_xres; g_display.pixel_in_dps_y = (float)g_display.pixel_yres / g_display.dp_yres; INFO_LOG(G3D, "RecalcDPI: display_xres=%d display_yres=%d pixel_xres=%d pixel_yres=%d", display_xres, display_yres, g_display.pixel_xres, g_display.pixel_yres); INFO_LOG(G3D, "RecalcDPI: g_dpi=%f g_dpi_scale_x=%f g_dpi_scale_y=%f dp_xres=%d dp_yres=%d", g_display.dpi, g_display.dpi_scale_x, g_display.dpi_scale_y, g_display.dp_xres, g_display.dp_yres); } extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_backbufferResize(JNIEnv *, jclass, jint bufw, jint bufh, jint format) { INFO_LOG(SYSTEM, "NativeApp.backbufferResize(%d x %d)", bufw, bufh); bool new_size = g_display.pixel_xres != bufw || g_display.pixel_yres != bufh; int old_w = g_display.pixel_xres; int old_h = g_display.pixel_yres; // pixel_*res is the backbuffer resolution. g_display.pixel_xres = bufw; g_display.pixel_yres = bufh; backbuffer_format = format; if (IsVREnabled()) { GetVRResolutionPerEye(&g_display.pixel_xres, &g_display.pixel_yres); } recalculateDpi(); if (new_size) { INFO_LOG(G3D, "Size change detected (previously %d,%d) - calling NativeResized()", old_w, old_h); NativeResized(); } else { INFO_LOG(G3D, "NativeApp::backbufferResize: Size didn't change."); } } void System_Notify(SystemNotification notification) { switch (notification) { case SystemNotification::ROTATE_UPDATED: PushCommand("rotate", ""); break; case SystemNotification::FORCE_RECREATE_ACTIVITY: PushCommand("recreate", ""); break; case SystemNotification::IMMERSIVE_MODE_CHANGE: PushCommand("immersive", ""); break; case SystemNotification::SUSTAINED_PERF_CHANGE: PushCommand("sustainedPerfMode", ""); break; } } bool System_MakeRequest(SystemRequestType type, int requestId, const std::string ¶m1, const std::string ¶m2, int param3) { switch (type) { case SystemRequestType::EXIT_APP: PushCommand("finish", ""); return true; case SystemRequestType::RESTART_APP: PushCommand("graphics_restart", param1); return true; case SystemRequestType::INPUT_TEXT_MODAL: { std::string serialized = StringFromFormat("%d:@:%s:@:%s", requestId, param1.c_str(), param2.c_str()); PushCommand("inputbox", serialized.c_str()); return true; } case SystemRequestType::BROWSE_FOR_IMAGE: PushCommand("browse_image", StringFromFormat("%d", requestId)); return true; case SystemRequestType::BROWSE_FOR_FILE: PushCommand("browse_file", StringFromFormat("%d", requestId)); return true; case SystemRequestType::BROWSE_FOR_FOLDER: PushCommand("browse_folder", StringFromFormat("%d", requestId)); return true; case SystemRequestType::CAMERA_COMMAND: PushCommand("camera_command", param1); return true; case SystemRequestType::GPS_COMMAND: PushCommand("gps_command", param1); return true; case SystemRequestType::MICROPHONE_COMMAND: PushCommand("microphone_command", param1); return true; case SystemRequestType::SHARE_TEXT: PushCommand("share_text", param1); return true; case SystemRequestType::NOTIFY_UI_STATE: PushCommand("uistate", param1); return true; default: return false; } } extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_sendRequestResult(JNIEnv *env, jclass, jint jrequestID, jboolean result, jstring jvalue, jint jintValue) { std::string value = GetJavaString(env, jvalue); static jint lastSeqID = -1; if (lastSeqID == jrequestID) { // We send this on dismiss, so twice in many cases. WARN_LOG(SYSTEM, "Ignoring duplicate sendInputBox"); return; } lastSeqID = jrequestID; if (result) { g_requestManager.PostSystemSuccess(jrequestID, value.c_str()); } else { g_requestManager.PostSystemFailure(jrequestID); } } void LockedNativeUpdateRender() { std::lock_guard renderGuard(renderLock); NativeUpdate(); NativeRender(graphicsContext); } void UpdateRunLoopAndroid(JNIEnv *env) { LockedNativeUpdateRender(); std::lock_guard guard(frameCommandLock); if (!nativeActivity) { ERROR_LOG(SYSTEM, "No activity, clearing commands"); while (!frameCommands.empty()) frameCommands.pop(); return; } // Still under lock here. ProcessFrameCommands(env); } extern "C" void Java_org_ppsspp_ppsspp_NativeRenderer_displayRender(JNIEnv *env, jobject obj) { static bool hasSetThreadName = false; if (!hasSetThreadName) { hasSetThreadName = true; SetCurrentThreadName("AndroidRender"); } if (IsVREnabled() && !StartVRRender()) return; if (useCPUThread) { // This is the "GPU thread". Call ThreadFrame. if (!graphicsContext || !graphicsContext->ThreadFrame()) { return; } } else { UpdateRunLoopAndroid(env); } if (IsVREnabled()) { UpdateVRInput(g_Config.bHapticFeedback, g_display.dpi_scale_x, g_display.dpi_scale_y); FinishVRRender(); } } void System_AskForPermission(SystemPermission permission) { switch (permission) { case SYSTEM_PERMISSION_STORAGE: PushCommand("ask_permission", "storage"); break; } } PermissionStatus System_GetPermissionStatus(SystemPermission permission) { if (androidVersion < 23) { return PERMISSION_STATUS_GRANTED; } else { return permissions[permission]; } } extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_touch (JNIEnv *, jclass, float x, float y, int code, int pointerId) { float scaledX = x * g_display.dpi_scale_x; float scaledY = y * g_display.dpi_scale_y; TouchInput touch; touch.id = pointerId; touch.x = scaledX; touch.y = scaledY; touch.flags = code; NativeTouch(touch); } extern "C" jboolean Java_org_ppsspp_ppsspp_NativeApp_keyDown(JNIEnv *, jclass, jint deviceId, jint key, jboolean isRepeat) { KeyInput keyInput; keyInput.deviceId = deviceId; keyInput.keyCode = key; keyInput.flags = KEY_DOWN; if (isRepeat) { keyInput.flags |= KEY_IS_REPEAT; } return NativeKey(keyInput); } extern "C" jboolean Java_org_ppsspp_ppsspp_NativeApp_keyUp(JNIEnv *, jclass, jint deviceId, jint key) { KeyInput keyInput; keyInput.deviceId = deviceId; keyInput.keyCode = key; keyInput.flags = KEY_UP; return NativeKey(keyInput); } extern "C" void Java_org_ppsspp_ppsspp_NativeApp_joystickAxis( JNIEnv *env, jclass, jint deviceId, jint axisId, jfloat value) { if (!renderer_inited) return; AxisInput axis; axis.axisId = axisId; axis.deviceId = deviceId; axis.value = value; NativeAxis(axis); } extern "C" jboolean Java_org_ppsspp_ppsspp_NativeApp_mouseWheelEvent( JNIEnv *env, jclass, jint stick, jfloat x, jfloat y) { if (!renderer_inited) return false; // TODO: Support mousewheel for android return true; } extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_accelerometer(JNIEnv *, jclass, float x, float y, float z) { if (!renderer_inited) return; AxisInput axis; axis.deviceId = DEVICE_ID_ACCELEROMETER; axis.axisId = JOYSTICK_AXIS_ACCELEROMETER_X; axis.value = x; NativeAxis(axis); axis.axisId = JOYSTICK_AXIS_ACCELEROMETER_Y; axis.value = y; NativeAxis(axis); axis.axisId = JOYSTICK_AXIS_ACCELEROMETER_Z; axis.value = z; NativeAxis(axis); } extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_sendMessage(JNIEnv *env, jclass, jstring message, jstring param) { std::string msg = GetJavaString(env, message); std::string prm = GetJavaString(env, param); // Some messages are caught by app-android. TODO: Should be all. if (msg == "moga") { mogaVersion = prm; } else if (msg == "permission_pending") { INFO_LOG(SYSTEM, "STORAGE PERMISSION: PENDING"); // TODO: Add support for other permissions permissions[SYSTEM_PERMISSION_STORAGE] = PERMISSION_STATUS_PENDING; } else if (msg == "permission_denied") { INFO_LOG(SYSTEM, "STORAGE PERMISSION: DENIED"); permissions[SYSTEM_PERMISSION_STORAGE] = PERMISSION_STATUS_DENIED; } else if (msg == "permission_granted") { INFO_LOG(SYSTEM, "STORAGE PERMISSION: GRANTED"); permissions[SYSTEM_PERMISSION_STORAGE] = PERMISSION_STATUS_GRANTED; } else if (msg == "sustained_perf_supported") { sustainedPerfSupported = true; } else if (msg == "safe_insets") { // INFO_LOG(SYSTEM, "Got insets: %s", prm.c_str()); // We don't bother with supporting exact rectangular regions. Safe insets are good enough. int left, right, top, bottom; if (4 == sscanf(prm.c_str(), "%d:%d:%d:%d", &left, &right, &top, &bottom)) { g_safeInsetLeft = (float)left * g_display.dpi_scale_x; g_safeInsetRight = (float)right * g_display.dpi_scale_x; g_safeInsetTop = (float)top * g_display.dpi_scale_y; g_safeInsetBottom = (float)bottom * g_display.dpi_scale_y; } } // Ensures that the receiver can handle it on a sensible thread. NativeMessageReceived(msg.c_str(), prm.c_str()); } extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeActivity_requestExitVulkanRenderLoop(JNIEnv *env, jobject obj) { if (!renderLoopRunning) { ERROR_LOG(SYSTEM, "Render loop already exited"); return; } exitRenderLoop = true; while (renderLoopRunning) { sleep_ms(10); } } extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_setDisplayParameters(JNIEnv *, jclass, jint xres, jint yres, jint dpi, jfloat refreshRate) { INFO_LOG(G3D, "NativeApp.setDisplayParameters(%d x %d, dpi=%d, refresh=%0.2f)", xres, yres, dpi, refreshRate); if (IsVREnabled()) { int width, height; GetVRResolutionPerEye(&width, &height); xres = width; yres = height * 272 / 480; dpi = 320; } bool changed = false; changed = changed || display_xres != xres || display_yres != yres; changed = changed || display_dpi_x != dpi || display_dpi_y != dpi; changed = changed || g_display.display_hz != refreshRate; if (changed) { display_xres = xres; display_yres = yres; display_dpi_x = dpi; display_dpi_y = dpi; g_display.display_hz = refreshRate; recalculateDpi(); NativeResized(); } } std::vector System_GetCameraDeviceList() { jclass cameraClass = findClass("org/ppsspp/ppsspp/CameraHelper"); jmethodID deviceListMethod = getEnv()->GetStaticMethodID(cameraClass, "getDeviceList", "()Ljava/util/ArrayList;"); jobject deviceListObject = getEnv()->CallStaticObjectMethod(cameraClass, deviceListMethod); jclass arrayListClass = getEnv()->FindClass("java/util/ArrayList"); jmethodID arrayListSize = getEnv()->GetMethodID(arrayListClass, "size", "()I"); jmethodID arrayListGet = getEnv()->GetMethodID(arrayListClass, "get", "(I)Ljava/lang/Object;"); jint arrayListObjectLen = getEnv()->CallIntMethod(deviceListObject, arrayListSize); std::vector deviceListVector; for (int i = 0; i < arrayListObjectLen; i++) { jstring dev = static_cast(getEnv()->CallObjectMethod(deviceListObject, arrayListGet, i)); const char* cdev = getEnv()->GetStringUTFChars(dev, nullptr); deviceListVector.push_back(cdev); getEnv()->ReleaseStringUTFChars(dev, cdev); getEnv()->DeleteLocalRef(dev); } return deviceListVector; } extern "C" jint Java_org_ppsspp_ppsspp_NativeApp_getSelectedCamera(JNIEnv *, jclass) { int cameraId = 0; sscanf(g_Config.sCameraDevice.c_str(), "%d:", &cameraId); return cameraId; } extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_setGpsDataAndroid(JNIEnv *, jclass, jlong time, jfloat hdop, jfloat latitude, jfloat longitude, jfloat altitude, jfloat speed, jfloat bearing) { GPS::setGpsData(time, hdop, latitude, longitude, altitude, speed, bearing); } extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_setSatInfoAndroid(JNIEnv *, jclass, jshort index, jshort id, jshort elevation, jshort azimuth, jshort snr, jshort good) { GPS::setSatInfo(index, id, elevation, azimuth, snr, good); } extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_pushCameraImageAndroid(JNIEnv *env, jclass, jbyteArray image) { if (image != NULL) { jlong size = env->GetArrayLength(image); jbyte* buffer = env->GetByteArrayElements(image, NULL); Camera::pushCameraImage(size, (unsigned char *)buffer); env->ReleaseByteArrayElements(image, buffer, JNI_ABORT); } } // Call this under frameCommandLock. static void ProcessFrameCommands(JNIEnv *env) { while (!frameCommands.empty()) { FrameCommand frameCmd; frameCmd = frameCommands.front(); frameCommands.pop(); INFO_LOG(SYSTEM, "frameCommand '%s' '%s'", frameCmd.command.c_str(), frameCmd.params.c_str()); jstring cmd = env->NewStringUTF(frameCmd.command.c_str()); jstring param = env->NewStringUTF(frameCmd.params.c_str()); env->CallVoidMethod(nativeActivity, postCommand, cmd, param); env->DeleteLocalRef(cmd); env->DeleteLocalRef(param); } } // This runs in Vulkan mode only. extern "C" bool JNICALL Java_org_ppsspp_ppsspp_NativeActivity_runVulkanRenderLoop(JNIEnv *env, jobject obj, jobject _surf) { _assert_(!useCPUThread); if (!graphicsContext) { ERROR_LOG(G3D, "runVulkanRenderLoop: Tried to enter without a created graphics context."); return false; } exitRenderLoop = false; // This is up here to prevent race conditions, in case we pause during init. renderLoopRunning = true; ANativeWindow *wnd = _surf ? ANativeWindow_fromSurface(env, _surf) : nullptr; WARN_LOG(G3D, "runVulkanRenderLoop. display_xres=%d display_yres=%d desiredBackbufferSizeX=%d desiredBackbufferSizeY=%d", display_xres, display_yres, desiredBackbufferSizeX, desiredBackbufferSizeY); if (!wnd) { // This shouldn't ever happen. ERROR_LOG(G3D, "Error: Surface is null."); renderLoopRunning = false; return false; } if (!graphicsContext->InitFromRenderThread(wnd, desiredBackbufferSizeX, desiredBackbufferSizeY, backbuffer_format, androidVersion)) { // 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. ERROR_LOG(G3D, "Failed to initialize graphics context."); System_Toast("Failed to initialize graphics context."); delete graphicsContext; graphicsContext = nullptr; renderLoopRunning = false; return false; } if (!exitRenderLoop) { if (!NativeInitGraphics(graphicsContext)) { ERROR_LOG(G3D, "Failed to initialize graphics."); // Gonna be in a weird state here.. } graphicsContext->ThreadStart(); renderer_inited = true; static bool hasSetThreadName = false; if (!hasSetThreadName) { hasSetThreadName = true; SetCurrentThreadName("AndroidRender"); } } while (!exitRenderLoop) { LockedNativeUpdateRender(); graphicsContext->SwapBuffers(); ProcessFrameCommands(env); } INFO_LOG(G3D, "Leaving EGL/Vulkan render loop."); NativeShutdownGraphics(); renderer_inited = false; graphicsContext->ThreadEnd(); // Shut the graphics context down to the same state it was in when we entered the render thread. INFO_LOG(G3D, "Shutting down graphics context from render thread..."); graphicsContext->ShutdownFromRenderThread(); renderLoopRunning = false; WARN_LOG(G3D, "Render loop function exited."); return true; } // NOTE: This is defunct and not working, due to how the Android storage functions currently require // a PpssppActivity specifically and we don't have one here. extern "C" jstring Java_org_ppsspp_ppsspp_ShortcutActivity_queryGameName(JNIEnv *env, jclass, jstring jpath) { bool teardownThreadManager = false; if (!g_threadManager.IsInitialized()) { INFO_LOG(SYSTEM, "No thread manager - initializing one"); // Need a thread manager. teardownThreadManager = true; g_threadManager.Init(1, 1); } Path path = Path(GetJavaString(env, jpath)); INFO_LOG(SYSTEM, "queryGameName(%s)", path.c_str()); std::string result = ""; GameInfoCache *cache = new GameInfoCache(); std::shared_ptr info = cache->GetInfo(nullptr, path, 0); // Wait until it's done: this is synchronous, unfortunately. if (info) { INFO_LOG(SYSTEM, "GetInfo successful, waiting"); cache->WaitUntilDone(info); INFO_LOG(SYSTEM, "Done waiting"); if (info->fileType != IdentifiedFileType::UNKNOWN) { result = info->GetTitle(); // Pretty arbitrary, but the home screen will often truncate titles. // Let's remove "The " from names since it's common in English titles. if (result.length() > strlen("The ") && startsWithNoCase(result, "The ")) { result = result.substr(strlen("The ")); } INFO_LOG(SYSTEM, "queryGameName: Got '%s'", result.c_str()); } else { INFO_LOG(SYSTEM, "queryGameName: Filetype unknown"); } } else { INFO_LOG(SYSTEM, "No info from cache"); } delete cache; if (teardownThreadManager) { g_threadManager.Teardown(); } return env->NewStringUTF(result.c_str()); }