CORETECH ENGINEER BLOG

株式会社サイバーエージェント SGEコア技術本部 技術ブログ

Unity 6.5 の新機能解説:Tile-Only Mode と On Tile Post Processing

みなさんこんにちは、コアテクのチャン ユービンです。

今日は Unity 6.5 の URP 17.5 で追加された Tile-Only ModeOn Tile Post Processing について解説します。

先に結論から述べますが、これらは新しい Tile-Based Rendering(以下 TBR)を実現するための新パイプラインではなく、既存の URP の中で TBR をできる限り維持するための制約の仕組みです。汎用的な最適化スイッチではなく、帯域が極めて逼迫した低スペック端末向けの、最後の最適化手段と考えるといいでしょう。

Tile-Only Mode は、TBR Pass を途切れさせる一連の機能の無効化・Fallback・検証を担います。

On Tile Post Processing は、URP 既存 Post-Processing のうち対応可能なサブセットを Framebuffer Fetch 実装に置き換え(Color Grading、Tonemapping、Vignette など)、近傍/スクリーンスペースサンプリングに依存するエフェクト(Bloom、DoF、Motion Blur など)は対象外とします。

本記事は以下の流れで解説していきます。

  • TBR の基本原理と、モバイルでのメリット。
  • 2 つの機能の役割、Tile-Only Mode による機能の無効化と導入時の注意点。
  • 実際のレンダリングの流れ、導入手順、カスタム Pass を On-Tile に対応させる方法。

おまけ(内容が多いため折りたたんでいます。興味がある方は見てみてください):

  • ソースコード解説:Tile-Only Mode の制約と検証の仕組み、および On Tile Post Processing の Framebuffer Fetch 実装。

執筆時の環境は以下のとおりです。

  • Unity version:6000.5.0f1
  • URP package version:17.5.0

TBR の基本原理とモバイルでのメリット

TBR の基本的な考え方は、ジオメトリ処理の後にプリミティブを各タイルへ振り分け、タイルごとにラスタライズとフラグメント処理を完了させることで、そのタイルの Color や Depth といった Framebuffer データをできる限りオンチップの高速メモリに留めておく、というものです。

これはモバイルでは非常に重要です。モバイル端末は消費電力・発熱・メモリ帯域幅の制約が厳しいため、Color や Depth といった Attachment を 1 つの Render Pass 内でできるだけタイルメモリに留め、Render Target の Load/Store を減らすことが重要になります。タイルの処理が終わると、GPU は Render Pass の Load/Store/Resolve/Invalidate のセマンティクスに従って、どの結果をシステムメモリへ書き戻し、どれを破棄するかを決定します。

この記事では、Attachment がシステムメモリへの Store/Load を経ずにタイルメモリ内に保たれている状態を On-Tile 保持 と呼びます。逆にシステムメモリへの書き戻し・読み込みが発生してしまう状態は Off-Tile と呼びます。(on-chip / off-chip と書く資料もあります)。

とはいえ、On-Tile にデータを留めるメリットには前提があります。処理がタイルごとに完結でき、現在の Framebuffer の他タイルのデータに依存しないことです。一部の Post-Processing は広範囲の周辺ピクセルを読む必要があります(例:ブラー系)。この場合、Color バッファを一度システムメモリへストアしなければ、通常の Texture として後続 Pass からサンプリングできません。これは同一 Render Pass 内の On-Tile 保持を途切れさせ、追加の外部メモリ読み書きを生み、帯域と消費電力を増やします。

TBR、Load/Store Action、Framebuffer Fetch のより詳しい解説は以下の記事を参照してください。

Tile-Only Mode と On Tile Post Processing の主な機能と利用上の制限

Tile-Only Mode と On Tile Post Processing の主な役割は、まさに On-Tile 保持を壊す処理を減らすことです。前者は機能の無効化と検証によって、保護対象のメイン Camera ターゲットが通常の Texture としてサンプリングされるのを防ぎます。後者は Framebuffer Fetch によって、タイル内で完結する Post-Processing 実装を提供し、URP が Camera 描画と対応 Post-Processing を同一の Native Render Pass にまとめられるようにします。

先に結論を表にまとめます。

機能 主な役割 適用範囲
Tile-Only Mode On-Tile 保持を途切れさせる処理がレンダリングに加わるのを制限し、Color・Depth などメイン Camera の Attachment をできる限り On-Tile に保つ レンダリングエフェクトを追加するものではなく、Post-Processing の実装も行わない
On Tile Post Processing URP 既存 Post-Processing のうちピクセル単位で完結できるサブセットを Framebuffer Fetch 実装に置き換え、タイル内で完結させる URP Post-Processing の完全な代替ではなく、タイルをまたぐ/広範囲の近傍サンプリングが必要なエフェクトには非対応

