WWDC Quick Look 💓 By SwiftGGTeam
Meet RealityKit Trace

Meet RealityKit Trace

元の動画を見る

ハイライト

Instruments 15 は RealityKit Trace テンプレートを導入し、フレーム単位のレンダリング分析、ボトルネック検出、システム電力影響の測定を提供し、開発者が visionOS 上の空間コンピューティングアプリを最適化できるようにします。

主要内容

visionOS アプリはシミュレーターではスムーズに動くのに、実機ではカクつきます。原因は GPU、CPU、オフスクリーンレンダリング、または SwiftUI ビュー body での過剰な処理かもしれません。従来のプラットフォームでは、たまに数フレーム落ちても気づかれないことがあります。visionOS ではレンダリング遅延が直接視覚的不連続に対応し、体験の低下から不快感まで引き起こします。

RealityKit Trace は visionOS のレンダリングパイプライン向けに設計された Instruments 15 の新テンプレートです。アプリプロセス、レンダーサーバー、コンポジターの 3 コンポーネントをカバーする専用ツールセットです。

レンダリングパイプラインアーキテクチャ

00:49)1 フレームはコードから画面まで 3 段階を経ます。

  1. アプリプロセス:SwiftUI/RealityKit コードがここで動作し、描画コマンドを送信
  2. レンダーサーバー:GPU リソースを一元管理し実際のレンダリングを実行するシステムデーモン
  3. コンポジター:レンダリング結果を表示バッファに合成し、頭部運動に対するタイムワープを適用

Shared Space では複数アプリがレンダーサーバーとコンポジターを共有します。他アプリのレンダリングがフレームレートに影響します。Full Space では自分のアプリのみがレンダリングし、他アプリの影響を受けません。

01:52)テスト戦略:パフォーマンス調査や消費電力分析では、まず隔離環境(Full Space)でテスト。他アプリとの共存体験の検証では Shared Space でテストします。

RealityKit Frames Instrument

04:13)この instrument はレンダリング時間をフレーム単位で追跡します。各フレームは色分けされます。緑は締切に余裕、オレンジはぎりぎり、赤は締切超過(ドロップフレーム)。目標フレームレートは 90fps ですが、コンテンツと環境に応じてシステムが動的に調整します。

05:49)RealityKit Metrics instrument はトップレベルで検出されたボトルネックを直接表示します。これらはレンダリングパイプライン全体の総合タイミング情報から生成されます。赤フレームと同時に現れるボトルネックを優先してください。

実践事例:Hello World アプリの最適化

講演者は Hello World アプリで完全な最適化フローをデモしました。起動画面(SwiftUI)、軌道物体リスト(3D モデル)、没入型地球軌道体験を含みます。

事例 1:SwiftUI ビューのオフスクリーンレンダリング

07:48)起動画面に多くのドロップフレームがありました。RealityKit Metrics は Core Animation Encoding をボトルネックと特定。Core Animation トラックを展開すると、offscreen prepares が推奨閾値を大幅に超え、平均 180 回でした。

10:50)SwiftUI コードを確認すると、ボタンに .shadow(radius: 10) がありました。シャドウは特に高コストで、透明度と組み合わせるとさらに。シャドウを無効にするとオフスクリーンレンダリングが 4 分の 1 に減り、ドロップフレームはほぼ消えました。

オフスクリーンレンダリングを引き起こす主な操作は 4 つ:shadows、masking、rounded rectangles、visual effects です。

事例 2:3D モデルのポリゴン数過多

11:41)軌道物体ビューに多くの GPU Work Stalls ボトルネックがありました。3D Render トラックを展開すると、三角形と頂点数が推奨閾値を大幅に超えていました。低ポリモデルに切り替えると全フレームが締切内に完了し、3D レンダリング統計が大幅に減少、視覚品質の明らかな損失はありませんでした。

事例 3:CPU 過負荷による消費電力急上昇

14:06)地球をズームすると操作がカクつきました。System Power Impact が長時間高位に。Time Profiler は CPU 平均 100%、最重スタックは Entity.makeModelEntity.generateCollisionShapes でした。

16:34)根本原因:makeGlobe() が SwiftUI ビュー body で呼ばれていました。状態変化のたびにモデル再読み込みと衝突形状再生成 — 高コスト操作です。修正:ViewModel で再利用可能な Earth エンティティを作成し、ビュー body でキャッシュ済みエンティティを直接使用。

修正後、消費電力は nominal に戻り、CPU は 100% から 10% に低下しました。

詳細

高オフスクリーンレンダリングの SwiftUI ビュー

private struct Item: View {
    var module: Module
    let cornerRadius = 20.0

    var body: some View {
        NavigationLink(value: module) {
            VStack(alignment: .leading, spacing: 3) {
                Text(module.eyebrow)
                    .font(.titleHeading)
                    .foregroundStyle(.secondary)
                VStack(alignment: .leading, spacing: 7) {
                    Text(module.heading)
                        .font(.largeTitle)
                    Text(module.abstract)
                }
            }
            .padding(.horizontal, 5)
            .padding(.vertical, 20)
        }
        .buttonStyle(.bordered)
        .shadow(radius: 10)  // オフスクリーンレンダリングを発生させる
        .buttonBorderShape(.roundedRectangle(radius: cornerRadius))
        .frame(minWidth: 150, maxWidth: 280)
    }
}

