Custom Render Pass as Vulkan Subpass

Hi everyone,

I’m trying to implement a Vulkan subpass for color correction as described in the Meta documentation (link to article). My goal is to apply color correction while the render texture is still in tile memory to avoid unnecessary load/store operations between passes. I’m running this on Meta Quest 2 using Unity 2022.3.17f and URP Core RP version 14.0.9.


The Problem

When I enable my custom render pass, I get the following error:

Assertion failed
UnityEngine.Rendering.Blitter:BlitCameraTexture
VulkanColorCorrectionFeature/VulkanColorCorrectionPass:Execute (at Assets/Scripts/Rendering/VulkanColorCorrectionFeature.cs:38)
UnityEngine.GUIUtility:ProcessEvent (int,intptr,bool&)

It seems like something is wrong with how I’m setting up the render target or handling the camera color input. The Frame Debugger shows that when I enable my render pass the rendering pipeline fails right at the beginning, and notthing gets executed.


What I’ve Tried

  1. Custom Render Pass:
    I’ve written a ScriptableRendererFeature with a custom render pass. The pass attempts to use Blitter.BlitCameraTexture to apply the material with my color correction shader. Here’s the core part of my Execute method:

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) {
        CommandBuffer cmd = CommandBufferPool.Get("Color Correction Buffer");
    
        using (new ProfilingScope(cmd, new ProfilingSampler("Vulkan Color Correction"))) {
            RTHandle colorTarget = renderingData.cameraData.renderer.cameraColorTargetHandle;
            Blitter.BlitCameraTexture(cmd, colorTarget, colorTarget, 0, material);
        }
    
        context.ExecuteCommandBuffer(cmd);
        CommandBufferPool.Release(cmd);
    }
    
  2. Shader:
    My shader is a basic color correction pass using _CameraOpaqueTexture:

    TEXTURE2D_X(_CameraOpaqueTexture);
    SAMPLER(sampler_CameraOpaqueTexture);
    
    half4 frag(Varyings input) : SV_Target {
        float4 color = SAMPLE_TEXTURE2D_X(_CameraOpaqueTexture, sampler_CameraOpaqueTexture, input.uv);
        return color * float4(1, 1, 1, 1);
    }
    
  3. Render Pass Event:
    I’ve tried several injection points like AfterRenderingPostProcessing and BeforeRenderingPostProcessing, but the error persists.


What I Suspect

  • I might be incorrectly handling the render target (cameraColorTargetHandle).
  • Blitter.BlitCameraTexture could be the wrong approach for this use case in XR.
  • I may need to use DrawProcedural instead of BlitCameraTexture, as the article suggests explicitly avoiding load operations.

My Questions

  1. How do I correctly set up the render target for Vulkan subpasses in URP?
    I want to ensure the color correction happens in tile memory and avoid load/store to RAM.
  2. Is Blitter.BlitCameraTexture compatible with Vulkan subpasses on XR, or should I switch to DrawProcedural?
  3. Are there specific XR-related considerations I’m missing when handling render targets or texture arrays?

Any guidance, examples, or relevant documentation links would be greatly appreciated!


Environment Details

  • Target Platform: Meta Quest 2
  • Unity Version: 2022.3.17f
  • URP Core RP Version: 14.0.9

Full Code

Custom Render Feature

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class VulkanColorCorrectionFeature : ScriptableRendererFeature {
    public Material colorCorrectionMaterial;
    public RenderPassEvent renderPassEvent = RenderPassEvent.AfterRenderingPostProcessing;

    private VulkanColorCorrectionPass colorCorrectionPass;

    public override void Create() {
        colorCorrectionPass = new VulkanColorCorrectionPass(colorCorrectionMaterial, renderPassEvent);
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) {
        if (colorCorrectionMaterial == null) {
            Debug.LogWarning("VulkanColorCorrectionFeature: Missing material. Pass will not execute.");
            return;
        }

        if (renderingData.cameraData.cameraType == CameraType.Game) {
            renderer.EnqueuePass(colorCorrectionPass);
        }
    }

    class VulkanColorCorrectionPass : ScriptableRenderPass {
        private Material material;

        public VulkanColorCorrectionPass(Material material, RenderPassEvent renderPassEvent) {
            this.material = material;
            this.renderPassEvent = renderPassEvent;
        }

        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) {
            CommandBuffer cmd = CommandBufferPool.Get("Color Correction Buffer");

            using (new ProfilingScope(cmd, new ProfilingSampler("Vulkan Color Correction"))) {
                RTHandle colorTarget = renderingData.cameraData.renderer.cameraColorTargetHandle;
                Blitter.BlitCameraTexture(cmd, colorTarget, colorTarget, 0, material);
            }

            context.ExecuteCommandBuffer(cmd);
            CommandBufferPool.Release(cmd);
        }
    }
}

Shader Code

Shader "ColorBlit" {
    SubShader {
        Tags { "RenderType"="Opaque" "RenderPipeline" = "UniversalPipeline"}
        LOD 100
        ZWrite Off Cull Off
        Pass {
            Name "ColorBlitPass"

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            struct Attributes {
                float4 positionHCS   : POSITION;
                float2 uv           : TEXCOORD0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct Varyings {
                float4  positionCS  : SV_POSITION;
                float2  uv          : TEXCOORD0;
                UNITY_VERTEX_OUTPUT_STEREO
            };

            Varyings vert(Attributes input) {
                Varyings output;
                UNITY_SETUP_INSTANCE_ID(input);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);

                output.positionCS = float4(input.positionHCS.xyz, 1.0);
                output.uv = input.uv;
                return output;
            }

            TEXTURE2D_X(_CameraOpaqueTexture);
            SAMPLER(sampler_CameraOpaqueTexture);

            half4 frag (Varyings input) : SV_Target {
                float4 color = SAMPLE_TEXTURE2D_X(_CameraOpaqueTexture, sampler_CameraOpaqueTexture, input.uv);
                return color * float4(1, 1, 1, 1);
            }
            ENDHLSL
        }
    }
}

Thanks in advance for any help!