両者は並列の代替関係ではなく、前提条件と実行主体の関係です。Tile-Only Mode は「このパイプラインはタイル内に留まれる」という制約環境を用意し、On Tile Post Processing はその環境の中で、対応する Post-Processing を Input Attachment 方式で Camera Color の後段に接続します。

Tile-Only Mode で制限される URP 機能

機能 Tile-Only Mode での扱い 理由
WebGL ❌ 強制 Fallback WebGL は On-Tile Renderer 非対応。Tile-Only Mode を有効にしても自動で無効化され、通常レンダリングに戻る
Deferred / Deferred+ ❌ 無効 GBuffer の読み書き関係が複雑で、メイン Camera ターゲットの On-Tile 保持を保証しづらい
Camera Stacking ❌ 無効 Overlay Camera は Base Camera の結果を読んで合成する必要があり、単一 Camera の Render Pass マージを壊す
HDR ❌ 無効 追加の Blit/Resolve Pass が必要で、完全な On-Tile を保てない
Camera Opaque / Depth Texture ❌ 無効 Color や Depth をサンプリング用に複製する必要があり、Attachment が通常 Texture になる
URP 組み込み Post-Processing ❌ 無効 スクリーン Texture のサンプリングに依存し、Camera Color を先にストアさせてしまう
FXAA / SMAA / TAA ❌ 無効 Camera の Antialiasing Pass に属し、Tile-Only Mode ではまとめて無効化される
GPU Occlusion Culling ❌ 無効 現在の Camera Depth を読み、Compute Pass でオクルージョン用 Depth ピラミッドを構築する必要がある
MSAA 🟡 条件付き対応 プラットフォームが Multisampled Backbuffer に対応し、かつ明示的な MSAA Resolve が不要な場合のみ保持。それ以外は 1 Sample に戻る
Upscaling 🟡 条件付き対応 異なる解像度の中間 Texture に描画してから Backbuffer へスケーリングする必要があり On-Tile を途切れさせる。XR Mobile のみ保持し、他プラットフォームではまとめて無効化
Unsafe / Compute Pass ⚠️ 制限付き 独立したリソースは扱えるが、保護対象の On-Tile Camera ターゲットの読み書きは不可。同じリソースを共有する 2 つの Raster Pass の間に挟むことも不可

On Tile Post Processing が対応/非対応の Post-Processing

エフェクト 可否 理由
Color Grading 🟢 対応 LUT で現在ピクセルに Color マッピング
Tonemapping 🟢 対応 現在ピクセルの HDR から表示空間へのマッピング
Vignette 🟢 対応 スクリーン位置に応じて現在ピクセルに重み付け
Film Grain 🟢 対応 現在ピクセルにノイズを加算
Dithering 🟢 対応 現在ピクセルの出力にディザを加える
Bloom ❌ 非対応 高輝度抽出・ブラー・多段 Downsample/Upsample・合成が必要
Depth of Field ❌ 非対応 Depth に依存した近傍ブラーが必要
Motion Blur ❌ 非対応 速度 Texture と近傍サンプリングが必要
Chromatic Aberration ❌ 非対応 チャンネルごとにオフセットしたサンプリングが必要
Lens Distortion / Panini ❌ 非対応 スクリーンスペースのリマップサンプリングが必要
Lens Flare / Lens Dirt ❌ 非対応 追加 Texture やスクリーンスペース合成、複数 Pass に依存

なお、Tile-Only Mode が無効のときは、On Tile Post Processing は Texture Sampling Fallback で動作します。描画結果に変わりはありませんが、On-Tile による帯域削減のメリットは得られません。

注意点

