Will [RegisterGenericJobType] be required for all generic jobs going forward?

Hi all. Looking for some clarification:

When using Entities 0.14 & Unity 2020.2, new errors will appear in the console:

(...) generic jobs cannot have their reflection data auto-registered - you must use the assembly-level RegisterGenericJobType attribute to specify which instantiations you need

Simply, I’m wondering if this will be a requirement for DOTS going forward: Is this an intentional design change by the DOTS team? Will it be required that we use the RegisterGenericJobTypeAttribute to explicitly define each permutation that we use for a generic job?

Additional Notes:

  • There is an excellent thread about a related issue by [mention|WoQo1QBmbRSlItmJiQ7Ksw==], here . But that thread ended up being about a different issue: Bugs that arise even when you do use the RegisterGenericJobTypeAttribute. I wanted to start a thread to find out if even having to use the attribute is a bug, or an intended change.

  • These errors do not occur when you have both Entities. 0.14 and Unity 2020.1.5 installed. They only arise with the 2020.2 beta. Does this mean that Entities 0.14 is not intended to require this attribute, and that this error is just a bug introduced in 2020.2? Or does this mean that - when the latest packages and unity version are installed - we’re seeing the future direction?

Thank you very much for any insight. This could have major ramifications for how our team has been writing our code…ramifications we hope to avoid…

You see that only with 2020.2+ because it’s wrapped into UNITY_2020_2_OR_NEWER define.
Library/PackageCache/com.unity.entities@0.14.0-preview.19/Unity.Entities.CodeGen/JobReflectionDataPostProcessor.cs
6335025--703242--upload_2020-9-22_1-24-39.png

        public static DiagnosticMessage DC3002(TypeReference jobStructType)
        {
            return MakeError(nameof(DC3002), $"{jobStructType.FullName}: generic jobs cannot have their reflection data auto-registered - you must use the assembly-level RegisterGenericJobType attribute to specify which instantiations you need", method: null, instruction: null);
        }

Thank you, @eizenhorn !

That comment makes it more ambiguous. Is this attribute just a stop-gap measure until they can add that “separate solution for generic jobs” in a future release?

Or - is that comment saying that the RegisterGenericJobTypeAttribute is the “separate solution”?

It’s still unclear if using RegisterGenericJobTypeAttribute for all generic jobs will be required going forward. Is this the new normal?

If any Unity devs could give some clarification on this, it would help our team a lot. We are currently trying to decide wether to begin a giant restructuring of our code (to account for this new attribute), or to wait for a new “solution for generic jobs” to make that unnecessary.

Fully Bursted, generic jobs make up the great majority of the jobs we use in our DOTS projects. We have been very happy that we don’t have to define each type permutation before using them. We are hoping this new attribute won’t always be required in the future.

[RegisterGenericJobTypeAttribute] is necessary. The burst compiler has to be able to know which burst jobs will be used at runtime because it compiles ahead of time.

Thank you for commenting here.

Our generic jobs were successfully Bursted (in AOT builds) before. We followed the guidelines you guys had published before about generic jobs to make sure they would be.

And they were still successfully Bursted in 0.14 until we updated to 2020.2. So it’s not necessary. It should only be necessary to use for generic jobs that Burst can’t catch.

So why the artificial change, for jobs that could be Bursted fine without this attribute? This seems like throwing the baby out with the bath water. It will make our code more cumbersome and much less clean, when it doesn’t have to be.

1 Like

It was working in builds? My understanding was the above warning was added because previously it was possible to think it was working in Editor but in reality it was never supported and wouldn’t work in builds. I could be wrong.

Yes. Our generic jobs have been successfully Bursted in builds for many months (maybe over a year at this point).

There are some types of generic jobs which Burst cannot find for AOT compilation, for reasons which aren’t Unity’s fault. The RegisterGenericJobTypeAttribute seems like a good solution for just those jobs.

But some generic jobs can be found and compiled by Burst for AOT builds with no problem. Unity released detailed docs on which types worked and which did not a while back. I’ll track that down and post it here when there’s a chance.

2 Likes

