Search Unity

Weird bug with simple setup of a counter-type compute buffer

Discussion in 'Shaders' started by Mytino, Apr 6, 2020.

  1. Mytino

    Mytino

    Joined:
    Jan 28, 2019
    Posts:
    16
    Here's a download link for the project: https://drive.google.com/open?id=1VQMyjX6zqWHGj7kB_iYE6V34evUVqYpo
    The project is a simplified version of a larger project I had, where I removed everything that was unrelated to the bug.

    I'm making a particle system with compute shaders. I'm using a compute buffer of type
    ComputeBufferType.Counter to track how many particles exist at the current moment. My problem is that the particles won't render correctly whenever I remove particles from the buffer. You can see this happening in the video below. I hold shift+left click to remove particles, but whenever it's held down, the particles all render at 0,0. You can see they're all there because I have an opacity of 0.2 black for each particle. When I hold left click without shift I place particles, and that works completely fine.

    Video: https://i.imgur.com/Gp0lqKC.mp4

    As you can see in the project, I have two compute buffers for the particles; one for positions (two floats) and one for color (4 floats). Only the position buffer is of type counter. I could've put everything in a single particle-struct compute buffer of type counter, but I want to go the structure-of-arrays way for better performance.

    When I hold shift and left click, I call this code:
    Code (CSharp):
    1. kernel = simCs.FindKernel("DestroyParticles");
    2. posBufW.SetCounterValue(0);
    3. simCs.SetBuffer(kernel, "PosBufR", posBufR);
    4. simCs.SetBuffer(kernel, "PosBufW", posBufW);
    5. simCs.SetBuffer(kernel, "ColorBufR", colorBufR);
    6. simCs.SetBuffer(kernel, "ColorBufW", colorBufW);
    7. simCs.SetFloats("DestroyParticlesPos", new float[] {mousePosWorld.x, mousePosWorld.y});
    8. simCs.SetFloat("DestroyParticlesRadius", 20f);
    9. simCs.Dispatch(kernel, threadGroups, 1, 1);
    10. VariableUtils.Swap(ref posBufR, ref posBufW);
    11. VariableUtils.Swap(ref colorBufR, ref colorBufW);
    posBufR and posBufW are respectfully the read and write buffer for the position data of the particles. colorBufR and colorBufW are the read and write buffer for the color data of the particles. I dispatch the "DestroyParticles" kernel, and swap the buffers. I then later render using posBufR and colorBufR which now contains the new data with the removed particles.

    Here's the full compute shader:
    Code (CSharp):
    1. #define THREADS_PER_GROUP_X 512
    2.  
    3. StructuredBuffer<float2> PosBufR; RWStructuredBuffer<float2> PosBufW;
    4. StructuredBuffer<float4> ColorBufR; RWStructuredBuffer<float4> ColorBufW;
    5.  
    6. uint ParticleCount;
    7.  
    8. float2 AddParticlesPos;
    9. int AddParticlesType;
    10. #pragma kernel AddParticles
    11. [numthreads(1, 1, 1)]
    12. void AddParticles(uint id : SV_DispatchThreadID) {
    13.     uint j = PosBufW.IncrementCounter();
    14.     PosBufW[j] = AddParticlesPos;
    15.     ColorBufW[j] = float4(0.0, 0, 0, 0.2);
    16. }
    17.  
    18. float2 DestroyParticlesPos;
    19. float DestroyParticlesRadius;
    20. #pragma kernel DestroyParticles
    21. [numthreads(THREADS_PER_GROUP_X, 1, 1)]
    22. void DestroyParticles(uint id : SV_DispatchThreadID) {
    23.     if (id < ParticleCount) {
    24.         float2 pos = PosBufR[id];
    25.         if (distance(pos, DestroyParticlesPos) > DestroyParticlesRadius) {
    26.             uint j = PosBufW.IncrementCounter();
    27.             PosBufW[j] = pos;
    28.             ColorBufW[j] = ColorBufR[id];
    29.         }
    30.     }
    31. }
    The way DestroyParticles works, is by reading over all particles, and only writing the particles that are outside a radius around the mouse cursor. Here you can also see the reason I need counter buffers as opposed to append buffers; because I need the "j" index so I can make sure the pos and color data is for the same particle. If I were to do two append calls to each buffer, the data could be shuffled and particles would switch colors.

    After DestroyParticles is dispatched and the buffers are swapped, posBufR should now have the new counter value that determines the ParticleCount to use, as well as how many particles to render.

    I render particles like this on the CPU side:
    Code (CSharp):
    1. void OnRenderObject() {
    2.         visualizationMaterial.SetPass(0);
    3.         visualizationMaterial.SetBuffer("VertexBufR", vertexBuf);
    4.         visualizationMaterial.SetBuffer("ColorBufR", colorBufR);
    5.         visualizationMaterial.SetBuffer("PosBufR", posBufR);
    6.         Graphics.DrawProceduralIndirectNow(MeshTopology.Triangles, drawArgBuf, 0);
    7. }
    With shader code:
    Code (CSharp):
    1. Shader "Custom/Visualization" {
    2.     SubShader {
    3.         Pass {
    4.             ZTest Always Cull Off ZWrite Off
    5.             Fog { Mode off }
    6.             Blend SrcAlpha OneMinusSrcAlpha, One OneMinusSrcAlpha
    7.  
    8.             CGPROGRAM
    9.  
    10.             #include "UnityCG.cginc"
    11.             #pragma target 5.0
    12.             #pragma vertex Vertex
    13.             #pragma fragment Fragment
    14.  
    15.             uniform StructuredBuffer<float2> VertexBufR;
    16.             uniform StructuredBuffer<float2> PosBufR;
    17.             uniform StructuredBuffer<float4> ColorBufR;
    18.  
    19.             struct VertexOutput {
    20.                 float4 pos : SV_POSITION;
    21.                 float4 color : COLOR;
    22.             };
    23.  
    24.             VertexOutput Vertex(uint instanceId : SV_InstanceID, uint vertexId : SV_VertexID) {
    25.                 VertexOutput output;
    26.                 output.pos = UnityObjectToClipPos(float4(PosBufR[instanceId] + VertexBufR[vertexId], 0, 1));
    27.                 output.color = ColorBufR[instanceId];
    28.                 return output;
    29.             }
    30.  
    31.             float4 Fragment(VertexOutput input) : COLOR {
    32.                 return input.color;
    33.             }
    34.  
    35.             ENDCG
    36.         }
    37.     }
    38. }
    39.  
    Again, see the project download link above for how the VertexBufR is built. This is just a vertex buffer for an individual particle circle.

    In order to make sure the counter value is correct before rendering, I do some hacky code like this:
    Code (CSharp):
    1.     void OnParticleCountUpdate() {
    2.         ComputeBuffer.CopyCount(posBufR, drawArgBuf, sizeof(int));
    3.    
    4.         int[] arguments = new int[4];
    5.         drawArgBuf.GetData(arguments);
    6.         particleCount = (uint)arguments[1];
    7.  
    8.         posBufW.SetCounterValue(particleCount);
    9.         simCs.SetInt("ParticleCount", (int)particleCount);
    10.     }
    I can't figure out any other way to do it, but commenting out this function call avoids the bug. CopyCount copies the counter value into the indirect argument buffer at index 1 (sizeof(int)*1) which is the number of instances to render. This actually runs fine, it's the rest that causes the bug: I get the data because I need the count in order to update the ParticleCount uniform which I use in almost every kernel in my actual project. I then set the counter value for the posBufW buffer, which is weird in itself. Thinking back at it, I added this code and it fixed a bug, and I didn't think back at why. Removing it now still brings back that similar second bug, which is a sort of flickering which might be related. It's strange that it does this because I set its counter value to 0 before I run the dispatch and swap it with posBufR. It's almost like the SetCounterValue is buggy in a way internal to Unity. This could be the cause for both issues.

    Can anyone confirm the issue shown in the video? And do you know any workarounds or solutions to this problem? Do you think it's an internal Unity bug?

    Thank you
     
    Last edited: Apr 6, 2020
  2. Mytino

    Mytino

    Joined:
    Jan 28, 2019
    Posts:
    16
    I found a solution! :D

    It seems the cause of the bug was "Graphics.DrawProceduralIndirectNow(MeshTopology.Triangles, drawArgBuf, 0);" in OnRenderObject posted above. The solution is sort of a workaround solution: Instead of using DrawProceduralIndirectNow, I use a command buffer, and add DrawProceduralIndirect to it, then execute it. This renders all particles perfectly as can be seen here: https://i.imgur.com/V5UCcdn.mp4

    The new working OnRenderObject code then looks like this:
    Code (CSharp):
    1. void OnRenderObject() {
    2.     visualizationMaterial.SetPass(0);
    3.     visualizationMaterial.SetBuffer("VertexBufR", vertexBuf);
    4.     visualizationMaterial.SetBuffer("ColorBufR", colorBufR);
    5.     visualizationMaterial.SetBuffer("PosBufR", posBufR);
    6.    
    7.     CommandBuffer cb = new CommandBuffer();
    8.     cb.Clear();
    9.     cb.DrawProceduralIndirect(Matrix4x4.identity, visualizationMaterial, 0, MeshTopology.Triangles, drawArgBuf, 0);
    10.     Graphics.ExecuteCommandBuffer(cb);
    11. }
    The command buffer creation can be done outside this event.

    This explains why the bug was purely visual.

    Not sure why this bug happens, as I would expect the "Now" keyword to render it immediately, and in that case I can't see why the two versions would yield different results. So I will report it as a bug.

    As for the weirdness with "posBufW.SetCounterValue(particleCount);" in "OnParticleCountUpdate" explained above, I figured this out as well. It turns out you can indeed remove that line, but you have to initialize the counter, for example by just doing "posBufW.SetCounterValue(0);" after you create the compute buffer. If I don't, all particles render at 0,0, even before I've done anything with posBufW. Kind of strange, it's like a silent error or crash or something.