上の 2 つの表に加えて、導入前につまずきやすい点がいくつかあります。

  • Tile-Only Mode は実行時に切り替えられないUniversalRenderer のコンストラクタ時に固定され、それに基づいて Pass を生成・構成します。Renderer アセット上で事前に有効化するしかなく、実行時スイッチには向きません。
  • 組み込み Post-Processing と On Tile Post Processing は併用不可:先に Renderer の組み込み Post-Processing を無効化してから On Tile Post Processing Feature を追加します。両方を同時に有効にすると、AddRenderPasses() 実行時(つまり毎フレーム)に Debug.LogError が出力され、その回の Pass 追加だけがスキップされます。Feature 自体は Renderer Features リストに残ったままで、On Tile Post Processing は機能しません。
  • On-Tile Pass は Memoryless な中間 Texture に依存:On Tile Post Processing は中間 Texture が Memoryless であることを要求します。「Tile-Only Mode 無効」以外にも、中間 Texture が Memoryless でない場合は Off-Tile の Texture Sampling Pass にフォールバックします。描画結果は正常ですが帯域削減は得られません。
  • 開発時に検証レイヤー(On-Tile Validation Layer)がある:Editor と Development Build では、URP は保護対象の Camera ターゲットを通常 Texture としてサンプリングする Pass や、その読み書き経路に Unsafe/Compute Pass を挟む Pass がないかを検証し、該当すると Render Graph Record 段階で例外を投げます。リリースビルドではこの検証は毎フレーム実行されず、継続的な実行時コストはありません。
  • Render Scale は強制的に 1 になる:Upscaling を無効化する副作用として、XR Mobile 以外のプラットフォームでは Render Scale が実行時に 1.0 へクランプされます。URP Asset で 1 以外の値を設定しても無視され、Tile-Only Mode では解像度スケーリングはできません。
  • Motion Vector は実質使えない:Tile-Only Mode は MotionVectorPass を 1 行で明示的に無効化しているわけではありませんが、その内部の要求元(TAA、Motion Blur、STP)がすべて削られるため、既定では誰も Motion Vector の生成を要求せず、この Pass は実行されません。カスタム Pass で強制的に要求すると、Depth Texture の生成も巻き込んで強制し(「Motion vectors imply depth」)、On-Tile Pass と衝突します。開発時には検証レイヤーの例外も発生します。

実際の流れとプロジェクトへの導入

レンダリングの流れを通して見る

Tile-Only Mode と On Tile Post Processing を同一フレーム内に置くと、次のような流れになります(下図は設定から実行までを概念的につないだもので、単一フレームの厳密な時系列ではありません)。

理想的には、Render Graph はこの経路を同一の Native Render Pass 内の一連の subpass にコンパイルします。Camera の Color / Depth Attachment は終始タイルメモリに留まり、システムメモリへ書き戻す必要がありません。Color は Post-Processing が Framebuffer Fetch でオンチップから直接読み取り、「Store してから通常 Texture として読み戻す」往復を省きます。Depth は Depth テストに使うだけで、使い終わったら破棄されます。

導入手順

以下の手順で Tile-Only Mode + On Tile Post Processing を有効化します。

  1. 対象の Universal Renderer Data を開く。
  2. Renderer アセット上の組み込み Post-Processing を無効化する。
  3. Tile-Only Mode を有効化する。
  4. Renderer Features に On Tile Post Processing を追加する。

注意:On Tile Post Processing Feature 自体は、どのエフェクトも自動では有効化しません。実行可能な経路を用意するだけで、別途 Camera 側で Post Processing を有効にする必要があり、具体的なエフェクトは Volume で設定します。

Render Graph Viewer で On-Tile Pass に入ったか確認する

Render Graph Viewer では、Camera 描画と On Tile Post Processing が同一の Native Render Pass にまとめられ、Camera Color が追加の Store/Blit ではなく Framebuffer Input を通っていることを確認できます。これによって、本当に On-Tile Pass に入ったかを判断できます。

カスタム Renderer Feature を Tile-Only Mode に対応させるには

要点は 1 つだけです。Pass が Camera Color / Depth Attachment を On-Tile 状態から追い出してはいけない。具体的には次の 3 点です。

  1. Camera Attachment は現在ピクセルのみ読む:通常の Sample / Load ではなく Framebuffer Fetch を使う。近傍サンプリング・スクリーンスペースリマップ・ヒストリーフレームはいずれも、Attachment を先に通常 Texture へストアさせてしまう。
  2. 追加の Camera Texture を持ち込まない_CameraOpaqueTexture_CameraDepthTextureなどはすべてメイン Camera ターゲットの複製や派生であり、持ち込むだけで On-Tile が途切れる。
  3. Raster Pass として実装できること:Unsafe / Compute Pass は Native Render Pass にマージできない。

注意したいのは、近傍サンプリング自体が問題なのではなく、「Camera Attachment への近傍サンプリング」が問題だという点です。自分で作った Texture に対して近傍サンプリング・リマップ・ヒストリーフレームを行うのは制約の対象外です(検証レイヤーはこれらのリソースを追跡しません)。ただしその計算自体は Off-Tile になります。また Compute / Unsafe Pass の場合は、Camera の保護対象リソースを連続して使う Raster チェーンの間に挟まないようにし、Camera の On-Tile レンダリング区間の前後に独立した段として配置するのが最も安全です。

逆に、On-Tile と両立できないエフェクトがどうしても必要なら、Tile-Only Mode を無理に使うべきではありません。

まとめ

