Search Unity

Feature Request ChangeFilters on Client (proof of concept)

Discussion in 'NetCode for ECS' started by tertle, Jul 21, 2022.

  1. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,761
    I really like what Unity is trying to do with NetCode but I have an issue with the fact it triggers ChangeFilter on Clients continuously. ChangeFilters are my favourite feature of Entities and I've been doing a lot of work recently on extreme scale performance (100,000,000 entities 2ms per frame) and static archetypes and ChangeFitlers are paramount to the ability to scale to extremes.

    Is this a topic Unity is planning on addressing at some point?

    I've done a lot of experimentation for future projects recently and decided to dig into seeing if I could make NetCode respect change filters to allow me to build an architecture I want that will scale for my needs.

    Anyway I have a proof of concept working up here: https://github.com/tertle/com.unity.netcode

    Before you consider using it, it's currently only setup to work on IComponent changes via CopySnapshot. I have not done IBufferElement, Child elements or PredictionBackup. It's purely a proof of concept.

    My test is just incrementing a value every ~5 seconds on server and listening to this change via ChangeFilter on Client.

    Before:
    upload_2022-7-21_18-34-34.png
    After:
    upload_2022-7-21_18-34-39.png
    Note the far right values - the number of times this method was called on client for each value.
    The times are backwards because I forgot to record before and reverted code changes and recorded it after

    It's a pretty simple change, instead of just opening the chunk component with RW it opens with RO and does a MemCmp afterwards to see if it's changed and then manually bumps the Change Version. Now this is obviously a little more overhead but I've found MemCmp to be really fast. Personally I think the benefit of potential user code improvements will greatly outweigh this cost.

    The code:
    Original
    Code (CSharp):
    1. var compData = (byte*)chunk.GetDynamicComponentDataArrayReinterpret<byte>(ghostChunkComponentTypesPtr[compIdx], compSize).GetUnsafeReadOnlyPtr();
    2. deserializerState.SendToOwner = GhostComponentCollection[serializerIdx].SendToOwner;
    3. for (var rangeIdx = 0; rangeIdx < entityRange.Length; ++rangeIdx)
    4. {
    5.     var range = entityRange[rangeIdx];
    6.     var snapshotData = (byte*)dataAtTick.GetUnsafeReadOnlyPtr();
    7.     snapshotData += snapshotDataAtTickSize * range.x;
    8.     GhostComponentCollection[serializerIdx].CopyFromSnapshot.Ptr.Invoke((System.IntPtr)UnsafeUtility.AddressOf(ref deserializerState), (System.IntPtr)snapshotData, snapshotOffset, snapshotDataAtTickSize, (System.IntPtr)(compData + range.x*compSize), compSize, range.y-range.x);
    9. }
    10. snapshotOffset += snapshotSize;
    Proof of Concept
    Code (CSharp):
    1.  
    2. var data = chunk.GetDynamicComponentDataArrayReinterpretRO<byte>(ghostChunkComponentTypesPtr[compIdx], compSize); // Changed to RO
    3. var compData = (byte*)data.GetUnsafePtr();
    4. m_chunkBuffer.CopyFrom(data);
    5. deserializerState.SendToOwner = GhostComponentCollection[serializerIdx].SendToOwner;
    6. for (var rangeIdx = 0; rangeIdx < entityRange.Length; ++rangeIdx)
    7. {
    8.     var range = entityRange[rangeIdx];
    9.     var snapshotData = (byte*)dataAtTick.GetUnsafeReadOnlyPtr();
    10.     snapshotData += snapshotDataAtTickSize * range.x;
    11.     GhostComponentCollection[serializerIdx].CopyFromSnapshot.Ptr.Invoke((System.IntPtr)UnsafeUtility.AddressOf(ref deserializerState), (System.IntPtr)snapshotData, snapshotOffset, snapshotDataAtTickSize, (System.IntPtr)(compData + range.x*compSize), compSize, range.y-range.x);
    12. }
    13. if (UnsafeUtility.MemCmp(compData, m_chunkBuffer.GetUnsafeReadOnlyPtr(), data.Length) != 0)
    14. {
    15.     chunk.SetChangeFilter(ghostChunkComponentTypesPtr[compIdx]); // Manually trigger change filter only if there's a change
    16. }
    17. snapshotOffset += snapshotSize;
    The Simple Test
    Code (CSharp):
    1.     [UpdateInWorld(TargetWorld.Client)]
    2.     public partial class TestClientSystem : SystemBase
    3.     {
    4.         private EntityQuery query;
    5.  
    6.         protected override void OnCreate()
    7.         {
    8.             this.query = this.GetEntityQuery(ComponentType.ReadOnly<TestComponent>());
    9.             this.query.SetChangedVersionFilter(typeof(TestComponent));
    10.         }
    11.  
    12.         protected override void OnUpdate()
    13.         {
    14.             if (!this.query.IsEmpty)
    15.                 Debug.Log($"New value on client {this.query.GetSingleton<TestComponent>().Test}");
    16.         }
    17.     }
    Alternative Way
    While I was looking at a way to do this I thought of an alternative approach if I was willing to touch the codegen that might work better with how NetCode has been built as a lot of the components in the chunks wouldn't be touched in the snapshot.
    This alternate way would be to add a change check into the code generated by PortableFunctionPointer<RestoreFromBackupDelegate> RestoreFromBackup and make it return a bool. It would look something like this (completely theoretical and untested.)

    Code (CSharp):
    1.  
    2. var compData = (byte*)chunk.GetDynamicComponentDataArrayReinterpretRO<byte>(ghostChunkComponentTypesPtr[compIdx], compSize).GetUnsafeReadOnlyPtr(); // Changed to RO
    3. deserializerState.SendToOwner = GhostComponentCollection[serializerIdx].SendToOwner;
    4. bool changed = false;
    5. for (var rangeIdx = 0; rangeIdx < entityRange.Length; ++rangeIdx)
    6. {
    7.     var range = entityRange[rangeIdx];
    8.     var snapshotData = (byte*)dataAtTick.GetUnsafeReadOnlyPtr();
    9.     snapshotData += snapshotDataAtTickSize * range.x;
    10.     changed |= GhostComponentCollection[serializerIdx].CopyFromSnapshot.Ptr.Invoke((System.IntPtr)UnsafeUtility.AddressOf(ref deserializerState), (System.IntPtr)snapshotData, snapshotOffset, snapshotDataAtTickSize, (System.IntPtr)(compData + range.x*compSize), compSize, range.y-range.x);
    11. }
    12. if (changed)
    13. {
    14.     chunk.SetChangeFilter(ghostChunkComponentTypesPtr[compIdx]); // Manually trigger change filter only if there's a change
    15. }
    16. snapshotOffset += snapshotSize;
    17.  
    18.  
    Anyway really just posting my thoughts and a proof of concept of something I would REALLY like to be supported. I do not expect Unity to implement this exact solution and I just wanted to prove to myself a proof of concept was possible to ease any concerns I might have before embarking in a project. There might well be a much better way to do this as I'm much less familiar with the NetCode package than I am with Entities. This was just a 30min brain storm using some concepts I developed recently from a high performance library.

    If this is not something Unity intends to support there is a good chance I fork and flesh this out properly at some point as it's something I consider mandatory for future work I want to do.

    Again before anyone uses it, it's purely a proof of concept without any thorough testing to see consequences
     
    Last edited: Jul 21, 2022
    Opeth001, skiplist, Shinyclef and 3 others like this.
  2. Enzi

    Enzi

    Joined:
    Jan 28, 2013
    Posts:
    967
    Big +1!

    It's been a long time I've done anything with Netcode. Will get back to it eventually.
    When working with it this was a huge problem for me. Change filters are great(!) and that functionality is thrown out the window with the current Netcode implementation. I struggled with some systems hard because I was expecting Netcode to respect change filtering and didn't want systems to run all the time when there are no changes. Often it's hard to code systems to work properly, when you don't expect them to run. Then you need lots of additional branching and other synced values to keep some form of state.

    Having a better understanding now, mainly through working the stuff out with tertle, I know it's doable and would make writing systems a lot more clean instead of relying on some synced fields or queries with structural changes.
     
  3. NikiWalker

    NikiWalker

    Unity Technologies

    Joined:
    May 18, 2021
    Posts:
    316
    Hey tertle (and Enzi)! Yes we'd like to fix this soon, it's in our backlog. I can't give ETAs but it's a high priority item for me as it affects both scalability and QoL. I've added this thread to the ticket, will try to remember to ping when completed. Cheers!
     
  4. optimise

    optimise

    Joined:
    Jan 22, 2014
    Posts:
    2,129
    How's the current progress now? Which exp.x version will get this feature?
     
  5. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,761
    I ended up running this in my project (just on components not buffers as it's a lot less overhead) since this post without ever running into an issue. It ended up being amazing for workflow and reacting to server updates.

    -edit-

    i should say i haven't tried to implement it in 1.0 yet, other things to focus on.
     
    Last edited: Oct 28, 2022
    NikiWalker likes this.
  6. NikiWalker

    NikiWalker

    Unity Technologies

    Joined:
    May 18, 2021
    Posts:
    316
    Unfortunately there is a breaking change in the API in 1.0 that makes this a non-trivial fix. It's still a high priority item, but we have a backlog of high priority fixes uncovered during our hardening push.
    I.e. Do NOT expect it to land for 1.0. Apologies!
     
    Opeth001, Kmsxkuse and optimise like this.
  7. optimise

    optimise

    Joined:
    Jan 22, 2014
    Posts:
    2,129
    :eek: Oh no. Then I still need to stick with ugly if check current and previous components to update UI for long time.
     
  8. CMarastoni

    CMarastoni

    Unity Technologies

    Joined:
    Mar 18, 2020
    Posts:
    900
    Well.. there is still an "if ugly check".. is just that you don't see it :)
    The cost of that DidChange is not null. Especially if is not used in the right way. When it comes to Entities.ForEach and check on a the chunk directly (inside a job for example) things are ok.
    But using the check for ComponentLookup for example can be really slow, especially if the archetype has many component (there is a linear search involved).

    Remember also that the change check are on a per-chunk basis. Don't get the illusion they are on a Entity by Entity basis even if API like ComponentLookup as something like:

    Code (csharp):
    1.  
    2. var changes = lookup.DidChange(entity, lastVersion);
    3.  
    That, internally translate in something like:
    Code (csharp):
    1.  
    2. var chunk = GetChunk(entity).
    3. var changed = chunk.DidChange(componentTypeHandle, lastVersion)
    4.  
    So, don't get me wrong. The change check if fundamental. But as soon as there is something that change a single entity in that chunk for that component, all entities are invalidate.

    Long story short: you probably still need to check if the value is changed.. :D. Just less frequently though.
     
  9. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,761
    if you really need it I've updated my proof of concept in the repo for 1.0 as I was curious.

    Before
    upload_2022-10-28_23-5-51.png

    After
    upload_2022-10-28_23-6-24.png

    Please note: I am not actively working with netcode at the moment so will not be thoroughly testing this.

    There is an odd behavior on ghost spawn as it triggers a few times so something is definitely up however, I don't have time to investigate why as I have too many projects that need finishing.

    I can not recommend using it unless you are willing to investigate this yourself. The risk is yours.

    -edit-

    yeah after leaving it a bit longer there are weird periods where it triggers when it shouldn't
    it'd still provide significantly fewer updates but you could not rely on it to be accurate.

    upload_2022-10-28_23-15-6.png

    (I have a pretty good idea why from my glimpse at the code but again, will not investigate further. Wait for an official implementation.)
     
    Last edited: Oct 28, 2022
  10. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    992
    That's why to this day I fail to understand how to use this feature efficiently.
    But now that in 1.0 the enable/disable feature forced a 128 entity cap on Chunk capacity. Could a similar approach be used to Provide per entity change filter ?
     
  11. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,761
    My entire project is built around change filters. It's how I can scale to 100 million entities for testing (I hit memory limits at this point) and still execute frame in 20ms. Change filters are extremely efficient when setup properly.

    IMO they are the most powerful feature in ECS if your goal is scale, you should investigate them more!
     
    Last edited: Nov 28, 2022
    NikiWalker and Anthiese like this.
  12. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    992
    I agree, I just can't wrap my head around it.
    From what I understand, as soon as you declare a read/write acces to a component in a job, the version is bumped even if you don't actually write to any of the component. So any other job that uses a change filter will be triggered even if nothing actually changed...

    And I fail to see how to write to a component without declaring it read/write or using EntityManager/ECB...

    Something obvious probably eludes me.
    I'll need to make a sample project only for that and experiment with all the read / write positbilites to understand what workls and what doesn't.
    I just haven't had the time to invest in this and keep putting it off because there is always something else to learn :p
     
  13. sngdan

    sngdan

    Joined:
    Feb 7, 2014
    Posts:
    1,154
    NikiWalker and Opeth001 like this.
  14. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    992
  15. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,761
    It's that time of the month again, where I decide to add netcode support to one of my libraries and I'm dying for some change filter support... so again I've added support back to the latest netcode (pre.15) and this time I also added support for buffers.
    https://github.com/tertle/com.unity.netcode

    Before:
    upload_2022-12-30_19-17-12.png

    After:
    upload_2022-12-30_19-14-34.png
     
    Last edited: Dec 30, 2022
  16. NikiWalker

    NikiWalker

    Unity Technologies

    Joined:
    May 18, 2021
    Posts:
    316
    I'm happy to report that "Client Change Filtering" has landed in our dev branch!
    • It supports components on root entities and children.
    • It handles enabled bits too.
    • Due to API limitations, EntitiesJournaling will report false positive writes.
      • Example: You may see journaling entries saying that the GhostUpdateSystem has written to your ghost component HpComponent, even if the value of HpComponent.Value never changes. This is unfortunately unavoidable.
    • Client Change Filtering primarily an optimization for Interpolated ghosts, as we have not added change filtering to the rollback of predicted ghost components (yet?). If using change filtering on predicted ghosts is an important use-case for y'all, please let us know.
    • If you're using a custom implementation, please be aware that: For child chunks, multiple threads may be reading and writing to the same child chunk at the same time. We noticed that our own first implementation was causing race conditions here, and fixed it.
    • I've converted one of our serialization tests into a "test all permutations of serialization in netcode", which now includes coverage of client change filtering.
    As always, I can't say when the pre-release containing this fix is scheduled for, but I can confirm that this fix won't be in the very next pre-release.

    This thread was instrumental in getting this feature prioritized, so thanks all.
     
  17. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,761
    Woo thanks Niki! So excited.

    If anyone is interested in why I was originally looking into this or wants inspiration for change filter uses, I started this thread because I wanted to efficiently watch changes to a bit fields coming from server for a clustering/fracturing system.



    (This video is from back in 0.51)
    The red planks are ghosts, the rest are client only simulated (the full boards are also ghosts before they are fractured)

    Now i could have totally worked around not having change filters on client, but it's a lot nicer that I can run the exact same system with the same checks on both client/server and don't need separate code paths.
     
    Last edited: Jan 21, 2023
  18. optimise

    optimise

    Joined:
    Jan 22, 2014
    Posts:
    2,129
    I think I need change filter support for owner prediced ghost to update UI.
     
  19. NikiWalker

    NikiWalker

    Unity Technologies

    Joined:
    May 18, 2021
    Posts:
    316
    It will work with predicted ghosts too, assuming your predicted systems all try to maintain and respect change filtering rules too (which is simply less likely than with interpolated ghosts).
     
    Kmsxkuse and optimise like this.
  20. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,761
    These are working awesome btw, thanks a bunch. I know there was a bit of discussion in here about some users wanting to look at using change filters and one of biggest issues you can have with them is after you've setup your work flow, months later accidentally triggering them every frame and not realizing.

    Had this issue happen to me after updating netcode and testing my filters again and one of them was busted, so I wrote a tool to track my change filters and notify me if I've broken them!

    Just chunk an attribute on your component you want to protect

    Code (CSharp):
    1. [ChangeFilterTracking]
    2. public struct FractureShardsDirty : IComponentData
    And it'll notify if you ever go above 85% chunk activation over 600 frames (I'll allow customization of these values some point in the future) as well as appear in my new change filter window.

    upload_2023-2-23_19-23-49.png

    As always available in my core library, it's in master but I recommend the 0.13 branch as I just significantly improved performance of the window https://gitlab.com/tertle/com.bovin...3/BovineLabs.Core.Editor/ChangeFilterTracking
     
    ScallyGames, NikiWalker and WAYNGames like this.
  21. NikiWalker

    NikiWalker

    Unity Technologies

    Joined:
    May 18, 2021
    Posts:
    316
    That's super useful, nice! We have a test for this in NetCode instead, but yeah it won't catch user error.

    One other thing to note is that additional work will need to be done for predicted ghosts to eliminate all of the false positives. The GhostPredictionHistorySystem's RestoreFromBackup will always mark as changed, for example, and that is called often (although not always).

    Our initial assumption was that your own prediction user-code would modify these component chunks every frame (because why predict a ghost if you're not even updating many of these components?). But it could be true that you're only often modifying a low % of these components in your prediction loop.

    If you have a project with:
    • A high quantity of predicted ghosts.
    • Where change filtering is used and respected.
    • Which has the hot/cold data situation described above.
    Sending it to us would be appreciated (via a bug report).
     
  22. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,761
    Most of these components (all the bottom stuff except navmesh) don't even exist on my client so probably should have shown server view, but I have no good way of separating them at the moment.

    My normal change rate for all of the above is 0% (as is my objective for performance), I had to specifically spam a sequence of operations to actually have any % show up ^_^'.

    As for predicted, I haven't tested that yet unfortunately as apart from a couple of temporary predicted spawns which switch to interpolated, I currently I only have 1 predicted ghost in this project which isn't relying on any change filters at this time. I guess I'll find out if I have any problems when I work on that area more in the future.
     
  23. NikiWalker

    NikiWalker

    Unity Technologies

    Joined:
    May 18, 2021
    Posts:
    316
    No worries. My above request is for any & all stakeholders in change filtering too.
    I.e. If anyone has a use-case for heavily change filtered predicted entities, a project repro would be superb. Cheers!
     
    PolarTron likes this.
  24. ScallyGames

    ScallyGames

    Joined:
    Sep 10, 2012
    Posts:
    49
    Any updates on when this might be available (or is it in 1.2 already)?
    Also is there any good way to track the release of features like this? The roadmap doesn't seem to be granular enough
     
  25. NikiWalker

    NikiWalker

    Unity Technologies

    Joined:
    May 18, 2021
    Posts:
    316
    https://docs.unity3d.com/Packages/c...angelog/CHANGELOG.html#100-pre44---2023-02-13
    GhostUpdateSystem now supports Change Filtering, so components on the client will now only be marked as changed when they actually are changed. We strongly recommend implementing change filtering when reading components containing [GhostField]s and [GhostEnabledBit]s on the client.


    Yeah apologies, we typically reply in the forums for individual features like this. I totally forgot to here. The changelog is the best source of truth. The Roadmap is for high level overviews.
     
    te_headfirst and ScallyGames like this.