From eadebc46d57b6fed3bbf4a2a39edfbbe61877fa4 Mon Sep 17 00:00:00 2001 From: MaxsTechReview Date: Sat, 10 Oct 2026 19:00:33 -0400 Subject: [PATCH] Performance: idle front end, proot stops, launch-time passes and log collection (#529) * Front end: draw the capsule wall without clipping it every frame The tilted wall clipped each capsule to a rounded rect and recomposed its columns on every drift step, and a rotated rounded clip is rasterised in software on the render threads: the idle main page held about 1.7 cores. The art is now loaded at the capsule's size with its corners already rounded, each column draws its capsules itself in one layer moved by the drift, and the drift advances at most every 15 ms. The idle page takes about a third of a core. * libfakeinput: let calls on descriptors it does not serve skip its lock Every read, write, ioctl, close and poll of every process in a Proton session went through the controller lock and the three map lookups, Wine's wineserver pipes and the game's files alike. A bitmap of the descriptors the maps hold, set before one is handed out and cleared when it is closed, lets everything else go straight to the kernel. * Pad motion: ask the display for its rotation four times a second The rotation was looked up for every sensor reading, every 2 ms, and cost more than the rest of the reading; it is now refreshed every 250 ms, and the readings no longer allocate pairs. * Session: keep the library passes out of a game's launch The redist seeder stamped each prefix with the checksum of the whole set of names, which differs between the session's pass and the game launch's (the game's own names are added), so every launch re-appended the markers to every prefix in the library while the game loaded. Each prefix's marker now lists the names it was given and only the missing ones are added. The beat's seed and compatibility passes, and the Lossless Scaling ownership probe (which loads steamclient.so), also wait while a wineserver runs: a launch rewrites the manifest and made them due at exactly that moment. The game's own launch seeds its prefix regardless. * Agent: keep the focus watcher's X connection open while nothing changes The connect timeout stayed on the socket, so with no property change the read timed out, and the watcher dropped the connection, reconnected and took a fresh snapshot every few seconds. * Game environment: start Cyberpunk 2077 without REDlauncher The prelauncher opens REDlauncher, a Qt WebEngine window that hands over to the game half a minute later and then stays up beside it, well over a core busy for as long as the game runs; its own --launcher-skip started the game twice. The game's executable is started directly instead. * netif: answer Wine's network driver from a reading of the link Mortal Kombat X has Wine's network driver enumerate every interface, address and route table hundreds of times a second. Each enumeration read /etc/droiddeck-net several times, asked the kernel for its interface dump again (a netlink socket bound under proot, refused each time) and looked the real functions up with dlsym. The link file is now read at most once a second, a refused dump is not asked for again within that second, and the real functions are looked up once. * Fast path: create memfds without a stop in proot proot looks at a memfd's name only to refuse the few that are then executed or locked; every other memfd_create is now issued from the trampoline. The session's /proc/net and status copies made one per read, a stop each. Without memfd in the kernel the call still goes to proot's ashmem. * Logs: run each redaction rule only on a line that could match it A session's end copies some 70 MB of the Steam client's logs and puts every line through all twenty-odd rules, most of a minute of a core on the phone after every session. Each rule now runs only on a line holding what it cannot match without: its keyword, the five separators of a MAC, the four dashes of a GUID, a long enough hex or token run, the learned account or address text. Lines with non-ASCII text still meet every case-insensitive rule. Over 895,000 lines of real client logs, with the device's accounts and addresses learned, the output is identical and the pass takes a twentieth of the time. --- app/src/main/cpp/fakeinput_steam.cpp | 49 +++++- .../droiddeck/launcher/core/LogRedactor.kt | 113 ++++++++++---- .../com/droiddeck/launcher/input/PadMotion.kt | 29 +++- .../com/droiddeck/launcher/ui/SteamHome.kt | 145 ++++++++++++------ .../com/droiddeck/launcher/ui/StoresPage.kt | 9 +- .../overlay/usr/local/bin/droiddeck-agent | 3 + .../overlay/usr/local/bin/droiddeck-game-env | 14 ++ .../usr/local/bin/droiddeck-seed-redists | 35 +++-- .../overlay/usr/local/bin/droiddeck-session | 13 +- tools/linuxfs/preload/netif.c | 118 +++++++++++--- tools/proot/fastpath/fastpath.c | 13 ++ 11 files changed, 416 insertions(+), 125 deletions(-) diff --git a/app/src/main/cpp/fakeinput_steam.cpp b/app/src/main/cpp/fakeinput_steam.cpp index 913d11914..64b6ddec4 100644 --- a/app/src/main/cpp/fakeinput_steam.cpp +++ b/app/src/main/cpp/fakeinput_steam.cpp @@ -266,6 +266,31 @@ static std::unordered_map &ff_effects() { return *map; } +// Every fd the maps hold, readable without controller_mutex(): the hooks see each read, write, +// ioctl and poll a process makes, and almost none are on these. Set before an fd is handed out, +// cleared once no map holds it; an fd past the bitmap takes the lock once any such fd exists. +static constexpr int kWatchedFds = 1 << 16; +static constexpr int kWatchedBits = sizeof(unsigned long) * 8; +static std::atomic watched_fds[kWatchedFds / kWatchedBits]; +static std::atomic watched_beyond{false}; + +static void watch_fd(int fd) { + if (fd < 0) return; + if (fd >= kWatchedFds) watched_beyond.store(true, std::memory_order_release); + else watched_fds[fd / kWatchedBits].fetch_or(1UL << (fd % kWatchedBits), std::memory_order_release); +} + +static void unwatch_fd(int fd) { + if (fd >= 0 && fd < kWatchedFds) + watched_fds[fd / kWatchedBits].fetch_and(~(1UL << (fd % kWatchedBits)), std::memory_order_release); +} + +static bool watched(int fd) { + if (fd < 0) return false; + if (fd >= kWatchedFds) return watched_beyond.load(std::memory_order_acquire); + return (watched_fds[fd / kWatchedBits].load(std::memory_order_acquire) >> (fd % kWatchedBits)) & 1; +} + static const char *fake_hook_dir() { return config().hook_dir; } static const char *fake_udev_data_dir() { return config().udev_data_dir; } static bool fake_vibration_enabled() { return config().vibration_enabled; } @@ -692,6 +717,7 @@ open_fake_input_ring(const char *event, int flags) { { std::lock_guard guard(controller_mutex()); controller_map()[fd] = controller; + watch_fd(fd); } Logger::log("Adding ring-backed controller, fd %d event %s slot %d\n", fd, @@ -774,11 +800,13 @@ copy_slot_ioctl_string(int op, void *argp, const char *format, int event_number) } __attribute__((visibility("hidden"))) static bool is_fake_input_fd(int fd) { + if (!watched(fd)) return false; std::lock_guard guard(controller_mutex()); return controller_map().find(fd) != controller_map().end(); } __attribute__((visibility("hidden"))) static bool fake_fd_is_stale(int fd) { + if (!watched(fd)) return false; std::lock_guard guard(controller_mutex()); auto controller = controller_map().find(fd); return controller != controller_map().end() && @@ -892,6 +920,7 @@ __attribute__((visibility("hidden"))) static int open_uinput(int flags) { writer->owner = getpid(); std::lock_guard guard(controller_mutex()); uinput_map()[fd] = writer; + watch_fd(fd); Logger::log("uinput: opened as fd %d\n", fd); return fd; } @@ -1556,6 +1585,7 @@ __attribute__((visibility("hidden"))) static int open_deck_hidraw(int flags) { { std::lock_guard guard(controller_mutex()); deck_map()[pair[0]] = deck; + watch_fd(pair[0]); } pthread_t thread; auto *arg = new std::shared_ptr(deck); @@ -1563,6 +1593,7 @@ __attribute__((visibility("hidden"))) static int open_deck_hidraw(int flags) { delete arg; std::lock_guard guard(controller_mutex()); deck_map().erase(pair[0]); + unwatch_fd(pair[0]); syscall(SYS_close, pair[0]); syscall(SYS_close, pair[1]); munmap(mapping, mapping_size); @@ -1898,6 +1929,7 @@ __attribute__((visibility("hidden"))) static int open_touch_hidraw(int flags) { { std::lock_guard guard(controller_mutex()); deck_map()[pair[0]] = touch; + watch_fd(pair[0]); } touch_state([](TouchRingFile &r) { r.opened++; }); pthread_t thread; @@ -1906,6 +1938,7 @@ __attribute__((visibility("hidden"))) static int open_touch_hidraw(int flags) { delete arg; std::lock_guard guard(controller_mutex()); deck_map().erase(pair[0]); + unwatch_fd(pair[0]); syscall(SYS_close, pair[0]); syscall(SYS_close, pair[1]); touch_state([](TouchRingFile &r) { if (r.opened) r.opened--; }); @@ -2689,6 +2722,7 @@ static int fake_stat_path(const char *pathname, S *statbuf, Real real) { template static int fake_stat_fd(int fd, S *buf, Real real) { int ret = real(fd, buf); + if (ret != 0 || !watched(fd)) return ret; std::lock_guard guard(controller_mutex()); if (ret == 0 && deck_map().count(fd)) { @@ -2898,6 +2932,7 @@ EXPORT int ioctl(int fd, ioctl_request_t op, ...) { // The lock must not be held across the passthrough: binder's transport is a // blocking ioctl(BINDER_WRITE_READ), so a parked binder pool thread would own // controller_mutex() for as long as it waits and deadlock every other ioctl. + if (!watched(fd)) return syscall(SYS_ioctl, fd, op, argp); std::unique_lock guard(controller_mutex()); auto maker = uinput_map().find(fd); if (maker != uinput_map().end()) return ioctl_uinput(*maker->second, op, argp); @@ -3117,6 +3152,7 @@ EXPORT int ioctl(int fd, ioctl_request_t op, ...) { EXPORT int close(int fd) { static auto my_close = reinterpret_cast(dlsym(RTLD_NEXT, "close")); + if (!watched(fd)) return my_close(fd); close_uinput(fd); std::unique_lock guard(controller_mutex()); deck_map().erase(fd); @@ -3127,6 +3163,7 @@ EXPORT int close(int fd) { controller->second->closed = true; controller_map().erase(fd); } + unwatch_fd(fd); guard.unlock(); return my_close(fd); @@ -3135,6 +3172,7 @@ EXPORT int close(int fd) { static constexpr int kMaxRingWaitMs = 2; EXPORT ssize_t read(int fd, void *buf, size_t count) { + if (!watched(fd)) return syscall(SYS_read, fd, buf, count); std::unique_lock guard(controller_mutex()); auto controller = controller_map().find(fd); if (controller == controller_map().end()) { @@ -3233,6 +3271,7 @@ EXPORT ssize_t read(int fd, void *buf, size_t count) { EXPORT ssize_t write(int fd, const void *buf, size_t count) { static auto my_write = reinterpret_cast(dlsym(RTLD_NEXT, "write")); + if (!watched(fd)) return my_write(fd, buf, count); std::unique_lock guard(controller_mutex()); auto made = uinput_map().find(fd); if (made != uinput_map().end()) return write_uinput(*made->second, buf, count); @@ -3262,6 +3301,7 @@ EXPORT ssize_t write(int fd, const void *buf, size_t count) { } EXPORT ssize_t writev(int fd, const struct iovec *iov, int iovcnt) { + if (!watched(fd)) return syscall(SYS_writev, fd, iov, iovcnt); std::unique_lock guard(controller_mutex()); if (deck_map().count(fd)) { ssize_t total = 0; @@ -3312,11 +3352,14 @@ static int poll_fake(struct pollfd *fds, nfds_t nfds, int timeout, static auto my_poll = reinterpret_cast(dlsym(RTLD_NEXT, "poll")); static auto my_ppoll = reinterpret_cast(dlsym(RTLD_NEXT, "ppoll")); + nfds_t first = 0; + while (first < nfds && !watched(fds[first].fd)) first++; bool has_fake_fds = false; std::vector real_fds; - real_fds.reserve(nfds); - std::vector> fake_fds(nfds); - { + std::vector> fake_fds; + if (first < nfds) { + real_fds.reserve(nfds); + fake_fds.resize(nfds); std::lock_guard guard(controller_mutex()); for (nfds_t i = 0; i < nfds; i++) { real_fds.push_back(fds[i]); diff --git a/app/src/main/java/com/droiddeck/launcher/core/LogRedactor.kt b/app/src/main/java/com/droiddeck/launcher/core/LogRedactor.kt index ad48f2664..4406a7028 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/LogRedactor.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/LogRedactor.kt @@ -93,9 +93,12 @@ object LogRedactor { "(?i)(? = emptyList() + private var accounts: List = emptyList() /** * Learns every Steam account on the device from the client's loginusers.vdf - its AccountName @@ -103,12 +106,13 @@ object LogRedactor { * Names under three characters are skipped: blanking every "a" would ruin a log. */ fun learnAccounts(loginUsers: java.io.File) { - val found = ArrayList() + val found = ArrayList() try { if (loginUsers.isFile) { val kv = Regex("\"(AccountName|PersonaName)\"\\s+\"([^\"]*)\"") kv.findAll(loginUsers.readText()).map { it.groupValues[2].trim() }.filter { it.length >= 3 }.distinct().forEach { name -> - found += Regex("(?i)(? = emptyList() + private var own: List = emptyList() /** * Learns the device's addresses from the link file the app writes for the session @@ -133,7 +137,7 @@ object LogRedactor { * private, link-local and loopback addresses identify nobody and are left out. */ fun learnOwnAddresses(linkFile: java.io.File) { - val found = ArrayList() + val found = ArrayList() try { if (linkFile.isFile) linkFile.forEachLine { line -> val addr = line.trim().takeIf { it.startsWith("addr ") }?.split(Regex("\\s+"))?.getOrNull(1) ?: return@forEachLine @@ -142,9 +146,12 @@ object LogRedactor { val groups = expand6(addr)?.take(4) ?: return@forEachLine // Any spelling of an address in this /64: leading zeros dropped, :: anywhere after. val prefix = groups.joinToString(":") { g -> "0{0,3}" + Regex.escape(g.trimStart('0').ifEmpty { "0" }) } - found += Regex("(?i)(? found += Regex("(? found += Learned(Regex("(?$q" } + /** + * A line on its way through [redact]. Each rule runs only on a line holding what it cannot match + * without - a word, enough separators, a long enough run: most of a Steam log is lines no rule + * touches, and a session's logs went through every rule at about a megabyte a second. A line with anything but ASCII in it goes through + * every case-insensitive rule, since Android's regex folds case more widely than lowercase(). + */ + private class Line(var text: String) { + private val unfolded = text.any { it.code >= 0x80 } + private var lower: String? = null + + fun has(vararg words: String): Boolean { + if (unfolded) return true + val folded = lower ?: text.lowercase().also { lower = it } + return words.any { folded.contains(it) } + } + + fun set(next: String) { + if (next != text) { + text = next + lower = null + } + } + + fun holds(c: Char, times: Int): Boolean { + var n = 0 + for (ch in text) if (ch == c && ++n >= times) return true + return false + } + + inline fun hasRun(length: Int, inRun: (Char) -> Boolean): Boolean { + var n = 0 + for (ch in text) if (!inRun(ch)) n = 0 else if (++n >= length) return true + return false + } + } + + private fun isHex(c: Char) = c in '0'..'9' || c in 'a'..'f' || c in 'A'..'F' + /** [line] with every credential shape replaced. Null- and exception-safe by construction. */ fun redact(line: String): String { if (line.isEmpty()) return line return try { - var out = line - out = MAC_FIELD.replace(out) { "${it.groupValues[1]}" } - out = MAC.replace(out, "") - out = DEVICE_IDENTIFIER.replace(out) { + val out = Line(line) + if (out.has("mac")) out.set(MAC_FIELD.replace(out.text) { "${it.groupValues[1]}" }) + if (out.holds(':', 5) || out.holds('-', 5)) out.set(MAC.replace(out.text, "")) + if (out.has("serial", "android")) out.set(DEVICE_IDENTIFIER.replace(out.text) { val value = it.groupValues[2] val quote = value.first().takeIf { char -> char == '\"' || char == '\'' }?.toString().orEmpty() "${it.groupValues[1]}${quote}${quote}" + }) + if (out.holds('-', 4)) out.set(GUID.replace(out.text, "")) + if (out.has("jwt")) out.set(JWT_LABELLED.replace(out.text) { "${it.groupValues[1]}" }) + if ("ey" in out.text) out.set(JWT_BASE64.replace(out.text, "")) + if (out.has("token", "auth", "ticket", "sessionid", "steamloginsecure", "api", "machine", "pass", "pwd", "secret")) { + out.set(SECRET_KV.replace(out.text) { "${it.groupValues[1]}${it.groupValues[2]}" }) } - out = GUID.replace(out, "") - out = JWT_LABELLED.replace(out) { "${it.groupValues[1]}" } - out = JWT_BASE64.replace(out, "") - out = SECRET_KV.replace(out) { "${it.groupValues[1]}${it.groupValues[2]}" } - out = GUARD_CODE.replace(out) { "${it.groupValues[1]}" } - out = WEBAPI_KEY.replace(out, "") + if (out.has("guard", "factor", "2fa")) out.set(GUARD_CODE.replace(out.text) { "${it.groupValues[1]}" }) + if (out.hasRun(32, ::isHex)) out.set(WEBAPI_KEY.replace(out.text, "")) // Mask, not delete: the last four digits let a reader correlate lines to one account. - out = STEAMID64.replace(out) { "${it.groupValues[1]}********${it.groupValues[3]}" } - out = STEAMID3.replace(out) { m -> + if ("76561" in out.text) out.set(STEAMID64.replace(out.text) { "${it.groupValues[1]}********${it.groupValues[3]}" }) + if ("[U:1:" in out.text) out.set(STEAMID3.replace(out.text) { m -> val id = m.groupValues[1] "[U:1:${if (id.length > 4) "*".repeat(id.length - 4) + id.takeLast(4) else id}]" + }) + if (out.has("external")) out.set(EXTERNAL_ADDR.replace(out.text) { "${it.groupValues[1]}" }) + for (r in own) if (r.needs == null || out.has(r.needs)) out.set(r.regex.replace(out.text, "")) + if (out.has("onloginstatechange")) out.set(LOGIN_STATE.replace(out.text) { "${it.groupValues[1]}" }) + if ("OnLoginUsersChanged " in out.text) out.set(LOGIN_USERS.replace(out.text) { "${it.groupValues[1]}" }) + if (out.has("account", "user")) { + out.set(ACCOUNT_KV.replace(out.text) { "${it.groupValues[1]}${it.groupValues[2]}${quoted(it.groupValues[3])}" }) } - out = EXTERNAL_ADDR.replace(out) { "${it.groupValues[1]}" } - for (r in own) out = r.replace(out, "") - out = LOGIN_STATE.replace(out) { "${it.groupValues[1]}" } - out = LOGIN_USERS.replace(out) { "${it.groupValues[1]}" } - out = ACCOUNT_KV.replace(out) { "${it.groupValues[1]}${it.groupValues[2]}${quoted(it.groupValues[3])}" } - out = LOGIN_KV.replace(out) { "${it.groupValues[1]}${it.groupValues[2]}${quoted(it.groupValues[3])}" } - for (r in accounts) out = r.replace(out, "") - out = EMAIL.replace(out, "") - out = RESIDUAL.replace(out) { "${it.groupValues[1]}=" } - out = LONG_TOKEN.replace(out, "") - out + if (out.has("login")) out.set(LOGIN_KV.replace(out.text) { "${it.groupValues[1]}${it.groupValues[2]}${quoted(it.groupValues[3])}" }) + for (r in accounts) if (r.needs == null || out.has(r.needs)) out.set(r.regex.replace(out.text, "")) + if ('@' in out.text) out.set(EMAIL.replace(out.text, "")) + if (out.has("jwt", "token", "ticket", "sessionid", "steamloginsecure", "machineauth")) { + out.set(RESIDUAL.replace(out.text) { "${it.groupValues[1]}=" }) + } + if (out.hasRun(88) { it in 'a'..'z' || it in 'A'..'Z' || it in '0'..'9' || it == '_' || it == '-' }) { + out.set(LONG_TOKEN.replace(out.text, "")) + } + out.text } catch (t: Throwable) { // A log line is never worth crashing a session over, but an unscrubbed one must not // reach the file either. diff --git a/app/src/main/java/com/droiddeck/launcher/input/PadMotion.kt b/app/src/main/java/com/droiddeck/launcher/input/PadMotion.kt index 0d26272b8..7058bf300 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/PadMotion.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/PadMotion.kt @@ -35,6 +35,9 @@ class PadMotion(private val context: Context, private val rotation: () -> Int) : private val gyro = ShortArray(3) private var reading = false private var idlePolls = 0 + // Asking the display costs more than the rest of a reading, which arrives every 2 ms. + private var turn = Surface.ROTATION_0 + private var turnAt = 0L fun start() { if (thread != null || sensors == null) return @@ -86,6 +89,7 @@ class PadMotion(private val context: Context, private val rotation: () -> Int) : idlePolls = if (wanted) 0 else idlePolls + 1 if (wanted && !reading) { reading = true + turnAt = 0L gyroscope?.let { sensors?.registerListener(this, it, SAMPLING_US, handler) } accelerometer?.let { sensors?.registerListener(this, it, SAMPLING_US, handler) } Log.i(TAG, "pad motion: reading") @@ -107,17 +111,25 @@ class PadMotion(private val context: Context, private val rotation: () -> Int) : override fun onSensorChanged(event: SensorEvent) { if (!active) return val values = event.values + if (event.timestamp - turnAt > ROTATION_REFRESH_NS) { + turn = rotation() + turnAt = event.timestamp + } // Natural orientation to the screen's: Android's display-rotation transform. - val (x, y) = when (rotation()) { - Surface.ROTATION_90 -> -values[1] to values[0] - Surface.ROTATION_180 -> -values[0] to -values[1] - Surface.ROTATION_270 -> values[1] to -values[0] - else -> values[0] to values[1] + val x: Float + val y: Float + when (turn) { + Surface.ROTATION_90 -> { x = -values[1]; y = values[0] } + Surface.ROTATION_180 -> { x = -values[0]; y = -values[1] } + Surface.ROTATION_270 -> { x = values[1]; y = -values[0] } + else -> { x = values[0]; y = values[1] } } val z = values[2] - val (target, scale) = when (event.sensor.type) { - Sensor.TYPE_GYROSCOPE -> gyro to GYRO_COUNTS_PER_RAD_S - Sensor.TYPE_ACCELEROMETER -> accel to ACCEL_COUNTS_PER_M_S2 + val target: ShortArray + val scale: Float + when (event.sensor.type) { + Sensor.TYPE_GYROSCOPE -> { target = gyro; scale = GYRO_COUNTS_PER_RAD_S } + Sensor.TYPE_ACCELEROMETER -> { target = accel; scale = ACCEL_COUNTS_PER_M_S2 } else -> return } // Screen frame (right, up, towards the player) to the Deck's (right, away, up). @@ -140,6 +152,7 @@ class PadMotion(private val context: Context, private val rotation: () -> Int) : private const val SAMPLING_US = 4000 /** How often [follow] checks whether the client wants motion. */ private const val POLL_MS = 500L + private const val ROTATION_REFRESH_NS = 250_000_000L private const val ACCEL_COUNTS_PER_M_S2 = 16384f / SensorManager.GRAVITY_EARTH private const val GYRO_COUNTS_PER_RAD_S = (32768f / 2000f) * (180f / Math.PI.toFloat()) } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/SteamHome.kt b/app/src/main/java/com/droiddeck/launcher/ui/SteamHome.kt index db261e51a..c5e1a1cb7 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/SteamHome.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/SteamHome.kt @@ -4,7 +4,6 @@ import androidx.compose.ui.platform.testTag import com.droiddeck.launcher.R import androidx.compose.ui.res.stringResource import androidx.compose.animation.core.FastOutSlowInEasing -import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.RepeatMode import androidx.compose.runtime.State import androidx.compose.animation.core.animateFloat @@ -12,7 +11,6 @@ import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.tween import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints @@ -41,21 +39,38 @@ import androidx.compose.ui.layout.boundsInParent import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage import com.droiddeck.launcher.frontend.Library +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.withFrameNanos +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.drawscope.inset +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import coil.compose.rememberAsyncImagePainter +import coil.request.ImageRequest +import coil.size.Scale +import coil.transform.RoundedCornersTransformation +import kotlin.math.roundToInt /** From this many games the Steam tab is a wall; fewer would only repeat the same covers. */ private const val WALL_MIN_GAMES = 4 +private const val DRIFT_STEP_NS = 15_000_000L + /** * The Steam tab: the library as a slowly drifting, tilted wall of capsules (a few games instead * lean on their own; none, the DroidDeck mark wandering a blank wall), and over it the wordmark and the one thing to do here - @@ -126,32 +141,49 @@ internal fun CapsuleWall( games: List, driftMs: Int = if (games.isEmpty()) 75_000 else 40_000, modifier: Modifier = Modifier, -) = TiltedWall(games.size, driftMs, modifier) { i, m -> - Capsule(if (games.isEmpty()) null else games[i], RoundedCornerShape(10.dp), m) +) { + val pal = LocalPalette.current + val blank = remember(pal) { WallCapsule(Brush.linearGradient(listOf(pal.surface, pal.background)), edge = pal.line) } + TiltedWall(games.size, driftMs, modifier) { i -> if (games.isEmpty()) blank else WallCapsule(artBrush(hueOf(games[i].name)), games[i].art) } } +/** One capsule of a [TiltedWall]: its ground, the art laid over it once loaded, and an outline. */ +internal class WallCapsule(val ground: Brush, val art: Any? = null, val edge: Color? = null) + /** - * [CapsuleWall]'s tilted, drifting columns for any [count] of capsules; [capsule] draws number i - * (0 until [count], or 0 for each blank one when [count] is 0) at the modifier it is given. + * [CapsuleWall]'s tilted, drifting columns for any [count] of capsules; [capsule] describes number i + * (0 until [count], or 0 for each blank one when [count] is 0). Each column is one drawing with the + * covers' corners cut into their bitmaps: a rounded clip under the tilt is rasterised on the CPU, + * for every capsule on every frame. */ @Composable internal fun TiltedWall( count: Int, driftMs: Int, modifier: Modifier = Modifier, - capsule: @Composable (index: Int, modifier: Modifier) -> Unit, + capsule: (index: Int) -> WallCapsule, ) { val capW = 112.dp val capH = 168.dp val gap = 14.dp - val transition = rememberInfiniteTransition(label = "wall") - val drift = transition.animateFloat( - 0f, 1f, - infiniteRepeatable(tween(driftMs, easing = LinearEasing)), - label = "drift", - ) // Animations off in the system settings: a still wall. val still = Motion.scale == 0f + // The wall moves a few pixels a step. Stepped on frames at most every 15 ms it looks the same as + // stepped on every frame, which on a 120 Hz screen drew it twice as often. + val drift = remember { mutableFloatStateOf(0f) } + if (!still) LaunchedEffect(driftMs) { + val period = driftMs * 1_000_000.0 * Motion.scale + var start = 0L + var shown = 0L + while (true) withFrameNanos { now -> + if (start == 0L) start = now + if (shown == 0L || now - shown >= DRIFT_STEP_NS) { + shown = now + drift.floatValue = ((now - start) % period / period).toFloat() + } + } + } + val density = LocalDensity.current BoxWithConstraints(modifier.fillMaxSize()) { // Centred on the page and big enough that, once tilted, it still covers every corner. val tilt = Math.toRadians(13.0) @@ -162,6 +194,17 @@ internal fun TiltedWall( val columns = ((wallW + gap) / (capW + gap)).toInt() + 1 val perRun = ((wallH + gap) / (capH + gap)).toInt() + 1 val run = (capH + gap) * perRun + val cap = with(density) { Size(capW.toPx(), capH.toPx()) } + val step = with(density) { (capH + gap).toPx() } + val radius = with(density) { 10.dp.toPx() } + val line = with(density) { 1.dp.toPx() } + val indexOf = { c: Int, i: Int -> if (count == 0) 0 else (c * 5 + (i % perRun) * 3) % count } + val shown = remember(count, columns, perRun) { (0 until columns).flatMap { c -> (0 until perRun).map { indexOf(c, it) } }.distinct() } + val looks = HashMap>(shown.size * 2) + for (index in shown) key(index) { + val look = capsule(index) + looks[index] = look to look.art?.let { rememberCapsuleArt(it, cap, radius) } + } Row( horizontalArrangement = Arrangement.spacedBy(gap), modifier = Modifier @@ -171,26 +214,52 @@ internal fun TiltedWall( .graphicsLayer { rotationZ = -13f }, ) { repeat(columns) { c -> - Column( - verticalArrangement = Arrangement.spacedBy(gap), - modifier = Modifier.wrapContentHeight(Alignment.Top, unbounded = true).graphicsLayer { - val shift = if (still) 0f else run.toPx() * drift.value - translationY = if (c % 2 == 0) -shift else shift - run.toPx() - }, - ) { - repeat(perRun * 2) { i -> - capsule(if (count == 0) 0 else (c * 5 + (i % perRun) * 3) % count, Modifier.size(capW, capH)) - } - } + Spacer( + Modifier.width(capW).wrapContentHeight(Alignment.Top, unbounded = true).height(run * 2 - gap) + .graphicsLayer { + val shift = if (still) 0f else run.toPx() * drift.floatValue + translationY = if (c % 2 == 0) -shift else shift - run.toPx() + } + .drawBehind { + for (i in 0 until perRun * 2) { + val (look, art) = looks[indexOf(c, i)] ?: continue + val top = i * step + inset(0f, top, 0f, size.height - top - cap.height) { drawCapsule(look, art, radius, line) } + } + }, + ) } } } } +/** A cover sized for a capsule, its corners already cut, so it draws without a clip. */ +@Composable +private fun rememberCapsuleArt(model: Any, size: Size, radius: Float): Painter { + val context = LocalContext.current + val w = size.width.roundToInt() + val h = size.height.roundToInt() + return rememberAsyncImagePainter( + remember(model, w, h, radius) { + ImageRequest.Builder(context).data(model).size(w, h).scale(Scale.FILL) + .transformations(RoundedCornersTransformation(radius)).build() + }, + ) +} + +private fun DrawScope.drawCapsule(look: WallCapsule, art: Painter?, radius: Float, line: Float) { + drawRoundRect(look.ground, size = size, cornerRadius = CornerRadius(radius)) + if (art != null) with(art) { draw(size) } + look.edge?.let { + drawRoundRect(it, Offset(line / 2, line / 2), Size(size.width - line, size.height - line), CornerRadius(radius - line / 2), style = Stroke(line)) + } +} + /** One to three games, each a large capsule leaning on the others and floating a little. */ @Composable private fun CapsuleFan(games: List) { val pal = LocalPalette.current + val density = LocalDensity.current val transition = rememberInfiniteTransition(label = "fan") val floats = listOf(9_000, 11_000, 13_000).map { ms -> transition.animateFloat(0f, 1f, infiniteRepeatable(tween(ms, easing = FastOutSlowInEasing), RepeatMode.Reverse), label = "float") @@ -212,12 +281,14 @@ private fun CapsuleFan(games: List) { ) }, ) { + val radius = with(density) { 14.dp.toPx() } for (i in games.indices.reversed()) { val slot = slots[i] val h = maxHeight * slot.height val v: State = floats[i] - Capsule( - games[i], RoundedCornerShape(14.dp), + val look = WallCapsule(artBrush(hueOf(games[i].name)), games[i].art) + val art = look.art?.let { rememberCapsuleArt(it, with(density) { Size((h * 2f / 3f).toPx(), h.toPx()) }, radius) } + Spacer( Modifier .offset(x = maxWidth * slot.x, y = maxHeight * slot.y) .size(h * 2f / 3f, h) @@ -228,23 +299,11 @@ private fun CapsuleFan(games: List) { shadowElevation = 24.dp.toPx() shape = RoundedCornerShape(14.dp) clip = false - }, + } + .drawBehind { drawCapsule(look, art, radius, 0f) }, ) } } } private class FanSlot(val x: Float, val y: Float, val height: Float, val lean: Float) - -/** A game's portrait capsule, or a blank one standing in for a game yet to be installed. */ -@Composable -private fun Capsule(g: Library.SteamGame?, shape: RoundedCornerShape, modifier: Modifier) { - val pal = LocalPalette.current - if (g == null) { - Box(modifier.clip(shape).background(Brush.linearGradient(listOf(pal.surface, pal.background))).border(1.dp, pal.line, shape)) - return - } - Box(modifier.clip(shape).background(artBrush(hueOf(g.name)))) { - if (g.art != null) AsyncImage(model = g.art, contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.matchParentSize()) - } -} diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StoresPage.kt b/app/src/main/java/com/droiddeck/launcher/ui/StoresPage.kt index ec3516dbe..a23c9ebaf 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StoresPage.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StoresPage.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.draw.blur import androidx.compose.foundation.layout.wrapContentHeight @@ -774,14 +775,12 @@ private fun SignInCard(store: Store, height: Dp) { .takeIf { it.size >= 8 }.orEmpty() } val shape = RoundedCornerShape(20.dp) - val capsule = RoundedCornerShape(10.dp) + val blank = WallCapsule(Brush.linearGradient(listOf(c.fill, colors.background)), edge = c.dot.copy(alpha = 0.22f)) + val ground = SolidColor(c.fill) Box(Modifier.fillMaxWidth().height(height.coerceAtLeast(320.dp)).clip(shape).background(colors.background).border(1.dp, pal.line, shape)) { // The storefront arrives a moment after the page: its wall fades in over the blank one. androidx.compose.animation.Crossfade(art, animationSpec = Motion.tw(600), label = "signInWall") { tiles -> - TiltedWall(tiles.size, if (tiles.isEmpty()) 75_000 else 50_000) { i, m -> - if (tiles.isEmpty()) Box(m.clip(capsule).background(Brush.linearGradient(listOf(c.fill, colors.background))).border(1.dp, c.dot.copy(alpha = 0.22f), capsule)) - else Box(m.clip(capsule).background(c.fill)) { AsyncImage(model = tiles[i], contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.matchParentSize()) } - } + TiltedWall(tiles.size, if (tiles.isEmpty()) 75_000 else 50_000) { i -> if (tiles.isEmpty()) blank else WallCapsule(ground, tiles[i]) } } // The wall gives way to the words: dark at the left and the foot, and the store's glow under them. Box( diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-agent b/tools/linuxfs/overlay/usr/local/bin/droiddeck-agent index 972c1502b..0df6d5363 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-agent +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-agent @@ -287,6 +287,9 @@ class FocusWatcher(threading.Thread): x = X(os.environ["DISPLAY"]) wanted = {x.atom(name) for name in FOCUS_PROPS} x.select(x.root, 0x400000) # PropertyChangeMask + # Waits for events as long as there are none: the connect timeout left in place + # here dropped and remade the connection, and took a snapshot, every few seconds. + x.s.settimeout(None) self.changed.set() while True: event = x.next_event() diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env b/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env index e8f5605a4..067212e34 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env @@ -106,6 +106,19 @@ def godot_sentry(game): return {"WINEDLLOVERRIDES": ";".join(n + "=d" for n in sentry)}, [], "Godot: Sentry crash handler off" +def skip_red_launcher(command): + """Cyberpunk 2077's prelauncher opens REDlauncher, a Qt WebEngine window that hands over to the + game half a minute later and then stays up beside it, well over a core busy for as long as the + game runs. The game is started itself instead; the prelauncher's own --launcher-skip started it + twice.""" + if len(command) > 2 and os.path.basename(command[2]).lower() == "redprelauncher.exe": + game = os.path.join(os.path.dirname(command[2]), "bin", "x64", "Cyberpunk2077.exe") + if os.path.isfile(game): + print("droiddeck-game-env: REDlauncher skipped", file=sys.stderr) + return command[:2] + [game] + command[3:] + return command + + def engine_fixes(exe, args): """Environment and extra arguments a game needs because of what it is built with.""" game = Game(exe, args) @@ -269,6 +282,7 @@ def main(): waiting = len(command) > 1 and command[1] == "waitforexitandrun" fixes = {} if waiting: + command = skip_red_launcher(command) fixes, extra, notes = engine_fixes(command[2], command[3:]) if len(command) > 2 else ({}, [], []) for note in notes: print("droiddeck-game-env: " + note, file=sys.stderr) diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-seed-redists b/tools/linuxfs/overlay/usr/local/bin/droiddeck-seed-redists index 467a35987..7b430422a 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-seed-redists +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-seed-redists @@ -108,12 +108,18 @@ vcrt() { done } -# The stamp is the set of names, taken before the block below dates its keys: stamped from the -# dated block it never matched, and every prefix had the whole block appended again on every beat. -stamp=$(printf '%s vcrt %s.%s.%s' "$pairs" "$VCRT_MAJOR" "$VCRT_MINOR" "$VCRT_BUILD" | cksum) +# Each prefix's marker lists the names it was given, so it is handed only what it lacks. One stamp +# for the whole set flipped between the session's set and the evaluator's (which adds the game's +# own names) on every launch, and every prefix was appended to again, inside the launch. +want=() +declare -A wanted=() +while IFS= read -r line; do + [[ $line == *$'\t'* ]] && [ -z "${wanted[$line]:-}" ] && wanted[$line]=1 && want+=("$line") +done <<<"$pairs" +want+=("vcrt"$'\t'"$VCRT_MAJOR.$VCRT_MINOR.$VCRT_BUILD") printf -v now '%(%s)T' -1 -block=$(printf '%s\n' "$pairs" | awk -F'\t' -v now="$now" ' +block_for() { printf '%s\n' "$@" | awk -F'\t' -v now="$now" ' { path = $1 sub(/^HKEY_LOCAL_MACHINE\\\\/, "", path) @@ -127,9 +133,10 @@ block=$(printf '%s\n' "$pairs" | awk -F'\t' -v now="$now" ' next printf "\n[%s] %d\n\"%s\"=dword:00000001\n", "Software\\\\Wow6432Node\\\\" substr(path, 11), now, $2 } -') -block="$block -$(vcrt "$now")" +' + local line + for line; do [[ $line == vcrt$'\t'* ]] && vcrt "$now"; done +} # Proton builds a new prefix from its template, and the client runs the install scripts in that # prefix moments later, so the template carries the markers too. Its date is kept: Proton reads a @@ -147,13 +154,17 @@ for reg in "$steam_root"/steamapps/common/Proton*/files/share/default_pfx*/syste if [ -f "$legacy" ] && [ ! -f "$seeded" ]; then mv -f "$legacy" "$seeded" fi - have= - [ -f "$seeded" ] && read -r have < "$seeded" - [ "$have" = "$stamp" ] && continue + declare -A had=() + if [ -f "$seeded" ]; then + while IFS= read -r line; do [[ $line == *$'\t'* ]] && had[$line]=1; done <"$seeded" + fi + missing=() + for line in "${want[@]}"; do [ -n "${had[$line]:-}" ] || missing+=("$line"); done + [ ${#missing[@]} -eq 0 ] && continue dated=$(mktemp) || continue touch -r "$reg" "$dated" - if printf '%s\n' "$block" >>"$reg"; then - printf '%s\n' "$stamp" >"$seeded" + if printf '%s\n' "$(block_for "${missing[@]}")" >>"$reg"; then + printf '%s\n' "${!had[@]}" "${missing[@]}" >"$seeded" echo "== seeded redist markers into $reg" fi touch -r "$dated" "$reg" diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-session b/tools/linuxfs/overlay/usr/local/bin/droiddeck-session index 4d5f0c04d..d8cd26482 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-session +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-session @@ -642,11 +642,18 @@ case $mode in beat=$((beat + 1)) [ "$((beat % 3))" -eq 0 ] || continue libraries=("$steam_root/steamapps" /mnt/droiddeck/steamapps /mnt/droiddeck-sd/steamapps /root/Games/*/steamapps) - if seed_due; then + seed=; compat= + seed_due && seed=1 + compat_due && compat=1 + [ -n "$seed$compat" ] || continue + # Not beside a running game: the client rewrites its manifest at each launch, which made both + # due while it loaded. Its own launch seeds its prefix (droiddeck-game-env) regardless. + pgrep -x wineserver >/dev/null 2>&1 && continue + if [ -n "$seed" ]; then : > "$seed_stamp" droiddeck-seed-redists "$steam_root" >/dev/null 2>&1 || true fi - if compat_due; then + if [ -n "$compat" ]; then : > "$compat_stamp" steam-compatibility "$steam_root" >/dev/null 2>&1 || true fi @@ -869,6 +876,8 @@ BUSCONF for ((_i = 0; _i < 60; _i++)); do nap 30 [ -e "$owned" ] && exit 0 + # Loading steamclient.so under proot takes seconds of CPU: not while a game loads or runs. + pgrep -x wineserver >/dev/null 2>&1 && continue answer=$(droiddeck-steam-games probe "$steam_root" 993090 2>/dev/null) || continue case $answer in *'"993090": true'*) mkdir -p "${owned%/*}" && : > "$owned" && echo "== Lossless Scaling: owned" ;; diff --git a/tools/linuxfs/preload/netif.c b/tools/linuxfs/preload/netif.c index a7b91a5bb..0e394a7e7 100644 --- a/tools/linuxfs/preload/netif.c +++ b/tools/linuxfs/preload/netif.c @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -28,6 +29,7 @@ #include #include #include +#include #include #define LINK_FILE "/etc/droiddeck-net" @@ -35,6 +37,7 @@ #define LO_INDEX 1 #define LO_MTU 65536 #define MAX_ADDRS 16 +#define FRESH_NS 1000000000LL struct link_addr { int family; @@ -61,10 +64,11 @@ static int parse_addr(const char *text, struct link_addr *out) { return 1; } -/* Read on every call: the app rewrites the file when the device changes network. */ -static int load_link(struct link *link) { - typedef FILE *(*fopen_fn)(const char *, const char *); - fopen_fn real_fopen = (fopen_fn) dlsym(RTLD_NEXT, "fopen"); +typedef FILE *(*fopen_fn)(const char *, const char *); + +static int read_link(struct link *link) { + static fopen_fn real_fopen; + if (real_fopen == NULL) real_fopen = (fopen_fn) dlsym(RTLD_NEXT, "fopen"); FILE *f = real_fopen ? real_fopen(LINK_FILE, "re") : NULL; if (f == NULL) return 0; @@ -93,6 +97,50 @@ static int load_link(struct link *link) { return link->name[0] != '\0' && link->index > LO_INDEX && link->addr_count > 0; } +static long long now_ns(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC_COARSE, &ts); + return ts.tv_sec * 1000000000LL + ts.tv_nsec; +} + +static int fresh(long long *at) { + long long then = __atomic_load_n(at, __ATOMIC_RELAXED); + return then != 0 && now_ns() - then < FRESH_NS; +} + +/* Wine's network driver reads the link several times for each table it builds, and some games + * have it build them all hundreds of times a second. The app rewrites the file when the device + * changes network, so a reading stands for a second. */ +static pthread_mutex_t link_lock = PTHREAD_MUTEX_INITIALIZER; +static struct link link_read; +static int link_ok; +static long long link_at; + +static int load_link(struct link *link) { + pthread_mutex_lock(&link_lock); + if (!fresh(&link_at)) { + link_ok = read_link(&link_read); + __atomic_store_n(&link_at, now_ns(), __ATOMIC_RELAXED); + } + int ok = link_ok; + if (ok) *link = link_read; + pthread_mutex_unlock(&link_lock); + return ok; +} + +/* See drm.c: a fork must not leave the lock held by a thread the child does not have. */ +static void link_lock_before_fork(void) { pthread_mutex_lock(&link_lock); } +static void link_unlock_after_fork(void) { pthread_mutex_unlock(&link_lock); } +static void link_reset_after_fork(void) { pthread_mutex_init(&link_lock, NULL); } + +__attribute__((constructor)) static void install_link_fork_handlers(void) { + pthread_atfork(link_lock_before_fork, link_unlock_after_fork, link_reset_after_fork); +} + +/* When the kernel's own list was of no use, it is not asked again for a second: each asking binds + * a netlink socket, a stop in the tracer, only to be refused again. */ +static long long ifaddrs_unusable_at, nameindex_unusable_at; + static void prefix_mask(int family, unsigned prefix, unsigned char *out) { int len = family == AF_INET ? 4 : 16; for (int i = 0; i < len; i++, prefix -= prefix < 8 ? prefix : 8) { @@ -168,25 +216,35 @@ static int real_list_usable(const struct ifaddrs *list) { return 0; } +typedef void (*freeifaddrs_fn)(struct ifaddrs *); +static freeifaddrs_fn real_freeifaddrs; + int getifaddrs(struct ifaddrs **out) { typedef int (*getifaddrs_fn)(struct ifaddrs **); - typedef void (*freeifaddrs_fn)(struct ifaddrs *); - getifaddrs_fn real = (getifaddrs_fn) dlsym(RTLD_NEXT, "getifaddrs"); + static getifaddrs_fn real; + if (real == NULL) real = (getifaddrs_fn) dlsym(RTLD_NEXT, "getifaddrs"); + if (real_freeifaddrs == NULL) real_freeifaddrs = (freeifaddrs_fn) dlsym(RTLD_NEXT, "freeifaddrs"); struct ifaddrs *list = NULL; - int rc = real(&list); - int saved = errno; - if (rc == 0 && real_list_usable(list)) { - *out = list; - return 0; + int asked = !fresh(&ifaddrs_unusable_at); + int rc = -1, saved = errno; + if (asked) { + rc = real(&list); + saved = errno; + if (rc == 0 && real_list_usable(list)) { + *out = list; + return 0; + } } struct link link; if (!load_link(&link)) { + if (!asked) return real(out); if (rc == 0) *out = list; errno = saved; return rc; } - if (rc == 0 && list) ((freeifaddrs_fn) dlsym(RTLD_NEXT, "freeifaddrs"))(list); + if (asked) __atomic_store_n(&ifaddrs_unusable_at, now_ns(), __ATOMIC_RELAXED); + if (rc == 0 && list) real_freeifaddrs(list); /* A leading slot keeps fill_entry()'s back link in bounds; the list starts after it. */ struct entry *block = calloc((size_t) link.addr_count + 5, sizeof(*block)); @@ -210,13 +268,13 @@ int getifaddrs(struct ifaddrs **out) { } void freeifaddrs(struct ifaddrs *list) { - typedef void (*freeifaddrs_fn)(struct ifaddrs *); if (list == NULL) return; if (list->ifa_data == list) { free((struct entry *) list - 1); return; } - ((freeifaddrs_fn) dlsym(RTLD_NEXT, "freeifaddrs"))(list); + if (real_freeifaddrs == NULL) real_freeifaddrs = (freeifaddrs_fn) dlsym(RTLD_NEXT, "freeifaddrs"); + real_freeifaddrs(list); } /* Names live in the same allocation as the array, right behind it; glibc strdup()s each one. */ @@ -227,16 +285,29 @@ struct name_block { struct if_nameindex *if_nameindex(void) { typedef struct if_nameindex *(*if_nameindex_fn)(void); - struct if_nameindex *list = ((if_nameindex_fn) dlsym(RTLD_NEXT, "if_nameindex"))(); + static if_nameindex_fn real; + if (real == NULL) real = (if_nameindex_fn) dlsym(RTLD_NEXT, "if_nameindex"); + struct if_nameindex *list = NULL; + int asked = !fresh(&nameindex_unusable_at); int saved = errno; - if (list && list[0].if_index != 0) return list; + if (asked) { + list = real(); + saved = errno; + if (list && list[0].if_index != 0) return list; + } struct link link; struct name_block *block; - if (!load_link(&link) || (block = calloc(1, sizeof(*block))) == NULL) { + if (!load_link(&link)) { + if (!asked) return real(); errno = saved; return list; } + if (asked) __atomic_store_n(&nameindex_unusable_at, now_ns(), __ATOMIC_RELAXED); + if ((block = calloc(1, sizeof(*block))) == NULL) { + if (asked) errno = saved; + return list; + } if (list) if_freenameindex(list); snprintf(block->names[0], IFNAMSIZ, "%s", LO_NAME); snprintf(block->names[1], IFNAMSIZ, "%s", link.name); @@ -489,8 +560,7 @@ static FILE *synthesize(const char *table) { /* tracer.c */ int bl_status_without_tracer(const char *path, int flags) __attribute__((visibility("hidden"))); -static FILE *open_stream(const char *symbol, const char *path, const char *mode) { - typedef FILE *(*fopen_fn)(const char *, const char *); +static FILE *open_stream(fopen_fn real, const char *path, const char *mode) { int status = mode && mode[0] == 'r' && !strchr(mode, '+') ? bl_status_without_tracer(path, O_RDONLY | O_CLOEXEC) : -1; if (status >= 0) { @@ -498,7 +568,7 @@ static FILE *open_stream(const char *symbol, const char *path, const char *mode) if (copy == NULL) close(status); return copy; } - FILE *f = ((fopen_fn) dlsym(RTLD_NEXT, symbol))(path, mode); + FILE *f = real(path, mode); if (f || path == NULL || mode == NULL || mode[0] != 'r') return f; int saved = errno; const char *table = denied_table(path); @@ -508,9 +578,13 @@ static FILE *open_stream(const char *symbol, const char *path, const char *mode) } FILE *fopen(const char *path, const char *mode) { - return open_stream("fopen", path, mode); + static fopen_fn real; + if (real == NULL) real = (fopen_fn) dlsym(RTLD_NEXT, "fopen"); + return open_stream(real, path, mode); } FILE *fopen64(const char *path, const char *mode) { - return open_stream("fopen64", path, mode); + static fopen_fn real; + if (real == NULL) real = (fopen_fn) dlsym(RTLD_NEXT, "fopen64"); + return open_stream(real, path, mode); } diff --git a/tools/proot/fastpath/fastpath.c b/tools/proot/fastpath/fastpath.c index a49703192..3914423a0 100644 --- a/tools/proot/fastpath/fastpath.c +++ b/tools/proot/fastpath/fastpath.c @@ -1651,6 +1651,19 @@ int isatty(int fd) { return r == 0; } +/* proot reads a memfd's name only to refuse the few that are then executed or locked; the rest are + * made here. The session's /proc/net and status copies, wineserver and Xwayland make them often. + * Without memfd in the kernel, proot's ashmem stands in. */ +int memfd_create(const char *name, unsigned flags) { + REAL(memfd_create, int (*)(const char *, unsigned)); + if (!name || !strncmp(name, "JITCode:", 8) || !strcmp(name, "opcache_lock") || !strncmp(name, "lib/apk/exec/", 13) + || !fp_ready()) return real_memfd_create(name, flags); + long r = sc(SYS_memfd_create, (long)name, flags, 0, 0, 0); + if (r == -ENOSYS) return real_memfd_create(name, flags); + hits++; + return ret(r); +} + /* ---------------------------------------------------------------- passwd */ /*