Burst 1.5.1 calling directly from managed code

1.5.1 added support for calling burst methods directly from c# code, but I cannot get it to work in a build. Seems to work fine in the editor. Here is my code and the error

        [BurstCompile]
        public static void GetScale(this in float4x4 m, out float3 result)
        {
            float x = math.sqrt(m.c0.x * m.c0.x + m.c1.x * m.c1.x + m.c2.x * m.c2.x);
            float y = math.sqrt(m.c0.y * m.c0.y + m.c1.y * m.c1.y + m.c2.y * m.c2.y);
            float z = math.sqrt(m.c0.z * m.c0.z + m.c1.z * m.c1.z + m.c2.z * m.c2.z);

            result = new float3(x, y, z);
        }
NullReferenceException: Could not find the delegate type ''
  at Unity.Burst.BurstCompiler.CompileUnsafeStaticMethod (System.RuntimeMethodHandle handle) [0x00154] in <77d4f7b017f44c82b0afbd7ab189d40f>:0
  at Geometry.Extensions.BurstExtensions+GetScale_00000499$BurstDirectCall.GetFunctionPointerDiscard (System.IntPtr& ) [0x0000a] in <b5a2df695f014d9b96756e83709714d2>:0
  at Geometry.Extensions.BurstExtensions+GetScale_00000499$BurstDirectCall.GetFunctionPointer () [0x00003] in <b5a2df695f014d9b96756e83709714d2>:0
  at Geometry.Extensions.BurstExtensions+GetScale_00000499$BurstDirectCall.Constructor () [0x00000] in <b5a2df695f014d9b96756e83709714d2>:0
  at Geometry.Extensions.BurstExtensions+GetScale_00000499$BurstDirectCall..cctor () [0x00000] in <b5a2df695f014d9b96756e83709714d2>:0
Rethrow as TypeInitializationException: The type initializer for 'Geometry.Extensions.GetScale_00000499$BurstDirectCall' threw an exception.
  at Geometry.Extensions.BurstExtensions.GetScale (Unity.Mathematics.float4x4& m, Unity.Mathematics.float3& result) [0x00000] in <b5a2df695f014d9b96756e83709714d2>:0

Are your static class for this extension also has BurstCompile attribute? And what’s your scripting backend target - mono or il2cpp?

yes, and mono. Standalone Windows x64, 2020.2.3f1

namespace Geometry.Extensions
{
    [BurstCompile]
    public static class BurstMath
    {
        [BurstCompile]
        public static void GetScale(this in float4x4 m, out float3 result)
        {
            float x = math.sqrt(m.c0.x * m.c0.x + m.c1.x * m.c1.x + m.c2.x * m.c2.x);
            float y = math.sqrt(m.c0.y * m.c0.y + m.c1.y * m.c1.y + m.c2.y * m.c2.y);
            float z = math.sqrt(m.c0.z * m.c0.z + m.c1.z * m.c1.z + m.c2.z * m.c2.z);

            result = new float3(x, y, z);
        }
    }
}

I’ve tried a few things, such as not writing it as an extension method, but that didn’t fix it either.

Hmm tested your code and works fine for me on 2020.3, 2021.1 and mono backend (in build). Where did you call this method? From main thread code?

yeah, I wonder if it is related to using assembly definitions and that sort of thing

I tested your code myself too and couldn’t get it to fail.

If you could submit a repro that’d be great for us!

I don’t think I can submit my entire repo, it’s just too big. I could play around with it and see if I can get it to work - it works fine in the editor, and the jobs are visible in the burst inspector, it just fails at build time. Do you have any clues what might be cause it?

I figured it out - if I set “Managed Stripping” to disabled, it works. If it’s set to High, it’s broken

would definitely prefer to keep stripping enabled - it greatly reduces our binary size for webgl

Ok - that sounds legit. I think we can make Burst preserve this symbol, will look into it!

if you still can’t find a repro I could give it a go - it’s 100% reproduceable for me.

BTW this feature is badass - it dramatically reduces the amount of boilerplate requires to get those juicy burst performance gains which is great to see

I’m slightly curious about the restriction to not be able to return a struct type, but you can pass any struct as an out parameter, and what causes that under the hood?

