C++ コンパイル時「出力」で画像ファイル生成

[C++ Advent Calendar 2023](https://qiita.com/advent-calendar/2023/cxx) 16日目の記事は @niina さんの[2023年のコンパイル時レイトレーシング](https://in-neuro.hatenablog.com/entry/2023/12/16/233332)でした。
constexpr 発展の歴史と C++23 版コンパイル時レイトレーシングの2本立てで、とても面白い記事でした。
やはりコンパイル時処理はわくわくしますね。

しかし、この記事を読んでいて私は思いました。

> ファイルの入出力は流石にコンパイル時にはできません。

***誰ができないって決めたんですか？***

というわけで、この記事では画像ファイルの出力をコンパイル時にやります。



# コンパイル時出力可能な画像形式

## コンパイル時出力

**コンパイル時に出力はできます。**

昨年、そのことを記事にしました。

- [**C++ コンパイル時「出力」　～C++にできないことはない～**](https://qiita.com/Raclamusi/items/418d08f8e64fa1959850)
    - この記事では、`static_assert` のエラーメッセージをエスケープシーケンスで無理やり繋げて、コンパイル時に文字列を出力する方法を紹介しました。
- [**ついに実現！実用的なC++20コンパイル時出力**](https://qiita.com/Raclamusi/items/c76fb05c37b97472a3cf)
    - この記事では、アセンブリの `.print` 命令を用いてアセンブル時に出力する方法を紹介しました。

このように、コンパイル時であってもターミナルに文字列を出力することは可能です。

ここで、標準出力（もしくは標準エラー出力）に出力された内容は、**リダイレクト**によって**ファイルに出力**することができます。
つまり、画像ファイルの内容をコンパイル時に出力できれば、コンパイル時に画像ファイルを作成することができます！

さて、コンパイル時に出力可能な画像ファイル形式はあるのでしょうか？


## `.print` 命令

昨年の記事では、エスケープシーケンスによって、余分な出力を隠したり、改行の直前までカーソルを戻したりすることによって、任意の文字列の出力を実現していました。
しかし、この手法はリダイレクトによるファイル出力では使えません。
エスケープシーケンスはターミナル上の見た目を整えるものであって、リダイレクトするときは、エスケープシーケンスはそのままファイルに流れ込むだけで、何の効果も発揮しません。
なので、余分な出力がたくさん含まれる `static_assert` によるコンパイル時出力は、使い物になりません。
アセンブラの `.print` 命令によるアセンブル時出力で実装する方針を考えます。

`.print` 命令は、それをアセンブルするときに、指定した文字列を末尾に改行をつけて出力するものである。

```:test.s
.print "Hello, Assembly!"
.print "42"
```

```shell-session:実行例
$ gcc test.s -c
Hello, Assembly!
42
```

これでアセンブル時に好きな文字列が出力できますが、コンパイル時に計算した内容を指定して出力することはできません。
そこで、すべての出力しうる文字列についてそれぞれ出力のコードを用意し、コンパイル時に constexpr if でどのコードをアセンブリに出力するかを選択することで、コンパイル時計算の結果をアセンブリコードに反映します。
つまり、出力できる文字列が用意したものに限られます。
しかし、画像のビットマップデータの出力には、画素値を出力するコードを 256 個[^16bit]用意しておけばよいので、さほど問題ではありません。
画像の幅、高さなどのヘッダに含まれる情報は、マクロで何とかしましょう（妥協）。

[^16bit]: 16 bit に拡張したりするともっと必要です。

問題なのは、**末尾に改行がつく**ことです。
画素ごとに出力すると、その間にいちいち改行が挿入されることになります。
つまり、コンパイル時に出力できる画像ファイルは、画素値同士が改行で区切られるようなファイル形式のものに限られます。

そんな都合のいいファイル形式、あるはずもありません。


## PPM (Portable PixMap format)

ありました。
**PPM の ASCII 形式**です。

https://ja.wikipedia.org/wiki/PNM_(%E7%94%BB%E5%83%8F%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%83%E3%83%88)

https://netpbm.sourceforge.net/doc/ppm.html

これは、カラー画像を ASCII 文字列で表現するファイル形式で、画素値は改行を含む空白文字で区切ります。
つまり、コンパイル時出力可能の要件を満たします。
やったぜ！



# コンパイル時画像出力

## 実装

実装したものがこちらになります。

https://github.com/Raclamusi/constexpr_ppm_printer

実装はいたってシンプルで、テンプレート引数として受け取った `std::array` オブジェクトの要素を、畳み込み式によるループで出力しているだけです。
`print_value()` は、例によって手心をこめて実装しています。

```c++
template <std::uint8_t Value, std::size_t, int, std::uintmax_t>
void print_value() {
    if constexpr (Value == 0) CONSTEXPR_PPM_PRINTER_PRINT("0");
    else if constexpr (Value == 1) CONSTEXPR_PPM_PRINTER_PRINT("1");
    else if constexpr (Value == 2) CONSTEXPR_PPM_PRINTER_PRINT("2");
    else if constexpr (Value == 3) CONSTEXPR_PPM_PRINTER_PRINT("3");
    else if constexpr (Value == 4) CONSTEXPR_PPM_PRINTER_PRINT("4");
    else if constexpr (Value == 5) CONSTEXPR_PPM_PRINTER_PRINT("5");
    else if constexpr (Value == 6) CONSTEXPR_PPM_PRINTER_PRINT("6");
    else if constexpr (Value == 7) CONSTEXPR_PPM_PRINTER_PRINT("7");
    else if constexpr (Value == 8) CONSTEXPR_PPM_PRINTER_PRINT("8");
    else if constexpr (Value == 9) CONSTEXPR_PPM_PRINTER_PRINT("9");
    else if constexpr (Value == 10) CONSTEXPR_PPM_PRINTER_PRINT("10");
    else if constexpr (Value == 11) CONSTEXPR_PPM_PRINTER_PRINT("11");
    else if constexpr (Value == 12) CONSTEXPR_PPM_PRINTER_PRINT("12");
    else if constexpr (Value == 13) CONSTEXPR_PPM_PRINTER_PRINT("13");
    else if constexpr (Value == 14) CONSTEXPR_PPM_PRINTER_PRINT("14");
    else if constexpr (Value == 15) CONSTEXPR_PPM_PRINTER_PRINT("15");
    else if constexpr (Value == 16) CONSTEXPR_PPM_PRINTER_PRINT("16");
    else if constexpr (Value == 17) CONSTEXPR_PPM_PRINTER_PRINT("17");
    else if constexpr (Value == 18) CONSTEXPR_PPM_PRINTER_PRINT("18");
    else if constexpr (Value == 19) CONSTEXPR_PPM_PRINTER_PRINT("19");
    // ...
```


## constexpr をつけない

[昨年のコンパイル時出力](https://qiita.com/Raclamusi/items/c76fb05c37b97472a3cf)では、出力関数を constexpr にしようと頑張っていたみたいですが、constexpr 関数であろうがなかろうがアセンブリはコンパイル時に評価されるので、別に constexpr 関数にこだわる必要はありません。
constexpr 関数でなければ、constexpr 関数内でインラインアセンブラが評価できないことに悩まされることもありません。

`constexpr_ppm_printer::print_ppm()` は constexpr ではありませんが、出力はコンパイル時（正確にはアセンプル時）に行われ、実装はより簡単になっています。


## 呼び出し毎にインスタンス化

アセンプル時出力では、コードの評価時ではなく、アセンブリの生成時に出力されます。
つまり、出力するごとに関数を作る、すなわち、コードの再利用をさせないようにする必要があります。

各画素については、そのインデックスをテンプレートパラメータとして `print_value()` に与えることで、`print_value()` は画素ごとにインスタンス化され、アセンブリを生成します。
しかし、（あまりないと思いますが）画像を複数出力したいときはこれでは不十分で、画素値とインデックスが同じときに `print_value()` がインスタンス化されず、アセンブリが生成されません[^opt]。
そこで、`print_ppm()` のテンプレートパラメータに `Counter` を追加し、この値を変更することでもう一度すべての画素でインスタンス化されるようになります。

[^opt]: 実際には、関数のインライン展開でアセンブリが生成されることがあります。

また、`Counter` を自動で設定して画像を出力する関数形式マクロ `print_ppm_constexpr()` を用意しています。
このマクロは、`Counter` の値に `__COUNTER__` マクロの値を設定します。
`__COUNTER__` マクロは展開されるごとにユニークな数値を返します。


## 実行

3つの円を描いた画像を作るサンプル ([sample.cpp](https://github.com/Raclamusi/constexpr_ppm_printer/blob/main/sample.cpp)) を用意したので、それを実行してみます。

C++20 以上で、**最適化をつけて**コンパイルします。
最適化をつけないと、アセンブラが死にました（たぶん、インスタンス化したテンプレートの情報が多すぎて、オーバーフローしてる？）。

ここで、**リダイレクトで結果を受け取る**ようにします。

```shell-session
$ g++ -std=c++20 -O2 sample.cpp > image.ppm
```

これを実行すると、次の画像が出力されます！
（結構時間がかかります。このサイズ (64×48) の画像でも、私の環境 (CPU: i5-10210U, RAM: 8GB) では1分ぐらいかかりました。）

<img width="400" alt="コンパイル時に出力した画像" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643544/daf1df84-09ae-f105-3291-46977c75f6aa.png">

今回はエスケープシーケンスを使ってないので、画像サイズが小さければ Wandbox でも動きます。
https://wandbox.org/permlink/LzwjG1nSaPYo2nN5

また、実際に出力されるのはアセンブル時であるので、（狭義の）コンパイルだけを行うことで、計算済みで後は出力するだけのアセンブリファイルを作ることができます。
このファイルをアセンプルすると、同じように出力画像が得られます。

```shell-session
$ g++ -std=c++20 -O2 -S sample.s sample.cpp
$ g++ sample.s > image.ppm
```



# 完全コンパイル時レイトレーシング

コンパイル時画像出力ができたということは、ついにこれができます。

出力までコンパイル時に済ませる、**完全コンパイル時レイトレーシング**です！

レイトレーシングのコードは[2023年のコンパイル時レイトレーシング](https://in-neuro.hatenablog.com/entry/2023/12/16/233332)のものを拝借して、main.cpp のインクルードと `main()` を書き換えます。
~~ひよって~~画像サイズを小さめに設定しています。

https://github.com/ToruNiina/constexpr-raytracing

```c++
// （省略）

#define IMAGE_SIZE_X  80
#define IMAGE_SIZE_Y  40
#include "config.hpp"

#define CONSTEXPR_PPM_PRINTER_WIDTH  IMAGE_SIZE_X
#define CONSTEXPR_PPM_PRINTER_HEIGHT  IMAGE_SIZE_Y
#include "constexpr_ppm_printer.hpp"

// （省略）

int main(int argc, char** argv)
{
    constexpr auto image = conray::make_image(conray::cam, conray::w);

    print_ppm_constexpr(image);

    return 0;
}
```

CMake の出力がコンパイル時出力と混ざるので、CMake は使わずにビルドします[^cmake]。

[^cmake]: 決して Windows での CMake の使い方がわからなかったわけではないです。

```shell-session
~/constexpr-raytracing/src $ g++ -std=c++23 -O2 -fconstexpr-ops-limit=34359738367 -I ../include -S main.s main.cpp > image.ppm
```

すると、コンパイルしかしてないのに、次の画像ができています！

<img width="400" alt="コンパイル時に出力したコンパイル時レイトレーシングの結果画像" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643544/f3769c7e-c8e0-839d-2159-64e35f7dc3ca.png">

出力も含めて15分ぐらいでできましたが、メモリが限界そうだったので、画像サイズをこれ以上大きくするのはやめておきます。



# おわりに

[中3女子の constexpr レイトレ](https://www.slideshare.net/GenyaMurakami/constexpr-29223898)を知ってから、「画像の出力までコンパイル時にできるか」はずっと気になっていたことだったので、コンパイル時画像出力を実現できたのは結構うれしいです。
正直、本当にできるとは思ってませんでした。
コンパイル時出力、これはまだ擦れそうですね……。

constexpr の制限が緩くなって面白くなくなったと思うなら、また新たな~~縛りプレイ~~挑戦を見つけて挑めばいいのです。
みなさんもよいコンパイル時処理ライフを！