Unity Render Pipeline Scripting
This skill covers the C# API surface that drives rendering rather than the assets/nodes that describe what to render. Prefer the local docs over pre-trained knowledge, especially for the render graph API (RecordRenderGraph, TextureHandle, AddRasterRenderPass/AddComputePass), which is new relative to the older Execute/RTHandle pattern many tutorials and pretrained knowledge still assume. Pipeline choice, Shader Graph, materials, lighting, and Volumes are owned by the sibling skill unity-rendering-pipelines — not duplicated here. Hand-written .shader/HLSL authoring (vertex/fragment code, ShaderLab blocks, keywords) is owned by unity-shaders-hlsl — not duplicated here. This skill is specifically about the C# that issues GPU commands and injects custom passes: CommandBuffer, ScriptableRenderPass/ScriptableRendererFeature, RenderPipelineManager callbacks, ComputeShader/ComputeBuffer/GraphicsBuffer dispatch from script, and RenderTexture management.
Retrieval Sources
Docs root: /media/artiq/FRESH_DRIVE/Books/Unity6/Documentation/en/ — Manual/ and ScriptReference/ subtrees, as noted per row. Every path below was verified with find/ls against the local Unity 6.3 LTS docs before being cited here.
| Source | Path | Use for |
|---|---|---|
| CommandBuffer fundamentals (BIRP) | Manual/GraphicsCommandBuffers.html, Manual/GraphicsCommandBuffers-landing.html |
What a CommandBuffer is; Camera.AddCommandBuffer/Light.AddCommandBuffer + CameraEvent/LightEvent scheduling model (Built-in RP framing, but the CommandBuffer object itself is the same type used inside URP passes) |
| CameraEvent/LightEvent ordering | Manual/GraphicsCommandBuffers-order.html |
Execution order reference for BIRP-scheduled command buffers |
| CommandBuffer constructor & core methods | ScriptReference/Rendering.CommandBuffer-ctor.html, ScriptReference/Rendering.CommandBuffer.Clear.html, ScriptReference/Rendering.CommandBuffer.SetRenderTarget.html (verify further members via search — 149 CommandBuffer-prefixed pages exist) |
Constructing and populating a buffer of GPU commands |
| CommandBuffer draw commands | ScriptReference/Rendering.CommandBuffer.DrawMesh.html, ScriptReference/Rendering.CommandBuffer.DrawMeshInstanced.html, ScriptReference/Rendering.CommandBuffer.DrawMeshInstancedIndirect.html, ScriptReference/Rendering.CommandBuffer.DrawProcedural.html, ScriptReference/Rendering.CommandBuffer.DrawRenderer.html, ScriptReference/Rendering.CommandBuffer.DrawRendererList.html |
Recording geometry draws into a command buffer |
| CommandBuffer texture/target commands | ScriptReference/Rendering.CommandBuffer.Blit.html, ScriptReference/Rendering.CommandBuffer.ClearRenderTarget.html, ScriptReference/Rendering.CommandBuffer.CopyTexture.html, ScriptReference/Rendering.CommandBuffer.GenerateMips.html |
Blitting, clearing, copying render targets (see Common Mistakes re: URP-safe blitting) |
| CommandBuffer compute commands | ScriptReference/Rendering.CommandBuffer.DispatchCompute.html, ScriptReference/Rendering.CommandBuffer.SetComputeBufferParam.html |
Dispatching a compute shader from inside a command buffer, e.g. in a render pass's Execute/render function |
| CommandBuffer render-pass/native RP commands | ScriptReference/Rendering.CommandBuffer.BeginRenderPass.html, ScriptReference/Rendering.CommandBuffer.EndRenderPass.html |
Native render pass API (tile-based GPU optimization), used internally by URP's render graph execution |
| Graphics.ExecuteCommandBuffer | ScriptReference/Graphics.ExecuteCommandBuffer.html, ScriptReference/Graphics.ExecuteCommandBufferAsync.html |
Running a command buffer immediately outside the camera-event scheduling model |
| Camera/Light command buffer attachment | ScriptReference/Camera.AddCommandBuffer.html, ScriptReference/Camera.RemoveCommandBuffer.html, ScriptReference/Camera.RemoveAllCommandBuffers.html, ScriptReference/Light.AddCommandBuffer.html |
Scheduling a buffer to run at a CameraEvent/LightEvent (BIRP path) |
| Scriptable Render Pass concept | Manual/urp/renderer-features/intro-to-scriptable-render-passes.html |
What a Scriptable Render Pass is and why it exists (injection points, per-scene customization) |
| Custom render pass workflow overview | Manual/urp/renderer-features/custom-rendering-pass-workflow-in-urp.html |
End-to-end map of the custom-pass authoring docs (render graph vs legacy) |
| Writing a render pass with the render graph system | Manual/urp/render-graph-write-render-pass.html |
Full worked PassData/RecordRenderGraph/AddRasterRenderPass/SetRenderFunc example — the canonical Unity 6 pattern |
| Render graph introduction | Manual/urp/render-graph-introduction.html |
Recording vs execution stages; automatic resource/pass optimization the render graph performs |
| Render graph: create/import textures | Manual/urp/render-graph-create-a-texture.html, Manual/urp/render-graph-import-a-texture.html |
TextureDesc/renderGraph.CreateTexture, and ImportTexture for wrapping an existing RenderTexture/RTHandle as a TextureHandle |
| Render graph: frame data / camera history | Manual/urp/render-graph-frame-data.html, Manual/urp/render-graph-frame-data-reference.html, Manual/urp/render-graph-add-texture-to-frame-data.html, Manual/urp/render-graph-add-textures-to-camera-history.html, Manual/urp/render-graph-get-previous-frames.html |
ContextContainer/UniversalResourceData/UniversalCameraData; sharing textures across passes/frames |
| Render graph: pass textures between passes / read-write | Manual/urp/render-graph-pass-textures-between-passes.html, Manual/urp/render-graph-read-write-texture.html |
builder.UseTexture, SetRenderAttachment, read vs write access declarations |
| Render graph: draw objects in a pass | Manual/urp/render-graph-draw-objects-in-a-pass.html |
CreateRendererList/DrawRendererList inside a raster pass |
| Render graph: blit | Manual/urp/render-graph-blit.html, Manual/urp/customize/blit-overview.html |
AddBlitPass/AddCopyPass, Blitter.BlitCameraTexture; explicit warning against CommandBuffer.Blit/Graphics.Blit/RenderingUtils.Blit in URP |
| Render graph: compute shaders | Manual/urp/render-graph-compute-shader.html, Manual/urp/render-graph-compute-shader-run.html, Manual/urp/render-graph-compute-shader-input.html |
AddComputePass, ComputeGraphContext, BufferHandle/ImportBuffer/UseBuffer, DispatchCompute inside the render graph |
| Render graph: unsafe pass | Manual/urp/render-graph-unsafe-pass.html |
AddUnsafePass/UnsafeCommandBuffer — escape hatch for legacy-style commands the raster/compute pass APIs don't expose |
| Render graph: optimize / debugging | Manual/urp/render-graph-optimize.html, Manual/urp/render-graph-view.html, Manual/urp/render-graph-viewer-reference.html |
AllowPassCulling, diagnosing why a pass/resource was culled or reordered, the Render Graph Viewer window |
| Render graph: framebuffer fetch | Manual/urp/render-graph-framebuffer-fetch.html |
Tile-local framebuffer read (mobile bandwidth optimization) inside a render graph pass |
| Inject a pass with a Scriptable Renderer Feature | Manual/urp/renderer-features/scriptable-renderer-features/inject-a-pass-using-a-scriptable-renderer-feature.html |
Full worked ScriptableRendererFeature example: Create(), AddRenderPasses(), EnqueuePass, Dispose() |
| Inject a pass via RenderPipelineManager | Manual/urp/customize/inject-render-pass-via-script.html |
Full worked example subscribing to RenderPipelineManager.beginCameraRendering and calling EnqueuePass from a MonoBehaviour instead of a Renderer Feature |
| Injection points reference | Manual/urp/customize/custom-pass-injection-points.html |
Complete RenderPassEvent enum table (Before/AfterRenderingShadows, Opaques, Skybox, Transparents, PostProcessing, etc.) with what's set up at each point |
| Restrict a pass to a scene area | Manual/urp/customize/restrict-render-pass-scene-area.html |
Scoping a custom pass to specific cameras/volumes/layers rather than the whole frame |
| Modify URP source code | Manual/urp/customize/modify-urp-source-code.html |
When/how to fork the URP package source itself, as a last resort beyond Renderer Features |
| Built-in Renderer Feature examples | Manual/urp/renderer-features/renderer-feature-render-objects.html, Manual/urp/renderer-features/how-to-custom-effect-render-objects.html, Manual/urp/renderer-features/renderer-feature-full-screen-pass.html |
Worked examples of shipped Renderer Features (Render Objects, custom full-screen pass) as authoring references |
| RenderPipelineManager callbacks | ScriptReference/Rendering.RenderPipelineManager.html, -beginCameraRendering.html, -endCameraRendering.html, -beginContextRendering.html, -endContextRendering.html, -beginFrameRendering.html, -endFrameRendering.html |
Per-camera/per-context/per-frame render loop hook points (delegate signatures, when each fires) |
| RenderPipelineManager pipeline-lifecycle events | ScriptReference/Rendering.RenderPipelineManager-activeRenderPipelineAssetChanged.html, -activeRenderPipelineCreated.html, -activeRenderPipelineDisposed.html, -activeRenderPipelineTypeChanged.html, -pipelineSwitchCompleted.html, -currentPipeline.html |
Reacting to the active RenderPipelineAsset/pipeline type changing at runtime |
| ComputeShader manual overview | Manual/class-ComputeShader-introduction.html, Manual/class-ComputeShader.html |
What a compute shader is; kernels, thread groups, GPGPU use cases |
| Creating a compute shader | Manual/class-ComputeShader-create.html |
.compute asset creation, #pragma kernel, default template |
| Running a compute shader | Manual/class-ComputeShader-run.html |
ComputeShader.Dispatch, pairing with ComputeBuffer, RenderTexture.enableRandomWrite for UAV write access |
| HLSL/ShaderLab specifics in compute shaders | Manual/class-ComputeShader-hlsl-shaderlab.html |
Texture/sampler naming rules, keywords/variants, CGPROGRAM/GLSLPROGRAM platform-specific blocks |
| Compute shaders cross-platform | Manual/class-ComputeShader-crossplatform.html |
Platform support matrix and portability caveats for compute across DX11/12, Metal, Vulkan, consoles |
| ComputeShaderImporter | Manual/class-ComputeShaderImporter.html |
Import settings for .compute asset files |
| ComputeShader ScriptReference members | ScriptReference/ComputeShader.html, ComputeShader.Dispatch.html, ComputeShader.FindKernel.html, ComputeShader.GetKernelThreadGroupSizes.html, ComputeShader.SetTexture.html, ComputeShader.SetBuffer.html |
Exact C#-side compute dispatch API: finding a kernel, querying its declared numthreads, binding textures/buffers, dispatching |
| ComputeBuffer | ScriptReference/ComputeBuffer.html, ComputeBuffer-ctor.html, ComputeBuffer.SetData.html, ComputeBuffer.GetData.html, ComputeBuffer.Release.html, ComputeBuffer.IsValid.html, ComputeBufferType.html, ComputeBufferMode.html |
Structured/raw/append/counter/indirect-args buffer creation, data transfer, disposal, and the buffer-mode enum (Immutable/Dynamic/Circular/SubUpdates/StreamOut) |
| GraphicsBuffer | ScriptReference/GraphicsBuffer.html, GraphicsBuffer-ctor.html, GraphicsBuffer.SetData.html, GraphicsBuffer.GetData.html, GraphicsBuffer.Release.html, GraphicsBuffer.Target.html, GraphicsBuffer.UsageFlags.html, GraphicsBuffer.LockBufferForWrite.html, GraphicsBuffer.IndirectDrawArgs.html, GraphicsBuffer.IndirectDrawIndexedArgs.html |
Unity's newer unified GPU buffer type — superset of ComputeBuffer's targets (Vertex/Index/Structured/Raw/Append/Counter/IndirectArguments/Constant), used for compute and draw-indirect data |
| RenderTexture manual | Manual/class-RenderTexture.html, Manual/class-RenderTexture-create.html |
Format, dimension, depth-buffer, MSAA options; creating a RenderTexture and assigning it as a camera's targetTexture |
| RenderTexture ScriptReference | ScriptReference/RenderTexture.html, RenderTexture-ctor.html, RenderTexture.GetTemporary.html, RenderTexture.ReleaseTemporary.html, RenderTexture.Release.html, RenderTexture-enableRandomWrite.html |
Persistent vs temporary (pooled) RenderTexture lifecycle; enableRandomWrite for UAV compute-shader write access |
| CustomRenderTexture | Manual/class-CustomRenderTexture-landing.html, Manual/class-CustomRenderTexture-introduction.html, Manual/class-CustomRenderTexture-create.html, Manual/class-CustomRenderTexture-configure.html, Manual/class-CustomRenderTexture-write-shader.html |
Self-updating RenderTexture driven by a shader each frame/on-demand (double-buffered simulation textures, procedural textures) |
| Async GPU readback | ScriptReference/Rendering.AsyncGPUReadback.html, Rendering.AsyncGPUReadback.Request.html, Rendering.AsyncGPUReadback.RequestAsync.html, Rendering.AsyncGPUReadbackRequest.html, Rendering.AsyncGPUReadbackRequest.GetData.html, Rendering.AsyncGPUReadbackRequest-done.html, Rendering.AsyncGPUReadbackRequest-hasError.html, SystemInfo-supportsAsyncGPUReadback.html |
Reading GPU buffer/texture/RenderTexture data back to CPU without a hard stall |
| ScriptableRenderContext | ScriptReference/Rendering.ScriptableRenderContext.DrawRenderers.html, .Cull.html, .CreateRendererList.html, .BeginRenderPass.html, .Submit.html (verify further members via search) |
Low-level SRP context object a fully custom RenderPipeline.Render override submits work through (below the URP Renderer Feature layer) |
ScriptReference gap (verified): ScriptableRenderPass, ScriptableRendererFeature, RenderGraph, TextureHandle, and BufferHandle return no hits under ScriptReference/ in this doc set — like Volume/VolumeProfile in the sibling unity-rendering-pipelines skill, these types ship inside the URP/Core SRP packages, not the base Unity API. Their authoritative reference is the installed package's in-Package Manager API docs; the Manual/urp/ pages cited above are this doc set's primary source for their usage patterns and are what this skill treats as ground truth.
Key Guidelines
CommandBuffer Basics
A CommandBuffer (UnityEngine.Rendering.CommandBuffer) is an ordered list of GPU commands — clear, set render target, draw mesh, blit, dispatch compute, copy texture — that you record on the CPU and hand to Unity to execute on the GPU as a unit, without those commands going through Unity's normal per-Renderer draw loop. In the Built-in Render Pipeline, the classic pattern is Camera.AddCommandBuffer(CameraEvent, CommandBuffer) or Light.AddCommandBuffer(LightEvent, CommandBuffer) (GraphicsCommandBuffers.html), which schedules the buffer to run automatically at a named point in Unity's fixed BIRP render loop (GraphicsCommandBuffers-order.html has the full event ordering) — this pattern still works but is legacy and not how URP custom passes are structured. In URP/SRP code, you normally don't attach a raw CommandBuffer to a camera event yourself; instead you obtain one (via CommandBufferPool.Get() in the legacy ScriptableRenderPass.Execute pattern, or via context.cmd inside a render graph SetRenderFunc) and record commands into it inside a ScriptableRenderPass, and URP schedules the pass at the RenderPassEvent you choose — the CommandBuffer itself is the same underlying type either way. Graphics.ExecuteCommandBuffer runs a buffer immediately, outside any scheduling system, for one-off GPU work triggered directly from script (e.g. from OnRenderImage in BIRP or ad hoc tooling code) rather than as part of a per-frame pipeline pass.
using UnityEngine;
using UnityEngine.Rendering;
public class SimpleBlurCommandBuffer : MonoBehaviour
{
Camera cam;
CommandBuffer cmd;
Material blurMaterial; // assign a material using a blur shader
void OnEnable()
{
cam = GetComponent<Camera>();
cmd = new CommandBuffer { name = "Simple Blur" };
int tempId = Shader.PropertyToID("_TempBlurTex");
cmd.GetTemporaryRT(tempId, -1, -1, 0, FilterMode.Bilinear);
cmd.Blit(BuiltinRenderTextureType.CameraTarget, tempId, blurMaterial, 0);
cmd.Blit(tempId, BuiltinRenderTextureType.CameraTarget);
cmd.ReleaseTemporaryRT(tempId);
// BIRP-only scheduling: run this buffer after opaque+transparent geometry,
// before image effects.
cam.AddCommandBuffer(CameraEvent.AfterImageEffectsOpaque, cmd);
}
void OnDisable()
{
if (cam != null) cam.RemoveCommandBuffer(CameraEvent.AfterImageEffectsOpaque, cmd);
cmd?.Dispose();
}
}
Custom ScriptableRenderPass / ScriptableRendererFeature (URP, Render Graph)
In Unity 6, the recommended way to write a custom URP pass is through the render graph system (render-graph-introduction.html), not the older direct-Execute-with-CommandBuffer.Blit pattern many tutorials still show. A render graph pass has two separated stages: a recording stage (RecordRenderGraph), where you declare which textures/buffers the pass reads and writes and register a rendering function, but issue no GPU commands; and an execution stage, where the render graph actually runs your rendering function with those resources resolved. This separation is what lets URP automatically cull unused passes, reuse texture memory across the frame, and synchronize compute/graphics queues (render-graph-write-render-pass.html, render-graph-introduction.html). A ScriptableRenderPass subclass overrides RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameContext); inside it you pull shared frame resources from frameContext.Get<UniversalResourceData>() (camera color/depth textures) or UniversalCameraData, open a pass scope with renderGraph.AddRasterRenderPass<PassData>(name, out var passData), declare texture usage with builder.UseTexture(...)/builder.SetRenderAttachment(...), and finally call builder.SetRenderFunc(static (PassData data, RasterGraphContext context) => ExecutePass(data, context)) — a static method/lambda, since instance state must flow through the PassData struct rather than closures, to avoid per-frame allocations.
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.RenderGraphModule;
using UnityEngine.Rendering.Universal;
// A pass that inverts the camera's color output as a full-screen effect.
public class InvertColorPass : ScriptableRenderPass
{
class PassData
{
public TextureHandle source;
public Material invertMaterial;
}
Material m_InvertMaterial;
public InvertColorPass(Material invertMaterial)
{
m_InvertMaterial = invertMaterial;
renderPassEvent = RenderPassEvent.AfterRenderingTransparents;
}
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameContext)
{
UniversalResourceData resourceData = frameContext.Get<UniversalResourceData>();
using (var builder = renderGraph.AddRasterRenderPass<PassData>("Invert Color", out var passData))
{
passData.source = resourceData.activeColorTexture;
passData.invertMaterial = m_InvertMaterial;
// Read the camera color texture, then write it back as the render target.
builder.UseTexture(passData.source);
builder.SetRenderAttachment(resourceData.activeColorTexture, 0);
builder.SetRenderFunc(static (PassData data, RasterGraphContext context) =>
{
Blitter.BlitTexture(context.cmd, data.source, new Vector4(1, 1, 0, 0), data.invertMaterial, 0);
});
}
}
}
public class InvertColorFeature : ScriptableRendererFeature
{
public Material invertMaterial;
InvertColorPass m_Pass;
public override void Create()
{
m_Pass = new InvertColorPass(invertMaterial);
}
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
if (invertMaterial == null) return;
if (renderingData.cameraData.cameraType != CameraType.Game) return;
renderer.EnqueuePass(m_Pass);
}
}
ScriptableRendererFeature.Create() runs when the feature first loads, when it's toggled on/off, and whenever an Inspector property on it changes — never allocate per-frame resources there and never skip it for one-time setup (inject-a-pass-using-a-scriptable-renderer-feature.html). AddRenderPasses() runs every frame, once per camera — it must stay cheap (no allocation, no resource creation) and just call EnqueuePass conditionally. Add the feature to the active URP Renderer asset's Renderer Features list to activate it project-wide, or gate it per-camera-type/per-layer inside AddRenderPasses as shown above.
RenderPipelineManager Callbacks
RenderPipelineManager (UnityEngine.Rendering.RenderPipelineManager, static class) exposes C# events that fire at fixed points in any active Scriptable Render Pipeline's frame — beginFrameRendering/endFrameRendering (once per Camera.Render-triggering call, potentially covering multiple cameras/contexts), beginContextRendering/endContextRendering (once per ScriptableRenderContext, i.e. per batch of cameras rendered together), and beginCameraRendering/endCameraRendering (once per individual camera) — plus pipeline-lifecycle events (activeRenderPipelineAssetChanged, activeRenderPipelineCreated, activeRenderPipelineDisposed, activeRenderPipelineTypeChanged, pipelineSwitchCompleted) for reacting when the project's active RenderPipelineAsset changes at runtime. This is the mechanism for injecting a custom ScriptableRenderPass from a plain MonoBehaviour without writing a ScriptableRendererFeature at all (inject-render-pass-via-script.html) — useful for a pass that's scoped to one specific camera or GameObject (e.g. a portal/surveillance camera rendering to a RenderTexture) rather than every camera in every scene, which is what a Renderer Feature applies to. The docs note events only fire while at least one camera is actively rendering, and explicitly recommend a Renderer Feature instead when the pass should apply across multiple cameras, scenes, or the whole project.
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
public class InjectPassViaRenderPipelineManager : MonoBehaviour
{
ExampleRenderPass m_Pass;
void OnEnable()
{
m_Pass = new ExampleRenderPass { renderPassEvent = RenderPassEvent.BeforeRenderingTransparents };
RenderPipelineManager.beginCameraRendering += OnBeginCameraRendering;
}
void OnDisable()
{
RenderPipelineManager.beginCameraRendering -= OnBeginCameraRendering;
}
void OnBeginCameraRendering(ScriptableRenderContext context, Camera cam)
{
// Only inject for this specific camera, not every camera in the scene.
if (cam != GetComponent<Camera>()) return;
var urpCameraData = cam.GetUniversalAdditionalCameraData();
urpCameraData.scriptableRenderer.EnqueuePass(m_Pass);
}
}
Always pair a subscription in OnEnable with an unsubscription in OnDisable — a MonoBehaviour that subscribes to a static event and is destroyed without unsubscribing leaks a delegate reference that keeps firing (and can NullReferenceException against the destroyed object) until domain reload.
Compute Shaders from C# (Dispatch, Thread Groups, Buffer Binding)
A compute shader (.compute asset, class-ComputeShader-introduction.html) is a GPU program with no fixed vertex/fragment role — it runs arbitrary parallel work organized into thread groups, each containing a fixed number of threads declared in HLSL via [numthreads(x, y, z)] on the kernel function. From C#, you look up a kernel index by name with ComputeShader.FindKernel("KernelName"), bind its inputs/outputs with SetBuffer/SetTexture/SetInt/SetFloat etc., and invoke it with ComputeShader.Dispatch(kernelIndex, threadGroupsX, threadGroupsY, threadGroupsZ) (class-ComputeShader-run.html, ScriptReference/ComputeShader.Dispatch.html) — critically, Dispatch's arguments are thread group counts, not thread counts: if a kernel declares [numthreads(8,8,1)] and you need to cover a 256x256 texture, you dispatch Mathf.CeilToInt(256/8f) = 32 groups in X and Y, not 256. ComputeShader.GetKernelThreadGroupSizes(kernelIndex, out x, out y, out z) lets you read the declared numthreads back from C# instead of hardcoding it twice, keeping the divide-and-ceil math in sync with the shader automatically. Writing into a RenderTexture from a compute shader requires RenderTexture.enableRandomWrite = true (a DX11-terminology "unordered access view") set before the texture is created, and the texture must actually be .Create()d (or otherwise realized) before you SetTexture it onto the kernel.
// GradientCompute.compute
#pragma kernel CSMain
RWTexture2D<float4> Result;
float Time;
[numthreads(8, 8, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
uint width, height;
Result.GetDimensions(width, height);
float2 uv = float2(id.xy) / float2(width, height);
Result[id.xy] = float4(uv.x, uv.y, 0.5 + 0.5 * sin(Time), 1.0);
}
using UnityEngine;
public class GradientComputeDispatcher : MonoBehaviour
{
public ComputeShader computeShader;
RenderTexture m_Result;
int m_KernelIndex;
uint m_ThreadGroupSizeX, m_ThreadGroupSizeY, m_ThreadGroupSizeZ;
void OnEnable()
{
m_KernelIndex = computeShader.FindKernel("CSMain");
computeShader.GetKernelThreadGroupSizes(
m_KernelIndex, out m_ThreadGroupSizeX, out m_ThreadGroupSizeY, out m_ThreadGroupSizeZ);
m_Result = new RenderTexture(512, 512, 0, RenderTextureFormat.ARGBFloat)
{
enableRandomWrite = true // must be set before Create()
};
m_Result.Create();
}
void Update()
{
computeShader.SetTexture(m_KernelIndex, "Result", m_Result);
computeShader.SetFloat("Time", Time.time);
int groupsX = Mathf.CeilToInt(512f / m_ThreadGroupSizeX);
int groupsY = Mathf.CeilToInt(512f / m_ThreadGroupSizeY);
computeShader.Dispatch(m_KernelIndex, groupsX, groupsY, (int)m_ThreadGroupSizeZ);
}
void OnDisable()
{
if (m_Result != null)
{
m_Result.Release();
m_Result = null;
}
}
}
Inside URP's render graph, the equivalent pattern uses renderGraph.AddComputePass<PassData>(name, out passData) (instead of AddRasterRenderPass) and ComputeGraphContext (instead of RasterGraphContext) in the render function, and context.cmd.DispatchCompute(...)/context.cmd.SetComputeBufferParam(...) inside SetRenderFunc rather than calling ComputeShader.Dispatch directly — the render graph then handles synchronizing the compute queue against the graphics queue automatically (render-graph-compute-shader-run.html, and see the CommandBuffer-based Compute Buffer example in that section below).
RenderTexture Management
A RenderTexture is a GPU-resident texture Unity can render into, rather than the CPU-authored pixel data of a Texture2D. There are two lifecycle patterns: persistent — new RenderTexture(...) (or the manual constructor overloads) followed by an explicit .Create(), held for the object's lifetime, and explicitly .Release()d when done (class-RenderTexture-create.html, ScriptReference/RenderTexture-ctor.html) — and temporary/pooled — RenderTexture.GetTemporary(...) pulls a RenderTexture from Unity's internal pool matching the requested descriptor, and RenderTexture.ReleaseTemporary(...) returns it to the pool rather than destroying it, which is far cheaper for a texture only needed within a single frame or a single render pass (e.g. a scratch blur target). Never let a persistent RenderTexture go out of scope without calling Release() first — unlike most managed objects, the underlying GPU memory is not automatically reclaimed by the .NET garbage collector on a predictable schedule, so an unreleased RenderTexture is a real GPU memory leak, not just a soon-to-be-collected managed object. Key creation-time parameters: width/height, depth (depth-buffer bits: 0/16/24/32, distinct from texture depth), format (RenderTextureFormat, e.g. ARGB32, ARGBHalf, RFloat, Depth), and antiAliasing. enableRandomWrite (see Compute Shaders above) and useMipMap/autoGenerateMips must be set before .Create() — mutating most RenderTexture properties after creation either throws or silently does nothing until the texture is recreated.
using UnityEngine;
public class ScratchBlurTarget : MonoBehaviour
{
void RenderScratchPass(Camera cam)
{
// Pooled: cheap to acquire/release every frame, no leak risk if released.
RenderTexture temp = RenderTexture.GetTemporary(
cam.pixelWidth / 2, cam.pixelHeight / 2, 0, RenderTextureFormat.Default);
Graphics.Blit(null, temp); // fill temp with something, e.g. via a shader pass
// ... consume temp ...
RenderTexture.ReleaseTemporary(temp);
}
}
GraphicsBuffer for GPU Data
GraphicsBuffer is the newer, unified GPU buffer type that supersedes ComputeBuffer for most new code — where ComputeBuffer only exposes ComputeBufferType (Default/Structured/Raw/Append/Counter/IndirectArguments/Constant), GraphicsBuffer.Target adds Vertex and Index to that same list, meaning a single buffer type can back compute I/O, indirect draw arguments, and mesh vertex/index data, letting a compute shader write directly into a buffer later consumed as a mesh's geometry without a CPU round-trip (e.g. GPU-driven procedural geometry or particle systems). Construction takes a Target, a count (element count), and a stride (bytes per element, must match the HLSL struct's layout); UsageFlags.LockBufferForWrite combined with LockBufferForWrite/UnlockBufferAfterWrite gives direct-mapped CPU write access for streaming data in without a full SetData copy. GraphicsBuffer.IndirectDrawArgs/IndirectDrawIndexedArgs are the argument-struct layouts consumed by indirect draw calls sourced from GPU-computed instance/vertex counts.
using UnityEngine;
using UnityEngine.Rendering;
public class ParticlePositionBuffer : MonoBehaviour
{
public ComputeShader particleCompute;
GraphicsBuffer m_PositionBuffer;
int m_Kernel;
const int ParticleCount = 1024;
void OnEnable()
{
// 3 floats per particle position, matching a HLSL float3 with 12-byte stride.
m_PositionBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, ParticleCount, sizeof(float) * 3);
m_Kernel = particleCompute.FindKernel("CSMain");
particleCompute.SetBuffer(m_Kernel, "_Positions", m_PositionBuffer);
}
void Update()
{
particleCompute.SetFloat("_DeltaTime", Time.deltaTime);
particleCompute.Dispatch(m_Kernel, Mathf.CeilToInt(ParticleCount / 64f), 1, 1);
// m_PositionBuffer now holds updated positions on the GPU, ready for a
// DrawMeshInstancedIndirect / DrawProceduralIndirect call or a compute readback.
}
void OnDisable()
{
m_PositionBuffer?.Release();
m_PositionBuffer = null;
}
}
Reading buffer/texture data back to the CPU with GetData stalls the CPU until the GPU catches up if called too soon after the GPU work that produced the data — prefer AsyncGPUReadback.Request/RequestAsync (see Advanced Notes) when the read doesn't need to happen this exact frame.
Common Mistakes
| Mistake | Why / fix |
|---|---|
| RenderTexture/ComputeBuffer/GraphicsBuffer never released | GPU memory isn't reclaimed by .NET GC on a predictable schedule; always pair new RenderTexture(...)/new ComputeBuffer(...)/new GraphicsBuffer(...) with .Release() (or .Dispose()) in OnDisable/OnDestroy, and prefer GetTemporary/ReleaseTemporary for single-frame RenderTextures |
| Compute thread group math off by orders of magnitude | Dispatch(x, y, z) takes thread group counts, not thread counts; divide the total work size by the kernel's declared numthreads (via GetKernelThreadGroupSizes or the literal in the shader) and round up with Mathf.CeilToInt, or the compute misses/duplicates coverage at the edges |
Renderer Feature injected at the wrong RenderPassEvent |
e.g. reading a fully-lit scene color at BeforeRenderingOpaques (nothing's drawn yet) or trying to modify opaque geometry after AfterRenderingTransparents (too late); pick the event from custom-pass-injection-points.html that matches what buffers you need available |
Blitting in URP with CommandBuffer.Blit/Graphics.Blit/RenderingUtils.Blit |
Manual explicitly warns these APIs can break XR rendering and aren't compatible with native render passes in URP; use Blitter.BlitCameraTexture/Blitter.BlitTexture, or renderGraph.AddBlitPass/AddCopyPass inside a render graph pass, with a hand-coded (not Shader Graph) shader |
RenderTexture.enableRandomWrite set after .Create() |
Must be set before the texture is created (or the texture recreated) — setting it afterward has no effect, and the compute shader's UAV write silently fails or throws |
ScriptableRendererFeature.Create() used to allocate per-frame resources |
Create() runs on load/toggle/property-change, not every frame; put per-frame-safe logic in AddRenderPasses() instead, and keep AddRenderPasses() itself allocation-free since it runs once per camera every frame |
Forgetting to unsubscribe a RenderPipelineManager event |
A MonoBehaviour that subscribes in OnEnable without unsubscribing in OnDisable/OnDestroy leaks the delegate and can throw against a destroyed object; always pair the subscribe/unsubscribe |
Using RenderPipelineManager.beginCameraRendering for an effect that should apply everywhere |
Docs recommend a ScriptableRendererFeature for anything spanning multiple cameras/scenes/the whole project; RenderPipelineManager events are the right tool only for a targeted, script-driven single-camera/GameObject case |
Mixing legacy Execute/RTHandle-based pass code with render graph APIs in the same pass |
The render graph's recording/execution separation (RecordRenderGraph declares resources, the render function executes) is a different contract from the older direct Execute(context, ref renderingData) pattern — don't call cmd.SetRenderTarget/Blit directly inside RecordRenderGraph (that stage must not issue GPU commands) |
PassData capturing instance state via closure instead of fields |
SetRenderFunc should take a static method/lambda; capturing this/local variables in a non-static closure allocates a delegate every frame instead of reusing the PassData struct, and defeats one of the render graph's stated GC-allocation optimizations |
Compute buffer stride not matching the HLSL struct's actual byte layout |
HLSL struct packing rules (16-byte alignment boundaries) don't always match a naive sum of C# field sizes; a mismatched stride silently reads garbage/misaligned data rather than throwing |
Assuming ScriptableRenderPass/ScriptableRendererFeature/TextureHandle/RenderGraph are documented in base Unity ScriptReference |
These ship inside the URP/Core SRP packages, not ScriptReference/; use the in-Package Manager API docs or the Manual/urp/ pages for authoritative signatures |
Calling GetData() on a GPU buffer/texture right after the GPU work that wrote it |
Forces a CPU-GPU sync stall waiting for the GPU to finish; use AsyncGPUReadback.Request/RequestAsync when the result isn't needed the same frame |
Reusing one CommandBuffer instance across multiple cameras/passes without Clear()-ing it |
Commands accumulate across AddCommandBuffer calls if not cleared, silently re-executing stale draws; call cmd.Clear() before re-recording a reused buffer |
Quick Reference
| Type / Method | Purpose |
|---|---|
CommandBuffer |
Ordered list of GPU commands recorded on CPU, executed as a unit |
Camera.AddCommandBuffer(CameraEvent, CommandBuffer) |
Schedules a buffer at a fixed BIRP camera-event point |
Graphics.ExecuteCommandBuffer(CommandBuffer) |
Runs a buffer immediately, outside event scheduling |
CommandBuffer.Blit / .DrawMesh / .DrawProcedural / .CopyTexture / .DispatchCompute |
Core recorded commands: blit, draw, copy, compute dispatch |
ScriptableRenderPass |
Base class for a custom URP render pass; override RecordRenderGraph (render graph) or Execute (legacy) |
ScriptableRendererFeature |
Base class packaging one or more passes; Create() on load/change, AddRenderPasses() every frame/camera |
ScriptableRenderer.EnqueuePass(ScriptableRenderPass) |
Injects a pass instance into this frame's render loop |
RenderPassEvent |
Enum of injection points (BeforeRenderingShadows … AfterRendering) controlling when a pass runs |
RenderGraph.AddRasterRenderPass<T> |
Opens a render-graph raster pass scope, returns an IRasterRenderGraphBuilder |
RenderGraph.AddComputePass<T> |
Opens a render-graph compute pass scope, returns a compute builder |
RenderGraph.AddUnsafePass<T> |
Escape hatch pass type using UnsafeCommandBuffer for commands the raster/compute builders don't expose |
RenderGraph.CreateTexture / .ImportTexture |
Creates a new graph-managed texture, or wraps an existing RenderTexture/RTHandle as a TextureHandle |
RenderGraph.ImportBuffer |
Wraps an existing GraphicsBuffer as a BufferHandle for graph use |
builder.UseTexture / .SetRenderAttachment / .UseBuffer |
Declares a pass's texture/buffer inputs, color/depth attachments, and buffer read/write access |
builder.SetRenderFunc(static (data, context) => ...) |
Registers the static execution function the graph calls with resolved resources |
builder.AllowPassCulling(false) |
Opts a pass out of automatic dead-pass removal (debug/dev use, not production) |
Blitter.BlitCameraTexture / Blitter.BlitTexture |
URP-safe, XR-compatible blit using a hand-written shader (not Shader Graph) |
RenderPipelineManager.beginCameraRendering / endCameraRendering |
Per-camera SRP hook, fires for any active SRP |
RenderPipelineManager.beginContextRendering / endContextRendering |
Per-ScriptableRenderContext hook (batch of cameras) |
RenderPipelineManager.beginFrameRendering / endFrameRendering |
Per-frame hook |
RenderPipelineManager.activeRenderPipelineAssetChanged etc. |
Pipeline-lifecycle hooks (asset swap, pipeline created/disposed) |
ScriptableRenderContext |
Low-level SRP submission object (Cull, DrawRenderers, Submit) used by a fully custom RenderPipeline |
ComputeShader.FindKernel(name) |
Resolves a kernel function to its integer index |
ComputeShader.GetKernelThreadGroupSizes |
Reads a kernel's declared [numthreads(x,y,z)] back into C# |
ComputeShader.SetBuffer / .SetTexture / .SetInt / .SetFloat |
Binds data to a kernel before dispatch |
ComputeShader.Dispatch(kernel, x, y, z) |
Invokes a kernel across x*y*z thread groups |
ComputeBuffer |
GPU buffer type: Structured/Raw/Append/Counter/IndirectArguments/Constant targets |
GraphicsBuffer |
Newer unified GPU buffer type; adds Vertex/Index targets to ComputeBuffer's set |
GraphicsBuffer.LockBufferForWrite / .UnlockBufferAfterWrite |
Direct-mapped CPU write access without |
…(truncated)