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

How does WithChangeFilter() work exactly?

Discussion in 'Entity Component System' started by illinar, Jan 7, 2020.

  1. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    857
    Hi, everyone, could you please remind me how the change filters work in DOTS and maybe share some tips on using it. Because as far as I remember it gets triggered when a system that is writing to a certain archetype gets called. Or is it when a writing query get's called (that would make more sense)?

    If it's the latter, then things are more straight forward and usable because I can avoid querying for entities I'm not going to actually write to. eg.
    if A=X Entities.Foreach(ref MyComponentType c), otherwise do nothing and change won't trigger


    So which way does it work?

    And I understand that the change is per chunk, or is it per archetype? If it is per chunk then every chunk pulled by a query will get marked, and that means that a query for one archetype will trigger change on a completely unrelated archetype query just because they share the chunks.
     
  2. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,554
    The last written moment number (change version number) is recorded per type of each chunk, when that type was requested with write access. The write also needs to be related to OnUpdate of a system since the number came from update code that get +1 from the previous system that updated.

    The per type of each chunk granularity could also be learned from .DidChange on ArchetypeChunk which has <T>.

    But it looks like the change triggered per chunk since EntityQuery/IJobForEach bring you things in chunk as the most granular unit. It could do no better than bringing chunks that has only one of its type marked as changed.

    ForEach with `ref` is a write access to that type of all matched chunks regardless if the lambda code actually writes or not. If you use chunk iteration then you can look through the chunks broadly without triggering write until you call .GetNativeArray with ACCT with isReadOnly: false

    It is the latter way
     
  3. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    857
    Thank you. To be clear though, because this sounds a little confusing:
    Is it per chunk and not [per type + per chunk]? What would make most sense and would be most useful is if it was per type per chunk.
     
  4. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,554
    I remembered each chunk has a version for each of its type. For example you have an archetype with types A B C but you create many entities that they spans 3 chunks.

    Then System X has an EQ that says A B C so it got those 3 chunks.
    System Y has an EQ that also says A B C, but has changed filter set on B type added to EQ. [UpdateAfter(X)]

    - If System X used ForEach(ref B, in A, in C) then all 3 chunks are dirty on the type B. System Y queried all 3 chunks since they are all changed.
    - If I change the prior ForEach to ref A, and supposed that it is just per chunk and not per type + per chunk, system Y would pick up the change caused by A even if it said change filter is on B.
    - If System X used chunk iteration, and was able to stop itself from ac.GetNativeArray<B> for some of the chunks while iterating ArchetypeChunk (e.g. check on shared component index and see something). System Y may now get less than 3 chunks.
     
    YurySedyakin and illinar like this.
  5. YurySedyakin

    YurySedyakin

    Joined:
    Jul 25, 2018
    Posts:
    61
    I've got a few additional questions regarding WithChangeFilter:
    - There is an overload with two type arguments WithChangeFilter<T1, T2>. Does it mean when either of T1 or T2 has changed? Or both (would be strange but just to make sure)?
    - What if I need to change-check more then two components? Can I use multiple WithChangeFilter calls with different types? Are they combined by AND or OR then? And if I can't use multiple WithChangeFilter calls then what? Do I fall back to chunk iteration with manual DidChange calls?
     
  6. joepl

    joepl

    Unity Technologies

    Joined:
    Jul 6, 2017
    Posts:
    85
    WithChangeFilter with two components should trigger when either of those components has changed.

    We do let you use multiple WithChangeFilters in Entities.ForEach (and merge the results) but it looks like the underlying mechanism does not allow for more than two change filter types. I'll look into loosening that restriction.
     
    BigRookGames, 5argon and YurySedyakin like this.
  7. BobFlame

    BobFlame

    Joined:
    Nov 12, 2018
    Posts:
    94
    Hi, @joepl. I have a question. Will change flag be set if component is modified by "ComponentDataFromEntity"?
     
  8. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,626
    Yes. To be precise if you step through a little it's set in this function.

    Code (CSharp):
    1.  
    2. public static byte* GetComponentDataWithTypeRW(Chunk* chunk, Archetype* archetype, int index, int typeIndex, uint globalSystemVersion, ref LookupCache lookupCache)
    3. {
    4.     if (lookupCache.Archetype != archetype)
    5.     {
    6.         lookupCache.Archetype = archetype;
    7.         GetIndexInTypeArray(archetype, typeIndex, ref lookupCache.IndexInArcheType);
    8.         lookupCache.ComponentOffset = archetype->Offsets[lookupCache.IndexInArcheType];
    9.         lookupCache.ComponentSizeOf = archetype->SizeOfs[lookupCache.IndexInArcheType];
    10.     }
    11.  
    12.     // Write Component to Chunk. ChangeVersion:Yes OrderVersion:No
    13.     chunk->SetChangeVersion(lookupCache.IndexInArcheType, globalSystemVersion);
    14.     return chunk->Buffer + (lookupCache.ComponentOffset + lookupCache.ComponentSizeOf * index);
    15. }
     
    BobFlame likes this.
  9. BobFlame

    BobFlame

    Joined:
    Nov 12, 2018
    Posts:
    94
    Thank you. And for commandbuffer.setcomponent, does it also set the flag?
    I did use the change filter, but the modified value is not selected. maybe it's ecb problem?
     
    Last edited: Mar 21, 2021
  10. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,653
    Keep in mind, that ECB bumps version not on queueing step but on Playback which happens manually or when it's own EntityCommandBufferSystem updates (because this is the exact time of actual change).
    upload_2021-3-22_12-41-48.png
     
    deus0 and BobFlame like this.
  11. davenirline

    davenirline

    Joined:
    Jul 7, 2010
    Posts:
    943
    What's the equivalent of WithChangeFilter() when using a job struct (IJobChunk or IJobEntityBatch)? I gotta try this to one of my systems.
     
  12. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,626
    From memory

    chunk.GetChangeVersion() and compare it to the systems LastSystemVersion
     
  13. davenirline

    davenirline

    Joined:
    Jul 7, 2010
    Posts:
    943
    Does it automatically mean that if they are not equal, the chunk is considered as changed?
     
  14. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,626
    You pass it a component type handler. If they're not equal it means the component for that chunk has had a write query access it at the very least since the system last ran.
     
    davenirline likes this.
  15. davenirline

    davenirline

    Joined:
    Jul 7, 2010
    Posts:
    943
    I found DidChange(). I hope this is correct:
    Code (CSharp):
    1. public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) {
    2.     if (!chunk.DidChange(this.componentType, this.lastSystemVersion)) {
    3.         // This means that the components in the chunk have not been queried with write access
    4.         // There must be no changes at the least
    5.         return;
    6.     }
    7.  
    8.     ...
    9. }
     
  16. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,626
    Oh yeah, that's the one. Does the check for you.
     
  17. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,653
    Depends on your use case - don't forget, that DidChange (same ChangeFilter) wouldn't handle removing entity from the chunk after Destroy\Removing component from an entity (as result moved to different chunk), for the handle chunks that have structural changes with removing entities from chunk - use DidOrderChange.
     
  18. BobFlame

    BobFlame

    Joined:
    Nov 12, 2018
    Posts:
    94
    It seems that change filter does not apply to newly instantiated entities, only record changes to already exist entities.
     
  19. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,653
    It applies, newly created entities trigger chunk change filter. Only destroyed (as I already mentioned) not.
    Simple proof:
    Code (CSharp):
    1. public struct SomeComponent : IComponentData
    2. {
    3.     public int Value;
    4. }
    5.  
    6. public class SystemA : SystemBase
    7. {
    8.     protected override void OnUpdate()
    9.     {
    10.         if (Input.GetKeyDown(KeyCode.Space))
    11.         {
    12.             EntityManager.CreateEntity(typeof(SomeComponent));
    13.         }
    14.     }
    15. }
    16.  
    17. [UpdateAfter(typeof(SystemA))]
    18. public class SystemB : SystemBase
    19. {
    20.     protected override void OnUpdate()
    21.     {
    22.         var frame = UnityEngine.Time.frameCount;
    23.  
    24.         Entities
    25.            .WithChangeFilter<SomeComponent>()
    26.            .ForEach((Entity e, in SomeComponent component) =>
    27.         {
    28.             Debug.Log($"Has changed in frame:{frame}");
    29.         }).Schedule();
    30.     }
    31. }
    And as you can see I pressed space 4 times.
    upload_2021-4-15_15-49-59.png
    And by count at the right side, you can see there is 1 2 3 and finally 4 entities inside triggered chunk as they all in the same chunk. Because new entities fit in the existing chunk thus added to the same chunk and trigger change filtter.
     
    Samsle, BobFlame and Lukas_Kastern like this.
  20. BobFlame

    BobFlame

    Joined:
    Nov 12, 2018
    Posts:
    94
    Your right, I maybe encounter a situation like this causing to ignored the newly created entities change flag.
    Three system update order in A, B, C.
    Some Entity is spawned in system A, system B need to check entity's change flag, and in the mean time, it query it along with component filled in System C.
    So system B is just passing by the frame entity is spawned, the lastSystemVersion go exceed the component's change Version. And the changes could never be detected.
     
  21. BobFlame

    BobFlame

    Joined:
    Nov 12, 2018
    Posts:
    94
    So, there is an interesting thing. Using ComponentDataFromEntity or EntityCommandBuffer could perform more accurate operation than Entities.ForEach(ComponentTypeHandle) on change flags. But I remember someone has said direct component access has better performance than indirect component accesss. Do you guys think this point is trade-off need to be considerated seriously in architecture design, or just choose one no matter situations.
     
  22. BobFlame

    BobFlame

    Joined:
    Nov 12, 2018
    Posts:
    94
    What I found more accurate flag operation is very worthy is netwoking, where entities state synchronization is rely heavily on change filters
     
  23. BobFlame

    BobFlame

    Joined:
    Nov 12, 2018
    Posts:
    94
    There seems to be a bug when DynamicBuffer is modified by EntityCommandBuffer's SetBuffer method, WithChangeFilter can't detect this.
     
  24. twaananen

    twaananen

    Joined:
    Jul 24, 2017
    Posts:
    23
    Don't know if this is a bug or intended, but using Entities.WithChangeFilter() doesn't seem to work correctly with netcode ghosts.
    I tried syncing level info from the server to clients with a simple component instantiated on the server, with the ghost authoring set to interpolated and static. I would then determine loading and unloading of scenes with the change or existence of this component. Works on the server, where .WithChangeFilter() only loads the scene once.

    The client on the other hand detects change every update, so it unloads and loads the scene every update.
     
    deus0 likes this.
  25. deus0

    deus0

    Joined:
    May 12, 2015
    Posts:
    256
    Is it normal for WithChangeFilter to change the query to ReadWrite (was testing with Translation), when SetChangedVersionFilter works more correctly and only flags the component in the query as ReadOnly as intended? Seems like a bug imo.
     
  26. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    3,983
    There's a lot of bugs with change filtering and codegen. One of those being that they generate sync points on the main thread on the components you set change filters for. The fastest (and unfortunately verbose) way to do change filtering is to check DidChange in an IJobEntityBatch.
     
  27. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,626
    Not sure if I consider it a bug because any use of SetFilter on a query causes a sync point and that's all it's doing.

    That said, I do think codegen should be changed to use DidChange instead of using query.SetFilter to avoid the sync point.
     
  28. julian-moschuering

    julian-moschuering

    Joined:
    Apr 15, 2014
    Posts:
    529
    Yes, codegen does that for some reason reason. I reported that but didn't get a response yet. I quickfixed this in codegen to be always ReadOnly and this fixed some dependency performance issues I had.