mirror of
https://github.com/hrydgard/ppsspp.git
synced 2025-02-25 00:22:10 +00:00
1779 lines
56 KiB
C++
1779 lines
56 KiB
C++
// 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 <cstdlib>
|
|
#include <cstdint>
|
|
|
|
#include <sstream>
|
|
#include <queue>
|
|
#include <mutex>
|
|
#include <thread>
|
|
#include <atomic>
|
|
|
|
#ifndef _MSC_VER
|
|
|
|
#include <jni.h>
|
|
#include <android/native_window_jni.h>
|
|
#include <android/log.h>
|
|
|
|
#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;
|
|
typedef jobject jintArray;
|
|
typedef jobject jfloatArray;
|
|
|
|
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/OSD.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/KeyMap.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,
|
|
};
|
|
|
|
// OpenGL emu thread
|
|
static std::thread emuThread;
|
|
static std::atomic<int> emuThreadState((int)EmuThreadState::DISABLED);
|
|
|
|
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<FrameCommand> 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.
|
|
std::string g_nativeLibDir; // App native library dir
|
|
|
|
static std::vector<std::string> 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.
|
|
static int display_xres;
|
|
static 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.
|
|
extern JavaVM *gJvm;
|
|
|
|
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<bool> exitRenderLoop;
|
|
static std::atomic<bool> renderLoopRunning;
|
|
static bool renderer_inited = false;
|
|
static std::mutex renderLock;
|
|
|
|
static bool sustainedPerfSupported = false;
|
|
|
|
static std::map<SystemPermission, PermissionStatus> 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
|
|
|
|
static void ProcessFrameCommands(JNIEnv *env);
|
|
|
|
void AndroidLogger::Log(const LogMessage &message) {
|
|
int mode;
|
|
switch (message.level) {
|
|
case LogLevel::LWARNING:
|
|
mode = ANDROID_LOG_WARN;
|
|
break;
|
|
case LogLevel::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<jclass>(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) {
|
|
DEBUG_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) {
|
|
DEBUG_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() {
|
|
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?
|
|
JNIEnv *env;
|
|
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(5);
|
|
}
|
|
|
|
// 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) {
|
|
{
|
|
std::lock_guard<std::mutex> renderGuard(renderLock);
|
|
NativeFrame(graphicsContext);
|
|
}
|
|
|
|
std::lock_guard<std::mutex> guard(frameCommandLock);
|
|
if (!nativeActivity) {
|
|
ERROR_LOG(SYSTEM, "No activity, clearing commands");
|
|
while (!frameCommands.empty())
|
|
frameCommands.pop();
|
|
return;
|
|
}
|
|
// Still under lock here.
|
|
ProcessFrameCommands(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 PushCommand(std::string cmd, std::string param) {
|
|
std::lock_guard<std::mutex> guard(frameCommandLock);
|
|
frameCommands.push(FrameCommand(cmd, param));
|
|
}
|
|
|
|
// Android implementation of callbacks to the Java part of the app
|
|
void System_Toast(std::string_view text) {
|
|
PushCommand("toast", std::string(text));
|
|
}
|
|
|
|
void System_ShowKeyboard() {
|
|
PushCommand("showKeyboard", "");
|
|
}
|
|
|
|
void System_Vibrate(int length_ms) {
|
|
char temp[32];
|
|
snprintf(temp, sizeof(temp), "%d", length_ms);
|
|
PushCommand("vibrate", temp);
|
|
}
|
|
|
|
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;
|
|
case SYSPROP_BUILD_VERSION:
|
|
return PPSSPP_GIT_VERSION;
|
|
default:
|
|
return "";
|
|
}
|
|
}
|
|
|
|
std::vector<std::string> System_GetPropertyStringVec(SystemProperty prop) {
|
|
switch (prop) {
|
|
case SYSPROP_ADDITIONAL_STORAGE_DIRS:
|
|
return g_additionalStorageDirs;
|
|
|
|
case SYSPROP_TEMP_DIRS:
|
|
default:
|
|
return std::vector<std::string>();
|
|
}
|
|
}
|
|
|
|
int64_t 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_TEXT_INPUT_DIALOG:
|
|
return true;
|
|
case SYSPROP_HAS_OPEN_DIRECTORY:
|
|
return false; // We have this implemented but it may or may not work depending on if a file explorer is installed.
|
|
case SYSPROP_HAS_ADDITIONAL_STORAGE:
|
|
return !g_additionalStorageDirs.empty();
|
|
case SYSPROP_HAS_BACK_BUTTON:
|
|
return true;
|
|
case SYSPROP_HAS_IMAGE_BROWSER:
|
|
return deviceType != DEVICE_TYPE_VR;
|
|
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) && (deviceType != DEVICE_TYPE_VR); // 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 deviceType != DEVICE_TYPE_VR;
|
|
case SYSPROP_HAS_ACCELEROMETER:
|
|
return deviceType == DEVICE_TYPE_MOBILE;
|
|
case SYSPROP_CAN_CREATE_SHORTCUT:
|
|
return false; // We can't create shortcuts directly from game code, but we can from the Android UI.
|
|
#ifndef HTTPS_NOT_AVAILABLE
|
|
case SYSPROP_SUPPORTS_HTTPS:
|
|
return !g_Config.bDisableHTTPS;
|
|
#endif
|
|
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;
|
|
}
|
|
|
|
// Easy way for the Java side to ask the C++ side for configuration options, such as
|
|
// the rotation lock which must be controlled from Java on Android.
|
|
static std::string QueryConfig(std::string query) {
|
|
char temp[128];
|
|
if (query == "screenRotation") {
|
|
INFO_LOG(G3D, "g_Config.screenRotation = %d", g_Config.iScreenRotation);
|
|
snprintf(temp, sizeof(temp), "%d", g_Config.iScreenRotation);
|
|
return std::string(temp);
|
|
} else if (query == "immersiveMode") {
|
|
return std::string(g_Config.bImmersiveMode ? "1" : "0");
|
|
} else if (query == "sustainedPerformanceMode") {
|
|
return std::string(g_Config.bSustainedPerformanceMode ? "1" : "0");
|
|
} else if (query == "androidJavaGL") {
|
|
// If we're using Vulkan, we say no... need C++ to use Vulkan.
|
|
if (GetGPUBackend() == GPUBackend::VULKAN) {
|
|
return "false";
|
|
}
|
|
// Otherwise, some devices prefer the Java init so play it safe.
|
|
return "true";
|
|
} else {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
extern "C" jstring Java_org_ppsspp_ppsspp_NativeApp_queryConfig
|
|
(JNIEnv *env, jclass, jstring jquery) {
|
|
std::string query = GetJavaString(env, jquery);
|
|
std::string result = QueryConfig(query);
|
|
jstring jresult = env->NewStringUTF(result.c_str());
|
|
return jresult;
|
|
}
|
|
|
|
static void parse_args(std::vector<std::string> &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 jNativeLibDir, 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<std::mutex> guard(renderLock);
|
|
renderer_inited = false;
|
|
exitRenderLoop = 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);
|
|
std::string nativeLibDir = GetJavaString(env, jNativeLibDir);
|
|
|
|
g_externalDir = externalStorageDir;
|
|
g_extFilesDir = externalFilesDir;
|
|
g_nativeLibDir = nativeLibDir;
|
|
|
|
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<const char *> args;
|
|
std::vector<std::string> temp;
|
|
args.push_back(app_name.c_str());
|
|
if (!shortcut_param.empty()) {
|
|
EARLY_LOG("NativeInit shortcut param %s", shortcut_param.c_str());
|
|
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 VulkanMaybeAvailable).
|
|
// 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);
|
|
|
|
System_PostUIMessage(UIMessage::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) {
|
|
INFO_LOG(SYSTEM, "NativeApp.shutdown() -- begin");
|
|
|
|
if (renderer_inited && useCPUThread && graphicsContext) {
|
|
// Only used in Java EGL path.
|
|
|
|
// We can't lock renderLock here because the emu thread will be in NativeFrame
|
|
// which locks renderLock already, and only gets out once we call ThreadFrame()
|
|
// in a loop before, to empty the queue.
|
|
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();
|
|
}
|
|
|
|
{
|
|
std::lock_guard<std::mutex> guard(renderLock);
|
|
|
|
if (graphicsContext) {
|
|
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");
|
|
}
|
|
|
|
NativeShutdown();
|
|
g_VFS.Clear();
|
|
}
|
|
|
|
{
|
|
std::lock_guard<std::mutex> 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) {
|
|
g_OSD.Show(OSDType::MESSAGE_ERROR, details, 5.0);
|
|
}, 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) {
|
|
g_OSD.Show(OSDType::MESSAGE_ERROR, details, 5.0);
|
|
}, nullptr);
|
|
|
|
graphicsContext->ThreadStart();
|
|
renderer_inited = true;
|
|
}
|
|
|
|
System_PostUIMessage(UIMessage::RECREATE_VIEWS);
|
|
|
|
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;
|
|
case SystemNotification::TEST_JAVA_EXCEPTION:
|
|
PushCommand("testException", "This is a test exception");
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
bool System_MakeRequest(SystemRequestType type, int requestId, const std::string ¶m1, const std::string ¶m2, int64_t param3, int64_t param4) {
|
|
switch (type) {
|
|
case SystemRequestType::EXIT_APP:
|
|
PushCommand("finish", "");
|
|
return true;
|
|
case SystemRequestType::RESTART_APP:
|
|
PushCommand("graphics_restart", param1);
|
|
return true;
|
|
case SystemRequestType::RECREATE_ACTIVITY:
|
|
PushCommand("recreate", 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:
|
|
{
|
|
BrowseFileType fileType = (BrowseFileType)param3;
|
|
std::string params = StringFromFormat("%d", requestId);
|
|
switch (fileType) {
|
|
case BrowseFileType::SOUND_EFFECT:
|
|
PushCommand("browse_file_audio", params);
|
|
break;
|
|
case BrowseFileType::ZIP:
|
|
PushCommand("browse_file_zip", params);
|
|
break;
|
|
default:
|
|
PushCommand("browse_file", params);
|
|
break;
|
|
}
|
|
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::INFRARED_COMMAND:
|
|
PushCommand("infrared_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::SET_KEEP_SCREEN_BRIGHT:
|
|
PushCommand("set_keep_screen_bright", param3 ? "on" : "off");
|
|
return true;
|
|
case SystemRequestType::SHOW_FILE_IN_FOLDER:
|
|
PushCommand("show_folder", 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 = jvalue ? GetJavaString(env, jvalue) : "(no value)";
|
|
INFO_LOG(SYSTEM, "Received result of request %d from Java: %d: %d '%s'", jrequestID, (int)result, jintValue, value.c_str());
|
|
if (result) {
|
|
g_requestManager.PostSystemSuccess(jrequestID, value.c_str());
|
|
} else {
|
|
g_requestManager.PostSystemFailure(jrequestID);
|
|
}
|
|
}
|
|
|
|
extern "C" void Java_org_ppsspp_ppsspp_NativeRenderer_displayRender(JNIEnv *env, jobject obj) {
|
|
// This doesn't get called on the Vulkan path.
|
|
_assert_(useCPUThread);
|
|
|
|
static bool hasSetThreadName = false;
|
|
if (!hasSetThreadName) {
|
|
hasSetThreadName = true;
|
|
SetCurrentThreadName("AndroidRender");
|
|
}
|
|
|
|
if (IsVREnabled() && !StartVRRender())
|
|
return;
|
|
|
|
// This is the "GPU thread". Call ThreadFrame.
|
|
if (!graphicsContext || !graphicsContext->ThreadFrame()) {
|
|
return;
|
|
}
|
|
|
|
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) {
|
|
if (!renderer_inited)
|
|
return;
|
|
TouchInput touch;
|
|
touch.id = pointerId;
|
|
touch.x = x * g_display.dpi_scale_x;
|
|
touch.y = y * g_display.dpi_scale_y;
|
|
touch.flags = code;
|
|
NativeTouch(touch);
|
|
}
|
|
|
|
extern "C" jboolean Java_org_ppsspp_ppsspp_NativeApp_keyDown(JNIEnv *, jclass, jint deviceId, jint key, jboolean isRepeat) {
|
|
if (!renderer_inited) {
|
|
return false; // could probably return true here too..
|
|
}
|
|
if (key == 0 && deviceId >= DEVICE_ID_PAD_0 && deviceId <= DEVICE_ID_PAD_9) {
|
|
// Ignore keycode 0 from pads. Stadia controllers seem to produce them when pressing L2/R2 for some reason, confusing things.
|
|
return true; // need to eat the key so it doesn't go through legacy path
|
|
}
|
|
|
|
KeyInput keyInput;
|
|
keyInput.deviceId = (InputDeviceID)deviceId;
|
|
keyInput.keyCode = (InputKeyCode)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) {
|
|
if (!renderer_inited) {
|
|
return false; // could probably return true here too..
|
|
}
|
|
if (key == 0 && deviceId >= DEVICE_ID_PAD_0 && deviceId <= DEVICE_ID_PAD_9) {
|
|
// Ignore keycode 0 from pads. Stadia controllers seem to produce them when pressing L2/R2 for some reason, confusing things.
|
|
return true; // need to eat the key so it doesn't go through legacy path
|
|
}
|
|
|
|
KeyInput keyInput;
|
|
keyInput.deviceId = (InputDeviceID)deviceId;
|
|
keyInput.keyCode = (InputKeyCode)key;
|
|
keyInput.flags = KEY_UP;
|
|
return NativeKey(keyInput);
|
|
}
|
|
|
|
extern "C" void Java_org_ppsspp_ppsspp_NativeApp_joystickAxis(
|
|
JNIEnv *env, jclass, jint deviceId, jintArray axisIds, jfloatArray values, jint count) {
|
|
if (!renderer_inited)
|
|
return;
|
|
|
|
AxisInput *axis = new AxisInput[count];
|
|
_dbg_assert_(count <= env->GetArrayLength(axisIds));
|
|
_dbg_assert_(count <= env->GetArrayLength(values));
|
|
jint *axisIdBuffer = env->GetIntArrayElements(axisIds, NULL);
|
|
jfloat *valueBuffer = env->GetFloatArrayElements(values, NULL);
|
|
|
|
// These are dirty-filtered on the Java side.
|
|
for (int i = 0; i < count; i++) {
|
|
axis[i].deviceId = (InputDeviceID)(int)deviceId;
|
|
axis[i].axisId = (InputAxis)(int)axisIdBuffer[i];
|
|
axis[i].value = valueBuffer[i];
|
|
}
|
|
NativeAxis(axis, count);
|
|
delete[] 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: Mousewheel should probably be an axis instead.
|
|
int wheelDelta = y * 30.0f;
|
|
if (wheelDelta > 500) wheelDelta = 500;
|
|
if (wheelDelta < -500) wheelDelta = -500;
|
|
|
|
KeyInput key;
|
|
key.deviceId = DEVICE_ID_MOUSE;
|
|
if (wheelDelta < 0) {
|
|
key.keyCode = NKCODE_EXT_MOUSEWHEEL_DOWN;
|
|
wheelDelta = -wheelDelta;
|
|
} else {
|
|
key.keyCode = NKCODE_EXT_MOUSEWHEEL_UP;
|
|
}
|
|
// There's no separate keyup event for mousewheel events,
|
|
// so we release it with a slight delay.
|
|
key.flags = KEY_DOWN | KEY_HASWHEELDELTA | (wheelDelta << 16);
|
|
NativeKey(key);
|
|
return true;
|
|
}
|
|
|
|
extern "C" void Java_org_ppsspp_ppsspp_NativeApp_mouseDelta(
|
|
JNIEnv * env, jclass, jfloat x, jfloat y) {
|
|
if (!renderer_inited)
|
|
return;
|
|
NativeMouseDelta(x, y);
|
|
}
|
|
|
|
extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_accelerometer(JNIEnv *, jclass, float x, float y, float z) {
|
|
if (!renderer_inited)
|
|
return;
|
|
NativeAccelerometer(x, y, z);
|
|
}
|
|
|
|
extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_sendMessageFromJava(JNIEnv *env, jclass, jstring jmessage, jstring jparam) {
|
|
std::string msg = GetJavaString(env, jmessage);
|
|
std::string prm = GetJavaString(env, jparam);
|
|
|
|
// A bit ugly, see InputDeviceState.java.
|
|
static InputDeviceID nextInputDeviceID = DEVICE_ID_ANY;
|
|
|
|
// 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;
|
|
// Don't need to send along, nothing else is listening.
|
|
} else if (msg == "permission_denied") {
|
|
INFO_LOG(SYSTEM, "STORAGE PERMISSION: DENIED");
|
|
permissions[SYSTEM_PERMISSION_STORAGE] = PERMISSION_STATUS_DENIED;
|
|
// Don't need to send along, nothing else is listening.
|
|
} else if (msg == "permission_granted") {
|
|
INFO_LOG(SYSTEM, "STORAGE PERMISSION: GRANTED");
|
|
permissions[SYSTEM_PERMISSION_STORAGE] = PERMISSION_STATUS_GRANTED;
|
|
// Send along.
|
|
System_PostUIMessage(UIMessage::PERMISSION_GRANTED, prm);
|
|
} 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;
|
|
}
|
|
} else if (msg == "inputDeviceConnectedID") {
|
|
nextInputDeviceID = (InputDeviceID)parseLong(prm);
|
|
} else if (msg == "inputDeviceConnected") {
|
|
KeyMap::NotifyPadConnected(nextInputDeviceID, prm);
|
|
} else if (msg == "core_powerSaving") {
|
|
// Forward.
|
|
System_PostUIMessage(UIMessage::POWER_SAVING, prm);
|
|
} else if (msg == "exception") {
|
|
g_OSD.Show(OSDType::MESSAGE_ERROR, std::string("Java Exception"), prm, 10.0f);
|
|
} else if (msg == "shortcutParam") {
|
|
if (prm.empty()) {
|
|
WARN_LOG(SYSTEM, "shortcutParam empty");
|
|
return;
|
|
}
|
|
INFO_LOG(SYSTEM, "shortcutParam received: %s", prm.c_str());
|
|
System_PostUIMessage(UIMessage::REQUEST_GAME_BOOT, StripQuotes(prm));
|
|
} else {
|
|
ERROR_LOG(SYSTEM, "Got unexpected message from Java, ignoring: %s / %s", msg.c_str(), prm.c_str());
|
|
}
|
|
}
|
|
|
|
void correctRatio(int &sz_x, int &sz_y, float scale) {
|
|
float x = (float)sz_x;
|
|
float y = (float)sz_y;
|
|
float ratio = x / y;
|
|
INFO_LOG(G3D, "CorrectRatio: Considering size: %0.2f/%0.2f=%0.2f for scale %f", x, y, ratio, scale);
|
|
float targetRatio;
|
|
|
|
// Try to get the longest dimension to match scale*PSP resolution.
|
|
if (x >= y) {
|
|
targetRatio = 480.0f / 272.0f;
|
|
x = 480.f * scale;
|
|
y = 272.f * scale;
|
|
} else {
|
|
targetRatio = 272.0f / 480.0f;
|
|
x = 272.0f * scale;
|
|
y = 480.0f * scale;
|
|
}
|
|
|
|
float correction = targetRatio / ratio;
|
|
INFO_LOG(G3D, "Target ratio: %0.2f ratio: %0.2f correction: %0.2f", targetRatio, ratio, correction);
|
|
if (ratio < targetRatio) {
|
|
y *= correction;
|
|
} else {
|
|
x /= correction;
|
|
}
|
|
|
|
sz_x = x;
|
|
sz_y = y;
|
|
INFO_LOG(G3D, "Corrected ratio: %dx%d", sz_x, sz_y);
|
|
}
|
|
|
|
void getDesiredBackbufferSize(int &sz_x, int &sz_y) {
|
|
sz_x = display_xres;
|
|
sz_y = display_yres;
|
|
|
|
int scale = g_Config.iAndroidHwScale;
|
|
// Override hw scale for TV type devices.
|
|
if (System_GetPropertyInt(SYSPROP_DEVICE_TYPE) == DEVICE_TYPE_TV)
|
|
scale = 0;
|
|
|
|
if (scale == 1) {
|
|
// If g_Config.iInternalResolution is also set to Auto (1), we fall back to "Device resolution" (0). It works out.
|
|
scale = g_Config.iInternalResolution;
|
|
} else if (scale >= 2) {
|
|
scale -= 1;
|
|
}
|
|
|
|
int max_res = std::max(System_GetPropertyInt(SYSPROP_DISPLAY_XRES), System_GetPropertyInt(SYSPROP_DISPLAY_YRES)) / 480 + 1;
|
|
|
|
scale = std::min(scale, max_res);
|
|
|
|
if (scale > 0) {
|
|
correctRatio(sz_x, sz_y, scale);
|
|
} else {
|
|
sz_x = 0;
|
|
sz_y = 0;
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_computeDesiredBackbufferDimensions() {
|
|
getDesiredBackbufferSize(desiredBackbufferSizeX, desiredBackbufferSizeY);
|
|
}
|
|
|
|
extern "C" jint JNICALL Java_org_ppsspp_ppsspp_NativeApp_getDesiredBackbufferWidth(JNIEnv *, jclass) {
|
|
return desiredBackbufferSizeX;
|
|
}
|
|
|
|
extern "C" jint JNICALL Java_org_ppsspp_ppsspp_NativeApp_getDesiredBackbufferHeight(JNIEnv *, jclass) {
|
|
return desiredBackbufferSizeY;
|
|
}
|
|
|
|
extern "C" jint JNICALL Java_org_ppsspp_ppsspp_NativeApp_getDisplayFramerateMode(JNIEnv *, jclass) {
|
|
return g_Config.iDisplayFramerateMode;
|
|
}
|
|
|
|
std::vector<std::string> 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<std::string> deviceListVector;
|
|
|
|
for (int i = 0; i < arrayListObjectLen; i++) {
|
|
jstring dev = static_cast<jstring>(getEnv()->CallObjectMethod(deviceListObject, arrayListGet, i));
|
|
const char *cdev = getEnv()->GetStringUTFChars(dev, nullptr);
|
|
if (!cdev) {
|
|
getEnv()->DeleteLocalRef(dev);
|
|
continue;
|
|
}
|
|
deviceListVector.push_back(std::string(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);
|
|
}
|
|
}
|
|
|
|
std::thread g_renderLoopThread;
|
|
|
|
static void VulkanEmuThread(ANativeWindow *wnd);
|
|
|
|
// This runs in Vulkan mode only.
|
|
// This handles the entire lifecycle of the Vulkan context, init and exit.
|
|
extern "C" 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;
|
|
}
|
|
|
|
if (g_renderLoopThread.joinable()) {
|
|
ERROR_LOG(G3D, "runVulkanRenderLoop: Already running");
|
|
return false;
|
|
}
|
|
|
|
ANativeWindow *wnd = _surf ? ANativeWindow_fromSurface(env, _surf) : nullptr;
|
|
|
|
if (!wnd) {
|
|
// This shouldn't ever happen.
|
|
ERROR_LOG(G3D, "Error: Surface is null.");
|
|
renderLoopRunning = false;
|
|
return false;
|
|
}
|
|
|
|
g_renderLoopThread = std::thread(VulkanEmuThread, wnd);
|
|
return true;
|
|
}
|
|
|
|
extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeActivity_requestExitVulkanRenderLoop(JNIEnv * env, jobject obj) {
|
|
if (!renderLoopRunning) {
|
|
ERROR_LOG(SYSTEM, "Render loop already exited");
|
|
return;
|
|
}
|
|
_assert_(g_renderLoopThread.joinable());
|
|
exitRenderLoop = true;
|
|
g_renderLoopThread.join();
|
|
_assert_(!g_renderLoopThread.joinable());
|
|
g_renderLoopThread = std::thread();
|
|
}
|
|
|
|
// TODO: Merge with the Win32 EmuThread and so on, and the Java EmuThread?
|
|
static void VulkanEmuThread(ANativeWindow *wnd) {
|
|
SetCurrentThreadName("EmuThread");
|
|
|
|
AndroidJNIThreadContext ctx;
|
|
JNIEnv *env = getEnv();
|
|
|
|
if (!graphicsContext) {
|
|
ERROR_LOG(G3D, "runVulkanRenderLoop: Tried to enter without a created graphics context.");
|
|
renderLoopRunning = false;
|
|
exitRenderLoop = false;
|
|
return;
|
|
}
|
|
|
|
if (exitRenderLoop) {
|
|
WARN_LOG(G3D, "runVulkanRenderLoop: ExitRenderLoop requested at start, skipping the whole thing.");
|
|
renderLoopRunning = false;
|
|
exitRenderLoop = false;
|
|
return;
|
|
}
|
|
|
|
// This is up here to prevent race conditions, in case we pause during init.
|
|
renderLoopRunning = true;
|
|
|
|
WARN_LOG(G3D, "runVulkanRenderLoop. display_xres=%d display_yres=%d desiredBackbufferSizeX=%d desiredBackbufferSizeY=%d",
|
|
display_xres, display_yres, desiredBackbufferSizeX, desiredBackbufferSizeY);
|
|
|
|
if (!graphicsContext->InitFromRenderThread(wnd, desiredBackbufferSizeX, desiredBackbufferSizeY, backbuffer_format, androidVersion)) {
|
|
// 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;
|
|
}
|
|
|
|
if (!exitRenderLoop) {
|
|
if (!NativeInitGraphics(graphicsContext)) {
|
|
ERROR_LOG(G3D, "Failed to initialize graphics.");
|
|
// Gonna be in a weird state here..
|
|
}
|
|
graphicsContext->ThreadStart();
|
|
renderer_inited = true;
|
|
|
|
while (!exitRenderLoop) {
|
|
{
|
|
std::lock_guard<std::mutex> renderGuard(renderLock);
|
|
NativeFrame(graphicsContext);
|
|
}
|
|
{
|
|
std::lock_guard<std::mutex> guard(frameCommandLock);
|
|
ProcessFrameCommands(env);
|
|
}
|
|
}
|
|
INFO_LOG(G3D, "Leaving Vulkan main loop.");
|
|
} else {
|
|
INFO_LOG(G3D, "Not entering main 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...");
|
|
graphicsContext->ShutdownFromRenderThread();
|
|
renderLoopRunning = false;
|
|
exitRenderLoop = false;
|
|
|
|
WARN_LOG(G3D, "Render loop function exited.");
|
|
}
|
|
|
|
// 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<GameInfo> info = cache->GetInfo(nullptr, path, GameInfoFlags::PARAM_SFO);
|
|
// Wait until it's done: this is synchronous, unfortunately.
|
|
if (info) {
|
|
INFO_LOG(SYSTEM, "GetInfo successful, waiting");
|
|
while (!info->Ready(GameInfoFlags::PARAM_SFO)) {
|
|
sleep_ms(1);
|
|
}
|
|
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());
|
|
}
|
|
|
|
|
|
extern "C"
|
|
JNIEXPORT jbyteArray JNICALL
|
|
Java_org_ppsspp_ppsspp_ShortcutActivity_queryGameIcon(JNIEnv * env, jclass clazz, 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);
|
|
}
|
|
// TODO: implement requestIcon()
|
|
|
|
Path path = Path(GetJavaString(env, jpath));
|
|
|
|
INFO_LOG(SYSTEM, "queryGameIcon(%s)", path.c_str());
|
|
|
|
jbyteArray result = nullptr;
|
|
|
|
GameInfoCache *cache = new GameInfoCache();
|
|
std::shared_ptr<GameInfo> info = cache->GetInfo(nullptr, path, GameInfoFlags::ICON);
|
|
// Wait until it's done: this is synchronous, unfortunately.
|
|
if (info) {
|
|
INFO_LOG(SYSTEM, "GetInfo successful, waiting");
|
|
int attempts = 1000;
|
|
while (!info->Ready(GameInfoFlags::ICON)) {
|
|
sleep_ms(1);
|
|
attempts--;
|
|
if (!attempts) {
|
|
break;
|
|
}
|
|
}
|
|
INFO_LOG(SYSTEM, "Done waiting");
|
|
if (info->Ready(GameInfoFlags::ICON)) {
|
|
if (!info->icon.data.empty()) {
|
|
INFO_LOG(SYSTEM, "requestIcon: Got icon");
|
|
result = env->NewByteArray(info->icon.data.size());
|
|
env->SetByteArrayRegion(result, 0, info->icon.data.size(), (const jbyte *)info->icon.data.data());
|
|
}
|
|
} else {
|
|
INFO_LOG(SYSTEM, "requestIcon: Filetype unknown");
|
|
}
|
|
} else {
|
|
INFO_LOG(SYSTEM, "No info from cache");
|
|
}
|
|
|
|
delete cache;
|
|
|
|
if (teardownThreadManager) {
|
|
g_threadManager.Teardown();
|
|
}
|
|
|
|
return result;
|
|
}
|