Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Scheduling 100s of jobs just for one gameplay system; could it eventually become a valid approach?

Discussion in 'Entity Component System' started by PhilSA, May 3, 2020.

  1. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    I'm trying to come up with the best "design pattern" to allow for maximum extensibility of a certain system. Aside from scheduling a very huge amount of jobs, all the other solutions I could think of involve either a bunch of "gotchas", or writing tons of boilerplate, or relying very heavily on codegen. I'd prefer to avoid these things if possible. (I started a similar topic a while ago, but I'm giving it another try)

    Here's a practical example of the kind of use case I have in mind:
    You want to implement a "lazer system". A lazer is fired from a point, and depending on the surface it hits, can either bounce, go through, or stop on that surface. And in your project, you'll need different kinds of lazers that all behave differently (bounce on different kinds of surfaces, go through different kinds of surfaces, etc....)


    This is a very simplified example that doesn't actually represent the true complexity of the real use cases I'm facing, so you need to assume a few things:
    1. the conditions for bounce/go-though/stop could depend on a million different things. We need a solution that allows us to create new types of lazers that need access to ComponentDataFromEntity types that were not originally provided to the lazer job (for example, making a lazer that checks if there's a MySurfaceType component on the hit entity, and needs to read data from MySurfaceType in order to determine if it goes through it)
    2. we're not allowed to modify the source code of the original lazer job because it'll be in a 3rd party package
    It may seem like a very specific problem, but I ended up encountering it very often so far for very common gameplay scenarios (a character controller system, a projectile system, etc...)

    Now here's the solution I am thinking of, which involves tons of jobs to be scheduled just for that one lazer system:
    You can tell a lazer to bounce up to X amount of times. Let's say that amount of times is 20, and we have 10 different kinds of lazers in our project, we'd schedule this batch of jobs 20 times:
    1. The BaseLazerSystem schedules a job that raycasts the lazer collisions for this iteration, and stores all raycast hits in a dynamicBuffer on the lazer entity (but exits early if the direction of the lazer is float3.zero)
    2. Our 10 SpecificLazerSystems schedule their job to remove all hits in that dynamicBuffer if the lazer is supposed to go through them
    3. The BaseLazerSystem schedules a job to select the closest hit among the remaining hits in the dynamicBuffer
    4. Our 10 SpecificLazerSystems schedule their job to determine if that selected closest hit is a "bounce" or a "stop"
    5. The BaseLazerSystem schedules a job to determine the new direction of the lazer for the following iteration (math.reflect() if it bounces, float3.zero if it stopped)
    Now that means 20 iterations * 23 jobs = a whooping 460 jobs scheduled, just for these lazers. (or 240 jobs if we combine points 2,3,4 in a single specific-lazer job. Still a lot). Currently, job scheduling has a relatively high cost, so this solution is completely out of the question. But since the cost of job scheduling is believed to improve eventually, I'm wondering if this could eventually become realistic? Depends on what kind of improvement is expected...

    Very briefly, here's my other (current) approach, and why I'm not satisfied with it:
    The BaseLazerSystem<T> schedules only one job that does all the iterations, and take a generic struct type whose purpose is to:
    • hold all the additional ComponentDataFromEntity arrays that the specific lazer system could need access to
    • implement IsHitValid() and IsHitBounce(), which are called during the single LazerJob that the BaseLazerSystem launches.
    What I don't like about this solution is:
    1. every single component array types in the BaseLazerSystemJob needs to be ComponentDataFromEntity just in case we'll need them in one of our IsHitValid() and IsHitBounce() implementations. If our IsHitBounce() needed to know about the hit body's translation for example, we wouldn't be able to add a ComponentDataFromEntity<Translation> on top of the already-existing ArchetypeChunkComponentType<Translation> for example, because it would create aliasing errors.
    2. All of the ComponentDataFromEntity in the base lazer job need to be [NativeDisableParallelForRestriction] and cannot be [ReadOnly], just in case we need to write to them (imagine we want to make a specific lazer implementation that loses energy on each hit, etc...). Users have to be aware that it's only safe to write in those arrays if it's on the entity of the lazer itself, and the lack of ReadOnly means a potential performance loss
    3. It will make it impossible to implement IsHitValid() and IsHitBounce() logic through visual scripting, while the "lots of jobs" approach makes this possible
     
    Last edited: Aug 12, 2020
    CookieStealer2 likes this.
  2. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    3,983
    This is generally the approach I come to for these kinds of problems as well. Think of it this way. There is some high number of lazers where this approach makes unquestionable sense. That number decreases when switch from parallel jobs to single-threaded jobs. That number decreases more when switching from jobs to running on the main thread. And that number will continue to shrink even more once Unity rolls out struct systems that execute entirely in Burst.

    In this particular case, you are worried about the number of unique jobs causing an overhead issue (a complexity cost). If that were the case, you could trade complexity cost for operation cost and possibly gain a speedup.

    1) The base lazer system performs the raycasts. It then sorts the hits in order for each lazer.
    2) Each specialized lazer system walks through the hits and marks any hit type it applies to as Ignore, Bounce, or Stop. The system can exit early after the first Bounce or Stop.
    3) The base lazer system computes the next trajectory of the lazer.

    This approach cuts the number of jobs in half.

    If the bounce iteration count is the bigger issue (sequential iteration of complexity problem), then you can pre-compute that complexity (determine all obstacles' interactions with all lazer types in advance), or ask Unity to make ComponentDataFromEntityDynamic so that you can use a context object and function pointers not too different from what Hybrid Renderer V2 does.
     
  3. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    true. However, I'm still wondering if 200 jobs is in the ballpark of "way too heavy in scheduling time compared to what it's doing". I'd even feel uncertain about 50

    Also the effects of scheduling lots of jobs get multiplied when using several worlds, as is the case with Netcode

    Interesting. I'm not familiar with this, could you give me a tip on where to look in HRv2?
     
    Last edited: May 3, 2020
  4. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    3,983
    Have a look at BurstCompatibleTypeArray in HybridV2RenderSystem.cs

    The idea is that you can create a struct with a bunch of dynamic component types and then your specializations could reserve indices for specific component types during initialization. Function pointers would then be able to cast the dynamic types back to the specific types for the specializations.

    It is a non-trivial setup and I would only ever do it for something I could not solve any other way.
     
    PhilSA likes this.
  5. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    I think it's insane to try to schedule 200 jobs for this case. The approach of using ComponentDataFromEntity / raycasting inline in the job is simpler and faster than scheduling jobs could ever be, even if we spent an infinite amount of time optimising job scheduling performance...

    I think fundamentally, problems that can't be batched but require immediate mode API's just don't fit into batched API's...
    Your problem definition is very specifically requiring immediate mode responses.

    You could change the problem definition to allow for batching by allowing one frame delay. One bounce per frame etc. But if the problem you need to solve has that exact definition then lets not fit a square pig into a round hole...


    A couple points on that:
    1. ComponentDataFromEntity is random memory access, and thats not the end of the world...

    When writing DOTS code we sometimes fall into the trap of "Oh my god, everything must be linear". While thats a great general rule by default and definately are rule you want to follow when you process many things. It's not something that makes sense to blindly follow on everything.

    It's important to note that scheduling jobs internally has plenty random memory accesses :) Its just that you don't see them because you just call it through an API...

    2. In terms of modularity the best approach is to offer users a new job type they can hook their own code into.

    interface IIsHitBounceProcessor
    {
    bool IsHit(Entity, ...);
    }


    Then you make a utility method that schedules a single job processing the outerloop of all the raycasting or whatever you need, and you call this IIsHitBounceProcessor.

    The alternative is to let the user control the outerloop and simply have utility methods he can call to make the logic easy enough.

    My preference is always to give users control of the outerloop but there are many cases where the outerloop is just too complex to wrap in something nice or where it can't be done efficiently in a naive way. In that case using IIsHitBounceProcessor style thing makes most sense.

    My own design process for this choice in particular is to write multiple pseudo code examples of both versions using both API's and then look at the both and reason about what I like better. Making a pros / cons list. Sometimes its not only about API usability but also other tradeoffs.
     
  6. Lieene-Guo

    Lieene-Guo

    Joined:
    Aug 20, 2013
    Posts:
    547
    What a coincidence, I am just writing this function. And this thread pops up.
    stopCollideWith is the mask that stops the chain.
    reflectCollideWith is the mask reflects the chain.
    and targetCollideWith is the target that can take damage from the chain, stop/reflect on target defined by stopOnTarget.

    Code (CSharp):
    1.         public static bool CastReflectiveChain2D(
    2.             [ReadOnly][NoAlias] CollisionWorld2D CollisionWorld, float2 startPosition, float2 startDirection, float maxDistance,
    3.             bool stopOnTarget, uint belongTo, uint stopCollideWith, uint reflectCollideWith, uint targetCollideWith, int cutstomGroup,
    4.             [NoAlias] NativeSlice<float3> verticesOut, [NoAlias] out int verticeCount,
    5.             MapMode2D3D map, float ground,
    6.             [NoAlias] DynamicBuffer<HitTarget> hits, float damageScale = 1, float damageScaleFade = 1)
    7.         {
    8.             Assert.IsFalse(math.all(startDirection.Approximately(0, MathX.Eps_Length)));
    9.             Assert.IsTrue(verticesOut.Length > 1);
    10.  
    11.             startDirection = math.normalize(startDirection);
    12.             var hitOrReflectFilter = new CollisionFilter() { BelongsTo = belongTo, CollidesWith = stopCollideWith | reflectCollideWith | targetCollideWith, GroupIndex = cutstomGroup };
    13.             verticeCount = 0;
    14.             var vertEnd = verticesOut.Length;
    15.             bool hitAnyTarget = false;
    16.             bool reflect = false;
    17.             verticesOut[verticeCount++] = startPosition.MapTo3D(map, ground);
    18.             while (!(hitAnyTarget & stopOnTarget) & reflect & (maxDistance > MathX.Eps_Length) & (verticeCount < vertEnd))
    19.             {
    20.                 var ray = new RaycastInput2D() { Origin = startPosition, Displacement = startDirection * maxDistance, Filter = hitOrReflectFilter };
    21.                 if (CollisionWorld.CastRay(ray, out var hit))
    22.                 {
    23.                     var body = CollisionWorld.Bodies[hit.RigidBodyIndex];
    24.                     var bodyBelongTo = body.Collider.Value.Filter.BelongsTo;
    25.                     if ((bodyBelongTo & targetCollideWith) != 0)
    26.                     {
    27.                         hits.Add(new HitTarget() { Target = body.Entity, DamageScale = damageScale });
    28.                         damageScale *= damageScaleFade;
    29.                         hitAnyTarget = true;
    30.                     }
    31.                     reflect = (bodyBelongTo & reflectCollideWith) > 0;
    32.                     verticesOut[verticeCount++] = hit.Position.MapTo3D(map, ground);
    33.                     startDirection = math.reflect(startDirection, hit.SurfaceNormal);
    34.                     startPosition = hit.Position + startDirection * MathX.Eps_Length;
    35.                     maxDistance *= (1 - math.saturate(hit.Fraction));
    36.                 }
    37.                 else
    38.                 {
    39.                     verticesOut[verticeCount++] = ray.End.MapTo3D(map, ground);
    40.                     maxDistance = 0;
    41.                 }
    42.             }
    43.             return hitAnyTarget;
    44.         }
    The 2D part is because I made a physic2D from Unity.Physic. identical API just 2D, and run faster as GJK EPA RayCast and Jacobin Is much simpler in 2D.
     
  7. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    939
    What is an acceptable number of system for a DOTS game ?
    If I take my ability package as exemple, I have 2 systems per effects and one of the system as 3 jobs.
    Each effect has it's own static data but may also require to read data from the caster and target of the ability.
    That mean I have to provide the necessary components to the job in order to process my effect.
    And that component set is not identical to all effects.

    With that pattern for abilities, the number of system could quickly grow and I don't see how to avoid that...
     
  8. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    I think ~1000-2000 system updates & 4000 Job schedules in a 60FPS complex AAA game is reasonable.

    (Using up 200 job schedules for a single system is not...)
     
  9. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    Exactly. This is a perfectly reasonable way of writing this type of code. Obviously it's not how you would write that if you have thousands of those things. In that case you have to relax constraints of immediacy & write everything batched. But for simple specific non-scalable game play problems this is fine.
     
    jashan likes this.
  10. Lieene-Guo

    Lieene-Guo

    Joined:
    Aug 20, 2013
    Posts:
    547
    In my case reflection count is limited (by the size of NativeSilice, about 6 times of reflection maximum),
    And parallel happens when there are hundreds of Lasers the are shot at the same frame.
    this function can be called in parallel.
    I can be used so that the laser is forwarded for a limited amount of reflections each frame.
    If it is a ray in a pair of close and parallel mirrors, then nothing could help. the limit would be C/distance.
    ;) as we are in a Quantum World.
     
    Last edited: Oct 6, 2020
  11. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    3,983
    This is a fun thread to revisit.
    I am really surprised you suggested this. This is actually the "current solution" in the original post. This is also my preferred approach to this kind of problem. Here is a real use case I have: https://github.com/Dreaming381/Lati...hysics/Utilities/Queries/Physics.FindPairs.cs
    At the top is the interface (which I also suffixed with "Processor").
    Underneath that is the struct payload that gets passed into the Execute method of the interface. I marked it with [NativeContainer] to prevent it from being stored in a container by the implementer of IFindPairsProcessor. There's an intermediate struct to make the API fluent-style to help separate the config from the scheduling, and then there's the schedulers which are just scheduling generic jobs. To get the generic jobs to run in builds, I have a custom tool which patches an assembly to find all implementations of a given interface and all generic jobs with that interface and define concrete types. This only runs for builds, as it is unnecessary in the Editor in 2020.1 and earlier. https://github.com/Dreaming381/Lati.../Core.Editor/BurstPatcher/BurstPatcherHook.cs

    Here's the problem:
    First, this approach is now broken in 2020.2 as the patching would now have to be done in the editor and not just builds according to this post: Will [RegisterGenericJobType] be required for all generic jobs going forward?
    Maybe there's a clean pipeline hook where my logic will fit fine, but I am not aware of it.

    Second, if I were to manually require users to specify the generic job instances, that defeats the abstraction that the scheduling methods provide. It would also require that I expose the jobs publicly, which I don't want to do.

    Third, while I could get around these issues if I were to write a custom job type, that has its own issues. Some of the original conversations can be found here: https://forum.unity.com/threads/12-21-19-my-personal-feedback-of-the-full-dots-spectrum.752576/ Posts 1, 11, and 16. But to summarize:
    1) I would have to do some really sketchy unsafe stuff to get access to the native containers that are required by the outer loop by not exposed to the inner loop.
    2) I have to write the custom scheduling methods for job types when the scheduling I want imitates already existing job types (mostly IJob and IJobFor).
    3) I have to write a custom job type scheduler for every variant (similar to what IJobParallelForFilter does) on top of the schedulers for scheduling the job in the public-facing API.

    While I do think that interfaces like this are the way to go, there's a lot of dragons here. Hence why I am surprised you suggested this approach.
     
  12. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    It was not intentional and we fixed it in master. See other thread.
     
  13. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    Other than that temporary regression in 2020.2 beta... What are the other dragons?
     
  14. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    3,983
    Actually all of them. The "temporary regression" was actually only one of two "regressions" (one that actually didn't affect me at all but I made suggestions to help out the others who were affected), although the other I don't expect you to fix as it is unintuitive and alternative solutions would be better.

    So let's assume that I decided to use the interface approach because my outer loop is really, really complicated. There is a very high chance that the outer loop is complicated and requires custom setup by a method. I have two options:

    First Option - Generic Jobs Scheduled by Generic Methods
    In 2020.1, this works in the Editor. That means I can iterate on my code quickly. It does not work in builds; however, I have a little more time to wait on builds, and so I can afford running my more expensive codegen tool to resolve it.

    But now, from my understanding, these generic jobs scheduled from generic methods no longer work in the Editor in 2020.2. That means, I need to make my codegen fast and efficient in the Editor. The problem is that codegen for code running in the Editor is a not-very-well-documented fast-moving target.

    Dragon 1: Not being able to schedule generic jobs from generic methods breaks the encapsulation of the generic job and setup.

    Dragon 2: The fix for Dragon 1 involves performing codegen for every compilation in the Editor. This is not well-documented and changes often.

    Maybe this is just a matter of me needing to have a conversation with someone who understands the compilation pipeline so that I can figure out how to
    1) Run my tool after initial compilation has finished
    2) Detect which assemblies were actually modified so that I can skip searching through types using Cecil
    3) Remember which types exist in an unchanged assembly (what's the best place to store this information?)
    4) Differentiate between Editor and build compilation so that I don't repeat work and don't modify the wrong assembly.

    Side note: My tool generates source code that gets compiled to an assembly. The tool then overwrites a dummy assembly in the package. The assembly is only there to be read by Burst. The code inside is not executed at runtime.

    Second Option - Use Custom Jobs
    Custom jobs have a ton of benefits, but the API is still unnecessarily clunky when you just want to imitate an existing job type with some wrapper data and logic.

    Dragon 3: I have to write the boilerplate for the job scheduler which includes dealing with reflection data when really I just want an existing job type (IJob or IJobFor typically).

    Dragon 4: The documentation for custom job types does not discuss extra containers that are used in the custom job but not necessarily exposed to the custom job type (the outer loop containers). While there is an example of this with IJobParallelForFilter, it is not obvious which parts of the code are actually necessary and why.
     
  15. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    >First Option - Generic Jobs Scheduled by Generic Methods
    These will actually work. Automatically. We are fixing the regresssions we introduced in 2020.2.
    On top of this we are making editor / AOT be the same.

    This is not correct. Look at the two samples public enum provided. They will work correctly.
     
    MNNoxMortem likes this.
  16. brunocoimbra

    brunocoimbra

    Joined:
    Sep 2, 2015
    Posts:
    677
  17. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
  18. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    3,983
    I think I see the difference between those examples and mine. I will write up some simple tests tonight which highlight the difference. I suspect that this little difference will trip a lot of people up and will need some detailed documentation.
     
  19. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    3,983
    @Joachim_Ante
    I want to apologize for my previous rants and thank you for pushing me to explore this topic further. I had no idea I was so trivially close to being able to throw out that build tool and have Burst do all the work for me!

    I made a repo highlighting my findings: https://github.com/Dreaming381/BurstGenericsTest
    The repo showcases several approaches that work in the Editor but not in Builds using 2020.1 and Burst 1.3. It also shows alternatives that work in all scenarios. Hopefully people find these examples helpful.

    On a side-note, I think the "right-most" approach could be a really good solution for SortJob in NativeSort.cs, assuming you haven't already fixed it internally.
     
    Timboc and PublicEnumE like this.
  20. PublicEnumE

    PublicEnumE

    Joined:
    Feb 3, 2019
    Posts:
    729
  21. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    To fully understand your sample code.

    Can you confirm:
    1. A & B do not work with the burst generic rules we have today
    2. C, D, E do work with the burst generic rules we have today


    Do we agree that the rules for generics make sense?
    Our aim is to achieve both great iteration speed & enabling as much generics as possible within that. Do you agree that this is what the current implementation does?


    >On a side-note, I think the "right-most" approach could be a really good solution for SortJob in NativeSort.cs, assuming >you haven't already fixed it internally.
    I believe the rules for bursted generics are not yet fully understood by everyone on the DOTS team. So this thread bringing all this to light is very useful.


    So these being the rules a couple extra thoughts:
    1. We need to make sure that when the wrong pattern is used we have really good error messages that link back to the documentation. The rules aren't trivial & when someone hits them, the error message shouldn't guide into an overreaction of "S*** i have to remove all generics" but rather changing the code slightly to fit into the rules.
    2. Is the documentation on this topic helpful & accurate or is there something that would have helped on this topic? For sure it seems, the documentation should really recommend a fluent syntax as a recommend solution.
     
  22. PublicEnumE

    PublicEnumE

    Joined:
    Feb 3, 2019
    Posts:
    729
    I really appreciate the work you've done here, and you sharing this. Thank you!

    I wanted to comment on a note you made in your 'C' example, which is most like what we're currently doing. In that code, you have this comment on the syntax for scheduling a job:

    Code (CSharp):
    1. //The invocation here is complicated, as now I have to repeat the type of the processor at the beginning of the chain.
    2.             //From an API design POV, I don't like this.
    3.             Api<CheckBurstProcessor>.RepeatExecute(1, processor).Run();
    We agree! We would also prefer a cleaner syntax, which didn't require you to repeat the processor type as a generic argument.

    We've looked into ways to avoid this. In your 'D' and 'E' examples, you propose some good ones, which make use of type inference.

    The problem for us comes when the processor interface (in your example case, IRepeatExecuteProcessor) is itself generic. In our case it must be, because our 'processor' inner loop need to work on some components. The signature of our processor's 'Execute' method needs to include specific Component references, which must be passed into it from the outer loop. That means the outer loop (wrapper job) must know about those ComponentTypes.

    But when 'IRepeatExecuteProcessor' becomes 'IRepeatExecuteProcessor<TComponent>', it adds some undesirable wonkiness to the scheduling api. Since c# can't infer generic types from parameter constraints (and doesn't seem like it will anytime soon), it seems unavoidable that you end up needing to repeat all the component types involved (including the processor type) as generic arguments in your scheduling method. :(

    Here's an example. I adapted your 'D' (Job in Config) example to support a generic processor interface. This is as close as I could get to your original design:

    Code (CSharp):
    1. using Unity.Burst;
    2. using Unity.Entities;
    3. using Unity.Jobs;
    4. namespace JobInConfig
    5. {
    6.     public struct Foo : IComponentData
    7.     {
    8.         public int value;
    9.     }
    10.  
    11.     public interface IExecuteProcessor<TComponent> where TComponent : IComponentData
    12.     {
    13.         void Execute();
    14.     }
    15.  
    16.     public class F_JobInConfig_GenericInterface
    17.     {
    18.         protected void RunProcessor(ExampleProcessor processor)
    19.         {
    20.             // Frustrating need to repeat processor type, as well as its generic component type
    21.             // it would be great not to need this
    22.             Api.RepeatExecute<ExampleProcessor, Foo>(1, processor).Run();
    23.         }
    24.  
    25.         public struct ExampleProcessor : IExecuteProcessor<Foo>
    26.         {
    27.             public void Execute()
    28.             {
    29.  
    30.             }
    31.         }
    32.     }
    33.  
    34.     public struct Config<TProcessor, TComponent> where TProcessor : struct, IExecuteProcessor<TComponent> where TComponent : IComponentData
    35.     {
    36.         public TProcessor processor;
    37.         public int repeatCount;
    38.  
    39.         public void Run()
    40.         {
    41.             new RepeatJob { repeatCount = repeatCount, processor = processor }.Run();
    42.         }
    43.  
    44.         [BurstCompile(CompileSynchronously = true)]
    45.         public struct RepeatJob : IJob
    46.         {
    47.             public int repeatCount;
    48.             public TProcessor processor;
    49.  
    50.             public void Execute()
    51.             {
    52.                 for (int i = 0; i < repeatCount; i++)
    53.                 {
    54.                     processor.Execute();
    55.                 }
    56.             }
    57.         }
    58.     }
    59.  
    60.     public static class Api
    61.     {
    62.         public static Config<TProcessor, TComponent> RepeatExecute<TProcessor, TComponent>(int repeatCount, TProcessor processor) where TProcessor : struct, IExecuteProcessor<TComponent> where TComponent : IComponentData
    63.         {
    64.             return new Config<TProcessor, TComponent> { repeatCount = repeatCount, processor = processor };
    65.         }
    66.     }
    67. }
    It would be wonderful to find a way around this. If we're missing something that's jumping out at you, we'd be very appreciative if you'd point it out.

    I believe Unity also ran into this frustrating c# limitation when designing their IJobForEach api. There was a comment in that source code that read "This would be a lot simpler if c# could infer generic types based on constraints!". This may have been behind the need to have such extensive code-gen as part of the IJobForEach design, and was maybe a contributing factor to its demise.

    I feel you, DOTS engineer. I feel you...

    - - - - - - - - - - - - -

    Worth a note: Default interface methods in C# 8.0 seemed like it might offer a solution. In that case, you could potentially hide the 'RepeatExecute' method call inside a default interface method:

    Code (CSharp):
    1. public interface IExecuteProcessor<TComponent> where TComponent : IComponentData
    2. {
    3.     void Execute();
    4.  
    5.     void Schedule(int repeatCount)
    6.     {
    7.         Api.RepeatExecute<TComponent>(repeatCount, this).Run();
    8.     }
    9. }
    10.  
    11. public class F_JobInConfig_GenericInterface
    12. {
    13.     protected void RunProcessor(ExampleProcessor processor)
    14.     {
    15.         // Hey, no more explicit generic arguments!
    16.         processor.Schedule(1);
    17.     }
    18.  
    19.     public struct ExampleProcessor : IExecuteProcessor<Foo>
    20.     {
    21.         public void Execute()
    22.         {
    23.  
    24.         }
    25.     }
    26. }
    But there seem to be a lot of caveats: First, I'm not sure if that syntax would actually work (haven't tried it in c# 8 yet). Second - it sounds like default interface implementations will require boxing (as well as that iffy 'this' call), so Burst is unlikely to support them. and Third, Unity has omitted this c# 8 feature from 2020.2 anyhow, so it's probably a moot point.
     
    Last edited: Oct 8, 2020
  23. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    3,983
    Can confirm. In 2020.1, A and B do work in the Editor but not in builds and do not show up in the Burst Inspector. These were the cases my tool addressed. C, D, and E do work in both Editor and builds and show up in the Burst Inspector without any intervention.

    There are definitely still dragons here, but these ones are friendly once you get to know them. In other words, documentation and good warning messages are really important.

    I think it is a good local maximum in that regard. I think the next step would be for a mechanism to "propagate" generic types to other generic types using attributes or something. In other words, let's say I have GenericSystem<T> which uses that static class library with Config<T>. If there were an attribute or some other trick like making a static member of Config<T> that could specify that any concrete instance of GenericSystem<T> should also force a concrete instance of Config<T>, I think that would solve a lot of additional use cases. I won't have time until Saturday, but I can write up an example then if necessary.

    100% agree. You probably don't want to do this with static analyzers though. I generic type with jobs used in a generic context may not necessarily be using jobs within that context. (Example: extension methods to Config<T> which modify its members) So I suspect this will have to be limited to a runtime error message.
    From the documentation, I did not know that you could use generic static classes the way @PublicEnumE did. I thought Burst was specifically looking for "instances" of types. Also, the fact that the return value from a concrete invocation of a generic method can satisfy the rules was a bit of a surprise. I did not expect that to work. I'm glad it does though! What the documentation states looking back is accurate. But more examples highlighting the boundaries and some complex usages would go a long ways.
     
  24. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    3,983
    Your use case is different from mine, so it is important to note that the ideal API for you may be different. In my case, making Api generic meant that users needed to know whether the method they wanted was generic or not before choosing which static class to use. With the improved design, the IDE suggests all options which makes API discoverability much friendlier.

    I don't think default interface methods solve this.

    Anyways, there's a couple things you can try if you want better type inferencing (although your massaged usage is still a huge improvement of the generic static class). I don't remember the exact rules, but I think if you could provide arguments that allow inference of all the generic types, then that is legal. So by having an additional argument, say a ComponentDataFromEntity<Foo>, you might be able to get away with it. To further extend that thought, you might be able to break up your generic function into multiple functions in a fluent chain. One method creates ConfigA<T> and then a second method uses ConfigA<T> as an argument and produces ConfigB<T, U>. The latter would contain the jobs.
     
  25. PublicEnumE

    PublicEnumE

    Joined:
    Feb 3, 2019
    Posts:
    729
    I’m not sure I agree with this. That variation of the syntax was something we had previously tried, and decided against. But...

    I had not considered a fluent approach here. I’m often not a big fan of them, but if it provides a way to avoid explicit generic arguments - or even if it just avoids needing to be explicit about the Processor struct type - then it’s worth a look! Thank you.
     
    Last edited: Oct 8, 2020
  26. PublicEnumE

    PublicEnumE

    Joined:
    Feb 3, 2019
    Posts:
    729
    After reading up on how type inference works in fluent apis, I don’t see a way for it to actually cut down on the number of generic argument’s you’d need to explicitly define.

    I don’t want to derail this thread with an unrelated topic. But you know a way that I don’t, please PM me (or reply here if you think it’s appropriate). I’d love to learn about this - Thank you!
     
    Last edited: Oct 8, 2020
  27. Arowx

    Arowx

    Joined:
    Nov 12, 2009
    Posts:
    8,194
    This sounds similar to the idea I had about a single atomic reusable game system. If you take this logic to the furthest extreme you write a system for every logic operator and then write entities that make up the games code triggering systems as needed.

    The trouble with working this way is you are hogging CPU memory bandwidth for data whilst reducing the bandwidth load on programming code.

    Ideally you want to balance operations/code bandwidth with data bandwidth.

    Worst case scenario you could be thrashing the CPU cache reloading the same data multiple times just to run through separate systems that could have their code combined to fewer systems with huge performance savings.

    Imagine Systems A -> B -> C and data cache loads 1, 2, 3, 4, 5

    On a 2 core system c0, c1

    C1 A1,A3,A4,A5,B4,B5,C3,C5 -- 8 Cycles
    C2 A2,B1,B2,B3,C1,C2,C4

    Now with an ABC system called D

    C1 D1,D3,D5 -- 3 Cycles
    C2 D2,D4,

    They key point here is each cycle would be reading and writing to main memory the slowest part of the whole operation and the main performance point of DOTS/ECS systems.

    Side note:
    Regarding lasers this type of system could be optimised with a bit masked layers approach where lasers that hit other objects are matched with objects that impact them. With two masks one for reflection and one for modify and passthrough you would massively reduce the processing/data needed to run a system that handles both cases.

    Combine this with a grid or even better Binary Volume Hierarchy and you would be able to limit the data sizes at each stage of testing. If you can keep this within the cache size available to the CPU you should see massive performance gains.​
     
    Last edited: Oct 8, 2020
  28. PublicEnumE

    PublicEnumE

    Joined:
    Feb 3, 2019
    Posts:
    729
    would you mind specifying which post or topic that is is in response to? I got confused. :p
     
  29. Arowx

    Arowx

    Joined:
    Nov 12, 2009
    Posts:
    8,194
    I did a TLDR so it's based on the idea in the original post.
     
    PublicEnumE likes this.
  30. Arowx

    Arowx

    Joined:
    Nov 12, 2009
    Posts:
    8,194
    Mind you if you could prefetch the core data you need for those systems so it is all stored in the CPUs cache then you would be only using the code cache to load in the next system to process the data.

    Potential benefits, blazingly fast performance for the core of your game, atomic and reusable systems, but you could be hardware constrained due to CPU cache sizes.
     
    Last edited: Oct 9, 2020
  31. Ruchir

    Ruchir

    Joined:
    May 26, 2015
    Posts:
    927
    Can someone post the TL;DR: Solution for this problem? I'm also searching for this
     
  32. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    3,983
    I can't speak for @PhilSA but I think the TL;DR is that generic jobs are way more viable and supported than we initially all thought. Since this thread, there's been official documentation released on the topic: https://docs.unity3d.com/Packages/com.unity.entities@0.17/manual/ecs_generic_jobs.html

    And this repo takes things one step further to showcase one of several possible API designs using this information: https://github.com/Dreaming381/BurstGenericsTest
     
    Ruchir and Sarkahn like this.
  33. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    like DreamingImLatios said, generics are the way to go for this (much better idea than scheduling tons of jobs)

    In my first post I mentioned that the downside of generic jobs is that there might be cases where you want more control over the data arrays that are in the job by default. In these situations, the solution I like is to simply leave the creation of the entire job to the users, but provide static utility functions that they can call in the job's update loop. The static function's parameters will inform the user of everything it needs in order to perform the work. Now they just have to come up with a job that feeds those parameters to the static function

    It involves a bit of boilerplate, for sure, but I think it is a very robust and "non-weird" approach. By using the in/out/ref keywords sensibly, it's 100% clear what data is being used and what data is being modified. And the user keeps total freedom over the specifics of the job and its data (what BurstCompile options the job is using, what's readonly, what's chunk-iterated, what's FromEntity, etc...)
     
    Last edited: Feb 8, 2021
    Sarkahn, Timboc, Ruchir and 2 others like this.
  34. echeg

    echeg

    Joined:
    Aug 1, 2012
    Posts:
    90
    Can you share your 2D solution? I have a 2D game and lags start on a large number of spherecasts...