So this system is going to fail in Burst + 2020.2?
It is working fine now in 2020.1 Entity 0.14

    [OptionalSystem, UpdateInGroup(typeof(UpdateOrBuildBehaviourSystemGroup))]
    public class MaxHPAuthorHPSystem : 
        BehaviourBuilderSystemBase<MaxHealthPoint, HealthPoint> { };

    public abstract class BehaviourBuilderSystemBase<TD, TB> : SystemBase
        where TD : struct, IComponentData, IBehaviourBuilder<TB>
        where TB : struct, IComponentData
    {
        EntityCommandBufferSystem ECBS;
        EntityQuery BuildQuery;
        protected override void OnCreate()
        {
            ECBS = World.GetOrCreateSystem<BuildBehaviourCommandBufferSystem>();
            BuildQuery = GetEntityQuery(new EntityQueryDesc()
            {
                All = new ComponentType[] { ComponentType.ReadOnly<TD>() },
                None = new ComponentType[] { ComponentType.ReadWrite<TB>() },
                Options = EntityQueryOptions.IncludePrefab | EntityQueryOptions.IncludeDisabled
            });
        }

        protected override void OnUpdate()
        {
            var ECBP = ECBS.CreateCommandBuffer().AsParallelWriter();
            var buildDep = new BuildBehaviourComponent()
            {
                ECBP = ECBP,
                DataType = GetComponentTypeHandle<TD>(true),
                EntityType = GetEntityTypeHandle(),
            }.ScheduleParallel(BuildQuery, Dependency);
            ECBS.AddJobHandleForProducer(buildDep);
            Dependency = buildDep;
        }

        [BurstCompile]
        struct BuildBehaviourComponent : IJobChunk
        {
            public EntityCommandBuffer.ParallelWriter ECBP;
            [ReadOnly] public ComponentTypeHandle<TD> DataType;
            [ReadOnly] public EntityTypeHandle EntityType;
            public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
            {
                var chunkData = chunk.GetNativeArray(DataType);
                var chunkEntity = chunk.GetNativeArray(EntityType);
                for (int i = 0, len = chunk.Count; i < len; i++, firstEntityIndex++)
                {
                    ECBP.AddComponent(firstEntityIndex, chunkEntity[i], chunkData[i].Build());
                }
            }
        }
    }

Here’s the link to Unity’s documentation on which generic jobs can be detected and compiled by Burst:

https://docs.unity3d.com/Packages/com.unity.burst@1.3/manual/index.html#generic-jobs

From that page:

We followed these guidelines to the letter, and found great success. Code we’re really happy with, that’s easy for our developers to use.

If there’s been a new decision made for 2020.2 to artificially limit Burst from being able to find any generic jobs, just because it can’t find some generic jobs (maybe for consistency) - that’s a decision which is about to blow up our code.

3 Likes

Do you actually have an example of a specific job that is failing now? Because I actually tried a few combinations trying to trigger the same warning on your original post a month or so back without success.

Yes. There is a specific case we use as a fundamental part of our code, which no longer works with this change. It’s similar to the problem outlined here…

…How to solve cases in which you have a large number of jobs which share 80% of the same component types and logic, with the remaining 20% being unique to each job.

Using generic jobs, we had developed a good solution, which kept our devs from needing to worry about boilerplate, and cut down on potential human error (which might emerge when identical code is repeated across dozens of jobs).

This post is actually very close to what we are doing:

Without Burst being able to auto-compile generic jobs of any kind, it’s difficult to think of a replacement solution. But we really need to find one. If it’s really true that no generic jobs will be supported going forward, this has set us back 6 months. So - no pride here - we will need to find an answer. Thank you for your interest.

Some life events have made it difficult for me to code since Thursday. I may have some free time this weekend, and I’ll post example code here.

Actually I ran into this error yesterday. Not a big deal for me as I only need 1 generic implementation of it per project but I did find another ‘bug’

If you have AssemblyA with a generic system and a generic job used inside it
Then create an implementation for it in AssemblyB and add a RegisterGenericJobType as required for your implementation
It will still complain that you need to use RegisterGenericJobType as above, even though you have one i AssemblyB.

To fix this you need to create a dummy implementation in AssemblyA and RegisterGenericJobType for the dummy type and then both implementations will work without error…

@Joachim_Ante_1 Please, please explain why you guys made this decision. Why did you remove the ability for Burst to detect generic jobs - jobs that it was able to detect before? Jobs that your current documentation still says it should be able to detect - even for AOT builds.