Tile-Only Mode と On Tile Post Processing は新しいレンダリングパイプラインではなく、URP の「リミッター」です。前者は Native Render Pass を途切れさせる機能を無効化し、メイン Camera ターゲットができる限り通常 Texture としてサンプリングされないようにします。後者は SetInputAttachmentFramebuffer Fetch を使い、タイル内で Color Grading、Tonemapping、Vignette などのピクセル単位 Post-Processing を完結させます。

その代償として、かなり多くの機能(HDR、組み込み Post-Processing、Antialiasing、Render Scale、Motion Vector など)が使えなくなる、または制限されます。

おまけ:ソースコード解説

Tile-Only Mode ソースコード解説:レンダラー単位の制約モード

ソースコード解説を開く

Tile-Only Mode の入口は UniversalRendererData にあります。

UniversalRendererData.cs:157

[SerializeField] bool m_TileOnlyMode = false;

UniversalRendererData.cs:355

public bool tileOnlyMode
{
    get => m_TileOnlyMode;
    set
    {
        if (m_TileOnlyMode == value)
            return;

        SetDirty();
        m_TileOnlyMode = value;
    }
}

UniversalRenderer はコンストラクタでこの値を読み、読み取り専用プロパティとしてキャッシュします。

UniversalRenderer.cs:205

// A Renderer cannot be switched from On-Tile to not On-Tile.
// We make assumptions in the constructor to create the passes.
internal bool useTileOnlyMode { get; }

UniversalRenderer.cs:266

useTileOnlyMode = data.tileOnlyMode;

つまり Tile-Only Mode は、実行時に気軽にオン/オフするようなエフェクト設定ではありません。Renderer はコンストラクタ時にこれを基に Pass を生成・構成し、ソースコードのコメントでも On-Tile と非 On-Tile の間で切り替えられないと明記されています。

コンストラクタ時には WebGL 向けの処理もあります。On-Tile Renderer は WebGL に非対応なので、アセット上で Tile-Only Mode を有効にしていても、ここで直接オフにされ通常レンダリングへ戻ります。ここでの IsWebGL() はランタイムでグラフィックスデバイスの能力を調べるものではなく、#if PLATFORM_WEBGL によるコンパイル時のプラットフォーム判定です。なお IsWebGL() 自身のコメントは Apple Arm64 上の depth priming の問題について言及したもので、On-Tile Renderer が WebGL を非対応とする理由をソースコードのコメントが直接説明しているわけではありません。

UniversalRenderer.cs:269

// The On-Tile Renderer does not support WebGL.
if (useTileOnlyMode && IsWebGL())
{
    useTileOnlyMode = false;
}

有効化すると URP が無効化する機能

本当に重要なロジックは UniversalRenderer.UpdateSupportedRenderingFeatures() にあります。useTileOnlyModetrue のとき、URP は On-Tile Pass を壊しやすい一連の機能をオフにします。

UniversalRenderer.cs:435

if (useTileOnlyMode)
{
    supportedRenderingFeatures.supportsHDR = false;
    supportedRenderingFeatures.postProcessing = false;
    supportedRenderingFeatures.cameraOpaqueTexture = false;
    supportedRenderingFeatures.cameraDepthTexture = false;
    supportedRenderingFeatures.upscaling = PlatformAutoDetect.isXRMobile;
    supportedRenderingFeatures.antiAliasing = false;
    supportedRenderingFeatures.gpuOcclusionCulling = false;
    supportedRenderingFeatures.deferredLighting = false;
    supportedRenderingFeatures.overlayCamera = false;

    supportedRenderingFeatures.msaa =
        !UniversalRenderer.PlatformRequiresExplicitMsaaResolve()
        && SystemInfo.supportsMultisampledBackBuffer
        && !supportedRenderingFeatures.deferredLighting;
}

このうち supportedRenderingFeatures.antiAliasing = false は、さらに Camera データの初期化に影響します。FXAA、SMAA、TAA はいずれも cameraData.antialiasing が扱う Camera の Antialiasing モードに属するため、Tile-Only Mode ではまとめて None にクリアされます。

UniversalRenderPipeline.cs:1594

if (!supportedRenderingFeatures.antiAliasing)
    cameraData.antialiasing = AntialiasingMode.None;

MSAA は直接無効化されるわけではありません。Tile-Only Mode では、プラットフォームが Multisampled Backbuffer に対応し、かつ明示的な MSAA Resolve が不要な場合のみ MSAA を保持します。それ以外は、たとえ URP Asset で MSAA を設定していても、最終的な Camera ディスクリプタは 1 Sample に戻ります。

UniversalRenderPipeline.cs:1505

bool rendererSupportsMSAA = renderer != null && renderer.supportedRenderingFeatures.msaa;

int msaaSamples = 1;
if (camera.allowMSAA && asset.msaaSampleCount > 1 && rendererSupportsMSAA)
    msaaSamples = (camera.targetTexture != null) ? camera.targetTexture.antiAliasing : asset.msaaSampleCount;

