Shader stops rendering when assets are updated

So I’ve been having this problem for the last few days and have been unable to find anything in the forums or elsewhere.

Everything works fine until, at the slightest change in assets, my shader stops working entirely. There isn’t even any wireframe showing up, so it’s not just a texture problem. I assume that means my shader breaks when it’s reloaded by the editor but I have no idea how. And if it is, I don’t know whether I did something wrong and where.

I have a custom instanced shader where I use SV_InstanceID and a structured buffer of a custom struct to retrieve my instances’ data:

StructuredBuffer<Properties> _Properties;

Properties InstanceProperties(uint instanceID)
{
    return _Properties[instanceID];
}

Interpolator vert (MeshVertex mesh, uint instanceID: SV_InstanceID)
{
    Interpolator interpolator;
    
    // my custom instance struct
    Properties properties = InstanceProperties(instanceID);
    
    // the transform matrix is computed in csharp
    float4 pos = mul(properties.mat, mesh.position);
    interpolator.position = UnityObjectToClipPos(pos); // : SV_Position

    // ... setting the interpolator up
    return interpolator;
}

On the C# side, my ComputeBuffer is created once, and assigned to my material at the same time with the following code:

propertiesBuffer = new ComputeBuffer(/* args */);
material.SetBuffer("_Properties", propertiesBuffer);

Then whenever my custom render pass needs to render, I update my buffer with ComputeBuffer.BeginWrite and ComputeBuffer.EndWrite. And render using CommandBuffer.DrawMeshInstancedIndirect. (Note: I had the same problem when I was using Graphics.DrawMeshInstancedIndirect in an Update so I don’t think the render pass is the issue here).

public void Draw(CommandBuffer cmd)
{
    cmd.DrawMeshInstancedIndirect(mesh, 0, material, -1, argsBuffer);
}

The material is created at runtime from the shader:

material = new Material(shader);
material.enableInstancing = true;

And the shader is retrieved using addressables:

Addressables.LoadAssetAsync<Shader>("Sprite shader").Completed += handle =>
{
    if (handle.Status == AsyncOperationStatus.Succeeded)
    {
        shader = handle.Result;
    }
    else
    {
        Debug.LogError("Failed to load sprite shader.");
    }
};

I don’t know what step is broken by the asset hot reload, does anyone have a clue ?

I’ve managed to find a solution to my problem!

It turns out compute buffers are broken by an AssetDatabse refresh, but their IsValid() method does not reflect that.

What I ended up doing is creating an AssetPostprocessor so that I could implement the OnPostprocessAllAssets callback (this script is in an Editor folder).

class ComputeBufferResetter : AssetPostprocessor
{
    static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
    {
        // GameObject static method
        RenderManager.ResetComputeBuffers();
    }
}

I then wait one frame before releasing and creating my compute buffers back. If I do it on the spot, it has no effect.

public static void ResetComputeBuffers()
{
    instance?.StartCoroutine(instance.ResetComputeBuffersRoutine());
}

private IEnumerator ResetComputeBuffersRoutine()
{
    yield return new WaitForEndOfFrame();
    // this call resets the buffer in my code
    InstancedMeshDrawer.ResetComputeBuffers();
}

The only drawback I’ve encountered so far is that there IS indeed a missing frame. Unless someone has a solution to this problem, I’ll accommodate. I can’t sink more days into this editor problem.

1 Like

I’m seeing the same thing. Thanks for writing a follow-up! A few precisions from what I’m observing on my side: the ComputeBuffers seem to survive fine, but it seems only to be the material.SetBuffer("_Properties", propertiesBuffer); link that gets lost. I can fix it by calling SetBuffer() again (in the next frame, as you suggest).

More precisely, it seems fine if I call SetBuffer again in the next OnPreCull event, which also avoids the missing frame problem. Doesn’t it also work if you replace yield return new WaitForEndOfFrame(); with yield return null; in your case? I would guess that the problem is that it must be done “later”, but it doesn’t necessarily have to be after the next frame was rendered.

We have the same problem! Due to this, all instanced geometry disappears after any material, shader, or script is changed in play mode. We have hundreds of materials with buffers and it seems that the only solution is to call SetBuffer on each of them after asset refresh.

If anyone from Unity sees this, can you please fix it? It would be highly appreciated.

My understanding is that Unity has to serialize and deserialize temporary materials when reloading assets. All shader uniforms that don’t have a corresponding Shaderlab property get lost. So one way to fix that would be to add a shaderlab property for the uniform. Unfortunately, there is no shaderlab property for buffers, as far as I know. That only leaves you with setting the buffer again after refreshing (or setting it every frame).