I’m working on a Mass-based projectile framework, with the goal of simulating and displaying large numbers of moving projectiles while keeping the CPU cost low. While profiling it, I noticed that native Mass ISM representation was taking a substantial amount of frame time, particularly in UMassVisualizationComponent::EndVisualChanges(). This led me to experiment with Niagara Mesh Renderer and Niagara Data Channels as another way to display the same Mass entities.
The initial Niagara implementation also had significant CPU overhead. Switching to a persistent native writer with cached typed field access reduced that cost considerably. That raised another question: how much of the ISM cost came from the way transforms were submitted?
To investigate, I created a separate benchmark project comparing native Mass ISM, custom bulk ISM updates, and Niagara Data Channels. The goal is to understand where the representation cost occurs and how much the submission path affects it, while keeping the underlying Mass movement simulation consistent.
Made a separate project for this benchmark with the following setup:
- Engine: Unreal Engine 5.8.4
- Build: Packaged Development
- Platform: W11, Dx12 (SM6)
- CPU: AMD Ryzen 9 3900X
- GPU: NVIDIA GeForce RTX 3080
- RAM: 32GB
- Entity count: 20,000
- Profiling: Unreal Insights CPU timing and stack sampling
The entities are created once in a grid. A shared Mass movement processor updates their FTransformFragment with deterministic vertical wave motion. Switching representation keeps the existing entities and movement simulation running.
The available modes are:
- 0 - None: movement simulation without visual representation.
- 1 - Native ISM: Unreal’s Mass Representation update path.
- 2 - Niagara: persistent GPU mesh particles reading current and previous transforms from a Niagara Data Channel.
- 3 - Custom ISM: a custom Mass processor collecting transforms by stable instance index and submitting one bulk ISM update per frame.
The ISM paths use the same benchmark mesh and material configuration, with shadows, collision, and navigation disabled. Native Mass representation significance is fixed to avoid distance-based representation changes during the comparison.
All three paths read transforms produced by the same Mass movement processor. Their main difference is how those transforms reach the visual representation:
- Native Mass ISM:
UMassUpdateISMProcessorcollects entity transforms into Mass Representation’s buffers.UMassVisualizationComponent::EndVisualChanges()then resolves entity-to-instance IDs, checks for duplicates and removals, and applies current and previous transforms through individual ISM update calls. - Custom bulk ISM: Each entity stores a stable ISM instance index. A custom processor gathers transforms into arrays indexed by that value, then calls
BatchUpdateInstancesTransforms()once per frame on a standardUInstancedStaticMeshComponent. This bypasses Mass Representation’s lookup and reconciliation work and reduces repeated component-level operations. It retains previous transforms for motion vectors. The implementation assumes a fixed population and does not handle individual instance removal or representation LOD transitions. - Niagara Data Channels: Each entity receives a reusable visual slot. A custom processor writes its current and previous position, rotation, and scale, together with an active flag into the corresponding NDC record. Persistent GPU particles store their slot index at spawn and read that record every update. Publication uses a persistent typed writer derived from FNDCWriterBase, which caches variable offsets. One write session covers the full pool each frame, including inactive slots. Collection and writing remain sequential on the game thread.
Niagara receives the same current and previous transform data supplied to the ISM paths, while each representation retains its own update and rendering pipeline.
These are inclusive CPU Frame timer statistics from steady sections of each mode, approximately 10 seconds per selection.
Difference in average CPU Frame duration relative to baseline:
- Mass ISM: +6.719 ms
- NDC: +1.337 ms
- Custom ISM: +2.994 ms
What the investigation suggests
Bulk submission substantially improved the ISM result without modifying engine classes. This suggests the native representation’s submission and bookkeeping path contributes meaningfully to the cost in this workload.
During separate editor/standalone sampling, I observed entity-to-instance hash lookups, transform conversions, instance change tracking, and UObject::Modify() calls beneath EndVisualChanges(). Those samples identify work being performed, but do not establish which operation dominates the packaged result.
Custom ISM still incurs downstream bounds, instance-buffer, and rendering updates. Niagara uses a different simulation and rendering pipeline, so matching transform data does not make their total workloads identical.
Limitations and questions
These are preliminary results from short selections, without repeated-run statistics or a GPU-time comparison yet. The custom implementation also handles a narrower workload than the general native representation system.
In relation to the discussion about specialized versus larger processors, this experiment changed the representation submission path; it does not isolate processor count or organization.
For this fixed-population:
- Is there an intended native Mass Representation path that provides comparable bulk submission?
- Is the per-instance application in
EndVisualChanges()necessary for its general bookkeeping or could a bulk path preserve that functionality? - Are there configuration choices I should check before attributing the difference to the implementation?




