#ECB GetSingleton Pattern

1 messages · Page 1 of 1 (latest)

granite slate
#

Would someone be able to explain how the CannonBallSystem code in the ECS tutorial at https://github.com/Unity-Technologies/EntityComponentSystemSamples/blob/master/DOTS_Guide/ecs_tutorial/README.md manages to not run into the issues outlined in the description of EntityCommandBufferSystem? EntityCommandBufferSystem states that when you write to a command buffer from a job, you must add the JobHandle of that job to the system using AddJobHandleForProducer. Which makes sense for system/job ordering. But somehow, the DOTS guide skips that step, and uses the singleton instead. I can't find any explanation of how that operates?

GitHub

Contribute to Unity-Technologies/EntityComponentSystemSamples development by creating an account on GitHub.

cyan sonnet
#

ECB system's currently have 2 ways!
The old:

  1. Get Access to an ECBSystem
  2. Use the CreateCommandBuffer method on said system
  3. Remember to track jobhandle using AddJobHandleForProducer

The new:

  1. Get access to ECBSystem.Singleton
  2. Use the CreateCommandBuffer method on said singleton
  3. None, dependency is automatically tracked :3
granite slate
#

#3 in the new way is great! Can you explain in more depth how? I don't see anything in the codegen to explain how that's possible, and the singleton documentation section seems to indicate that singleton's don't have dependency tracking.

#

@cyan sonnet to elaborate on that, since you pass in just the ECB handle that you get into the job via a field, is there special tracking in the job system for that or something?

#

I'd like to use this pattern in my own singleton, is the thing I'm trying to do.

cyan sonnet
#

Haha, well the guy who did it is on holiday 😆.. So magic, but in the meantime, I'll find a sorcerer to learn you the ways

granite slate
#

Hah! That's the answer I was afraid of!

quick scaffold
#

There's no real magic needed here and it's easy to setup for your own singletons. Let me load up a pc and get you some code.

granite slate
#

Well, there was someone in the DOTS channel yesterday asking about an error with https://pastebin.com/ei0AUnXz which is pretty much what I'm trying to do.

#

Which is to say, if you use GetSingleton there's a concurrency error.

#

Which looks awfully similar to the EntityCommandBufferSystem.Singleton code.

quick scaffold
#

Dependency management is tracked on a system level and calling GetSingleton will had a dependency on the component to your system.

In EntityCommandBufferSystem RegisterSingleton extension it adds a query using RW to the system

ref var s = ref query.GetSingletonRW<T>().ValueRW;```

If you're reading system of the singleton, in this case, EntityCommandBufferSystem needs it on main thread you just need to call CompleteDependency at the start of your Update()
fickle estuary
#

singleton documentation section seems to indicate that singleton's don't have dependency tracking
Regarding this, the difference is getting singletons don't complete dependencies

granite slate
#

Maybe the issue is just the documentation, then?

fickle estuary
#

Tracking still occurs

cyan sonnet
#

GetSingleton will AddDependency, just no completion :3

granite slate
#

Yes, I understand that. But I'm interested in the job.

#

You aren't using GetSingleton in the job, right? You just pass in a product from the result.

quick scaffold
#

EntityCommandBufferSystem never actually reads the singleton itself though. the singleton stores a UnsafeList<EntityCommandBuffer> which is held directly in the system and there are no off thread jobs in ECBS

granite slate
#

To clarify - My question is how it is that the ECBS at the end of the simulation group waits for the jobs that are writing to that list.

quick scaffold
#

It calls this.Dependency.Complete()

#

nothing more

granite slate
#

Because I don't really follow how it even knows they exist.

quick scaffold
granite slate
#

Are you saying that once you use GetSingleton in the system that uses the ECBS, all jobs that spin off from that system are... tracked somewhere?

quick scaffold
#

Any call to GetX whether it's GetLookup, GetTypehHandle, GetSingleton all add a dependency to the system.

granite slate
#

I fully understand how the ECBS itself includes itself in the dependency chain.

#

It uses the query, like you mentioned. Makes total sense to me.

#

I'm thinking about the other systems that actually use it, and specifically their jobs that they spin off. I don't understand how the jobs that the producer systems spin off are tracked.

#

It's just like the pastebin I mentioned: https://pastebin.com/ei0AUnXz - I understand how SystemB is tracked in the dependency chain for the singleton. That's not the issue, though. It's SystemC, which is the user of that singleton and isn't really looking it up.

quick scaffold
#

All SystemAPI.GetSingleton does is the exact same thing that was done manually above


__query_802763820_0 = this.CheckedStateRef.GetEntityQuery(new Unity.Entities.EntityQueryDesc{All = new Unity.Entities.ComponentType[]{Unity.Entities.ComponentType.ReadOnly<BovineLabs.Draw.DrawSystem.Singleton>()}, Any = new Unity.Entities.ComponentType[]{}, None = new Unity.Entities.ComponentType[]{}, Options = Unity.Entities.EntityQueryOptions.Default | Unity.Entities.EntityQueryOptions.IncludeSystems});```
#