upscaling = false は、cameraData 初期化段階で Render Scale にも連動して影響します。Render Scale ≠ 1 はまさに URP が Upscaling/Downscaling を起動し、中間 Texture と独立した Scaling Pass を必要とする入口なので、Upscaling が無効化されると Render Scale は 1.0 へ直接クランプされます。

UniversalRenderPipeline.cs:1614

bool disableRenderScale = (Mathf.Abs(1.0f - settings.renderScale) < kRenderScaleThreshold)
    || isScenePreviewOrReflectionCamera
    || !supportedRenderingFeatures.upscaling;
cameraData.renderScale = disableRenderScale ? 1.0f : settings.renderScale;

!supportedRenderingFeatures.upscaling が成立すると disableRenderScaletrue になり、cameraData.renderScale1.0f に設定され、URP Asset で設定した 1 以外の値は効かなくなります。

これらの制限の背後にある原則は一貫しています。Camera の中間 Color・Depth を通常 Texture として露出させる必要があるもの、あるいは追加の Blit・Resolve・Copy が必要なものは、いずれも Native Render Pass のマージを途切れさせ得る、ということです。

例えば:

  • Camera Opaque/Depth Texture は Camera の Color や Depth をサンプリング可能な Texture に複製する必要がある。
  • 組み込み URP Post-Processing は通常、フルスクリーン Texture のサンプリングが必要。
  • Deferred/GBuffer Pass はより複雑な Attachment 読み書き関係を持ち込むため、Tile-Only Mode では除外される。
  • FXAA、SMAA、TAA は Camera データ段階で無効化され、On Tile Post Processing も今のところ代替実装を提供していない。
  • Overlay Camera や一部の UI/IMGUI Pass は非互換な Pass を挟む可能性がある。

ソースコードでは、組み込み Post-Processing に対するコンストラクタ時の Fallback も行っています。Tile-Only Mode が有効で、かつ Renderer アセットに組み込み Post-Processing が設定されている場合、URP は Warning を出力し、postProcessEnabledfalse にします。

UniversalRenderer.cs:318

if (useTileOnlyMode)
{
    if (postProcessEnabled)
    {
        Debug.LogWarning(...);
        postProcessEnabled = false;
    }

    if (renderingModeRequested == RenderingMode.Deferred ||
        renderingModeRequested == RenderingMode.DeferredPlus)
    {
        Debug.LogWarning(...);
    }
}

ただし、この 2 つの分岐は同じ形をしていても中身が異なります。postProcessEnabled の分岐は実際に値を false へ書き換えますが、Deferred/DeferredPlus の分岐は Warning を出すだけで、renderingModeRequested 自体は変更しません。Deferred が実際に無効化されるのは、後述する UpdateSupportedRenderingFeatures() 内で supportedRenderingFeatures.deferredLighting = false が設定されることによるもので、ここでの Warning はあくまで「設定がそのままだと想定通りに動かない」というユーザー向けの注意喚起です。

これが、前述の導入手順で組み込み URP Post-Processing を無効化してから On Tile Post Processing Renderer Feature を追加するよう求める理由でもあります。

単に Memoryless にしているわけではない

誤解しやすい点として、Tile-Only Mode は URP のレイヤーですべての中間 RenderTexture を Memoryless にしているわけではありません。ソースコードでより本質的なのは、いくつかの Render Graph 設定と保護です。

  1. UV origin の伝播

UniversalRenderPipelineRenderGraph.cs では、Renderer が Tile-Only Mode を使うとき、Render Graph パラメータに次を使います。

UniversalRenderPipelineRenderGraph.cs:12行あたり

RenderTextureUVOriginStrategy.PropagateAttachmentOrientation

これにより中間 Attachment が Backbuffer の向きを継承し、Texture サンプリングのセマンティクスの違いによる反転やコピーを減らします。

  1. Backbuffer の Store/Resolve 判断

UniversalRendererRenderGraph.ImportBackBuffers() には重要な判定があります。

UniversalRendererRenderGraph.cs:1614

bool intermediateTexturesAreSampledAsTextures =
    s_RequiresIntermediateAttachments && !useTileOnlyMode;

bool noStoreOnlyResolveBBColor =
    !intermediateTexturesAreSampledAsTextures
    && !isNativeRenderingAfterURP
    && (cameraData.cameraTargetDescriptor.msaaSamples > 1);

Tile-Only Mode では、中間 Attachment が存在しても、URP はそれらを「通常 Texture としてサンプリングされる中間 Texture」とはみなしません。これは Backbuffer Import の discardOnLastUse に影響し、一部の MSAA シーンで Store せず Resolve のみにします。

  1. MSAA 中間 Color Attachment のバインド