1 Like

I can repro it with the stripping set to high - that would cause Unity to think the delegate we generate wasn’t required, and then cause your error. Working on a fix now :slight_smile:

The reason we don’t support returning a struct is because in practice when you return a struct, you aren’t actually returning a struct when you get down to assembly.

Instead we have to transform the code into a ‘struct-return’ - basically we would have the compiler autogenerate the out parameter. The ABI for when we should transform these structs its platform dependent, architecture dependent, and a lot of work to get right. For instance Clang is still fixing bugs in their ABI even as recently as 2019 (and that’s only the last time I can remember!) - for core targets like Arm 64-bit! Difficult problem that we really want to avoid spending a lot of time on.

4 Likes

In addition to what @sheredom is explaining, we have also platforms that disallow us to use anything else than pointers and primitive types between managed/native boundaries, hence the lowest common denominator.

4 Likes

As a temporary workaround, you could use link.xml files to prevent the method from being stripped: Unity - Manual: Managed code stripping

1 Like

thanks, that’s interesting - it’s definitely just syntax sugar and doesn’t really impact the usability of it. If anything just the more vanilla the C# code, the more of our engineers can write it without even thinking

    <assembly fullname="Geometry">
        <namespace fullname="Geometry.Extensions" preserve="all"/>
    </assembly>

adding this to link.xml did work - thanks!

what kind of overheads are there to calling burst methods - could very simple methods be slower than vanilla c# due to going between managed and unmanaged code?

Also, if you have nested burst methods, will burst generate a method that calls another function pointer, or would it compile the entire burst method as a unique piece of c# as a delegate?

For DirectCall it boils down to a check that we’ve compiled the method + a calli to the method (so a virtual call effectively). Not free but not expensive either.

If you have a nested Direct Call then we’ll look through the function pointer and compile the code together. This lets us optimize the code much more.

Usually, don’t do it for very simple methods as there is definitely a good chunk of fixed cost with transition from managed to native, but things are getting a bit more difficult with Mono + Debug or Release, so best way to know is to benchmark the cases you care about.

smaller other issue: I’m also having a lot of issues when two methods have similar names/ signatures, for example:

        [BurstCompile]
        public static void ClosestPointOnLine(in float3 dir, in float3 point, out float3 result)
        {
            float3 origin = new float3(0f, 0f, 0f);
            result = origin + dir * math.dot(dir, point - origin);
        }
     
        [BurstCompile]
        public static void ClosestPointOnLine(in Vector3 origin, in Vector3 dir, in Vector3 point, out Vector3 result)
        {
            result = origin + dir * Vector3.Dot(dir, point - origin);
        }

for example when compiling for il2cpp I get exceptions in 2021.1

[23:06:45] :     [Step 1/1] Exception: IL2CPP error for type 'Geometry.MathsHelpers.Vectors.VectorMaths/Geometry.MathsHelpers.Vectors.ClosestPointOnLine$PostfixBurstDelegate' in assembly 'C:\BuildAgent\work\39dc1e80e0ed6651\Temp\StagingArea\assets\bin\Data\Managed\Geometry.dll'
[23:06:45] :     [Step 1/1] Unity.IL2CPP.HashCodeCollisionException: Hash code collision on value `2FFDF951644FD8723D1ECF8D0EE1E5BDC9CCDB4C`
[23:06:45] :     [Step 1/1] Existing Item was : `Geometry.MathsHelpers.Vectors.VectorMaths/Geometry.MathsHelpers.Vectors.ClosestPointOnLine$PostfixBurstDelegate`
[23:06:45] :     [Step 1/1] Colliding Item was : `Geometry.MathsHelpers.Vectors.VectorMaths/Geometry.MathsHelpers.Vectors.ClosestPointOnLine$PostfixBurstDelegate`

I think I have actually had in-editor exceptions for two methods with identical signatues except using vector3/float3 interchanged

2 Likes

another weird error: getting this

UnityException: set_JobCompilerEnabled can only be called from the main thread. Constructors and field initializers will be executed from the loading thread when loading a scene. Don't use this function in the constructor or field initializers, instead move initialization code to the Awake or Start function.

when running these methods from other threads. Is it unsupported?