From a746d56dc8b26a4dad6f6ee11fe2d6b4e38da3ab Mon Sep 17 00:00:00 2001 From: maxjivi05 Date: Tue, 13 Oct 2026 09:50:24 -0400 Subject: [PATCH] Keep Steam alive with SteamVR registered, run SteamVR at the glasses' rate, keep the glasses lip on top --- .../com/droiddeck/launcher/ui/DropSheet.kt | 9 +-- tools/proot/PATCHES.md | 6 ++ .../0015-robust-list-from-seccomp.patch | 61 +++++++++++++++++++ tools/vr/src/driver_droiddeck.cpp | 26 +++++--- tools/vr/src/vk_layer_droiddeck_vr.c | 49 +++++++++++++-- 5 files changed, 130 insertions(+), 21 deletions(-) create mode 100644 tools/proot/patches/0015-robust-list-from-seccomp.patch diff --git a/app/src/main/java/com/droiddeck/launcher/ui/DropSheet.kt b/app/src/main/java/com/droiddeck/launcher/ui/DropSheet.kt index a6e4512b6..898badbd6 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/DropSheet.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/DropSheet.kt @@ -17,7 +17,6 @@ import androidx.compose.foundation.focusGroup import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.hoverable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraintsScope import androidx.compose.foundation.layout.Column @@ -204,9 +203,6 @@ internal fun BoxWithConstraintsScope.DropSheet( val narrow = LocalNarrowPane.current val chipFocus = remember(tabs.size) { List(tabs.size) { FocusRequester() } } val buttonFocus = remember { FocusRequester() } - val buttonSrc = remember { MutableInteractionSource() } - val buttonHovered by buttonSrc.collectIsHoveredAsState() - var buttonFocused by remember { mutableStateOf(false) } val rightForButton = remember { booleanArrayOf(false) } var rest by remember { mutableStateOf(null) } val openState = rememberUpdatedState(state.open) @@ -277,8 +273,7 @@ internal fun BoxWithConstraintsScope.DropSheet( BackHandler(enabled = state.open) { close() } if (state.ride.isRunning || (state.shown && !state.open)) VeilRing() - val lipWanted = !state.open && (!state.shown || state.lipBack) && covered() < 0.5f && - (buttonFocused || buttonHovered || inputMode.inputMode == InputMode.Touch) + val lipWanted = !state.open && (!state.shown || state.lipBack) && covered() < 0.5f LaunchedEffect(lipWanted) { if (lipWanted) { if (!state.lipBack) delay(Motion.ms(160).toLong()) @@ -453,9 +448,7 @@ internal fun BoxWithConstraintsScope.DropSheet( .focusProperties { canFocus = enabled } .onFocusChanged { if (it.hasFocus && rightForButton[0]) { rightForButton[0] = false; if (openState.value) close() } - buttonFocused = it.hasFocus } - .hoverable(buttonSrc) .onPreviewKeyEvent { e -> val k = e.nativeKeyEvent if (k.keyCode != KeyEvent.KEYCODE_DPAD_DOWN && !(state.open && k.keyCode == KeyEvent.KEYCODE_DPAD_LEFT)) false diff --git a/tools/proot/PATCHES.md b/tools/proot/PATCHES.md index 342bf79e6..d893f23c6 100644 --- a/tools/proot/PATCHES.md +++ b/tools/proot/PATCHES.md @@ -80,6 +80,12 @@ Added by DroidDeck: The `faccessat2` exit stop (termux 5ba8b95, for glibc's ENOSYS fallback on kernels before 5.8) and the `statx` one (emulation on kernels before 4.11) are dropped when the running kernel is newer. Host build: `ioctl` 31.6 -> 0.5 us; `stat`/`statx`/`faccessat2` about 44 -> 28 us (one stop). +- `0015-robust-list-from-seccomp.patch` - Android's app policy refuses `set_robust_list` and + `get_robust_list`. proot still answers the first with `ENOSYS` but keeps the head each thread + offered (cleared on `execve`, none in a new task), and answers the second for the calling thread + with it. SteamVR's `vrclient.so` asks for the head through libc's `syscall()`, which + `libblsession.so`'s robust.c never sees: `libopenvr_api.so` opens it `RTLD_DEEPBIND`. Steam + aborted on "futex robust_list not initialized by pthreads" as soon as SteamVR was registered. Prototype (inert unless `PROOT_FASTPATH` is set; see `docs/development/proot-performance.md`): diff --git a/tools/proot/patches/0015-robust-list-from-seccomp.patch b/tools/proot/patches/0015-robust-list-from-seccomp.patch new file mode 100644 index 000000000..d9e5c6181 --- /dev/null +++ b/tools/proot/patches/0015-robust-list-from-seccomp.patch @@ -0,0 +1,61 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: DroidDeck +Date: Tue, 13 Oct 2026 00:00:00 +0000 +Subject: [PATCH] seccomp: answer get_robust_list with the head set_robust_list + was refused for + +diff --git a/src/execve/exit.c b/src/execve/exit.c +index f32d3ec..b5475b3 100644 +--- a/src/execve/exit.c ++++ b/src/execve/exit.c +@@ -473,6 +473,8 @@ void translate_execve_exit(Tracee *tracee) + talloc_set_name_const(tracee->exe, "$exe"); + } + ++ tracee->robust_list = 0; ++ + /* New processes have no heap. */ + if (talloc_reference_count(tracee->heap) >= 1) { + talloc_unlink(tracee, tracee->heap); +diff --git a/src/tracee/seccomp.c b/src/tracee/seccomp.c +index 77d5b27..0af232b 100644 +--- a/src/tracee/seccomp.c ++++ b/src/tracee/seccomp.c +@@ -559,6 +559,24 @@ static int handle_seccomp_event_common(Tracee *tracee) + } + + case PR_set_robust_list: ++ tracee->robust_list = peek_reg(tracee, CURRENT, SYSARG_1); ++ set_result_after_seccomp(tracee, -ENOSYS); ++ break; ++ ++ case PR_get_robust_list: ++ { ++ pid_t pid = (pid_t) peek_reg(tracee, CURRENT, SYSARG_1); ++ if (tracee->robust_list == 0 || (pid != 0 && pid != tracee->pid)) { ++ set_result_after_seccomp(tracee, -ENOSYS); ++ break; ++ } ++ poke_word(tracee, peek_reg(tracee, CURRENT, SYSARG_2), tracee->robust_list); ++ if (errno == 0) ++ poke_word(tracee, peek_reg(tracee, CURRENT, SYSARG_3), 3 * sizeof_word(tracee)); ++ set_result_after_seccomp(tracee, errno != 0 ? -EFAULT : 0); ++ break; ++ } ++ + default: + /* Set errno to -ENOSYS */ + set_result_after_seccomp(tracee, -ENOSYS); +diff --git a/src/tracee/tracee.h b/src/tracee/tracee.h +index 1fca85f..496ca75 100644 +--- a/src/tracee/tracee.h ++++ b/src/tracee/tracee.h +@@ -248,6 +248,8 @@ typedef struct tracee { + * has to be ignored as it's same syscall entry */ + bool seccomp_already_handled_enter; + ++ word_t robust_list; ++ + /********************************************************************** + * Shared or private resources, depending on the CLONE_FS/VM flags. * + **********************************************************************/ diff --git a/tools/vr/src/driver_droiddeck.cpp b/tools/vr/src/driver_droiddeck.cpp index 0ed96beee..11a7aeb86 100644 --- a/tools/vr/src/driver_droiddeck.cpp +++ b/tools/vr/src/driver_droiddeck.cpp @@ -464,6 +464,7 @@ public: EVRInitError Activate(uint32_t unObjectId) override { id_ = unObjectId; PropertyContainerHandle_t c = VRProperties()->TrackedDeviceToPropertyContainer(id_); + container_ = c; width_ = VRSettings()->GetInt32(kSettingsSection, "windowWidth"); height_ = VRSettings()->GetInt32(kSettingsSection, "windowHeight"); if (width_ <= 0) width_ = 1920; @@ -480,6 +481,7 @@ public: } if (pose_.Read(&hp) && hp.display_hz >= 24.0f) hz = hp.display_hz; Vsync::SetRate(hz); + sentHz_ = hz; sentTan_ = tanHalf_; sentAspect_ = eyeAspect_; VRProperties()->SetStringProperty(c, Prop_ModelNumber_String, "DroidDeck Cardboard"); @@ -509,7 +511,7 @@ public: VRProperties()->SetBoolProperty(c, Prop_ContainsProximitySensor_Bool, true); VRDriverInput()->CreateBooleanComponent(c, "/proximity", &proximity_); VRDriverInput()->UpdateBooleanComponent(proximity_, true, 0); - Log("HMD activated: %dx%d at %.0f Hz, eye tan(half fov) %.3f, eye aspect %.3f", width_, height_, hz, tanHalf_, eyeAspect_); + Log("HMD activated: %dx%d at %.0f Hz, eye tan(half fov) %.3f, eye aspect %.3f", width_, height_, hz, tanHalf_.load(), eyeAspect_.load()); running_ = true; thread_ = std::thread([this] { PoseLoop(); }); @@ -565,8 +567,9 @@ public: bool IsDisplayRealDisplay() override { return false; } void GetRecommendedRenderTargetSize(uint32_t *w, uint32_t *h) override { // Rendered a little larger than the panel: the lens pass magnifies the middle. - *w = (uint32_t)(height_ * 1.4f * eyeAspect_); - *h = (uint32_t)(height_ * 1.4f); + const float scale = renderScale_; + *w = (uint32_t)(height_ * scale * eyeAspect_); + *h = (uint32_t)(height_ * scale); } void GetEyeOutputViewport(EVREye eye, uint32_t *x, uint32_t *y, uint32_t *w, uint32_t *h) override { *y = 0; *w = (uint32_t)width_ / 2; *h = (uint32_t)height_; @@ -592,6 +595,7 @@ private: void UpdateEye(const droiddeck_headpose &hp) { sentTan_ = tanHalf_ = hp.tan_half_fov; sentAspect_ = eyeAspect_ = hp.eye_aspect; + renderScale_ = (hp.flags & DROIDDECK_HEADPOSE_MONO) ? 1.0f : 1.4f; HmdRect2_t r; r.vTopLeft.v[0] = -tanHalf_ * eyeAspect_; r.vTopLeft.v[1] = -tanHalf_; @@ -601,10 +605,17 @@ private: uint32_t w = 0, h = 0; GetRecommendedRenderTargetSize(&w, &h); VRServerDriverHost()->SetRecommendedRenderTargetSize(id_, w, h); - Log("eye now tan(half fov) %.3f, aspect %.3f (%ux%u eye%s), render %ux%u", tanHalf_, eyeAspect_, hp.eye_w, hp.eye_h, + Log("eye now tan(half fov) %.3f, aspect %.3f (%ux%u eye%s), render %ux%u", tanHalf_.load(), eyeAspect_.load(), hp.eye_w, hp.eye_h, (hp.flags & DROIDDECK_HEADPOSE_MONO) ? ", one eye shown" : "", w, h); } + void UpdateRate(float hz) { + sentHz_ = hz; + Vsync::SetRate(hz); + VRProperties()->SetFloatProperty(container_, Prop_DisplayFrequency_Float, hz); + Log("panel now %.0f Hz", hz); + } + void PoseLoop() { uint32_t lastSeq = 0; auto lastSent = std::chrono::steady_clock::now(); @@ -622,7 +633,7 @@ private: if (hp.eye_w > 0 && hp.tan_half_fov > 0.0f && hp.eye_aspect > 0.0f && (fabsf(hp.tan_half_fov - sentTan_) > 1e-3f || fabsf(hp.eye_aspect - sentAspect_) > 1e-3f)) UpdateEye(hp); - if (hp.display_hz >= 24.0f) Vsync::SetRate(hp.display_hz); + if (hp.display_hz >= 24.0f && fabsf(hp.display_hz - sentHz_) > 0.5f) UpdateRate(hp.display_hz); const bool worn = (hp.flags & DROIDDECK_HEADPOSE_ENABLED) != 0; if (worn != worn_) { worn_ = worn; @@ -642,8 +653,9 @@ private: uint32_t id_ = k_unTrackedDeviceIndexInvalid; int32_t width_ = 1920, height_ = 1080; - float tanHalf_ = 1.0f, eyeAspect_ = 0.889f; - float sentTan_ = 0.0f, sentAspect_ = 0.0f; + std::atomic tanHalf_{1.0f}, eyeAspect_{0.889f}, renderScale_{1.4f}; + float sentTan_ = 0.0f, sentAspect_ = 0.0f, sentHz_ = 0.0f; + PropertyContainerHandle_t container_ = k_ulInvalidPropertyContainer; public: void SetHands(CardboardController *left, CardboardController *right) { hands_[0] = left; hands_[1] = right; } diff --git a/tools/vr/src/vk_layer_droiddeck_vr.c b/tools/vr/src/vk_layer_droiddeck_vr.c index 8ac54888f..8b1a58474 100644 --- a/tools/vr/src/vk_layer_droiddeck_vr.c +++ b/tools/vr/src/vk_layer_droiddeck_vr.c @@ -312,21 +312,25 @@ static void VKAPI_CALL DestroyDevice(VkDevice device, const VkAllocationCallback * One pretend display with one mode and one plane. vrcompositor keeps a direct mode window on it even * when a driver-direct driver takes the frames, and waits on that window's presents, so the display * needs a working swapchain: its surface is a headless one (VK_EXT_headless_surface, Mesa's WSI). - * The vblank side of VK_EXT_display_control is a 60 Hz clock. Size and rate: - * DROIDDECK_VR_DISPLAY_MODE=WIDTHxHEIGHT@HZ (default 1920x1080@60). */ + * The vblank side of VK_EXT_display_control is a clock at the panel's rate. Size and rate: + * DROIDDECK_VR_DISPLAY_MODE=WIDTHxHEIGHT@HZ (default 1920x1080, at the head pose file's rate, else 60). */ +#include +#include #include +#include "../../../app/src/main/cpp/waylandcomp/src/headpose_shm.h" #define FAKE_DISPLAY ((VkDisplayKHR)(uintptr_t)0xdd0001) #define FAKE_MODE ((VkDisplayModeKHR)(uintptr_t)0xdd0002) static uint32_t g_mode_w = 1920, g_mode_h = 1080, g_mode_mhz = 60000; +static int g_mode_fixed; static void mode_init(void) { static int done; if (done++) return; const char *m = getenv("DROIDDECK_VR_DISPLAY_MODE"); unsigned w, h, hz; - if (m && sscanf(m, "%ux%u@%u", &w, &h, &hz) == 3 && w && h && hz) { g_mode_w = w; g_mode_h = h; g_mode_mhz = hz * 1000; } + if (m && sscanf(m, "%ux%u@%u", &w, &h, &hz) == 3 && w && h && hz) { g_mode_w = w; g_mode_h = h; g_mode_mhz = hz * 1000; g_mode_fixed = 1; } } static uint64_t now_ns(void) { @@ -334,7 +338,40 @@ static uint64_t now_ns(void) { clock_gettime(CLOCK_MONOTONIC, &ts); return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec; } -static uint64_t frame_ns(void) { return 1000000000000ull / g_mode_mhz; } +static _Atomic(const struct droiddeck_headpose *) g_pose; +static _Atomic uint64_t g_pose_tried; + +static const struct droiddeck_headpose *pose_map(void) { + const struct droiddeck_headpose *p = atomic_load(&g_pose); + if (p) return p; + uint64_t t = now_ns(), last = atomic_load(&g_pose_tried); + if (last && t - last < 1000000000ull) return NULL; + atomic_store(&g_pose_tried, t); + int fd = open("/run/droiddeck/" DROIDDECK_HEADPOSE_FILE, O_RDONLY | O_CLOEXEC); + if (fd < 0) return NULL; + void *m = mmap(NULL, sizeof(struct droiddeck_headpose), PROT_READ, MAP_SHARED, fd, 0); + close(fd); + if (m == MAP_FAILED) return NULL; + const struct droiddeck_headpose *expected = NULL; + if (!atomic_compare_exchange_strong(&g_pose, &expected, (const struct droiddeck_headpose *)m)) { + munmap(m, sizeof(struct droiddeck_headpose)); + return expected; + } + return m; +} + +static uint32_t mode_mhz(void) { + mode_init(); + if (g_mode_fixed) return g_mode_mhz; + const struct droiddeck_headpose *p = pose_map(); + if (!p || __atomic_load_n(&p->magic, __ATOMIC_RELAXED) != DROIDDECK_HEADPOSE_MAGIC) return g_mode_mhz; + uint32_t bits = __atomic_load_n((const uint32_t *)&p->display_hz, __ATOMIC_RELAXED); + float hz; + memcpy(&hz, &bits, sizeof(hz)); + return hz >= 24.0f && hz <= 240.0f ? (uint32_t)(hz * 1000.0f + 0.5f) : g_mode_mhz; +} + +static uint64_t frame_ns(void) { return 1000000000000ull / mode_mhz(); } static VkDisplayPropertiesKHR display_props(void) { mode_init(); @@ -384,7 +421,7 @@ static VkResult VKAPI_CALL GetDisplayPlaneSupportedDisplaysKHR(VkPhysicalDevice } static VkDisplayModePropertiesKHR mode_props(void) { mode_init(); - VkDisplayModePropertiesKHR m = {FAKE_MODE, {{g_mode_w, g_mode_h}, g_mode_mhz}}; + VkDisplayModePropertiesKHR m = {FAKE_MODE, {{g_mode_w, g_mode_h}, mode_mhz()}}; return m; } static VkResult VKAPI_CALL GetDisplayModePropertiesKHR(VkPhysicalDevice pd, VkDisplayKHR d, uint32_t *count, VkDisplayModePropertiesKHR *p) { @@ -452,7 +489,7 @@ static VkResult VKAPI_CALL GetRandROutputDisplayEXT(VkPhysicalDevice pd, void *d *d = FAKE_DISPLAY; return VK_SUCCESS; } -/* VK_EXT_display_control on the device: a 60 Hz clock. A vblank event's fence comes back signalled +/* VK_EXT_display_control on the device: a clock at the panel's rate. A vblank event's fence comes back signalled * once the next tick has passed (the caller's vblank thread waits right after registering). */ static VkResult VKAPI_CALL DisplayPowerControlEXT(VkDevice d, VkDisplayKHR disp, const VkDisplayPowerInfoEXT *info) { return VK_SUCCESS; } static VkResult signalled_fence(VkDevice d, const VkAllocationCallbacks *a, VkFence *fence) {