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?
#ECB GetSingleton Pattern
1 messages · Page 1 of 1 (latest)
ECB system's currently have 2 ways!
The old:
- Get Access to an ECBSystem
- Use the CreateCommandBuffer method on said system
- Remember to track jobhandle using AddJobHandleForProducer
The new:
- Get access to ECBSystem.Singleton
- Use the CreateCommandBuffer method on said singleton
- None, dependency is automatically tracked :3
#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.
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
Hah! That's the answer I was afraid of!
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.
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.
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()
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
Maybe the issue is just the documentation, then?
Tracking still occurs
GetSingleton will AddDependency, just no completion :3
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.
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
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.
Because I don't really follow how it even knows they exist.
Because in it's setup it added a dependency here to the system in OnCreate ^
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?
Any call to GetX whether it's GetLookup, GetTypehHandle, GetSingleton all add a dependency to the system.
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.
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
But that query is not part of the job.
Automatic dependency management in ECS is system based
Not job based
Common misconception
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
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.
yes so inside a system, you need to manually schedule dependencies between jobs because there is no automatic dependency management
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
Oookay, so the DependencyManager is tracking the call to GetSingleton. That would be the magic that was mentioned.
It's just using the built in automatic component tracking between systems.
The slight annoyance is that you need to complete the handle yourself
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.
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
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
It's related enough to push it onto @fickle estuary 's area instead 😆
Agreed. 🤝
Well. We should actually discuss that @cyan sonnet lol but certainly it's a bug that should be addressed
Agreed, btw related to:
#archived-dots message
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
@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.
Let me load it up quickly for you and test
You'll want to uncomment the line to schedule the singleton job in SingletonSystem to see the error.
Oh? SystemAPI.GetSingletonRW<SingletonComponent>().ValueRW.value++; isn't causing an error?
No, I use state.CompleteDependency before it.
i'm pretty sure this is actually a Handle.Update issue
Handle.Update
SystemAPI.GetSingletonRW invalidates handle
Job runs
You can remove the GetSingletonRW line and the error still occurs.
oh in that case let me actually test
I just added that at the last minute to make sure it worked that way.
just loading up a project i can run this in, current project not in a valid state
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.
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)
I just ran that exact code and get the error.
this is just a normal system
hmm
it's weird that SingletonSystem updates before normal system
Before or after, makes no difference.
I think it's cycling over to the next frame.
ok wait no i can repo it again with what you said
And the error occurs there.
yeah ok it does seem like it doesn't like dangling singleton jobs
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.
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
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.
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
Right. And I think that's why the title of this thread is kinda bad. Because the ECB doesn't use a job.
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
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.
you still need to call complete but that 1 writing system it's not such a big deal
is effectively how my draw system works
except instead of an int* it stores an UnsafeList<T>* like commandbuffers do
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
Was just about to link I asked about this as well 😅
getsingleton has been pushed as a default workflow and having to do a dependency check/complete every time would actually be costly
Oh, @fringe sinew, should've mentioned you. Thanks for the sample code!
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
I think the big problem here is that the system that owns and maintains the state of the singleton... can't really use jobs.
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
Sure - that's what I mean.
you can just call
SystemAPI.GetComponent<Singleton>(SystemAPI.GetSingletonEntity<Singleton>())
i think would replicate 0.51 behaviour
"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."
but yeah bit gross
Yeah. A bit.
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)
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!
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