AviUtl1.10で追加された共有メモリキャッシュ関数の扱い方
2019/10/3 ver 1.10 共有メモリキャッシュをプラグイン外部関数で利用できるように改良した。
このような記述がありますが、PluginSDKの最終更新は 2012/6/19 なので当然その使い方は書かれていません。
この記事は、解析の結果「こう使うだろうな」といったものを纏めた内容です。(どこまで正しいかは分かりません)
sdkにあるfilter.hを更新します。
外部関数構造体(EXFUNC)の下のほうにある int reserve[7]; を以下に書きかえます
code:c++
void* (*create_shared_mem)( int key1, int key2, SIZE_T size, void* *smemp );
// 共有メモリキャッシュへ保存される領域を作成します
// key1,key2 : 共有メモリキャッシュのキー
// size : 確保するサイズ
// smemp : 共有メモリキャッシュハンドルを取得する(NULLなら取得しない)
// 戻り値 : 領域のポインタ(失敗ならNULL)
void* (*get_shared_mem)( int key1, int key2, void* smemp );
// 共有メモリキャッシュに保存されたデータを展開します
// key1,key2 : 共有メモリキャッシュのキー
// smemp : 共有メモリキャッシュハンドル(keyが一致していなければ失敗)
// NULLの場合、keyと一致するデータを1つ取得します
// 戻り値 : 領域のポインタ(失敗ならNULL)
void (*delete_shared_mem)( int key1, void* smemp );
// 共有メモリキャッシュを削除します
// key1 : 共有メモリキャッシュのキー
// smemp : 共有メモリキャッシュハンドル(keyが一致していなければ失敗)
// NULLの場合、key1と一致する共有メモリキャッシュを全て削除します
追加された関数は作成・取得・削除です
概要
基本的に共有することを目的としません。4GB制限とは別の領域に保存できるため、積極的に使いたいキャッシュ領域です。
https://scrapbox.io/files/6390aab2004f0f0020a8161f.pnghttps://scrapbox.io/files/6390aab54e5e90001f33526c.png
https://scrapbox.io/files/6390aab8951e11002069d0ec.pnghttps://scrapbox.io/files/6390aababb9eb6001f265291.png
https://scrapbox.io/files/6390aabd2531e4001d58251a.pnghttps://scrapbox.io/files/6390aac14760330023d89e9c.png
※厳密には仮想メモリは2つまで同時にある状態となり、共有メモリへ入るのは次の次に取得や作成関数を呼び出した時
サンプルコード
フィルタオブジェクト「共有メモリテスト」を置くと最初64フレームはキャッシュを作り、その後はそのキャッシュからループ表示していくプラグイン
code:c++
FILTER_DLL filter = {
FILTER_FLAG_NO_CONFIG,
0,0,
const_cast<char*>("共有メモリテスト"),
NULL,NULL,NULL,
NULL,NULL,
NULL,NULL,NULL,
func_proc,
};
EXTERN_C FILTER_DLL __declspec(dllexport)* __stdcall GetFilterTable(void){
return &filter;
}
static BOOL func_proc(FILTER* fp, FILTER_PROC_INFO* fpip) {
int size = fpip->h * fpip->max_w * 6;
int i = fpip->frame & 63;
if (fpip->frame < 64) {
fp->exfunc->delete_shared_mem(1111, smemi); }
void* ptr = fp->exfunc->create_shared_mem(1111, 222, size, &smemi); if (ptr != NULL) {
memcpy(ptr, fpip->ycp_edit, size);
}
} else {
void* ptr = fp->exfunc->get_shared_mem(1111, 222, smemi); if (ptr != NULL) {
memcpy(fpip->ycp_edit, ptr, size);
}
}
}
return TRUE;
}
以下は共有メモリキャッシュハンドルを使用しない例
code:c++
static BOOL func_proc(FILTER* fp, FILTER_PROC_INFO* fpip) {
int size = fpip->h * fpip->max_w * 6;
int i = fpip->frame & 63;
if (fpip->frame < 64) {
fp->exfunc->delete_shared_mem(i, NULL);
void* ptr = fp->exfunc->create_shared_mem(i, 1234, size, NULL);
if (ptr != NULL) {
memcpy(ptr, fpip->ycp_edit, size);
}
} else {
void* ptr = fp->exfunc->get_shared_mem(i, 1234, NULL);
if (ptr != NULL) {
memcpy(fpip->ycp_edit, ptr, size);
}
}
return TRUE;
}
戻り値(領域のポインタ)の寿命
共有メモリキャッシュ関数で取得する領域の寿命は、次の次の共有メモリキャッシュが呼び出されるまでです。そのタイミングでデータは共有メモリへと格納され、一時的に使っていた仮想メモリは解放されるため、戻り値の領域ポインタは使えなくなります。(解放後のポインタを呼び出しているのが0.93rc1バグの原因です)
exfunc->get_ycp_source_cache exfunc->avi_file_read_video exfunc->avi_file_get_video_pixelp等の関数でも間接的にcreate関数が呼ばれるので注意してください。
以下、エラーコードの例
code:c++
void* ptr1 = fp->exfunc->get_shared_mem(a, b, c);
void* ptr2 = fp->exfunc->get_shared_mem(d, e, f);
void* ptr3 = fp->exfunc->get_shared_mem(g, h, i); // この時点でptr1は使ってはいけないポインタとなる
memcpy(ptr1, ptr2, size);
共有メモリキャッシュの寿命
キャッシュサイズを超えると最終呼び出しの古いキャッシュから削除されていきます。
キャッシュが削除される際にはcreate_shared_memで渡した共有メモリキャッシュハンドルにNULLが入るようになっています。(1つめのサンプルコードではif (smem[i] != NULL)として消されていないかを判定をしています)
create_shared_memの罠
既存のデータに同じキーで作成されたものがあるか というような判定は組み込まれておらず、同一キー・同一ハンドルで2回create関数を呼び出した場合は2個分のデータが生成されます。そしてその状態でのget関数では基本的に1個目のデータを取得することになります。対策としては、サンプルのように複数回呼び出される部分にはdeleteを置く必要があります。
delete_shared_memの罠
引数でハンドルを指定していない場合、引数のkey1と一致するデータはすべて削除となります。ここで特に考えなければいけない点は、他プラグインへの影響です。2個目のサンプルのようにハンドル無しでfp->exfunc->delete_shared_mem(i, NULL);と書くと他プラグインで作成されたキャッシュでもkey1が同じものは(そのプラグイン側ではハンドルを使用していたとしても)巻き添えで削除されることになります。なのでkey1はなるべく被らないような値を使うか、原則としてハンドルを使用するようにしなければ競合が起こる恐れがあります。(AviUtlの多重起動は気にしなくて良いようになっています)
参考として拡張編集0.93rc1ではkey1にはグローバル変数のアドレスを入れています。
patch.aulテスト版のシーンキャッシュ機能はハンドル無しでやる方が良さそうなのでkey1は周辺の関数のアドレス+シーン番号にしました。
共有メモリキャッシュハンドルの中身