axiom-metal-migration について
OpenGL、OpenGL ES、DirectX の描画を Apple プラットフォームの Metal に移行する開発者向けガイドです。変換レイヤーとネイティブ書き換えの選択、段階的な移行、シェーダーや状態、座標系の違いへの対処を扱います。
主な機能
- 01変換レイヤーとネイティブ Metal の比較
- 02段階的な移行計画
- 03GLSL と MSL の概念対応
- 04状態と座標系の違いへの対処
- 05検証ツールと GPU デバッグの活用
活用シーン
- 01Metal への移行方式の評価
- 02段階的なレンダラー書き換えの計画
- 03GLSL シェーダーの MSL 変換
- 04描画や同期の問題の調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-metal-migrationインストール後、Claude Code で/axiom-metal-migrationを入力して読み込まれたか確認します。
詳細
Metal への移行
Apple プラットフォームで OpenGL/OpenGL ES または DirectX のコードを Metal に移植します。
このスキルを使う場面
次の場合にこのスキルを使用してください:
- OpenGL/OpenGL ES のコードベースを iOS/macOS に移植する
- DirectX のコードベースを Apple プラットフォームに移植する
- 翻訳レイヤー(MetalANGLE)とネイティブでの書き直しのどちらにするか決める
- 段階的な移行戦略を立てる
- 労力とパフォーマンスのトレードオフを評価する
注意すべき兆候
❌ 「MetalANGLEを使って、そのままリリースすればいい」— 翻訳レイヤーには10~30%のオーバーヘッドが加わります。デモには適していますが、本番環境には向きません。
❌ 「計画なしにシェーダーを1つずつ変換する」— 状態管理の仕組みは根本的に異なるため、二度書き直すことになります。
❌ 「GLのステートマシンのメンタルモデルを維持する」— Metalは明示的な設計です。GLの考え方を続けると、見つけにくいバグが発生します。
❌ 「すべてを一度に移植する」— 段階的な移行なら問題を早期に発見できます。一括移行では、複合的に悪化するバグが見えにくくなります。
❌ 「開発中に検証レイヤーを無効にする」— Metalの検証機能は、明確なメッセージで移植時のバグの80%を検出します。
❌ 「座標系の問題は後で考える」— Y軸の反転やNDCの違いが、デバッグに最も時間を要する原因になります。
❌ 「パフォーマンスは自動的に同等以上になる」— Metalでは明示的な最適化が必要です。単純な移植では、かえって遅くなる場合があります。
移行戦略の判断フローチャート
Starting a port to Metal?
│
├─ Need working demo in <1 week?
│ ├─ OpenGL ES source? → MetalANGLE (translation layer)
│ │ └─ Caveats: 10-30% overhead, ES 2/3 only, no compute
│ │
│ └─ Vulkan available? → MoltenVK
│ └─ Caveats: Vulkan complexity, indirect translation
│
├─ Production app with performance requirements?
│ └─ Native Metal rewrite (recommended)
│ ├─ Phased: Keep GL for reference, port module-by-module
│ └─ Full: Clean rewrite using Metal idioms from start
│
├─ DirectX/HLSL source?
│ └─ Metal Shader Converter (Apple tool)
│ └─ Converts DXIL bytecode → Metal library
│ └─ See metal-migration-ref for usage
│
└─ Hybrid approach?
└─ MetalANGLE for demo → Native Metal incrementally
└─ Best of both: fast validation, optimal end state
パターン1:翻訳レイヤー(デモを素早く作る場合)
使用場面:実現可能性の検証、関係者の賛同獲得、プロトタイプの作成
MetalANGLEのセットアップ(OpenGL ES → Metal)
// 1. Add MetalANGLE via SPM or CocoaPods
// GitHub: nicklockwood/MetalANGLE
// 2. Replace EAGLContext with MGLContext
import MetalANGLE
let context = MGLContext(api: kMGLRenderingAPIOpenGLES3)
MGLContext.setCurrent(context)
// 3. Replace GLKView with MGLKView
let glView = MGLKView(frame: bounds, context: context)
glView.delegate = self
glView.drawableDepthFormat = .format24
// 4. Existing GL code works unchanged
glClearColor(0, 0, 0, 1)
glClear(GL_COLOR_BUFFER_BIT)
// ... your existing GL rendering code
トレードオフの一覧
| 項目 | MetalANGLE | ネイティブMetal |
|---|---|---|
| デモまでの期間 | 数時間 | 数日~数週間 |
| 実行時のオーバーヘッド | 10~30% | 基準値 |
| シェーダーの変更 | なし | 全面的な書き直し |
| コンピュートシェーダー | 非対応 | 全面対応 |
| 将来への対応 | 翻訳による負債 | Apple推奨 |
| デバッグ | GLツールのみ | GPUフレームキャプチャ |
| 熱/バッテリー | 大きい | 最適化可能 |
MetalANGLEが使えない場合
次のコードでは、MetalANGLEは動作しません。
- コアES 2/3に含まれないOpenGL ES拡張機能を使用している
- コンピュートシェーダー(GL_COMPUTE_SHADER)に依存している
- GLステートマシンの厳密なセマンティクスが必要である
- ネイティブ版から10%以内のパフォーマンスが必要である
- visionOSを対象としている(翻訳レイヤーに非対応)
パターン2:ネイティブMetalへの書き直し(本番環境向け)
使用場面:本番アプリ、パフォーマンスが重要なレンダリング、長期的なメンテナンス
段階的な移行戦略
Phase 1: Abstraction Layer (1-2 weeks)
├─ Create renderer interface hiding GL/Metal specifics
├─ Keep GL implementation as reference
├─ Define clear boundaries: setup, resources, draw, present
└─ Validate abstraction with existing tests
Phase 2: Metal Backend (2-4 weeks)
├─ Implement Metal renderer behind same interface
├─ Convert shaders GLSL → MSL (use metal-migration-ref)
├─ Run GL and Metal side-by-side for visual diff
├─ GPU Frame Capture for debugging
└─ Milestone: Feature parity, visual match
Phase 3: Optimization (1-2 weeks)
├─ Remove abstraction overhead where it hurts
├─ Use Metal-specific features (argument buffers, indirect)
├─ Profile with Metal System Trace
├─ Tune for thermal envelope and battery
└─ Remove GL backend entirely
GLSLからMetal Shading Language(MSL)への変換
| GLSL | MSL | 備考 |
|---|---|---|
attribute / varying | [[stage_in]] 構造体 | 頂点属性は構造体を介して渡します |
uniform | [[buffer(N)]] パラメーター | バインディングインデックスを明示します |
gl_Position | 頂点関数からfloat4を返す | 頂点関数の戻り値 |
gl_FragColor | フラグメント関数からfloat4を返す | フラグメント関数の戻り値 |
texture2D(tex, uv) | tex.sample(sampler, uv) | サンプラーオブジェクトは別に指定します |
vec2/3/4 | float2/3/4 | 型名が異なります |
mat4 | float4x4 | 行列型が異なります |
mix() | mix() | 名前は同じです |
precision mediump float | (不要) | Metalが精度を推定します |
#version 300 es | #include <metal_stdlib> | プリアンブルが異なります |
変換例:
// GLSL vertex shader
#version 300 es
uniform mat4 u_mvp;
in vec3 a_position;
in vec2 a_texCoord;
out vec2 v_texCoord;
void main() {
v_texCoord = a_texCoord;
gl_Position = u_mvp * vec4(a_position, 1.0);
}
// Equivalent MSL vertex shader
#include <metal_stdlib>
using namespace metal;
struct VertexIn {
float3 position [[attribute(0)]];
float2 texCoord [[attribute(1)]];
};
struct VertexOut {
float4 position [[position]];
float2 texCoord;
};
struct Uniforms {
float4x4 mvp;
};
vertex VertexOut vertexShader(VertexIn in [[stage_in]],
constant Uniforms &uniforms [[buffer(1)]]) {
VertexOut out;
out.texCoord = in.texCoord;
out.position = uniforms.mvp * float4(in.position, 1.0);
return out;
}
注意すべき主な違い:
- GLSLのグローバル変数 →
[[attribute]]修飾子を付けたMSLの関数パラメーター - 暗黙のuniformバインディング → 明示的な
[[buffer(N)]]インデックス sampler2Dはテクスチャとサンプラーをまとめます → Metalではtexture2dとsamplerを分けます- GLSLのプリプロセッサ → MetalではC++の
#includeとusing namespace metalを使用します
アーキテクチャの主な違い
| 概念 | OpenGL | Metal |
|---|---|---|
| 状態モデル | 暗黙的、変更可能 | 明示的、不変のPSO |
| 検証 | 描画時 | PSO作成時 |
| シェーダーのコンパイル | 実行時(JIT) | ビルド時(AOT) |
| コマンド送信 | 暗黙的 | 明示的なコマンドバッファー |
| リソースのバインディング | グローバル状態 | エンコーダーごとのバインディング |
| 同期 | ドライバーが管理 | アプリが管理 |
MTKViewのセットアップ(ネイティブMetal)
import MetalKit
class MetalRenderer: NSObject, MTKViewDelegate {
let device: MTLDevice
let commandQueue: MTLCommandQueue
var pipelineState: MTLRenderPipelineState!
init?(metalView: MTKView) {
guard let device = MTLCreateSystemDefaultDevice(),
let queue = device.makeCommandQueue() else {
return nil
}
self.device = device
self.commandQueue = queue
metalView.device = device
metalView.clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 1)
metalView.depthStencilPixelFormat = .depth32Float
super.init()
metalView.delegate = self
buildPipeline(metalView: metalView)
}
private func buildPipeline(metalView: MTKView) {
let library = device.makeDefaultLibrary()!
let vertexFunction = library.makeFunction(name: "vertexShader")
let fragmentFunction = library.makeFunction(name: "fragmentShader")
let descriptor = MTLRenderPipelineDescriptor()
descriptor.vertexFunction = vertexFunction
descriptor.fragmentFunction = fragmentFunction
descriptor.colorAttachments[0].pixelFormat = metalView.colorPixelFormat
descriptor.depthAttachmentPixelFormat = metalView.depthStencilPixelFormat
// Pre-validated at creation, not at draw time
pipelineState = try! device.makeRenderPipelineState(descriptor: descriptor)
}
func draw(in view: MTKView) {
guard let drawable = view.currentDrawable,
let descriptor = view.currentRenderPassDescriptor,
let commandBuffer = commandQueue.makeCommandBuffer(),
let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else {
return
}
encoder.setRenderPipelineState(pipelineState)
// Bind resources explicitly - nothing persists between draws
encoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
encoder.setFragmentTexture(texture, index: 0)
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertexCount)
encoder.endEncoding()
commandBuffer.present(drawable)
commandBuffer.commit()
}
}
よくある移行時のアンチパターン
アンチパターン 1:GL のステートマシンの考え方を引きずる
❌ 悪い例 — GL の暗黙的な状態を前提に考える:
// GL mental model: "set state, then draw"
glBindTexture(GL_TEXTURE_2D, texture)
glBindBuffer(GL_ARRAY_BUFFER, vbo)
glUseProgram(program)
glDrawArrays(GL_TRIANGLES, 0, vertexCount)
// State persists until changed — can draw again without rebinding
✅ 良い例 — Metal の明示的なモデル:
// Metal: encode everything explicitly per draw
let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: rpd)!
encoder.setRenderPipelineState(pipelineState) // Always set
encoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) // Always bind
encoder.setFragmentTexture(texture, index: 0) // Always bind
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: count)
encoder.endEncoding()
// Nothing persists — next encoder starts fresh
所要時間:「テクスチャが消えたのはなぜか」のデバッグに 30~60 分かかるのに対し、事前にモデルを理解すれば 2 分です。
アンチパターン 2:座標系の違いを無視する
❌ 悪い例 — GL の座標が Metal でも使えると思い込む:
OpenGL:
- Origin: bottom-left
- Y-axis: up
- NDC Z range: [-1, 1]
- Texture origin: bottom-left
Metal:
- Origin: top-left
- Y-axis: down
- NDC Z range: [0, 1]
- Texture origin: top-left
✅ 良い例 — 座標を明示的に処理する:
// Option 1: Flip Y in vertex shader
vertex float4 vertexShader(VertexIn in [[stage_in]]) {
float4 pos = uniforms.mvp * float4(in.position, 1.0);
pos.y = -pos.y; // Flip Y for Metal's coordinate system
return pos;
}
// Option 2: Flip texture coordinates in fragment shader
fragment float4 fragmentShader(VertexOut in [[stage_in]],
texture2d<float> tex [[texture(0)]],
sampler samp [[sampler(0)]]) {
float2 uv = in.texCoord;
uv.y = 1.0 - uv.y; // Flip V for Metal's texture origin
return tex.sample(samp, uv);
}
// Option 3: Use MTKTextureLoader with origin option
let options: [MTKTextureLoader.Option: Any] = [
.origin: MTKTextureLoader.Origin.bottomLeft // Match GL convention
]
let texture = try textureLoader.newTexture(URL: url, options: options)
所要時間:「上下逆」や「鏡像」のレンダリングのデバッグに 2~4 時間かかるのに対し、このパターンを読むのに 5 分です。
アンチパターン 3:開発中に検証レイヤーを使わない
❌ 悪い例 —「パフォーマンス」のために検証を無効にする:
// No validation — API misuse silently corrupts or crashes later
✅ 良い例 — 開発中は常に有効にする:
In Xcode: Edit Scheme → Run → Diagnostics
✓ Metal API Validation
✓ Metal Shader Validation
✓ GPU Frame Capture (Metal)
所要時間:気づかないままデータが破損する問題のデバッグに何時間もかかるのに対し、検証レイヤーならコールスタック付きのエラーメッセージがすぐに表示されます。
アンチパターン 4:同期なしで単一のバッファーを使う
❌ 悪い例 — CPU と GPU が同じバッファーを取り合う:
// Frame N: CPU writes to buffer
// Frame N: GPU reads from buffer
// Frame N+1: CPU writes again — RACE CONDITION
buffer.contents().copyMemory(from: data, byteCount: size)
✅ 良い例 — セマフォを使ったトリプルバッファリング:
class TripleBufferedRenderer {
let inflightSemaphore = DispatchSemaphore(value: 3)
var buffers: [MTLBuffer] = []
var bufferIndex = 0
func draw(in view: MTKView) {
// Wait for a buffer to become available
inflightSemaphore.wait()
let buffer = buffers[bufferIndex]
// Safe to write — GPU finished with this buffer
buffer.contents().copyMemory(from: data, byteCount: size)
let commandBuffer = commandQueue.makeCommandBuffer()!
commandBuffer.addCompletedHandler { [weak self] _ in
self?.inflightSemaphore.signal() // Release buffer
}
// ... encode and commit
bufferIndex = (bufferIndex + 1) % 3
}
}
所要時間:断続的な表示の乱れのデバッグに何時間もかかるのに対し、トリプルバッファリングの実装は 15 分です。
プレッシャーのかかる状況
シナリオ 1:「とにかく MetalANGLE で出荷しよう」
状況:締め切りまで 2 週間です。MetalANGLE のデモは動作しています。PM は出荷しようと言っています。
プレッシャー:「あとで最適化できます。ユーザーは 20% のオーバーヘッドに気づきません。」
うまくいかない理由:
- 複雑なシーン(ビジュアライザー、ゲーム)では変換のオーバーヘッドが積み重なります
- コンピュートシェーダーに対応していないため、今後追加できる機能が限られます
- 技術的負債が増えます。チームは Metal ではなく、MetalANGLE 固有の癖を覚えることになります
- Apple による非推奨化のリスクがあります(OpenGL ES は iOS 12 以降、非推奨です)
- ユーザーからバッテリーや熱に関する苦情が寄せられます
回答テンプレート:
「デモのマイルストーンには MetalANGLE が適しています。本番向けには、レンダーループをネイティブ Metal で実装するために 3 週間の余裕を確保することをおすすめします。これにより 20~30% のオーバーヘッドを解消し、非推奨化のリスクもなくせます。ネイティブ Metal で締め切りに間に合わせるため、MVP のビジュアルエフェクトを減らすことはできますか?」
シナリオ 2:「今スプリント中にすべてのシェーダーを移植しよう」
状況:GLSL シェーダーが 50 個あります。スプリントは 2 週間です。マネージャーはすべての変換を求めています。
プレッシャー:「ただのテキストファイルでしょう。シェーダーの変換がどれほど難しいというのですか?」
うまくいかない理由:
- GLSL から MSL への変換は 1 対 1 ではありません(精度修飾子、組み込み関数、サンプリング)
- 各シェーダーにはコンパイルだけでなく、見た目の検証も必要です
- 複雑なシェーダーにはパフォーマンスのプロファイリングが必要です
- バグが重なり、壊れたシェーダー A が壊れたシェーダー B を隠してしまいます
回答テンプレート:
「シェーダーの変換にはコンパイルだけでなく、見た目の検証も必要です。確実に進められるのは週に 10~15 個です。50 個のシェーダーについては、(1)使用状況に応じて優先順位を付け、使用頻度の高い 10 個から変換する、(2)型変換や定型コードなどのマッピングを自動化する、(3)GL と Metal を並行して動かし、検証する、という方法が考えられます。品質を確保した全面的な変換には、現実的には 4~5 週間かかります。」
シナリオ 3:「GPU フレームキャプチャは必要ない」
状況:開発者が「シェーダーのデバッグには print 文を使えばいい」と言っています。
プレッシャー:「GPU ツールは大げさです。何をしているかは分かっています。」
うまくいかない理由:
- シェーダーでは print 文が使えません
- 見た目の不具合を調べるには、中間レンダーターゲットを確認する必要があります
- パフォーマンスの問題を調べるには、GPU のタイムライン分析が必要です
- Metal の検証エラーには、コールスタックのコンテキストが必要です
回答テンプレート:
「GPU フレームキャプチャは、シェーダー変数の確認、中間テクスチャの表示、GPU のタイミングの把握を行う唯一の方法です。フレームのキャプチャにかかる時間は 30 秒です。これがなければ、推測に頼ることになり、シェーダーのデバッグは 10 倍遅くなります。」
移行前チェックリスト
移植を開始する前に:
- シェーダーの一覧を作成する:GLSL/HLSL ファイルの数と複雑さ(LOC、使用機能)を確認します
- 拡張機能を特定する:コードで使用している GL 拡張機能と、Metal に相当するものを確認します
- 状態管理を監査する:レンダラーがどの程度ステートフルか、グローバル状態の数を確認します
- コンピュートの使用状況を確認する:GL コンピュートシェーダーや GPGPU を使用しているか確認します(MetalANGLE では対応できません)
- ベースラインをプロファイリングする:基準プラットフォームでの FPS、フレーム時間、メモリ、温度を測定します
- 成功基準を定義する:目標 FPS、メモリの予算、許容温度範囲を決めます
- A/B テストを設定する:検証のために GL と Metal を並行して実行できるか確認します
- 検証を有効にする:Metal API Validation、Shader Validation、Frame Capture を有効にします
移行後チェックリスト
移植の完了後:
- 視覚的な同等性:並べて比較したスクリーンショットが基準と一致すること
- 同等以上のパフォーマンス:フレーム時間 ≤ GL のベースライン
- 検証エラーがないこと:Metal 検証を有効にしてエラーなく実行できること
- 温度が許容範囲内であること:通常の使用中にデバイスのパフォーマンスが制限されないこと
- メモリが安定していること:長時間使用してもメモリリークがないこと
- すべてのコードパスをテストする:エッジケース、エラー状態、サイズ変更/回転を確認します
リソース
WWDC:2016-00602、2018-00604、2019-00611
ドキュメント:/metal/migrating-opengl-code-to-metal、/metal/shader-converter
ツール:MetalANGLE、MoltenVK
スキル:axiom-metal-migration-ref、axiom-metal-migration-diag
最終更新日:2025-12-29 プラットフォーム:iOS 12+、macOS 10.14+、tvOS 12+ ステータス:本番環境で使用可能な Metal 移行パターン