OSX El CapitanでのNSOpenGLViewの挙動の変化

結論から先に書くと、OSX El Capitanになって、NSOpenGLViewがOpenGL Contextを、非同期に生成するようになった。プログラム中で、(NIBファイルとかいう悪を使わずに)、NSOpenGLViewのサブクラスを生成するとき、Yosemiteまでのバージョンでは、オブジェクトを生成して直後に、prepareOpenGL関数の完了を待たずに、OpenGLの関数を呼ぶことができた。が、El Capitanでは、いったん、イベントループに戻って、prepareOpenGL関数がCocoaのフレームワークから呼び出されるまで、OpenGLの関数を呼ぶことができなくなったらしい。

いや、びっくりした。土曜の晩になって、学生さんから連絡があって、授業で使ってるOpenGLのフレームワークが、OSXのバージョンを上げたら動かない、とのこと。

僕は、自宅で作業用のMac MiniはまたYosemiteのまま使っている。本当はMavericksで使いたかったのだが、買って来たら既にYosemiteが入っていてダウングレードできなかった。

OSベンダーは、ユーザーが一人も新しいOSなんか欲しがってないということをどうしても理解しない。いや、MicrosoftはWindows 10で固定と表明してるから、案外やっと理解したのかな?新しいOSになって、悪いことの方が良いことより多いのだ。全体的にスピードが遅くなる。今まで普通に使えていたプログラムが使えなくなる。多少古びてきても大事に使っているプリンタが動かなくなる。時には、PCそのものが使えなくなる。新しいOSを発表して古いOSを使えなくすることで、末端のユーザは余計な出費が増えるだけだ。OSは名脇役であるべきなのだ。ユーザが欲しいのは、快適にアプリケーションを使う環境であって、その背後にどんなOSが動いていようが気にしない。むしろ、気に入っていたプログラムが使えなくなると、ユーザの反感を買うだけだ。それが、どうしても、理解できないらしい。

その悪いことが、また発生した。とは、言っても、今回は、ちょっと僕が手を抜いて書いていた部分にヒットしたので、僕がAppleの想定外の使い方をしていたからと言われても、まあ、仕方ない。それと、YSFLIGHTその他のプログラムは影響を受けない。

うちのMac Miniをアップグレードして、作業環境崩壊の危機に直面するわけにはいかないので、休日出勤して、大学に置いてあった2008年後期モデルのMacBook Proを急きょEl Capitanにアップグレードした。新しいOSは迷惑だが、少なくとも、7年前のMacBook ProまでサポートしているAppleの姿勢は評価できる、と、思う。そして、グラフィックスフレームワークを使って、コンパイルしてみた。どうも、ビューの生成などには何もエラーが無いらしい。試しに、ウィンドウを開いてビューを作った後、普通にイベントループを回し始めてからビューポートとかを設定するようにしてみたら、普通に動作した。

僕のフレームワーク内では、NSWindowから派生したYsOpenGLWindowに、NSOpenGLViewから派生したYsOpenGLViewをひっつけて使っていた。以下のような関数で、ウィンドウを生成していて、YsOpenGLViewは、YsWindowを開くタイミングで、生成して、生成した直後に、仮に(0,0)-(w-1,h-1)のウィンドウ座標で二次元のグラフィックスがすぐ描画できるようにOpenGLを設定していた。目的は、もちろん、学生さんにすぐグラフィックスの描画に入ってもらえるようにするためで、やっぱり、まず、なんか目に見えるものができるのと、ビューポートの設定だの、行列の設定だのいろいろやってやっとこさ絵が出るのとでは、とっつきやすさが違う。

もともと使っていたコードは、これ。緑でハイライトした箇所で、Viewを作ってるわけなのだが、Yosemite以前だと、Viewができた時点で、既にOpenGL Contextもできていて、OpenGLの関数は即座に呼ぶことができた。そうだったので、旧バージョンでは、そのまま関数から抜けずに、オレンジにハイライトしたところで、OpenGLの関数を呼んでいたのだが、El CapitanではYosemite以前と違って、この段階ではまだOpenGLコンテキストができていないらしく、この部分がすべて無視されていた。

