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 */ /*