ホームページ>開発ツール>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:を用います。
後はひたすらコードを置き換えていくだけなのですが、実装してわかった問題点(疑問点)があったので、対処法含めて記しておきます。以上を踏まえ、(残りの)仕様は以下の通りとしました。
- 頂点データは構造体なのだが、構造体の配列をDeclareで渡す方法が分からないため、MemoryBlockで渡そうとしたところ、正しく描画されなかった。
(幸運にも)ちょっとした勘違いが元で、頂点情報もカラー情報と同じ4メンバー相当(16byte)にしたところ、正常に表示された。
ちょっと解せない面もあるが、データ構造アライメントが関係しているのかも。
- MetalのメソッドMTLCreateSystemDefaultDevice()はアップルのドキュメントではMetal Frameworkに含まれているようなのだが、DeclareでMetalを指定すると、常用する2017 Release 1.1ではリンクエラー(ld: framework not found)が発生する。
この現象は、これまでも64bitビルドでは時々発生していて(32bitビルドでは通る)、LibraryNameを変えると動いていたのだが、今回はダメだった。
一方、手持ちの最新版である2019 Release 2ではリンクエラーは発生せず、実行しても機能している(ように見える)ことから、今回はこちらを用いることにした。
理由はよくわからないが、以下のケースのような状況が内部的に発生しているのかも。
参考サイト(5):[XCode][Cocoapods]ビルドエラー時の対処法 - Qiita
- MTKViewはクラス化する。
- 簡素化のため、汎用性を考えればインスタンス側で行う処理も、クラス側(共有メソッド)で行う。
- 簡素化のため、オリジナルとはファイル構成を変更する。
metallibのビルド(Xcode)
- Xcodeで新規プロジェクトを作成(テンプレートダイアログで「Metal Library」を選択。ファイル名はここでは「AAPLShaders」)
- AAPLShadersフォルダに「new file...」でファイル作成(テンプレートダイアログで「Header File」を選択。ファイル名はここでは「AAPLShaderTypes」)
- AAPLShaders.metalに、サンプルの「AAPLShaders.metal」の内容をコピー&ペースト(全文置き換え)
- AAPLShaderTypes.hに、サンプルの「AAPLShaderTypes.h」の内容をコピー&ペースト(全文置き換え)
- ビルド
- 出来上がったAAPLShaders.metallibをXojoのプロジェクトと同じフォルダ内に置く
Xojoでの実装
【ソースコードのコピー&ペーストについて】
・ソースコード(グレー背景部分の全文)をコピーし、指定のオブジェクトにペーストすると、(新規作成して名前等を個別にコピー&ペーストしなくても)復元されます。
・ペーストはオブジェクトに行って下さい。オブジェクト内のEvent Handlers/Methods/Properties等にペーストしても、うまくいかない場合があります。
・それでもペーストできない場合は、各項目のカッコ内を適用して下さい。
実行してみたところ、アップルのサンプルと同等な結果が得られることを確認しました。
- Xojoで新規プロジェクトを作成(64bitビルド)
- 以下をWindow1にペースト(できなければ、Sub - Endの間をOpeningイベントに記述)
Sub Opening() Handles Opening // MTKView初期化。引数は、配置するウィンドウ Dim inst As MTKView = new MTKView(self) End Sub
- 新規クラス(名前は、ここでは「MTKView」)を作成
- 以下を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
- 以下を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
- 以下を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
- 以下をMTKViewにペースト(できなければプロパティに、名前:viewInst、データ型:Ptr、を追加)
Private Property viewInst as Ptr
- 以下を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
- 以下を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
- 以下を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
- 以下を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
- 以下をMTKViewにペースト(できなければ共有プロパティに、名前:commandQueue、データ型:Ptr、を追加)
Private Shared Property commandQueue as Ptr
- 以下をMTKViewにペースト(できなければ共有プロパティに、名前:NSVewClass、データ型:Ptr、を追加)
Private Shared Property NSVewClass as Ptr
- 以下をMTKViewにペースト(できなければ共有プロパティに、名前:pipelineState、データ型:Ptr、を追加)
Private Shared Property pipelineState as Ptr
- 以下をMTKViewにペースト(できなければ共有プロパティに、名前:viewportSize、データ型:vector_uint2、を追加)
Private Shared Property viewportSize as vector_uint2
- 以下をMTKViewにペースト
Private Structure MTLViewport originX As double originY As double width As double height As double znear As double zfar As double End Structure
- 以下をMTKViewにペースト
Private Structure vector_uint2 x As UInt32 y As UInt32 End Structure
- 他に、NSMakeRect(メソッド)、NSRect/CGSize(構造体)が必要ですが、それらはmacoslibからコピーさせて頂きました。(上記MTKViewまたは別途モジュールを用意してコピーする。)
注)macoslibではNSMakeRectの引数、NSRect/CGSizeのメンバーの型にSingleが割り当てられているが、64bitにも対応するため、CGFloatに書き換える。
左:アップルのサンプル 右:今回の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]