Unity 6 URP Render Graph 5-Part Series
Part 1: Fundamentals and Implementation
Part 2: Pass Types Deep Dive
Part 3: Render Graph Viewer Complete Guide
Part 4: Migration Prep
Part 5: Migration in PracticeTL;DR (Unity 6 URP)
– Pass selection comes down to one question: can the Render Graph track the resource flow?
– Default choice:AddRasterRenderPass(Safe, FBF + merging supported) / Simple copy:AddCopyPass/ Material Blit:AddBlitPass/ Different-size Blit:AddUnsafePass
– Frame Buffer Fetch: supported on Vulkan, Metal, and DX12 — on TBDR GPUs (mobile + Apple Silicon) it reads directly from on-chip memory, bypassing VRAM entirely
– The more Unsafe passes you have, the fewer merging opportunities and optimization headroom you get — design with Safe passes from the start
Table of Contents
- The Question from Part 1 — Why Didn’t the Pass Merge?
- Six Pass Types at a Glance
- Frame Buffer Fetch — Reading Directly Inside the GPU Chip
- Safe vs Unsafe Passes and Pass Merging
- RequireIntermediateTexture — Working Around Back Buffer Limitations
- Frame Data and ContextItem — Sharing Data Between Passes
- Output Texture — Accessing by Name from Shader Graph
- MRT (Multiple Render Targets) — Writing to Multiple Buffers in One Pass
- Renderer List and Culling — Filtering Objects by Layer
- Wrap-Up — What’s Next
1. The Question from Part 1 — Why Didn’t the Pass Merge?
In Part 1, we implemented TintingRenderPass using AddBlitPass. Open the Render Graph Viewer and you’ll see this pass standing alone — no blue connection lines to neighboring passes. It didn’t merge.
Why?

