Using local scopes in SystemBase.OnUpdate can cause captured variables to silently get wrong values

This is not equivalent at all :slight_smile: Your initial code wouldn’t look like this at all, it will look absolutely different (see below).

#define ENABLE_PROFILER
using System.Runtime.CompilerServices;
using Unity.Burst;
using Unity.Entities;
using Unity.Jobs;
using Unity.Jobs.LowLevel.Unsafe;
using Unity.Profiling;
using UnityEngine;

namespace Systems
{
    internal class ReproSystem : SystemBase
    {
        [BurstCompile]
        [NoAlias]
        [Unity.Entities.DOTSCompilerGenerated]
        private struct <>c__DisplayClass_OnUpdate_LambdaJob0 : IJob
        {
            public float bar;

            public float foo;

            private static InternalCompilerInterface.JobRunWithoutJobSystemDelegate s_RunWithoutJobSystemDelegateFieldNoBurst;

            private static InternalCompilerInterface.JobRunWithoutJobSystemDelegate s_RunWithoutJobSystemDelegateFieldBurst;

            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            internal void OriginalLambdaBody()
            {
                foo = bar;
            }

            public void ReadFromDisplayClass(ref <>c__DisplayClass0_1 displayClass)
            {
                bar = displayClass.bar;
                foo = displayClass.CS$<>8__locals1.foo;
            }

            public void WriteToDisplayClass(ref <>c__DisplayClass0_1 displayClass)
            {
                displayClass.bar = bar;
                displayClass.CS$<>8__locals1.foo = foo;
            }

            public void Execute()
            {
                OriginalLambdaBody();
            }

            public void ScheduleTimeInitialize(ReproSystem componentSystem, ref <>c__DisplayClass0_1 displayClass)
            {
                ReadFromDisplayClass(ref displayClass);
            }

            [BurstCompile]
            [Unity.Entities.MonoPInvokeCallback(typeof(InternalCompilerInterface.JobRunWithoutJobSystemDelegate))]
            public unsafe static void RunWithoutJobSystem(void* jobData)
            {
                ((<>c__DisplayClass_OnUpdate_LambdaJob0*)jobData)->Execute();
            }
        }

        [Unity.Entities.DOTSCompilerGenerated]
        [BurstCompile]
        [NoAlias]
        private struct <>c__DisplayClass_OnUpdate_LambdaJob1 : IJob
        {
            public float foo;

            private static InternalCompilerInterface.JobRunWithoutJobSystemDelegate s_RunWithoutJobSystemDelegateFieldNoBurst;

            private static InternalCompilerInterface.JobRunWithoutJobSystemDelegate s_RunWithoutJobSystemDelegateFieldBurst;

            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            internal void OriginalLambdaBody()
            {
                foo = 5f;
            }

            public void ReadFromDisplayClass(ref <>c__DisplayClass0_0 displayClass)
            {
                foo = displayClass.foo;
            }

            public void WriteToDisplayClass(ref <>c__DisplayClass0_0 displayClass)
            {
                displayClass.foo = foo;
            }

            public void Execute()
            {
                OriginalLambdaBody();
            }

            public void ScheduleTimeInitialize(ReproSystem componentSystem, ref <>c__DisplayClass0_0 displayClass)
            {
                ReadFromDisplayClass(ref displayClass);
            }

            [BurstCompile]
            [Unity.Entities.MonoPInvokeCallback(typeof(InternalCompilerInterface.JobRunWithoutJobSystemDelegate))]
            public unsafe static void RunWithoutJobSystem(void* jobData)
            {
                ((<>c__DisplayClass_OnUpdate_LambdaJob1*)jobData)->Execute();
            }
        }

        private ProfilerMarker <>OnUpdate_LambdaJob0_profilerMarker;

        private ProfilerMarker <>OnUpdate_LambdaJob1_profilerMarker;

