mirror of
https://github.com/baketnk/frame-yap.git
synced 2026-10-06 03:00:04 +02:00
Add opt-in experimental overlay input priority
This commit is contained in:
1 parent
46b3c37a8a
commit
69f5de5a09
14 files changed
+131
-49
No files matched your search
@@ -117,6 +117,7 @@ if(BUILD_TESTING AND NOT CMAKE_CROSSCOMPILING)
|
|||||||
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||||
add_executable(frameyap_config_test tests/config_test.cpp)
|
add_executable(frameyap_config_test tests/config_test.cpp)
|
||||||
target_link_libraries(frameyap_config_test PRIVATE frameyap_config)
|
target_link_libraries(frameyap_config_test PRIVATE frameyap_config)
|
||||||
|
target_compile_options(frameyap_config_test PRIVATE -UNDEBUG)
|
||||||
add_test(NAME frameyap.config COMMAND frameyap_config_test "${CMAKE_CURRENT_SOURCE_DIR}/assets")
|
add_test(NAME frameyap.config COMMAND frameyap_config_test "${CMAKE_CURRENT_SOURCE_DIR}/assets")
|
||||||
add_executable(frameyap_mount_test tests/mount_test.cpp)
|
add_executable(frameyap_mount_test tests/mount_test.cpp)
|
||||||
target_link_libraries(frameyap_mount_test PRIVATE frameyap_mount)
|
target_link_libraries(frameyap_mount_test PRIVATE frameyap_mount)
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ See [third-party notes](docs/third-party.md). No GitHub release is published yet
|
|||||||
panel colors, a font path and Frame controller button mappings; missing fonts
|
panel colors, a font path and Frame controller button mappings; missing fonts
|
||||||
fall back to bundled Inconsolata. The installer creates/checks this file and
|
fall back to bundled Inconsolata. The installer creates/checks this file and
|
||||||
backs it up before repairs. See [overlay configuration](docs/overlay.md#user-theme-and-controller-configuration).
|
backs it up before repairs. See [overlay configuration](docs/overlay.md#user-theme-and-controller-configuration).
|
||||||
|
- **Experimental input priority:** set `"input_priority": "experimental"` in
|
||||||
|
that config and enable SteamVR's Developer option **Enable global input from
|
||||||
|
overlays**. FrameYap then requests priority for its bound controller sources.
|
||||||
|
This may consume controls used by games or the dashboard; coexistence on Frame
|
||||||
|
is under test. The default is `"normal"`; restart FrameYap after changing it.
|
||||||
- **Review-first:** focus your destination, then press Insert. No automatic insertion
|
- **Review-first:** focus your destination, then press Insert. No automatic insertion
|
||||||
or submission. Maximum clip 20 seconds; accidental taps under 200 ms are discarded.
|
or submission. Maximum clip 20 seconds; accidental taps under 200 ms are discarded.
|
||||||
- While the native app is Ready, it keeps the mic device open and discards idle
|
- While the native app is Ready, it keeps the mic device open and discards idle
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"font": "",
|
"font": "",
|
||||||
|
"input_priority": "normal",
|
||||||
"theme": {
|
"theme": {
|
||||||
"background": "#0c101b",
|
"background": "#0c101b",
|
||||||
"card": "#141c2b",
|
"card": "#141c2b",
|
||||||
|
|||||||
+5
-3
@@ -112,9 +112,11 @@ silently takes a game's button away. Check action activity and neutral rearm.
|
|||||||
|
|
||||||
OpenVR 2.15.6 documents experimental overlay action-set priorities
|
OpenVR 2.15.6 documents experimental overlay action-set priorities
|
||||||
`0x01000000..0x01FFFFFF`, gated by SteamVR's **Experimental overlay input overrides**
|
`0x01000000..0x01FFFFFF`, gated by SteamVR's **Experimental overlay input overrides**
|
||||||
setting. This can selectively override scene input, but is not guaranteed enabled
|
setting. FrameYap now requests the minimum experimental priority when its config
|
||||||
or usable on this Frame. Do not toggle it automatically. Begin with overlay mic
|
has `"input_priority": "experimental"`; the default is `"normal"` (priority zero).
|
||||||
controls; validate a global PTT binding separately with a scene active, dashboard
|
This can selectively override scene input, but delivered input and dashboard
|
||||||
|
coexistence still need testing on Frame. FrameYap only reads the SteamVR permission
|
||||||
|
setting and never toggles it automatically. Validate bindings with a scene active, dashboard
|
||||||
open/closed, lost tracking, and reconnection. Overlay interactivity/input ownership
|
open/closed, lost tracking, and reconnection. Overlay interactivity/input ownership
|
||||||
is distinct from OS keyboard focus.
|
is distinct from OS keyboard focus.
|
||||||
|
|
||||||
|
|||||||
@@ -51,3 +51,11 @@ not establish SteamVR texture acceptance, orientation, click behavior or a
|
|||||||
flicker fix. No microphone, inference or text/Enter delivery was exercised.
|
flicker fix. No microphone, inference or text/Enter delivery was exercised.
|
||||||
The next human headset check should compare both static action clicks and
|
The next human headset check should compare both static action clicks and
|
||||||
content-changing tabs using the installed Vulkan build's `--check-controls`.
|
content-changing tabs using the installed Vulkan build's `--check-controls`.
|
||||||
|
|
||||||
|
## Subsequent wearer report — 2026-09-24
|
||||||
|
|
||||||
|
After testing the installed Vulkan build, the user confirmed that the flicker
|
||||||
|
is fixed. They separately reported controller shortcuts becoming unavailable
|
||||||
|
in system laser/dashboard interaction states while pointer clicks work. This
|
||||||
|
is wearer confirmation of the visual fix and a distinct input-routing issue;
|
||||||
|
it does not establish microphone, transcription or text-delivery acceptance.
|
||||||
+38
-6
@@ -73,6 +73,7 @@ installer creates one with defaults on first install. Copy the shipped
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"font": "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
"font": "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||||
|
"input_priority": "normal",
|
||||||
"theme": {
|
"theme": {
|
||||||
"background": "#0c101b", "card": "#141c2b", "ink": "#e6f0f9",
|
"background": "#0c101b", "card": "#141c2b", "ink": "#e6f0f9",
|
||||||
"muted": "#97adc1", "accent": "#1ff0a4", "warning": "#ff6e87",
|
"muted": "#97adc1", "accent": "#1ff0a4", "warning": "#ff6e87",
|
||||||
@@ -101,8 +102,36 @@ the exact prior bytes under `config.json.backup-*` before a repair and refuses
|
|||||||
symlink/oversized config paths; valid customizations remain intact. The installed
|
symlink/oversized config paths; valid customizations remain intact. The installed
|
||||||
launcher no longer pins `--font`, so this selection takes effect. Direct native
|
launcher no longer pins `--font`, so this selection takes effect. Direct native
|
||||||
launches with bad JSON, colors or button mappings fail startup rather than
|
launches with bad JSON, colors or button mappings fail startup rather than
|
||||||
silently changing input behavior. This does not enable experimental SteamVR
|
silently changing input behavior.
|
||||||
action overrides or prove delivery in games.
|
|
||||||
|
### Experimental controller input priority
|
||||||
|
|
||||||
|
There are two independent gates:
|
||||||
|
|
||||||
|
1. SteamVR's Developer setting **Enable global input from overlays** (called
|
||||||
|
**Experimental overlay input overrides** in the SDK documentation) permits
|
||||||
|
global action priority. FrameYap reads it and never changes it.
|
||||||
|
2. FrameYap config `"input_priority": "experimental"` requests
|
||||||
|
`k_nActionSetOverlayGlobalPriorityMin` (`0x01000000`) for its existing action
|
||||||
|
set. `"normal"` or an omitted field requests priority zero. Restart FrameYap
|
||||||
|
after changing the config. Invalid values fail native startup; the installer
|
||||||
|
repairs them to normal and backs up the prior bytes.
|
||||||
|
|
||||||
|
This applies to all controller sources bound to FrameYap actions, including
|
||||||
|
custom SteamVR bindings. It does not replace the action manifest or change the
|
||||||
|
physical button mappings. Bound sources can take input away from games or the
|
||||||
|
dashboard. The request stays the same with the dashboard open/closed and Lasers
|
||||||
|
anytime on/off so the experiment can compare those modes. Successful API calls
|
||||||
|
do not prove that controller actions arrive or that dashboard interaction works.
|
||||||
|
|
||||||
|
Native startup logs the requested priority and SteamVR permission separately.
|
||||||
|
The no-audio/no-delivery `--check-controls` probe also logs dashboard state, the
|
||||||
|
Lasers anytime flag, `IsInputAvailable`, panel visibility/focus, and activity,
|
||||||
|
press state and pose/role acceptance for all six actions. The laser flag is our
|
||||||
|
request, not a detector for every system laser. Compare the same bindings in
|
||||||
|
each dashboard/laser state; check pointer clicks and press/release through mode
|
||||||
|
transitions too. Return `input_priority` to `normal` and relaunch to end the
|
||||||
|
FrameYap experiment.
|
||||||
|
|
||||||
### Placement settings
|
### Placement settings
|
||||||
|
|
||||||
@@ -139,7 +168,10 @@ missing, symlinked or invalid files mean off. A failed save keeps the new
|
|||||||
choice only for the running session and shows a warning. To turn it back on
|
choice only for the running session and shows a warning. To turn it back on
|
||||||
after disabling it with the dashboard closed, open the dashboard to use its
|
after disabling it with the dashboard closed, open the dashboard to use its
|
||||||
laser on the Settings button. Controls-only checks can toggle it temporarily
|
laser on the Settings button. Controls-only checks can toggle it temporarily
|
||||||
but do not save the preference. This has not yet been accepted in a headset.
|
but do not save the preference. The wearer reports that pointer clicks work,
|
||||||
|
while normal-priority controller actions become unavailable in system laser
|
||||||
|
mode, including when this preference is enabled. Experimental-priority
|
||||||
|
coexistence remains unverified.
|
||||||
|
|
||||||
### Hardware-free UI checks
|
### Hardware-free UI checks
|
||||||
|
|
||||||
@@ -199,9 +231,9 @@ action explicitly begins on down and ends on up. On tracking-pose invalidity,
|
|||||||
action inactivity or overlay focus loss, a held capture emits Cancel, and
|
action inactivity or overlay focus loss, a held capture emits Cancel, and
|
||||||
reconnection requires a neutral observation before any new press. PTT and left
|
reconnection requires a neutral observation before any new press. PTT and left
|
||||||
Enter require an enabled panel; clickable Record remains available for retry
|
Enter require an enabled panel; clickable Record remains available for retry
|
||||||
after an error and Cancel is always available. The action set has normal priority: global input
|
after an error and Cancel is always available. The action set defaults to normal
|
||||||
while a scene is active is not guaranteed, and experimental overlay overrides
|
priority; the experimental config request is described above. Neither priority
|
||||||
are not switched on automatically.
|
guarantees delivery while a game or dashboard owns input.
|
||||||
|
|
||||||
The UI cannot itself guarantee a capture started when a BeginRecord action
|
The UI cannot itself guarantee a capture started when a BeginRecord action
|
||||||
arrives: the owning runtime checks worker readiness. `Cancel` invalidates
|
arrives: the owning runtime checks worker readiness. `Cancel` invalidates
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ VERSION_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,95}\Z")
|
|||||||
DIGEST_RE = re.compile(r"[a-fA-F0-9]{64}\Z")
|
DIGEST_RE = re.compile(r"[a-fA-F0-9]{64}\Z")
|
||||||
CONFIG_DEFAULTS = {
|
CONFIG_DEFAULTS = {
|
||||||
"font": "",
|
"font": "",
|
||||||
|
"input_priority": "normal",
|
||||||
"theme": {"background": "#0c101b", "card": "#141c2b", "ink": "#e6f0f9",
|
"theme": {"background": "#0c101b", "card": "#141c2b", "ink": "#e6f0f9",
|
||||||
"muted": "#97adc1", "accent": "#1ff0a4", "warning": "#ff6e87",
|
"muted": "#97adc1", "accent": "#1ff0a4", "warning": "#ff6e87",
|
||||||
"frame_start": "#1fff91", "frame_end": "#1f70ff"},
|
"frame_start": "#1fff91", "frame_end": "#1f70ff"},
|
||||||
@@ -87,6 +88,8 @@ def normalized_config(data):
|
|||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
data = {}
|
data = {}
|
||||||
fixed = {"font": data.get("font") if isinstance(data.get("font"), str) else ""}
|
fixed = {"font": data.get("font") if isinstance(data.get("font"), str) else ""}
|
||||||
|
priority = data.get("input_priority", "normal")
|
||||||
|
fixed["input_priority"] = priority if priority in ("normal", "experimental") else "normal"
|
||||||
for section in ("theme", "buttons"):
|
for section in ("theme", "buttons"):
|
||||||
source = data.get(section)
|
source = data.get(section)
|
||||||
source = source if isinstance(source, dict) else {}
|
source = source if isinstance(source, dict) else {}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ VERSION_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,95}\Z")
|
|||||||
DIGEST_RE = re.compile(r"[a-fA-F0-9]{64}\Z")
|
DIGEST_RE = re.compile(r"[a-fA-F0-9]{64}\Z")
|
||||||
CONFIG_DEFAULTS = {
|
CONFIG_DEFAULTS = {
|
||||||
"font": "",
|
"font": "",
|
||||||
|
"input_priority": "normal",
|
||||||
"theme": {"background": "#0c101b", "card": "#141c2b", "ink": "#e6f0f9",
|
"theme": {"background": "#0c101b", "card": "#141c2b", "ink": "#e6f0f9",
|
||||||
"muted": "#97adc1", "accent": "#1ff0a4", "warning": "#ff6e87",
|
"muted": "#97adc1", "accent": "#1ff0a4", "warning": "#ff6e87",
|
||||||
"frame_start": "#1fff91", "frame_end": "#1f70ff"},
|
"frame_start": "#1fff91", "frame_end": "#1f70ff"},
|
||||||
@@ -76,6 +77,8 @@ def normalized_config(data):
|
|||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
data = {}
|
data = {}
|
||||||
fixed = {"font": data.get("font") if isinstance(data.get("font"), str) else ""}
|
fixed = {"font": data.get("font") if isinstance(data.get("font"), str) else ""}
|
||||||
|
priority = data.get("input_priority", "normal")
|
||||||
|
fixed["input_priority"] = priority if priority in ("normal", "experimental") else "normal"
|
||||||
for section in ("theme", "buttons"):
|
for section in ("theme", "buttons"):
|
||||||
source = data.get(section)
|
source = data.get(section)
|
||||||
source = source if isinstance(source, dict) else {}
|
source = source if isinstance(source, dict) else {}
|
||||||
|
|||||||
@@ -173,6 +173,10 @@ Config load_config(const std::filesystem::path& path) {
|
|||||||
if (key == "font") {
|
if (key == "font") {
|
||||||
if (!value.is_string) throw std::runtime_error("Config font must be a path string");
|
if (!value.is_string) throw std::runtime_error("Config font must be a path string");
|
||||||
config.font = value.value;
|
config.font = value.value;
|
||||||
|
} else if (key == "input_priority") {
|
||||||
|
if (!value.is_string || (value.value != "normal" && value.value != "experimental"))
|
||||||
|
throw std::runtime_error("Config input_priority must be normal or experimental");
|
||||||
|
config.experimental_input_priority = value.value == "experimental";
|
||||||
} else if (key == "theme") {
|
} else if (key == "theme") {
|
||||||
if (!value.is_object) throw std::runtime_error("Config theme must be an object");
|
if (!value.is_object) throw std::runtime_error("Config theme must be an object");
|
||||||
for (const auto& [name, v] : value.object) {
|
for (const auto& [name, v] : value.object) {
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ struct Theme {
|
|||||||
struct Config {
|
struct Config {
|
||||||
Theme theme;
|
Theme theme;
|
||||||
std::string font; // absolute TTF/OTF path; empty uses the bundled face
|
std::string font; // absolute TTF/OTF path; empty uses the bundled face
|
||||||
|
// Requests OpenVR's experimental global action priority; SteamVR must allow it too.
|
||||||
|
bool experimental_input_priority = false;
|
||||||
// OpenVR action name -> physical Frame controller input path; empty disables it.
|
// OpenVR action name -> physical Frame controller input path; empty disables it.
|
||||||
std::map<std::string, std::string> buttons;
|
std::map<std::string, std::string> buttons;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ void help() {
|
|||||||
"Theme, font and Frame button mappings: $XDG_CONFIG_HOME/frameyap/config.json\n"
|
"Theme, font and Frame button mappings: $XDG_CONFIG_HOME/frameyap/config.json\n"
|
||||||
"(or ~/.config/frameyap/config.json). CLI --font overrides config; missing fonts\n"
|
"(or ~/.config/frameyap/config.json). CLI --font overrides config; missing fonts\n"
|
||||||
"fall back to bundled Inconsolata, then a system DejaVu face.\n"
|
"fall back to bundled Inconsolata, then a system DejaVu face.\n"
|
||||||
|
"Config input_priority: normal (default) or experimental; also requires\n"
|
||||||
|
"SteamVR Developer setting Enable global input from overlays.\n"
|
||||||
"Right X: hold to speak, release to review (default Frame binding).\n"
|
"Right X: hold to speak, release to review (default Frame binding).\n"
|
||||||
"Grip gestures are remappable but may be unavailable in the dashboard.\n"
|
"Grip gestures are remappable but may be unavailable in the dashboard.\n"
|
||||||
"Left grip: double-tap for explicit Enter when active.\n"
|
"Left grip: double-tap for explicit Enter when active.\n"
|
||||||
|
|||||||
+41
-39
@@ -34,6 +34,7 @@ void configure_registry() {
|
|||||||
::setenv("VR_PATHREG_OVERRIDE", registry.c_str(), 0);
|
::setenv("VR_PATHREG_OVERRIDE", registry.c_str(), 0);
|
||||||
}
|
}
|
||||||
constexpr int W = PanelSurface::width, H = PanelSurface::height;
|
constexpr int W = PanelSurface::width, H = PanelSurface::height;
|
||||||
|
constexpr std::array<const char*, 6> action_names{{"left_grip", "right_grip", "ptt", "cancel", "insert", "enter"}};
|
||||||
void overlay_check(vr::EVROverlayError err, vr::IVROverlay* api, const char* op) {
|
void overlay_check(vr::EVROverlayError err, vr::IVROverlay* api, const char* op) {
|
||||||
if (err != vr::VROverlayError_None)
|
if (err != vr::VROverlayError_None)
|
||||||
throw std::runtime_error(std::string(op) + ": " + api->GetOverlayErrorNameFromEnum(err));
|
throw std::runtime_error(std::string(op) + ": " + api->GetOverlayErrorNameFromEnum(err));
|
||||||
@@ -136,15 +137,13 @@ struct Overlay::Impl {
|
|||||||
throw std::runtime_error("Could not set OpenVR action manifest path");
|
throw std::runtime_error("Could not set OpenVR action manifest path");
|
||||||
if (input->GetActionSetHandle("/actions/frameyap", &action_set) != vr::VRInputError_None)
|
if (input->GetActionSetHandle("/actions/frameyap", &action_set) != vr::VRInputError_None)
|
||||||
throw std::runtime_error("Could not find FrameYap action set");
|
throw std::runtime_error("Could not find FrameYap action set");
|
||||||
constexpr std::array<const char*, 6> names{{"left_grip", "right_grip", "ptt", "cancel", "insert", "enter"}};
|
for (size_t i = 0; i < action_names.size(); ++i)
|
||||||
for (size_t i = 0; i < names.size(); ++i)
|
if (input->GetActionHandle((std::string("/actions/frameyap/in/") + action_names[i]).c_str(), &actions[i]) != vr::VRInputError_None)
|
||||||
if (input->GetActionHandle((std::string("/actions/frameyap/in/") + names[i]).c_str(), &actions[i]) != vr::VRInputError_None)
|
throw std::runtime_error(std::string("Could not find action ") + action_names[i]);
|
||||||
throw std::runtime_error(std::string("Could not find action ") + names[i]);
|
|
||||||
overlay_check(overlay->CreateOverlay("local.frameyap.overlay.panel", "FrameYap", &handle), overlay, "CreateOverlay");
|
overlay_check(overlay->CreateOverlay("local.frameyap.overlay.panel", "FrameYap", &handle), overlay, "CreateOverlay");
|
||||||
overlay_check(overlay->SetOverlayWidthInMeters(handle, 0.85f), overlay, "SetOverlayWidthInMeters");
|
overlay_check(overlay->SetOverlayWidthInMeters(handle, 0.85f), overlay, "SetOverlayWidthInMeters");
|
||||||
overlay_check(overlay->SetOverlayInputMethod(handle, vr::VROverlayInputMethod_Mouse), overlay, "SetOverlayInputMethod");
|
overlay_check(overlay->SetOverlayInputMethod(handle, vr::VROverlayInputMethod_Mouse), overlay, "SetOverlayInputMethod");
|
||||||
// Normal priority actions stay unchanged. This separate, explicit
|
// This separate preference requests system-wide laser mouse mode only while the
|
||||||
// preference requests system-wide laser mouse mode only while the
|
|
||||||
// panel is visible; it may affect interaction with a running game.
|
// panel is visible; it may affect interaction with a running game.
|
||||||
if (lasers_anytime && overlay->SetOverlayFlag(handle, vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible, true) != vr::VROverlayError_None) {
|
if (lasers_anytime && overlay->SetOverlayFlag(handle, vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible, true) != vr::VROverlayError_None) {
|
||||||
lasers_anytime = false;
|
lasers_anytime = false;
|
||||||
@@ -157,6 +156,7 @@ struct Overlay::Impl {
|
|||||||
system->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, 0, poses.data(), uint32_t(poses.size()));
|
system->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, 0, poses.data(), uint32_t(poses.size()));
|
||||||
place();
|
place();
|
||||||
draw(Panel{"Disabled", "", "Record to start local worker", false, false});
|
draw(Panel{"Disabled", "", "Record to start local worker", false, false});
|
||||||
|
std::cout << input_mode_status() << std::endl;
|
||||||
} catch (...) { cleanup(); throw; }
|
} catch (...) { cleanup(); throw; }
|
||||||
}
|
}
|
||||||
~Impl() { cleanup(); }
|
~Impl() { cleanup(); }
|
||||||
@@ -250,6 +250,28 @@ struct Overlay::Impl {
|
|||||||
for (size_t i = 1; i < edges.size(); ++i) edges[i].reset();
|
for (size_t i = 1; i < edges.size(); ++i) edges[i].reset();
|
||||||
surface.reset_pointers();
|
surface.reset_pointers();
|
||||||
}
|
}
|
||||||
|
int32_t action_priority() const {
|
||||||
|
return config.experimental_input_priority ? vr::k_nActionSetOverlayGlobalPriorityMin : 0;
|
||||||
|
}
|
||||||
|
std::string input_mode_status() const {
|
||||||
|
// Read the runtime permission separately: requesting a priority does not
|
||||||
|
// enable SteamVR's global setting or prove delivery of controller input.
|
||||||
|
vr::EVRSettingsError settings_error = vr::VRSettingsError_None;
|
||||||
|
auto* settings = vr::VRSettings();
|
||||||
|
const bool allowed = settings && settings->GetBool(vr::k_pch_SteamVR_Section,
|
||||||
|
vr::k_pch_SteamVR_AllowGlobalActionSetPriority, &settings_error);
|
||||||
|
const char* permission = !settings || settings_error != vr::VRSettingsError_None ? "unavailable" :
|
||||||
|
allowed ? "enabled" : "disabled";
|
||||||
|
bool laser_flag = false;
|
||||||
|
const auto laser_error = overlay->GetOverlayFlag(handle,
|
||||||
|
vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible, &laser_flag);
|
||||||
|
return std::string("Input priority-request=") + (config.experimental_input_priority ? "experimental" : "normal") +
|
||||||
|
" (" + std::to_string(action_priority()) + ") SteamVR-global-input=" + permission +
|
||||||
|
"\nMode dashboard=" + (overlay->IsDashboardVisible() ? "Y" : "N") +
|
||||||
|
" lasers-anytime=" + (laser_error == vr::VROverlayError_None ? (laser_flag ? "Y" : "N") : "n/a") +
|
||||||
|
" system-input-available=" + (system->IsInputAvailable() ? "Y" : "N") +
|
||||||
|
" panel-shown=" + (shown ? "Y" : "N") + " focus-gate=" + (focus ? "Y" : "N");
|
||||||
|
}
|
||||||
// Bound actions are accepted only with a connected tracked source. A held input
|
// Bound actions are accepted only with a connected tracked source. A held input
|
||||||
// following loss of activity must return to neutral before generating an edge.
|
// following loss of activity must return to neutral before generating an edge.
|
||||||
std::pair<bool, bool> digital(size_t index) {
|
std::pair<bool, bool> digital(size_t index) {
|
||||||
@@ -350,7 +372,10 @@ struct Overlay::Impl {
|
|||||||
vr::VRActiveActionSet_t set{};
|
vr::VRActiveActionSet_t set{};
|
||||||
set.ulActionSet = action_set;
|
set.ulActionSet = action_set;
|
||||||
set.ulRestrictedToDevice = vr::k_ulInvalidInputValueHandle;
|
set.ulRestrictedToDevice = vr::k_ulInvalidInputValueHandle;
|
||||||
set.nPriority = 0; // normal priority; no experimental scene-input overrides
|
// Explicit opt-in affects only sources bound to our existing action set.
|
||||||
|
// Keep requesting it across dashboard/laser states: those are what the
|
||||||
|
// experiment compares. SteamVR's separate permission gate is never changed here.
|
||||||
|
set.nPriority = action_priority();
|
||||||
action_update_error = input->UpdateActionState(&set, sizeof(set), 1);
|
action_update_error = input->UpdateActionState(&set, sizeof(set), 1);
|
||||||
if (action_update_error != vr::VRInputError_None) {
|
if (action_update_error != vr::VRInputError_None) {
|
||||||
reset_input(result); return result;
|
reset_input(result); return result;
|
||||||
@@ -392,42 +417,19 @@ Overlay::~Overlay() = default;
|
|||||||
std::vector<UiAction> Overlay::poll() { return impl_->poll(); }
|
std::vector<UiAction> Overlay::poll() { return impl_->poll(); }
|
||||||
void Overlay::draw(const Panel& panel) { impl_->draw(panel); }
|
void Overlay::draw(const Panel& panel) { impl_->draw(panel); }
|
||||||
std::string Overlay::controls_status() {
|
std::string Overlay::controls_status() {
|
||||||
// Diagnostic only: distinguish SteamVR binding/activity from our stricter
|
// Compare the same actions across modes, before and after our pose/role gate.
|
||||||
// pose/role gate. Raw legacy state is read-only and may be unavailable.
|
// IsInputAvailable and a successful UpdateActionState are not delivery proof.
|
||||||
bool laser_flag = false;
|
std::string result = impl_->input_mode_status() +
|
||||||
const auto laser_error = impl_->overlay->GetOverlayFlag(impl_->handle,
|
" update=" + std::to_string(int(impl_->action_update_error));
|
||||||
vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible, &laser_flag);
|
for (size_t i = 0; i < action_names.size(); ++i) {
|
||||||
std::string result = "SteamVR update=" + std::to_string(int(impl_->action_update_error)) +
|
|
||||||
" dashboard=" + (impl_->overlay->IsDashboardVisible() ? "Y" : "N") +
|
|
||||||
" lasers-anytime=" + (laser_error == vr::VROverlayError_None ? (laser_flag ? "Y" : "N") : "n/a");
|
|
||||||
for (size_t i = 0; i < 2; ++i) {
|
|
||||||
const auto role = i == 0 ? vr::TrackedControllerRole_LeftHand : vr::TrackedControllerRole_RightHand;
|
|
||||||
const auto device = impl_->system->GetTrackedDeviceIndexForControllerRole(role);
|
|
||||||
const bool tracked = device < impl_->poses.size() && impl_->poses[device].bPoseIsValid &&
|
|
||||||
impl_->system->IsTrackedDeviceConnected(device);
|
|
||||||
vr::InputDigitalActionData_t data{};
|
vr::InputDigitalActionData_t data{};
|
||||||
const auto error = impl_->input->GetDigitalActionData(impl_->actions[i], &data, sizeof(data), vr::k_ulInvalidInputValueHandle);
|
const auto error = impl_->input->GetDigitalActionData(impl_->actions[i], &data, sizeof(data), vr::k_ulInvalidInputValueHandle);
|
||||||
vr::InputOriginInfo_t origin{};
|
const auto [accepted, down] = impl_->digital(i);
|
||||||
const bool origin_ok = error == vr::VRInputError_None && data.activeOrigin != vr::k_ulInvalidInputValueHandle &&
|
result += std::string("\nAction ") + action_names[i] +
|
||||||
impl_->input->GetOriginTrackedDeviceInfo(data.activeOrigin, &origin, sizeof(origin)) == vr::VRInputError_None &&
|
|
||||||
origin.trackedDeviceIndex == device;
|
|
||||||
vr::VRControllerState_t raw{};
|
|
||||||
const bool raw_ok = device < impl_->poses.size() && impl_->system->GetControllerState(device, &raw, sizeof(raw));
|
|
||||||
result += std::string(i == 0 ? "\nLeft:" : "\nRight:") +
|
|
||||||
" err=" + std::to_string(int(error)) + " active=" + (data.bActive ? "Y" : "N") +
|
" err=" + std::to_string(int(error)) + " active=" + (data.bActive ? "Y" : "N") +
|
||||||
" down=" + (data.bState ? "Y" : "N") + " pose=" + (tracked ? "Y" : "N") +
|
" down=" + (data.bState ? "Y" : "N") +
|
||||||
" origin=" + (origin_ok ? "Y" : "N") +
|
" pose-role-accepted=" + (accepted ? "Y" : "N") + " gated-down=" + (down ? "Y" : "N");
|
||||||
" raw=" + (raw_ok ? ((raw.ulButtonPressed & vr::ButtonMaskFromId(vr::k_EButton_Grip)) ? "down" : "up") : "n/a");
|
|
||||||
}
|
}
|
||||||
// The default Frame binding maps right X to the named hold-to-talk action.
|
|
||||||
// Report the action's own activity separately from the stricter pose gate;
|
|
||||||
// remapped bindings may intentionally have a different origin.
|
|
||||||
vr::InputDigitalActionData_t ptt{};
|
|
||||||
const auto ptt_error = impl_->input->GetDigitalActionData(impl_->actions[2], &ptt, sizeof(ptt), vr::k_ulInvalidInputValueHandle);
|
|
||||||
const auto [accepted, down] = impl_->digital(2);
|
|
||||||
result += "\nPTT (default right X): err=" + std::to_string(int(ptt_error)) +
|
|
||||||
" active=" + (ptt.bActive ? "Y" : "N") + " down=" + (ptt.bState ? "Y" : "N") +
|
|
||||||
" accepted=" + (accepted ? "Y" : "N") + " gated-down=" + (down ? "Y" : "N");
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
std::string Overlay::pointer_status() const {
|
std::string Overlay::pointer_status() const {
|
||||||
|
|||||||
@@ -27,10 +27,24 @@ int main(int argc, char** argv) {
|
|||||||
assert(default_config_path().empty());
|
assert(default_config_path().empty());
|
||||||
::setenv("XDG_CONFIG_HOME", dir.c_str(), 1);
|
::setenv("XDG_CONFIG_HOME", dir.c_str(), 1);
|
||||||
assert(load_config(path).buttons.empty());
|
assert(load_config(path).buttons.empty());
|
||||||
|
assert(!load_config(path).experimental_input_priority);
|
||||||
auto example = load_config(std::filesystem::path(argv[1]) / "config.example.json");
|
auto example = load_config(std::filesystem::path(argv[1]) / "config.example.json");
|
||||||
assert(example.buttons.at("ptt") == "/user/hand/right/input/x");
|
assert(example.buttons.at("ptt") == "/user/hand/right/input/x");
|
||||||
assert(example.font.empty());
|
assert(example.font.empty());
|
||||||
|
assert(!example.experimental_input_priority);
|
||||||
std::filesystem::create_directories(path.parent_path());
|
std::filesystem::create_directories(path.parent_path());
|
||||||
|
put(path, R"({"input_priority":"experimental"})");
|
||||||
|
auto experimental = load_config(path);
|
||||||
|
assert(experimental.experimental_input_priority);
|
||||||
|
// A priority request must preserve the user's existing action manifest/bindings.
|
||||||
|
assert(action_manifest(argv[1], experimental) == std::filesystem::absolute(std::filesystem::path(argv[1]) / "actions.json"));
|
||||||
|
put(path, R"({"input_priority":"normal"})");
|
||||||
|
assert(!load_config(path).experimental_input_priority);
|
||||||
|
for (const auto* invalid : {R"({"input_priority":true})", R"({"input_priority":16777216})",
|
||||||
|
R"({"input_priority":"highest"})", R"({"input_priority":null})"}) {
|
||||||
|
put(path, invalid);
|
||||||
|
fails([&] { load_config(path); });
|
||||||
|
}
|
||||||
put(path, R"({"theme":{"background":"#123ABC","accent":"#abcdef","frame_end":"#010203"},"font":"/nonexistent/face.ttf","buttons":{"ptt":"/user/hand/left/input/y","cancel":"/user/hand/right/input/b","right_grip":""}})");
|
put(path, R"({"theme":{"background":"#123ABC","accent":"#abcdef","frame_end":"#010203"},"font":"/nonexistent/face.ttf","buttons":{"ptt":"/user/hand/left/input/y","cancel":"/user/hand/right/input/b","right_grip":""}})");
|
||||||
auto config = load_config(path);
|
auto config = load_config(path);
|
||||||
assert((config.theme.background == Rgba{0x12, 0x3a, 0xbc, 255}));
|
assert((config.theme.background == Rgba{0x12, 0x3a, 0xbc, 255}));
|
||||||
|
|||||||
@@ -124,21 +124,24 @@ class InstallTests(unittest.TestCase):
|
|||||||
self.assertEqual(fixed["buttons"]["ptt"], "/user/hand/left/input/y")
|
self.assertEqual(fixed["buttons"]["ptt"], "/user/hand/left/input/y")
|
||||||
self.assertEqual(fixed["theme"]["card"], installer.CONFIG_DEFAULTS["theme"]["card"])
|
self.assertEqual(fixed["theme"]["card"], installer.CONFIG_DEFAULTS["theme"]["card"])
|
||||||
self.assertEqual(fixed["buttons"]["cancel"], "")
|
self.assertEqual(fixed["buttons"]["cancel"], "")
|
||||||
|
self.assertEqual(fixed["input_priority"], "normal")
|
||||||
backups = list(config.parent.glob("config.json.backup-*"))
|
backups = list(config.parent.glob("config.json.backup-*"))
|
||||||
self.assertEqual(len(backups), 1)
|
self.assertEqual(len(backups), 1)
|
||||||
self.assertEqual(backups[0].read_bytes(), original)
|
self.assertEqual(backups[0].read_bytes(), original)
|
||||||
|
fixed["input_priority"] = "experimental"
|
||||||
compact = json.dumps(fixed, separators=(",", ":")).encode()
|
compact = json.dumps(fixed, separators=(",", ":")).encode()
|
||||||
config.write_bytes(compact)
|
config.write_bytes(compact)
|
||||||
self.install("v1", archive, digest)
|
self.install("v1", archive, digest)
|
||||||
self.assertEqual(config.read_bytes(), compact)
|
self.assertEqual(config.read_bytes(), compact)
|
||||||
self.assertEqual(len(list(config.parent.glob("config.json.backup-*"))), 1)
|
self.assertEqual(len(list(config.parent.glob("config.json.backup-*"))), 1)
|
||||||
original = b'{"font":"/system/face.ttf","theme":{"ink":"bad","retired":"#123456"},"buttons":{"ptt":"/user/hand/left/input/grip"},"old_option":4}'
|
original = b'{"font":"/system/face.ttf","input_priority":"highest","theme":{"ink":"bad","retired":"#123456"},"buttons":{"ptt":"/user/hand/left/input/grip"},"old_option":4}'
|
||||||
config.write_bytes(original)
|
config.write_bytes(original)
|
||||||
self.install("v1", archive, digest)
|
self.install("v1", archive, digest)
|
||||||
fixed = json.loads(config.read_text())
|
fixed = json.loads(config.read_text())
|
||||||
self.assertEqual(fixed["font"], "/system/face.ttf")
|
self.assertEqual(fixed["font"], "/system/face.ttf")
|
||||||
self.assertEqual(fixed["theme"]["ink"], installer.CONFIG_DEFAULTS["theme"]["ink"])
|
self.assertEqual(fixed["theme"]["ink"], installer.CONFIG_DEFAULTS["theme"]["ink"])
|
||||||
self.assertEqual(fixed["buttons"], installer.CONFIG_DEFAULTS["buttons"]) # colliding paths reset
|
self.assertEqual(fixed["buttons"], installer.CONFIG_DEFAULTS["buttons"]) # colliding paths reset
|
||||||
|
self.assertEqual(fixed["input_priority"], "normal")
|
||||||
self.assertNotIn("old_option", fixed)
|
self.assertNotIn("old_option", fixed)
|
||||||
self.assertEqual(sorted(p.read_bytes() for p in config.parent.glob("config.json.backup-*")), sorted([backups[0].read_bytes(), original]))
|
self.assertEqual(sorted(p.read_bytes() for p in config.parent.glob("config.json.backup-*")), sorted([backups[0].read_bytes(), original]))
|
||||||
original = b'{"font":"one","font":"two"'
|
original = b'{"font":"one","font":"two"'
|
||||||
|
|||||||
Reference in new issue
Block a user