Compute Shader demo conversion to C#

Godot Version

Godot v4.4.stable.mono - Windows 10 (build 19045) - Multi-window, 1 monitor - Vulkan (Forward+) - dedicated AMD Radeon RX 5700 XT (Advanced Micro Devices, Inc.; 32.0.12033.1030) - AMD Ryzen 9 3900X 12-Core Processor (24 threads)

Question

I’m trying to convert the Compute water ripple shader demo written by Bastiaan Olij (@mux213) from GdScript to C#. But I’ve gotten stuck.

Can anyone see the issue?

using Godot;
using System;

//[Tool]
public partial class water_plane : Area3D
{
    [Export] public float RainSize = 3.0f;
    [Export] public float MouseSize = 5.0f;
    [Export] public Vector2I TextureSize = new Vector2I(512, 512);
    [Export(PropertyHint.Range, "1.0,10.0,0.1")] public float Damp = 1.0f;

    private float t = 0.0f;
    private float maxT = 0.1f;

    private Texture2Drd texture;
    private int nextTexture = 0;

    private Vector4 addWavePoint;
    private Vector2 mousePos;
    private bool mousePressed = false;

    public override void _Ready()
    {
        RenderingServer.CallOnRenderThread(Callable.From(() => InitializeComputeCode(TextureSize)));

        var material = GetNode<MeshInstance3D>("MeshInstance3D").MaterialOverride as ShaderMaterial;
        if (material != null)
        {
            material.SetShaderParameter("effect_texture_size", TextureSize);
            texture = (Texture2Drd)material.GetShaderParameter("effect_texture");
            if(texture == null)
                GD.PrintErr("Shader parameter 'effect_texture' is null.");
        }
        else
            GD.PrintErr("Water plane material is null.");
    }

    public override void _ExitTree()
    {
        if (texture != null)
            texture.TextureRdRid = new Rid();
        else
            GD.PrintErr("texture is null");
        RenderingServer.CallOnRenderThread(Callable.From(FreeComputeResources));
    }

    public override void _UnhandledInput(InputEvent @event)
    {
        if (Engine.IsEditorHint()) return;

        if (@event is InputEventMouseMotion or InputEventMouseButton)
            mousePos = ((InputEventMouse)@event).GlobalPosition;

        if (@event is InputEventMouseButton mouseEvent && mouseEvent.ButtonIndex == MouseButton.Left)
            mousePressed = mouseEvent.Pressed;
    }

    private void CheckMousePos()
    {
        var camera = GetViewport().GetCamera3D();
        var parameters = new PhysicsRayQueryParameters3D()
        {
            From = camera.ProjectRayOrigin(mousePos),
            To = camera.ProjectRayOrigin(mousePos) + camera.ProjectRayNormal(mousePos) * 100.0f,
            CollisionMask = 1,
            CollideWithBodies = false,
            CollideWithAreas = true
        };


        var result = GetWorld3D().DirectSpaceState.IntersectRay(parameters);
        if (result.Count > 0)
        {
            var pos = GlobalTransform.AffineInverse() * ((Vector3)result["position"]);
            addWavePoint.X = Mathf.Clamp(pos.X / 5.0f, -0.5f, 0.5f) * TextureSize.X + 0.5f * TextureSize.X;
            addWavePoint.Y = Mathf.Clamp(pos.Z / 5.0f, -0.5f, 0.5f) * TextureSize.Y + 0.5f * TextureSize.Y;
            addWavePoint.W = 1.0f;
        }
        else
        {
            addWavePoint = Vector4.Zero;
        }
    }

