Graphics.DrawProcedural on Vulkan - "Shader requires vertex data"

Hello!

I’m trying to render vertex data that exists in a StructuredBuffer, generated from a compute shader using Graphics.DrawProcedural.

Everything works perfectly on Windows DX11, but on Android (Vulkan) my data isn’t being rendered, and I get the following error in logcat:

Shader requires vertex data and is not compatible with DrawIndexedNullGeometry

I get a value of 72 for SystemInfo.maxComputeBufferInputsVertex, so the GPU definitely seems to support StructuredBuffers. I feel like this should definitely be working, so I’m wondering whether this is perhaps a Unity bug?
I’ve also confirmed that SUPPORT_STRUCTUREDBUFFER is being defined on Vulkan.

Details:

  • Unity Version: 2019.4.23f1
  • Graphics API: Vulkan
  • Phone: Xiaomi Mi 9T Pro
  • Android Version: 10 QKQ1.190825.002

Edit: I just tried it on the most recent version of Unity, 2021.1.3f1, and I get an identical error

Has anyone else had success getting Graphics.DrawProcedural to work on Android?

I’ve made a very pared-down example, which still has the error - literally just building a procedural quad on the CPU and sending it to a shader as a ComputeBuffer. Works on PC, fails with the same error on Android.

The example project is attached to the post (31kB unity project)

=====================================================================

Here’s the code:

using UnityEngine;

public class MinimalTest : MonoBehaviour
{
    public Material DrawMaterial;
    private ComputeBuffer _buffer;
    private GraphicsBuffer _indexBuffer;
    private Bounds _bounds;

    public void Awake()
    {
        _buffer = new ComputeBuffer(4, sizeof(float) * 4);
        _buffer.SetData(new[]
        {
            new Vector4(-0.5f, 0f, -0.5f, 0.0f),
            new Vector4(-0.5f, 0f,  0.5f, 0.3f),
            new Vector4( 0.5f, 0f,  0.5f, 0.6f),
            new Vector4( 0.5f, 0f, -0.5f, 1.0f),
        });

        _indexBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Index, 6, sizeof(int));
        _indexBuffer.SetData(new[] {0, 1, 2, 2, 3, 0});
      
        _bounds = new Bounds(Vector3.zero, Vector3.one * 10000f);
      
        DrawMaterial.SetBuffer("_VertexBuffer", _buffer);
    }

    public void Update()
    {
        Graphics.DrawProcedural(DrawMaterial, _bounds, MeshTopology.Triangles, _indexBuffer, _indexBuffer.count);
    }

    public void OnDestroy()
    {
        _buffer.Release();
        _indexBuffer.Release();
    }
}

And here’s the shader:

Shader "Test/MinimalTest"
{
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        CGPROGRAM
        #pragma surface surf Standard fullforwardshadows vertex:vert
        #pragma target 5.0
     
#if SHADER_TARGET >= 35 && (defined(SHADER_API_D3D11) || defined(SHADER_API_GLES3) || defined(SHADER_API_GLCORE) || defined(SHADER_API_XBOXONE) || defined(SHADER_API_PSSL) || defined(SHADER_API_SWITCH) || defined(SHADER_API_VULKAN) || (defined(SHADER_API_METAL) && defined(UNITY_COMPILER_HLSLCC)))
        #define SUPPORT_STRUCTUREDBUFFER
#endif
     
#ifdef SUPPORT_STRUCTUREDBUFFER
        StructuredBuffer<float4> _VertexBuffer;
#endif

        struct Input {
            float4 color : COLOR;
        };
     
        struct appdata
        {
            float4 vertex : POSITION;
            float3 normal : NORMAL;
            uint vid : SV_VertexID;
        };
     
        void vert(inout appdata v, out Input o)
        {
            UNITY_INITIALIZE_OUTPUT(Input, o);
#ifdef SUPPORT_STRUCTUREDBUFFER
            float4 CurrentVertex = _VertexBuffer[v.vid];
         
            v.vertex.xyz = CurrentVertex;
            v.normal = float3(0, 0, 1);
            o.color = float4(1, 1, 1, 1);
#endif
        }

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            o.Albedo = float4(1, 1, 1, 1);
        }
        ENDCG
    }
}

7045669–835468–AndroidDrawProcedural_MinimalTest.zip (30.5 KB)

Your vertex shader (indirectly) reads the vertex position of appdata and because DrawProcedural does not bind any vertex buffers we generate an error and skip the draw to avoid potentially crashing.

As a workaround for this case you can add ‘v.vertex.w = 0.0;’ after ‘v.vertex.xyz = CurrentVertex;’.
By overwriting ‘v.vertex’ completely Unity’s shader compiler can strip the code that would otherwise read the input vertex position.

Oohhhh snap it works! THANK YOU!!!

This is actually a huge deal for me - I’ve basically given up on Android / Oculus Quest development for the last two-ish years since this problem broke all my compute shader stuff and that’s 90% of my work. I’m so happy right now c’:

It would be cool if the error was a little more specific or if perhaps the documentation on DrawProcedural mentioned the requirement that the vertex shader completely overwrites the data with the POSITION semantic.

There is a solution for this that is not even a workaround:

void vert(inout appdata v, out Input o)

Should actually be

void vert(out appdata v, out Input o)

The inout qualifier means that it wants to read from it. By explicitly specifying it as out there is no need to do any fancy tricks to get the compiler to optimize it away. And that’s the intented way to use it. inout basically means that the shader indeed wants a vertexbuffer, and thus cannot be used with the DrawProcedural.

Though in this particular case it’s not fully supported, as the surface shader system wants you to actually have a vertexbuffer.

That makes a lot of sense - thanks for the clarity, I’ll definitely do it that way from now on!