中間 Camera Color Attachment を作成するとき:

UniversalRendererRenderGraph.cs:1746

desc.bindTextureMS = useTileOnlyMode && desc.msaaSamples != MSAASamples.None;

これは MSAA シーンでより適切なバインド方式を保ち、早すぎる Resolve を避けます。

  1. Final Blit の保護

最終的に中間 Texture から Backbuffer へブリットする必要が残った場合、URP は Tile-Only Mode 下でアサーションを発火させます。

UniversalRendererRenderGraph.cs:1424

Debug.Assert(!useTileOnlyMode,
    "Adding the final blit pass when Tile-Only Mode is on. This is not valid...");

これらのロジックが共通して示すのは、Tile-Only Mode の狙いが個別の最適化ではなく、Render Graph コンパイラが Native Render Pass をマージできる条件をひとそろい作り出し、同時によくある Store/Load の発生経路を避けることだ、という点です。

On-Tile Validation Layer:開発時の検証

Tile-Only Mode のエンジニアリング上の価値の一つは、開発時に OnTileValidationLayer を組み込んでいることです。この検証レイヤーは Core パッケージにあります。

com.unity.render-pipelines.core@f43b729f1f6e/
Runtime-PrivateShared/OnTileValidationLayer.cs

URP 側は ValidationHandler を通してこれを取り付けます。

ValidationHandler.cs:36

InternalRenderGraphValidation.SetAdditionalValidationLayer(
    renderGraph,
    active ? m_OnTileValidationLayer : null);

if (m_OnTileValidationLayer != null && active)
{
    m_OnTileValidationLayer.renderGraph = renderGraph;
    m_OnTileValidationLayer.Add(resourceData.activeColorTexture);
    m_OnTileValidationLayer.Add(resourceData.activeDepthTexture);
}

ソースコードには GBuffer の登録ロジックも残っています。

ValidationHandler.cs:54

foreach (TextureHandle handle in resourceData.gBuffer)
{
    m_OnTileValidationLayer.Add(handle);
}

ただし注意したいのは、Tile-Only Mode は supportedRenderingFeatures.deferredLighting を強制的に false にするため、実際の Tile-Only レンダリング経路では Deferred/GBuffer は有効になりません。ここでの GBuffer 登録は、Tile-Only Mode 下の通常の実行経路というより、検証レイヤーの汎用的な保護ロジックに近いものです。

ここで登録されるリソースこそ、検証レイヤーが On-Tile 制約下に留めるべきと考える重要な Camera ターゲットです。

何を検出するか

検証レイヤーは主に 3 種類の問題を捕捉します。

1 つ目:On-Tile リソースを通常 Texture としてサンプリングする。

OnTileValidationLayer.cs:137

override public void UseTexture(in TextureHandle input, AccessFlags flags)
{
    if (!IsTrackedOnTile(in input))
        return;

    if (m_CurrentPass.info.type == RenderGraphPassType.Raster)
    {
        ThrowTextureSamplingException(in input, k_UseTexture);
    }
    else
    {
        ThrowNotRasterPassException(in input, k_UseTexture);
    }
}

あるカスタム Raster Pass が Camera Color に対して UseTexture を呼ぶと、これはまさに Camera Color を VRAM へストアさせる典型的な書き方です。

2 つ目:Unsafe または Compute Pass で On-Tile リソースを使う。検証レイヤーのコメントが明言するとおり、Native Render Pass にマージできるのは Raster Pass だけで、Unsafe/Compute はできません。

OnTileValidationLayer.cs:124

void ThrowNotRasterPassException(in TextureHandle input, string methodName)
{
    var resourceName = renderGraph.GetTextureName(in input);
    throw new InvalidOperationException($"{k_ErrorMessageValidationIssue} render pass '{m_CurrentPass.info.name}' calls '{methodName}' with resource '{resourceName}'. " +
        $"Unsafe and Compute render passes can't be merged. Use a Raster render pass and ensure that no load/store action will be performed." +
        $"\n{errorMessageHowToResolve}");
}

3 つ目:2 つの Raster Pass が同じ On-Tile リソースを使うが、その間に Unsafe または Compute Pass を挟む。間の Unsafe/Compute Pass がそのリソースに直接アクセスしなくても、前後 2 つの Raster Pass をマージ不能にし、Store/Load を招きます。

強調しておくと、Tile-Only Mode は Unsafe/Compute Pass を完全に禁止するわけではありません。禁止するのは On-Tile Pass を壊す 2 つの使い方です。1 つは Unsafe/Compute Pass が保護対象の Active Color/depth/GBuffer を直接 UseTexture で読む、または書き込むこと。もう 1 つは Unsafe/Compute Pass を、同じ保護対象リソースを使う 2 つの Raster Pass の間に挟むこと。Compute Pass が自分で作った Texture・buffer・LUT・テーブルデータだけを扱い、メイン Camera Attachment の読み書き経路に関与しないなら、ここでの制限対象ではありません。