It creates a query which adds dependency to the system and calls GetSingleton from it

granite slate
#

But that query is not part of the job.

quick scaffold
#

Automatic dependency management in ECS is system based

#

Not job based

#

Common misconception

fickle estuary
#

So the built-in ECBS's use UpdateInGroup to position them either at the beginning or end of the the system group, that way the ECBSystem at the end, for instance, has dependency is on all the other systems in that system group. From there automatic dependency tracking within the systems takes care of the per-job bits

granite slate
#

But if I look at the codegen, when I spin off the job, I see
state.Dependency = __ScheduleViaJobChunkExtension_0(job, __query_259922272_0, state.Dependency, ref state);

#

There's also ``` __query_259922272_1 = state.GetEntityQuery(new Unity.Entities.EntityQueryDesc{All = new Unity.Entities.ComponentType[]{Unity.Entities.ComponentType.ReadOnly<Unity.Entities.EndSimulationEntityCommandBufferSystem.Singleton>()}, Any = new Unity.Entities.ComponentType[]{}, None = new Unity.Entities.ComponentType[]{}, Options = Unity.Entities.EntityQueryOptions.Default | Unity.Entities.EntityQueryOptions.IncludeSystems});

#

Which is the singleton query, which is just... not included in that chain.

quick scaffold
#

after a system runs, it calls AfterOnUpdate

                    m_JobDependencyForReadingSystems.Length, m_JobDependencyForWritingSystems.Ptr,
                    m_JobDependencyForWritingSystems.Length, m_JobHandle);```
#

and writes the final dependency handle you've been updating which is now the entire systems dependency handle which depends on the singleton because of the getsingleton call tot he dependencymanager

#

so any other system doesn't see the individual job handles, they only see this system had this dependency on component X and this is the final jobhandle for the entire system

granite slate
#

Oookay, so the DependencyManager is tracking the call to GetSingleton. That would be the magic that was mentioned.

quick scaffold
#

It's just using the built in automatic component tracking between systems.
The slight annoyance is that you need to complete the handle yourself

granite slate
#

I think that clears up my confusion, then. Because the line in the codegen when you get the singleton doesn't really do anything with the dependency: ``` var ecbSingleton = __query_259922272_1.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>();

#

But that doesn't matter.

#

Because it's being tracked anyways via a list or something that's resultant in the call.

fickle estuary
#

The slight annoyance is that you need to complete the handle yourself
(I really need to remember shift+enter instead of ctrl+enter)

Yes I have an idea around that and hope to look at it soon

quick scaffold
#

On a side related note @cyan sonnet SystemAPI.GetSingleton in OnStartRunning is busted because BeforeOnUpdate that updates the systems dependency isn't called till after OnStartRunning so calling Dependency.Complete doesn't work

OnStartRunning
BeforeOnUpdate -> NeedToGetDependencyFromSafetyManager = true
OnUpdate

You have to bypass SystemAPI create a custom query and manually complete the dependency
query.CompleteDependency();
Though I suspect this is not your area