void FsOpenWindowC(int x0,int y0,int wid,int hei,int useDoubleBuffer,int useMultiSampleBuffer,const char windowTitle[])
{
    #if !__has_feature(objc_arc)
    NSAutoreleasePool *pool=[[NSAutoreleasePool alloc] init];
    #endif

    [NSApplication sharedApplication];
    [NSBundle loadNibNamed:@"MainMenu" owner:NSApp];

    ysDelegate=[YsMacDelegate alloc];
    [ysDelegate init];
    [NSApp setDelegate: ysDelegate];

    [NSApp finishLaunching];

    NSRect contRect;
    contRect=NSMakeRect(x0,y0,wid,hei);

    unsigned int winStyle=
       NSTitledWindowMask|
        NSClosableWindowMask|
        NSMiniaturizableWindowMask|
        NSResizableWindowMask;

    ysWnd=[YsOpenGLWindow alloc];
    [ysWnd
        initWithContentRect:contRect
        styleMask:winStyle
        backing:NSBackingStoreBuffered 
        defer:NO];

    int nFormatAttrib=0;
    NSOpenGLPixelFormatAttribute formatAttrib[32]={0};
    AddOpenGLPixelFormatAttrib(&nFormatAttrib,formatAttrib,NSOpenGLPFAWindow);
    AddOpenGLPixelFormatAttrib(&nFormatAttrib,formatAttrib,NSOpenGLPFADepthSize);
    AddOpenGLPixelFormatAttrib(&nFormatAttrib,formatAttrib,(NSOpenGLPixelFormatAttribute)32);
    if(0!=useDoubleBuffer)
    {
        AddOpenGLPixelFormatAttrib(&nFormatAttrib,formatAttrib,NSOpenGLPFADoubleBuffer);
    }
    if(0!=useMultiSampleBuffer)
    {
        AddOpenGLPixelFormatAttrib(&nFormatAttrib,formatAttrib,NSOpenGLPFAMultisample);
        AddOpenGLPixelFormatAttrib(&nFormatAttrib,formatAttrib,NSOpenGLPFASampleBuffers);
        AddOpenGLPixelFormatAttrib(&nFormatAttrib,formatAttrib,(NSOpenGLPixelFormatAttribute)1);
        AddOpenGLPixelFormatAttrib(&nFormatAttrib,formatAttrib,NSOpenGLPFASamples);
        AddOpenGLPixelFormatAttrib(&nFormatAttrib,formatAttrib,(NSOpenGLPixelFormatAttribute)4);
    }

    NSOpenGLPixelFormat *format=[NSOpenGLPixelFormat alloc];
    [format initWithAttributes: formatAttrib];

    ysView=[YsOpenGLView alloc];
    contRect=NSMakeRect(0,0,800,600);
    [ysView
        initWithFrame:contRect
        pixelFormat:format];
    
    [ysWnd setContentView:ysView];
    [ysWnd makeFirstResponder:ysView];

    [ysWnd makeKeyAndOrderFront:nil];

    [ysWnd makeMainWindow];

    [NSApp activateIgnoringOtherApps:YES];

    YsAddMenu();

#if !__has_feature(objc_arc)
    [pool release];
#endif

    int i;
    for(i=0; i<FSKEY_NUM_KEYCODE; i++)
    {
        fsKeyIsDown[i]=0;
    }

    glClearColor(1.0F,1.0F,1.0F,0.0F);
    glClearDepth(1.0F);
    glDisable(GL_DEPTH_TEST);

    glViewport(0,0,wid,hei);

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(0,(float)wid-1,(float)hei-1,0,-1,1);

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

    glShadeModel(GL_FLAT);
    glPointSize(1);
    glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
    glColor3ub(0,0,0);
}

これを、修正して、ソースの先頭付近に、

    static bool openGLprepared=false;

フラッグを追加。そして、今まで空っぽだった、YsOpenGLViewのprepareOpenGL関数内で、OpenGLの初期設定を記述。

@implementation YsOpenGLView

-(void) prepareOpenGL
{
    printf("%s\n",__FUNCTION__);

    NSRect rect;
    rect=self.frame;
    int wid=rect.size.width;
    int hei=rect.size.height;

    glClearColor(1.0F,1.0F,1.0F,0.0F);
    glClearDepth(1.0F);
    glDisable(GL_DEPTH_TEST);

    glViewport(0,0,wid,hei);

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(0,(float)wid-1,(float)hei-1,0,-1,1);

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

    glShadeModel(GL_FLAT);
    glPointSize(1);
    glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
    glColor3ub(0,0,0);

    openGLprepared=true;
}

....YsOpenGLViewの中で完結してるから、スタティックな変数にしないで、YsOpenGLViewのメンバにすればよかったか。Objective-Cはなるべく相手にしない方向だからまあいいや。

本当に思うんだけど、あるプログラミング言語を習得するっていうことは、書式を覚えるだけでは済まない。結構じっくりかけて、書き方を工夫していかないと、プログラミング言語は使いこなせるようにならない。ある、特定のベンダーの環境でしか使えないプログラミング言語とか、流行にのってポッと出て来たけど、いつ消滅するかわからないプログラミング言語に、何年もキャリアを費やすのは、リスクが高すぎると思う。

まあ、ハイリスク・ハイリターンだね。他の人より早く使いこなせるようになれば、流行ってる間は大儲けだろうし。そうか、僕はそのハイリスクを狙わなかったから、給料が上がらないのか。だけど、人生はギャンブルなのか?そういう側面があるのは仕方ないけど、本質がギャンブルなのか?

