Objective-C
イベント
該当するコンテンツが見つかりませんでした
マガジン
該当するコンテンツが見つかりませんでした
技術ブログ
本記事は【 Advent Calendar 】の9日目の記事です。 AI戦略室のM・Wです。この記事を書いているのは12月1日です。 11月30日にジャパンカップで カランダガン の単勝と三連単を取りました。現地で観戦をしていたのですが、大変興奮したレースでした。ゴール間際の2頭の叩き合いに混ざる空馬。 ぜひYoutubeで動画を見てください。以上、12月11日の記事でした! 沼の淵 突然ですが、forkやspawnの違いについて知っていますでしょうか。 私は知らなかったです。いまも正直わかりません。 その結果、沼にハマってしまいました。抜け出せないまま今日を過ごしています。 これは自分用のメモ+同じようなことを実装する人がいた際の一助になればいいなと思って執筆しています。 まずとあるAgentを考えます。このAgentは「環境」を知覚し「行動」を決定するAgentです。 Agentは「行動」を行うと「環境」から「報酬」を手にすることができ、「環境」は次の「状態」に遷移します。 図にするとこんな感じです。これは基本的な強化学習を説明する際に用いられる簡略図です。 今回、ボン◯ーマンのようなPvPをするゲームについて、強化学習を使ってエージェントを作る機会がありました。つまり上の画像に照らし合わせると下記の表となります。 用語 対応 環境 ゲーム自体 エージェント 操作キャラクター 状態 その時時の盤面 行動 移動+ボム配置+キック 報酬 勝利/敗北など レギュレーションとして自陣営は2つのBotを用意する2vs2の対決と提示されたため、上記のエージェントが2つ必要なマルチエージェント学習を行う必要がありました。 (ちなみに特に実装は指定されなかったのでロジックで記述するでもOKです。AI部署なので強化学習で挑みました) 沼にダイブ ここでいよいよ掲題に出てきたマルチプロセス学習の話ができてます。 最新の強化学習アルゴリズムに疎かったので、一旦Actor-CriticベースのA3Cアルゴリズムを選択しました。実装したモデル構造は下記となります。 盤面のエンコードやモデル設計については以下のモデルを参考としています。Cursor先生ありがとう! Multi-Agent Training for Pommerman: Curriculum Learning and Population-based Self-Play Approach: https://arxiv.org/abs/2407.00662 マルチプロセス学習の全体感としては下記の図です。 ゲーム盤面を提供するホスト側とエージェント間はWebSocketによる疎通を行う必要があるため、各BotごとにWebSocketサーバを子プロセスとして作成します。 つまり子プロセス上では、ゲーム開始から終了まで実行されますが、そこで収集したデータについては何かしらの方法を使って親プロセス上に移動させないと、子プロセスがterminateされた際に破棄されてしまう問題があります。 そのため 共有メモリ を使って誤差勾配を親プロセス側と共有を行い、最適化器を使って共有モデルのパラメータを更新、次の学習ループで子プロセスへ配分を繰り返し学習を行っていきます。 沼の底 学習モデルを共有メモリに配置するためには model.share_memory() を実施しますが、今回の実装のケースにおいてたびたび子プロセスの model.backward() が失敗する事象がありました。 いくつか問題はありましたが、 ・下記のログが出力され、backward処理が失敗するケース objc[8623]: +[MPSGraphObject initialize] may have been in progress in another thread when fork() was called.objc[8623]: +[MPSGraphObject initialize] may have been in progress in another thread when fork() was called. We cannot safely call it or ignore it in the fork() child process. Crashing instead. Set a breakpoint on objc_initializeAfterForkError to debug. ・backward()が呼び出された際に子プロセスがクラッシュして音もなく消える 使用してる端末がMacOSであるため、前者のケースでは $export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES を実行することでエラー文が出力されなくなります。しかしながらこれはワークアラウンドとなります。 MacOSでは fork() が実行された際に、親プロセスで既に初期化されつつあったObjective-Cのデータ構造が子プロセスに引き継がれると、子プロセス側でその初期化が安全に完了できない状況が発生することがあります。これにより、子プロセスがクラッシュ(通常はEXC_BAD_ACCESS)したり、デッドロックしたりします。 Pythonのmultiprocessingは、デフォルトでは「fork」方式を使って新しいプロセスを生成するため、この問題に直面しやすいです。そのため2点目の音もなく子プロセスが消えた原因も含めて、backward()処理がうまく動作しないのは fork() による子プロセスの生成に問題があると結論付けました。 ちなみにPyTorchからマルチプロセス学習する際のベストプラクティスでも「Use an alternative process start methods, such as spawn or forkserver, which ensures a clean initialization of each process.」と記載があります。 https://docs.pytorch.org/docs/stable/notes/multiprocessing.html#poison-fork-in-multiprocessing spawn は fork とは違い、Pythonインタープリタごとプロセスを生成します。起動が遅いというデメリットはありますが、必要な情報のみを親プロセスから引き継ぐ点や上記の初期化に起因するようなエラーを回避できるメリットがあります。 実験 簡単なNNを構築してspawnを使用したbackward()がうまくいくかどうかを実験してみます。 import torchimport torch.nn as nnimport torch.multiprocessing as mpimport torch.nn.functional as Fimport torch.optim as optimclass ExampleNN(nn.Module): def __init__(self): super(ExampleNN, self).__init__() self.fc = nn.Linear(10, 1) def forward(self, x): out = self.fc(x) return F.sigmoid(out)def train(xs, target): model = ExampleNN() optimizer = optim.SGD(model.parameters()) out = model(xs) loss = F.binary_cross_entropy(out, target) optimizer.zero_grad() loss.backward() optimizer.step() print('trainメソッドが終了したよ') 上記のtrainメソッドをforkで動かしてみます。 if __name__ == '__main__': x = torch.rand(1, 5, 10) t = torch.Tensor([0, 1, 1, 0, 1]).reshape(1, 5, 1) train(x, t) print('-'*54) ctx = mp.get_context('fork') processes = [] for i in range(5): p = ctx.Process(target=train, args=(x, t)) p.start() processes.append(p) for p in processes: p.join()>>> trainメソッドが終了したよ------------------------------------------------------RuntimeError: Unable to handle autograd's threading in combination with fork-based multiprocessing. See https://github.com/pytorch/pytorch/wiki/Autograd-and-Fork 無事エラーが出ました。 ちなみに途中の train(x, t) をコメントアウトすると正常に動作します。これは上記のGithubにも記載の通り、backward()で呼び出されているAutogradがすでに使用されている状態でfork()されたことに起因するエラーとなります。 Autograd engine relies on threads pool, which makes it vulnerable to fork. We detect such situations and warn users to use spawn method of multiprocessing. 指示通りspawnでやってみましょう。 if __name__ == '__main__': x = torch.rand(1, 5, 10) t = torch.Tensor([0, 1, 1, 0, 1]).reshape(1, 5, 1) train(x, t) print('-'*54) ctx = mp.get_context('spawn') processes = [] for i in range(5): p = ctx.Process(target=train, args=(x, t)) p.start() processes.append(p) for p in processes: p.join() >>> trainメソッドが終了したよ------------------------------------------------------trainメソッドが終了したよtrainメソッドが終了したよtrainメソッドが終了したよtrainメソッドが終了したよtrainメソッドが終了したよ 無事エラーなく実行することができました。 この結果から spawn を明示的に指定することでbackward()処理のエラーを回避できることがわかります。 実際の実装は下記のような形となっています。 ctx = mp.get_context('spawn') # mp.set_start_method('spawn', force=True) # エピソードループ for episode in tqdm(range(num_episodes)): clients: List[BombermanClient] = self.create_clients() process_pool = [] for c in clients: p = ctx.Process(target=c.run) p.start() process_pool.append(p) # エピソードの学習 loop = asyncio.get_event_loop() loop.run_until_complete(self.train_episode(max_steps)) print("ロールアウト終了") # 子プロセスの終了を待つ for p in process_pool: if p.is_alive(): p.terminate() p.join(timeout=3) # 最大3秒待つ if p.is_alive(): # それでも終了しない場合は強制終了 print(f"Warning: Process {p.pid} did not terminate gracefully, forcing...") p.kill() p.join() そもそもMacOSだとデフォルトで spawn だそうです。つまりわざわざ明示的に fork を使ったところから全ては始まったのです。Cursorくん、君は頑なにforkを推していたよね・・・。 これで 戦 に臨めます! 終わりに 元々のスタート地点として、マイナビグループの中の一つである Mynavi Techtus Vietnam さんから挑戦状を叩きつけられたところからこの沼は始まりました。 Techtusさん「ボン◯ーマンAIを作成してPvPをやろう!それでどこのチームが強いか勝負だ!」 結果は・・・ 全敗 でした。 そもそもこの問題を解決したのは戦いが終わった3週間後ぐらいなんです! つまり、戦には裸同然で挑んだことになる・・・?すでに次の戦は始まっているという決意を胸に日々沼の中で生きたいと思います! イベント告知 12月23日にイベントを開催します!申し込みはこちらから▼ https://mynaviit.connpass.com/event/376769
はじめに こんにちは、WEARフロントエンド部iOSブロックの西山です。普段はWEAR iOSチームのマネジメント兼アプリの開発を担当しています。今年のWWDC25で、新しいソフトウェアデザインのLiquid Glassが発表されました。透明感のあるUIと流動的なアニメーションが特徴的なこの新デザインは、WEARアプリに大きな影響を与えました。鋭意進行中の取り組みとして、本記事では、Liquid Glass対応を計画的に進めるための取り組みを紹介します。 目次 はじめに 目次 Liquid Glassとは 課題 UIが大きく変わることでデザイン崩れが発生 タブバー タブバーの裏側にコンテンツが透過しない タブバーとコンポーネントが被る ナビゲーションバー ボタンに枠がつく ナビゲーションバーの下に配置してあるコンポーネントとの一体感を失う その他のコンポーネント Switchが大きくなったことによるレイアウト崩れ アラート(アクションシート)の変化 デザイナー、エンジニア、QAのコスト増大が予想される Liquid Glassの学習コスト 移行戦略 開発プロセスの整備 ブランチ戦略 Xcode 26対応 Xcode 16を使用している場合の例 Liquid Glassを無効にする Liquid Glass効果を動的に切り替えられるようにする Liquid Glass対応で分岐できるようにする 対応内容を一部紹介 contentInsetAdjustmentBehavior = .never の対応 contentInsetAdjustmentBehavior = .never の問題点 extendedLayoutIncludesOpaqueBars = true の対応 まとめ Liquid Glassとは iOS 26から導入される新しいデザインで、次の特徴があります。 ガラスのような質感と透明感 光の反射/屈折のようなリアルな動き 流動性のあるアニメーション https://www.apple.com/jp/newsroom/2025/06/apple-introduces-a-delightful-and-elegant-new-software-design/ より引用 ナビゲーションバーやタブバーなどの標準コンポーネントにデザインが適用されるので既存アプリの確認が必要になります。 課題 WEARアプリの確認と現在の状況を踏まえ、次のような課題がありました。 UIが大きく変わることでデザイン崩れが発生 デザイナー、エンジニア、QAのコスト増大が予想される Liquid Glassの学習コスト UIが大きく変わることでデザイン崩れが発生 タブバーとナビゲーションバーを透過させて確認したところ複数の問題が見つかりました。 タブバー タブバーの裏側にコンテンツが透過しない Before After タブバーとコンポーネントが被る Before After ナビゲーションバー ボタンに枠がつく Before After ナビゲーションバーの下に配置してあるコンポーネントとの一体感を失う Before After その他のコンポーネント Switchが大きくなったことによるレイアウト崩れ Before After アラート(アクションシート)の変化 Before After こちらは一部で、この他にも細かいところの問題は存在しています。 デザイナー、エンジニア、QAのコスト増大が予想される WEARは、基本的に3つのOSをサポートしています。今は過渡期のためiOS 16もサポート中ですが、近々iOS 17, 18, 26のサポートになります。サポートOSは、メジャーバージョンが登場してから数ヶ月後の更新となるため、1年に1回更新されます。そのため、現時点から約2年間は、iOS 26未満をサポートする必要があり、Liquid Glassに対応すると新旧それぞれのデザインを並行管理していく必要が出てきます。それぞれのデザイン調整、開発、テストを行うことで、担当する各チームのコストが増大すると予想されます。 Liquid Glassの学習コスト 新しい概念のため、まずはLiquid Glassを理解する必要があります。既存のコンポーネントのどこをどのような形にすることでLiquid Glassとしてベストなのか見極める必要もあります。まだ登場して間もないため、ノウハウも少なく手探り状態になることは否めません。さらにデザイナーのリソースもこちらに多くを割ける状態ではありませんでした。 移行戦略 リスクと対応コストを考慮し、次の方針で進めます。 Liquid Glassを無効にするオプションをフルで利用する レイアウト修正を最優先で行う レイアウト修正を行いながら原因を把握し、デザインや実装方法を見直す レイアウト修正完了後、Liquid Glassのベストプラクティスを探る Liquid Glassの為に構造を大きく変える変更は可能であればiOS 18以前にも適用させる 1に関してですが、Appleは、Liquid Glassを無効にするオプションを用意しており、現時点では 次のメジャーバージョンリリースまで(約1年)有効 とされています。 開発プロセスの整備 修正後は新旧デザインの確認が必要になるため、効率よく進めるための状態を作る必要があります。 ブランチ戦略 普段WEAR iOSでは、developブランチを除外したGit-flowで開発しています。今回の対応では、Liquid Glass用の開発ブランチを作成することも考えられます。しかし、生存期間は長くなりマージコストの増大が懸念されるため既存と同じように進めます。そのため、旧デザインに影響を与えないように対応していく必要があります。 Xcode 26対応 当たり前ですが、Xcode 26対応は必須です。Xcode 26対応の前に先行して進めることもできますがとても非効率になります。 Xcode 16を使用している場合の例 Xcode 26でビルドが通るブランチを用意 Xcode 26で不具合の修正 Xcode 16用のブランチに切り替え Xcode 16用のブランチに修正を取り込む Xcode 16でビルドし確認 問題があれば2から繰り返し Liquid Glassを無効にする Liquid Glassを無効にする設定です。 Info.plist に UIDesignRequiresCompatibility を追加することで無効にできます。 Liquid Glass効果を動的に切り替えられるようにする こちらを参考に swizzling を使ってRelease版には影響がないよう、デバッグメニューから動的に変えられるようにしています。 zenn.dev デバッグメニューの設定 Liquid Glass対応で分岐できるようにする 同一ブランチでの開発になるため、タブバーの透過等で分岐が必要になります。不具合の修正では極力Liquid Glass用の分岐は入れたくないですが、どうしても必要になるケースも考えられます。また、Liquid Glass無効化オプションの廃止後も、iOS 18をサポートする期間があることを考慮します。 class LiquidGlass : NSObject { @objc static var isEnabled : Bool { UserDefaults.standard.isLiquidGlassEnabled } } WEARには一部Objective-Cが残っているため、Objective-Cからも参照できるようにしています。今はデバッグメニューから切り替えられる値を参照してますが、オプションが無効になってからは次のように変える予定です。 class LiquidGlass : NSObject { @objc static var isEnabled : Bool { if #available(iOS 26.0 , * ) { true } else { false } } } 対応内容を一部紹介 レイアウト修正の中で、最初に取り掛かったのはタブバー周りの対応です。タブバーの背景を透過させるだけでもLiquid Glassらしさが出ます(個人の感想です) Before After WEARのタブバーは背景色が指定されており、タブバーの裏に画面が回り込むような実装になっていない画面が多数ありました。 そういった画面の大多数はスクロールできる画面なので、 ScrollView 系を修正します。 ※WEARでは、SwiftUIの導入も徐々に進めてはいますが、まだまだUIKitがメインなのでUIKitの対応になります。 主に次の2つを利用しているところの修正が必要でした。 contentInsetAdjustmentBehavior = .never extendedLayoutIncludesOpaqueBars = true contentInsetAdjustmentBehavior = .never の対応 iOS 11以前は scrollView.contentInset を実装側で調整する必要がありましたが、iOS 11からは自動で調整してくれるようになりました。当時の選択肢としては、 never or automatic の2択が取れましたが、 WEARでは never を採用していました。その背景もあり scrollView.contentInset.top は contentInsetAdjustmentBehavior = .never を指定して調整していました。 contentInsetAdjustmentBehavior = .never の問題点 contentInset.top だけの設定であればまだ良かったのですが、今回の対応で contentInset.bottom も調整する必要があります。 never のままだと、タブバーの高さをセットする必要があり煩雑になるので、次の方法に変更しました。 contentInsetAdjustmentBehavior = automatic にする(デフォルト値なので指定なし) オートレイアウトの bottom は superview に貼る 注意点は、今まで scrollView.contentInset を参照している場所は scrollView.adjustedContentInset を参照するように変える必要があるところです。 adjustedContentInset は自動調整された後の値になっているので automatic にした際は、こちらの参照が適切になる箇所が出てきます。 extendedLayoutIncludesOpaqueBars = true の対応 こちらは、ナビゲーションバーの裏側に画面を通すために使用しており、 edgesForExtendedLayout = .top とセットで使用されています。タブバーを透過させるため bottom も追加します。 extendedLayoutIncludesOpaqueBars = true edgesForExtendedLayout = [.top, .bottom] ただこれだと旧デザインで、スクロールした最下部のアイテムがタブバーの裏に隠れてしまいます。 最下部のアイテムがタブバーの裏に隠れている そのため、次のようにします。 extendedLayoutIncludesOpaqueBars = true edgesForExtendedLayout = isLiquidGlassEnabled ? [.top, .bottom] : .top 極力分岐はしたくないと上述してましたが、早速使ってしまっています。ただこちらは、Liquid Glassのリリースをする前にタブバーを半透明にする対応をリリースし、事前に分岐を無くしておきたいと考えています。 まとめ 本記事ではWEARアプリにおけるLiquid Glass対応を進めるための取り組みを紹介しました。これからLiquid Glass対応に取り組もうとしている方の参考に少しでもなれば幸いです。まずは不具合を修正し、その後、最適なLiquid Glassの対応を模索していければと思います。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、ZOZO New Zealandの 中岡 です。普段はZOZOMAT/ZOZOGLASSの運用・保守や計測技術を使った新規事業の開発をしています。 目次 はじめに 目次 ZOZOMATとは ZOZOMATの構成 移行の背景 検討したアプローチ 移行後の構成 レンダリングバックエンドの抽象化 ポインタによる抽象化 移行の際に当たった課題と工夫点 Objective-CとC/C++のメモリ管理の違い CFBridgingを使ったリソース管理 座標系の違い まとめ ZOZOMATとは オンラインで靴を購入する際に、サイズが合わないという問題を解決する仕組みです。1台のスマートフォンと紙製のZOZOMATだけで、正確に足のサイズを測れます。足をスキャンすると、高精度の3Dモデルが生成されます。最適なサイズの靴も表示されるので、すぐに靴を購入できます。 ZOZOMATの構成 ZOZOMATの機能は社内ライブラリとして開発されており、ZOZOTOWNに組み込まれています。以下は依存関係の一部です。ZOZOMATの機能を提供しているライブラリはZOZOMATフレームワークと呼ばれており、フレームワークはさらに計測結果の3Dモデルの表示するためのZOZOMAT Rendererに依存しています。 本記事ではタイトルにもあるとおり、そのZOZOMAT RendererのOpenGL ESからMetalへの移行についてお話しします。 移行の背景 足の3Dモデルのレンダリング使っているOpenGL ESはiOS12でDeprecatedになっており、将来的に利用できなくなる可能性がありAppleもMetalへの移行を推奨しています。 developer.apple.com 検討したアプローチ ZOZOMAT Rendererは以下の図にあるようにクロスプラットフォームに対応しています。そのため、単純にプラットフォーム非依存レイヤーの中のOpenGL ESをMetalに書き換えることはできません。 techblog.zozo.com 移行するためには引き続きAndroidをサポートしつつiOSでのみMetalで動作するようにしなければいけません。そのためのアプローチは大きく分けて2つありました。 bgfx のようなMetalをバックエンドとして利用可能なクロスプラットフォームのレンダリングライブラリに移行する バッファ作成や描画処理といったグラフィックスAPIを呼び出す処理を抽象化し、プラットフォームごとにOpenGL ES/Metalを呼び出す 最終的に、2番目のアプローチを選択しました。その理由は以下の通りです。 Android側の実装に極力影響を与えず、最小限の工数で進められる 描画対象が比較的シンプルな3Dモデルであり、外部ライブラリの導入に見合うメリットが少なかった 移行後の構成 以下は移行後の簡単な構成図です。プラットフォーム非依存レイヤー(MVP行列の管理・シーン管理などのコアロジック)から、実際の描画呼び出し部分を切り出しました。そして、Cヘッダーで定義した抽象インタフェースを経由しOpenGL ES/Metalの各バックエンド実装に振り分けるといった構成です。 レンダリングバックエンドの抽象化 グラフィックスAPIを使ったレンダリングには主に以下のようなステップがあり、Cヘッダーの抽象インタフェースはこれらの処理をするメソッドがステップごとに定義されています。 ステップ OpenGL ES Metal 1. シェーダーの読み込み - GLSL ソースをコンパイル・リンク - プログラムオブジェクトを生成 - MSL ソースをライブラリ化 - MTLRenderPipelineState を生成 2. バッファの作成 - VBO/EBO を生成してバインド - MTLBuffer を生成 3. 描画 - プログラムをアクティブ化してユニフォーム設定 - glDrawElements を実行 - コマンドバッファ/エンコーダを作成 - 頂点/インデックスをエンコーダにセット - drawIndexedPrimitives を実行 ポインタによる抽象化 Metalではバックエンドレイヤーで MTLBuffer や MTLRenderPipelineState の生成をするために MTLDevice が必要です。また、各フレームで drawIndexedPrimitives を呼ぶ際に MTLRenderCommandEncoder も必要です。これらのオブジェクトはiOS側で生成しプラットフォーム非依存レイヤーを経由してバックエンドレイヤーに渡さなければいけませんでした。この際にMetal固有の型を隠蔽するためにポインタを使います。 以下は簡単なサンプルコードです。context_tにMetal固有の型を定義してその型のポインタをプラットフォーム非依存レイヤーを経由してバックエンドレイヤーに渡し型キャストして利用します。 // context.h #import <Metal/Metal.h> typedef struct context_t { id <MTLDevice> metalDevice; id <MTLRenderCommandEncoder> currentRenderCommandEncoder; } context_t; // ZMRMetalView.m // プラットフォーム(iOS)側 #import "context.h" @interface ZMRMetalView () { id <MTLCommandQueue> _commandQueue; context_t context; // 省略 } @end @implementation ZMRMetalView // 省略 - ( void ) setup { context.metalDevice = MTLCreateSystemDefaultDevice (); _commandQueue = [_device newCommandQueue]; // 省略 // ここでcontextの参照をプラットフォーム非依存レイヤーに渡す zmrInit (&context); } // 毎フレーム呼ばれる - ( void ) drawView:( id )sender { id <MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer]; MTLRenderPassDescriptor *renderPassDescriptor = [MTLRenderPassDescriptor renderPassDescriptor]; // 省略 // MTLRenderCommandEncoderの生成しcontextに渡す context.currentRenderCommandEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor]; } // プラットフォーム非依存レイヤー void zmrInit ( void *context) { // バックエンド側にcontextをそのまま渡す initBackend (context); } #import "context.h" static context_t *metalContext void initBackend ( void *context) { // 汎用ポインタからキャスト metalContext = (context_t *)context; } 移行の際に当たった課題と工夫点 Objective-CとC/C++のメモリ管理の違い Objective-CはARC(Automatic Reference Counting)を使用しており、C/C++は手動でメモリ管理します。今回の移行では、既存のクロスプラットフォーム設計を維持するため、MetalオブジェクトをCの構造体に保持する必要がありました。この際、ARCと手動メモリ管理の境界で適切なブリッジングをします。 CFBridgingを使ったリソース管理 MetalオブジェクトをCの構造体で管理する際の参照カウントの変化は以下です。 作成時(参照カウント+1) : CFBridgingRetain でARC管理からC構造体の手動管理に移行 newBufferWithBytes: などでMetalオブジェクトを作成(参照カウント=1) CFBridgingRetain で参照カウントを+1し、C側で保持(参照カウント=2) ARC管理下のローカル変数がスコープを抜けると-1(参照カウント=1、C側のみが保持) 使用時(参照カウント変化なし) : __bridge で一時的にObjective-CオブジェクトとしてObjective-C++で参照 参照カウントは変化せず、単にキャストのみ実行 破棄時(参照カウント-1) : __bridge_transfer で手動管理からARC管理に戻して自動解放 C側の所有権をARCに移譲(参照カウントは変化しない) ARCがスコープ終了時に自動的に-1して解放(参照カウント=0) // Cの構造体でリソースハンドルを管理 typedef struct { uint64_t vertexBufferHandle; uint64_t indexBufferHandle; // その他のメンバー... } RenderResource; // Metalリソースの作成 void setupRenderingResources (RenderResource *resource) { // Metalバッファを作成(ARCで管理) id <MTLBuffer> vertexBuffer = [device newBufferWithBytes:vertices length:vertexDataSize options:MTLResourceStorageModeShared]; // CFBridgingRetainでCの構造体にリソースを保存 resource->vertexBufferHandle = ( uint64_t ) CFBridgingRetain (vertexBuffer); } // Objective-C++側でMetalリソースを使用 void drawFrame (RenderResource *resource) { // __bridgeでハンドルをMetalオブジェクトに戻す(所有権は移さない) id <MTLBuffer> buffer = ( __bridge id <MTLBuffer>)( void *)resource->vertexBufferHandle; [currentEncoder setVertexBuffer:buffer offset: 0 atIndex: 0 ]; // 描画処理... } // リソースのクリーンアップ void cleanupRenderingResources (RenderResource *resource) { // __bridge_transferで手動管理からARCに所有権を戻す id <MTLBuffer> buffer = ( __bridge_transfer id <MTLBuffer>)( void *)resource->vertexBufferHandle; // bufferはここでスコープを抜けてARCによって自動的に解放される resource->vertexBufferHandle = 0 ; } 座標系の違い OpenGLESとMetalではNDC(正規化デバイス座標)のZ座標の範囲が異なるため、同じ投影行列を使用する場合は注意が必要です。もともとOpenGLESの座標系に従った行列が渡されるため、Metalでは以下のように頂点シェーダーでZ軸の変換をする処理を加えました。 GLSL // 頂点シェーダー(GLSL) layout ( location = 0 ) in vec3 position; uniform mat4 projection; uniform mat4 view; uniform mat4 model; void main() { gl_Position = projection * view * model * vec4 (position, 1.0 ); // OpenGLはそのままNDC座標を使用 } MSL // 頂点シェーダー(MSL) vertex float4 foot_vertex (float3 position [[ attribute ( 0 )]], constant float4x4 & view [[ buffer ( 1 )]], constant float4x4 &projection [[ buffer ( 2 )]], constant float4x4 &model [[ buffer ( 3 )]]) { float4 pos = float4 (position, 1.0 ); float4 clipPos = projection * view * model * pos; // OpenGLのNDC Z座標 [-1,1] をMetalの [0,1] に変換 float newZ = (clipPos.z * 0.5 ) + 0.5 ; return float4 (clipPos.xy, newZ, clipPos.w); } まとめ 本記事ではZOZOMAT RendererのOpenGL ESからMetalへの移行について、既存のクロスプラットフォーム設計を維持するための抽象化アプローチやその際の注意点を解説しました。 また現在の実装では、GLSLとMSLのシェーダーが二重管理となっています。そのため、将来的には SPIRV-Cross のようなシェーダー変換ツールの導入を検討しています。SPIRV-Crossを使用することで、単一のシェーダーソースからOpenGL(GLSL)とMetal(MSL)両方のシェーダーを自動生成できるようになり、シェーダーの一元管理が可能になります。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
動画
該当するコンテンツが見つかりませんでした










