ホームページ開発ツール>Xojo / Real Studio Trial and Error・XcodeとCocoaのDeclareでMetalを試す

 Xojo / Real Studio Trial and Error

XcodeとCocoaのDeclareでMetalを試す

目次
 はじめに

 以下は、Xojo Cocoaビルドについての話題です。

 XojoからMetalが使えるか、調べてみました。

 なお検証には、Xojo 2019 Release 2(注)とXcode 11.3.1を用いています。(Mac mini 2018 + macOS 10.14.6 Mojave)
(注:2017 Release 1.1では後述の通りリンクエラーが発生するため、2019 Release 2を用いた。)


 方針

 Metalについては以下のアップル公式を始め、様々なサイトで解説されています。

 参考サイト(1):Metal - Apple Developer

 説明を読み進めていって分かったのは、Metalではシェーダーに専用ファイル(.metalファイル。ビルドすると.metallibになる)を使う、ということで、どうもこの部分はXojoでは直接扱えなさそうです。
 一時は諦めかけたのですが、以下が目に留まりました。

 参考サイト(2):MacでもDeep Learningがしたい。PythonからMetal APIを呼び出す簡易実験の話|noppoman|note
 参考サイト(3):Metal Shading Language コンパイラの使い方 - n-yoda's blog

 即ち、metallibをXcodeで作ってあげれば、他はXojoでなんとかなるかも、という線が見えてきました。
 これ以上は実験で試してみるしかなさそうだったので、まずはアップルのサンプルをそのまま移植してみます。
 ここでは以下を選んでみました。

 参考サイト(4):Using a Render Pipeline to Render Primitives | Apple Developer Documentation

 処理の中心となるMTKViewは、お馴染みのObjective-CのランタイムAPIを用いて、インスタンスを動的にウィンドウに追加します。
 描画のためのdrawInMTKView:やリサイズ時に呼ばれるmtkView:drawableSizeWillChange:はDelegateメソッドなので、これも同様の手法で割り当てます。
 metallibの読み込みについては、(オリジナルの)プロジェクト内に組み込まれているものを使う方式から、ファイルパスを指定して読み込む方式に変更します。具体的には、newDefaultLibraryの代わりにnewLibraryWithFile:error:を用います。

 後はひたすらコードを置き換えていくだけなのですが、実装してわかった問題点(疑問点)があったので、対処法含めて記しておきます。  以上を踏まえ、(残りの)仕様は以下の通りとしました。

 metallibのビルド(Xcode)
  1. Xcodeで新規プロジェクトを作成(テンプレートダイアログで「Metal Library」を選択。ファイル名はここでは「AAPLShaders」)
  2. AAPLShadersフォルダに「new file...」でファイル作成(テンプレートダイアログで「Header File」を選択。ファイル名はここでは「AAPLShaderTypes」)
  3. AAPLShaders.metalに、サンプルの「AAPLShaders.metal」の内容をコピー&ペースト(全文置き換え)
  4. AAPLShaderTypes.hに、サンプルの「AAPLShaderTypes.h」の内容をコピー&ペースト(全文置き換え)
  5. ビルド
  6. 出来上がったAAPLShaders.metallibをXojoのプロジェクトと同じフォルダ内に置く

 Xojoでの実装
