Why
デバイス時刻の校正と wrap 処理をここに閉じる。この 2 つが狂うと表示される F 数が丸ごと無意味になる。純粋なロジックとして切り出し、合成データでテストできる形にする。
What
src/timeline.rs を作る。hidapi に依存させない。
pub struct InputEvent {
pub kind: EventKind,
pub target: Target,
pub at_us: u64,
pub gap_before: bool,
}
pub enum EventKind { Press, Release }
pub enum Target { Button(Buttons), Dpad(Direction), Stick(Direction) }
pub struct Timeline { /* 内部状態 */ }
impl Timeline {
pub fn new(device: Device, stick_deadzone: f32, stick_hysteresis_deg: f32) -> Self;
pub fn push(&mut self, state: PadState, host_qpc_us: u64) -> Vec<InputEvent>;
pub fn now_us(&self) -> u64;
pub fn scale_us_per_tick(&self) -> f64;
pub fn scale_warning(&self) -> Option<String>;
}
やることは 5 つ。
1 つ目はスケール校正。デバイス時刻の 1 tick が何µs かを決める。公称値は DualSense が 0.33、DualShock 4 が 5.33。機種と FW によって DualSense が 1.0 になる分岐が SDL に存在するため、決め打ちしない。最初の 2 秒 (host_qpc_us で判定) のレポートを溜め、host_qpc_us の差を device_ts の差で割って推定する。推定が終わるまでは公称値を使う。推定値が公称値から 20% 以上外れたら scale_warning に文言を入れる。
2 つ目は wrap 処理。device_ts は DualSense が 32bit、DualShock 4 が 16bit で回る。5.33µs × 65536 は約 349ms なので、DualShock 4 では無入力が 349ms を超えると前回値との比較だけでは wrap 回数が決まらない。host_qpc_us の経過時間から期待される経過 tick 数を出し、それに最も近い wrap 回数を選ぶ。
3 つ目はイベント化。前回の PadState と比較し、立ったビットを Press、落ちたビットを Release にする。dpad は方向が変わったら前の方向の Release と新しい方向の Press を出す。左スティックは 8 方向に量子化してから同じ扱いにする。量子化は中心からの正規化距離が stick_deadzone を割ったらニュートラルとする。
量子化だけだと方向が境界で揺れる。8 方向の境界にスティックを置くと、量子化結果が隣り合う 2 方向を往復し、Release と Press の対が延々と出る。スティックはアナログなので、境界の上に止めるのは難しくない。
方向が変わるのは、新しい方向の中心軸から stick_hysteresis_deg 以内に入ったときだけにする。既定は 15 度。それより手前では前の方向を保つ。ニュートラルへの復帰は stick_deadzone だけで決める。
4 つ目は現在時刻の公開。now_us は最後に push したレポートの at_us を返す。イベントを 1 件も出さなかったレポートでも進む。押しっぱなしの持続時間を数えるのと、無入力が続いたことを検出するのに使う。どちらもイベントが来ないので、イベントの列だけでは時刻が進まない。
5 つ目は欠落の検出。seq が 1 つ進んでいなければ gap_before を true にする。DualSense は 8bit、DualShock 4 は 6bit で回るので、それぞれの幅で比較する。欠落を補間しない。
at_us は最初のレポートを 0 とした連続値にする。
ゴール
以下がそれぞれ独立にテストされている。
- スケール校正が公称値どおりの合成データで公称値を推定する
- スケール校正が 3 倍ずれた合成データで
scale_warning を返す
- DualShock 4 の 16bit wrap が 1 回、2 回、5 回の各ケースで正しい
at_us になる
- 無入力 400ms を挟んだ wrap が
host_qpc_us から解決される
seq の欠落が gap_before に出る
- dpad の方向変更が
Release と Press の対で出る
- 左スティックが
stick_deadzone を割ったときにニュートラル扱いになる
- 8 方向の境界の上でスティックを微動させた合成データで、方向が 1 度も変わらない
- 境界から
stick_hysteresis_deg を超えて踏み込んだ合成データで、方向が変わる
- イベントを 1 件も出さないレポートを続けて
push したあとでも now_us が進む
あわせて以下を満たす。
timeline.rs が hidapi に依存していない
cargo coupling --check --min-grade=B --max-circular=0 ./src が通る
分解: #1 (comment) / Task 3 / e29acba562d5
Why
デバイス時刻の校正と wrap 処理をここに閉じる。この 2 つが狂うと表示される F 数が丸ごと無意味になる。純粋なロジックとして切り出し、合成データでテストできる形にする。
What
src/timeline.rsを作る。hidapiに依存させない。やることは 5 つ。
1 つ目はスケール校正。デバイス時刻の 1 tick が何µs かを決める。公称値は DualSense が
0.33、DualShock 4 が5.33。機種と FW によって DualSense が1.0になる分岐が SDL に存在するため、決め打ちしない。最初の 2 秒 (host_qpc_usで判定) のレポートを溜め、host_qpc_usの差をdevice_tsの差で割って推定する。推定が終わるまでは公称値を使う。推定値が公称値から 20% 以上外れたらscale_warningに文言を入れる。2 つ目は wrap 処理。
device_tsは DualSense が 32bit、DualShock 4 が 16bit で回る。5.33µs × 65536は約 349ms なので、DualShock 4 では無入力が 349ms を超えると前回値との比較だけでは wrap 回数が決まらない。host_qpc_usの経過時間から期待される経過 tick 数を出し、それに最も近い wrap 回数を選ぶ。3 つ目はイベント化。前回の
PadStateと比較し、立ったビットをPress、落ちたビットをReleaseにする。dpad は方向が変わったら前の方向のReleaseと新しい方向のPressを出す。左スティックは 8 方向に量子化してから同じ扱いにする。量子化は中心からの正規化距離がstick_deadzoneを割ったらニュートラルとする。量子化だけだと方向が境界で揺れる。8 方向の境界にスティックを置くと、量子化結果が隣り合う 2 方向を往復し、
ReleaseとPressの対が延々と出る。スティックはアナログなので、境界の上に止めるのは難しくない。方向が変わるのは、新しい方向の中心軸から
stick_hysteresis_deg以内に入ったときだけにする。既定は 15 度。それより手前では前の方向を保つ。ニュートラルへの復帰はstick_deadzoneだけで決める。4 つ目は現在時刻の公開。
now_usは最後にpushしたレポートのat_usを返す。イベントを 1 件も出さなかったレポートでも進む。押しっぱなしの持続時間を数えるのと、無入力が続いたことを検出するのに使う。どちらもイベントが来ないので、イベントの列だけでは時刻が進まない。5 つ目は欠落の検出。
seqが 1 つ進んでいなければgap_beforeをtrueにする。DualSense は 8bit、DualShock 4 は 6bit で回るので、それぞれの幅で比較する。欠落を補間しない。at_usは最初のレポートを 0 とした連続値にする。ゴール
以下がそれぞれ独立にテストされている。
scale_warningを返すat_usになるhost_qpc_usから解決されるseqの欠落がgap_beforeに出るReleaseとPressの対で出るstick_deadzoneを割ったときにニュートラル扱いになるstick_hysteresis_degを超えて踏み込んだ合成データで、方向が変わるpushしたあとでもnow_usが進むあわせて以下を満たす。
timeline.rsがhidapiに依存していないcargo coupling --check --min-grade=B --max-circular=0 ./srcが通る分解: #1 (comment) / Task 3 / e29acba562d5