CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!
CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!
Path: blob/master/android/jni/app-android.cpp
Views: 1401
// This is generic code that is included in all Android apps that use the1// Native framework by Henrik Rydgard (https://github.com/hrydgard/native).23// It calls a set of methods defined in NativeApp.h. These should be implemented4// by your game or app.56#include <cstdlib>7#include <cstdint>89#include <sstream>10#include <queue>11#include <mutex>12#include <thread>13#include <atomic>1415#ifndef _MSC_VER1617#include <jni.h>18#include <android/native_window_jni.h>19#include <android/log.h>2021#elif !defined(JNIEXPORT)22// Just for better highlighting in MSVC if opening this file.23// Not having types makes it get confused and say everything is wrong.24struct JavaVM;25typedef void *jmethodID;26typedef void *jfieldID;2728typedef uint8_t jboolean;29typedef int8_t jbyte;30typedef int16_t jshort;31typedef int32_t jint;32typedef int64_t jlong;33typedef jint jsize;34typedef float jfloat;35typedef double jdouble;3637class _jobject {};38class _jclass : public _jobject {};39typedef _jobject *jobject;40typedef _jclass *jclass;41typedef jobject jstring;42typedef jobject jbyteArray;43typedef jobject jintArray;44typedef jobject jfloatArray;4546struct JNIEnv {};4748#define JNIEXPORT49#define JNICALL50// Just a random value to make MSVC highlighting happy.51#define JNI_VERSION_1_6 1652#endif5354#include "Common/Log.h"55#include "Common/LogReporting.h"5657#include "Common/Net/Resolve.h"58#include "android/jni/AndroidAudio.h"59#include "Common/GPU/OpenGL/GLCommon.h"60#include "Common/GPU/OpenGL/GLFeatures.h"6162#include "Common/System/Display.h"63#include "Common/System/NativeApp.h"64#include "Common/System/System.h"65#include "Common/System/OSD.h"66#include "Common/System/Request.h"67#include "Common/Thread/ThreadUtil.h"68#include "Common/File/Path.h"69#include "Common/File/DirListing.h"70#include "Common/File/VFS/VFS.h"71#include "Common/File/VFS/DirectoryReader.h"72#include "Common/File/VFS/ZipFileReader.h"73#include "Common/File/AndroidStorage.h"74#include "Common/Input/InputState.h"75#include "Common/Input/KeyCodes.h"76#include "Common/Profiler/Profiler.h"77#include "Common/Math/math_util.h"78#include "Common/Data/Text/Parsers.h"79#include "Common/VR/PPSSPPVR.h"80#include "Common/GPU/Vulkan/VulkanLoader.h"8182#include "Common/GraphicsContext.h"83#include "Common/StringUtils.h"84#include "Common/TimeUtil.h"8586#include "AndroidGraphicsContext.h"87#include "AndroidVulkanContext.h"88#include "AndroidJavaGLContext.h"8990#include "Core/Config.h"91#include "Core/ConfigValues.h"92#include "Core/Loaders.h"93#include "Core/FileLoaders/LocalFileLoader.h"94#include "Core/KeyMap.h"95#include "Core/System.h"96#include "Core/HLE/sceUsbCam.h"97#include "Core/HLE/sceUsbGps.h"98#include "Common/CPUDetect.h"99#include "Common/Log.h"100#include "UI/GameInfoCache.h"101102#include "app-android.h"103104bool useCPUThread = true;105106enum class EmuThreadState {107DISABLED,108START_REQUESTED,109RUNNING,110QUIT_REQUESTED,111STOPPED,112};113114// OpenGL emu thread115static std::thread emuThread;116static std::atomic<int> emuThreadState((int)EmuThreadState::DISABLED);117118AndroidAudioState *g_audioState;119120struct FrameCommand {121FrameCommand() {}122FrameCommand(std::string cmd, std::string prm) : command(cmd), params(prm) {}123124std::string command;125std::string params;126};127128static std::mutex frameCommandLock;129static std::queue<FrameCommand> frameCommands;130131static std::string systemName;132static std::string langRegion;133static std::string mogaVersion;134static std::string boardName;135136std::string g_externalDir; // Original external dir (root of Android storage).137std::string g_extFilesDir; // App private external dir.138std::string g_nativeLibDir; // App native library dir139140static std::vector<std::string> g_additionalStorageDirs;141142static int optimalFramesPerBuffer = 0;143static int optimalSampleRate = 0;144static int sampleRate = 0;145static int framesPerBuffer = 0;146static int androidVersion;147static int deviceType;148149// Should only be used for display detection during startup (for config defaults etc)150// This is the ACTUAL display size, not the hardware scaled display size.151// Exposed so it can be displayed on the touchscreen test.152static int display_xres;153static int display_yres;154static int display_dpi_x;155static int display_dpi_y;156static int backbuffer_format; // Android PixelFormat enum157158static int desiredBackbufferSizeX;159static int desiredBackbufferSizeY;160161// Cache the class loader so we can use it from native threads. Required for TextAndroid.162extern JavaVM *gJvm;163164static jobject gClassLoader;165static jmethodID gFindClassMethod;166167static float g_safeInsetLeft = 0.0;168static float g_safeInsetRight = 0.0;169static float g_safeInsetTop = 0.0;170static float g_safeInsetBottom = 0.0;171172static jmethodID postCommand;173static jmethodID getDebugString;174175static jobject nativeActivity;176177static std::atomic<bool> exitRenderLoop;178static std::atomic<bool> renderLoopRunning;179static bool renderer_inited = false;180static std::mutex renderLock;181182static bool sustainedPerfSupported = false;183184static std::map<SystemPermission, PermissionStatus> permissions;185186static AndroidGraphicsContext *graphicsContext;187188#ifndef LOG_APP_NAME189#define LOG_APP_NAME "PPSSPP"190#endif191192#define MessageBox(a, b, c, d) __android_log_print(ANDROID_LOG_INFO, APP_NAME, "%s %s", (b), (c));193194#if PPSSPP_ARCH(ARMV7)195// Old Android workaround196extern "C" {197int utimensat(int fd, const char *path, const struct timespec times[2]) {198return -1;199}200}201#endif202203static void ProcessFrameCommands(JNIEnv *env);204205void AndroidLogger::Log(const LogMessage &message) {206int mode;207switch (message.level) {208case LogLevel::LWARNING:209mode = ANDROID_LOG_WARN;210break;211case LogLevel::LERROR:212mode = ANDROID_LOG_ERROR;213break;214default:215mode = ANDROID_LOG_INFO;216break;217}218219// Long log messages need splitting up.220// Not sure what the actual limit is (seems to vary), but let's be conservative.221const size_t maxLogLength = 512;222if (message.msg.length() < maxLogLength) {223// Log with simplified headers as Android already provides timestamp etc.224__android_log_print(mode, LOG_APP_NAME, "[%s] %s", message.log, message.msg.c_str());225} else {226std::string msg = message.msg;227228// Ideally we should split at line breaks, but it's at least fairly usable anyway.229std::string first_part = msg.substr(0, maxLogLength);230__android_log_print(mode, LOG_APP_NAME, "[%s] %s", message.log, first_part.c_str());231msg = msg.substr(maxLogLength);232233while (msg.length() > maxLogLength) {234std::string first_part = msg.substr(0, maxLogLength);235__android_log_print(mode, LOG_APP_NAME, "%s", first_part.c_str());236msg = msg.substr(maxLogLength);237}238// Print the final part.239__android_log_print(mode, LOG_APP_NAME, "%s", msg.c_str());240}241}242243JNIEnv* getEnv() {244JNIEnv *env;245int status = gJvm->GetEnv((void**)&env, JNI_VERSION_1_6);246_assert_msg_(status >= 0, "'%s': Can only call getEnv if you've attached the thread already!", GetCurrentThreadName());247return env;248}249250jclass findClass(const char* name) {251return static_cast<jclass>(getEnv()->CallObjectMethod(gClassLoader, gFindClassMethod, getEnv()->NewStringUTF(name)));252}253254void Android_AttachThreadToJNI() {255JNIEnv *env;256int status = gJvm->GetEnv((void **)&env, JNI_VERSION_1_6);257if (status < 0) {258DEBUG_LOG(Log::System, "Attaching thread '%s' (not already attached) to JNI.", GetCurrentThreadName());259JavaVMAttachArgs args{};260args.version = JNI_VERSION_1_6;261args.name = GetCurrentThreadName();262status = gJvm->AttachCurrentThread(&env, &args);263264if (status < 0) {265// bad, but what can we do other than report..266ERROR_LOG_REPORT_ONCE(threadAttachFail, Log::System, "Failed to attach thread %s to JNI.", GetCurrentThreadName());267}268} else {269WARN_LOG(Log::System, "Thread %s was already attached to JNI.", GetCurrentThreadName());270}271}272273void Android_DetachThreadFromJNI() {274if (gJvm->DetachCurrentThread() == JNI_OK) {275DEBUG_LOG(Log::System, "Detached thread from JNI: '%s'", GetCurrentThreadName());276} else {277WARN_LOG(Log::System, "Failed to detach thread '%s' from JNI - never attached?", GetCurrentThreadName());278}279}280281JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *pjvm, void *reserved) {282INFO_LOG(Log::System, "JNI_OnLoad");283gJvm = pjvm; // cache the JavaVM pointer284auto env = getEnv();285//replace with one of your classes in the line below286auto randomClass = env->FindClass("org/ppsspp/ppsspp/NativeActivity");287jclass classClass = env->GetObjectClass(randomClass);288auto classLoaderClass = env->FindClass("java/lang/ClassLoader");289auto getClassLoaderMethod = env->GetMethodID(classClass, "getClassLoader",290"()Ljava/lang/ClassLoader;");291gClassLoader = env->NewGlobalRef(env->CallObjectMethod(randomClass, getClassLoaderMethod));292gFindClassMethod = env->GetMethodID(classLoaderClass, "findClass",293"(Ljava/lang/String;)Ljava/lang/Class;");294295RegisterAttachDetach(&Android_AttachThreadToJNI, &Android_DetachThreadFromJNI);296297TimeInit();298return JNI_VERSION_1_6;299}300301// Only used in OpenGL mode.302static void EmuThreadFunc() {303SetCurrentThreadName("EmuThread");304305// Name the thread in the JVM, because why not (might result in better debug output in Play Console).306// TODO: Do something clever with getEnv() and stored names from SetCurrentThreadName?307JNIEnv *env;308JavaVMAttachArgs args{};309args.version = JNI_VERSION_1_6;310args.name = "EmuThread";311gJvm->AttachCurrentThread(&env, &args);312313INFO_LOG(Log::System, "Entering emu thread");314315// Wait for render loop to get started.316INFO_LOG(Log::System, "Runloop: Waiting for displayInit...");317while (!graphicsContext || graphicsContext->GetState() == GraphicsContextState::PENDING) {318sleep_ms(5);319}320321// Check the state of the graphics context before we try to feed it into NativeInitGraphics.322if (graphicsContext->GetState() != GraphicsContextState::INITIALIZED) {323ERROR_LOG(Log::G3D, "Failed to initialize the graphics context! %d", (int)graphicsContext->GetState());324emuThreadState = (int)EmuThreadState::QUIT_REQUESTED;325gJvm->DetachCurrentThread();326return;327}328329if (!NativeInitGraphics(graphicsContext)) {330_assert_msg_(false, "NativeInitGraphics failed, might as well bail");331emuThreadState = (int)EmuThreadState::QUIT_REQUESTED;332gJvm->DetachCurrentThread();333return;334}335336INFO_LOG(Log::System, "Graphics initialized. Entering loop.");337338// There's no real requirement that NativeInit happen on this thread.339// We just call the update/render loop here.340emuThreadState = (int)EmuThreadState::RUNNING;341while (emuThreadState != (int)EmuThreadState::QUIT_REQUESTED) {342{343std::lock_guard<std::mutex> renderGuard(renderLock);344NativeFrame(graphicsContext);345}346347std::lock_guard<std::mutex> guard(frameCommandLock);348if (!nativeActivity) {349ERROR_LOG(Log::System, "No activity, clearing commands");350while (!frameCommands.empty())351frameCommands.pop();352return;353}354// Still under lock here.355ProcessFrameCommands(env);356}357358INFO_LOG(Log::System, "QUIT_REQUESTED found, left EmuThreadFunc loop. Setting state to STOPPED.");359emuThreadState = (int)EmuThreadState::STOPPED;360361NativeShutdownGraphics();362363// Also ask the main thread to stop, so it doesn't hang waiting for a new frame.364graphicsContext->StopThread();365366gJvm->DetachCurrentThread();367INFO_LOG(Log::System, "Leaving emu thread");368}369370static void EmuThreadStart() {371INFO_LOG(Log::System, "EmuThreadStart");372emuThreadState = (int)EmuThreadState::START_REQUESTED;373emuThread = std::thread(&EmuThreadFunc);374}375376// Call EmuThreadStop first, then keep running the GPU (or eat commands)377// as long as emuThreadState isn't STOPPED and/or there are still things queued up.378// Only after that, call EmuThreadJoin.379static void EmuThreadStop(const char *caller) {380INFO_LOG(Log::System, "EmuThreadStop - stopping (%s)...", caller);381emuThreadState = (int)EmuThreadState::QUIT_REQUESTED;382}383384static void EmuThreadJoin() {385emuThread.join();386emuThread = std::thread();387INFO_LOG(Log::System, "EmuThreadJoin - joined");388}389390static void PushCommand(std::string cmd, std::string param) {391std::lock_guard<std::mutex> guard(frameCommandLock);392frameCommands.push(FrameCommand(cmd, param));393}394395// Android implementation of callbacks to the Java part of the app396void System_Toast(std::string_view text) {397PushCommand("toast", std::string(text));398}399400void System_ShowKeyboard() {401PushCommand("showKeyboard", "");402}403404void System_Vibrate(int length_ms) {405char temp[32];406snprintf(temp, sizeof(temp), "%d", length_ms);407PushCommand("vibrate", temp);408}409410void System_LaunchUrl(LaunchUrlType urlType, const char *url) {411switch (urlType) {412case LaunchUrlType::BROWSER_URL: PushCommand("launchBrowser", url); break;413case LaunchUrlType::MARKET_URL: PushCommand("launchMarket", url); break;414case LaunchUrlType::EMAIL_ADDRESS: PushCommand("launchEmail", url); break;415}416}417418std::string System_GetProperty(SystemProperty prop) {419switch (prop) {420case SYSPROP_NAME:421return systemName;422case SYSPROP_LANGREGION: // "en_US"423return langRegion;424case SYSPROP_MOGA_VERSION:425return mogaVersion;426case SYSPROP_BOARDNAME:427return boardName;428case SYSPROP_BUILD_VERSION:429return PPSSPP_GIT_VERSION;430default:431return "";432}433}434435std::vector<std::string> System_GetPropertyStringVec(SystemProperty prop) {436switch (prop) {437case SYSPROP_ADDITIONAL_STORAGE_DIRS:438return g_additionalStorageDirs;439440case SYSPROP_TEMP_DIRS:441default:442return std::vector<std::string>();443}444}445446int64_t System_GetPropertyInt(SystemProperty prop) {447switch (prop) {448case SYSPROP_SYSTEMVERSION:449return androidVersion;450case SYSPROP_DEVICE_TYPE:451return deviceType;452case SYSPROP_DISPLAY_XRES:453return display_xres;454case SYSPROP_DISPLAY_YRES:455return display_yres;456case SYSPROP_AUDIO_SAMPLE_RATE:457return sampleRate;458case SYSPROP_AUDIO_FRAMES_PER_BUFFER:459return framesPerBuffer;460case SYSPROP_AUDIO_OPTIMAL_SAMPLE_RATE:461return optimalSampleRate;462case SYSPROP_AUDIO_OPTIMAL_FRAMES_PER_BUFFER:463return optimalFramesPerBuffer;464default:465return -1;466}467}468469float System_GetPropertyFloat(SystemProperty prop) {470switch (prop) {471case SYSPROP_DISPLAY_REFRESH_RATE:472return g_display.display_hz;473case SYSPROP_DISPLAY_SAFE_INSET_LEFT:474return g_safeInsetLeft;475case SYSPROP_DISPLAY_SAFE_INSET_RIGHT:476return g_safeInsetRight;477case SYSPROP_DISPLAY_SAFE_INSET_TOP:478return g_safeInsetTop;479case SYSPROP_DISPLAY_SAFE_INSET_BOTTOM:480return g_safeInsetBottom;481default:482return -1;483}484}485486bool System_GetPropertyBool(SystemProperty prop) {487switch (prop) {488case SYSPROP_SUPPORTS_PERMISSIONS:489if (androidVersion < 23) {490// 6.0 Marshmallow introduced run time permissions.491return false;492} else {493// It gets a bit complicated here. If scoped storage enforcement is on,494// we also don't need to request permissions. We'll have the access we request495// on a per-folder basis.496return !System_GetPropertyBool(SYSPROP_ANDROID_SCOPED_STORAGE);497}498case SYSPROP_SUPPORTS_SUSTAINED_PERF_MODE:499return sustainedPerfSupported; // 7.0 introduced sustained performance mode as an optional feature.500case SYSPROP_HAS_TEXT_INPUT_DIALOG:501return androidVersion >= 11; // honeycomb502case SYSPROP_HAS_TEXT_CLIPBOARD:503return true;504case SYSPROP_HAS_OPEN_DIRECTORY:505return false; // We have this implemented but it may or may not work depending on if a file explorer is installed.506case SYSPROP_HAS_ADDITIONAL_STORAGE:507return !g_additionalStorageDirs.empty();508case SYSPROP_HAS_BACK_BUTTON:509return true;510case SYSPROP_HAS_IMAGE_BROWSER:511return deviceType != DEVICE_TYPE_VR;512case SYSPROP_HAS_FILE_BROWSER:513// It's only really needed with scoped storage, but why not make it available514// as far back as possible - works just fine.515return (androidVersion >= 19) && (deviceType != DEVICE_TYPE_VR); // when ACTION_OPEN_DOCUMENT was added516case SYSPROP_HAS_FOLDER_BROWSER:517// Uses OPEN_DOCUMENT_TREE to let you select a folder.518// Doesn't actually mean it's usable though, in many early versions of Android519// this dialog is complete garbage and only lets you select subfolders of the Downloads folder.520return (androidVersion >= 21) && (deviceType != DEVICE_TYPE_VR); // when ACTION_OPEN_DOCUMENT_TREE was added521case SYSPROP_SUPPORTS_OPEN_FILE_IN_EDITOR:522return false; // Update if we add support in FileUtil.cpp: OpenFileInEditor523case SYSPROP_APP_GOLD:524#ifdef GOLD525return true;526#else527return false;528#endif529case SYSPROP_CAN_JIT:530return true;531case SYSPROP_ANDROID_SCOPED_STORAGE:532// We turn this on for Android 30+ (11) now that when we target Android 11+.533// Along with adding:534// android:preserveLegacyExternalStorage="true"535// To the already requested:536// android:requestLegacyExternalStorage="true"537//538// This will cause Android 11+ to still behave like Android 10 until the app539// is manually uninstalled. We can detect this state with540// Android_IsExternalStoragePreservedLegacy(), but most of the app will just see541// that scoped storage enforcement is disabled in this case.542if (androidVersion >= 30) {543// Here we do a check to see if we ended up in the preserveLegacyExternalStorage path.544// That won't last if the user uninstalls/reinstalls though, but would preserve the user545// experience for simple upgrades so maybe let's support it.546return !Android_IsExternalStoragePreservedLegacy();547} else {548return false;549}550case SYSPROP_HAS_KEYBOARD:551return deviceType != DEVICE_TYPE_VR;552case SYSPROP_HAS_ACCELEROMETER:553return deviceType == DEVICE_TYPE_MOBILE;554case SYSPROP_CAN_CREATE_SHORTCUT:555return false; // We can't create shortcuts directly from game code, but we can from the Android UI.556#ifndef HTTPS_NOT_AVAILABLE557case SYSPROP_SUPPORTS_HTTPS:558return !g_Config.bDisableHTTPS;559#endif560default:561return false;562}563}564565std::string Android_GetInputDeviceDebugString() {566if (!nativeActivity) {567return "(N/A)";568}569auto env = getEnv();570571jstring jparam = env->NewStringUTF("InputDevice");572jstring jstr = (jstring)env->CallObjectMethod(nativeActivity, getDebugString, jparam);573if (!jstr) {574env->DeleteLocalRef(jparam);575return "(N/A)";576}577578const char *charArray = env->GetStringUTFChars(jstr, 0);579std::string retVal = charArray;580env->ReleaseStringUTFChars(jstr, charArray);581env->DeleteLocalRef(jstr);582env->DeleteLocalRef(jparam);583return retVal;584}585586std::string GetJavaString(JNIEnv *env, jstring jstr) {587if (!jstr)588return "";589const char *str = env->GetStringUTFChars(jstr, 0);590std::string cpp_string = std::string(str);591env->ReleaseStringUTFChars(jstr, str);592return cpp_string;593}594595extern "C" void Java_org_ppsspp_ppsspp_NativeActivity_registerCallbacks(JNIEnv *env, jobject obj) {596nativeActivity = env->NewGlobalRef(obj);597postCommand = env->GetMethodID(env->GetObjectClass(obj), "postCommand", "(Ljava/lang/String;Ljava/lang/String;)V");598getDebugString = env->GetMethodID(env->GetObjectClass(obj), "getDebugString", "(Ljava/lang/String;)Ljava/lang/String;");599_dbg_assert_(postCommand);600_dbg_assert_(getDebugString);601602Android_RegisterStorageCallbacks(env, obj);603Android_StorageSetNativeActivity(nativeActivity);604}605606extern "C" void Java_org_ppsspp_ppsspp_NativeActivity_unregisterCallbacks(JNIEnv *env, jobject obj) {607Android_StorageSetNativeActivity(nullptr);608env->DeleteGlobalRef(nativeActivity);609nativeActivity = nullptr;610}611612// This is now only used as a trigger for GetAppInfo as a function to all before Init.613// On Android we don't use any of the values it returns.614extern "C" jboolean Java_org_ppsspp_ppsspp_NativeApp_isLandscape(JNIEnv *env, jclass) {615std::string app_name, app_nice_name, version;616bool landscape;617NativeGetAppInfo(&app_name, &app_nice_name, &landscape, &version);618return landscape;619}620621// Allow the app to intercept the back button.622extern "C" jboolean Java_org_ppsspp_ppsspp_NativeApp_isAtTopLevel(JNIEnv *env, jclass) {623return NativeIsAtTopLevel();624}625626extern "C" void Java_org_ppsspp_ppsspp_NativeApp_audioConfig627(JNIEnv *env, jclass, jint optimalFPB, jint optimalSR) {628optimalFramesPerBuffer = optimalFPB;629optimalSampleRate = optimalSR;630}631632// Easy way for the Java side to ask the C++ side for configuration options, such as633// the rotation lock which must be controlled from Java on Android.634static std::string QueryConfig(std::string query) {635char temp[128];636if (query == "screenRotation") {637INFO_LOG(Log::G3D, "g_Config.screenRotation = %d", g_Config.iScreenRotation);638snprintf(temp, sizeof(temp), "%d", g_Config.iScreenRotation);639return std::string(temp);640} else if (query == "immersiveMode") {641return std::string(g_Config.bImmersiveMode ? "1" : "0");642} else if (query == "sustainedPerformanceMode") {643return std::string(g_Config.bSustainedPerformanceMode ? "1" : "0");644} else if (query == "androidJavaGL") {645// If we're using Vulkan, we say no... need C++ to use Vulkan.646if (GetGPUBackend() == GPUBackend::VULKAN) {647return "false";648}649// Otherwise, some devices prefer the Java init so play it safe.650return "true";651} else {652return "";653}654}655656extern "C" jstring Java_org_ppsspp_ppsspp_NativeApp_queryConfig657(JNIEnv *env, jclass, jstring jquery) {658std::string query = GetJavaString(env, jquery);659std::string result = QueryConfig(query);660jstring jresult = env->NewStringUTF(result.c_str());661return jresult;662}663664static void parse_args(std::vector<std::string> &args, const std::string value) {665// Simple argument parser so we can take args from extra params.666const char *p = value.c_str();667668while (*p != '\0') {669while (isspace(*p)) {670p++;671}672if (*p == '\0') {673break;674}675676bool done = false;677bool quote = false;678std::string arg;679680while (!done) {681size_t sz = strcspn(p, "\"\\ \r\n\t");682arg += std::string(p, sz);683p += sz;684685switch (*p) {686case '"':687quote = !quote;688p++;689break;690691case '\\':692p++;693arg += std::string(p, 1);694p++;695break;696697case '\0':698done = true;699break;700701default:702// If it's not the above, it's whitespace.703if (!quote) {704done = true;705} else {706sz = strspn(p, " \r\n\t");707arg += std::string(p, sz);708p += sz;709}710break;711}712}713714args.push_back(arg);715716while (isspace(*p)) {717p++;718}719}720}721722// Need to use raw Android logging before NativeInit.723#define EARLY_LOG(...) __android_log_print(ANDROID_LOG_INFO, "PPSSPP", __VA_ARGS__)724725extern "C" void Java_org_ppsspp_ppsspp_NativeApp_init726(JNIEnv * env, jclass, jstring jmodel, jint jdeviceType, jstring jlangRegion, jstring japkpath,727jstring jdataDir, jstring jexternalStorageDir, jstring jexternalFilesDir, jstring jNativeLibDir, jstring jadditionalStorageDirs, jstring jcacheDir, jstring jshortcutParam,728jint jAndroidVersion, jstring jboard) {729SetCurrentThreadName("androidInit");730731// Makes sure we get early permission grants.732ProcessFrameCommands(env);733734EARLY_LOG("NativeApp.init() -- begin");735PROFILE_INIT();736737std::lock_guard<std::mutex> guard(renderLock);738renderer_inited = false;739exitRenderLoop = false;740androidVersion = jAndroidVersion;741deviceType = jdeviceType;742743Path apkPath(GetJavaString(env, japkpath));744g_VFS.Register("", ZipFileReader::Create(apkPath, "assets/"));745746systemName = GetJavaString(env, jmodel);747langRegion = GetJavaString(env, jlangRegion);748749EARLY_LOG("NativeApp.init(): device name: '%s'", systemName.c_str());750751std::string externalStorageDir = GetJavaString(env, jexternalStorageDir);752std::string additionalStorageDirsString = GetJavaString(env, jadditionalStorageDirs);753std::string externalFilesDir = GetJavaString(env, jexternalFilesDir);754std::string nativeLibDir = GetJavaString(env, jNativeLibDir);755756g_externalDir = externalStorageDir;757g_extFilesDir = externalFilesDir;758g_nativeLibDir = nativeLibDir;759760if (!additionalStorageDirsString.empty()) {761SplitString(additionalStorageDirsString, ':', g_additionalStorageDirs);762for (auto &str : g_additionalStorageDirs) {763EARLY_LOG("Additional storage: %s", str.c_str());764}765}766767std::string user_data_path = GetJavaString(env, jdataDir);768if (user_data_path.size() > 0)769user_data_path += "/";770std::string shortcut_param = GetJavaString(env, jshortcutParam);771std::string cacheDir = GetJavaString(env, jcacheDir);772std::string buildBoard = GetJavaString(env, jboard);773boardName = buildBoard;774EARLY_LOG("NativeApp.init(): External storage path: %s", externalStorageDir.c_str());775EARLY_LOG("NativeApp.init(): Launch shortcut parameter: %s", shortcut_param.c_str());776777std::string app_name;778std::string app_nice_name;779std::string version;780bool landscape;781782// Unfortunately, on the Samsung Galaxy S7, this isn't in /proc/cpuinfo.783// We also can't read it from __system_property_get.784if (buildBoard == "universal8890") {785cpu_info.sQuirks.bExynos8890DifferingCachelineSizes = true;786}787788NativeGetAppInfo(&app_name, &app_nice_name, &landscape, &version);789790// If shortcut_param is not empty, pass it as additional arguments to the NativeInit() method.791// NativeInit() is expected to treat extra argument as boot_filename, which in turn will start game immediately.792// NOTE: Will only work if ppsspp started from Activity.onCreate(). Won't work if ppsspp app start from onResume().793794std::vector<const char *> args;795std::vector<std::string> temp;796args.push_back(app_name.c_str());797if (!shortcut_param.empty()) {798EARLY_LOG("NativeInit shortcut param %s", shortcut_param.c_str());799parse_args(temp, shortcut_param);800for (const auto &arg : temp) {801args.push_back(arg.c_str());802}803}804805NativeInit((int)args.size(), &args[0], user_data_path.c_str(), externalStorageDir.c_str(), cacheDir.c_str());806807// In debug mode, don't allow creating software Vulkan devices (reject by VulkanMaybeAvailable).808// Needed for #16931.809#ifdef NDEBUG810if (!VulkanMayBeAvailable()) {811// If VulkanLoader decided on no viable backend, let's force Vulkan off in release builds at least.812g_Config.iGPUBackend = 0;813}814#endif815816// No need to use EARLY_LOG anymore.817818retry:819switch (g_Config.iGPUBackend) {820case (int)GPUBackend::OPENGL:821useCPUThread = true;822INFO_LOG(Log::System, "NativeApp.init() -- creating OpenGL context (JavaGL)");823graphicsContext = new AndroidJavaEGLGraphicsContext();824INFO_LOG(Log::System, "NativeApp.init() - launching emu thread");825EmuThreadStart();826break;827case (int)GPUBackend::VULKAN:828{829INFO_LOG(Log::System, "NativeApp.init() -- creating Vulkan context");830useCPUThread = false;831// The Vulkan render manager manages its own thread.832// We create and destroy the Vulkan graphics context in the app main thread though.833AndroidVulkanContext *ctx = new AndroidVulkanContext();834if (!ctx->InitAPI()) {835INFO_LOG(Log::System, "Failed to initialize Vulkan, switching to OpenGL");836g_Config.iGPUBackend = (int)GPUBackend::OPENGL;837SetGPUBackend(GPUBackend::OPENGL);838goto retry;839} else {840graphicsContext = ctx;841}842break;843}844default:845ERROR_LOG(Log::System, "NativeApp.init(): iGPUBackend %d not supported. Switching to OpenGL.", (int)g_Config.iGPUBackend);846g_Config.iGPUBackend = (int)GPUBackend::OPENGL;847goto retry;848}849850if (IsVREnabled()) {851Version gitVer(PPSSPP_GIT_VERSION);852InitVROnAndroid(gJvm, nativeActivity, systemName.c_str(), gitVer.ToInteger(), "PPSSPP");853SetVRCallbacks(NativeAxis, NativeKey, NativeTouch);854}855}856857extern "C" void Java_org_ppsspp_ppsspp_NativeApp_audioInit(JNIEnv *, jclass) {858sampleRate = optimalSampleRate;859if (optimalSampleRate == 0) {860sampleRate = 44100;861}862if (optimalFramesPerBuffer > 0) {863framesPerBuffer = optimalFramesPerBuffer;864} else {865framesPerBuffer = 512;866}867868// Some devices have totally bonkers buffer sizes like 8192. They will have terrible latency anyway, so to avoid having to869// create extra smart buffering code, we'll just let their regular mixer deal with it, missing the fast path (as if they had one...)870if (framesPerBuffer > 512) {871framesPerBuffer = 512;872sampleRate = 44100;873}874875INFO_LOG(Log::Audio, "NativeApp.audioInit() -- Using OpenSL audio! frames/buffer: %i optimal sr: %i actual sr: %i", optimalFramesPerBuffer, optimalSampleRate, sampleRate);876if (!g_audioState) {877g_audioState = AndroidAudio_Init(&NativeMix, framesPerBuffer, sampleRate);878} else {879ERROR_LOG(Log::Audio, "Audio state already initialized");880}881}882883extern "C" void Java_org_ppsspp_ppsspp_NativeApp_audioShutdown(JNIEnv *, jclass) {884if (g_audioState) {885AndroidAudio_Shutdown(g_audioState);886g_audioState = nullptr;887} else {888ERROR_LOG(Log::Audio, "Audio state already shutdown!");889}890}891892extern "C" void Java_org_ppsspp_ppsspp_NativeApp_audioRecording_1SetSampleRate(JNIEnv *, jclass, jint sampleRate) {893AndroidAudio_Recording_SetSampleRate(g_audioState, sampleRate);894}895896extern "C" void Java_org_ppsspp_ppsspp_NativeApp_audioRecording_1Start(JNIEnv *, jclass) {897AndroidAudio_Recording_Start(g_audioState);898}899900extern "C" void Java_org_ppsspp_ppsspp_NativeApp_audioRecording_1Stop(JNIEnv *, jclass) {901AndroidAudio_Recording_Stop(g_audioState);902}903904bool System_AudioRecordingIsAvailable() {905return true;906}907908bool System_AudioRecordingState() {909return AndroidAudio_Recording_State(g_audioState);910}911912extern "C" void Java_org_ppsspp_ppsspp_NativeApp_resume(JNIEnv *, jclass) {913INFO_LOG(Log::System, "NativeApp.resume() - resuming audio");914AndroidAudio_Resume(g_audioState);915916System_PostUIMessage(UIMessage::APP_RESUMED);917}918919extern "C" void Java_org_ppsspp_ppsspp_NativeApp_pause(JNIEnv *, jclass) {920INFO_LOG(Log::System, "NativeApp.pause() - pausing audio");921AndroidAudio_Pause(g_audioState);922}923924extern "C" void Java_org_ppsspp_ppsspp_NativeApp_shutdown(JNIEnv *, jclass) {925INFO_LOG(Log::System, "NativeApp.shutdown() -- begin");926927if (renderer_inited && useCPUThread && graphicsContext) {928// Only used in Java EGL path.929930// We can't lock renderLock here because the emu thread will be in NativeFrame931// which locks renderLock already, and only gets out once we call ThreadFrame()932// in a loop before, to empty the queue.933EmuThreadStop("shutdown");934INFO_LOG(Log::System, "BeginAndroidShutdown");935graphicsContext->BeginAndroidShutdown();936// Now, it could be that we had some frames queued up. Get through them.937// We're on the render thread, so this is synchronous.938do {939INFO_LOG(Log::System, "Executing graphicsContext->ThreadFrame to clear buffers");940} while (graphicsContext->ThreadFrame());941graphicsContext->ThreadEnd();942INFO_LOG(Log::System, "ThreadEnd called.");943graphicsContext->ShutdownFromRenderThread();944INFO_LOG(Log::System, "Graphics context now shut down from NativeApp_shutdown");945946INFO_LOG(Log::System, "Joining emuthread");947EmuThreadJoin();948}949950{951std::lock_guard<std::mutex> guard(renderLock);952953if (graphicsContext) {954INFO_LOG(Log::G3D, "Shutting down renderer");955graphicsContext->Shutdown();956delete graphicsContext;957graphicsContext = nullptr;958renderer_inited = false;959} else {960INFO_LOG(Log::G3D, "Not shutting down renderer - not initialized");961}962963NativeShutdown();964g_VFS.Clear();965}966967{968std::lock_guard<std::mutex> guard(frameCommandLock);969while (frameCommands.size())970frameCommands.pop();971}972INFO_LOG(Log::System, "NativeApp.shutdown() -- end");973}974975// JavaEGL. This doesn't get called on the Vulkan path.976// This gets called from onSurfaceCreated.977extern "C" bool Java_org_ppsspp_ppsspp_NativeRenderer_displayInit(JNIEnv * env, jobject obj) {978_assert_(useCPUThread);979980INFO_LOG(Log::G3D, "NativeApp.displayInit()");981bool firstStart = !renderer_inited;982983// We should be running on the render thread here.984std::string errorMessage;985if (renderer_inited) {986// Would be really nice if we could get something on the GL thread immediately when shutting down,987// but the only mechanism for handling lost devices seems to be that onSurfaceCreated is called again,988// which ends up calling displayInit.989990INFO_LOG(Log::G3D, "NativeApp.displayInit() restoring");991EmuThreadStop("displayInit");992graphicsContext->BeginAndroidShutdown();993INFO_LOG(Log::G3D, "BeginAndroidShutdown. Looping until emu thread done...");994// Skipping GL calls here because the old context is lost.995while (graphicsContext->ThreadFrame()) {996continue;997}998INFO_LOG(Log::G3D, "Joining emu thread");999EmuThreadJoin();10001001graphicsContext->ThreadEnd();1002graphicsContext->ShutdownFromRenderThread();10031004INFO_LOG(Log::G3D, "Shut down both threads. Now let's bring it up again!");10051006if (!graphicsContext->InitFromRenderThread(nullptr, 0, 0, 0, 0)) {1007System_Toast("Graphics initialization failed. Quitting.");1008return false;1009}10101011graphicsContext->GetDrawContext()->SetErrorCallback([](const char *shortDesc, const char *details, void *userdata) {1012g_OSD.Show(OSDType::MESSAGE_ERROR, details, 5.0);1013}, nullptr);10141015EmuThreadStart();10161017graphicsContext->ThreadStart();10181019INFO_LOG(Log::G3D, "Restored.");1020} else {1021INFO_LOG(Log::G3D, "NativeApp.displayInit() first time");1022if (!graphicsContext || !graphicsContext->InitFromRenderThread(nullptr, 0, 0, 0, 0)) {1023System_Toast("Graphics initialization failed. Quitting.");1024return false;1025}10261027graphicsContext->GetDrawContext()->SetErrorCallback([](const char *shortDesc, const char *details, void *userdata) {1028g_OSD.Show(OSDType::MESSAGE_ERROR, details, 5.0);1029}, nullptr);10301031graphicsContext->ThreadStart();1032renderer_inited = true;1033}10341035System_PostUIMessage(UIMessage::RECREATE_VIEWS);10361037if (IsVREnabled()) {1038EnterVR(firstStart, graphicsContext->GetAPIContext());1039}1040return true;1041}10421043static void recalculateDpi() {1044g_display.dpi = display_dpi_x;1045g_display.dpi_scale_x = 240.0f / display_dpi_x;1046g_display.dpi_scale_y = 240.0f / display_dpi_y;1047g_display.dpi_scale_real_x = g_display.dpi_scale_x;1048g_display.dpi_scale_real_y = g_display.dpi_scale_y;10491050g_display.dp_xres = display_xres * g_display.dpi_scale_x;1051g_display.dp_yres = display_yres * g_display.dpi_scale_y;10521053g_display.pixel_in_dps_x = (float)g_display.pixel_xres / g_display.dp_xres;1054g_display.pixel_in_dps_y = (float)g_display.pixel_yres / g_display.dp_yres;10551056INFO_LOG(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);1057INFO_LOG(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);1058}10591060extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_backbufferResize(JNIEnv *, jclass, jint bufw, jint bufh, jint format) {1061INFO_LOG(Log::System, "NativeApp.backbufferResize(%d x %d)", bufw, bufh);10621063bool new_size = g_display.pixel_xres != bufw || g_display.pixel_yres != bufh;1064int old_w = g_display.pixel_xres;1065int old_h = g_display.pixel_yres;1066// pixel_*res is the backbuffer resolution.1067g_display.pixel_xres = bufw;1068g_display.pixel_yres = bufh;1069backbuffer_format = format;10701071if (IsVREnabled()) {1072GetVRResolutionPerEye(&g_display.pixel_xres, &g_display.pixel_yres);1073}10741075recalculateDpi();10761077if (new_size) {1078INFO_LOG(Log::G3D, "Size change detected (previously %d,%d) - calling NativeResized()", old_w, old_h);1079NativeResized();1080} else {1081INFO_LOG(Log::G3D, "NativeApp::backbufferResize: Size didn't change.");1082}1083}10841085void System_Notify(SystemNotification notification) {1086switch (notification) {1087case SystemNotification::ROTATE_UPDATED:1088PushCommand("rotate", "");1089break;1090case SystemNotification::FORCE_RECREATE_ACTIVITY:1091PushCommand("recreate", "");1092break;1093case SystemNotification::IMMERSIVE_MODE_CHANGE:1094PushCommand("immersive", "");1095break;1096case SystemNotification::SUSTAINED_PERF_CHANGE:1097PushCommand("sustainedPerfMode", "");1098break;1099case SystemNotification::TEST_JAVA_EXCEPTION:1100PushCommand("testException", "This is a test exception");1101break;1102default:1103break;1104}1105}11061107bool System_MakeRequest(SystemRequestType type, int requestId, const std::string ¶m1, const std::string ¶m2, int64_t param3, int64_t param4) {1108switch (type) {1109case SystemRequestType::EXIT_APP:1110PushCommand("finish", "");1111return true;1112case SystemRequestType::RESTART_APP:1113PushCommand("graphics_restart", param1);1114return true;1115case SystemRequestType::RECREATE_ACTIVITY:1116PushCommand("recreate", param1);1117return true;1118case SystemRequestType::COPY_TO_CLIPBOARD:1119PushCommand("copy_to_clipboard", param1);1120return true;1121case SystemRequestType::INPUT_TEXT_MODAL:1122{1123std::string serialized = StringFromFormat("%d:@:%s:@:%s", requestId, param1.c_str(), param2.c_str());1124PushCommand("inputbox", serialized.c_str());1125return true;1126}1127case SystemRequestType::BROWSE_FOR_IMAGE:1128PushCommand("browse_image", StringFromFormat("%d", requestId));1129return true;1130case SystemRequestType::BROWSE_FOR_FILE:1131{1132BrowseFileType fileType = (BrowseFileType)param3;1133std::string params = StringFromFormat("%d", requestId);1134switch (fileType) {1135case BrowseFileType::SOUND_EFFECT:1136PushCommand("browse_file_audio", params);1137break;1138case BrowseFileType::ZIP:1139PushCommand("browse_file_zip", params);1140break;1141default:1142PushCommand("browse_file", params);1143break;1144}1145return true;1146}1147case SystemRequestType::BROWSE_FOR_FOLDER:1148PushCommand("browse_folder", StringFromFormat("%d", requestId));1149return true;11501151case SystemRequestType::CAMERA_COMMAND:1152PushCommand("camera_command", param1);1153return true;1154case SystemRequestType::GPS_COMMAND:1155PushCommand("gps_command", param1);1156return true;1157case SystemRequestType::INFRARED_COMMAND:1158PushCommand("infrared_command", param1);1159return true;1160case SystemRequestType::MICROPHONE_COMMAND:1161PushCommand("microphone_command", param1);1162return true;1163case SystemRequestType::SHARE_TEXT:1164PushCommand("share_text", param1);1165return true;1166case SystemRequestType::SET_KEEP_SCREEN_BRIGHT:1167PushCommand("set_keep_screen_bright", param3 ? "on" : "off");1168return true;1169case SystemRequestType::SHOW_FILE_IN_FOLDER:1170PushCommand("show_folder", param1);1171return true;1172default:1173return false;1174}1175}11761177extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_sendRequestResult(JNIEnv *env, jclass, jint jrequestID, jboolean result, jstring jvalue, jint jintValue) {1178std::string value = jvalue ? GetJavaString(env, jvalue) : "(no value)";1179INFO_LOG(Log::System, "Received result of request %d from Java: %d: %d '%s'", jrequestID, (int)result, jintValue, value.c_str());1180if (result) {1181g_requestManager.PostSystemSuccess(jrequestID, value.c_str());1182} else {1183g_requestManager.PostSystemFailure(jrequestID);1184}1185}11861187extern "C" void Java_org_ppsspp_ppsspp_NativeRenderer_displayRender(JNIEnv *env, jobject obj) {1188// This doesn't get called on the Vulkan path.1189_assert_(useCPUThread);11901191static bool hasSetThreadName = false;1192if (!hasSetThreadName) {1193hasSetThreadName = true;1194SetCurrentThreadName("AndroidRender");1195}11961197if (IsVREnabled() && !StartVRRender())1198return;11991200// This is the "GPU thread". Call ThreadFrame.1201if (!graphicsContext || !graphicsContext->ThreadFrame()) {1202return;1203}12041205if (IsVREnabled()) {1206UpdateVRInput(g_Config.bHapticFeedback, g_display.dpi_scale_x, g_display.dpi_scale_y);1207FinishVRRender();1208}1209}12101211void System_AskForPermission(SystemPermission permission) {1212switch (permission) {1213case SYSTEM_PERMISSION_STORAGE:1214PushCommand("ask_permission", "storage");1215break;1216}1217}12181219PermissionStatus System_GetPermissionStatus(SystemPermission permission) {1220if (androidVersion < 23) {1221return PERMISSION_STATUS_GRANTED;1222} else {1223return permissions[permission];1224}1225}12261227extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_touch1228(JNIEnv *, jclass, float x, float y, int code, int pointerId) {1229if (!renderer_inited)1230return;1231TouchInput touch;1232touch.id = pointerId;1233touch.x = x * g_display.dpi_scale_x;1234touch.y = y * g_display.dpi_scale_y;1235touch.flags = code;1236NativeTouch(touch);1237}12381239extern "C" jboolean Java_org_ppsspp_ppsspp_NativeApp_keyDown(JNIEnv *, jclass, jint deviceId, jint key, jboolean isRepeat) {1240if (!renderer_inited) {1241return false; // could probably return true here too..1242}1243if (key == 0 && deviceId >= DEVICE_ID_PAD_0 && deviceId <= DEVICE_ID_PAD_9) {1244// Ignore keycode 0 from pads. Stadia controllers seem to produce them when pressing L2/R2 for some reason, confusing things.1245return true; // need to eat the key so it doesn't go through legacy path1246}12471248KeyInput keyInput;1249keyInput.deviceId = (InputDeviceID)deviceId;1250keyInput.keyCode = (InputKeyCode)key;1251keyInput.flags = KEY_DOWN;1252if (isRepeat) {1253keyInput.flags |= KEY_IS_REPEAT;1254}1255return NativeKey(keyInput);1256}12571258extern "C" jboolean Java_org_ppsspp_ppsspp_NativeApp_keyUp(JNIEnv *, jclass, jint deviceId, jint key) {1259if (!renderer_inited) {1260return false; // could probably return true here too..1261}1262if (key == 0 && deviceId >= DEVICE_ID_PAD_0 && deviceId <= DEVICE_ID_PAD_9) {1263// Ignore keycode 0 from pads. Stadia controllers seem to produce them when pressing L2/R2 for some reason, confusing things.1264return true; // need to eat the key so it doesn't go through legacy path1265}12661267KeyInput keyInput;1268keyInput.deviceId = (InputDeviceID)deviceId;1269keyInput.keyCode = (InputKeyCode)key;1270keyInput.flags = KEY_UP;1271return NativeKey(keyInput);1272}12731274extern "C" void Java_org_ppsspp_ppsspp_NativeApp_joystickAxis(1275JNIEnv *env, jclass, jint deviceId, jintArray axisIds, jfloatArray values, jint count) {1276if (!renderer_inited)1277return;12781279AxisInput *axis = new AxisInput[count];1280_dbg_assert_(count <= env->GetArrayLength(axisIds));1281_dbg_assert_(count <= env->GetArrayLength(values));1282jint *axisIdBuffer = env->GetIntArrayElements(axisIds, NULL);1283jfloat *valueBuffer = env->GetFloatArrayElements(values, NULL);12841285// These are dirty-filtered on the Java side.1286for (int i = 0; i < count; i++) {1287axis[i].deviceId = (InputDeviceID)(int)deviceId;1288axis[i].axisId = (InputAxis)(int)axisIdBuffer[i];1289axis[i].value = valueBuffer[i];1290}1291NativeAxis(axis, count);1292delete[] axis;1293}12941295extern "C" jboolean Java_org_ppsspp_ppsspp_NativeApp_mouseWheelEvent(1296JNIEnv *env, jclass, jint stick, jfloat x, jfloat y) {1297if (!renderer_inited)1298return false;1299// TODO: Mousewheel should probably be an axis instead.1300int wheelDelta = y * 30.0f;1301if (wheelDelta > 500) wheelDelta = 500;1302if (wheelDelta < -500) wheelDelta = -500;13031304KeyInput key;1305key.deviceId = DEVICE_ID_MOUSE;1306if (wheelDelta < 0) {1307key.keyCode = NKCODE_EXT_MOUSEWHEEL_DOWN;1308wheelDelta = -wheelDelta;1309} else {1310key.keyCode = NKCODE_EXT_MOUSEWHEEL_UP;1311}1312// There's no separate keyup event for mousewheel events,1313// so we release it with a slight delay.1314key.flags = KEY_DOWN | KEY_HASWHEELDELTA | (wheelDelta << 16);1315NativeKey(key);1316return true;1317}13181319extern "C" void Java_org_ppsspp_ppsspp_NativeApp_mouseDelta(1320JNIEnv * env, jclass, jfloat x, jfloat y) {1321if (!renderer_inited)1322return;1323NativeMouseDelta(x, y);1324}13251326extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_accelerometer(JNIEnv *, jclass, float x, float y, float z) {1327if (!renderer_inited)1328return;1329NativeAccelerometer(x, y, z);1330}13311332extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_sendMessageFromJava(JNIEnv *env, jclass, jstring jmessage, jstring jparam) {1333std::string msg = GetJavaString(env, jmessage);1334std::string prm = GetJavaString(env, jparam);13351336// A bit ugly, see InputDeviceState.java.1337static InputDeviceID nextInputDeviceID = DEVICE_ID_ANY;13381339// Some messages are caught by app-android. TODO: Should be all.1340if (msg == "moga") {1341mogaVersion = prm;1342} else if (msg == "permission_pending") {1343INFO_LOG(Log::System, "STORAGE PERMISSION: PENDING");1344// TODO: Add support for other permissions1345permissions[SYSTEM_PERMISSION_STORAGE] = PERMISSION_STATUS_PENDING;1346// Don't need to send along, nothing else is listening.1347} else if (msg == "permission_denied") {1348INFO_LOG(Log::System, "STORAGE PERMISSION: DENIED");1349permissions[SYSTEM_PERMISSION_STORAGE] = PERMISSION_STATUS_DENIED;1350// Don't need to send along, nothing else is listening.1351} else if (msg == "permission_granted") {1352INFO_LOG(Log::System, "STORAGE PERMISSION: GRANTED");1353permissions[SYSTEM_PERMISSION_STORAGE] = PERMISSION_STATUS_GRANTED;1354// Send along.1355System_PostUIMessage(UIMessage::PERMISSION_GRANTED, prm);1356} else if (msg == "sustained_perf_supported") {1357sustainedPerfSupported = true;1358} else if (msg == "safe_insets") {1359// INFO_LOG(Log::System, "Got insets: %s", prm.c_str());1360// We don't bother with supporting exact rectangular regions. Safe insets are good enough.1361int left, right, top, bottom;1362if (4 == sscanf(prm.c_str(), "%d:%d:%d:%d", &left, &right, &top, &bottom)) {1363g_safeInsetLeft = (float)left * g_display.dpi_scale_x;1364g_safeInsetRight = (float)right * g_display.dpi_scale_x;1365g_safeInsetTop = (float)top * g_display.dpi_scale_y;1366g_safeInsetBottom = (float)bottom * g_display.dpi_scale_y;1367}1368} else if (msg == "inputDeviceConnectedID") {1369nextInputDeviceID = (InputDeviceID)parseLong(prm);1370} else if (msg == "inputDeviceConnected") {1371KeyMap::NotifyPadConnected(nextInputDeviceID, prm);1372} else if (msg == "core_powerSaving") {1373// Forward.1374System_PostUIMessage(UIMessage::POWER_SAVING, prm);1375} else if (msg == "exception") {1376g_OSD.Show(OSDType::MESSAGE_ERROR, std::string("Java Exception"), prm, 10.0f);1377} else if (msg == "shortcutParam") {1378if (prm.empty()) {1379WARN_LOG(Log::System, "shortcutParam empty");1380return;1381}1382INFO_LOG(Log::System, "shortcutParam received: %s", prm.c_str());1383System_PostUIMessage(UIMessage::REQUEST_GAME_BOOT, StripQuotes(prm));1384} else {1385ERROR_LOG(Log::System, "Got unexpected message from Java, ignoring: %s / %s", msg.c_str(), prm.c_str());1386}1387}13881389void correctRatio(int &sz_x, int &sz_y, float scale) {1390float x = (float)sz_x;1391float y = (float)sz_y;1392float ratio = x / y;1393INFO_LOG(Log::G3D, "CorrectRatio: Considering size: %0.2f/%0.2f=%0.2f for scale %f", x, y, ratio, scale);1394float targetRatio;13951396// Try to get the longest dimension to match scale*PSP resolution.1397if (x >= y) {1398targetRatio = 480.0f / 272.0f;1399x = 480.f * scale;1400y = 272.f * scale;1401} else {1402targetRatio = 272.0f / 480.0f;1403x = 272.0f * scale;1404y = 480.0f * scale;1405}14061407float correction = targetRatio / ratio;1408INFO_LOG(Log::G3D, "Target ratio: %0.2f ratio: %0.2f correction: %0.2f", targetRatio, ratio, correction);1409if (ratio < targetRatio) {1410y *= correction;1411} else {1412x /= correction;1413}14141415sz_x = x;1416sz_y = y;1417INFO_LOG(Log::G3D, "Corrected ratio: %dx%d", sz_x, sz_y);1418}14191420void getDesiredBackbufferSize(int &sz_x, int &sz_y) {1421sz_x = display_xres;1422sz_y = display_yres;14231424int scale = g_Config.iAndroidHwScale;1425// Override hw scale for TV type devices.1426if (System_GetPropertyInt(SYSPROP_DEVICE_TYPE) == DEVICE_TYPE_TV)1427scale = 0;14281429if (scale == 1) {1430// If g_Config.iInternalResolution is also set to Auto (1), we fall back to "Device resolution" (0). It works out.1431scale = g_Config.iInternalResolution;1432} else if (scale >= 2) {1433scale -= 1;1434}14351436int max_res = std::max(System_GetPropertyInt(SYSPROP_DISPLAY_XRES), System_GetPropertyInt(SYSPROP_DISPLAY_YRES)) / 480 + 1;14371438scale = std::min(scale, max_res);14391440if (scale > 0) {1441correctRatio(sz_x, sz_y, scale);1442} else {1443sz_x = 0;1444sz_y = 0;1445}1446}14471448extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_setDisplayParameters(JNIEnv *, jclass, jint xres, jint yres, jint dpi, jfloat refreshRate) {1449INFO_LOG(Log::G3D, "NativeApp.setDisplayParameters(%d x %d, dpi=%d, refresh=%0.2f)", xres, yres, dpi, refreshRate);14501451if (IsVREnabled()) {1452int width, height;1453GetVRResolutionPerEye(&width, &height);1454xres = width;1455yres = height * 272 / 480;1456dpi = 320;1457}14581459bool changed = false;1460changed = changed || display_xres != xres || display_yres != yres;1461changed = changed || display_dpi_x != dpi || display_dpi_y != dpi;1462changed = changed || g_display.display_hz != refreshRate;14631464if (changed) {1465display_xres = xres;1466display_yres = yres;1467display_dpi_x = dpi;1468display_dpi_y = dpi;1469g_display.display_hz = refreshRate;14701471recalculateDpi();1472NativeResized();1473}1474}14751476extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_computeDesiredBackbufferDimensions() {1477getDesiredBackbufferSize(desiredBackbufferSizeX, desiredBackbufferSizeY);1478}14791480extern "C" jint JNICALL Java_org_ppsspp_ppsspp_NativeApp_getDesiredBackbufferWidth(JNIEnv *, jclass) {1481return desiredBackbufferSizeX;1482}14831484extern "C" jint JNICALL Java_org_ppsspp_ppsspp_NativeApp_getDesiredBackbufferHeight(JNIEnv *, jclass) {1485return desiredBackbufferSizeY;1486}14871488extern "C" jint JNICALL Java_org_ppsspp_ppsspp_NativeApp_getDisplayFramerateMode(JNIEnv *, jclass) {1489return g_Config.iDisplayFramerateMode;1490}14911492std::vector<std::string> System_GetCameraDeviceList() {1493jclass cameraClass = findClass("org/ppsspp/ppsspp/CameraHelper");1494jmethodID deviceListMethod = getEnv()->GetStaticMethodID(cameraClass, "getDeviceList", "()Ljava/util/ArrayList;");1495jobject deviceListObject = getEnv()->CallStaticObjectMethod(cameraClass, deviceListMethod);1496jclass arrayListClass = getEnv()->FindClass("java/util/ArrayList");1497jmethodID arrayListSize = getEnv()->GetMethodID(arrayListClass, "size", "()I");1498jmethodID arrayListGet = getEnv()->GetMethodID(arrayListClass, "get", "(I)Ljava/lang/Object;");14991500jint arrayListObjectLen = getEnv()->CallIntMethod(deviceListObject, arrayListSize);1501std::vector<std::string> deviceListVector;15021503for (int i = 0; i < arrayListObjectLen; i++) {1504jstring dev = static_cast<jstring>(getEnv()->CallObjectMethod(deviceListObject, arrayListGet, i));1505const char *cdev = getEnv()->GetStringUTFChars(dev, nullptr);1506if (!cdev) {1507getEnv()->DeleteLocalRef(dev);1508continue;1509}1510deviceListVector.push_back(std::string(cdev));1511getEnv()->ReleaseStringUTFChars(dev, cdev);1512getEnv()->DeleteLocalRef(dev);1513}1514return deviceListVector;1515}15161517extern "C" jint Java_org_ppsspp_ppsspp_NativeApp_getSelectedCamera(JNIEnv *, jclass) {1518int cameraId = 0;1519sscanf(g_Config.sCameraDevice.c_str(), "%d:", &cameraId);1520return cameraId;1521}15221523extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_setGpsDataAndroid(JNIEnv *, jclass,1524jlong time, jfloat hdop, jfloat latitude, jfloat longitude, jfloat altitude, jfloat speed, jfloat bearing) {1525GPS::setGpsData(time, hdop, latitude, longitude, altitude, speed, bearing);1526}15271528extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_setSatInfoAndroid(JNIEnv *, jclass,1529jshort index, jshort id, jshort elevation, jshort azimuth, jshort snr, jshort good) {1530GPS::setSatInfo(index, id, elevation, azimuth, snr, good);1531}15321533extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_pushCameraImageAndroid(JNIEnv *env, jclass, jbyteArray image) {1534if (image != NULL) {1535jlong size = env->GetArrayLength(image);1536jbyte* buffer = env->GetByteArrayElements(image, NULL);1537Camera::pushCameraImage(size, (unsigned char *)buffer);1538env->ReleaseByteArrayElements(image, buffer, JNI_ABORT);1539}1540}15411542// Call this under frameCommandLock.1543static void ProcessFrameCommands(JNIEnv *env) {1544while (!frameCommands.empty()) {1545FrameCommand frameCmd;1546frameCmd = frameCommands.front();1547frameCommands.pop();15481549INFO_LOG(Log::System, "frameCommand '%s' '%s'", frameCmd.command.c_str(), frameCmd.params.c_str());15501551jstring cmd = env->NewStringUTF(frameCmd.command.c_str());1552jstring param = env->NewStringUTF(frameCmd.params.c_str());1553env->CallVoidMethod(nativeActivity, postCommand, cmd, param);1554env->DeleteLocalRef(cmd);1555env->DeleteLocalRef(param);1556}1557}15581559std::thread g_renderLoopThread;15601561static void VulkanEmuThread(ANativeWindow *wnd);15621563// This runs in Vulkan mode only.1564// This handles the entire lifecycle of the Vulkan context, init and exit.1565extern "C" bool JNICALL Java_org_ppsspp_ppsspp_NativeActivity_runVulkanRenderLoop(JNIEnv * env, jobject obj, jobject _surf) {1566_assert_(!useCPUThread);15671568if (!graphicsContext) {1569ERROR_LOG(Log::G3D, "runVulkanRenderLoop: Tried to enter without a created graphics context.");1570return false;1571}15721573if (g_renderLoopThread.joinable()) {1574ERROR_LOG(Log::G3D, "runVulkanRenderLoop: Already running");1575return false;1576}15771578ANativeWindow *wnd = _surf ? ANativeWindow_fromSurface(env, _surf) : nullptr;15791580if (!wnd) {1581// This shouldn't ever happen.1582ERROR_LOG(Log::G3D, "Error: Surface is null.");1583renderLoopRunning = false;1584return false;1585}15861587g_renderLoopThread = std::thread(VulkanEmuThread, wnd);1588return true;1589}15901591extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeActivity_requestExitVulkanRenderLoop(JNIEnv * env, jobject obj) {1592if (!renderLoopRunning) {1593ERROR_LOG(Log::System, "Render loop already exited");1594return;1595}1596_assert_(g_renderLoopThread.joinable());1597exitRenderLoop = true;1598g_renderLoopThread.join();1599_assert_(!g_renderLoopThread.joinable());1600g_renderLoopThread = std::thread();1601}16021603// TODO: Merge with the Win32 EmuThread and so on, and the Java EmuThread?1604static void VulkanEmuThread(ANativeWindow *wnd) {1605SetCurrentThreadName("EmuThread");16061607AndroidJNIThreadContext ctx;1608JNIEnv *env = getEnv();16091610if (!graphicsContext) {1611ERROR_LOG(Log::G3D, "runVulkanRenderLoop: Tried to enter without a created graphics context.");1612renderLoopRunning = false;1613exitRenderLoop = false;1614return;1615}16161617if (exitRenderLoop) {1618WARN_LOG(Log::G3D, "runVulkanRenderLoop: ExitRenderLoop requested at start, skipping the whole thing.");1619renderLoopRunning = false;1620exitRenderLoop = false;1621return;1622}16231624// This is up here to prevent race conditions, in case we pause during init.1625renderLoopRunning = true;16261627WARN_LOG(Log::G3D, "runVulkanRenderLoop. display_xres=%d display_yres=%d desiredBackbufferSizeX=%d desiredBackbufferSizeY=%d",1628display_xres, display_yres, desiredBackbufferSizeX, desiredBackbufferSizeY);16291630if (!graphicsContext->InitFromRenderThread(wnd, desiredBackbufferSizeX, desiredBackbufferSizeY, backbuffer_format, androidVersion)) {1631// On Android, if we get here, really no point in continuing.1632// The UI is supposed to render on any device both on OpenGL and Vulkan. If either of those don't work1633// on a device, we blacklist it. Hopefully we should have already failed in InitAPI anyway and reverted to GL back then.1634ERROR_LOG(Log::G3D, "Failed to initialize graphics context.");1635System_Toast("Failed to initialize graphics context.");16361637delete graphicsContext;1638graphicsContext = nullptr;1639renderLoopRunning = false;1640return;1641}16421643if (!exitRenderLoop) {1644if (!NativeInitGraphics(graphicsContext)) {1645ERROR_LOG(Log::G3D, "Failed to initialize graphics.");1646// Gonna be in a weird state here..1647}1648graphicsContext->ThreadStart();1649renderer_inited = true;16501651while (!exitRenderLoop) {1652{1653std::lock_guard<std::mutex> renderGuard(renderLock);1654NativeFrame(graphicsContext);1655}1656{1657std::lock_guard<std::mutex> guard(frameCommandLock);1658ProcessFrameCommands(env);1659}1660}1661INFO_LOG(Log::G3D, "Leaving Vulkan main loop.");1662} else {1663INFO_LOG(Log::G3D, "Not entering main loop.");1664}16651666NativeShutdownGraphics();16671668renderer_inited = false;1669graphicsContext->ThreadEnd();16701671// Shut the graphics context down to the same state it was in when we entered the render thread.1672INFO_LOG(Log::G3D, "Shutting down graphics context...");1673graphicsContext->ShutdownFromRenderThread();1674renderLoopRunning = false;1675exitRenderLoop = false;16761677WARN_LOG(Log::G3D, "Render loop function exited.");1678}16791680// NOTE: This is defunct and not working, due to how the Android storage functions currently require1681// a PpssppActivity specifically and we don't have one here.1682extern "C" jstring Java_org_ppsspp_ppsspp_ShortcutActivity_queryGameName(JNIEnv * env, jclass, jstring jpath) {1683bool teardownThreadManager = false;1684if (!g_threadManager.IsInitialized()) {1685INFO_LOG(Log::System, "No thread manager - initializing one");1686// Need a thread manager.1687teardownThreadManager = true;1688g_threadManager.Init(1, 1);1689}16901691Path path = Path(GetJavaString(env, jpath));16921693INFO_LOG(Log::System, "queryGameName(%s)", path.c_str());16941695std::string result = "";16961697GameInfoCache *cache = new GameInfoCache();1698std::shared_ptr<GameInfo> info = cache->GetInfo(nullptr, path, GameInfoFlags::PARAM_SFO);1699// Wait until it's done: this is synchronous, unfortunately.1700if (info) {1701INFO_LOG(Log::System, "GetInfo successful, waiting");1702while (!info->Ready(GameInfoFlags::PARAM_SFO)) {1703sleep_ms(1);1704}1705INFO_LOG(Log::System, "Done waiting");1706if (info->fileType != IdentifiedFileType::UNKNOWN) {1707result = info->GetTitle();17081709// Pretty arbitrary, but the home screen will often truncate titles.1710// Let's remove "The " from names since it's common in English titles.1711if (result.length() > strlen("The ") && startsWithNoCase(result, "The ")) {1712result = result.substr(strlen("The "));1713}17141715INFO_LOG(Log::System, "queryGameName: Got '%s'", result.c_str());1716} else {1717INFO_LOG(Log::System, "queryGameName: Filetype unknown");1718}1719} else {1720INFO_LOG(Log::System, "No info from cache");1721}1722delete cache;17231724if (teardownThreadManager) {1725g_threadManager.Teardown();1726}17271728return env->NewStringUTF(result.c_str());1729}173017311732extern "C"1733JNIEXPORT jbyteArray JNICALL1734Java_org_ppsspp_ppsspp_ShortcutActivity_queryGameIcon(JNIEnv * env, jclass clazz, jstring jpath) {1735bool teardownThreadManager = false;1736if (!g_threadManager.IsInitialized()) {1737INFO_LOG(Log::System, "No thread manager - initializing one");1738// Need a thread manager.1739teardownThreadManager = true;1740g_threadManager.Init(1, 1);1741}1742// TODO: implement requestIcon()17431744Path path = Path(GetJavaString(env, jpath));17451746INFO_LOG(Log::System, "queryGameIcon(%s)", path.c_str());17471748jbyteArray result = nullptr;17491750GameInfoCache *cache = new GameInfoCache();1751std::shared_ptr<GameInfo> info = cache->GetInfo(nullptr, path, GameInfoFlags::ICON);1752// Wait until it's done: this is synchronous, unfortunately.1753if (info) {1754INFO_LOG(Log::System, "GetInfo successful, waiting");1755int attempts = 1000;1756while (!info->Ready(GameInfoFlags::ICON)) {1757sleep_ms(1);1758attempts--;1759if (!attempts) {1760break;1761}1762}1763INFO_LOG(Log::System, "Done waiting");1764if (info->Ready(GameInfoFlags::ICON)) {1765if (!info->icon.data.empty()) {1766INFO_LOG(Log::System, "requestIcon: Got icon");1767result = env->NewByteArray(info->icon.data.size());1768env->SetByteArrayRegion(result, 0, info->icon.data.size(), (const jbyte *)info->icon.data.data());1769}1770} else {1771INFO_LOG(Log::System, "requestIcon: Filetype unknown");1772}1773} else {1774INFO_LOG(Log::System, "No info from cache");1775}17761777delete cache;17781779if (teardownThreadManager) {1780g_threadManager.Teardown();1781}17821783return result;1784}178517861787