Affects: 5.8 (verified on Release-5.8), Windows, Editor
Adding fx.Niagara.DataChannels.Enabled=true to an ini file makes the Niagara editor assert when a Data Channel Read is created. The same operation does not assert when the cvar is left untouched.
Add the following to <Project>/Config/DefaultEngine.ini:
[ConsoleVariables] fx.Niagara.DataChannels.Enabled=true
Then:
Assertion at Engine/Plugins/FX/Niagara/Source/Niagara/Private/NiagaraTypeRegistry.cpp:331:
bNeedsRegistration = !OldType.IsValid();
checkf(bNeedsRegistration || (NewType == OldType),
TEXT("bNeedsRegistration: %d, NewType: %s, OldType: %s NewTypeHash: %x OldTypeHash: %x"), ...);
NewType and OldType are two different types that resolved to the same RegisteredTypeDefIndex, and the NewTypeHash / OldTypeHash values printed by the assert are identical.
Creating a Data Channel Read does not assert, regardless of whether the cvar was set from an ini file.
Engine/Plugins/FX/Niagara/Source/Niagara/Private/NiagaraModule.cpp:168
void INiagaraModule::OnDataChannelsEnabledChanged(IConsoleVariable* Variable)
{
...
FNiagaraTypeRegistry::Register(UNiagaraDataInterfaceDataChannelWrite::StaticClass(), Flags);
FNiagaraTypeRegistry::Register(UNiagaraDataInterfaceDataChannelRead::StaticClass(), Flags);
These are the only FNiagaraTypeRegistry::Register call sites for these two classes in the engine — there is no registration in StartupModule or anywhere else.
The cvar default is already true (bool INiagaraModule::bDataChannelsEnabled = true;), so writing =true in ini does not change the value, but the ini apply path still calls Set() and fires the change callback. The callback therefore runs during early startup, at a point where the UClass objects are not yet fully initialized and StaticClass()->GetFName() returns NAME_None.
Engine/Plugins/FX/Niagara/Source/Niagara/Private/NiagaraModule.cpp:1659
void FNiagaraTypeDefinition::GeneratePathNameHash() { if (ClassStructOrEnum) { UnderlyingTypePathNameHash = GetTypeHash(ClassStructOrEnum->GetFName()); const UObject* Parent = ClassStructOrEnum; while ((Parent = Parent->GetOuter()) != nullptr) { UnderlyingTypePathNameHash = HashCombineFast(UnderlyingTypePathNameHash, GetTypeHash(Parent->GetFName())); } } else { UnderlyingTypePathNameHash = GetTypeHash(NAME_None); } }
Engine/Plugins/FX/Niagara/Source/Niagara/Public/NiagaraTypes.h:1195
friend inline uint32 GetTypeHash(const FNiagaraTypeDefinition& Type) { return HashCombine(HashCombine(Type.UnderlyingTypePathNameHash, GetTypeHash(Type.UnderlyingType)), GetTypeHash(Type.GetFlags())); }
The hash has only three inputs. When GetFName() returns NAME_None the name chain contributes nothing distinguishing, and UnderlyingType and Flags are identical for the two data interfaces (the same Flags value is passed at both call sites). All three inputs therefore match, so UNiagaraDataInterfaceDataChannelRead and UNiagaraDataInterfaceDataChannelWrite produce the same type hash.
Engine/Plugins/FX/Niagara/Source/Niagara/Private/NiagaraTypeRegistry.cpp:290
const uint32 TypeHash = GetTypeHash(NewType); ... if (const int32* ExistingIndex = RegisteredTypeIndexMap.Find(TypeHash)) { NewType.RegisteredTypeDefIndex = *ExistingIndex; }
Both classes end up sharing a single RegisteredTypeDefIndex, i.e. a single entry in RegisteredTypes.
When a properly initialized Data Channel Read type reaches RegisterTypeInternal, it resolves onto that shared/stale index. OldType is valid so bNeedsRegistration is false, NewType != OldType, and the checkf at NiagaraTypeRegistry.cpp:331 fires.
)}} and FNiagaraSystemUpdateContext Context; Context.AddAll(true); (commented "Re-init everything for now") during this early startup invocation, even though the cvar value did not actually change.
There's no existing public thread on this issue, so head over to Questions & Answers just mention UE-399252 in the post.
| 0 |
| Component | UE - Rendering - Architecture - Niagara |
|---|---|
| Affects Versions | 5.8 |
| Target Fix | 6.0 |
| Created | Sep 28, 2026 |
|---|---|
| Updated | Sep 30, 2026 |