ソースコードのコメントも、この検証レイヤーが Store/Load を引き起こし得るすべてのケースを捕捉しようとはしていないと強調しています。位置づけは、よくある破壊的なミスを捕まえ、Render Graph Record 段階で直ちに例外を投げ、開発者に明確なコールスタックを渡すことです。

リリースビルドでは追加コストなし

ValidationHandler の関連メソッドには条件付きコンパイル属性が付いています。OnBeginRenderGraphFrame()OnBeforeRendering()OnBeforeGBuffers() の 3 メソッドがいずれも同じ属性を持ち、前述の SetAdditionalValidationLayer 呼び出しと GBuffer 登録ループも、それぞれ OnBeforeRendering()OnBeforeGBuffers() の内側にあります。

ValidationHandler.cs:32

[Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")]
public void OnBeforeRendering(...)

そのため、これらは主に Editor と Development Build 向けの開発時ツールであり、リリースビルドではこの検証ロジックが毎フレーム実行されることはありません。ただし ValidationHandler 自身のコンストラクタには [Conditional] が付いていないため、Tile-Only Mode 有効時はリリースビルドでも OnTileValidationLayer のインスタンスが初期化時に 1 回だけ生成されます。ごく小さな一度限りのコストであり、「追加の実行時コストはありません」というより「フレームごとに継続する検証コストはない」と捉えるのが正確です。

On Tile Post Processing ソースコード解説:Framebuffer Fetch によるピクセル単位の Post-Processing

ソースコード解説を開く

On Tile Post Processing の実装は 1 つの ScriptableRendererFeature です。

OnTilePostProcessFeature.cs:10

[DisallowMultipleRendererFeature("On Tile Post Processing")]
public partial class OnTilePostProcessFeature : ScriptableRendererFeature

Feature の作成時には 2 つの Pass を用意します。

  • ColorGradingLutPass:URP 既存の LUT 生成 Pass を再利用し、内部の Color Grading LUT を事前生成して、後段の On-Tile Uber Post に渡す。
  • OnTilePostProcessPass:実際の On-Tile Uber Post を実行する。

中心となる作成ロジック:

OnTilePostProcessFeature.cs:67

m_ColorGradingLutPass =
    new ColorGradingLutPass(RenderPassEvent.BeforeRenderingPrePasses, m_PostProcessData);

m_OnTilePostProcessPass =
    new OnTilePostProcessPass(m_PostProcessData);

// On-tile PP requires memoryless intermediate texture to work.
// In case intermediate texture is not memoryless, on-tile PP will falls back to off-tile rendering.
m_OnTilePostProcessPass.requiresIntermediateTexture = true;

supportedRenderingFeatures.supportsHDR = true;
supportedRenderingFeatures.postProcessing = true;

ここでの supportedRenderingFeatures.postProcessing = true は Feature が自身の能力を宣言しているもので、URP 組み込み Post-Processing を再び有効化するものではありません。AddRenderPasses() では、組み込み Post-Processing が有効になっていないかを明示的にチェックします。

OnTilePostProcessFeature.cs:86行あたり

if (universalRenderer.postProcessEnabled)
{
    Debug.LogError("URP renderer(Universal Renderer Data) has post processing enabled...");
    return;
}

Fallback の分岐はとてもシンプル

Feature はキュー投入前に、Tile-Only Mode に応じて Fallback フラグを設定します。

OnTilePostProcessFeature.cs:102

m_OnTilePostProcessPass.m_UseTextureReadFallback =
    !universalRenderer.useTileOnlyMode;

この 1 行が、On Tile Post Processing と Tile-Only Mode の関係を要約しています。

  • Tile-Only Mode 有効:Framebuffer Fetch / Input Attachment 経路を通る。
  • Tile-Only Mode 無効:Texture Sampling Fallback を通る。

つまりプロジェクトで「On Tile Post Processing Feature を追加した」だけでは、必ずしも On-Tile の性能メリットが得られるとは限りません。本当に On-Tile になるかは、Renderer の Tile-Only Mode、プラットフォーム能力、そして Render Graph の最終的なマージ結果次第です。

SetInputAttachmentUseTexture の違い

本当の On-Tile Pass は OnTilePostProcessPass.RecordRenderGraph() にあります。

OnTilePostProcessPass.cs:164

if (m_UseTextureReadFallback)
{
    builder.UseTexture(passData.source = source, AccessFlags.Read);
}
else
{
    builder.SetInputAttachment(source, 0);
    builder.AllowGlobalStateModification(true);
}

builder.UseTexture(lutTexture, AccessFlags.Read);
builder.SetRenderAttachment(passData.destination = destination, 0, AccessFlags.WriteAll);

ここには 2 つの分岐があります。

  • Fallback:UseTexture(source)、source を通常 Texture として読む。
  • On-Tile:SetInputAttachment(source, 0)、source を現在の Render Pass の Input Attachment として読む。

この 2 つの API は、Render Graph と下層のグラフィックス API に対する意味がまったく異なります。UseTexture は「この Pass は Texture をサンプリングする」を表すため、その Texture はサンプリング可能なリソースとして存在しなければなりません。Camera Color のような On-Tile Attachment では、これは通常その前段でストアする必要があることを意味します。SetInputAttachment は「この Pass は前の subpass の Attachment を読む」を表し、下層では Vulkan/Metal の Native Render Pass の Input Attachment や Framebuffer Fetch にマップでき、TBR GPU はオンチップのタイルメモリから現在ピクセルを直接取得できます。

続いて Pass は Backbuffer へ書き戻します。

OnTilePostProcessPass.cs:185

builder.SetRenderAttachment(passData.destination = destination, 0, AccessFlags.WriteAll);

そして Pass の開始前に次を呼びます。

OnTilePostProcessPass.cs:109

resourceData.SwitchActiveTexturesToBackbuffer();

これは URP に「現在の Active Target は既に Backbuffer へ切り替わった、以降 Final Blit を追加する必要はない」と伝えます。

シェーダー側で現在の Framebuffer を読む方法

OnTileUberPost.shader における On-Tile Pass の要点は次のとおりです。

OnTileUberPost.shader:183

FRAMEBUFFER_INPUT_X_HALF(0);

half4 FragUberPost(Varyings input) : SV_Target
{
    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);

    float2 uv = input.texcoord;
    half4 inputColor = LOAD_FRAMEBUFFER_X_INPUT(0, input.positionCS.xy);
    return UberPost(inputColor, uv);
}

