How do I get the MassStationaryVisualizationTrait to work?

Hey,

I just spent three days on this and got to the root of the problem and I’m able to get it working by just adding an extra processor:


#pragma once

#include "MassProcessor.h"
#include "MassStationaryProcessor.generated.h"

/**
 * Processor that ensures stationary ISM entities are maintained in the ISM system
 * by marking them as processed each frame to prevent auto-removal
 */
UCLASS()
class UMassStationaryProcessor : public UMassProcessor
{
    GENERATED_BODY()

public:
    UMassStationaryProcessor();

protected:
    virtual void ConfigureQueries() override;
    virtual void Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context) override;

    FMassEntityQuery EntityQuery;
};

//////////////////////////////////////////

#include "MassStationaryProcessor.h"
#include "MassEntityView.h"
#include "MassRepresentationFragments.h"
#include "MassCommonFragments.h"
#include "MassCommonTypes.h"
#include "MassStationaryISMSwitcherProcessor.h"
#include "MassExecutionContext.h"
#include "MassRepresentationSubsystem.h"

UMassStationaryProcessor::UMassStationaryProcessor()
    : EntityQuery(*this)
{
    ExecutionOrder.ExecuteInGroup = UE::Mass::ProcessorGroupNames::Representation;

    // Make sure this processor runs after the ISM switcher processor
    ExecutionOrder.ExecuteAfter.Add(UMassStationaryISMSwitcherProcessor::StaticClass()->GetFName());

    bAutoRegisterWithProcessingPhases = true;
    ExecutionFlags = static_cast<int32>(EProcessorExecutionFlags::AllNetModes);
}

void UMassStationaryProcessor::ConfigureQueries()
{
    // Target entities with:
    // 1. Static representation tag (stationary entities)
    // 2. StaticMeshInstance representation
    // 3. Required components for visualization

    EntityQuery.AddTagRequirement<FMassStaticRepresentationTag>(EMassFragmentPresence::All);
    EntityQuery.AddTagRequirement<FMassStationaryISMSwitcherProcessorTag>(EMassFragmentPresence::All);

    EntityQuery.AddRequirement<FTransformFragment>(EMassFragmentAccess::ReadOnly);
    EntityQuery.AddRequirement<FMassRepresentationLODFragment>(EMassFragmentAccess::ReadOnly);
    EntityQuery.AddRequirement<FMassRepresentationFragment>(EMassFragmentAccess::ReadWrite);

    EntityQuery.AddSharedRequirement<FMassRepresentationSubsystemSharedFragment>(EMassFragmentAccess::ReadWrite);
}

void UMassStationaryProcessor::Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context)
{
    EntityQuery.ForEachEntityChunk(EntityManager, Context, [](FMassExecutionContext& Context)
        {
            UMassRepresentationSubsystem* RepresentationSubsystem = Context.GetMutableSharedFragment<FMassRepresentationSubsystemSharedFragment>().RepresentationSubsystem;
            check(RepresentationSubsystem);

            FMassInstancedStaticMeshInfoArrayView ISMInfos = RepresentationSubsystem->GetMutableInstancedStaticMeshInfos();

            const TConstArrayView<FTransformFragment> TransformList = Context.GetFragmentView<FTransformFragment>();
            const TConstArrayView<FMassRepresentationLODFragment> RepresentationLODList = Context.GetFragmentView<FMassRepresentationLODFragment>();
            const TArrayView<FMassRepresentationFragment> RepresentationList = Context.GetMutableFragmentView<FMassRepresentationFragment>();

            const int32 NumEntities = Context.GetNumEntities();
            for (int32 EntityIdx = 0; EntityIdx < NumEntities; EntityIdx++)
            {
                const FMassEntityHandle EntityHandle = Context.GetEntity(EntityIdx);
                const FTransformFragment& TransformFragment = TransformList[EntityIdx];
                const FMassRepresentationLODFragment& RepresentationLOD = RepresentationLODList[EntityIdx];
                FMassRepresentationFragment& Representation = RepresentationList[EntityIdx];

                // Only process entities with StaticMeshInstance representation type
                if (Representation.CurrentRepresentation == EMassRepresentationType::StaticMeshInstance &&
                    Representation.StaticMeshDescHandle.IsValid())
                {
                    // The key part - add this entity to the update list to prevent it from being auto-removed
                    // We pass the same transform for both current and previous transform, since it's stationary
                    ISMInfos[Representation.StaticMeshDescHandle.ToIndex()].AddBatchedTransform(
                        EntityHandle,
                        TransformFragment.GetTransform(),
                        TransformFragment.GetTransform(),
                        RepresentationLOD.LODSignificance,
                        Representation.PrevLODSignificance
                    );

                    // Update the PrevLODSignificance to match current
                    Representation.PrevLODSignificance = RepresentationLOD.LODSignificance;
                }
            }
        });
}

I believe it’s due to this part of the code in UMassVisualizationComponent::EndVisualChanges - without some updates the instances are considered unprocessed and removed - I did notice when I simulated that I could see some meshes for a frame sometimes and there’s a comment just below “note that we’re not clearing the dirty flag on purpose - these components require constant updates” that made me suspicious.

TArray<FPrimitiveInstanceId> RemovedISMInstanceIds;
RemovedISMInstanceIds.Reserve(Unprocessed.Num());
{
    for(TConstSetBitIterator<> BitIt(Unprocessed); BitIt; ++BitIt)
    {
        Experimental::FHashElementId ElementId(BitIt.GetIndex());
        if (SharedIdMap.ContainsElementId(ElementId))
        {
            FPrimitiveInstanceId InstanceId = SharedIdMap.GetByElementId(ElementId).Value;
            check(InstanceId.IsValid());
            SharedIdMap.RemoveByElementId(ElementId);
            RemovedISMInstanceIds.Add(InstanceId);
        }
    }       
    ISMComponent->RemoveInstancesById(RemovedISMInstanceIds);
}

I will roll with this for the moment but I suspect I may overhaul this, it’s kind of a pointless system and should just be fixed within UMassVisualizationComponent.