キーポイント:

  • .shadow(radius: 10).buttonStyle(.bordered) の組み合わせがオフスクリーンレンダリングを引き起こします
  • visionOS では空間アプリが継続レンダリングするため、オフスクリーンレンダリングは毎フレーム発生します
  • 静的 UI はシステム目標フレームレートを維持できるほど効率的である必要があります
  • 最適化:不要なシャドウを削除、またはリアルタイムシャドウの代わりにプリレンダーテクスチャを使用

エンティティファクトリーパターン

class EarthEntity: Entity {
    static func makeGlobe() -> EarthEntity {
        EarthEntity(earthModel: Entity.makeModel(
            name: "Earth",
            filename: "Globe",
            radius: 0.35,
            color: .blue)
        )
    }

    static func makeCloudyEarth() -> EarthEntity {
        let earthModel = Entity()
        earthModel.name = "Earth"

        Task {
            if let scene = await loadFromRealityComposerPro(
                named: WorldAssets.rootNodeName,
                fromSceneNamed: WorldAssets.sceneName
            ) {
                earthModel.addChild(scene)
            } else {
                fatalError("Unable to load earth model")
            }
        }

        return EarthEntity(earthModel: earthModel)
    }
}

キーポイント:

  • makeGlobe() は手続き型モデルを使用 — 作成コスト低、非同期読み込みなし
  • makeCloudyEarth() は Reality Composer Pro から非同期読み込み — 数フレーム遅延の可能性
  • ビュー body で makeGlobe() を呼ぶと状態変化のたびにモデル再作成
  • パフォーマンス推奨:ViewModel でエンティティをキャッシュ、body で高コスト操作を避ける

最適化された ViewModel

class ViewModel: ObservableObject {
    // 再利用可能な地球エンティティをキャッシュし、ビューの body 内での重複作成を避ける
    @Published var globeEarthEntity: EarthEntity = .makeGlobe()
    @Published var orbitEarthEntity: EarthEntity = .makeGlobe()

    @Published var isShowingGlobe: Bool = false
    @Published var isShowingOrbit: Bool = false
    @Published var orbitImmersionStyle: ImmersionStyle = .mixed

    // Orbit 設定
    @Published var orbitEarth: EarthEntity.Configuration = .orbitEarthDefault
    @Published var orbitSatellite: SatelliteEntity.Configuration = .orbitSatelliteDefault
    @Published var orbitMoon: SatelliteEntity.Configuration = .orbitMoonDefault
    @Published var orbitSunAngle: Angle = .degrees(150)
}

キーポイント:

  • エンティティは ViewModel 初期化時に 1 回作成され、その後再利用されます
  • @Published プロパティ変化はビュー更新をトリガーしますが、エンティティ自体は再読み込みされません
  • 設定オブジェクト(EarthEntity.Configuration)は軽量で頻繁な更新に適しています
  • 「エンティティ」(重)と「設定」(軽)の分離は RealityKit アプリの重要なアーキテクチャパターンです

Orbit ビュー

struct Orbit: View {
    @EnvironmentObject private var model: ViewModel

    var body: some View {
        Earth(
            world: model.orbitEarthEntity,  // キャッシュ済みエンティティを使用
            earthConfiguration: model.orbitEarth,
            satelliteConfiguration: [model.orbitSatellite],
            moonConfiguration: model.orbitMoon,
            showSun: true,
            sunAngle: model.orbitSunAngle,
            animateUpdates: true
        )
        .place(
            initialPosition: Point3D([475, -1200.0, -1200.0]),
            scale: $model.orbitEarth.scale)
    }
}

キーポイント:

  • world: model.orbitEarthEntity は ViewModel のキャッシュ済みエンティティを使用
  • animateUpdates: true で状態変化のアニメーション遷移を有効化
  • .place() で 3D 空間位置をメートル単位で設定
  • ビュー body では軽量な設定の受け渡しのみ、モデル読み込みは行わない

重要ポイント

1. パフォーマンスベンチマークテストを確立する

アプリの主要シーン(起動、メインインタラクション、複雑シーン読み込み)で RealityKit Trace を使いパフォーマンストレースを記録し、フレームレート、GPU 時間、消費電力のベースラインを確立します。コード変更のたびにベースラインと比較し、早期にリグレッションを検出します。入口:Instruments 15 の RealityKit Trace テンプレート + 実機テスト。

2. SwiftUI ビューのオフスクリーンレンダリングを最適化する

アプリ内の shadow、mask、rounded rectangle、visual effect の使用箇所を確認します。visionOS ではこれらの効果が毎フレームオフスクリーンレンダリングを引き起こし、従来プラットフォームよりコストが高くなります。入口:RealityKit Metrics の Core Animation トラックの “Offscreens” 指標。

3. 高コスト操作を SwiftUI ビュー body から移す

モデル読み込み、衝突形状生成、テクスチャデコードなどはビュー body や @Published プロパティの getter に絶対に置かないでください。ViewModel 初期化時またはバックグラウンドの非同期タスクで完了させます。入口:Time Profiler でビュー body のコールスタックを確認し、Entity.makeModelgenerateCollisionShapes などの呼び出しを探す。

4. 3D アセットを簡素化する

Reality Composer Pro で三角形数、頂点数、描画呼び出しを確認します。推奨閾値を超える場合はジオメトリを簡素化するか、インスタンシングで同一メッシュを再利用します。入口:Reality Composer Pro の統計パネル + RealityKit Metrics の 3D Render トラック。

関連セッション

コメント

GitHub Issues · utterances