Use controller-tagged compositor scroll for grab depth

This commit is contained in:
baketnk committed 2026-10-04 22:03:33 -04:00
1 parent fe8726864e
commit f43848731b
6 files changed
+96 -13

No files matched your search

+9 -3
View File
@@ -390,10 +390,16 @@ current depth; the other hand's stick and scale drags do not change it. A lost
axis stops adding depth without resetting placement. Frame controller bindings axis stops adding depth without resetting placement. Frame controller bindings
provide left/right vector2 actions in a separate `/actions/grab` set, including provide left/right vector2 actions in a separate `/actions/grab` set, including
in generated custom button manifests. Only a grab activates this set, restricted in generated custom button manifests. Only a grab activates this set, restricted
to the grabbing hand at overlay priority; it requires SteamVR Experimental to the grabbing hand at overlay priority; that app-action route requires SteamVR Experimental
overlay input overrides. It does not promote recording or text actions, and does overlay input overrides. If the app axis is unavailable, controller-tagged
compositor smooth-scroll events provide depth at 0.04 m per scroll unit, bounded
to 3 cm per event and the same total travel limit. Only the grabbing controller
is accepted. Smooth events are requested only during grab, integrated once
without multiplying by elapsed time, and discarded whenever the app axis is
available to avoid applying both streams. It does not promote recording or text actions, and does
not depend on FrameYap's ordinary `input_priority` preference. Axis delivery during laser drag and dashboard/game focus needs a not depend on FrameYap's ordinary `input_priority` preference. Axis delivery during laser drag and dashboard/game focus needs a
separate headset check. Laser scroll events are ignored while dragging. separate headset check. Grab scroll events affect only depth; scale scroll
events are ignored, and ordinary UI scrolling resumes after release.
Release leaves the last pose in the chosen mount frame; head/wrist mounts continue Release leaves the last pose in the chosen mount frame; head/wrist mounts continue
following that anchor afterward. A completed grab or scale on Head, Left wrist or following that anchor afterward. A completed grab or scale on Head, Left wrist or
Right wrist saves the full device-relative canvas position, rotation and scale Right wrist saves the full device-relative canvas position, rotation and scale
+1 -1
View File
@@ -45,7 +45,7 @@ import urllib.request
KEY = "local.frameyap.overlay" KEY = "local.frameyap.overlay"
# The pinned release this installer belongs to (set in the release commit that # The pinned release this installer belongs to (set in the release commit that
# is tagged vRELEASE_VERSION); empty in a development tree. # is tagged vRELEASE_VERSION); empty in a development tree.
RELEASE_VERSION = "0.1.202610050153" RELEASE_VERSION = "0.1.202610050203"
MARKER = "# FrameYap managed launcher v1\n" MARKER = "# FrameYap managed launcher v1\n"
ARCHIVE_LIMIT = 12 * 1024**3 ARCHIVE_LIMIT = 12 * 1024**3
MEMBER_LIMIT = 50000 MEMBER_LIMIT = 50000
+1 -1
View File
@@ -25,7 +25,7 @@ import urllib.request
KEY = "local.frameyap.overlay" KEY = "local.frameyap.overlay"
# The pinned release this installer belongs to (set in the release commit that # The pinned release this installer belongs to (set in the release commit that
# is tagged vRELEASE_VERSION); empty in a development tree. # is tagged vRELEASE_VERSION); empty in a development tree.
RELEASE_VERSION = "0.1.202610050153" RELEASE_VERSION = "0.1.202610050203"
MARKER = "# FrameYap managed launcher v1\n" MARKER = "# FrameYap managed launcher v1\n"
ARCHIVE_LIMIT = 12 * 1024**3 ARCHIVE_LIMIT = 12 * 1024**3
MEMBER_LIMIT = 50000 MEMBER_LIMIT = 50000
+33 -6
View File
@@ -145,6 +145,8 @@ struct Overlay::Impl {
bool enabled = [] { const char* flag = std::getenv("FRAMEYAP_PROFILE"); bool enabled = [] { const char* flag = std::getenv("FRAMEYAP_PROFILE");
return flag && std::string_view(flag) == "1"; }(); return flag && std::string_view(flag) == "1"; }();
uint64_t renders = 0, uploads = 0, motion_ticks = 0, retained_frames = 0, motion_redraws = 0, motion_uploads = 0; uint64_t renders = 0, uploads = 0, motion_ticks = 0, retained_frames = 0, motion_redraws = 0, motion_uploads = 0;
uint64_t depth_samples = 0, depth_axis_active = 0, depth_scroll_events = 0, depth_scroll_accepted = 0;
double max_depth = 0;
double render_ms = 0, upload_ms = 0, motion_ms = 0, motion_max_ms = 0; double render_ms = 0, upload_ms = 0, motion_ms = 0, motion_max_ms = 0;
static double elapsed(std::chrono::steady_clock::time_point start) { static double elapsed(std::chrono::steady_clock::time_point start) {
return std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count(); return std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count();
@@ -155,7 +157,10 @@ struct Overlay::Impl {
<< " uploads=" << uploads << " upload_ms=" << upload_ms << " uploads=" << uploads << " upload_ms=" << upload_ms
<< " motion_ticks=" << motion_ticks << " motion_ms=" << motion_ms << " motion_ticks=" << motion_ticks << " motion_ms=" << motion_ms
<< " motion_max_ms=" << motion_max_ms << " retained_frames=" << retained_frames << " motion_max_ms=" << motion_max_ms << " retained_frames=" << retained_frames
<< " motion_redraws=" << motion_redraws << " motion_uploads=" << motion_uploads << std::endl; << " motion_redraws=" << motion_redraws << " motion_uploads=" << motion_uploads
<< " depth_samples=" << depth_samples << " depth_axis_active=" << depth_axis_active
<< " depth_scroll_events=" << depth_scroll_events << " depth_scroll_accepted=" << depth_scroll_accepted
<< " max_depth=" << max_depth << std::endl;
} }
} performance; } performance;
@@ -233,9 +238,9 @@ struct Overlay::Impl {
persistence.laser_change_failed = true; persistence.laser_change_failed = true;
} }
overlay_check(overlay->SetOverlayFlag(handle, vr::VROverlayFlags_SendVRDiscreteScrollEvents, true), overlay, "Laser scroll"); overlay_check(overlay->SetOverlayFlag(handle, vr::VROverlayFlags_SendVRDiscreteScrollEvents, true), overlay, "Laser scroll");
// Smooth events are optional on some runtimes; discrete wheel events // Normal UI scrolling uses discrete events. Request the compositor
// keep Settings usable if the compositor declines the smooth mode. // smooth stream only while grabbing for the depth fallback.
overlay->SetOverlayFlag(handle, vr::VROverlayFlags_SendVRSmoothScrollEvents, true); overlay->SetOverlayFlag(handle, vr::VROverlayFlags_SendVRSmoothScrollEvents, false);
surface.set_lasers_anytime(lasers_anytime); surface.set_lasers_anytime(lasers_anytime);
surface.set_wrist_world_fallback(config.wrist_world_fallback); surface.set_wrist_world_fallback(config.wrist_world_fallback);
surface.set_advanced_debug(config.advanced_debug); surface.set_advanced_debug(config.advanced_debug);
@@ -422,6 +427,8 @@ struct Overlay::Impl {
} }
dragging.scale = size_scale; dragging.canvas = canvas_pose; dragging.scale = size_scale; dragging.canvas = canvas_pose;
dragging.started = dragging.last_depth_sample = std::chrono::steady_clock::now(); dragging.started = dragging.last_depth_sample = std::chrono::steady_clock::now();
if (kind == PanelDragKind::Grab)
overlay->SetOverlayFlag(handle, vr::VROverlayFlags_SendVRSmoothScrollEvents, true);
vr::VRControllerState_t state{}; vr::VRControllerState_t state{};
dragging.trigger_observed = system->GetControllerState(dragging.device, &state, sizeof(state)) && dragging.trigger_observed = system->GetControllerState(dragging.device, &state, sizeof(state)) &&
(state.ulButtonPressed & vr::ButtonMaskFromId(vr::k_EButton_SteamVR_Trigger)); (state.ulButtonPressed & vr::ButtonMaskFromId(vr::k_EButton_SteamVR_Trigger));
@@ -429,6 +436,7 @@ struct Overlay::Impl {
" device=" + std::to_string(dragging.device) + " trigger-watch=" + (dragging.trigger_observed ? "Y" : "N"); " device=" + std::to_string(dragging.device) + " trigger-watch=" + (dragging.trigger_observed ? "Y" : "N");
} }
void finish_drag(bool released) { void finish_drag(bool released) {
overlay->SetOverlayFlag(handle, vr::VROverlayFlags_SendVRSmoothScrollEvents, false);
if (!released && applied_mount && *applied_mount == mount) { if (!released && applied_mount && *applied_mount == mount) {
canvas_pose = dragging.canvas; canvas_pose = dragging.canvas;
size_scale = dragging.scale; size_scale = dragging.scale;
@@ -477,8 +485,16 @@ struct Overlay::Impl {
const auto now = std::chrono::steady_clock::now(); const auto now = std::chrono::steady_clock::now();
const double dt = std::chrono::duration<double>(now - dragging.last_depth_sample).count(); const double dt = std::chrono::duration<double>(now - dragging.last_depth_sample).count();
dragging.last_depth_sample = now; dragging.last_depth_sample = now;
if (dragging.kind == PanelDragKind::Grab && diagnostics.action_update_error == vr::VRInputError_None) if (dragging.kind == PanelDragKind::Grab) {
if (auto axis = depth_axis()) dragging.panel.step_depth(*axis, dt); const auto axis = diagnostics.action_update_error == vr::VRInputError_None ? depth_axis() : std::nullopt;
if (performance.enabled) {
++performance.depth_samples;
performance.depth_axis_active += bool(axis);
}
dragging.panel.update_depth(axis, dt);
if (performance.enabled)
performance.max_depth = std::max(performance.max_depth, std::abs(dragging.panel.depth()));
}
const auto change = dragging.panel.update(*source); const auto change = dragging.panel.update(*source);
if (!change) { finish_drag(false); return; } if (!change) { finish_drag(false); return; }
const float scale = dragging.kind == PanelDragKind::Scale ? std::clamp(dragging.scale * change->factor, .5f, 2.f) : size_scale; const float scale = dragging.kind == PanelDragKind::Scale ? std::clamp(dragging.scale * change->factor, .5f, 2.f) : size_scale;
@@ -654,6 +670,17 @@ struct Overlay::Impl {
} }
break; break;
case vr::VREvent_ScrollDiscrete: case vr::VREvent_ScrollSmooth: case vr::VREvent_ScrollDiscrete: case vr::VREvent_ScrollSmooth:
if (dragging.panel.active()) {
if (event.eventType == vr::VREvent_ScrollSmooth && dragging.kind == PanelDragKind::Grab) {
const bool accepted = dragging.panel.queue_scroll_depth(event.trackedDeviceIndex,
dragging.device, event.data.scroll.ydelta);
if (performance.enabled) {
++performance.depth_scroll_events;
performance.depth_scroll_accepted += accepted;
}
}
break; // depth consumes the grab input; never scroll the UI
}
if (!dragging.panel.active() && event.data.scroll.cursorIndex < cursor_positions.size() && if (!dragging.panel.active() && event.data.scroll.cursorIndex < cursor_positions.size() &&
cursor_positions[event.data.scroll.cursorIndex]) { cursor_positions[event.data.scroll.cursorIndex]) {
const auto [x, y] = *cursor_positions[event.data.scroll.cursorIndex]; const auto [x, y] = *cursor_positions[event.data.scroll.cursorIndex];
+22 -2
View File
@@ -4,6 +4,9 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <cstdint>
#include <limits>
#include <utility>
#include <optional> #include <optional>
namespace frameyap { namespace frameyap {
@@ -140,6 +143,23 @@ public:
depth_ = std::clamp(depth_ + velocity * std::min(seconds, .05), -1.5, 1.5); depth_ = std::clamp(depth_ + velocity * std::min(seconds, .05), -1.5, 1.5);
} }
// Compositor smooth scroll is already integrated over time. Accept only
// the captured controller, cap each event at 3 cm, and consume it once.
bool queue_scroll_depth(uint32_t source, uint32_t grabbing_device, float delta) {
if (!active_ || kind_ != PanelDragKind::Grab ||
source == std::numeric_limits<uint32_t>::max() || source != grabbing_device ||
!std::isfinite(delta)) return false;
pending_scroll_ = std::clamp(pending_scroll_ + std::clamp(double(delta) * .04, -.03, .03), -1.5, 1.5);
return true;
}
void update_depth(std::optional<float> axis, double seconds) {
const double scroll = std::exchange(pending_scroll_, 0.0);
if (!active_ || kind_ != PanelDragKind::Grab) return;
if (axis && std::isfinite(*axis)) step_depth(*axis, seconds);
else depth_ = std::clamp(depth_ + scroll, -1.5, 1.5);
}
double depth() const { return depth_; } double depth() const { return depth_; }
std::optional<PanelDragUpdate> update(const Matrix34& controller_pose) const { std::optional<PanelDragUpdate> update(const Matrix34& controller_pose) const {
@@ -167,7 +187,7 @@ public:
return PanelDragUpdate{panel_pose_, static_cast<float>(factor)}; return PanelDragUpdate{panel_pose_, static_cast<float>(factor)};
} }
void reset() { active_ = false; depth_ = 0; } void reset() { active_ = false; depth_ = pending_scroll_ = 0; }
bool active() const { return active_; } bool active() const { return active_; }
private: private:
@@ -177,7 +197,7 @@ private:
Matrix34 panel_pose_{}, relative_panel_{}; Matrix34 panel_pose_{}, relative_panel_{};
Vec3 center_{}, normal_{}, anchor_{}, down_hit_{}, local_ray_{}; Vec3 center_{}, normal_{}, anchor_{}, down_hit_{}, local_ray_{};
double baseline_sq_ = 0; double baseline_sq_ = 0;
double depth_ = 0; double depth_ = 0, pending_scroll_ = 0;
}; };
} // namespace frameyap } // namespace frameyap
+30
View File
@@ -146,6 +146,36 @@ int main() {
} }
drag.reset(); drag.reset();
// Compositor events are source-tagged and already time-integrated.
assert(drag.begin(PanelDragKind::Grab, angled_panel, 1, 1, .5f, .5f, down));
assert(!drag.queue_scroll_depth(2, 1, 1.f)); // the other hand
assert(!drag.queue_scroll_depth(std::numeric_limits<uint32_t>::max(), 1, 1.f));
assert(!drag.queue_scroll_depth(1, 1, std::numeric_limits<float>::quiet_NaN()));
assert(drag.queue_scroll_depth(1, 1, .5f));
drag.update_depth({}, .001); near(float(drag.depth()), .02f);
drag.update_depth({}, .05); near(float(drag.depth()), .02f); // no replay
depth_pose = drag.update(down);
assert(depth_pose); near_pose(depth_pose->pose, translated(angled_panel, -.02f, 0, 0));
assert(drag.queue_scroll_depth(1, 1, 100.f));
drag.update_depth({}, 100.); near(float(drag.depth()), .05f); // capped, no dt multiplication
assert(drag.queue_scroll_depth(1, 1, -.5f));
drag.update_depth({}, 0); near(float(drag.depth()), .03f); // opposite direction
assert(drag.queue_scroll_depth(1, 1, 1.f));
drag.update_depth(0.f, .05); near(float(drag.depth()), .03f); // active centered axis wins
assert(drag.queue_scroll_depth(1, 1, 1.f));
drag.update_depth(1.f, .05); near(float(drag.depth()), .06f); // no double application
for (int i = 0; i < 100; ++i) assert(drag.queue_scroll_depth(1, 1, 1.f));
drag.update_depth({}, .01); near(float(drag.depth()), 1.5f);
assert(drag.queue_scroll_depth(1, 1, -.5f));
drag.reset(); // queued events cannot leak into a later grab
assert(drag.begin(PanelDragKind::Grab, angled_panel, 1, 1, .5f, .5f, down));
drag.update_depth({}, .05); near(float(drag.depth()), 0);
assert(drag.begin(PanelDragKind::Scale, identity, 1, 1, .5f, .5f, down));
assert(!drag.queue_scroll_depth(1, 1, 1.f));
drag.update_depth({}, .05); near(float(drag.depth()), 0);
drag.reset();
assert(!drag.queue_scroll_depth(1, 1, 1.f));
grab_rotation_case(pitch(), Matrix34{{{{1.f, 0.f, 0.f, .25f}}, grab_rotation_case(pitch(), Matrix34{{{{1.f, 0.f, 0.f, .25f}},
{{0.f, 0.f, -1.f, 1.3f}}, {{0.f, 0.f, -1.f, 1.3f}},
{{0.f, 1.f, 0.f, 1.4f}}}}); {{0.f, 1.f, 0.f, 1.4f}}}});