TSG Advent Calendar 2025の10日目の記事です。
はじめに:なぜ車輪を再発明するのか
こんにちは、liesegangです。
突然ですが、皆さんは動画編集ソフト何を使っていますか? After EffectsはプロユースですがUIが複雑怪奇で動作も重い。AviUtlは拡張性が高く素晴らしいですが、設計が古く(32bitの壁とか, そうこうしていうちに64bit版が半年弱くらい前に出ましたが、それは一旦忘れておきます)、様々なユーザーによって魔改造されてまるで九龍城のように混沌として見えます。
私の趣味はモーショングラフィックス、特にvtuberなどのリリックビデオを作ることです。 「もっと直感的に、2Dの複雑なモーションを制御したい」「GPUをフル活用してヌルヌル動くプレビューが見たい」 そんな欲望が爆発した結果、「Rustで自分だけの最強の動画編集ソフトを作る」という沼に足を踏み入れることにしました。
本記事は、その開発初期にして最大の障壁、「GUIライブラリ選定」における死闘の記録です。
要件:ただのGUIではダメな理由
ToDoアプリを作るなら、適当なライブラリを選べば終わりです。しかし、動画編集ソフトには過酷な要件(または、私のこだわり)があります。
-
リアルタイムGPUプレビュー:
- 4K/60fpsの非圧縮バッファを遅延なく表示する必要があります。
- つまり、GPUのメモリ空間(Vulkan/OpenGL Context)をGUIと動画レンダラーで共有できなければなりません。
-
日本語入力 (IME) 必須:
- リリックビデオ(歌詞動画)を作るため、テキストボックスで日本語が打てないライブラリは論外です。
-
モダンでCoolなUI:
- モチベーション維持のため、見た目は重要です。
この要件を胸に、Rust界隈の主要GUIライブラリを片っ端から試しました。以下がその検証ログです。
候補1:CxxQt —— 巨人の肩に乗ろうとして押し潰された
最初は「業界標準」を狙いました。MayaやNukeなど、プロ用映像ツールの多くはQt (C++) で作られています。
RustからQtを使える CxxQt なら、QMLでモダンなUIを書きつつ、バックエンドはRustで書けるはずだと考えました。
辛かったところ:バインディングの複雑怪奇さ
C++とRustの間のFFI(Foreign Function Interface)がとにかく辛い。
Qtのオブジェクトシステム(MOC)とRustの所有権モデルを調停するために、cxx クレートなどを駆使するのですが、ボイラープレートが膨大になります。
// cxxqtのブリッジ定義のイメージ
// UIのボタン一つ繋ぐだけで、C++の世界とRustの世界を行き来する呪文が必要
#[cxx_qt::bridge]
pub mod qobject {
unsafe extern "C++" {
include!("cxx-qt-lib/qimage.h");
include!("cxx-qt-lib/qstring.h");
include!("cxx-qt-lib/qlistitem.h");
/// An alias to the QString type
type QImage = cxx_qt_lib::QImage;
type QString = cxx_qt_lib::QString;
type QVector = cxx_qt_lib::QListItem;
}
unsafe extern "RustQt" {
// The QObject definition
// We tell CXX-Qt that we want a QObject class with the name MyObject
// based on the Rust struct MyObjectRust.
#[qobject]
#[qml_element]
#[qproperty(QString, name)]
#[qproperty(u32, height)]
#[namespace = "canvas_image"]
type TrackInfo = super::TrackInfoRust;
#[qobject]
#[qml_element]
#[qproperty(QString, name)]
#[qproperty(f64, start)]
#[qproperty(f64, duration)]
#[qproperty(QString, color)]
#[qproperty(u32, track)]
#[namespace = "canvas_image"]
type VideoClipInfo = super::VideoClipInfoRust;
#[qobject]
#[qml_element]
#[qproperty(QImage, image)]
#[qproperty(QVector<TrackInfo>, tracks)]
#[qproperty(QVector<VideoClipInfo>, video_clips)]
#[namespace = "canvas_image"]
type CanvasImage = super::CanvasImageRust;
}
unsafe extern "RustQt" {
// Declare the invokable methods we want to expose on the QObject
#[qinvokable]
#[cxx_name = "updateImage"]
unsafe fn update_image(self: Pin<&mut CanvasImage>);
#[qinvokable]
#[cxx_name = "updateProject"]
unsafe fn update_project(self: Pin<&mut CanvasImage>);
}
}
use core::pin::Pin;
use cxx_qt_lib::{QImage, QImageFormat};
/// The Rust struct for the QObject
#[derive(Default)]
pub struct CanvasImageRust {
image: QImage,
tracks: Vec<TrackInfo>,
video_clips: Vec<VideoClipInfo>,
}
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct TrackInfoRust {
pub name: String,
pub height: u32,
}
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct VideoClipInfoRust {
pub start: f64,
pub duration: f64,
pub name: String,
pub color: String,
pub track: u32,
}
...
「QMLを書く時間」より「ビルドエラー(CMakeとCargoの戦争)を直す時間」の方が長くなり、「私は動画編集ソフトを作りたいのであって、バインディング職人になりたいわけではない」と思い、撤退を決めました。
候補2:Tauri —— Webの夢、帯域の現実
次に目をつけたのは、飛ぶ鳥を落とす勢いの Tauri です。
Webフロントエンド(React/Vueなど)の知識でUIが作れるのは魅力的。VSCodeのようなモダンなルックも余裕です。
辛かったところ:4K動画はブラウザには重すぎる
UIは最高です。しかし、「プレビュー画面」の実装で詰みました。
Rust側でレンダリングした画像データ(RGBA非圧縮)をWebViewに送る際、以下の壁にぶつかります。
- IPCのボトルネック: Base64等で送るとCPU負荷で死にます。
- メモリ制限: 動画制作において、メモリがあればあるほどキャッシュに動画を載せれて、リアルタイムプレビュー時間が長くなります。しかし、数十~百GB近い画像シーケンスデータをブラウザ(V8)のヒープに置こうとすればクラッシュします。数GB程度では全然足りないのです。
- オーバーレイの不具合: 「WebViewに穴を開けて、後ろにあるネイティブウィンドウを見せる(Transparent Overlay)」という回避策も試しましたが、そもそもWebViewに透明な穴を開ける事自体が無理でした。
// 理想:Rustからポインタを渡して描画
// 現実:IPC経由でデータを流すしかなく、fpsが出ない
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![send_frame]) // <- ここが遅い
.run(tauri::generate_context!())
.expect("error while running tauri application");
候補3:Iced / Slint —— 日本語入力の壁
Rust製(Native)のGUIライブラリ、Iced や Slint も試しました。
これらはRustの所有権モデルに馴染むように設計されており、開発体験は良さそうでした。
辛かったところ:IMEサポート
致命的だったのが 日本語入力 (IME) のサポート状況 です。
アルファベット圏の開発者が多いためか、変換候補ウィンドウの位置がおかしかったり、入力中の未確定文字が表示されなかったりと、リリックビデオ制作には耐えられない挙動でした。
(※現在進行形で改善は進んでいるようですが、まだ実用段階ではありませんでした)
候補4:egui —— 最有力候補、しかしコンテキスト戦争へ
最終的にたどり着いたのが、即時モード(Immediate Mode)GUIの egui です。
ゲームエンジンのように毎フレーム描画する設計は、動画プレビューとの相性が抜群。日本語入力も winit 経由ながら、泥臭い修正がされており、そこそこ動きます。
辛かったところ:GPUコンテキストの競合
これで決まりかと思いきや、ラスボスが現れました。
「eguiが使うGPUコンテキスト」と「自作動画レンダラー(Vulkan/Skia)が使うコンテキスト」の競合です。
別なコンテキストを作ってフレームバッファの共有しようとしても、なぜかegui側でcontextを初期化すると、rendering側スレッドのGPUコンテキストがエラーを起こします。
また、同じコンテキストを使いまわそうとしても、RenderingPassが競合してうまくいきませんでした。
// 概念コード:コンテキストの取り合い
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
// ここでeguiが描画コマンドを発行する
// 私「頼む!動画のプレビューをここに描画させてくれ!」
// WGPU「ダメです。RenderPassは現在使用中です」
self.my_renderer.render_to_frame(frame); // -> Error!
}
}
まとめ:結局どうするのか
結論として、現在は egui + skiaでGPU shared memoryを作ってフレームバッファを共有 のルートで更に模索しています。
GUIライブラリ選定は、「何を作るか」によって正解がまるで違うことがわかりました。
- 普通のツールなら Tauri 一択だと思います。webの開発経験があるなら尚更です。
- 既存資産があるなら CxxQt も選択肢になるでしょう。
- IME非対応でいいなら Iced も良い選択肢です。Rustだけで掛けますから。
- そして、データの双方向バインディングとか気にせずにimmediate modeが使いたいなら egui でしょうか。複雑なUIを作るのには向いていそうです。
リポジトリ
とりあえず、現状の動画作成ソフトの進捗を以下においておきます。また、気が向いたら開発します。
それでは、明日のTSG Advent Calendarもお楽しみに!
