Firstborn/Library/PackageCache/com.unity.burst@1.7.3/Documentation~/docs/CSharpLanguageSupport_Lang.md
Schaken-Mods b486678290 Library -Artifacts
Library -Artifacts
2023-03-28 12:24:16 -05:00

11 KiB

Language Support

Burst supports most of the expressions and statements:

  • Regular C# control flows:
    • if/else/switch/case/for/while/break/continue.
  • Extension methods.
  • Unsafe code, pointer manipulation...etc.
  • Instance methods of structs.
  • By ref/out parameters.
  • DllImport and internal calls.
  • Limited support for throw expressions, assuming simple throw patterns (e.g throw new ArgumentException("Invalid argument")). In that case, we will try to extract the static string exception message to include it in the generated code.
  • Some special IL opcodes like cpblk, initblk, sizeof.
  • Loading from static readonly fields.
  • Support for fixed statements.
  • Support for try/finally and associated IDisposable patterns (using and foreach)
    • Note that if an exception occurs, the behavior will differ from .NET. In .NET, if an exception occurs inside a try block, control flow would go to the finally block. In Burst, if an exception occurs whether inside or outside a try block, the currently running job or function pointer will terminate immediately.
  • Partial support for strings and Debug.Log.
  • Support for strings and ProfilerMarker.

Burst also provides alternatives for some C# constructions not directly accessible to HPC#:

Burst does not support:

  • Catching exceptions catch in a try/catch.
  • Storing to static fields except via Shared Static
  • Any methods related to managed objects (e.g string methods...etc.)

Static readonly fields and static constructors

Burst will try to evaluate all static fields and all static constructors at compile-time. The same base language is supported at compile-time, with some exceptions:

  • Managed arrays and strings are supported.
  • Calling external functions and function pointers is not supported.
  • Only some intrinsics are supported:
    • Assertions:
      • UnityEngine.Debug.Assert
      • NUnit.Framework.Assert.AreEqual
      • NUnit.Framework.Assert.AreNotEqual
    • Unity.Burst.BurstCompiler.IsEnabled
    • Unity.Burst.BurstRuntime.GetHashCode32 and Unity.Burst.BurstRuntime.GetHashCode64
    • Vector type construction

Simple throw patterns are also supported. Exceptions thrown during evaluation become compiler errors.

All the static fields and the static constructor for a given struct are evaluated together. Having non-readonly static fields in a Burst compiled struct will therefore cause a compilation error, since only readonly static fields are supported. If any static field or the static constructor fails to evaluate then all of them fail to evaluate for that struct. When compile-time evaluation fails Burst will try to fall back to compiling all static initialization code into an initialization function to be called once at runtime. This means that if code fails compile time evaluation it needs to be Burst compatible, otherwise compilation will fail. An exception to this is that there's still limited support for initializing static readonly array fields as long as they are initialized from either an array constructor or from static data:

  • static readonly int[] MyArray0 = { 1, 2, 3, .. };
  • static readonly int[] MyArray1 = new int[10];

Directly Calling Burst Compiled Code

Burst supports the ability to call out to Burst compiled methods directly from managed code (2019.4+ only). Calling generic methods or methods whose declaring type is generic is not supported, otherwise the rules as for Function pointers apply. However as a user you don't need to worry about the extra boiler plate needed for function pointers.

As an example here is a utility class which is Burst compiled (notice because we are using structs we are passing by reference as per the Function Pointer rules).

[BurstCompile]
public static class MyBurstUtilityClass
{
    [BurstCompile]
    public static void BurstCompiled_MultiplyAdd(in float4 mula, in float4 mulb, in float4 add, out float4 result)
    {
        result = mula * mulb + add;
    }
}

Now we can utilise this method from our Managed code e.g.

public class MyMonoBehaviour : MonoBehaviour
{
    void Start()
    {
        var mula = new float4(1, 2, 3, 4);
        var mulb = new float4(-1,1,-1,1);
        var add = new float4(99,0,0,0);
        MyBurstUtilityClass.BurstCompiled_MultiplyAdd(mula, mulb, add, out var result);
        Debug.Log(result);
    }
}

and if we were to run (with the managed script attached to an object), we should now see in the log float4(98f, 2f, -3f, 4f).

This is achieved by using IL Post Processing in order to automatically transform the code into a function pointer and call, which is why you must adhere to the rules of Function pointers.

You can disable the direct call transformation by adding DisableDirectCall = true to the BurstCompile options, which will prevent the Post Processor from running on the code. E.g.