そして、最後に、YsOpenWindowC関数を修正。旧バージョンでは、OpenGLバージョンを3.2未満にするか、3.2以上にするか指定してなかったので、それを追加したけど、これはあっても無くてもとりあえず関係なかった。後半で、FsPollDeviceCっていうのは、イベントループをポーリングする関数で、何かイベントがあれば必要に応じてコールバック関数が呼び出されるので、ここで、prepareOpenGLが実行されるまで待つ。が、ただし、無限ループにはまってもこまるので、最大2秒まで。

こうすることで、FsOpenWindowCが終わった段階で、OpenGLの関数はいつでも呼べる。

void FsOpenWindowC(int x0,int y0,int wid,int hei,int useDoubleBuffer,int useMultiSampleBuffer,const char windowTitle[])
{
#if !__has_feature(objc_arc)
    NSAutoreleasePool *pool=[[NSAutoreleasePool alloc] init];
#endif

    [NSApplication sharedApplication];
    [NSBundle loadNibNamed:@"MainMenu" owner:NSApp];

    ysDelegate=[YsMacDelegate alloc];
    [ysDelegate init];
    [NSApp setDelegate: ysDelegate];

    [NSApp finishLaunching];

    NSRect contRect;
    contRect=NSMakeRect(x0,y0,wid,hei);

    unsigned int winStyle=
        NSTitledWindowMask|
        NSClosableWindowMask|
        NSMiniaturizableWindowMask|
        NSResizableWindowMask;

    ysWnd=[YsOpenGLWindow alloc];
    [ysWnd
        initWithContentRect:contRect
        styleMask:winStyle
        backing:NSBackingStoreBuffered 
        defer:NO];

    int nFormatAttrib=0;
    NSOpenGLPixelFormatAttribute formatAttrib[64]={0};

    AddOpenGLPixelFormatAttrib(&nFormatAttrib,formatAttrib,NSOpenGLPFAOpenGLProfile);
    AddOpenGLPixelFormatAttrib(&nFormatAttrib,formatAttrib,NSOpenGLProfileVersionLegacy);
    // AddOpenGLPixelFormatAttrib(&nFormatAttrib,formatAttrib,NSOpenGLProfileVersion3_2Core);

    // Deprecated? AddOpenGLPixelFormatAttrib(&nFormatAttrib,formatAttrib,NSOpenGLPFAWindow);
    AddOpenGLPixelFormatAttrib(&nFormatAttrib,formatAttrib,NSOpenGLPFADepthSize);
    AddOpenGLPixelFormatAttrib(&nFormatAttrib,formatAttrib,(NSOpenGLPixelFormatAttribute)32);
    if(0!=useDoubleBuffer)
    {
        AddOpenGLPixelFormatAttrib(&nFormatAttrib,formatAttrib,NSOpenGLPFADoubleBuffer);
    }
    if(0!=useMultiSampleBuffer)
    {
        AddOpenGLPixelFormatAttrib(&nFormatAttrib,formatAttrib,NSOpenGLPFAMultisample);
        AddOpenGLPixelFormatAttrib(&nFormatAttrib,formatAttrib,NSOpenGLPFASampleBuffers);
        AddOpenGLPixelFormatAttrib(&nFormatAttrib,formatAttrib,(NSOpenGLPixelFormatAttribute)1);
        AddOpenGLPixelFormatAttrib(&nFormatAttrib,formatAttrib,NSOpenGLPFASamples);
        AddOpenGLPixelFormatAttrib(&nFormatAttrib,formatAttrib,(NSOpenGLPixelFormatAttribute)4);
    }

    NSOpenGLPixelFormat *format=[NSOpenGLPixelFormat alloc];
    [format initWithAttributes: formatAttrib];

    openGLprepared=false;

    ysView=[YsOpenGLView alloc];
    contRect=NSMakeRect(0,0,800,600);
    [ysView
    initWithFrame:contRect
    pixelFormat:format];

    [ysWnd setContentView:ysView];
    [ysWnd makeFirstResponder:ysView];

    [ysWnd makeKeyAndOrderFront:nil];

    [ysWnd makeMainWindow];


    [NSApp activateIgnoringOtherApps:YES];

    YsAddMenu();


#if !__has_feature(objc_arc)
    [pool release];
#endif

    int i;
    for(i=0; i<FSKEY_NUM_KEYCODE; i++)
    {
        fsKeyIsDown[i]=0;
    }

    // 2015/10/04
    // OSX El Capitan creates OpenGL context asynchronously.
    // Therefore, the initialization needs to be delayed until prepareOpenGL.
    time_t t0=time(NULL);
    while(time(NULL)-t0<2)
    {
        FsPollDeviceC();
        if(true==openGLprepared)
        {
            printf("OpenGL context prepared. Good to go.\n");
            break;
        }
    }
}

この修正で、El Capitanでも使えるようになった。