Files
mitch030504--AstroQuest-Frame/tools/tests/held_pad_test.cpp
T
bigmak94andClaude Opus 5.5 ee4f9aa03f Quest app: the headset's own controllers play the game, and what other gamepads lack is made up for
The app was made for a DualSense and took everything else for a lesser one of those. Players
without one could play on the PC, with the headset's controllers, and not on the headset.

The headset's own controllers: the app reads them through the headset's runtime (OpenXR
actions, the layout the PC host has: A cross, B square, X circle, Y triangle and both
together to blow, grips L1/R1, triggers L2/R2, right stick the finger on the touchpad, left
menu button OPTIONS, both sticks in to reset the view), the right one is the controller in
the game with where it is and how it points, both shake with the game's rumble. The gamepad
the system makes of them for other apps is no longer taken for a gamepad: it says neither
where it is nor whether anybody holds it.

Which of the two: a small router (quest-host/cpp/pad_router.h) between the activity, which
hears of gamepads, and the session, which reads the controllers. The game is played with the
one used last. From a gamepad only a button under a thumb or a stick takes over, never a
trigger or a grip: while a gamepad is held it is hands the headset sees. What buttons mean
about the view besides (the first press of X takes the seat, OPTIONS held resets the view)
moved there from the activity, so that it is the same for both. The start-up menu takes
either; what confirmed it is not pressed in the game.

Gamepads that are not a PlayStation's:
- The right stick as the touchpad's finger is Input::StickFinger now, as on the PC: the
  finger lifts where the stick was let go, which the catapult at the end of a level needs.
  A button that presses the touchpad touches it in its middle.
- A gamepad without motion sensors followed the hands to where they were and kept pointing
  straight ahead. Now the hands turn and tilt it (quest-host/cpp/held_pad.h): the line
  between the palms is its sideways axis, the way the hands point is its tilt. The app says
  so with a new flag of the pad pose message (HeldInHands), and the emulator lets go of a
  controller that was a tracked device once it no longer is (it stayed where that device
  was last seen for as long as no motion sensor of its own spoke up).
- Motion sensors are only taken from PlayStation controllers: which way is up for one is
  each maker's own affair.
- A right stick on RX/RY, triggers that are only buttons, a Back button the system takes
  for its own "back".

For trying a build on a headset whose owner plays with the installed one:
quest-host/build.sh --test-package builds the app under another name, which installs next to
it; the test tools take ASTRO_PACKAGE. The self-test can report a controller the way a
session does (HOST_PAD) and keep pictures of it (HOST_SHOTS).

Tried on a Quest 3 without wearing it: the runtime takes the bindings, the app starts and
plays with a made-up gamepad, and in the game's picture the controller is where and how the
self-test says a tracked controller or hands around a gamepad are, and comes back when they
are lost. tools/tests/pad_router_test.cpp and held_pad_test.cpp try the rest on the PC.
Not tried: any of it worn, and no gamepad other than a DualSense.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:38:59 +02:00

205 lines
9.9 KiB
C++

