アドベントカレンダー2023 18日目の記事です. 外国人なので日本語で読みにくいところが多いに申し訳ないです.
はじめに
最近, 私のPCは(OpenBSD以外)全部Waylandに移行したが, 絵描きソフトが全部waylandで使えなく/バッグが多くようになりました. さらに, 以前からオープンソース絵描きソフトの重さに不満だった. 例えば: プラグインは埋め込み言語で開発(GIMPのscript-fu)に限られる. ライブラリとして再利用することは基本的に無理. つまり, どこでもすぐ起動して使える絵描きソフトを作りたい.
現在の絵描きソフトはほぼCPUで処理する, そのため細かいパフォーマンス最適化が必要なところが多い. しかしCPU性能の限界があるのため, ブラシなどの操作はdamage areaから追跡できるが, 一部の操作はスムーズに実行することは不可能です. 例えば, レイヤーの回転,移動操作はCPUで実行すれば リアルタイムでプレビューする時はかなり重くになる. というわけで, vulkanで高速化するソフトを自作することを決めた.
定義
-
絵描きソフト: レイヤーや筆圧検知などの機能を持っているペイントソフト.
-
viewport rendering, またはviewport: ブレンドされた画像を画面で出力するのプログラム.
-
camera: カメラは, ユーザー操作による回転/移動/拡大縮小/反転 パラメーターをtransformation matrixに変換するプログラムです.
-
stroke/segment: ブラスツールを使う時, 1つのmousemove/motionイベントは1つのsegmentをブラシエンジンでrasterizeする. スタイラスペンをタブレットから引き上げる時, 複数のsegmentによってstroke 1つを作成する. Undo/redoの粒度はsegmentではなくstrokeである.
-
edit buffer/submit: undo buffer/ブレンドの正しさを確保するためには, segmentはレイヤーに直接に上書きするではなく, edit buffer(編集バッファ)に一旦保存して, strokeを完成した時点まとめて アップロード → ブレンド → undoを保存する流れ(submitと呼ばれる)を実行する.
-
レイヤーブレンド: 順番に複数のレイヤーをアルファブレンドするプログラム.
-
editブレンド: ユーザーの編集をレイヤーにブレンドするの方法. 絵描きソフトは基本的には2つのブレンド設定がある: ブラシのブレンド関数(=このeditブレンド)と前のレイヤーブレンド関数.
-
undo list/pointer/walking: undo歴史はlistのように保存する. 現在の状態はundo list中のひとつのnodeのポインターと表示される. ユーザーはundo/redoを実行する時, undo pointerをundo listに後ろ/前に一歩移動(walk)して, そのpatchを
undo ↔ redo
切り替える. 例ば:
. undo . undo @ redo . redo . redo .
<ユーザーredo>
. undo . undo . undo @ redo . redo .
仕組み
vwdraw(この絵描きソフト)はC言語でlinux+waylandを使用環境として開発している. ライブラリ依頼(現在)はvulkan
, wayland-client
, cglm
, stb_image
だけ. 主なライブラリ(赤色)を簡単に説明します:
-
vwdraw: 他のライブラリを統合して, 実行ファイルをコンパイルするのプロジェクト.
-
sib: ブラシラスタライザー. 今はペン/消しゴムつの機能だけを持っている.
-
vwdedit: 編集内容をCPUバッファから同期してリアルタイムでレイヤーに反映してプレビューする.
-
vwdlayout: レイヤーブレンド. 今の実装は順序にブレンドするだけです.
-
vwdview: cameraと入力処理.
-
imgview: viewport部分.
まだ開発中ですが, コードは全部githubで同期している (この記事の下の自己紹介のホームページに). 機能が少ないが一応それを使ってこの記事の説明グラフを描いてみた.
同期モデル
今の絵描きソフトはほとんどCPU側でレイヤーブレンドを実行する. 例えば, kritaのGPUで高速化した部分はviewportだけ. Vwdrawにはundo/ブラシrasterizer以外全ての処理がGPU側で実行されている. レイヤーデータも, RAMではなくVRAMで保存する. 今回紹介するのは, CPU-GPU同期必要の3つの部分:
-
ブラシエンジンのリアルタイムプレビュー.
-
submit時undo patch作成: 編集前のGPUレイヤーデータをCPUのundo listに転送.
-
undo/redo list walking: CPUのundo listからの編集patchをGPUレイヤーデータに上書きする.
CPU-GPUの同期は必要とはいえ, それほど複雑な問題ではない. なぜなら, 3Dゲームと違う絵描きソフトは compute-boundではなく, input latency bound問題です. input latencyを最小限にしたければ, vulkanと言えばframe-in-flightなどの同期技術はいらないです.
まずはメインループ(フレームの処理):
1つフレームの時間は計算フェーズと同期フェーズを構成される.
-
計算フェーズ: CPUとGPUで並列計算する部分です, CPUはユーザー入力を分析して, ブラシエンジンでstrokeをrasterizeする. その間GPUは前のフレームをブレンドしている.
-
同期フェーズ: 計算フェーズで 変更したedit buffer(対応のdamage areaだけ)をGPUに送る.
ユーザーの入力から次のpresentは前のフレームsubmitしたimageから, 入力の遅延は1~2=平均1.5フレームです. さらに遅延を短縮したい場合,内部FPSはモニター出力FPSの2倍に設定すれば平均遅延は1フレームぐらいになる (最も速い場合にも0~1=平均0.5フレームの遅延がある).
GPUデータが必要時の処理
Vwdrawのレイヤーデータは全てGPUで保存する一方, undo patchesは全てCPUで保存する. その理由は, undo patchのメモリ使用率はよく非常に大きくになる (大きい画像を編集する時). CPUで保存すればメモリ不足の時データはDiskに移行できる.
同期モデルは簡単ですが,計算フェーズの中GPUデータは取得できない. 例えば, save/load操作は全てのレイヤーをCPUに同期してファイルにexportする. Save/load操作は次のフレームまで待ってもいいが, undo listに関する操作, 例えばsubmitとundo list walkingは次のフレームを待つことができない. 次の同期フェーズに待っていれば, edit bufferは"同期待ち"状態のままになったら, その後に起こった全ての操作(例えばブラシエンジン)は処理できなくようになる.
Undo/submitは頻繁な操作ですが, ブラシと同じ遅延レベルまで必要はない. だから, 今の解決方法はGPU操作を完成するまでCPUをブロックする.
submitの場合, GPU操作が完成する時, 次のコマンドをレコードする:
-
edit bufferを同期する.
-
元のレイヤーからdamage areaをCPUに保存する (今はcommand recordから, まだCPUに保存されていない).
-
編集中のレイヤーをブレンドする.
-
GPUレイヤー(ブレンドされたpreviewから, 同じdamage area)の更新.
-
edit buffer(CPU側)をクリアする.
このコマンドbufferはもう一度submitしてCPUをブロックする. これでsubmitが完成した, CPU側のレイヤーから同じdamage areaの画像をクリップしてundo patchを作成する.
Undo list walkingの場合もほぼ同じ: まずは編集中のデータをGPUに同期, そしてレイヤーを更新+ブレンドして, ブレンドしたレイヤーでレイヤーを上書きして, コマンドをsubmitして,完成までCPUをブロックする. 最後はundo/repo patchを切り替える.
まとめに
絵描きソフトについて, 入力遅延を最小限に抑えることは最も重要なので, 基本的には全部同期するだけでいい.
vwdraw(と他のライブラリ)は自分の初めて書いた(大型)Cプロジェクトです. 以前使っているRustのいろんなvulkan binding(vulkano/ash/wgpu-rs)より, 全てのライブラリはdynamic linkingのおかげで, コンパイル速度(Rust+sccache, w/o shaderc-rsに比べて)は10倍以上, binaryファイルサイズは(Rust静的リンクされたexecutableに比べて)10%以下に改善した.
明日の担当は @dogwood_flo です, お楽しみに!