This massive change wasn’t documented anywhere, in any release notes or wiki pages. It has stopped us dead in our tracks, and made our previously working code invalid. Making sure we could use generic jobs to avoid repeating code in our jobs was one of the critical features that convinced us to get on board with DOTS. Now we might have to abandon it.

For the past week, we’ve been trying to figure out a new way to rewrite our code, and work around this new limitation. But it seems like it might just not be possible.

We are now unable to work, and if a workaround can’t be found, we’re set back 7 months as a team. Please explain why the DOTS team made the choice to remove this previously working Burst functionality. I’m begging you.

In case it will help, here’s what we’ve been doing. This is what broke when using Entities 0.14 and Unity 2020.2. This used to compile fine - Burst had no problem detecting these generic job for JIT and AOT:

We started with a problem: Most of our jobs shared 90% of the same logic & component types. But 10% was always unique to each job. We knew it would be crucial to our workflow to figure out a way not to repeat a bunch of the same logic in each of our dozens or hundreds of such jobs.

(Our particular need in these jobs was to iterate over DynamicBuffers, and perform some logic on each of their elements).

The solution was to have our devs write unique structs inside of their systems, which contained the custom logic they wanted to perform on each of the DB elements. Then, to schedule that logic as a job, we would encapsulate that unique struct inside of a generic wrapper job. That wrapper job would house all of the shared job code, which otherwise would need to be repeated in each of the structs our devs write:

So given these component types:

public struct Foo
{
    public int num;
}

public struct Bar : IBufferElementData
{
    public int num;
}

public struct Baz : IComponentData
{
    public int num;
}

We wrote a generic static class which held our wrapper job type, along with a special Schedule() method:

public static class IJobCustomUtility<TJob, T0> where TJob : struct, IJobCustom<T0> where T0 : struct, IBufferElementData
{
    [BurstCompile]
    public struct WrapperJob : IJobChunk
    {
        // wrapped job ends up here
        public TJob wrappedJob;

        [ReadOnly]
        [DeallocateOnJobCompletion]
        public NativeArray<Foo> foos;

        [ReadOnly]
        public EntityTypeHandle entityType;
        public ComponentTypeHandle<Baz> bazType;
        public BufferTypeHandle<T0> t0Type;
        public bool isReadOnly_T0;

        public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
        {
            var entities = chunk.GetNativeArray(entityType);
            var bazArray = chunk.GetNativeArray(bazType);
            var t0Buffers = chunk.GetBufferAccessor(t0Type);

            for (int i = 0; i < chunk.Count; i++)
            {
                var entity = entities[i];
                var baz = bazArray[i];
                var t0Buffer = t0Buffers[i];

                for (int iT0 = 0; iT0 < t0Buffer.Length; iT0++)
                {
                    var t0 = t0Buffer[iT0];

                    // wrapped job Execute method is called
                    wrappedJob.Execute(entity, ref t0, ref baz, foos);

                    bazArray[i] = baz;

                    if (!isReadOnly_T0)
                    {
                        t0Buffer[iT0] = t0;
                    }
                }
            }
        }
    }

    public static JobHandle Schedule(TJob jobData, CustomSystemBase system, JobHandle dependentOn)
    {
        EntityQuery query = GetExistingEntityQuery(system);

        if (query == default)
        {
            ComponentType[] componentTypes;
            if (!componentTypesByJobType.TryGetValue(typeof(TJob), out componentTypes))
            {
                componentTypes = GetIJobCustomComponentTypes();
            }

            query = system.GetEntityQueryPublic(componentTypes);

            system.RequireForUpdate(query);

            if (query.CalculateChunkCount() == 0)
            {
                return dependentOn;
            }
        }

        WrapperJob wrapperJob = new WrapperJob
        {
            wrappedJob = jobData,
            foos = new NativeArray<Foo>(10, Allocator.TempJob),
            entityType = system.EntityManager.GetEntityTypeHandle(),
            bazType = system.EntityManager.GetComponentTypeHandle<Baz>(false),
            t0Type = system.EntityManager.GetBufferTypeHandle<T0>(false),
            isReadOnly_T0 = false
        };

        return wrapperJob.ScheduleParallel(query, dependentOn);
    }