[BurstCompile]
public static class MyBurstUtilityClass
{
    [BurstCompile(DisableDirectCall = true)]
    public static void BurstCompiled_MultiplyAdd(in float4 mula, in float4 mulb, in float4 add, out float4 result)
    {
        result = mula * mulb + add;
    }
}

Throw and Exceptions

Burst supports throw expressions for exceptions in the Editor (2019.3+ only), but crucially does not in Standalone Player builds. Exceptions in Burst are to be used solely for exceptional behavior. To ensure that code does not end up relying on exceptions for things like general control flow, Burst will produce the following warning on code that tries to throw within a method not attributed by [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")]:

Burst warning BC1370: An exception was thrown from a function without the correct [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")] guard. Exceptions only work in the editor and so should be protected by this guard

Partial support for strings and Debug.Log

Burst provides partial support for using strings in the following two main scenarios:

  • Debug.Log (see below)
  • Assignment of a string to various FixedString structs provided by Unity.Collections (e.g FixedString128)

A string can be either:

  • A string literal (e.g "This is a string literal").
  • An interpolated string (using $"This is an integer {value} or using string.Format) where the string to format is also a string literal

For example, the following constructions are supported:

  • Logging with a string literal:
Debug.Log("This a string literal");
  • Logging using string interpolation:
int value = 256;
Debug.Log($"This is an integer value {value}"); 

Which is equivalent to using string.Format directly:

int value = 256;
Debug.Log(string.Format("This is an integer value {0}", value));

While it is possible to pass a managed string literal or an interpolated string directly to Debug.Log, it is not possible to pass a string to a user method or to use them as fields in a struct. In order to pass or store strings around, you need to use one of the FixedString structs provided by the Unity.Collections package:

int value = 256;
FixedString128 text = $"This is an integer value {value} used with FixedString128";
MyCustomLog(text);

// ...

// String can be passed as an argument to a method using a FixedString, 
// but not using directly a managed `string`:
public static void MyCustomLog(in FixedString128 log)
{
    Debug.Log(text);
}

Burst has limited support for string format arguments and specifiers:

int value = 256;

// Padding left: "This value `  256`
Debug.Log($"This value `{value,5}`");

// Padding right: "This value `256  `
Debug.Log($"This value `{value,-5}`");

// Hexadecimal uppercase: "This value `00FF`
Debug.Log($"This value `{value:X4}`");

// Hexadecimal lowercase: "This value `00ff`
Debug.Log($"This value `{value:x4}`");

// Decimal with leading-zero: "This value `0256`
Debug.Log($"This value `{value:D4}`");

What is supported currently:

  • The following Debug.Log methods:
    • Debug.Log(object)
    • Debug.LogWarning(object)
    • Debug.LogError(object)
  • String interpolation is working with the following caveats:
    • The string to format must be a literal.
    • Only the following string.Format methods are supported:
      • string.Format(string, object), string.Format(string, object, object), string.Format(string, object, object, object) and more if the .NET API provides specializations with object arguments.
      • string.Format(string, object[]): which can happen for a string interpolation that would contain more than 3 arguments (e.g $"{arg1} {arg2} {arg3} {arg4} {arg5}..."). In that case, we expect the object[] array to be of a constant size and no arguments should involve control flows (e.g $"This is a {(cond ? arg1 : arg2)}")
    • Only value types.
    • Only primitive type arguments: char, boolean, byte, sbyte, ushort, short, uint, int, ulong, long, float, double.
    • All vector types e.g int2, float3 are supported, except half vector types.
      var value = new float3(1.0f, 2.0f, 3.0f);
      // Logs "This value float3(1f, 2f, 3f)"
      Debug.Log($"This value `{value}`");
      
    • Does not support ToString() of structs. It will display the full name of the struct instead.
  • The supported string format specifiers are only G, g, D, d and X, x, with padding and specifier. For more details:

You can also use System.Runtime.CompilerServices attributes [CallerLineNumber], [CallerMemberName], and [CallerFilePath] on arguments to Burst functions, but the strings can only be passed directly to calls to Debug.Log at present.

Support for strings and ProfilerMarker

In Unity 2020.2 and above, you can call new ProfilerMarker("MarkerName") from Burst code:

[BurstCompile]
private static class ProfilerMarkerWrapper
{
    private static readonly ProfilerMarker StaticMarker = new ProfilerMarker("TestStaticBurst");

    [BurstCompile(CompileSynchronously = true)]
    public static int CreateAndUseProfilerMarker(int start)
    {
        using (StaticMarker.Auto())
        {
            var p = new ProfilerMarker("TestBurst");
            p.Begin();
            var result = 0;
            for (var i = start; i < start + 100000; i++)
            {
                result += i;
            }
            p.End();
            return result;
        }
    }
}