mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
Add the encoding and the report schedule for HD rumble on Switch controllers, with their check.
runtime/rumble_hd.c turns one of the game's vibration values (a low and a high band, each with a frequency and an amplitude) into the four bytes a linear actuator's rumble report takes, clamps the frequencies to each band's ends and the amplitude to the last safe step, and decides when a report is written: the newest value only, a held one again every 30 ms, the neutral value three times once the game stops. Nothing calls it yet. tests/rumble_hd_check.py builds it alone, checks known values, the clamps and a batch reduced to its reports, and decodes a sweep of values with formulae of its own. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
@@ -529,6 +529,14 @@ files.
|
||||
A change to how a gamepad's axes are read (`runtime/input.c` `pad_axis`: an axis counts once it has been seen at rest)
|
||||
passes `python tests\pad_rest_check.py`, which builds that function with stand-ins for SDL. No game files.
|
||||
|
||||
A change to HD rumble's encoding or report schedule (`runtime/rumble_hd.c`: a vibration value as the bytes of a Switch
|
||||
controller's rumble report, and which reports are written when) passes `python tests\rumble_hd_check.py`, which builds
|
||||
that file alone: known values and the neutral one, the frequency and amplitude clamps (no amplitude step above the
|
||||
actuators' safe maximum), a report's data for two actuators and for one, and a batch of values reduced to the reports
|
||||
written (the newest value at once, a held one again every 30 ms, the neutral value three times once the game stops).
|
||||
The script then decodes a sweep of encoded values with formulae of its own. No game files, no SDL, no controller: what
|
||||
a controller does with the reports is not covered.
|
||||
|
||||
A change to how a mesh pipeline's fragment module gets the inputs no mesh output feeds (`runtime/vk_pipeline.c`
|
||||
`spirv_mesh_outputs`, `spirv_fs_fill_inputs`) passes `python tests\fs_fill_inputs_check.py`, which builds both
|
||||
functions and runs them on mesh and fragment shaders it compiles with glslang, then validates the result. No game
|
||||
|
||||
133
runtime/rumble_hd.c
Normal file
133
runtime/rumble_hd.c
Normal file
@@ -0,0 +1,133 @@
|
||||
// HD rumble for controllers with linear actuators: the encoding of a vibration value and the report schedule
|
||||
// (rumble_hd.h). The byte layout and the step tables are those of the Switch controllers' rumble output report.
|
||||
//
|
||||
// One actuator's value is four bytes, for a high band (hf, 9 bits; its amplitude ha, 7 bits) and a low band (lf,
|
||||
// 7 bits; its amplitude la, 7 bits):
|
||||
// byte 0 hf bits 0-7
|
||||
// byte 1 hf bit 8 in bit 0, ha in bits 1-7
|
||||
// byte 2 lf in bits 0-6, la bit 0 in bit 7
|
||||
// byte 3 0x40 + la bits 1-6
|
||||
// Frequencies are steps of 1/32 octave above 10 Hz: n = round(32 * log2(f / 10)). The low band takes n 0x41..0xBF
|
||||
// (40.9 to 626.3 Hz) as lf = n - 0x40, the high band n 0x61..0xDF (81.8 to 1252.6 Hz) as hf = (n - 0x60) * 4.
|
||||
// Amplitudes are steps 0..100 on a scale that is finer where the amplitude is larger (rumble_hd_step_amp), written to
|
||||
// both bands as ha = la = step. Steps above 100 exist in the format and drive the actuator past what it is built for;
|
||||
// rumble_hd_amp_step never returns one.
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "rumble_hd.h"
|
||||
|
||||
const uint8_t rumble_hd_neutral[RUMBLE_HD_DATA] = {0x00, 0x01, 0x40, 0x40, 0x00, 0x01, 0x40, 0x40};
|
||||
|
||||
// The amplitude scale: step 0 is off, step 1 the smallest amplitude there is, steps 2 to 15 a quarter octave apart,
|
||||
// 16 to 31 a sixteenth, 32 to 100 a thirty-second, ending at 1.0.
|
||||
#define AMP_STEP1 0.007843f
|
||||
#define AMP_STEP2 0.011823f
|
||||
|
||||
float rumble_hd_step_amp(int step) {
|
||||
if (step <= 0) return 0;
|
||||
if (step == 1) return AMP_STEP1;
|
||||
if (step < 16) return AMP_STEP2 * powf(2, (float)(step - 2) / 4);
|
||||
if (step < 32) return powf(2, (float)step / 16) / 17;
|
||||
if (step > RUMBLE_HD_AMP_STEPS) step = RUMBLE_HD_AMP_STEPS;
|
||||
return powf(2, (float)step / 32) / 8.7f;
|
||||
}
|
||||
|
||||
static int clampi(int v, int lo, int hi) { return v < lo ? lo : v > hi ? hi : v; }
|
||||
|
||||
int rumble_hd_amp_step(float a) {
|
||||
if (!(a >= AMP_STEP1 / 2)) return 0; // also a NaN
|
||||
if (a >= 1) return RUMBLE_HD_AMP_STEPS;
|
||||
// The three ranges meet half way (on the log scale) between their neighbouring steps: 1 and 2, 15 and 16, 31 and 32.
|
||||
if (a < 0.00963f) return 1;
|
||||
if (a < 0.1150f) return clampi(2 + (int)lroundf(4 * log2f(a / AMP_STEP2)), 2, 15);
|
||||
if (a < 0.2276f) return clampi((int)lroundf(16 * log2f(a * 17)), 16, 31);
|
||||
return clampi((int)lroundf(32 * log2f(a * 8.7f)), 32, RUMBLE_HD_AMP_STEPS);
|
||||
}
|
||||
|
||||
// The frequency step of f Hz within lo..hi (step numbers); a NaN or a frequency at or below 0 is the lowest.
|
||||
static int freq_step(float f, int lo, int hi) {
|
||||
if (!(f > 0)) return lo;
|
||||
float n = 32 * log2f(f / 10);
|
||||
return n <= (float)lo ? lo : n >= (float)hi ? hi : clampi((int)lroundf(n), lo, hi);
|
||||
}
|
||||
|
||||
void rumble_hd_encode(const RumbleHdValue *v, float strength, uint8_t out[RUMBLE_HD_SIDE]) {
|
||||
float s = strength > 0 ? strength : 0;
|
||||
int la = rumble_hd_amp_step(v->amp_low * s), ha = rumble_hd_amp_step(v->amp_high * s);
|
||||
if (!la && !ha) { memcpy(out, rumble_hd_neutral, RUMBLE_HD_SIDE); return; }
|
||||
int lf = freq_step(v->freq_low, 0x41, 0xBF) - 0x40, hf = (freq_step(v->freq_high, 0x61, 0xDF) - 0x60) * 4;
|
||||
out[0] = (uint8_t)(hf & 0xff);
|
||||
out[1] = (uint8_t)((hf >> 8) | ha << 1);
|
||||
out[2] = (uint8_t)(lf | (la & 1) << 7);
|
||||
out[3] = (uint8_t)(0x40 + (la >> 1));
|
||||
}
|
||||
|
||||
void rumble_hd_data(const RumbleHdValue v[2], const int live[2], float strength, int one, uint8_t out[RUMBLE_HD_DATA]) {
|
||||
memcpy(out, rumble_hd_neutral, RUMBLE_HD_DATA);
|
||||
float power[2] = {0, 0};
|
||||
for (int s = 0; s < 2; s++) {
|
||||
if (!live[s]) continue;
|
||||
rumble_hd_encode(&v[s], strength, out + s * RUMBLE_HD_SIDE);
|
||||
// how strong the side is, for a controller that can play only one: the two bands' amplitudes as energy
|
||||
float al = v[s].amp_low > 0 ? v[s].amp_low : 0, ah = v[s].amp_high > 0 ? v[s].amp_high : 0;
|
||||
power[s] = al * al + ah * ah;
|
||||
}
|
||||
if (!one) return;
|
||||
int pick = power[1] > power[0];
|
||||
memcpy(out + (1 - pick) * RUMBLE_HD_SIDE, out + pick * RUMBLE_HD_SIDE, RUMBLE_HD_SIDE);
|
||||
}
|
||||
|
||||
void rumble_hd_report(const uint8_t data[RUMBLE_HD_DATA], unsigned number, uint8_t *out, unsigned n) {
|
||||
memset(out, 0, n);
|
||||
out[0] = RUMBLE_HD_REPORT_ID;
|
||||
out[1] = (uint8_t)(number & 0xf);
|
||||
memcpy(out + 2, data, RUMBLE_HD_DATA);
|
||||
}
|
||||
|
||||
static int is_neutral(const uint8_t d[RUMBLE_HD_DATA]) { return !memcmp(d, rumble_hd_neutral, RUMBLE_HD_DATA); }
|
||||
|
||||
// What the controller is to play at `now`: the game's data while it is fresh and the path is on, else the neutral value.
|
||||
static const uint8_t *target(const RumbleHd *h, int on, uint64_t now) {
|
||||
return on && h->have && now - h->want_at <= RUMBLE_HD_STALE_NS ? h->want : rumble_hd_neutral;
|
||||
}
|
||||
|
||||
void rumble_hd_post(RumbleHd *h, const uint8_t data[RUMBLE_HD_DATA], uint64_t now) {
|
||||
memcpy(h->want, data, RUMBLE_HD_DATA);
|
||||
h->want_at = now;
|
||||
h->have = 1;
|
||||
}
|
||||
|
||||
int rumble_hd_due(const RumbleHd *h, int on, uint64_t now, uint64_t gap_ns, uint8_t out[RUMBLE_HD_DATA]) {
|
||||
const uint8_t *t = target(h, on, now);
|
||||
int due;
|
||||
if (!h->any) due = !is_neutral(t); // a controller that got nothing yet is at rest already
|
||||
else if (memcmp(t, h->sent, RUMBLE_HD_DATA)) due = now - h->sent_at >= gap_ns;
|
||||
else if (is_neutral(t)) due = h->neutral_left > 0 && now - h->sent_at >= gap_ns;
|
||||
else due = now - h->sent_at >= RUMBLE_HD_REFRESH_NS;
|
||||
if (due) memcpy(out, t, RUMBLE_HD_DATA);
|
||||
return due;
|
||||
}
|
||||
|
||||
void rumble_hd_sent(RumbleHd *h, const uint8_t data[RUMBLE_HD_DATA], uint64_t now) {
|
||||
if (!is_neutral(data)) h->neutral_left = 0;
|
||||
else if (!h->any || !is_neutral(h->sent)) h->neutral_left = RUMBLE_HD_NEUTRAL_REPORTS - 1;
|
||||
else if (h->neutral_left > 0) h->neutral_left--;
|
||||
memcpy(h->sent, data, RUMBLE_HD_DATA);
|
||||
h->sent_at = now;
|
||||
h->any = 1;
|
||||
}
|
||||
|
||||
int rumble_hd_idle(const RumbleHd *h, int on, uint64_t now) {
|
||||
if (!is_neutral(target(h, on, now))) return 0;
|
||||
return !h->any || (is_neutral(h->sent) && h->neutral_left == 0);
|
||||
}
|
||||
|
||||
int rumble_hd_buzzing(const RumbleHd *h) { return h->any && !is_neutral(h->sent); }
|
||||
|
||||
void rumble_hd_forget(RumbleHd *h) {
|
||||
h->any = 0;
|
||||
h->neutral_left = 0;
|
||||
h->sent_at = 0;
|
||||
memset(h->sent, 0, sizeof h->sent);
|
||||
}
|
||||
75
runtime/rumble_hd.h
Normal file
75
runtime/rumble_hd.h
Normal file
@@ -0,0 +1,75 @@
|
||||
// HD rumble: the game's vibration values for a controller with linear actuators (Switch Pro Controller, Joy-Con), as
|
||||
// the bytes of the controller's rumble output report, and the decision when a report is written. Such an actuator takes
|
||||
// a frequency and an amplitude for a low and a high band, which is what the game sends (ov_input.c VibrationValue), so
|
||||
// nothing is reduced to motor strengths here. Pure: no SDL and no platform calls; input.c (hd_tick) writes what this
|
||||
// file decides. Checked by tests/rumble_hd_check.py.
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
// One actuator's value as the game sends it: amplitudes 0..1, frequencies in Hz.
|
||||
typedef struct { float amp_low, freq_low, amp_high, freq_high; } RumbleHdValue;
|
||||
|
||||
enum {
|
||||
RUMBLE_HD_SIDE = 4, // bytes of one actuator's value in a report
|
||||
RUMBLE_HD_DATA = 8, // ... and of a report's two: the left actuator's, then the right one's
|
||||
RUMBLE_HD_REPORT_ID = 0x10, // the output report that carries nothing but rumble data
|
||||
RUMBLE_HD_REPORT = 10, // its bytes that count: the id, the report's number, the data
|
||||
RUMBLE_HD_AMP_STEPS = 100, // the highest amplitude step written (amplitude 1.0): the steps above it overdrive the actuator
|
||||
RUMBLE_HD_NEUTRAL_REPORTS = 3, // how many times in a row the neutral value is written when the vibration ends
|
||||
};
|
||||
// The frequencies an actuator takes: the low band's and the high band's ends, in Hz. Others are clamped to them.
|
||||
#define RUMBLE_HD_LOW_MIN 40.875885f
|
||||
#define RUMBLE_HD_LOW_MAX 626.286133f
|
||||
#define RUMBLE_HD_HIGH_MIN 81.75177f
|
||||
#define RUMBLE_HD_HIGH_MAX 1252.572266f
|
||||
#define RUMBLE_HD_STALE_NS 250000000ull // values the game has not renewed for this long count as neutral
|
||||
#define RUMBLE_HD_REFRESH_NS 30000000ull // a value that still holds is written again this often
|
||||
#define RUMBLE_HD_GAP_NS 15000000ull // the default time between two reports (rumble_hd_due's gap)
|
||||
|
||||
// The value of an actuator at rest (no amplitude, 160 Hz and 320 Hz), twice.
|
||||
extern const uint8_t rumble_hd_neutral[RUMBLE_HD_DATA];
|
||||
|
||||
// The amplitude step (0..RUMBLE_HD_AMP_STEPS) nearest to a, and that step's amplitude. A NaN or a value below half of
|
||||
// the first step is step 0; 1 and everything above it is the last step.
|
||||
int rumble_hd_amp_step(float a);
|
||||
float rumble_hd_step_amp(int step);
|
||||
|
||||
// One actuator's value as its four report bytes. strength scales both amplitudes (1 = as the game sent them) before
|
||||
// they are clamped to 0..1. With both amplitudes at step 0 the bytes are the neutral value's, whatever the frequencies.
|
||||
void rumble_hd_encode(const RumbleHdValue *v, float strength, uint8_t out[RUMBLE_HD_SIDE]);
|
||||
|
||||
// A report's data from the game's newest value per side (v[0] left, v[1] right). live[s] 0: that side is at rest
|
||||
// (never sent, stale, or vibration not permitted). one: the controller has a single actuator (one Joy-Con): the
|
||||
// stronger side's value goes into both halves.
|
||||
void rumble_hd_data(const RumbleHdValue v[2], const int live[2], float strength, int one, uint8_t out[RUMBLE_HD_DATA]);
|
||||
|
||||
// The output report: id, number (0..15, going round), data, zeros up to n bytes (n >= RUMBLE_HD_REPORT).
|
||||
void rumble_hd_report(const uint8_t data[RUMBLE_HD_DATA], unsigned number, uint8_t *out, unsigned n);
|
||||
|
||||
// What one controller is to get and what it got. All zero at the start.
|
||||
typedef struct {
|
||||
uint8_t want[RUMBLE_HD_DATA]; // the newest data from the game (rumble_hd_post) ...
|
||||
uint64_t want_at; // ... and when it came
|
||||
int have; // ... once there is one
|
||||
uint8_t sent[RUMBLE_HD_DATA]; // the data of the last report written (rumble_hd_sent) ...
|
||||
uint64_t sent_at; // ... and when
|
||||
int any; // ... once there is one
|
||||
int neutral_left; // neutral reports still to write after the last one
|
||||
} RumbleHd;
|
||||
|
||||
// The game's newest data: it replaces what was not written yet (nothing is queued).
|
||||
void rumble_hd_post(RumbleHd *h, const uint8_t data[RUMBLE_HD_DATA], uint64_t now);
|
||||
// 1 when a report is to be written at `now`, with its data in out. on 0: the controller gets the neutral value whatever
|
||||
// the game sent (the setting is off, the process ends). A changed value waits until gap_ns have passed since the last
|
||||
// report; a value that still holds is written again every RUMBLE_HD_REFRESH_NS, since the actuators stop by themselves
|
||||
// when no report comes; data older than RUMBLE_HD_STALE_NS is neutral; the neutral value is written
|
||||
// RUMBLE_HD_NEUTRAL_REPORTS times, gap_ns apart, and then nothing until the game posts something else.
|
||||
int rumble_hd_due(const RumbleHd *h, int on, uint64_t now, uint64_t gap_ns, uint8_t out[RUMBLE_HD_DATA]);
|
||||
// The report with data was written at `now`.
|
||||
void rumble_hd_sent(RumbleHd *h, const uint8_t data[RUMBLE_HD_DATA], uint64_t now);
|
||||
// 1 when nothing is to be written until the next rumble_hd_post (or until `on` changes).
|
||||
int rumble_hd_idle(const RumbleHd *h, int on, uint64_t now);
|
||||
// 1 while the last report written was not the neutral value: the actuators may still be running.
|
||||
int rumble_hd_buzzing(const RumbleHd *h);
|
||||
// Another controller takes the reports: what was written no longer counts, the game's newest data stays.
|
||||
void rumble_hd_forget(RumbleHd *h);
|
||||
201
tests/rumble_hd_check.c
Normal file
201
tests/rumble_hd_check.c
Normal file
@@ -0,0 +1,201 @@
|
||||
// runtime/rumble_hd.c: a vibration value as the bytes of a linear actuator's rumble report (known values, the clamps,
|
||||
// the neutral value), a report's data from the two sides, and which reports are written when the game sends a batch.
|
||||
// Built and run by tests/rumble_hd_check.py; prints each failed check and exits 1 on any. With --sweep it prints
|
||||
// values and their bytes, one per line, for the script to decode with formulae of its own.
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "rumble_hd.h"
|
||||
|
||||
static int fails;
|
||||
#define CHECK(c, ...) do { if (!(c)) { fails++; printf("FAIL line %d: ", __LINE__); printf(__VA_ARGS__); printf("\n"); } } while (0)
|
||||
|
||||
static int enc_is(float al, float fl, float ah, float fh, float strength, uint8_t b0, uint8_t b1, uint8_t b2, uint8_t b3,
|
||||
uint8_t got[4]) {
|
||||
RumbleHdValue v = {al, fl, ah, fh};
|
||||
rumble_hd_encode(&v, strength, got);
|
||||
return got[0] == b0 && got[1] == b1 && got[2] == b2 && got[3] == b3;
|
||||
}
|
||||
#define ENC(al, fl, ah, fh, st, b0, b1, b2, b3) do { uint8_t g_[4]; \
|
||||
CHECK(enc_is(al, fl, ah, fh, st, b0, b1, b2, b3, g_), "%g@%g Hz %g@%g Hz x%g gave %02x %02x %02x %02x, expected %02x %02x %02x %02x", \
|
||||
(double)(al), (double)(fl), (double)(ah), (double)(fh), (double)(st), g_[0], g_[1], g_[2], g_[3], b0, b1, b2, b3); } while (0)
|
||||
|
||||
static void sweep(void) {
|
||||
static const float amps[] = {0, 0.001f, 0.0038f, 0.004f, 0.006f, 0.0078f, 0.0095f, 0.0098f, 0.012f, 0.02f, 0.05f, 0.09f,
|
||||
0.113f, 0.116f, 0.15f, 0.2f, 0.226f, 0.229f, 0.25f, 0.3f, 0.4f, 0.5f, 0.66f, 0.8f, 0.9f,
|
||||
0.99f, 1, 1.3f, 4};
|
||||
for (size_t a = 0; a < sizeof amps / sizeof *amps; a++)
|
||||
for (size_t b = 0; b < sizeof amps / sizeof *amps; b += 3)
|
||||
for (float f = 20; f < 3000; f *= 1.0717f) {
|
||||
RumbleHdValue v = {amps[a], f, amps[b], f * 1.9f};
|
||||
uint8_t o[4];
|
||||
rumble_hd_encode(&v, 1, o);
|
||||
printf("%.9g %.9g %.9g %.9g %u %u %u %u\n", (double)v.amp_low, (double)v.freq_low, (double)v.amp_high,
|
||||
(double)v.freq_high, o[0], o[1], o[2], o[3]);
|
||||
}
|
||||
}
|
||||
|
||||
#define MS 1000000ull
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc > 1 && !strcmp(argv[1], "--sweep")) { sweep(); return 0; }
|
||||
|
||||
// ---- known values
|
||||
// The neutral value, as the game sends it and as the controller expects it.
|
||||
ENC(0, 160, 0, 320, 1, 0x00, 0x01, 0x40, 0x40);
|
||||
CHECK(!memcmp(rumble_hd_neutral, "\x00\x01\x40\x40\x00\x01\x40\x40", 8), "the neutral data is not 00 01 40 40 twice");
|
||||
// Both bands at 150 Hz and amplitude 0.9: frequency step 125 (low 0x3d, high 0x74), amplitude step 95.
|
||||
ENC(0.9f, 150, 0.9f, 150, 1, 0x74, 0xbe, 0xbd, 0x6f);
|
||||
// Full amplitude at the neutral frequencies: amplitude step 100, the last one.
|
||||
ENC(1, 160, 1, 320, 1, 0x00, 0xc9, 0x40, 0x72);
|
||||
// Amplitude 0.5 is step 68; one band alone leaves the other at step 0.
|
||||
ENC(0.5f, 160, 0, 320, 1, 0x00, 0x01, 0x40, 0x62);
|
||||
ENC(0, 160, 0.5f, 320, 1, 0x00, 0x89, 0x40, 0x40);
|
||||
// An odd amplitude step puts its low bit on top of the low frequency byte: 0.245 is step 35.
|
||||
ENC(0.245f, 160, 0.245f, 320, 1, 0x00, 0x47, 0xc0, 0x51);
|
||||
|
||||
// ---- the clamps
|
||||
// Frequencies outside a band go to its ends: low 1..0x7f, high 4..0x1fc.
|
||||
ENC(0.5f, 10, 0.5f, 5000, 1, 0xfc, 0x89, 0x01, 0x62);
|
||||
ENC(0.5f, 5000, 0.5f, 10, 1, 0x04, 0x88, 0x7f, 0x62);
|
||||
ENC(0.5f, RUMBLE_HD_LOW_MIN, 0.5f, RUMBLE_HD_HIGH_MIN, 1, 0x04, 0x88, 0x01, 0x62);
|
||||
ENC(0.5f, RUMBLE_HD_LOW_MAX, 0.5f, RUMBLE_HD_HIGH_MAX, 1, 0xfc, 0x89, 0x7f, 0x62);
|
||||
ENC(0.5f, 0, 0.5f, -3, 1, 0x04, 0x88, 0x01, 0x62);
|
||||
ENC(0.5f, NAN, 0.5f, NAN, 1, 0x04, 0x88, 0x01, 0x62);
|
||||
// Amplitudes above 1, by themselves or through the strength, stop at step 100; the strength scales below it.
|
||||
ENC(7, 160, 7, 320, 1, 0x00, 0xc9, 0x40, 0x72);
|
||||
ENC(0.5f, 160, 0.5f, 320, 3, 0x00, 0xc9, 0x40, 0x72);
|
||||
ENC(1, 160, 1, 320, 0.5f, 0x00, 0x89, 0x40, 0x62);
|
||||
ENC(INFINITY, 160, INFINITY, 320, 1, 0x00, 0xc9, 0x40, 0x72);
|
||||
// No amplitude is the neutral value whatever the frequencies say: negative, NaN, too small, strength 0.
|
||||
ENC(0, 55, 0, 1000, 1, 0x00, 0x01, 0x40, 0x40);
|
||||
ENC(-1, 55, -0.5f, 1000, 1, 0x00, 0x01, 0x40, 0x40);
|
||||
ENC(NAN, 55, NAN, 1000, 1, 0x00, 0x01, 0x40, 0x40);
|
||||
ENC(0.003f, 55, 0.001f, 1000, 1, 0x00, 0x01, 0x40, 0x40);
|
||||
ENC(1, 55, 1, 1000, 0, 0x00, 0x01, 0x40, 0x40);
|
||||
ENC(1, 55, 1, 1000, -2, 0x00, 0x01, 0x40, 0x40);
|
||||
|
||||
// ---- the amplitude scale
|
||||
static const struct { int step; float amp; } table[] = {{0, 0}, {1, 0.007843f}, {2, 0.011823f}, {6, 0.023648f},
|
||||
{15, 0.112491f}, {16, 0.117471f}, {31, 0.224982f}, {32, 0.229904f}, {64, 0.459808f}, {100, 1.002867f}};
|
||||
for (size_t i = 0; i < sizeof table / sizeof *table; i++) {
|
||||
float a = rumble_hd_step_amp(table[i].step);
|
||||
CHECK(fabsf(a - table[i].amp) <= table[i].amp * 0.003f, "step %d is %f, the table says %f", table[i].step, (double)a,
|
||||
(double)table[i].amp);
|
||||
}
|
||||
for (int k = 0; k <= RUMBLE_HD_AMP_STEPS; k++) {
|
||||
CHECK(rumble_hd_amp_step(rumble_hd_step_amp(k)) == k, "step %d's amplitude %f gives step %d", k,
|
||||
(double)rumble_hd_step_amp(k), rumble_hd_amp_step(rumble_hd_step_amp(k)));
|
||||
if (k) CHECK(rumble_hd_step_amp(k) > rumble_hd_step_amp(k - 1), "step %d is not above step %d", k, k - 1);
|
||||
}
|
||||
int top = 0;
|
||||
for (float a = -1; a < 20; a += 0.0007f) {
|
||||
int k = rumble_hd_amp_step(a);
|
||||
if (k > top) top = k;
|
||||
CHECK(k >= 0 && k <= RUMBLE_HD_AMP_STEPS, "amplitude %f gives step %d", (double)a, k);
|
||||
}
|
||||
CHECK(top == RUMBLE_HD_AMP_STEPS && RUMBLE_HD_AMP_STEPS == 100, "the highest step reached is %d", top);
|
||||
|
||||
// ---- a report's data from the two sides
|
||||
RumbleHdValue v[2] = {{0.5f, 160, 0, 320}, {0, 160, 0.9f, 150}};
|
||||
int both[2] = {1, 1}, left[2] = {1, 0}, none[2] = {0, 0};
|
||||
uint8_t d[8];
|
||||
rumble_hd_data(v, both, 1, 0, d);
|
||||
CHECK(!memcmp(d, "\x00\x01\x40\x62\x74\xbe\x40\x40", 8), "two sides: %02x %02x %02x %02x | %02x %02x %02x %02x", d[0], d[1],
|
||||
d[2], d[3], d[4], d[5], d[6], d[7]);
|
||||
rumble_hd_data(v, left, 1, 0, d);
|
||||
CHECK(!memcmp(d, "\x00\x01\x40\x62\x00\x01\x40\x40", 8), "a side at rest is not neutral");
|
||||
rumble_hd_data(v, none, 1, 0, d);
|
||||
CHECK(!memcmp(d, rumble_hd_neutral, 8), "both sides at rest are not neutral");
|
||||
rumble_hd_data(v, both, 1, 1, d); // one actuator: the stronger side (the right one, 0.9) in both halves
|
||||
CHECK(!memcmp(d, "\x74\xbe\x40\x40\x74\xbe\x40\x40", 8), "one actuator: %02x %02x %02x %02x | %02x %02x %02x %02x", d[0],
|
||||
d[1], d[2], d[3], d[4], d[5], d[6], d[7]);
|
||||
rumble_hd_data(v, left, 1, 1, d); // ... and the side that is not at rest when the stronger one is
|
||||
CHECK(!memcmp(d, "\x00\x01\x40\x62\x00\x01\x40\x62", 8), "one actuator, right side at rest");
|
||||
rumble_hd_data(v, both, 0, 0, d);
|
||||
CHECK(!memcmp(d, rumble_hd_neutral, 8), "strength 0 is not neutral");
|
||||
|
||||
// ---- the output report
|
||||
uint8_t r[49];
|
||||
memset(r, 0xee, sizeof r);
|
||||
rumble_hd_report((const uint8_t *)"\x74\xbe\xbd\x6f\x00\x01\x40\x40", 0x25, r, sizeof r);
|
||||
CHECK(r[0] == 0x10 && r[1] == 0x05 && !memcmp(r + 2, "\x74\xbe\xbd\x6f\x00\x01\x40\x40", 8), "report head %02x %02x", r[0], r[1]);
|
||||
for (int i = 10; i < 49; i++) CHECK(r[i] == 0, "report byte %d is %02x", i, r[i]);
|
||||
|
||||
// ---- from a batch to the reports written: a tick every 5 ms, 15 ms between reports
|
||||
static const uint8_t A[8] = {1, 2, 3, 4, 5, 6, 7, 8}, B[8] = {9, 9, 9, 9, 9, 9, 9, 9}, C[8] = {3, 3, 3, 3, 3, 3, 3, 3},
|
||||
D[8] = {4, 4, 4, 4, 4, 4, 4, 4}, E[8] = {5, 5, 5, 5, 5, 5, 5, 5};
|
||||
RumbleHd h = {0};
|
||||
uint8_t o[8];
|
||||
uint64_t t0 = 1000 * MS;
|
||||
CHECK(!rumble_hd_due(&h, 1, t0, RUMBLE_HD_GAP_NS, o) && rumble_hd_idle(&h, 1, t0), "a report is due before the game sent anything");
|
||||
rumble_hd_post(&h, rumble_hd_neutral, t0);
|
||||
CHECK(!rumble_hd_due(&h, 1, t0, RUMBLE_HD_GAP_NS, o) && rumble_hd_idle(&h, 1, t0), "the neutral value is written to a controller at rest");
|
||||
// three values in one batch: the newest is written, at once
|
||||
rumble_hd_post(&h, A, t0);
|
||||
rumble_hd_post(&h, B, t0);
|
||||
rumble_hd_post(&h, C, t0 + MS);
|
||||
struct { uint64_t at; uint8_t d[8]; } rep[64];
|
||||
int n = 0;
|
||||
for (uint64_t t = t0 + MS; t <= t0 + 400 * MS; t += 5 * MS) {
|
||||
if (t == t0 + 6 * MS) rumble_hd_post(&h, D, t); // 5 ms after the first report: too soon to write
|
||||
if (t == t0 + 11 * MS) rumble_hd_post(&h, E, t); // replaces D before D was written
|
||||
if (rumble_hd_due(&h, 1, t, RUMBLE_HD_GAP_NS, o) && n < 64) {
|
||||
rep[n].at = t - t0;
|
||||
memcpy(rep[n++].d, o, 8);
|
||||
rumble_hd_sent(&h, o, t);
|
||||
}
|
||||
}
|
||||
CHECK(n >= 2 && rep[0].at == MS && !memcmp(rep[0].d, C, 8), "the first report is not the batch's newest value, at once");
|
||||
CHECK(n >= 2 && rep[1].at == 16 * MS && !memcmp(rep[1].d, E, 8), "the second report is not the newest value 15 ms later (at %llu ms)",
|
||||
n >= 2 ? (unsigned long long)(rep[1].at / MS) : 0ull);
|
||||
int held = 0, neutral = 0;
|
||||
uint64_t last_e = 0, first_neutral = 0, last_neutral = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
CHECK(memcmp(rep[i].d, A, 8) && memcmp(rep[i].d, B, 8) && memcmp(rep[i].d, D, 8), "a replaced value was written at %llu ms",
|
||||
(unsigned long long)(rep[i].at / MS));
|
||||
if (i) CHECK(rep[i].at - rep[i - 1].at >= RUMBLE_HD_GAP_NS, "two reports %llu ms apart", (unsigned long long)((rep[i].at - rep[i - 1].at) / MS));
|
||||
if (!memcmp(rep[i].d, E, 8)) {
|
||||
if (last_e) CHECK(rep[i].at - last_e == RUMBLE_HD_REFRESH_NS, "a held value was written again after %llu ms",
|
||||
(unsigned long long)((rep[i].at - last_e) / MS));
|
||||
last_e = rep[i].at, held++;
|
||||
CHECK(!neutral, "a value after the neutral one");
|
||||
}
|
||||
if (!memcmp(rep[i].d, rumble_hd_neutral, 8)) {
|
||||
if (!neutral++) first_neutral = rep[i].at;
|
||||
last_neutral = rep[i].at;
|
||||
}
|
||||
}
|
||||
// E came at 11 ms and is stale 250 ms later: written at 16 ms and every 30 ms until then
|
||||
CHECK(held == 9, "the held value was written %d times", held);
|
||||
CHECK(neutral == RUMBLE_HD_NEUTRAL_REPORTS, "the neutral value was written %d times", neutral);
|
||||
CHECK(first_neutral > 261 * MS && first_neutral <= 261 * MS + RUMBLE_HD_GAP_NS, "the neutral value came %llu ms in",
|
||||
(unsigned long long)(first_neutral / MS));
|
||||
CHECK(last_neutral - first_neutral == 2 * RUMBLE_HD_GAP_NS, "the neutral reports are not 15 ms apart");
|
||||
CHECK(rumble_hd_idle(&h, 1, t0 + 400 * MS) && !rumble_hd_buzzing(&h), "not idle after the neutral reports");
|
||||
|
||||
// the path turned off while the game sends: the neutral value, whatever the game's data
|
||||
RumbleHd g = {0};
|
||||
rumble_hd_post(&g, A, t0);
|
||||
CHECK(rumble_hd_due(&g, 1, t0, RUMBLE_HD_GAP_NS, o) && !memcmp(o, A, 8), "a first value is not due at once");
|
||||
rumble_hd_sent(&g, o, t0);
|
||||
CHECK(!rumble_hd_idle(&g, 1, t0 + MS) && rumble_hd_buzzing(&g), "idle while a value is held");
|
||||
CHECK(!rumble_hd_due(&g, 0, t0 + 5 * MS, RUMBLE_HD_GAP_NS, o), "off: a report inside the gap");
|
||||
CHECK(rumble_hd_due(&g, 0, t0 + 15 * MS, RUMBLE_HD_GAP_NS, o) && !memcmp(o, rumble_hd_neutral, 8), "off: no neutral report");
|
||||
CHECK(rumble_hd_due(&g, 0, t0 + 6 * MS, 5 * MS, o) && !memcmp(o, rumble_hd_neutral, 8), "a 5 ms gap is not honoured");
|
||||
rumble_hd_sent(&g, o, t0 + 15 * MS);
|
||||
CHECK(!rumble_hd_buzzing(&g) && !rumble_hd_idle(&g, 0, t0 + 16 * MS), "idle before the neutral value was repeated");
|
||||
// back on with the game's data still fresh: it is written again
|
||||
CHECK(rumble_hd_due(&g, 1, t0 + 30 * MS, RUMBLE_HD_GAP_NS, o) && !memcmp(o, A, 8), "on again: the game's value is not due");
|
||||
rumble_hd_sent(&g, o, t0 + 30 * MS);
|
||||
// another controller: what was written is forgotten, the game's value is due at once
|
||||
rumble_hd_forget(&g);
|
||||
CHECK(!rumble_hd_buzzing(&g), "buzzing after forget");
|
||||
CHECK(rumble_hd_due(&g, 1, t0 + 31 * MS, RUMBLE_HD_GAP_NS, o) && !memcmp(o, A, 8), "after forget: the value is not due at once");
|
||||
CHECK(!rumble_hd_due(&g, 1, t0 + 300 * MS, RUMBLE_HD_GAP_NS, o) && rumble_hd_idle(&g, 1, t0 + 300 * MS),
|
||||
"after forget: a stale value is written to a controller that got nothing");
|
||||
|
||||
printf(fails ? "rumble hd: %d failures\n" : "rumble hd: ok\n", fails);
|
||||
return fails != 0;
|
||||
}
|
||||
118
tests/rumble_hd_check.py
Normal file
118
tests/rumble_hd_check.py
Normal file
@@ -0,0 +1,118 @@
|
||||
"""HD rumble for controllers with linear actuators (runtime/rumble_hd.c): builds it with tests/rumble_hd_check.c and
|
||||
checks
|
||||
|
||||
- known values: the neutral value, full amplitude, single bands, an odd amplitude step;
|
||||
- the clamps: frequencies to each band's ends, amplitudes to 0..1 with the last step at 100, no amplitude as the
|
||||
neutral value;
|
||||
- a report's data from the two sides, for two actuators and for one, and the output report's layout;
|
||||
- from a batch to the reports written: the newest value at once, replaced values never, a held value again every
|
||||
30 ms, the neutral value three times once the game stops, then nothing.
|
||||
|
||||
Then it decodes a sweep of encoded values with formulae of its own (this script's, not the C file's): each frequency
|
||||
comes back within half a step of the clamped input, each amplitude as the step nearest to it, and no amplitude step is
|
||||
above 100.
|
||||
|
||||
No game files, no SDL, no controller. Usage: python tests/rumble_hd_check.py [--cc <clang>]
|
||||
"""
|
||||
import argparse
|
||||
import math
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
MSYS_BIN = Path(r'C:\msys64\ucrt64\bin')
|
||||
|
||||
LOW = (40.875885, 626.286133) # Hz, the low band's ends
|
||||
HIGH = (81.75177, 1252.572266) # ... and the high band's
|
||||
|
||||
|
||||
def step_amp(k):
|
||||
"""The amplitude of step k, from the published table's rule: step 1 alone, then quarter, sixteenth and
|
||||
thirty-second octaves."""
|
||||
if k == 0:
|
||||
return 0.0
|
||||
if k == 1:
|
||||
return 0.007843
|
||||
if k < 16:
|
||||
return 0.011823 * 2 ** ((k - 2) / 4)
|
||||
if k < 32:
|
||||
return 2 ** (k / 16) / 17
|
||||
return 2 ** (k / 32) / 8.7
|
||||
|
||||
|
||||
def decode(b):
|
||||
"""Four report bytes to (amp_low step, freq_low Hz, amp_high step, freq_high Hz)."""
|
||||
hf = b[0] | (b[1] & 1) << 8
|
||||
ha = b[1] >> 1
|
||||
lf = b[2] & 0x7f
|
||||
la = (b[3] - 0x40) * 2 + (b[2] >> 7)
|
||||
return la, 10 * 2 ** ((lf + 0x40) / 32), ha, 10 * 2 ** ((hf / 4 + 0x60) / 32)
|
||||
|
||||
|
||||
def nearest_step(a):
|
||||
if not a >= 0.007843 / 2:
|
||||
return 0
|
||||
a = min(a, 1.0)
|
||||
return min(range(1, 101), key=lambda k: abs(math.log(a / step_amp(k))))
|
||||
|
||||
|
||||
def check_sweep(lines):
|
||||
fails = 0
|
||||
half = 2 ** (1 / 64) * 1.0005 # half a frequency step, and the rounding of a float
|
||||
for line in lines:
|
||||
f = line.split()
|
||||
al, fl, ah, fh = (float(x) for x in f[:4])
|
||||
b = [int(x) for x in f[4:]]
|
||||
la, dfl, ha, dfh = decode(b)
|
||||
what = f'{al}@{fl} Hz {ah}@{fh} Hz -> {" ".join("%02x" % x for x in b)}'
|
||||
if not (0 <= la <= 100 and 0 <= ha <= 100 and 0x40 <= b[3] <= 0x72):
|
||||
print(f'FAIL {what}: amplitude steps {la}, {ha} outside 0..100')
|
||||
fails += 1
|
||||
continue
|
||||
for name, a, got in (('low', al, la), ('high', ah, ha)):
|
||||
want = nearest_step(a)
|
||||
# at a boundary between two steps either neighbour is the nearest
|
||||
if got != want and (got == 0 or want == 0 or
|
||||
abs(abs(math.log(min(a, 1.0) / step_amp(got))) -
|
||||
abs(math.log(min(a, 1.0) / step_amp(want)))) > 2e-3):
|
||||
print(f'FAIL {what}: {name} amplitude is step {got}, the nearest is {want}')
|
||||
fails += 1
|
||||
if la == 0 and ha == 0:
|
||||
if b != [0x00, 0x01, 0x40, 0x40]:
|
||||
print(f'FAIL {what}: no amplitude is not the neutral value')
|
||||
fails += 1
|
||||
continue
|
||||
for name, hz, got, (lo, hi) in (('low', fl, dfl, LOW), ('high', fh, dfh, HIGH)):
|
||||
want = min(max(hz, lo), hi)
|
||||
if not want / half <= got <= want * half:
|
||||
print(f'FAIL {what}: {name} frequency decodes to {got:.2f} Hz, expected {want:.2f}')
|
||||
fails += 1
|
||||
return fails
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument('--cc', help='the C compiler (default: MSYS2 UCRT64 clang on Windows, clang elsewhere)')
|
||||
args = ap.parse_args()
|
||||
win = os.name == 'nt'
|
||||
cc = args.cc or os.environ.get('CC') or (str(MSYS_BIN / 'clang.exe') if win and MSYS_BIN.exists() else 'clang')
|
||||
env = dict(os.environ)
|
||||
if win and MSYS_BIN.exists():
|
||||
env['PATH'] = str(MSYS_BIN) + os.pathsep + env.get('PATH', '')
|
||||
work = ROOT / 'build' / 'test' / 'rumble_hd'
|
||||
work.mkdir(parents=True, exist_ok=True)
|
||||
exe = work / ('rumble_hd_check.exe' if win else 'rumble_hd_check')
|
||||
subprocess.check_call([cc, '-O1', '-Wall', '-Wextra', '-fno-strict-aliasing', '-I', str(ROOT / 'runtime'),
|
||||
str(ROOT / 'tests' / 'rumble_hd_check.c'), str(ROOT / 'runtime' / 'rumble_hd.c'),
|
||||
'-o', str(exe)] + ([] if win else ['-lm']), env=env)
|
||||
rc = subprocess.run([str(exe)], env=env).returncode
|
||||
out = subprocess.run([str(exe), '--sweep'], env=env, capture_output=True, text=True, check=True).stdout.splitlines()
|
||||
fails = check_sweep(out)
|
||||
print(f'rumble hd sweep: {len(out)} values decoded, ' + (f'{fails} failures' if fails else 'ok'))
|
||||
return 1 if rc or fails or not out else 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user