【ソースコードのコピー&ペーストについて】
・ソースコード(グレー背景部分の全文)をコピーし、指定のオブジェクトにペーストすると、(新規作成して名前等を個別にコピー&ペーストしなくても)復元されます。
・ペーストはオブジェクトに行って下さい。オブジェクト内のEvent Handlers/Methods/Properties等にペーストしても、うまくいかない場合があります。
・それでもペーストできない場合は、各項目のカッコ内を適用して下さい。
  1. Xojoで新規プロジェクトを作成(64bitビルド)
  2. 以下をWindow1にペースト(できなければ、Sub - Endの間をOpeningイベントに記述)
    Sub Opening() Handles Opening
      // MTKView初期化。引数は、配置するウィンドウ
      Dim inst As MTKView = new MTKView(self)
    End Sub
    
  3. 新規クラス(名前は、ここでは「MTKView」)を作成
  4. 以下をMTKViewにペースト
    Public Sub Constructor(win As Window)
      // MTKView生成
      InitMTKView(win)
      
      // MTKViewセットアップ
      SetMTKView()
      
      // MTKViewのLockingの設定
      Declare Sub setAutoresizingMask Lib "Cocoa" selector "setAutoresizingMask:" (class_id As Ptr, flg As Integer)
      setAutoresizingMask(viewInst, 2+16)  // NSViewWidthSizable = 2 , NSViewHeightSizable = 16
      
      // 現在のView(=ウィンドウ)のサイズでviewportSizeを初期化
      Declare Function myDelegate Lib "Cocoa" Selector "delegate" (receiver As Ptr) As Ptr
      Dim delg As Ptr = myDelegate(viewInst)
      Declare Function drawableSize Lib "Cocoa" selector "drawableSize" (obj_id As Ptr) As CGSize
      Dim size As CGSize = drawableSize(viewInst)
      Declare Sub drawableSizeWillChange Lib "Cocoa" selector "mtkView:drawableSizeWillChange:" (obj_id As Ptr, view As Ptr, size As CGSize)
      drawableSizeWillChange(delg, viewInst, size)
    End Sub
    
  5. 以下をMTKViewにペースト
    Private Sub InitMTKView(win As Window)
      // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。
      Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr
      
      // NSViewを継承したカスタムクラスを作成。初回のみ
      makeClass()
      
      // インスタンスを作成
      Dim rect As NSRect = NSMakeRect(0, 0, win.Width, win.Height)  // Windowサイズに合わせる
      Declare Function alloc Lib "Cocoa" selector "alloc" (class_id As Ptr) As Ptr
      Declare Function initWithFrame Lib "Cocoa" selector "initWithFrame:" (obj_id As Ptr, frame As NSRect) As Ptr
      Dim subclassId As Ptr = initWithFrame(alloc(NSVewClass), rect)
      
      // Delegateを設定
      Declare Sub setDelegate Lib "Cocoa" Selector "setDelegate:" (receiver As Ptr, obj As Ptr)
      setDelegate(subclassId, makeDelegate())
      
      // ウィンドウのビューを取得
      Declare Function contentView Lib "Cocoa" selector "contentView" (class_id As Integer) As Ptr
      Dim cview As Ptr = contentView(win.handle)
      
      // 生成したコントロールをビューに追加
      Declare Sub addSubview Lib "Cocoa" selector "addSubview:" (class_id As Ptr, view As Ptr)
      addSubview(cview, subclassId)
      
      // インスタンスを保持
      viewInst=subclassId
    End Sub
    
  6. 以下をMTKViewにペースト
    Private Sub SetMTKView()
      // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。
      Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr
      
      Dim error As Ptr
      
      // Device(GPUアクセスの主体)を生成してviewにセット
      Declare Function MTLCreateSystemDefaultDevice Lib "Metal" () As Ptr
      Dim device As Ptr = MTLCreateSystemDefaultDevice()
      Declare Sub setDevice Lib "Cocoa" Selector "setDevice:" (receiver As Ptr, obj As Ptr)
      setDevice(viewInst, device)
      
      // metallibファイルの取得(ビルド後はアプリケーションパッケージ内のResourcesフォルダ内から取得。ビルド後に何らかの手段でコピーしておく。)
      Dim f As FolderItem = SpecialFolder.Resource("AAPLShaders.metallib")  // 2019r1.1以前は、SpecialFolder.GetResource
      if f=nil or f.Exists=false then
        // デバッグ時はプロジェクトファイルの場所から取得
        f=App.ExecutableFile.Parent.Parent.Parent.Parent.Child("AAPLShaders.metallib")
        if f=nil or f.Exists=false then
          return
        end if
      end if
      Dim libPath As String = f.NativePath
      
      // metallibファイルを読み込んで、Functionを取得
      Declare Function newLibraryWithFile Lib "Cocoa" selector "newLibraryWithFile:error:" (obj_id As Ptr, name As CFStringRef, byRef err As Ptr) As Ptr
      Dim defaultLibrary As Ptr = newLibraryWithFile(device, libPath, error)
      Declare Function newFunctionWithName Lib "Cocoa" selector "newFunctionWithName:" (obj_id As Ptr, name As CFStringRef) As Ptr
      Dim vertexFunction As Ptr = newFunctionWithName(defaultLibrary, "vertexShader")
      Dim fragmentFunction As Ptr = newFunctionWithName(defaultLibrary, "fragmentShader")
      
      // PipelineDescriptorを生成
      Dim pipelineStateDescriptor As Ptr = NSClassFromString("MTLRenderPipelineDescriptor")
      Declare Function alloc Lib "Cocoa" selector "alloc" (class_id As Ptr) As Ptr
      Declare Function init Lib "Cocoa" selector "init" (obj_id As Ptr) As Ptr
      pipelineStateDescriptor = init(alloc(pipelineStateDescriptor))
      Declare Sub setLabel Lib "Cocoa" Selector "setLabel:" (receiver As Ptr, obj As CFStringRef)
      setLabel(pipelineStateDescriptor, "Simple Pipeline")
      Declare Sub setVertexFunction Lib "Cocoa" Selector "setVertexFunction:" (receiver As Ptr, obj As Ptr)
      setVertexFunction(pipelineStateDescriptor, vertexFunction)
      Declare Sub setFragmentFunction Lib "Cocoa" Selector "setFragmentFunction:" (receiver As Ptr, obj As Ptr)
      setFragmentFunction(pipelineStateDescriptor, fragmentFunction)
      Declare Function colorAttachments Lib "Cocoa" selector "colorAttachments" (obj_id As Ptr) As Ptr
      Dim att As Ptr = colorAttachments(pipelineStateDescriptor)
      Declare Function objectAtIndexedSubscript Lib "Cocoa" selector "objectAtIndexedSubscript:" (obj_id As Ptr, no As Integer) As Ptr
      Dim elm As Ptr = objectAtIndexedSubscript(att, 0)
      Declare Function colorPixelFormat Lib "Cocoa" selector "colorPixelFormat" (obj_id As Ptr) As Ptr
      Dim clr As Ptr = colorPixelFormat(viewInst)
      Declare Sub setPixelFormat Lib "Cocoa" Selector "setPixelFormat:" (receiver As Ptr, obj As Ptr)
      setPixelFormat(elm, clr)
      
      // PipelineDescriptorを使ってDeviceからPipelineStateを生成
      Declare Function newRenderPipelineStateWithDescriptor Lib "Cocoa" selector "newRenderPipelineStateWithDescriptor:error:" (obj_id As Ptr, pline As Ptr, byRef err As Ptr) As Ptr
      pipelineState = newRenderPipelineStateWithDescriptor(device, pipelineStateDescriptor, error)
      
      // DeviceからCommandQueueを生成
      Declare Function newCommandQueue Lib "Cocoa" selector "newCommandQueue" (obj_id As Ptr) As Ptr
      commandQueue = newCommandQueue(device)
    End Sub
    
  7. 以下をMTKViewにペースト(できなければプロパティに、名前:viewInst、データ型:Ptr、を追加)
    Private Property viewInst as Ptr
    
  8. 以下をMTKViewにペースト
    Private Shared Sub makeClass()
      // 文字列を指定してクラスオブジェクト/セレクタを取得する。最初に一回宣言しておけばよい。
      Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr
      Declare Function NSSelectorFromString Lib "Cocoa" (aSelName As CFStringRef) As Ptr
      
      // Declare宣言
      Declare Function objc_allocateClassPair Lib "Cocoa" (superclass As Ptr, name As CString, extraBytes As Integer) as Ptr
      Declare Sub objc_registerClassPair Lib "Cocoa" (cls As Ptr)
      Declare Function class_addMethod Lib "Cocoa" (cls As Ptr, name As Ptr, imp As Ptr, types As CString) As Boolean
      
      // 既にクラス作成済なら戻る
      if NSVewClass <> nil then
        return
      end if
      
      // クラス名をmyMTKView(名前は任意。少なくとも今回のケースでは参照されない。)、メタクラス名をMTKViewにして、生成
      Dim newClassId As Ptr = objc_allocateClassPair(NSClassFromString("MTKView"), "myMTKView", 0)
      // ランタイムに登録(参照を可能とするため)
      objc_registerClassPair newClassId
      
      // クラスを保持
      NSVewClass = newClassId
    End Sub
    
  9. 以下をMTKViewにペースト
    Private Shared Function makeDelegate() as Ptr
      // 文字列を指定してクラスオブジェクト/セレクタを取得する。最初に一回宣言しておけばよい。
      Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr
      Declare Function NSSelectorFromString Lib "Cocoa" (aSelName As CFStringRef) As Ptr
      
      // Declare宣言
      Declare Function objc_allocateClassPair Lib "Cocoa" (superclass As Ptr, name As CString, extraBytes As Integer) as Ptr
      Declare Sub objc_registerClassPair Lib "Cocoa" (cls As Ptr)
      Declare Function class_addMethod Lib "Cocoa" (cls As Ptr, name As Ptr, imp As Ptr, types As CString) As Boolean
      
      // クラス名をmyMTKViewDelegate(名前は任意。少なくとも今回のケースでは参照されない。)、メタクラス名をNSObjectにして、生成
      Dim newClassId As Ptr = objc_allocateClassPair(NSClassFromString("NSObject"), "myMTKViewDelegate", 0)
      // ランタイムに登録(参照を可能とするため)
      objc_registerClassPair newClassId
      // Delegateの対象となるメソッドを追加(drawInMTKView:をXojo側で用意したmyDrawInMTKViewメソッドで受け取る。)
      if not class_addMethod (newClassId, NSSelectorFromString("drawInMTKView:"), AddressOf myDrawInMTKView, "v24@0:8@16") then
        msgBox "error1."
        return nil
      end if
      // Delegateの対象となるメソッドを追加(mtkView:drawableSizeWillChange:をXojo側で用意したmySizeWillChangeメソッドで受け取る。)
      if not class_addMethod (newClassId, NSSelectorFromString("mtkView:drawableSizeWillChange:"), AddressOf mySizeWillChange, "v24@0:8@16") then
        msgBox "error2."
        return nil
      end if
      
      // 上記で生成したクラスのインスタンスを作成
      Declare Function alloc Lib "Cocoa" selector "alloc" (class_id As Ptr) As Ptr
      Declare Function init Lib "Cocoa" selector "init" (obj_id As Ptr) As Ptr
      Dim delegateId As Ptr = init(alloc(newClassId))
      
      // インスタンスを返す
      return delegateId
    End Function
    
  10. 以下をMTKViewにペースト
    Private Shared Sub myDrawInMTKView(id As Ptr, sel As CString, view As Ptr)
      // 三角形の頂点座標とカラーの情報
      Dim triangleVertices As MemoryBlock = new MemoryBlock(8*3*4)
      triangleVertices.SingleValue(0)=250
      triangleVertices.SingleValue(1*4)=-250
      triangleVertices.SingleValue(2*4)=0  // 構造体の定義上では本来不要のものだが、ないと正常処理しない
      triangleVertices.SingleValue(3*4)=0  // 構造体の定義上では本来不要のものだが、ないと正常処理しない
      triangleVertices.SingleValue(4*4)=1
      triangleVertices.SingleValue(5*4)=0
      triangleVertices.SingleValue(6*4)=0
      triangleVertices.SingleValue(7*4)=1
      
      triangleVertices.SingleValue(8*4)=-250
      triangleVertices.SingleValue(9*4)=-250
      triangleVertices.SingleValue(10*4)=0  // 構造体の定義上では本来不要のものだが、ないと正常処理しない
      triangleVertices.SingleValue(11*4)=0  // 構造体の定義上では本来不要のものだが、ないと正常処理しない
      triangleVertices.SingleValue(12*4)=0
      triangleVertices.SingleValue(13*4)=1
      triangleVertices.SingleValue(14*4)=0
      triangleVertices.SingleValue(15*4)=1
      
      triangleVertices.SingleValue(16*4)=0
      triangleVertices.SingleValue(17*4)=250
      triangleVertices.SingleValue(18*4)=0  // 構造体の定義上では本来不要のものだが、ないと正常処理しない
      triangleVertices.SingleValue(19*4)=0  // 構造体の定義上では本来不要のものだが、ないと正常処理しない
      triangleVertices.SingleValue(20*4)=0
      triangleVertices.SingleValue(21*4)=0
      triangleVertices.SingleValue(22*4)=1
      triangleVertices.SingleValue(23*4)=1
      
      // CommandQueueからCommandBufferを作成
      Declare Function commandBuffer Lib "Cocoa" selector "commandBuffer" (obj_id As Ptr) As Ptr
      Dim commandBuffer As Ptr = commandBuffer(commandQueue)
      Declare Sub setLabel Lib "Cocoa" Selector "setLabel:" (receiver As Ptr, obj As CFStringRef)
      setLabel(commandBuffer, "MyCommand")
      
      // viewの現在のRenderPassDescriptorを取得
      Declare Function currentRenderPassDescriptor Lib "Cocoa" selector "currentRenderPassDescriptor" (obj_id As Ptr) As Ptr
      Dim renderPassDescriptor As Ptr = currentRenderPassDescriptor(view)
      if renderPassDescriptor = nil then return
      
      // RenderPassDescriptorを用いてCommandBufferからRenderCommandEncoderを生成
      Declare Function renderCommandEncoderWithDescriptor Lib "Cocoa" selector "renderCommandEncoderWithDescriptor:" (obj_id As Ptr, desc As Ptr) As Ptr
      Dim renderEncoder As Ptr = renderCommandEncoderWithDescriptor(commandBuffer, renderPassDescriptor)
      setLabel(renderEncoder, "MyRenderEncoder")
      
      // RenderCommandEncoderに描画領域をセット
      Dim vp As MTLViewport
      vp.originX=0.0
      vp.originY=0.0
      vp.Width=viewportSize.x
      vp.Height=viewportSize.y
      vp.znear=0.0
      vp.zfar=1.0
      Declare Sub setViewport Lib "Cocoa" Selector "setViewport:" (receiver As Ptr, vport As MTLViewport)
      setViewport(renderEncoder, vp)
      
      // RenderCommandEncoderにPipelineStateをセット
      Declare Sub setRenderPipelineState Lib "Cocoa" Selector "setRenderPipelineState:" (receiver As Ptr, obj As Ptr)
      setRenderPipelineState(renderEncoder, pipelineState)
      
      // RenderCommandEncoderにパラメーターをセット
      Declare Sub setVertexBytes Lib "Cocoa" selector "setVertexBytes:length:atIndex:" (obj_id As Ptr, byts As Ptr, lng As UInteger, idx As UInteger)
      setVertexBytes(renderEncoder, triangleVertices, 8*3*4, 0)  // AAPLVertexInputIndexVertices = 0
      Declare Sub setVertexBytesA Lib "Cocoa" selector "setVertexBytes:length:atIndex:" (obj_id As Ptr, byRef byts As vector_uint2, lng As UInteger, idx As UInteger)
      setVertexBytesA(renderEncoder, viewportSize, 8, 1)  // AAPLVertexInputIndexViewportSize = 1
      
      // RenderCommandEncoderに三角形を描画
      Declare Sub drawPrimitives Lib "Cocoa" selector "drawPrimitives:vertexStart:vertexCount:" (obj_id As Ptr, stt As UInteger, cnt As UInteger, cnt2 As UInteger)
      drawPrimitives(renderEncoder, 3, 0, 3)  // MTLPrimitiveTypeTriangle = 3
      
      // RenderCommandEncoderを終了
      Declare Sub endEncoding Lib "Cocoa" Selector "endEncoding" (receiver As Ptr)
      endEncoding(renderEncoder)
      
      // CommandBufferに現在のDrawableをセット
      Declare Function currentDrawable Lib "Cocoa" selector "currentDrawable" (obj_id As Ptr) As Ptr
      Dim cd As Ptr = currentDrawable(view)
      Declare Sub presentDrawable Lib "Cocoa" Selector "presentDrawable:" (receiver As Ptr, obj As Ptr)
      presentDrawable(commandBuffer, cd)
      
      // CommandBufferをGPUにプッシュ
      Declare Sub commit Lib "Cocoa" Selector "commit" (receiver As Ptr)
      commit(commandBuffer)
    End Sub
    
  11. 以下をMTKViewにペースト
    Private Shared Sub mySizeWillChange(id As Ptr, sel As CString, size As CGSize)
      // ウィンドウのリサイズに、viewportSizeを追随させる
      viewportSize.x = size.width
      viewportSize.y = size.height
    End Sub
    
  12. 以下をMTKViewにペースト(できなければ共有プロパティに、名前:commandQueue、データ型:Ptr、を追加)
    Private Shared Property commandQueue as Ptr
    
  13. 以下をMTKViewにペースト(できなければ共有プロパティに、名前:NSVewClass、データ型:Ptr、を追加)
    Private Shared Property NSVewClass as Ptr
    
  14. 以下をMTKViewにペースト(できなければ共有プロパティに、名前:pipelineState、データ型:Ptr、を追加)
    Private Shared Property pipelineState as Ptr
    
  15. 以下をMTKViewにペースト(できなければ共有プロパティに、名前:viewportSize、データ型:vector_uint2、を追加)
    Private Shared Property viewportSize as vector_uint2
    
  16. 以下をMTKViewにペースト
    Private Structure MTLViewport
      originX As double
      originY As double
      width As double
      height As double
      znear As double
      zfar As double
    End Structure
    
  17. 以下をMTKViewにペースト
    Private Structure vector_uint2
      x As UInt32
      y As UInt32
    End Structure
    
  18. 他に、NSMakeRect(メソッド)、NSRect/CGSize(構造体)が必要ですが、それらはmacoslibからコピーさせて頂きました。(上記MTKViewまたは別途モジュールを用意してコピーする。)
    注)macoslibではNSMakeRectの引数、NSRect/CGSizeのメンバーの型にSingleが割り当てられているが、64bitにも対応するため、CGFloatに書き換える。
 実行してみたところ、アップルのサンプルと同等な結果が得られることを確認しました。
