mirror of
https://github.com/ARMSX2/ARMSX3.git
synced 2026-08-24 16:58:52 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dca685b67d | ||
|
|
78650f32d8 | ||
|
|
24951bb863 | ||
|
|
620eaf5066 | ||
|
|
5d91f8c56b | ||
|
|
06b33abf27 |
Vendored
+1
-1
Submodule 3rdparty/discord-rpc/discord-rpc updated: fb04b1766b...3dc2c326cb
Vendored
+1
-1
Submodule 3rdparty/libsdl-org/SDL updated: 147a8ee32d...f87239e71e
@@ -34,8 +34,8 @@ android {
|
||||
// agree -- an APK that installs below its core's target is a dlopen failure at boot.
|
||||
minSdk = (project.findProperty("armsx3.minSdk") as String?)?.toInt() ?: 33
|
||||
targetSdk = 37
|
||||
versionCode = 22
|
||||
versionName = "0.9.4.2"
|
||||
versionCode = 23
|
||||
versionName = "0.9.4.3"
|
||||
|
||||
// ARMSX2's UI reads these. STORAGE_ALL_FILES gates the all-files storage path in
|
||||
// onboarding; IN_APP_UPDATER gates the in-app GitHub-release updater.
|
||||
|
||||
@@ -23,10 +23,21 @@ namespace { bool g_started = false; }
|
||||
|
||||
extern "C" {
|
||||
|
||||
/* Defined below. port_new tears a stale run down through it rather than trusting
|
||||
* g_started, so the declaration has to come first. */
|
||||
void flux_port_free();
|
||||
|
||||
/* preset is 1..6, matching the saver's own DEFAULTS1..DEFAULTS6. */
|
||||
int flux_port_new(int preset)
|
||||
{
|
||||
if (g_started) return 1;
|
||||
/* NOT "if (g_started) return 1": nativeInit calls gl1_lost() before every create, so
|
||||
* gl1 is guaranteed DOWN on entry now. Reporting success here would hand the caller a
|
||||
* saver with no shim under it. That guard was only ever safe because gl1 state
|
||||
* survived between savers -- which is exactly the property gl1_lost() removed, so it
|
||||
* went from redundant to wrong. Unreachable today (port_free always clears g_started),
|
||||
* but the rule is that a stale run is torn down and gl1_init() always runs, rather
|
||||
* than that every caller gets the ordering right. */
|
||||
if (g_started) flux_port_free();
|
||||
if (!gl1_init()) return 0;
|
||||
|
||||
if (preset < 1 || preset > 6) preset = 1;
|
||||
@@ -40,7 +51,14 @@ int flux_port_new(int preset)
|
||||
|
||||
saver_flux::initSaver();
|
||||
g_started = saver_flux::readyToDraw != 0;
|
||||
return g_started ? 1 : 0;
|
||||
if (!g_started) {
|
||||
/* Returning 0 means the JNI never calls port_free, so this is the only chance to give
|
||||
* gl1 back. Leaving it up would strand g.ready with names from a context that is about
|
||||
* to die, and gl1_init() early-returns on g.ready -- poisoning the NEXT saver. */
|
||||
gl1_shutdown();
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
void flux_port_resize(int width, int height)
|
||||
@@ -58,11 +76,20 @@ void flux_port_draw()
|
||||
|
||||
void flux_port_free()
|
||||
{
|
||||
if (!g_started) return;
|
||||
saver_flux::cleanUp();
|
||||
/* NOT "if (!g_started) return": port_new releases gl1 itself on the path where it
|
||||
* returns 0, so this is unreachable with g_started false today. It is written this
|
||||
* way so that stays true by construction rather than by that argument -- the rule is
|
||||
* that gl1 goes back on every exit, in every port, without a caller having to reason
|
||||
* about which ones can be skipped. */
|
||||
if (g_started) {
|
||||
saver_flux::cleanUp();
|
||||
saver_flux::readyToDraw = 0;
|
||||
g_started = false;
|
||||
}
|
||||
/* Belongs to the EGL context that is about to be destroyed; leaving g.ready set means
|
||||
* gl1_init() early-returns for the NEXT saver and hands it dead GL names. Idempotent,
|
||||
* and a no-op if gl1 was never up. */
|
||||
gl1_shutdown();
|
||||
saver_flux::readyToDraw = 0;
|
||||
g_started = false;
|
||||
}
|
||||
|
||||
} /* extern "C" */
|
||||
|
||||
@@ -72,7 +72,13 @@
|
||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "Savers", __VA_ARGS__)
|
||||
|
||||
#define STACK_DEPTH 32
|
||||
#define GL1_MAX_LISTS 16
|
||||
/* Sixteen was not enough for two of the savers, and the failure was silent-ish: glGenLists
|
||||
* returned 0, every subsequent glNewList/glCallList logged "bad list", and the geometry those
|
||||
* lists held simply never drew. Lattice asks for 20 in one range (NUMOBJECTS), and Skyrocket
|
||||
* accumulates 16 across separate calls -- flare 4, smoke 5, and seven singles in world.cpp.
|
||||
* Names are 1-based, so the old ceiling allowed a longest run of 15 and neither could ever
|
||||
* succeed. Sixty-four is a slot table of a pointer and two ints apiece: about a kilobyte. */
|
||||
#define GL1_MAX_LISTS 64
|
||||
|
||||
typedef struct {
|
||||
float pos[3];
|
||||
|
||||
@@ -24,10 +24,21 @@ namespace { bool g_started = false; }
|
||||
|
||||
extern "C" {
|
||||
|
||||
/* Defined below. port_new tears a stale run down through it rather than trusting
|
||||
* g_started, so the declaration has to come first. */
|
||||
void hyperspace_port_free();
|
||||
|
||||
int hyperspace_port_new(int preset)
|
||||
{
|
||||
(void) preset; /* No presets upstream; every knob was a registry value. */
|
||||
if (g_started) return 1;
|
||||
/* NOT "if (g_started) return 1": nativeInit calls gl1_lost() before every create, so
|
||||
* gl1 is guaranteed DOWN on entry now. Reporting success here would hand the caller a
|
||||
* saver with no shim under it. That guard was only ever safe because gl1 state
|
||||
* survived between savers -- which is exactly the property gl1_lost() removed, so it
|
||||
* went from redundant to wrong. Unreachable today (port_free always clears g_started),
|
||||
* but the rule is that a stale run is torn down and gl1_init() always runs, rather
|
||||
* than that every caller gets the ordering right. */
|
||||
if (g_started) hyperspace_port_free();
|
||||
if (!gl1_init()) return 0;
|
||||
|
||||
saver_hyperspace::setDefaults();
|
||||
@@ -59,11 +70,21 @@ void hyperspace_port_draw()
|
||||
|
||||
void hyperspace_port_free()
|
||||
{
|
||||
if (!g_started) return;
|
||||
saver_hyperspace::cleanUp();
|
||||
/* NOT "if (!g_started) return": port_new sets g_started on every path that returns 1 today, so this
|
||||
* is defensive -- but the point is that gl1 is released regardless of it. See the note below. */
|
||||
if (g_started) {
|
||||
saver_hyperspace::cleanUp();
|
||||
saver_hyperspace::readyToDraw = 0;
|
||||
g_started = false;
|
||||
}
|
||||
/* gl1_init() ran in port_new, and everything it holds -- the shader program, the vertex
|
||||
* buffers -- belongs to the EGL context that is about to be destroyed. Returning without
|
||||
* gl1_shutdown() leaves gl1's g.ready set with GL names from a DEAD context, and gl1_init()
|
||||
* early-returns on g.ready. The next saver, in a NEW context, would then run against those
|
||||
* dead names: undefined behaviour that some drivers answer with a segfault rather than a GL
|
||||
* error, which takes the whole app down. So gl1 is torn down whether or not this saver's own
|
||||
* init ever got as far as running. gl1_shutdown() is idempotent. */
|
||||
gl1_shutdown();
|
||||
saver_hyperspace::readyToDraw = 0;
|
||||
g_started = false;
|
||||
}
|
||||
|
||||
} /* extern "C" */
|
||||
|
||||
@@ -23,9 +23,20 @@ int g_preset = 1;
|
||||
|
||||
extern "C" {
|
||||
|
||||
/* Defined below. port_new tears a stale run down through it rather than trusting
|
||||
* g_started, so the declaration has to come first. */
|
||||
void lattice_port_free();
|
||||
|
||||
int lattice_port_new(int preset)
|
||||
{
|
||||
if (g_started) return 1;
|
||||
/* NOT "if (g_started) return 1": nativeInit calls gl1_lost() before every create, so
|
||||
* gl1 is guaranteed DOWN on entry now. Reporting success here would hand the caller a
|
||||
* saver with no shim under it. That guard was only ever safe because gl1 state
|
||||
* survived between savers -- which is exactly the property gl1_lost() removed, so it
|
||||
* went from redundant to wrong. Unreachable today (port_free always clears g_started),
|
||||
* but the rule is that a stale run is torn down and gl1_init() always runs, rather
|
||||
* than that every caller gets the ordering right. */
|
||||
if (g_started) lattice_port_free();
|
||||
if (!gl1_init()) return 0;
|
||||
|
||||
g_preset = (preset >= 1 && preset <= 6) ? preset : 1;
|
||||
@@ -53,11 +64,21 @@ void lattice_port_draw()
|
||||
|
||||
void lattice_port_free()
|
||||
{
|
||||
if (!g_started) return;
|
||||
saver_lattice::cleanUp();
|
||||
/* NOT "if (!g_started) return": this saver waits for a surface size before it initialises,
|
||||
* so it can be created and torn down having never started. See the note below. */
|
||||
if (g_started) {
|
||||
saver_lattice::cleanUp();
|
||||
saver_lattice::readyToDraw = 0;
|
||||
g_started = false;
|
||||
}
|
||||
/* gl1_init() ran in port_new, and everything it holds -- the shader program, the vertex
|
||||
* buffers -- belongs to the EGL context that is about to be destroyed. Returning without
|
||||
* gl1_shutdown() leaves gl1's g.ready set with GL names from a DEAD context, and gl1_init()
|
||||
* early-returns on g.ready. The next saver, in a NEW context, would then run against those
|
||||
* dead names: undefined behaviour that some drivers answer with a segfault rather than a GL
|
||||
* error, which takes the whole app down. So gl1 is torn down whether or not this saver's own
|
||||
* init ever got as far as running. gl1_shutdown() is idempotent. */
|
||||
gl1_shutdown();
|
||||
saver_lattice::readyToDraw = 0;
|
||||
g_started = false;
|
||||
}
|
||||
|
||||
} /* extern "C" */
|
||||
|
||||
@@ -16,9 +16,20 @@ namespace { bool g_started = false; }
|
||||
|
||||
extern "C" {
|
||||
|
||||
/* Defined below. port_new tears a stale run down through it rather than trusting
|
||||
* g_started, so the declaration has to come first. */
|
||||
void plasma_port_free();
|
||||
|
||||
int plasma_port_new(int preset)
|
||||
{
|
||||
if (g_started) return 1;
|
||||
/* NOT "if (g_started) return 1": nativeInit calls gl1_lost() before every create, so
|
||||
* gl1 is guaranteed DOWN on entry now. Reporting success here would hand the caller a
|
||||
* saver with no shim under it. That guard was only ever safe because gl1 state
|
||||
* survived between savers -- which is exactly the property gl1_lost() removed, so it
|
||||
* went from redundant to wrong. Unreachable today (port_free always clears g_started),
|
||||
* but the rule is that a stale run is torn down and gl1_init() always runs, rather
|
||||
* than that every caller gets the ordering right. */
|
||||
if (g_started) plasma_port_free();
|
||||
if (!gl1_init()) return 0;
|
||||
|
||||
saver_plasma::setDefaults();
|
||||
@@ -42,7 +53,14 @@ int plasma_port_new(int preset)
|
||||
|
||||
saver_plasma::initSaver();
|
||||
g_started = saver_plasma::readyToDraw != 0;
|
||||
return g_started ? 1 : 0;
|
||||
if (!g_started) {
|
||||
/* Returning 0 means the JNI never calls port_free, so this is the only chance to give
|
||||
* gl1 back. Leaving it up would strand g.ready with names from a context that is about
|
||||
* to die, and gl1_init() early-returns on g.ready -- poisoning the NEXT saver. */
|
||||
gl1_shutdown();
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
void plasma_port_resize(int width, int height)
|
||||
@@ -59,11 +77,20 @@ void plasma_port_draw()
|
||||
|
||||
void plasma_port_free()
|
||||
{
|
||||
if (!g_started) return;
|
||||
saver_plasma::cleanUp();
|
||||
/* NOT "if (!g_started) return": port_new releases gl1 itself on the path where it
|
||||
* returns 0, so this is unreachable with g_started false today. It is written this
|
||||
* way so that stays true by construction rather than by that argument -- the rule is
|
||||
* that gl1 goes back on every exit, in every port, without a caller having to reason
|
||||
* about which ones can be skipped. */
|
||||
if (g_started) {
|
||||
saver_plasma::cleanUp();
|
||||
saver_plasma::readyToDraw = 0;
|
||||
g_started = false;
|
||||
}
|
||||
/* Belongs to the EGL context that is about to be destroyed; leaving g.ready set means
|
||||
* gl1_init() early-returns for the NEXT saver and hands it dead GL names. Idempotent,
|
||||
* and a no-op if gl1 was never up. */
|
||||
gl1_shutdown();
|
||||
saver_plasma::readyToDraw = 0;
|
||||
g_started = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
#include <cstddef>
|
||||
#include <mutex>
|
||||
|
||||
#include "gl1.h"
|
||||
|
||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "Savers", __VA_ARGS__)
|
||||
|
||||
#define SAVER_DECL(name) \
|
||||
@@ -82,6 +84,21 @@ Java_com_armsx2_ui_home_SaverNative_nativeInit(JNIEnv *, jobject, jint effect, j
|
||||
/* A previous saver may still be up if the outgoing view has not torn down yet. */
|
||||
if (g_active) g_active->destroy();
|
||||
|
||||
/* Every nativeInit arrives on a freshly created EGL context -- one view, one render thread,
|
||||
* one context, one create. So whatever GL names gl1 is still holding belong to a context
|
||||
* that no longer exists, and gl1_init() early-returns on g.ready, which would hand those
|
||||
* dead names to the saver we are about to start. gl1_lost() drops them without calling GL
|
||||
* on them (gl1_shutdown() would try to delete them, in the wrong context).
|
||||
*
|
||||
* The ports each give gl1 back on their own failure and teardown paths, so this should
|
||||
* already be a no-op. It is here because it is the invariant that actually matters -- a
|
||||
* saver added later that forgets, or an upstream cleanup that returns early, would
|
||||
* otherwise poison the NEXT saver rather than fail visibly in its own. Drivers answer a
|
||||
* draw against a dead program name with anything from a black screen to a segfault, and a
|
||||
* segfault here is unrecoverable: the library is the first screen, so the app would crash
|
||||
* on every launch. */
|
||||
gl1_lost();
|
||||
|
||||
g_active = &k_savers[effect];
|
||||
if (!g_active->create(preset)) {
|
||||
LOGE("%s failed to start (preset %d)", g_active->name, preset);
|
||||
|
||||
@@ -26,10 +26,21 @@ bool g_started = false;
|
||||
|
||||
extern "C" {
|
||||
|
||||
/* Defined below. port_new tears a stale run down through it rather than trusting
|
||||
* g_started, so the declaration has to come first. */
|
||||
void skyrocket_port_free();
|
||||
|
||||
int skyrocket_port_new(int preset)
|
||||
{
|
||||
(void) preset; /* No presets upstream; every knob was a registry value. */
|
||||
if (g_started) return 1;
|
||||
/* NOT "if (g_started) return 1": nativeInit calls gl1_lost() before every create, so
|
||||
* gl1 is guaranteed DOWN on entry now. Reporting success here would hand the caller a
|
||||
* saver with no shim under it. That guard was only ever safe because gl1 state
|
||||
* survived between savers -- which is exactly the property gl1_lost() removed, so it
|
||||
* went from redundant to wrong. Unreachable today (port_free always clears g_started),
|
||||
* but the rule is that a stale run is torn down and gl1_init() always runs, rather
|
||||
* than that every caller gets the ordering right. */
|
||||
if (g_started) skyrocket_port_free();
|
||||
if (!gl1_init()) return 0;
|
||||
|
||||
saver_skyrocket::setDefaults();
|
||||
@@ -69,11 +80,21 @@ void skyrocket_port_draw()
|
||||
|
||||
void skyrocket_port_free()
|
||||
{
|
||||
if (!g_started) return;
|
||||
saver_skyrocket::cleanup();
|
||||
/* NOT "if (!g_started) return": this saver waits for a surface size before it initialises,
|
||||
* so it can be created and torn down having never started. See the note below. */
|
||||
if (g_started) {
|
||||
saver_skyrocket::cleanup();
|
||||
saver_skyrocket::readyToDraw = 0;
|
||||
g_started = false;
|
||||
}
|
||||
/* gl1_init() ran in port_new, and everything it holds -- the shader program, the vertex
|
||||
* buffers -- belongs to the EGL context that is about to be destroyed. Returning without
|
||||
* gl1_shutdown() leaves gl1's g.ready set with GL names from a DEAD context, and gl1_init()
|
||||
* early-returns on g.ready. The next saver, in a NEW context, would then run against those
|
||||
* dead names: undefined behaviour that some drivers answer with a segfault rather than a GL
|
||||
* error, which takes the whole app down. So gl1 is torn down whether or not this saver's own
|
||||
* init ever got as far as running. gl1_shutdown() is idempotent. */
|
||||
gl1_shutdown();
|
||||
saver_skyrocket::readyToDraw = 0;
|
||||
g_started = false;
|
||||
}
|
||||
|
||||
} /* extern "C" */
|
||||
|
||||
@@ -15,11 +15,22 @@ namespace { bool g_started = false; }
|
||||
|
||||
extern "C" {
|
||||
|
||||
/* Defined below. port_new tears a stale run down through it rather than trusting
|
||||
* g_started, so the declaration has to come first. */
|
||||
void solarwinds_port_free();
|
||||
|
||||
/* preset is 1..6 from the UI. Upstream's DEFAULTS1..DEFAULTS6 is a zero-based ENUM here, not
|
||||
* the 1-based #defines Flux uses, so the UI value is shifted down. */
|
||||
int solarwinds_port_new(int preset)
|
||||
{
|
||||
if (g_started) return 1;
|
||||
/* NOT "if (g_started) return 1": nativeInit calls gl1_lost() before every create, so
|
||||
* gl1 is guaranteed DOWN on entry now. Reporting success here would hand the caller a
|
||||
* saver with no shim under it. That guard was only ever safe because gl1 state
|
||||
* survived between savers -- which is exactly the property gl1_lost() removed, so it
|
||||
* went from redundant to wrong. Unreachable today (port_free always clears g_started),
|
||||
* but the rule is that a stale run is torn down and gl1_init() always runs, rather
|
||||
* than that every caller gets the ordering right. */
|
||||
if (g_started) solarwinds_port_free();
|
||||
if (!gl1_init()) return 0;
|
||||
|
||||
if (preset < 1 || preset > 6) preset = 1;
|
||||
@@ -27,7 +38,14 @@ int solarwinds_port_new(int preset)
|
||||
|
||||
saver_solarwinds::initSaver();
|
||||
g_started = saver_solarwinds::readyToDraw != 0;
|
||||
return g_started ? 1 : 0;
|
||||
if (!g_started) {
|
||||
/* Returning 0 means the JNI never calls port_free, so this is the only chance to give
|
||||
* gl1 back. Leaving it up would strand g.ready with names from a context that is about
|
||||
* to die, and gl1_init() early-returns on g.ready -- poisoning the NEXT saver. */
|
||||
gl1_shutdown();
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
void solarwinds_port_resize(int width, int height)
|
||||
@@ -45,11 +63,20 @@ void solarwinds_port_draw()
|
||||
|
||||
void solarwinds_port_free()
|
||||
{
|
||||
if (!g_started) return;
|
||||
saver_solarwinds::cleanUp();
|
||||
/* NOT "if (!g_started) return": port_new releases gl1 itself on the path where it
|
||||
* returns 0, so this is unreachable with g_started false today. It is written this
|
||||
* way so that stays true by construction rather than by that argument -- the rule is
|
||||
* that gl1 goes back on every exit, in every port, without a caller having to reason
|
||||
* about which ones can be skipped. */
|
||||
if (g_started) {
|
||||
saver_solarwinds::cleanUp();
|
||||
saver_solarwinds::readyToDraw = 0;
|
||||
g_started = false;
|
||||
}
|
||||
/* Belongs to the EGL context that is about to be destroyed; leaving g.ready set means
|
||||
* gl1_init() early-returns for the NEXT saver and hands it dead GL names. Idempotent,
|
||||
* and a no-op if gl1 was never up. */
|
||||
gl1_shutdown();
|
||||
saver_solarwinds::readyToDraw = 0;
|
||||
g_started = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -906,6 +906,9 @@ val EN: Map<String, String> = mapOf(
|
||||
"pad.players.help" to "The PS3 has seven controller ports and no multitap, so up to seven pads work with no setup. Connect them before launching — the order they first press a button in is the order they are assigned.",
|
||||
"pad.rumble.description" to "Master switch for controller rumble and the device's built-in vibration. Turn off to silence all haptics.",
|
||||
"pad.rumble.label" to "Rumble / Vibration",
|
||||
"pad.rumblePhone.label" to "Vibrate the phone",
|
||||
"pad.rumblePhone.description" to
|
||||
"Use the phone's own motor when no controller has one. Turn this off to keep rumble on the controller only.",
|
||||
"pad.hapticStrength.description" to "Scales all vibration — controller rumble and on-screen touch haptics alike. Below 100% tames a strong motor; above 100% boosts a weak one.",
|
||||
"pad.hapticStrength.label" to "Vibration Strength",
|
||||
"pad.scopeHint.global" to "â—‹ Editing GLOBAL controls (all games).",
|
||||
|
||||
@@ -393,6 +393,17 @@ object ControllerMappings {
|
||||
com.armsx3.NativeApp.sRumbleEnabled = on
|
||||
}
|
||||
|
||||
// Whether the PHONE's motor may be used. Rumble prefers a connected controller's motor and
|
||||
// falls back to the phone; this gates only that fallback, so playing on a pad need not mean
|
||||
// the phone buzzes too. Mirrored into NativeApp.sPhoneRumbleEnabled the same way KEY_RUMBLE
|
||||
// is — live on change and at app start. Default on, so a phone-only player is unaffected.
|
||||
private const val KEY_RUMBLE_PHONE = "pad.rumble.phone"
|
||||
fun phoneRumbleEnabled(): Boolean = MainActivityRuntime.prefs.getBoolean(KEY_RUMBLE_PHONE, true)
|
||||
fun setPhoneRumbleEnabled(on: Boolean) {
|
||||
MainActivityRuntime.prefs.edit { putBoolean(KEY_RUMBLE_PHONE, on) }
|
||||
com.armsx3.NativeApp.sPhoneRumbleEnabled = on
|
||||
}
|
||||
|
||||
// Haptic strength: one multiplier scaling ALL vibration — controller rumble AND on-screen
|
||||
// touch ticks both funnel through NativeApp.rumbleOne. 0..200 % (100 = as the game/UI
|
||||
// authored it), so it tames a too-strong motor or boosts a weak one. Persisted and mirrored
|
||||
|
||||
@@ -2157,6 +2157,7 @@ open class MainActivityRuntime : ComponentActivity() {
|
||||
startAutosaveIntervalJob()
|
||||
// Restore the saved rumble master toggle into the native gate (NativeApp.onPadRumble).
|
||||
NativeApp.sRumbleEnabled = ControllerMappings.rumbleEnabled()
|
||||
NativeApp.sPhoneRumbleEnabled = ControllerMappings.phoneRumbleEnabled()
|
||||
// Push the saved haptic strength + achievement-sound volume into their native gates before
|
||||
// any rumble or unlock sound can fire (both default to 1.0 = as authored until set here).
|
||||
ControllerMappings.syncHapticIntensity()
|
||||
|
||||
@@ -147,6 +147,19 @@ fun HomeScreen(
|
||||
var showClearRecentsConfirm by remember { mutableStateOf(false) }
|
||||
// #9 custom library background — inert until the user picks an image.
|
||||
LaunchedEffect(Unit) { LibraryBackground.ensureLoaded(); CoverArtStyle.load() }
|
||||
// The animated background switched itself off because the last run died with it on screen
|
||||
// (LibraryBackground.armSaver). Say so -- silently reverting a setting the user chose reads
|
||||
// as the setting being broken, and the name tells them which one to avoid.
|
||||
LaunchedEffect(LibraryBackground.crashedSaver.value) {
|
||||
LibraryBackground.crashedSaver.value?.let { kind ->
|
||||
LibraryBackground.crashedSaver.value = null
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Animated background turned off: ${LibraryBackground.saverName(kind)} crashed last time.",
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
val backgroundPicker = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { picked ->
|
||||
picked?.let { LibraryBackground.set(context, it) }
|
||||
}
|
||||
|
||||
@@ -21,6 +21,12 @@ object LibraryBackground {
|
||||
private const val PREF_FLURRY_PRESET = "library.background.flurry.preset"
|
||||
private const val PREF_SAVER_KIND = "library_saver_kind"
|
||||
private const val PREF_RSS_PRESET = "library_rss_preset"
|
||||
|
||||
/**
|
||||
* Which saver is CURRENTLY running, written synchronously before its GL thread starts and
|
||||
* cleared when that thread exits in an orderly way. See [armSaver].
|
||||
*/
|
||||
private const val PREF_ARMED = "library.saver.armed"
|
||||
val uri = mutableStateOf<String?>(null)
|
||||
|
||||
/**
|
||||
@@ -60,6 +66,53 @@ object LibraryBackground {
|
||||
/** Preset for whichever Really Slick saver is selected, 1..6. 99 = pick one each time. */
|
||||
val rssPreset = mutableStateOf(99)
|
||||
|
||||
/**
|
||||
* Set at startup when the previous run died with a saver on screen. Holds the [saverKind]
|
||||
* that was running so the library can say which one, and so the user knows their background
|
||||
* was turned off deliberately rather than forgotten. Read once and cleared by the reader.
|
||||
*/
|
||||
val crashedSaver = mutableStateOf<Int?>(null)
|
||||
|
||||
/** Display name for a [saverKind], for the message above. Matches the settings list. */
|
||||
fun saverName(kind: Int): String = when (kind) {
|
||||
1 -> "Flux"; 2 -> "Plasma"; 3 -> "SolarWinds"
|
||||
4 -> "Hyperspace"; 5 -> "Lattice"; 6 -> "Skyrocket"
|
||||
else -> "Flurry"
|
||||
}
|
||||
|
||||
/**
|
||||
* Crash-loop breaker.
|
||||
*
|
||||
* The savers are native GL code, and native GL code can take the process down in ways no
|
||||
* `runCatching` can see -- a SIGSEGV in a driver, or a hang that Android resolves by killing
|
||||
* us. Because the choice is persisted and the library is the FIRST screen, a saver that dies
|
||||
* on startup dies again on every launch: the app never gets far enough for anyone to reach
|
||||
* Settings and switch it off. The only escape is clearing app data, which on Android takes
|
||||
* the memory cards and save states with it.
|
||||
*
|
||||
* So the setting arms itself before the GL thread starts and disarms when that thread exits
|
||||
* normally. Finding it still armed at startup means last run ended while a saver was on
|
||||
* screen -- the background is switched off and the user is told which one did it. The write
|
||||
* must be commit() rather than apply(): apply() is asynchronous, and the whole point is that
|
||||
* the process may be about to die.
|
||||
*/
|
||||
fun armSaver() {
|
||||
runCatching {
|
||||
MainActivityRuntime.prefs.edit().putInt(PREF_ARMED, saverKind.value).commit()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Orderly teardown -- the saver ran without taking the process with it. Idempotent.
|
||||
*
|
||||
* apply() rather than commit() on purpose, and the asymmetry with [armSaver] is the point:
|
||||
* this one is not racing the process's death, and if it were lost the cost is a background
|
||||
* switched off for no reason, which is recoverable from Settings. The arm must never be lost.
|
||||
*/
|
||||
fun disarmSaver() {
|
||||
runCatching { MainActivityRuntime.prefs.edit().remove(PREF_ARMED).apply() }
|
||||
}
|
||||
|
||||
private var loaded = false
|
||||
|
||||
fun ensureLoaded() {
|
||||
@@ -71,6 +124,14 @@ object LibraryBackground {
|
||||
flurryPreset.value = runCatching { MainActivityRuntime.prefs.getInt(PREF_FLURRY_PRESET, 99) }.getOrDefault(99)
|
||||
saverKind.value = runCatching { MainActivityRuntime.prefs.getInt(PREF_SAVER_KIND, 0) }.getOrDefault(0)
|
||||
rssPreset.value = runCatching { MainActivityRuntime.prefs.getInt(PREF_RSS_PRESET, 99) }.getOrDefault(99)
|
||||
|
||||
// Still armed = the previous run died with a saver up. Break the loop (see armSaver).
|
||||
val armed = runCatching { MainActivityRuntime.prefs.getInt(PREF_ARMED, -1) }.getOrDefault(-1)
|
||||
if (armed >= 0) {
|
||||
crashedSaver.value = armed
|
||||
setFlurry(false)
|
||||
disarmSaver()
|
||||
}
|
||||
}
|
||||
|
||||
fun setAnimated2D(on: Boolean) {
|
||||
|
||||
@@ -97,8 +97,22 @@ class SaverGlView(context: Context, private val spec: SaverSpec) :
|
||||
}
|
||||
|
||||
override fun onSurfaceTextureAvailable(st: SurfaceTexture, w: Int, h: Int) {
|
||||
thread = RenderThread(st, w, h, spec) { ok -> post { onGlStatus?.invoke(ok) } }
|
||||
.also { it.start() }
|
||||
// Arm the crash-loop breaker for as long as native GL code is running on our behalf; the
|
||||
// thread disarms it when it exits in an orderly way. See LibraryBackground.armSaver.
|
||||
LibraryBackground.armSaver()
|
||||
// start() asks for a 16MB stack (see STACK_BYTES) and can throw OutOfMemoryError on a
|
||||
// constrained device. That would be an uncaught throw on the MAIN thread -- the process
|
||||
// dies, and since this is the first screen the app would be unlaunchable. Fall back to
|
||||
// the 2D backdrop instead, the same way an EGL failure does.
|
||||
thread = runCatching {
|
||||
RenderThread(st, w, h, spec) { ok -> post { onGlStatus?.invoke(ok) } }
|
||||
.also { it.start() }
|
||||
}.getOrElse {
|
||||
Log.w(TAG, "saver thread failed to start", it)
|
||||
LibraryBackground.disarmSaver()
|
||||
onGlStatus?.invoke(false)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSurfaceTextureSizeChanged(st: SurfaceTexture, w: Int, h: Int) {
|
||||
@@ -152,9 +166,26 @@ class SaverGlView(context: Context, private val spec: SaverSpec) :
|
||||
private var saver: Saver? = null
|
||||
|
||||
fun resize(w: Int, h: Int) { width = w; height = h; sizeDirty = true }
|
||||
fun finish() { running = false; runCatching { join(500) } }
|
||||
fun finish() {
|
||||
running = false
|
||||
runCatching { join(500) }
|
||||
// join() is bounded, so the thread's own finally may not have run yet. We asked it to
|
||||
// stop and the process is still here, which is all the breaker needs to know.
|
||||
LibraryBackground.disarmSaver()
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
// Reaching the end of this function at all -- however the saver did -- means the
|
||||
// process survived it, which is the only thing the breaker is asking about. A native
|
||||
// crash or a kill never gets here, and that is what leaves the flag set.
|
||||
try {
|
||||
render()
|
||||
} finally {
|
||||
LibraryBackground.disarmSaver()
|
||||
}
|
||||
}
|
||||
|
||||
private fun render() {
|
||||
if (!initEgl()) { onStatus(false); teardown(); return }
|
||||
|
||||
val s = spec.newSaver()
|
||||
|
||||
@@ -260,6 +260,16 @@ fun PadTab(state: MutableState<Settings>) {
|
||||
ControllerMappings.setRumbleEnabled(it)
|
||||
refreshToken.intValue++
|
||||
}
|
||||
// Rumble prefers a connected controller's motor; this gates only the phone fallback,
|
||||
// so playing on a pad need not mean the phone buzzes along with it.
|
||||
ToggleRow(
|
||||
str("pad.rumblePhone.label"),
|
||||
ControllerMappings.phoneRumbleEnabled(),
|
||||
description = str("pad.rumblePhone.description"),
|
||||
) {
|
||||
ControllerMappings.setPhoneRumbleEnabled(it)
|
||||
refreshToken.intValue++
|
||||
}
|
||||
// Vibration strength: one multiplier over BOTH controller rumble and on-screen touch
|
||||
// haptics (they share the motor path), so an over-eager motor can be tamed or a weak
|
||||
// one boosted. 100% = as authored; 0% = off.
|
||||
|
||||
@@ -101,6 +101,7 @@ internal val SETTINGS_SEARCH_INDEX: List<SettingsSearchEntry> = listOf(
|
||||
SettingsSearchEntry("pad.section.playerRumble", true, SettingsCategory.Controls),
|
||||
SettingsSearchEntry("pad.editing.label", true, SettingsCategory.Controls),
|
||||
SettingsSearchEntry("pad.rumble.label", true, SettingsCategory.Controls),
|
||||
SettingsSearchEntry("pad.rumblePhone.label", true, SettingsCategory.Controls),
|
||||
SettingsSearchEntry("pad.hapticStrength.label", true, SettingsCategory.Controls),
|
||||
SettingsSearchEntry("pad.pressureAmount.label", true, SettingsCategory.Controls),
|
||||
SettingsSearchEntry("pad.section.analogSticks", true, SettingsCategory.Controls),
|
||||
|
||||
@@ -247,6 +247,13 @@ public final class NativeApp {
|
||||
/** Master rumble toggle. */
|
||||
public static volatile boolean sRumbleEnabled = true;
|
||||
|
||||
/**
|
||||
* Whether the PHONE's own motor may be used. A controller's motor is always allowed; this
|
||||
* only gates the fallback, so a user playing on a pad can stop the phone buzzing in their
|
||||
* pocket or dock without giving up rumble entirely (issue #89). Default on.
|
||||
*/
|
||||
public static volatile boolean sPhoneRumbleEnabled = true;
|
||||
|
||||
/** Volume applied to UI sounds played through NativeApp.playSound. */
|
||||
public static volatile float sSoundVolume = 1.0f;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user