cyan sonnet
#

It's related enough to push it onto @fickle estuary 's area instead 😆

fickle estuary
#

Agreed. 🤝

#

Well. We should actually discuss that @cyan sonnet lol but certainly it's a bug that should be addressed

cyan sonnet
fickle estuary
#

Yeah - it certainly seems reasonable. I have other work in flight that this would fit nicely into, so I'll include this investigation. I agree it's unlikely SystemAPI jurisdiction

granite slate
#

@quick scaffold @fickle estuary I hate to be a dunce, but just wanted to double-check my understanding here around this subject. I rewrote the pastebin I linked above to make it a bit more understandable for myself, and I'm still getting the error the original creator mentioned, but I just wanted to make sure I understand exactly why.

Basically, the code has two systems, one that launches a job over a regular component and uses GetSingleton to put the value in the context, and one that launches a job for the singleton itself. From my understanding, the error is occurring because the normal job's GetSingleton call does not wait for the Singleton system's job. Is that correct? And thus the solution that ECB uses is to not launch a job for the Singleton and do it in the OnUpdate directly. Which obviously makes sense for an ECB system.

It gets a little more confusing, I think, because it seems like the error occurs regardless of system ordering (I'm pretty sure it's just happening on the next frame).

InvalidOperationException: The previously scheduled job SingletonSystem:SingletonJob writes to the ComponentTypeHandle<SingletonComponent> SingletonJob.JobData.__SingletonComponentComponentTypeHandle. You must call JobHandle.Complete() on the job SingletonSystem:SingletonJob, before you can read from the ComponentTypeHandle<SingletonComponent> safely.

https://pastebin.com/4qCxwZTq

quick scaffold
#

Let me load it up quickly for you and test

granite slate
#

You'll want to uncomment the line to schedule the singleton job in SingletonSystem to see the error.

quick scaffold
#

Oh? SystemAPI.GetSingletonRW<SingletonComponent>().ValueRW.value++; isn't causing an error?

granite slate
#

No, I use state.CompleteDependency before it.

quick scaffold
#

i'm pretty sure this is actually a Handle.Update issue

#

Handle.Update
SystemAPI.GetSingletonRW invalidates handle
Job runs

granite slate
#

You can remove the GetSingletonRW line and the error still occurs.

quick scaffold
#

oh in that case let me actually test

granite slate
#

I just added that at the last minute to make sure it worked that way.

quick scaffold
#

just loading up a project i can run this in, current project not in a valid state

granite slate
#

Hah! Yeah, it took me a while. I did notice the line you mentioned about the EntityQueryBuilder call in the ECBSystem's RegisterSingleton is new in the pre.15 version. But I don't think it does anything. You can add/remove it without any impacts.

#

I do find it odd, because in the experimental version I was on, it was just using GetSingleton instead of that rather elaborate query. So I'm not sure why it was changed. But I don't think it matters.

quick scaffold
#

oh yeah ok i can see the error

#

if you remove
// var query = new EntityQueryBuilder(state.WorldUpdateAllocator).WithAllRW<SingletonComponent>().Build(ref state);

#
public partial struct SingletonSystem : ISystem
{
    public void OnCreate(ref SystemState state)
    {
        var e = state.WorldUnmanaged.EntityManager.CreateEntity(ComponentType.ReadWrite<SingletonComponent>());
    }

    public void OnDestroy(ref SystemState state)
    {
    }

    public void OnUpdate(ref SystemState state)
    {
        state.Dependency = new SingletonJob().Schedule(state.Dependency);
    }