S Shot1
左:アップルのサンプル 右:今回のXojoプロジェクト


 おわりに

 Xcodeの併用という点では、プラグインで機能を補完するイメージに近いのかなという感じです。

 ただ、部分的であれXcodeを使うなら全部Xcodeでいいじゃん、というのも尤もな話で、例えば、GUIエディタは使い慣れたXojoにしたいとか、既にXojoで作成済のグラフィック処理のコア部分のみMetalに置き換えたいとか、有用となる局面は限定されるかもしれません。

 なお、参考サイト(3)に説明がありますが、.metallibはコマンドラインでビルドすることもできるので、工夫すれば、見かけはXojoだけで完結しているような形に持っていけるかもしれませんが、そこまでする必要はないかも。


 お世話になったサイト

 貴重な情報をご提供頂いている皆様に、お礼申し上げます。(以下、順不同)

 参考サイト(1):Metal - Apple Developer
 参考サイト(2):MacでもDeep Learningがしたい。PythonからMetal APIを呼び出す簡易実験の話|noppoman|note
 参考サイト(3):Metal Shading Language コンパイラの使い方 - n-yoda's blog
 参考サイト(4):Using a Render Pipeline to Render Primitives | Apple Developer Documentation
 参考サイト(5):[XCode][Cocoapods]ビルドエラー時の対処法 - Qiita


 更新履歴

 2020.09.07 タイトルの先頭に「Xcodeと」を追加
 2020.07.27 新規作成


[Home]  [MacSoft]  [Donation]  [History]  [Privacy Policy]  [Affiliate Policy]