UE4 Cascade - Disabled Emitters Overhead

Posted on September 15, 2019

Table of Contents

Context

Creating VFX in Cascade is an iterative workflow, so emitter variations and older versions are often stored for backup and reference during the process. This is done by disabling the emitters. But is there a downside to this method?

TL;DR

Particle system templates load all their emitters, including disabled ones. This includes the emitters’ materials and other TypeData module assets like meshes for example. So the overhead here consists of asset loading and memory occupancy. But only if those assets aren’t already loaded and being used by something else in the scene.

Particle system components ignore disabled emitters entirely by removing their references from their internal lists. So the performance overhead doesn’t get worse with each component instance. It’s “pay once per particle system template”.

Modules

Disabled modules behave in the same way. The only different is that they’re not removed from internal lists. Instead every particle system component always checks if a module is enabled when it loops over its modules (at least once per Tick). Is this terrible? I don’t believe it’s significant but it could be a micro-optimization.

Solution

Banning disabled emitters/modules outright is disruptive to the user workflow.

A better solution would be to strip them out in the asset cook step? Note that some code might rely on emitter/module index so this requires caution. Also it would mean that emitters/modules won’t be toggle-able at runtime anymore, but I doubt anyone relies on it since those toggles are template-specific, not component-specific.

Another maybe simpler solution would be to prevent loading of assets in PostLoad for disabled emitters.

Code Analysis

In UParticleModuleTypeDataMesh::PostLoad() (the mesh particles module class) we have:

if (Mesh != nullptr)
{
    Mesh->ConditionalPostLoad();
}

In UParticleSpriteEmitter::PostLoad() (the particle system template class) we have:

if (RequiredModule->Material)
{
    RequiredModule->Material->ConditionalPostLoad();
}

I opened a bare-bones test level I made which only contained a particle system component. This triggered breakpoints in both places for disabled emitters.

However you won’t find the disabled emitters in UParticleSystemComponent::EmitterInstances (and this is why I say it’s a per-template overhead, not a per-component overhead).

In UParticleSystemComponent::InitParticles there’s a line for determining if an emitter instance should be created (HasAnyEnabledLODs returns false for disabled emitters):

const bool bShouldCreateAndOrInit = bDetailModeAllowsRendering && Emitter->HasAnyEnabledLODs() && bCanEverRender;

If bShouldCreateAndOrInit is false the function doesn’t create an emitter instance for that emitter, and deletes the existing instance if there is one. EmitterInstances stores NULL for that instance to conserve correct indexing.

bCookedOut

There exists a UParticleEmitter::bCookedOut property but it doesn’t seem to be set by anything at all? I did a project-wide search (also the entire repository on GitHub) and there were no results. Seems to be a leftover from old times, but the good news is that this is something we can leverage for cooking out disabled emitters :)

/** 
    *	If true, then this emitter was 'cooked out' by the cooker. 
    *	This means it was completely disabled, but to preserve any
    *	indexing schemes, it is left in place.
    */
UPROPERTY()
uint8 bCookedOut:1;

What manages Modules?

LOD level spawn/update/etc modules lists are filled using UParticleLODLevel::Modules. But I had a hard time finding what fills the Modules list in the first place.

Well turns out it’s the Cascade UI directly (ex: FCascade::OnNewModule). Modules and the derived lists include disabled modules because there’s no code to exclude them. Also confirmed with my test scene and breakpoints. Perhaps UParticleLODLevel::CompileModules (where the derived lists are created) is a good place to filter out disabled modules.