    public override void _Process(double delta)
    {
        if (Engine.IsEditorHint())
        {
            addWavePoint.W = 0.0f;
        }
        else
        {
            CheckMousePos();
        }

        if (addWavePoint.W == 0.0f)
        {
            t += (float)delta;
            if (t > maxT)
            {
                t = 0;
                addWavePoint.X = GD.RandRange(0, TextureSize.X);
                addWavePoint.Y = GD.RandRange(0, TextureSize.Y);
                addWavePoint.Z = RainSize;
            }
            else
            {
                addWavePoint.Z = 0.0f;
            }
        }
        else
        {
            addWavePoint.Z = mousePressed ? MouseSize : 0.0f;
        }

        nextTexture = (nextTexture + 1) % 3;


        if (texture != null)
        {
            texture.TextureRdRid = textureRds[nextTexture];
        }
        else
        {
            GD.PrintErr("Texture update failed: Invalid texture or index.");
        }

        RenderingServer.CallOnRenderThread(Callable.From(() => RenderProcess(nextTexture, addWavePoint, TextureSize, Damp)));
    }

    private RenderingDevice rd;
    private Rid shader;
    private Rid pipeline;

    private Rid[] textureRds = new Rid[3];
    private Rid[] textureSets = new Rid[3];

    private Rid CreateUniformSet(Rid textureRd)
    {
        var uniform = new RDUniform()
        {
            UniformType = RenderingDevice.UniformType.Image,
            Binding = 0
        };
        uniform.AddId(textureRd);
        return rd.UniformSetCreate([uniform], shader, 0);
    }

    private void InitializeComputeCode(Vector2I initWithTextureSize)
    {
        rd = RenderingServer.CreateLocalRenderingDevice();

        var shaderFile = GD.Load<RDShaderFile>("res://water_plane/water_compute.glsl");
        var shaderSpirv = shaderFile.GetSpirV();
        shader = rd.ShaderCreateFromSpirV(shaderSpirv);
        pipeline = rd.ComputePipelineCreate(shader);

        var tf = new RDTextureFormat()
        {
            Format = RenderingDevice.DataFormat.R32Sfloat,
            TextureType = RenderingDevice.TextureType.Type2D,
            Width = (uint)initWithTextureSize.X,
            Height = (uint)initWithTextureSize.Y,
            Depth = 1,
            ArrayLayers = 1,
            Mipmaps = 1,
            UsageBits = RenderingDevice.TextureUsageBits.SamplingBit |
                         RenderingDevice.TextureUsageBits.ColorAttachmentBit |
                         RenderingDevice.TextureUsageBits.StorageBit |
                         RenderingDevice.TextureUsageBits.CanUpdateBit |
                         RenderingDevice.TextureUsageBits.CanCopyToBit
        };

        for (int i = 0; i < 3; i++)
        {
            textureRds[i] = rd.TextureCreate(tf, new RDTextureView(), []);
            if (textureRds == null)
            {
                GD.PrintErr("Failed to create texture!");
            }
            rd.TextureClear(textureRds[i], new Color(0, 0, 0, 0), 0, 1, 0, 1);
            textureSets[i] = CreateUniformSet(textureRds[i]);
        }
    }


    public void RenderProcess(int withNextTexture, Vector4 wavePoint, Vector2I texSize, float pDamp)
    {
        float[] pushConstant = new float[]
        {
            wavePoint.X, wavePoint.Y, wavePoint.Z, wavePoint.W,
            texSize.X, texSize.Y, pDamp, 0.0f
        };

        uint xGroups = (uint)(texSize.X - 1) / 8 + 1;
        uint yGroups = (uint)(texSize.Y - 1) / 8 + 1;

        Rid nextSet = textureSets[withNextTexture];
        Rid currentSet = textureSets[(withNextTexture + 2) % 3];
        Rid previousSet = textureSets[(withNextTexture + 1) % 3];

        // Convert float list to byte array
        byte[] pushConstantBytes = new byte[pushConstant.Length * 4];
        Buffer.BlockCopy(pushConstant, 0, pushConstantBytes, 0, pushConstantBytes.Length);

        long computeList = rd.ComputeListBegin();
        rd.ComputeListBindComputePipeline(computeList, pipeline);
        rd.ComputeListBindUniformSet(computeList, currentSet, 0);
        rd.ComputeListBindUniformSet(computeList, previousSet, 1);
        rd.ComputeListBindUniformSet(computeList, nextSet, 2);
        rd.ComputeListSetPushConstant(computeList, pushConstantBytes, (uint)pushConstantBytes.Length);
        rd.ComputeListDispatch(computeList, xGroups, yGroups, 1);
        rd.ComputeListEnd();

        rd.Submit();
        rd.Sync();
    }