    public partial struct SingletonJob : IJobEntity
    {
        public void Execute(ref SingletonComponent component)
        {
            component.value++;
        }
    }
}```
#

this runs fine

#

(just building a baseline)

granite slate
#

I just ran that exact code and get the error.

quick scaffold
#

this is just a normal system

#

hmm

#

it's weird that SingletonSystem updates before normal system

granite slate
#

Before or after, makes no difference.

#

I think it's cycling over to the next frame.

quick scaffold
#

ok wait no i can repo it again with what you said

granite slate
#

And the error occurs there.

quick scaffold
#

yeah ok it does seem like it doesn't like dangling singleton jobs

granite slate
#

Right.

#

And I think that's just because the GetSingleton call doesn't complete the dependencies. And because that singleton value is just added as a field to the normal job, there's no dependency inside the normal job either.

#

So this is expected.

#

One solution would be to fall back to the "old" ECB behavior @cyan sonnet mentioned at the very start of the thread.

quick scaffold
#

0.51

        {
            var typeIndex = TypeManager.GetTypeIndex<T>();
#if ENABLE_UNITY_COLLECTIONS_CHECKS || UNITY_DOTS_DEBUG
            if (TypeManager.IsZeroSized(typeIndex))
                throw new InvalidOperationException($"Can't call GetSingleton<{typeof(T)}>() with zero-size type {typeof(T)}.");
#endif
            _Access->DependencyManager->CompleteWriteDependencyNoChecks(typeIndex);```

1.0

```        public T GetSingleton<T>() where T : unmanaged, IComponentData
        {
            var typeIndex = TypeManager.GetTypeIndex<T>();
#if ENABLE_UNITY_COLLECTIONS_CHECKS || UNITY_DOTS_DEBUG
            if (TypeManager.IsZeroSized(typeIndex))
                throw new InvalidOperationException($"Can't call GetSingleton<{typeof(T)}>() with zero-size type {typeof(T)}.");
            if (TypeManager.IsEnableable(typeIndex))
                throw new InvalidOperationException(
                    $"Can't call GetSingleton<{typeof(T)}>() with enableable component type {typeof(T)}.");
#endif
#if ENABLE_UNITY_COLLECTIONS_CHECKS
            _Access->DependencyManager->Safety.CompleteWriteDependency(typeIndex);
#endif```
#

it's weird to me
_Access->DependencyManager->Safety.CompleteWriteDependency(typeIndex);
is now wrapped in ENABLE_UNITY_COLLECTIONS_CHECKS

#

though it's clearly not completing what it needs anyway

granite slate
#

I'd guess they didn't want people to "accidentally" create sync points when using singletons? But really, I don't know for sure. I'm just trying to make sure this is the expected behavior.

quick scaffold
#

oh yeah

        {
            var arrayIndex = m_TypeArrayIndices[typeIndex.Index];
            if (arrayIndex == NullTypeIndex)
                return;

            AtomicSafetyHandle.CheckReadAndThrow(m_ComponentSafetyHandles[arrayIndex].SafetyHandle);
            AtomicSafetyHandle.CheckReadAndThrow(m_ComponentSafetyHandles[arrayIndex].BufferHandle);
        }```
#

doesn't complete shit anymore - that's a poorly named method, should be CheckWriteDependency or something

#

it just throws your error instead

#

ok so getsingleton never completes dependency which is why this behaviour is different in 1.0 vs 0.51

#

so from what i can tell here
a) you either always use a specific singleton on same thread (read/write)
b) you always use it in a job (read/write)
if you use 1 on main thread and 1 in job it's going to break for you

granite slate
#

Right. And I think that's why the title of this thread is kinda bad. Because the ECB doesn't use a job.

quick scaffold
#

you can use a job though, i do it in my draw system but this explains an issue i was having

#

if you want to support 1 main thread + job then your writer can only be writing to a pointer on the component, not the component itself

#

let me update your example to show you what i mean

granite slate
#

Technically you can use 1 in job and 1 in main thread. It's just that the writing needs to be done on the main thread.

#

Or rather, if you're going to rely on the built-in dependency management system and the built-in GetSingleton functions, writing needs to be done on the main thread.

#

Just as a thought, maybe you could create two "bookending" systems? Because at the start of the frame/system group, you could just have a system that specifically synchronously waits for the singleton job? I can try that.

quick scaffold
#

is effectively how my draw system works

#

except instead of an int* it stores an UnsafeList<T>* like commandbuffers do

granite slate
#

Nope, tried the bookending and it didn't work.

#

Let me try that code...

quick scaffold
#

to quote dani from another of these threads

The general idea with Singletons in their current state is that they essentially are Query.SingleWithoutSync - Essentially, the point of em is that they don't sync. The idea is that you have one system that owns and maintains the state of said singleton, and all other systems just use it. E.g. if you had a component struct MyComponent : IComponentData { public NativeArray<int> ints } you can set up ints in one system, own the creation and disposal etc. then all other systems can just ask for the MyComponent, and read from the collection.

As a result, by very design anything else, like writing from another system is a little harder to do by default, as to not encourage singletons to be anything else than write rare, read often. ```
#