    private static Dictionary<Type, ComponentType[]> componentTypesByJobType = new Dictionary<Type, ComponentType[]>();

    private static EntityQuery GetExistingEntityQuery(ComponentSystemBase system)
    {
        ComponentType[] componentTypes;
        if (!componentTypesByJobType.TryGetValue(typeof(TJob), out componentTypes))
        {
            return default;
        }

        for (var i = 0; i != system.EntityQueries.Length; i++)
        {
            if (system.EntityQueries[i].CompareComponents(componentTypes))
                return system.EntityQueries[i];
        }

        return default;
    }

    private static ComponentType[] GetIJobCustomComponentTypes()
    {
        // Temporary. A final version might use reflection to find the read/write attributes from the wrapper job's Execute method.
        return new ComponentType[]
            {
            ComponentType.ReadWrite<T0>(),
            ComponentType.ReadWrite<Baz>()
            };
    }

}

We then created a custom interface - this is the ‘job’ type our devs would actually write in their systems. This is what the wrapper job actually “wraps”:

public interface IJobCustom<T> where T : struct, IBufferElementData
{
    void Execute(Entity entity, ref T t0, ref Baz baz, NativeArray<Foo> foos);
}

We created a custom system type, just so we could expose the GetEntityQuery() function:

public abstract class CustomSystemBase : SystemBase
{
    public EntityQuery GetEntityQueryPublic(ComponentType[] componentTypes)
    {
        return GetEntityQuery(componentTypes);
    }
}

…Then, this is what one of our dev’s actual systems might look like:

public class MySystem : CustomSystemBase
{
    protected override void OnUpdate()
    {
        Dependency = IJobCustomUtility<WrappedCustomJob, Bar>.Schedule(new WrappedCustomJob
        {
            toAdd = 10
        }, this, Dependency);
    }

    public struct WrappedCustomJob : IJobCustom<Bar>
    {
        public int toAdd;

        public void Execute(Entity entity, ref Bar t0, ref Baz baz, NativeArray<Foo> foos)
        {
            // do custom logic here
            t0.num += toAdd;
        }
    }
}

You can see in this last code snippet that all this dev had to do was define a struct that implements the IJobCustom interface.

Then in OnUpdate(), they call the Schedule method and pass in a new instance of this IJobCustom. The Schedule method takes it from there, and wraps that IJobCustom in the wrapper job, populates the wrapper job with a bunch of data, and then schedules that wrapper job.

This had so many advantages for us:

  • No code was repeated between the jobs they wrote day to day. So there was no boilerplate for our devs to worry about. They didn’t have to think about what was going on behind the scenes - only what logic to do on each element in those DynamicBuffers.
  • No repeated code means no human error when repeating that code.
  • A single source for shared code means that code was easy to change (which has happened dozens of times since we implemented this pattern).
  • The jobs they defined in their systems could still include their own local variables - for example, the ‘toAdd’ integer in the example above. ← we had previously looked into using FunctionPointers, but they wouldn’t have allowed for this.

We fretted for weeks over wether this was a “bad” way to solve this problem. I mean, we’re devs - we fret that way about every solution.

But after a lot of testing, it worked, and worked quickly. Everything abided by the guidelines in your Generic Jobs Burst documentation, and so everything Burst-compiled fine in our AOT builds. It was so fast and convenient, that it made huge DOTS believers out of us. It’s been a critical part of our code since then, and a big part of why we’re DOTS cheerleaders over here.

But now, that solution just won’t work. If we were to try to use the [RegisterGenericJobType] attribute, the [assembly: ... ] line we’d have to write for it would be giant and convoluted. We’d have to mark almost everything involved public, just so we could access it from that line. And it would break our initial goal of not forcing our devs to think about the boiletplate behind the scenes in their day-to-day work.

If you, or anything else, can think of an elegant way to adapt our pattern, it would help us tremendously. You would have solved a problem that I’ve been failing at for over a week.

But so far, it looks like there’s no current workaround that will give us the features we had before. It’s just not a viable option for us to repeat all of that shared code in all of our jobs, every day. We need some way to share common code like this between them, or we’re going to have to reconsider our ability to use DOTS in our products.

