Qt DirectFB で、devmem の領域が解放されない

Qt Embedded を DirectFB の devmem で動かすと、devmem の領域が解放されない、という問題があった。記録して役に立つかは分からないが、一応メモしておく。
環境は Qt Embedded 4.7.3 と DirectFB 1.4.3。DirectFB は devmem と自前の gfxdriver で blit, fill などの描画処理をアクセラレータで行うようにしている。


問題は Qt の demos/embedded/lightmap を実行したときに起きた。
何故か blit のさい、devmem の領域が余分に allocation され、それが解放されないのである。解放されないままだと devmem の領域が圧迫されるので、プログラムを動かしている途中で OUT OF MEMORY のエラーが起きてしまう。


問題の原因を調べるため、まず、何故 lightmap の blit で allocation が余分に呼ばれるのか、理由を調査した。
DirectFB の blit は、blit 元の src surface が system memory で、かつ blit 先の dst surface が devmem の領域である場合、ハードウェアを使って描画を行うために、わざわざ以下のような処理を行う:
1. src surface と同じサイズの領域を devmem から allocate する
2. src surface のデータを devmem から allocate した仮想アドレスにコピーする(このコピーはソフトウェアで行う)
3. devmem から allocate した物理アドレスから、dst sourface の物理アドレスに、ハードウェアを使ってコピーをする


ハードウェアを使ってコピーするために、わざわざ、devmem から領域を取り直し、ソフトウェアを使ってコピーし、その後ハードウェアでコピーしているのである。高々 blit するだけの処理なのに、実に無駄だ。
以上が、余分に allocate が呼ばれる理由である。
これはまぁ仕方ないとして、次の問題は allocate されたバッファがいつ解放されるか、である。


同じ問題を発生させる、以下のようなシンプルなテストを作り、問題を調査してみた:

#include 
#include      // for sleep()
#include 

static IDirectFB *dfb = NULL;
static IDirectFBSurface *devmem_surface = NULL;
static int screen_width  = 800;
static int screen_height = 480;

#define DFBCHECK(x...) {                                        \
        DFBResult err = x;                                      \
                                                                \
        if (err != DFB_OK) {                                    \
        fprintf( stderr, "%s <%d>:\n\t", __FILE__, __LINE__ );  \
        DirectFBErrorFatal( #x, err )                           \
        }                                                       \
    }

int
main(int argc, char **argv)
{
    char *filename;
    DFBSurfaceDescription dsc_image;
    DFBSurfaceDescription dsc_devmem;
    IDirectFBImageProvider *provider;
    IDirectFBSurface *image_surface;

    if (argc < 2) {
        fprintf(stderr, "Usage: %s [image-file]\n", argv[0]);
        return 1;
    }

    filename = argv[1];

    DFBCHECK( DirectFBInit(&argc, &argv) );
    DFBCHECK( DirectFBCreate(&dfb) );
    DFBCHECK( dfb->SetCooperativeLevel(dfb, DFSCL_FULLSCREEN) );

    //-----------------------------------------------------------------
    // create devmem surface
    dsc_devmem.flags = DSDESC_CAPS;
    dsc_devmem.caps = DSCAPS_VIDEOONLY;
    dsc_devmem.width = screen_width;
    dsc_devmem.height = screen_height;
    DFBCHECK( dfb->CreateSurface(dfb, &dsc_devmem, &devmem_surface) );

    DFBCHECK( devmem_surface->SetColor( devmem_surface, 190, 200, 180, 255 ) );
    DFBCHECK( devmem_surface->FillRectangle( devmem_surface, 0, 0, screen_width, screen_height ) );

    //-----------------------------------------------------------------
    // create image surface using SYSTEM MEMORY
    dsc_image.flags = DSDESC_CAPS;
    dsc_image.caps = DSCAPS_NONE;
    dsc_image.width = screen_width;
    dsc_image.height = screen_height;
    DFBCHECK( dfb->CreateImageProvider(dfb, filename, &provider) );
    DFBCHECK( dfb->CreateSurface(dfb, &dsc_image, &image_surface) );
    DFBCHECK( provider->RenderTo(provider, image_surface, NULL) );
                                 provider->Release(provider);

    DFBCHECK( devmem_surface->SetBlittingFlags(devmem_surface, DSBLIT_NOFX) );

    DFBCHECK( devmem_surface->Blit(devmem_surface, image_surface, NULL,
                            (screen_width - dsc_devmem.width) / 2,
                            (screen_height - dsc_devmem.height) / 2) );

    sleep(1);
    printf( "\n wait...\n\n" );
    sleep(4);

    image_surface->Release(image_surface);
    devmem_surface->Release(devmem_surface);
    dfb->Release(dfb);

    return 0;
}

