mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 11:01:07 +00:00
* chore(i18n): translate every missing string in all locales Adds the 87 strings the bottom-screen Dashboard and in-game stats pages introduced to all 32 locales, plus the 51 older settings strings Japanese and the 50 Chinese locales were still missing. Every locale now has all 3456 translatable strings with the same placeholders as English. Also translates 36 entries that were still English where the language does not use the English word, and makes es-419 touch_app_id use the same %1$s placeholder as English. * chore(patches): drop comments from the lines downstream patches add Removes comment lines and trailing comments from lines our patches add, with hunk headers recounted. Lines that a later patch uses as context and the upstream backport 0123 are left as they are. The gamescope, FEX, proot and xodus series still apply to their pinned sources, and the patched trees differ only in those comments. test_hud_frames now finds the HUD writer by its first declaration. * docs: credit Greenlight, Better xCloud, viture-v2, GameNative and DiRTTelemetryErrorFix These were credited only in source headers and NOTICE files. * chore: remove source comments and unused functions Removes explanatory comments from first-party Kotlin, Java, C, C++, Rust, TypeScript, Python, shell, Gradle, CMake and workflow files. License and attribution headers stay, and vendored third-party trees, generated files and license files are untouched. A shellcheck directive is kept, and udevmon's fallthrough comments became fallthrough attributes. Syntax trees and token streams match the previous sources apart from the comments and Python docstrings. Module docstrings that argparse prints are kept, and device-smoke.sh prints the same --help text from a heredoc. Also removes 37 functions and 4 fields that nothing referenced. * chore: remove comments from the Syncthing integration and the latest main sources * docs: credit Syncthing and the syncthing-android fork * chore(i18n): translate the Syncthing strings in all locales The 107 strings the Syncthing integration added are now in all 32 locales. sync_web_path_locked names the Syncthing tab by its label. * docs: credit @xXJSONDeruloXx for the HUD metric readers The CPU, GPU load, thermal and battery readers come from their PerformanceHudView pull requests #777, #877, #991, #1406 and #1573. The FusionHUD credit line is unchanged. * docs: credit the HUD metric readers to @xXJSONDeruloXx's work
1957 lines
96 KiB
C++
1957 lines
96 KiB
C++
#include "openvr_driver.h"
|
|
|
|
#include <algorithm>
|
|
#include <string>
|
|
#include <atomic>
|
|
#include <cstdarg>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <ctime>
|
|
#include <cstring>
|
|
#include <cstddef>
|
|
#include <fcntl.h>
|
|
#include <map>
|
|
#include <sched.h>
|
|
#include <sys/mman.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/un.h>
|
|
#include <mutex>
|
|
#include <thread>
|
|
#include <vector>
|
|
#include <unistd.h>
|
|
|
|
#include "../../../app/src/main/cpp/waylandcomp/src/headpose_shm.h"
|
|
#include "arm_model.h"
|
|
#include "hand_motion.h"
|
|
#include "hand_pose.h"
|
|
#include "handcam_shm.h"
|
|
#include "hand_skeleton.h"
|
|
#include "index_pose.h"
|
|
#include "pad_controller_config.h"
|
|
#include "joycon_shm.h"
|
|
#include "presenter.h"
|
|
#include "spatial_tracking.h"
|
|
#include "luma_joycon.h"
|
|
#include "render_target.h"
|
|
#include "xr_tracking_file.h"
|
|
#include "xr_frame_math.h"
|
|
#include "xr_vsync_timing.h"
|
|
#include "xr_touch_input.h"
|
|
#include "xr_chaperone.h"
|
|
|
|
#define DD_EXPORT extern "C" __attribute__((visibility("default")))
|
|
|
|
using namespace vr;
|
|
using namespace droiddeck;
|
|
|
|
namespace {
|
|
|
|
const char *const kSettingsSection = "driver_droiddeck";
|
|
const char *const kPosePath = "/run/droiddeck/" DROIDDECK_HEADPOSE_FILE;
|
|
const char *const kJoyConPath = "/run/droiddeck/" DROIDDECK_JOYCON_FILE;
|
|
const char *const kHandCamPath = "/run/droiddeck/" DROIDDECK_HANDCAM_FILE;
|
|
const char *const kHandTraceFlag = "/run/droiddeck/droiddeck-handtrace.on";
|
|
const char *const kHandTracePath = "/run/droiddeck/droiddeck-handtrace.csv";
|
|
constexpr float kMinPulseMs = 30.0f;
|
|
constexpr int64_t kJoyConStaleNs = 500000000;
|
|
constexpr int64_t kHandCamStaleNs = 300000000;
|
|
constexpr int64_t kBareHandsStaleNs = 2000000000;
|
|
|
|
void Log(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
|
|
void Log(const char *fmt, ...) {
|
|
char buf[1024];
|
|
va_list ap;
|
|
va_start(ap, fmt);
|
|
vsnprintf(buf, sizeof(buf), fmt, ap);
|
|
va_end(ap);
|
|
if (VRDriverLog()) VRDriverLog()->Log(buf);
|
|
fprintf(stderr, "[droiddeck] %s\n", buf);
|
|
}
|
|
|
|
class HandTrace {
|
|
public:
|
|
void Poll(int64_t nowNs) {
|
|
if (checkedNs_ && nowNs - checkedNs_ < 1000000000) return;
|
|
checkedNs_ = nowNs;
|
|
const bool want = access(kHandTraceFlag, F_OK) == 0;
|
|
if (want && !file_) {
|
|
file_ = fopen(kHandTracePath, "a");
|
|
if (file_) {
|
|
fprintf(file_, "S,%lld\n", (long long)nowNs);
|
|
Log("hand trace started");
|
|
}
|
|
} else if (!want && file_) {
|
|
fclose(file_);
|
|
file_ = nullptr;
|
|
Log("hand trace stopped");
|
|
}
|
|
if (file_) fflush(file_);
|
|
}
|
|
|
|
FILE *File() const { return file_; }
|
|
|
|
private:
|
|
FILE *file_ = nullptr;
|
|
int64_t checkedNs_ = 0;
|
|
};
|
|
|
|
HandTrace gHandTrace;
|
|
|
|
class PoseFile {
|
|
public:
|
|
bool Read(droiddeck_headpose *out) {
|
|
if (!map_ && !Open()) return false;
|
|
for (int tries = 0; tries < 8; tries++) {
|
|
uint32_t a = __atomic_load_n(&map_->seq, __ATOMIC_ACQUIRE);
|
|
if (a & 1u) continue;
|
|
memcpy(out, (const void *)map_, sizeof(*out));
|
|
__atomic_thread_fence(__ATOMIC_ACQUIRE);
|
|
if (__atomic_load_n(&map_->seq, __ATOMIC_RELAXED) == a)
|
|
return out->magic == DROIDDECK_HEADPOSE_MAGIC && out->version == DROIDDECK_HEADPOSE_VERSION;
|
|
}
|
|
return false;
|
|
}
|
|
~PoseFile() { if (map_) munmap((void *)map_, sizeof(*map_)); }
|
|
|
|
private:
|
|
bool Open() {
|
|
int fd = open(kPosePath, O_RDONLY | O_CLOEXEC);
|
|
if (fd < 0) return false;
|
|
void *p = mmap(nullptr, sizeof(droiddeck_headpose), PROT_READ, MAP_SHARED, fd, 0);
|
|
close(fd);
|
|
if (p == MAP_FAILED) return false;
|
|
map_ = (volatile droiddeck_headpose *)p;
|
|
Log("head pose file mapped: %s", kPosePath);
|
|
return true;
|
|
}
|
|
volatile droiddeck_headpose *map_ = nullptr;
|
|
};
|
|
|
|
HmdQuaternion_t QuatFromMatrix(const float m[9]) {
|
|
HmdQuaternion_t q;
|
|
float tr = m[0] + m[4] + m[8];
|
|
if (tr > 0.0f) {
|
|
float s = sqrtf(tr + 1.0f) * 2.0f;
|
|
q.w = 0.25f * s; q.x = (m[7] - m[5]) / s; q.y = (m[2] - m[6]) / s; q.z = (m[3] - m[1]) / s;
|
|
} else if (m[0] > m[4] && m[0] > m[8]) {
|
|
float s = sqrtf(1.0f + m[0] - m[4] - m[8]) * 2.0f;
|
|
q.w = (m[7] - m[5]) / s; q.x = 0.25f * s; q.y = (m[1] + m[3]) / s; q.z = (m[2] + m[6]) / s;
|
|
} else if (m[4] > m[8]) {
|
|
float s = sqrtf(1.0f + m[4] - m[0] - m[8]) * 2.0f;
|
|
q.w = (m[2] - m[6]) / s; q.x = (m[1] + m[3]) / s; q.y = 0.25f * s; q.z = (m[5] + m[7]) / s;
|
|
} else {
|
|
float s = sqrtf(1.0f + m[8] - m[0] - m[4]) * 2.0f;
|
|
q.w = (m[3] - m[1]) / s; q.x = (m[2] + m[6]) / s; q.y = (m[5] + m[7]) / s; q.z = 0.25f * s;
|
|
}
|
|
return q;
|
|
}
|
|
|
|
double VsyncPeriodSeconds();
|
|
|
|
HmdMatrix34_t XrPoseMatrix(const DroidDeckXrPose &pose) {
|
|
const float qx = pose.orientation[0], qy = pose.orientation[1], qz = pose.orientation[2], qw = pose.orientation[3];
|
|
HmdMatrix34_t matrix{};
|
|
matrix.m[0][0] = 1 - 2 * (qy * qy + qz * qz); matrix.m[0][1] = 2 * (qx * qy - qw * qz); matrix.m[0][2] = 2 * (qx * qz + qw * qy);
|
|
matrix.m[1][0] = 2 * (qx * qy + qw * qz); matrix.m[1][1] = 1 - 2 * (qx * qx + qz * qz); matrix.m[1][2] = 2 * (qy * qz - qw * qx);
|
|
matrix.m[2][0] = 2 * (qx * qz - qw * qy); matrix.m[2][1] = 2 * (qy * qz + qw * qx); matrix.m[2][2] = 1 - 2 * (qx * qx + qy * qy);
|
|
for (int axis = 0; axis < 3; axis++) matrix.m[axis][3] = pose.position[axis];
|
|
return matrix;
|
|
}
|
|
|
|
HmdMatrix34_t XrEyeToHead(const DroidDeckXrPose &head, const DroidDeckXrPose &eye) {
|
|
HmdMatrix34_t headMatrix = XrPoseMatrix(head), eyeMatrix = XrPoseMatrix(eye), result{};
|
|
for (int row = 0; row < 3; row++) {
|
|
for (int column = 0; column < 3; column++)
|
|
for (int inner = 0; inner < 3; inner++) result.m[row][column] += headMatrix.m[inner][row] * eyeMatrix.m[inner][column];
|
|
for (int inner = 0; inner < 3; inner++)
|
|
result.m[row][3] += headMatrix.m[inner][row] * (eyeMatrix.m[inner][3] - headMatrix.m[inner][3]);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
class VirtualDisplay : public IVRVirtualDisplay {
|
|
public:
|
|
void Present(const PresentInfo_t *info, uint32_t size) override {
|
|
frames_++;
|
|
if (frames_ <= 5 || frames_ % 600 == 0)
|
|
Log("virtual display Present #%llu: handle 0x%llx vsync %d frame %llu (info %u bytes)",
|
|
(unsigned long long)frames_, (unsigned long long)info->backbufferTextureHandle, (int)info->vsync,
|
|
(unsigned long long)info->nFrameId, size);
|
|
last_ = std::chrono::steady_clock::now();
|
|
}
|
|
void WaitForPresent() override {}
|
|
bool GetTimeSinceLastVsync(float *seconds, uint64_t *counter) override {
|
|
using namespace std::chrono;
|
|
const double period = VsyncPeriodSeconds();
|
|
std::lock_guard<std::mutex> lock(vsyncMutex_);
|
|
const double t = duration<double>(steady_clock::now().time_since_epoch()).count();
|
|
if (period != period_) {
|
|
vsyncBase_ = period_ > 0.0 ? vsyncBase_ + (uint64_t)((t - vsyncAt_) / period_) : (uint64_t)(t / period);
|
|
vsyncAt_ = t;
|
|
period_ = period;
|
|
}
|
|
const double since = t - vsyncAt_;
|
|
*seconds = (float)fmod(since, period);
|
|
*counter = vsyncBase_ + (uint64_t)(since / period);
|
|
return true;
|
|
}
|
|
|
|
private:
|
|
uint64_t frames_ = 0;
|
|
std::chrono::steady_clock::time_point last_;
|
|
std::mutex vsyncMutex_;
|
|
double period_ = 0.0, vsyncAt_ = 0.0;
|
|
uint64_t vsyncBase_ = 0;
|
|
};
|
|
|
|
class Vsync {
|
|
public:
|
|
static constexpr std::chrono::microseconds kLead{2000};
|
|
|
|
static std::chrono::nanoseconds Interval() { return std::chrono::nanoseconds(intervalNs_.load()); }
|
|
static void SetRate(float hz) {
|
|
if (hz >= 24.0f && hz <= 240.0f) intervalNs_ = (int64_t)(1e9 / hz);
|
|
}
|
|
|
|
static void Start() {
|
|
if (running_.exchange(true)) return;
|
|
timing_.Reset();
|
|
thread_ = std::thread([] {
|
|
XrTrackingFile xrFile;
|
|
auto next = std::chrono::steady_clock::now() + Interval();
|
|
while (running_) {
|
|
auto interval = Interval();
|
|
DroidDeckXrTracking xr;
|
|
if (xrFile.Read(&xr) && XrTrackingFile::Fresh(xr)) {
|
|
interval = std::chrono::nanoseconds(xr.period_ns);
|
|
auto now = std::chrono::steady_clock::now();
|
|
next = timing_.Target(now, next, xr.predicted_ns, interval);
|
|
}
|
|
auto announce = next - kLead;
|
|
std::this_thread::sleep_until(announce);
|
|
auto now = std::chrono::steady_clock::now();
|
|
double offset = std::chrono::duration<double>(next - now).count();
|
|
timing_.Record(next, interval);
|
|
VRServerDriverHost()->VsyncEvent(offset);
|
|
next += interval;
|
|
now = std::chrono::steady_clock::now();
|
|
while (next <= now) next += interval;
|
|
}
|
|
});
|
|
}
|
|
static void Stop() {
|
|
running_ = false;
|
|
if (thread_.joinable()) thread_.join();
|
|
}
|
|
static std::chrono::steady_clock::time_point Deadline(uint32_t framesToThrottle) {
|
|
return timing_.Deadline(framesToThrottle);
|
|
}
|
|
|
|
private:
|
|
static inline std::atomic<bool> running_{false};
|
|
static inline std::atomic<int64_t> intervalNs_{16666667};
|
|
static inline std::thread thread_;
|
|
static inline XrVsyncTiming timing_;
|
|
};
|
|
|
|
double VsyncPeriodSeconds() { return Vsync::Interval().count() / 1e9; }
|
|
|
|
class DirectMode : public IVRDriverDirectModeComponent {
|
|
public:
|
|
void ConfigureXr(const DroidDeckXrTracking &tracking) {
|
|
xrWidth_ = tracking.eyes[0].width * 2; xrHeight_ = tracking.eyes[0].height;
|
|
for (int eye = 0; eye < 2; eye++) eyeToHead_[eye] = XrEyeToHead(tracking.head, tracking.eyes[eye].pose);
|
|
}
|
|
struct Set {
|
|
SharedTextureHandle_t handles[3] = {};
|
|
int fds[3] = {-1, -1, -1};
|
|
uint32_t pid = 0, index = 0;
|
|
SwapTextureSetDesc_t desc = {};
|
|
};
|
|
|
|
void CreateSwapTextureSet(uint32_t pid, const SwapTextureSetDesc_t *d, SwapTextureSet_t *out) override {
|
|
Log("CreateSwapTextureSet pid %u: format %u, %ux%u, %u samples", pid, d->nFormat, d->nWidth, d->nHeight, d->nSampleCount);
|
|
Set set;
|
|
set.pid = pid;
|
|
set.desc = *d;
|
|
const uint32_t usage = 0x1 | 0x4 | 0x80 ;
|
|
const uint32_t create = 0x8 ;
|
|
for (int i = 0; i < 3; i++) {
|
|
SharedTextureHandle_t h = 0;
|
|
if (!VRIPCResourceManager()->NewSharedVulkanImage(d->nFormat, d->nWidth, d->nHeight, true, false, false, 1, 1,
|
|
create, usage, &h)) {
|
|
Log("NewSharedVulkanImage failed");
|
|
return;
|
|
}
|
|
uint64_t ipc = 0;
|
|
VRIPCResourceManager()->RefResource(h, &ipc);
|
|
int fd = -1;
|
|
if (!VRIPCResourceManager()->ReceiveSharedFd(ipc, &fd)) Log("ReceiveSharedFd failed");
|
|
set.handles[i] = h;
|
|
set.fds[i] = fd;
|
|
out->rSharedTextureHandles[i] = h;
|
|
}
|
|
out->unTextureFlags = 0;
|
|
Log(" -> handles 0x%llx 0x%llx 0x%llx, fds %d %d %d", (unsigned long long)set.handles[0],
|
|
(unsigned long long)set.handles[1], (unsigned long long)set.handles[2], set.fds[0], set.fds[1], set.fds[2]);
|
|
droiddeck::Presenter::AddTextures(set.handles, set.fds, d->nFormat, d->nWidth, d->nHeight, pid);
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
sets_.push_back(set);
|
|
}
|
|
|
|
void DestroySwapTextureSet(SharedTextureHandle_t h) override {
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
for (auto it = sets_.begin(); it != sets_.end(); ++it) {
|
|
if (it->handles[0] == h || it->handles[1] == h || it->handles[2] == h) {
|
|
Release(*it);
|
|
sets_.erase(it);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void DestroyAllSwapTextureSets(uint32_t pid) override {
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
for (auto it = sets_.begin(); it != sets_.end();) {
|
|
if (it->pid == pid) { Release(*it); it = sets_.erase(it); } else ++it;
|
|
}
|
|
}
|
|
|
|
void GetNextSwapTextureSetIndex(SharedTextureHandle_t handles[2], uint32_t (*indices)[2]) override {
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
for (int e = 0; e < 2; e++) {
|
|
Set *s = Find(handles[e]);
|
|
if (!s) { (*indices)[e] = 0; continue; }
|
|
s->index = (s->index + 1) % 3;
|
|
(*indices)[e] = s->index;
|
|
}
|
|
}
|
|
|
|
void SubmitLayer(const SubmitLayerPerEye_t (&eyes)[2]) override {
|
|
layers_++;
|
|
if (layerInFrame_++ == 0) {
|
|
frame_.render = {};
|
|
if (xrWidth_) {
|
|
frame_.render.magic = DROIDDECK_XR_FRAME_MAGIC; frame_.render.version = DROIDDECK_XR_FRAME_VERSION;
|
|
frame_.render.width = xrWidth_; frame_.render.height = xrHeight_;
|
|
frame_.render.bytes = xrWidth_ * xrHeight_ * 4;
|
|
frame_.render.color_space = DROIDDECK_XR_COLOR_LINEAR_RGBA8;
|
|
}
|
|
for (int e = 0; e < 2; e++) {
|
|
frame_.texture[e] = eyes[e].hTexture;
|
|
frame_.bounds[e][0] = eyes[e].bounds.uMin; frame_.bounds[e][1] = eyes[e].bounds.vMin;
|
|
frame_.bounds[e][2] = eyes[e].bounds.uMax; frame_.bounds[e][3] = eyes[e].bounds.vMax;
|
|
std::memcpy(frame_.render.projection[e], eyes[e].mProjection.m, sizeof(eyes[e].mProjection.m));
|
|
xr_frame_eye_pose(frame_.render.eye_pose[e], eyes[e].mHmdPose.m, eyeToHead_[e].m);
|
|
}
|
|
}
|
|
if (frames_ < 3)
|
|
Log("SubmitLayer: textures 0x%llx / 0x%llx, bounds %.2f,%.2f-%.2f,%.2f",
|
|
(unsigned long long)eyes[0].hTexture, (unsigned long long)eyes[1].hTexture,
|
|
eyes[0].bounds.uMin, eyes[0].bounds.vMin, eyes[0].bounds.uMax, eyes[0].bounds.vMax);
|
|
}
|
|
|
|
void Present(SharedTextureHandle_t sync) override {
|
|
frames_++;
|
|
frame_.render.frame_id = frames_; frame_.render.captured_ns = XrTrackingFile::Now();
|
|
frame_.layers = layerInFrame_;
|
|
if (layerInFrame_) droiddeck::Presenter::Submit(frame_);
|
|
if (layerInFrame_ != lastLayerCount_) {
|
|
static int said;
|
|
if (said++ < 20) Log("frame %llu: %d layer(s) (was %d)", (unsigned long long)frames_, layerInFrame_, lastLayerCount_);
|
|
lastLayerCount_ = layerInFrame_;
|
|
}
|
|
layerInFrame_ = 0;
|
|
if (frames_ <= 3 || frames_ % 600 == 0)
|
|
Log("Present #%llu (%llu layers so far), sync 0x%llx", (unsigned long long)frames_,
|
|
(unsigned long long)layers_, (unsigned long long)sync);
|
|
}
|
|
|
|
void PostPresent(const Throttling_t *t) override {
|
|
auto until = Vsync::Deadline(t ? t->nFramesToThrottle : 0);
|
|
if (until > std::chrono::steady_clock::now()) std::this_thread::sleep_until(until);
|
|
}
|
|
|
|
void GetFrameTiming(DriverDirectMode_FrameTiming *t) override { t->m_nReprojectionFlags = 0; }
|
|
|
|
private:
|
|
Set *Find(SharedTextureHandle_t h) {
|
|
for (auto &s : sets_)
|
|
for (int i = 0; i < 3; i++)
|
|
if (s.handles[i] == h) return &s;
|
|
return nullptr;
|
|
}
|
|
static void Release(Set &s) {
|
|
for (int i = 0; i < 3; i++) {
|
|
if (s.handles[i]) droiddeck::Presenter::RemoveTexture(s.handles[i]);
|
|
if (s.fds[i] >= 0) close(s.fds[i]);
|
|
if (s.handles[i]) VRIPCResourceManager()->UnrefResource(s.handles[i]);
|
|
}
|
|
}
|
|
|
|
std::mutex mutex_;
|
|
std::vector<Set> sets_;
|
|
uint64_t frames_ = 0, layers_ = 0;
|
|
int layerInFrame_ = 0, lastLayerCount_ = -1;
|
|
droiddeck::EyeFrame frame_{};
|
|
uint32_t xrWidth_ = 0, xrHeight_ = 0;
|
|
HmdMatrix34_t eyeToHead_[2]{};
|
|
};
|
|
|
|
struct PadState {
|
|
uint32_t buttons = 0;
|
|
int16_t axes[8] = {};
|
|
};
|
|
enum : uint32_t {
|
|
PAD_A = 1u << 0, PAD_B = 1u << 1, PAD_X = 1u << 2, PAD_Y = 1u << 3, PAD_LB = 1u << 4, PAD_RB = 1u << 5,
|
|
PAD_SELECT = 1u << 6, PAD_START = 1u << 7, PAD_L3 = 1u << 8, PAD_R3 = 1u << 9, PAD_GUIDE = 1u << 10,
|
|
};
|
|
|
|
class PadRing {
|
|
public:
|
|
bool Read(PadState *out) {
|
|
if (!map_ && !Open()) return false;
|
|
for (int tries = 0; tries < 8; tries++) {
|
|
uint64_t a = __atomic_load_n((const uint64_t *)(map_ + 32), __ATOMIC_ACQUIRE);
|
|
if (a & 1u) continue;
|
|
memcpy(&out->buttons, (const void *)(map_ + 40), 4);
|
|
memcpy(out->axes, (const void *)(map_ + 44), 16);
|
|
__atomic_thread_fence(__ATOMIC_ACQUIRE);
|
|
if (__atomic_load_n((const uint64_t *)(map_ + 32), __ATOMIC_RELAXED) == a) return true;
|
|
}
|
|
return false;
|
|
}
|
|
~PadRing() { if (map_) munmap((void *)map_, 64); }
|
|
|
|
private:
|
|
bool Open() {
|
|
auto now = std::chrono::steady_clock::now();
|
|
if (now < retry_) return false;
|
|
retry_ = now + std::chrono::seconds(2);
|
|
std::string path;
|
|
if (const char *env = getenv("DROIDDECK_VR_PAD_RING")) path = env;
|
|
if (path.empty()) {
|
|
const char *paths = getenv("FAKE_EVDEV_MEMFD_PATHS");
|
|
if (paths && !strncmp(paths, "0=", 2)) path.assign(paths + 2, strcspn(paths + 2, ";"));
|
|
}
|
|
if (path.empty()) {
|
|
if (const char *dir = getenv("BL_LAUNCH_DIR")) path = std::string(dir) + "/dev/fakeinput-rings/ring0";
|
|
}
|
|
if (path.empty()) return false;
|
|
int fd = open(path.c_str(), O_RDONLY | O_CLOEXEC);
|
|
if (fd < 0) return false;
|
|
void *p = mmap(nullptr, 64, PROT_READ, MAP_SHARED, fd, 0);
|
|
close(fd);
|
|
if (p == MAP_FAILED) return false;
|
|
uint32_t magic;
|
|
memcpy(&magic, p, 4);
|
|
if (magic != 0x46494252u) { munmap(p, 64); return false; }
|
|
map_ = (const volatile uint8_t *)p;
|
|
Log("game pad ring mapped: %s", path.c_str());
|
|
return true;
|
|
}
|
|
const volatile uint8_t *map_ = nullptr;
|
|
std::chrono::steady_clock::time_point retry_{};
|
|
};
|
|
|
|
template <typename T>
|
|
class ShmFile {
|
|
public:
|
|
ShmFile(const char *path, uint32_t magic, uint32_t version, const char *name)
|
|
: path_(path), magic_(magic), version_(version), name_(name) {}
|
|
bool Read(T *out) {
|
|
if (!map_ && !Open()) return false;
|
|
for (int tries = 0; tries < 64; tries++) {
|
|
if (tries) sched_yield();
|
|
const uint32_t a = __atomic_load_n(&map_->seq, __ATOMIC_ACQUIRE);
|
|
if (a & 1u) continue;
|
|
memcpy(out, (const void *)map_, sizeof(*out));
|
|
__atomic_thread_fence(__ATOMIC_ACQUIRE);
|
|
if (__atomic_load_n(&map_->seq, __ATOMIC_RELAXED) == a) return out->magic == magic_ && out->version == version_;
|
|
}
|
|
return false;
|
|
}
|
|
~ShmFile() { if (map_) munmap((void *)map_, sizeof(*map_)); }
|
|
|
|
private:
|
|
bool Open() {
|
|
const auto now = std::chrono::steady_clock::now();
|
|
if (now < retry_) return false;
|
|
retry_ = now + std::chrono::seconds(2);
|
|
int fd = open(path_, O_RDONLY | O_CLOEXEC);
|
|
if (fd < 0) return false;
|
|
struct stat st;
|
|
if (fstat(fd, &st) != 0 || st.st_size < (off_t)sizeof(T)) {
|
|
close(fd);
|
|
return false;
|
|
}
|
|
void *p = mmap(nullptr, sizeof(T), PROT_READ, MAP_SHARED, fd, 0);
|
|
close(fd);
|
|
if (p == MAP_FAILED) return false;
|
|
map_ = (volatile T *)p;
|
|
Log("%s file mapped: %s", name_, path_);
|
|
return true;
|
|
}
|
|
const char *const path_;
|
|
const uint32_t magic_, version_;
|
|
const char *const name_;
|
|
volatile T *map_ = nullptr;
|
|
std::chrono::steady_clock::time_point retry_{};
|
|
};
|
|
|
|
void SendRumble(uint16_t slot, const VREvent_HapticVibration_t &h) {
|
|
if (!(h.fAmplitude > 0.0f) || !(h.fFrequency > 0.0f)) return;
|
|
const float seconds = std::isfinite(h.fDurationSeconds) ? h.fDurationSeconds : 0.0f;
|
|
const uint16_t ms = (uint16_t)std::clamp(seconds * 1000.0f, kMinPulseMs, 5000.0f);
|
|
const uint16_t strength = (uint16_t)std::lround(std::min(h.fAmplitude, 1.0f) * 65535.0f);
|
|
const int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
|
|
if (fd < 0) return;
|
|
sockaddr_un addr = {};
|
|
addr.sun_family = AF_UNIX;
|
|
const char name[] = "droiddeck-rumble";
|
|
memcpy(addr.sun_path + 1, name, sizeof(name) - 1);
|
|
const socklen_t length = (socklen_t)(offsetof(sockaddr_un, sun_path) + sizeof(name));
|
|
if (connect(fd, (const sockaddr *)&addr, length) == 0) {
|
|
const uint16_t data[4] = {strength, strength, ms, slot};
|
|
send(fd, data, sizeof(data), MSG_DONTWAIT | MSG_NOSIGNAL);
|
|
}
|
|
close(fd);
|
|
}
|
|
|
|
int64_t MonotonicNs() {
|
|
return std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
|
|
}
|
|
|
|
bool JoyConLive(const droiddeck_joycon_hand &hand, int64_t nowNs) {
|
|
return (hand.flags & DROIDDECK_JOYCON_CONNECTED) && hand.time_ns > 0 &&
|
|
nowNs - hand.time_ns < kJoyConStaleNs && hand.time_ns - nowNs < kJoyConStaleNs;
|
|
}
|
|
|
|
class HandController : public ITrackedDeviceServerDriver {
|
|
public:
|
|
explicit HandController(bool right) : right_(right), arm_(right), camera_(right), luma_(right) {}
|
|
|
|
EVRInitError Activate(uint32_t id) override {
|
|
auto props = VRProperties();
|
|
const PropertyContainerHandle_t c = props->TrackedDeviceToPropertyContainer(id);
|
|
props->SetStringProperty(c, Prop_ModelNumber_String, right_ ? "Knuckles Right" : "Knuckles Left");
|
|
props->SetStringProperty(c, Prop_ManufacturerName_String, "DroidDeck");
|
|
props->SetStringProperty(c, Prop_TrackingSystemName_String, "droiddeck");
|
|
props->SetStringProperty(c, Prop_RenderModelName_String, RenderModel(expectBare_.load()));
|
|
props->SetStringProperty(c, Prop_ControllerType_String, "knuckles");
|
|
props->SetStringProperty(c, Prop_InputProfilePath_String, "{indexcontroller}/input/index_controller_profile.json");
|
|
props->SetStringProperty(c, Prop_RegisteredDeviceType_String, right_ ? "droiddeck/hand_right" : "droiddeck/hand_left");
|
|
props->SetInt32Property(c, Prop_ControllerRoleHint_Int32,
|
|
right_ ? TrackedControllerRole_RightHand : TrackedControllerRole_LeftHand);
|
|
props->SetInt32Property(c, Prop_DeviceClass_Int32, TrackedDeviceClass_Controller);
|
|
props->SetInt32Property(c, Prop_ControllerHandSelectionPriority_Int32, 100);
|
|
props->SetBoolProperty(c, Prop_DeviceProvidesBatteryStatus_Bool, true);
|
|
props->SetBoolProperty(c, Prop_DeviceCanPowerOff_Bool, false);
|
|
container_ = c;
|
|
pose_.qWorldFromDriverRotation.w = 1;
|
|
pose_.qDriverFromHeadRotation.w = 1;
|
|
pose_.qRotation.w = 1;
|
|
pose_.result = TrackingResult_Uninitialized;
|
|
auto in = VRDriverInput();
|
|
const auto boolean = [&](const char *path, VRInputComponentHandle_t *h) { in->CreateBooleanComponent(c, path, h); };
|
|
const auto oneSided = [&](const char *path, VRInputComponentHandle_t *h) {
|
|
in->CreateScalarComponent(c, path, h, VRScalarType_Absolute, VRScalarUnits_NormalizedOneSided);
|
|
};
|
|
const auto twoSided = [&](const char *path, VRInputComponentHandle_t *h) {
|
|
in->CreateScalarComponent(c, path, h, VRScalarType_Absolute, VRScalarUnits_NormalizedTwoSided);
|
|
};
|
|
boolean("/input/system/click", &systemClick_);
|
|
boolean("/input/system/touch", &systemTouch_);
|
|
boolean("/input/a/click", &aClick_);
|
|
boolean("/input/a/touch", &aTouch_);
|
|
boolean("/input/b/click", &bClick_);
|
|
boolean("/input/b/touch", &bTouch_);
|
|
boolean("/input/trigger/click", &triggerClick_);
|
|
boolean("/input/trigger/touch", &triggerTouch_);
|
|
oneSided("/input/trigger/value", &trigger_);
|
|
boolean("/input/grip/touch", &gripTouch_);
|
|
oneSided("/input/grip/value", &grip_);
|
|
oneSided("/input/grip/force", &gripForce_);
|
|
twoSided("/input/thumbstick/x", &stickX_);
|
|
twoSided("/input/thumbstick/y", &stickY_);
|
|
boolean("/input/thumbstick/click", &stickClick_);
|
|
boolean("/input/thumbstick/touch", &stickTouch_);
|
|
twoSided("/input/trackpad/x", &padX_);
|
|
twoSided("/input/trackpad/y", &padY_);
|
|
oneSided("/input/trackpad/force", &padForce_);
|
|
boolean("/input/trackpad/touch", &padTouch_);
|
|
oneSided("/input/finger/index", &fingers_[0]);
|
|
oneSided("/input/finger/middle", &fingers_[1]);
|
|
oneSided("/input/finger/ring", &fingers_[2]);
|
|
oneSided("/input/finger/pinky", &fingers_[3]);
|
|
in->CreateSkeletonComponent(c, right_ ? "/input/skeleton/right" : "/input/skeleton/left",
|
|
right_ ? "/skeleton/hand/right" : "/skeleton/hand/left", "/pose/raw",
|
|
VRSkeletalTracking_Estimated, nullptr, 0, &skeleton_);
|
|
in->CreateHapticComponent(c, "/output/haptic", &haptic_);
|
|
SendSkeleton();
|
|
id_.store(id, std::memory_order_release);
|
|
Log("%s hand activated", right_ ? "right" : "left");
|
|
return VRInitError_None;
|
|
}
|
|
void Deactivate() override { id_.store(k_unTrackedDeviceIndexInvalid, std::memory_order_release); }
|
|
void EnterStandby() override {}
|
|
void *GetComponent(const char *) override { return nullptr; }
|
|
void DebugRequest(const char *, char *resp, uint32_t size) override { if (size) resp[0] = 0; }
|
|
|
|
DriverPose_t GetPose() override {
|
|
std::lock_guard<std::mutex> lock(poseMutex_);
|
|
return pose_;
|
|
}
|
|
|
|
void ExpectBare(bool bare) { expectBare_ = bare; }
|
|
|
|
VRInputComponentHandle_t Haptic() const {
|
|
return id_.load(std::memory_order_acquire) == k_unTrackedDeviceIndexInvalid || bare_.load() ? 0 : haptic_;
|
|
}
|
|
|
|
void SeeCamera(const Vec3 &fromHead, const Vec3 *joints, int64_t timeNs, bool estimated, int64_t nowNs) {
|
|
if (connected_) {
|
|
if (!aligning_) kalman_.Measure(timeNs, kHeadPosition + fromHead, estimated ? kEstimatedSigma : kCameraSigma);
|
|
} else if (bare_.load()) {
|
|
camera_.See(timeNs, kHeadPosition + fromHead, joints, kHeadPosition, nowNs);
|
|
}
|
|
}
|
|
|
|
bool Update(const droiddeck_joycon_hand *hand, bool bare, const HmdQuaternion_t &headPose, int64_t nowNs,
|
|
const droiddeck_spatialpose *spatial = nullptr) {
|
|
const uint32_t id = id_.load(std::memory_order_acquire);
|
|
if (id == k_unTrackedDeviceIndexInvalid) return false;
|
|
const auto now = std::chrono::steady_clock::now();
|
|
const double dt = std::min(std::chrono::duration<double>(now - lastUpdate_).count(), 0.1);
|
|
lastUpdate_ = now;
|
|
if (lumaMode_ != (spatial != nullptr) || (spatial &&
|
|
(lumaConnection_ != spatial->connection || lumaOrigin_ != spatial->origin))) {
|
|
Disconnect(id);
|
|
luma_.Reset();
|
|
lumaMode_ = spatial != nullptr;
|
|
if (spatial) { lumaConnection_ = spatial->connection; lumaOrigin_ = spatial->origin; }
|
|
}
|
|
if (spatial && !SpatialFresh(spatial->head, nowNs)) {
|
|
if (connected_ && hand) {
|
|
luma_.Reset();
|
|
lumaSeen_ = false;
|
|
DriverPose_t p = GetPose();
|
|
p.poseIsValid = false;
|
|
p.result = TrackingResult_Running_OutOfRange;
|
|
p.poseTimeOffset = 0;
|
|
std::fill_n(p.vecVelocity, 3, 0.0);
|
|
std::fill_n(p.vecAngularVelocity, 3, 0.0);
|
|
droiddeck_joycon_hand idle{};
|
|
SendInputs(idle, 1.0);
|
|
{
|
|
std::lock_guard<std::mutex> lock(poseMutex_);
|
|
pose_ = p;
|
|
}
|
|
VRServerDriverHost()->TrackedDevicePoseUpdated(id, p, sizeof(p));
|
|
} else Disconnect(id);
|
|
LeaveBare(id);
|
|
return true;
|
|
}
|
|
if (!hand) {
|
|
Disconnect(id);
|
|
if (bare) {
|
|
UpdateBare(id, nowNs, dt);
|
|
return true;
|
|
}
|
|
LeaveBare(id);
|
|
return false;
|
|
}
|
|
LeaveBare(id);
|
|
const Quat head{headPose.w, headPose.x, headPose.y, headPose.z};
|
|
if (spatial) {
|
|
double norm = 0;
|
|
for (float v : hand->rotation) norm += v * v;
|
|
if (!std::isfinite(norm) || norm < 0.5 || norm > 1.5 ||
|
|
!std::all_of(std::begin(hand->angular_velocity), std::end(hand->angular_velocity),
|
|
[](float v) { return std::isfinite(v); })) {
|
|
Disconnect(id);
|
|
return true;
|
|
}
|
|
}
|
|
const Quat q = Normalize({hand->rotation[0], hand->rotation[1], hand->rotation[2], hand->rotation[3]});
|
|
if (!connected_ || hand->recenter != recenter_) {
|
|
recenter_ = hand->recenter;
|
|
aligning_ = true;
|
|
}
|
|
connected_ = true;
|
|
double headYaw = 0.0, handYaw = 0.0;
|
|
if (aligning_ && Heading(head, &headYaw) && Heading(q, &handYaw)) {
|
|
yaw_ = headYaw - handYaw;
|
|
aligning_ = false;
|
|
arm_.Reset();
|
|
follower_.Reset();
|
|
kalman_.Reset();
|
|
luma_.Reset();
|
|
shown_ = false;
|
|
Log("%s Joy-Con facing forward", right_ ? "right" : "left");
|
|
}
|
|
const Quat turn = AboutY(yaw_);
|
|
const Quat aligned = turn * q;
|
|
const Vec3 rate{hand->angular_velocity[0], hand->angular_velocity[1], hand->angular_velocity[2]};
|
|
Vec3 grip, velocity;
|
|
if (spatial) {
|
|
const auto tracked = luma_.Update(*spatial, aligned, Length(rate), nowNs, dt);
|
|
if (!tracked.valid || aligning_) {
|
|
Disconnect(id);
|
|
return true;
|
|
}
|
|
grip = tracked.position;
|
|
if (!lumaSeen_ || lumaCamera_ != tracked.camera) {
|
|
Log("%s Luma Joy-Con position: %s", right_ ? "right" : "left",
|
|
tracked.camera ? "stereo hand" : "6DoF arm estimate");
|
|
lumaSeen_ = true; lumaCamera_ = tracked.camera;
|
|
}
|
|
} else {
|
|
const Vec3 palm = arm_.Update(kHeadPosition, head, aligned, Length(rate), dt);
|
|
const Vec3 moved = Rotate(turn, {hand->displacement[0], hand->displacement[1], hand->displacement[2]});
|
|
const Vec3 inertial = Rotate(turn, {hand->inertial_velocity[0], hand->inertial_velocity[1], hand->inertial_velocity[2]});
|
|
const bool holding = (hand->buttons & (DROIDDECK_JOYCON_TRIGGER | DROIDDECK_JOYCON_GRIP)) != 0;
|
|
kalman_.Input(hand->time_ns - kJoyConDelayNs, inertial, Length(rate));
|
|
const bool camera = kalman_.Ready() && kalman_.TimeNs() - kalman_.SeenNs() <= kCameraHoldNs;
|
|
bool switched = false;
|
|
if (camera != cameraPlaced_) {
|
|
cameraPlaced_ = camera;
|
|
switched = true;
|
|
Log("%s Joy-Con hand placed by %s", right_ ? "right" : "left", camera ? "the camera" : "the arm model");
|
|
if (!camera && shown_) follower_.Place(shownAt_, moved);
|
|
}
|
|
const double settle = holding ? kHoldingSettleSeconds : kSettleSeconds;
|
|
const Vec3 source = camera ? kalman_.Position() : follower_.Update(palm, moved, dt, settle, kMaxOffset);
|
|
velocity = camera ? kalman_.Velocity() : Rotate(turn, {hand->velocity[0], hand->velocity[1], hand->velocity[2]});
|
|
if (shown_ && (switched || Length(source - source_) > kJump)) blend_ = shownAt_ - source;
|
|
else if (!shown_) blend_ = Vec3{};
|
|
blend_ = blend_ * std::exp(-dt / kBlendSeconds);
|
|
grip = source + blend_;
|
|
source_ = source;
|
|
shownAt_ = grip;
|
|
shown_ = true;
|
|
if (FILE *trace = gHandTrace.File()) {
|
|
const Vec3 k = kalman_.Ready() ? kalman_.Position() : Vec3{};
|
|
fprintf(trace, "J,%lld,%d,%lld,%.4f,%.4f,%.4f,%.4f,%.4f,%.4f,%d,%.4f,%.4f,%.4f,%.4f,%.4f,%.4f,%.3f,%.4f,%.4f,%.4f,%u,%.4f,%.4f,%.4f,%.4f,%.4f,%.4f,%.4f,%d\n",
|
|
(long long)nowNs, right_ ? 1 : 0, (long long)hand->time_ns, moved.x, moved.y, moved.z, palm.x, palm.y, palm.z,
|
|
camera ? 1 : 0, k.x, k.y, k.z, grip.x, grip.y, grip.z, Length(rate), velocity.x, velocity.y, velocity.z,
|
|
hand->buttons, aligned.w, aligned.x, aligned.y, aligned.z, inertial.x, inertial.y, inertial.z, kalman_.Ready() ? 1 : 0);
|
|
}
|
|
}
|
|
Quat raw;
|
|
Vec3 position;
|
|
IndexRawPose(right_, aligned, grip, &raw, &position);
|
|
const Vec3 spin = Rotate(aligned, rate);
|
|
DriverPose_t p = {};
|
|
p.qWorldFromDriverRotation.w = 1;
|
|
p.qDriverFromHeadRotation.w = 1;
|
|
p.qRotation = {raw.w, raw.x, raw.y, raw.z};
|
|
p.vecPosition[0] = position.x;
|
|
p.vecPosition[1] = position.y;
|
|
p.vecPosition[2] = position.z;
|
|
p.vecVelocity[0] = velocity.x;
|
|
p.vecVelocity[1] = velocity.y;
|
|
p.vecVelocity[2] = velocity.z;
|
|
p.vecAngularVelocity[0] = spin.x;
|
|
p.vecAngularVelocity[1] = spin.y;
|
|
p.vecAngularVelocity[2] = spin.z;
|
|
p.poseTimeOffset = spatial ? 0.0 : -std::max(0.0, (nowNs - hand->time_ns) * 1e-9);
|
|
p.willDriftInYaw = true;
|
|
p.poseIsValid = true;
|
|
p.result = TrackingResult_Running_OK;
|
|
p.deviceIsConnected = true;
|
|
{
|
|
std::lock_guard<std::mutex> lock(poseMutex_);
|
|
pose_ = p;
|
|
}
|
|
VRServerDriverHost()->TrackedDevicePoseUpdated(id, p, sizeof(p));
|
|
SendInputs(*hand, dt);
|
|
SendBattery(*hand);
|
|
return true;
|
|
}
|
|
|
|
private:
|
|
void UpdateBare(uint32_t id, int64_t nowNs, double dt) {
|
|
if (!bare_.load()) {
|
|
bare_ = true;
|
|
camera_.Reset();
|
|
bareSeen_ = false;
|
|
VRProperties()->SetBoolProperty(container_, Prop_DeviceProvidesBatteryStatus_Bool, false);
|
|
VRProperties()->SetStringProperty(container_, Prop_RenderModelName_String, RenderModel(true));
|
|
Log("%s hand follows the camera (no controller connected)", right_ ? "right" : "left");
|
|
}
|
|
const CameraHandPose hand = camera_.Update(nowNs, dt);
|
|
if (hand.visible != bareSeen_) {
|
|
bareSeen_ = hand.visible;
|
|
Log("%s camera hand %s", right_ ? "right" : "left", hand.visible ? "seen" : "lost");
|
|
}
|
|
DriverPose_t p;
|
|
{
|
|
std::lock_guard<std::mutex> lock(poseMutex_);
|
|
p = pose_;
|
|
}
|
|
p.qWorldFromDriverRotation = {1, 0, 0, 0};
|
|
p.qDriverFromHeadRotation = {1, 0, 0, 0};
|
|
p.deviceIsConnected = true;
|
|
p.willDriftInYaw = false;
|
|
p.poseTimeOffset = 0.0;
|
|
for (int i = 0; i < 3; i++) p.vecAngularVelocity[i] = 0.0;
|
|
if (hand.visible) {
|
|
p.qRotation = {hand.rotation.w, hand.rotation.x, hand.rotation.y, hand.rotation.z};
|
|
p.vecPosition[0] = hand.position.x;
|
|
p.vecPosition[1] = hand.position.y;
|
|
p.vecPosition[2] = hand.position.z;
|
|
p.vecVelocity[0] = hand.velocity.x;
|
|
p.vecVelocity[1] = hand.velocity.y;
|
|
p.vecVelocity[2] = hand.velocity.z;
|
|
p.poseIsValid = true;
|
|
p.result = TrackingResult_Running_OK;
|
|
} else {
|
|
for (int i = 0; i < 3; i++) p.vecVelocity[i] = 0.0;
|
|
p.poseIsValid = false;
|
|
p.result = TrackingResult_Running_OutOfRange;
|
|
}
|
|
{
|
|
std::lock_guard<std::mutex> lock(poseMutex_);
|
|
pose_ = p;
|
|
}
|
|
VRServerDriverHost()->TrackedDevicePoseUpdated(id, p, sizeof(p));
|
|
if (FILE *trace = gHandTrace.File()) {
|
|
const HandButtons &b = hand.buttons;
|
|
fprintf(trace, "B,%lld,%d,%d,%.4f,%.4f,%.4f,%.4f,%.4f,%.4f,%.4f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%d%d%d%d%d%d%d,%.4f\n",
|
|
(long long)nowNs, right_ ? 1 : 0, hand.visible ? 1 : 0, hand.position.x, hand.position.y, hand.position.z,
|
|
hand.rotation.w, hand.rotation.x, hand.rotation.y, hand.rotation.z, hand.curls.thumb, hand.curls.index,
|
|
hand.curls.middle, hand.curls.ring, hand.curls.pinky, b.trigger, b.grip, b.triggerClick, b.gripClick, b.a, b.b,
|
|
b.system, b.facing, b.triggerTouch, camera_.Rms());
|
|
}
|
|
SendBareInputs(hand);
|
|
}
|
|
|
|
void LeaveBare(uint32_t id) {
|
|
if (!bare_.load()) return;
|
|
bare_ = false;
|
|
camera_.Reset();
|
|
VRProperties()->SetBoolProperty(container_, Prop_DeviceProvidesBatteryStatus_Bool, true);
|
|
VRProperties()->SetStringProperty(container_, Prop_RenderModelName_String, RenderModel(false));
|
|
if (!connected_) {
|
|
DriverPose_t p = {};
|
|
p.qWorldFromDriverRotation.w = 1;
|
|
p.qDriverFromHeadRotation.w = 1;
|
|
p.qRotation.w = 1;
|
|
p.result = TrackingResult_Uninitialized;
|
|
{
|
|
std::lock_guard<std::mutex> lock(poseMutex_);
|
|
pose_ = p;
|
|
}
|
|
VRServerDriverHost()->TrackedDevicePoseUpdated(id, p, sizeof(p));
|
|
droiddeck_joycon_hand idle = {};
|
|
SendInputs(idle, 1.0);
|
|
}
|
|
Log("%s hand no longer follows the camera", right_ ? "right" : "left");
|
|
}
|
|
|
|
void SendBareInputs(const CameraHandPose &hand) {
|
|
const HandButtons &b = hand.buttons;
|
|
SetBool(systemClick_, b.system);
|
|
SetBool(systemTouch_, b.system);
|
|
SetBool(aClick_, b.a);
|
|
SetBool(aTouch_, b.a);
|
|
SetBool(bClick_, b.b);
|
|
SetBool(bTouch_, b.b);
|
|
SetBool(triggerClick_, b.triggerClick);
|
|
SetBool(triggerTouch_, b.triggerTouch);
|
|
SetScalar(trigger_, b.trigger);
|
|
SetBool(gripTouch_, b.grip >= kGripTouch);
|
|
SetScalar(grip_, b.grip);
|
|
SetScalar(gripForce_, b.gripClick ? 1.0f : 0.0f);
|
|
SetScalar(stickX_, 0.0f);
|
|
SetScalar(stickY_, 0.0f);
|
|
SetBool(stickClick_, false);
|
|
SetBool(stickTouch_, false);
|
|
SetScalar(padX_, 0.0f);
|
|
SetScalar(padY_, 0.0f);
|
|
SetScalar(padForce_, 0.0f);
|
|
SetBool(padTouch_, false);
|
|
if (hand.visible) curls_ = hand.curls;
|
|
SetScalar(fingers_[0], (float)curls_.index);
|
|
SetScalar(fingers_[1], (float)curls_.middle);
|
|
SetScalar(fingers_[2], (float)curls_.ring);
|
|
SetScalar(fingers_[3], (float)curls_.pinky);
|
|
if (std::chrono::steady_clock::now() - lastSkeleton_ >= kSkeletonPeriod) SendSkeleton();
|
|
}
|
|
|
|
const char *RenderModel(bool bare) const {
|
|
if (bare) return right_ ? "{droiddeck}droiddeck_hand_right" : "{droiddeck}droiddeck_hand_left";
|
|
return right_ ? "{indexcontroller}valve_controller_knu_1_0_right" : "{indexcontroller}valve_controller_knu_1_0_left";
|
|
}
|
|
|
|
void Disconnect(uint32_t id) {
|
|
if (!connected_) return;
|
|
connected_ = false;
|
|
lumaSeen_ = false;
|
|
follower_.Reset();
|
|
kalman_.Reset();
|
|
luma_.Reset();
|
|
shown_ = false;
|
|
DriverPose_t p = {};
|
|
p.qWorldFromDriverRotation.w = 1;
|
|
p.qDriverFromHeadRotation.w = 1;
|
|
p.qRotation.w = 1;
|
|
p.result = TrackingResult_Uninitialized;
|
|
{
|
|
std::lock_guard<std::mutex> lock(poseMutex_);
|
|
pose_ = p;
|
|
}
|
|
droiddeck_joycon_hand idle = {};
|
|
SendInputs(idle, 1.0);
|
|
VRServerDriverHost()->TrackedDevicePoseUpdated(id, p, sizeof(p));
|
|
Log("%s Joy-Con hand disconnected", right_ ? "right" : "left");
|
|
}
|
|
|
|
void SendInputs(const droiddeck_joycon_hand &hand, double dt) {
|
|
const uint32_t b = hand.buttons;
|
|
const float x = std::clamp(hand.stick[0], -1.0f, 1.0f), y = std::clamp(hand.stick[1], -1.0f, 1.0f);
|
|
const bool stickTouched = (b & DROIDDECK_JOYCON_STICK) || x * x + y * y > kStickTouch * kStickTouch;
|
|
const bool trigger = b & DROIDDECK_JOYCON_TRIGGER, grip = b & DROIDDECK_JOYCON_GRIP;
|
|
const bool thumb = stickTouched || (b & (DROIDDECK_JOYCON_A | DROIDDECK_JOYCON_B | DROIDDECK_JOYCON_SYSTEM)) != 0;
|
|
const bool system = b & DROIDDECK_JOYCON_SYSTEM, a = b & DROIDDECK_JOYCON_A, bButton = b & DROIDDECK_JOYCON_B;
|
|
SetBool(systemClick_, system);
|
|
SetBool(systemTouch_, system);
|
|
SetBool(aClick_, a);
|
|
SetBool(aTouch_, a);
|
|
SetBool(bClick_, bButton);
|
|
SetBool(bTouch_, bButton);
|
|
SetBool(triggerClick_, trigger);
|
|
SetBool(triggerTouch_, trigger);
|
|
SetScalar(trigger_, trigger ? 1.0f : 0.0f);
|
|
SetBool(gripTouch_, grip);
|
|
SetScalar(grip_, grip ? 1.0f : 0.0f);
|
|
SetScalar(gripForce_, grip ? 1.0f : 0.0f);
|
|
SetScalar(stickX_, x);
|
|
SetScalar(stickY_, y);
|
|
SetBool(stickClick_, (b & DROIDDECK_JOYCON_STICK) != 0);
|
|
SetBool(stickTouch_, stickTouched);
|
|
SetScalar(padX_, 0.0f);
|
|
SetScalar(padY_, 0.0f);
|
|
SetScalar(padForce_, 0.0f);
|
|
SetBool(padTouch_, false);
|
|
const double follow = 1.0 - std::exp(-dt / kCurlSeconds);
|
|
curls_.thumb += ((thumb ? 1.0 : 0.0) - curls_.thumb) * follow;
|
|
curls_.index += ((trigger ? 1.0 : 0.0) - curls_.index) * follow;
|
|
const double rest = grip ? 1.0 : 0.0;
|
|
curls_.middle += (rest - curls_.middle) * follow;
|
|
curls_.ring = curls_.pinky = curls_.middle;
|
|
SetScalar(fingers_[0], (float)curls_.index);
|
|
for (int f = 1; f < 4; f++) SetScalar(fingers_[f], (float)curls_.middle);
|
|
const auto now = std::chrono::steady_clock::now();
|
|
if (now - lastSkeleton_ >= kSkeletonPeriod) SendSkeleton();
|
|
}
|
|
|
|
void SendSkeleton() {
|
|
lastSkeleton_ = std::chrono::steady_clock::now();
|
|
VRBoneTransform_t bones[kHandBones];
|
|
HandSkeleton(right_, curls_, bones);
|
|
VRDriverInput()->UpdateSkeletonComponent(skeleton_, VRSkeletalMotionRange_WithController, bones, kHandBones);
|
|
VRDriverInput()->UpdateSkeletonComponent(skeleton_, VRSkeletalMotionRange_WithoutController, bones, kHandBones);
|
|
}
|
|
|
|
void SendBattery(const droiddeck_joycon_hand &hand) {
|
|
const bool charging = hand.flags & DROIDDECK_JOYCON_CHARGING;
|
|
if (hand.battery >= 0.0f && std::fabs(hand.battery - battery_) > 0.005f) {
|
|
battery_ = hand.battery;
|
|
VRProperties()->SetFloatProperty(container_, Prop_DeviceBatteryPercentage_Float, std::min(battery_, 1.0f));
|
|
}
|
|
if (charging != charging_) {
|
|
charging_ = charging;
|
|
VRProperties()->SetBoolProperty(container_, Prop_DeviceIsCharging_Bool, charging_);
|
|
}
|
|
}
|
|
|
|
void SetBool(VRInputComponentHandle_t h, bool on) {
|
|
auto &last = booleans_[h];
|
|
if (last == (on ? 2 : 1)) return;
|
|
last = on ? 2 : 1;
|
|
VRDriverInput()->UpdateBooleanComponent(h, on, 0);
|
|
}
|
|
void SetScalar(VRInputComponentHandle_t h, float value) {
|
|
auto it = scalars_.find(h);
|
|
if (it != scalars_.end() && it->second == value) return;
|
|
scalars_[h] = value;
|
|
VRDriverInput()->UpdateScalarComponent(h, value, 0);
|
|
}
|
|
|
|
static constexpr float kStickTouch = 0.1f, kGripTouch = 0.5f;
|
|
static constexpr double kCurlSeconds = 0.05;
|
|
static constexpr std::chrono::milliseconds kSkeletonPeriod{11};
|
|
static constexpr Vec3 kHeadPosition{0.0, 1.6, 0.0};
|
|
static constexpr double kSettleSeconds = 0.6, kHoldingSettleSeconds = 1.5, kMaxOffset = 0.35;
|
|
static constexpr double kCameraSigma = 0.03, kEstimatedSigma = 0.06, kJump = 0.08, kBlendSeconds = 0.25;
|
|
static constexpr int64_t kCameraHoldNs = 750000000, kJoyConDelayNs = 45000000;
|
|
const bool right_;
|
|
ArmModel arm_;
|
|
HandFollower follower_;
|
|
HandKalman kalman_;
|
|
CameraHand camera_;
|
|
LumaJoyConPosition luma_;
|
|
bool lumaMode_ = false, lumaSeen_ = false, lumaCamera_ = false;
|
|
uint64_t lumaConnection_ = 0, lumaOrigin_ = 0;
|
|
std::atomic<bool> bare_{false}, expectBare_{false};
|
|
bool bareSeen_ = false;
|
|
Vec3 source_, shownAt_, blend_;
|
|
bool shown_ = false;
|
|
bool cameraPlaced_ = false;
|
|
std::atomic<uint32_t> id_{k_unTrackedDeviceIndexInvalid};
|
|
PropertyContainerHandle_t container_ = k_ulInvalidPropertyContainer;
|
|
std::mutex poseMutex_;
|
|
DriverPose_t pose_ = {};
|
|
bool connected_ = false, aligning_ = true, charging_ = false;
|
|
uint32_t recenter_ = 0;
|
|
double yaw_ = 0.0;
|
|
float battery_ = -1.0f;
|
|
FingerCurls curls_;
|
|
std::map<VRInputComponentHandle_t, int> booleans_;
|
|
std::map<VRInputComponentHandle_t, float> scalars_;
|
|
std::chrono::steady_clock::time_point lastUpdate_ = std::chrono::steady_clock::now(), lastSkeleton_{};
|
|
VRInputComponentHandle_t systemClick_ = 0, systemTouch_ = 0, aClick_ = 0, aTouch_ = 0, bClick_ = 0, bTouch_ = 0,
|
|
triggerClick_ = 0, triggerTouch_ = 0, trigger_ = 0, gripTouch_ = 0, grip_ = 0, gripForce_ = 0,
|
|
stickX_ = 0, stickY_ = 0, stickClick_ = 0, stickTouch_ = 0, padX_ = 0, padY_ = 0, padForce_ = 0,
|
|
padTouch_ = 0, fingers_[4] = {}, skeleton_ = 0, haptic_ = 0;
|
|
};
|
|
|
|
class CardboardController : public ITrackedDeviceServerDriver {
|
|
public:
|
|
explicit CardboardController(bool right) : right_(right) {
|
|
pose_.qWorldFromDriverRotation.w = pose_.qDriverFromHeadRotation.w = pose_.qRotation.w = 1;
|
|
}
|
|
|
|
void SetProfile(PadProfile profile) { profile_ = profile; }
|
|
|
|
EVRInitError Activate(uint32_t id) override {
|
|
id_ = id;
|
|
PropertyContainerHandle_t c = VRProperties()->TrackedDeviceToPropertyContainer(id_);
|
|
XrTrackingFile tracking;
|
|
DroidDeckXrTracking packet{};
|
|
xrMode_ = tracking.Read(&packet) && XrTrackingFile::Fresh(packet);
|
|
const PadProfile profile = xrMode_ ? PadProfile::Touch : profile_;
|
|
touch_ = profile == PadProfile::Touch;
|
|
index_ = profile == PadProfile::Index;
|
|
const char *model = touch_ ? "DroidDeck Quest Touch" : index_ ? "DroidDeck Index Hand" : "DroidDeck Pad Hand";
|
|
VRProperties()->SetStringProperty(c, Prop_ModelNumber_String, model);
|
|
VRProperties()->SetStringProperty(c, Prop_ManufacturerName_String, touch_ ? "Meta" : index_ ? "Valve" : "DroidDeck");
|
|
VRProperties()->SetStringProperty(c, Prop_TrackingSystemName_String, "droiddeck");
|
|
VRProperties()->SetStringProperty(c, Prop_RenderModelName_String, touch_ ?
|
|
(right_ ? "oculus_quest_plus_controller_right" : "oculus_quest_plus_controller_left") :
|
|
(right_ ? "{indexcontroller}valve_controller_knu_1_0_right" : "{indexcontroller}valve_controller_knu_1_0_left"));
|
|
VRProperties()->SetStringProperty(c, Prop_ControllerType_String, touch_ ? "oculus_touch" : index_ ? "knuckles" : "vive_controller");
|
|
VRProperties()->SetStringProperty(c, Prop_InputProfilePath_String, touch_ ? "{oculus}/input/touch_profile.json" :
|
|
index_ ? "{indexcontroller}/input/index_controller_profile.json" : "{htc}/input/vive_controller_profile.json");
|
|
VRProperties()->SetInt32Property(c, Prop_ControllerRoleHint_Int32,
|
|
right_ ? TrackedControllerRole_RightHand : TrackedControllerRole_LeftHand);
|
|
VRProperties()->SetInt32Property(c, Prop_DeviceClass_Int32, TrackedDeviceClass_Controller);
|
|
auto in = VRDriverInput();
|
|
const bool stick = touch_ || index_;
|
|
const char *stickPath = touch_ ? "/input/joystick" : "/input/thumbstick";
|
|
const auto path = [](const char *base, const char *leaf) { return std::string(base) + leaf; };
|
|
in->CreateBooleanComponent(c, "/input/system/click", &system_);
|
|
if (!index_) in->CreateBooleanComponent(c, "/input/application_menu/click", &menu_);
|
|
in->CreateBooleanComponent(c, stick ? "/input/grip/touch" : "/input/grip/click", &grip_);
|
|
in->CreateBooleanComponent(c, "/input/trigger/click", &triggerClick_);
|
|
in->CreateScalarComponent(c, "/input/trigger/value", &trigger_, VRScalarType_Absolute, VRScalarUnits_NormalizedOneSided);
|
|
const char *axisBase = stick ? stickPath : "/input/trackpad";
|
|
in->CreateScalarComponent(c, path(axisBase, "/x").c_str(), &padX_, VRScalarType_Absolute, VRScalarUnits_NormalizedTwoSided);
|
|
in->CreateScalarComponent(c, path(axisBase, "/y").c_str(), &padY_, VRScalarType_Absolute, VRScalarUnits_NormalizedTwoSided);
|
|
in->CreateBooleanComponent(c, path(axisBase, "/click").c_str(), &padClick_);
|
|
in->CreateBooleanComponent(c, path(axisBase, "/touch").c_str(), &padTouch_);
|
|
if (stick) {
|
|
const bool letters = touch_ && !right_;
|
|
in->CreateScalarComponent(c, "/input/grip/value", &gripValue_, VRScalarType_Absolute, VRScalarUnits_NormalizedOneSided);
|
|
if (index_) in->CreateScalarComponent(c, "/input/grip/force", &gripForce_, VRScalarType_Absolute, VRScalarUnits_NormalizedOneSided);
|
|
in->CreateBooleanComponent(c, "/input/trigger/touch", &triggerTouch_);
|
|
in->CreateBooleanComponent(c, letters ? "/input/x/click" : "/input/a/click", &primaryClick_);
|
|
in->CreateBooleanComponent(c, letters ? "/input/y/click" : "/input/b/click", &secondaryClick_);
|
|
in->CreateBooleanComponent(c, letters ? "/input/x/touch" : "/input/a/touch", &primaryTouch_);
|
|
in->CreateBooleanComponent(c, letters ? "/input/y/touch" : "/input/b/touch", &secondaryTouch_);
|
|
if (touch_) in->CreateBooleanComponent(c, "/input/thumbrest/touch", &thumbrestTouch_);
|
|
}
|
|
in->CreateHapticComponent(c, "/output/haptic", &haptic_);
|
|
Log("%s hand activated (%s)", right_ ? "right" : "left", xrMode_ ? "Quest Touch" : model);
|
|
return VRInitError_None;
|
|
}
|
|
void Deactivate() override { ClearInputs(); id_ = k_unTrackedDeviceIndexInvalid; }
|
|
void EnterStandby() override {}
|
|
void *GetComponent(const char *) override { return nullptr; }
|
|
void DebugRequest(const char *, char *resp, uint32_t size) override { if (size) resp[0] = 0; }
|
|
|
|
DriverPose_t GetPose() override { return pose_; }
|
|
|
|
void ClearInputs() {
|
|
const bool optical = opticalActive_;
|
|
opticalInputs_.Reset();
|
|
opticalPose_.Reset();
|
|
steadyHandAim_ = false;
|
|
opticalActive_ = false;
|
|
lastOpticalState_ = ~0u;
|
|
if (id_ == k_unTrackedDeviceIndexInvalid) return;
|
|
auto in = VRDriverInput();
|
|
EVRInputError error = VRInputError_None;
|
|
for (VRInputComponentHandle_t h : {system_, menu_, grip_, triggerClick_, padClick_, padTouch_, triggerTouch_,
|
|
primaryClick_, secondaryClick_, primaryTouch_, secondaryTouch_}) {
|
|
if (!h) continue;
|
|
const auto result = in->UpdateBooleanComponent(h, false, 0);
|
|
if (result != VRInputError_None) error = result;
|
|
}
|
|
for (VRInputComponentHandle_t h : {trigger_, padX_, padY_, gripValue_, gripForce_}) {
|
|
if (!h) continue;
|
|
const auto result = in->UpdateScalarComponent(h, 0.0f, 0);
|
|
if (result != VRInputError_None) error = result;
|
|
}
|
|
if (optical || error != VRInputError_None)
|
|
Log("%s controller inputs cleared: error=%d", right_ ? "right" : "left", int(error));
|
|
}
|
|
|
|
void UpdateXr(const DroidDeckXrHand &hand, int64_t predicted) {
|
|
if (id_ == k_unTrackedDeviceIndexInvalid) return;
|
|
DriverPose_t pose{};
|
|
pose.qWorldFromDriverRotation.w = pose.qDriverFromHeadRotation.w = 1;
|
|
pose.qRotation.w = 1;
|
|
pose.deviceIsConnected = hand.active;
|
|
pose.poseIsValid = hand.active && XrTrackingFile::Tracked(hand.pose);
|
|
if (pose.poseIsValid) {
|
|
pose.qRotation = {hand.pose.orientation[3], hand.pose.orientation[0], hand.pose.orientation[1], hand.pose.orientation[2]};
|
|
for (int axis = 0; axis < 3; axis++) pose.vecPosition[axis] = hand.pose.position[axis];
|
|
pose.poseTimeOffset = XrTrackingFile::PoseTimeOffset(predicted, XrTrackingFile::Now());
|
|
}
|
|
pose.result = pose.poseIsValid ? TrackingResult_Running_OK : TrackingResult_Running_OutOfRange;
|
|
pose_ = pose;
|
|
VRServerDriverHost()->TrackedDevicePoseUpdated(id_, pose, sizeof(pose));
|
|
XrTouchInput input = xr_touch_input(hand, right_);
|
|
auto driverInput = VRDriverInput();
|
|
driverInput->UpdateScalarComponent(trigger_, input.trigger, 0);
|
|
driverInput->UpdateBooleanComponent(triggerClick_, input.triggerClick, 0);
|
|
driverInput->UpdateBooleanComponent(grip_, input.gripClick, 0);
|
|
driverInput->UpdateScalarComponent(padX_, input.stickX, 0);
|
|
driverInput->UpdateScalarComponent(padY_, input.stickY, 0);
|
|
driverInput->UpdateBooleanComponent(padClick_, input.stickClick, 0);
|
|
driverInput->UpdateBooleanComponent(padTouch_, input.stickTouch, 0);
|
|
driverInput->UpdateBooleanComponent(menu_, false, 0);
|
|
driverInput->UpdateBooleanComponent(system_, input.systemClick, 0);
|
|
if (xrMode_) {
|
|
driverInput->UpdateScalarComponent(gripValue_, input.grip, 0);
|
|
driverInput->UpdateBooleanComponent(triggerTouch_, input.triggerTouch, 0);
|
|
driverInput->UpdateBooleanComponent(primaryClick_, input.primaryClick, 0);
|
|
driverInput->UpdateBooleanComponent(secondaryClick_, input.secondaryClick, 0);
|
|
driverInput->UpdateBooleanComponent(primaryTouch_, input.primaryTouch, 0);
|
|
driverInput->UpdateBooleanComponent(secondaryTouch_, input.secondaryTouch, 0);
|
|
driverInput->UpdateBooleanComponent(thumbrestTouch_, input.thumbrestTouch, 0);
|
|
}
|
|
}
|
|
|
|
VRInputComponentHandle_t Haptic() const { return id_ == k_unTrackedDeviceIndexInvalid ? 0 : haptic_; }
|
|
bool XrMode() const { return xrMode_; }
|
|
|
|
void Disconnect() {
|
|
ClearInputs();
|
|
if (id_ == k_unTrackedDeviceIndexInvalid) return;
|
|
DriverPose_t p = {};
|
|
p.qWorldFromDriverRotation.w = 1;
|
|
p.qDriverFromHeadRotation.w = 1;
|
|
p.qRotation.w = 1;
|
|
p.result = TrackingResult_Uninitialized;
|
|
pose_ = p;
|
|
VRServerDriverHost()->TrackedDevicePoseUpdated(id_, p, sizeof(p));
|
|
}
|
|
|
|
void UpdateOptical(const dd_spatial_hand &hand, bool headValid, const PadState *pad, int64_t now,
|
|
uint64_t connection, uint64_t origin, uint16_t inputEpochs = 0,
|
|
const dd_spatial_pose *aimHead = nullptr) {
|
|
if (id_ == k_unTrackedDeviceIndexInvalid) return;
|
|
opticalActive_ = true;
|
|
const bool inputValid = opticalInputs_.Update(hand, headValid, now, connection, origin, inputEpochs);
|
|
if (steadyHandAim_ != (aimHead != nullptr)) {
|
|
opticalPose_.Reset();
|
|
steadyHandAim_ = aimHead != nullptr;
|
|
}
|
|
auto visualHand = hand;
|
|
if (aimHead && headValid && SpatialFresh(*aimHead, now) && SpatialHandFresh(hand, now)) {
|
|
const auto &h = *aimHead;
|
|
const auto &p = hand.pose;
|
|
const auto q = droiddeck::ShoulderAim({h.position[0], h.position[1], h.position[2]},
|
|
{h.rotation[3], h.rotation[0], h.rotation[1], h.rotation[2]},
|
|
{p.position[0], p.position[1], p.position[2]}, right_,
|
|
{p.rotation[3], p.rotation[0], p.rotation[1], p.rotation[2]});
|
|
visualHand.pose.rotation[0] = q.x; visualHand.pose.rotation[1] = q.y;
|
|
visualHand.pose.rotation[2] = q.z; visualHand.pose.rotation[3] = q.w;
|
|
}
|
|
const bool valid = opticalPose_.Update(visualHand, headValid, now, connection, origin);
|
|
DriverPose_t p{};
|
|
p.qWorldFromDriverRotation.w = p.qDriverFromHeadRotation.w = p.qRotation.w = 1;
|
|
if (valid) {
|
|
const auto &observed = opticalPose_.rendered;
|
|
p.qRotation = {observed.rotation[3], observed.rotation[0], observed.rotation[1], observed.rotation[2]};
|
|
for (int i=0; i<3; ++i) p.vecPosition[i] = observed.position[i];
|
|
p.poseTimeOffset = double(observed.time_ns-now)/1e9;
|
|
}
|
|
p.deviceIsConnected = true; p.poseIsValid = valid;
|
|
p.result = valid ? TrackingResult_Running_OK : TrackingResult_Running_OutOfRange;
|
|
auto in = VRDriverInput();
|
|
const uint32_t b = pad ? pad->buttons : 0;
|
|
const auto gripError = in->UpdateBooleanComponent(grip_, opticalInputs_.grip, 0);
|
|
if (gripValue_) in->UpdateScalarComponent(gripValue_, opticalInputs_.grip ? 1.0f : 0.0f, 0);
|
|
const auto clickError = in->UpdateBooleanComponent(triggerClick_, opticalInputs_.click, 0);
|
|
const auto triggerError = in->UpdateScalarComponent(trigger_, opticalInputs_.trigger, 0);
|
|
if (triggerTouch_) in->UpdateBooleanComponent(triggerTouch_, opticalInputs_.trigger > 0.05f, 0);
|
|
const float x = pad ? pad->axes[right_ ? 2 : 0]/32767.0f : 0;
|
|
const float y = pad ? -pad->axes[right_ ? 3 : 1]/32767.0f : 0;
|
|
UpdatePadButtons(b, x, y, true);
|
|
LogOpticalInputs(hand, inputValid, valid, now, connection, origin, inputEpochs, gripError, clickError, triggerError);
|
|
pose_ = p;
|
|
VRServerDriverHost()->TrackedDevicePoseUpdated(id_, p, sizeof(p));
|
|
}
|
|
|
|
static constexpr float kReachSpeed = 0.6f, kReachMin = -0.2f, kReachMax = 1.0f;
|
|
|
|
static bool Grabbing(const PadState &pad, bool right) {
|
|
return pad.axes[right ? 4 : 5] > kGrabTrigger || (pad.buttons & (right ? PAD_RB : PAD_LB));
|
|
}
|
|
|
|
void Update(const DriverPose_t &headPose, const PadState *pad, bool sticksMoveHands, float reach) {
|
|
const auto &head = headPose.qRotation;
|
|
if (id_ == k_unTrackedDeviceIndexInvalid) return;
|
|
if (opticalActive_) ClearInputs();
|
|
const auto now = std::chrono::steady_clock::now();
|
|
const float dt = std::min(std::chrono::duration<float>(now - lastUpdate_).count(), 0.1f);
|
|
lastUpdate_ = now;
|
|
const float follow = 1.0f - std::exp(-dt / kHandSmoothing);
|
|
const bool grab = pad && Grabbing(*pad, right_);
|
|
if (grab && pad->axes[7]) pull_ = std::clamp(pull_ - (pad->axes[7] > 0 ? 1.0f : -1.0f) * kReachSpeed * dt, kReachMin - reach, kReachMax - reach);
|
|
else if (!grab) pull_ -= pull_ * follow;
|
|
const float stickX = pad ? pad->axes[right_ ? 2 : 0] / 32767.0f : 0.0f, stickY = pad ? -pad->axes[right_ ? 3 : 1] / 32767.0f : 0.0f;
|
|
float aimX = 0.0f, aimY = 0.0f;
|
|
const float tilt = std::sqrt(stickX * stickX + stickY * stickY);
|
|
if (sticksMoveHands && tilt > kStickDeadzone) {
|
|
const float scale = std::min((tilt - kStickDeadzone) / (1.0f - kStickDeadzone), 1.0f) / tilt;
|
|
aimX = stickX * scale;
|
|
aimY = stickY * scale;
|
|
}
|
|
handX_ += (aimX * kHandRangeX - handX_) * follow;
|
|
handY_ += (aimY * (aimY > 0.0f ? kHandRangeUp : kHandRangeDown) - handY_) * follow;
|
|
DriverPose_t p = {};
|
|
p.qWorldFromDriverRotation.w = 1;
|
|
p.qDriverFromHeadRotation.w = 1;
|
|
const double yaw = std::atan2(-handX_, kHandPivot) / 2, pitch = std::atan2(handY_, std::hypot(kHandPivot, handX_)) / 2;
|
|
const double cy = std::cos(yaw), sy = std::sin(yaw), cp = std::cos(pitch), sp = std::sin(pitch);
|
|
const double lw = cy * cp, lx = cy * sp, ly = sy * cp, lz = -sy * sp;
|
|
p.qRotation.w = head.w * lw - head.x * lx - head.y * ly - head.z * lz;
|
|
p.qRotation.x = head.w * lx + head.x * lw + head.y * lz - head.z * ly;
|
|
p.qRotation.y = head.w * ly - head.x * lz + head.y * lw + head.z * lx;
|
|
p.qRotation.z = head.w * lz + head.x * ly - head.y * lx + head.z * lw;
|
|
const double off[3] = {(right_ ? 0.18 : -0.18) + handX_, -0.25 + handY_, -0.35 - reach - pull_};
|
|
double m[3][3];
|
|
const double w = head.w, x = head.x, y = head.y, z = head.z;
|
|
m[0][0] = 1 - 2 * (y * y + z * z); m[0][1] = 2 * (x * y - z * w); m[0][2] = 2 * (x * z + y * w);
|
|
m[1][0] = 2 * (x * y + z * w); m[1][1] = 1 - 2 * (x * x + z * z); m[1][2] = 2 * (y * z - x * w);
|
|
m[2][0] = 2 * (x * z - y * w); m[2][1] = 2 * (y * z + x * w); m[2][2] = 1 - 2 * (x * x + y * y);
|
|
for (int i = 0; i < 3; i++) p.vecPosition[i] = headPose.vecPosition[i] + m[i][0] * off[0] + m[i][1] * off[1] + m[i][2] * off[2];
|
|
p.poseIsValid = true;
|
|
p.result = TrackingResult_Running_OK;
|
|
p.deviceIsConnected = true;
|
|
pose_ = p;
|
|
VRServerDriverHost()->TrackedDevicePoseUpdated(id_, p, sizeof(p));
|
|
if (!pad) return;
|
|
auto in = VRDriverInput();
|
|
const uint32_t b = pad->buttons;
|
|
const float trig = pad->axes[right_ ? 4 : 5] / 255.0f;
|
|
const bool grip = (b & (right_ ? PAD_RB : PAD_LB)) != 0;
|
|
in->UpdateScalarComponent(trigger_, trig, 0);
|
|
in->UpdateBooleanComponent(triggerClick_, trig > 0.6f, 0);
|
|
if (triggerTouch_) in->UpdateBooleanComponent(triggerTouch_, trig > 0.05f, 0);
|
|
in->UpdateBooleanComponent(grip_, grip, 0);
|
|
if (gripValue_) in->UpdateScalarComponent(gripValue_, grip ? 1.0f : 0.0f, 0);
|
|
if (gripForce_) in->UpdateScalarComponent(gripForce_, grip ? 1.0f : 0.0f, 0);
|
|
UpdatePadButtons(b, stickX, stickY, !sticksMoveHands);
|
|
}
|
|
|
|
private:
|
|
void UpdatePadButtons(uint32_t b, float stickX, float stickY, bool stickToGame) {
|
|
auto in = VRDriverInput();
|
|
const bool touch = stickToGame && stickX * stickX + stickY * stickY > 0.01f;
|
|
in->UpdateBooleanComponent(system_, right_ && (b & PAD_GUIDE), 0);
|
|
in->UpdateScalarComponent(padX_, touch ? stickX : 0.0f, 0);
|
|
in->UpdateScalarComponent(padY_, touch ? stickY : 0.0f, 0);
|
|
if (!touch_ && !index_) {
|
|
in->UpdateBooleanComponent(menu_, (b & (right_ ? PAD_START : PAD_SELECT)) != 0, 0);
|
|
in->UpdateBooleanComponent(padTouch_, touch || (b & (right_ ? PAD_A : PAD_X)), 0);
|
|
in->UpdateBooleanComponent(padClick_, (b & (right_ ? (PAD_A | PAD_R3) : PAD_X)) != 0, 0);
|
|
return;
|
|
}
|
|
const bool primary = b & (right_ ? PAD_A : PAD_X), secondary = b & (right_ ? PAD_B : PAD_Y);
|
|
const bool both = (b & PAD_L3) && (b & PAD_R3);
|
|
if (menu_) in->UpdateBooleanComponent(menu_, !right_ && (b & (PAD_START | PAD_SELECT)), 0);
|
|
in->UpdateBooleanComponent(primaryClick_, primary, 0);
|
|
in->UpdateBooleanComponent(secondaryClick_, secondary, 0);
|
|
in->UpdateBooleanComponent(primaryTouch_, primary, 0);
|
|
in->UpdateBooleanComponent(secondaryTouch_, secondary, 0);
|
|
in->UpdateBooleanComponent(padTouch_, touch, 0);
|
|
in->UpdateBooleanComponent(padClick_, !both && (b & (right_ ? PAD_R3 : PAD_L3)), 0);
|
|
}
|
|
|
|
void LogOpticalInputs(const dd_spatial_hand &hand, bool inputValid, bool poseValid, int64_t now,
|
|
uint64_t connection, uint64_t origin, uint16_t inputEpochs, EVRInputError gripError,
|
|
EVRInputError clickError, EVRInputError triggerError) {
|
|
const uint32_t triggerBand = !std::isfinite(hand.trigger) ? 3 : hand.trigger < .25f ? 0 : hand.trigger > .65f ? 2 : 1;
|
|
const uint32_t state = uint32_t(poseValid) | uint32_t(inputValid)<<1 | uint32_t(opticalInputs_.click)<<2 |
|
|
uint32_t(opticalInputs_.grip)<<3 | uint32_t(opticalInputs_.armed)<<4 | uint32_t(opticalInputs_.gripArmed)<<5 |
|
|
(hand.pose.flags & 7u)<<6 | triggerBand<<9 | uint32_t(hand.grip != 0)<<11;
|
|
const int errors[] = {int(gripError),int(clickError),int(triggerError)};
|
|
if (state == lastOpticalState_ && connection == lastOpticalConnection_ && origin == lastOpticalOrigin_ &&
|
|
inputEpochs == lastOpticalEpochs_ &&
|
|
std::equal(errors,errors+3,lastOpticalErrors_)) return;
|
|
lastOpticalState_ = state; lastOpticalConnection_ = connection; lastOpticalOrigin_ = origin;
|
|
lastOpticalEpochs_ = inputEpochs;
|
|
std::copy_n(errors,3,lastOpticalErrors_);
|
|
Log("%s optical input pose=%d input=%d flags=%u raw=%.3f/%u out=%.3f/%d/%d armed=%d/%d error=%d/%d/%d ageMs=%.1f connection=%llu origin=%llu epochs=%u/%u",
|
|
right_ ? "right" : "left", int(poseValid), int(inputValid), hand.pose.flags, double(hand.trigger), hand.grip,
|
|
double(opticalInputs_.trigger), int(opticalInputs_.click), int(opticalInputs_.grip),
|
|
int(opticalInputs_.armed), int(opticalInputs_.gripArmed), errors[0], errors[1], errors[2],
|
|
hand.pose.time_ns > 0 ? double(now-hand.pose.time_ns)/1e6 : -1.,
|
|
static_cast<unsigned long long>(connection), static_cast<unsigned long long>(origin),
|
|
unsigned(inputEpochs & 255), unsigned(inputEpochs >> 8));
|
|
}
|
|
OpticalInputs opticalInputs_;
|
|
OpticalPose opticalPose_;
|
|
bool steadyHandAim_ = false;
|
|
bool opticalActive_ = false;
|
|
uint32_t lastOpticalState_ = ~0u;
|
|
uint64_t lastOpticalConnection_ = 0, lastOpticalOrigin_ = 0;
|
|
uint16_t lastOpticalEpochs_ = 0;
|
|
int lastOpticalErrors_[3]{};
|
|
static constexpr int16_t kGrabTrigger = 153;
|
|
static constexpr float kStickDeadzone = 0.12f, kHandRangeX = 0.40f, kHandRangeUp = 0.50f, kHandRangeDown = 0.30f, kHandSmoothing = 0.05f, kHandPivot = 0.45f;
|
|
bool right_;
|
|
bool xrMode_ = false, touch_ = false, index_ = false;
|
|
PadProfile profile_ = PadProfile::Index;
|
|
uint32_t id_ = k_unTrackedDeviceIndexInvalid;
|
|
DriverPose_t pose_ = {};
|
|
float pull_ = 0.0f, handX_ = 0.0f, handY_ = 0.0f;
|
|
std::chrono::steady_clock::time_point lastUpdate_ = std::chrono::steady_clock::now();
|
|
VRInputComponentHandle_t system_ = 0, menu_ = 0, grip_ = 0, triggerClick_ = 0, trigger_ = 0, padX_ = 0, padY_ = 0,
|
|
padClick_ = 0, padTouch_ = 0, haptic_ = 0;
|
|
VRInputComponentHandle_t gripValue_ = 0, gripForce_ = 0, triggerTouch_ = 0, primaryClick_ = 0, secondaryClick_ = 0,
|
|
primaryTouch_ = 0, secondaryTouch_ = 0, thumbrestTouch_ = 0;
|
|
};
|
|
|
|
class CardboardHmd : public ITrackedDeviceServerDriver, public IVRDisplayComponent {
|
|
public:
|
|
EVRInitError Activate(uint32_t unObjectId) override {
|
|
id_ = unObjectId;
|
|
PropertyContainerHandle_t c = VRProperties()->TrackedDeviceToPropertyContainer(id_);
|
|
container_ = c;
|
|
width_ = VRSettings()->GetInt32(kSettingsSection, "windowWidth");
|
|
height_ = VRSettings()->GetInt32(kSettingsSection, "windowHeight");
|
|
if (width_ <= 0) width_ = 1920;
|
|
if (height_ <= 0) height_ = 1080;
|
|
float hz = VRSettings()->GetFloat(kSettingsSection, "displayFrequency");
|
|
if (hz <= 0.0f) hz = 60.0f;
|
|
DroidDeckXrTracking xr;
|
|
xrMode_ = xr_.Read(&xr) && XrTrackingFile::Fresh(xr);
|
|
if (xrMode_) {
|
|
width_ = xr.eyes[0].width * 2; height_ = xr.eyes[0].height;
|
|
hz = 1000000000.f / xr.period_ns;
|
|
}
|
|
tanHalf_ = 1.0f;
|
|
eyeAspect_ = (width_ * 0.5f) / height_;
|
|
droiddeck_headpose hp;
|
|
if (pose_.Read(&hp) && hp.tan_half_fov > 0.0f) {
|
|
tanHalf_ = hp.tan_half_fov;
|
|
if (hp.eye_aspect > 0.0f) eyeAspect_ = hp.eye_aspect;
|
|
sentMono_ = (hp.flags & DROIDDECK_HEADPOSE_MONO) != 0;
|
|
renderHeight_ = hp.render_height;
|
|
panelHeight_ = hp.eye_h;
|
|
}
|
|
if (!xrMode_ && pose_.Read(&hp) && hp.display_hz >= 24.0f) hz = hp.display_hz;
|
|
Vsync::SetRate(hz);
|
|
sentHz_ = hz;
|
|
sentTan_ = tanHalf_;
|
|
sentAspect_ = eyeAspect_;
|
|
VRProperties()->SetStringProperty(c, Prop_ModelNumber_String, xrMode_ ? "DroidDeck Quest OpenXR" : "DroidDeck Cardboard");
|
|
VRProperties()->SetStringProperty(c, Prop_ManufacturerName_String, "DroidDeck");
|
|
VRProperties()->SetStringProperty(c, Prop_TrackingSystemName_String, "droiddeck");
|
|
VRProperties()->SetFloatProperty(c, Prop_UserIpdMeters_Float, 0.064f);
|
|
if (xrMode_) {
|
|
VRServerDriverHost()->SetDisplayEyeToHead(id_, XrEyeToHead(xr.head, xr.eyes[0].pose), XrEyeToHead(xr.head, xr.eyes[1].pose));
|
|
dm_.ConfigureXr(xr);
|
|
float distanceSquared = 0;
|
|
for (int axis = 0; axis < 3; axis++) {
|
|
float difference = xr.eyes[0].pose.position[axis] - xr.eyes[1].pose.position[axis];
|
|
distanceSquared += difference * difference;
|
|
}
|
|
VRProperties()->SetFloatProperty(c, Prop_UserIpdMeters_Float, std::sqrt(distanceSquared));
|
|
}
|
|
VRProperties()->SetFloatProperty(c, Prop_UserHeadToEyeDepthMeters_Float, 0.0f);
|
|
VRProperties()->SetFloatProperty(c, Prop_DisplayFrequency_Float, hz);
|
|
VRProperties()->SetFloatProperty(c, Prop_SecondsFromVsyncToPhotons_Float, 0.011f);
|
|
VRProperties()->SetUint64Property(c, Prop_CurrentUniverseId_Uint64, 2);
|
|
chaperone_.Reset();
|
|
if (xrMode_) PublishChaperone(xr);
|
|
VRProperties()->SetBoolProperty(c, Prop_IsOnDesktop_Bool, false);
|
|
VRProperties()->SetBoolProperty(c, Prop_DisplayDebugMode_Bool, false);
|
|
directMode_ = VRSettings()->GetBool(kSettingsSection, "directMode");
|
|
const char *presentation = std::getenv("DROIDDECK_VR_PRESENT");
|
|
if (xrMode_ && presentation && !std::strcmp(presentation, "questwsi")) directMode_ = false;
|
|
VRProperties()->SetBoolProperty(c, Prop_HasDriverDirectModeComponent_Bool, directMode_);
|
|
if (directMode_) {
|
|
VRProperties()->SetBoolProperty(c, Prop_DriverDirectModeSendsVsyncEvents_Bool, true);
|
|
VRProperties()->SetBoolProperty(c, Prop_Hmd_AllowsClientToControlTextureIndex, true);
|
|
}
|
|
VRProperties()->SetBoolProperty(c, Prop_HasDisplayComponent_Bool, true);
|
|
virtualDisplay_ = VRSettings()->GetBool(kSettingsSection, "virtualDisplay");
|
|
VRProperties()->SetBoolProperty(c, Prop_HasVirtualDisplayComponent_Bool, virtualDisplay_);
|
|
VRProperties()->SetBoolProperty(c, Prop_ContainsProximitySensor_Bool, true);
|
|
VRDriverInput()->CreateBooleanComponent(c, "/proximity", &proximity_);
|
|
VRDriverInput()->UpdateBooleanComponent(proximity_, true, 0);
|
|
Log("HMD activated: %dx%d at %.0f Hz, eye tan(half fov) %.3f, eye aspect %.3f", width_, height_, hz, tanHalf_.load(), eyeAspect_.load());
|
|
uint32_t renderW, renderH;
|
|
GetRecommendedRenderTargetSize(&renderW, &renderH);
|
|
Log("recommended render target: %ux%u per eye (selected height %u)", renderW, renderH, renderHeight_.load());
|
|
|
|
running_ = true;
|
|
thread_ = std::thread([this] { PoseLoop(); });
|
|
if (directMode_) Vsync::Start();
|
|
return VRInitError_None;
|
|
}
|
|
|
|
bool Running() const { return running_; }
|
|
|
|
void Deactivate() override {
|
|
Vsync::Stop();
|
|
droiddeck::Presenter::Shutdown();
|
|
running_ = false;
|
|
if (thread_.joinable()) thread_.join();
|
|
id_ = k_unTrackedDeviceIndexInvalid;
|
|
}
|
|
|
|
void EnterStandby() override {}
|
|
|
|
void *GetComponent(const char *name) override {
|
|
Log("component asked for: %s", name);
|
|
if (!strncmp(name, "IVRDisplayComponent_", 20)) return static_cast<IVRDisplayComponent *>(this);
|
|
if (virtualDisplay_ && !strcmp(name, IVRVirtualDisplay_Version)) return static_cast<IVRVirtualDisplay *>(&vd_);
|
|
if (directMode_ && !strncmp(name, "IVRDriverDirectModeComponent_", 29)) return static_cast<IVRDriverDirectModeComponent *>(&dm_);
|
|
return nullptr;
|
|
}
|
|
|
|
void DebugRequest(const char *, char *resp, uint32_t size) override { if (size) resp[0] = 0; }
|
|
|
|
DriverPose_t GetPose() override { return GetPose(nullptr); }
|
|
|
|
DriverPose_t GetPose(droiddeck_spatialpose *snapshot) {
|
|
std::lock_guard<std::mutex> lock(spatialMutex_);
|
|
DriverPose_t p = {};
|
|
p.qWorldFromDriverRotation.w = 1;
|
|
p.qDriverFromHeadRotation.w = 1;
|
|
p.qRotation.w = 1;
|
|
DroidDeckXrTracking xr;
|
|
bool freshXr = xr_.Read(&xr) && XrTrackingFile::Fresh(xr);
|
|
if (freshXr || xrMode_) {
|
|
p.deviceIsConnected = true;
|
|
p.poseIsValid = freshXr && XrTrackingFile::Tracked(xr.head);
|
|
p.result = p.poseIsValid ? TrackingResult_Running_OK : TrackingResult_Running_OutOfRange;
|
|
if (p.poseIsValid) {
|
|
p.qRotation = {xr.head.orientation[3], xr.head.orientation[0], xr.head.orientation[1], xr.head.orientation[2]};
|
|
for (int axis = 0; axis < 3; axis++) p.vecPosition[axis] = xr.head.position[axis];
|
|
p.poseTimeOffset = XrTrackingFile::PoseTimeOffset(xr.predicted_ns, XrTrackingFile::Now());
|
|
}
|
|
return p;
|
|
}
|
|
p.vecPosition[1] = 1.6;
|
|
droiddeck_headpose hp;
|
|
bool ok = pose_.Read(&hp);
|
|
if (ok) {
|
|
p.qRotation = QuatFromMatrix(hp.rot);
|
|
p.poseTimeOffset = 0;
|
|
if (hp.tan_half_fov > 0.0f) tanHalf_ = hp.tan_half_fov;
|
|
if (hp.eye_aspect > 0.0f) eyeAspect_ = hp.eye_aspect;
|
|
}
|
|
p.poseIsValid = true;
|
|
p.result = TrackingResult_Running_OK;
|
|
p.deviceIsConnected = true;
|
|
droiddeck_spatialpose spatial{};
|
|
const bool spatialRead = spatial_.Read(&spatial);
|
|
if (spatialRead) lastSpatialSnapshot_ = spatial;
|
|
else {
|
|
spatial = lastSpatialSnapshot_;
|
|
spatial.head.flags = 0;
|
|
for (auto &hand : spatial.hands) hand.pose.flags = 0;
|
|
}
|
|
if (snapshot) *snapshot = spatial;
|
|
if (spatial.flags & DD_SPATIAL_ACTIVE) {
|
|
if (SpatialFresh(spatial.head, SpatialNow())) {
|
|
for (int i=0; i<3; ++i) p.vecPosition[i] = spatial.head.position[i];
|
|
p.qRotation = {spatial.head.rotation[3], spatial.head.rotation[0], spatial.head.rotation[1], spatial.head.rotation[2]};
|
|
lastSpatial_ = p;
|
|
} else {
|
|
if (lastSpatial_.deviceIsConnected) p = lastSpatial_;
|
|
p.poseIsValid = false;
|
|
p.result = TrackingResult_Running_OutOfRange;
|
|
}
|
|
}
|
|
return p;
|
|
}
|
|
|
|
void GetWindowBounds(int32_t *x, int32_t *y, uint32_t *w, uint32_t *h) override {
|
|
*x = 0; *y = 0; *w = (uint32_t)width_; *h = (uint32_t)height_;
|
|
}
|
|
bool IsDisplayOnDesktop() override { return !directMode_; }
|
|
bool IsDisplayRealDisplay() override { return false; }
|
|
void GetRecommendedRenderTargetSize(uint32_t *w, uint32_t *h) override {
|
|
if (xrMode_) { *w = width_ / 2; *h = height_; return; }
|
|
const auto size = droiddeck::RenderTarget(eyeAspect_, renderHeight_, panelHeight_);
|
|
*w = size.width;
|
|
*h = size.height;
|
|
}
|
|
void GetEyeOutputViewport(EVREye eye, uint32_t *x, uint32_t *y, uint32_t *w, uint32_t *h) override {
|
|
*y = 0; *w = (uint32_t)width_ / 2; *h = (uint32_t)height_;
|
|
*x = eye == Eye_Left ? 0 : (uint32_t)width_ / 2;
|
|
}
|
|
void GetProjectionRaw(EVREye eye, float *left, float *right, float *top, float *bottom) override {
|
|
DroidDeckXrTracking xr;
|
|
if (xrMode_ && xr_.Read(&xr) && XrTrackingFile::Fresh(xr)) {
|
|
const float *fov = xr.eyes[eye == Eye_Right ? 1 : 0].fov;
|
|
xr_frame_projection_raw(fov, left, right, top, bottom);
|
|
return;
|
|
}
|
|
*left = -tanHalf_ * eyeAspect_; *right = tanHalf_ * eyeAspect_;
|
|
*top = -tanHalf_; *bottom = tanHalf_;
|
|
}
|
|
DistortionCoordinates_t ComputeDistortion(EVREye, float u, float v) override {
|
|
DistortionCoordinates_t d;
|
|
d.rfRed[0] = d.rfGreen[0] = d.rfBlue[0] = u;
|
|
d.rfRed[1] = d.rfGreen[1] = d.rfBlue[1] = v;
|
|
return d;
|
|
}
|
|
bool ComputeInverseDistortion(HmdVector2_t *result, EVREye, uint32_t, float u, float v) override {
|
|
result->v[0] = u; result->v[1] = v;
|
|
return true;
|
|
}
|
|
|
|
private:
|
|
void UpdateEye(const droiddeck_headpose &hp) {
|
|
sentTan_ = tanHalf_ = hp.tan_half_fov;
|
|
sentAspect_ = eyeAspect_ = hp.eye_aspect;
|
|
sentMono_ = (hp.flags & DROIDDECK_HEADPOSE_MONO) != 0;
|
|
renderHeight_ = hp.render_height;
|
|
panelHeight_ = hp.eye_h;
|
|
HmdRect2_t r;
|
|
r.vTopLeft.v[0] = -tanHalf_ * eyeAspect_;
|
|
r.vTopLeft.v[1] = -tanHalf_;
|
|
r.vBottomRight.v[0] = tanHalf_ * eyeAspect_;
|
|
r.vBottomRight.v[1] = tanHalf_;
|
|
VRServerDriverHost()->SetDisplayProjectionRaw(id_, r, r);
|
|
uint32_t w = 0, h = 0;
|
|
GetRecommendedRenderTargetSize(&w, &h);
|
|
VRServerDriverHost()->SetRecommendedRenderTargetSize(id_, w, h);
|
|
VRServerDriverHost()->VendorSpecificEvent(id_, VREvent_LensDistortionChanged, VREvent_Data_t{}, 0.0);
|
|
Log("eye now tan(half fov) %.3f, aspect %.3f (%ux%u eye%s), render %ux%u", tanHalf_.load(), eyeAspect_.load(), hp.eye_w, hp.eye_h,
|
|
(hp.flags & DROIDDECK_HEADPOSE_MONO) ? ", one eye shown" : "", w, h);
|
|
}
|
|
|
|
void UpdateRate(float hz) {
|
|
sentHz_ = hz;
|
|
Vsync::SetRate(hz);
|
|
VRProperties()->SetFloatProperty(container_, Prop_DisplayFrequency_Float, hz);
|
|
Log("panel now %.0f Hz", hz);
|
|
}
|
|
|
|
void PublishChaperone(const DroidDeckXrTracking &tracking) {
|
|
chaperone_.Update(tracking, [this](const DroidDeckXrPose &head) {
|
|
char timestamp[32];
|
|
std::time_t now = std::time(nullptr); std::tm utc{}; gmtime_r(&now, &utc);
|
|
std::strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%SZ", &utc);
|
|
const auto json = xr_chaperone_json(head, timestamp);
|
|
const auto container = VRProperties()->TrackedDeviceToPropertyContainer(id_);
|
|
return VRProperties()->SetStringProperty(container, Prop_DriverProvidedChaperoneJson_String, json.c_str()) == TrackedProp_Success;
|
|
});
|
|
}
|
|
void PoseLoop() {
|
|
uint32_t lastSeq = 0;
|
|
droiddeck_joycon joyCons = {};
|
|
bool haveJoyCons = false;
|
|
auto lastSent = std::chrono::steady_clock::now();
|
|
while (running_) {
|
|
DroidDeckXrTracking xr;
|
|
if (xrMode_) {
|
|
bool fresh = xr_.Read(&xr) && XrTrackingFile::Fresh(xr);
|
|
if (fresh) PublishChaperone(xr);
|
|
DriverPose_t head = GetPose();
|
|
VRServerDriverHost()->TrackedDevicePoseUpdated(id_, head, sizeof(head));
|
|
for (int hand = 0; hand < 2; hand++) if (CardboardController *pad = ActiveHand(hand)) {
|
|
DroidDeckXrHand state = fresh ? xr.hands[hand] : DroidDeckXrHand{};
|
|
pad->UpdateXr(state, fresh ? xr.predicted_ns : 0);
|
|
}
|
|
VRDriverInput()->UpdateBooleanComponent(proximity_, fresh, 0);
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
|
continue;
|
|
}
|
|
droiddeck_headpose hp;
|
|
bool fresh = pose_.Read(&hp) && hp.seq != lastSeq;
|
|
auto now = std::chrono::steady_clock::now();
|
|
PadState pad;
|
|
const bool havePad = pad_.Read(&pad);
|
|
const bool padChanged = havePad && (pad.buttons != lastPad_.buttons || memcmp(pad.axes, lastPad_.axes, sizeof(pad.axes)));
|
|
droiddeck_joycon read;
|
|
if (joyConFile_.Read(&read)) {
|
|
joyCons = read;
|
|
haveJoyCons = true;
|
|
}
|
|
const bool joyConsChanged = haveJoyCons && joyCons.seq != lastJoyConSeq_;
|
|
if ((fresh || padChanged || joyConsChanged || now - lastSent > std::chrono::milliseconds(10)) && id_ != k_unTrackedDeviceIndexInvalid) {
|
|
if (joyConsChanged) lastJoyConSeq_ = joyCons.seq;
|
|
if (fresh) {
|
|
lastSeq = hp.seq;
|
|
if (hp.eye_w > 0 && hp.tan_half_fov > 0.0f && hp.eye_aspect > 0.0f &&
|
|
(fabsf(hp.tan_half_fov - sentTan_) > 1e-3f || fabsf(hp.eye_aspect - sentAspect_) > 1e-3f ||
|
|
((hp.flags & DROIDDECK_HEADPOSE_MONO) != 0) != sentMono_ ||
|
|
hp.render_height != renderHeight_ || hp.eye_h != panelHeight_))
|
|
UpdateEye(hp);
|
|
if (hp.display_hz >= 24.0f && fabsf(hp.display_hz - sentHz_) > 0.5f) UpdateRate(hp.display_hz);
|
|
const bool worn = (hp.flags & DROIDDECK_HEADPOSE_ENABLED) != 0;
|
|
if (worn != worn_) {
|
|
worn_ = worn;
|
|
VRDriverInput()->UpdateBooleanComponent(proximity_, worn, 0);
|
|
Log("headset %s (Cardboard mode %s)", worn ? "worn" : "taken off", worn ? "on" : "off");
|
|
}
|
|
}
|
|
droiddeck_spatialpose spatial{};
|
|
DriverPose_t head = GetPose(&spatial);
|
|
VRServerDriverHost()->TrackedDevicePoseUpdated(id_, head, sizeof(DriverPose_t));
|
|
const bool lumaJoyCons = (spatial.flags & DD_SPATIAL_ACTIVE) && (spatial.flags & DD_SPATIAL_JOYCONS);
|
|
const bool optical = !lumaJoyCons && (spatial.flags & DD_SPATIAL_ACTIVE) && (spatial.flags & DD_SPATIAL_HANDS);
|
|
const uint32_t chord = PAD_L3 | PAD_R3;
|
|
if (!optical && havePad && (pad.buttons & chord) == chord && (lastPad_.buttons & chord) != chord) {
|
|
sticksMoveHands_.store(!sticksMoveHands_.load());
|
|
Log("sticks now move %s", sticksMoveHands_.load() ? "the hands" : "in the game");
|
|
}
|
|
const float dt = std::min(std::chrono::duration<float>(now - lastReach_).count(), 0.1f);
|
|
lastReach_ = now;
|
|
if (havePad && pad.axes[7] && !CardboardController::Grabbing(pad, false) && !CardboardController::Grabbing(pad, true))
|
|
reach_ = std::clamp(reach_ - (pad.axes[7] > 0 ? 1.0f : -1.0f) * CardboardController::kReachSpeed * dt,
|
|
CardboardController::kReachMin, CardboardController::kReachMax);
|
|
if (now - lastScan_ > std::chrono::seconds(1)) {
|
|
lastScan_ = now;
|
|
const bool other = OtherControllers();
|
|
if (other != otherControllers_) {
|
|
otherControllers_ = other;
|
|
Log("%s", other ? "other VR controllers connected: pad hands off" : "no other VR controllers: pad hands on");
|
|
}
|
|
}
|
|
const int64_t nowNs = MonotonicNs();
|
|
gHandTrace.Poll(nowNs);
|
|
double headYaw = 0.0;
|
|
if (Heading({head.qRotation.w, head.qRotation.x, head.qRotation.y, head.qRotation.z}, &headYaw))
|
|
headings_.Add(nowNs, {-std::sin(headYaw), 0.0, -std::cos(headYaw)});
|
|
droiddeck_handcam cam;
|
|
if (handCamFile_.Read(&cam)) {
|
|
const bool running = (cam.flags & DROIDDECK_HANDCAM_RUNNING) != 0;
|
|
if (cam.frame != lastCamFrame_) {
|
|
lastCamFrame_ = cam.frame;
|
|
if (!lumaJoyCons && !optical && running && std::llabs(nowNs - cam.time_ns) < kHandCamStaleNs) SeeCamera(cam, nowNs);
|
|
}
|
|
bareFlag_ = running && (cam.flags & DROIDDECK_HANDCAM_BARE_HANDS);
|
|
bareTimeNs_ = cam.time_ns;
|
|
}
|
|
const bool bare = !otherControllers_ && !optical && !lumaJoyCons && bareFlag_ && std::llabs(nowNs - bareTimeNs_) < kBareHandsStaleNs;
|
|
if (bare != bareHands_) {
|
|
bareHands_ = bare;
|
|
Log("%s", bare ? "no controllers: the camera tracks bare hands" : "camera bare hands off");
|
|
}
|
|
for (int side = 0; side < 2; side++) {
|
|
const droiddeck_joycon_hand *joyCon =
|
|
!otherControllers_ && !optical && haveJoyCons && JoyConLive(joyCons.hands[side], nowNs) ? &joyCons.hands[side] : nullptr;
|
|
if (handControllers_[side]) handControllers_[side]->ExpectBare(!joyCon && bare);
|
|
if (joyCon || bare) wantHand_[side] = true;
|
|
const bool covered = handControllers_[side] && handControllers_[side]->Update(joyCon, bare, head.qRotation, nowNs,
|
|
lumaJoyCons ? &spatial : nullptr);
|
|
CardboardController *hand = ActiveHand(side);
|
|
if (!hand) continue;
|
|
if (otherControllers_ || covered || bare || lumaJoyCons) {
|
|
if (!padHandOff_[side]) hand->Disconnect();
|
|
padHandOff_[side] = true;
|
|
continue;
|
|
}
|
|
padHandOff_[side] = false;
|
|
if (optical) hand->UpdateOptical(spatial.hands[side], SpatialFresh(spatial.head, SpatialNow()),
|
|
havePad ? &pad : nullptr, SpatialNow(), spatial.connection, spatial.origin,
|
|
uint16_t(spatial.input_epochs >> (side*16)),
|
|
spatial.flags & DD_SPATIAL_STEADY_AIM ? &spatial.head : nullptr);
|
|
else hand->Update(head, havePad ? &pad : nullptr, sticksMoveHands_.load(), reach_);
|
|
}
|
|
if (havePad) lastPad_ = pad;
|
|
lastSent = now;
|
|
}
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
|
}
|
|
}
|
|
|
|
void SeeCamera(const droiddeck_handcam &cam, int64_t nowNs) {
|
|
Vec3 forward;
|
|
const bool heading = headings_.At(cam.time_ns, &forward) && Length(forward) > 0.5;
|
|
if ((cam.flags & DROIDDECK_HANDCAM_FACE) && heading) {
|
|
const double dt = lastFaceNs_ ? std::max(0.0, (cam.time_ns - lastFaceNs_) * 1e-9) : 0.0;
|
|
lastFaceNs_ = cam.time_ns;
|
|
cameraYaw_.Add(std::atan2(-forward.x, -forward.z) - cam.face_yaw, dt);
|
|
}
|
|
double yaw = 0.0;
|
|
const bool aligned = cameraYaw_.Get(&yaw);
|
|
if (aligned != cameraAligned_) {
|
|
cameraAligned_ = aligned;
|
|
Log("hand camera %s", aligned ? "lined up with the headset" : "waiting to see your face");
|
|
}
|
|
if (FILE *trace = gHandTrace.File())
|
|
fprintf(trace, "C,%lld,%u,%lld,%u,%.4f,%.4f,%d,%.4f,%u,%.4f,%.4f,%.4f,%u,%.4f,%.4f,%.4f\n", (long long)nowNs, cam.frame,
|
|
(long long)cam.time_ns, cam.flags, cam.face_yaw, heading ? std::atan2(-forward.x, -forward.z) : 0.0, aligned ? 1 : 0,
|
|
aligned ? yaw : 0.0, cam.hands[0].flags, cam.hands[0].position[0], cam.hands[0].position[1], cam.hands[0].position[2],
|
|
cam.hands[1].flags, cam.hands[1].position[0], cam.hands[1].position[1], cam.hands[1].position[2]);
|
|
if (!aligned) return;
|
|
const Quat turn = AboutY(yaw);
|
|
for (int side = 0; side < 2; side++) {
|
|
const droiddeck_handcam_hand &hand = cam.hands[side];
|
|
if (!(hand.flags & DROIDDECK_HANDCAM_SEEN) || !handControllers_[side]) continue;
|
|
Vec3 joints[kHandJoints];
|
|
const bool haveJoints = (hand.flags & DROIDDECK_HANDCAM_JOINTS) != 0;
|
|
if (haveJoints)
|
|
for (int i = 0; i < kHandJoints; i++) joints[i] = Rotate(turn, {hand.joints[i][0], hand.joints[i][1], hand.joints[i][2]});
|
|
handControllers_[side]->SeeCamera(Rotate(turn, {hand.position[0], hand.position[1], hand.position[2]}), haveJoints ? joints : nullptr,
|
|
cam.time_ns, (hand.flags & DROIDDECK_HANDCAM_ESTIMATED) != 0, nowNs);
|
|
}
|
|
}
|
|
|
|
static bool OtherControllers() {
|
|
TrackedDevicePose_t poses[k_unMaxTrackedDeviceCount];
|
|
VRServerDriverHost()->GetRawTrackedDevicePoses(0.0f, poses, k_unMaxTrackedDeviceCount);
|
|
for (uint32_t i = 0; i < k_unMaxTrackedDeviceCount; i++) {
|
|
if (!poses[i].bDeviceIsConnected) continue;
|
|
PropertyContainerHandle_t c = VRProperties()->TrackedDeviceToPropertyContainer(i);
|
|
if (VRProperties()->GetInt32Property(c, Prop_DeviceClass_Int32) != TrackedDeviceClass_Controller) continue;
|
|
if (VRProperties()->GetStringProperty(c, Prop_TrackingSystemName_String) != "droiddeck") return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
uint32_t id_ = k_unTrackedDeviceIndexInvalid;
|
|
int32_t width_ = 1920, height_ = 1080;
|
|
std::atomic<float> tanHalf_{1.0f}, eyeAspect_{0.889f};
|
|
std::atomic<uint32_t> renderHeight_{720}, panelHeight_{0};
|
|
float sentTan_ = 0.0f, sentAspect_ = 0.0f, sentHz_ = 0.0f;
|
|
bool sentMono_ = false;
|
|
PropertyContainerHandle_t container_ = k_ulInvalidPropertyContainer;
|
|
public:
|
|
void SetHands(CardboardController *left, CardboardController *right) { hands_[0] = left; hands_[1] = right; }
|
|
void SetSticksMoveHands(bool on) { sticksMoveHands_ = on; }
|
|
|
|
CardboardController *ActiveHand(int side) {
|
|
CardboardController *wanted = hands_[side].load();
|
|
if (wanted != active_[side]) {
|
|
if (active_[side]) active_[side]->Disconnect();
|
|
active_[side] = wanted;
|
|
padHandOff_[side] = false;
|
|
}
|
|
return wanted;
|
|
}
|
|
void SetHandControllers(HandController *left, HandController *right) { handControllers_[0] = left; handControllers_[1] = right; }
|
|
bool WantsHand(int side) const { return wantHand_[side].load(); }
|
|
|
|
private:
|
|
PoseFile pose_;
|
|
SpatialFile spatial_;
|
|
std::mutex spatialMutex_;
|
|
DriverPose_t lastSpatial_{};
|
|
droiddeck_spatialpose lastSpatialSnapshot_{};
|
|
XrTrackingFile xr_;
|
|
XrChaperoneOrigin chaperone_;
|
|
bool xrMode_ = false;
|
|
PadRing pad_;
|
|
PadState lastPad_;
|
|
VRInputComponentHandle_t proximity_ = 0;
|
|
bool worn_ = true;
|
|
std::atomic<bool> sticksMoveHands_{false};
|
|
float reach_ = 0.0f;
|
|
std::chrono::steady_clock::time_point lastReach_ = std::chrono::steady_clock::now();
|
|
std::chrono::steady_clock::time_point lastScan_{};
|
|
bool otherControllers_ = false;
|
|
std::atomic<CardboardController *> hands_[2] = {nullptr, nullptr};
|
|
CardboardController *active_[2] = {nullptr, nullptr};
|
|
bool padHandOff_[2] = {false, false};
|
|
ShmFile<droiddeck_joycon> joyConFile_{kJoyConPath, DROIDDECK_JOYCON_MAGIC, DROIDDECK_JOYCON_VERSION, "Joy-Con"};
|
|
uint32_t lastJoyConSeq_ = 0;
|
|
ShmFile<droiddeck_handcam> handCamFile_{kHandCamPath, DROIDDECK_HANDCAM_MAGIC, DROIDDECK_HANDCAM_VERSION, "hand camera"};
|
|
uint32_t lastCamFrame_ = 0;
|
|
int64_t lastFaceNs_ = 0;
|
|
TimeHistory headings_;
|
|
CameraYaw cameraYaw_;
|
|
bool cameraAligned_ = false;
|
|
bool bareHands_ = false, bareFlag_ = false;
|
|
int64_t bareTimeNs_ = 0;
|
|
HandController *handControllers_[2] = {nullptr, nullptr};
|
|
std::atomic<bool> wantHand_[2] = {false, false};
|
|
bool virtualDisplay_ = false, directMode_ = false;
|
|
VirtualDisplay vd_;
|
|
DirectMode dm_;
|
|
std::atomic<bool> running_{false};
|
|
std::thread thread_;
|
|
};
|
|
|
|
class Provider : public IServerTrackedDeviceProvider {
|
|
public:
|
|
~Provider() {
|
|
if (hmd_.Running()) hmd_.Deactivate();
|
|
}
|
|
|
|
EVRInitError Init(IVRDriverContext *ctx) override {
|
|
VR_INIT_SERVER_DRIVER_CONTEXT(ctx);
|
|
focusEnabled_ = access("/run/droiddeck/droiddeck-xr-frames", F_OK) == 0;
|
|
Log("DroidDeck Cardboard driver loaded");
|
|
if (!VRServerDriverHost()->TrackedDeviceAdded("droiddeck-cardboard-hmd", TrackedDeviceClass_HMD, &hmd_))
|
|
Log("TrackedDeviceAdded failed");
|
|
padsEnabled_ = VRSettings()->GetBool(kSettingsSection, "gamepadControllers");
|
|
ApplyPadConfig(true);
|
|
hmd_.SetHandControllers(&handLeft_, &handRight_);
|
|
return VRInitError_None;
|
|
}
|
|
void Cleanup() override {
|
|
if (focusEnabled_) unlink(kFocusPath);
|
|
if (haptics_) { munmap(haptics_, sizeof(*haptics_)); haptics_ = nullptr; }
|
|
VR_CLEANUP_SERVER_DRIVER_CONTEXT();
|
|
}
|
|
const char *const *GetInterfaceVersions() override { return k_InterfaceVersions; }
|
|
void RunFrame() override {
|
|
const auto now = std::chrono::steady_clock::now();
|
|
if (now - lastConfigCheck_ > std::chrono::seconds(1)) {
|
|
lastConfigCheck_ = now;
|
|
ApplyPadConfig(false);
|
|
}
|
|
for (int side = 0; side < 2; side++) {
|
|
if (handAdded_[side] || !hmd_.WantsHand(side)) continue;
|
|
handAdded_[side] = true;
|
|
if (!VRServerDriverHost()->TrackedDeviceAdded(side ? "droiddeck-hand-right" : "droiddeck-hand-left",
|
|
TrackedDeviceClass_Controller, side ? &handRight_ : &handLeft_))
|
|
Log("%s hand could not be added", side ? "right" : "left");
|
|
}
|
|
VREvent_t e;
|
|
while (VRServerDriverHost()->PollNextEvent(&e, sizeof(e))) {
|
|
if (focusEnabled_) {
|
|
bool changed = false;
|
|
if (e.eventType == VREvent_SceneApplicationChanged) { scenePid_ = e.data.process.pid; changed = true; }
|
|
if (e.eventType == VREvent_DashboardActivated) { dashboard_ = true; changed = true; }
|
|
if (e.eventType == VREvent_DashboardDeactivated) { dashboard_ = false; changed = true; }
|
|
if (changed) PublishFocus();
|
|
}
|
|
if (e.eventType != VREvent_Input_HapticVibration) continue;
|
|
const VREvent_HapticVibration_t &h = e.data.hapticVibration;
|
|
if (!h.componentHandle) continue;
|
|
if (h.componentHandle == handLeft_.Haptic()) SendRumble(DROIDDECK_JOYCON_RUMBLE_SLOT, h);
|
|
else if (h.componentHandle == handRight_.Haptic()) SendRumble(DROIDDECK_JOYCON_RUMBLE_SLOT + 1, h);
|
|
else for (int p = 0; p < kPadProfiles; p++) {
|
|
if (!padAdded_[p] || (h.componentHandle != pads_[p][0].Haptic() && h.componentHandle != pads_[p][1].Haptic())) continue;
|
|
if (pads_[p][0].XrMode()) SendHaptic(h.componentHandle == pads_[p][1].Haptic() ? 1 : 0, h);
|
|
else SendRumble(0, h);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
bool ShouldBlockStandbyMode() override { return false; }
|
|
void EnterStandby() override {}
|
|
void LeaveStandby() override {}
|
|
|
|
private:
|
|
static constexpr const char *kFocusPath = "/run/droiddeck/droiddeck-steamvr/scene-focus.json";
|
|
bool focusEnabled_ = false, dashboard_ = true;
|
|
uint32_t scenePid_ = 0;
|
|
void PublishFocus() {
|
|
const std::string temporary = std::string(kFocusPath) + ".tmp";
|
|
int fd = open(temporary.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC | O_NOFOLLOW, 0600);
|
|
if (fd < 0) return;
|
|
char body[128];
|
|
int length = snprintf(body, sizeof(body), "{\"serverPid\":%d,\"scenePid\":%u,\"dashboard\":%s}\n",
|
|
getpid(), scenePid_, dashboard_ ? "true" : "false");
|
|
bool written = write(fd, body, length) == length;
|
|
close(fd);
|
|
if (written) rename(temporary.c_str(), kFocusPath);
|
|
else unlink(temporary.c_str());
|
|
}
|
|
DroidDeckXrHaptics *haptics_ = nullptr;
|
|
void SendHaptic(int hand, const VREvent_HapticVibration_t &pulse) {
|
|
if (!std::isfinite(pulse.fAmplitude) || !std::isfinite(pulse.fFrequency) || !std::isfinite(pulse.fDurationSeconds)) return;
|
|
if (!haptics_) {
|
|
int descriptor = open("/run/droiddeck/" DROIDDECK_XR_HAPTIC_FILE, O_RDWR | O_CLOEXEC);
|
|
if (descriptor < 0) return;
|
|
struct stat status{};
|
|
if (fstat(descriptor, &status) != 0 || status.st_size != sizeof(*haptics_)) { close(descriptor); return; }
|
|
void *mapping = mmap(nullptr, sizeof(*haptics_), PROT_READ | PROT_WRITE, MAP_SHARED, descriptor, 0);
|
|
close(descriptor);
|
|
if (mapping == MAP_FAILED) return;
|
|
haptics_ = static_cast<DroidDeckXrHaptics *>(mapping);
|
|
}
|
|
uint32_t sequence = __atomic_load_n(&haptics_->seq, __ATOMIC_RELAXED);
|
|
__atomic_store_n(&haptics_->seq, sequence | 1u, __ATOMIC_SEQ_CST);
|
|
__atomic_thread_fence(__ATOMIC_SEQ_CST);
|
|
auto &target = haptics_->hands[hand];
|
|
target.id++;
|
|
target.duration_ns = (int64_t)(std::clamp(pulse.fDurationSeconds, 0.001f, 1.f) * 1000000000.0);
|
|
target.amplitude = std::clamp(pulse.fAmplitude, 0.f, 1.f);
|
|
target.frequency = std::max(pulse.fFrequency, 0.f);
|
|
__atomic_store_n(&haptics_->seq, (sequence | 1u) + 1u, __ATOMIC_RELEASE);
|
|
}
|
|
void ApplyPadConfig(bool initial) {
|
|
const PadControllerConfig pads = read_pad_controller_config("/run/droiddeck/" DROIDDECK_VR_CONTROLLERS_FILE);
|
|
if (!initial && pads.profile == configProfile_ && pads.sticksMoveHands == configSticks_) return;
|
|
configProfile_ = pads.profile;
|
|
if (initial || pads.sticksMoveHands != configSticks_) hmd_.SetSticksMoveHands(pads.sticksMoveHands);
|
|
configSticks_ = pads.sticksMoveHands;
|
|
const PadProfile profile = padsEnabled_ ? pads.profile : PadProfile::Off;
|
|
if (!initial && profile == profile_) return;
|
|
profile_ = profile;
|
|
Log("game pad hands: %s, sticks start in %s", pad_profile_name(profile), pads.sticksMoveHands ? "the hands" : "the game");
|
|
if (profile == PadProfile::Off) { hmd_.SetHands(nullptr, nullptr); return; }
|
|
const int p = int(profile) - 1;
|
|
if (!padAdded_[p]) {
|
|
static const char *const kNames[kPadProfiles][2] = {
|
|
{"droiddeck-pad-left", "droiddeck-pad-right"},
|
|
{"droiddeck-pad-touch-left", "droiddeck-pad-touch-right"},
|
|
{"droiddeck-pad-vive-left", "droiddeck-pad-vive-right"},
|
|
};
|
|
for (int side = 0; side < 2; side++) {
|
|
pads_[p][side].SetProfile(profile);
|
|
VRServerDriverHost()->TrackedDeviceAdded(kNames[p][side], TrackedDeviceClass_Controller, &pads_[p][side]);
|
|
}
|
|
padAdded_[p] = true;
|
|
}
|
|
hmd_.SetHands(&pads_[p][0], &pads_[p][1]);
|
|
}
|
|
|
|
static constexpr int kPadProfiles = 3;
|
|
CardboardHmd hmd_;
|
|
CardboardController pads_[kPadProfiles][2] = {{CardboardController(false), CardboardController(true)},
|
|
{CardboardController(false), CardboardController(true)},
|
|
{CardboardController(false), CardboardController(true)}};
|
|
bool padAdded_[kPadProfiles] = {false, false, false};
|
|
bool padsEnabled_ = true, configSticks_ = false;
|
|
PadProfile profile_ = PadProfile::Off;
|
|
PadProfile configProfile_ = PadProfile::Index;
|
|
std::chrono::steady_clock::time_point lastConfigCheck_{};
|
|
HandController handLeft_{false}, handRight_{true};
|
|
bool handAdded_[2] = {false, false};
|
|
};
|
|
|
|
Provider g_provider;
|
|
|
|
}
|
|
|
|
DD_EXPORT void *HmdDriverFactory(const char *interfaceName, int *returnCode) {
|
|
if (!strcmp(interfaceName, IServerTrackedDeviceProvider_Version)) return &g_provider;
|
|
if (returnCode) *returnCode = VRInitError_Init_InterfaceNotFound;
|
|
return nullptr;
|
|
}
|