mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40461caf4a |
No files matched your search
@@ -327,6 +327,7 @@ class MainActivity : ComponentActivity() {
|
||||
private var renderer by mutableStateOf("vulkan")
|
||||
private var gameStorage by mutableStateOf("")
|
||||
private var storageDiagnostics by mutableStateOf(false)
|
||||
private var skipPreallocation by mutableStateOf(false)
|
||||
private var storageOptions by mutableStateOf<List<Pair<String, String>>>(emptyList())
|
||||
private val pickGameStorage = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { r ->
|
||||
if (r.resultCode == RESULT_OK) InAppFilePicker.pickedPath(r.data)?.let { path -> setGameStorage(path, GameStorage.labelFor(this, path)) }
|
||||
@@ -1154,6 +1155,7 @@ class MainActivity : ComponentActivity() {
|
||||
gameStorage = if (mode == SessionService.MODE_STEAM) gameStorage else null,
|
||||
storageOptions = storageOptions,
|
||||
storageDiagnostics = mode == SessionService.MODE_STEAM && storageDiagnostics,
|
||||
skipPreallocation = mode == SessionService.MODE_STEAM && skipPreallocation,
|
||||
fexPreset = if (mode == SessionService.MODE_STEAM) fexPreset else null,
|
||||
forceSsbs = forceSsbs,
|
||||
syncBackend = if (mode == SessionService.MODE_STEAM) SessionPrefs.syncBackendOf(fastSync, fsyncFirst, syncFallback) else null,
|
||||
@@ -1227,6 +1229,10 @@ class MainActivity : ComponentActivity() {
|
||||
SessionPrefs.setStorageDiagnosticsEnabled(this, on)
|
||||
storageDiagnostics = on
|
||||
},
|
||||
onSkipPreallocation = { on ->
|
||||
SessionPrefs.setSkipLibraryPreallocation(this, on)
|
||||
skipPreallocation = on
|
||||
},
|
||||
onFexPreset = { id -> SessionPrefs.setFexPreset(this, id); fexPreset = id },
|
||||
onForceSsbs = { on -> SessionPrefs.setForceSsbs(this, on); forceSsbs = on },
|
||||
onSyncBackend = { id ->
|
||||
@@ -1378,6 +1384,7 @@ class MainActivity : ComponentActivity() {
|
||||
renderer = SessionPrefs.desktopRenderer(this)
|
||||
gameStorage = SessionPrefs.gameStorage(this)
|
||||
storageDiagnostics = SessionPrefs.storageDiagnosticsEnabled(this)
|
||||
skipPreallocation = SessionPrefs.skipLibraryPreallocation(this)
|
||||
settingsMode = mode
|
||||
// The page opens at once, on what was last read; the slow part (driver files, a walk of the
|
||||
// added-games folders, the storage volumes) lands while it animates in.
|
||||
|
||||
@@ -177,6 +177,7 @@ object DeviceReport {
|
||||
SessionPrefs.GAME_STORAGE_OFF -> "internal only"
|
||||
else -> g
|
||||
})
|
||||
k("Skip space reservation", SessionPrefs.skipLibraryPreallocation(context))
|
||||
|
||||
h("Android process limits")
|
||||
k("Phantom proc monitor", PhantomProcessLimit.reportValue(PhantomProcessLimit.read(context)))
|
||||
|
||||
@@ -467,6 +467,14 @@ object SessionPrefs {
|
||||
prefs(context).edit().putBoolean("storageDiagnostics", on).apply()
|
||||
}
|
||||
|
||||
/** Experimental: Steam's space reservation on the second library reports success untouched. */
|
||||
fun skipLibraryPreallocation(context: Context): Boolean =
|
||||
prefs(context).getBoolean("skipLibraryPreallocation", false)
|
||||
|
||||
fun setSkipLibraryPreallocation(context: Context, on: Boolean) {
|
||||
prefs(context).edit().putBoolean("skipLibraryPreallocation", on).apply()
|
||||
}
|
||||
|
||||
// ── Per-mode display ────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
@@ -826,6 +826,20 @@ class SessionService : Service() {
|
||||
} else {
|
||||
Log.i(TAG, "game storage: internal only")
|
||||
}
|
||||
// guest already has the session command at this point. These go with the `env -i`
|
||||
// assignments so they reach the Steam process instead of becoming script args.
|
||||
val storageEnv = ArrayList<String>()
|
||||
val skipLibrary = storageDiagnosticLibrary
|
||||
if (SessionState.mode == MODE_STEAM && skipLibrary != null && SessionPrefs.skipLibraryPreallocation(this)) {
|
||||
val device = runCatching { android.system.Os.stat(skipLibrary.absolutePath).st_dev }.getOrNull()
|
||||
if (device != null) {
|
||||
storageEnv.add("BL_STORAGE_DEVICE=$device")
|
||||
storageEnv.add("BL_STORAGE_PREALLOCATE_SKIP=1")
|
||||
Log.i(TAG, "game storage: Steam's space reservation on the second library is skipped")
|
||||
} else {
|
||||
Log.w(TAG, "game storage: library could not be identified; space reservation not skipped")
|
||||
}
|
||||
}
|
||||
if (SessionState.mode == MODE_STEAM && SessionPrefs.storageDiagnosticsEnabled(this)) {
|
||||
val target = storageDiagnosticLibrary ?: File(LinuxRuntime.rootDir(this), "root/.local/share/Steam")
|
||||
val device = StorageDiagnostics.writeSnapshot(
|
||||
@@ -836,16 +850,18 @@ class SessionService : Service() {
|
||||
} ?: false,
|
||||
)
|
||||
if (device != null) {
|
||||
// guest already has the session command at this point. Put these with the `env -i`
|
||||
// assignments so they reach the Steam process instead of becoming script args.
|
||||
val envAt = guest.indexOf(LinuxRuntime.SESSION_SCRIPT).takeIf { it >= 0 } ?: guest.size
|
||||
guest.add(envAt, "BL_STORAGE_DIAGNOSTICS=1")
|
||||
guest.add(envAt + 1, "BL_STORAGE_DEVICE=$device")
|
||||
guest.add(envAt + 2, "BL_STORAGE_LOG=${File(sessionDir, "storage.log").absolutePath}")
|
||||
// Both observe the same library, so a device the skip already set is the same one.
|
||||
if (storageEnv.none { it.startsWith("BL_STORAGE_DEVICE=") }) storageEnv.add("BL_STORAGE_DEVICE=$device")
|
||||
storageEnv.add("BL_STORAGE_DIAGNOSTICS=1")
|
||||
storageEnv.add("BL_STORAGE_LOG=${File(sessionDir, "storage.log").absolutePath}")
|
||||
} else {
|
||||
Log.w(TAG, "storage diagnostics: selected library could not be identified; metadata snapshot only")
|
||||
}
|
||||
}
|
||||
if (storageEnv.isNotEmpty()) {
|
||||
val envAt = guest.indexOf(LinuxRuntime.SESSION_SCRIPT).takeIf { it >= 0 } ?: guest.size
|
||||
guest.addAll(envAt, storageEnv)
|
||||
}
|
||||
// The user's own games folder (the Steam cog's "Added games"), bound at a fixed place so
|
||||
// the shortcuts the app writes point somewhere whatever storage the folder is on.
|
||||
for (root in com.droiddeck.launcher.frontend.AddedGames.roots(this)) {
|
||||
|
||||
@@ -69,6 +69,8 @@ class ModeSettings(
|
||||
val storageOptions: List<Pair<String, String>> = emptyList(),
|
||||
/** Steam only: record allocation and sampled storage timings in the next session's Share logs. */
|
||||
val storageDiagnostics: Boolean = false,
|
||||
/** Steam only: report Steam's space reservation on the second library as done (experimental). */
|
||||
val skipPreallocation: Boolean = false,
|
||||
val fexPreset: String? = null,
|
||||
/** Steam only: Force SSBS for Proton games (SessionPrefs.forceSsbs). */
|
||||
val forceSsbs: Boolean = false,
|
||||
@@ -130,6 +132,7 @@ class ModeSettingsActions(
|
||||
val onGameStorage: (path: String, label: String) -> Unit = { _, _ -> },
|
||||
val onPickGameStorageFolder: () -> Unit = {},
|
||||
val onStorageDiagnostics: (Boolean) -> Unit = {},
|
||||
val onSkipPreallocation: (Boolean) -> Unit = {},
|
||||
val onFexPreset: (String) -> Unit = {},
|
||||
val onForceSsbs: (Boolean) -> Unit = {},
|
||||
val onSyncBackend: (String) -> Unit = {},
|
||||
@@ -505,6 +508,11 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
|
||||
stringResource(R.string.mode_storage_diagnostics_hint), s.storageDiagnostics,
|
||||
onChange = a.onStorageDiagnostics,
|
||||
)
|
||||
ToggleRow(
|
||||
host, "skipPreallocation", stringResource(R.string.mode_skip_preallocation),
|
||||
stringResource(R.string.mode_skip_preallocation_hint), s.skipPreallocation,
|
||||
onChange = a.onSkipPreallocation,
|
||||
)
|
||||
}
|
||||
if (!steam && tab == ModeSettingsTab.DISPLAY && s.renderer != null) SettingsGroup(stringResource(R.string.mode_renderer)) {
|
||||
ChoiceRow(
|
||||
|
||||
@@ -358,6 +358,8 @@
|
||||
<string name="mode_second_library">Second library</string>
|
||||
<string name="mode_storage_note">Games that stream assets from SD or shared storage may stutter.</string>
|
||||
<string name="mode_storage_diagnostics">Storage diagnostics</string>
|
||||
<string name="mode_skip_preallocation">Skip space reservation (experimental)</string>
|
||||
<string name="mode_skip_preallocation_hint">Steam starts downloading to the game storage folder straight away instead of reserving every file\'s space first, which can take many minutes on an SD card. If the card fills up, the download stops with a disk write error. Applies next session.</string>
|
||||
<string name="mode_storage_diagnostics_hint">Record allocation results and sampled storage timings in Share logs. May slow installs while enabled. Applies next session.</string>
|
||||
<string name="mode_choose_folder">Choose a folder…</string>
|
||||
<string name="mode_renderer">Renderer</string>
|
||||
|
||||
@@ -7,6 +7,12 @@
|
||||
* each process, allocation details are capped, writes are sampled, and storage.log has a global
|
||||
* size limit because every Steam child shares it.
|
||||
*
|
||||
* One deliberate exception: when the user opts to skip space reservation, plain reservation
|
||||
* requests on the selected library report success without touching the file. On a portable SD
|
||||
* card Steam's reservation goes through Android's FUSE layer and can take tens of minutes; with it
|
||||
* skipped Steam goes straight to downloading, and a full card shows up as a write error during the
|
||||
* download rather than up front.
|
||||
*
|
||||
* LD_PRELOAD cannot see direct syscalls or libc-internal calls that bypass public symbols. A
|
||||
* missing entry therefore does not prove the operation did not happen.
|
||||
*/
|
||||
@@ -45,6 +51,8 @@ static volatile unsigned int alloc_details;
|
||||
static long long selected_device;
|
||||
static int init_state;
|
||||
static int enabled;
|
||||
static int device_valid;
|
||||
static int skip_preallocation;
|
||||
|
||||
static uint64_t add(volatile uint64_t *slot, uint64_t value) {
|
||||
return __atomic_add_fetch(slot, value, __ATOMIC_RELAXED);
|
||||
@@ -71,14 +79,17 @@ static void initialize(void) {
|
||||
return;
|
||||
}
|
||||
const char *on = getenv("BL_STORAGE_DIAGNOSTICS");
|
||||
const char *skip = getenv("BL_STORAGE_PREALLOCATE_SKIP");
|
||||
const char *device = getenv("BL_STORAGE_DEVICE");
|
||||
if (on != NULL && strcmp(on, "1") == 0 && device != NULL && *device != '\0') {
|
||||
if (device != NULL && *device != '\0') {
|
||||
char *end = NULL;
|
||||
errno = 0;
|
||||
long long parsed = strtoll(device, &end, 10);
|
||||
if (errno == 0 && end != device && *end == '\0') {
|
||||
selected_device = parsed;
|
||||
enabled = 1;
|
||||
device_valid = 1;
|
||||
enabled = on != NULL && strcmp(on, "1") == 0;
|
||||
skip_preallocation = skip != NULL && strcmp(skip, "1") == 0;
|
||||
}
|
||||
}
|
||||
__atomic_store_n(&init_state, 2, __ATOMIC_RELEASE);
|
||||
@@ -90,10 +101,11 @@ static uint64_t now_ns(void) {
|
||||
return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec;
|
||||
}
|
||||
|
||||
static int on_selected_device(int fd, const struct stat *known) {
|
||||
/* Diagnostics watch every wrapped call; skipping only concerns the allocation wrappers. */
|
||||
static int on_selected_device(int fd, const struct stat *known, int allocation) {
|
||||
struct stat local;
|
||||
initialize();
|
||||
if (!enabled) return 0;
|
||||
if (!device_valid || !(enabled || (allocation && skip_preallocation))) return 0;
|
||||
if (known == NULL) {
|
||||
if (next_fstat == NULL) next_fstat = dlsym(RTLD_NEXT, "fstat");
|
||||
if (next_fstat == NULL || next_fstat(fd, &local) != 0) return 0;
|
||||
@@ -158,15 +170,21 @@ int fallocate(int fd, int mode, off_t offset, off_t length) {
|
||||
int entry_errno = errno;
|
||||
if (next_fallocate == NULL) next_fallocate = dlsym(RTLD_NEXT, "fallocate");
|
||||
if (next_fallocate == NULL) { errno = ENOSYS; return -1; }
|
||||
int selected = on_selected_device(fd, NULL);
|
||||
int selected = on_selected_device(fd, NULL, 1);
|
||||
errno = entry_errno;
|
||||
if (!selected) return next_fallocate(fd, mode, offset, length);
|
||||
uint64_t start = now_ns();
|
||||
/* Only a plain reservation is skipped; hole punching and the other modes change contents. */
|
||||
if (skip_preallocation && (mode == 0 || mode == FALLOC_FL_KEEP_SIZE)) {
|
||||
if (enabled) allocation_line("fallocate_skipped", 0, entry_errno, mode, offset, length, now_ns() - start);
|
||||
errno = entry_errno;
|
||||
return 0;
|
||||
}
|
||||
errno = entry_errno;
|
||||
int result = next_fallocate(fd, mode, offset, length);
|
||||
int call_errno = errno;
|
||||
uint64_t elapsed = now_ns() - start;
|
||||
allocation_line("fallocate", result, call_errno, mode, offset, length, elapsed);
|
||||
if (enabled) allocation_line("fallocate", result, call_errno, mode, offset, length, elapsed);
|
||||
errno = call_errno;
|
||||
return result;
|
||||
}
|
||||
@@ -175,7 +193,7 @@ int posix_fallocate(int fd, off_t offset, off_t length) {
|
||||
int entry_errno = errno;
|
||||
if (next_posix_fallocate == NULL) next_posix_fallocate = dlsym(RTLD_NEXT, "posix_fallocate");
|
||||
if (next_posix_fallocate == NULL) { errno = entry_errno; return ENOSYS; }
|
||||
int selected = on_selected_device(fd, NULL);
|
||||
int selected = on_selected_device(fd, NULL, 1);
|
||||
errno = entry_errno;
|
||||
if (!selected) {
|
||||
int result = next_posix_fallocate(fd, offset, length);
|
||||
@@ -183,11 +201,16 @@ int posix_fallocate(int fd, off_t offset, off_t length) {
|
||||
return result;
|
||||
}
|
||||
uint64_t start = now_ns();
|
||||
if (skip_preallocation) {
|
||||
if (enabled) allocation_line("posix_fallocate_skipped", 0, entry_errno, 0, offset, length, now_ns() - start);
|
||||
errno = entry_errno;
|
||||
return 0;
|
||||
}
|
||||
errno = entry_errno;
|
||||
int result = next_posix_fallocate(fd, offset, length);
|
||||
int call_errno = errno;
|
||||
uint64_t elapsed = now_ns() - start;
|
||||
allocation_line("posix_fallocate", result, call_errno, 0, offset, length, elapsed);
|
||||
if (enabled) allocation_line("posix_fallocate", result, call_errno, 0, offset, length, elapsed);
|
||||
errno = entry_errno; /* POSIX requires the error to be returned, leaving errno unchanged. */
|
||||
return result;
|
||||
}
|
||||
@@ -199,7 +222,7 @@ ssize_t pwrite(int fd, const void *buffer, size_t count, off_t offset) {
|
||||
initialize();
|
||||
uint64_t sequence = enabled ? add(&pwrite_seen, 1) : 1;
|
||||
int sample = enabled && (sequence % 32ULL) == 0;
|
||||
int selected = sample && on_selected_device(fd, NULL);
|
||||
int selected = sample && on_selected_device(fd, NULL, 0);
|
||||
errno = entry_errno;
|
||||
if (!selected)
|
||||
return next_pwrite(fd, buffer, count, offset);
|
||||
@@ -221,7 +244,7 @@ int ftruncate(int fd, off_t length) {
|
||||
int entry_errno = errno;
|
||||
if (next_ftruncate == NULL) next_ftruncate = dlsym(RTLD_NEXT, "ftruncate");
|
||||
if (next_ftruncate == NULL) { errno = ENOSYS; return -1; }
|
||||
int selected = on_selected_device(fd, NULL);
|
||||
int selected = on_selected_device(fd, NULL, 0);
|
||||
errno = entry_errno;
|
||||
if (!selected) return next_ftruncate(fd, length);
|
||||
uint64_t start = now_ns();
|
||||
@@ -249,7 +272,7 @@ int fstat(int fd, struct stat *out) {
|
||||
int result = next_fstat(fd, out);
|
||||
int call_errno = errno;
|
||||
uint64_t elapsed = now_ns() - start;
|
||||
if (result == 0 && on_selected_device(fd, out)) {
|
||||
if (result == 0 && on_selected_device(fd, out, 0)) {
|
||||
add(&stats.fstat_calls, 1);
|
||||
add(&stats.fstat_elapsed_ns, elapsed);
|
||||
update_max(&stats.fstat_max_ns, elapsed);
|
||||
@@ -262,7 +285,7 @@ int fsync(int fd) {
|
||||
int entry_errno = errno;
|
||||
if (next_fsync == NULL) next_fsync = dlsym(RTLD_NEXT, "fsync");
|
||||
if (next_fsync == NULL) { errno = ENOSYS; return -1; }
|
||||
int selected = on_selected_device(fd, NULL);
|
||||
int selected = on_selected_device(fd, NULL, 0);
|
||||
errno = entry_errno;
|
||||
if (!selected) return next_fsync(fd);
|
||||
uint64_t start = now_ns();
|
||||
|
||||
Reference in new issue
Block a user