    private void FreeComputeResources()
    {
        foreach (var rid in textureRds)
        {
            if (rid.IsValid)
                rd.FreeRid(rid);
        }
        if (shader.IsValid)
            rd.FreeRid(shader);
    }
}

1 Like

Why are you trying to convert it?

The .NET version of Godot supports GDScript and C#. Just use the existing one as-is.

1 Like

You can still use the GDScript one in a .NET project.
Unless this is for practice, it’s almost entirely pointless.

I can code in either of them, true. But there is a performance increases by using C#. I could keep performance testing different scenarios as I further develop, but since C# shows better result each time, it’s just be faster to do everything directly in C# and skip the comparisons. I’m making a VR game, so high frame rate and high resolution is important, I need every performance gain I can get.

This is the first time I’m using compute shader though. So converting it gave me a teaching opportunity, except I failed. From my different variable prints, the values are correct though, the byte data as well.

1 Like

You didn’t fail (yet); you just haven’t succeeded (yet). Keep taking a look piece by piece.

1 Like

Can you describe specifically how and where it is failing?

(Disclaimer: I likely can’t answer your question; I don’t know enough about compute shaders. But I’m interested in seeing the answer and learning.)

You’ve given people nothing to go on except a big wall of code; so even people who might be able to find the answer are going to be less inclined to help, since it just a big block of code that you’re saying does not work, without any further details. If you can get more detailed with what seems to be going wrong, if you can narrow down where you think the issue is, people might respond more.

1 Like

Thank you for the encouragement :slight_smile:

        GD.Print("Push Constant Data:");
        for (int i = 0; i < 8; i++)
        {
            string hexValues = "";
            for (int j = 0; j < 4; j++)
            {
                hexValues += pushConstantBytes[i * 4 + j].ToString("X2") + " ";
            }

            float value = BitConverter.ToSingle(pushConstantBytes, i * 4);
            GD.Print($"[{hexValues}] -> {value}");
        }

From above code prints the byte data and also prints the byte converted to float. The values matches the initial float values. The code is added in the RenderProcess function and I get the result as bellow, which is as it should be:
Push Constant Data:

[00 00 04 43 ] → 132 (wavePoint.X)
[00 80 F4 43 ] → 489 (wavePoint.Y)
[00 00 40 40 ] → 3 (wavePoint.Z, rainsize)
[00 00 00 00 ] → 0 (wavePoint.W, (ab)used as boolean)
[00 00 00 44 ] → 512 (texSize.X)
[00 00 00 44 ] → 512 (texSize.Y)
[00 00 80 3F ] → 1 (pDamp)
[00 00 00 00 ] → 0 (0.0)

Godot gives the error every tick:

E 0:00:02:719 NativeCalls.cs:2479 @ void Godot.NativeCalls.godot_icall_1_274(nint, nint, Godot.Rid): Condition “!RD::get_singleton()->texture_is_valid(p_texture_rd_rid)” is true.
<C++ Source> scene/resources/texture_rd.cpp:90 @ _set_texture_rd_rid()
NativeCalls.cs:2479 @ void Godot.NativeCalls.godot_icall_1_274(nint, nint, Godot.Rid)
Texture2Drd.cs:62 @ void Godot.Texture2Drd.SetTextureRdRid(Godot.Rid)
Texture2Drd.cs:26 @ void Godot.Texture2Drd.set_TextureRdRid(Godot.Rid)
water_plane.cs:125 @ void water_plane._Process(double)
Node.cs:2536 @ bool Godot.Node.InvokeGodotClassMethod(Godot.NativeInterop.godot_string_name&, Godot.NativeInterop.NativeVariantPtrArgs, Godot.NativeInterop.godot_variant&)
Node3D.cs:1101 @ bool Godot.Node3D.InvokeGodotClassMethod(Godot.NativeInterop.godot_string_name&, Godot.NativeInterop.NativeVariantPtrArgs, Godot.NativeInterop.godot_variant&)
CollisionObject3D.cs:607 @ bool Godot.CollisionObject3D.InvokeGodotClassMethod(Godot.NativeInterop.godot_string_name&, Godot.NativeInterop.NativeVariantPtrArgs, Godot.NativeInterop.godot_variant&)
Area3D.cs:1131 @ bool Godot.Area3D.InvokeGodotClassMethod(Godot.NativeInterop.godot_string_name&, Godot.NativeInterop.NativeVariantPtrArgs, Godot.NativeInterop.godot_variant&)
water_plane_ScriptMethods.generated.cs:118 @ bool water_plane.InvokeGodotClassMethod(Godot.NativeInterop.godot_string_name&, Godot.NativeInterop.NativeVariantPtrArgs, Godot.NativeInterop.godot_variant&)
CSharpInstanceBridge.cs:24 @ Godot.NativeInterop.godot_bool Godot.Bridge.CSharpInstanceBridge.Call(nint, Godot.NativeInterop.godot_string_name*, Godot.NativeInterop.godot_variant**, int, Godot.NativeInterop.godot_variant_call_error*, Godot.NativeInterop.godot_variant*)

Row 125 in waterplane is:

        if (texture != null)
        {
            texture.TextureRdRid = textureRds[nextTexture];
        }
        else
        {
            GD.PrintErr("Texture update failed: Invalid texture or index.");
        }

“nextTexture” is just an integer. And I get no error for “texture” nor “textureRds”.

Bellow is the simulation texture as Albedo in the shader.
Result with GDScript


Result with C#

This is a lot of good info; it seems pretty clear it’s failing when you try to set the TextureRdRid property. That tells me there’s probably an issue with how you set up/ initialized the RIDs. But I’m out of my depth, here; so I could be way off.

I wonder if the linked github issue might provide some insight where someone was experiencing a similar issue. I would double/triple check that you properly translated the demo code and didn’t miss anything; or see if this is one of the cases where something is a little different in C# than in gdscript.

1 Like

Thank you so much!
This solved it

        //rd = RenderingServer.CreateLocalRenderingDevice();
        rd = RenderingServer.GetRenderingDevice();

I also removed some code from my RenderProcess. It wasn’t in the original and it caused an error

        //rd.Submit();
        //rd.Sync();

1 Like

Glad you sorted it!

Totally support having a C# version of the example, definitely will be handy for those who wish to create their own compute shaders and I prefer to work in C#. My C# is rusty and I haven’t used it in Godot so not sure how much help I can be. The error itself would indicate that the render process step is executed before the textures have been created (or they have failed to create).

Note that if (textureRds == null) should be if (textureRds[i] == null) so maybe you are not catching that the texture is not being created?

But there is a performance increases by using C#

I did want to react on this, in this particular case performance should not be a decision point. The code that runs each frame is negligible, the real work is done by the GPU and in C++ code. I doubt you’ll find a meaningful difference between doing this in GDScript compared to C#.

This solved it

Ah only just noticed there was another response. Yes this makes sense, if you use the local rendering device the textures you fill belong to that local device and are not accessible to the rest of the renderer.

The local rendering device is meant for processes that stand completely alone. Things like baking stuff to textures that are then saved to disc, etc.

Thank you for the answers.
Checking the entire array at once feels obviously wrong now hehe
And thank you for the info about rendering devices, it still is somewhat confusing for me. But I will dive into some environment simulations and hopefully get something good out of it