話が逸れるが、DirectFB は、surface の allocate するときに、ディスクリプタをチェックし、どの領域から allocate するかを判断する。
ハードウェアを使う場合は devmem から allocate する必要があるし、ハードウェアを使わないのなら system memory から allocate される。
テストの image_surface は、CreateSurface() する際に

dsc_image.caps = DSCAPS_NONE;

とすることで、surface の使用目的( というより、どこから allocate するか) を指定しないため、system memory から優先して allocate される。


問題に戻ると、問題は image_suraface->Release() を呼んでも deallocate が呼ばれないことだが、これは Blit のさい、devmem_surface が image_surface への参照を残しているためであった。DirectFB は fusion を使って参照カウンタを管理しているが、参照が残っている場合、その surface は Release() が呼ばれても解放されない。
参照を増やすコードは、Blit の延長で呼ばれる dfb_sate_set_source() である:

DirectFB-1.4.3/src/core/state.c :

213 DFBResult
214 dfb_state_set_source( CardState *state, CoreSurface *source )
215 {
216      D_MAGIC_ASSERT( state, CardState );
217
218      dfb_state_lock( state );
219
220      if (state->source != source) {
221           if (source && dfb_surface_ref( source )) {
222                D_WARN( "could not ref() source" );
223                dfb_state_unlock( state );
224                return DFB_DEAD;
225           }
226
227           if (state->source) {
228                D_ASSERT( D_FLAGS_IS_SET( state->flags, CSF_SOURCE ) );
229                dfb_surface_unref( state->source );
230           }
231
232           state->source    = source;

221行目で dfb_surface_ref() を呼ぶため、source の参照カウンタがインクリメントされる。もし、別の Blit が呼ばれて、かつ source が以前と違う場合、229行目で参照カウンタがデクリメントされる仕組みになっている。


以上が、 image_suraface->Release() を呼んでも deallocate が呼ばれない理由である。
( image_surface が最終的にいつ解放されるか、というと、devmem_surface->Release() の延長で呼ばれる、dfb_state_set_source( &data->state, NULL ); によって、参照カウンタがゼロになり、その延長で deallocate される)


lightmap の使っている QDirectFBPixmapData::fromImage() のコードを抜粋すると、以下のようになる:

qt-everywhere-opensource-src-4.7.3/src/plugins/gfxdrivers/directfb/qdirectfbpixmap.cpp:

299 void QDirectFBPixmapData::fromImage(const QImage &img, Qt::ImageConversionFlags flags)
300 {

313     IDirectFBSurface *imageSurface = screen->createDFBSurface(image, image.format(), QDirectFBScreen::DontTrackSurface);

320     dfbSurface = screen->createDFBSurface(image.size(), imageFormat, QDirectFBScreen::TrackSurface);

334     dfbSurface->Blit(dfbSurface, imageSurface, 0, 0, 0);
335     imageSurface->Release(imageSurface);

335行目で Release() を呼んでも、前述した参照カウンタが邪魔して、dfbSurface が Release されない限り、解放はされない。


7/21追記
その後、ReleaseSource() http://directfb.org/docs/DirectFB_Reference_1_4/IDirectFBSurface_ReleaseSource.html という、まさに上記の問題に対応するための API があることが分かった。というか、普通に仕様書に載っており、見落としていた。
こうなると、Qt が QDirectFBPixmapData::fromImage() にて、ReleaseSource() を呼んで無いのが問題だと思える。
Qt に以下のパッチを当てることで問題は解決される。imageSource を Release する以上は、imageSource への参照は不要であるはずなので、単純な呼び忘れなのだろうか、、、

diff --git a/qt-everywhere-opensource-src-4.7.3/src/plugins/gfxdrivers/directfb/
index d030a9b..7c28aee 100644
--- a/qt-everywhere-opensource-src-4.7.3/src/plugins/gfxdrivers/directfb/qdirect
+++ b/qt-everywhere-opensource-src-4.7.3/src/plugins/gfxdrivers/directfb/qdirect
@@ -332,6 +332,7 @@ void QDirectFBPixmapData::fromImage(const QImage &img, Qt::I
     }

     dfbSurface->Blit(dfbSurface, imageSurface, 0, 0, 0);
+    dfbSurface->ReleaseSource(dfbSurface);
     imageSurface->Release(imageSurface);

     w = image.width();

7/21 さらに追記
Qt のバグトラッカーにバグを登録しようと問題を書いていて気付いたのだが、良く考えると blit が非同期で動くような場合、src が解放されてはまずいような気が、直観的にする、、、
なので上記の修正は恐らく正しく無い。