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]