// Checks what the Quest app makes of two hands holding a gamepad (quest-host/cpp/held_pad.h):
// where the gamepad is, which way it is turned, how far it is rolled and how far its front
// is tilted up, each with the right sign.
//
// clang-cl /std:c++latest /EHsc -fuse-ld=lld /I quest-host/cpp tools/tests/held_pad_test.cpp
// /Fe:build/tests/held_pad_test.exe
#include <cmath>
#include <cstdio>
#include <string>
#include "held_pad.h"
using namespace HeldPad;
static int failures = 0;
static void Check(bool ok, const std::string& what) {
std::printf("%s %s\n", ok ? "ok " : "FAIL", what.c_str());
failures += ok ? 0 : 1;
}
static bool Near(float a, float b, float within = 0.002f) {
return std::abs(a - b) <= within;
}
static bool Near(const Vec& a, const Vec& b, float within = 0.002f) {
return Near(a.x, b.x, within) && Near(a.y, b.y, within) && Near(a.z, b.z, within);
}
static constexpr float Pi = 3.14159265f;
static float Radians(float degrees) {
return degrees * Pi / 180.0f;
}
/// Two hands around a gamepad whose middle is at `middle`, turned `yaw` to the left and with
/// its right side `roll` above level, the hands pointing `pitch` above level.
static Hands Hold(const Vec& middle, float yaw, float roll, float pitch, float apart = 0.2f) {
const Vec right{std::cos(yaw) * std::cos(roll), std::sin(roll), -std::sin(yaw) * std::cos(roll)};
const Vec level_front{-std::sin(yaw), 0.0f, -std::cos(yaw)};
const Vec level_up = Cross(right, level_front);
const Vec pointing{level_front.x * std::cos(pitch) + level_up.x * std::sin(pitch),
level_front.y * std::cos(pitch) + level_up.y * std::sin(pitch),
level_front.z * std::cos(pitch) + level_up.z * std::sin(pitch)};
Hands hands;
hands.left_palm = Sub(middle, Scale(right, apart * 0.5f));
hands.right_palm = Sub(middle, Scale(right, -apart * 0.5f));
hands.pointing_known = true;
// Each hand points inwards as well, by a third of a right angle.
const auto inwards = [&](float side) {
const Vec v{pointing.x * std::cos(0.5f) + right.x * side * std::sin(0.5f),
pointing.y * std::cos(0.5f) + right.y * side * std::sin(0.5f),
pointing.z * std::cos(0.5f) + right.z * side * std::sin(0.5f)};
return v;
};
hands.left_pointing = inwards(1.0f);
hands.right_pointing = inwards(-1.0f);
return hands;
}
int main() {
const Vec x{1.0f, 0.0f, 0.0f}, y{0.0f, 1.0f, 0.0f}, z{0.0f, 0.0f, 1.0f};
// The rotation from three directions, and back.
{
const Quat q = FromAxes(x, y, z);
Check(Near(q.x, 0.0f) && Near(q.y, 0.0f) && Near(q.z, 0.0f) && Near(std::abs(q.w), 1.0f),
"right, up and back where they always are: no rotation");
bool all = true;
for (int i = 0; i < 200; ++i) {
// Any attitude: turned, then tilted, then rolled.
const float a = 0.37f * static_cast<float>(i), b = 0.11f * static_cast<float>(i) - 1.4f,
c = 0.23f * static_cast<float>(i);
const Vec right{std::cos(a) * std::cos(c), std::sin(c), -std::sin(a) * std::cos(c)};
const Vec level_front{-std::sin(a), 0.0f, -std::cos(a)};
const Vec level_up = Cross(right, level_front);
const Vec front{level_front.x * std::cos(b) + level_up.x * std::sin(b),
level_front.y * std::cos(b) + level_up.y * std::sin(b),
level_front.z * std::cos(b) + level_up.z * std::sin(b)};
const Vec up = Cross(right, front);
const Quat turned = FromAxes(right, up, Scale(front, -1.0f));
all = all && Near(Rotate(turned, x), right, 0.001f) && Near(Rotate(turned, y), up, 0.001f) &&
Near(Rotate(turned, z), Scale(front, -1.0f), 0.001f);
}
Check(all, "any three directions make the rotation that takes the axes to them");
}
// Held level, straight ahead.
{
const auto pose = Locate(Hold({0.0f, -0.3f, -0.4f}, 0.0f, 0.0f, 0.0f), 0.0f, 0.5f);
Check(pose.has_value() && pose->turned_known, "two hands a gamepad's width apart hold one");
Check(Near(pose->position, {0.0f, -0.285f, -0.435f}),
"its middle is a little ahead of and above the middle between the palms");
Check(Near(pose->yaw, 0.0f) && Near(pose->pitch, 0.0f) && pose->pitch_measured,
"level hands pointing ahead: neither turned nor tilted");
Check(Near(Rotate(pose->orientation, {0.0f, 0.0f, -1.0f}), {0.0f, 0.0f, -1.0f}) &&
Near(Rotate(pose->orientation, y), y),
"its front points straight ahead, its face up");
}
// Not a gamepad.
Check(!Locate(Hold({}, 0.0f, 0.0f, 0.0f, 0.4f), 0.0f, 0.0f).has_value(),
"hands further apart than a gamepad is wide hold none");
Check(!Locate(Hold({}, 0.0f, 0.0f, 0.0f, 0.03f), 0.0f, 0.0f).has_value(),
"nor do hands that touch");
// Turned to the left.
{
const auto pose = Locate(Hold({0.1f, -0.3f, -0.4f}, Radians(30.0f), 0.0f, 0.0f), 0.0f, 0.0f);
Check(pose && Near(pose->yaw, Radians(30.0f)), "turned to the left: a heading to the left");
const Vec front = Rotate(pose->orientation, {0.0f, 0.0f, -1.0f});
Check(Near(front, {-0.5f, 0.0f, -std::cos(Radians(30.0f))}), "its front points to the left");
Check(Near(pose->position.x, 0.1f - 0.5f * 0.035f) &&
Near(pose->position.z, -0.4f - std::cos(Radians(30.0f)) * 0.035f),
"and its middle is ahead of the palms the way it points");
}
{
const auto pose = Locate(Hold({}, Radians(-45.0f), 0.0f, 0.0f), 0.0f, 0.0f);
Check(pose && Near(pose->yaw, Radians(-45.0f)) &&
Rotate(pose->orientation, {0.0f, 0.0f, -1.0f}).x > 0.7f,
"turned to the right: a heading to the right");
}
// Rolled: the right hand higher.
{
const auto pose = Locate(Hold({}, 0.0f, Radians(20.0f), 0.0f), 0.0f, 0.0f);
const Vec right = Rotate(pose->orientation, x);
const Vec front = Rotate(pose->orientation, {0.0f, 0.0f, -1.0f});
Check(pose && Near(right.y, std::sin(Radians(20.0f))), "the right hand higher: its right side is up");
Check(Near(front, {0.0f, 0.0f, -1.0f}) && Near(pose->pitch, 0.0f),
"and its front still points level and ahead");
}
// Tilted: the hands point up.
{
const auto pose = Locate(Hold({}, 0.0f, 0.0f, Radians(30.0f)), 0.0f, 0.0f);
const Vec front = Rotate(pose->orientation, {0.0f, 0.0f, -1.0f});
Check(pose && pose->pitch_measured && Near(pose->pitch, Radians(30.0f)),
"hands pointing up: tilted up by as much, however far they point inwards");
Check(Near(front, {0.0f, 0.5f, -std::cos(Radians(30.0f))}), "its front points up");
Check(Rotate(pose->orientation, y).z > 0.49f, "and its face leans towards the player");
}
{
const auto pose = Locate(Hold({}, 0.0f, 0.0f, Radians(-40.0f)), 0.0f, 0.0f);
Check(pose && Near(pose->pitch, Radians(-40.0f)) &&
Rotate(pose->orientation, {0.0f, 0.0f, -1.0f}).y < -0.6f,
"hands pointing down: tilted down");
}
{
const auto pose = Locate(Hold({}, 0.0f, 0.0f, Radians(30.0f)), Radians(-10.0f), 0.0f);
Check(pose && Near(pose->pitch, Radians(30.0f)) &&
Near(Rotate(pose->orientation, {0.0f, 0.0f, -1.0f}).y, std::sin(Radians(20.0f))),
"a gamepad taken to point lower than the hands does, by so much");
}
// All at once.
{
const float yaw = Radians(25.0f), roll = Radians(-15.0f), pitch = Radians(35.0f);
const auto pose = Locate(Hold({0.05f, -0.2f, -0.5f}, yaw, roll, pitch), 0.0f, 0.0f);
const Vec right = Rotate(pose->orientation, x);
const Vec front = Rotate(pose->orientation, {0.0f, 0.0f, -1.0f});
const Vec up = Rotate(pose->orientation, y);
Check(pose && Near(pose->yaw, yaw) && Near(pose->pitch, pitch) && Near(right.y, std::sin(roll)),
"turned, rolled and tilted at once: each is told apart");
Check(Near(Dot(right, front), 0.0f) && Near(Dot(right, up), 0.0f) && Near(Dot(front, up), 0.0f) &&
Near(Length(right), 1.0f) && Near(Length(front), 1.0f),
"and what comes out is a rotation");
}
// What the headset cannot tell.
{
Hands hands = Hold({}, 0.0f, 0.0f, Radians(50.0f));
hands.pointing_known = false;
const auto pose = Locate(hands, 0.0f, Radians(20.0f));
Check(pose && pose->turned_known && !pose->pitch_measured && Near(pose->pitch, Radians(20.0f)) &&
Near(Rotate(pose->orientation, {0.0f, 0.0f, -1.0f}).y, std::sin(Radians(20.0f))),
"where the hands point unknown: tilted the way it was taken to be");
}
{
Hands hands;
hands.left_palm = {0.0f, -0.4f, -0.4f};
hands.right_palm = {0.02f, -0.2f, -0.4f};
const auto pose = Locate(hands, 0.0f, 0.0f);
Check(pose && !pose->turned_known && Near(pose->position, {0.01f, -0.285f, -0.4f}),
"one hand above the other: somewhere, but turned no telling how");
}
// Following, a little behind.
{
const Quat quarter = FromAxes({0.0f, 0.0f, -1.0f}, y, x);
const Quat half = Towards(Quat{}, quarter, 0.5f);
const Vec front = Rotate(half, {0.0f, 0.0f, -1.0f});
Check(Near(front, {-std::sqrt(0.5f), 0.0f, -std::sqrt(0.5f)}, 0.01f),
"half the way to a quarter turn is an eighth of a turn");
const Quat other{-quarter.x, -quarter.y, -quarter.z, -quarter.w};
const Vec same = Rotate(Towards(Quat{}, other, 0.5f), {0.0f, 0.0f, -1.0f});
Check(Near(same, front, 0.01f), "whichever of its two quaternions the attitude is given as");
Check(Near(Rotate(Towards(Quat{}, quarter, 1.0f), x), {0.0f, 0.0f, -1.0f}, 0.01f),
"all the way is there");
}
std::printf("%d failed\n", failures);
return failures == 0 ? 0 : 1;
}