it actually makes sense for performance to remove complete in here

fringe sinew
#

Was just about to link I asked about this as well 😅

quick scaffold
#

getsingleton has been pushed as a default workflow and having to do a dependency check/complete every time would actually be costly

granite slate
#

Oh, @fringe sinew, should've mentioned you. Thanks for the sample code!

quick scaffold
#

it just basically makes the writing system have to run mainthread or create per system instances (ECBS/my draw system)

#

anyway this has cleared up a few things i was a bit foggy on myself as well

#

because i had been using these singletons properly, i just wasn't clear why i had to do it a certain way

#

i didn't realize query.GetSingleton had been changed from 0.51

granite slate
#

I think the big problem here is that the system that owns and maintains the state of the singleton... can't really use jobs.

quick scaffold
#

it can if you are setting up per system data (ecbs/drawsystem) or if you only allow reading in jobs and just use getsingletonentity and pass that to the job instead

#

but yes i'm not sure a good approach here that maintains the fast getsingleton path for ecbs etc

granite slate
#

Sure - that's what I mean.

quick scaffold
#

you can just call
SystemAPI.GetComponent<Singleton>(SystemAPI.GetSingletonEntity<Singleton>())

#

i think would replicate 0.51 behaviour

granite slate
#

"Or rather, if you're going to rely on the built-in dependency management system and the built-in GetSingleton functions, writing needs to be done on the main thread."

quick scaffold
#

but yeah bit gross

granite slate
#

Yeah. A bit.

quick scaffold
#

maybe an optional param to complete
public static T GetSingleton<T>(Entity entity, bool completeDependency = false) where T : unmanaged, IComponentData => throw InternalCompilerInterface.ThrowCodeGenException();

#

or an optional complete dependency method

#

alternatively a SystemAPI.GetSingletonComplete<T>(Entity entity)

granite slate
#

I suppose I could just write my own GetSingleton wrapper. Not sure if that would mess up the codegen.

#

Anyways - for me in particular, I can totally just do it without a singleton job. I would have preferred to do it in a singleton job, but the amount of maintenance I need to do on the singleton is... significant, but not enormous.

#

Maybe in the future we'll see a new GetSingletonSynced wrapper or some other alternative.

#

Thanks for all your help, everyone!

fickle estuary
#

oh yeah
public void CompleteWriteDependency(TypeIndex typeIndex)
{
var arrayIndex = m_TypeArrayIndices[typeIndex.Index];
if (arrayIndex == NullTypeIndex)
return;

        AtomicSafetyHandle.CheckReadAndThrow(m_ComponentSafetyHandles[arrayIndex].SafetyHandle);
        AtomicSafetyHandle.CheckReadAndThrow(m_ComponentSafetyHandles[arrayIndex].BufferHandle);
    }

doesn't complete shit anymore - that's a poorly named method, should be CheckWriteDependency or something
That is the code from ComponentSafetyHandles.CompleteWriteDependency which perhaps should just be internal, as it is part of the CompleteWriteDependency process, but only handles the ASH cleanup parts (it does more than check/throw under the hood). Likely you're looking for ComponentDependencyManager.CompleteWriteDependency, which completes dependencies as well as calls that particular method to validate and reset safety handles

And then EntityManager.CompleteDependencyBeforeRO<MyType>() wraps ComponentDependencyManager.CompleteWriteDependency for ease of use