mcpField
A
axiom-metal-migration

axiom-metal-migration

github.com/charleswiltgen開発1.2k169 回インストール今日更新
リポジトリ

axiom-metal-migration について

OpenGL、OpenGL ES、DirectX の描画を Apple プラットフォームの Metal に移行する開発者向けガイドです。変換レイヤーとネイティブ書き換えの選択、段階的な移行、シェーダーや状態、座標系の違いへの対処を扱います。

主な機能

  1. 01変換レイヤーとネイティブ Metal の比較
  2. 02段階的な移行計画
  3. 03GLSL と MSL の概念対応
  4. 04状態と座標系の違いへの対処
  5. 05検証ツールと GPU デバッグの活用

活用シーン

  1. 01Metal への移行方式の評価
  2. 02段階的なレンダラー書き換えの計画
  3. 03GLSL シェーダーの MSL 変換
  4. 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)への変換
GLSLMSL備考
attribute / varying[[stage_in]] 構造体頂点属性は構造体を介して渡します
uniform[[buffer(N)]] パラメーターバインディングインデックスを明示します
gl_Position頂点関数からfloat4を返す頂点関数の戻り値
gl_FragColorフラグメント関数からfloat4を返すフラグメント関数の戻り値
texture2D(tex, uv)tex.sample(sampler, uv)サンプラーオブジェクトは別に指定します
vec2/3/4float2/3/4型名が異なります
mat4float4x4行列型が異なります
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を使用します
アーキテクチャの主な違い
概念OpenGLMetal
状態モデル暗黙的、変更可能明示的、不変の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 移行パターン

コメント