RigLogic: half-float restore keeps a dead fp32 copy of every FloatArray resident (clear() does not free)

FloatArrayDeserializer<std::uint16_t> reads the archived fp32 data, converts it to fp16, then calls instance.fp32.clear(). clear() destroys the elements but keeps the capacity, so every FloatArray restored in half-float mode keeps a dead fp32 buffer twice the size of its live fp16 data. This covers the joint storage values and NeuralNet::flatData.

In our content that is about 3.8 MB per MetaHuman head, which adds up quickly with several heads resident on console. Peak load memory is unaffected; this is purely memory that should have been returned after load.

Still present in //UE5/Main and //UE6/Main (checked against UE6/Main CL 58437171).

Suggested fix (we’ve taken this locally in 5.8). In FloatArrayDeserializer<std::uint16_t, TF256, TF128>::operator():

- instance.fp32.clear();

+ AlignedVector<float>{instance.fp32.get_allocator()}.swap(instance.fp32);

The temporary is built from the same allocator (same MemoryResource and alignment, so the PolyAllocators compare equal), which makes the swap well-defined, and the old buffer is freed when the temporary is destroyed. shrink_to_fit() would also work but is non-binding.

FloatArraySerializer<std::uint16_t> has the same clear() after archiving fp32. That only runs on save/dump so it matters less, but the same swap would be consistent there.

The builder paths (BPCMJointsBuilder / QuaternionJointsBuilder) write straight into fp16 and are not affected - only the restore path from cooked data is.

-        instance.fp32.clear();
+        AlignedVector<float>{instance.fp32.get_allocator()}.swap(instance.fp32);

[Image Removed]

The temporary is built from the same allocator (same MemoryResource and alignment, so the PolyAllocators compare equal), which makes the swap well-defined, and the old buffer is freed when the temporary is destroyed. shrink_to_fit() would also work but is non-binding.

FloatArraySerializer<std::uint16_t> has the same clear() after archiving fp32. That only runs on save/dump so it matters less, but the same swap would be consistent there.

The builder paths (BPCMJointsBuilder / QuaternionJointsBuilder) write straight into fp16 and are not affected - only the restore path from cooked data is.

[Attachment Removed]

Thank you for the report, yes, we confirm this issue is valid. We have applied a fix, which will be released with UE 5.8.5.

Until then, the p4 streams already contain the patch.

[Attachment Removed]