はじめに
ゆっくり動画制作ツール「ゆっくりMovieMaker4(YMM4)」は、公式プラグインAPIを備えた拡張可能なアーキテクチャを持っています。本記事ではYMM4向けに開発したプラグインパック「YMM4-Radeon_Fast_Plugin_Pack」の実装を題材に、以下3つの技術アプローチを解説します。
- 0Harmony を使ったクローズドアプリへの実行時モンキーパッチ
- libvips (C) + Pinned Object Heap による GC を最小化した高速画像デコード
- Radeon AMF SDK による VRAM ゼロコピーのハードウェアエンコード
ソースコードは MIT License で GitHub に公開しています。
GPU 互換性について
本パックに含まれる3プラグインのうち、Radeon GPU が必須なのは動画書き出しプラグイン (RadeonAmfPlugin) のみです。
ファイル読み込み高速化・PSD立ち絵高速化の2プラグインは NVIDIA / Intel 環境でも動作します。
プラグイン一覧
| プラグイン | Radeon 必須 | 概要 |
|---|---|---|
| RadeonAmfPlugin | 必須 | AMF SDK を直接叩いて VRAM → エンコーダへゼロコピー書き出し |
| RadeonFastFileSourcePlugin | 不要 | 画像/音声/動画の非同期先読み・libvips デコード・キャッシュ |
| RadeonFastPsdTachiePlugin | 不要 | 立ち絵パラメーターのハッシュ化で無駄な PSD 再ラスタライズをスキップ |
1. 0Harmony によるリバースエンジニアリング基盤
公式プラグイン API だけでは YMM4 の内部ファイルロードや描画ループのタイミングを把握できません。そこで 0Harmony を使い、実行時に YMM4 の内部メソッドを prefix/postfix フックします。
// InternalInjectionProfiler.cs (抜粋)
var harmonyType = FindHarmonyType();
var harmony = Activator.CreateInstance(harmonyType,
"radeon.fastfilesource.injection.profiler");
// 非公開メソッドをリフレクションで取得してパッチ
var methodPatch = AccessTools.Method(targetType, "InternalLoadMethod");
harmonyType.GetMethod("Patch")!.Invoke(harmony, new object[]
{
methodPatch,
new HarmonyMethod(prefixMethod),
new HarmonyMethod(postfixMethod)
});
DLL のパスは YMM4 のプラグインフォルダを複数候補で試して動的に解決しています。このフックにより、各ファイル読み込みの所要時間をミリ秒単位で radeon_fast_filesource_log.txt へ書き出し、ボトルネック箇所を特定できました。
2. libvips + POH による GC ヒットゼロ画像デコード
問題:WIC のシングルスレッド・LOH 汚染
YMM4 がデフォルトで使用する WIC (Windows Imaging Component) は逐次デコードが基本で、多数の大きな画像素材を読み込むとガベージコレクションのスパイクが顕著に出ます。
解決策:libvips ネイティブ DLL + Pinned Object Heap
JPEG/PNG/WebP/AVIF/JXL 等の主要フォーマットを libvips でデコードするネイティブ DLL(C言語)を実装し、C# 側は P/Invoke で呼び出します。
// RadeonFastNativeImage.c (C実装、libvipsラッパー)
RF_EXPORT int rf_image_decode_bgra(const wchar_t* path, RfImage* outImage)
{
// libvips でロード → sRGB 変換 → BGRA メモリ書き出し
VipsImage* image = vips_image_new_from_file(utf8, "access", VIPS_ACCESS_SEQUENTIAL, NULL);
// ...BGRA変換後にメモリコピー...
outImage->data = bgra;
}
デコード後のピクセルデータを C# へ受け取る際、Pinned Object Heap (POH) に直接アロケートして LOH を避けます。
// NativeImageBitmapFactory.cs
// movable な LOH に乗るとフルGCのたびにピン留めが発生する。
// POH に固定アロケートすることで GC 時のオーバーヘッドをゼロにする。
var pixels = GC.AllocateUninitializedArray<byte>((int)image.Bytes, pinned: true);
Marshal.Copy(image.Data, pixels, 0, pixels.Length);
decoded = new DecodedNativeImage(image.Width, image.Height, image.Stride, pixels);
POH 上のピクセルは ID2D1Bitmap へのアップロード時もピン留めコストが発生しません。
キャッシュ戦略
同一パスへの複数回アクセスは ImageBitmapCache がデコード済みの ID2D1Bitmap を使い回し、WarmupManager が事前にバックグラウンドデコードを済ませておくことでタイムライン再生中のスパイクを防いでいます。
3. PSD 立ち絵:状態ハッシュによる Update スキップ
問題:毎フレーム走るPSD再描画
口パク・目パチアニメーションを持つ PSD 立ち絵は ITachieSource.Update() が毎フレーム呼ばれますが、実際には「口も目も変わっていない」フレームが大半です。
解決策:パラメーター全体を文字列キーにシリアライズして差分比較
// TachieStateKey.cs
public static string Create(TachieSourceDescription desc)
{
var builder = new StringBuilder(512);
builder.Append("mouth=").Append(desc.MouthShape).Append(';');
builder.Append("voice=")
.Append(desc.VoiceVolume.ToString("F3", CultureInfo.InvariantCulture))
.Append(';');
// キャラクター・アイテム・顔差分の各プロパティをリフレクションで展開
AppendObject(builder, "character", desc.Tachie?.CharacterParameter);
AppendObject(builder, "item", desc.Tachie?.ItemParameter);
foreach (var face in desc.Tachie?.Faces?.OrderBy(f => f.Layer) ?? [])
AppendObject(builder, "face", face.FaceParameter);
return builder.ToString();
}
前フレームのキーと一致した場合は inner.Update(desc) 自体を呼ばず即リターン。スキップ回数とスロー更新(≥3ms)の回数を計測してログに残します。
4. Radeon AMF による VRAM ゼロコピーエンコード(Radeon 専用)
このプラグインは AMD Radeon GPU + AMD Software: Adrenalin Edition が必須です。
通常のエンコードパイプライン(問題点)
[YMM4 GPU フレーム (VRAM)]
→ CPU 読み戻し (PCIe バス経由)
→ CPU/GPU エンコード
→ ファイル書き出し
PCIe 帯域がボトルネックになり、4K などの高解像度では転送だけで数十ms かかることもあります。
AMF 直接エンコードパイプライン
[YMM4 → ID2D1Bitmap1 (VRAM)]
→ IDXGISurface / ID3D11Texture2D として取り出し
→ AMF: CreateSurfaceFromDX11Native() で VRAM のまま登録
→ Radeon VCN (ハードウェアエンコーダ) で H.264 / H.265 直接処理
→ MP4 mux → ファイル書き出し
YMM4 の IVideoFileWriter2 が渡す ID2D1Bitmap1 から DXGI サーフェスを取り出し、AMF ネイティブ API に直接渡すことで CPU への読み戻しを完全にカットしています。
音声は Media Foundation AAC エンコーダ、MP4 コンテナへのマルチプレクスはネイティブ側の writer を使用しています。また、AMF の入力キュー深度(GPU Queue Depth)を調整することで、YMM4 のフレーム供給とエンコーダの出力取得を非同期にパイプライン化し、スループットを最大化しています。
ビルド前提
- .NET 10.0 / C# (YMM4 本家に準拠)
- AMD AMF SDK (ヘッダーのみ、ランタイムはドライバに同梱)
- Visual Studio C++ Build Tools + CMake (ネイティブ DLL のビルド用)
- Windows SDK
おわりに
「既存アプリを壊さずにパフォーマンスを改善する」というテーマで、0Harmony / libvips / AMF SDK という3つのレイヤーにわたるアプローチを組み合わせた開発でした。
- 動的パッチによるボトルネック計測(Harmony)
- GC を意識したネイティブ連携(libvips + POH)
- GPU バッファをゼロコピーでエンコーダへ流す(AMF SDK + D3D11)
これらの手法は YMM4 以外の .NET + DirectX アプリの最適化にも応用できると思います。ぜひ参考にしてみてください。
GitHub: https://github.com/harumaki4649/YMM4-Radeon_Fast_Plugin_Pack