So, I agree Unity should fix this. However, if this is a blocker, you can scan the assembly with Cecil and find them, then automatically generate those attributes. Someone figured out how to do that performantly before; I’ll see if I can find that post.

1 Like

My understanding is that we did not regress any behaviours in AOT build. We simply made it so that the Editor Burst JIT mode has the same behaviour as the AOT mode.

If that is not true, then please post a single specific example of where the AOT previously picked something up and now it doesn’t.

That’s also very non-ideal in the context of third-party assets. Requiring everyday users to add the [assembly: ] line to all their variants of the package’s generic jobs hurts the easy of use of the product

It’s not like it makes it impossible to use, but it adds undesired friction & weirdness

2 Likes

Here we go; found the thread about it: Detect jobs skipped by burst

And the repo: DOTS-Stackray/Packages/com.stackray.burst at master · GilbertoGojira/DOTS-Stackray · GitHub

It’s from 8 months ago, I have no idea if it still works.

That is truly great news. Thank you.

Here is a simple, specific example:

This code compiles fine with Entities 0.14, using Unity 2020.1.5f1. Its generic job is Bursted, both in JIT (editor) and in AOT (builds). This was confirmed using the Profiler in both cases.

However, if the same code is added to a project with Entities 0.14 and Unity 2020.2.0b5, it will throw an error: “generic jobs cannot have their reflection data auto-registered - you must use the assembly-level RegisterGenericJobType attribute to specify which instantiations you need”

using Unity.Burst;
using Unity.Jobs;
using Unity.Entities;

namespace Sample
{
    public struct Foo : IComponentData
    {
        public int value;
    }

    public static class TestUtility<T> where T : struct, IComponentData
    {
        [BurstCompile]
        public struct ProcessChunks : IJobChunk
        {
            public ComponentTypeHandle<T> typeHandle;

            public void Execute(ArchetypeChunk chunk, int chunkIndex, int entityOffset)
            {
                for (int i = 0; i < 10000; i++)
                {
                    var testDataArray = chunk.GetNativeArray(typeHandle);
                    testDataArray[0] = new T();
                }
            }
        }
    }

    public class MySystem : SystemBase
    {
        EntityQuery query;

        protected override void OnCreate()
        {
            query = GetEntityQuery(ComponentType.ReadWrite<Foo>());

            // for testing purposes. Create an entity with a Foo component
            EntityManager.CreateEntity(ComponentType.ReadWrite<Foo>());
        }

        protected override void OnUpdate()
        {
            Dependency = new TestUtility<Foo>.ProcessChunks
            {
                typeHandle = GetComponentTypeHandle<Foo>()
            }.ScheduleParallel(query, Dependency);
        }
    }
}

If any more information would be helpful, please let me know.

Thank you again.

Please consider this second example as well. This is what’s actually breaking in our projects:


Like the first example, this one compiles fine when using Entities 0.14 and Unity 2020.1.5f1. Its generic job is Bursted, both in JIT (editor) and in AOT (builds). This was confirmed using the Profiler in both cases.

However, if the same code is added to a project with Entities 0.14 and Unity 2020.2.0b5, it will throw an error: “generic jobs cannot have their reflection data auto-registered - you must use the assembly-level RegisterGenericJobType attribute to specify which instantiations you need”

using System;
using System.Collections.Generic;
using Unity.Burst;
using Unity.Jobs;
using Unity.Collections;
using Unity.Entities;

namespace Sample
{
    public struct Foo
    {
        public int num;
    }

    public struct Bar : IBufferElementData
    {
        public int num;
    }

    public struct Baz : IComponentData
    {
        public int num;
    }

    public interface IJobCustom<T> where T : struct, IBufferElementData
    {
        void Execute(Entity entity, ref T t0, ref Baz baz, NativeArray<Foo> foos);
    }

    public static class TestUtility<TJob, T0> where TJob : struct, IJobCustom<T0> where T0 : struct, IBufferElementData
    {
        [BurstCompile]
        public struct WrapperJob : IJobChunk
        {
            // wrapped job ends up here
            public TJob wrappedJob;

            [ReadOnly]
            [DeallocateOnJobCompletion]
            public NativeArray<Foo> foos;

