e-tipsmemo

ごった煮

Piston_window with Nintendo Switch Procon

e-tipsmemo.hatenablog.com

前回NESエミュレータでキー入力からのPAD操作を実装することができていたが、
やはりゲームパッドでコントロールしたいという願望があったため、Nintendo Switchのプロコンを買った。

しかしながら、piston_windowは(少なくとも)Switchプロコンには(おそらく)対応していなかった。
(入力イベントが発生しない)

そこで今回piston_windowでRender Eventの60fpsを維持しつつ、プロコンの入力を得るために試行錯誤した。

使用するクレートはjoycon-rs


Gamepadから読み取りを行うのはそれなりに遅いので、
Receive Report Exampleのようにプロコンの情報を読み取る処理を別スレッドに分ける。
joycon_rs - Rust

Gamepad 読み出しスレッド

雑なエラーハンドルだが、
DeviceTypeがプロコンのものがあったら、driverをリターンし、HIDmodeで扱う。
(本当は動的なConnectionとDisconnectionにも対応したかったが後回し)

let driver = devices
        .iter()
        .flat_map(|device| SimpleJoyConDriver::new(&device))
        .find(|driver| driver.joycon().deref().device_type() == JoyConDeviceType::ProCon);

    let pad = match driver {
        Some(driver) => SimpleHIDMode::new(driver).ok(),
        None => None,
    };

    let pad_read = Arc::new(Mutex::new(pad::PadButton::empty()));
    let pad_thread = Arc::clone(&pad_read);

    if let Some(procon) = pad {
        std::thread::spawn(move || {
            // let tx = tx.clone();
            loop {
                let res = procon.read_input_report();
                // println!("{:?}", report);
                if let Ok(report) = res {
                    let SimpleHIDReport {
                        input_report_id,
                        pushed_buttons,
                        stick_direction,
                        filler_data,
                    } = report;

                    if input_report_id == 63 {
                        let btns = pushed_buttons
                            .into_iter()
                            .fold(pad::PadButton::empty(), |pad_btn, procon_btn| {
                                pad_btn | pad::button_match(procon_btn)
                            });
                        let pov = pad::pov_match(stick_direction);

                        let mut pad = pad_thread.lock().unwrap();
                        *pad = btns | pov;
                    }
                }
            }
        });
    }

pad情報をArc<Mutex<T>>を介して、共有する。

PadButtonはNESのPadに対応し、button_matchとpov_matchはプロコンの入力を変換するだけ。

bitflags! {
    #[derive(Default)]
    pub struct PadButton:u8{
        const A = 1 << 0;
        const B = 1 << 1;
        const START = 1 << 2;
        const SELECT = 1 << 3;
        const UP = 1 << 4;
        const DOWN = 1 << 5;
        const LEFT = 1 << 6;
        const RIGHT = 1 << 7;
    }
}

ゲームループ

共有変数のlockを何回も要求することがないように、pad_statusに代入している。

 let mut pad_status = PadButton::empty();

 while let Some(event) = window.next() {
        match event {
            Event::Loop(Loop::Render(_)) => {
                tick = std::time::Instant::now();
                let duration = tick - last_tick;
                last_tick = tick;
                let fps = 1000.0 / (duration.as_millis() as f32);
                window.set_title(format!("fps:{:.*}", 1, fps));

                pad_status = *pad_read.lock().unwrap();

                let r = if pad_status.contains(PadButton::A) { 1.0 } else { 0.0 };
                let g = if pad_status.contains(PadButton::B) { 1.0 } else { 0.0 };
                let b = if pad_status.contains(PadButton::START) { 1.0 } else { 0.0 };

                window.draw_2d(&event, |context, graphics, _device| {
                    clear([1.0; 4], graphics);
                    rectangle(
                        [r, g, b, 1.0], // red
                        [0.0, 0.0, 100.0, 100.0],
                        context.transform,
                        graphics,
                    );
                });
            }
            _ => {}
        }
    }

しかしlockは要求・取得した時点でのスコープを抜けるまでlockし続けている気がする(要調査)ので、
lockしている時間を最小にするためには、それ自体を別のスコープで区切ったらいいのかもしれない(本当か?)。
こんな感じで?

...
window.set_title(format!("fps:{:.*}", 1, fps));
 {
  pad_status = *pad_read.lock().unwrap();
 }
let r = if pad_status.contains(PadButton::A) { 1.0 } else { 0.0 };
...

これでも動いた。

まとめ

この拙い実装をNESエミュレータにいれたところ、しっかりと描写は60fpsを維持できて、マリオもストレスなくプレイできた。
最初はサンプルにあるような

let (tx, rx) = std::sync::mpsc::channel()

でPAD情報をsend, recvしたところ、プロコンの入力をからキャラが動くまでに遅延が生じたり、描写fpsが一瞬だけ60fps --> 10fpsになったりと不明な動きをしていた。
割と遅いIOを扱うために、マルチスレッドを使用する意義があったと思うし
そこでArc<Mutex<T>>といった扱いやすい標準のライブラリを使えたことでRustので実装している意義もあったと思う。

そもそもとして、piston_windowがプロコンに対応していれば問題なかったのだが、ほかの純正コントローラーは値段が高い。