tl;dr
- pistonで60fps出なかった
- Windowsの
Sleep
はデフォルトだと精度が悪い - sleepを呼ぶ前に
timeBeginPeriod
を呼んで精度を良くしたら60fps出るようになった
pistonで60fps出なかった
趣味でNESのエミュレーターを作っていた時に発生した事象として、pistonというライブラリを使うと60fpsを出せないというものがあった。
60fpsという要件を達成するには、1回の更新毎に約16.6msずつ待ち合わせる必要がある。考えられる実現方法としては、ゲームのロジックを実行する際にその処理が何ms掛かったのかを記録しておき、16.6msからその時間を引いた時間だけ処理を停止する、というものがシンプルで良さそうだ。
use std::time; fn main() { let frame_rate = 60; let sleep_ms = time::Duration::from_secs(1) / frame_rate; loop { let start = time::Instant::now(); game_logic(); // ちょっと時間がかかる let elapsed = time::Instant::now() - start; if sleep_ms < elapsed { // 経過時間より1fの規定時間のほうが長かったらスキップ break; } time::thread::sleep(sleep_ms - elapsed); // 16.6ms - Nms 分だけ停止する } }
この基本方針はRustのゲーム用ライブラリであるpistonも同じだった。同じようにフレーム毎の待ち合いに std::thread::sleep
を使用する。
piston/lib.rs at 380dad8fe2ac21bfd581f6f7338ae45546d6fd9a · naari3/piston · GitHub
State::UpdateLoop(ref mut idle) => { # snip let current_time = Instant::now(); let next_frame = self.last_frame + ns_to_duration(self.dt_frame_in_ns); let next_update = self.last_update + ns_to_duration(self.dt_update_in_ns); let next_event = cmp::min(next_frame, next_update); if next_event > current_time { # snip sleep(next_event - current_time); State::UpdateLoop(Idle::No) # snip } # snip }
ただ、このコードをWindowsの環境上で動かすとうまいこと行かない。60fpsには届かず、自分の手元の環境だと34fpsくらいしか出なかった。
Windowsの Sleep
はデフォルトだと精度が悪い
僕はあまり知らなかったが、有名な話としてWindowsの Sleep
で実際にスリープされる秒数はタイマー割り込みの周期に依存している、というものがある。
例えば Sleep(1)
と実行しても実際にスリープされるのは10msくらいだったり16msくらいだったり、場合によっては55msくらいだったりするらしい*1。
また、Rustの std::thread::sleep
は内部でWindowsの Sleep
を使用している。
つまり、ゲームのロジック実行で13ms掛かった場合、本来は3msだけ待たなければならない所をそれを大幅に超える16msも待ってしまうということになる*2。
sleepを呼ぶ前に timeBeginPeriod
を呼んで精度を良くしたら60fps出るようになった
問題の原因としてはデフォルトのタイマー割り込みの周期が結構広いことが挙げられるので、これを調整してやれば良さそう。
幸いにも timeBeginPeriod
というAPIが用意されているのでこれを叩いてやるように変更する必要がある。
今回この対応をpistonにも導入した。
pistonの方針があまりわからなかったので、WindowsのAPIである timeBeginPeriod
をどのように 呼ぶべきか分からなかった*3。とりあえずこのあたりをうまく実装していそうなライブラリの spin-sleep に依存するようにした。
- sleep(next_event - current_time); + spin_sleep::sleep(next_event - current_time);
spin-sleepは sleep
というAPIを持っているが、Windowsの環境で実行する場合は std::thread::sleep
を実行する前に timeBeginPeriod
を実行し、std::thread::sleep
実行後は timeEndPeriod
を実行する。他の環境に向けてもよしなな実装が入っているため、sleepの精度が良くなりそうだった。
これで無事に60fpsが出るようになった。めでたしめでたし。
*1:http://hp.vector.co.jp/authors/VA007219/rtc_pic.html
*2:13ms + 16ms ≒ 約29ms ≒ 1s / 34
*3:素直にwinapiに依存させるべきか迷ったが、後述の理由もあるのでやめた