RustでWindows用キーボード入力表示ソフトを作るときに悩んだりしたこと

前回：

https://qiita.com/horyu/items/12f6cd13ceb217782df3

## ソフトの概要

https://gitlab.com/horyu/zanei

設定が書かれたYAMLファイルを読み込み、キーボードの切り替えイベントを2/60秒以上3/60秒以下表示する。

- 想定環境
  - OS: Windows 10 以上
  - ディスプレイ: リフレッシュレート60Hz以上
- 設定項目
  - `colors` : `Color3` に対応
  - `fonts` : `IDWriteTextFormat` に対応
  - `window` : ウィンドウの位置・サイズ・背景色を指定
  - `defaultVkCode` : キーの表示で使うデフォルトの幅・高さ・背景色・文字色・フォントを指定
  - `vkCode` : それぞれの vkCode に対応して表示するテキスト・表示位置・表示サイズなどを指定

## WindowsもWin32APIもDirectXもスワップチェーンもフックも何も分からん

- 最初にしっかり目を通しておけば良かったなと思った資料
  - [Get Started with Win32 and C++ - Win32 apps | Microsoft Docs](https://docs.microsoft.com/en-us/windows/win32/learnwin32/learn-to-program-for-windows)
  - [Windows and Messages - Win32 apps | Microsoft Docs](https://docs.microsoft.com/en-us/windows/win32/winmsg/windowing)
  - [DXGI flip model - Win32 apps | Microsoft Docs](https://docs.microsoft.com/en-us/windows/win32/direct3ddxgi/dxgi-flip-model?redirectedfrom=MSDN)
  - [Hooks - Win32 apps | Microsoft Docs](https://docs.microsoft.com/en-us/windows/win32/winmsg/hooks)
- 都度お世話になった資料
  - [データ型 - Windows API 入門](http://kaitei.net/winapi/data-types/)
  - [WinMainを使わずにウィンドウを開く | ぬの部屋（仮）](https://suzulang.com/window-without-winmain/)
  - [[ 884\_文字フォントやコードページと文字セットの関係 ] - Mr.XRAY](http://mrxray.on.coocan.jp/Delphi/plSamples/884_FontCharSetCodePages.htm)
  <!-- - [マルチディスプレイ時の各ディスプレイの座標及び解像度を取得する](http://yamatyuu.net/computer/program/sdk/base/enumdisplay2/index.html) -->
  - [ぶびびんぶろぐ: 大まかな概念：DirectWriteを使った文字列描画](http://bubibinba.blogspot.com/2012/10/directwrite.html)
  - [Direct3D\* 12 フリップモデルのスワップチェーンのサンプル・アプリケーション | iSUS](https://www.isus.jp/games/sample-app-for-direct3d-12-flip-model-swap-chains/)

## serge_yamlにマージ機能がない？

- ない
  - マージ機能はYAML 1.1の仕様
  - serge_yamlはYAML 1.2をサポート
  - [YAML Anchor, Aliases and Merge Keys](https://ktomk.github.io/writing/yaml-anchor-alias-and-merge-key.html)
  - [YAML Merge key << not supported · Issue #163 · dtolnay/serde-yaml](https://github.com/dtolnay/serde-yaml/issues/163)
- マージ機能を後付するクレートはある
  - [yaml-merge-keys - crates.io: Rust Package Registry](https://crates.io/crates/yaml-merge-keys)

## serge_yamlで順序付きマップ(IndexMap)はどうSerializeされるの？

- そもそもYAMLはキーの順序を保証していない
  - [Example 2.26 Ordered Mappings](https://yaml.org/spec/1.2.2/#example-ordered-mappings)
- 順序を保証する形式にするマクロは用意されている
  - [indexmap::serde_seq - Rust](https://docs.rs/indexmap/1.9.1/indexmap/serde_seq/index.html)

```Cargo.toml
[dependencies]
serde = {version = "1.0", features = ["derive"]}
serde_yaml = "0.8"
indexmap = { version = "1.9", features = ["serde"]}
```

```rust
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct MyStruct {
    mymap1: IndexMap<i32, String>,
    #[serde(with = "indexmap::serde_seq")]
    mymap2: IndexMap<i32, String>,
}

fn main() {
    let mut im_num_string = IndexMap::new();
    im_num_string.insert(5, "five".to_string());
    im_num_string.insert(1, "one".to_string());
    im_num_string.insert(10, "ten".to_string());
    println!("{}", serde_yaml::to_string(&im_num_string).unwrap());

    let my_struct = MyStruct{
        mymap1: im_num_string.clone(),
        mymap2: im_num_string.clone(),
    };
    println!("{}", serde_yaml::to_string(&my_struct).unwrap());
}
```

```txt
---
5: five
1: one
10: ten

---
mymap1:
  5: five
  1: one
  10: ten
mymap2:
  - - 5
    - five
  - - 1
    - one
  - - 10
    - ten
```

## Arrayの中にvec!を書いて初期化できない

```rust
PS P:\program\rust\novcs> cargo build
   Compiling novcs v0.1.0 (P:\program\rust\novcs)
error[E0277]: the trait bound `Vec<bool>: Copy` is not satisfied
  --> src\main.rs:11:7
   |
11 |     [[vec![false]; 2]];
   |       ^^^^^^^^^^^ the trait `Copy` is not implemented for `Vec<bool>`
   |
   = note: the `Copy` trait is required because this value will be copied for each element of the array
   = note: this error originates in the macro `vec` (in Nightly builds, run with -Z macro-backtrace 
for more info)

For more information about this error, try `rustc --explain E0277`.
error: could not compile `novcs` due to previous error
```

- それを行うマクロを定義したクレートがいくつかある
  - [【Rust】配列を初期化するn通りの方法と制約 - Kludge Factory](https://tyfkda.github.io/blog/2020/03/19/rust-init-array.html)
  - [array-macro - crates.io: Rust Package Registry](https://crates.io/crates/array-macro/)

```Cargo.toml
[dependencies]
array-macro = "2.1.5"
```

```rust
use array_macro::array;

fn main() {
    const SIZE: usize = 2;
    let arr: [Vec<bool>; SIZE] = array![vec![false]; SIZE];
    // べた書きでも可能ではある
    assert!([vec![false], vec![false]] == arr);
}
```

## スタックオーバーフローする

```txt
PS P:\program\rust\zanei> cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.10s
     Running `target\debug\zanei.exe`

thread 'main' has overflowed its stack
error: process didn't exit successfully: `target\debug\zanei.exe` (exit code: 0xc00000fd, STATUS_STACK_OVERFLOW)
```

- 標準のスタックサイズ（MSVCのリンカーの場合 1MB）だと足りないので指定すれば良い
  - [/STACK (スタック割り当て) | Microsoft Docs](https://docs.microsoft.com/ja-jp/cpp/build/reference/stack-stack-allocations?view=msvc-170)
  - [/F (スタック サイズの設定) | Microsoft Docs](https://docs.microsoft.com/ja-jp/cpp/build/reference/f-set-stack-size?view=msvc-170)

- `config.toml` で設定する場合
  - [Configuration - The Cargo Book](https://doc.rust-lang.org/cargo/reference/config.html)

```.cargo\config.toml
[build]
# 2 * (2 ** 20) byte
rustflags = ["-C", "link-args=/STACK:2097152"]
```

- `build.rs` で設定する場合
  - [Build Scripts - The Cargo Book](https://doc.rust-lang.org/cargo/reference/build-scripts.html#cargorustc-link-argflag)

```rust:build.rs
fn main() {
    println!("cargo:rustc-link-arg=/STACK:{}", 2usize * 2usize.pow(20));
}
```

## コマンドプロンプトが邪魔

- 消せる
  - [RustでWindowsのコマンドプロンプトウィンドウを抑止する方法 - Qiita](https://qiita.com/LNSEAB/items/6f60da458460274e768d)
  - [1665-windows-subsystem - The Rust RFC Book](https://rust-lang.github.io/rfcs/1665-windows-subsystem.html)
  - [/SUBSYSTEM (サブシステムの指定) | Microsoft Docs](https://docs.microsoft.com/ja-jp/cpp/build/reference/subsystem-specify-subsystem?view=msvc-170)

```rust:src/main.rs
// 常時消す
#![windows_subsystem = "windows"]
// デバッグビルドでないなら消す
// #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
```

## キーボード入力がラグくなる

- 原因：フリップモデルのスワップチェーンで `Present(1, 0)` を実行すると次のvertical blankまでメインスレッドがブロックされ、その間、キーボード入力にフックしたコールバック関数が詰まる
  - [DXGI overview - Win32 apps | Microsoft Docs](https://docs.microsoft.com/en-us/windows/win32/direct3ddxgi/d3d10-graphics-programming-guide-dxgi#multithread-considerations)
    - > Be careful that you never have the message-pump thread wait on the render thread when you use full-screen swap chains. For instance, calling IDXGISwapChain1::Present1 (from the render thread) may cause the render thread to wait on the message-pump thread.
  - [Windowsのフックのはまりどころ備忘録 - Qiita](https://qiita.com/kob58im/items/df01765c5059f725174c)
      - > コールバック関数を実行するスレッドをブロックしてしまっている（未検証）
- 対応：レンダリングを別スレッドにする

## 実行ファイルにアイコンを設定したい

- 楽に設定させてくれるクレートがある
    - [Setting a Rust Executable’s Icon in Windows : rust](https://www.reddit.com/r/rust/comments/kram74/setting_a_rust_executables_icon_in_windows/)
    - [mxre/winres: Create and set windows icons and metadata for executables with a rust build script](https://github.com/mxre/winres)

- `winres` の場合
    1. [Windows SDK - Windows app development | Microsoft Developer](https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/) から Windows SDK をインストール
    2. icoファイルを用意する（Cargoプロジェクトルートに `test.ico` を配置したとする）
    3. `Cargo.toml` と `build.rs` を設定する
    4. `cargo build`

```Cargo.toml
[build-dependencies]
winres = "0.1"
```

```rust:build.rs
fn main() {
  if cfg!(target_os = "windows") {
    let mut res = winres::WindowsResource::new();
    res.set_icon("test.ico");
    res.compile().unwrap();
  }
}
```

![exe-ico.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/186959/2f5b3b2f-492b-2109-2d82-b88a24bbdb84.png)

- 左上：エクスプローラーのアイコン
- 右上：ウィンドウアイコン
- 左下：タスクバーのアイコン


## どうやって60FPSで動くゲームと同期するの

- 分からん
- ディスプレイのリフレッシュレートが60Hzの場合は同期する認識
- フリップモデルのスワップチェーンで Present(1, 0) 実行直後の時間をキーボード入力表示ソフトの基準時間にすることも考えましたが、ゲームと同期されるか分からないのでやめました
    - リフレッシュレートが120Hzのとき、ゲームは偶数フレーム更新、キーボード入力表示ソフトが奇数フレーム更新になりえる

## 感想

### 自分が必要とする機能だけあればいい

他の方が作って公開しているキーボード入力表示ソフトには [ThoNohT/NohBoard](https://github.com/ThoNohT/NohBoard) や [ApVpk](https://forum.speeddemosarchive.com/post/a_new_keyboardmouse_visualizer_create_your_own_layouts.html) がある。最初はこれらに実装されている機能を基本的に実装しようと思っていたけど、使わない機能を実装するモチベーションが湧かなかった。

- 設定編集用GUI
- アウトライン
- 特殊キー
- 画像

### 設定ファイルが面倒

どのキーを押したか分かるようにするために、キーごとに文字を設定するのが普通だと思う。その上でWindowsで文字を表示するために必要な引数が多すぎる（参考： [ぶびびんぶろぐ: 大まかな概念：DirectWriteを使った文字列描画](http://bubibinba.blogspot.com/2012/10/directwrite.html) ）。レイアウト周りや `D2D1_DRAW_TEXT_OPTIONS` は固定値にした。

ENUMとして関数に渡される設定項目は、値と識別子のどちらを設定ファイルに記述させるか悩み、最終的に値にした。同じ値に複数の識別子が割り当てられてることがあるので、値のほうが検証の記述量を抑えられそうだった。[DWRITE\_FONT\_WEIGHT (dwrite.h) - Win32 apps | Microsoft Docs](https://docs.microsoft.com/en-us/windows/win32/api/dwrite/ne-dwrite-dwrite_font_weight) が顕著。

```cpp
typedef enum DWRITE_FONT_WEIGHT {
  DWRITE_FONT_WEIGHT_THIN = 100,
  DWRITE_FONT_WEIGHT_EXTRA_LIGHT = 200,
  DWRITE_FONT_WEIGHT_ULTRA_LIGHT = 200,
  DWRITE_FONT_WEIGHT_LIGHT = 300,
  DWRITE_FONT_WEIGHT_SEMI_LIGHT = 350,
  DWRITE_FONT_WEIGHT_NORMAL = 400,
  DWRITE_FONT_WEIGHT_REGULAR = 400,
  DWRITE_FONT_WEIGHT_MEDIUM = 500,
  DWRITE_FONT_WEIGHT_DEMI_BOLD = 600,
  DWRITE_FONT_WEIGHT_SEMI_BOLD = 600,
  DWRITE_FONT_WEIGHT_BOLD = 700,
  DWRITE_FONT_WEIGHT_EXTRA_BOLD = 800,
  DWRITE_FONT_WEIGHT_ULTRA_BOLD = 800,
  DWRITE_FONT_WEIGHT_BLACK = 900,
  DWRITE_FONT_WEIGHT_HEAVY = 900,
  DWRITE_FONT_WEIGHT_EXTRA_BLACK = 950,
  DWRITE_FONT_WEIGHT_ULTRA_BLACK = 950
} ;
```

### MITライセンスでソースコードを公開すると思うとサボれる

「気になる所があったら、公開しているソースコードを使って好き勝手やってくれ！」と踏ん切りをつけられた。ただ自分が望む機能は実装済みなので、管理する気ないよアピールとしてアーカイブ化するべきかもしれない。