あまつぶ

6.28 【bottleneck procedure】

 QT-Qで大きな絵を開いてPICTで保存したりクリップボードにコピーしたりするとメモリが足りなくなって壊れたデータを出力してしまうというバグが以前からあった。原因も対処法もわかっていたのだけどなんだかめんどくさそうで、結局今までかかってしまった。
 PICTを作成するにはOpenPicture()〜ClosePicture()を使用するのだけど、通常はアプリケーションヒープを使うから途中でメモリが足りなくなると正常なデータは得られない。アプリケーションのメモリがあまりなくても大きなデータを扱えるようにするためには、bottleneck procedureとかいうQuickDrawの描画ルーチンのputPicProcを書き換えて自分で用意したテンポラリメモリなどに書き出していくようにすればいい。もちろん、ファイルに保存したい場合は直接データを保存していけば問題ない。
 putPicProcというのはどんな関数かというと、例えばファイルに書き出すなら、

pascal void PutPicToFile(Ptr p,short bytes)
{
	long	count;
	
	if ( gError )  return;	/* エラーが起こっていればもうなにもしない */
	
	count=bytes;
	gError=FSWrite(gRefNum,&count,p);
	if (gError) return;
	gPictureSize += bytes;
}


こんな形になる(ほぼQT-Qから引用)。OpenPicture()〜ClosePicture()間でデータの書き出しが起こるとこのルーチンが呼ばれ、書き出すべきデータをファイルに保存していくというわけ(見ればわかると思うけど、pにはデータのアドレス、bytesにはデータのサイズが入っている)。テンポラリメモリに保存していきたいのであればそのように書き換えればちゃんと動くはず。
 リスト中で「g」で始まる変数が3つ使われているが、これらはグローバル変数でここが呼び出される前に初期化されていなければならない。gErrorは保存中に起こったを記録しておくものだからnoErrに、gRefNumは保存するファイルへのリファレンスナンバに、gPictureSizeは保存したPICTのサイズだから0に初期化しておく。

 先ほど「通常は」と書いたとおり、通常のままでは自分で書いたルーチンを通ってくれないから通ってもらうようになんとかする必要がある。モノクログラフポートの構造体を見ると、最後の方に(一番最後だったような気もするが)QDProcsPtr型のgrafProcsというメンバがある(QuickDraw.h参照)。QDProcsPtrとはなにかといえばQDProcs型へのポインタで、QDProcs型は同じヘッダファイル内でいくつかのメンバを持った構造体として定義されている。ま、いってみれば描画ルーチンのリストみたいなものだ。その中をチェックすると、QDPutPicUPP型のputPicProcというメンバが見つかる。これを自分の書いたルーチン(正確にはそのUniversal ProcPtr)に書き換えてやるわけ。
 具体的には、

{
	QDProcs	procs;
	QDProcsPtr	savedProcs;
	GrafPort	port;
	
	GetPort(&port)
	savedProcs=port->grafProcs;
	SetStdProcs(&procs);
	procs.putPicProc=NewQDPutPicProc(PutPicToFile);
	port->grafProcs=&procs;
	
	/* 処理 */
	
	port->grafProcs=savedProcs;
}


こんな感じ。まず、あとで元に戻すことが可能なようにsavedProcsに現在のリストを保存しておく(通常は0になっているようだが)。そして、SetStdProcs()を呼ぶ。putPicProcしか書き換えずにあとは通常のQuickDrawのルーチンを呼んでもらいたいわけだから、これを使って通常のルーチンを得ておくわけ。それから自分で作ったルーチンを設定して、グラフポートの描画ルーチンのリストを更新する。処理が終わったら、保存しておいたリストに戻しておく。

 さて、ここまででファイルに保存するための準備は整った。描画ルーチンを書き換えてグローバル変数を初期化して最初に512バイトの0を書き込んだファイルを用意して、あとはいつもと同じようにOpenPicture()〜ClosePicture()の流れでPICTファイルが完成……かと思ったらまだそうはいかない。
 まず、PICTのヘッダ(データサイズとイメージの高さ、幅)は描画ルーチンには送られてこないから自分で書き出してやらなくてはならない。これはたいしたことはない。一番楽な方法はOpenPicture()にいく前に自前でPicture型の変数を用意して(PicHandleでないところに注意)、中身を埋めて保存しておけばいいだけ。OpenPicture()直後にPicHandleの内容を保存するという手もあるが、OpenPicture()を呼んだ時点ですでに数十バイト書き込んでしまっているようなので、ちょっとまずいみたい。余談だが、OpenPicture()時の数十バイトを無視したPICTファイルを作成すると、SimpleTextでは読めるがPictureViewerでは読めないという不思議なPICTファイルが出来上がる。
 次に、できたPicHandleの後処理。これもたいしたことはなくてDisposeHandle()で破棄するだけ。KillPictureでもいけるかもしれないが、内容がないのでDisposeHandle()がいいと思う。
 これで正常なPICTファイルを書き出すことができるはずだが、ぼくがやったときはまだだめだった。テンポラリメモリにデータを書き出してコピーの場合はこのままでできたのだが、なぜかPICTファイルの場合はだめ。謎だ。

 正常に書き出すことができたPICTファイルと失敗したPICTファイルを比べてみると、ほとんどの部分は同じなのだけど1バイト大きさが違う。それも、最後から3バイト目に00があるかないかだけの違い(正常な方には、ある)。この1バイトがないだけの違いで、そのPICTファイルを開くと固まってしまう(苦笑)。不思議なのは、コピーでは問題なかったことと、正常に保存できている場合もあることだ。
 正常に保存できた場合は、ファイルサイズが偶数バイトになっていて、正常でないときは奇数バイトというのはわかったが、なんの解決にもならない。後ろから3バイト目が書き出されるべきタイミングを調べると、ClosePicture()の直前らしいことはわかった(ClosePicture()を呼んだときに最後の2バイトが書き出される)。その前にしていることはCopyBits()だが、これがなにかまずいことでもしているのだろうか。
 原因はさっぱりわからないが、とにかくCopyBits()後に偶数バイトに補正してやれば正常なPICTファイルができるらしい(ダミーの1バイトの0を書き出す)。なんでこんなことをしないといけないのかわからないが、とりあえずはこれで問題ないようだ。IMにはそんなややこしいことは書いていないので、ぼくがどこかで勘違いをしているだけなのかも知れないが……。

to June 25, 1999 ↑ to June index → to June 29, 1999