Fallback Pass は通常の Texture サンプリングです。

OnTileUberPost.shader:157

half4 FragUberPostTextureSample(Varyings input) : SV_Target0
{
    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);

    float2 uv = SCREEN_COORD_APPLY_SCALEBIAS(
        UnityStereoTransformScreenSpaceTex(input.texcoord));
    half4 inputColor = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv);
    return UberPost(inputColor, uv);
}

この 2 つのコードは On Tile Post Processing の本質的な違いを示しています。On-Tile Path はフルスクリーン Texture を読むのではなく、現在の Fragment 位置で Framebuffer Input を読みます。現在ピクセルだけで完結する Post-Processing に向いており、近傍サンプリングやリマップサンプリングが必要なエフェクトには向きません。

なぜ一部の Post-Processing しか対応しないのか

対応/非対応のエフェクトは前述のとおりです。ソースコードからも分かるように、実装は Volume スタックから On-Tile Path で実行可能な次の数種類のコンポーネントしか読み取りません。

OnTilePostProcessPass.cs:89

var vignette = stack.GetComponent<Vignette>();
var colorLookup = stack.GetComponent<ColorLookup>();
var colorAdjustments = stack.GetComponent<ColorAdjustments>();
var tonemapping = stack.GetComponent<Tonemapping>();
var filmgrain = stack.GetComponent<FilmGrain>();

注意したいのは、前述の「対応」リストにある Dithering はこの GetComponent 群には含まれない点です。Dithering は Volume Override ではなく Camera 単位のスイッチで、cameraData.isDitheringEnabled で決まり、SetupDithering でノイズ Texture と Keyword を設定します。

OnTilePostProcessPass.cs:424

void SetupDithering(Material onTileUberMaterial, UniversalCameraData cameraData, PostProcessData data)
{
    CoreUtils.SetKeyword(onTileUberMaterial, ShaderKeywordStrings.Dithering, cameraData.isDitheringEnabled);
    ...
}

もう一つ直感に反する点があります。URP の多くの Post-Processing Override クラスは依然として IPostProcessComponent.IsTileCompatible() を実装しており、例えば Tonemapping は true、Bloom は false を返します。しかしこのインターフェースメソッドは既に Obsolete とマークされています。

IPostProcessComponent.cs:20

[Obsolete("Unused #from(2023.1)")]
bool IsTileCompatible() => false;

現在の On Tile Post Processing は、対応エフェクトを決めるためにこのインターフェースを動的に読むことはしません。対応範囲は OnTilePostProcessFeatureOnTilePostProcessPassOnTileUberPost.shader の実装でハードコードされています。

参考資料