Files
totk-pc-recomp/runtime/motion_fusion.c
T
Valiantsin DzerakhandClaude Opus 5.5 7d89fde2f0 Drop the six-axis samples from before StartSixAxisSensor
motion_restart only flagged the next sample to start a fresh frame, so a read in the frame of the Start returned the
previous session's 16 states as live, and for the next 60 ms 15 of the 16 were from before it, with the angle jumping
back to zero between two reads. Motion aim was dead when the bow was first drawn and lagged after that. A restart now
hides the older samples and zeroes the angle; sampling numbers keep counting.

--probe-services checks a restart in the middle of a turn: 16 old states in the Start frame before, none after.
Reported with the cause and a harness in #104.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 00:22:08 +03:00

153 lines
6.6 KiB
C

// Adapted from MacNativeGames games/totk/runtime/motion_fusion.c @ b75be8cf (MIT, Copyright (c) 2026 MacNativeGames
// contributors).
// Six-axis sensor states (nn::hid::SixAxisSensorState) from gyro and accelerometer samples. Input per sample, in the
// Switch controller frame (x to the right, y toward the top edge, z out of the face): acceleration in G including
// gravity, as gravity reads (lying face up z = -1 G), and rotation rate in rad/s. Output per sample, as the Switch
// reports it: acceleration in G, angular velocity in revolutions per second, angle = the integrated revolutions per
// axis, direction = the controller's three axes in a reference frame whose z is up. The orientation is integrated from
// the gyro and, while the controller is not being accelerated (|a| within 15 % of 1 G), pulled toward the measured
// gravity (Mahony, gain 0.5/s: about 2 s), as the Switch's sensor fusion does; yaw has no reference and drifts with the
// gyro's bias. The newest MOTION_RING samples are kept; reads are newest first. A restart (StartSixAxisSensor) drops
// the samples before it: reads return nothing until the next sample, which starts a fresh frame and angle; sampling
// numbers go on. The feeding thread writes, guest threads read, both under the instance's SRW lock.
#include <windows.h>
#include <math.h>
#include <stdlib.h>
#include <string.h>
#include "host.h"
#include "input.h"
enum { MOTION_RING = 64 };
#define STALE_NS 200000000ull // no sample for 200 ms: no motion
struct MotionFusion {
SRWLOCK lock;
MotionState ring[MOTION_RING];
uint64_t produced, last_ns;
uint64_t first; // produced at the last restart: older samples are not read
double R[3][3]; // controller -> reference; row i = reference axis i in controller coordinates
double angle[3];
int restart;
};
MotionFusion *motion_new(void) {
MotionFusion *m = calloc(1, sizeof *m);
if (!m) rt_fatal("motion: out of memory");
InitializeSRWLock(&m->lock);
m->restart = 1;
return m;
}
MotionFusion *motion_global(void) {
static MotionFusion *g;
static INIT_ONCE once = INIT_ONCE_STATIC_INIT;
BOOL pending;
if (InitOnceBeginInitialize(&once, 0, &pending, NULL) && pending) {
g = motion_new();
InitOnceComplete(&once, 0, NULL);
}
return g;
}
static double norm3(const double v[3]) { return sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); }
static void cross3(const double a[3], const double b[3], double out[3]) {
out[0] = a[1] * b[2] - a[2] * b[1];
out[1] = a[2] * b[0] - a[0] * b[2];
out[2] = a[0] * b[1] - a[1] * b[0];
}
// Rows, from the up row (the one the gravity correction keeps right).
static void orthonormalize(double R[3][3]) {
double n = norm3(R[2]);
for (int j = 0; j < 3; j++) R[2][j] /= n;
double d = R[0][0] * R[2][0] + R[0][1] * R[2][1] + R[0][2] * R[2][2];
for (int j = 0; j < 3; j++) R[0][j] -= d * R[2][j];
n = norm3(R[0]);
for (int j = 0; j < 3; j++) R[0][j] /= n;
cross3(R[2], R[0], R[1]);
}
// A fresh orientation: z up against the measured gravity, x as close to the controller's x as that allows.
static void align_to(double R[3][3], const double acc[3]) {
double n = norm3(acc), up[3] = {0, 0, 1}, x[3] = {1, 0, 0};
if (n > 0.5)
for (int j = 0; j < 3; j++) up[j] = -acc[j] / n;
if (fabs(up[0]) > 0.9) { x[0] = 0; x[1] = 1; }
memcpy(R[2], up, sizeof up);
memcpy(R[0], x, sizeof x);
orthonormalize(R);
}
// Body-frame rate: R = R * exp([w] dt), the rate plus the gravity pull.
static void integrate(double R[3][3], const double gyro[3], const double acc[3], double dt) {
double w[3] = {gyro[0], gyro[1], gyro[2]}, n = norm3(acc);
if (fabs(n - 1) < 0.15) { // not being accelerated: turn the estimated down toward the measured one
const double kp = 0.5;
double measured[3] = {acc[0] / n, acc[1] / n, acc[2] / n}, estimated[3] = {-R[2][0], -R[2][1], -R[2][2]}, e[3];
cross3(measured, estimated, e);
for (int j = 0; j < 3; j++) w[j] += kp * e[j];
}
double rate = norm3(w), theta = rate * dt;
if (theta < 1e-12) return;
double k[3] = {w[0] / rate, w[1] / rate, w[2] / rate}, s = sin(theta), c = 1 - cos(theta);
double K[3][3] = {{0, -k[2], k[1]}, {k[2], 0, -k[0]}, {-k[1], k[0], 0}}, E[3][3], out[3][3];
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++) {
double kk = 0;
for (int q = 0; q < 3; q++) kk += K[i][q] * K[q][j];
E[i][j] = (i == j) + s * K[i][j] + c * kk;
}
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++) {
double v = 0;
for (int q = 0; q < 3; q++) v += R[i][q] * E[q][j];
out[i][j] = v;
}
memcpy(R, out, sizeof out);
orthonormalize(R);
}
void motion_feed(MotionFusion *m, const double acc[3], const double gyro[3], uint64_t now) {
AcquireSRWLockExclusive(&m->lock);
double dt = m->last_ns && now > m->last_ns ? (double)(now - m->last_ns) / 1e9 : 0.004;
if (dt > 0.05) dt = 0.004; // a gap (no controller, sensors were off): no integration across it
m->last_ns = now;
if (m->restart) {
align_to(m->R, acc);
memset(m->angle, 0, sizeof m->angle);
m->restart = 0;
} else integrate(m->R, gyro, acc, dt);
MotionState *s = &m->ring[m->produced % MOTION_RING];
*s = (MotionState){.delta_ns = (int64_t)(dt * 1e9), .sampling = (int64_t)m->produced + 1, .attributes = 1};
for (int i = 0; i < 3; i++) {
double rps = gyro[i] / (2 * M_PI);
m->angle[i] += rps * dt;
s->acc[i] = (float)acc[i];
s->gyro[i] = (float)rps;
s->angle[i] = (float)m->angle[i];
for (int j = 0; j < 3; j++) s->dir[i * 3 + j] = (float)m->R[j][i]; // direction i = controller axis i in the reference frame
}
m->produced++;
ReleaseSRWLockExclusive(&m->lock);
}
int motion_read(MotionFusion *m, MotionState *out, int max, uint64_t now) {
if (max <= 0) return 0;
int n = 0;
AcquireSRWLockShared(&m->lock);
if (m->produced > m->first && (now < m->last_ns || now - m->last_ns < STALE_NS)) {
uint64_t have = m->produced - m->first < MOTION_RING ? m->produced - m->first : MOTION_RING;
n = (int)(have < (uint64_t)max ? have : (uint64_t)max);
for (int i = 0; i < n; i++) out[i] = m->ring[(m->produced - 1 - (uint64_t)i) % MOTION_RING];
}
ReleaseSRWLockShared(&m->lock);
return n;
}
void motion_restart(MotionFusion *m) {
AcquireSRWLockExclusive(&m->lock);
m->restart = 1;
m->first = m->produced;
m->last_ns = 0; // the first sample after the restart integrates nothing across the stop
memset(m->angle, 0, sizeof m->angle);
ReleaseSRWLockExclusive(&m->lock);
}