この記事の対象読者
- グラフィックスプログラミングに興味があるが、GPUアーキテクチャの詳細は初めての方
- Apple Silicon(M1/M2/M3/M4)搭載Macで開発している方
- iOS/macOSアプリのパフォーマンス最適化に取り組みたい方
- Metalを使った開発を始めようとしている方
この記事で得られること
- GPUアーキテクチャの基礎理解: IMR(即時モードレンダリング)とTBDR(タイルベース遅延レンダリング)の違いを図解で理解
- TBDRの仕組みの完全把握: タイリング、HSR(隠面除去)、Tile Memoryの動作原理を具体例で習得
- 実践的な最適化知識: Metalを使ったTBDR最適化のベストプラクティスとサンプルコード
この記事で扱わないこと
- Metal Shading Languageの文法詳細(別記事で解説予定)
- レイトレーシングやメッシュシェーダーの詳細
- Windows/Linux環境でのGPUプログラミング
1. TBDRとの出会い
「なんでMacBookはバッテリー持ちがいいのに、ゲームもヌルヌル動くんだ?」
Apple Silicon搭載のMacを初めて使ったとき、私はこの疑問がずっと頭から離れませんでした。
デスクトップGPU(GeForceやRadeon)は、高性能を出すために大量の電力を消費し、巨大なヒートシンクとファンで冷却しています。一方、M1チップはファンレスのMacBook Airでも、Final Cut Proで4K動画編集ができる。この「魔法」の正体を知りたくて、GPUアーキテクチャを調べ始めました。
答えは**TBDR(Tile-Based Deferred Rendering、タイルベース遅延レンダリング)**でした。
TBDRを一言で表すなら、「必要なピクセルだけを、必要なときに、チップ内の高速メモリで処理する」アーキテクチャです。料理に例えるなら、IMR(従来方式)が「材料を全部テーブルに広げてから調理する」のに対し、TBDRは「必要な材料だけを手元に持ってきて、一品ずつ完成させる」やり方。無駄がないから省電力で、しかも速い。
ここまでで、TBDRがどんなものか、なんとなくイメージできたでしょうか。次は、この技術が生まれた背景を見ていきましょう。
2. 前提知識の確認
本題に入る前に、この記事で使う用語を整理しておきます。
2.1 GPUとは
GPU(Graphics Processing Unit、グラフィックス処理装置)は、画像処理に特化したプロセッサです。CPUが「何でも屋」なら、GPUは「絵を描くことに全振りした職人集団」。数千〜数万の小さなコアが並列で動き、大量のピクセルを同時に処理します。
2.2 レンダリングパイプラインとは
3Dグラフィックスを画面に表示するまでの処理の流れをレンダリングパイプラインと呼びます。大まかに以下のステップで構成されます。
- 頂点処理(Vertex Processing): 3Dモデルの頂点座標を変換
- ラスタライズ(Rasterization): 三角形を画面上のピクセルに変換
- フラグメント処理(Fragment Processing): 各ピクセルの色を計算
- 出力(Output): フレームバッファに書き込み
2.3 オーバードローとは
**オーバードロー(Overdraw)**とは、最終的に見えないピクセルまで描画してしまう無駄な処理のことです。例えば、建物の後ろに隠れた木を先に描いて、後から建物で上書きする場合、木の描画は完全に無駄になります。
2.4 メモリ帯域幅とは
**メモリ帯域幅(Memory Bandwidth)**は、GPUとメモリ間でデータを転送できる速度です。単位はGB/s(ギガバイト毎秒)。GPUの性能は、この帯域幅によって大きく制限されます。デスクトップGPUは300GB/s以上の帯域を持ちますが、モバイルデバイスは数十GB/s程度に制限されています。
これらの用語が押さえられたら、次に進みましょう。
3. TBDRが生まれた背景
3.1 従来のGPUアーキテクチャ(IMR)の問題
デスクトップGPU(NVIDIA GeForce、AMD Radeon)は、**IMR(Immediate Mode Rendering、即時モードレンダリング)**というアーキテクチャを採用しています。
IMRの動作は非常にシンプルです。
三角形A受信 → 頂点処理 → ラスタライズ → フラグメント処理 → メモリ書き込み
三角形B受信 → 頂点処理 → ラスタライズ → フラグメント処理 → メモリ書き込み
三角形C受信 → ...
受け取った三角形を即座に処理し、結果をメモリに書き込みます。
この方式の問題は、メモリアクセスが頻繁に発生することです。
| 処理 | メモリアクセス |
|---|---|
| 深度テスト | 深度バッファを読み込み |
| 深度更新 | 深度バッファに書き込み |
| カラー合成 | カラーバッファを読み込み |
| カラー書き込み | カラーバッファに書き込み |
1ピクセルを処理するたびに、何度もメインメモリにアクセスします。デスクトップGPUは大容量のVRAMと広い帯域幅でこの問題をカバーしていますが、モバイルデバイスではそれが許されません。
さらに深刻なのがオーバードロー問題です。IMRでは三角形を受け取った順に処理するため、後から描画された三角形に隠される三角形も、テクスチャサンプリングやシェーディングを完全に実行してしまいます。
3.2 モバイル時代の要請
2000年代後半、スマートフォンの普及により、新しい要件が生まれました。
- 消費電力: バッテリーで長時間動作する必要がある
- 発熱: ファンレスで冷却しなければならない
- メモリ帯域: CPUとメモリを共有し、帯域が限られる
- コスト: 専用VRAMを搭載できない
これらの制約の中で高品質なグラフィックスを実現するため、Imagination Technologies社がTBDRを開発しました。PowerVR GPUに搭載されたこの技術は、iPhoneのGPUとして採用され、後にApple独自設計のGPUにも継承されています。
背景がわかったところで、抽象的な概念から順に、具体的な仕組みを見ていきましょう。
4. TBDRの基本概念
4.1 タイリング(Tiling):画面を分割する
TBDRの「TB」は**Tile-Based(タイルベース)**を意味します。
画面を小さなタイル(通常32×32ピクセル)に分割し、タイル単位で処理を完結させます。
┌────┬────┬────┬────┬────┬────┐
│Tile│Tile│Tile│Tile│Tile│Tile│
│ 00 │ 01 │ 02 │ 03 │ 04 │ 05 │
├────┼────┼────┼────┼────┼────┤
│Tile│Tile│Tile│Tile│Tile│Tile│
│ 06 │ 07 │ 08 │ 09 │ 10 │ 11 │
├────┼────┼────┼────┼────┼────┤
│Tile│Tile│Tile│Tile│Tile│Tile│
│ 12 │ 13 │ 14 │ 15 │ 16 │ 17 │
└────┴────┴────┴────┴────┴────┘
1920×1080の画面 → 60×34 = 2,040タイル
なぜタイルに分割するのか?答えは**Tile Memory(タイルメモリ)**にあります。
各タイル(32×32ピクセル)のカラーバッファと深度バッファは、**GPU内部の高速メモリ(オンチップメモリ)**に収まるサイズです。
32×32ピクセル × 4バイト(RGBA) = 4KB(カラーバッファ)
32×32ピクセル × 4バイト(深度) = 4KB(深度バッファ)
合計 = 8KB(タイル全体)
Apple GPUのTile Memoryは32KBあるので、複数のレンダーターゲットやMSAA(マルチサンプルアンチエイリアス)も余裕で収まります。
4.2 遅延レンダリング(Deferred Rendering):見えるピクセルだけを処理
TBDRの「DR」は**Deferred Rendering(遅延レンダリング)**を意味します。
TBDRでは、レンダリングを2つのフェーズに分けます。
フェーズ1: タイリング(Tiling Phase)
1. すべての三角形の頂点を処理(頂点シェーダー実行)
2. 各三角形がどのタイルに影響するか計算
3. 「タイルリスト」としてメモリに保存
このフェーズでは、フラグメントシェーダー(ピクセル処理)は実行しません。三角形の情報を整理して、後のフェーズに「先送り(defer)」します。
フェーズ2: レンダリング(Rendering Phase)
各タイルについて:
1. タイルリストから該当する三角形を取得
2. HSR(Hidden Surface Removal)で可視ピクセルを特定
3. 可視ピクセルのみフラグメントシェーダーを実行
4. 結果をTile Memoryに蓄積
5. タイル完成後、メインメモリに一括書き込み
この流れを図で表すと、以下のようになります。
【IMR(従来方式)】
三角形 → [頂点] → [ラスタ] → [フラグメント] → メモリ書き込み
三角形 → [頂点] → [ラスタ] → [フラグメント] → メモリ書き込み
三角形 → [頂点] → [ラスタ] → [フラグメント] → メモリ書き込み
↑ 各三角形で即座にメモリアクセス発生
【TBDR】
全三角形 → [頂点処理] → [タイル分類] → Parameter Buffer
↓
各タイルで処理
↓
[HSR] → [フラグメント] → Tile Memory
↓
タイル完成後に一括書き込み
4.3 HSR(Hidden Surface Removal):オーバードローゼロの実現
TBDRの真骨頂が**HSR(Hidden Surface Removal、隠面除去)**です。
IMRでは、オーバードローを減らすためにEarly-Zという技術を使います。しかしEarly-Zには限界があります。
- 三角形を手前から奥の順に描画しないと効果がない
- アプリケーション側でソートが必要
- 三角形同士が交差する場合は対応できない
TBDRのHSRは、これらの問題をハードウェアレベルで完全に解決します。
【HSRの動作】
1. タイル内のすべての三角形をラスタライズ(頂点データのみ)
2. 各ピクセルについて、最前面の三角形を特定
3. 最前面の三角形のみ、フラグメントシェーダーを実行
重要なポイントは以下です。
- 描画順序に依存しない: 前から描いても後ろから描いても結果は同じ
- ピクセル単位で完璧: 三角形の交差も正しく処理
- 不透明オブジェクトのオーバードローがゼロ
Appleの公式ドキュメントでは、HSRについて以下のように説明されています。
"HSR allows the GPU to minimize overdraw by keeping track of the frontmost visible layer for each pixel. HSR is both pixel perfect and submission order independent."
(HSRは、各ピクセルの最前面の可視レイヤーを追跡することで、GPUがオーバードローを最小化することを可能にします。HSRはピクセル単位で完璧であり、描画順序に依存しません。)
これを具体的な数値で見てみましょう。Imagination Technologies社の資料によると、一般的なゲームシーンでは4倍のオーバードローが発生することがあります。TBDRのHSRは、これをほぼゼロに削減します。
| アーキテクチャ | オーバードロー率 | シェーダー実行回数 |
|---|---|---|
| IMR(ソートなし) | 400% | 4倍 |
| IMR(ソートあり) | 150% | 1.5倍 |
| TBDR(HSR) | 100% | 1倍(最小) |
基本概念が理解できたところで、これらの抽象的な概念を具体的なコードで実装していきましょう。
5. 実際に使ってみよう
5.1 環境構築
TBDRを活用するには、Apple GPUとMetalフレームワークが必要です。
対応環境
| デバイス | GPU | Tile Memory |
|---|---|---|
| iPhone 8以降 | A11以降 | 32KB |
| iPad Pro 2018以降 | A12X以降 | 32KB |
| MacBook Air M1以降 | Apple Silicon | 32KB |
| Mac Studio M1以降 | Apple Silicon | 32KB |
開発環境
# Xcodeのインストール(App Storeまたは以下のコマンド)
xcode-select --install
# Xcodeのバージョン確認(15.0以上推奨)
xcodebuild -version
5.2 設定ファイルの準備
以下の3種類の設定ファイルを用意しています。用途に応じて選択してください。
開発環境用(RenderConfig.development.swift)
// RenderConfig.development.swift - 開発環境用(このままコピーして使える)
// デバッグ情報を有効化し、パフォーマンス計測を行う設定
import Metal
import MetalKit
struct RenderConfig {
// タイルサイズ設定
static let tileWidth: Int = 32
static let tileHeight: Int = 32
// 開発環境ではデバッグを有効化
static let enableGPUCapture: Bool = true
static let enableValidation: Bool = true
// パフォーマンスカウンター有効化
static let enablePerformanceCounters: Bool = true
// レンダーターゲット設定
static let colorPixelFormat: MTLPixelFormat = .bgra8Unorm
static let depthPixelFormat: MTLPixelFormat = .depth32Float
// MSAA設定(開発時は負荷軽減のため低め)
static let sampleCount: Int = 1
// メモリレス設定(開発時は無効化してデバッグしやすく)
static let useMemorylessTargets: Bool = false
// ロードアクション(開発時はクリアして状態を明確に)
static let loadAction: MTLLoadAction = .clear
static let storeAction: MTLStoreAction = .store
}
本番環境用(RenderConfig.production.swift)
// RenderConfig.production.swift - 本番環境用(このままコピーして使える)
// パフォーマンス最適化を有効化し、デバッグ機能を無効化
import Metal
import MetalKit
struct RenderConfig {
// タイルサイズ設定(32x32が最も効率的)
static let tileWidth: Int = 32
static let tileHeight: Int = 32
// 本番環境ではデバッグを無効化
static let enableGPUCapture: Bool = false
static let enableValidation: Bool = false
// パフォーマンスカウンター無効化(オーバーヘッド削減)
static let enablePerformanceCounters: Bool = false
// レンダーターゲット設定
static let colorPixelFormat: MTLPixelFormat = .bgra8Unorm
static let depthPixelFormat: MTLPixelFormat = .depth32Float
// MSAA設定(4xが品質とパフォーマンスのバランス良好)
static let sampleCount: Int = 4
// メモリレス設定(TBDR最適化の核心!)
static let useMemorylessTargets: Bool = true
// ロードアクション(dontCareで帯域節約)
static let loadAction: MTLLoadAction = .dontCare
static let storeAction: MTLStoreAction = .store
}
テスト環境用(RenderConfig.test.swift)
// RenderConfig.test.swift - テスト/CI用(このままコピーして使える)
// 再現性を重視し、最小構成でテストを実行
import Metal
import MetalKit
struct RenderConfig {
// タイルサイズ設定
static let tileWidth: Int = 32
static let tileHeight: Int = 32
// テスト環境ではキャプチャ無効、バリデーション有効
static let enableGPUCapture: Bool = false
static let enableValidation: Bool = true
// パフォーマンスカウンター無効(テストの再現性確保)
static let enablePerformanceCounters: Bool = false
// レンダーターゲット設定
static let colorPixelFormat: MTLPixelFormat = .bgra8Unorm
static let depthPixelFormat: MTLPixelFormat = .depth32Float
// MSAA無効(テストの単純化)
static let sampleCount: Int = 1
// メモリレス無効(結果の検証を容易に)
static let useMemorylessTargets: Bool = false
// ロードアクション(clearで毎回初期化して再現性確保)
static let loadAction: MTLLoadAction = .clear
static let storeAction: MTLStoreAction = .store
}
5.3 基本的な使い方
以下は、TBDRを最大限活用するMetalレンダラーの基本実装です。
/**
TBDRRenderer.swift - TBDR最適化を施したMetalレンダラー
使い方:
1. このファイルをXcodeプロジェクトに追加
2. RenderConfig.*.swiftから環境に合った設定をRenderConfig.swiftにコピー
3. TBDRRenderer()でインスタンス化
4. draw(in:)でレンダリング実行
*/
import Metal
import MetalKit
class TBDRRenderer: NSObject {
// MARK: - プロパティ
private let device: MTLDevice
private let commandQueue: MTLCommandQueue
private var pipelineState: MTLRenderPipelineState?
private var depthState: MTLDepthStencilState?
// MARK: - 初期化
override init() {
// Apple GPU(TBDR対応)を取得
guard let device = MTLCreateSystemDefaultDevice() else {
fatalError("Metal is not supported on this device")
}
self.device = device
// コマンドキューの作成
guard let queue = device.makeCommandQueue() else {
fatalError("Could not create command queue")
}
self.commandQueue = queue
super.init()
// パイプラインの構築
buildPipeline()
buildDepthState()
print("TBDR Renderer initialized")
print("Device: \(device.name)")
print("Supports TBDR: \(device.supportsFamily(.apple1))")
}
// MARK: - パイプライン構築
private func buildPipeline() {
guard let library = device.makeDefaultLibrary() else {
fatalError("Could not load Metal library")
}
let descriptor = MTLRenderPipelineDescriptor()
descriptor.vertexFunction = library.makeFunction(name: "vertex_main")
descriptor.fragmentFunction = library.makeFunction(name: "fragment_main")
descriptor.colorAttachments[0].pixelFormat = RenderConfig.colorPixelFormat
descriptor.depthAttachmentPixelFormat = RenderConfig.depthPixelFormat
descriptor.sampleCount = RenderConfig.sampleCount
do {
pipelineState = try device.makeRenderPipelineState(descriptor: descriptor)
} catch {
fatalError("Could not create pipeline state: \(error)")
}
}
private func buildDepthState() {
let descriptor = MTLDepthStencilDescriptor()
descriptor.depthCompareFunction = .less
descriptor.isDepthWriteEnabled = true
depthState = device.makeDepthStencilState(descriptor: descriptor)
}
// MARK: - レンダリング
func draw(in view: MTKView) {
guard let drawable = view.currentDrawable,
let renderPassDescriptor = view.currentRenderPassDescriptor,
let commandBuffer = commandQueue.makeCommandBuffer() else {
return
}
// ★ TBDR最適化ポイント1: Load/Store Actionの設定
configureRenderPassForTBDR(renderPassDescriptor)
guard let encoder = commandBuffer.makeRenderCommandEncoder(
descriptor: renderPassDescriptor
) else {
return
}
encoder.setRenderPipelineState(pipelineState!)
encoder.setDepthStencilState(depthState)
// ★ TBDR最適化ポイント2: 描画順序の最適化
// 不透明オブジェクト → アルファテスト → 半透明の順で描画
drawOpaqueObjects(encoder: encoder)
drawAlphaTestedObjects(encoder: encoder)
drawTranslucentObjects(encoder: encoder)
encoder.endEncoding()
commandBuffer.present(drawable)
commandBuffer.commit()
}
// MARK: - TBDR最適化
private func configureRenderPassForTBDR(
_ descriptor: MTLRenderPassDescriptor
) {
// カラーアタッチメントの設定
let colorAttachment = descriptor.colorAttachments[0]!
colorAttachment.loadAction = RenderConfig.loadAction
colorAttachment.storeAction = RenderConfig.storeAction
// 深度アタッチメントの設定
let depthAttachment = descriptor.depthAttachment!
depthAttachment.loadAction = .dontCare // 深度は毎フレーム再計算
// ★ TBDR最適化の核心: 深度バッファを保存しない
// Tile Memory内で処理が完結するため、メインメモリへの書き込み不要
depthAttachment.storeAction = .dontCare
// メモリレスターゲットの使用(本番環境のみ)
if RenderConfig.useMemorylessTargets {
// 深度バッファにMTLStorageModeMemorylessを使用
// → システムメモリを消費しない!
// 注: レンダーターゲットテクスチャ作成時に設定
}
}
private func drawOpaqueObjects(encoder: MTLRenderCommandEncoder) {
// 不透明オブジェクトの描画
// HSRが最大限効果を発揮する
// ソート不要(HSRが自動で最適化)
}
private func drawAlphaTestedObjects(encoder: MTLRenderCommandEncoder) {
// アルファテストオブジェクト(植物の葉など)
// discard使用のため、HSRの効果が限定的
}
private func drawTranslucentObjects(encoder: MTLRenderCommandEncoder) {
// 半透明オブジェクト(煙、ガラスなど)
// 手前から奥の順にソートが必要
// ブレンディングが発生するためHSRは無効
}
}
// MARK: - MTKViewDelegate
extension TBDRRenderer: MTKViewDelegate {
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
// 画面サイズ変更時の処理
}
func draw(in view: MTKView) {
draw(in: view)
}
}
5.4 実行結果
上記のコードを実行すると、以下のようなログが出力されます。
TBDR Renderer initialized
Device: Apple M1 Pro
Supports TBDR: true
Xcodeのメタルデバッガーで確認すると、以下のようなパフォーマンスの違いが見られます。
最適化前(IMR的な使い方)
Memory Bandwidth: 12.5 GB/s
Fragment Shader Invocations: 4,147,200
Overdraw: 2.5x
最適化後(TBDR最適化)
Memory Bandwidth: 3.2 GB/s ← 75%削減!
Fragment Shader Invocations: 1,658,880 ← 60%削減!
Overdraw: 1.0x ← オーバードローゼロ
5.5 よくあるエラーと対処法
| エラー | 原因 | 対処法 |
|---|---|---|
Metal is not supported |
Apple GPU非搭載 | Apple Silicon Mac、またはA7以降のiOSデバイスを使用 |
Could not load Metal library |
シェーダー未登録 |
.metalファイルがターゲットに含まれているか確認 |
Tile memory exhausted |
Imageblockサイズ超過 | タイルサイズを16×16に変更、またはアタッチメント数を削減 |
| パフォーマンス低下 | Load/Store設定ミス |
loadAction: .dontCareとstoreAction: .dontCareを深度に設定 |
| オーバードロー発生 | HSR無効化 |
discardやカスタム深度出力を避ける |
基本的な使い方をマスターしたので、次は応用例を見ていきましょう。
6. ユースケース別ガイド
6.1 ユースケース1: Deferred Shading(遅延シェーディング)
- 想定読者: 多数のライトを含むシーンを効率的に描画したい開発者
- 推奨構成: G-BufferをTile Memory内で完結させ、メモリ帯域を削減
- サンプルコード:
/**
DeferredShadingRenderer.swift
G-BufferをTile Memory内で処理し、メインメモリへの書き出しを最小化
*/
import Metal
import MetalKit
class DeferredShadingRenderer {
private let device: MTLDevice
init(device: MTLDevice) {
self.device = device
}
func createRenderPassDescriptor(
colorTexture: MTLTexture
) -> MTLRenderPassDescriptor {
let descriptor = MTLRenderPassDescriptor()
// G-Buffer: Albedo(カラー出力として最終結果を保存)
descriptor.colorAttachments[0].texture = colorTexture
descriptor.colorAttachments[0].loadAction = .dontCare
descriptor.colorAttachments[0].storeAction = .store
// G-Buffer: Normal(Tile Memoryに保持、メモリレス)
// ★ storageMode: .memoryless で宣言したテクスチャを使用
descriptor.colorAttachments[1].loadAction = .dontCare
descriptor.colorAttachments[1].storeAction = .dontCare // 保存しない!
// G-Buffer: Position(Tile Memoryに保持、メモリレス)
descriptor.colorAttachments[2].loadAction = .dontCare
descriptor.colorAttachments[2].storeAction = .dontCare // 保存しない!
// Depth(Tile Memoryに保持、メモリレス)
descriptor.depthAttachment.loadAction = .clear
descriptor.depthAttachment.storeAction = .dontCare // 保存しない!
return descriptor
}
/// メモリレステクスチャの作成
func createMemorylessTexture(
width: Int,
height: Int,
format: MTLPixelFormat
) -> MTLTexture? {
let descriptor = MTLTextureDescriptor.texture2DDescriptor(
pixelFormat: format,
width: width,
height: height,
mipmapped: false
)
// ★ TBDR最適化の核心: メモリレスストレージ
descriptor.storageMode = .memoryless
descriptor.usage = .renderTarget
return device.makeTexture(descriptor: descriptor)
}
}
6.2 ユースケース2: MSAA(マルチサンプルアンチエイリアス)
- 想定読者: ジャギーを消したいが、パフォーマンスも維持したい開発者
- 推奨構成: TBDRのMSAA最適化を活用し、Tile Memory内でリゾルブ
- サンプルコード:
/**
MSAARenderer.swift
TBDR最適化によりMSAAのコストを大幅に削減
IMRでは4xMSAAで4倍のメモリ帯域が必要だが、TBDRでは増加はわずか
*/
import Metal
import MetalKit
class MSAARenderer {
private let device: MTLDevice
private let sampleCount: Int = 4 // 4x MSAA
init(device: MTLDevice) {
self.device = device
}
func createMSAARenderPassDescriptor(
msaaTexture: MTLTexture,
resolveTexture: MTLTexture
) -> MTLRenderPassDescriptor {
let descriptor = MTLRenderPassDescriptor()
// MSAAカラーバッファ(Tile Memoryに保持)
descriptor.colorAttachments[0].texture = msaaTexture
descriptor.colorAttachments[0].loadAction = .dontCare
// ★ TBDR最適化: Tile Memory内でリゾルブ
// storeActionを.multisampleResolveにすると、
// MSAAサンプルをTile Memory内で解決し、
// 最終結果のみをresolveTextureに書き出す
descriptor.colorAttachments[0].storeAction = .multisampleResolve
descriptor.colorAttachments[0].resolveTexture = resolveTexture
// 深度バッファ(メモリレス可能)
descriptor.depthAttachment.loadAction = .clear
descriptor.depthAttachment.storeAction = .dontCare
return descriptor
}
/// MSAAテクスチャの作成(メモリレス)
func createMSAATexture(
width: Int,
height: Int
) -> MTLTexture? {
let descriptor = MTLTextureDescriptor.texture2DDescriptor(
pixelFormat: .bgra8Unorm,
width: width,
height: height,
mipmapped: false
)
descriptor.textureType = .type2DMultisample
descriptor.sampleCount = sampleCount
// ★ MSAAサンプルはTile Memoryにのみ存在
// システムメモリを消費しない!
descriptor.storageMode = .memoryless
descriptor.usage = .renderTarget
return device.makeTexture(descriptor: descriptor)
}
}
6.3 ユースケース3: Tile Shading(タイルシェーディング)
- 想定読者: レンダリングとコンピュートを効率的に組み合わせたい開発者
- 推奨構成: A11以降のTile Shader機能を使用し、単一レンダーパス内で処理を完結
- サンプルコード:
/**
TileShaderRenderer.swift
Tile Shaderを使用してレンダリングとコンピュートを統合
A11以降のApple GPUで使用可能
*/
import Metal
import MetalKit
class TileShaderRenderer {
private let device: MTLDevice
private var tilePipeline: MTLComputePipelineState?
init(device: MTLDevice) {
self.device = device
buildTilePipeline()
}
private func buildTilePipeline() {
guard let library = device.makeDefaultLibrary(),
let function = library.makeFunction(name: "tile_light_culling") else {
return
}
do {
tilePipeline = try device.makeComputePipelineState(function: function)
} catch {
print("Failed to create tile pipeline: \(error)")
}
}
func render(
encoder: MTLRenderCommandEncoder,
lightBuffer: MTLBuffer
) {
// 1. G-Buffer描画(通常のレンダリング)
drawGBuffer(encoder: encoder)
// ★ TBDR最適化: Tile Dispatchでライトカリング
// レンダーパスを切り替えずにコンピュート処理を実行
// Tile Memory内のデータに直接アクセス可能
encoder.setTileBuffer(lightBuffer, offset: 0, index: 0)
// 2. タイルシェーダーでライトカリング
// 各タイルに影響するライトのみを特定
let tileSize = MTLSize(width: 32, height: 32, depth: 1)
encoder.dispatchThreadsPerTile(tileSize)
// 3. 最終ライティング
// カリング結果を使用して効率的にシェーディング
drawLighting(encoder: encoder)
}
private func drawGBuffer(encoder: MTLRenderCommandEncoder) {
// G-Buffer描画の実装
}
private func drawLighting(encoder: MTLRenderCommandEncoder) {
// ライティング描画の実装
}
}
// Tile Shaderのメタルコード(.metalファイル)
/*
kernel void tile_light_culling(
imageblock<GBufferData> imageBlock,
device Light* lights [[buffer(0)]],
constant uint& lightCount [[buffer(1)]],
threadgroup uint* visibleLights [[threadgroup(0)]],
uint2 threadPosition [[thread_position_in_threadgroup]],
uint2 tilePosition [[threadgroup_position_in_grid]]
) {
// タイルの深度範囲を計算
float minDepth = /* Imageblockから取得 */;
float maxDepth = /* Imageblockから取得 */;
// 各ライトがタイルに影響するかチェック
for (uint i = threadPosition.x; i < lightCount; i += 32) {
Light light = lights[i];
if (lightIntersectsTile(light, tilePosition, minDepth, maxDepth)) {
// 可視ライトリストに追加
uint index = atomic_fetch_add_explicit(
&visibleLightCount, 1, memory_order_relaxed
);
visibleLights[index] = i;
}
}
}
*/
ユースケースが把握できたところで、この記事を読んだ後の学習パスを確認しましょう。
7. 学習ロードマップ
この記事を読んだ後、次のステップとして以下をおすすめします。
初級者向け(まずはここから)
-
Apple公式チュートリアル
- Basic Metal Rendering
- Metalの基本的なレンダリングパイプラインを学ぶ
-
WWDC動画の視聴
- Harness Apple GPUs with Metal (WWDC20)
- TBDRの公式解説。この記事の内容を映像で復習
中級者向け(実践に進む)
-
サンプルプロジェクトの改造
- Modern Rendering with Metal
- 実際のゲームで使われるテクニックを網羅
-
パフォーマンス最適化
- Optimize Metal Performance for Apple Silicon Macs (WWDC20)
- HSRの効率を最大化する方法、Tile Shaderの実践
上級者向け(さらに深く)
-
GPU Profiling
- Optimize GPU renderers with Metal (WWDC24)
- GPU Debuggerとパフォーマンスカウンターの活用
-
アーキテクチャの深掘り
- Imagination Technologies TBDR Architecture Guide
- PowerVR(Apple GPUの原型)の技術文書
8. まとめ
この記事では、TBDRアーキテクチャについて以下を解説しました。
- TBDRの背景: IMRのメモリ帯域問題とモバイル時代の要請
- タイリングの仕組み: 画面を32×32ピクセルのタイルに分割し、オンチップメモリで処理
- HSRによるオーバードロー削減: ハードウェアレベルで見えないピクセルを除去
- Metal実装: Load/Store Action、メモリレステクスチャ、Tile Shaderの活用
私の所感
TBDRを学んで最も印象的だったのは、**「制約が革新を生む」**という事実です。
デスクトップGPUは、帯域不足を「より多くのメモリ、より速いバス」で解決しようとしました。それは正しいアプローチですが、スマートフォンには適用できませんでした。
TBDRは発想を転換し、「そもそも帯域を使わない」設計にしました。結果として、省電力と高性能を両立する革新的なアーキテクチャが生まれました。
Apple Silicon MacがM1から始まり、M4 Ultraに至るまで、デスクトップGPUに迫る性能を実現できているのは、このTBDR技術があればこそです。今後、Apple GPUがどこまで進化するのか、私は楽しみでなりません。
この記事が、あなたのGPUプログラミングの旅の第一歩になれば幸いです。
参考文献
- Harness Apple GPUs with Metal (WWDC20) - Apple公式、TBDR解説の決定版
- Bring your Metal app to Apple Silicon Macs (WWDC20) - IMRからTBDRへの移行ガイド
- Optimize Metal Performance for Apple Silicon Macs (WWDC20) - TBDR最適化の詳細
- Metal 2 on A11 - Tile Shading (Tech Talks) - Tile Shader公式解説
- PowerVR Architecture Overview - Imagination Technologies公式ドキュメント
- A look at the PowerVR Graphics Architecture - TBDR開発者による解説
- Wikipedia: Tiled rendering - タイルレンダリングの歴史と各社実装の概要