From 3460e610d199a2710fe3835a1019fae779db115d Mon Sep 17 00:00:00 2001 From: DeeJanuz <45082401+DeeJanuz@users.noreply.github.com> Date: Fri, 2 Oct 2026 11:37:24 -0600 Subject: [PATCH] Pointer helper: runtime state in a Pointer struct Structure only, no behaviour change. main()'s runtime locals become members of Pointer, declared in the same order with the same initial values and comments. The local types (Gaze, Src, Hold, DevicePress, KeyPress) move in with them, and Clock moves to file scope. Pointer::Init does the setup, in the original order: settings, VR_Init, the overlays, the laser width, the sockets, the controller buttons' manifest, vrhello, the overlay list. Members that were set during setup (sys, overlay, the three overlay handles, the sockets, systemPointer) are set there at the same point, and so are the timestamps that started at "now" (lastVisible, lastDebug, followAt, lastSlow). The lambdas and the main loop sit unchanged in Pointer::Run for now; the next commits split them up. applyConfig becomes Pointer::ApplyConfig, since Init and the "reload" command both call it. Co-Authored-By: Claude Opus 5.5 --- pointer/helper/ft-pointer.cpp | 191 ++++++++++++++++++++-------------- 1 file changed, 110 insertions(+), 81 deletions(-) diff --git a/pointer/helper/ft-pointer.cpp b/pointer/helper/ft-pointer.cpp index 2725f99..1948f1f 100644 --- a/pointer/helper/ft-pointer.cpp +++ b/pointer/helper/ft-pointer.cpp @@ -655,87 +655,37 @@ bool OnSettingsPage(const vr::HmdMatrix34_t &t, Vec3 eye, Vec3 d) { // Placement speeds (see "Placement" at the top). constexpr double kPlaceDegPerSec = 60; // tested: 40 deg/s is applied exactly -int main() { +using Clock = std::chrono::steady_clock; + +// Everything the pointer keeps: its settings, what Init sets up, and the state the main loop +// carries from one frame to the next. +struct Pointer { PointerConfig cfg; // Head follow (see the top). followConf is POINTER_FOLLOW as last read: a reload only // overrides a "follow" command when the setting itself changed. bool follow = false, followConf = false, followReset = true; // Gaze mode (see the top); gazeConf is POINTER_GAZE as last read, like followConf. bool gazeOn = false, gazeConf = false; - // After cfg.Load: a changed POINTER_FOLLOW or POINTER_GAZE turns head follow or gaze mode on or off. - auto applyConfig = [&] { - if (cfg.follow != followConf) follow = followConf = cfg.follow, followReset = true; - if (cfg.gaze != gazeConf) gazeOn = gazeConf = cfg.gaze; - }; - cfg.Load(ReadConfig()); - applyConfig(); - const float laserWidth = float(ConfDouble(ReadConfig(), "POINTER_LASER_WIDTH", 0.8)); - - vr::EVRInitError err = vr::VRInitError_None; - while (true) { - vr::VR_Init(&err, vr::VRApplication_Background); - if (err == vr::VRInitError_None) { - vr::VR_Shutdown(); - vr::VR_Init(&err, vr::VRApplication_Overlay); - } - if (err == vr::VRInitError_None) break; - std::fprintf(stderr, "waiting for SteamVR: %s\n", vr::VR_GetVRInitErrorAsEnglishDescription(err)); - std::this_thread::sleep_for(std::chrono::seconds(2)); - } - auto *sys = vr::VRSystem(); - auto *overlay = vr::VROverlay(); - + // Set in Init, after VR_Init. + vr::IVRSystem *sys = nullptr; + vr::IVROverlay *overlay = nullptr; + // The dot, the marker, and laser mode's overlay, created in Init. vr::VROverlayHandle_t cursor = vr::k_ulOverlayHandleInvalid; - overlay->CreateOverlay("frametop.pointer.cursor", "Frametop pointer", &cursor); - const int texSize = 64; - auto tex = DotTexture(texSize); - overlay->SetOverlayRaw(cursor, tex.data(), texSize, texSize, 4); - overlay->SetOverlayInputMethod(cursor, vr::VROverlayInputMethod_Mouse); // the laser can land on it - overlay->SetOverlaySortOrder(cursor, 200); - // Same dot, not interactive, drawn on panels at the hit point; the laser passes through. vr::VROverlayHandle_t marker = vr::k_ulOverlayHandleInvalid; - overlay->CreateOverlay("frametop.pointer.marker", "Frametop pointer marker", &marker); - overlay->SetOverlayRaw(marker, tex.data(), texSize, texSize, 4); - overlay->SetOverlayInputMethod(marker, vr::VROverlayInputMethod_None); - overlay->SetOverlaySortOrder(marker, 201); - // Laser mode (see the top of the file). vr::VROverlayHandle_t laserMode = vr::k_ulOverlayHandleInvalid; - overlay->CreateOverlay("frametop.pointer.lasermode", "Frametop pointer laser mode", &laserMode); - std::vector clear(4 * 4 * 4, 0); - overlay->SetOverlayRaw(laserMode, clear.data(), 4, 4, 4); - overlay->SetOverlayWidthInMeters(laserMode, 0.001f); - overlay->SetOverlayInputMethod(laserMode, vr::VROverlayInputMethod_Mouse); // the flag needs an input method - overlay->SetOverlayFlag(laserMode, vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible, true); - vr::HmdMatrix34_t below{}; - below.m[0][0] = below.m[1][1] = below.m[2][2] = 1; - below.m[1][3] = -50; - overlay->SetOverlayTransformTrackedDeviceRelative(laserMode, vr::k_unTrackedDeviceIndex_Hmd, &below); bool laserModeShown = false; - // Controller beams keep the user's width; nothing here changes it any more. - vr::VRSettings()->SetFloat("dashboard", "laserRayWidthScale", laserWidth); - - const int in = AbstractSocket("ft_pointer_helper", true); - const int out = AbstractSocket(nullptr, false); - // Frame controller buttons (vrbuttons.h). The build puts the binary in pointer/helper/build. - ControllerButtons controllerButtons; - { - const std::string manifest = ExeDir() + "/../actions/ft_pointer_actions.json"; - char real[PATH_MAX]; - controllerButtons.Init(realpath(manifest.c_str(), real) ? real : manifest); - } - SendTo(out, "frametop_relay", "vrhello"); // the relay answers with the mapped buttons + int in = -1, out = -1; // @ft_pointer_helper, and the socket we send from (opened in Init) + ControllerButtons controllerButtons; // Frame controller buttons (vrbuttons.h) OverlayList overlays; - overlays.Start(); std::map handles; std::map sceneGraph; // no texture: plane test instead of ComputeOverlayIntersection std::map visible; // refreshed every 50 ms - auto lastVisible = std::chrono::steady_clock::now(); + Clock::time_point lastVisible{}; // set in Init // The plane of the last panel the cursor was on, and the last point on it (panel edges). Vec3 edgePoint, edgeNormal, edgeLast; std::string edgeKey; bool active = false, recenter = false, anchored = false; - using Clock = std::chrono::steady_clock; Clock::time_point lastMouse{}, claimAt{}, claimRelease{}, wokeAt{}, noWakeUntil{}; bool claimPending = false, claimHeld = false; // Last used wins: since when each controller has been moving (zero: it isn't). @@ -761,26 +711,15 @@ int main() { // The ft-screens panel the left button was pressed on, and where it was then (see the top). std::string pressKey; vr::HmdMatrix34_t pressPose{}; - auto lastDebug = Clock::now(); - vr::VROverlayHandle_t systemPointer = vr::k_ulOverlayHandleInvalid; - overlay->FindOverlay("system.pointer", &systemPointer); + Clock::time_point lastDebug{}; // set in Init + vr::VROverlayHandle_t systemPointer = vr::k_ulOverlayHandleInvalid; // found in Init Vec3 pivot, tiltOrigin, lastPoint, lastOrigin, lastAim{0, 0, -1}; Basis tiltBasis{}; bool headsetOff = false; // nobody is wearing the headset (see the main loop) - auto wake = [&](Clock::time_point t) { - if (t < noWakeUntil || headsetOff) return; - wokeAt = t; - active = true; - recenter = true; - SendTo(out, "ft_pointer", "role " + cfg.role); // POINTER_ROLE, before it takes it - SendTo(out, "ft_pointer", "show"); - claimPending = true; // take the laser without clicking, once SteamVR has bound the device - claimAt = t + std::chrono::milliseconds(300); - }; Vec3 anchor; double yaw = 0, pitch = 0; Vec3 followRef{0, 0, -1}; // head follow's reference direction (see the top) - auto followAt = std::chrono::steady_clock::now(); // its last update, for the easing + Clock::time_point followAt{}; // its last update, for the easing (set in Init) bool following = false; // past the leash: easing toward the head double followLag = 0; // radians the reference trails the head std::chrono::steady_clock::time_point leashOutSince{}; // head past the leash since (delay) @@ -852,6 +791,94 @@ int main() { // came up, so a press in progress ends without a click. Clock::time_point calPanelUntil{}; bool calOpened = false; + Clock::time_point lastSlow{}; // set in Init + + void ApplyConfig(); + void Init(); + int Run(); +}; + +// After cfg.Load: a changed POINTER_FOLLOW or POINTER_GAZE turns head follow or gaze mode on or off. +void Pointer::ApplyConfig() { + if (cfg.follow != followConf) follow = followConf = cfg.follow, followReset = true; + if (cfg.gaze != gazeConf) gazeOn = gazeConf = cfg.gaze; +} + +void Pointer::Init() { + cfg.Load(ReadConfig()); + ApplyConfig(); + const float laserWidth = float(ConfDouble(ReadConfig(), "POINTER_LASER_WIDTH", 0.8)); + + vr::EVRInitError err = vr::VRInitError_None; + while (true) { + vr::VR_Init(&err, vr::VRApplication_Background); + if (err == vr::VRInitError_None) { + vr::VR_Shutdown(); + vr::VR_Init(&err, vr::VRApplication_Overlay); + } + if (err == vr::VRInitError_None) break; + std::fprintf(stderr, "waiting for SteamVR: %s\n", vr::VR_GetVRInitErrorAsEnglishDescription(err)); + std::this_thread::sleep_for(std::chrono::seconds(2)); + } + sys = vr::VRSystem(); + overlay = vr::VROverlay(); + + overlay->CreateOverlay("frametop.pointer.cursor", "Frametop pointer", &cursor); + const int texSize = 64; + auto tex = DotTexture(texSize); + overlay->SetOverlayRaw(cursor, tex.data(), texSize, texSize, 4); + overlay->SetOverlayInputMethod(cursor, vr::VROverlayInputMethod_Mouse); // the laser can land on it + overlay->SetOverlaySortOrder(cursor, 200); + // Same dot, not interactive, drawn on panels at the hit point; the laser passes through. + overlay->CreateOverlay("frametop.pointer.marker", "Frametop pointer marker", &marker); + overlay->SetOverlayRaw(marker, tex.data(), texSize, texSize, 4); + overlay->SetOverlayInputMethod(marker, vr::VROverlayInputMethod_None); + overlay->SetOverlaySortOrder(marker, 201); + // Laser mode (see the top of the file). + overlay->CreateOverlay("frametop.pointer.lasermode", "Frametop pointer laser mode", &laserMode); + std::vector clear(4 * 4 * 4, 0); + overlay->SetOverlayRaw(laserMode, clear.data(), 4, 4, 4); + overlay->SetOverlayWidthInMeters(laserMode, 0.001f); + overlay->SetOverlayInputMethod(laserMode, vr::VROverlayInputMethod_Mouse); // the flag needs an input method + overlay->SetOverlayFlag(laserMode, vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible, true); + vr::HmdMatrix34_t below{}; + below.m[0][0] = below.m[1][1] = below.m[2][2] = 1; + below.m[1][3] = -50; + overlay->SetOverlayTransformTrackedDeviceRelative(laserMode, vr::k_unTrackedDeviceIndex_Hmd, &below); + // Controller beams keep the user's width; nothing here changes it any more. + vr::VRSettings()->SetFloat("dashboard", "laserRayWidthScale", laserWidth); + + in = AbstractSocket("ft_pointer_helper", true); + out = AbstractSocket(nullptr, false); + // Frame controller buttons (vrbuttons.h). The build puts the binary in pointer/helper/build. + { + const std::string manifest = ExeDir() + "/../actions/ft_pointer_actions.json"; + char real[PATH_MAX]; + controllerButtons.Init(realpath(manifest.c_str(), real) ? real : manifest); + } + SendTo(out, "frametop_relay", "vrhello"); // the relay answers with the mapped buttons + overlays.Start(); + lastVisible = std::chrono::steady_clock::now(); + lastDebug = Clock::now(); + overlay->FindOverlay("system.pointer", &systemPointer); + followAt = std::chrono::steady_clock::now(); + lastSlow = std::chrono::steady_clock::now() - std::chrono::seconds(10); + + std::printf("ft-pointer running: free distance %.2f m, dot %.2f deg\n", cfg.freeDistance, cfg.cursorDeg); + std::fflush(stdout); +} + +int Pointer::Run() { + auto wake = [&](Clock::time_point t) { + if (t < noWakeUntil || headsetOff) return; + wokeAt = t; + active = true; + recenter = true; + SendTo(out, "ft_pointer", "role " + cfg.role); // POINTER_ROLE, before it takes it + SendTo(out, "ft_pointer", "show"); + claimPending = true; // take the laser without clicking, once SteamVR has bound the device + claimAt = t + std::chrono::milliseconds(300); + }; auto pressLeft = [&] { // ft-screens sends the keyboard to the panel clicked last; it sees clicks on // its own screens, but only we know when one lands on another panel. @@ -996,7 +1023,6 @@ int main() { return gazeOn && cfg.gazeMouseHeld && !inGame && !headsetOff && Clock::now() - gz.at < std::chrono::seconds(1) && !aimHeld && !leftHeld && !tilting && hold.src == Src::None && !clickPress && !clickRelease; }; - auto lastSlow = std::chrono::steady_clock::now() - std::chrono::seconds(10); // --- Panel placement (see "Placement" at the top of the file) --- // Device pose, given in the standing universe, sent to the driver in raw space. @@ -1151,9 +1177,6 @@ int main() { return msg; }; - std::printf("ft-pointer running: free distance %.2f m, dot %.2f deg\n", cfg.freeDistance, cfg.cursorDeg); - std::fflush(stdout); - while (true) { // The headset off: SteamVR drops the HMD's activity to idle as soon as it comes off. // An awake pointer (a connected controller, SteamVR's laser mode forced on) kept the @@ -1384,7 +1407,7 @@ int main() { recenter = true; } else if (std::strncmp(buf, "reload", 6) == 0) { cfg.Load(ReadConfig()); - applyConfig(); + ApplyConfig(); lastSlow = Clock::now() - std::chrono::seconds(10); // apply POINTER_IGNORE now std::printf("reloaded: free distance %.2f m, dot %.2f deg, origin %.2f, head follow %s, leash %.0f deg, " "controller pickup %.1fx, %zu ignored\n", @@ -2183,3 +2206,9 @@ int main() { std::this_thread::sleep_for(std::chrono::milliseconds(8)); } } + +int main() { + Pointer p; + p.Init(); + return p.Run(); +}