Search Unity

Feedback Could codegen make this job type possible?

Discussion in 'Entity Component System' started by PhilSA, Aug 12, 2020.

  1. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    Consider this situation:
    • MyJob lives in a package
    • MyJob takes a function pointer or generic struct type to extend its functionality in its update loop logic
    • I am in my project using the MyJob package, and the function pointer I want to pass to MyJob would need component data access that MyJob couldn't have planned for in advance. For example it could want to check if a certain entity has a certain component specific to my project
    I'd like this use case to be as simple, straightforward, and not-weird as possible for users that aren't very DOTS-savvy and wish to use such functionality. (the kind of users that would probably just stick to a simple Entities.ForEach for everything). I do know about ways around this problem currently, but they all feel a bit too complex and scary for beginners

    So I'm interested in the possibility of a codegen'd job type that would allow this: I want to be able to create a job that doesn't necessarily have to know in advance all the component data access it will require. It could just be generated based on whatever component types/accesses it ends up needing in all the places in the codebase where we ask for specific component accesses from this job. This would allow us great flexibility in creating several "variants" of a generic job. Kind of a replacement for what you'd do with virtual function overrides in OOP.

    In this code example, the "DoSomethingInJobIteration()" function reads/writes to rotation even though the "MyJob" calling this function was never made aware in its own code that it might need rotation access in its own update loop. MyJob would know which component sets to operate on based only on an EntityQuery:

    Code (CSharp):
    1. public static class MyUtilities
    2. {
    3.     public static void DoSomethingInJobIteration(in MyJob job)
    4.     {
    5.         // make the entity rotate
    6.         Rotation rotation = job.GetComponent<Rotation>();
    7.         RotationSpeed speed = job.GetComponent<RotationSpeed>();
    8.  
    9.         rotation.Value = math.mul(math.normalize(rotation.Value), quaternion.AxisAngle(math.up(), speed.RadiansPerSecond * job.DeltaTime));
    10.  
    11.         job.SetComponent(rotation);
    12.     }
    13. }
    14.  
    15. [BurstCompile]
    16. struct MyJob : IJobEntityGeneric
    17. {
    18.     public float DeltaTime;
    19.  
    20.     public void OnUpdateEntity()
    21.     {
    22.         MyUtilities.DoSomethingInJobIteration(in this);
    23.     }
    24. }

    With this sort of job API, we could imagine that creating a custom "variant" of a system/job would be as simple as this:

    Code (CSharp):
    1.  
    2. public class MyCustomSystemVariant : MyBaseSystem<MyCustomLogicHandler>
    3. { }
    4.  
    5. public struct MyCustomLogicHandler : IJobCustomLogicHandler
    6. {
    7.     // This gets called by the MyBaseJob<T> job in MyBaseSystem<T>
    8.     // MyBaseJob also does logic of its own before/after this gets called
    9.     public void DoSomethingInJobIteration(ref MyBaseJob<IJobCustomLogicHandler> job)
    10.     {
    11.         // This function uses Translation access even though no Translation access was declared in MyBaseJob in any way
    12.         MoveToTarget moveToTarget = job.GetComponent<MoveToTarget>();
    13.         Translation selfTranslation = job.GetComponent<Translation>();
    14.         Translation targetTranslation = job.GetComponentOnEntity<Translation>(moveToTarget.TargetEntity);
    15.         selfTranslation += math.normalizesafe(targetTranslation - selfTranslation) * moveToTarget.speed;
    16.         job.SetComponent(selfTranslation)
    17.     }
    18. }

    Note: The generated data arrays in this job would either be ComponentTypeHandles, or ComponentDataFromEntity, or [ReadOnly], etc... depending on all of the combined needs of every function in the codebase that uses MyJob.Get/SetComponent(...). If the Base job had write access to Rotation on the current entity in iteration, we would just generate a ComponentTypeHandle<Rotation> and write in chunks. BUT, if one of the extension functions requires read access to RotationFromEntity as well, it would end up generating a [NativeDisableParallelForRestriction] ComponentDataFromEntity<Rotation> instead. Etc...

    It would be a very "accessible" option for making jobs I think, despite the drawbacks of manual writeback into component arrays, and not having clear visibility on what types those jobs get access to. We'd basically not have to care anymore about how to obtain access to the data we need in a job, and the end result would be as highly-optimized as doing things manually in an IJobChunk. We'd also have to figure out how to inform users of parallel writing from entity risks and make them manually acknowledge that they understand what they're doing. Maybe with an attribute over their custom function
     
    Last edited: Aug 14, 2020
  2. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    Last edited: Aug 13, 2020
  3. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,759
    Maybe I'm misunderstanding, but can't you achieve this with function pointers already?
     
  4. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    the issue is that if you pass a function pointer to a job that only has rotation access, and your function pointer would like to have translation access too, you can't make it work unless you manually go add translation access in the job and pass it along

    The idea I'm presenting is a way for jobs to not have to already know every possible component access that function pointers passed to it might possibly need. So with my proposed approach, the codegen would scan the codebase, see that there is a function somewhere that expects to have translation access from MyJob, and then add the required translation arrays to MyJob. If there's another function somewhere else that needs write access to PhysicsVelocity from MyJob, it would also add PhysicsVelocity arrays to MyJob, etc....

    The generated data arrays would either be ComponentTypeHandles, or ComponentDataFromEntity, or [ReadOnly], etc... depending on all of the combined needs of every function in the codebase that uses MyJob.Get/SetComponent(...)

    (if that is technically possible, that is)
     
    Last edited: Aug 12, 2020
  5. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,759
    Ah, not sure if this will help you but I had to implement something like this in my (utility) AI system, however I decided to avoid using any code gen (my original implementation required it though but I figured out how to go without.)

    Basically the AI tree is generated from a node graph but the AI system has no idea what this graph is. Users can implement custom nodes with data requirements etc.

    However the entire thing runs off a very small, single job. It turned out very elegant. I wasn't intending to show this off till I had finished with the node editor.

    Code (CSharp):
    1. public class UtilityAISystem : SystemBase
    2. {
    3.     private static readonly ProfilerMarker PopulateContextMarker = new ProfilerMarker("PopulateContext");
    4.  
    5.     private readonly IAISettings settings;
    6.  
    7.     private NativeHashMap<int, Reference<Selector>> aiUtilityTrees;
    8.     private NativeArray<Reference<Selector>> treeRoots;
    9.  
    10.     /// <inheritdoc/>
    11.     protected override void OnUpdate()
    12.     {
    13.         using (PopulateContextMarker.Auto())
    14.         {
    15.             // Populate all trees with current frame data
    16.             foreach (var tree in this.treeRoots)
    17.             {
    18.                 tree.Value.PopulateContext(this, this.settings);
    19.             }
    20.         }
    21.  
    22.         var utilityTrees = this.aiUtilityTrees;
    23.  
    24.         this.Entities
    25.             .ForEach((Entity entity, in AI ai) =>
    26.             {
    27.                 var selector = utilityTrees[ai.Current];
    28.                 selector.Value.Select(entity).Value.Execute(entity);
    29.             })
    30.             .WithReadOnly(utilityTrees)
    31.             .ScheduleParallel();
    32.     }
    There we have it. I have just publicly posted my entire AI system enjoy!
    Reference is pretty much just a pointer.
    Populate context iterates the entire graph and assigns all the required GetComponentFromEntity or user assigned data etc.

    The main trick for my implementation though is it requires c#8 (so 2020.2a) to get access to Unmanaged constructed types

    -edit-

    my point is, you can pass a pointer in to a struct that has the data it requires.
    blobassetreference works fine
     
    Last edited: Aug 12, 2020
  6. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    The downside I see in this approach (and correct me if I'm wrong) is that it forces all of your data to be "FromEntity" because you have to plan for the worst case scenario of data access, so there is a performance loss

    I'm guessing it's not a big deal in your case because AI is surely a very non-linear problem though
     
    Last edited: Aug 12, 2020
  7. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,759
    You're right, it's definitely the issue but I feel you could maybe use the same concept with IJobChunk and ComponentTypeHandle; though I haven't tried or thought about the problem much.

    (side note, I only just realized they renamed ArchetypeChunkComponentType to ComponentTypeHandle)
     
  8. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    The problem I've encountered regarding this:
    • My BaseJob has chunk iteration for Translation, Rotation, PhysicsVelcity, and 4 other component types. I want this for performance
    • My custom function that gets passed to the job might want to read the translation on another entity, for example. I can't know in advance
    • Just because of that, my 7 chunk-iterated arrays in the base job have to be ComponentDataFromEntity instead. Just in case the custom function passed to the job needs them
    • I cannot add an extra ComponentDataFromEntity<Translation> to the job via a generic struct, because that would do aliasing errors with the existing translation chunk arrays
    All in all, replacing my chunk iteration with FromEntity iteration gives me a ~10-15% performance loss

    It feels like an unfortunate sacrifice, especially since the use cases for FromEntity acces to one of those 7 types are possible but rare. And this is pretty much the point in my reasoning where I came up with the proposal of the codegen job of the first post. So that it optimizes the job's data arrays to fit the requirements of anything that might use it. And if it turns out that nothing needs a translationFromEntity from this specific job in a given project, the codegen would make sure that translation remains chunk-iterated rather than FromEntity
     
    Last edited: Aug 12, 2020
  9. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    3,356
    Is this an attempt to try and offer an integration layer to your end users? Few thoughts...


    The custom function approach seems problematic. So your job does what it does, produces the data it needs for that. Another concern should handle getting component references on it's own. Like you provide a callback with your data, what the end user does with it that's up to them. They sort out the dependencies, you might provide some input/output dependencies to help them. But keep separate concerns separate. What component data some end user might want for reasons you have no idea of are their concern.

    The graph idea above follow this paradigm also, the graph doesn't try to solve for fetching data individual nodes need that's up to them.

    I can't see any good reason why your jobs have to fetch data for concerns you don't even know about. That just seems like a bad idea on it's face.
     
  10. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    It's pretty much just something that would generate an exact equivalent of an IJobChunk that we would setup manually to be optimized for the exact requirements of our project, but it does all the manual work for us.

    When I make a system that lives in a package, I cannot know the exact requirements of the projects it will be used in in advance. Therefore; my only option would be to plan for the worst case scenario of how much data access those projects will need, and make everything 100% accessible (by making everything work with ComponentDataFromEntity, just in case). That means I get a performance loss that is probably not justified for most projects. See description of problem here.

    This sort of codegen is the only way I can think of to not have to make that sacrifice if the project doesn't need it. Actually I can see an alternative where 100% of the job setup is handled by users, but I've tested out that approach in practice to give it a try, and it's no good. It would require users to write hundreds of lines of code, with many ways things could go wrong if they don't do the right thing in the right order, just to create a simple moving character. It also requires tons of confusing interface callbacks that for example asks you to write back Translation into arrays, but it gives you either the entity or the chunk index to write to, depending on whether you ended up with chunk iteration or FromEntity iteration for your Translation. That's just bound to confuse people. And that's even if I assume that they'll have access to the eventual IJobEntity which hides a lot of the IJobChunk boilerplate
     
    Last edited: Aug 13, 2020
  11. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    I definitely see some weirdness in the fact that any code in the project that asks for a component type for a certain job will end up contributing to that job's data access, regardless of it it will actually be used or not and regardless of any notion of game context...

    But perhaps there are other ways to make this work without resorting to that

    I also think about how easy and accessible this would make dots programming, with few disadvantages other than low visibility on a job's data access (unless we have some editor tool to debug it), or someone mistakingly making a job function that is never used and giving the job more data than it needs. I think it's worth considering at least
     
    Last edited: Aug 13, 2020
  12. brunocoimbra

    brunocoimbra

    Joined:
    Sep 2, 2015
    Posts:
    679
    Isn't something similar already achievable by chaining jobs? If your intent is to create some API to deal with your package and the user will need to insert code at some point (be it before or after some API call) seems like that just creating smaller jobs - one that preprocess the data that the user will need to modify and another one to receive the data postprocessed by the user - will achieve the exact same goal without any codegen or complication involved.

    Another option is to just create the methods in some static utility class and let the user deal with all the boilerplate needed to either use ComponentTypeHandle or ComponentDataFromEntity, but if it is the boilerplate that you want to avoid then the first option would be the way to go.
     
  13. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    I think I might just have a really tough & specific case in my hands. I've calculated the number of jobs required to schedule if I go with the job chaining approach, and it would be something like 40 jobs per "variant" of my system. In practice that could mean hundreds of jobs scheduled just for that one gameplay feature, because of the many variants

    The link at the end of the "additional comments" spoiler of my first post explains why I need so many jobs (different use case but similar to mine)
     
    Last edited: Aug 13, 2020
  14. brunocoimbra

    brunocoimbra

    Joined:
    Sep 2, 2015
    Posts:
    679
    Oh, didn't saw the second spoiler tag Haha

    In that case, I would say that preparing for the worst (FromEntity everywhere) would be ok. Not sure how codegen could optimize all that work for you without falling into the issue where we will need to check the decompile window all the time to ensure that the things are running as planned. In the end I think it is the usual tradeoff between something generic/reusable and something specific, with the later being more performant almost always.

    But it would be cool to know that Unity already have something in mind to improve code re-use.
     
  15. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,683
    Didn't read the thread carefully, sorry if missed something, but if you sure in safety you can disable aliasing error and use handle and CDFE at the same time (Not checked with generic, but with non generic jobs it will work fine)
    Code (CSharp):
    1. [ReadOnly\WriteOnly\Without attribute]
    2. public ComponentTypeHandle<Translation> translationHandle;
    3. [NativeDisableContainerSafetyRestriction]
    4. public ComponentDataFromEntity<Translation> translationData;
     
    Last edited: Aug 13, 2020
    thelebaron and PhilSA like this.
  16. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    Oh I didn't know that was possible! That could be a decent solution

    However I don't fully grasp the concept of aliasing and I imagine the error is there for a reason. What should I be aware of, when disabling the restrictions? Is it just that I need to realize that something else might be wanting to write to that array before/after I write to it myself?
     
  17. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,683
    If you disable this restriction you should be sure that no one will
    read and write
    \
    write and write
    at the same element from CDFE and ComponentTypeHandle at the same time as it will be a possible hidden race condition with very weird and unexpected results otherwise.
     
    PhilSA likes this.
  18. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    I see. In this case my CDFE would always be readonly so this is looking like a good plan, thanks