            [ReadOnly]
            public EntityTypeHandle entityType;
            public ComponentTypeHandle<Baz> bazType;
            public BufferTypeHandle<T0> t0Type;
            public bool isReadOnly_T0;

            public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
            {
                var entities = chunk.GetNativeArray(entityType);
                var bazArray = chunk.GetNativeArray(bazType);
                var t0Buffers = chunk.GetBufferAccessor(t0Type);

                for (int i = 0; i < chunk.Count; i++)
                {
                    var entity = entities[i];
                    var baz = bazArray[i];
                    var t0Buffer = t0Buffers[i];

                    for (int iT0 = 0; iT0 < t0Buffer.Length; iT0++)
                    {
                        var t0 = t0Buffer[iT0];

                        // wrapped job Execute method is called
                        wrappedJob.Execute(entity, ref t0, ref baz, foos);

                        bazArray[i] = baz;

                        if (!isReadOnly_T0)
                        {
                            t0Buffer[iT0] = t0;
                        }
                    }
                }
            }
        }

        public static JobHandle Schedule(CustomSystemBase system, JobHandle dependentOn, TJob jobData)
        {
            EntityQuery query = GetExistingEntityQuery(system);

            if (query == default)
            {
                ComponentType[] componentTypes;
                if (!componentTypesByJobType.TryGetValue(typeof(TJob), out componentTypes))
                {
                    componentTypes = GetIJobCustomComponentTypes();
                }

                query = system.GetEntityQueryPublic(componentTypes);

                system.RequireForUpdate(query);

                if (query.CalculateChunkCount() == 0)
                {
                    return dependentOn;
                }
            }

            WrapperJob wrapperJob = new WrapperJob
            {
                wrappedJob = jobData,
                foos = new NativeArray<Foo>(10, Allocator.TempJob),
                entityType = system.EntityManager.GetEntityTypeHandle(),
                bazType = system.EntityManager.GetComponentTypeHandle<Baz>(false),
                t0Type = system.EntityManager.GetBufferTypeHandle<T0>(false),
                isReadOnly_T0 = false
            };

            return wrapperJob.ScheduleParallel(query, dependentOn);
        }

        private static Dictionary<Type, ComponentType[]> componentTypesByJobType = new Dictionary<Type, ComponentType[]>();

        private static EntityQuery GetExistingEntityQuery(ComponentSystemBase system)
        {
            ComponentType[] componentTypes;
            if (!componentTypesByJobType.TryGetValue(typeof(TJob), out componentTypes))
            {
                return default;
            }

            for (var i = 0; i != system.EntityQueries.Length; i++)
            {
                if (system.EntityQueries[i].CompareComponents(componentTypes))
                    return system.EntityQueries[i];
            }

            return default;
        }

        private static ComponentType[] GetIJobCustomComponentTypes()
        {
            // Simplified for testing purposes. The real version uses reflection to get this data, and caches it for later.
            return new ComponentType[]
                {
            ComponentType.ReadWrite<T0>(),
            ComponentType.ReadWrite<Baz>()
                };
        }
    }

    public abstract class CustomSystemBase : SystemBase
    {
        public EntityQuery GetEntityQueryPublic(ComponentType[] componentTypes)
        {
            return GetEntityQuery(componentTypes);
        }
    }
    public class MySystem : CustomSystemBase
    {
        protected override void OnCreate()
        {
            // Creating a single entity for testing purposes
            Entity entity = EntityManager.CreateEntity(ComponentType.ReadWrite<Bar>(), ComponentType.ReadWrite<Baz>());

            DynamicBuffer<Bar> barBuffer = EntityManager.GetBuffer<Bar>(entity);
            barBuffer.Add(new Bar { num = 0 });
        }

        protected override void OnUpdate()
        {
            Dependency = TestUtility<WrappedCustomJob, Bar>.Schedule(this, Dependency, new WrappedCustomJob
            {
                toAdd = 10
            });
        }

        public struct WrappedCustomJob : IJobCustom<Bar>
        {
            public int toAdd;

            public void Execute(Entity entity, ref Bar t0, ref Baz baz, NativeArray<Foo> foos)
            {
                // do custom logic here
                t0.num += toAdd;
            }
        }
    }
}

Sincerely, thank you.

Please let me know if anything else would be useful.