StateTree: Stale Task Completion Status bit leaks into a newly entered state

There is a bug within the StateTree execution logic, where if a state has no tasks marked Considered for completion, the compiler reserves an extra state completion bit (StateTreeCompiler.cpp: MakeCompletionTasksMask()). States that are sibling in the tree share the same bit range, so that reserved bit can coincide with a task bit of a sibling. On entering the state, FStateTreeTasksCompletionStatus::Push() calls ResetStatus(State.TasksNum), which never clears the reserved bit. If any previous sibling completed via that bit, the new state reads from it as Succeeded immediately and its completion transitions (or any OnCompletion transitions up the chain) fire on the next tick, even though its tasks are still running and can’t complete.

The bug is present in both our 5.9 derrived version and vanilla 5.8 engine versions

Here is the rewind view of the repro example above:

Timeline:

[Image Removed]When Completing State completes:

[Image Removed]Next tick on the Non-completing State: (immediately succeedes)

[Image Removed]

[Attachment Removed]

Steps to Reproduce
Make two sibling states, one of them has two completing tasks (delays for example) and the other a non-completing state (forever delay that doesn’t complete)

[Image Removed]

Completing Sibling State:

[Image Removed]

Non-completing sibling: (important to have this state tick, either via ticking task or OnTick transition)

[Image Removed]

[Attachment Removed]

This is indeed a bug, and one that I think we have fixed post-5.8.

In 5.8, when a state has no task considered for completion, MakeCompletionTasksMask() sets the reserved bit and then shifts it left once per task, so it lands at BitIndex + TasksNum. FStateTreeTasksCompletionStatus::Push() then calls ResetStatus(State.TasksNum), which only clears bits BitIndex to BitIndex + TasksNum - 1. The reserved bit is never cleared, and because siblings all start at the parent’s NextBitIndexForCompletionMask, a Succeeded status left there by a previous sibling is read back as the new state’s completion. The same TasksNum reset is also used for sustained states in FStateTreeExecutionContext, so that path has the same problem.

This is fixed in UE5 Main but has not made it into a 5.8 release. The fix adds a CompletionTasksNumberOfBits member to FCompactStateTreeState, fills it from the compiler’s NumberOfBitsNeeded (which includes the reserved bit), and passes it to both ResetStatus() calls instead of TasksNum:

https://github.com/EpicGames/UnrealEngine/commit/71b807f41310a603478f5a958d05a93dbf95cecb

Because it changes FCompactStateTreeState in a public header, it will not go into a hotfix. The new member is filled in by the compiler, so your StateTree assets must be recompiled after you take it.

My best recommendation is to cherry-pick that change into both of your engines. If you would rather not touch the header, you can get the same result locally by working out the bit count from the mask at the two ResetStatus() call sites:

// StateTreeTasksStatus.cpp, FStateTreeTasksCompletionStatus::Push()

const int32 ReservedBitIndex = State.CompletionTasksMaskBitsOffset + State.TasksNum;

const bool bHasReservedBit = ReservedBitIndex < 32 && (State.CompletionTasksMask & (1u << ReservedBitIndex)) != 0;

GetStatus(State).ResetStatus(State.TasksNum + (bHasReservedBit ? 1 : 0));

Apply the same change where StateTreeExecutionContext.cpp calls ResetStatus(CurrentState.TasksNum). The task bits sit below BitIndex + TasksNum, so the mask can only have that bit set when it is the reserved one.

I believe you already build from UE5 Main rather than the release branch, could you please confirm which UE5 Main CL or commit it is based on? If FStateTreeTasksCompletionStatus::Push() in that tree already calls ResetStatus(State.CompletionTasksNumberOfBits) and you still see this, that would be a different issue.

Let me know if this fix covers it or if others.

-James

[Attachment Removed]

Hi, yes, we are behind that fix CL. Cherry-picking that CL fixed the issue.

Thanks for the reply

Simon

[Attachment Removed]

That is great news! I’m glad this solved the issue. It should be in 5.9, but it missed the 5.8 release since it had some header changes. If you encounter any other issues, let us know.

-James

[Attachment Removed]