Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
hrydgard
GitHub Repository: hrydgard/ppsspp
Path: blob/master/android/jni/app-android.cpp
3185 views
1
// This is generic code that is included in all Android apps that use the
2
// Native framework by Henrik Rydgard (https://github.com/hrydgard/native).
3
4
// It calls a set of methods defined in NativeApp.h. These should be implemented
5
// by your game or app.
6
7
#include <cstdlib>
8
#include <cstdint>
9
10
#include <sstream>
11
#include <queue>
12
#include <mutex>
13
#include <thread>
14
#include <atomic>
15
16
#ifndef _MSC_VER
17
18
#include <jni.h>
19
#include <android/native_window_jni.h>
20
#include <android/log.h>
21
22
#elif !defined(JNIEXPORT)
23
// Just for better highlighting in MSVC if opening this file.
24
// Not having types makes it get confused and say everything is wrong.
25
struct JavaVM;
26
typedef void *jmethodID;
27
typedef void *jfieldID;
28
29
typedef uint8_t jboolean;
30
typedef int8_t jbyte;
31
typedef int16_t jshort;
32
typedef int32_t jint;
33
typedef int64_t jlong;
34
typedef jint jsize;
35
typedef float jfloat;
36
typedef double jdouble;
37
38
class _jobject {};
39
class _jclass : public _jobject {};
40
typedef _jobject *jobject;
41
typedef _jclass *jclass;
42
typedef jobject jstring;
43
typedef jobject jbyteArray;
44
typedef jobject jintArray;
45
typedef jobject jfloatArray;
46
47
struct JNIEnv {};
48
49
#define JNIEXPORT
50
#define JNICALL
51
// Just a random value to make MSVC highlighting happy.
52
#define JNI_VERSION_1_6 16
53
#endif
54
55
#include "Common/Log.h"
56
#include "Common/LogReporting.h"
57
58
#include "Common/Net/Resolve.h"
59
#include "Common/Net/URL.h"
60
#include "android/jni/AndroidAudio.h"
61
#include "Common/GPU/OpenGL/GLCommon.h"
62
#include "Common/GPU/OpenGL/GLFeatures.h"
63
64
#include "Common/System/Display.h"
65
#include "Common/System/NativeApp.h"
66
#include "Common/System/System.h"
67
#include "Common/System/OSD.h"
68
#include "Common/System/Request.h"
69
#include "Common/Thread/ThreadUtil.h"
70
#include "Common/File/Path.h"
71
#include "Common/File/DirListing.h"
72
#include "Common/File/VFS/VFS.h"
73
#include "Common/File/VFS/DirectoryReader.h"
74
#include "Common/File/VFS/ZipFileReader.h"
75
#include "Common/File/AndroidStorage.h"
76
#include "Common/Input/InputState.h"
77
#include "Common/Input/KeyCodes.h"
78
#include "Common/Profiler/Profiler.h"
79
#include "Common/Math/math_util.h"
80
#include "Common/Data/Text/Parsers.h"
81
#include "Common/VR/PPSSPPVR.h"
82
#include "Common/GPU/Vulkan/VulkanLoader.h"
83
84
#include "Common/GraphicsContext.h"
85
#include "Common/StringUtils.h"
86
#include "Common/TimeUtil.h"
87
88
#include "AndroidGraphicsContext.h"
89
#include "AndroidVulkanContext.h"
90
#include "AndroidJavaGLContext.h"
91
92
#include "Core/Config.h"
93
#include "Core/ConfigValues.h"
94
#include "Core/Loaders.h"
95
#include "Core/FileLoaders/LocalFileLoader.h"
96
#include "Core/KeyMap.h"
97
#include "Core/System.h"
98
#include "Core/HLE/sceUsbCam.h"
99
#include "Core/HLE/sceUsbGps.h"
100
#include "Common/CPUDetect.h"
101
#include "Common/Log.h"
102
#include "UI/GameInfoCache.h"
103
104
#include "app-android.h"
105
106
bool useCPUThread = true;
107
108
enum class EmuThreadState {
109
DISABLED,
110
START_REQUESTED,
111
RUNNING,
112
QUIT_REQUESTED,
113
STOPPED,
114
};
115
116
// OpenGL emu thread
117
static std::thread emuThread;
118
static std::atomic<int> emuThreadState((int)EmuThreadState::DISABLED);
119
120
AndroidAudioState *g_audioState;
121
122
struct FrameCommand {
123
FrameCommand() {}
124
FrameCommand(std::string cmd, std::string prm) : command(cmd), params(prm) {}
125
126
std::string command;
127
std::string params;
128
};
129
130
static std::mutex frameCommandLock;
131
static std::queue<FrameCommand> frameCommands;
132
133
static std::string systemName;
134
static std::string langRegion;
135
static std::string mogaVersion;
136
static std::string boardName;
137
138
std::string g_externalDir; // Original external dir (root of Android storage).
139
std::string g_extFilesDir; // App private external dir.
140
std::string g_nativeLibDir; // App native library dir
141
142
static std::vector<std::string> g_additionalStorageDirs;
143
144
static int optimalFramesPerBuffer = 0;
145
static int optimalSampleRate = 0;
146
static int sampleRate = 0;
147
static int framesPerBuffer = 0;
148
static int androidVersion;
149
static int deviceType;
150
151
// This is the ACTUAL display size, not the hardware scaled display size.
152
static int display_xres;
153
static int display_yres;
154
static int display_dpi;
155
static float display_scale_x; // Scale factor due to backbuffer scaling
156
static float display_scale_y;
157
158
static int backbuffer_format; // Android PixelFormat enum
159
160
static int desiredBackbufferSizeX;
161
static int desiredBackbufferSizeY;
162
163
// Cache the class loader so we can use it from native threads. Required for TextAndroid.
164
extern JavaVM *gJvm;
165
166
static jobject gClassLoader;
167
static jmethodID gFindClassMethod;
168
169
static float g_safeInsetLeft = 0.0;
170
static float g_safeInsetRight = 0.0;
171
static float g_safeInsetTop = 0.0;
172
static float g_safeInsetBottom = 0.0;
173
174
static jmethodID postCommand;
175
static jmethodID getDebugString;
176
177
static jobject nativeActivity;
178
179
static std::atomic<bool> exitRenderLoop;
180
static std::atomic<bool> renderLoopRunning;
181
static bool renderer_inited = false;
182
static std::mutex renderLock;
183
184
static bool sustainedPerfSupported = false;
185
186
static std::map<SystemPermission, PermissionStatus> permissions;
187
188
static AndroidGraphicsContext *graphicsContext;
189
190
#define MessageBox(a, b, c, d) __android_log_print(ANDROID_LOG_INFO, APP_NAME, "%s %s", (b), (c));
191
192
#if PPSSPP_ARCH(ARMV7)
193
// Old Android workaround
194
extern "C" {
195
int utimensat(int fd, const char *path, const struct timespec times[2]) {
196
return -1;
197
}
198
}
199
#endif
200
201
static void ProcessFrameCommands(JNIEnv *env);
202
203
JNIEnv* getEnv() {
204
JNIEnv *env;
205
int status = gJvm->GetEnv((void**)&env, JNI_VERSION_1_6);
206
_assert_msg_(status >= 0, "'%s': Can only call getEnv if you've attached the thread already!", GetCurrentThreadName());
207
return env;
208
}
209
210
jclass findClass(const char* name) {
211
return static_cast<jclass>(getEnv()->CallObjectMethod(gClassLoader, gFindClassMethod, getEnv()->NewStringUTF(name)));
212
}
213
214
void Android_AttachThreadToJNI() {
215
JNIEnv *env;
216
int status = gJvm->GetEnv((void **)&env, JNI_VERSION_1_6);
217
if (status < 0) {
218
DEBUG_LOG(Log::System, "Attaching thread '%s' (not already attached) to JNI.", GetCurrentThreadName());
219
JavaVMAttachArgs args{};
220
args.version = JNI_VERSION_1_6;
221
args.name = GetCurrentThreadName();
222
status = gJvm->AttachCurrentThread(&env, &args);
223
224
if (status < 0) {
225
// bad, but what can we do other than report..
226
ERROR_LOG(Log::System, "Failed to attach thread %s to JNI.", GetCurrentThreadName());
227
}
228
} else {
229
WARN_LOG(Log::System, "Thread %s was already attached to JNI.", GetCurrentThreadName());
230
}
231
}
232
233
void Android_DetachThreadFromJNI() {
234
if (gJvm->DetachCurrentThread() == JNI_OK) {
235
DEBUG_LOG(Log::System, "Detached thread from JNI: '%s'", GetCurrentThreadName());
236
} else {
237
WARN_LOG(Log::System, "Failed to detach thread '%s' from JNI - never attached?", GetCurrentThreadName());
238
}
239
}
240
241
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *pjvm, void *reserved) {
242
INFO_LOG(Log::System, "JNI_OnLoad");
243
gJvm = pjvm; // cache the JavaVM pointer
244
auto env = getEnv();
245
//replace with one of your classes in the line below
246
auto randomClass = env->FindClass("org/ppsspp/ppsspp/NativeActivity");
247
jclass classClass = env->GetObjectClass(randomClass);
248
auto classLoaderClass = env->FindClass("java/lang/ClassLoader");
249
auto getClassLoaderMethod = env->GetMethodID(classClass, "getClassLoader",
250
"()Ljava/lang/ClassLoader;");
251
gClassLoader = env->NewGlobalRef(env->CallObjectMethod(randomClass, getClassLoaderMethod));
252
gFindClassMethod = env->GetMethodID(classLoaderClass, "findClass",
253
"(Ljava/lang/String;)Ljava/lang/Class;");
254
255
RegisterAttachDetach(&Android_AttachThreadToJNI, &Android_DetachThreadFromJNI);
256
257
TimeInit();
258
return JNI_VERSION_1_6;
259
}
260
261
// Only used in OpenGL mode.
262
static void EmuThreadFunc() {
263
SetCurrentThreadName("EmuThread");
264
265
// Name the thread in the JVM, because why not (might result in better debug output in Play Console).
266
// TODO: Do something clever with getEnv() and stored names from SetCurrentThreadName?
267
JNIEnv *env;
268
JavaVMAttachArgs args{};
269
args.version = JNI_VERSION_1_6;
270
args.name = "EmuThread";
271
gJvm->AttachCurrentThread(&env, &args);
272
273
INFO_LOG(Log::System, "Entering emu thread");
274
275
// Wait for render loop to get started.
276
INFO_LOG(Log::System, "Runloop: Waiting for displayInit...");
277
while (!graphicsContext || graphicsContext->GetState() == GraphicsContextState::PENDING) {
278
sleep_ms(5, "graphics-poll");
279
}
280
281
// Check the state of the graphics context before we try to feed it into NativeInitGraphics.
282
if (graphicsContext->GetState() != GraphicsContextState::INITIALIZED) {
283
ERROR_LOG(Log::G3D, "Failed to initialize the graphics context! %d", (int)graphicsContext->GetState());
284
emuThreadState = (int)EmuThreadState::QUIT_REQUESTED;
285
gJvm->DetachCurrentThread();
286
return;
287
}
288
289
if (!NativeInitGraphics(graphicsContext)) {
290
_assert_msg_(false, "NativeInitGraphics failed, might as well bail");
291
emuThreadState = (int)EmuThreadState::QUIT_REQUESTED;
292
gJvm->DetachCurrentThread();
293
return;
294
}
295
296
INFO_LOG(Log::System, "Graphics initialized. Entering loop.");
297
298
// There's no real requirement that NativeInit happen on this thread.
299
// We just call the update/render loop here.
300
emuThreadState = (int)EmuThreadState::RUNNING;
301
while (emuThreadState != (int)EmuThreadState::QUIT_REQUESTED) {
302
{
303
std::lock_guard<std::mutex> renderGuard(renderLock);
304
NativeFrame(graphicsContext);
305
}
306
307
std::lock_guard<std::mutex> guard(frameCommandLock);
308
if (!nativeActivity) {
309
ERROR_LOG(Log::System, "No activity, clearing commands");
310
while (!frameCommands.empty())
311
frameCommands.pop();
312
return;
313
}
314
// Still under lock here.
315
ProcessFrameCommands(env);
316
}
317
318
INFO_LOG(Log::System, "QUIT_REQUESTED found, left EmuThreadFunc loop. Setting state to STOPPED.");
319
emuThreadState = (int)EmuThreadState::STOPPED;
320
321
NativeShutdownGraphics();
322
323
// Also ask the main thread to stop, so it doesn't hang waiting for a new frame.
324
graphicsContext->StopThread();
325
326
gJvm->DetachCurrentThread();
327
INFO_LOG(Log::System, "Leaving emu thread");
328
}
329
330
static void EmuThreadStart() {
331
INFO_LOG(Log::System, "EmuThreadStart");
332
emuThreadState = (int)EmuThreadState::START_REQUESTED;
333
emuThread = std::thread(&EmuThreadFunc);
334
}
335
336
// Call EmuThreadStop first, then keep running the GPU (or eat commands)
337
// as long as emuThreadState isn't STOPPED and/or there are still things queued up.
338
// Only after that, call EmuThreadJoin.
339
static void EmuThreadStop(const char *caller) {
340
INFO_LOG(Log::System, "EmuThreadStop - stopping (%s)...", caller);
341
emuThreadState = (int)EmuThreadState::QUIT_REQUESTED;
342
}
343
344
static void EmuThreadJoin() {
345
emuThread.join();
346
emuThread = std::thread();
347
INFO_LOG(Log::System, "EmuThreadJoin - joined");
348
}
349
350
static void PushCommand(std::string cmd, std::string param) {
351
std::lock_guard<std::mutex> guard(frameCommandLock);
352
frameCommands.push(FrameCommand(std::move(cmd), std::move(param)));
353
}
354
355
// Android implementation of callbacks to the Java part of the app
356
void System_Toast(std::string_view text) {
357
PushCommand("toast", std::string(text));
358
}
359
360
void System_ShowKeyboard() {
361
PushCommand("showKeyboard", "");
362
}
363
364
void System_Vibrate(int length_ms) {
365
char temp[32];
366
snprintf(temp, sizeof(temp), "%d", length_ms);
367
PushCommand("vibrate", temp);
368
}
369
370
void System_LaunchUrl(LaunchUrlType urlType, const char *url) {
371
switch (urlType) {
372
case LaunchUrlType::BROWSER_URL: PushCommand("launchBrowser", url); break;
373
case LaunchUrlType::MARKET_URL: PushCommand("launchMarket", url); break;
374
case LaunchUrlType::EMAIL_ADDRESS: PushCommand("launchEmail", url); break;
375
}
376
}
377
378
std::string System_GetProperty(SystemProperty prop) {
379
switch (prop) {
380
case SYSPROP_NAME:
381
return systemName;
382
case SYSPROP_LANGREGION: // "en_US"
383
return langRegion;
384
case SYSPROP_MOGA_VERSION:
385
return mogaVersion;
386
case SYSPROP_BOARDNAME:
387
return boardName;
388
case SYSPROP_BUILD_VERSION:
389
return PPSSPP_GIT_VERSION;
390
default:
391
return "";
392
}
393
}
394
395
std::vector<std::string> System_GetPropertyStringVec(SystemProperty prop) {
396
switch (prop) {
397
case SYSPROP_ADDITIONAL_STORAGE_DIRS:
398
return g_additionalStorageDirs;
399
400
case SYSPROP_TEMP_DIRS:
401
default:
402
return {};
403
}
404
}
405
406
int64_t System_GetPropertyInt(SystemProperty prop) {
407
switch (prop) {
408
case SYSPROP_SYSTEMVERSION:
409
return androidVersion;
410
case SYSPROP_DEVICE_TYPE:
411
return deviceType;
412
case SYSPROP_DISPLAY_XRES:
413
return display_xres;
414
case SYSPROP_DISPLAY_YRES:
415
return display_yres;
416
case SYSPROP_DISPLAY_DPI:
417
return display_dpi;
418
case SYSPROP_AUDIO_SAMPLE_RATE:
419
return sampleRate;
420
case SYSPROP_AUDIO_FRAMES_PER_BUFFER:
421
return framesPerBuffer;
422
case SYSPROP_AUDIO_OPTIMAL_SAMPLE_RATE:
423
return optimalSampleRate;
424
case SYSPROP_AUDIO_OPTIMAL_FRAMES_PER_BUFFER:
425
return optimalFramesPerBuffer;
426
default:
427
return -1;
428
}
429
}
430
431
float System_GetPropertyFloat(SystemProperty prop) {
432
switch (prop) {
433
case SYSPROP_DISPLAY_REFRESH_RATE:
434
return g_display.display_hz;
435
case SYSPROP_DISPLAY_SAFE_INSET_LEFT:
436
return g_safeInsetLeft * display_scale_x * g_display.dpi_scale_x;
437
case SYSPROP_DISPLAY_SAFE_INSET_RIGHT:
438
return g_safeInsetRight * display_scale_x * g_display.dpi_scale_x;
439
case SYSPROP_DISPLAY_SAFE_INSET_TOP:
440
return g_safeInsetTop * display_scale_y * g_display.dpi_scale_y;
441
case SYSPROP_DISPLAY_SAFE_INSET_BOTTOM:
442
return g_safeInsetBottom * display_scale_y * g_display.dpi_scale_y;
443
default:
444
return -1;
445
}
446
}
447
448
bool System_GetPropertyBool(SystemProperty prop) {
449
switch (prop) {
450
case SYSPROP_SUPPORTS_PERMISSIONS:
451
if (androidVersion < 23) {
452
// 6.0 Marshmallow introduced run time permissions.
453
return false;
454
} else {
455
// It gets a bit complicated here. If scoped storage enforcement is on,
456
// we also don't need to request permissions. We'll have the access we request
457
// on a per-folder basis.
458
return !System_GetPropertyBool(SYSPROP_ANDROID_SCOPED_STORAGE);
459
}
460
case SYSPROP_SUPPORTS_SUSTAINED_PERF_MODE:
461
return sustainedPerfSupported; // 7.0 introduced sustained performance mode as an optional feature.
462
case SYSPROP_HAS_TEXT_INPUT_DIALOG:
463
return androidVersion >= 11; // honeycomb
464
case SYSPROP_HAS_TEXT_CLIPBOARD:
465
return true;
466
case SYSPROP_HAS_OPEN_DIRECTORY:
467
return false; // We have this implemented but it may or may not work depending on if a file explorer is installed.
468
case SYSPROP_HAS_ADDITIONAL_STORAGE:
469
return !g_additionalStorageDirs.empty();
470
case SYSPROP_HAS_BACK_BUTTON:
471
return true;
472
case SYSPROP_HAS_IMAGE_BROWSER:
473
return deviceType != DEVICE_TYPE_VR;
474
case SYSPROP_HAS_FILE_BROWSER:
475
// It's only really needed with scoped storage, but why not make it available
476
// as far back as possible - works just fine.
477
return (androidVersion >= 19) && (deviceType != DEVICE_TYPE_VR); // when ACTION_OPEN_DOCUMENT was added
478
case SYSPROP_HAS_FOLDER_BROWSER:
479
// Uses OPEN_DOCUMENT_TREE to let you select a folder.
480
// Doesn't actually mean it's usable though, in many early versions of Android
481
// this dialog is complete garbage and only lets you select subfolders of the Downloads folder.
482
return (androidVersion >= 21) && (deviceType != DEVICE_TYPE_VR); // when ACTION_OPEN_DOCUMENT_TREE was added
483
case SYSPROP_SUPPORTS_OPEN_FILE_IN_EDITOR:
484
return false; // Update if we add support in FileUtil.cpp: OpenFileInEditor
485
case SYSPROP_APP_GOLD:
486
#ifdef GOLD
487
return true;
488
#else
489
return false;
490
#endif
491
case SYSPROP_CAN_JIT:
492
return true;
493
case SYSPROP_ANDROID_SCOPED_STORAGE:
494
// We turn this on for Android 30+ (11) now that when we target Android 11+.
495
// Along with adding:
496
// android:preserveLegacyExternalStorage="true"
497
// To the already requested:
498
// android:requestLegacyExternalStorage="true"
499
//
500
// This will cause Android 11+ to still behave like Android 10 until the app
501
// is manually uninstalled. We can detect this state with
502
// Android_IsExternalStoragePreservedLegacy(), but most of the app will just see
503
// that scoped storage enforcement is disabled in this case.
504
if (androidVersion >= 30) {
505
// Here we do a check to see if we ended up in the preserveLegacyExternalStorage path.
506
// That won't last if the user uninstalls/reinstalls though, but would preserve the user
507
// experience for simple upgrades so maybe let's support it.
508
return !Android_IsExternalStoragePreservedLegacy();
509
} else {
510
return false;
511
}
512
case SYSPROP_HAS_KEYBOARD:
513
return deviceType != DEVICE_TYPE_VR;
514
case SYSPROP_HAS_ACCELEROMETER:
515
return deviceType == DEVICE_TYPE_MOBILE;
516
case SYSPROP_CAN_CREATE_SHORTCUT:
517
return false; // We can't create shortcuts directly from game code, but we can from the Android UI.
518
#ifndef HTTPS_NOT_AVAILABLE
519
case SYSPROP_SUPPORTS_HTTPS:
520
return !g_Config.bDisableHTTPS;
521
#endif
522
default:
523
return false;
524
}
525
}
526
527
std::string Android_GetInputDeviceDebugString() {
528
if (!nativeActivity) {
529
return "(N/A)";
530
}
531
auto env = getEnv();
532
533
jstring jparam = env->NewStringUTF("InputDevice");
534
jstring jstr = (jstring)env->CallObjectMethod(nativeActivity, getDebugString, jparam);
535
if (!jstr) {
536
env->DeleteLocalRef(jparam);
537
return "(N/A)";
538
}
539
540
const char *charArray = env->GetStringUTFChars(jstr, nullptr);
541
std::string retVal = charArray;
542
env->ReleaseStringUTFChars(jstr, charArray);
543
env->DeleteLocalRef(jstr);
544
env->DeleteLocalRef(jparam);
545
return retVal;
546
}
547
548
std::string GetJavaString(JNIEnv *env, jstring jstr) {
549
if (!jstr)
550
return "";
551
const char *str = env->GetStringUTFChars(jstr, nullptr);
552
std::string cpp_string = std::string(str);
553
env->ReleaseStringUTFChars(jstr, str);
554
return cpp_string;
555
}
556
557
extern "C" void Java_org_ppsspp_ppsspp_NativeActivity_registerCallbacks(JNIEnv *env, jobject obj) {
558
nativeActivity = env->NewGlobalRef(obj);
559
postCommand = env->GetMethodID(env->GetObjectClass(obj), "postCommand", "(Ljava/lang/String;Ljava/lang/String;)V");
560
getDebugString = env->GetMethodID(env->GetObjectClass(obj), "getDebugString", "(Ljava/lang/String;)Ljava/lang/String;");
561
_dbg_assert_(postCommand);
562
_dbg_assert_(getDebugString);
563
564
Android_RegisterStorageCallbacks(env, obj);
565
Android_StorageSetNativeActivity(nativeActivity);
566
}
567
568
extern "C" void Java_org_ppsspp_ppsspp_NativeActivity_unregisterCallbacks(JNIEnv *env, jobject obj) {
569
Android_StorageSetNativeActivity(nullptr);
570
env->DeleteGlobalRef(nativeActivity);
571
nativeActivity = nullptr;
572
}
573
574
// This is now only used as a trigger for GetAppInfo as a function to all before Init.
575
// On Android we don't use any of the values it returns.
576
extern "C" jboolean Java_org_ppsspp_ppsspp_NativeApp_isLandscape(JNIEnv *env, jclass) {
577
std::string app_name, app_nice_name, version;
578
bool landscape;
579
NativeGetAppInfo(&app_name, &app_nice_name, &landscape, &version);
580
return landscape;
581
}
582
583
// Allow the app to intercept the back button.
584
extern "C" jboolean Java_org_ppsspp_ppsspp_NativeApp_isAtTopLevel(JNIEnv *env, jclass) {
585
return NativeIsAtTopLevel();
586
}
587
588
extern "C" void Java_org_ppsspp_ppsspp_NativeApp_audioConfig
589
(JNIEnv *env, jclass, jint optimalFPB, jint optimalSR) {
590
optimalFramesPerBuffer = optimalFPB;
591
optimalSampleRate = optimalSR;
592
}
593
594
// Easy way for the Java side to ask the C++ side for configuration options, such as
595
// the rotation lock which must be controlled from Java on Android.
596
static std::string QueryConfig(std::string_view query) {
597
char temp[128];
598
if (query == "screenRotation") {
599
INFO_LOG(Log::G3D, "g_Config.screenRotation = %d", g_Config.iScreenRotation);
600
snprintf(temp, sizeof(temp), "%d", g_Config.iScreenRotation);
601
return temp;
602
} else if (query == "immersiveMode") {
603
return g_Config.bImmersiveMode ? "1" : "0";
604
} else if (query == "sustainedPerformanceMode") {
605
return g_Config.bSustainedPerformanceMode ? "1" : "0";
606
} else if (query == "androidJavaGL") {
607
// If we're using Vulkan, we say no... need C++ to use Vulkan.
608
if (GetGPUBackend() == GPUBackend::VULKAN) {
609
return "false";
610
}
611
// Otherwise, some devices prefer the Java init so play it safe.
612
return "true";
613
} else {
614
return "";
615
}
616
}
617
618
extern "C" jstring Java_org_ppsspp_ppsspp_NativeApp_queryConfig
619
(JNIEnv *env, jclass, jstring jquery) {
620
std::string query = GetJavaString(env, jquery);
621
std::string result = QueryConfig(query);
622
jstring jresult = env->NewStringUTF(result.c_str());
623
return jresult;
624
}
625
626
static void parse_args(std::vector<std::string> &args, const std::string value) {
627
// Simple argument parser so we can take args from extra params.
628
const char *p = value.c_str();
629
630
while (*p != '\0') {
631
while (isspace(*p)) {
632
p++;
633
}
634
if (*p == '\0') {
635
break;
636
}
637
638
bool done = false;
639
bool quote = false;
640
std::string arg;
641
642
while (!done) {
643
size_t sz = strcspn(p, "\"\\ \r\n\t");
644
arg += std::string(p, sz);
645
p += sz;
646
647
switch (*p) {
648
case '"':
649
quote = !quote;
650
p++;
651
break;
652
653
case '\\':
654
p++;
655
arg += std::string(p, 1);
656
p++;
657
break;
658
659
case '\0':
660
done = true;
661
break;
662
663
default:
664
// If it's not the above, it's whitespace.
665
if (!quote) {
666
done = true;
667
} else {
668
sz = strspn(p, " \r\n\t");
669
arg += std::string(p, sz);
670
p += sz;
671
}
672
break;
673
}
674
}
675
676
args.push_back(arg);
677
678
while (isspace(*p)) {
679
p++;
680
}
681
}
682
}
683
684
// Need to use raw Android logging before NativeInit.
685
#define EARLY_LOG(...) __android_log_print(ANDROID_LOG_INFO, "PPSSPP", __VA_ARGS__)
686
687
static bool bFirstResume = false;
688
689
extern "C" void Java_org_ppsspp_ppsspp_NativeApp_init
690
(JNIEnv * env, jclass, jstring jmodel, jint jdeviceType, jstring jlangRegion, jstring japkpath,
691
jstring jdataDir, jstring jexternalStorageDir, jstring jexternalFilesDir, jstring jNativeLibDir, jstring jadditionalStorageDirs, jstring jcacheDir, jstring jshortcutParam,
692
jint jAndroidVersion, jstring jboard) {
693
SetCurrentThreadName("androidInit");
694
695
// Makes sure we get early permission grants.
696
ProcessFrameCommands(env);
697
698
EARLY_LOG("NativeApp.init() -- begin");
699
PROFILE_INIT();
700
701
std::lock_guard<std::mutex> guard(renderLock);
702
renderer_inited = false;
703
exitRenderLoop = false;
704
androidVersion = jAndroidVersion;
705
deviceType = jdeviceType;
706
707
Path apkPath(GetJavaString(env, japkpath));
708
g_VFS.Register("", ZipFileReader::Create(apkPath, "assets/"));
709
710
systemName = GetJavaString(env, jmodel);
711
langRegion = GetJavaString(env, jlangRegion);
712
713
EARLY_LOG("NativeApp.init(): device name: '%s'", systemName.c_str());
714
715
std::string externalStorageDir = GetJavaString(env, jexternalStorageDir);
716
std::string additionalStorageDirsString = GetJavaString(env, jadditionalStorageDirs);
717
std::string externalFilesDir = GetJavaString(env, jexternalFilesDir);
718
std::string nativeLibDir = GetJavaString(env, jNativeLibDir);
719
720
g_externalDir = externalStorageDir;
721
g_extFilesDir = externalFilesDir;
722
g_nativeLibDir = nativeLibDir;
723
724
if (!additionalStorageDirsString.empty()) {
725
SplitString(additionalStorageDirsString, ':', g_additionalStorageDirs);
726
for (auto &str : g_additionalStorageDirs) {
727
EARLY_LOG("Additional storage: %s", str.c_str());
728
}
729
}
730
731
std::string user_data_path = GetJavaString(env, jdataDir);
732
if (!user_data_path.empty())
733
user_data_path += "/";
734
std::string shortcut_param = GetJavaString(env, jshortcutParam);
735
std::string cacheDir = GetJavaString(env, jcacheDir);
736
std::string buildBoard = GetJavaString(env, jboard);
737
boardName = buildBoard;
738
EARLY_LOG("NativeApp.init(): External storage path: %s", externalStorageDir.c_str());
739
EARLY_LOG("NativeApp.init(): Launch shortcut parameter: %s", shortcut_param.c_str());
740
741
std::string app_name;
742
std::string app_nice_name;
743
std::string version;
744
bool landscape;
745
746
// Unfortunately, on the Samsung Galaxy S7, this isn't in /proc/cpuinfo.
747
// We also can't read it from __system_property_get.
748
if (buildBoard == "universal8890") {
749
cpu_info.sQuirks.bExynos8890DifferingCachelineSizes = true;
750
}
751
752
NativeGetAppInfo(&app_name, &app_nice_name, &landscape, &version);
753
754
// If shortcut_param is not empty, pass it as additional arguments to the NativeInit() method.
755
// NativeInit() is expected to treat extra argument as boot_filename, which in turn will start game immediately.
756
// NOTE: Will only work if ppsspp started from Activity.onCreate(). Won't work if ppsspp app start from onResume().
757
758
std::vector<const char *> args;
759
std::vector<std::string> temp;
760
args.push_back(app_name.c_str());
761
if (!shortcut_param.empty()) {
762
EARLY_LOG("NativeInit shortcut param %s", shortcut_param.c_str());
763
parse_args(temp, shortcut_param);
764
for (const auto &arg : temp) {
765
args.push_back(arg.c_str());
766
}
767
}
768
769
NativeInit((int)args.size(), &args[0], user_data_path.c_str(), externalStorageDir.c_str(), cacheDir.c_str());
770
771
bFirstResume = true;
772
773
// In debug mode, don't allow creating software Vulkan devices (reject by VulkanMaybeAvailable).
774
// Needed for #16931.
775
#ifdef NDEBUG
776
if (!VulkanMayBeAvailable()) {
777
// If VulkanLoader decided on no viable backend, let's force Vulkan off in release builds at least.
778
g_Config.iGPUBackend = 0;
779
}
780
#endif
781
782
// No need to use EARLY_LOG anymore.
783
784
retry:
785
switch (g_Config.iGPUBackend) {
786
case (int)GPUBackend::OPENGL:
787
useCPUThread = true;
788
INFO_LOG(Log::System, "NativeApp.init() -- creating OpenGL context (JavaGL)");
789
graphicsContext = new AndroidJavaEGLGraphicsContext();
790
INFO_LOG(Log::System, "NativeApp.init() - launching emu thread");
791
EmuThreadStart();
792
break;
793
case (int)GPUBackend::VULKAN:
794
{
795
INFO_LOG(Log::System, "NativeApp.init() -- creating Vulkan context");
796
useCPUThread = false;
797
// The Vulkan render manager manages its own thread.
798
// We create and destroy the Vulkan graphics context in the app main thread though.
799
AndroidVulkanContext *ctx = new AndroidVulkanContext();
800
if (!ctx->InitAPI()) {
801
INFO_LOG(Log::System, "Failed to initialize Vulkan, switching to OpenGL");
802
g_Config.iGPUBackend = (int)GPUBackend::OPENGL;
803
SetGPUBackend(GPUBackend::OPENGL);
804
goto retry;
805
} else {
806
graphicsContext = ctx;
807
}
808
break;
809
}
810
default:
811
ERROR_LOG(Log::System, "NativeApp.init(): iGPUBackend %d not supported. Switching to OpenGL.", (int)g_Config.iGPUBackend);
812
g_Config.iGPUBackend = (int)GPUBackend::OPENGL;
813
goto retry;
814
}
815
816
if (IsVREnabled()) {
817
Version gitVer(PPSSPP_GIT_VERSION);
818
InitVROnAndroid(gJvm, nativeActivity, systemName.c_str(), gitVer.ToInteger(), "PPSSPP");
819
SetVRCallbacks(NativeAxis, NativeKey, NativeTouch);
820
}
821
}
822
823
AudioBackend *System_CreateAudioBackend() {
824
// Use legacy mechanisms.
825
return nullptr;
826
}
827
828
extern "C" void Java_org_ppsspp_ppsspp_NativeApp_audioInit(JNIEnv *, jclass) {
829
sampleRate = optimalSampleRate;
830
if (optimalSampleRate == 0) {
831
sampleRate = 44100;
832
}
833
if (optimalFramesPerBuffer > 0) {
834
framesPerBuffer = optimalFramesPerBuffer;
835
} else {
836
framesPerBuffer = 512;
837
}
838
839
// Some devices have totally bonkers buffer sizes like 8192. They will have terrible latency anyway, so to avoid having to
840
// create extra smart buffering code, we'll just let their regular mixer deal with it, missing the fast path (as if they had one...)
841
if (framesPerBuffer > 512) {
842
framesPerBuffer = 512;
843
sampleRate = 44100;
844
}
845
846
INFO_LOG(Log::Audio, "NativeApp.audioInit() -- Using OpenSL audio! frames/buffer: %i optimal sr: %i actual sr: %i", optimalFramesPerBuffer, optimalSampleRate, sampleRate);
847
if (!g_audioState) {
848
g_audioState = AndroidAudio_Init(&NativeMix, framesPerBuffer, sampleRate);
849
} else {
850
ERROR_LOG(Log::Audio, "Audio state already initialized");
851
}
852
}
853
854
extern "C" void Java_org_ppsspp_ppsspp_NativeApp_audioShutdown(JNIEnv *, jclass) {
855
if (g_audioState) {
856
AndroidAudio_Shutdown(g_audioState);
857
g_audioState = nullptr;
858
} else {
859
ERROR_LOG(Log::Audio, "Audio state already shutdown!");
860
}
861
}
862
863
extern "C" void Java_org_ppsspp_ppsspp_NativeApp_audioRecording_1SetSampleRate(JNIEnv *, jclass, jint sampleRate) {
864
AndroidAudio_Recording_SetSampleRate(g_audioState, sampleRate);
865
}
866
867
extern "C" void Java_org_ppsspp_ppsspp_NativeApp_audioRecording_1Start(JNIEnv *, jclass) {
868
AndroidAudio_Recording_Start(g_audioState);
869
}
870
871
extern "C" void Java_org_ppsspp_ppsspp_NativeApp_audioRecording_1Stop(JNIEnv *, jclass) {
872
AndroidAudio_Recording_Stop(g_audioState);
873
}
874
875
bool System_AudioRecordingIsAvailable() {
876
return true;
877
}
878
879
bool System_AudioRecordingState() {
880
return AndroidAudio_Recording_State(g_audioState);
881
}
882
883
extern "C" void Java_org_ppsspp_ppsspp_NativeApp_resume(JNIEnv *, jclass) {
884
INFO_LOG(Log::System, "NativeApp.resume() - resuming audio");
885
AndroidAudio_Resume(g_audioState);
886
887
System_PostUIMessage(UIMessage::APP_RESUMED, bFirstResume ? "first" : "");
888
889
bFirstResume = false;
890
}
891
892
extern "C" void Java_org_ppsspp_ppsspp_NativeApp_pause(JNIEnv *, jclass) {
893
INFO_LOG(Log::System, "NativeApp.pause() - pausing audio");
894
AndroidAudio_Pause(g_audioState);
895
}
896
897
extern "C" void Java_org_ppsspp_ppsspp_NativeApp_shutdown(JNIEnv *, jclass) {
898
INFO_LOG(Log::System, "NativeApp.shutdown() -- begin");
899
900
if (renderer_inited && useCPUThread && graphicsContext) {
901
// Only used in Java EGL path.
902
903
// We can't lock renderLock here because the emu thread will be in NativeFrame
904
// which locks renderLock already, and only gets out once we call ThreadFrame()
905
// in a loop before, to empty the queue.
906
EmuThreadStop("shutdown");
907
INFO_LOG(Log::System, "BeginAndroidShutdown");
908
graphicsContext->BeginAndroidShutdown();
909
// Now, it could be that we had some frames queued up. Get through them.
910
// We're on the render thread, so this is synchronous.
911
do {
912
INFO_LOG(Log::System, "Executing graphicsContext->ThreadFrame to clear buffers");
913
} while (graphicsContext->ThreadFrame());
914
graphicsContext->ThreadEnd();
915
INFO_LOG(Log::System, "ThreadEnd called.");
916
graphicsContext->ShutdownFromRenderThread();
917
INFO_LOG(Log::System, "Graphics context now shut down from NativeApp_shutdown");
918
919
INFO_LOG(Log::System, "Joining emuthread");
920
EmuThreadJoin();
921
}
922
923
{
924
std::lock_guard<std::mutex> guard(renderLock);
925
926
if (graphicsContext) {
927
INFO_LOG(Log::G3D, "Shutting down renderer");
928
graphicsContext->Shutdown();
929
delete graphicsContext;
930
graphicsContext = nullptr;
931
renderer_inited = false;
932
} else {
933
INFO_LOG(Log::G3D, "Not shutting down renderer - not initialized");
934
}
935
936
NativeShutdown();
937
g_VFS.Clear();
938
}
939
940
{
941
std::lock_guard<std::mutex> guard(frameCommandLock);
942
while (frameCommands.size())
943
frameCommands.pop();
944
}
945
INFO_LOG(Log::System, "NativeApp.shutdown() -- end");
946
}
947
948
// JavaEGL. This doesn't get called on the Vulkan path.
949
// This gets called from onSurfaceCreated.
950
extern "C" jboolean Java_org_ppsspp_ppsspp_NativeRenderer_displayInit(JNIEnv * env, jobject obj) {
951
_assert_(useCPUThread);
952
953
INFO_LOG(Log::G3D, "NativeApp.displayInit()");
954
bool firstStart = !renderer_inited;
955
956
// We should be running on the render thread here.
957
std::string errorMessage;
958
if (renderer_inited) {
959
// Would be really nice if we could get something on the GL thread immediately when shutting down,
960
// but the only mechanism for handling lost devices seems to be that onSurfaceCreated is called again,
961
// which ends up calling displayInit.
962
963
INFO_LOG(Log::G3D, "NativeApp.displayInit() restoring");
964
EmuThreadStop("displayInit");
965
graphicsContext->BeginAndroidShutdown();
966
INFO_LOG(Log::G3D, "BeginAndroidShutdown. Looping until emu thread done...");
967
// Skipping GL calls here because the old context is lost.
968
while (graphicsContext->ThreadFrame()) {
969
}
970
INFO_LOG(Log::G3D, "Joining emu thread");
971
EmuThreadJoin();
972
973
graphicsContext->ThreadEnd();
974
graphicsContext->ShutdownFromRenderThread();
975
976
INFO_LOG(Log::G3D, "Shut down both threads. Now let's bring it up again!");
977
978
if (!graphicsContext->InitFromRenderThread(nullptr, 0, 0, 0, 0)) {
979
System_Toast("Graphics initialization failed. Quitting.");
980
return false;
981
}
982
983
graphicsContext->GetDrawContext()->SetErrorCallback([](const char *shortDesc, const char *details, void *userdata) {
984
g_OSD.Show(OSDType::MESSAGE_ERROR, details, 5.0);
985
}, nullptr);
986
987
EmuThreadStart();
988
989
graphicsContext->ThreadStart();
990
991
INFO_LOG(Log::G3D, "Restored.");
992
} else {
993
INFO_LOG(Log::G3D, "NativeApp.displayInit() first time");
994
if (!graphicsContext || !graphicsContext->InitFromRenderThread(nullptr, 0, 0, 0, 0)) {
995
System_Toast("Graphics initialization failed. Quitting.");
996
return false;
997
}
998
999
graphicsContext->GetDrawContext()->SetErrorCallback([](const char *shortDesc, const char *details, void *userdata) {
1000
g_OSD.Show(OSDType::MESSAGE_ERROR, details, 5.0);
1001
}, nullptr);
1002
1003
graphicsContext->ThreadStart();
1004
renderer_inited = true;
1005
}
1006
1007
System_PostUIMessage(UIMessage::RECREATE_VIEWS);
1008
1009
if (IsVREnabled()) {
1010
EnterVR(firstStart);
1011
}
1012
return true;
1013
}
1014
1015
extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_backbufferResize(JNIEnv *, jclass, jint pixel_xres, jint pixel_yres, jint format) {
1016
INFO_LOG(Log::System, "NativeApp.backbufferResize(%d x %d)", pixel_xres, pixel_yres);
1017
1018
int old_w = g_display.pixel_xres;
1019
int old_h = g_display.pixel_yres;
1020
1021
// pixel_*res is the backbuffer resolution.
1022
backbuffer_format = format;
1023
1024
if (IsVREnabled()) {
1025
GetVRResolutionPerEye(&pixel_xres, &pixel_yres);
1026
}
1027
1028
// Compute display scale factor. Always < 1.0f (well, as long as we use buffers sized smaller than the screen...)
1029
display_scale_x = (float)pixel_xres / (float)display_xres;
1030
display_scale_y = (float)pixel_yres / (float)display_yres;
1031
1032
float dpi_x = (1.0f / display_scale_x) * (240.0f / (float)display_dpi);
1033
float dpi_y = (1.0f / display_scale_y) * (240.0f / (float)display_dpi);
1034
1035
bool new_size = g_display.Recalculate(pixel_xres, pixel_yres, dpi_x, dpi_y, UIScaleFactorToMultiplier(g_Config.iUIScaleFactor));
1036
1037
INFO_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);
1038
INFO_LOG(Log::G3D, "RecalcDPI: g_dpi=%d scaled_dpi_x=%f scaled_dpi_y=%f display_scale_x=%f display_scale_y=%f g_dpi_scale_x=%f g_dpi_scale_y=%f dp_xres=%d dp_yres=%d",
1039
display_dpi, dpi_x, dpi_y, display_scale_x, display_scale_y, g_display.dpi_scale_x, g_display.dpi_scale_y, g_display.dp_xres, g_display.dp_yres);
1040
1041
if (new_size) {
1042
INFO_LOG(Log::G3D, "Size change detected (previously %d,%d) - calling NativeResized()", old_w, old_h);
1043
NativeResized();
1044
} else {
1045
INFO_LOG(Log::G3D, "NativeApp::backbufferResize: Size didn't change.");
1046
}
1047
}
1048
1049
void System_Notify(SystemNotification notification) {
1050
switch (notification) {
1051
case SystemNotification::ROTATE_UPDATED:
1052
PushCommand("rotate", "");
1053
break;
1054
case SystemNotification::FORCE_RECREATE_ACTIVITY:
1055
PushCommand("recreate", "");
1056
break;
1057
case SystemNotification::IMMERSIVE_MODE_CHANGE:
1058
PushCommand("immersive", "");
1059
break;
1060
case SystemNotification::SUSTAINED_PERF_CHANGE:
1061
PushCommand("sustainedPerfMode", "");
1062
break;
1063
case SystemNotification::TEST_JAVA_EXCEPTION:
1064
PushCommand("testException", "This is a test exception");
1065
break;
1066
default:
1067
break;
1068
}
1069
}
1070
1071
bool System_MakeRequest(SystemRequestType type, int requestId, const std::string &param1, const std::string &param2, int64_t param3, int64_t param4) {
1072
switch (type) {
1073
case SystemRequestType::EXIT_APP:
1074
PushCommand("finish", "");
1075
return true;
1076
case SystemRequestType::RESTART_APP:
1077
PushCommand("graphics_restart", param1);
1078
return true;
1079
case SystemRequestType::RECREATE_ACTIVITY:
1080
PushCommand("recreate", param1);
1081
return true;
1082
case SystemRequestType::COPY_TO_CLIPBOARD:
1083
PushCommand("copy_to_clipboard", param1);
1084
return true;
1085
case SystemRequestType::INPUT_TEXT_MODAL:
1086
{
1087
std::string serialized = StringFromFormat("%d:@:%s:@:%s", requestId, param1.c_str(), param2.c_str());
1088
PushCommand("inputbox", serialized);
1089
return true;
1090
}
1091
case SystemRequestType::BROWSE_FOR_IMAGE:
1092
PushCommand("browse_image", StringFromFormat("%d", requestId));
1093
return true;
1094
case SystemRequestType::BROWSE_FOR_FILE:
1095
{
1096
BrowseFileType fileType = (BrowseFileType)param3;
1097
std::string params = StringFromFormat("%d", requestId);
1098
switch (fileType) {
1099
case BrowseFileType::SOUND_EFFECT:
1100
PushCommand("browse_file_audio", params);
1101
break;
1102
case BrowseFileType::ZIP:
1103
PushCommand("browse_file_zip", params);
1104
break;
1105
default:
1106
PushCommand("browse_file", params);
1107
break;
1108
}
1109
return true;
1110
}
1111
case SystemRequestType::BROWSE_FOR_FOLDER:
1112
PushCommand("browse_folder", StringFromFormat("%d", requestId));
1113
return true;
1114
1115
case SystemRequestType::CAMERA_COMMAND:
1116
PushCommand("camera_command", param1);
1117
return true;
1118
case SystemRequestType::GPS_COMMAND:
1119
PushCommand("gps_command", param1);
1120
return true;
1121
case SystemRequestType::INFRARED_COMMAND:
1122
PushCommand("infrared_command", param1);
1123
return true;
1124
case SystemRequestType::MICROPHONE_COMMAND:
1125
PushCommand("microphone_command", param1);
1126
return true;
1127
case SystemRequestType::SHARE_TEXT:
1128
PushCommand("share_text", param1);
1129
return true;
1130
case SystemRequestType::SET_KEEP_SCREEN_BRIGHT:
1131
PushCommand("set_keep_screen_bright", param3 ? "on" : "off");
1132
return true;
1133
case SystemRequestType::SHOW_FILE_IN_FOLDER:
1134
PushCommand("show_folder", param1);
1135
return true;
1136
default:
1137
return false;
1138
}
1139
}
1140
1141
extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_sendRequestResult(JNIEnv *env, jclass, jint jrequestID, jboolean result, jstring jvalue, jint jintValue) {
1142
std::string value = jvalue ? GetJavaString(env, jvalue) : "(no value)";
1143
INFO_LOG(Log::System, "Received result of request %d from Java: %d: %d '%s'", jrequestID, (int)result, jintValue, value.c_str());
1144
if (result) {
1145
g_requestManager.PostSystemSuccess(jrequestID, value.c_str());
1146
} else {
1147
g_requestManager.PostSystemFailure(jrequestID);
1148
}
1149
}
1150
1151
extern "C" void Java_org_ppsspp_ppsspp_NativeRenderer_displayRender(JNIEnv *env, jobject obj) {
1152
// This doesn't get called on the Vulkan path.
1153
_assert_(useCPUThread);
1154
1155
static bool hasSetThreadName = false;
1156
if (!hasSetThreadName) {
1157
hasSetThreadName = true;
1158
SetCurrentThreadName("AndroidRender");
1159
}
1160
1161
if (IsVREnabled() && !StartVRRender())
1162
return;
1163
1164
// This is the "GPU thread". Call ThreadFrame.
1165
if (!graphicsContext || !graphicsContext->ThreadFrame()) {
1166
return;
1167
}
1168
1169
if (IsVREnabled()) {
1170
UpdateVRInput(g_Config.bHapticFeedback, g_display.dpi_scale_x, g_display.dpi_scale_y);
1171
FinishVRRender();
1172
}
1173
}
1174
1175
void System_AskForPermission(SystemPermission permission) {
1176
switch (permission) {
1177
case SYSTEM_PERMISSION_STORAGE:
1178
PushCommand("ask_permission", "storage");
1179
break;
1180
}
1181
}
1182
1183
PermissionStatus System_GetPermissionStatus(SystemPermission permission) {
1184
if (androidVersion < 23) {
1185
return PERMISSION_STATUS_GRANTED;
1186
} else {
1187
return permissions[permission];
1188
}
1189
}
1190
1191
extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_touch
1192
(JNIEnv *, jclass, float x, float y, int code, int pointerId) {
1193
if (!renderer_inited)
1194
return;
1195
TouchInput touch{};
1196
touch.id = pointerId;
1197
touch.x = x * display_scale_x * g_display.dpi_scale_x;
1198
touch.y = y * display_scale_y * g_display.dpi_scale_y;
1199
touch.flags = code;
1200
NativeTouch(touch);
1201
}
1202
1203
extern "C" jboolean Java_org_ppsspp_ppsspp_NativeApp_keyDown(JNIEnv *, jclass, jint deviceId, jint key, jboolean isRepeat) {
1204
if (!renderer_inited) {
1205
return false; // could probably return true here too..
1206
}
1207
if (key == 0 && deviceId >= DEVICE_ID_PAD_0 && deviceId <= DEVICE_ID_PAD_9) {
1208
// Ignore keycode 0 from pads. Stadia controllers seem to produce them when pressing L2/R2 for some reason, confusing things.
1209
return true; // need to eat the key so it doesn't go through legacy path
1210
}
1211
1212
KeyInput keyInput;
1213
keyInput.deviceId = (InputDeviceID)deviceId;
1214
keyInput.keyCode = (InputKeyCode)key;
1215
keyInput.flags = KEY_DOWN;
1216
if (isRepeat) {
1217
keyInput.flags |= KEY_IS_REPEAT;
1218
}
1219
return NativeKey(keyInput);
1220
}
1221
1222
extern "C" jboolean Java_org_ppsspp_ppsspp_NativeApp_keyUp(JNIEnv *, jclass, jint deviceId, jint key) {
1223
if (!renderer_inited) {
1224
return false; // could probably return true here too..
1225
}
1226
if (key == 0 && deviceId >= DEVICE_ID_PAD_0 && deviceId <= DEVICE_ID_PAD_9) {
1227
// Ignore keycode 0 from pads. Stadia controllers seem to produce them when pressing L2/R2 for some reason, confusing things.
1228
return true; // need to eat the key so it doesn't go through legacy path
1229
}
1230
1231
KeyInput keyInput;
1232
keyInput.deviceId = (InputDeviceID)deviceId;
1233
keyInput.keyCode = (InputKeyCode)key;
1234
keyInput.flags = KEY_UP;
1235
return NativeKey(keyInput);
1236
}
1237
1238
extern "C" void Java_org_ppsspp_ppsspp_NativeApp_joystickAxis(
1239
JNIEnv *env, jclass, jint deviceId, jintArray axisIds, jfloatArray values, jint count) {
1240
if (!renderer_inited)
1241
return;
1242
1243
AxisInput *axis = new AxisInput[count];
1244
_dbg_assert_(count <= env->GetArrayLength(axisIds));
1245
_dbg_assert_(count <= env->GetArrayLength(values));
1246
jint *axisIdBuffer = env->GetIntArrayElements(axisIds, nullptr);
1247
jfloat *valueBuffer = env->GetFloatArrayElements(values, nullptr);
1248
1249
// These are dirty-filtered on the Java side.
1250
for (int i = 0; i < count; i++) {
1251
axis[i].deviceId = (InputDeviceID)(int)deviceId;
1252
axis[i].axisId = (InputAxis)(int)axisIdBuffer[i];
1253
axis[i].value = valueBuffer[i];
1254
}
1255
NativeAxis(axis, count);
1256
delete[] axis;
1257
env->ReleaseIntArrayElements(axisIds, axisIdBuffer, JNI_ABORT); // ABORT just means we don't want changes copied back!
1258
env->ReleaseFloatArrayElements(values, valueBuffer, JNI_ABORT); // ABORT just means we don't want changes copied back!
1259
}
1260
1261
extern "C" jboolean Java_org_ppsspp_ppsspp_NativeApp_mouse(
1262
JNIEnv *env, jclass, jfloat x, jfloat y, int button, int action) {
1263
if (!renderer_inited)
1264
return false;
1265
TouchInput input{};
1266
1267
static float last_x = 0.0f;
1268
static float last_y = 0.0f;
1269
1270
if (x == -1.0f) {
1271
x = last_x;
1272
} else {
1273
last_x = x;
1274
}
1275
if (y == -1.0f) {
1276
y = last_y;
1277
} else {
1278
last_y = y;
1279
}
1280
1281
x *= g_display.dpi_scale_x;
1282
y *= g_display.dpi_scale_y;
1283
1284
if (button == 0) {
1285
// It's a pure mouse move.
1286
input.flags = TOUCH_MOUSE | TOUCH_MOVE;
1287
input.x = x;
1288
input.y = y;
1289
input.id = 0;
1290
} else {
1291
input.buttons = button;
1292
input.x = x;
1293
input.y = y;
1294
switch (action) {
1295
case 1:
1296
input.flags = TOUCH_MOUSE | TOUCH_DOWN;
1297
break;
1298
case 2:
1299
input.flags = TOUCH_MOUSE | TOUCH_UP;
1300
break;
1301
}
1302
input.id = 0;
1303
}
1304
INFO_LOG(Log::System, "New-style mouse event: %f %f %d %d -> x: %f y: %f buttons: %d flags: %04x", x, y, button, action, input.x, input.y, input.buttons, input.flags);
1305
NativeTouch(input);
1306
1307
// Also send mouse button key events, for binding.
1308
if (button) {
1309
KeyInput input{};
1310
input.deviceId = DEVICE_ID_MOUSE;
1311
switch (button) {
1312
case 1: input.keyCode = NKCODE_EXT_MOUSEBUTTON_1; break;
1313
case 2: input.keyCode = NKCODE_EXT_MOUSEBUTTON_2; break;
1314
case 3: input.keyCode = NKCODE_EXT_MOUSEBUTTON_3; break;
1315
default: WARN_LOG(Log::System, "Unexpected mouse button %d", button);
1316
}
1317
input.flags = action == 1 ? KEY_DOWN : KEY_UP;
1318
if (input.keyCode != 0) {
1319
NativeKey(input);
1320
}
1321
}
1322
return true;
1323
}
1324
1325
extern "C" jboolean Java_org_ppsspp_ppsspp_NativeApp_mouseWheelEvent(
1326
JNIEnv *env, jclass, jfloat x, jfloat y) {
1327
if (!renderer_inited)
1328
return false;
1329
// TODO: Mousewheel should probably be an axis instead.
1330
int wheelDelta = y * 30.0f;
1331
if (wheelDelta > 500) wheelDelta = 500;
1332
if (wheelDelta < -500) wheelDelta = -500;
1333
1334
KeyInput key;
1335
key.deviceId = DEVICE_ID_MOUSE;
1336
if (wheelDelta < 0) {
1337
key.keyCode = NKCODE_EXT_MOUSEWHEEL_DOWN;
1338
wheelDelta = -wheelDelta;
1339
} else {
1340
key.keyCode = NKCODE_EXT_MOUSEWHEEL_UP;
1341
}
1342
// There's no separate keyup event for mousewheel events,
1343
// so we release it with a slight delay.
1344
key.flags = KEY_DOWN | KEY_HASWHEELDELTA | (wheelDelta << 16);
1345
NativeKey(key);
1346
return true;
1347
}
1348
1349
extern "C" void Java_org_ppsspp_ppsspp_NativeApp_mouseDelta(
1350
JNIEnv * env, jclass, jfloat x, jfloat y) {
1351
if (!renderer_inited)
1352
return;
1353
NativeMouseDelta(x, y);
1354
}
1355
1356
extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_accelerometer(JNIEnv *, jclass, float x, float y, float z) {
1357
if (!renderer_inited)
1358
return;
1359
NativeAccelerometer(x, y, z);
1360
}
1361
1362
extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_sendMessageFromJava(JNIEnv *env, jclass, jstring jmessage, jstring jparam) {
1363
std::string msg = GetJavaString(env, jmessage);
1364
std::string prm = GetJavaString(env, jparam);
1365
1366
// A bit ugly, see InputDeviceState.java.
1367
static InputDeviceID nextInputDeviceID = DEVICE_ID_ANY;
1368
1369
// Some messages are caught by app-android. TODO: Should be all.
1370
if (msg == "moga") {
1371
mogaVersion = prm;
1372
} else if (msg == "permission_pending") {
1373
INFO_LOG(Log::System, "STORAGE PERMISSION: PENDING");
1374
// TODO: Add support for other permissions
1375
permissions[SYSTEM_PERMISSION_STORAGE] = PERMISSION_STATUS_PENDING;
1376
// Don't need to send along, nothing else is listening.
1377
} else if (msg == "permission_denied") {
1378
INFO_LOG(Log::System, "STORAGE PERMISSION: DENIED");
1379
permissions[SYSTEM_PERMISSION_STORAGE] = PERMISSION_STATUS_DENIED;
1380
// Don't need to send along, nothing else is listening.
1381
} else if (msg == "permission_granted") {
1382
INFO_LOG(Log::System, "STORAGE PERMISSION: GRANTED");
1383
permissions[SYSTEM_PERMISSION_STORAGE] = PERMISSION_STATUS_GRANTED;
1384
// Send along.
1385
System_PostUIMessage(UIMessage::PERMISSION_GRANTED, prm);
1386
} else if (msg == "sustained_perf_supported") {
1387
sustainedPerfSupported = true;
1388
} else if (msg == "safe_insets") {
1389
// INFO_LOG(Log::System, "Got insets: %s", prm.c_str());
1390
// We don't bother with supporting exact rectangular regions. Safe insets are good enough.
1391
int left, right, top, bottom;
1392
if (4 == sscanf(prm.c_str(), "%d:%d:%d:%d", &left, &right, &top, &bottom)) {
1393
g_safeInsetLeft = (float)left;
1394
g_safeInsetRight = (float)right;
1395
g_safeInsetTop = (float)top;
1396
g_safeInsetBottom = (float)bottom;
1397
}
1398
} else if (msg == "inputDeviceConnectedID") {
1399
nextInputDeviceID = (InputDeviceID)parseLong(prm);
1400
} else if (msg == "inputDeviceConnected") {
1401
KeyMap::NotifyPadConnected(nextInputDeviceID, prm);
1402
} else if (msg == "core_powerSaving") {
1403
// Forward.
1404
System_PostUIMessage(UIMessage::POWER_SAVING, prm);
1405
} else if (msg == "exception") {
1406
g_OSD.Show(OSDType::MESSAGE_ERROR, std::string("Java Exception"), prm, 10.0f);
1407
} else if (msg == "shortcutParam") {
1408
if (prm.empty()) {
1409
WARN_LOG(Log::System, "shortcutParam empty");
1410
return;
1411
}
1412
INFO_LOG(Log::System, "shortcutParam received: %s", prm.c_str());
1413
1414
prm = StripQuotes(prm);
1415
// NOTE: The parameter can be a file:// URL, which we need to take care of here. Similar to in NativeApp.cpp, search for file://
1416
if (startsWith(prm, "file:///")) {
1417
std::string param = prm;
1418
prm = UriDecode(prm.substr(7));
1419
INFO_LOG(Log::IO, "Decoding '%s' to '%s'", param.c_str(), prm.c_str());
1420
}
1421
System_PostUIMessage(UIMessage::REQUEST_GAME_BOOT, StripQuotes(prm));
1422
} else {
1423
ERROR_LOG(Log::System, "Got unexpected message from Java, ignoring: %s / %s", msg.c_str(), prm.c_str());
1424
}
1425
}
1426
1427
void correctRatio(int &sz_x, int &sz_y, float scale) {
1428
float x = (float)sz_x;
1429
float y = (float)sz_y;
1430
float ratio = x / y;
1431
INFO_LOG(Log::G3D, "CorrectRatio: Considering size: %0.2f/%0.2f=%0.2f for scale %f", x, y, ratio, scale);
1432
float targetRatio;
1433
1434
// Try to get the longest dimension to match scale*PSP resolution.
1435
if (x >= y) {
1436
targetRatio = 480.0f / 272.0f;
1437
x = 480.f * scale;
1438
y = 272.f * scale;
1439
} else {
1440
targetRatio = 272.0f / 480.0f;
1441
x = 272.0f * scale;
1442
y = 480.0f * scale;
1443
}
1444
1445
float correction = targetRatio / ratio;
1446
INFO_LOG(Log::G3D, "Target ratio: %0.2f ratio: %0.2f correction: %0.2f", targetRatio, ratio, correction);
1447
if (ratio < targetRatio) {
1448
y *= correction;
1449
} else {
1450
x /= correction;
1451
}
1452
1453
sz_x = x;
1454
sz_y = y;
1455
INFO_LOG(Log::G3D, "Corrected ratio: %dx%d", sz_x, sz_y);
1456
}
1457
1458
void getDesiredBackbufferSize(int &sz_x, int &sz_y) {
1459
sz_x = display_xres;
1460
sz_y = display_yres;
1461
1462
int scale = g_Config.iAndroidHwScale;
1463
// Override hw scale for TV type devices.
1464
if (System_GetPropertyInt(SYSPROP_DEVICE_TYPE) == DEVICE_TYPE_TV)
1465
scale = 0;
1466
1467
if (scale == 1) {
1468
// If g_Config.iInternalResolution is also set to Auto (1), we fall back to "Device resolution" (0). It works out.
1469
scale = g_Config.iInternalResolution;
1470
} else if (scale >= 2) {
1471
scale -= 1;
1472
}
1473
1474
int max_res = std::max(System_GetPropertyInt(SYSPROP_DISPLAY_XRES), System_GetPropertyInt(SYSPROP_DISPLAY_YRES)) / 480 + 1;
1475
1476
scale = std::min(scale, max_res);
1477
1478
if (scale > 0) {
1479
correctRatio(sz_x, sz_y, scale);
1480
} else {
1481
sz_x = 0;
1482
sz_y = 0;
1483
}
1484
}
1485
1486
extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_setDisplayParameters(JNIEnv *, jclass, jint xres, jint yres, jint dpi, jfloat refreshRate) {
1487
INFO_LOG(Log::G3D, "NativeApp.setDisplayParameters(%d x %d, dpi=%d, refresh=%0.2f)", xres, yres, dpi, refreshRate);
1488
1489
if (IsVREnabled()) {
1490
int width, height;
1491
GetVRResolutionPerEye(&width, &height);
1492
xres = width;
1493
yres = height * 272 / 480;
1494
dpi = 320;
1495
}
1496
1497
// Hard parameters for the display. Actual DPI recalculation happens in BackbufferResize.
1498
display_xres = xres;
1499
display_yres = yres;
1500
display_dpi = dpi;
1501
g_display.display_hz = refreshRate;
1502
}
1503
1504
extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_computeDesiredBackbufferDimensions(JNIEnv *, jclass) {
1505
getDesiredBackbufferSize(desiredBackbufferSizeX, desiredBackbufferSizeY);
1506
}
1507
1508
extern "C" jint JNICALL Java_org_ppsspp_ppsspp_NativeApp_getDesiredBackbufferWidth(JNIEnv *, jclass) {
1509
return desiredBackbufferSizeX;
1510
}
1511
1512
extern "C" jint JNICALL Java_org_ppsspp_ppsspp_NativeApp_getDesiredBackbufferHeight(JNIEnv *, jclass) {
1513
return desiredBackbufferSizeY;
1514
}
1515
1516
extern "C" jint JNICALL Java_org_ppsspp_ppsspp_NativeApp_getDisplayFramerateMode(JNIEnv *, jclass) {
1517
return g_Config.iDisplayFramerateMode;
1518
}
1519
1520
std::vector<std::string> System_GetCameraDeviceList() {
1521
jclass cameraClass = findClass("org/ppsspp/ppsspp/CameraHelper");
1522
jmethodID deviceListMethod = getEnv()->GetStaticMethodID(cameraClass, "getDeviceList", "()Ljava/util/ArrayList;");
1523
jobject deviceListObject = getEnv()->CallStaticObjectMethod(cameraClass, deviceListMethod);
1524
jclass arrayListClass = getEnv()->FindClass("java/util/ArrayList");
1525
jmethodID arrayListSize = getEnv()->GetMethodID(arrayListClass, "size", "()I");
1526
jmethodID arrayListGet = getEnv()->GetMethodID(arrayListClass, "get", "(I)Ljava/lang/Object;");
1527
1528
jint arrayListObjectLen = getEnv()->CallIntMethod(deviceListObject, arrayListSize);
1529
std::vector<std::string> deviceListVector;
1530
1531
for (int i = 0; i < arrayListObjectLen; i++) {
1532
jstring dev = static_cast<jstring>(getEnv()->CallObjectMethod(deviceListObject, arrayListGet, i));
1533
const char *cdev = getEnv()->GetStringUTFChars(dev, nullptr);
1534
if (!cdev) {
1535
getEnv()->DeleteLocalRef(dev);
1536
continue;
1537
}
1538
deviceListVector.emplace_back(cdev);
1539
getEnv()->ReleaseStringUTFChars(dev, cdev);
1540
getEnv()->DeleteLocalRef(dev);
1541
}
1542
return deviceListVector;
1543
}
1544
1545
extern "C" jint Java_org_ppsspp_ppsspp_NativeApp_getSelectedCamera(JNIEnv *, jclass) {
1546
int cameraId = 0;
1547
sscanf(g_Config.sCameraDevice.c_str(), "%d:", &cameraId);
1548
return cameraId;
1549
}
1550
1551
extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_setGpsDataAndroid(JNIEnv *, jclass,
1552
jlong time, jfloat hdop, jfloat latitude, jfloat longitude, jfloat altitude, jfloat speed, jfloat bearing) {
1553
GPS::setGpsData(time, hdop, latitude, longitude, altitude, speed, bearing);
1554
}
1555
1556
extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_setSatInfoAndroid(JNIEnv *, jclass,
1557
jshort index, jshort id, jshort elevation, jshort azimuth, jshort snr, jshort good) {
1558
GPS::setSatInfo(index, id, elevation, azimuth, snr, good);
1559
}
1560
1561
extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_pushCameraImageAndroid(JNIEnv *env, jclass, jbyteArray image) {
1562
if (image) {
1563
jlong size = env->GetArrayLength(image);
1564
jbyte* buffer = env->GetByteArrayElements(image, nullptr);
1565
Camera::pushCameraImage(size, (unsigned char *)buffer);
1566
env->ReleaseByteArrayElements(image, buffer, JNI_ABORT);
1567
}
1568
}
1569
1570
// Call this under frameCommandLock.
1571
static void ProcessFrameCommands(JNIEnv *env) {
1572
while (!frameCommands.empty()) {
1573
FrameCommand frameCmd;
1574
frameCmd = frameCommands.front();
1575
frameCommands.pop();
1576
1577
DEBUG_LOG(Log::System, "frameCommand '%s' '%s'", frameCmd.command.c_str(), frameCmd.params.c_str());
1578
1579
jstring cmd = env->NewStringUTF(frameCmd.command.c_str());
1580
jstring param = env->NewStringUTF(frameCmd.params.c_str());
1581
env->CallVoidMethod(nativeActivity, postCommand, cmd, param);
1582
env->DeleteLocalRef(cmd);
1583
env->DeleteLocalRef(param);
1584
}
1585
}
1586
1587
std::thread g_renderLoopThread;
1588
1589
static void VulkanEmuThread(ANativeWindow *wnd);
1590
1591
// This runs in Vulkan mode only.
1592
// This handles the entire lifecycle of the Vulkan context, init and exit.
1593
extern "C" jboolean JNICALL Java_org_ppsspp_ppsspp_NativeActivity_runVulkanRenderLoop(JNIEnv * env, jobject obj, jobject _surf) {
1594
_assert_(!useCPUThread);
1595
1596
if (!graphicsContext) {
1597
ERROR_LOG(Log::G3D, "runVulkanRenderLoop: Tried to enter without a created graphics context.");
1598
return false;
1599
}
1600
1601
if (g_renderLoopThread.joinable()) {
1602
ERROR_LOG(Log::G3D, "runVulkanRenderLoop: Already running");
1603
return false;
1604
}
1605
1606
ANativeWindow *wnd = _surf ? ANativeWindow_fromSurface(env, _surf) : nullptr;
1607
1608
if (!wnd) {
1609
// This shouldn't ever happen.
1610
ERROR_LOG(Log::G3D, "Error: Surface is null.");
1611
renderLoopRunning = false;
1612
return false;
1613
}
1614
1615
g_renderLoopThread = std::thread(VulkanEmuThread, wnd);
1616
return true;
1617
}
1618
1619
extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeActivity_requestExitVulkanRenderLoop(JNIEnv * env, jobject obj) {
1620
if (!renderLoopRunning) {
1621
ERROR_LOG(Log::System, "Render loop already exited");
1622
return;
1623
}
1624
_assert_(g_renderLoopThread.joinable());
1625
exitRenderLoop = true;
1626
g_renderLoopThread.join();
1627
_assert_(!g_renderLoopThread.joinable());
1628
g_renderLoopThread = std::thread();
1629
}
1630
1631
// TODO: Merge with the Win32 EmuThread and so on, and the Java EmuThread?
1632
static void VulkanEmuThread(ANativeWindow *wnd) {
1633
SetCurrentThreadName("EmuThread");
1634
1635
AndroidJNIThreadContext ctx;
1636
JNIEnv *env = getEnv();
1637
1638
if (!graphicsContext) {
1639
ERROR_LOG(Log::G3D, "runVulkanRenderLoop: Tried to enter without a created graphics context.");
1640
renderLoopRunning = false;
1641
exitRenderLoop = false;
1642
return;
1643
}
1644
1645
if (exitRenderLoop) {
1646
WARN_LOG(Log::G3D, "runVulkanRenderLoop: ExitRenderLoop requested at start, skipping the whole thing.");
1647
renderLoopRunning = false;
1648
exitRenderLoop = false;
1649
return;
1650
}
1651
1652
// This is up here to prevent race conditions, in case we pause during init.
1653
renderLoopRunning = true;
1654
1655
WARN_LOG(Log::G3D, "runVulkanRenderLoop. display_xres=%d display_yres=%d desiredBackbufferSizeX=%d desiredBackbufferSizeY=%d",
1656
display_xres, display_yres, desiredBackbufferSizeX, desiredBackbufferSizeY);
1657
1658
if (!graphicsContext->InitFromRenderThread(wnd, desiredBackbufferSizeX, desiredBackbufferSizeY, backbuffer_format, androidVersion)) {
1659
// On Android, if we get here, really no point in continuing.
1660
// The UI is supposed to render on any device both on OpenGL and Vulkan. If either of those don't work
1661
// on a device, we blacklist it. Hopefully we should have already failed in InitAPI anyway and reverted to GL back then.
1662
ERROR_LOG(Log::G3D, "Failed to initialize graphics context.");
1663
System_Toast("Failed to initialize graphics context.");
1664
1665
delete graphicsContext;
1666
graphicsContext = nullptr;
1667
renderLoopRunning = false;
1668
return;
1669
}
1670
1671
if (!exitRenderLoop) {
1672
if (!NativeInitGraphics(graphicsContext)) {
1673
ERROR_LOG(Log::G3D, "Failed to initialize graphics.");
1674
// Gonna be in a weird state here..
1675
}
1676
graphicsContext->ThreadStart();
1677
renderer_inited = true;
1678
1679
while (!exitRenderLoop) {
1680
{
1681
std::lock_guard<std::mutex> renderGuard(renderLock);
1682
NativeFrame(graphicsContext);
1683
}
1684
{
1685
std::lock_guard<std::mutex> guard(frameCommandLock);
1686
ProcessFrameCommands(env);
1687
}
1688
}
1689
INFO_LOG(Log::G3D, "Leaving Vulkan main loop.");
1690
} else {
1691
INFO_LOG(Log::G3D, "Not entering main loop.");
1692
}
1693
1694
NativeShutdownGraphics();
1695
1696
renderer_inited = false;
1697
graphicsContext->ThreadEnd();
1698
1699
// Shut the graphics context down to the same state it was in when we entered the render thread.
1700
INFO_LOG(Log::G3D, "Shutting down graphics context...");
1701
graphicsContext->ShutdownFromRenderThread();
1702
renderLoopRunning = false;
1703
exitRenderLoop = false;
1704
1705
WARN_LOG(Log::G3D, "Render loop function exited.");
1706
}
1707
1708
// NOTE: This is defunct and not working, due to how the Android storage functions currently require
1709
// a PpssppActivity specifically and we don't have one here.
1710
extern "C" jstring Java_org_ppsspp_ppsspp_ShortcutActivity_queryGameName(JNIEnv * env, jclass, jstring jpath) {
1711
bool teardownThreadManager = false;
1712
if (!g_threadManager.IsInitialized()) {
1713
INFO_LOG(Log::System, "No thread manager - initializing one");
1714
// Need a thread manager.
1715
teardownThreadManager = true;
1716
g_threadManager.Init(1, 1);
1717
}
1718
1719
Path path = Path(GetJavaString(env, jpath));
1720
1721
INFO_LOG(Log::System, "queryGameName(%s)", path.c_str());
1722
1723
std::string result;
1724
1725
GameInfoCache *cache = new GameInfoCache();
1726
std::shared_ptr<GameInfo> info = cache->GetInfo(nullptr, path, GameInfoFlags::PARAM_SFO);
1727
// Wait until it's done: this is synchronous, unfortunately.
1728
if (info) {
1729
INFO_LOG(Log::System, "GetInfo successful, waiting");
1730
while (!info->Ready(GameInfoFlags::PARAM_SFO)) {
1731
sleep_ms(1, "info-poll");
1732
}
1733
INFO_LOG(Log::System, "Done waiting");
1734
if (info->fileType != IdentifiedFileType::UNKNOWN) {
1735
result = info->GetTitle();
1736
1737
// Pretty arbitrary, but the home screen will often truncate titles.
1738
// Let's remove "The " from names since it's common in English titles.
1739
if (result.length() > strlen("The ") && startsWithNoCase(result, "The ")) {
1740
result = result.substr(strlen("The "));
1741
}
1742
1743
INFO_LOG(Log::System, "queryGameName: Got '%s'", result.c_str());
1744
} else {
1745
INFO_LOG(Log::System, "queryGameName: Filetype unknown");
1746
}
1747
} else {
1748
INFO_LOG(Log::System, "No info from cache");
1749
}
1750
delete cache;
1751
1752
if (teardownThreadManager) {
1753
g_threadManager.Teardown();
1754
}
1755
1756
return env->NewStringUTF(result.c_str());
1757
}
1758
1759
1760
extern "C"
1761
JNIEXPORT jbyteArray JNICALL
1762
Java_org_ppsspp_ppsspp_ShortcutActivity_queryGameIcon(JNIEnv * env, jclass clazz, jstring jpath) {
1763
bool teardownThreadManager = false;
1764
if (!g_threadManager.IsInitialized()) {
1765
INFO_LOG(Log::System, "No thread manager - initializing one");
1766
// Need a thread manager.
1767
teardownThreadManager = true;
1768
g_threadManager.Init(1, 1);
1769
}
1770
// TODO: implement requestIcon()
1771
1772
Path path = Path(GetJavaString(env, jpath));
1773
1774
INFO_LOG(Log::System, "queryGameIcon(%s)", path.c_str());
1775
1776
jbyteArray result = nullptr;
1777
1778
GameInfoCache *cache = new GameInfoCache();
1779
std::shared_ptr<GameInfo> info = cache->GetInfo(nullptr, path, GameInfoFlags::ICON);
1780
// Wait until it's done: this is synchronous, unfortunately.
1781
if (info) {
1782
INFO_LOG(Log::System, "GetInfo successful, waiting");
1783
int attempts = 1000;
1784
while (!info->Ready(GameInfoFlags::ICON)) {
1785
sleep_ms(1, "icon-poll");
1786
attempts--;
1787
if (!attempts) {
1788
break;
1789
}
1790
}
1791
INFO_LOG(Log::System, "Done waiting");
1792
if (info->Ready(GameInfoFlags::ICON)) {
1793
if (!info->icon.data.empty()) {
1794
INFO_LOG(Log::System, "requestIcon: Got icon");
1795
result = env->NewByteArray((jsize)info->icon.data.size());
1796
env->SetByteArrayRegion(result, 0, (jsize)info->icon.data.size(), (const jbyte *)info->icon.data.data());
1797
}
1798
} else {
1799
INFO_LOG(Log::System, "requestIcon: Filetype unknown");
1800
}
1801
} else {
1802
INFO_LOG(Log::System, "No info from cache");
1803
}
1804
1805
delete cache;
1806
1807
if (teardownThreadManager) {
1808
g_threadManager.Teardown();
1809
}
1810
1811
return result;
1812
}
1813
1814