        protected unsafe override void OnUpdate()
        {
            <>c__DisplayClass0_0 displayClass = default(<>c__DisplayClass0_0);
            displayClass.foo = 0f;
            <>c__DisplayClass0_1 displayClass2 = default(<>c__DisplayClass0_1);
            displayClass2.CS$<>8__locals1 = displayClass;
            displayClass2.bar = 1f;
            _ = base.Job;
            <>c__DisplayClass_OnUpdate_LambdaJob0 jobData = default(<>c__DisplayClass_OnUpdate_LambdaJob0);
            jobData.ScheduleTimeInitialize(this, ref displayClass2);
            CompleteDependency();
            InternalCompilerInterface.JobRunWithoutJobSystemDelegate functionPointer = JobsUtility.JobCompilerEnabled ? <>c__DisplayClass_OnUpdate_LambdaJob0.s_RunWithoutJobSystemDelegateFieldBurst : <>c__DisplayClass_OnUpdate_LambdaJob0.s_RunWithoutJobSystemDelegateFieldNoBurst;
            <>OnUpdate_LambdaJob0_profilerMarker.Begin();
            try
            {
                InternalCompilerInterface.RunIJob(ref jobData, functionPointer);
            }
            finally
            {
                <>OnUpdate_LambdaJob0_profilerMarker.End();
            }
            jobData.WriteToDisplayClass(ref displayClass2);
            Debug.Log(displayClass.foo);
            _ = base.Job;
            <>c__DisplayClass_OnUpdate_LambdaJob1 jobData2 = default(<>c__DisplayClass_OnUpdate_LambdaJob1);
            jobData2.ScheduleTimeInitialize(this, ref displayClass);
            CompleteDependency();
            InternalCompilerInterface.JobRunWithoutJobSystemDelegate functionPointer2 = JobsUtility.JobCompilerEnabled ? <>c__DisplayClass_OnUpdate_LambdaJob1.s_RunWithoutJobSystemDelegateFieldBurst : <>c__DisplayClass_OnUpdate_LambdaJob1.s_RunWithoutJobSystemDelegateFieldNoBurst;
            <>OnUpdate_LambdaJob1_profilerMarker.Begin();
            try
            {
                InternalCompilerInterface.RunIJob(ref jobData2, functionPointer2);
            }
            finally
            {
                <>OnUpdate_LambdaJob1_profilerMarker.End();
            }
            jobData2.WriteToDisplayClass(ref displayClass);
        }

        protected internal unsafe override void OnCreateForCompiler()
        {
            base.OnCreateForCompiler();
            <>c__DisplayClass_OnUpdate_LambdaJob0.s_RunWithoutJobSystemDelegateFieldNoBurst = <>c__DisplayClass_OnUpdate_LambdaJob0.RunWithoutJobSystem;
            <>c__DisplayClass_OnUpdate_LambdaJob0.s_RunWithoutJobSystemDelegateFieldBurst = InternalCompilerInterface.BurstCompile(<>c__DisplayClass_OnUpdate_LambdaJob0.s_RunWithoutJobSystemDelegateFieldNoBurst);
            <>OnUpdate_LambdaJob0_profilerMarker = new ProfilerMarker("OnUpdate_LambdaJob0");
            <>c__DisplayClass_OnUpdate_LambdaJob1.s_RunWithoutJobSystemDelegateFieldNoBurst = <>c__DisplayClass_OnUpdate_LambdaJob1.RunWithoutJobSystem;
            <>c__DisplayClass_OnUpdate_LambdaJob1.s_RunWithoutJobSystemDelegateFieldBurst = InternalCompilerInterface.BurstCompile(<>c__DisplayClass_OnUpdate_LambdaJob1.s_RunWithoutJobSystemDelegateFieldNoBurst);
            <>OnUpdate_LambdaJob1_profilerMarker = new ProfilerMarker("OnUpdate_LambdaJob1");
        }
    }
}

Unity using Roslyn’s compiler DisplayClass approach and has issues with scopes etc in Unity’s modification. Unity improving this and moving forward from display classes (as far as I know from chat with them about this topic last time in 2020). Issue here is that Roslyn generates two display classes (0_0 and 0_1) for this foo and bar and they both in a different one and both Job.WithCode writes to different DisplayClass and in case of second display class it writes foo into own DisplayClass.CS$<>8__locals1 and you might think “Hey why it’s not by reference its called Class” but it’s not. Both DisplayClass0_0 and DisplayClass0_1 are structs, and now you see that foo in DisplayClass.CS$<>8__locals1 it’s a copy, not reference and despite DisplayClasss-es itself passed with ref, CS$<>8__locals1 field not reference to DisplayClass0_0 but copy and Job changing this value of the foo to bar in DisplayClass0_1, but it wouldn’t affect original DisplayClass0_0 from which you getting foo for Debug.Log.

And the exact equivalent of compiled by Roslyn code in C# will work exactly the same:

public struct TestDisplay1 //Value type
    {
        public float foo;
    }
 
    public struct TestDisplay2 //Value type
    {
        public float bar;
        public TestDisplay1 locals; //Value type field
    }

    public class ReproSystem : SystemBase
    {
        private static void RunLambda(LambdaSingleJobDescriptionConstructionMethods.WithCodeAction lambda)
        {
            lambda();
        }
        protected override void OnUpdate()
        {
            var displayClass1 = default(TestDisplay1);
            displayClass1.foo = 0f;
      
            var displayClass2 = default(TestDisplay2);
            displayClass2.locals = displayClass1; //Take a close look at this especially on declaration
            displayClass2.bar = 1f;

            RunLambda(() =>
            {
                displayClass2.locals.foo = displayClass2.bar; //And this
            });
      
            Debug.Log(displayClass1.foo);
        }
    }

6892841--806192--upload_2021-3-2_12-21-27.png

Compiled code itself works as expected, issue here is that Roslyn generates bad code, but as I already told, Unity should be aware of these issues with DisplayClass. As we discussed these locals issues 9 month’s ago with Joel.
6892841--806189--upload_2021-3-2_12-17-56.png

6892841--806186--upload_2021-3-2_12-16-9.png

6 Likes