MassEntity threading model in 5.8

Hello,
I upgraded my plugin (https://www.fab.com/portal/listings/bfcc52a6-6ee7-4d78-a7be-c65ce4f0a18c/edit) for UE 5.8 with the release earlier this week. However, there is a significant change in the threading model that causes the performance of my plugin to be cut by half. Essentially it boils down to Mass Processors queries ParallelForEach now being executed inline instead of through a task per chunk. The EParallelExecutionFlags on the methods is ignored and instead a global UE::Mass::Tweakables::bForceInlineProcessorExecution takes over and cancels out the multithreading that we had since 5.4.
The value of UE::Mass::Tweakables::bForceInlineProcessorExecution is private, and a readonly cvar so it seems it cannot be changed. However, when, at runtime, I manually go change the value in memory for that variable, I get back to the old threading model and my performance comes back to what is expected and even better than in 5.7 (I had 55FPS on a test scene with 10K entities, this drops to 22FPS with 5.8).

So I think there was an omission here or perhaps some API change I do not understand. Essentially, all the ParallelForEach methods have become useless as they do not respect the expected parallelism, regardless of the EParallelExecutionFlags provided. I have not seen anything in the project settings to get the old behaviour back, nor can I see a way to configure a processor to multithread the chunks processing with ParallelForEach calls.

Am I missing something? This really looks like a breaking change.

4 Likes

This is of great interest to me. Please let me know if you’re able to solve this, thanks

1 Like

I’ve also experienced a massive performance degredation with our mass processors, dropping from 60fps with 10k entities to about 10fps. This lack of parallelization is most likely the culprit, so also very keen to hear if anyone figures out a way to restore this functionality.

1 Like

Yes, we have a local solution. We had written our own implementation of ParallellForEach for UE 5.1 to 5.3 since it wasn’t available yet. For now, we use that implementation in 5.8.
It’s somewhat simple. We simply create an ExecutionContext copy and start a TaskGraph task for each chunk, before waiting for all tasks completion.
MassEntity did this with a ParallellFor call but now they changed the threading model considerably for 5.8, breaking it in the process but I am hopeful they’ll fix it.

How does the performance of the plugin fare with your implementation of ParallelForEach? Is it still slower than it was at UE 5.7?

A bit faster than 5.7 but within margin of error.

Faster? Time to upgrade then. Thanks!

Yeah. This is the code we use:

            void ParallelForEachEntityChunk(FMassEntityQuery& Query, FMassEntityManager& EntityManager, FMassExecutionContext& ExecutionContext, const FMassExecuteFunction& ExecuteFunction)
            {
                if (Query.IsEmpty())
                    return;

#if MEGACOMPAT_UE_VERSION_LESS(5, 4) || MEGACOMPAT_MASSENTITY_ALWAYS_USE_TASKGRAPH_PARALLELISM
                // No official support for parallel processing before 5.4, do it using taskgraph
                FGraphEventArray ThreadedTasks;
                Query.ForEachEntityChunk(EntityManager, ExecutionContext,
                    [&ExecuteFunction, &ThreadedTasks](FMassExecutionContext& Context)
                    {
                        // Context is voluntarily copied below because MassEntity reuses the same object for each chunk. Therefore, when multithreaded,
                        // our fragment/entity views will change destination before starting the job and will point to the wrong array views.
                        ThreadedTasks.Add(FFunctionGraphTask::CreateAndDispatchWhenReady([&ExecuteFunction, Context] () mutable { ExecuteFunction(Context); }));
                    });

                if (!ThreadedTasks.IsEmpty())
                {
                    // Wait for all to complete before exiting
                    FTaskGraphInterface::Get().WaitUntilTasksComplete(ThreadedTasks);
                }
#elif MEGACOMPAT_UE_VERSION_LESS(5, 6)
                Query.ParallelForEachEntityChunk(EntityManager, ExecutionContext, ExecuteFunction);
#else
                Query.ParallelForEachEntityChunk(ExecutionContext, ExecuteFunction, FMassEntityQuery::EParallelExecutionFlags::Force);
#endif
            }

MEGACOMPAT_MASSENTITY_ALWAYS_USE_TASKGRAPH_PARALLELISM being set to 1 for 5.8 in our plugin.
MEGACOMPAT_UE_VERSION_* are equivalent to Unreal’s UE_VERSION_NEWER/OLDER_THAN

Of course we also have versions for entity collections but you can probably figure those out with the above code.

1 Like

This is great. I’ll show you guys what I’m up to once I migrate fully to 5.8

1 Like

The fix was pushed to version 0.5.0 @Jet_Scalawag
Along with SDF based environment avoidance (which I don’t think is relevant to you)

Reach out when you want by email. Hopefully the additions of BP in 0.4 fits your needs but let me know.

Symptom: With any AMegaBoidsSpawner placed in a level, the editor crashes with EXCEPTION_ACCESS_VIOLATION reading 0xffffffffffffffff inside UnrealEditor-Renderer.dll on the first editor-viewport draw after map load. 100% reproducible; PIE never starts. Callstack shows only Renderer/Engine frames (crash is downstream of proxy relevance gathering).

Root cause (bisected): FMegaBoidsSpawnerVisualizerSceneProxy (subclass of FDebugRenderSceneProxy) — its GetViewRelevance constructs a mostly-default FPrimitiveViewRelevance (bDynamicRelevance = true, no render-pass flags). UE 5.8’s renderer appears to consume relevance bits this proxy leaves unset, ultimately indexing with -1. Spawning the owning actor into a live 5.8 session crashes the editor the same frame, which is how we isolated it; every other actor class in our map was exonerated first.

Workaround we ship: UMegaBoidsSpawnerVisualizerComponent::CreateSceneProxy() returns nullptr on engine ≥ 5.8 — editor debug-bounds visualization is lost, everything else works.

Suggested fix: derive the relevance from the base class result (or fully populate the pass flags) rather than constructing from scratch.

Happy to provide the Aftermath dump hash or test a patched build. (Also: 0.5.0’s TaskGraph fallback for the 5.8 bForceInlineProcessorExecution regression works great — same solution we’d independently landed.) downstream of proxy relevance gathering).

Hello @Jet_Scalawag
This thread isn’t about the plugin so let’s sort that out over email. I’ll have a look and contact you.