diff --git a/docs/overlay.md b/docs/overlay.md index 7d67537..701604c 100644 --- a/docs/overlay.md +++ b/docs/overlay.md @@ -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 provide left/right vector2 actions in a separate `/actions/grab` set, including in generated custom button manifests. Only a grab activates this set, restricted -to the grabbing hand at overlay priority; it requires SteamVR Experimental -overlay input overrides. It does not promote recording or text actions, and does +to the grabbing hand at overlay priority; that app-action route requires SteamVR Experimental +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 -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 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 diff --git a/install.sh b/install.sh index c789cca..fa577a4 100755 --- a/install.sh +++ b/install.sh @@ -45,7 +45,7 @@ import urllib.request KEY = "local.frameyap.overlay" # The pinned release this installer belongs to (set in the release commit that # 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" ARCHIVE_LIMIT = 12 * 1024**3 MEMBER_LIMIT = 50000 diff --git a/scripts/install_payload.py b/scripts/install_payload.py index 4d11bf7..e4b12ca 100644 --- a/scripts/install_payload.py +++ b/scripts/install_payload.py @@ -25,7 +25,7 @@ import urllib.request KEY = "local.frameyap.overlay" # The pinned release this installer belongs to (set in the release commit that # 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" ARCHIVE_LIMIT = 12 * 1024**3 MEMBER_LIMIT = 50000 diff --git a/src/overlay.cpp b/src/overlay.cpp index 1e0433a..44a46c0 100644 --- a/src/overlay.cpp +++ b/src/overlay.cpp @@ -145,6 +145,8 @@ struct Overlay::Impl { bool enabled = [] { const char* flag = std::getenv("FRAMEYAP_PROFILE"); 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 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; static double elapsed(std::chrono::steady_clock::time_point start) { return std::chrono::duration(std::chrono::steady_clock::now() - start).count(); @@ -155,7 +157,10 @@ struct Overlay::Impl { << " uploads=" << uploads << " upload_ms=" << upload_ms << " motion_ticks=" << motion_ticks << " motion_ms=" << motion_ms << " 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; @@ -233,9 +238,9 @@ struct Overlay::Impl { persistence.laser_change_failed = true; } overlay_check(overlay->SetOverlayFlag(handle, vr::VROverlayFlags_SendVRDiscreteScrollEvents, true), overlay, "Laser scroll"); - // Smooth events are optional on some runtimes; discrete wheel events - // keep Settings usable if the compositor declines the smooth mode. - overlay->SetOverlayFlag(handle, vr::VROverlayFlags_SendVRSmoothScrollEvents, true); + // Normal UI scrolling uses discrete events. Request the compositor + // smooth stream only while grabbing for the depth fallback. + overlay->SetOverlayFlag(handle, vr::VROverlayFlags_SendVRSmoothScrollEvents, false); surface.set_lasers_anytime(lasers_anytime); surface.set_wrist_world_fallback(config.wrist_world_fallback); surface.set_advanced_debug(config.advanced_debug); @@ -422,6 +427,8 @@ struct Overlay::Impl { } dragging.scale = size_scale; dragging.canvas = canvas_pose; 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{}; dragging.trigger_observed = system->GetControllerState(dragging.device, &state, sizeof(state)) && (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"); } void finish_drag(bool released) { + overlay->SetOverlayFlag(handle, vr::VROverlayFlags_SendVRSmoothScrollEvents, false); if (!released && applied_mount && *applied_mount == mount) { canvas_pose = dragging.canvas; size_scale = dragging.scale; @@ -477,8 +485,16 @@ struct Overlay::Impl { const auto now = std::chrono::steady_clock::now(); const double dt = std::chrono::duration(now - dragging.last_depth_sample).count(); dragging.last_depth_sample = now; - if (dragging.kind == PanelDragKind::Grab && diagnostics.action_update_error == vr::VRInputError_None) - if (auto axis = depth_axis()) dragging.panel.step_depth(*axis, dt); + if (dragging.kind == PanelDragKind::Grab) { + 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); if (!change) { finish_drag(false); return; } 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; 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() && cursor_positions[event.data.scroll.cursorIndex]) { const auto [x, y] = *cursor_positions[event.data.scroll.cursorIndex]; diff --git a/src/panel_drag.hpp b/src/panel_drag.hpp index 1a1f390..4745b8c 100644 --- a/src/panel_drag.hpp +++ b/src/panel_drag.hpp @@ -4,6 +4,9 @@ #include #include +#include +#include +#include #include namespace frameyap { @@ -140,6 +143,23 @@ public: 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::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 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_; } std::optional update(const Matrix34& controller_pose) const { @@ -167,7 +187,7 @@ public: return PanelDragUpdate{panel_pose_, static_cast(factor)}; } - void reset() { active_ = false; depth_ = 0; } + void reset() { active_ = false; depth_ = pending_scroll_ = 0; } bool active() const { return active_; } private: @@ -177,7 +197,7 @@ private: Matrix34 panel_pose_{}, relative_panel_{}; Vec3 center_{}, normal_{}, anchor_{}, down_hit_{}, local_ray_{}; double baseline_sq_ = 0; - double depth_ = 0; + double depth_ = 0, pending_scroll_ = 0; }; } // namespace frameyap diff --git a/tests/panel_drag_test.cpp b/tests/panel_drag_test.cpp index 9ac4280..cb3d8f0 100644 --- a/tests/panel_drag_test.cpp +++ b/tests/panel_drag_test.cpp @@ -146,6 +146,36 @@ int main() { } 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::max(), 1, 1.f)); + assert(!drag.queue_scroll_depth(1, 1, std::numeric_limits::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}}, {{0.f, 0.f, -1.f, 1.3f}}, {{0.f, 1.f, 0.f, 1.4f}}}});