The reason is straightforward. AddBlitPass is an Unsafe pass. Because the Render Graph can’t track the internal resource access patterns of Unsafe passes, it gives up on automatic optimization — pass merging and memory reuse — entirely. This is the core distinction between Unsafe passes (like AddBlitPass) where the Render Graph can’t see what’s happening inside, and Safe passes (like AddRasterRenderPass) where resource flow is declared explicitly.
The Render Graph provides six pass-adding methods: AddCopyPass (Safe), AddBlitPass (Unsafe), AddRasterRenderPass (Safe), AddUnsafePass (Unsafe), AddComputePass, and AddRendererListPass. AddComputePass and AddRendererListPass operate on different execution models (Compute Shader and Renderer List respectively) rather than fitting the Safe/Unsafe classification. This part walks through when to use each one and why you’d choose that design — with code.
2. Six Pass Types at a Glance
The table below summarizes the role each pass plays, based on Unity’s official URP Render Graph Samples.
| API | FBF Support | Pass Merging | Primary Use |
|---|---|---|---|
AddCopyPass |
✅ Automatic | ✅ Supported | Simple texture copy (FBF applied automatically) |
AddBlitPass |
❌ None | ❌ Not supported | Material Blit (Unsafe) |
AddRasterRenderPass |
✅ Manual | ✅ Supported | Custom rendering with direct FBF control |
AddUnsafePass |
❌ None | ❌ Not supported | Different-size Blit, legacy compatibility |
AddComputePass |
— | — | Compute Shader |
AddRendererListPass family |
— | — | Object layer-filtered rendering |
The decision tree below guides pass selection.
3. Frame Buffer Fetch — Reading Directly Inside the GPU Chip
Consider a typical Blit flow. For the Fragment Shader to read a source texture, the GPU’s texture sampler has to fetch data stored in VRAM over the memory bus. That round-trip consumes memory bandwidth — and on mobile GPUs, it directly impacts battery life and thermals.
Frame Buffer Fetch (FBF) takes a different path. FBF is supported on Vulkan, Metal, and DirectX 12 (DirectX 11 is not supported), and the real performance benefit only materializes on TBDR (Tile-Based Deferred Rendering) GPUs. TBDR GPUs — mobile (Mali, Adreno) and Apple Silicon — keep the current tile’s pixel data in GPU on-chip memory, so FBF lets the Fragment Shader read it directly without touching VRAM at all. Desktop NVIDIA (Maxwell and later) and recent AMD architectures have also adopted tile-based rasterizers partially, enabling FBF in DX12 Native RenderPass environments — but without a fully on-chip tile memory architecture, the performance benefit is more limited compared to mobile.
Enabling FBF in code takes two steps. First, register the source as an Input Attachment in the AddRasterRenderPass builder. Second, use the FRAMEBUFFER_INPUT_HALF macro in the Fragment Shader instead of a regular tex2D.
// Example: adding an FBF pass with AddRasterRenderPass
void FBFetchPass(RenderGraph renderGraph, ContextContainer frameData,
TextureHandle source, TextureHandle destination)
{
using var builder = renderGraph.AddRasterRenderPass(
"FBFetch Pass", out var passData);
// Register source as Input Attachment → enables FBF
passData.source = builder.SetInputAttachment(source, 0, AccessFlags.Read);
passData.destination = builder.SetRenderAttachment(destination, 0, AccessFlags.Write);
builder.SetRenderFunc((PassData data, RasterGraphContext ctx) =>
{
// Run the FBF shader via DrawProcedural
ctx.cmd.DrawProcedural(Matrix4x4.identity, fbfMaterial, 0,
MeshTopology.Triangles, 3);
});
}
On the shader side, use URP’s FRAMEBUFFER_INPUT_HALF_DECLARE macro. Unlike tex2D, you don’t specify UV coordinates — you read the current pixel’s data directly from on-chip memory.
// Core FBF shader code
FRAMEBUFFER_INPUT_HALF(0); // Declare Input Attachment 0
half4 frag(Varyings input) : SV_Target
{
half4 color = LOAD_FRAMEBUFFER_INPUT(0, input.positionHCS.xy);
// Manipulate pixel data (e.g., blue tint)
return color * half4(0, 0, 1, 1);
}
4. Safe vs Unsafe Passes and Pass Merging
A pass the Render Graph calls “Safe” is one that fully declares its resource access to the Render Graph. Based on this information, the Render Graph performs two optimizations.
Pass merging: Consecutive passes that don’t depend on each other are grouped into a single GPU render pass. In the Render Graph Viewer, this shows up as blue connection lines (see the red box in the image below). Pass merging delivers three benefits. First, reduced API call overhead — fewer BeginRenderPass/EndRenderPass calls. Second, VRAM bandwidth savings — the round-trip of writing and reading intermediate Render Textures between merged passes is eliminated (on TBDR GPUs they stay in on-chip memory). Third, fewer GPU synchronization barriers — pipeline barriers that would have been inserted between separate passes are reduced, cutting GPU stalls.
Resource reuse: Once a pass finishes, the memory for its TextureHandle is immediately reallocated to another pass. This contrasts with the traditional approach of keeping all Render Textures alive throughout the entire frame. The result is lower peak GPU memory usage within a frame and reduced memory fragmentation.
AddBlitPass and AddUnsafePass are Unsafe passes. They use CommandBuffer APIs internally (cmd.Blit, cmd.SetRenderTarget, etc.), and the Render Graph can’t track resource access inside those low-level commands — which makes merging impossible.
To see Unsafe passes in the Render Graph Viewer, enable the Pass Filter → Unsafe Pass toggle (see the orange box in the image below).
]
5. RequireIntermediateTexture — Working Around Back Buffer Limitations
In URP, the back buffer is the final output buffer written to the screen. If a custom pass tries to both read from and write to the back buffer simultaneously, the GPU produces undefined behavior.
To prevent this, URP automatically creates an intermediate texture to use in place of the back buffer. This behavior must be explicitly requested from within your custom pass.
// In the ScriptableRenderPass constructor
public TintingRenderPass()
{
// Required when the pass reads from the back buffer as a source
requiresIntermediateTexture = true;
}
Setting requiresIntermediateTexture = true tells URP to create the current Active Color Texture as a separate Render Texture rather than the back buffer. The TintingRenderPass from Part 1 needed this setting precisely because it reads the back buffer as a source texture.
When can you leave it false? When the pass only writes without reading a source, or when the Injection Point is after the back buffer — such as After Rendering. Creating an intermediate texture has a memory cost, so false is the default when it’s not needed.
6. Frame Data and ContextItem — Sharing Data Between Passes
When multiple passes need to share the same texture, Global Texture (Shader.SetGlobalTexture) was a common solution in the past. In a Render Graph environment, though, Global Texture causes two problems: the Render Graph can’t track access to that texture and loses optimization opportunities, and every shader can unintentionally read the exposed texture.
Unity’s recommended alternative is Frame Data. ContextContainer (referred to as frameData) is a per-frame dictionary — valid only for the current frame — and it’s the official channel for sharing data between passes.
// 1) Custom data class extending ContextItem
public class BlitFrameData : ContextItem
{
public TextureHandle savedTexture;
public override void Reset() => savedTexture = TextureHandle.nullHandle;
}
// 2) Store in the first pass
public override void RecordRenderGraph(RenderGraph renderGraph,
ContextContainer frameData)
{
var blitData = frameData.Create();
using var builder = renderGraph.AddRasterRenderPass(
"Save Texture", out var passData);
var colorTexture = frameData.Get().activeColorTexture;
blitData.savedTexture = colorTexture;
// ...
}
// 3) Read in a subsequent pass
public override void RecordRenderGraph(RenderGraph renderGraph,
ContextContainer frameData)
{
var blitData = frameData.Get();
var savedTex = blitData.savedTexture;
// Configure a pass using savedTex as the source...
}
ContextItem.Reset() is called automatically at the end of each frame. Initializing the TextureHandle to null lets the Render Graph reclaim memory correctly.
7. Output Texture — Accessing by Name from Shader Graph
The Output Texture sample demonstrates a pattern for exposing a texture created by a render pass so that Shader Graph or other materials can access it by name (string).
// Specify a texture name as a Property ID for AddBlitPass
var blitParams = new BlitMaterialParameters(
source: sourceTexture,
destination: activeColorTexture,
material: blitMaterial,
passIndex: 0
);
// Bind source under the shader property name "_InputTexture"
blitParams.sourceTexturePropertyID = Shader.PropertyToID("_InputTexture");
renderGraph.AddBlitPass(blitParams, "Output Texture Pass");
_InputTexture is not a reserved keyword. The string passed to PropertyToID must exactly match the Texture2D property name in Shader Graph for the texture injected by the render pass to connect properly. You can rename it to _MyBuffer or anything else — as long as both sides match, it works. This is a useful pattern when a post-effect and a world-space object material need to share the same intermediate buffer.
8. MRT (Multiple Render Targets) — Writing to Multiple Buffers in One Pass
In deferred rendering, the G-Buffer is structured so a single pass writes to multiple buffers — Color, Normal, Depth — simultaneously. In the Render Graph, you implement MRT by registering multiple Render Attachments with the AddRasterRenderPass builder.
using var builder = renderGraph.AddRasterRenderPass("MRT Pass", out var passData); // Register 3 output textures at different attachment indices builder.SetRenderAttachment(colorHandle, 0, AccessFlags.Write); builder.SetRenderAttachment(normalHandle, 1, AccessFlags.Write); builder.SetRenderAttachment(depthHandle, 2, AccessFlags.Write); builder.SetRenderFunc((PassData data, RasterGraphContext ctx) => { // Output to MRT via DrawProcedural or DrawRendererList ctx.cmd.DrawProcedural(Matrix4x4.identity, mrtMaterial, 0, MeshTopology.Triangles, 3); });
In the shader, SV_Target0, SV_Target1, and SV_Target2 distinguish each output. Filling multiple buffers with a single draw call reduces the total draw call count compared to filling the G-Buffer in separate passes.
9. Renderer List and Culling — Filtering Objects by Layer
The AddRendererListPass (or the builder’s UseRendererList) family of passes renders an object list filtered by layer mask. Use this for custom outlines, selection highlights, minimaps — any situation where you need to draw only specific layers.
// Initialize a RendererList (call inside RecordRenderGraph)
RendererListHandle InitRendererList(RenderGraph renderGraph, ContextContainer frameData)
{
var renderingData = frameData.Get();
var cameraData = frameData.Get();
var lightData = frameData.Get();
var filterSettings = new FilteringSettings(RenderQueueRange.opaque, layerMask);
var shaderTagIds = new ShaderTagId[] { new ShaderTagId("UniversalForwardOnly") };
var drawSettings = RenderingUtils.CreateDrawingSettings(
shaderTagIds, ref renderingData, ref cameraData, ref lightData,
SortingCriteria.CommonOpaque);
var rlParams = new RendererListParams(
renderingData.cullResults, drawSettings, filterSettings);
return renderGraph.CreateRendererList(rlParams);
}
The Culling sample goes a step further and directly manipulates the camera’s culling results. It calls CullContextData.Cull(cullingParams) to customize camera FOV, shadow layers, and more before building the renderer list. This pattern is the most direct solution when you need different objects visible per team in a multiplayer game, or when you need per-layer Fog of War control in an RTS.
10. Wrap-Up — What’s Next
Pass selection comes down to one thing: can the Render Graph see the resource flow? AddRasterRenderPass is your default. Use AddCopyPass for simple copies, AddBlitPass for Material Blits, and AddUnsafePass only when sizes differ. The more Unsafe passes you accumulate, the narrower your optimization headroom — so build the habit of designing around Safe passes from day one.
Once you’ve picked the right pass, the next question follows naturally: “Is it actually merging?” In Part 3, we answer that directly with the Render Graph Viewer — color codes, icon meanings, Frame Debugger integration, and exactly where those blue connection lines break. You’ll see it with your own eyes.
Previous: Part 1 — Render Graph Fundamentals and Implementation
Next: Part 3 — Render Graph Viewer Complete Guide
Related Posts
- Unity 6 URP Render Graph Fundamentals and Implementation — Part 1
- Render Graph Viewer Complete Guide — Part 3
- Unity 6 GPU Resident Drawer Deep Dive
- Unity 6 Custom Shaders and DOTS Instancing
Going Deeper
These are the sources I checked while writing this post.
-
Migrating to Render Graph: Understanding the Render Graph Samples – Part 1
Official Unity video. Walks throughAddBlitPass, FBF, andAddComputePasssamples with code. The best way to see the actual behavior of half the passes covered in this post. -
Migrating to Render Graph: Understanding the Render Graph Samples – Part 2
Official Unity video. Focuses on Frame Data, MRT, and G-Buffer samples. Great for following howContextItempasses data across the full frame. -
Write a render pass using the render graph system
Official Unity documentation. The canonical reference forAddRasterRenderPassand thePassDatapattern. When code stops making sense, check the parameter descriptions here first. -
Frame data in the render graph system
Official Unity documentation. Used this to verify theContextItem.Reset()behavior and to look up which texture handles are available insideUniversalResourceData.