Repository: nfrechette/acl (cpp)

Issue:
Add staged rotation decompression
When decompressing, a number of things happen:

*  Data is unpacked
*  Rotations are converted to their final format (W reconstructed)
*  Interpolation between two samples (rotations are normalized here as well)
*  We write the data into the output buffer

Being able to break down these into states could allow faster decompression. AVX and wider SIMD registers could be used for the rotation conversion and the interpolation of several inputs at the same time in a Structure of Array form.

Splitting things into stages will reduce the current decompression function size by splitting it up and it might improve its inlinability.

Instead of unpacking, reconstructing the W, interpolating and normalizing all in the same function, we would work as follow:

*  Unpack up to 8 rotations into a buffer into XXXX, YYYY, ZZZZ form (6x writes per rotation)
*  Once we have 8 rotations, reconstruct their W (75% fewer floating point operations) 4 at a time, interleaved to reduce stalls (we have plenty of registers on ARM64 and probably enough on x64)
*  Interpolate our rotations 4 at a time (25-75% fewer floating point operations), interleaved
*  Swizzle our 4 rotations back into AoS (8x shuffles), interleaved
*  Write the 4 rotations with the pose writer

Staged decompression will add a bit of complexity and a few more instructions to manage all of it on top of extra read/writes into the stack/L1 but we will reduce dramatically the number of floating point operations. Right now, reconstructing the W uses scalar mul/sub operations, a scalar sqrt, and scalar abs. Interpolation does 2x dot products with partial SIMD mul/add operations (where some lanes are unused) with a scalar comparison/fsel and a scalar sqrt. All of these operations operate mostly on 1 SIMD lane, sometimes 2. Staged decompression will allow 100% SIMD lane utilization for all floating math involved and even though the overall instruction count might go up, we will be reducing the number of expensive instructions (instead of 12 scalar sqrt for 4 bones, we'll be doing 3 SIMD sqrt) and interleaving will reduce the likelihood of stalls in the pipeline (all 12 scalar sqrt stall on the result but the interleaved SIMD sqrt can have their latency hidden).

Staged decompression also has another hidden benefit. It will allow time for hardware prefetching to work its magic in the background. By doing all of this every 8 bones, while working entirely in the L1 and in registers the prefetching will pull the next cache lines, hiding the memory latency.


Discussion:
A good first pass has been made with very promising results. This is clearly the way forward but more work remains.

Patch:
diff --git a/includes/acl/compression/impl/animated_track_utils.h b/includes/acl/compression/impl/animated_track_utils.h
--- a/includes/acl/compression/impl/animated_track_utils.h
+++ b/includes/acl/compression/impl/animated_track_utils.h
@@ -84,137 +84,45 @@ namespace acl
 			get_num_sub_tracks(segment, animated_group_filter_action, out_num_animated_rotation_sub_tracks, out_num_animated_translation_sub_tracks, out_num_animated_scale_sub_tracks);
 		}
 
-		inline animation_track_type8* calculate_sub_track_groups(const SegmentContext& segment, const uint32_t* output_bone_mapping, uint32_t num_output_bones, uint32_t& out_num_groups,
-			const std::function<bool(animation_track_type8 group_type, uint32_t bone_index)>& group_filter_action)
-		{
-			uint32_t num_rotation_sub_tracks = 0;
-			uint32_t num_translation_sub_tracks = 0;
-			uint32_t num_scale_sub_tracks = 0;
-			get_num_sub_tracks(segment, group_filter_action, num_rotation_sub_tracks, num_translation_sub_tracks, num_scale_sub_tracks);
-
-			const uint32_t num_rotation_groups = (num_rotation_sub_tracks + 3) / 4;
-			const uint32_t num_translation_groups = (num_translation_sub_tracks + 3) / 4;
-			const uint32_t num_scale_groups = (num_scale_sub_tracks + 3) / 4;
-			const uint32_t num_groups = num_rotation_groups + num_translation_groups + num_scale_groups;
-
-			animation_track_type8* sub_track_groups = allocate_type_array<animation_track_type8>(*segment.clip->allocator, num_groups);
-			std::memset(sub_track_groups, 0xFF, num_groups * sizeof(animation_track_type8));
-
-			// Simulate reading in groups of 4
-			uint32_t num_cached_rotations = 0;
-			uint32_t num_left_rotations = num_rotation_sub_tracks;
-
-			uint32_t num_cached_translations = 0;
-			uint32_t num_left_translations = num_translation_sub_tracks;
-
-			uint32_t num_cached_scales = 0;
-			uint32_t num_left_scales = num_scale_sub_tracks;
-
-			uint32_t current_group_index = 0;
-
-			for (uint32_t output_index = 0; output_index < num_output_bones; ++output_index)
-			{
-				if ((output_index % 4) == 0)
-				{
-					if (num_cached_rotations < 4 && num_left_rotations != 0)
-					{
-						sub_track_groups[current_group_index++] = animation_track_type8::rotation;
-						const uint32_t num_unpacked = std::min<uint32_t>(num_left_rotations, 4);
-						num_left_rotations -= num_unpacked;
-						num_cached_rotations += num_unpacked;
-					}
-
-					if (num_cached_translations < 4 && num_left_translations != 0)
-					{
-						sub_track_groups[current_group_index++] = animation_track_type8::translation;
-						const uint32_t num_unpacked = std::min<uint32_t>(num_left_translations, 4);
-						num_left_translations -= num_unpacked;
-						num_cached_translations += num_unpacked;
-					}
-
-					if (num_cached_scales < 4 && num_left_scales != 0)
-					{
-						sub_track_groups[current_group_index++] = animation_track_type8::scale;
-						const uint32_t num_unpacked = std::min<uint32_t>(num_left_scales, 4);
-						num_left_scales -= num_unpacked;
-						num_cached_scales += num_unpacked;
-					}
-				}
-
-				const uint32_t bone_index = output_bone_mapping[output_index];
-
-				if (group_filter_action(animation_track_type8::rotation, bone_index))
-					num_cached_rotations--;		// Consumed
-
-				if (group_filter_action(animation_track_type8::translation, bone_index))
-					num_cached_translations--;	// Consumed
-
-				if (group_filter_action(animation_track_type8::scale, bone_index))
-					num_cached_scales--;		// Consumed
-			}
-
-			ACL_ASSERT(current_group_index == num_groups, "Unexpected number of groups written");
-
-			out_num_groups = num_groups;
-			return sub_track_groups;
-		}
-
 		inline void group_writer(const SegmentContext& segment, const uint32_t* output_bone_mapping, uint32_t num_output_bones,
 			const std::function<bool(animation_track_type8 group_type, uint32_t bone_index)>& group_filter_action,
 			const std::function<void(animation_track_type8 group_type, uint32_t group_size, uint32_t bone_index)>& group_entry_action,
 			const std::function<void(animation_track_type8 group_type, uint32_t group_size)>& group_flush_action)
 		{
-			uint32_t num_groups = 0;
-			animation_track_type8* sub_track_groups = calculate_sub_track_groups(segment, output_bone_mapping, num_output_bones, num_groups, group_filter_action);
+			// Data is ordered in groups of 4 animated sub-tracks (e.g rot0, rot1, rot2, rot3)
+			// Groups are sorted per sub-track type. All rotation groups come first followed by translations then scales.
+			// The last group of each sub-track may or may not have padding. The last group might be less than 4 sub-tracks.
 
-			uint32_t group_size = 0;
-
-			uint32_t rotation_output_index = 0;
-			uint32_t translation_output_index = 0;
-			uint32_t scale_output_index = 0;
-			for (uint32_t group_index = 0; group_index < num_groups; ++group_index)
+			const auto group_writer_impl = [output_bone_mapping, num_output_bones, &group_filter_action, &group_entry_action, &group_flush_action](animation_track_type8 group_type)
 			{
-				const animation_track_type8 group_type = sub_track_groups[group_index];
+				uint32_t group_size = 0;
 
-				if (group_type == animation_track_type8::rotation)
+				for (uint32_t output_index = 0; output_index < num_output_bones; ++output_index)
 				{
-					for (; group_size < 4 && rotation_output_index < num_output_bones; ++rotation_output_index)
-					{
-						const uint32_t bone_index = output_bone_mapping[rotation_output_index];
+					const uint32_t bone_index = output_bone_mapping[output_index];
 
-						if (group_filter_action(animation_track_type8::rotation, bone_index))
-							group_entry_action(group_type, group_size++, bone_index);
-					}
-				}
-				else if (group_type == animation_track_type8::translation)
-				{
-					for (; group_size < 4 && translation_output_index < num_output_bones; ++translation_output_index)
-					{
-						const uint32_t bone_index = output_bone_mapping[translation_output_index];
+					if (group_filter_action(group_type, bone_index))
+						group_entry_action(group_type, group_size++, bone_index);
 
-						if (group_filter_action(animation_track_type8::translation, bone_index))
-							group_entry_action(group_type, group_size++, bone_index);
-					}
-				}
-				else // scale
-				{
-					for (; group_size < 4 && scale_output_index < num_output_bones; ++scale_output_index)
+					if (group_size == 4)
 					{
-						const uint32_t bone_index = output_bone_mapping[scale_output_index];
-
-						if (group_filter_action(animation_track_type8::scale, bone_index))
-							group_entry_action(group_type, group_size++, bone_index);
+						// Group full, write it out and move onto to the next group
+						group_flush_action(group_type, group_size);
+						group_size = 0;
 					}
 				}
 
-				ACL_ASSERT(group_size != 0, "Group cannot be empty");
+				// If group has leftover tracks, write it out
+				if (group_size != 0)
+					group_flush_action(group_type, group_size);
+			};
 
-				// Group full or we ran out of tracks, write it out and move onto to the next group
-				group_flush_action(group_type, group_size);
-				group_size = 0;
-			}
+			// Output sub-tracks in natural order: rot, trans, scale
+			group_writer_impl(animation_track_type8::rotation);
+			group_writer_impl(animation_track_type8::translation);
 
-			deallocate_type_array(*segment.clip->allocator, sub_track_groups, num_groups);
+			if (segment.clip->has_scale)
+				group_writer_impl(animation_track_type8::scale);
 		}
 
 		inline void animated_group_writer(const SegmentContext& segment, const uint32_t* output_bone_mapping, uint32_t num_output_bones,
@@ -235,24 +143,6 @@ namespace acl
 
 			group_writer(segment, output_bone_mapping, num_output_bones, animated_group_filter_action, group_entry_action, group_flush_action);
 		}
-
-		inline void constant_group_writer(const SegmentContext& segment, const uint32_t* output_bone_mapping, uint32_t num_output_bones,
-			const std::function<void(animation_track_type8 group_type, uint32_t group_size, uint32_t bone_index)>& group_entry_action,
-			const std::function<void(animation_track_type8 group_type, uint32_t group_size)>& group_flush_action)
-		{
-			const auto constant_group_filter_action = [&](animation_track_type8 group_type, uint32_t bone_index)
-			{
-				const BoneStreams& bone_stream = segment.bone_streams[bone_index];
-				if (group_type == animation_track_type8::rotation)
-					return !bone_stream.is_rotation_default && bone_stream.is_rotation_constant;
-				else if (group_type == animation_track_type8::translation)
-					return !bone_stream.is_translation_default && bone_stream.is_translation_constant;
-				else
-					return !bone_stream.is_scale_default && bone_stream.is_scale_constant;
-			};
-
-			group_writer(segment, output_bone_mapping, num_output_bones, constant_group_filter_action, group_entry_action, group_flush_action);
-		}
 	}
 }
 
diff --git a/includes/acl/compression/impl/compress.database.impl.h b/includes/acl/compression/impl/compress.database.impl.h
--- a/includes/acl/compression/impl/compress.database.impl.h
+++ b/includes/acl/compression/impl/compress.database.impl.h
@@ -445,6 +445,8 @@ namespace acl
 				const uint32_t animated_pose_bit_size = headers[segment_index].animated_pose_bit_size;
 
 				out_headers[segment_index].animated_pose_bit_size = animated_pose_bit_size;
+				out_headers[segment_index].animated_rotation_bit_size = headers[segment_index].animated_rotation_bit_size;
+				out_headers[segment_index].animated_translation_bit_size = headers[segment_index].animated_translation_bit_size;
 				out_headers[segment_index].segment_data = segment_data_offset;
 				out_headers[segment_index].sample_indices = build_sample_indices(tier_mapping, tracks_index, segment_index);
 
@@ -540,8 +542,14 @@ namespace acl
 				const optional_metadata_header& input_metadata_header = get_optional_metadata_header(*input_tracks);
 
 				const uint32_t num_sub_tracks_per_bone = input_header.get_has_scale() ? 3 : 2;
-				const uint32_t num_sub_tracks = input_header.num_tracks * num_sub_tracks_per_bone;
-				const bitset_description bitset_desc = bitset_description::make_from_num_bits(num_sub_tracks);
+
+				// Calculate how many sub-track packed entries we have
+				// Each sub-track is 2 bits packed within a 32 bit entry
+				// For each sub-track type, we round up to simplify bookkeeping
+				// For example, if we have 3 tracks made up of rotation/translation we'll have one entry for each with unused padding
+				// All rotation types come first, followed by all translation types, and with scale types at the end when present
+				const uint32_t num_sub_track_entries = ((input_header.num_tracks + k_num_sub_tracks_per_packed_entry - 1) / k_num_sub_tracks_per_packed_entry) * num_sub_tracks_per_bone;
+				const uint32_t packed_sub_track_buffer_size = num_sub_track_entries * sizeof(packed_sub_track_types);
 
 				// Adding an extra index at the end to delimit things, the index is always invalid: 0xFFFFFFFF
 				const uint32_t segment_start_indices_size = clip_error.num_segments > 1 ? (sizeof(uint32_t) * (clip_error.num_segments + 1)) : 0;
@@ -550,11 +558,8 @@ namespace acl
 				// Range data follows constant data, use that to calculate our size
 				const uint32_t constant_data_size = (uint32_t)input_transforms_header.clip_range_data_offset - (uint32_t)input_transforms_header.constant_track_data_offset;
 
-				// Animated group size type data follows the range data, use that to calculate our size
-				const uint32_t clip_range_data_size = (uint32_t)input_transforms_header.animated_group_types_offset - (uint32_t)input_transforms_header.clip_range_data_offset;
-
-				// The data from our first segment follows the animated group types, use that to calculate our size
-				const uint32_t animated_group_types_size = (uint32_t)input_segment_headers[0].segment_data - (uint32_t)input_transforms_header.animated_group_types_offset;
+				// The data from our first segment follows the clip range data, use that to calculate our size
+				const uint32_t clip_range_data_size = (uint32_t)input_segment_headers[0].segment_data - (uint32_t)input_transforms_header.clip_range_data_offset;
 
 				// Calculate the new size of our clip
 				uint32_t buffer_size = 0;
@@ -572,14 +577,12 @@ namespace acl
 				buffer_size = align_to(buffer_size, 4);								// Align database header
 				buffer_size += sizeof(tracks_database_header);						// Database header
 
-				buffer_size = align_to(buffer_size, 4);								// Align bitsets
-				buffer_size += bitset_desc.get_num_bytes();							// Default tracks bitset
-				buffer_size += bitset_desc.get_num_bytes();							// Constant tracks bitset
+				buffer_size = align_to(buffer_size, 4);								// Align sub-track types
+				buffer_size += packed_sub_track_buffer_size;						// Packed sub-track types sorted by type
 				buffer_size = align_to(buffer_size, 4);								// Align constant track data
 				buffer_size += constant_data_size;									// Constant track data
 				buffer_size = align_to(buffer_size, 4);								// Align range data
 				buffer_size += clip_range_data_size;								// Range data
-				buffer_size += animated_group_types_size;							// Our animated group types
 
 				// Per segment data
 				for (uint32_t segment_index = 0; segment_index < input_transforms_header.num_segments; ++segment_index)
@@ -669,11 +672,9 @@ namespace acl
 				const uint32_t segment_start_indices_offset = align_to<uint32_t>(sizeof(transform_tracks_header), 4);	// Relative to the start of our transform_tracks_header
 				transforms_header->database_header_offset = align_to(segment_start_indices_offset + segment_start_indices_size, 4);
 				transforms_header->segment_headers_offset = align_to(transforms_header->database_header_offset + sizeof(tracks_database_header), 4);
-				transforms_header->default_tracks_bitset_offset = align_to(transforms_header->segment_headers_offset + segment_headers_size, 4);
-				transforms_header->constant_tracks_bitset_offset = transforms_header->default_tracks_bitset_offset + bitset_desc.get_num_bytes();
-				transforms_header->constant_track_data_offset = align_to(transforms_header->constant_tracks_bitset_offset + bitset_desc.get_num_bytes(), 4);
+				transforms_header->sub_track_types_offset = align_to(transforms_header->segment_headers_offset + segment_headers_size, 4);
+				transforms_header->constant_track_data_offset = align_to(transforms_header->sub_track_types_offset + packed_sub_track_buffer_size, 4);
 				transforms_header->clip_range_data_offset = align_to(transforms_header->constant_track_data_offset + constant_data_size, 4);
-				transforms_header->animated_group_types_offset = transforms_header->clip_range_data_offset + clip_range_data_size;
 
 				// Copy our segment start indices, they do not change
 				if (input_transforms_header.has_multiple_segments())
@@ -688,12 +689,11 @@ namespace acl
 				clip_header_offset += sizeof(database_runtime_segment_header) * input_transforms_header.num_segments;
 
 				// Write our new segment headers
-				const uint32_t segment_data_base_offset = transforms_header->animated_group_types_offset + animated_group_types_size;
+				const uint32_t segment_data_base_offset = transforms_header->clip_range_data_offset + clip_range_data_size;
 				write_segment_headers(tier_mapping, list_index, input_transforms_header, input_segment_headers, segment_data_base_offset, transforms_header->get_segment_tier0_headers());
 
-				// Copy our bitsets, they do not change
-				std::memcpy(transforms_header->get_default_tracks_bitset(), input_transforms_header.get_default_tracks_bitset(), bitset_desc.get_num_bytes());
-				std::memcpy(transforms_header->get_constant_tracks_bitset(), input_transforms_header.get_constant_tracks_bitset(), bitset_desc.get_num_bytes());
+				// Copy our sub-track types, they do not change
+				std::memcpy(transforms_header->get_sub_track_types(), input_transforms_header.get_sub_track_types(), packed_sub_track_buffer_size);
 
 				// Copy our constant track data, it does not change
 				std::memcpy(transforms_header->get_constant_track_data(), input_transforms_header.get_constant_track_data(), constant_data_size);
@@ -701,9 +701,6 @@ namespace acl
 				// Copy our clip range data, it does not change
 				std::memcpy(transforms_header->get_clip_range_data(), input_transforms_header.get_clip_range_data(), clip_range_data_size);
 
-				// Copy our animated group type data, it does not change
-				std::memcpy(transforms_header->get_animated_group_types(), input_transforms_header.get_animated_group_types(), animated_group_types_size);
-
 				// Write our new segment data
 				write_segment_data(tier_mapping, list_index, input_transforms_header, input_segment_headers, *transforms_header, transforms_header->get_segment_tier0_headers());
 
@@ -1085,7 +1082,7 @@ namespace acl
 
 						uint8_t* animated_data = segment_chunk_header.samples_offset.add_to(bulk_data);
 						const uint32_t size = write_segment_data(segment_frames, num_segment_frames, animated_data);
-						ACL_ASSERT(size == segment_data_size, "Unexpected segment data size"); (void)size;
+						ACL_ASSERT(size == segment_data_size, "Unexpected segment data size"); (void)size; (void)segment_data_size;
 
 						chunk_segment_index++;
 					}
diff --git a/includes/acl/compression/impl/compress.impl.h b/includes/acl/compression/impl/compress.impl.h
--- a/includes/acl/compression/impl/compress.impl.h
+++ b/includes/acl/compression/impl/compress.impl.h
@@ -46,8 +46,8 @@
 #include "acl/compression/impl/segment_streams.h"
 #include "acl/compression/impl/write_segment_data.h"
 #include "acl/compression/impl/write_stats.h"
-#include "acl/compression/impl/write_stream_bitsets.h"
 #include "acl/compression/impl/write_stream_data.h"
+#include "acl/compression/impl/write_sub_track_types.h"
 #include "acl/compression/impl/write_track_metadata.h"
 
 #include <cstdint>
@@ -127,7 +127,7 @@ namespace acl
 			{
 				// Normalize our samples into the clip wide ranges per bone
 				normalize_clip_streams(lossy_clip_context, range_reduction);
-				clip_range_data_size = get_stream_range_data_size(lossy_clip_context, range_reduction, settings.rotation_format);
+				clip_range_data_size = get_clip_range_data_size(lossy_clip_context, range_reduction, settings.rotation_format);
 			}
 
 			segment_streams(allocator, lossy_clip_context, settings.segmenting);
@@ -154,24 +154,15 @@ namespace acl
 			uint32_t num_animated_variable_sub_tracks_padded = 0;
 			const uint32_t format_per_track_data_size = get_format_per_track_data_size(lossy_clip_context, settings.rotation_format, settings.translation_format, settings.scale_format, &num_animated_variable_sub_tracks_padded);
 
-			auto animated_group_filter_action = [&](animation_track_type8 group_type, uint32_t bone_index)
-			{
-				const BoneStreams& bone_stream = lossy_clip_context.segments[0].bone_streams[bone_index];
-				if (group_type == animation_track_type8::rotation)
-					return !bone_stream.is_rotation_constant;
-				else if (group_type == animation_track_type8::translation)
-					return !bone_stream.is_translation_constant;
-				else
-					return !bone_stream.is_scale_constant;
-			};
-
-			uint32_t num_animated_groups = 0;
-			animation_track_type8* animated_sub_track_groups = calculate_sub_track_groups(lossy_clip_context.segments[0], output_bone_mapping, num_output_bones, num_animated_groups, animated_group_filter_action);
-			const uint32_t animated_group_types_size = sizeof(animation_track_type8) * (num_animated_groups + 1);	// Includes terminator
-
 			const uint32_t num_sub_tracks_per_bone = lossy_clip_context.has_scale ? 3 : 2;
-			const uint32_t num_sub_tracks = num_output_bones * num_sub_tracks_per_bone;
-			const bitset_description bitset_desc = bitset_description::make_from_num_bits(num_sub_tracks);
+
+			// Calculate how many sub-track packed entries we have
+			// Each sub-track is 2 bits packed within a 32 bit entry
+			// For each sub-track type, we round up to simplify bookkeeping
+			// For example, if we have 3 tracks made up of rotation/translation we'll have one entry for each with unused padding
+			// All rotation types come first, followed by all translation types, and with scale types at the end when present
+			const uint32_t num_sub_track_entries = ((num_output_bones + k_num_sub_tracks_per_packed_entry - 1) / k_num_sub_tracks_per_packed_entry) * num_sub_tracks_per_bone;
+			const uint32_t packed_sub_track_buffer_size = num_sub_track_entries * sizeof(packed_sub_track_types);
 
 			// Adding an extra index at the end to delimit things, the index is always invalid: 0xFFFFFFFF
 			const uint32_t segment_start_indices_size = lossy_clip_context.num_segments > 1 ? (sizeof(uint32_t) * (lossy_clip_context.num_segments + 1)) : 0;
@@ -189,17 +180,15 @@ namespace acl
 			buffer_size += segment_start_indices_size;							// Segment start indices
 			buffer_size = align_to(buffer_size, 4);								// Align segment headers
 			buffer_size += segment_headers_size;								// Segment headers
-			buffer_size = align_to(buffer_size, 4);								// Align bitsets
+			buffer_size = align_to(buffer_size, 4);								// Align sub-track types
 
 			const uint32_t clip_segment_header_size = buffer_size - clip_header_size;
 
-			buffer_size += bitset_desc.get_num_bytes();							// Default tracks bitset
-			buffer_size += bitset_desc.get_num_bytes();							// Constant tracks bitset
+			buffer_size += packed_sub_track_buffer_size;						// Packed sub-track types sorted by type
 			buffer_size = align_to(buffer_size, 4);								// Align constant track data
 			buffer_size += constant_data_size;									// Constant track data
 			buffer_size = align_to(buffer_size, 4);								// Align range data
 			buffer_size += clip_range_data_size;								// Range data
-			buffer_size += animated_group_types_size;							// Our animated group types
 
 			const uint32_t clip_data_size = buffer_size - clip_segment_header_size - clip_header_size;
 
@@ -310,22 +299,19 @@ namespace acl
 			const uint32_t segment_start_indices_offset = align_to<uint32_t>(sizeof(transform_tracks_header), 4);	// Relative to the start of our transform_tracks_header
 			transforms_header->database_header_offset = invalid_ptr_offset();
 			transforms_header->segment_headers_offset = align_to(segment_start_indices_offset + segment_start_indices_size, 4);
-			transforms_header->default_tracks_bitset_offset = align_to(transforms_header->segment_headers_offset + segment_headers_size, 4);
-			transforms_header->constant_tracks_bitset_offset = transforms_header->default_tracks_bitset_offset + bitset_desc.get_num_bytes();
-			transforms_header->constant_track_data_offset = align_to(transforms_header->constant_tracks_bitset_offset + bitset_desc.get_num_bytes(), 4);
+			transforms_header->sub_track_types_offset = align_to(transforms_header->segment_headers_offset + segment_headers_size, 4);
+			transforms_header->constant_track_data_offset = align_to(transforms_header->sub_track_types_offset + packed_sub_track_buffer_size, 4);
 			transforms_header->clip_range_data_offset = align_to(transforms_header->constant_track_data_offset + constant_data_size, 4);
-			transforms_header->animated_group_types_offset = transforms_header->clip_range_data_offset + clip_range_data_size;
 
 			uint32_t written_segment_start_indices_size = 0;
 			if (lossy_clip_context.num_segments > 1)
 				written_segment_start_indices_size = write_segment_start_indices(lossy_clip_context, transforms_header->get_segment_start_indices());
 
-			const uint32_t segment_data_start_offset = transforms_header->animated_group_types_offset + animated_group_types_size;
+			const uint32_t segment_data_start_offset = transforms_header->clip_range_data_offset + clip_range_data_size;
 			const uint32_t written_segment_headers_size = write_segment_headers(lossy_clip_context, settings, transforms_header->get_segment_headers(), segment_data_start_offset);
 
-			uint32_t written_bitset_size = 0;
-			written_bitset_size += write_default_track_bitset(lossy_clip_context, transforms_header->get_default_tracks_bitset(), bitset_desc, output_bone_mapping, num_output_bones);
-			written_bitset_size += write_constant_track_bitset(lossy_clip_context, transforms_header->get_constant_tracks_bitset(), bitset_desc, output_bone_mapping, num_output_bones);
+			uint32_t written_sub_track_buffer_size = 0;
+			written_sub_track_buffer_size += write_packed_sub_track_types(lossy_clip_context, transforms_header->get_sub_track_types(), output_bone_mapping, num_output_bones);
 
 			uint32_t written_constant_data_size = 0;
 			if (constant_data_size != 0)
@@ -335,8 +321,6 @@ namespace acl
 			if (range_reduction != range_reduction_flags8::none)
 				written_clip_range_data_size = write_clip_range_data(lossy_clip_context, range_reduction, transforms_header->get_clip_range_data(), clip_range_data_size, output_bone_mapping, num_output_bones);
 
-			const uint32_t written_animated_group_types_size = write_animated_group_types(animated_sub_track_groups, num_animated_groups, transforms_header->get_animated_group_types(), animated_group_types_size);
-
 			const uint32_t written_segment_data_size = write_segment_data(lossy_clip_context, settings, range_reduction, transforms_header->get_segment_headers(), *transforms_header, output_bone_mapping, num_output_bones);
 
 			// Optional metadata header is last
@@ -411,13 +395,12 @@ namespace acl
 				buffer += written_segment_start_indices_size;
 				buffer = align_to(buffer, 4);								// Align segment headers
 				buffer += written_segment_headers_size;
-				buffer = align_to(buffer, 4);								// Align bitsets
-				buffer += written_bitset_size;
+				buffer = align_to(buffer, 4);								// Align sub-track types
+				buffer += written_sub_track_buffer_size;
 				buffer = align_to(buffer, 4);								// Align constant track data
 				buffer += written_constant_data_size;
 				buffer = align_to(buffer, 4);								// Align range data
 				buffer += written_clip_range_data_size;
-				buffer += written_animated_group_types_size;
 				buffer += written_segment_data_size;
 
 				if (metadata_size != 0)
@@ -435,10 +418,9 @@ namespace acl
 				ACL_ASSERT(written_segment_start_indices_size == segment_start_indices_size, "Wrote too little or too much data");
 				ACL_ASSERT(written_segment_headers_size == segment_headers_size, "Wrote too little or too much data");
 				ACL_ASSERT(written_segment_data_size == segment_data_size, "Wrote too little or too much data");
-				ACL_ASSERT(written_bitset_size == (bitset_desc.get_num_bytes() * 2), "Wrote too little or too much data");
+				ACL_ASSERT(written_sub_track_buffer_size == packed_sub_track_buffer_size, "Wrote too little or too much data");
 				ACL_ASSERT(written_constant_data_size == constant_data_size, "Wrote too little or too much data");
 				ACL_ASSERT(written_clip_range_data_size == clip_range_data_size, "Wrote too little or too much data");
-				ACL_ASSERT(written_animated_group_types_size == animated_group_types_size, "Wrote too little or too much data");
 				ACL_ASSERT(writter_metadata_track_list_name_size == metadata_track_list_name_size, "Wrote too little or too much data");
 				ACL_ASSERT(written_metadata_track_names_size == metadata_track_names_size, "Wrote too little or too much data");
 				ACL_ASSERT(written_metadata_parent_track_indices_size == metadata_parent_track_indices_size, "Wrote too little or too much data");
@@ -455,11 +437,10 @@ namespace acl
 #else
 			(void)written_segment_start_indices_size;
 			(void)written_segment_headers_size;
-			(void)written_bitset_size;
+			(void)written_sub_track_buffer_size;
 			(void)written_constant_data_size;
 			(void)written_clip_range_data_size;
 			(void)written_segment_data_size;
-			(void)written_animated_group_types_size;
 			(void)segment_data_size;
 			(void)buffer_start;
 #endif
@@ -471,7 +452,6 @@ namespace acl
 				write_stats(allocator, track_list, lossy_clip_context, *out_compressed_tracks, settings, range_reduction, raw_clip_context, additive_base_clip_context, compression_time, out_stats);
 #endif
 
-			deallocate_type_array(allocator, animated_sub_track_groups, num_animated_groups);
 			deallocate_type_array(allocator, output_bone_mapping, num_output_bones);
 			destroy_clip_context(lossy_clip_context);
 			destroy_clip_context(raw_clip_context);
diff --git a/includes/acl/compression/impl/write_range_data.h b/includes/acl/compression/impl/write_range_data.h
--- a/includes/acl/compression/impl/write_range_data.h
+++ b/includes/acl/compression/impl/write_range_data.h
@@ -49,7 +49,7 @@ namespace acl
 {
 	namespace acl_impl
 	{
-		inline uint32_t get_stream_range_data_size(const clip_context& clip, range_reduction_flags8 range_reduction, rotation_format8 rotation_format)
+		inline uint32_t get_clip_range_data_size(const clip_context& clip, range_reduction_flags8 range_reduction, rotation_format8 rotation_format)
 		{
 			const uint32_t rotation_size = are_any_enum_flags_set(range_reduction, range_reduction_flags8::rotations) ? get_range_reduction_rotation_size(rotation_format) : 0;
 			const uint32_t translation_size = are_any_enum_flags_set(range_reduction, range_reduction_flags8::translations) ? k_clip_range_reduction_vector3_range_size : 0;
@@ -73,117 +73,6 @@ namespace acl
 			return range_data_size;
 		}
 
-		inline void write_range_track_data_impl(const TrackStream& track, const TrackStreamRange& range, bool is_clip_range_data, uint8_t*& out_range_data)
-		{
-			const rtm::vector4f range_min = range.get_min();
-			const rtm::vector4f range_extent = range.get_extent();
-
-			if (is_clip_range_data)
-			{
-				const uint32_t range_member_size = sizeof(float) * 3;
-
-				std::memcpy(out_range_data, &range_min, range_member_size);
-				out_range_data += range_member_size;
-				std::memcpy(out_range_data, &range_extent, range_member_size);
-				out_range_data += range_member_size;
-			}
-			else
-			{
-				if (is_constant_bit_rate(track.get_bit_rate()))
-				{
-					const uint8_t* sample_ptr = track.get_raw_sample_ptr(0);
-					std::memcpy(out_range_data, sample_ptr, sizeof(uint16_t) * 3);
-					out_range_data += sizeof(uint16_t) * 3;
-				}
-				else
-				{
-					pack_vector3_u24_unsafe(range_min, out_range_data);
-					out_range_data += sizeof(uint8_t) * 3;
-					pack_vector3_u24_unsafe(range_extent, out_range_data);
-					out_range_data += sizeof(uint8_t) * 3;
-				}
-			}
-		}
-
-		inline uint32_t write_range_track_data(const BoneStreams* bone_streams, const BoneRanges* bone_ranges,
-			range_reduction_flags8 range_reduction, bool is_clip_range_data,
-			uint8_t* range_data, uint32_t range_data_size,
-			const uint32_t* output_bone_mapping, uint32_t num_output_bones)
-		{
-			ACL_ASSERT(range_data != nullptr, "'range_data' cannot be null!");
-			(void)range_data_size;
-
-#if defined(ACL_HAS_ASSERT_CHECKS)
-			const uint8_t* range_data_end = add_offset_to_ptr<uint8_t>(range_data, range_data_size);
-#endif
-
-			const uint8_t* range_data_start = range_data;
-
-			for (uint32_t output_index = 0; output_index < num_output_bones; ++output_index)
-			{
-				const uint32_t bone_index = output_bone_mapping[output_index];
-				const BoneStreams& bone_stream = bone_streams[bone_index];
-				const BoneRanges& bone_range = bone_ranges[bone_index];
-
-				// normalized value is between [0.0 .. 1.0]
-				// value = (normalized value * range extent) + range min
-				// normalized value = (value - range min) / range extent
-
-				if (are_any_enum_flags_set(range_reduction, range_reduction_flags8::rotations) && !bone_stream.is_rotation_constant)
-				{
-					const rtm::vector4f range_min = bone_range.rotation.get_min();
-					const rtm::vector4f range_extent = bone_range.rotation.get_extent();
-
-					if (is_clip_range_data)
-					{
-						const uint32_t range_member_size = bone_stream.rotations.get_rotation_format() == rotation_format8::quatf_full ? (sizeof(float) * 4) : (sizeof(float) * 3);
-
-						std::memcpy(range_data, &range_min, range_member_size);
-						range_data += range_member_size;
-						std::memcpy(range_data, &range_extent, range_member_size);
-						range_data += range_member_size;
-					}
-					else
-					{
-						if (bone_stream.rotations.get_rotation_format() == rotation_format8::quatf_full)
-						{
-							pack_vector4_32(range_min, true, range_data);
-							range_data += sizeof(uint8_t) * 4;
-							pack_vector4_32(range_extent, true, range_data);
-							range_data += sizeof(uint8_t) * 4;
-						}
-						else
-						{
-							if (is_constant_bit_rate(bone_stream.rotations.get_bit_rate()))
-							{
-								const uint8_t* rotation = bone_stream.rotations.get_raw_sample_ptr(0);
-								std::memcpy(range_data, rotation, sizeof(uint16_t) * 3);
-								range_data += sizeof(uint16_t) * 3;
-							}
-							else
-							{
-								pack_vector3_u24_unsafe(range_min, range_data);
-								range_data += sizeof(uint8_t) * 3;
-								pack_vector3_u24_unsafe(range_extent, range_data);
-								range_data += sizeof(uint8_t) * 3;
-							}
-						}
-					}
-				}
-
-				if (are_any_enum_flags_set(range_reduction, range_reduction_flags8::translations) && !bone_stream.is_translation_constant)
-					write_range_track_data_impl(bone_stream.translations, bone_range.translation, is_clip_range_data, range_data);
-
-				if (are_any_enum_flags_set(range_reduction, range_reduction_flags8::scales) && !bone_stream.is_scale_constant)
-					write_range_track_data_impl(bone_stream.scales, bone_range.scale, is_clip_range_data, range_data);
-
-				ACL_ASSERT(range_data <= range_data_end, "Invalid range data offset. Wrote too much data.");
-			}
-
-			ACL_ASSERT(range_data == range_data_end, "Invalid range data offset. Wrote too little data.");
-			return safe_static_cast<uint32_t>(range_data - range_data_start);
-		}
-
 		inline uint32_t write_clip_range_data(const clip_context& clip, range_reduction_flags8 range_reduction, uint8_t* range_data, uint32_t range_data_size, const uint32_t* output_bone_mapping, uint32_t num_output_bones)
 		{
 			// Only use the first segment, it contains the necessary information
@@ -193,12 +82,8 @@ namespace acl
 			(void)range_data_size;
 
 			// Data is ordered in groups of 4 animated sub-tracks (e.g rot0, rot1, rot2, rot3)
-			// Order depends on animated track order. If we have 6 animated rotation tracks before the first animated
-			// translation track, we'll have 8 animated rotation sub-tracks followed by 4 animated translation sub-tracks.
-			// Once we reach the end, there is no extra padding. The last group might be less than 4 sub-tracks.
-			// This is because we always process 4 animated sub-tracks at a time and cache the results.
-
-			// Groups are written in the order of first use and as such are sorted by their lowest sub-track index.
+			// Groups are sorted per sub-track type. All rotation groups come first followed by translations then scales.
+			// The last group of each sub-track may or may not have padding. The last group might be less than 4 sub-tracks.
 
 #if defined(ACL_HAS_ASSERT_CHECKS)
 			const uint8_t* range_data_end = add_offset_to_ptr<uint8_t>(range_data, range_data_size);
@@ -327,12 +212,8 @@ namespace acl
 			(void)range_data_size;
 
 			// Data is ordered in groups of 4 animated sub-tracks (e.g rot0, rot1, rot2, rot3)
-			// Order depends on animated track order. If we have 6 animated rotation tracks before the first animated
-			// translation track, we'll have 8 animated rotation sub-tracks followed by 4 animated translation sub-tracks.
-			// Once we reach the end, there is no extra padding. The last group might be less than 4 sub-tracks.
-			// This is because we always process 4 animated sub-tracks at a time and cache the results.
-
-			// Groups are written in the order of first use and as such are sorted by their lowest sub-track index.
+			// Groups are sorted per sub-track type. All rotation groups come first followed by translations then scales.
+			// The last group of each sub-track may or may not have padding. The last group might be less than 4 sub-tracks.
 
 			// normalized value is between [0.0 .. 1.0]
 			// value = (normalized value * range extent) + range min
diff --git a/includes/acl/compression/impl/write_segment_data.h b/includes/acl/compression/impl/write_segment_data.h
--- a/includes/acl/compression/impl/write_segment_data.h
+++ b/includes/acl/compression/impl/write_segment_data.h
@@ -76,6 +76,8 @@ namespace acl
 				ACL_ASSERT(header.animated_pose_bit_size == 0, "Buffer overrun detected");
 
 				header.animated_pose_bit_size = segment.animated_pose_bit_size;
+				header.animated_rotation_bit_size = segment.animated_rotation_bit_size;
+				header.animated_translation_bit_size = segment.animated_translation_bit_size;
 				header.segment_data = segment_data_offset;
 
 				segment_data_offset = align_to(segment_data_offset + format_per_track_data_size, 2);		// Aligned to 2 bytes
diff --git a/includes/acl/compression/impl/write_stats.h b/includes/acl/compression/impl/write_stats.h
--- a/includes/acl/compression/impl/write_stats.h
+++ b/includes/acl/compression/impl/write_stats.h
@@ -344,7 +344,7 @@ namespace acl
 			uint32_t result = 0;
 
 			for (const SegmentContext& segment : clip.segment_iterator())
-				result += align_to(segment.animated_rotation_bit_size, 8) / 8;	// Convert bits to bytes;
+				result += align_to(segment.animated_rotation_bit_size * segment.num_samples, 8) / 8;	// Convert bits to bytes;
 
 			return result;
 		}
@@ -354,7 +354,7 @@ namespace acl
 			uint32_t result = 0;
 
 			for (const SegmentContext& segment : clip.segment_iterator())
-				result += align_to(segment.animated_translation_bit_size, 8) / 8;	// Convert bits to bytes;
+				result += align_to(segment.animated_translation_bit_size * segment.num_samples, 8) / 8;	// Convert bits to bytes;
 
 			return result;
 		}
@@ -364,7 +364,7 @@ namespace acl
 			uint32_t result = 0;
 
 			for (const SegmentContext& segment : clip.segment_iterator())
-				result += align_to(segment.animated_scale_bit_size, 8) / 8;	// Convert bits to bytes;
+				result += align_to(segment.animated_scale_bit_size * segment.num_samples, 8) / 8;	// Convert bits to bytes;
 
 			return result;
 		}
diff --git a/includes/acl/compression/impl/write_stream_bitsets.h b/includes/acl/compression/impl/write_stream_bitsets.h
deleted file mode 100644
--- a/includes/acl/compression/impl/write_stream_bitsets.h
+++ /dev/null
@@ -1,102 +0,0 @@
-#pragma once
-
-////////////////////////////////////////////////////////////////////////////////
-// The MIT License (MIT)
-//
-// Copyright (c) 2017 Nicholas Frechette & Animation Compression Library contributors
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights
-// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in all
-// copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-// SOFTWARE.
-////////////////////////////////////////////////////////////////////////////////
-
-#include "acl/core/error.h"
-#include "acl/core/bitset.h"
-#include "acl/core/impl/compiler_utils.h"
-#include "acl/compression/impl/clip_context.h"
-
-#include <cstdint>
-
-ACL_IMPL_FILE_PRAGMA_PUSH
-
-namespace acl
-{
-	namespace acl_impl
-	{
-		inline uint32_t write_default_track_bitset(const clip_context& clip, uint32_t* default_tracks_bitset, bitset_description bitset_desc, const uint32_t* output_bone_mapping, uint32_t num_output_bones)
-		{
-			ACL_ASSERT(default_tracks_bitset != nullptr, "'default_tracks_bitset' cannot be null!");
-
-			// Only use the first segment, it contains the necessary information
-			const SegmentContext& segment = clip.segments[0];
-
-			uint32_t default_track_offset = 0;
-			uint32_t size_written = 0;
-
-			bitset_reset(default_tracks_bitset, bitset_desc, false);
-
-			for (uint32_t output_index = 0; output_index < num_output_bones; ++output_index)
-			{
-				const uint32_t bone_index = output_bone_mapping[output_index];
-				const BoneStreams& bone_stream = segment.bone_streams[bone_index];
-
-				bitset_set(default_tracks_bitset, bitset_desc, default_track_offset++, bone_stream.is_rotation_default);
-				bitset_set(default_tracks_bitset, bitset_desc, default_track_offset++, bone_stream.is_translation_default);
-
-				if (clip.has_scale)
-					bitset_set(default_tracks_bitset, bitset_desc, default_track_offset++, bone_stream.is_scale_default);
-
-				size_written += clip.has_scale ? 3 : 2;
-			}
-
-			ACL_ASSERT(default_track_offset <= bitset_desc.get_num_bits(), "Too many tracks found for bitset");
-			return ((size_written + 31) / 32) * sizeof(uint32_t);	// Convert bits to bytes
-		}
-
-		inline uint32_t write_constant_track_bitset(const clip_context& clip, uint32_t* constant_tracks_bitset, bitset_description bitset_desc, const uint32_t* output_bone_mapping, uint32_t num_output_bones)
-		{
-			ACL_ASSERT(constant_tracks_bitset != nullptr, "'constant_tracks_bitset' cannot be null!");
-
-			// Only use the first segment, it contains the necessary information
-			const SegmentContext& segment = clip.segments[0];
-
-			uint32_t constant_track_offset = 0;
-			uint32_t size_written = 0;
-
-			bitset_reset(constant_tracks_bitset, bitset_desc, false);
-
-			for (uint32_t output_index = 0; output_index < num_output_bones; ++output_index)
-			{
-				const uint32_t bone_index = output_bone_mapping[output_index];
-				const BoneStreams& bone_stream = segment.bone_streams[bone_index];
-
-				bitset_set(constant_tracks_bitset, bitset_desc, constant_track_offset++, bone_stream.is_rotation_constant);
-				bitset_set(constant_tracks_bitset, bitset_desc, constant_track_offset++, bone_stream.is_translation_constant);
-
-				if (clip.has_scale)
-					bitset_set(constant_tracks_bitset, bitset_desc, constant_track_offset++, bone_stream.is_scale_constant);
-
-				size_written += clip.has_scale ? 3 : 2;
-			}
-
-			ACL_ASSERT(constant_track_offset <= bitset_desc.get_num_bits(), "Too many tracks found for bitset");
-			return ((size_written + 31) / 32) * sizeof(uint32_t);	// Convert bits to bytes
-		}
-	}
-}
-
-ACL_IMPL_FILE_PRAGMA_POP
diff --git a/includes/acl/compression/impl/write_stream_data.h b/includes/acl/compression/impl/write_stream_data.h
--- a/includes/acl/compression/impl/write_stream_data.h
+++ b/includes/acl/compression/impl/write_stream_data.h
@@ -141,13 +141,12 @@ namespace acl
 						calculate_animated_data_size(bone_stream.scales, num_animated_pose_scale_data_bits);
 				}
 
-				const uint32_t num_samples = segment.num_samples;
 				const uint32_t num_animated_pose_bits = num_animated_pose_rotation_data_bits + num_animated_pose_translation_data_bits + num_animated_pose_scale_data_bits;
-				const uint32_t num_animated_data_bits = num_animated_pose_bits * num_samples;
+				const uint32_t num_animated_data_bits = num_animated_pose_bits * segment.num_samples;
 
-				segment.animated_rotation_bit_size = num_animated_pose_rotation_data_bits * num_samples;
-				segment.animated_translation_bit_size = num_animated_pose_translation_data_bits * num_samples;
-				segment.animated_scale_bit_size = num_animated_pose_scale_data_bits * num_samples;
+				segment.animated_rotation_bit_size = num_animated_pose_rotation_data_bits;
+				segment.animated_translation_bit_size = num_animated_pose_translation_data_bits;
+				segment.animated_scale_bit_size = num_animated_pose_scale_data_bits;
 				segment.animated_data_size = align_to(num_animated_data_bits, 8) / 8;
 				segment.animated_pose_bit_size = num_animated_pose_bits;
 			}
@@ -208,86 +207,6 @@ namespace acl
 
 			const uint8_t* constant_data_start = constant_data;
 
-#if defined(ACL_IMPL_USE_CONSTANT_GROUPS)
-			// Data is ordered in groups of 4 constant sub-tracks (e.g rot0, rot1, rot2, rot3)
-			// Order depends on animated track order. If we have 6 constant rotation tracks before the first constant
-			// translation track, we'll have 8 constant rotation sub-tracks followed by 4 constant translation sub-tracks.
-			// Once we reach the end, there is no extra padding. The last group might be less than 4 sub-tracks.
-			// This is because we always process 4 constant sub-tracks at a time and cache the results.
-
-			// Groups are written in the order of first use and as such are sorted by their lowest sub-track index.
-
-			// If our rotation format drops the W component, we swizzle the data to store XXXX, YYYY, ZZZZ
-			const bool swizzle_rotations = get_rotation_variant(rotation_format) == rotation_variant8::quat_drop_w;
-
-			float xxxx_group[4];
-			float yyyy_group[4];
-			float zzzz_group[4];
-			rtm::vector4f constant_group4[4];
-			rtm::float3f constant_group3[4];
-
-			auto group_entry_action = [&](animation_track_type8 group_type, uint32_t group_size, uint32_t bone_index)
-			{
-				const BoneStreams& bone_stream = segment.bone_streams[bone_index];
-
-				if (group_type == animation_track_type8::rotation)
-				{
-					if (swizzle_rotations)
-					{
-						const rtm::vector4f sample = bone_stream.rotations.get_raw_sample<rtm::vector4f>(0);
-						xxxx_group[group_size] = rtm::vector_get_x(sample);
-						yyyy_group[group_size] = rtm::vector_get_y(sample);
-						zzzz_group[group_size] = rtm::vector_get_z(sample);
-					}
-					else
-					{
-						const rtm::vector4f sample = bone_stream.rotations.get_raw_sample<rtm::vector4f>(0);
-						constant_group4[group_size] = sample;
-					}
-				}
-				else if (group_type == animation_track_type8::translation)
-				{
-					const rtm::vector4f sample = bone_stream.translations.get_raw_sample<rtm::vector4f>(0);
-					rtm::vector_store3(sample, &constant_group3[group_size]);
-				}
-				else
-				{
-					const rtm::vector4f sample = bone_stream.scales.get_raw_sample<rtm::vector4f>(0);
-					rtm::vector_store3(sample, &constant_group3[group_size]);
-				}
-			};
-
-			auto group_flush_action = [&](animation_track_type8 group_type, uint32_t group_size)
-			{
-				if (group_type == animation_track_type8::rotation)
-				{
-					if (swizzle_rotations)
-					{
-						std::memcpy(constant_data, &xxxx_group[0], group_size * sizeof(float));
-						constant_data += group_size * sizeof(float);
-						std::memcpy(constant_data, &yyyy_group[0], group_size * sizeof(float));
-						constant_data += group_size * sizeof(float);
-						std::memcpy(constant_data, &zzzz_group[0], group_size * sizeof(float));
-						constant_data += group_size * sizeof(float);
-					}
-					else
-					{
-						// If we don't swizzle, we have a full quaternion
-						std::memcpy(constant_data, &constant_group4[0], group_size * sizeof(rtm::vector4f));
-						constant_data += group_size * sizeof(rtm::vector4f);
-					}
-				}
-				else
-				{
-					std::memcpy(constant_data, &constant_group3[0], group_size * sizeof(rtm::float3f));
-					constant_data += group_size * sizeof(rtm::float3f);
-				}
-
-				ACL_ASSERT(constant_data <= constant_data_end, "Invalid constant data offset. Wrote too much data.");
-			};
-
-			constant_group_writer(segment, output_bone_mapping, num_output_bones, group_entry_action, group_flush_action);
-#else
 			// If our rotation format drops the W component, we swizzle the data to store XXXX, YYYY, ZZZZ
 			const bool swizzle_rotations = get_rotation_variant(rotation_format) == rotation_variant8::quat_drop_w;
 			float xxxx[4];
@@ -382,13 +301,12 @@ namespace acl
 					}
 				}
 			}
-#endif
 
 			ACL_ASSERT(constant_data == constant_data_end, "Invalid constant data offset. Wrote too little data.");
 			return safe_static_cast<uint32_t>(constant_data - constant_data_start);
 		}
 
-		inline void write_animated_track_data(const TrackStream& track_stream, uint32_t sample_index, uint8_t* animated_track_data_begin, uint8_t*& out_animated_track_data, uint64_t& out_bit_offset)
+		inline void write_animated_sample(const TrackStream& track_stream, uint32_t sample_index, uint8_t* animated_track_data_begin, uint64_t& out_bit_offset)
 		{
 			const uint8_t* raw_sample_ptr = track_stream.get_raw_sample_ptr(sample_index);
 
@@ -418,7 +336,6 @@ namespace acl
 				}
 
 				out_bit_offset += num_bits_at_bit_rate;
-				out_animated_track_data = animated_track_data_begin + (out_bit_offset / 8);
 			}
 			else
 			{
@@ -440,7 +357,6 @@ namespace acl
 				}
 
 				out_bit_offset += has_w_component ? 128 : 96;
-				out_animated_track_data = animated_track_data_begin + (out_bit_offset / 8);
 			}
 		}
 
@@ -457,26 +373,19 @@ namespace acl
 
 			const uint8_t* animated_track_data_start = animated_track_data;
 			uint64_t bit_offset = 0;
-			uint32_t num_samples = 0;
 
 			// Data is sorted first by time, second by bone.
 			// This ensures that all bones are contiguous in memory when we sample a particular time.
 
 			// Data is ordered in groups of 4 animated sub-tracks (e.g rot0, rot1, rot2, rot3)
-			// Order depends on animated track order. If we have 6 animated rotation tracks before the first animated
-			// translation track, we'll have 8 animated rotation sub-tracks followed by 4 animated translation sub-tracks.
-			// Once we reach the end, there is no extra padding. The last group might be less than 4 sub-tracks.
-			// This is because we always process 4 animated sub-tracks at a time and cache the results.
-
-			// Groups are written in the order of first use and as such are sorted by their lowest sub-track index.
+			// Groups are sorted per sub-track type. All rotation groups come first followed by translations then scales.
+			// The last group of each sub-track may or may not have padding. The last group might be less than 4 sub-tracks.
 
 			// For animated samples, when we have a constant bit rate (bit rate 0), we do not store samples
 			// and as such the group that contains that sub-track won't contain 4 samples.
 			// The largest sample is a full precision vector4f, we can contain at most 4 samples
 			alignas(16) uint8_t group_animated_track_data[sizeof(rtm::vector4f) * 4];
 			uint64_t group_bit_offset = 0;
-			uint32_t num_group_samples = 0;
-			uint8_t* dummy_animated_track_data_ptr = nullptr;
 
 			auto group_filter_action = [&](animation_track_type8 group_type, uint32_t bone_index)
 			{
@@ -491,15 +400,12 @@ namespace acl
 			auto group_flush_action = [&](animation_track_type8 group_type, uint32_t group_size)
 			{
 				(void)group_type;
-
-				if (group_size == 0)
-					return;	// Empty group, skip
+				(void)group_size;
 
 				memcpy_bits(animated_track_data_begin, bit_offset, &group_animated_track_data[0], 0, group_bit_offset);
 
 				bit_offset += group_bit_offset;
 				group_bit_offset = 0;
-				num_group_samples = 0;
 
 				animated_track_data = animated_track_data_begin + (bit_offset / 8);
 
@@ -509,8 +415,6 @@ namespace acl
 			// TODO: Use a group writer context object to avoid alloc/free/work in loop for every sample when it doesn't change
 			for (uint32_t sample_index = 0; sample_index < segment.num_samples; ++sample_index)
 			{
-				num_samples++;
-
 				auto group_entry_action = [&](animation_track_type8 group_type, uint32_t group_size, uint32_t bone_index)
 				{
 					(void)group_size;
@@ -519,26 +423,17 @@ namespace acl
 					if (group_type == animation_track_type8::rotation)
 					{
 						if (!is_constant_bit_rate(bone_stream.rotations.get_bit_rate()))
-						{
-							write_animated_track_data(bone_stream.rotations, sample_index, group_animated_track_data, dummy_animated_track_data_ptr, group_bit_offset);
-							num_group_samples++;
-						}
+							write_animated_sample(bone_stream.rotations, sample_index, group_animated_track_data, group_bit_offset);
 					}
 					else if (group_type == animation_track_type8::translation)
 					{
 						if (!is_constant_bit_rate(bone_stream.translations.get_bit_rate()))
-						{
-							write_animated_track_data(bone_stream.translations, sample_index, group_animated_track_data, dummy_animated_track_data_ptr, group_bit_offset);
-							num_group_samples++;
-						}
+							write_animated_sample(bone_stream.translations, sample_index, group_animated_track_data, group_bit_offset);
 					}
 					else
 					{
 						if (!is_constant_bit_rate(bone_stream.scales.get_bit_rate()))
-						{
-							write_animated_track_data(bone_stream.scales, sample_index, group_animated_track_data, dummy_animated_track_data_ptr, group_bit_offset);
-							num_group_samples++;
-						}
+							write_animated_sample(bone_stream.scales, sample_index, group_animated_track_data, group_bit_offset);
 					}
 				};
 
@@ -548,7 +443,7 @@ namespace acl
 			if (bit_offset != 0)
 				animated_track_data = animated_track_data_begin + ((bit_offset + 7) / 8);
 
-			ACL_ASSERT((bit_offset == 0 && num_samples == 0) || ((bit_offset / num_samples) == segment.animated_pose_bit_size), "Unexpected number of bits written");
+			ACL_ASSERT((bit_offset == 0 && segment.num_samples == 0) || ((bit_offset / segment.num_samples) == segment.animated_pose_bit_size), "Unexpected number of bits written");
 			ACL_ASSERT(animated_track_data == animated_track_data_end, "Invalid animated track data offset. Wrote too little data.");
 			return safe_static_cast<uint32_t>(animated_track_data - animated_track_data_start);
 		}
@@ -565,12 +460,8 @@ namespace acl
 			const uint8_t* format_per_track_data_start = format_per_track_data;
 
 			// Data is ordered in groups of 4 animated sub-tracks (e.g rot0, rot1, rot2, rot3)
-			// Order depends on animated track order. If we have 6 animated rotation tracks before the first animated
-			// translation track, we'll have 8 animated rotation sub-tracks followed by 4 animated translation sub-tracks.
-			// Once we reach the end, there is no extra padding. The last group might be less than 4 sub-tracks.
-			// This is because we always process 4 animated sub-tracks at a time and cache the results.
-
-			// Groups are written in the order of first use and as such are sorted by their lowest sub-track index.
+			// Groups are sorted per sub-track type. All rotation groups come first followed by translations then scales.
+			// The last group of each sub-track may or may not have padding. The last group might be less than 4 sub-tracks.
 
 			// To keep decompression simpler, rotations are padded to 4 elements even if the last group is partial
 			uint8_t format_per_track_group[4];
@@ -615,21 +506,6 @@ namespace acl
 
 			return safe_static_cast<uint32_t>(format_per_track_data - format_per_track_data_start);
 		}
-
-		inline uint32_t write_animated_group_types(const animation_track_type8* animated_sub_track_groups, uint32_t num_animated_groups, animation_track_type8* animated_sub_track_groups_data, uint32_t animated_sub_track_groups_data_size)
-		{
-			(void)animated_sub_track_groups_data_size;
-
-			const animation_track_type8* animated_sub_track_groups_data_start = animated_sub_track_groups_data;
-
-			std::memcpy(animated_sub_track_groups_data, animated_sub_track_groups, sizeof(animation_track_type8) * num_animated_groups);
-			animated_sub_track_groups_data += num_animated_groups;
-			animated_sub_track_groups_data[0] = static_cast<animation_track_type8>(0xFF);	// Terminator
-			animated_sub_track_groups_data++;
-
-			ACL_ASSERT(animated_sub_track_groups_data == animated_sub_track_groups_data_start + animated_sub_track_groups_data_size, "Too little or too much data written");
-			return static_cast<uint32_t>(animated_sub_track_groups_data - animated_sub_track_groups_data_start);
-		}
 	}
 }
 
diff --git a/includes/acl/compression/impl/write_sub_track_types.h b/includes/acl/compression/impl/write_sub_track_types.h
new file mode 100644
--- /dev/null
+++ b/includes/acl/compression/impl/write_sub_track_types.h
@@ -0,0 +1,164 @@
+#pragma once
+
+////////////////////////////////////////////////////////////////////////////////
+// The MIT License (MIT)
+//
+// Copyright (c) 2017 Nicholas Frechette & Animation Compression Library contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+////////////////////////////////////////////////////////////////////////////////
+
+#include "acl/core/error.h"
+#include "acl/core/impl/compiler_utils.h"
+#include "acl/core/impl/compressed_headers.h"
+#include "acl/compression/impl/clip_context.h"
+
+#include <cstdint>
+
+ACL_IMPL_FILE_PRAGMA_PUSH
+
+namespace acl
+{
+	namespace acl_impl
+	{
+		inline uint32_t write_packed_sub_track_types(const clip_context& clip, packed_sub_track_types* out_packed_types, const uint32_t* output_bone_mapping, uint32_t num_output_bones)
+		{
+			ACL_ASSERT(out_packed_types != nullptr, "'out_packed_types' cannot be null!");
+
+			// Only use the first segment, it contains the necessary information
+			const SegmentContext& segment = clip.segments[0];
+
+			uint32_t packed_entry_index = 0;
+			uint32_t packed_entry_size = 0;
+			uint32_t packed_entry = 0;
+
+			// Write rotations
+			for (uint32_t output_index = 0; output_index < num_output_bones; ++output_index)
+			{
+				if (packed_entry_size == 16)
+				{
+					// We are starting a new entry, write our old one out
+					out_packed_types[packed_entry_index++] = packed_sub_track_types{ packed_entry };
+					packed_entry_size = 0;
+					packed_entry = 0;
+				}
+
+				const uint32_t bone_index = output_bone_mapping[output_index];
+				const BoneStreams& bone_stream = segment.bone_streams[bone_index];
+
+				uint32_t packed_type;
+				if (bone_stream.is_rotation_default)
+					packed_type = 0;	// Default
+				else if (bone_stream.is_rotation_constant)
+					packed_type = 1;	// Constant
+				else
+					packed_type = 2;	// Animated
+
+				const uint32_t packed_index = output_index % 16;
+				packed_entry |= packed_type << ((15 - packed_index) * 2);
+				packed_entry_size++;
+			}
+
+			if (packed_entry_size != 0)
+			{
+				// We have a partial entry, write it out
+				out_packed_types[packed_entry_index++] = packed_sub_track_types{ packed_entry };
+				packed_entry_size = 0;
+				packed_entry = 0;
+			}
+
+			// Write translations
+			for (uint32_t output_index = 0; output_index < num_output_bones; ++output_index)
+			{
+				if (packed_entry_size == 16)
+				{
+					// We are starting a new entry, write our old one out
+					out_packed_types[packed_entry_index++] = packed_sub_track_types{ packed_entry };
+					packed_entry_size = 0;
+					packed_entry = 0;
+				}
+
+				const uint32_t bone_index = output_bone_mapping[output_index];
+				const BoneStreams& bone_stream = segment.bone_streams[bone_index];
+
+				uint32_t packed_type;
+				if (bone_stream.is_translation_default)
+					packed_type = 0;	// Default
+				else if (bone_stream.is_translation_constant)
+					packed_type = 1;	// Constant
+				else
+					packed_type = 2;	// Animated
+
+				const uint32_t packed_index = output_index % 16;
+				packed_entry |= packed_type << ((15 - packed_index) * 2);
+				packed_entry_size++;
+			}
+
+			if (packed_entry_size != 0)
+			{
+				// We have a partial entry, write it out
+				out_packed_types[packed_entry_index++] = packed_sub_track_types{ packed_entry };
+				packed_entry_size = 0;
+				packed_entry = 0;
+			}
+
+			if (clip.has_scale)
+			{
+				// Write translations
+				for (uint32_t output_index = 0; output_index < num_output_bones; ++output_index)
+				{
+					if (packed_entry_size == 16)
+					{
+						// We are starting a new entry, write our old one out
+						out_packed_types[packed_entry_index++] = packed_sub_track_types{ packed_entry };
+						packed_entry_size = 0;
+						packed_entry = 0;
+					}
+
+					const uint32_t bone_index = output_bone_mapping[output_index];
+					const BoneStreams& bone_stream = segment.bone_streams[bone_index];
+
+					uint32_t packed_type;
+					if (bone_stream.is_scale_default)
+						packed_type = 0;	// Default
+					else if (bone_stream.is_scale_constant)
+						packed_type = 1;	// Constant
+					else
+						packed_type = 2;	// Animated
+
+					const uint32_t packed_index = output_index % 16;
+					packed_entry |= packed_type << ((15 - packed_index) * 2);
+					packed_entry_size++;
+				}
+
+				if (packed_entry_size != 0)
+				{
+					// We have a partial entry, write it out
+					out_packed_types[packed_entry_index++] = packed_sub_track_types{ packed_entry };
+					packed_entry_size = 0;
+					packed_entry = 0;
+				}
+			}
+
+			return packed_entry_index * sizeof(packed_sub_track_types);
+		}
+	}
+}
+
+ACL_IMPL_FILE_PRAGMA_POP
diff --git a/includes/acl/core/error.h b/includes/acl/core/error.h
--- a/includes/acl/core/error.h
+++ b/includes/acl/core/error.h
@@ -45,7 +45,7 @@ ACL_IMPL_FILE_PRAGMA_PUSH
 // Throwing:
 //    In order to enable the throwing behavior, simply define the macro ACL_ON_ASSERT_THROW:
 //    #define ACL_ON_ASSERT_THROW
-//    Note that the type of the exception thrown is std::runtime_error.
+//    Note that the type of the exception thrown is acl::runtime_assert.
 //
 // Custom function:
 //    In order to enable the custom function calling behavior, define the macro ACL_ON_ASSERT_CUSTOM
diff --git a/includes/acl/core/impl/compressed_headers.h b/includes/acl/core/impl/compressed_headers.h
--- a/includes/acl/core/impl/compressed_headers.h
+++ b/includes/acl/core/impl/compressed_headers.h
@@ -36,9 +36,6 @@
 #include <atomic>
 #include <cstdint>
 
-// This is a bit slower because of the added bookkeeping when we unpack
-//#define ACL_IMPL_USE_CONSTANT_GROUPS
-
 ACL_IMPL_FILE_PRAGMA_PUSH
 
 namespace acl
@@ -160,6 +157,10 @@ namespace acl
 			// Number of bits used by a fully animated pose (excludes default/constant tracks).
 			uint32_t						animated_pose_bit_size;
 
+			// Number of bits used by a fully animated pose per sub-track type (excludes default/constant tracks).
+			uint32_t						animated_rotation_bit_size;
+			uint32_t						animated_translation_bit_size;
+
 			// Offset to the animated segment data, relative to the start of the transform_tracks_header
 			// Segment data is partitioned as follows:
 			//    - format per variable track (no alignment)
@@ -180,6 +181,10 @@ namespace acl
 			// Number of bits used by a fully animated pose (excludes default/constant tracks).
 			uint32_t						animated_pose_bit_size;
 
+			// Number of bits used by a fully animated pose per sub-track type (excludes default/constant tracks).
+			uint32_t						animated_rotation_bit_size;
+			uint32_t						animated_translation_bit_size;
+
 			// Offset to the animated segment data, relative to the start of the transform_tracks_header
 			// Segment data is partitioned as follows:
 			//    - format per variable track (no alignment)
@@ -224,6 +229,18 @@ namespace acl
 			const database_runtime_clip_header*				get_clip_header(const void* base) const { return clip_header_offset.add_to(base); }
 		};
 
+		//////////////////////////////////////////////////////////////////////////
+		// A 32 bit integer that contains packed sub-track types.
+		// Each sub-track type is packed on 2 bits starting with the MSB.
+		// 0 = default/padding, 1 = constant, 2 = animated
+		//////////////////////////////////////////////////////////////////////////
+		struct packed_sub_track_types
+		{
+			uint32_t types;
+		};
+
+		const uint32_t k_num_sub_tracks_per_packed_entry = 16;	// 2 bits each within a 32 bit entry
+
 		// Header for transform 'compressed_tracks'
 		struct transform_tracks_header
 		{
@@ -234,12 +251,12 @@ namespace acl
 			uint32_t						num_animated_variable_sub_tracks;		// Might be padded with dummy tracks for alignment
 			uint32_t						num_animated_rotation_sub_tracks;
 			uint32_t						num_animated_translation_sub_tracks;
-			uint32_t						num_animated_scale_sub_tracks;			// TODO: Not needed?
+			uint32_t						num_animated_scale_sub_tracks;
 
 			// The number of constant sub-track samples stored, does not include default samples
 			uint32_t						num_constant_rotation_samples;
 			uint32_t						num_constant_translation_samples;
-			uint32_t						num_constant_scale_samples;				// TODO: Not needed?
+			uint32_t						num_constant_scale_samples;
 
 			// Offset to the database metadata header.
 			ptr_offset32<tracks_database_header>	database_header_offset;
@@ -251,18 +268,14 @@ namespace acl
 				ptr_offset32<segment_tier0_header>	segment_tier0_headers_offset;
 			};
 
-			// Offsets to the default/constant tracks bitsets.
-			ptr_offset32<uint32_t>			default_tracks_bitset_offset;
-			ptr_offset32<uint32_t>			constant_tracks_bitset_offset;
+			// Offset to the packed sub-track types.
+			ptr_offset32<packed_sub_track_types>	sub_track_types_offset;
 
 			// Offset to the constant tracks data.
 			ptr_offset32<uint8_t>			constant_track_data_offset;
 
 			// Offset to the clip range data.
-			ptr_offset32<uint8_t>			clip_range_data_offset;					// TODO: Make this offset optional? Only present if normalized
-
-			// Offset to the animated group types. Ends with an invalid group type of 0xFF.
-			ptr_offset32<animation_track_type8>	animated_group_types_offset;
+			ptr_offset32<uint8_t>			clip_range_data_offset;					// TODO: Make this offset optional? Only present if using variable bit rate
 
 			//////////////////////////////////////////////////////////////////////////
 
@@ -283,14 +296,8 @@ namespace acl
 			segment_tier0_header*			get_segment_tier0_headers() { return segment_tier0_headers_offset.add_to(this); }
 			const segment_tier0_header*		get_segment_tier0_headers() const { return segment_tier0_headers_offset.add_to(this); }
 
-			animation_track_type8*			get_animated_group_types() { return animated_group_types_offset.add_to(this); }
-			const animation_track_type8*	get_animated_group_types() const { return animated_group_types_offset.add_to(this); }
-
-			uint32_t*						get_default_tracks_bitset() { return default_tracks_bitset_offset.add_to(this); }
-			const uint32_t*					get_default_tracks_bitset() const { return default_tracks_bitset_offset.add_to(this); }
-
-			uint32_t*						get_constant_tracks_bitset() { return constant_tracks_bitset_offset.add_to(this); }
-			const uint32_t*					get_constant_tracks_bitset() const { return constant_tracks_bitset_offset.add_to(this); }
+			packed_sub_track_types*			get_sub_track_types() { return sub_track_types_offset.add_to(this); }
+			const packed_sub_track_types*	get_sub_track_types() const { return sub_track_types_offset.add_to(this); }
 
 			uint8_t*						get_constant_track_data() { return constant_track_data_offset.add_to(this); }
 			const uint8_t*					get_constant_track_data() const { return constant_track_data_offset.add_to(this); }
@@ -391,8 +398,8 @@ namespace acl
 			std::atomic<uint64_t>			tier_metadata[2];
 		};
 
-		// Header for runtime database clips
-		struct database_runtime_clip_header
+		// Header for runtime database clips, 8 byte alignment to match database_runtime_segment_header
+		struct alignas(8) database_runtime_clip_header
 		{
 			// Hash of the compressed clip stored in this entry
 			uint32_t						clip_hash;
diff --git a/includes/acl/core/memory_utils.h b/includes/acl/core/memory_utils.h
--- a/includes/acl/core/memory_utils.h
+++ b/includes/acl/core/memory_utils.h
@@ -288,6 +288,9 @@ namespace acl
 	// We copy bits assuming big-endian ordering for 'dest' and 'src'
 	inline void memcpy_bits(void* dest, uint64_t dest_bit_offset, const void* src, uint64_t src_bit_offset, uint64_t num_bits_to_copy)
 	{
+		if (num_bits_to_copy == 0)
+			return;	// Nothing to copy
+
 		while (true)
 		{
 			uint64_t src_byte_offset = src_bit_offset / 8;
diff --git a/includes/acl/decompression/database/impl/database.impl.h b/includes/acl/decompression/database/impl/database.impl.h
--- a/includes/acl/decompression/database/impl/database.impl.h
+++ b/includes/acl/decompression/database/impl/database.impl.h
@@ -53,10 +53,11 @@ namespace acl
 			const uint32_t low_bitset_size = low_desc.get_num_bytes();
 
 			uint32_t runtime_data_size = 0;
-			runtime_data_size += medium_bitset_size;	// Loaded chunks
-			runtime_data_size += medium_bitset_size;	// Streaming chunks
-			runtime_data_size += low_bitset_size;		// Loaded chunks
-			runtime_data_size += low_bitset_size;		// Streaming chunks
+			runtime_data_size += medium_bitset_size;			// Loaded chunks
+			runtime_data_size += medium_bitset_size;			// Streaming chunks
+			runtime_data_size += low_bitset_size;				// Loaded chunks
+			runtime_data_size += low_bitset_size;				// Streaming chunks
+			runtime_data_size = align_to(runtime_data_size, 8);	// Align runtime headers
 			runtime_data_size += num_clips * sizeof(database_runtime_clip_header);
 			runtime_data_size += num_segments * sizeof(database_runtime_segment_header);
 
@@ -122,7 +123,7 @@ namespace acl
 		// Allocate a single buffer for everything we need. This is faster to allocate and it ensures better virtual
 		// memory locality which should help reduce the cost of TLB misses.
 		const uint32_t runtime_data_size = acl_impl::calculate_runtime_data_size(database);
-		uint8_t* runtime_data_buffer = allocate_type_array<uint8_t>(allocator, runtime_data_size);
+		uint8_t* runtime_data_buffer = allocate_type_array_aligned<uint8_t>(allocator, runtime_data_size, 16);
 
 		// Initialize everything to 0
 		std::memset(runtime_data_buffer, 0, runtime_data_size);
@@ -139,7 +140,7 @@ namespace acl
 		m_context.streaming_chunks[1] = reinterpret_cast<uint32_t*>(runtime_data_buffer);
 		runtime_data_buffer += low_bitset_size;
 
-		m_context.clip_segment_headers = runtime_data_buffer;
+		m_context.clip_segment_headers = align_to(runtime_data_buffer, 8);	// Align runtime headers
 
 		// Copy our clip hashes to setup our headers
 		const uint32_t num_clips = header.num_clips;
@@ -250,7 +251,7 @@ namespace acl
 		// Allocate a single buffer for everything we need. This is faster to allocate and it ensures better virtual
 		// memory locality which should help reduce the cost of TLB misses.
 		const uint32_t runtime_data_size = acl_impl::calculate_runtime_data_size(database);
-		uint8_t* runtime_data_buffer = allocate_type_array<uint8_t>(allocator, runtime_data_size);
+		uint8_t* runtime_data_buffer = allocate_type_array_aligned<uint8_t>(allocator, runtime_data_size, 16);
 
 		// Initialize everything to 0
 		std::memset(runtime_data_buffer, 0, runtime_data_size);
@@ -267,7 +268,7 @@ namespace acl
 		m_context.streaming_chunks[1] = reinterpret_cast<uint32_t*>(runtime_data_buffer);
 		runtime_data_buffer += low_bitset_size;
 
-		m_context.clip_segment_headers = runtime_data_buffer;
+		m_context.clip_segment_headers = align_to(runtime_data_buffer, 8);	// Align runtime headers
 
 		// Copy our clip hashes to setup our headers
 		const uint32_t num_clips = header.num_clips;
diff --git a/includes/acl/decompression/impl/transform_animated_track_cache.h b/includes/acl/decompression/impl/transform_animated_track_cache.h
--- a/includes/acl/decompression/impl/transform_animated_track_cache.h
+++ b/includes/acl/decompression/impl/transform_animated_track_cache.h
@@ -38,10 +38,46 @@
 #include <cstdint>
 
 #define ACL_IMPL_USE_ANIMATED_PREFETCH
-//#define ACL_IMPL_USE_AVX_DECOMP
+
+// On x86/x64 platforms the prefetching instruction can have a long latency and it requires
+// a few other registers to compute the address which is problematic when registers are scarce.
+// As such, we attempt to hide the prefetching behind longer latency instructions like square-roots
+// and divisions.
+// On other platforms (e.g. ARM), the instruction is cheaper and we have more registers which gives
+// the compiler more freedom to hide the address calculation cost between other instructions.
+// Because the CPU is generally slower as well, we want to prefetch as soon as possible without
+// waiting for the next expensive instruction.
+// If your target CPU has a high clock rate, you might benefit from disabling early prefetching
+#if !defined(ACL_NO_EARLY_PREFETCHING) && !defined(ACL_IMPL_PREFETCH_EARLY)
+	#if !defined(RTM_SSE2_INTRINSICS)
+		#define ACL_IMPL_PREFETCH_EARLY
+	#endif
+#endif
+
+// This defined enables the SIMD 8 wide AVX decompression code path
+// Note that currently, it is often slower than the regular SIMD 4 wide AVX code path
+// On Intel Haswell and AMD Zen2 CPUs, the 8 wide code is measurably slower
+// Perhaps it is faster on newer Intel CPUs but I don't have one to test with
+// Enable at your own risk
+//#define ACL_IMPL_USE_AVX_8_WIDE_DECOMP
+
+#if defined(ACL_IMPL_USE_AVX_8_WIDE_DECOMP)
+	#if !defined(RTM_AVX_INTRINSICS)
+		// AVX isn't enabled, disable the 8 wide code path
+		#undef ACL_IMPL_USE_AVX_8_WIDE_DECOMP
+	#endif
+#endif
 
 ACL_IMPL_FILE_PRAGMA_PUSH
 
+// We only initialize some variables when we need them which prompts the compiler to complain
+// The usage is perfectly safe and because this code is VERY hot and needs to be as fast as possible,
+// we disable the warning to avoid zeroing out things we don't need
+#if defined(ACL_COMPILER_GCC)
+	#pragma GCC diagnostic push
+	#pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
+#endif
+
 namespace acl
 {
 	namespace acl_impl
@@ -81,15 +117,479 @@ namespace acl
 			uint32_t animated_track_data_bit_offset;	// Bit offset of the current animated sub-track
 		};
 
-		struct animated_group_cursor_v0
+		struct alignas(32) segment_animated_scratch_v0
 		{
-			clip_animated_sampling_context_v0 clip_sampling_context;
-			segment_animated_sampling_context_v0 segment_sampling_context[2];
-			uint32_t group_size;
+			// We store out potential range data in SOA form and we have no W, just XYZ
+			// To facilitate AVX and wider SIMD usage, we store our data interleaved in a single contiguous array
+			// Segment 0 has a base offset of 0 bytes and afterwards every write has a 32 byte offset
+			// Segment 1 has a base offset of 16 bytes and afterwards every write has a 32 byte offset
+
+			// segment_range_min_xxxx0, segment_range_min_xxxx1, segment_range_min_yyyy0, segment_range_min_yyyy1, segment_range_min_zzzz0, segment_range_min_zzzz1
+			rtm::vector4f segment_range_min[6];
+
+			// segment_range_extent_xxxx0, segment_range_extent_xxxx1, segment_range_extent_yyyy0, segment_range_extent_yyyy1, segment_range_extent_zzzz0, segment_range_extent_zzzz1
+			rtm::vector4f segment_range_extent[6];
 		};
 
+#if defined(RTM_SSE2_INTRINSICS)
+		using range_reduction_masks_t = __m128i;
+#elif defined(RTM_NEON_INTRINSICS)
+		using range_reduction_masks_t = int16x8_t;
+#else
+		using range_reduction_masks_t = uint64_t;
+#endif
+
+		// About 9 cycles with AVX on Skylake
+		inline ACL_DISABLE_SECURITY_COOKIE_CHECK void unpack_segment_range_data(const uint8_t* segment_range_data, uint32_t scratch_offset, segment_animated_scratch_v0& output_scratch)
+		{
+			// Segment range is packed: min.xxxx, min.yyyy, min.zzzz, extent.xxxx, extent.yyyy, extent.zzzz
+
+#if defined(RTM_SSE2_INTRINSICS)
+			const __m128i zero = _mm_setzero_si128();
+
+			const __m128i segment_range_min_xxxx_yyyy_zzzz_extent_xxxx_u8 = _mm_loadu_si128((const __m128i*)segment_range_data);
+			const __m128i segment_range_extent_yyyy_zzzz_u8 = _mm_loadu_si128((const __m128i*)(segment_range_data + 16));
+
+			// Convert from u8 to u32
+			const __m128i segment_range_min_xxxx_yyyy_u16 = _mm_unpacklo_epi8(segment_range_min_xxxx_yyyy_zzzz_extent_xxxx_u8, zero);
+			const __m128i segment_range_min_zzzz_extent_xxxx_u16 = _mm_unpackhi_epi8(segment_range_min_xxxx_yyyy_zzzz_extent_xxxx_u8, zero);
+			const __m128i segment_range_extent_yyyy_zzzz_u16 = _mm_unpacklo_epi8(segment_range_extent_yyyy_zzzz_u8, zero);
+
+			__m128i segment_range_min_xxxx_u32 = _mm_unpacklo_epi16(segment_range_min_xxxx_yyyy_u16, zero);
+			__m128i segment_range_min_yyyy_u32 = _mm_unpackhi_epi16(segment_range_min_xxxx_yyyy_u16, zero);
+			__m128i segment_range_min_zzzz_u32 = _mm_unpacklo_epi16(segment_range_min_zzzz_extent_xxxx_u16, zero);
+
+			const __m128i segment_range_extent_xxxx_u32 = _mm_unpackhi_epi16(segment_range_min_zzzz_extent_xxxx_u16, zero);
+			const __m128i segment_range_extent_yyyy_u32 = _mm_unpacklo_epi16(segment_range_extent_yyyy_zzzz_u16, zero);
+			const __m128i segment_range_extent_zzzz_u32 = _mm_unpackhi_epi16(segment_range_extent_yyyy_zzzz_u16, zero);
+
+			__m128 segment_range_min_xxxx = _mm_cvtepi32_ps(segment_range_min_xxxx_u32);
+			__m128 segment_range_min_yyyy = _mm_cvtepi32_ps(segment_range_min_yyyy_u32);
+			__m128 segment_range_min_zzzz = _mm_cvtepi32_ps(segment_range_min_zzzz_u32);
+
+			__m128 segment_range_extent_xxxx = _mm_cvtepi32_ps(segment_range_extent_xxxx_u32);
+			__m128 segment_range_extent_yyyy = _mm_cvtepi32_ps(segment_range_extent_yyyy_u32);
+			__m128 segment_range_extent_zzzz = _mm_cvtepi32_ps(segment_range_extent_zzzz_u32);
+
+			const __m128 normalization_value = _mm_set_ps1(1.0F / 255.0F);
+
+			segment_range_min_xxxx = _mm_mul_ps(segment_range_min_xxxx, normalization_value);
+			segment_range_min_yyyy = _mm_mul_ps(segment_range_min_yyyy, normalization_value);
+			segment_range_min_zzzz = _mm_mul_ps(segment_range_min_zzzz, normalization_value);
+
+			segment_range_extent_xxxx = _mm_mul_ps(segment_range_extent_xxxx, normalization_value);
+			segment_range_extent_yyyy = _mm_mul_ps(segment_range_extent_yyyy, normalization_value);
+			segment_range_extent_zzzz = _mm_mul_ps(segment_range_extent_zzzz, normalization_value);
+#elif defined(RTM_NEON_INTRINSICS)
+			const uint8x16_t segment_range_min_xxxx_yyyy_zzzz_extent_xxxx_u8 = vld1q_u8(segment_range_data);
+			const uint8x8_t segment_range_extent_yyyy_zzzz_u8 = vld1_u8(segment_range_data + 16);
+
+			// Convert from u8 to u32
+			const uint16x8_t segment_range_min_xxxx_yyyy_u16 = vmovl_u8(vget_low_u8(segment_range_min_xxxx_yyyy_zzzz_extent_xxxx_u8));
+			const uint16x8_t segment_range_min_zzzz_extent_xxxx_u16 = vmovl_u8(vget_high_u8(segment_range_min_xxxx_yyyy_zzzz_extent_xxxx_u8));
+			const uint16x8_t segment_range_extent_yyyy_zzzz_u16 = vmovl_u8(segment_range_extent_yyyy_zzzz_u8);
+
+			uint32x4_t segment_range_min_xxxx_u32 = vmovl_u16(vget_low_u16(segment_range_min_xxxx_yyyy_u16));
+			uint32x4_t segment_range_min_yyyy_u32 = vmovl_u16(vget_high_u16(segment_range_min_xxxx_yyyy_u16));
+			uint32x4_t segment_range_min_zzzz_u32 = vmovl_u16(vget_low_u16(segment_range_min_zzzz_extent_xxxx_u16));
+
+			const uint32x4_t segment_range_extent_xxxx_u32 = vmovl_u16(vget_high_u16(segment_range_min_zzzz_extent_xxxx_u16));
+			const uint32x4_t segment_range_extent_yyyy_u32 = vmovl_u16(vget_low_u16(segment_range_extent_yyyy_zzzz_u16));
+			const uint32x4_t segment_range_extent_zzzz_u32 = vmovl_u16(vget_high_u16(segment_range_extent_yyyy_zzzz_u16));
+
+			float32x4_t segment_range_min_xxxx = vcvtq_f32_u32(segment_range_min_xxxx_u32);
+			float32x4_t segment_range_min_yyyy = vcvtq_f32_u32(segment_range_min_yyyy_u32);
+			float32x4_t segment_range_min_zzzz = vcvtq_f32_u32(segment_range_min_zzzz_u32);
+
+			float32x4_t segment_range_extent_xxxx = vcvtq_f32_u32(segment_range_extent_xxxx_u32);
+			float32x4_t segment_range_extent_yyyy = vcvtq_f32_u32(segment_range_extent_yyyy_u32);
+			float32x4_t segment_range_extent_zzzz = vcvtq_f32_u32(segment_range_extent_zzzz_u32);
+
+			const float normalization_value = 1.0F / 255.0F;
+
+			segment_range_min_xxxx = vmulq_n_f32(segment_range_min_xxxx, normalization_value);
+			segment_range_min_yyyy = vmulq_n_f32(segment_range_min_yyyy, normalization_value);
+			segment_range_min_zzzz = vmulq_n_f32(segment_range_min_zzzz, normalization_value);
+
+			segment_range_extent_xxxx = vmulq_n_f32(segment_range_extent_xxxx, normalization_value);
+			segment_range_extent_yyyy = vmulq_n_f32(segment_range_extent_yyyy, normalization_value);
+			segment_range_extent_zzzz = vmulq_n_f32(segment_range_extent_zzzz, normalization_value);
+#else
+			rtm::vector4f segment_range_min_xxxx = rtm::vector_set(float(segment_range_data[0]), float(segment_range_data[1]), float(segment_range_data[2]), float(segment_range_data[3]));
+			rtm::vector4f segment_range_min_yyyy = rtm::vector_set(float(segment_range_data[4]), float(segment_range_data[5]), float(segment_range_data[6]), float(segment_range_data[7]));
+			rtm::vector4f segment_range_min_zzzz = rtm::vector_set(float(segment_range_data[8]), float(segment_range_data[9]), float(segment_range_data[10]), float(segment_range_data[11]));
+
+			rtm::vector4f segment_range_extent_xxxx = rtm::vector_set(float(segment_range_data[12]), float(segment_range_data[13]), float(segment_range_data[14]), float(segment_range_data[15]));
+			rtm::vector4f segment_range_extent_yyyy = rtm::vector_set(float(segment_range_data[16]), float(segment_range_data[17]), float(segment_range_data[18]), float(segment_range_data[19]));
+			rtm::vector4f segment_range_extent_zzzz = rtm::vector_set(float(segment_range_data[20]), float(segment_range_data[21]), float(segment_range_data[22]), float(segment_range_data[23]));
+
+			const float normalization_value = 1.0F / 255.0F;
+
+			segment_range_min_xxxx = rtm::vector_mul(segment_range_min_xxxx, normalization_value);
+			segment_range_min_yyyy = rtm::vector_mul(segment_range_min_yyyy, normalization_value);
+			segment_range_min_zzzz = rtm::vector_mul(segment_range_min_zzzz, normalization_value);
+
+			segment_range_extent_xxxx = rtm::vector_mul(segment_range_extent_xxxx, normalization_value);
+			segment_range_extent_yyyy = rtm::vector_mul(segment_range_extent_yyyy, normalization_value);
+			segment_range_extent_zzzz = rtm::vector_mul(segment_range_extent_zzzz, normalization_value);
+#endif
+
+#if defined(ACL_IMPL_PREFETCH_EARLY)
+			// Prefetch the next cache line even if we don't have any data left
+			// By the time we unpack again, it will have arrived in the CPU cache
+			// If our format is full precision, we have at most 4 samples per cache line
+			// If our format is drop W, we have at most 5.33 samples per cache line
+
+			// If our pointer was already aligned to a cache line before we unpacked our 4 values,
+			// it now points to the first byte of the next cache line. Any offset between 0-63 will fetch it.
+			// If our pointer had some offset into a cache line, we might have spanned 2 cache lines.
+			// If this happens, we probably already read some data from the next cache line in which
+			// case we don't need to prefetch it and we can go to the next one. Any offset after the end
+			// of this cache line will fetch it. For safety, we prefetch 63 bytes ahead.
+			// Prefetch 4 samples ahead in all levels of the CPU cache
+			ACL_IMPL_ANIMATED_PREFETCH(segment_range_data + 64);
+#endif
+
+#if defined(ACL_IMPL_USE_AVX_8_WIDE_DECOMP)
+			// With AVX, we must duplicate our data for the first segment in case we don't have a second segment
+			if (scratch_offset == 0)
+			{
+				// First segment data is duplicated
+				// Use 256 bit stores to avoid doing too many stores which might stall
+				_mm256_store_ps(reinterpret_cast<float*>(&output_scratch.segment_range_min[0]), _mm256_set_m128(segment_range_min_xxxx, segment_range_min_xxxx));
+				_mm256_store_ps(reinterpret_cast<float*>(&output_scratch.segment_range_min[2]), _mm256_set_m128(segment_range_min_yyyy, segment_range_min_yyyy));
+				_mm256_store_ps(reinterpret_cast<float*>(&output_scratch.segment_range_min[4]), _mm256_set_m128(segment_range_min_zzzz, segment_range_min_zzzz));
+				_mm256_store_ps(reinterpret_cast<float*>(&output_scratch.segment_range_extent[0]), _mm256_set_m128(segment_range_extent_xxxx, segment_range_extent_xxxx));
+				_mm256_store_ps(reinterpret_cast<float*>(&output_scratch.segment_range_extent[2]), _mm256_set_m128(segment_range_extent_yyyy, segment_range_extent_yyyy));
+				_mm256_store_ps(reinterpret_cast<float*>(&output_scratch.segment_range_extent[4]), _mm256_set_m128(segment_range_extent_zzzz, segment_range_extent_zzzz));
+			}
+			else
+			{
+				// Second segment overwrites our data
+				output_scratch.segment_range_min[1] = segment_range_min_xxxx;
+				output_scratch.segment_range_min[3] = segment_range_min_yyyy;
+				output_scratch.segment_range_min[5] = segment_range_min_zzzz;
+				output_scratch.segment_range_extent[1] = segment_range_extent_xxxx;
+				output_scratch.segment_range_extent[3] = segment_range_extent_yyyy;
+				output_scratch.segment_range_extent[5] = segment_range_extent_zzzz;
+			}
+#else
+			output_scratch.segment_range_min[scratch_offset + 0] = segment_range_min_xxxx;
+			output_scratch.segment_range_min[scratch_offset + 2] = segment_range_min_yyyy;
+			output_scratch.segment_range_min[scratch_offset + 4] = segment_range_min_zzzz;
+			output_scratch.segment_range_extent[scratch_offset + 0] = segment_range_extent_xxxx;
+			output_scratch.segment_range_extent[scratch_offset + 2] = segment_range_extent_yyyy;
+			output_scratch.segment_range_extent[scratch_offset + 4] = segment_range_extent_zzzz;
+#endif
+		}
+
+		// About 19 cycles with AVX on Skylake
+		// Force inline this function, we only use it to keep the code readable
+		ACL_FORCE_INLINE ACL_DISABLE_SECURITY_COOKIE_CHECK void RTM_SIMD_CALL remap_segment_range_data4(const segment_animated_scratch_v0& segment_scratch, uint32_t scratch_offset, range_reduction_masks_t range_reduction_masks,
+			rtm::vector4f& xxxx, rtm::vector4f& yyyy, rtm::vector4f& zzzz)
+		{
+			// Load and mask out our segment range data
+			const rtm::vector4f one_v = rtm::vector_set(1.0F);
+
+			rtm::vector4f segment_range_min_xxxx = segment_scratch.segment_range_min[scratch_offset + 0];
+			rtm::vector4f segment_range_min_yyyy = segment_scratch.segment_range_min[scratch_offset + 2];
+			rtm::vector4f segment_range_min_zzzz = segment_scratch.segment_range_min[scratch_offset + 4];
+
+			rtm::vector4f segment_range_extent_xxxx = segment_scratch.segment_range_extent[scratch_offset + 0];
+			rtm::vector4f segment_range_extent_yyyy = segment_scratch.segment_range_extent[scratch_offset + 2];
+			rtm::vector4f segment_range_extent_zzzz = segment_scratch.segment_range_extent[scratch_offset + 4];
+
+#if defined(RTM_SSE2_INTRINSICS)
+			// Mask out the segment min we ignore
+			const rtm::mask4f segment_range_ignore_mask_v = _mm_castsi128_ps(_mm_unpacklo_epi16(range_reduction_masks, range_reduction_masks));
+
+			segment_range_min_xxxx = _mm_andnot_ps(segment_range_ignore_mask_v, segment_range_min_xxxx);
+			segment_range_min_yyyy = _mm_andnot_ps(segment_range_ignore_mask_v, segment_range_min_yyyy);
+			segment_range_min_zzzz = _mm_andnot_ps(segment_range_ignore_mask_v, segment_range_min_zzzz);
+#elif defined(RTM_NEON_INTRINSICS)
+			// Mask out the segment min we ignore
+			const uint32x4_t segment_range_ignore_mask_v = vreinterpretq_u32_s32(vmovl_s16(vget_low_s16(range_reduction_masks)));
+
+			segment_range_min_xxxx = vreinterpretq_f32_u32(vbicq_u32(vreinterpretq_u32_f32(segment_range_min_xxxx), segment_range_ignore_mask_v));
+			segment_range_min_yyyy = vreinterpretq_f32_u32(vbicq_u32(vreinterpretq_u32_f32(segment_range_min_yyyy), segment_range_ignore_mask_v));
+			segment_range_min_zzzz = vreinterpretq_f32_u32(vbicq_u32(vreinterpretq_u32_f32(segment_range_min_zzzz), segment_range_ignore_mask_v));
+#else
+			const rtm::vector4f zero_v = rtm::vector_zero();
+
+			const uint32_t segment_range_mask_u32 = uint32_t(range_reduction_masks);
+			const rtm::mask4f segment_range_ignore_mask_v = rtm::mask_set((segment_range_mask_u32 & 0x000000FF) != 0, (segment_range_mask_u32 & 0x0000FF00) != 0, (segment_range_mask_u32 & 0x00FF0000) != 0, (segment_range_mask_u32 & 0xFF000000) != 0);
+
+			segment_range_min_xxxx = rtm::vector_select(segment_range_ignore_mask_v, zero_v, segment_range_min_xxxx);
+			segment_range_min_yyyy = rtm::vector_select(segment_range_ignore_mask_v, zero_v, segment_range_min_yyyy);
+			segment_range_min_zzzz = rtm::vector_select(segment_range_ignore_mask_v, zero_v, segment_range_min_zzzz);
+#endif
+
+			// Mask out the segment extent we ignore
+			segment_range_extent_xxxx = rtm::vector_select(segment_range_ignore_mask_v, one_v, segment_range_extent_xxxx);
+			segment_range_extent_yyyy = rtm::vector_select(segment_range_ignore_mask_v, one_v, segment_range_extent_yyyy);
+			segment_range_extent_zzzz = rtm::vector_select(segment_range_ignore_mask_v, one_v, segment_range_extent_zzzz);
+
+			// Remap
+			xxxx = rtm::vector_mul_add(xxxx, segment_range_extent_xxxx, segment_range_min_xxxx);
+			yyyy = rtm::vector_mul_add(yyyy, segment_range_extent_yyyy, segment_range_min_yyyy);
+			zzzz = rtm::vector_mul_add(zzzz, segment_range_extent_zzzz, segment_range_min_zzzz);
+		}
+
+#if defined(ACL_IMPL_USE_AVX_8_WIDE_DECOMP)
+		// Force inline this function, we only use it to keep the code readable
+		ACL_FORCE_INLINE ACL_DISABLE_SECURITY_COOKIE_CHECK void RTM_SIMD_CALL remap_segment_range_data_avx8(const segment_animated_scratch_v0& segment_scratch,
+			range_reduction_masks_t range_reduction_masks0, range_reduction_masks_t range_reduction_masks1,
+			__m256& xxxx0_xxxx1, __m256& yyyy0_yyyy1, __m256& zzzz0_zzzz1)
+		{
+			// Load and mask out our segment range data
+			const __m256 one_v = _mm256_set1_ps(1.0F);
+
+			__m256 segment_range_min_xxxx0_xxxx1 = _mm256_load_ps(reinterpret_cast<const float*>(&segment_scratch.segment_range_min[0]));
+			__m256 segment_range_min_yyyy0_yyyy1 = _mm256_load_ps(reinterpret_cast<const float*>(&segment_scratch.segment_range_min[2]));
+			__m256 segment_range_min_zzzz0_zzzz1 = _mm256_load_ps(reinterpret_cast<const float*>(&segment_scratch.segment_range_min[4]));
+
+			__m256 segment_range_extent_xxxx0_xxxx1 = _mm256_load_ps(reinterpret_cast<const float*>(&segment_scratch.segment_range_extent[0]));
+			__m256 segment_range_extent_yyyy0_yyyy1 = _mm256_load_ps(reinterpret_cast<const float*>(&segment_scratch.segment_range_extent[2]));
+			__m256 segment_range_extent_zzzz0_zzzz1 = _mm256_load_ps(reinterpret_cast<const float*>(&segment_scratch.segment_range_extent[4]));
+
+			// Mask out the segment min we ignore
+			const __m128 segment_range_ignore_mask_v0 = _mm_castsi128_ps(_mm_unpacklo_epi16(range_reduction_masks0, range_reduction_masks0));
+			const __m128 segment_range_ignore_mask_v1 = _mm_castsi128_ps(_mm_unpacklo_epi16(range_reduction_masks1, range_reduction_masks1));
+
+			const __m256 segment_range_mask0_mask1 = _mm256_set_m128(segment_range_ignore_mask_v1, segment_range_ignore_mask_v0);
+
+			segment_range_min_xxxx0_xxxx1 = _mm256_andnot_ps(segment_range_mask0_mask1, segment_range_min_xxxx0_xxxx1);
+			segment_range_min_yyyy0_yyyy1 = _mm256_andnot_ps(segment_range_mask0_mask1, segment_range_min_yyyy0_yyyy1);
+			segment_range_min_zzzz0_zzzz1 = _mm256_andnot_ps(segment_range_mask0_mask1, segment_range_min_zzzz0_zzzz1);
+
+			segment_range_extent_xxxx0_xxxx1 = _mm256_blendv_ps(segment_range_extent_xxxx0_xxxx1, one_v, segment_range_mask0_mask1);
+			segment_range_extent_yyyy0_yyyy1 = _mm256_blendv_ps(segment_range_extent_yyyy0_yyyy1, one_v, segment_range_mask0_mask1);
+			segment_range_extent_zzzz0_zzzz1 = _mm256_blendv_ps(segment_range_extent_zzzz0_zzzz1, one_v, segment_range_mask0_mask1);
+
+			xxxx0_xxxx1 = _mm256_add_ps(_mm256_mul_ps(xxxx0_xxxx1, segment_range_extent_xxxx0_xxxx1), segment_range_min_xxxx0_xxxx1);
+			yyyy0_yyyy1 = _mm256_add_ps(_mm256_mul_ps(yyyy0_yyyy1, segment_range_extent_yyyy0_yyyy1), segment_range_min_yyyy0_yyyy1);
+			zzzz0_zzzz1 = _mm256_add_ps(_mm256_mul_ps(zzzz0_zzzz1, segment_range_extent_zzzz0_zzzz1), segment_range_min_zzzz0_zzzz1);
+		}
+#endif
+
+		// About 24 cycles with AVX on Skylake
+		// Force inline this function, we only use it to keep the code readable
+		ACL_FORCE_INLINE ACL_DISABLE_SECURITY_COOKIE_CHECK void RTM_SIMD_CALL remap_clip_range_data4(const uint8_t* clip_range_data, uint32_t num_to_unpack,
+			range_reduction_masks_t range_reduction_masks0, range_reduction_masks_t range_reduction_masks1,
+			rtm::vector4f& xxxx0, rtm::vector4f& yyyy0, rtm::vector4f& zzzz0,
+			rtm::vector4f& xxxx1, rtm::vector4f& yyyy1, rtm::vector4f& zzzz1)
+		{
+			// Always load 4x rotations, we might contain garbage in a few lanes but it's fine
+			const uint32_t load_size = num_to_unpack * sizeof(float);
+
+#if defined(RTM_SSE2_INTRINSICS)
+			const __m128 clip_range_mask0 = _mm_castsi128_ps(_mm_unpackhi_epi16(range_reduction_masks0, range_reduction_masks0));
+			const __m128 clip_range_mask1 = _mm_castsi128_ps(_mm_unpackhi_epi16(range_reduction_masks1, range_reduction_masks1));
+#elif defined(RTM_NEON_INTRINSICS)
+			const float32x4_t clip_range_mask0 = vreinterpretq_f32_s32(vmovl_s16(vget_high_s16(range_reduction_masks0)));
+			const float32x4_t clip_range_mask1 = vreinterpretq_f32_s32(vmovl_s16(vget_high_s16(range_reduction_masks1)));
+#else
+			const uint32_t clip_range_mask_u32_0 = uint32_t(range_reduction_masks0 >> 32);
+			const uint32_t clip_range_mask_u32_1 = uint32_t(range_reduction_masks1 >> 32);
+			const rtm::mask4f clip_range_mask0 = rtm::mask_set((clip_range_mask_u32_0 & 0x000000FF) != 0, (clip_range_mask_u32_0 & 0x0000FF00) != 0, (clip_range_mask_u32_0 & 0x00FF0000) != 0, (clip_range_mask_u32_0 & 0xFF000000) != 0);
+			const rtm::mask4f clip_range_mask1 = rtm::mask_set((clip_range_mask_u32_1 & 0x000000FF) != 0, (clip_range_mask_u32_1 & 0x0000FF00) != 0, (clip_range_mask_u32_1 & 0x00FF0000) != 0, (clip_range_mask_u32_1 & 0xFF000000) != 0);
+#endif
+
+			const rtm::vector4f clip_range_min_xxxx = rtm::vector_load(clip_range_data + load_size * 0);
+			const rtm::vector4f clip_range_min_yyyy = rtm::vector_load(clip_range_data + load_size * 1);
+			const rtm::vector4f clip_range_min_zzzz = rtm::vector_load(clip_range_data + load_size * 2);
+
+			const rtm::vector4f clip_range_extent_xxxx = rtm::vector_load(clip_range_data + load_size * 3);
+			const rtm::vector4f clip_range_extent_yyyy = rtm::vector_load(clip_range_data + load_size * 4);
+			const rtm::vector4f clip_range_extent_zzzz = rtm::vector_load(clip_range_data + load_size * 5);
+
+			// Mask out the clip ranges we ignore
+#if defined(RTM_SSE2_INTRINSICS)
+			const rtm::vector4f clip_range_min_xxxx0 = _mm_andnot_ps(clip_range_mask0, clip_range_min_xxxx);
+			const rtm::vector4f clip_range_min_yyyy0 = _mm_andnot_ps(clip_range_mask0, clip_range_min_yyyy);
+			const rtm::vector4f clip_range_min_zzzz0 = _mm_andnot_ps(clip_range_mask0, clip_range_min_zzzz);
+
+			const rtm::vector4f clip_range_min_xxxx1 = _mm_andnot_ps(clip_range_mask1, clip_range_min_xxxx);
+			const rtm::vector4f clip_range_min_yyyy1 = _mm_andnot_ps(clip_range_mask1, clip_range_min_yyyy);
+			const rtm::vector4f clip_range_min_zzzz1 = _mm_andnot_ps(clip_range_mask1, clip_range_min_zzzz);
+#elif defined(RTM_NEON_INTRINSICS)
+			const rtm::vector4f clip_range_min_xxxx0 = vreinterpretq_f32_u32(vbicq_u32(vreinterpretq_u32_f32(clip_range_min_xxxx), vreinterpretq_u32_f32(clip_range_mask0)));
+			const rtm::vector4f clip_range_min_yyyy0 = vreinterpretq_f32_u32(vbicq_u32(vreinterpretq_u32_f32(clip_range_min_yyyy), vreinterpretq_u32_f32(clip_range_mask0)));
+			const rtm::vector4f clip_range_min_zzzz0 = vreinterpretq_f32_u32(vbicq_u32(vreinterpretq_u32_f32(clip_range_min_zzzz), vreinterpretq_u32_f32(clip_range_mask0)));
+
+			const rtm::vector4f clip_range_min_xxxx1 = vreinterpretq_f32_u32(vbicq_u32(vreinterpretq_u32_f32(clip_range_min_xxxx), vreinterpretq_u32_f32(clip_range_mask1)));
+			const rtm::vector4f clip_range_min_yyyy1 = vreinterpretq_f32_u32(vbicq_u32(vreinterpretq_u32_f32(clip_range_min_yyyy), vreinterpretq_u32_f32(clip_range_mask1)));
+			const rtm::vector4f clip_range_min_zzzz1 = vreinterpretq_f32_u32(vbicq_u32(vreinterpretq_u32_f32(clip_range_min_zzzz), vreinterpretq_u32_f32(clip_range_mask1)));
+#else
+			const rtm::vector4f zero_v = rtm::vector_zero();
+
+			const rtm::vector4f clip_range_min_xxxx0 = rtm::vector_select(clip_range_mask0, zero_v, clip_range_min_xxxx);
+			const rtm::vector4f clip_range_min_yyyy0 = rtm::vector_select(clip_range_mask0, zero_v, clip_range_min_yyyy);
+			const rtm::vector4f clip_range_min_zzzz0 = rtm::vector_select(clip_range_mask0, zero_v, clip_range_min_zzzz);
+
+			const rtm::vector4f clip_range_min_xxxx1 = rtm::vector_select(clip_range_mask1, zero_v, clip_range_min_xxxx);
+			const rtm::vector4f clip_range_min_yyyy1 = rtm::vector_select(clip_range_mask1, zero_v, clip_range_min_yyyy);
+			const rtm::vector4f clip_range_min_zzzz1 = rtm::vector_select(clip_range_mask1, zero_v, clip_range_min_zzzz);
+#endif
+
+			const rtm::vector4f one_v = rtm::vector_set(1.0F);
+
+			const rtm::vector4f clip_range_extent_xxxx0 = rtm::vector_select(clip_range_mask0, one_v, clip_range_extent_xxxx);
+			const rtm::vector4f clip_range_extent_yyyy0 = rtm::vector_select(clip_range_mask0, one_v, clip_range_extent_yyyy);
+			const rtm::vector4f clip_range_extent_zzzz0 = rtm::vector_select(clip_range_mask0, one_v, clip_range_extent_zzzz);
+
+			const rtm::vector4f clip_range_extent_xxxx1 = rtm::vector_select(clip_range_mask1, one_v, clip_range_extent_xxxx);
+			const rtm::vector4f clip_range_extent_yyyy1 = rtm::vector_select(clip_range_mask1, one_v, clip_range_extent_yyyy);
+			const rtm::vector4f clip_range_extent_zzzz1 = rtm::vector_select(clip_range_mask1, one_v, clip_range_extent_zzzz);
+
+			xxxx0 = rtm::vector_mul_add(xxxx0, clip_range_extent_xxxx0, clip_range_min_xxxx0);
+			yyyy0 = rtm::vector_mul_add(yyyy0, clip_range_extent_yyyy0, clip_range_min_yyyy0);
+			zzzz0 = rtm::vector_mul_add(zzzz0, clip_range_extent_zzzz0, clip_range_min_zzzz0);
+
+			xxxx1 = rtm::vector_mul_add(xxxx1, clip_range_extent_xxxx1, clip_range_min_xxxx1);
+			yyyy1 = rtm::vector_mul_add(yyyy1, clip_range_extent_yyyy1, clip_range_min_yyyy1);
+			zzzz1 = rtm::vector_mul_add(zzzz1, clip_range_extent_zzzz1, clip_range_min_zzzz1);
+		}
+
+#if defined(ACL_IMPL_USE_AVX_8_WIDE_DECOMP)
+		// Force inline this function, we only use it to keep the code readable
+		ACL_FORCE_INLINE ACL_DISABLE_SECURITY_COOKIE_CHECK void RTM_SIMD_CALL remap_clip_range_data_avx8(const uint8_t* clip_range_data, uint32_t num_to_unpack,
+			range_reduction_masks_t range_reduction_masks0, range_reduction_masks_t range_reduction_masks1,
+			__m256& xxxx0_xxxx1, __m256& yyyy0_yyyy1, __m256& zzzz0_zzzz1)
+		{
+			const __m256 one_v = _mm256_set1_ps(1.0F);
+
+			// Always load 4x rotations, we might contain garbage in a few lanes but it's fine
+			const uint32_t load_size = num_to_unpack * sizeof(float);
+
+			const __m128 clip_range_mask0 = _mm_castsi128_ps(_mm_unpackhi_epi16(range_reduction_masks0, range_reduction_masks0));
+			const __m128 clip_range_mask1 = _mm_castsi128_ps(_mm_unpackhi_epi16(range_reduction_masks1, range_reduction_masks1));
+
+			const __m256 clip_range_mask0_mask1 = _mm256_set_m128(clip_range_mask1, clip_range_mask0);
+
+			const rtm::vector4f clip_range_min_xxxx = rtm::vector_load(clip_range_data + load_size * 0);
+			const rtm::vector4f clip_range_min_yyyy = rtm::vector_load(clip_range_data + load_size * 1);
+			const rtm::vector4f clip_range_min_zzzz = rtm::vector_load(clip_range_data + load_size * 2);
+
+			const rtm::vector4f clip_range_extent_xxxx = rtm::vector_load(clip_range_data + load_size * 3);
+			const rtm::vector4f clip_range_extent_yyyy = rtm::vector_load(clip_range_data + load_size * 4);
+			const rtm::vector4f clip_range_extent_zzzz = rtm::vector_load(clip_range_data + load_size * 5);
+
+			__m256 clip_range_min_xxxx_xxxx = _mm256_set_m128(clip_range_min_xxxx, clip_range_min_xxxx);
+			__m256 clip_range_min_yyyy_yyyy = _mm256_set_m128(clip_range_min_yyyy, clip_range_min_yyyy);
+			__m256 clip_range_min_zzzz_zzzz = _mm256_set_m128(clip_range_min_zzzz, clip_range_min_zzzz);
+
+			__m256 clip_range_extent_xxxx_xxxx = _mm256_set_m128(clip_range_extent_xxxx, clip_range_extent_xxxx);
+			__m256 clip_range_extent_yyyy_yyyy = _mm256_set_m128(clip_range_extent_yyyy, clip_range_extent_yyyy);
+			__m256 clip_range_extent_zzzz_zzzz = _mm256_set_m128(clip_range_extent_zzzz, clip_range_extent_zzzz);
+
+			// Mask out the clip ranges we ignore
+			clip_range_min_xxxx_xxxx = _mm256_andnot_ps(clip_range_mask0_mask1, clip_range_min_xxxx_xxxx);
+			clip_range_min_yyyy_yyyy = _mm256_andnot_ps(clip_range_mask0_mask1, clip_range_min_yyyy_yyyy);
+			clip_range_min_zzzz_zzzz = _mm256_andnot_ps(clip_range_mask0_mask1, clip_range_min_zzzz_zzzz);
+
+			clip_range_extent_xxxx_xxxx = _mm256_blendv_ps(clip_range_extent_xxxx_xxxx, one_v, clip_range_mask0_mask1);
+			clip_range_extent_yyyy_yyyy = _mm256_blendv_ps(clip_range_extent_yyyy_yyyy, one_v, clip_range_mask0_mask1);
+			clip_range_extent_zzzz_zzzz = _mm256_blendv_ps(clip_range_extent_zzzz_zzzz, one_v, clip_range_mask0_mask1);
+
+			xxxx0_xxxx1 = _mm256_add_ps(_mm256_mul_ps(xxxx0_xxxx1, clip_range_extent_xxxx_xxxx), clip_range_min_xxxx_xxxx);
+			yyyy0_yyyy1 = _mm256_add_ps(_mm256_mul_ps(yyyy0_yyyy1, clip_range_extent_yyyy_yyyy), clip_range_min_yyyy_yyyy);
+			zzzz0_zzzz1 = _mm256_add_ps(_mm256_mul_ps(zzzz0_zzzz1, clip_range_extent_zzzz_zzzz), clip_range_min_zzzz_zzzz);
+		}
+#endif
+
+		// About 31 cycles with AVX on Skylake
+		// Force inline this function, we only use it to keep the code readable
+		ACL_FORCE_INLINE ACL_DISABLE_SECURITY_COOKIE_CHECK rtm::vector4f RTM_SIMD_CALL quat_from_positive_w4(rtm::vector4f_arg0 xxxx, rtm::vector4f_arg1 yyyy, rtm::vector4f_arg2 zzzz)
+		{
+			const rtm::vector4f xxxx_squared = rtm::vector_mul(xxxx, xxxx);
+			const rtm::vector4f yyyy_squared = rtm::vector_mul(yyyy, yyyy);
+			const rtm::vector4f zzzz_squared = rtm::vector_mul(zzzz, zzzz);
+			const rtm::vector4f wwww_squared = rtm::vector_sub(rtm::vector_sub(rtm::vector_sub(rtm::vector_set(1.0F), xxxx_squared), yyyy_squared), zzzz_squared);
+
+			// w_squared can be negative either due to rounding or due to quantization imprecision, we take the absolute value
+			// to ensure the resulting quaternion is always normalized with a positive W component
+			return rtm::vector_sqrt(rtm::vector_abs(wwww_squared));
+		}
+
+#if defined(ACL_IMPL_USE_AVX_8_WIDE_DECOMP)
+		// Force inline this function, we only use it to keep the code readable
+		ACL_FORCE_INLINE ACL_DISABLE_SECURITY_COOKIE_CHECK __m256 RTM_SIMD_CALL quat_from_positive_w_avx8(__m256 xxxx0_xxxx1, __m256 yyyy0_yyyy1, __m256 zzzz0_zzzz1)
+		{
+			const __m256 one_v = _mm256_set1_ps(1.0F);
+
+			const __m256 xxxx0_xxxx1_squared = _mm256_mul_ps(xxxx0_xxxx1, xxxx0_xxxx1);
+			const __m256 yyyy0_yyyy1_squared = _mm256_mul_ps(yyyy0_yyyy1, yyyy0_yyyy1);
+			const __m256 zzzz0_zzzz1_squared = _mm256_mul_ps(zzzz0_zzzz1, zzzz0_zzzz1);
+
+			const __m256 wwww0_wwww1_squared = _mm256_sub_ps(_mm256_sub_ps(_mm256_sub_ps(one_v, xxxx0_xxxx1_squared), yyyy0_yyyy1_squared), zzzz0_zzzz1_squared);
+
+			const __m256i abs_mask = _mm256_set1_epi32(0x7FFFFFFFULL);
+			const __m256 wwww0_wwww1_squared_abs = _mm256_and_ps(wwww0_wwww1_squared, _mm256_castsi256_ps(abs_mask));
+
+			return _mm256_sqrt_ps(wwww0_wwww1_squared_abs);
+		}
+#endif
+
+		// About 28 cycles with AVX on Skylake
+		// Force inline this function, we only use it to keep the code readable
+		ACL_FORCE_INLINE ACL_DISABLE_SECURITY_COOKIE_CHECK void RTM_SIMD_CALL quat_lerp4(
+			rtm::vector4f_arg0 xxxx0, rtm::vector4f_arg1 yyyy0, rtm::vector4f_arg2 zzzz0, rtm::vector4f_arg3 wwww0,
+			rtm::vector4f_arg4 xxxx1, rtm::vector4f_arg5 yyyy1, rtm::vector4f_arg6 zzzz1, rtm::vector4f_arg7 wwww1,
+			float interpolation_alpha,
+			rtm::vector4f& interp_xxxx, rtm::vector4f& interp_yyyy, rtm::vector4f& interp_zzzz, rtm::vector4f& interp_wwww)
+		{
+			// Calculate the vector4 dot product: dot(start, end)
+			const rtm::vector4f xxxx_squared = rtm::vector_mul(xxxx0, xxxx1);
+			const rtm::vector4f yyyy_squared = rtm::vector_mul(yyyy0, yyyy1);
+			const rtm::vector4f zzzz_squared = rtm::vector_mul(zzzz0, zzzz1);
+			const rtm::vector4f wwww_squared = rtm::vector_mul(wwww0, wwww1);
+
+			const rtm::vector4f dot4 = rtm::vector_add(rtm::vector_add(rtm::vector_add(xxxx_squared, yyyy_squared), zzzz_squared), wwww_squared);
+
+			// Calculate the bias, if the dot product is positive or zero, there is no bias
+			// but if it is negative, we want to flip the 'end' rotation XYZW components
+			const rtm::vector4f neg_zero = rtm::vector_set(-0.0F);
+			const rtm::vector4f bias = acl_impl::vector_and(dot4, neg_zero);
+
+			// Apply our bias to the 'end'
+			const rtm::vector4f xxxx1_with_bias = acl_impl::vector_xor(xxxx1, bias);
+			const rtm::vector4f yyyy1_with_bias = acl_impl::vector_xor(yyyy1, bias);
+			const rtm::vector4f zzzz1_with_bias = acl_impl::vector_xor(zzzz1, bias);
+			const rtm::vector4f wwww1_with_bias = acl_impl::vector_xor(wwww1, bias);
+
+			// Lerp the rotation after applying the bias
+			// ((1.0 - alpha) * start) + (alpha * (end ^ bias)) == (start - alpha * start) + (alpha * (end ^ bias))
+			const rtm::vector4f alpha = rtm::vector_set(interpolation_alpha);
+
+			interp_xxxx = rtm::vector_mul_add(xxxx1_with_bias, alpha, rtm::vector_neg_mul_sub(xxxx0, alpha, xxxx0));
+			interp_yyyy = rtm::vector_mul_add(yyyy1_with_bias, alpha, rtm::vector_neg_mul_sub(yyyy0, alpha, yyyy0));
+			interp_zzzz = rtm::vector_mul_add(zzzz1_with_bias, alpha, rtm::vector_neg_mul_sub(zzzz0, alpha, zzzz0));
+			interp_wwww = rtm::vector_mul_add(wwww1_with_bias, alpha, rtm::vector_neg_mul_sub(wwww0, alpha, wwww0));
+		}
+
+		// About 9 cycles with AVX on Skylake
+		// Force inline this function, we only use it to keep the code readable
+		ACL_FORCE_INLINE ACL_DISABLE_SECURITY_COOKIE_CHECK void RTM_SIMD_CALL quat_normalize4(rtm::vector4f& xxxx, rtm::vector4f& yyyy, rtm::vector4f& zzzz, rtm::vector4f& wwww)
+		{
+			const rtm::vector4f xxxx_squared = rtm::vector_mul(xxxx, xxxx);
+			const rtm::vector4f yyyy_squared = rtm::vector_mul(yyyy, yyyy);
+			const rtm::vector4f zzzz_squared = rtm::vector_mul(zzzz, zzzz);
+			const rtm::vector4f wwww_squared = rtm::vector_mul(wwww, wwww);
+
+			const rtm::vector4f dot4 = rtm::vector_add(rtm::vector_add(rtm::vector_add(xxxx_squared, yyyy_squared), zzzz_squared), wwww_squared);
+
+			const rtm::vector4f len4 = rtm::vector_sqrt(dot4);
+			const rtm::vector4f inv_len4 = rtm::vector_div(rtm::vector_set(1.0F), len4);
+
+			xxxx = rtm::vector_mul(xxxx, inv_len4);
+			yyyy = rtm::vector_mul(yyyy, inv_len4);
+			zzzz = rtm::vector_mul(zzzz, inv_len4);
+			wwww = rtm::vector_mul(wwww, inv_len4);
+		}
+
 		template<class decompression_settings_type>
-		inline ACL_DISABLE_SECURITY_COOKIE_CHECK rtm::mask4f RTM_SIMD_CALL unpack_animated_quat(const persistent_transform_decompression_context_v0& decomp_context, rtm::vector4f output_scratch[4],
+		inline ACL_DISABLE_SECURITY_COOKIE_CHECK range_reduction_masks_t RTM_SIMD_CALL unpack_animated_quat(const persistent_transform_decompression_context_v0& decomp_context, rtm::vector4f output_scratch[4],
 			uint32_t num_to_unpack, segment_animated_sampling_context_v0& segment_sampling_context)
 		{
 			const rotation_format8 rotation_format = get_rotation_format<decompression_settings_type>(decomp_context.rotation_format);
@@ -105,6 +605,10 @@ namespace acl
 			// For SIMD, can we load constant samples and write them to scratch? Afterwards its the same as packed on 16 bits
 			// We get 4 short branches (test, cmp, 6x loads, 3x ORs, 3x writes, load immediate) followed by a common code path for all 4 samples
 
+			// Maybe we can write in SOA order directly, and we could even write the scaling value per lane, load ones multiply 3 times for xyz in SOA
+
+			// Try inlining the unpacking functions
+
 			for (uint32_t unpack_index = 0; unpack_index < num_to_unpack; ++unpack_index)
 			{
 				// Our decompressed rotation as a vector4
@@ -202,206 +706,36 @@ namespace acl
 			// case we don't need to prefetch it and we can go to the next one. Any offset after the end
 			// of this cache line will fetch it. For safety, we prefetch 63 bytes ahead.
 			// Prefetch 4 samples ahead in all levels of the CPU cache
-			ACL_IMPL_ANIMATED_PREFETCH(format_per_track_data + 63);
+#if defined(ACL_IMPL_PREFETCH_EARLY)
 			ACL_IMPL_ANIMATED_PREFETCH(animated_track_data + (animated_track_data_bit_offset / 8) + 63);
+#endif
 
 			// Update our pointers
 			if (rotation_format == rotation_format8::quatf_drop_w_variable && decompression_settings_type::is_rotation_format_supported(rotation_format8::quatf_drop_w_variable))
 			{
+#if defined(ACL_IMPL_PREFETCH_EARLY)
+				// Prefetch the next cache line in all levels of the CPU cache
+				ACL_IMPL_ANIMATED_PREFETCH(format_per_track_data + 64);
+#endif
+
 				// Skip our used metadata data, all groups are padded to 4 elements
 				segment_sampling_context.format_per_track_data = format_per_track_data + 4;
 			}
 
 			segment_sampling_context.animated_track_data_bit_offset = animated_track_data_bit_offset;
 
-			// Swizzle our samples into SOA form
-			// TODO: Optimize for NEON
-			rtm::vector4f tmp0 = rtm::vector_mix<rtm::mix4::x, rtm::mix4::y, rtm::mix4::a, rtm::mix4::b>(output_scratch[0], output_scratch[1]);
-			rtm::vector4f tmp1 = rtm::vector_mix<rtm::mix4::z, rtm::mix4::w, rtm::mix4::c, rtm::mix4::d>(output_scratch[0], output_scratch[1]);
-			rtm::vector4f tmp2 = rtm::vector_mix<rtm::mix4::x, rtm::mix4::y, rtm::mix4::a, rtm::mix4::b>(output_scratch[2], output_scratch[3]);
-			rtm::vector4f tmp3 = rtm::vector_mix<rtm::mix4::z, rtm::mix4::w, rtm::mix4::c, rtm::mix4::d>(output_scratch[2], output_scratch[3]);
-
-			rtm::vector4f sample_xxxx = rtm::vector_mix<rtm::mix4::x, rtm::mix4::z, rtm::mix4::a, rtm::mix4::c>(tmp0, tmp2);
-			rtm::vector4f sample_yyyy = rtm::vector_mix<rtm::mix4::y, rtm::mix4::w, rtm::mix4::b, rtm::mix4::d>(tmp0, tmp2);
-			rtm::vector4f sample_zzzz = rtm::vector_mix<rtm::mix4::x, rtm::mix4::z, rtm::mix4::a, rtm::mix4::c>(tmp1, tmp3);
-
-			rtm::mask4f clip_range_ignore_mask_v32f;	// function's return value
+			range_reduction_masks_t range_reduction_masks;	// function's return value
 
 			if (rotation_format == rotation_format8::quatf_drop_w_variable && decompression_settings_type::is_rotation_format_supported(rotation_format8::quatf_drop_w_variable))
 			{
-				// TODO: Move range remapping out of here and do it with AVX together with quat W reconstruction
-
-				const rtm::vector4f one_v = rtm::vector_set(1.0F);
-
 #if defined(RTM_SSE2_INTRINSICS)
 				const __m128i ignore_masks_v8 = _mm_set_epi32(0, 0, clip_range_ignore_mask, segment_range_ignore_mask);
-				const __m128i ignore_masks_v16 = _mm_unpacklo_epi8(ignore_masks_v8, ignore_masks_v8);
+				range_reduction_masks = _mm_unpacklo_epi8(ignore_masks_v8, ignore_masks_v8);
 #elif defined(RTM_NEON_INTRINSICS)
 				const int8x8_t ignore_masks_v8 = vcreate_s8((uint64_t(clip_range_ignore_mask) << 32) | segment_range_ignore_mask);
-				const int16x8_t ignore_masks_v16 = vmovl_s8(ignore_masks_v8);
-#endif
-
-				if (decomp_context.has_segments)
-				{
-					// TODO: prefetch segment data earlier, as soon as we are done loading
-
-					// Segment range is packed: min.xxxx, min.yyyy, min.zzzz, extent.xxxx, extent.yyyy, extent.zzzz
-
-#if defined(RTM_SSE2_INTRINSICS)
-					const __m128i zero = _mm_setzero_si128();
-
-					const __m128i segment_range_min_xxxx_yyyy_zzzz_extent_xxxx_u8 = _mm_loadu_si128((const __m128i*)segment_range_data);
-					const __m128i segment_range_extent_yyyy_zzzz_u8 = _mm_loadu_si128((const __m128i*)(segment_range_data + 16));
-
-					// Convert from u8 to u32
-					const __m128i segment_range_min_xxxx_yyyy_u16 = _mm_unpacklo_epi8(segment_range_min_xxxx_yyyy_zzzz_extent_xxxx_u8, zero);
-					const __m128i segment_range_min_zzzz_extent_xxxx_u16 = _mm_unpackhi_epi8(segment_range_min_xxxx_yyyy_zzzz_extent_xxxx_u8, zero);
-					const __m128i segment_range_extent_yyyy_zzzz_u16 = _mm_unpacklo_epi8(segment_range_extent_yyyy_zzzz_u8, zero);
-
-					__m128i segment_range_min_xxxx_u32 = _mm_unpacklo_epi16(segment_range_min_xxxx_yyyy_u16, zero);
-					__m128i segment_range_min_yyyy_u32 = _mm_unpackhi_epi16(segment_range_min_xxxx_yyyy_u16, zero);
-					__m128i segment_range_min_zzzz_u32 = _mm_unpacklo_epi16(segment_range_min_zzzz_extent_xxxx_u16, zero);
-
-					const __m128i segment_range_extent_xxxx_u32 = _mm_unpackhi_epi16(segment_range_min_zzzz_extent_xxxx_u16, zero);
-					const __m128i segment_range_extent_yyyy_u32 = _mm_unpacklo_epi16(segment_range_extent_yyyy_zzzz_u16, zero);
-					const __m128i segment_range_extent_zzzz_u32 = _mm_unpackhi_epi16(segment_range_extent_yyyy_zzzz_u16, zero);
-
-					// Mask out the segment min we ignore
-					const __m128i segment_range_ignore_mask_u32 = _mm_unpacklo_epi16(ignore_masks_v16, ignore_masks_v16);
-
-					segment_range_min_xxxx_u32 = _mm_andnot_si128(segment_range_ignore_mask_u32, segment_range_min_xxxx_u32);
-					segment_range_min_yyyy_u32 = _mm_andnot_si128(segment_range_ignore_mask_u32, segment_range_min_yyyy_u32);
-					segment_range_min_zzzz_u32 = _mm_andnot_si128(segment_range_ignore_mask_u32, segment_range_min_zzzz_u32);
-
-					__m128 segment_range_min_xxxx = _mm_cvtepi32_ps(segment_range_min_xxxx_u32);
-					__m128 segment_range_min_yyyy = _mm_cvtepi32_ps(segment_range_min_yyyy_u32);
-					__m128 segment_range_min_zzzz = _mm_cvtepi32_ps(segment_range_min_zzzz_u32);
-
-					__m128 segment_range_extent_xxxx = _mm_cvtepi32_ps(segment_range_extent_xxxx_u32);
-					__m128 segment_range_extent_yyyy = _mm_cvtepi32_ps(segment_range_extent_yyyy_u32);
-					__m128 segment_range_extent_zzzz = _mm_cvtepi32_ps(segment_range_extent_zzzz_u32);
-
-					const __m128 normalization_value = _mm_set_ps1(1.0F / 255.0F);
-
-					segment_range_min_xxxx = _mm_mul_ps(segment_range_min_xxxx, normalization_value);
-					segment_range_min_yyyy = _mm_mul_ps(segment_range_min_yyyy, normalization_value);
-					segment_range_min_zzzz = _mm_mul_ps(segment_range_min_zzzz, normalization_value);
-
-					segment_range_extent_xxxx = _mm_mul_ps(segment_range_extent_xxxx, normalization_value);
-					segment_range_extent_yyyy = _mm_mul_ps(segment_range_extent_yyyy, normalization_value);
-					segment_range_extent_zzzz = _mm_mul_ps(segment_range_extent_zzzz, normalization_value);
-
-					const rtm::mask4f segment_range_ignore_mask_v = _mm_castsi128_ps(segment_range_ignore_mask_u32);
-#elif defined(RTM_NEON_INTRINSICS)
-					const uint8x16_t segment_range_min_xxxx_yyyy_zzzz_extent_xxxx_u8 = vld1q_u8(segment_range_data);
-					const uint8x8_t segment_range_extent_yyyy_zzzz_u8 = vld1_u8(segment_range_data + 16);
-
-					// Convert from u8 to u32
-					const uint16x8_t segment_range_min_xxxx_yyyy_u16 = vmovl_u8(vget_low_u8(segment_range_min_xxxx_yyyy_zzzz_extent_xxxx_u8));
-					const uint16x8_t segment_range_min_zzzz_extent_xxxx_u16 = vmovl_u8(vget_high_u8(segment_range_min_xxxx_yyyy_zzzz_extent_xxxx_u8));
-					const uint16x8_t segment_range_extent_yyyy_zzzz_u16 = vmovl_u8(segment_range_extent_yyyy_zzzz_u8);
-
-					uint32x4_t segment_range_min_xxxx_u32 = vmovl_u16(vget_low_u16(segment_range_min_xxxx_yyyy_u16));
-					uint32x4_t segment_range_min_yyyy_u32 = vmovl_u16(vget_high_u16(segment_range_min_xxxx_yyyy_u16));
-					uint32x4_t segment_range_min_zzzz_u32 = vmovl_u16(vget_low_u16(segment_range_min_zzzz_extent_xxxx_u16));
-
-					const uint32x4_t segment_range_extent_xxxx_u32 = vmovl_u16(vget_high_u16(segment_range_min_zzzz_extent_xxxx_u16));
-					const uint32x4_t segment_range_extent_yyyy_u32 = vmovl_u16(vget_low_u16(segment_range_extent_yyyy_zzzz_u16));
-					const uint32x4_t segment_range_extent_zzzz_u32 = vmovl_u16(vget_high_u16(segment_range_extent_yyyy_zzzz_u16));
-
-					// Mask out the segment min we ignore
-					const uint32x4_t segment_range_ignore_mask_u32 = vreinterpretq_u32_s32(vmovl_s16(vget_low_s16(ignore_masks_v16)));
-
-					segment_range_min_xxxx_u32 = vbicq_u32(segment_range_min_xxxx_u32, segment_range_ignore_mask_u32);
-					segment_range_min_yyyy_u32 = vbicq_u32(segment_range_min_yyyy_u32, segment_range_ignore_mask_u32);
-					segment_range_min_zzzz_u32 = vbicq_u32(segment_range_min_zzzz_u32, segment_range_ignore_mask_u32);
-
-					float32x4_t segment_range_min_xxxx = vcvtq_f32_u32(segment_range_min_xxxx_u32);
-					float32x4_t segment_range_min_yyyy = vcvtq_f32_u32(segment_range_min_yyyy_u32);
-					float32x4_t segment_range_min_zzzz = vcvtq_f32_u32(segment_range_min_zzzz_u32);
-
-					float32x4_t segment_range_extent_xxxx = vcvtq_f32_u32(segment_range_extent_xxxx_u32);
-					float32x4_t segment_range_extent_yyyy = vcvtq_f32_u32(segment_range_extent_yyyy_u32);
-					float32x4_t segment_range_extent_zzzz = vcvtq_f32_u32(segment_range_extent_zzzz_u32);
-
-					const float normalization_value = 1.0F / 255.0F;
-
-					segment_range_min_xxxx = vmulq_n_f32(segment_range_min_xxxx, normalization_value);
-					segment_range_min_yyyy = vmulq_n_f32(segment_range_min_yyyy, normalization_value);
-					segment_range_min_zzzz = vmulq_n_f32(segment_range_min_zzzz, normalization_value);
-
-					segment_range_extent_xxxx = vmulq_n_f32(segment_range_extent_xxxx, normalization_value);
-					segment_range_extent_yyyy = vmulq_n_f32(segment_range_extent_yyyy, normalization_value);
-					segment_range_extent_zzzz = vmulq_n_f32(segment_range_extent_zzzz, normalization_value);
-
-					const rtm::mask4f segment_range_ignore_mask_v = vreinterpretq_f32_u32(segment_range_ignore_mask_u32);
-#else
-					rtm::vector4f segment_range_min_xxxx = rtm::vector_set(float(segment_range_data[0]), float(segment_range_data[1]), float(segment_range_data[2]), float(segment_range_data[3]));
-					rtm::vector4f segment_range_min_yyyy = rtm::vector_set(float(segment_range_data[4]), float(segment_range_data[5]), float(segment_range_data[6]), float(segment_range_data[7]));
-					rtm::vector4f segment_range_min_zzzz = rtm::vector_set(float(segment_range_data[8]), float(segment_range_data[9]), float(segment_range_data[10]), float(segment_range_data[11]));
-
-					rtm::vector4f segment_range_extent_xxxx = rtm::vector_set(float(segment_range_data[12]), float(segment_range_data[13]), float(segment_range_data[14]), float(segment_range_data[15]));
-					rtm::vector4f segment_range_extent_yyyy = rtm::vector_set(float(segment_range_data[16]), float(segment_range_data[17]), float(segment_range_data[18]), float(segment_range_data[19]));
-					rtm::vector4f segment_range_extent_zzzz = rtm::vector_set(float(segment_range_data[20]), float(segment_range_data[21]), float(segment_range_data[22]), float(segment_range_data[23]));
-
-					const float normalization_value = 1.0F / 255.0F;
-
-					segment_range_min_xxxx = rtm::vector_mul(segment_range_min_xxxx, normalization_value);
-					segment_range_min_yyyy = rtm::vector_mul(segment_range_min_yyyy, normalization_value);
-					segment_range_min_zzzz = rtm::vector_mul(segment_range_min_zzzz, normalization_value);
-
-					segment_range_extent_xxxx = rtm::vector_mul(segment_range_extent_xxxx, normalization_value);
-					segment_range_extent_yyyy = rtm::vector_mul(segment_range_extent_yyyy, normalization_value);
-					segment_range_extent_zzzz = rtm::vector_mul(segment_range_extent_zzzz, normalization_value);
-
-					// Mask out the segment min we ignore
-					if (segment_range_ignore_mask & 0x000000FF)
-					{
-						segment_range_min_xxxx = rtm::vector_set_x(segment_range_min_xxxx, 0.0F);
-						segment_range_min_yyyy = rtm::vector_set_x(segment_range_min_yyyy, 0.0F);
-						segment_range_min_zzzz = rtm::vector_set_x(segment_range_min_zzzz, 0.0F);
-					}
-
-					if (segment_range_ignore_mask & 0x0000FF00)
-					{
-						segment_range_min_xxxx = rtm::vector_set_y(segment_range_min_xxxx, 0.0F);
-						segment_range_min_yyyy = rtm::vector_set_y(segment_range_min_yyyy, 0.0F);
-						segment_range_min_zzzz = rtm::vector_set_y(segment_range_min_zzzz, 0.0F);
-					}
-
-					if (segment_range_ignore_mask & 0x00FF0000)
-					{
-						segment_range_min_xxxx = rtm::vector_set_z(segment_range_min_xxxx, 0.0F);
-						segment_range_min_yyyy = rtm::vector_set_z(segment_range_min_yyyy, 0.0F);
-						segment_range_min_zzzz = rtm::vector_set_z(segment_range_min_zzzz, 0.0F);
-					}
-
-					if (segment_range_ignore_mask & 0xFF000000)
-					{
-						segment_range_min_xxxx = rtm::vector_set_w(segment_range_min_xxxx, 0.0F);
-						segment_range_min_yyyy = rtm::vector_set_w(segment_range_min_yyyy, 0.0F);
-						segment_range_min_zzzz = rtm::vector_set_w(segment_range_min_zzzz, 0.0F);
-					}
-
-					const rtm::mask4f segment_range_ignore_mask_v = rtm::mask_set((segment_range_ignore_mask & 0x000000FF) != 0, (segment_range_ignore_mask & 0x0000FF00) != 0, (segment_range_ignore_mask & 0x00FF0000) != 0, (segment_range_ignore_mask & 0xFF000000) != 0);
-#endif
-
-					// Mask out the segment extent we ignore
-					segment_range_extent_xxxx = rtm::vector_select(segment_range_ignore_mask_v, one_v, segment_range_extent_xxxx);
-					segment_range_extent_yyyy = rtm::vector_select(segment_range_ignore_mask_v, one_v, segment_range_extent_yyyy);
-					segment_range_extent_zzzz = rtm::vector_select(segment_range_ignore_mask_v, one_v, segment_range_extent_zzzz);
-
-					sample_xxxx = rtm::vector_mul_add(sample_xxxx, segment_range_extent_xxxx, segment_range_min_xxxx);
-					sample_yyyy = rtm::vector_mul_add(sample_yyyy, segment_range_extent_yyyy, segment_range_min_yyyy);
-					sample_zzzz = rtm::vector_mul_add(sample_zzzz, segment_range_extent_zzzz, segment_range_min_zzzz);
-				}
-
-#if defined(RTM_SSE2_INTRINSICS)
-				clip_range_ignore_mask_v32f = _mm_castsi128_ps(_mm_unpackhi_epi16(ignore_masks_v16, ignore_masks_v16));
-#elif defined(RTM_NEON_INTRINSICS)
-				clip_range_ignore_mask_v32f = vreinterpretq_f32_s32(vmovl_s16(vget_high_s16(ignore_masks_v16)));
+				range_reduction_masks = vmovl_s8(ignore_masks_v8);
 #else
-				clip_range_ignore_mask_v32f = rtm::mask_set((clip_range_ignore_mask & 0x000000FF) != 0, (clip_range_ignore_mask & 0x0000FF00) != 0, (clip_range_ignore_mask & 0x00FF0000) != 0, (clip_range_ignore_mask & 0xFF000000) != 0);
+				range_reduction_masks = (uint64_t(clip_range_ignore_mask) << 32) | segment_range_ignore_mask;
 #endif
 
 				// Skip our used segment range data, all groups are padded to 4 elements
@@ -409,35 +743,19 @@ namespace acl
 
 				// Update our ptr
 				segment_sampling_context.segment_range_data = segment_range_data;
-
-				// Prefetch the next cache line even if we don't have any data left
-				// By the time we unpack again, it will have arrived in the CPU cache
-				// If our format is full precision, we have at most 4 samples per cache line
-				// If our format is drop W, we have at most 5.33 samples per cache line
-
-				// If our pointer was already aligned to a cache line before we unpacked our 4 values,
-				// it now points to the first byte of the next cache line. Any offset between 0-63 will fetch it.
-				// If our pointer had some offset into a cache line, we might have spanned 2 cache lines.
-				// If this happens, we probably already read some data from the next cache line in which
-				// case we don't need to prefetch it and we can go to the next one. Any offset after the end
-				// of this cache line will fetch it. For safety, we prefetch 63 bytes ahead.
-				// Prefetch 4 samples ahead in all levels of the CPU cache
-				ACL_IMPL_ANIMATED_PREFETCH(segment_range_data + 63);
 			}
 			else
 			{
-				const rtm::vector4f sample_wwww = rtm::vector_mix<rtm::mix4::y, rtm::mix4::w, rtm::mix4::b, rtm::mix4::d>(tmp1, tmp3);
-				output_scratch[3] = sample_wwww;
-
-				// TODO: Optimize for SSE/NEON, codegen for this might not be optimal
-				clip_range_ignore_mask_v32f = rtm::mask_set(0U, 0U, 0U, 0U);	// Won't be used, just initialize it to something
+#if defined(RTM_SSE2_INTRINSICS)
+				range_reduction_masks = _mm_setzero_si128();
+#elif defined(RTM_NEON_INTRINSICS)
+				range_reduction_masks = vcombine_s16(vcreate_s16(0ULL), vcreate_s16(0ULL));
+#else
+				range_reduction_masks = 0ULL;
+#endif
 			}
 
-			output_scratch[0] = sample_xxxx;
-			output_scratch[1] = sample_yyyy;
-			output_scratch[2] = sample_zzzz;
-
-			return clip_range_ignore_mask_v32f;
+			return range_reduction_masks;
 		}
 
 		template<class decompression_settings_type>
@@ -720,9 +1038,9 @@ namespace acl
 			// case we don't need to prefetch it and we can go to the next one. Any offset after the end
 			// of this cache line will fetch it. For safety, we prefetch 63 bytes ahead.
 			// Prefetch 4 samples ahead in all levels of the CPU cache
-			ACL_IMPL_ANIMATED_PREFETCH(format_per_track_data + 63);
+			ACL_IMPL_ANIMATED_PREFETCH(format_per_track_data + 60);
 			ACL_IMPL_ANIMATED_PREFETCH(animated_track_data + (animated_track_data_bit_offset / 8) + 63);
-			ACL_IMPL_ANIMATED_PREFETCH(segment_range_data + 63);
+			ACL_IMPL_ANIMATED_PREFETCH(segment_range_data + 48);
 		}
 
 		template<class decompression_settings_adapter_type>
@@ -823,6 +1141,66 @@ namespace acl
 			return sample;
 		}
 
+		// Force inline this function, we only use it to keep the code readable
+		ACL_FORCE_INLINE ACL_DISABLE_SECURITY_COOKIE_CHECK void count_animated_group_bit_size(
+			const uint8_t* format_per_track_data0, const uint8_t* format_per_track_data1, uint32_t num_groups_to_skip,
+			uint32_t& out_group_bit_size_per_component0, uint32_t& out_group_bit_size_per_component1)
+		{
+			// TODO: Do the same with NEON
+#if defined(RTM_AVX_INTRINSICS)
+			__m128i zero = _mm_setzero_si128();
+			__m128i group_bit_size_per_component0_v = zero;
+			__m128i group_bit_size_per_component1_v = zero;
+
+			// We add 4 at a time in SIMD
+			for (uint32_t group_index = 0; group_index < num_groups_to_skip; ++group_index)
+			{
+				const __m128i group_bit_size_per_component0_u8 = _mm_loadu_si128(reinterpret_cast<const __m128i*>(format_per_track_data0 + (group_index * 4)));
+				const __m128i group_bit_size_per_component1_u8 = _mm_loadu_si128(reinterpret_cast<const __m128i*>(format_per_track_data1 + (group_index * 4)));
+
+				group_bit_size_per_component0_v = _mm_add_epi32(group_bit_size_per_component0_v, _mm_unpacklo_epi16(_mm_unpacklo_epi8(group_bit_size_per_component0_u8, zero), zero));
+				group_bit_size_per_component1_v = _mm_add_epi32(group_bit_size_per_component1_v, _mm_unpacklo_epi16(_mm_unpacklo_epi8(group_bit_size_per_component1_u8, zero), zero));
+			}
+
+			// Now we sum horizontally
+			group_bit_size_per_component0_v = _mm_hadd_epi32(_mm_hadd_epi32(group_bit_size_per_component0_v, group_bit_size_per_component0_v), group_bit_size_per_component0_v);
+			group_bit_size_per_component1_v = _mm_hadd_epi32(_mm_hadd_epi32(group_bit_size_per_component1_v, group_bit_size_per_component1_v), group_bit_size_per_component1_v);
+
+			out_group_bit_size_per_component0 = _mm_cvtsi128_si32(group_bit_size_per_component0_v);
+			out_group_bit_size_per_component1 = _mm_cvtsi128_si32(group_bit_size_per_component1_v);
+#else
+			uint32_t group_bit_size_per_component0 = 0;
+			uint32_t group_bit_size_per_component1 = 0;
+
+			for (uint32_t group_index = 0; group_index < num_groups_to_skip; ++group_index)
+			{
+				group_bit_size_per_component0 += format_per_track_data0[(group_index * 4) + 0];
+				group_bit_size_per_component1 += format_per_track_data1[(group_index * 4) + 0];
+
+				group_bit_size_per_component0 += format_per_track_data0[(group_index * 4) + 1];
+				group_bit_size_per_component1 += format_per_track_data1[(group_index * 4) + 1];
+
+				group_bit_size_per_component0 += format_per_track_data0[(group_index * 4) + 2];
+				group_bit_size_per_component1 += format_per_track_data1[(group_index * 4) + 2];
+
+				group_bit_size_per_component0 += format_per_track_data0[(group_index * 4) + 3];
+				group_bit_size_per_component1 += format_per_track_data1[(group_index * 4) + 3];
+			}
+
+			out_group_bit_size_per_component0 = group_bit_size_per_component0;
+			out_group_bit_size_per_component1 = group_bit_size_per_component1;
+#endif
+		}
+
+		// Performance notes:
+		//    - Using SOA after unpacking vec3 appears to be slightly slower. Full groups aren't super common
+		//      because animated translation/scale isn't common. But even with clips with lots of full groups,
+		//      SOA remains slightly slower. It seems the longer dependency chains offsets the gain from
+		//      using all SIMD lanes.
+		//    - Removing the unpack prefetches seems to harm performance, especially on mobile
+		//      I also tried reworking them to be more optimal for single segment usage but while I could get
+		//      a small win on desktop, mobile remained under performing.
+
 		struct animated_track_cache_v0
 		{
 			track_cache_quatf_v0 rotations;
@@ -833,49 +1211,100 @@ namespace acl
 			rtm::vector4f scratch0[4];
 			rtm::vector4f scratch1[4];
 
-			clip_animated_sampling_context_v0 clip_sampling_context;
+			clip_animated_sampling_context_v0 clip_sampling_context_rotations;
+			clip_animated_sampling_context_v0 clip_sampling_context_translations;
+			clip_animated_sampling_context_v0 clip_sampling_context_scales;
 
-			segment_animated_sampling_context_v0 segment_sampling_context[2];
+			segment_animated_sampling_context_v0 segment_sampling_context_rotations[2];
+			segment_animated_sampling_context_v0 segment_sampling_context_translations[2];
+			segment_animated_sampling_context_v0 segment_sampling_context_scales[2];
 
-			ACL_DISABLE_SECURITY_COOKIE_CHECK void get_rotation_cursor(animated_group_cursor_v0& cursor) const
+			template<class decompression_settings_type, class decompression_settings_translation_adapter_type>
+			void ACL_DISABLE_SECURITY_COOKIE_CHECK initialize(const persistent_transform_decompression_context_v0& decomp_context)
 			{
-				cursor.clip_sampling_context = clip_sampling_context;
-				cursor.segment_sampling_context[0] = segment_sampling_context[0];
-				cursor.segment_sampling_context[1] = segment_sampling_context[1];
-				cursor.group_size = std::min<uint32_t>(rotations.num_left_to_unpack, 4);
-			}
+				const transform_tracks_header& transform_header = get_transform_tracks_header(*decomp_context.tracks);
 
-			ACL_DISABLE_SECURITY_COOKIE_CHECK void get_translation_cursor(animated_group_cursor_v0& cursor) const
-			{
-				cursor.clip_sampling_context = clip_sampling_context;
-				cursor.segment_sampling_context[0] = segment_sampling_context[0];
-				cursor.segment_sampling_context[1] = segment_sampling_context[1];
-				cursor.group_size = std::min<uint32_t>(translations.num_left_to_unpack, 4);
-			}
+				const segment_header* segment0 = decomp_context.segment_offsets[0].add_to(decomp_context.tracks);
+				const segment_header* segment1 = decomp_context.segment_offsets[1].add_to(decomp_context.tracks);
 
-			ACL_DISABLE_SECURITY_COOKIE_CHECK void get_scale_cursor(animated_group_cursor_v0& cursor) const
-			{
-				cursor.clip_sampling_context = clip_sampling_context;
-				cursor.segment_sampling_context[0] = segment_sampling_context[0];
-				cursor.segment_sampling_context[1] = segment_sampling_context[1];
-				cursor.group_size = std::min<uint32_t>(scales.num_left_to_unpack, 4);
-			}
+				const uint8_t* animated_track_data0 = decomp_context.animated_track_data[0];
+				const uint8_t* animated_track_data1 = decomp_context.animated_track_data[1];
 
-			void ACL_DISABLE_SECURITY_COOKIE_CHECK initialize(const persistent_transform_decompression_context_v0& decomp_context)
-			{
-				clip_sampling_context.clip_range_data = decomp_context.clip_range_data.add_to(decomp_context.tracks);
+				const uint8_t* clip_range_data_rotations = transform_header.get_clip_range_data();
+				clip_sampling_context_rotations.clip_range_data = clip_range_data_rotations;
 
-				segment_sampling_context[0].format_per_track_data = decomp_context.format_per_track_data[0];
-				segment_sampling_context[0].segment_range_data = decomp_context.segment_range_data[0];
-				segment_sampling_context[0].animated_track_data = decomp_context.animated_track_data[0];
-				segment_sampling_context[0].animated_track_data_bit_offset = decomp_context.key_frame_bit_offsets[0];
+				const uint8_t* format_per_track_data_rotations0 = decomp_context.format_per_track_data[0];
+				const uint8_t* segment_range_data_rotations0 = decomp_context.segment_range_data[0];
+				const uint32_t animated_track_data_bit_offset_rotations0 = decomp_context.key_frame_bit_offsets[0];
+				segment_sampling_context_rotations[0].format_per_track_data = format_per_track_data_rotations0;
+				segment_sampling_context_rotations[0].segment_range_data = segment_range_data_rotations0;
+				segment_sampling_context_rotations[0].animated_track_data = animated_track_data0;
+				segment_sampling_context_rotations[0].animated_track_data_bit_offset = animated_track_data_bit_offset_rotations0;
 
-				segment_sampling_context[1].format_per_track_data = decomp_context.format_per_track_data[1];
-				segment_sampling_context[1].segment_range_data = decomp_context.segment_range_data[1];
-				segment_sampling_context[1].animated_track_data = decomp_context.animated_track_data[1];
-				segment_sampling_context[1].animated_track_data_bit_offset = decomp_context.key_frame_bit_offsets[1];
+				const uint8_t* format_per_track_data_rotations1 = decomp_context.format_per_track_data[1];
+				const uint8_t* segment_range_data_rotations1 = decomp_context.segment_range_data[1];
+				const uint32_t animated_track_data_bit_offset_rotations1 = decomp_context.key_frame_bit_offsets[1];
+				segment_sampling_context_rotations[1].format_per_track_data = format_per_track_data_rotations1;
+				segment_sampling_context_rotations[1].segment_range_data = segment_range_data_rotations1;
+				segment_sampling_context_rotations[1].animated_track_data = animated_track_data1;
+				segment_sampling_context_rotations[1].animated_track_data_bit_offset = animated_track_data_bit_offset_rotations1;
 
-				const transform_tracks_header& transform_header = get_transform_tracks_header(*decomp_context.tracks);
+				const rotation_format8 rotation_format = get_rotation_format<decompression_settings_type>(decomp_context.rotation_format);
+				const bool are_rotations_variable = rotation_format == rotation_format8::quatf_drop_w_variable && decompression_settings_type::is_rotation_format_supported(rotation_format8::quatf_drop_w_variable);
+
+				const uint32_t num_animated_rotation_sub_tracks_padded = align_to(transform_header.num_animated_rotation_sub_tracks, 4);
+
+				// Rotation range data follows translations, no padding
+				const uint32_t rotation_clip_range_data_size = are_rotations_variable ? (sizeof(rtm::float3f) * 2) : 0;
+				const uint8_t* clip_range_data_translations = clip_range_data_rotations + (transform_header.num_animated_rotation_sub_tracks * rotation_clip_range_data_size);
+				clip_sampling_context_translations.clip_range_data = clip_range_data_translations;
+
+				// Rotation metadata is padded to 4 sub-tracks (1 byte each)
+				const uint32_t rotation_per_track_metadata_size = are_rotations_variable ? 1 : 0;
+				const uint8_t* format_per_track_data_translations0 = format_per_track_data_rotations0 + (num_animated_rotation_sub_tracks_padded * rotation_per_track_metadata_size);
+				const uint8_t* format_per_track_data_translations1 = format_per_track_data_rotations1 + (num_animated_rotation_sub_tracks_padded * rotation_per_track_metadata_size);
+				segment_sampling_context_translations[0].format_per_track_data = format_per_track_data_translations0;
+				segment_sampling_context_translations[1].format_per_track_data = format_per_track_data_translations1;
+
+				// Rotation range data is padded to 4 sub-tracks (6 bytes each)
+				const uint32_t rotation_segment_range_data_size = are_rotations_variable ? 6 : 0;
+				const uint8_t* segment_range_data_translations0 = segment_range_data_rotations0 + (num_animated_rotation_sub_tracks_padded * rotation_segment_range_data_size);
+				const uint8_t* segment_range_data_translations1 = segment_range_data_rotations1 + (num_animated_rotation_sub_tracks_padded * rotation_segment_range_data_size);
+				segment_sampling_context_translations[0].segment_range_data = segment_range_data_translations0;
+				segment_sampling_context_translations[1].segment_range_data = segment_range_data_translations1;
+
+				// Every sub-track uses the same base animated track data pointer
+				segment_sampling_context_translations[0].animated_track_data = animated_track_data0;
+				segment_sampling_context_translations[1].animated_track_data = animated_track_data1;
+
+				const uint32_t animated_track_data_bit_offset_translations0 = animated_track_data_bit_offset_rotations0 + segment0->animated_rotation_bit_size;
+				const uint32_t animated_track_data_bit_offset_translations1 = animated_track_data_bit_offset_rotations1 + segment1->animated_rotation_bit_size;
+				segment_sampling_context_translations[0].animated_track_data_bit_offset = animated_track_data_bit_offset_translations0;
+				segment_sampling_context_translations[1].animated_track_data_bit_offset = animated_track_data_bit_offset_translations1;
+
+				if (decomp_context.has_scale)
+				{
+					const vector_format8 translation_format = get_vector_format<decompression_settings_translation_adapter_type>(decompression_settings_translation_adapter_type::get_vector_format(decomp_context));
+					const bool are_translations_variable = translation_format == vector_format8::vector3f_variable && decompression_settings_translation_adapter_type::is_vector_format_supported(vector_format8::vector3f_variable);
+
+					// Scale data just follows the translation data without any extra padding
+					const uint32_t translation_clip_range_data_size = are_translations_variable ? (sizeof(rtm::float3f) * 2) : 0;
+					clip_sampling_context_scales.clip_range_data = clip_range_data_translations + (transform_header.num_animated_translation_sub_tracks * translation_clip_range_data_size);
+
+					const uint32_t translation_per_track_metadata_size = are_translations_variable ? 1 : 0;
+					segment_sampling_context_scales[0].format_per_track_data = format_per_track_data_translations0 + (transform_header.num_animated_translation_sub_tracks * translation_per_track_metadata_size);
+					segment_sampling_context_scales[1].format_per_track_data = format_per_track_data_translations1 + (transform_header.num_animated_translation_sub_tracks * translation_per_track_metadata_size);
+
+					const uint32_t translation_segment_range_data_size = are_translations_variable ? 6 : 0;
+					segment_sampling_context_scales[0].segment_range_data = segment_range_data_translations0 + (transform_header.num_animated_translation_sub_tracks * translation_segment_range_data_size);
+					segment_sampling_context_scales[1].segment_range_data = segment_range_data_translations1 + (transform_header.num_animated_translation_sub_tracks * translation_segment_range_data_size);
+
+					segment_sampling_context_scales[0].animated_track_data = animated_track_data0;
+					segment_sampling_context_scales[1].animated_track_data = animated_track_data1;
+
+					segment_sampling_context_scales[0].animated_track_data_bit_offset = animated_track_data_bit_offset_translations0 + segment0->animated_translation_bit_size;
+					segment_sampling_context_scales[1].animated_track_data_bit_offset = animated_track_data_bit_offset_translations1 + segment1->animated_translation_bit_size;
+				}
 
 				rotations.num_left_to_unpack = transform_header.num_animated_rotation_sub_tracks;
 				translations.num_left_to_unpack = transform_header.num_animated_translation_sub_tracks;
@@ -885,7 +1314,7 @@ namespace acl
 			template<class decompression_settings_type>
 			void ACL_DISABLE_SECURITY_COOKIE_CHECK unpack_rotation_group(const persistent_transform_decompression_context_v0& decomp_context)
 			{
-				uint32_t num_left_to_unpack = rotations.num_left_to_unpack;
+				const uint32_t num_left_to_unpack = rotations.num_left_to_unpack;
 				if (num_left_to_unpack == 0)
 					return;	// Nothing left to do, we are done
 
@@ -895,252 +1324,173 @@ namespace acl
 					return;	// Enough cached, nothing to do
 
 				const uint32_t num_to_unpack = std::min<uint32_t>(num_left_to_unpack, 4);
-				num_left_to_unpack -= num_to_unpack;
-				rotations.num_left_to_unpack = num_left_to_unpack;
+				rotations.num_left_to_unpack = num_left_to_unpack - num_to_unpack;
 
 				// Write index will be either 0 or 4 here since we always unpack 4 at a time
-				uint32_t cache_write_index = rotations.cache_write_index % 8;
+				const uint32_t cache_write_index = rotations.cache_write_index % 8;
 				rotations.cache_write_index += num_to_unpack;
 
-				const rtm::mask4f clip_range_mask0 = unpack_animated_quat<decompression_settings_type>(decomp_context, scratch0, num_to_unpack, segment_sampling_context[0]);
-				const rtm::mask4f clip_range_mask1 = unpack_animated_quat<decompression_settings_type>(decomp_context, scratch1, num_to_unpack, segment_sampling_context[1]);
-
-				rtm::vector4f scratch0_xxxx = scratch0[0];
-				rtm::vector4f scratch0_yyyy = scratch0[1];
-				rtm::vector4f scratch0_zzzz = scratch0[2];
-				rtm::vector4f scratch0_wwww;
-
-				rtm::vector4f scratch1_xxxx = scratch1[0];
-				rtm::vector4f scratch1_yyyy = scratch1[1];
-				rtm::vector4f scratch1_zzzz = scratch1[2];
-				rtm::vector4f scratch1_wwww;
-
-#if defined(RTM_AVX_INTRINSICS) && defined(ACL_IMPL_USE_AVX_DECOMP)
-				__m256 scratch_xxxx0_xxxx1 = _mm256_set_m128(scratch1_xxxx, scratch0_xxxx);
-				__m256 scratch_yyyy0_yyyy1 = _mm256_set_m128(scratch1_yyyy, scratch0_yyyy);
-				__m256 scratch_zzzz0_zzzz1 = _mm256_set_m128(scratch1_zzzz, scratch0_zzzz);
+				const rotation_format8 rotation_format = get_rotation_format<decompression_settings_type>(decomp_context.rotation_format);
 
-				const __m256 one_v = _mm256_set1_ps(1.0F);
-#endif
+				segment_animated_scratch_v0 segment_scratch;
 
-				// If we have a variable bit rate, we perform range reduction, skip the data we used
-				const rotation_format8 rotation_format = get_rotation_format<decompression_settings_type>(decomp_context.rotation_format);
+				// We start by unpacking our segment range data into our scratch memory
+				// We often only use a single segment to interpolate, we can avoid redundant work
 				if (rotation_format == rotation_format8::quatf_drop_w_variable && decompression_settings_type::is_rotation_format_supported(rotation_format8::quatf_drop_w_variable))
 				{
-					const uint8_t* clip_range_data = clip_sampling_context.clip_range_data;
+					if (decomp_context.has_segments)
+					{
+						unpack_segment_range_data(segment_sampling_context_rotations[0].segment_range_data, 0, segment_scratch);
 
-					// Always load 4x rotations, we might contain garbage in a few lanes but it's fine
-					const uint32_t load_size = num_to_unpack * sizeof(float);
+						// We are interpolating between two segments (rare)
+						if (!decomp_context.uses_single_segment)
+							unpack_segment_range_data(segment_sampling_context_rotations[1].segment_range_data, 1, segment_scratch);
 
-#if defined(RTM_AVX_INTRINSICS) && defined(ACL_IMPL_USE_AVX_DECOMP)
-					__m256 clip_range_mask0_mask1 = _mm256_set_m128(clip_range_mask1, clip_range_mask0);
+#if !defined(ACL_IMPL_PREFETCH_EARLY)
+						// Our segment range data takes 24 bytes per group (4 samples, 6 bytes each), each cache line fits 2.67 groups
+						// Prefetch every time while alternating between both segments
+						ACL_IMPL_ANIMATED_PREFETCH(segment_sampling_context_rotations[cache_write_index % 2].segment_range_data + 64);
 #endif
+					}
+				}
 
-					const rtm::vector4f clip_range_min_xxxx = rtm::vector_load(clip_range_data + load_size * 0);
-					const rtm::vector4f clip_range_min_yyyy = rtm::vector_load(clip_range_data + load_size * 1);
-					const rtm::vector4f clip_range_min_zzzz = rtm::vector_load(clip_range_data + load_size * 2);
+				const range_reduction_masks_t range_reduction_masks0 = unpack_animated_quat<decompression_settings_type>(decomp_context, scratch0, num_to_unpack, segment_sampling_context_rotations[0]);
+				const range_reduction_masks_t range_reduction_masks1 = unpack_animated_quat<decompression_settings_type>(decomp_context, scratch1, num_to_unpack, segment_sampling_context_rotations[1]);
 
-					const rtm::vector4f clip_range_extent_xxxx = rtm::vector_load(clip_range_data + load_size * 3);
-					const rtm::vector4f clip_range_extent_yyyy = rtm::vector_load(clip_range_data + load_size * 4);
-					const rtm::vector4f clip_range_extent_zzzz = rtm::vector_load(clip_range_data + load_size * 5);
+				// Swizzle our samples into SOA form
+				rtm::vector4f scratch0_xxxx;
+				rtm::vector4f scratch0_yyyy;
+				rtm::vector4f scratch0_zzzz;
+				rtm::vector4f scratch0_wwww;
+				RTM_MATRIXF_TRANSPOSE_4X4(scratch0[0], scratch0[1], scratch0[2], scratch0[3], scratch0_xxxx, scratch0_yyyy, scratch0_zzzz, scratch0_wwww);
 
-#if defined(RTM_AVX_INTRINSICS) && defined(ACL_IMPL_USE_AVX_DECOMP)
-					__m256 clip_range_min_xxxx_xxxx = _mm256_set_m128(clip_range_min_xxxx, clip_range_min_xxxx);
-					__m256 clip_range_min_yyyy_yyyy = _mm256_set_m128(clip_range_min_yyyy, clip_range_min_yyyy);
-					__m256 clip_range_min_zzzz_zzzz = _mm256_set_m128(clip_range_min_zzzz, clip_range_min_zzzz);
+				rtm::vector4f scratch1_xxxx;
+				rtm::vector4f scratch1_yyyy;
+				rtm::vector4f scratch1_zzzz;
+				rtm::vector4f scratch1_wwww;
+				RTM_MATRIXF_TRANSPOSE_4X4(scratch1[0], scratch1[1], scratch1[2], scratch1[3], scratch1_xxxx, scratch1_yyyy, scratch1_zzzz, scratch1_wwww);
 
-					__m256 clip_range_extent_xxxx_xxxx = _mm256_set_m128(clip_range_extent_xxxx, clip_range_extent_xxxx);
-					__m256 clip_range_extent_yyyy_yyyy = _mm256_set_m128(clip_range_extent_yyyy, clip_range_extent_yyyy);
-					__m256 clip_range_extent_zzzz_zzzz = _mm256_set_m128(clip_range_extent_zzzz, clip_range_extent_zzzz);
+#if defined(ACL_IMPL_USE_AVX_8_WIDE_DECOMP)
+				__m256 scratch_xxxx0_xxxx1 = _mm256_set_m128(scratch1_xxxx, scratch0_xxxx);
+				__m256 scratch_yyyy0_yyyy1 = _mm256_set_m128(scratch1_yyyy, scratch0_yyyy);
+				__m256 scratch_zzzz0_zzzz1 = _mm256_set_m128(scratch1_zzzz, scratch0_zzzz);
 #endif
 
-					// Mask out the clip ranges we ignore
-#if defined(RTM_AVX_INTRINSICS) && defined(ACL_IMPL_USE_AVX_DECOMP)
-					clip_range_min_xxxx_xxxx = _mm256_andnot_ps(clip_range_mask0_mask1, clip_range_min_xxxx_xxxx);
-					clip_range_min_yyyy_yyyy = _mm256_andnot_ps(clip_range_mask0_mask1, clip_range_min_yyyy_yyyy);
-					clip_range_min_zzzz_zzzz = _mm256_andnot_ps(clip_range_mask0_mask1, clip_range_min_zzzz_zzzz);
-#elif defined(RTM_SSE2_INTRINSICS)
-					const rtm::vector4f clip_range_min_xxxx0 = _mm_andnot_ps(clip_range_mask0, clip_range_min_xxxx);
-					const rtm::vector4f clip_range_min_yyyy0 = _mm_andnot_ps(clip_range_mask0, clip_range_min_yyyy);
-					const rtm::vector4f clip_range_min_zzzz0 = _mm_andnot_ps(clip_range_mask0, clip_range_min_zzzz);
-
-					const rtm::vector4f clip_range_min_xxxx1 = _mm_andnot_ps(clip_range_mask1, clip_range_min_xxxx);
-					const rtm::vector4f clip_range_min_yyyy1 = _mm_andnot_ps(clip_range_mask1, clip_range_min_yyyy);
-					const rtm::vector4f clip_range_min_zzzz1 = _mm_andnot_ps(clip_range_mask1, clip_range_min_zzzz);
-#elif defined(RTM_NEON_INTRINSICS)
-					const rtm::vector4f clip_range_min_xxxx0 = vreinterpretq_f32_u32(vbicq_u32(vreinterpretq_u32_f32(clip_range_min_xxxx), vreinterpretq_u32_f32(clip_range_mask0)));
-					const rtm::vector4f clip_range_min_yyyy0 = vreinterpretq_f32_u32(vbicq_u32(vreinterpretq_u32_f32(clip_range_min_yyyy), vreinterpretq_u32_f32(clip_range_mask0)));
-					const rtm::vector4f clip_range_min_zzzz0 = vreinterpretq_f32_u32(vbicq_u32(vreinterpretq_u32_f32(clip_range_min_zzzz), vreinterpretq_u32_f32(clip_range_mask0)));
-
-					const rtm::vector4f clip_range_min_xxxx1 = vreinterpretq_f32_u32(vbicq_u32(vreinterpretq_u32_f32(clip_range_min_xxxx), vreinterpretq_u32_f32(clip_range_mask1)));
-					const rtm::vector4f clip_range_min_yyyy1 = vreinterpretq_f32_u32(vbicq_u32(vreinterpretq_u32_f32(clip_range_min_yyyy), vreinterpretq_u32_f32(clip_range_mask1)));
-					const rtm::vector4f clip_range_min_zzzz1 = vreinterpretq_f32_u32(vbicq_u32(vreinterpretq_u32_f32(clip_range_min_zzzz), vreinterpretq_u32_f32(clip_range_mask1)));
+				// If we have a variable bit rate, we perform range reduction, skip the data we used
+				if (rotation_format == rotation_format8::quatf_drop_w_variable && decompression_settings_type::is_rotation_format_supported(rotation_format8::quatf_drop_w_variable))
+				{
+					if (decomp_context.has_segments)
+					{
+#if defined(ACL_IMPL_USE_AVX_8_WIDE_DECOMP)
+						remap_segment_range_data_avx8(segment_scratch, range_reduction_masks0, range_reduction_masks1, scratch_xxxx0_xxxx1, scratch_yyyy0_yyyy1, scratch_zzzz0_zzzz1);
 #else
-					const rtm::vector4f zero_v = rtm::vector_zero();
-
-					const rtm::vector4f clip_range_min_xxxx0 = rtm::vector_select(clip_range_mask0, zero_v, clip_range_min_xxxx);
-					const rtm::vector4f clip_range_min_yyyy0 = rtm::vector_select(clip_range_mask0, zero_v, clip_range_min_yyyy);
-					const rtm::vector4f clip_range_min_zzzz0 = rtm::vector_select(clip_range_mask0, zero_v, clip_range_min_zzzz);
-
-					const rtm::vector4f clip_range_min_xxxx1 = rtm::vector_select(clip_range_mask1, zero_v, clip_range_min_xxxx);
-					const rtm::vector4f clip_range_min_yyyy1 = rtm::vector_select(clip_range_mask1, zero_v, clip_range_min_yyyy);
-					const rtm::vector4f clip_range_min_zzzz1 = rtm::vector_select(clip_range_mask1, zero_v, clip_range_min_zzzz);
+						remap_segment_range_data4(segment_scratch, 0, range_reduction_masks0, scratch0_xxxx, scratch0_yyyy, scratch0_zzzz);
+						remap_segment_range_data4(segment_scratch, uint32_t(!decomp_context.uses_single_segment), range_reduction_masks1, scratch1_xxxx, scratch1_yyyy, scratch1_zzzz);
 #endif
+					}
 
-#if defined(RTM_AVX_INTRINSICS) && defined(ACL_IMPL_USE_AVX_DECOMP)
-					clip_range_extent_xxxx_xxxx = _mm256_blendv_ps(clip_range_extent_xxxx_xxxx, one_v, clip_range_mask0_mask1);
-					clip_range_extent_yyyy_yyyy = _mm256_blendv_ps(clip_range_extent_yyyy_yyyy, one_v, clip_range_mask0_mask1);
-					clip_range_extent_zzzz_zzzz = _mm256_blendv_ps(clip_range_extent_zzzz_zzzz, one_v, clip_range_mask0_mask1);
+					const uint8_t* clip_range_data = clip_sampling_context_rotations.clip_range_data;
 
-					scratch_xxxx0_xxxx1 = _mm256_add_ps(_mm256_mul_ps(scratch_xxxx0_xxxx1, clip_range_extent_xxxx_xxxx), clip_range_min_xxxx_xxxx);
-					scratch_yyyy0_yyyy1 = _mm256_add_ps(_mm256_mul_ps(scratch_yyyy0_yyyy1, clip_range_extent_yyyy_yyyy), clip_range_min_yyyy_yyyy);
-					scratch_zzzz0_zzzz1 = _mm256_add_ps(_mm256_mul_ps(scratch_zzzz0_zzzz1, clip_range_extent_zzzz_zzzz), clip_range_min_zzzz_zzzz);
+#if defined(ACL_IMPL_USE_AVX_8_WIDE_DECOMP)
+					remap_clip_range_data_avx8(clip_range_data, num_to_unpack, range_reduction_masks0, range_reduction_masks1, scratch_xxxx0_xxxx1, scratch_yyyy0_yyyy1, scratch_zzzz0_zzzz1);
 #else
-					const rtm::vector4f one_v = rtm::vector_set(1.0F);
-
-					const rtm::vector4f clip_range_extent_xxxx0 = rtm::vector_select(clip_range_mask0, one_v, clip_range_extent_xxxx);
-					const rtm::vector4f clip_range_extent_yyyy0 = rtm::vector_select(clip_range_mask0, one_v, clip_range_extent_yyyy);
-					const rtm::vector4f clip_range_extent_zzzz0 = rtm::vector_select(clip_range_mask0, one_v, clip_range_extent_zzzz);
-
-					const rtm::vector4f clip_range_extent_xxxx1 = rtm::vector_select(clip_range_mask1, one_v, clip_range_extent_xxxx);
-					const rtm::vector4f clip_range_extent_yyyy1 = rtm::vector_select(clip_range_mask1, one_v, clip_range_extent_yyyy);
-					const rtm::vector4f clip_range_extent_zzzz1 = rtm::vector_select(clip_range_mask1, one_v, clip_range_extent_zzzz);
-
-					scratch0_xxxx = rtm::vector_mul_add(scratch0_xxxx, clip_range_extent_xxxx0, clip_range_min_xxxx0);
-					scratch0_yyyy = rtm::vector_mul_add(scratch0_yyyy, clip_range_extent_yyyy0, clip_range_min_yyyy0);
-					scratch0_zzzz = rtm::vector_mul_add(scratch0_zzzz, clip_range_extent_zzzz0, clip_range_min_zzzz0);
-
-					scratch1_xxxx = rtm::vector_mul_add(scratch1_xxxx, clip_range_extent_xxxx1, clip_range_min_xxxx1);
-					scratch1_yyyy = rtm::vector_mul_add(scratch1_yyyy, clip_range_extent_yyyy1, clip_range_min_yyyy1);
-					scratch1_zzzz = rtm::vector_mul_add(scratch1_zzzz, clip_range_extent_zzzz1, clip_range_min_zzzz1);
+					remap_clip_range_data4(clip_range_data, num_to_unpack, range_reduction_masks0, range_reduction_masks1, scratch0_xxxx, scratch0_yyyy, scratch0_zzzz, scratch1_xxxx, scratch1_yyyy, scratch1_zzzz);
 #endif
 
 					// Skip our data
 					clip_range_data += num_to_unpack * sizeof(rtm::float3f) * 2;
-					clip_sampling_context.clip_range_data = clip_range_data;
+					clip_sampling_context_rotations.clip_range_data = clip_range_data;
 
-					// Clip range data is 24-32 bytes per sub-track and as such we need to prefetch two cache lines ahead to process 4 sub-tracks
-					ACL_IMPL_ANIMATED_PREFETCH(clip_range_data + 63);
-					ACL_IMPL_ANIMATED_PREFETCH(clip_range_data + 127);
+#if defined(ACL_IMPL_PREFETCH_EARLY)
+					// Clip range data is 24 bytes per sub-track and as such we need to prefetch two cache lines ahead to process 4 sub-tracks
+					ACL_IMPL_ANIMATED_PREFETCH(clip_range_data + 64);
+					ACL_IMPL_ANIMATED_PREFETCH(clip_range_data + 128);
+#endif
 				}
 
-				// For interpolation later
-				rtm::vector4f xxxx_squared;
-				rtm::vector4f yyyy_squared;
-				rtm::vector4f zzzz_squared;
-
 				// Reconstruct our quaternion W component in SOA
 				if (rotation_format != rotation_format8::quatf_full || !decompression_settings_type::is_rotation_format_supported(rotation_format8::quatf_full))
 				{
-					// quat_from_positive_w_soa
-#if defined(RTM_AVX_INTRINSICS) && defined(ACL_IMPL_USE_AVX_DECOMP)
-					const __m256 scratch_xxxx0_xxxx1_squared = _mm256_mul_ps(scratch_xxxx0_xxxx1, scratch_xxxx0_xxxx1);
-					const __m256 scratch_yyyy0_yyyy1_squared = _mm256_mul_ps(scratch_yyyy0_yyyy1, scratch_yyyy0_yyyy1);
-					const __m256 scratch_zzzz0_zzzz1_squared = _mm256_mul_ps(scratch_zzzz0_zzzz1, scratch_zzzz0_zzzz1);
-
-					const __m256 scratch_wwww0_wwww1_squared = _mm256_sub_ps(_mm256_sub_ps(_mm256_sub_ps(one_v, scratch_xxxx0_xxxx1_squared), scratch_yyyy0_yyyy1_squared), scratch_zzzz0_zzzz1_squared);
-
-					const __m256i abs_mask = _mm256_set1_epi32(0x7FFFFFFFULL);
-					const __m256 scratch_wwww0_wwww1_squared_abs = _mm256_and_ps(scratch_wwww0_wwww1_squared, _mm256_castsi256_ps(abs_mask));
-
-					__m256 scratch_wwww0_wwww1 = _mm256_sqrt_ps(scratch_wwww0_wwww1_squared_abs);
+#if defined(ACL_IMPL_USE_AVX_8_WIDE_DECOMP)
+					const __m256 scratch_wwww0_wwww1 = quat_from_positive_w_avx8(scratch_xxxx0_xxxx1, scratch_yyyy0_yyyy1, scratch_zzzz0_zzzz1);
 
-					// Try and help the compiler hide the latency from the square-root
+					// This is the last AVX step, unpack everything
 					scratch0_xxxx = _mm256_extractf128_ps(scratch_xxxx0_xxxx1, 0);
 					scratch1_xxxx = _mm256_extractf128_ps(scratch_xxxx0_xxxx1, 1);
 					scratch0_yyyy = _mm256_extractf128_ps(scratch_yyyy0_yyyy1, 0);
 					scratch1_yyyy = _mm256_extractf128_ps(scratch_yyyy0_yyyy1, 1);
 					scratch0_zzzz = _mm256_extractf128_ps(scratch_zzzz0_zzzz1, 0);
 					scratch1_zzzz = _mm256_extractf128_ps(scratch_zzzz0_zzzz1, 1);
-
-					xxxx_squared = rtm::vector_mul(scratch0_xxxx, scratch1_xxxx);
-					yyyy_squared = rtm::vector_mul(scratch0_yyyy, scratch1_yyyy);
-					zzzz_squared = rtm::vector_mul(scratch0_zzzz, scratch1_zzzz);
-
 					scratch0_wwww = _mm256_extractf128_ps(scratch_wwww0_wwww1, 0);
 					scratch1_wwww = _mm256_extractf128_ps(scratch_wwww0_wwww1, 1);
 #else
-					const rtm::vector4f scratch0_xxxx_squared = rtm::vector_mul(scratch0_xxxx, scratch0_xxxx);
-					const rtm::vector4f scratch0_yyyy_squared = rtm::vector_mul(scratch0_yyyy, scratch0_yyyy);
-					const rtm::vector4f scratch0_zzzz_squared = rtm::vector_mul(scratch0_zzzz, scratch0_zzzz);
-					const rtm::vector4f scratch0_wwww_squared = rtm::vector_sub(rtm::vector_sub(rtm::vector_sub(rtm::vector_set(1.0F), scratch0_xxxx_squared), scratch0_yyyy_squared), scratch0_zzzz_squared);
-
-					const rtm::vector4f scratch1_xxxx_squared = rtm::vector_mul(scratch1_xxxx, scratch1_xxxx);
-					const rtm::vector4f scratch1_yyyy_squared = rtm::vector_mul(scratch1_yyyy, scratch1_yyyy);
-					const rtm::vector4f scratch1_zzzz_squared = rtm::vector_mul(scratch1_zzzz, scratch1_zzzz);
-					const rtm::vector4f scratch1_wwww_squared = rtm::vector_sub(rtm::vector_sub(rtm::vector_sub(rtm::vector_set(1.0F), scratch1_xxxx_squared), scratch1_yyyy_squared), scratch1_zzzz_squared);
-
-					// w_squared can be negative either due to rounding or due to quantization imprecision, we take the absolute value
-					// to ensure the resulting quaternion is always normalized with a positive W component
-					scratch0_wwww = rtm::vector_sqrt(rtm::vector_abs(scratch0_wwww_squared));
-					scratch1_wwww = rtm::vector_sqrt(rtm::vector_abs(scratch1_wwww_squared));
-
-					// Try and help the compiler hide the latency from the square-root
-					xxxx_squared = rtm::vector_mul(scratch0_xxxx, scratch1_xxxx);
-					yyyy_squared = rtm::vector_mul(scratch0_yyyy, scratch1_yyyy);
-					zzzz_squared = rtm::vector_mul(scratch0_zzzz, scratch1_zzzz);
+					scratch0_wwww = quat_from_positive_w4(scratch0_xxxx, scratch0_yyyy, scratch0_zzzz);
+
+#if !defined(ACL_IMPL_PREFETCH_EARLY)
+					if (rotation_format == rotation_format8::quatf_drop_w_variable && decompression_settings_type::is_rotation_format_supported(rotation_format8::quatf_drop_w_variable))
+					{
+						// Our segment per track metadata takes 4 bytes per group (4 samples, 1 byte each), each cache line fits 16 groups
+						// Prefetch every other 8th group
+						// We prefetch here because we have a square-root in quat_from_positive_w4(..) that we'll wait after
+						// This allows us to insert the prefetch basically for free in its shadow
+						// Branching is faster than prefetching every time and alternating between the two
+						if (cache_write_index == 0)
+							ACL_IMPL_ANIMATED_PREFETCH(segment_sampling_context_rotations[0].format_per_track_data + 64);
+						else if (cache_write_index == 4)
+							ACL_IMPL_ANIMATED_PREFETCH(segment_sampling_context_rotations[1].format_per_track_data + 64);
+					}
 #endif
-				}
-				else
-				{
-					xxxx_squared = rtm::vector_mul(scratch0_xxxx, scratch1_xxxx);
-					yyyy_squared = rtm::vector_mul(scratch0_yyyy, scratch1_yyyy);
-					zzzz_squared = rtm::vector_mul(scratch0_zzzz, scratch1_zzzz);
 
-					scratch0_wwww = scratch0[3];
-					scratch1_wwww = scratch1[3];
+					scratch1_wwww = quat_from_positive_w4(scratch1_xxxx, scratch1_yyyy, scratch1_zzzz);
+
+#if !defined(ACL_IMPL_PREFETCH_EARLY)
+					if (rotation_format == rotation_format8::quatf_drop_w_variable && decompression_settings_type::is_rotation_format_supported(rotation_format8::quatf_drop_w_variable))
+					{
+						// Our clip range data is 24 bytes per sub-track and as such we need to prefetch two cache lines ahead to process 4 sub-tracks
+						// Each group is 96 bytes (4 samples, 24 bytes each), each cache line fits 0.67 groups
+						// We prefetch here because we have a square-root in quat_from_positive_w4(..) that we'll wait after
+						// This allows us to insert the prefetch basically for free in its shadow
+						ACL_IMPL_ANIMATED_PREFETCH(clip_sampling_context_rotations.clip_range_data + 64);
+						ACL_IMPL_ANIMATED_PREFETCH(clip_sampling_context_rotations.clip_range_data + 128);
+					}
+#endif
+#endif
 				}
 
 				// Interpolate linearly and store our rotations in SOA
 				{
-					// Calculate the vector4 dot product: dot(start, end)
-					//const rtm::vector4f xxxx_squared = rtm::vector_mul(scratch0_xxxx, scratch1_xxxx);
-					//const rtm::vector4f yyyy_squared = rtm::vector_mul(scratch0_yyyy, scratch1_yyyy);
-					//const rtm::vector4f zzzz_squared = rtm::vector_mul(scratch0_zzzz, scratch1_zzzz);
-					const rtm::vector4f wwww_squared = rtm::vector_mul(scratch0_wwww, scratch1_wwww);
-
-					const rtm::vector4f dot4 = rtm::vector_add(rtm::vector_add(rtm::vector_add(xxxx_squared, yyyy_squared), zzzz_squared), wwww_squared);
-
-					// Calculate the bias, if the dot product is positive or zero, there is no bias
-					// but if it is negative, we want to flip the 'end' rotation XYZW components
-					const rtm::vector4f neg_zero = rtm::vector_set(-0.0F);
-					const rtm::vector4f bias = acl_impl::vector_and(dot4, neg_zero);
-
-					// Apply our bias to the 'end'
-					scratch1_xxxx = acl_impl::vector_xor(scratch1_xxxx, bias);
-					scratch1_yyyy = acl_impl::vector_xor(scratch1_yyyy, bias);
-					scratch1_zzzz = acl_impl::vector_xor(scratch1_zzzz, bias);
-					scratch1_wwww = acl_impl::vector_xor(scratch1_wwww, bias);
-
-					// Lerp the rotation after applying the bias
-					// ((1.0 - alpha) * start) + (alpha * (end ^ bias)) == (start - alpha * start) + (alpha * (end ^ bias))
-					const rtm::vector4f alpha = rtm::vector_set(decomp_context.interpolation_alpha);
-
-					rtm::vector4f interp_xxxx = rtm::vector_mul_add(scratch1_xxxx, alpha, rtm::vector_neg_mul_sub(scratch0_xxxx, alpha, scratch0_xxxx));
-					rtm::vector4f interp_yyyy = rtm::vector_mul_add(scratch1_yyyy, alpha, rtm::vector_neg_mul_sub(scratch0_yyyy, alpha, scratch0_yyyy));
-					rtm::vector4f interp_zzzz = rtm::vector_mul_add(scratch1_zzzz, alpha, rtm::vector_neg_mul_sub(scratch0_zzzz, alpha, scratch0_zzzz));
-					rtm::vector4f interp_wwww = rtm::vector_mul_add(scratch1_wwww, alpha, rtm::vector_neg_mul_sub(scratch0_wwww, alpha, scratch0_wwww));
+					// Interpolate our quaternions without normalizing just yet
+					rtm::vector4f interp_xxxx;
+					rtm::vector4f interp_yyyy;
+					rtm::vector4f interp_zzzz;
+					rtm::vector4f interp_wwww;
+					quat_lerp4(scratch0_xxxx, scratch0_yyyy, scratch0_zzzz, scratch0_wwww,
+						scratch1_xxxx, scratch1_yyyy, scratch1_zzzz, scratch1_wwww,
+						decomp_context.interpolation_alpha,
+						interp_xxxx, interp_yyyy, interp_zzzz, interp_wwww);
 
 					// Due to the interpolation, the result might not be anywhere near normalized!
-					// Make sure to normalize afterwards before using
+					// Make sure to normalize afterwards if we need to
 					const bool normalize_rotations = decompression_settings_type::normalize_rotations();
 					if (normalize_rotations)
-					{
-						const rtm::vector4f interp_xxxx_squared = rtm::vector_mul(interp_xxxx, interp_xxxx);
-						const rtm::vector4f interp_yyyy_squared = rtm::vector_mul(interp_yyyy, interp_yyyy);
-						const rtm::vector4f interp_zzzz_squared = rtm::vector_mul(interp_zzzz, interp_zzzz);
-						const rtm::vector4f interp_wwww_squared = rtm::vector_mul(interp_wwww, interp_wwww);
+						quat_normalize4(interp_xxxx, interp_yyyy, interp_zzzz, interp_wwww);
 
-						const rtm::vector4f interp_dot4 = rtm::vector_add(rtm::vector_add(rtm::vector_add(interp_xxxx_squared, interp_yyyy_squared), interp_zzzz_squared), interp_wwww_squared);
-
-						const rtm::vector4f interp_len = rtm::vector_sqrt(interp_dot4);
-						const rtm::vector4f interp_inv_len = rtm::vector_div(rtm::vector_set(1.0F), interp_len);
-
-						interp_xxxx = rtm::vector_mul(interp_xxxx, interp_inv_len);
-						interp_yyyy = rtm::vector_mul(interp_yyyy, interp_inv_len);
-						interp_zzzz = rtm::vector_mul(interp_zzzz, interp_inv_len);
-						interp_wwww = rtm::vector_mul(interp_wwww, interp_inv_len);
+#if !defined(ACL_IMPL_PREFETCH_EARLY)
+					{
+						// Our animated variable bit packed data uses at most 32 bits per component
+						// When we use raw data, that means each group uses 64 bytes (4 bytes per component, 4 components, 4 samples in group), we have 1 group per cache line
+						// When we use variable data, the highest bit rate uses 32 bits per component and thus our upper bound is 48 bytes per group (4 bytes per component, 3 components, 4 samples in group), we have 1.33 group per cache line
+						// In practice, the highest bit rate is rare and the second higher uses 19 bits per component which brings us to 28.5 bytes per group, leading to 2.24 group per cache line
+						// We prefetch both key frames every time to help hide TLB miss latency in large clips
+						// We prefetch here because we have a square-root and division in quat_normalize4(..) that we'll wait after
+						// This allows us to insert the prefetch basically for free in their shadow
+						const uint8_t* animated_track_data = segment_sampling_context_rotations[0].animated_track_data + 64;	// One cache line ahead
+						const uint32_t animated_bit_offset0 = segment_sampling_context_rotations[0].animated_track_data_bit_offset;
+						const uint32_t animated_bit_offset1 = segment_sampling_context_rotations[1].animated_track_data_bit_offset;
+						ACL_IMPL_ANIMATED_PREFETCH(animated_track_data + (animated_bit_offset0 / 8));
+						ACL_IMPL_ANIMATED_PREFETCH(animated_track_data + (animated_bit_offset1 / 8));
 					}
+#endif
 
 					// Swizzle out our 4 samples
 					rtm::vector4f sample0;
@@ -1159,77 +1509,59 @@ namespace acl
 			}
 
 			template<class decompression_settings_type>
-			ACL_DISABLE_SECURITY_COOKIE_CHECK void skip_rotation_group(const persistent_transform_decompression_context_v0& decomp_context)
+			ACL_DISABLE_SECURITY_COOKIE_CHECK void skip_rotation_groups(const persistent_transform_decompression_context_v0& decomp_context, uint32_t num_groups_to_skip)
 			{
 				const uint32_t num_left_to_unpack = rotations.num_left_to_unpack;
-				ACL_ASSERT(num_left_to_unpack != 0, "Cannot skip rotations that aren't present");
+				const uint32_t num_to_skip = num_groups_to_skip * 4;
+				ACL_ASSERT(num_to_skip < num_left_to_unpack, "Cannot skip rotations that aren't present");
 
-				const uint32_t num_to_unpack = std::min<uint32_t>(num_left_to_unpack, 4);
-				rotations.num_left_to_unpack = num_left_to_unpack - num_to_unpack;
+				rotations.num_left_to_unpack = num_left_to_unpack - num_to_skip;
 
 				const rotation_format8 rotation_format = get_rotation_format<decompression_settings_type>(decomp_context.rotation_format);
 				if (rotation_format == rotation_format8::quatf_drop_w_variable && decompression_settings_type::is_rotation_format_supported(rotation_format8::quatf_drop_w_variable))
 				{
-					const uint8_t* format_per_track_data0 = segment_sampling_context[0].format_per_track_data;
-					const uint8_t* format_per_track_data1 = segment_sampling_context[1].format_per_track_data;
+					const uint8_t* format_per_track_data0 = segment_sampling_context_rotations[0].format_per_track_data;
+					const uint8_t* format_per_track_data1 = segment_sampling_context_rotations[1].format_per_track_data;
 
-					uint32_t group_size0 = 0;
-					uint32_t group_size1 = 0;
+					uint32_t group_bit_size_per_component0;
+					uint32_t group_bit_size_per_component1;
+					count_animated_group_bit_size(format_per_track_data0, format_per_track_data1, num_groups_to_skip, group_bit_size_per_component0, group_bit_size_per_component1);
 
-					// Fall-through intentional
-					switch (num_to_unpack)
-					{
-					default:
-					case 4:
-						group_size0 += format_per_track_data0[3];
-						group_size1 += format_per_track_data1[3];
-						ACL_SWITCH_CASE_FALLTHROUGH_INTENTIONAL;
-					case 3:
-						group_size0 += format_per_track_data0[2];
-						group_size1 += format_per_track_data1[2];
-						ACL_SWITCH_CASE_FALLTHROUGH_INTENTIONAL;
-					case 2:
-						group_size0 += format_per_track_data0[1];
-						group_size1 += format_per_track_data1[1];
-						ACL_SWITCH_CASE_FALLTHROUGH_INTENTIONAL;
-					case 1:
-						group_size0 += format_per_track_data0[0];
-						group_size1 += format_per_track_data1[0];
-					}
+					const uint32_t format_per_track_data_skip_size = num_groups_to_skip * 4;
+					const uint32_t segment_range_data_skip_size = num_groups_to_skip * 6 * 4;
 
-					// Per track data and segment range are always padded to 4 samples
-					segment_sampling_context[0].format_per_track_data += 4;
-					segment_sampling_context[0].segment_range_data += 6 * 4;
-					segment_sampling_context[0].animated_track_data_bit_offset += group_size0 * 3;
-					segment_sampling_context[1].format_per_track_data += 4;
-					segment_sampling_context[1].segment_range_data += 6 * 4;
-					segment_sampling_context[1].animated_track_data_bit_offset += group_size1 * 3;
+					segment_sampling_context_rotations[0].format_per_track_data = format_per_track_data0 + format_per_track_data_skip_size;
+					segment_sampling_context_rotations[0].segment_range_data += segment_range_data_skip_size;
+					segment_sampling_context_rotations[0].animated_track_data_bit_offset += group_bit_size_per_component0 * 3;
 
-					clip_sampling_context.clip_range_data += sizeof(rtm::float3f) * 2 * num_to_unpack;
+					segment_sampling_context_rotations[1].format_per_track_data = format_per_track_data1 + format_per_track_data_skip_size;
+					segment_sampling_context_rotations[1].segment_range_data += segment_range_data_skip_size;
+					segment_sampling_context_rotations[1].animated_track_data_bit_offset += group_bit_size_per_component1 * 3;
+
+					clip_sampling_context_rotations.clip_range_data += sizeof(rtm::float3f) * 2 * 4 * num_groups_to_skip;
 				}
 				else
 				{
-					uint32_t group_size;
+					uint32_t group_bit_size;
 					if (rotation_format == rotation_format8::quatf_full && decompression_settings_type::is_rotation_format_supported(rotation_format8::quatf_full))
-						group_size = 32 * 4 * num_to_unpack;
+						group_bit_size = 32 * 4 * 4 * num_groups_to_skip;
 					else // drop w full
-						group_size = 32 * 3 * num_to_unpack;
+						group_bit_size = 32 * 3 * 4 * num_groups_to_skip;
 
-					segment_sampling_context[0].animated_track_data_bit_offset += group_size;
-					segment_sampling_context[1].animated_track_data_bit_offset += group_size;
+					segment_sampling_context_rotations[0].animated_track_data_bit_offset += group_bit_size;
+					segment_sampling_context_rotations[1].animated_track_data_bit_offset += group_bit_size;
 				}
 			}
 
 			template<class decompression_settings_type>
-			ACL_DISABLE_SECURITY_COOKIE_CHECK rtm::quatf RTM_SIMD_CALL unpack_rotation_within_group(const persistent_transform_decompression_context_v0& decomp_context, const animated_group_cursor_v0& group_cursor, uint32_t unpack_index)
+			ACL_DISABLE_SECURITY_COOKIE_CHECK rtm::quatf RTM_SIMD_CALL unpack_rotation_within_group(const persistent_transform_decompression_context_v0& decomp_context, uint32_t unpack_index)
 			{
-				ACL_ASSERT(unpack_index < group_cursor.group_size, "Cannot unpack sample that isn't present");
+				ACL_ASSERT(unpack_index < rotations.num_left_to_unpack && unpack_index < 4, "Cannot unpack sample that isn't present");
 
-				const clip_animated_sampling_context_v0& cursor_clip_sampling_context = group_cursor.clip_sampling_context;
-				const uint32_t group_size = group_cursor.group_size;
+				const uint32_t group_size = std::min<uint32_t>(rotations.num_left_to_unpack, 4);
 
-				const rtm::vector4f sample_as_vec0 = unpack_single_animated_quat<decompression_settings_type>(decomp_context, unpack_index, group_size, cursor_clip_sampling_context, group_cursor.segment_sampling_context[0]);
-				const rtm::vector4f sample_as_vec1 = unpack_single_animated_quat<decompression_settings_type>(decomp_context, unpack_index, group_size, cursor_clip_sampling_context, group_cursor.segment_sampling_context[1]);
+				const rtm::vector4f sample_as_vec0 = unpack_single_animated_quat<decompression_settings_type>(decomp_context, unpack_index, group_size, clip_sampling_context_rotations, segment_sampling_context_rotations[0]);
+				const rtm::vector4f sample_as_vec1 = unpack_single_animated_quat<decompression_settings_type>(decomp_context, unpack_index, group_size, clip_sampling_context_rotations, segment_sampling_context_rotations[1]);
 
 				rtm::quatf sample0;
 				rtm::quatf sample1;
@@ -1266,27 +1598,27 @@ namespace acl
 			template<class decompression_settings_adapter_type>
 			ACL_DISABLE_SECURITY_COOKIE_CHECK void unpack_translation_group(const persistent_transform_decompression_context_v0& decomp_context)
 			{
-				uint32_t num_left_to_unpack = translations.num_left_to_unpack;
+				const uint32_t num_left_to_unpack = translations.num_left_to_unpack;
 				if (num_left_to_unpack == 0)
 					return;	// Nothing left to do, we are done
 
-							// If we have less than 4 cached samples, unpack 4 more and prefetch the next cache line
+				// If we have less than 4 cached samples, unpack 4 more and prefetch the next cache line
 				const uint32_t num_cached = translations.get_num_cached();
 				if (num_cached >= 4)
 					return;	// Enough cached, nothing to do
 
 				const uint32_t num_to_unpack = std::min<uint32_t>(num_left_to_unpack, 4);
-				num_left_to_unpack -= num_to_unpack;
-				translations.num_left_to_unpack = num_left_to_unpack;
+				translations.num_left_to_unpack = num_left_to_unpack - num_to_unpack;
 
 				// Write index will be either 0 or 4 here since we always unpack 4 at a time
-				uint32_t cache_write_index = translations.cache_write_index % 8;
+				const uint32_t cache_write_index = translations.cache_write_index % 8;
 				translations.cache_write_index += num_to_unpack;
 
-				unpack_animated_vector3<decompression_settings_adapter_type>(decomp_context, scratch0, num_to_unpack, clip_sampling_context, segment_sampling_context[0]);
-				unpack_animated_vector3<decompression_settings_adapter_type>(decomp_context, scratch1, num_to_unpack, clip_sampling_context, segment_sampling_context[1]);
+				unpack_animated_vector3<decompression_settings_adapter_type>(decomp_context, scratch0, num_to_unpack, clip_sampling_context_translations, segment_sampling_context_translations[0]);
+				unpack_animated_vector3<decompression_settings_adapter_type>(decomp_context, scratch1, num_to_unpack, clip_sampling_context_translations, segment_sampling_context_translations[1]);
 
-				const float interpolation_alpha = decomp_context.interpolation_alpha;
+				const rtm::scalarf interpolation_alpha = rtm::scalar_set(decomp_context.interpolation_alpha);
+				rtm::vector4f* cache_ptr = &translations.cached_samples[cache_write_index];
 				for (uint32_t unpack_index = 0; unpack_index < num_to_unpack; ++unpack_index)
 				{
 					const rtm::vector4f sample0 = scratch0[unpack_index];
@@ -1294,84 +1626,68 @@ namespace acl
 
 					const rtm::vector4f sample = rtm::vector_lerp(sample0, sample1, interpolation_alpha);
 
-					translations.cached_samples[cache_write_index] = sample;
-					cache_write_index++;
+					cache_ptr[unpack_index] = sample;
 				}
 
-				// If we have some range reduction, skip the data we read
-				if (are_any_enum_flags_set(decomp_context.range_reduction, range_reduction_flags8::translations))
-					clip_sampling_context.clip_range_data += num_to_unpack * sizeof(rtm::float3f) * 2;
+				// If we have clip range data, skip it
+				const vector_format8 format = get_vector_format<decompression_settings_adapter_type>(decompression_settings_adapter_type::get_vector_format(decomp_context));
+				if (format == vector_format8::vector3f_variable && decompression_settings_adapter_type::is_vector_format_supported(vector_format8::vector3f_variable))
+				{
+					clip_sampling_context_translations.clip_range_data += num_to_unpack * sizeof(rtm::float3f) * 2;
 
-				// Clip range data is 24 bytes per sub-track and as such we need to prefetch two cache lines ahead to process 4 sub-tracks
-				ACL_IMPL_ANIMATED_PREFETCH(clip_sampling_context.clip_range_data + 63);
-				ACL_IMPL_ANIMATED_PREFETCH(clip_sampling_context.clip_range_data + 127);
+					// Clip range data is 24 bytes per sub-track and as such we need to prefetch two cache lines ahead to process 4 sub-tracks
+					ACL_IMPL_ANIMATED_PREFETCH(clip_sampling_context_translations.clip_range_data + 64);
+					ACL_IMPL_ANIMATED_PREFETCH(clip_sampling_context_translations.clip_range_data + 128);
+				}
 			}
 
 			template<class decompression_settings_adapter_type>
-			ACL_DISABLE_SECURITY_COOKIE_CHECK void skip_translation_group(const persistent_transform_decompression_context_v0& decomp_context)
+			ACL_DISABLE_SECURITY_COOKIE_CHECK void skip_translation_groups(const persistent_transform_decompression_context_v0& decomp_context, uint32_t num_groups_to_skip)
 			{
 				const uint32_t num_left_to_unpack = translations.num_left_to_unpack;
-				ACL_ASSERT(num_left_to_unpack != 0, "Cannot skip translations that aren't present");
+				const uint32_t num_to_skip = num_groups_to_skip * 4;
+				ACL_ASSERT(num_to_skip < num_left_to_unpack, "Cannot skip translations that aren't present");
 
-				const uint32_t num_to_unpack = std::min<uint32_t>(num_left_to_unpack, 4);
-				translations.num_left_to_unpack = num_left_to_unpack - num_to_unpack;
+				translations.num_left_to_unpack = num_left_to_unpack - num_to_skip;
 
 				const vector_format8 format = get_vector_format<decompression_settings_adapter_type>(decompression_settings_adapter_type::get_vector_format(decomp_context));
 				if (format == vector_format8::vector3f_variable && decompression_settings_adapter_type::is_vector_format_supported(vector_format8::vector3f_variable))
 				{
-					const uint8_t* format_per_track_data0 = segment_sampling_context[0].format_per_track_data;
-					const uint8_t* format_per_track_data1 = segment_sampling_context[1].format_per_track_data;
+					const uint8_t* format_per_track_data0 = segment_sampling_context_translations[0].format_per_track_data;
+					const uint8_t* format_per_track_data1 = segment_sampling_context_translations[1].format_per_track_data;
 
-					uint32_t group_size0 = 0;
-					uint32_t group_size1 = 0;
+					uint32_t group_bit_size_per_component0;
+					uint32_t group_bit_size_per_component1;
+					count_animated_group_bit_size(format_per_track_data0, format_per_track_data1, num_groups_to_skip, group_bit_size_per_component0, group_bit_size_per_component1);
 
-					// Fall-through intentional
-					switch (num_to_unpack)
-					{
-					default:
-					case 4:
-						group_size0 += format_per_track_data0[3];
-						group_size1 += format_per_track_data1[3];
-						ACL_SWITCH_CASE_FALLTHROUGH_INTENTIONAL;
-					case 3:
-						group_size0 += format_per_track_data0[2];
-						group_size1 += format_per_track_data1[2];
-						ACL_SWITCH_CASE_FALLTHROUGH_INTENTIONAL;
-					case 2:
-						group_size0 += format_per_track_data0[1];
-						group_size1 += format_per_track_data1[1];
-						ACL_SWITCH_CASE_FALLTHROUGH_INTENTIONAL;
-					case 1:
-						group_size0 += format_per_track_data0[0];
-						group_size1 += format_per_track_data1[0];
-					}
+					const uint32_t format_per_track_data_skip_size = num_groups_to_skip * 4;
+					const uint32_t segment_range_data_skip_size = num_groups_to_skip * 6 * 4;
+
+					segment_sampling_context_translations[0].format_per_track_data = format_per_track_data0 + format_per_track_data_skip_size;
+					segment_sampling_context_translations[0].segment_range_data += segment_range_data_skip_size;
+					segment_sampling_context_translations[0].animated_track_data_bit_offset += group_bit_size_per_component0 * 3;
 
-					segment_sampling_context[0].format_per_track_data += num_to_unpack;
-					segment_sampling_context[0].segment_range_data += 6 * num_to_unpack;
-					segment_sampling_context[0].animated_track_data_bit_offset += group_size0 * 3;
-					segment_sampling_context[1].format_per_track_data += num_to_unpack;
-					segment_sampling_context[1].segment_range_data += 6 * num_to_unpack;
-					segment_sampling_context[1].animated_track_data_bit_offset += group_size1 * 3;
+					segment_sampling_context_translations[1].format_per_track_data = format_per_track_data1 + format_per_track_data_skip_size;
+					segment_sampling_context_translations[1].segment_range_data += segment_range_data_skip_size;
+					segment_sampling_context_translations[1].animated_track_data_bit_offset += group_bit_size_per_component1 * 3;
 
-					clip_sampling_context.clip_range_data += sizeof(rtm::float3f) * 2 * num_to_unpack;
+					clip_sampling_context_translations.clip_range_data += sizeof(rtm::float3f) * 2 * 4 * num_groups_to_skip;
 				}
 				else
 				{
-					const uint32_t group_size = 32 * 3 * num_to_unpack;
-					segment_sampling_context[0].animated_track_data_bit_offset += group_size;
-					segment_sampling_context[1].animated_track_data_bit_offset += group_size;
+					const uint32_t group_bit_size = 32 * 3 * 4 * num_groups_to_skip;
+					segment_sampling_context_translations[0].animated_track_data_bit_offset += group_bit_size;
+					segment_sampling_context_translations[1].animated_track_data_bit_offset += group_bit_size;
 				}
 			}
 
 			template<class decompression_settings_adapter_type>
-			ACL_DISABLE_SECURITY_COOKIE_CHECK rtm::vector4f RTM_SIMD_CALL unpack_translation_within_group(const persistent_transform_decompression_context_v0& decomp_context, const animated_group_cursor_v0& group_cursor, uint32_t unpack_index)
+			ACL_DISABLE_SECURITY_COOKIE_CHECK rtm::vector4f RTM_SIMD_CALL unpack_translation_within_group(const persistent_transform_decompression_context_v0& decomp_context, uint32_t unpack_index)
 			{
-				ACL_ASSERT(unpack_index < group_cursor.group_size, "Cannot unpack sample that isn't present");
-
-				const clip_animated_sampling_context_v0& cursor_clip_sampling_context = group_cursor.clip_sampling_context;
+				ACL_ASSERT(unpack_index < translations.num_left_to_unpack && unpack_index < 4, "Cannot unpack sample that isn't present");
 
-				const rtm::vector4f sample0 = unpack_single_animated_vector3<decompression_settings_adapter_type>(decomp_context, unpack_index, cursor_clip_sampling_context, group_cursor.segment_sampling_context[0]);
-				const rtm::vector4f sample1 = unpack_single_animated_vector3<decompression_settings_adapter_type>(decomp_context, unpack_index, cursor_clip_sampling_context, group_cursor.segment_sampling_context[1]);
+				const rtm::vector4f sample0 = unpack_single_animated_vector3<decompression_settings_adapter_type>(decomp_context, unpack_index, clip_sampling_context_translations, segment_sampling_context_translations[0]);
+				const rtm::vector4f sample1 = unpack_single_animated_vector3<decompression_settings_adapter_type>(decomp_context, unpack_index, clip_sampling_context_translations, segment_sampling_context_translations[1]);
 
 				return rtm::vector_lerp(sample0, sample1, decomp_context.interpolation_alpha);
 			}
@@ -1386,27 +1702,27 @@ namespace acl
 			template<class decompression_settings_adapter_type>
 			ACL_DISABLE_SECURITY_COOKIE_CHECK void unpack_scale_group(const persistent_transform_decompression_context_v0& decomp_context)
 			{
-				uint32_t num_left_to_unpack = scales.num_left_to_unpack;
+				const uint32_t num_left_to_unpack = scales.num_left_to_unpack;
 				if (num_left_to_unpack == 0)
 					return;	// Nothing left to do, we are done
 
-							// If we have less than 4 cached samples, unpack 4 more and prefetch the next cache line
+				// If we have less than 4 cached samples, unpack 4 more and prefetch the next cache line
 				const uint32_t num_cached = scales.get_num_cached();
 				if (num_cached >= 4)
 					return;	// Enough cached, nothing to do
 
 				const uint32_t num_to_unpack = std::min<uint32_t>(num_left_to_unpack, 4);
-				num_left_to_unpack -= num_to_unpack;
-				scales.num_left_to_unpack = num_left_to_unpack;
+				scales.num_left_to_unpack = num_left_to_unpack - num_to_unpack;
 
 				// Write index will be either 0 or 4 here since we always unpack 4 at a time
-				uint32_t cache_write_index = scales.cache_write_index % 8;
+				const uint32_t cache_write_index = scales.cache_write_index % 8;
 				scales.cache_write_index += num_to_unpack;
 
-				unpack_animated_vector3<decompression_settings_adapter_type>(decomp_context, scratch0, num_to_unpack, clip_sampling_context, segment_sampling_context[0]);
-				unpack_animated_vector3<decompression_settings_adapter_type>(decomp_context, scratch1, num_to_unpack, clip_sampling_context, segment_sampling_context[1]);
+				unpack_animated_vector3<decompression_settings_adapter_type>(decomp_context, scratch0, num_to_unpack, clip_sampling_context_scales, segment_sampling_context_scales[0]);
+				unpack_animated_vector3<decompression_settings_adapter_type>(decomp_context, scratch1, num_to_unpack, clip_sampling_context_scales, segment_sampling_context_scales[1]);
 
-				const float interpolation_alpha = decomp_context.interpolation_alpha;
+				const rtm::scalarf interpolation_alpha = rtm::scalar_set(decomp_context.interpolation_alpha);
+				rtm::vector4f* cache_ptr = &scales.cached_samples[cache_write_index];
 				for (uint32_t unpack_index = 0; unpack_index < num_to_unpack; ++unpack_index)
 				{
 					const rtm::vector4f sample0 = scratch0[unpack_index];
@@ -1414,80 +1730,70 @@ namespace acl
 
 					const rtm::vector4f sample = rtm::vector_lerp(sample0, sample1, interpolation_alpha);
 
-					scales.cached_samples[cache_write_index] = sample;
-					cache_write_index++;
+					cache_ptr[unpack_index] = sample;
 				}
 
-				// If we have some range reduction, skip the data we read
-				if (are_any_enum_flags_set(decomp_context.range_reduction, range_reduction_flags8::scales))
-					clip_sampling_context.clip_range_data += num_to_unpack * sizeof(rtm::float3f) * 2;
+				// If we have clip range data, skip it
+				const vector_format8 format = get_vector_format<decompression_settings_adapter_type>(decompression_settings_adapter_type::get_vector_format(decomp_context));
+				if (format == vector_format8::vector3f_variable && decompression_settings_adapter_type::is_vector_format_supported(vector_format8::vector3f_variable))
+				{
+					clip_sampling_context_scales.clip_range_data += num_to_unpack * sizeof(rtm::float3f) * 2;
 
-				// Clip range data is 24 bytes per sub-track and as such we need to prefetch two cache lines ahead to process 4 sub-tracks
-				ACL_IMPL_ANIMATED_PREFETCH(clip_sampling_context.clip_range_data + 63);
-				ACL_IMPL_ANIMATED_PREFETCH(clip_sampling_context.clip_range_data + 127);
+					// Clip range data is 24 bytes per sub-track and as such we need to prefetch two cache lines ahead to process 4 sub-tracks
+					ACL_IMPL_ANIMATED_PREFETCH(clip_sampling_context_scales.clip_range_data + 64);
+					ACL_IMPL_ANIMATED_PREFETCH(clip_sampling_context_scales.clip_range_data + 128);
+				}
 			}
 
 			template<class decompression_settings_adapter_type>
-			ACL_DISABLE_SECURITY_COOKIE_CHECK void skip_scale_group(const persistent_transform_decompression_context_v0& decomp_context)
+			ACL_DISABLE_SECURITY_COOKIE_CHECK void skip_scale_groups(const persistent_transform_decompression_context_v0& decomp_context, uint32_t num_groups_to_skip)
 			{
 				const uint32_t num_left_to_unpack = scales.num_left_to_unpack;
-				ACL_ASSERT(num_left_to_unpack != 0, "Cannot skip scales that aren't present");
+				const uint32_t num_to_skip = num_groups_to_skip * 4;
+				ACL_ASSERT(num_to_skip < num_left_to_unpack, "Cannot skip scales that aren't present");
 
-				const uint32_t num_to_unpack = std::min<uint32_t>(num_left_to_unpack, 4);
-				scales.num_left_to_unpack = num_left_to_unpack - num_to_unpack;
+				scales.num_left_to_unpack = num_left_to_unpack - num_to_skip;
 
 				const vector_format8 format = get_vector_format<decompression_settings_adapter_type>(decompression_settings_adapter_type::get_vector_format(decomp_context));
 				if (format == vector_format8::vector3f_variable && decompression_settings_adapter_type::is_vector_format_supported(vector_format8::vector3f_variable))
 				{
-					const uint8_t* format_per_track_data0 = segment_sampling_context[0].format_per_track_data;
-					const uint8_t* format_per_track_data1 = segment_sampling_context[1].format_per_track_data;
+					const uint8_t* format_per_track_data0 = segment_sampling_context_scales[0].format_per_track_data;
+					const uint8_t* format_per_track_data1 = segment_sampling_context_scales[1].format_per_track_data;
 
-					uint32_t group_size0 = 0;
-					uint32_t group_size1 = 0;
+					uint32_t group_bit_size_per_component0;
+					uint32_t group_bit_size_per_component1;
+					count_animated_group_bit_size(format_per_track_data0, format_per_track_data1, num_groups_to_skip, group_bit_size_per_component0, group_bit_size_per_component1);
 
-					// Fall-through intentional
-					switch (num_to_unpack)
-					{
-					default:
-					case 4:
-						group_size0 += format_per_track_data0[3];
-						group_size1 += format_per_track_data1[3];
-						ACL_SWITCH_CASE_FALLTHROUGH_INTENTIONAL;
-					case 3:
-						group_size0 += format_per_track_data0[2];
-						group_size1 += format_per_track_data1[2];
-						ACL_SWITCH_CASE_FALLTHROUGH_INTENTIONAL;
-					case 2:
-						group_size0 += format_per_track_data0[1];
-						group_size1 += format_per_track_data1[1];
-						ACL_SWITCH_CASE_FALLTHROUGH_INTENTIONAL;
-					case 1:
-						group_size0 += format_per_track_data0[0];
-						group_size1 += format_per_track_data1[0];
-					}
+					const uint32_t format_per_track_data_skip_size = num_groups_to_skip * 4;
+					const uint32_t segment_range_data_skip_size = num_groups_to_skip * 6 * 4;
 
-					segment_sampling_context[0].format_per_track_data += num_to_unpack;
-					segment_sampling_context[0].segment_range_data += 6 * num_to_unpack;
-					segment_sampling_context[0].animated_track_data_bit_offset += group_size0 * 3;
-					segment_sampling_context[1].format_per_track_data += num_to_unpack;
-					segment_sampling_context[1].segment_range_data += 6 * num_to_unpack;
-					segment_sampling_context[1].animated_track_data_bit_offset += group_size1 * 3;
+					segment_sampling_context_scales[0].format_per_track_data = format_per_track_data0 + format_per_track_data_skip_size;
+					segment_sampling_context_scales[0].segment_range_data += segment_range_data_skip_size;
+					segment_sampling_context_scales[0].animated_track_data_bit_offset += group_bit_size_per_component0 * 3;
 
-					clip_sampling_context.clip_range_data += sizeof(rtm::float3f) * 2 * num_to_unpack;
+					segment_sampling_context_scales[1].format_per_track_data = format_per_track_data1 + format_per_track_data_skip_size;
+					segment_sampling_context_scales[1].segment_range_data += segment_range_data_skip_size;
+					segment_sampling_context_scales[1].animated_track_data_bit_offset += group_bit_size_per_component1 * 3;
+
+					clip_sampling_context_scales.clip_range_data += sizeof(rtm::float3f) * 2 * 4 * num_groups_to_skip;
 				}
 				else
 				{
-					const uint32_t group_size = 32 * 3 * num_to_unpack;
-					segment_sampling_context[0].animated_track_data_bit_offset += group_size;
-					segment_sampling_context[1].animated_track_data_bit_offset += group_size;
+					const uint32_t group_bit_size = 32 * 3 * 4 * num_groups_to_skip;
+					segment_sampling_context_scales[0].animated_track_data_bit_offset += group_bit_size;
+					segment_sampling_context_scales[1].animated_track_data_bit_offset += group_bit_size;
 				}
 			}
 
 			template<class decompression_settings_adapter_type>
-			ACL_DISABLE_SECURITY_COOKIE_CHECK rtm::vector4f RTM_SIMD_CALL unpack_scale_within_group(const persistent_transform_decompression_context_v0& decomp_context, const animated_group_cursor_v0& group_cursor, uint32_t unpack_index)
+			ACL_DISABLE_SECURITY_COOKIE_CHECK rtm::vector4f RTM_SIMD_CALL unpack_scale_within_group(const persistent_transform_decompression_context_v0& decomp_context, uint32_t unpack_index)
 			{
-				// Same as translation but a different adapter
-				return unpack_translation_within_group<decompression_settings_adapter_type>(decomp_context, group_cursor, unpack_index);
+				ACL_ASSERT(unpack_index < scales.num_left_to_unpack && unpack_index < 4, "Cannot unpack sample that isn't present");
+
+				const rtm::vector4f sample0 = unpack_single_animated_vector3<decompression_settings_adapter_type>(decomp_context, unpack_index, clip_sampling_context_scales, segment_sampling_context_scales[0]);
+				const rtm::vector4f sample1 = unpack_single_animated_vector3<decompression_settings_adapter_type>(decomp_context, unpack_index, clip_sampling_context_scales, segment_sampling_context_scales[1]);
+
+				return rtm::vector_lerp(sample0, sample1, decomp_context.interpolation_alpha);
 			}
 
 			ACL_DISABLE_SECURITY_COOKIE_CHECK rtm::vector4f RTM_SIMD_CALL consume_scale()
@@ -1499,4 +1805,9 @@ namespace acl
 		};
 	}
 }
+
+#if defined(ACL_COMPILER_GCC)
+	#pragma GCC diagnostic pop
+#endif
+
 ACL_IMPL_FILE_PRAGMA_POP
diff --git a/includes/acl/decompression/impl/transform_constant_track_cache.h b/includes/acl/decompression/impl/transform_constant_track_cache.h
--- a/includes/acl/decompression/impl/transform_constant_track_cache.h
+++ b/includes/acl/decompression/impl/transform_constant_track_cache.h
@@ -36,10 +36,17 @@
 #include <cstdint>
 
 #define ACL_IMPL_USE_CONSTANT_PREFETCH
-//#define ACL_IMPL_VEC3_UNPACK
 
 ACL_IMPL_FILE_PRAGMA_PUSH
 
+// We only initialize some variables when we need them which prompts the compiler to complain
+// The usage is perfectly safe and because this code is VERY hot and needs to be as fast as possible,
+// we disable the warning to avoid zeroing out things we don't need
+#if defined(ACL_COMPILER_GCC)
+	#pragma GCC diagnostic push
+	#pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
+#endif
+
 namespace acl
 {
 #if defined(ACL_IMPL_USE_CONSTANT_PREFETCH)
@@ -50,154 +57,39 @@ namespace acl
 
 	namespace acl_impl
 	{
-		template<class decompression_settings_type>
-		ACL_FORCE_INLINE ACL_DISABLE_SECURITY_COOKIE_CHECK void unpack_constant_quat(const persistent_transform_decompression_context_v0& decomp_context, track_cache_quatf_v0& track_cache, const uint8_t*& constant_data)
+		struct constant_track_cache_v0
 		{
-			// Prefetch the next cache line even if we don't have any data left
-			// By the time we unpack again, it will have arrived in the CPU cache
-			// If our format is full precision, we have at most 4 samples per cache line
-			// If our format is drop W, we have at most 5.33 samples per cache line
-
-			// If our pointer was already aligned to a cache line before we unpacked our 4 values,
-			// it now points to the first byte of the next cache line. Any offset between 0-63 will fetch it.
-			// If our pointer had some offset into a cache line, we might have spanned 2 cache lines.
-			// If this happens, we probably already read some data from the next cache line in which
-			// case we don't need to prefetch it and we can go to the next one. Any offset after the end
-			// of this cache line will fetch it. For safety, we prefetch 63 bytes ahead.
-			// Prefetch 4 samples ahead in all levels of the CPU cache
-
-			uint32_t num_left_to_unpack = track_cache.num_left_to_unpack;
-			if (num_left_to_unpack == 0)
-				return;	// Nothing left to do, we are done
-
-						// If we have less than 4 cached samples, unpack 4 more and prefetch the next cache line
-			const uint32_t num_cached = track_cache.get_num_cached();
-			if (num_cached >= 4)
-				return;	// Enough cached, nothing to do
-
-			const rotation_format8 rotation_format = get_rotation_format<decompression_settings_type>(decomp_context.rotation_format);
-
-			const uint32_t num_to_unpack = std::min<uint32_t>(num_left_to_unpack, 4);
-			num_left_to_unpack -= num_to_unpack;
-			track_cache.num_left_to_unpack = num_left_to_unpack;
-
-			// Write index will be either 0 or 4 here since we always unpack 4 at a time
-			uint32_t cache_write_index = track_cache.cache_write_index % 8;
-			track_cache.cache_write_index += num_to_unpack;
-
-			const uint8_t* constant_track_data = constant_data;
-
-			if (rotation_format == rotation_format8::quatf_full && decompression_settings_type::is_rotation_format_supported(rotation_format8::quatf_full))
-			{
-				for (uint32_t unpack_index = num_to_unpack; unpack_index != 0; --unpack_index)
-				{
-					// Unpack
-					const rtm::quatf sample = unpack_quat_128(constant_track_data);
-
-					ACL_ASSERT(rtm::quat_is_finite(sample), "Rotation is not valid!");
-					ACL_ASSERT(rtm::quat_is_normalized(sample), "Rotation is not normalized!");
+			track_cache_quatf_v0 rotations;
 
-					// Cache
-					track_cache.cached_samples[cache_write_index] = sample;
-					cache_write_index++;
+			// Points to our packed sub-track data
+			const uint8_t*	constant_data_rotations;
+			const uint8_t*	constant_data_translations;
+			const uint8_t*	constant_data_scales;
 
-					// Update our read ptr
-					constant_track_data += sizeof(rtm::float4f);
-				}
-			}
-			else
+			template<class decompression_settings_type>
+			ACL_DISABLE_SECURITY_COOKIE_CHECK void initialize(const persistent_transform_decompression_context_v0& decomp_context)
 			{
-				// Unpack
-				// Always load 4x rotations, we might contain garbage in a few lanes but it's fine
-				const uint32_t load_size = num_to_unpack * sizeof(float);
-
-				const rtm::vector4f xxxx = rtm::vector_load(reinterpret_cast<const float*>(constant_track_data + load_size * 0));
-				const rtm::vector4f yyyy = rtm::vector_load(reinterpret_cast<const float*>(constant_track_data + load_size * 1));
-				const rtm::vector4f zzzz = rtm::vector_load(reinterpret_cast<const float*>(constant_track_data + load_size * 2));
-
-				// Update our read ptr
-				constant_track_data += load_size * 3;
-
-				// quat_from_positive_w_soa
-				const rtm::vector4f wwww_squared = rtm::vector_sub(rtm::vector_sub(rtm::vector_sub(rtm::vector_set(1.0F), rtm::vector_mul(xxxx, xxxx)), rtm::vector_mul(yyyy, yyyy)), rtm::vector_mul(zzzz, zzzz));
-
-				// w_squared can be negative either due to rounding or due to quantization imprecision, we take the absolute value
-				// to ensure the resulting quaternion is always normalized with a positive W component
-				const rtm::vector4f wwww = rtm::vector_sqrt(rtm::vector_abs(wwww_squared));
+				const transform_tracks_header& transform_header = get_transform_tracks_header(*decomp_context.tracks);
 
-				rtm::vector4f sample0;
-				rtm::vector4f sample1;
-				rtm::vector4f sample2;
-				rtm::vector4f sample3;
-				RTM_MATRIXF_TRANSPOSE_4X4(xxxx, yyyy, zzzz, wwww, sample0, sample1, sample2, sample3);
+				rotations.num_left_to_unpack = transform_header.num_constant_rotation_samples;
 
-				// Cache
-				rtm::quatf* cache_ptr = &track_cache.cached_samples[cache_write_index];
-				cache_ptr[0] = rtm::vector_to_quat(sample0);
-				cache_ptr[1] = rtm::vector_to_quat(sample1);
-				cache_ptr[2] = rtm::vector_to_quat(sample2);
-				cache_ptr[3] = rtm::vector_to_quat(sample3);
+				const rotation_format8 rotation_format = get_rotation_format<decompression_settings_type>(decomp_context.rotation_format);
+				const rotation_format8 packed_format = is_rotation_format_variable(rotation_format) ? get_highest_variant_precision(get_rotation_variant(rotation_format)) : rotation_format;
+				const uint32_t packed_rotation_size = get_packed_rotation_size(packed_format);
+				const uint32_t packed_translation_size = get_packed_vector_size(vector_format8::vector3f_full);
 
-#if defined(ACL_HAS_ASSERT_CHECKS)
-				for (uint32_t unpack_index = 0; unpack_index < num_to_unpack; ++unpack_index)
-				{
-					ACL_ASSERT(rtm::quat_is_finite(track_cache.cached_samples[cache_write_index + unpack_index]), "Rotation is not valid!");
-					ACL_ASSERT(rtm::quat_is_normalized(track_cache.cached_samples[cache_write_index + unpack_index]), "Rotation is not normalized!");
-				}
-#endif
+				constant_data_rotations = transform_header.get_constant_track_data();
+				constant_data_translations = constant_data_rotations + packed_rotation_size * transform_header.num_constant_rotation_samples;
+				constant_data_scales = constant_data_translations + packed_translation_size * transform_header.num_constant_translation_samples;
 			}
 
-			// Update our pointer
-			constant_data = constant_track_data;
-
-			ACL_IMPL_CONSTANT_PREFETCH(constant_track_data + 63);
-		}
-
-#if defined(ACL_IMPL_VEC3_UNPACK)
-		inline void unpack_constant_vector3(track_cache_vector4f_v0& track_cache, const uint8_t*& constant_data)
-		{
-			uint32_t num_left_to_unpack = track_cache.num_left_to_unpack;
-			if (num_left_to_unpack == 0)
-				return;	// Nothing left to do, we are done
-
-			const uint32_t packed_size = get_packed_vector_size(vector_format8::vector3f_full);
-
-			// If we have less than 4 cached samples, unpack 4 more and prefetch the next cache line
-			const uint32_t num_cached = track_cache.get_num_cached();
-			if (num_cached < 4)
+			template<class decompression_settings_type>
+			ACL_FORCE_INLINE ACL_DISABLE_SECURITY_COOKIE_CHECK void unpack_rotation_group(const persistent_transform_decompression_context_v0& decomp_context)
 			{
-				const uint32_t num_to_unpack = std::min<uint32_t>(num_left_to_unpack, 4);
-				num_left_to_unpack -= num_to_unpack;
-				track_cache.num_left_to_unpack = num_left_to_unpack;
-
-				// Write index will be either 0 or 4 here since we always unpack 4 at a time
-				uint32_t cache_write_index = track_cache.cache_write_index % 8;
-				track_cache.cache_write_index += num_to_unpack;
-
-				const uint8_t* constant_track_data = constant_data;
-
-				for (uint32_t unpack_index = num_to_unpack; unpack_index != 0; --unpack_index)
-				{
-					// Unpack
-					// Constant vector3 tracks store the remaining sample with full precision
-					const rtm::vector4f sample = unpack_vector3_96_unsafe(constant_track_data);
-					ACL_ASSERT(rtm::vector_is_finite3(sample), "Vector3 is not valid!");
-
-					// TODO: Fill in W component with something sensible?
-
-					// Cache
-					track_cache.cached_samples[cache_write_index] = sample;
-					cache_write_index++;
-
-					// Update our read ptr
-					constant_track_data += packed_size;
-				}
-
-				constant_data = constant_track_data;
-
 				// Prefetch the next cache line even if we don't have any data left
 				// By the time we unpack again, it will have arrived in the CPU cache
-				// With our full precision format, we have at most 5.33 samples per cache line
+				// If our format is full precision, we have at most 4 samples per cache line
+				// If our format is drop W, we have at most 5.33 samples per cache line
 
 				// If our pointer was already aligned to a cache line before we unpacked our 4 values,
 				// it now points to the first byte of the next cache line. Any offset between 0-63 will fetch it.
@@ -206,84 +98,90 @@ namespace acl
 				// case we don't need to prefetch it and we can go to the next one. Any offset after the end
 				// of this cache line will fetch it. For safety, we prefetch 63 bytes ahead.
 				// Prefetch 4 samples ahead in all levels of the CPU cache
-				ACL_IMPL_CONSTANT_PREFETCH(constant_track_data + 63);
-			}
-		}
-#endif
 
-		struct constant_track_cache_v0
-		{
-			track_cache_quatf_v0 rotations;
+				uint32_t num_left_to_unpack = rotations.num_left_to_unpack;
+				if (num_left_to_unpack == 0)
+					return;	// Nothing left to do, we are done
 
-#if defined(ACL_IMPL_VEC3_UNPACK)
-			track_cache_vector4f_v0 translations;
-			track_cache_vector4f_v0 scales;
-#endif
+							// If we have less than 4 cached samples, unpack 4 more and prefetch the next cache line
+				const uint32_t num_cached = rotations.get_num_cached();
+				if (num_cached >= 4)
+					return;	// Enough cached, nothing to do
 
-#if defined(ACL_IMPL_USE_CONSTANT_GROUPS)
-			// How many we have left to unpack in total
-			uint32_t		num_left_to_unpack_translations;
-			uint32_t		num_left_to_unpack_scales;
+				const rotation_format8 rotation_format = get_rotation_format<decompression_settings_type>(decomp_context.rotation_format);
 
-			// How many we have cached (faked for translations/scales)
-			uint32_t		num_unpacked_translations = 0;
-			uint32_t		num_unpacked_scales = 0;
+				const uint32_t num_to_unpack = std::min<uint32_t>(num_left_to_unpack, 4);
+				num_left_to_unpack -= num_to_unpack;
+				rotations.num_left_to_unpack = num_left_to_unpack;
 
-			// How many we have left in our group
-			uint32_t		num_group_translations[2];
-			uint32_t		num_group_scales[2];
+				// Write index will be either 0 or 4 here since we always unpack 4 at a time
+				uint32_t cache_write_index = rotations.cache_write_index % 8;
+				rotations.cache_write_index += num_to_unpack;
 
-			const uint8_t*	constant_data;
-			const uint8_t*	constant_data_translations[2];
-			const uint8_t*	constant_data_scales[2];
-#else
-			// Points to our packed sub-track data
-			const uint8_t*	constant_data_rotations;
-			const uint8_t*	constant_data_translations;
-			const uint8_t*	constant_data_scales;
-#endif
+				const uint8_t* constant_track_data = constant_data_rotations;
 
-			template<class decompression_settings_type>
-			ACL_DISABLE_SECURITY_COOKIE_CHECK void initialize(const persistent_transform_decompression_context_v0& decomp_context)
-			{
-				const transform_tracks_header& transform_header = get_transform_tracks_header(*decomp_context.tracks);
+				if (rotation_format == rotation_format8::quatf_full && decompression_settings_type::is_rotation_format_supported(rotation_format8::quatf_full))
+				{
+					for (uint32_t unpack_index = num_to_unpack; unpack_index != 0; --unpack_index)
+					{
+						// Unpack
+						const rtm::quatf sample = unpack_quat_128(constant_track_data);
 
-				rotations.num_left_to_unpack = transform_header.num_constant_rotation_samples;
+						ACL_ASSERT(rtm::quat_is_finite(sample), "Rotation is not valid!");
+						ACL_ASSERT(rtm::quat_is_normalized(sample), "Rotation is not normalized!");
 
-#if defined(ACL_IMPL_VEC3_UNPACK)
-				translations.num_left_to_unpack = transform_header.num_constant_translation_samples;
-				scales.num_left_to_unpack = transform_header.num_constant_scale_samples;
-#endif
+						// Cache
+						rotations.cached_samples[cache_write_index] = sample;
+						cache_write_index++;
 
-#if defined(ACL_IMPL_USE_CONSTANT_GROUPS)
-				num_left_to_unpack_translations = transform_header.num_constant_translation_samples;
-				num_left_to_unpack_scales = transform_header.num_constant_scale_samples;
+						// Update our read ptr
+						constant_track_data += sizeof(rtm::float4f);
+					}
+				}
+				else
+				{
+					// Unpack
+					// Always load 4x rotations, we might contain garbage in a few lanes but it's fine
+					const uint32_t load_size = num_to_unpack * sizeof(float);
 
-				constant_data = decomp_context.constant_track_data;
-				constant_data_translations[0] = constant_data_translations[1] = nullptr;
-				constant_data_scales[0] = constant_data_scales[1] = nullptr;
-				num_group_translations[0] = num_group_translations[1] = 0;
-				num_group_scales[0] = num_group_scales[1] = 0;
-#else
-				const rotation_format8 rotation_format = get_rotation_format<decompression_settings_type>(decomp_context.rotation_format);
-				const rotation_format8 packed_format = is_rotation_format_variable(rotation_format) ? get_highest_variant_precision(get_rotation_variant(rotation_format)) : rotation_format;
-				const uint32_t packed_rotation_size = get_packed_rotation_size(packed_format);
-				const uint32_t packed_translation_size = get_packed_vector_size(vector_format8::vector3f_full);
+					const rtm::vector4f xxxx = rtm::vector_load(reinterpret_cast<const float*>(constant_track_data + load_size * 0));
+					const rtm::vector4f yyyy = rtm::vector_load(reinterpret_cast<const float*>(constant_track_data + load_size * 1));
+					const rtm::vector4f zzzz = rtm::vector_load(reinterpret_cast<const float*>(constant_track_data + load_size * 2));
 
-				constant_data_rotations = decomp_context.constant_track_data.add_to(decomp_context.tracks);
-				constant_data_translations = constant_data_rotations + packed_rotation_size * transform_header.num_constant_rotation_samples;
-				constant_data_scales = constant_data_translations + packed_translation_size * transform_header.num_constant_translation_samples;
-#endif
-			}
+					// Update our read ptr
+					constant_track_data += load_size * 3;
 
-			template<class decompression_settings_type>
-			ACL_FORCE_INLINE ACL_DISABLE_SECURITY_COOKIE_CHECK void unpack_rotation_group(const persistent_transform_decompression_context_v0& decomp_context)
-			{
-#if defined(ACL_IMPL_USE_CONSTANT_GROUPS)
-				unpack_constant_quat<decompression_settings_type>(decomp_context, rotations, constant_data);
-#else
-				unpack_constant_quat<decompression_settings_type>(decomp_context, rotations, constant_data_rotations);
+					// quat_from_positive_w_soa
+					const rtm::vector4f wwww_squared = rtm::vector_sub(rtm::vector_sub(rtm::vector_sub(rtm::vector_set(1.0F), rtm::vector_mul(xxxx, xxxx)), rtm::vector_mul(yyyy, yyyy)), rtm::vector_mul(zzzz, zzzz));
+
+					// w_squared can be negative either due to rounding or due to quantization imprecision, we take the absolute value
+					// to ensure the resulting quaternion is always normalized with a positive W component
+					const rtm::vector4f wwww = rtm::vector_sqrt(rtm::vector_abs(wwww_squared));
+
+					rtm::vector4f sample0;
+					rtm::vector4f sample1;
+					rtm::vector4f sample2;
+					rtm::vector4f sample3;
+					RTM_MATRIXF_TRANSPOSE_4X4(xxxx, yyyy, zzzz, wwww, sample0, sample1, sample2, sample3);
+
+					// Cache
+					rtm::quatf* cache_ptr = &rotations.cached_samples[cache_write_index];
+					cache_ptr[0] = rtm::vector_to_quat(sample0);
+					cache_ptr[1] = rtm::vector_to_quat(sample1);
+					cache_ptr[2] = rtm::vector_to_quat(sample2);
+					cache_ptr[3] = rtm::vector_to_quat(sample3);
+
+#if defined(ACL_HAS_ASSERT_CHECKS)
+					for (uint32_t unpack_index = 0; unpack_index < num_to_unpack; ++unpack_index)
+					{
+						ACL_ASSERT(rtm::quat_is_finite(rotations.cached_samples[cache_write_index + unpack_index]), "Rotation is not valid!");
+						ACL_ASSERT(rtm::quat_is_normalized(rotations.cached_samples[cache_write_index + unpack_index]), "Rotation is not normalized!");
+					}
 #endif
+				}
+
+				// Update our pointer
+				constant_data_rotations = constant_track_data;
 			}
 
 			template<class decompression_settings_type>
@@ -347,33 +245,6 @@ namespace acl
 				return rotations.cached_samples[cache_read_index % 8];
 			}
 
-			ACL_DISABLE_SECURITY_COOKIE_CHECK void unpack_translation_group()
-			{
-#if defined(ACL_IMPL_VEC3_UNPACK)
-				unpack_constant_vector3(translations, constant_data_translations);
-#else
-#if defined(ACL_IMPL_USE_CONSTANT_GROUPS)
-				if (num_left_to_unpack_translations == 0 || num_unpacked_translations >= 4)
-					return;	// Enough unpacked or nothing to do
-
-				const uint32_t num_to_unpack = std::min<uint32_t>(num_left_to_unpack_translations, 4);
-				num_left_to_unpack_translations -= num_to_unpack;
-
-				// If we have data already unpacked, store in index 1 otherwise store in 0
-				const uint32_t unpack_index = num_unpacked_translations > 0 ? 1 : 0;
-				constant_data_translations[unpack_index] = constant_data;
-				num_group_translations[unpack_index] = num_to_unpack;
-				constant_data += sizeof(rtm::float3f) * num_to_unpack;
-
-				num_unpacked_translations += num_to_unpack;
-
-				ACL_IMPL_CONSTANT_PREFETCH(constant_data + 63);
-#else
-				ACL_IMPL_CONSTANT_PREFETCH(constant_data_translations + 63);
-#endif
-#endif
-			}
-
 			ACL_DISABLE_SECURITY_COOKIE_CHECK void skip_translation_groups(uint32_t num_groups_to_skip)
 			{
 				const uint8_t* constant_track_data = constant_data_translations;
@@ -400,58 +271,10 @@ namespace acl
 
 			ACL_DISABLE_SECURITY_COOKIE_CHECK rtm::vector4f RTM_SIMD_CALL consume_translation()
 			{
-#if defined(ACL_IMPL_VEC3_UNPACK)
-				ACL_ASSERT(translations.cache_read_index < translations.cache_write_index, "Attempting to consume a constant sample that isn't cached");
-				const uint32_t cache_read_index = translations.cache_read_index++;
-				return translations.cached_samples[cache_read_index % 8];
-#else
-#if defined(ACL_IMPL_USE_CONSTANT_GROUPS)
-				const rtm::vector4f sample = rtm::vector_load(constant_data_translations[0]);
-				num_group_translations[0]--;
-				num_unpacked_translations--;
-
-				// If we finished reading from the first group, swap it out otherwise increment our entry
-				if (num_group_translations[0] == 0)
-				{
-					constant_data_translations[0] = constant_data_translations[1];
-					num_group_translations[0] = num_group_translations[1];
-				}
-				else
-					constant_data_translations[0] += sizeof(rtm::float3f);
-#else
 				const rtm::vector4f sample = rtm::vector_load(constant_data_translations);
 				ACL_ASSERT(rtm::vector_is_finite3(sample), "Sample is not valid!");
 				constant_data_translations += sizeof(rtm::float3f);
-#endif
 				return sample;
-#endif
-			}
-
-			ACL_DISABLE_SECURITY_COOKIE_CHECK void unpack_scale_group()
-			{
-#if defined(ACL_IMPL_VEC3_UNPACK)
-				unpack_constant_vector3(scales, constant_data_scales);
-#else
-#if defined(ACL_IMPL_USE_CONSTANT_GROUPS)
-				if (num_left_to_unpack_scales == 0 || num_unpacked_scales >= 4)
-					return;	// Enough unpacked or nothing to do
-
-				const uint32_t num_to_unpack = std::min<uint32_t>(num_left_to_unpack_scales, 4);
-				num_left_to_unpack_scales -= num_to_unpack;
-
-				// If we have data already unpacked, store in index 1 otherwise store in 0
-				const uint32_t unpack_index = num_unpacked_scales > 0 ? 1 : 0;
-				constant_data_scales[unpack_index] = constant_data;
-				num_group_scales[unpack_index] = num_to_unpack;
-				constant_data += sizeof(rtm::float3f) * num_to_unpack;
-
-				num_unpacked_scales += num_to_unpack;
-
-				ACL_IMPL_CONSTANT_PREFETCH(constant_data + 63);
-#else
-				ACL_IMPL_CONSTANT_PREFETCH(constant_data_scales + 63);
-#endif
-#endif
 			}
 
 			ACL_DISABLE_SECURITY_COOKIE_CHECK void skip_scale_groups(uint32_t num_groups_to_skip)
@@ -480,33 +303,16 @@ namespace acl
 
 			ACL_DISABLE_SECURITY_COOKIE_CHECK rtm::vector4f RTM_SIMD_CALL consume_scale()
 			{
-#if defined(ACL_IMPL_VEC3_UNPACK)
-				ACL_ASSERT(scales.cache_read_index < scales.cache_write_index, "Attempting to consume a constant sample that isn't cached");
-				const uint32_t cache_read_index = scales.cache_read_index++;
-				return scales.cached_samples[cache_read_index % 8];
-#else
-#if defined(ACL_IMPL_USE_CONSTANT_GROUPS)
-				const rtm::vector4f scale = rtm::vector_load(constant_data_scales[0]);
-				num_group_scales[0]--;
-				num_unpacked_scales--;
-
-				// If we finished reading from the first group, swap it out otherwise increment our entry
-				if (num_group_scales[0] == 0)
-				{
-					constant_data_scales[0] = constant_data_scales[1];
-					num_group_scales[0] = num_group_scales[1];
-				}
-				else
-					constant_data_scales[0] += sizeof(rtm::float3f);
-#else
 				const rtm::vector4f scale = rtm::vector_load(constant_data_scales);
 				constant_data_scales += sizeof(rtm::float3f);
-#endif
 				return scale;
-#endif
 			}
 		};
 	}
 }
 
+#if defined(ACL_COMPILER_GCC)
+	#pragma GCC diagnostic pop
+#endif
+
 ACL_IMPL_FILE_PRAGMA_POP
diff --git a/includes/acl/decompression/impl/transform_decompression_context.h b/includes/acl/decompression/impl/transform_decompression_context.h
--- a/includes/acl/decompression/impl/transform_decompression_context.h
+++ b/includes/acl/decompression/impl/transform_decompression_context.h
@@ -42,49 +42,46 @@ namespace acl
 	{
 		struct alignas(64) persistent_transform_decompression_context_v0
 		{
-			// Clip related data							//   offsets
+			// Clip related data								//   offsets
 			// Only member used to detect if we are initialized, must be first
-			const compressed_tracks* tracks;				//   0 |   0
+			const compressed_tracks* tracks;					//   0 |   0
 
 			// Database context, optional
-			const database_context_v0* db;					//   4 |   8
+			const database_context_v0* db;						//   4 |   8
 
-			// Offsets relative to the 'tracks' pointer
-			ptr_offset32<uint32_t> constant_tracks_bitset;	//   8 |  16
-			ptr_offset32<uint8_t> constant_track_data;		//  12 |  20
-			ptr_offset32<uint32_t> default_tracks_bitset;	//  16 |  24
-			ptr_offset32<uint8_t> clip_range_data;			//  20 |  28
+			uint32_t clip_hash;									//   8 |  16
 
-			float clip_duration;							//  24 |  32
+			float clip_duration;								//  12 |  20
 
-			bitset_description bitset_desc;					//  28 |  36
+			rotation_format8 rotation_format;					//  16 |  24
+			vector_format8 translation_format;					//  17 |  25
+			vector_format8 scale_format;						//  18 |  26
 
-			uint32_t clip_hash;								//  32 |  40
+			uint8_t has_scale;									//  19 |  27
+			uint8_t has_segments;								//  20 |  28
 
-			rotation_format8 rotation_format;				//  36 |  44
-			vector_format8 translation_format;				//  37 |  45
-			vector_format8 scale_format;					//  38 |  46
-			range_reduction_flags8 range_reduction;			//  39 |  47
+			uint8_t padding0[22];								//  21 |  29
 
-			uint8_t num_rotation_components;				//  40 |  48
-			uint8_t has_segments;							//  41 |  49
+			// Seeking related data
+			uint8_t uses_single_segment;						//  43 |  51
 
-			uint8_t padding0[2];							//  42 |  50
+			float sample_time;									//  44 |  52
 
-			// Seeking related data
-			float sample_time;								//  44 |  52
+			// Offsets relative to the 'tracks' pointer
+			ptr_offset32<segment_header> segment_offsets[2];	//  48 |  56
 
-			const uint8_t* format_per_track_data[2];		//  48 |  56
-			const uint8_t* segment_range_data[2];			//  56 |  72
-			const uint8_t* animated_track_data[2];			//  64 |  88
+			const uint8_t* format_per_track_data[2];			//  56 |  64
+			const uint8_t* segment_range_data[2];				//  64 |  80
+			const uint8_t* animated_track_data[2];				//  72 |  96
 
-			uint32_t key_frame_bit_offsets[2];				//  72 | 104
+			// Offsets relative to the 'animated_track_data' pointers
+			uint32_t key_frame_bit_offsets[2];					//  80 | 112
 
-			float interpolation_alpha;						//  80 | 112
+			float interpolation_alpha;							//  88 | 120
 
-			uint8_t padding1[sizeof(void*) == 4 ? 44 : 12];	//  84 | 116
+			uint8_t padding1[sizeof(void*) == 4 ? 36 : 4];		//  92 | 124
 
-			//									Total size:	   128 | 128
+			//										Total size:	   128 | 128
 
 			//////////////////////////////////////////////////////////////////////////
 
diff --git a/includes/acl/decompression/impl/transform_track_decompression.h b/includes/acl/decompression/impl/transform_track_decompression.h
--- a/includes/acl/decompression/impl/transform_track_decompression.h
+++ b/includes/acl/decompression/impl/transform_track_decompression.h
@@ -49,6 +49,8 @@
 #include <cstdint>
 #include <type_traits>
 
+#define ACL_IMPL_USE_SEEK_PREFETCH
+
 ACL_IMPL_FILE_PRAGMA_PUSH
 
 #if defined(ACL_COMPILER_MSVC)
@@ -62,6 +64,12 @@ namespace acl
 {
 	namespace acl_impl
 	{
+#if defined(ACL_IMPL_USE_SEEK_PREFETCH)
+#define ACL_IMPL_SEEK_PREFETCH(ptr) memory_prefetch(ptr)
+#else
+#define ACL_IMPL_SEEK_PREFETCH(ptr) (void)(ptr)
+#endif
+
 		template<class decompression_settings_type>
 		constexpr bool is_database_supported_impl()
 		{
@@ -95,35 +103,11 @@ namespace acl
 			context.clip_hash = tracks.get_hash();
 			context.clip_duration = calculate_duration(header.num_samples, header.sample_rate);
 			context.sample_time = -1.0F;
-			context.default_tracks_bitset = ptr_offset32<uint32_t>(&tracks, transform_header.get_default_tracks_bitset());
-			context.constant_tracks_bitset = ptr_offset32<uint32_t>(&tracks, transform_header.get_constant_tracks_bitset());
-			context.constant_track_data = ptr_offset32<uint8_t>(&tracks, transform_header.get_constant_track_data());
-			context.clip_range_data = ptr_offset32<uint8_t>(&tracks, transform_header.get_clip_range_data());
-
-			for (uint32_t key_frame_index = 0; key_frame_index < 2; ++key_frame_index)
-			{
-				context.format_per_track_data[key_frame_index] = nullptr;
-				context.segment_range_data[key_frame_index] = nullptr;
-				context.animated_track_data[key_frame_index] = nullptr;
-			}
-
-			const bool has_scale = header.get_has_scale();
-			const uint32_t num_tracks_per_bone = has_scale ? 3 : 2;
-			context.bitset_desc = bitset_description::make_from_num_bits(header.num_tracks * num_tracks_per_bone);
-
-			range_reduction_flags8 range_reduction = range_reduction_flags8::none;
-			if (is_rotation_format_variable(rotation_format))
-				range_reduction |= range_reduction_flags8::rotations;
-			if (is_vector_format_variable(translation_format))
-				range_reduction |= range_reduction_flags8::translations;
-			if (is_vector_format_variable(scale_format))
-				range_reduction |= range_reduction_flags8::scales;
 
 			context.rotation_format = rotation_format;
 			context.translation_format = translation_format;
 			context.scale_format = scale_format;
-			context.range_reduction = range_reduction;
-			context.num_rotation_components = rotation_format == rotation_format8::quatf_full ? 4 : 3;
+			context.has_scale = header.get_has_scale();
 			context.has_segments = transform_header.has_multiple_segments();
 
 			return true;
@@ -150,11 +134,21 @@ namespace acl
 			if (context.sample_time == sample_time)
 				return;
 
-			context.sample_time = sample_time;
-
 			const tracks_header& header = get_tracks_header(*context.tracks);
 			const transform_tracks_header& transform_header = get_transform_tracks_header(*context.tracks);
 
+			// Prefetch our sub-track types, we'll need them soon when we start decompressing
+			// Most clips will have their sub-track types fit into 1 or 2 cache lines, we'll prefetch 2
+			// to be safe
+			{
+				const uint8_t* sub_track_types = reinterpret_cast<const uint8_t*>(transform_header.get_sub_track_types());
+
+				ACL_IMPL_SEEK_PREFETCH(sub_track_types);
+				ACL_IMPL_SEEK_PREFETCH(sub_track_types + 64);
+			}
+
+			context.sample_time = sample_time;
+
 			uint32_t key_frame0;
 			uint32_t key_frame1;
 			find_linear_interpolation_samples_with_sample_rate(header.num_samples, header.sample_rate, sample_time, rounding_policy, key_frame0, key_frame1, context.interpolation_alpha);
@@ -426,12 +420,24 @@ namespace acl
 				}
 			}
 
+			{
+				// Prefetch our constant rotation data, we'll need it soon when we start decompressing and we are about to cache miss on the segment headers
+				const uint8_t* constant_data_rotations = transform_header.get_constant_track_data();
+				ACL_IMPL_SEEK_PREFETCH(constant_data_rotations);
+				ACL_IMPL_SEEK_PREFETCH(constant_data_rotations + 64);
+			}
+
+			const bool uses_single_segment = segment_header0 == segment_header1;
+			context.uses_single_segment = uses_single_segment;
+
 			// Cache miss if we don't access the db data
 			transform_header.get_segment_data(*segment_header0, context.format_per_track_data[0], context.segment_range_data[0], context.animated_track_data[0]);
 
 			// More often than not the two segments are identical, when this is the case, just copy our pointers
-			if (segment_header0 != segment_header1)
+			if (!uses_single_segment)
+			{
 				transform_header.get_segment_data(*segment_header1, context.format_per_track_data[1], context.segment_range_data[1], context.animated_track_data[1]);
+			}
 			else
 			{
 				context.format_per_track_data[1] = context.format_per_track_data[0];
@@ -451,150 +457,924 @@ namespace acl
 
 			context.key_frame_bit_offsets[0] = segment_key_frame0 * segment_header0->animated_pose_bit_size;
 			context.key_frame_bit_offsets[1] = segment_key_frame1 * segment_header1->animated_pose_bit_size;
+
+			context.segment_offsets[0] = ptr_offset32<segment_header>(context.tracks, segment_header0);
+			context.segment_offsets[1] = ptr_offset32<segment_header>(context.tracks, segment_header1);
 		}
 
-		// TODO: Stage bitset decomp
+
 		// TODO: Merge the per track format and segment range info into a single buffer? Less to prefetch and used together
-		// TODO: How do we hide the cache miss after the seek to read the segment header? What work can we do while we prefetch?
-		// TODO: Port vector3 decomp to use SOA
-		// TODO: Unroll quat unpacking and convert to SOA
-		// TODO: Use AVX where we can
 		// TODO: Remove segment data alignment, no longer required?
 
 
-		template<class decompression_settings_type, class track_writer_type>
-		inline void decompress_tracks_v0(const persistent_transform_decompression_context_v0& context, track_writer_type& writer)
+		// Force inline this function, we only use it to keep the code readable
+		template<class track_writer_type>
+		ACL_FORCE_INLINE ACL_DISABLE_SECURITY_COOKIE_CHECK void RTM_SIMD_CALL unpack_default_rotation_sub_tracks(
+			const packed_sub_track_types* rotation_sub_track_types, uint32_t last_entry_index, uint32_t padding_mask,
+			rtm::quatf_arg0 default_rotation, track_writer_type& writer)
 		{
-			ACL_ASSERT(context.sample_time >= 0.0f, "Context not set to a valid sample time");
-			if (context.sample_time < 0.0F)
-				return;	// Invalid sample time, we didn't seek yet
+			for (uint32_t entry_index = 0, track_index = 0; entry_index <= last_entry_index; ++entry_index)
+			{
+				uint32_t packed_entry = rotation_sub_track_types[entry_index].types;
 
-			// Due to the SIMD operations, we sometimes overflow in the SIMD lanes not used.
-			// Disable floating point exceptions to avoid issues.
-			fp_environment fp_env;
-			if (decompression_settings_type::disable_fp_exeptions())
-				disable_fp_exceptions(fp_env);
+				// Mask out everything but default sub-tracks, this way we can early out when we iterate
+				// Each sub-track is either 0 (default), 1 (constant), or 2 (animated)
+				// By flipping the bits with logical NOT, 0 becomes 3, 1 becomes 2, and 2 becomes 1
+				// We then subtract 1 from every group so 3 becomes 2, 2 becomes 1, and 1 becomes 0
+				// Finally, we mask out everything but the second bit for each sub-track
+				// After this, our original default tracks are equal to 2, our constant tracks are equal to 1, and our animated tracks are equal to 0
+				// Testing for default tracks can be done by testing the second bit of each group (same as animated track testing)
+				packed_entry = (~packed_entry - 0x55555555) & 0xAAAAAAAA;
 
-			const tracks_header& header = get_tracks_header(*context.tracks);
+				// Because our last entry might have padding with 0 (default), we have to strip any padding we might have
+				const uint32_t entry_padding_mask = (entry_index == last_entry_index) ? padding_mask : 0xFFFFFFFF;
+				packed_entry &= entry_padding_mask;
 
-			using translation_adapter = acl_impl::translation_decompression_settings_adapter<decompression_settings_type>;
-			using scale_adapter = acl_impl::scale_decompression_settings_adapter<decompression_settings_type>;
+				uint32_t curr_entry_track_index = track_index;
 
-			const rtm::vector4f default_translation = rtm::vector_zero();
-			const rtm::vector4f default_scale = rtm::vector_set(float(header.get_default_scale()));
-			const bool has_scale = header.get_has_scale();
-			const uint32_t num_tracks = header.num_tracks;
+				// We might early out below, always skip 16 tracks
+				track_index += 16;
 
-			const uint32_t* default_tracks_bitset = context.default_tracks_bitset.add_to(context.tracks);
-			const uint32_t* constant_tracks_bitset = context.constant_tracks_bitset.add_to(context.tracks);
+				// Process 4 sub-tracks at a time
+				while (packed_entry != 0)
+				{
+					const uint32_t packed_group = packed_entry;
+					const uint32_t curr_group_track_index = curr_entry_track_index;
 
-			constant_track_cache_v0 constant_track_cache;
-			constant_track_cache.initialize<decompression_settings_type>(context);
+					// Move to the next group
+					packed_entry <<= 8;
+					curr_entry_track_index += 4;
 
-			animated_track_cache_v0 animated_track_cache;
-			animated_track_cache.initialize(context);
+					if ((packed_group & 0xAA000000) == 0)
+						continue;	// This group contains no default sub-tracks, skip it
+
+					if ((packed_group & 0x80000000) != 0)
+					{
+						const uint32_t track_index0 = curr_group_track_index + 0;
+
+						if (!track_writer_type::skip_all_rotations() && !writer.skip_track_rotation(track_index0))
+							writer.write_rotation(track_index0, default_rotation);
+					}
+
+					if ((packed_group & 0x20000000) != 0)
+					{
+						const uint32_t track_index1 = curr_group_track_index + 1;
+
+						if (!track_writer_type::skip_all_rotations() && !writer.skip_track_rotation(track_index1))
+							writer.write_rotation(track_index1, default_rotation);
+					}
+
+					if ((packed_group & 0x08000000) != 0)
+					{
+						const uint32_t track_index2 = curr_group_track_index + 2;
+
+						if (!track_writer_type::skip_all_rotations() && !writer.skip_track_rotation(track_index2))
+							writer.write_rotation(track_index2, default_rotation);
+					}
 
-			uint32_t sub_track_index = 0;
+					if ((packed_group & 0x02000000) != 0)
+					{
+						const uint32_t track_index3 = curr_group_track_index + 3;
+
+						if (!track_writer_type::skip_all_rotations() && !writer.skip_track_rotation(track_index3))
+							writer.write_rotation(track_index3, default_rotation);
+					}
+				}
+			}
+		}
 
-			for (uint32_t track_index = 0; track_index < num_tracks; ++track_index)
+		// Force inline this function, we only use it to keep the code readable
+		template<class decompression_settings_type, class track_writer_type>
+		ACL_FORCE_INLINE ACL_DISABLE_SECURITY_COOKIE_CHECK void RTM_SIMD_CALL unpack_constant_rotation_sub_tracks(
+			const packed_sub_track_types* rotation_sub_track_types, uint32_t last_entry_index,
+			const persistent_transform_decompression_context_v0& context,
+			constant_track_cache_v0& constant_track_cache, track_writer_type& writer)
+		{
+			for (uint32_t entry_index = 0, track_index = 0; entry_index <= last_entry_index; ++entry_index)
 			{
-				if ((track_index % 4) == 0)
+				// Mask out everything but constant sub-tracks, this way we can early out when we iterate
+				// Use and_not(..) to load our sub-track types directly from memory on x64 with BMI
+				uint32_t packed_entry = and_not(~0x55555555U, rotation_sub_track_types[entry_index].types);
+
+				uint32_t curr_entry_track_index = track_index;
+
+				// We might early out below, always skip 16 tracks
+				track_index += 16;
+
+				// Process 4 sub-tracks at a time
+				while (packed_entry != 0)
 				{
+					const uint32_t packed_group = packed_entry;
+					const uint32_t curr_group_track_index = curr_entry_track_index;
+
+					// Move to the next group
+					packed_entry <<= 8;
+					curr_entry_track_index += 4;
+
+					if ((packed_group & 0x55000000) == 0)
+						continue;	// This group contains no constant sub-tracks, skip it
+
 					// Unpack our next 4 tracks
 					constant_track_cache.unpack_rotation_group<decompression_settings_type>(context);
-					constant_track_cache.unpack_translation_group();
 
+					if ((packed_group & 0x40000000) != 0)
+					{
+						const uint32_t track_index0 = curr_group_track_index + 0;
+						const rtm::quatf rotation = constant_track_cache.consume_rotation();
+
+						if (!track_writer_type::skip_all_rotations() && !writer.skip_track_rotation(track_index0))
+							writer.write_rotation(track_index0, rotation);
+					}
+
+					if ((packed_group & 0x10000000) != 0)
+					{
+						const uint32_t track_index1 = curr_group_track_index + 1;
+						const rtm::quatf rotation = constant_track_cache.consume_rotation();
+
+						if (!track_writer_type::skip_all_rotations() && !writer.skip_track_rotation(track_index1))
+							writer.write_rotation(track_index1, rotation);
+					}
+
+					if ((packed_group & 0x04000000) != 0)
+					{
+						const uint32_t track_index2 = curr_group_track_index + 2;
+						const rtm::quatf rotation = constant_track_cache.consume_rotation();
+
+						if (!track_writer_type::skip_all_rotations() && !writer.skip_track_rotation(track_index2))
+							writer.write_rotation(track_index2, rotation);
+					}
+
+					if ((packed_group & 0x01000000) != 0)
+					{
+						const uint32_t track_index3 = curr_group_track_index + 3;
+						const rtm::quatf rotation = constant_track_cache.consume_rotation();
+
+						if (!track_writer_type::skip_all_rotations() && !writer.skip_track_rotation(track_index3))
+							writer.write_rotation(track_index3, rotation);
+					}
+				}
+			}
+		}
+
+		// Force inline this function, we only use it to keep the code readable
+		template<class decompression_settings_type, class track_writer_type>
+		ACL_FORCE_INLINE ACL_DISABLE_SECURITY_COOKIE_CHECK void RTM_SIMD_CALL unpack_animated_rotation_sub_tracks(
+			const packed_sub_track_types* rotation_sub_track_types, uint32_t last_entry_index,
+			const persistent_transform_decompression_context_v0& context,
+			animated_track_cache_v0& animated_track_cache, track_writer_type& writer)
+		{
+			for (uint32_t entry_index = 0, track_index = 0; entry_index <= last_entry_index; ++entry_index)
+			{
+				// Mask out everything but animated sub-tracks, this way we can early out when we iterate
+				// Use and_not(..) to load our sub-track types directly from memory on x64 with BMI
+				uint32_t packed_entry = and_not(~0xAAAAAAAAU, rotation_sub_track_types[entry_index].types);
+
+				uint32_t curr_entry_track_index = track_index;
+
+				// We might early out below, always skip 16 tracks
+				track_index += 16;
+
+				// Process 4 sub-tracks at a time
+				while (packed_entry != 0)
+				{
+					const uint32_t packed_group = packed_entry;
+					const uint32_t curr_group_track_index = curr_entry_track_index;
+
+					// Move to the next group
+					packed_entry <<= 8;
+					curr_entry_track_index += 4;
+
+					if ((packed_group & 0xAA000000) == 0)
+						continue;	// This group contains no animated sub-tracks, skip it
+
+					// Unpack our next 4 tracks
 					animated_track_cache.unpack_rotation_group<decompression_settings_type>(context);
+
+					if ((packed_group & 0x80000000) != 0)
+					{
+						const uint32_t track_index0 = curr_group_track_index + 0;
+						const rtm::quatf rotation = animated_track_cache.consume_rotation();
+
+						ACL_ASSERT(rtm::quat_is_finite(rotation), "Rotation is not valid!");
+						ACL_ASSERT(rtm::quat_is_normalized(rotation), "Rotation is not normalized!");
+
+						if (!track_writer_type::skip_all_rotations() && !writer.skip_track_rotation(track_index0))
+							writer.write_rotation(track_index0, rotation);
+					}
+
+					if ((packed_group & 0x20000000) != 0)
+					{
+						const uint32_t track_index1 = curr_group_track_index + 1;
+						const rtm::quatf rotation = animated_track_cache.consume_rotation();
+
+						ACL_ASSERT(rtm::quat_is_finite(rotation), "Rotation is not valid!");
+						ACL_ASSERT(rtm::quat_is_normalized(rotation), "Rotation is not normalized!");
+
+						if (!track_writer_type::skip_all_rotations() && !writer.skip_track_rotation(track_index1))
+							writer.write_rotation(track_index1, rotation);
+					}
+
+					if ((packed_group & 0x08000000) != 0)
+					{
+						const uint32_t track_index2 = curr_group_track_index + 2;
+						const rtm::quatf rotation = animated_track_cache.consume_rotation();
+
+						ACL_ASSERT(rtm::quat_is_finite(rotation), "Rotation is not valid!");
+						ACL_ASSERT(rtm::quat_is_normalized(rotation), "Rotation is not normalized!");
+
+						if (!track_writer_type::skip_all_rotations() && !writer.skip_track_rotation(track_index2))
+							writer.write_rotation(track_index2, rotation);
+					}
+
+					if ((packed_group & 0x02000000) != 0)
+					{
+						const uint32_t track_index3 = curr_group_track_index + 3;
+						const rtm::quatf rotation = animated_track_cache.consume_rotation();
+
+						ACL_ASSERT(rtm::quat_is_finite(rotation), "Rotation is not valid!");
+						ACL_ASSERT(rtm::quat_is_normalized(rotation), "Rotation is not normalized!");
+
+						if (!track_writer_type::skip_all_rotations() && !writer.skip_track_rotation(track_index3))
+							writer.write_rotation(track_index3, rotation);
+					}
+				}
+			}
+		}
+
+		// Force inline this function, we only use it to keep the code readable
+		template<class track_writer_type>
+		ACL_FORCE_INLINE ACL_DISABLE_SECURITY_COOKIE_CHECK void RTM_SIMD_CALL unpack_default_translation_sub_tracks(
+			const packed_sub_track_types* translation_sub_track_types, uint32_t last_entry_index, uint32_t padding_mask,
+			rtm::vector4f_arg0 default_translation, track_writer_type& writer)
+		{
+			for (uint32_t entry_index = 0, track_index = 0; entry_index <= last_entry_index; ++entry_index)
+			{
+				uint32_t packed_entry = translation_sub_track_types[entry_index].types;
+
+				// Mask out everything but default sub-tracks, this way we can early out when we iterate
+				// Each sub-track is either 0 (default), 1 (constant), or 2 (animated)
+				// By flipping the bits with logical NOT, 0 becomes 3, 1 becomes 2, and 2 becomes 1
+				// We then subtract 1 from every group so 3 becomes 2, 2 becomes 1, and 1 becomes 0
+				// Finally, we mask out everything but the second bit for each sub-track
+				// After this, our original default tracks are equal to 2, our constant tracks are equal to 1, and our animated tracks are equal to 0
+				// Testing for default tracks can be done by testing the second bit of each group (same as animated track testing)
+				packed_entry = (~packed_entry - 0x55555555) & 0xAAAAAAAA;
+
+				// Because our last entry might have padding with 0 (default), we have to strip any padding we might have
+				const uint32_t entry_padding_mask = (entry_index == last_entry_index) ? padding_mask : 0xFFFFFFFF;
+				packed_entry &= entry_padding_mask;
+
+				uint32_t curr_entry_track_index = track_index;
+
+				// We might early out below, always skip 16 tracks
+				track_index += 16;
+
+				// Process 4 sub-tracks at a time
+				while (packed_entry != 0)
+				{
+					const uint32_t packed_group = packed_entry;
+					const uint32_t curr_group_track_index = curr_entry_track_index;
+
+					// Move to the next group
+					packed_entry <<= 8;
+					curr_entry_track_index += 4;
+
+					if ((packed_group & 0xAA000000) == 0)
+						continue;	// This group contains no default sub-tracks, skip it
+
+					if ((packed_group & 0x80000000) != 0)
+					{
+						const uint32_t track_index0 = curr_group_track_index + 0;
+
+						if (!track_writer_type::skip_all_translations() && !writer.skip_track_translation(track_index0))
+							writer.write_translation(track_index0, default_translation);
+					}
+
+					if ((packed_group & 0x20000000) != 0)
+					{
+						const uint32_t track_index1 = curr_group_track_index + 1;
+
+						if (!track_writer_type::skip_all_translations() && !writer.skip_track_translation(track_index1))
+							writer.write_translation(track_index1, default_translation);
+					}
+
+					if ((packed_group & 0x08000000) != 0)
+					{
+						const uint32_t track_index2 = curr_group_track_index + 2;
+
+						if (!track_writer_type::skip_all_translations() && !writer.skip_track_translation(track_index2))
+							writer.write_translation(track_index2, default_translation);
+					}
+
+					if ((packed_group & 0x02000000) != 0)
+					{
+						const uint32_t track_index3 = curr_group_track_index + 3;
+
+						if (!track_writer_type::skip_all_translations() && !writer.skip_track_translation(track_index3))
+							writer.write_translation(track_index3, default_translation);
+					}
+				}
+			}
+		}
+
+		// Force inline this function, we only use it to keep the code readable
+		template<class track_writer_type>
+		ACL_FORCE_INLINE ACL_DISABLE_SECURITY_COOKIE_CHECK void RTM_SIMD_CALL unpack_constant_translation_sub_tracks(
+			const packed_sub_track_types* translation_sub_track_types, uint32_t last_entry_index,
+			constant_track_cache_v0& constant_track_cache, track_writer_type& writer)
+		{
+			for (uint32_t entry_index = 0, track_index = 0; entry_index <= last_entry_index; ++entry_index)
+			{
+				// Mask out everything but constant sub-tracks, this way we can early out when we iterate
+				// Use and_not(..) to load our sub-track types directly from memory on x64 with BMI
+				uint32_t packed_entry = and_not(~0x55555555U, translation_sub_track_types[entry_index].types);
+
+				uint32_t curr_entry_track_index = track_index;
+
+				// We might early out below, always skip 16 tracks
+				track_index += 16;
+
+				// Process 4 sub-tracks at a time
+				while (packed_entry != 0)
+				{
+					const uint32_t packed_group = packed_entry;
+					const uint32_t curr_group_track_index = curr_entry_track_index;
+
+					// Move to the next group
+					packed_entry <<= 8;
+					curr_entry_track_index += 4;
+
+					if ((packed_group & 0x55000000) == 0)
+						continue;	// This group contains no constant sub-tracks, skip it
+
+					if ((packed_group & 0x40000000) != 0)
+					{
+						const uint32_t track_index0 = curr_group_track_index + 0;
+						const rtm::vector4f translation = constant_track_cache.consume_translation();
+
+						ACL_ASSERT(rtm::vector_is_finite3(translation), "Translation is not valid!");
+
+						if (!track_writer_type::skip_all_translations() && !writer.skip_track_translation(track_index0))
+							writer.write_translation(track_index0, translation);
+					}
+
+					if ((packed_group & 0x10000000) != 0)
+					{
+						const uint32_t track_index1 = curr_group_track_index + 1;
+						const rtm::vector4f translation = constant_track_cache.consume_translation();
+
+						ACL_ASSERT(rtm::vector_is_finite3(translation), "Translation is not valid!");
+
+						if (!track_writer_type::skip_all_translations() && !writer.skip_track_translation(track_index1))
+							writer.write_translation(track_index1, translation);
+					}
+
+					if ((packed_group & 0x04000000) != 0)
+					{
+						const uint32_t track_index2 = curr_group_track_index + 2;
+						const rtm::vector4f translation = constant_track_cache.consume_translation();
+
+						ACL_ASSERT(rtm::vector_is_finite3(translation), "Translation is not valid!");
+
+						if (!track_writer_type::skip_all_translations() && !writer.skip_track_translation(track_index2))
+							writer.write_translation(track_index2, translation);
+					}
+
+					if ((packed_group & 0x01000000) != 0)
+					{
+						const uint32_t track_index3 = curr_group_track_index + 3;
+						const rtm::vector4f translation = constant_track_cache.consume_translation();
+
+						ACL_ASSERT(rtm::vector_is_finite3(translation), "Translation is not valid!");
+
+						if (!track_writer_type::skip_all_translations() && !writer.skip_track_translation(track_index3))
+							writer.write_translation(track_index3, translation);
+					}
+				}
+			}
+		}
+
+		// Force inline this function, we only use it to keep the code readable
+		template<class translation_adapter, class track_writer_type>
+		ACL_FORCE_INLINE ACL_DISABLE_SECURITY_COOKIE_CHECK void RTM_SIMD_CALL unpack_animated_translation_sub_tracks(
+			const packed_sub_track_types* translation_sub_track_types, uint32_t last_entry_index,
+			const persistent_transform_decompression_context_v0& context,
+			animated_track_cache_v0& animated_track_cache, track_writer_type& writer)
+		{
+			for (uint32_t entry_index = 0, track_index = 0; entry_index <= last_entry_index; ++entry_index)
+			{
+				// Mask out everything but animated sub-tracks, this way we can early out when we iterate
+				// Use and_not(..) to load our sub-track types directly from memory on x64 with BMI
+				uint32_t packed_entry = and_not(~0xAAAAAAAAU, translation_sub_track_types[entry_index].types);
+
+				uint32_t curr_entry_track_index = track_index;
+
+				// We might early out below, always skip 16 tracks
+				track_index += 16;
+
+				// Process 4 sub-tracks at a time
+				while (packed_entry != 0)
+				{
+					const uint32_t packed_group = packed_entry;
+					const uint32_t curr_group_track_index = curr_entry_track_index;
+
+					// Move to the next group
+					packed_entry <<= 8;
+					curr_entry_track_index += 4;
+
+					if ((packed_group & 0xAA000000) == 0)
+						continue;	// This group contains no animated sub-tracks, skip it
+
+					// Unpack our next 4 tracks
 					animated_track_cache.unpack_translation_group<translation_adapter>(context);
 
-					if (has_scale)
+					if ((packed_group & 0x80000000) != 0)
+					{
+						const uint32_t track_index0 = curr_group_track_index + 0;
+						const rtm::vector4f translation = animated_track_cache.consume_translation();
+
+						ACL_ASSERT(rtm::vector_is_finite3(translation), "Translation is not valid!");
+
+						if (!track_writer_type::skip_all_translations() && !writer.skip_track_translation(track_index0))
+							writer.write_translation(track_index0, translation);
+					}
+
+					if ((packed_group & 0x20000000) != 0)
 					{
-						constant_track_cache.unpack_scale_group();
-						animated_track_cache.unpack_scale_group<scale_adapter>(context);
+						const uint32_t track_index1 = curr_group_track_index + 1;
+						const rtm::vector4f translation = animated_track_cache.consume_translation();
+
+						ACL_ASSERT(rtm::vector_is_finite3(translation), "Translation is not valid!");
+
+						if (!track_writer_type::skip_all_translations() && !writer.skip_track_translation(track_index1))
+							writer.write_translation(track_index1, translation);
+					}
+
+					if ((packed_group & 0x08000000) != 0)
+					{
+						const uint32_t track_index2 = curr_group_track_index + 2;
+						const rtm::vector4f translation = animated_track_cache.consume_translation();
+
+						ACL_ASSERT(rtm::vector_is_finite3(translation), "Translation is not valid!");
+
+						if (!track_writer_type::skip_all_translations() && !writer.skip_track_translation(track_index2))
+							writer.write_translation(track_index2, translation);
+					}
+
+					if ((packed_group & 0x02000000) != 0)
+					{
+						const uint32_t track_index3 = curr_group_track_index + 3;
+						const rtm::vector4f translation = animated_track_cache.consume_translation();
+
+						ACL_ASSERT(rtm::vector_is_finite3(translation), "Translation is not valid!");
+
+						if (!track_writer_type::skip_all_translations() && !writer.skip_track_translation(track_index3))
+							writer.write_translation(track_index3, translation);
 					}
 				}
+			}
+		}
+
+		// Force inline this function, we only use it to keep the code readable
+		template<class track_writer_type>
+		ACL_FORCE_INLINE ACL_DISABLE_SECURITY_COOKIE_CHECK void RTM_SIMD_CALL unpack_default_scale_sub_tracks(
+			const packed_sub_track_types* scale_sub_track_types, uint32_t last_entry_index, uint32_t padding_mask,
+			rtm::vector4f_arg0 default_scale, track_writer_type& writer)
+		{
+			for (uint32_t entry_index = 0, track_index = 0; entry_index <= last_entry_index; ++entry_index)
+			{
+				uint32_t packed_entry = scale_sub_track_types[entry_index].types;
 
+				// Mask out everything but default sub-tracks, this way we can early out when we iterate
+				// Each sub-track is either 0 (default), 1 (constant), or 2 (animated)
+				// By flipping the bits with logical NOT, 0 becomes 3, 1 becomes 2, and 2 becomes 1
+				// We then subtract 1 from every group so 3 becomes 2, 2 becomes 1, and 1 becomes 0
+				// Finally, we mask out everything but the second bit for each sub-track
+				// After this, our original default tracks are equal to 2, our constant tracks are equal to 1, and our animated tracks are equal to 0
+				// Testing for default tracks can be done by testing the second bit of each group (same as animated track testing)
+				packed_entry = (~packed_entry - 0x55555555) & 0xAAAAAAAA;
+
+				// Because our last entry might have padding with 0 (default), we have to strip any padding we might have
+				const uint32_t entry_padding_mask = (entry_index == last_entry_index) ? padding_mask : 0xFFFFFFFF;
+				packed_entry &= entry_padding_mask;
+
+				uint32_t curr_entry_track_index = track_index;
+
+				// We might early out below, always skip 16 tracks
+				track_index += 16;
+
+				// Process 4 sub-tracks at a time
+				while (packed_entry != 0)
 				{
-					const bitset_index_ref track_index_bit_ref(context.bitset_desc, sub_track_index);
-					const bool is_sample_default = bitset_test(default_tracks_bitset, track_index_bit_ref);
-					rtm::quatf rotation;
-					if (is_sample_default)
+					const uint32_t packed_group = packed_entry;
+					const uint32_t curr_group_track_index = curr_entry_track_index;
+
+					// Move to the next group
+					packed_entry <<= 8;
+					curr_entry_track_index += 4;
+
+					if ((packed_group & 0xAA000000) == 0)
+						continue;	// This group contains no default sub-tracks, skip it
+
+					if ((packed_group & 0x80000000) != 0)
 					{
-						rotation = rtm::quat_identity();
+						const uint32_t track_index0 = curr_group_track_index + 0;
+
+						if (!track_writer_type::skip_all_scales() && !writer.skip_track_scale(track_index0))
+							writer.write_scale(track_index0, default_scale);
 					}
-					else
+
+					if ((packed_group & 0x20000000) != 0)
 					{
-						const bool is_sample_constant = bitset_test(constant_tracks_bitset, track_index_bit_ref);
-						if (is_sample_constant)
-							rotation = constant_track_cache.consume_rotation();
-						else
-							rotation = animated_track_cache.consume_rotation();
+						const uint32_t track_index1 = curr_group_track_index + 1;
+
+						if (!track_writer_type::skip_all_scales() && !writer.skip_track_scale(track_index1))
+							writer.write_scale(track_index1, default_scale);
 					}
 
-					ACL_ASSERT(rtm::quat_is_finite(rotation), "Rotation is not valid!");
-					ACL_ASSERT(rtm::quat_is_normalized(rotation), "Rotation is not normalized!");
+					if ((packed_group & 0x08000000) != 0)
+					{
+						const uint32_t track_index2 = curr_group_track_index + 2;
+
+						if (!track_writer_type::skip_all_scales() && !writer.skip_track_scale(track_index2))
+							writer.write_scale(track_index2, default_scale);
+					}
 
-					if (!track_writer_type::skip_all_rotations() && !writer.skip_track_rotation(track_index))
-						writer.write_rotation(track_index, rotation);
-					sub_track_index++;
+					if ((packed_group & 0x02000000) != 0)
+					{
+						const uint32_t track_index3 = curr_group_track_index + 3;
+
+						if (!track_writer_type::skip_all_scales() && !writer.skip_track_scale(track_index3))
+							writer.write_scale(track_index3, default_scale);
+					}
 				}
+			}
+		}
 
+		// Force inline this function, we only use it to keep the code readable
+		template<class track_writer_type>
+		ACL_FORCE_INLINE ACL_DISABLE_SECURITY_COOKIE_CHECK void RTM_SIMD_CALL unpack_constant_scale_sub_tracks(
+			const packed_sub_track_types* scale_sub_track_types, uint32_t last_entry_index,
+			constant_track_cache_v0& constant_track_cache, track_writer_type& writer)
+		{
+			for (uint32_t entry_index = 0, track_index = 0; entry_index <= last_entry_index; ++entry_index)
+			{
+				// Mask out everything but constant sub-tracks, this way we can early out when we iterate
+				// Use and_not(..) to load our sub-track types directly from memory on x64 with BMI
+				uint32_t packed_entry = and_not(~0x55555555U, scale_sub_track_types[entry_index].types);
+
+				uint32_t curr_entry_track_index = track_index;
+
+				// We might early out below, always skip 16 tracks
+				track_index += 16;
+
+				// Process 4 sub-tracks at a time
+				while (packed_entry != 0)
 				{
-					const bitset_index_ref track_index_bit_ref(context.bitset_desc, sub_track_index);
-					const bool is_sample_default = bitset_test(default_tracks_bitset, track_index_bit_ref);
-					rtm::vector4f translation;
-					if (is_sample_default)
+					const uint32_t packed_group = packed_entry;
+					const uint32_t curr_group_track_index = curr_entry_track_index;
+
+					// Move to the next group
+					packed_entry <<= 8;
+					curr_entry_track_index += 4;
+
+					if ((packed_group & 0x55000000) == 0)
+						continue;	// This group contains no constant sub-tracks, skip it
+
+					if ((packed_group & 0x40000000) != 0)
+					{
+						const uint32_t track_index0 = curr_group_track_index + 0;
+						const rtm::vector4f scale = constant_track_cache.consume_scale();
+
+						ACL_ASSERT(rtm::vector_is_finite3(scale), "Scale is not valid!");
+
+						if (!track_writer_type::skip_all_scales() && !writer.skip_track_scale(track_index0))
+							writer.write_scale(track_index0, scale);
+					}
+
+					if ((packed_group & 0x10000000) != 0)
 					{
-						translation = default_translation;
+						const uint32_t track_index1 = curr_group_track_index + 1;
+						const rtm::vector4f scale = constant_track_cache.consume_scale();
+
+						ACL_ASSERT(rtm::vector_is_finite3(scale), "Scale is not valid!");
+
+						if (!track_writer_type::skip_all_scales() && !writer.skip_track_scale(track_index1))
+							writer.write_scale(track_index1, scale);
 					}
-					else
+
+					if ((packed_group & 0x04000000) != 0)
 					{
-						const bool is_sample_constant = bitset_test(constant_tracks_bitset, track_index_bit_ref);
-						if (is_sample_constant)
-							translation = constant_track_cache.consume_translation();
-						else
-							translation = animated_track_cache.consume_translation();
+						const uint32_t track_index2 = curr_group_track_index + 2;
+						const rtm::vector4f scale = constant_track_cache.consume_scale();
+
+						ACL_ASSERT(rtm::vector_is_finite3(scale), "Scale is not valid!");
+
+						if (!track_writer_type::skip_all_scales() && !writer.skip_track_scale(track_index2))
+							writer.write_scale(track_index2, scale);
 					}
 
-					ACL_ASSERT(rtm::vector_is_finite3(translation), "Translation is not valid!");
+					if ((packed_group & 0x01000000) != 0)
+					{
+						const uint32_t track_index3 = curr_group_track_index + 3;
+						const rtm::vector4f scale = constant_track_cache.consume_scale();
+
+						ACL_ASSERT(rtm::vector_is_finite3(scale), "Scale is not valid!");
 
-					if (!track_writer_type::skip_all_translations() && !writer.skip_track_translation(track_index))
-						writer.write_translation(track_index, translation);
-					sub_track_index++;
+						if (!track_writer_type::skip_all_scales() && !writer.skip_track_scale(track_index3))
+							writer.write_scale(track_index3, scale);
+					}
 				}
+			}
+		}
+
+		// Force inline this function, we only use it to keep the code readable
+		template<class scale_adapter, class track_writer_type>
+		ACL_FORCE_INLINE ACL_DISABLE_SECURITY_COOKIE_CHECK void RTM_SIMD_CALL unpack_animated_scale_sub_tracks(
+			const packed_sub_track_types* scale_sub_track_types, uint32_t last_entry_index,
+			const persistent_transform_decompression_context_v0& context,
+			animated_track_cache_v0& animated_track_cache, track_writer_type& writer)
+		{
+			for (uint32_t entry_index = 0, track_index = 0; entry_index <= last_entry_index; ++entry_index)
+			{
+				// Mask out everything but animated sub-tracks, this way we can early out when we iterate
+				// Use and_not(..) to load our sub-track types directly from memory on x64 with BMI
+				uint32_t packed_entry = and_not(~0xAAAAAAAAU, scale_sub_track_types[entry_index].types);
 
-				if (has_scale)
+				uint32_t curr_entry_track_index = track_index;
+
+				// We might early out below, always skip 16 tracks
+				track_index += 16;
+
+				// Process 4 sub-tracks at a time
+				while (packed_entry != 0)
 				{
-					const bitset_index_ref track_index_bit_ref(context.bitset_desc, sub_track_index);
-					const bool is_sample_default = bitset_test(default_tracks_bitset, track_index_bit_ref);
-					rtm::vector4f scale;
-					if (is_sample_default)
+					const uint32_t packed_group = packed_entry;
+					const uint32_t curr_group_track_index = curr_entry_track_index;
+
+					// Move to the next group
+					packed_entry <<= 8;
+					curr_entry_track_index += 4;
+
+					if ((packed_group & 0xAA000000) == 0)
+						continue;	// This group contains no animated sub-tracks, skip it
+
+					// Unpack our next 4 tracks
+					animated_track_cache.unpack_scale_group<scale_adapter>(context);
+
+					if ((packed_group & 0x80000000) != 0)
+					{
+						const uint32_t track_index0 = curr_group_track_index + 0;
+						const rtm::vector4f scale = animated_track_cache.consume_scale();
+
+						ACL_ASSERT(rtm::vector_is_finite3(scale), "Scale is not valid!");
+
+						if (!track_writer_type::skip_all_scales() && !writer.skip_track_scale(track_index0))
+							writer.write_scale(track_index0, scale);
+					}
+
+					if ((packed_group & 0x20000000) != 0)
 					{
-						scale = default_scale;
+						const uint32_t track_index1 = curr_group_track_index + 1;
+						const rtm::vector4f scale = animated_track_cache.consume_scale();
+
+						ACL_ASSERT(rtm::vector_is_finite3(scale), "Scale is not valid!");
+
+						if (!track_writer_type::skip_all_scales() && !writer.skip_track_scale(track_index1))
+							writer.write_scale(track_index1, scale);
 					}
-					else
+
+					if ((packed_group & 0x08000000) != 0)
 					{
-						const bool is_sample_constant = bitset_test(constant_tracks_bitset, track_index_bit_ref);
-						if (is_sample_constant)
-							scale = constant_track_cache.consume_scale();
-						else
-							scale = animated_track_cache.consume_scale();
+						const uint32_t track_index2 = curr_group_track_index + 2;
+						const rtm::vector4f scale = animated_track_cache.consume_scale();
+
+						ACL_ASSERT(rtm::vector_is_finite3(scale), "Scale is not valid!");
+
+						if (!track_writer_type::skip_all_scales() && !writer.skip_track_scale(track_index2))
+							writer.write_scale(track_index2, scale);
 					}
 
-					ACL_ASSERT(rtm::vector_is_finite3(scale), "Scale is not valid!");
+					if ((packed_group & 0x02000000) != 0)
+					{
+						const uint32_t track_index3 = curr_group_track_index + 3;
+						const rtm::vector4f scale = animated_track_cache.consume_scale();
+
+						ACL_ASSERT(rtm::vector_is_finite3(scale), "Scale is not valid!");
+
+						if (!track_writer_type::skip_all_scales() && !writer.skip_track_scale(track_index3))
+							writer.write_scale(track_index3, scale);
+					}
+				}
+			}
+		}
+
+		template<class decompression_settings_type, class track_writer_type>
+		inline void decompress_tracks_v0(const persistent_transform_decompression_context_v0& context, track_writer_type& writer)
+		{
+			ACL_ASSERT(context.sample_time >= 0.0f, "Context not set to a valid sample time");
+			if (context.sample_time < 0.0F)
+				return;	// Invalid sample time, we didn't seek yet
+
+			// Due to the SIMD operations, we sometimes overflow in the SIMD lanes not used.
+			// Disable floating point exceptions to avoid issues.
+			fp_environment fp_env;
+			if (decompression_settings_type::disable_fp_exeptions())
+				disable_fp_exceptions(fp_env);
+
+			const tracks_header& header = get_tracks_header(*context.tracks);
+
+			using translation_adapter = acl_impl::translation_decompression_settings_adapter<decompression_settings_type>;
+			using scale_adapter = acl_impl::scale_decompression_settings_adapter<decompression_settings_type>;
+
+			const rtm::quatf default_rotation = rtm::quat_identity();
+			const rtm::vector4f default_translation = rtm::vector_zero();
+			const rtm::vector4f default_scale = rtm::vector_set(float(header.get_default_scale()));
+			const uint32_t has_scale = context.has_scale;
+			const uint32_t num_tracks = header.num_tracks;
+
+			const packed_sub_track_types* sub_track_types = get_transform_tracks_header(*context.tracks).get_sub_track_types();
+			const uint32_t num_sub_track_entries = (num_tracks + k_num_sub_tracks_per_packed_entry - 1) / k_num_sub_tracks_per_packed_entry;
+			const uint32_t num_padded_sub_tracks = (num_sub_track_entries * k_num_sub_tracks_per_packed_entry) - num_tracks;
+			const uint32_t last_entry_index = num_sub_track_entries - 1;
+
+			// Build a mask to strip the extra sub-tracks we don't need that live in the padding
+			// They are set to 0 which means they would be 'default' sub-tracks but they don't really exist
+			// If we have no padding, we retain every sub-track
+			// Sub-tracks that are kept have their bits set to 1 to mask them with logical AND later
+			const uint32_t padding_mask = num_padded_sub_tracks != 0 ? ~(0xFFFFFFFF >> ((k_num_sub_tracks_per_packed_entry - num_padded_sub_tracks) * 2)) : 0xFFFFFFFF;
+
+			const packed_sub_track_types* rotation_sub_track_types = sub_track_types;
+			const packed_sub_track_types* translation_sub_track_types = rotation_sub_track_types + num_sub_track_entries;
+			const packed_sub_track_types* scale_sub_track_types = translation_sub_track_types + num_sub_track_entries;
+
+			constant_track_cache_v0 constant_track_cache;
+			constant_track_cache.initialize<decompression_settings_type>(context);
+
+			{
+				// By now, our bit sets (1-2 cache lines) constant rotations (2 cache lines) have landed in the L2
+				// We prefetched them ahead in the seek(..) function call and due to cache misses when seeking,
+				// their latency should be fully hidden.
+				// Prefetch our 3rd constant rotation cache line to prime the hardware prefetcher and do the same for constant translations
+
+				ACL_IMPL_SEEK_PREFETCH(constant_track_cache.constant_data_rotations + 128);
+				ACL_IMPL_SEEK_PREFETCH(constant_track_cache.constant_data_translations);
+				ACL_IMPL_SEEK_PREFETCH(constant_track_cache.constant_data_translations + 64);
+				ACL_IMPL_SEEK_PREFETCH(constant_track_cache.constant_data_translations + 128);
+			}
+
+			animated_track_cache_v0 animated_track_cache;
+			animated_track_cache.initialize<decompression_settings_type, translation_adapter>(context);
+
+			{
+				// Start prefetching the per track metadata of both segments
+				// They might live in a different memory page than the clip's header and constant data
+				// and we need to prime VMEM translation and the TLB
+
+				ACL_IMPL_SEEK_PREFETCH(context.format_per_track_data[0]);
+				ACL_IMPL_SEEK_PREFETCH(context.format_per_track_data[1]);
+			}
+
+			// TODO: The first time we iterate over the sub-track types, unpack it into our output pose as a temporary buffer
+			// We can build a linked list
+			// Store on the stack the first animated rot/trans/scale
+			// For its rot/trans/scale, write instead the index of the next animated rot/trans/scale
+			// We can even unpack it first on its own
+			// Writer can expose this with something like write_rotation_index/read_rotation_index
+			// The writer can then allocate a separate buffer for this or re-use the pose buffer
+			// When the time comes to write our animated samples, we can unpack 4, grab the next 4 entries from the linked
+			// list and write our samples. We can do this until all samples are written which should be faster than iterating a bit set
+			// since it'll allow us to quickly skip entries we don't care about. The same scheme can be used for constant/default tracks.
+			// When we unpack our bitset, we can also count the number of entries for each type to help iterate
+
+			// Unpack our default rotation sub-tracks
+			// Default rotation sub-tracks are uncommon, this shouldn't take much more than 50 cycles
+			unpack_default_rotation_sub_tracks(rotation_sub_track_types, last_entry_index, padding_mask, default_rotation, writer);
+
+			// Unpack our constant rotation sub-tracks
+			// Constant rotation sub-tracks are very common, this should take at least 200 cycles
+			unpack_constant_rotation_sub_tracks<decompression_settings_type>(rotation_sub_track_types, last_entry_index, context, constant_track_cache, writer);
+
+			// By now, our constant translations (3 cache lines) have landed in L2 after our prefetching has completed
+			// We typically will do enough work above to hide the latency
+			// We do not prefetch our constant scales because scale is fairly rare
+			// Instead, we prefetch our segment range and animated data
+			// The second key frame of animated data might not live in the same memory page even if we use a single segment
+			// so this allows us to prime the TLB as well
+			{
+				const uint8_t* segment_range_data0 = animated_track_cache.segment_sampling_context_rotations[0].segment_range_data;
+				const uint8_t* segment_range_data1 = animated_track_cache.segment_sampling_context_rotations[1].segment_range_data;
+				const uint8_t* animated_data0 = animated_track_cache.segment_sampling_context_rotations[0].animated_track_data;
+				const uint8_t* animated_data1 = animated_track_cache.segment_sampling_context_rotations[1].animated_track_data;
+				const uint8_t* frame_animated_data0 = animated_data0 + (animated_track_cache.segment_sampling_context_rotations[0].animated_track_data_bit_offset / 8);
+				const uint8_t* frame_animated_data1 = animated_data1 + (animated_track_cache.segment_sampling_context_rotations[1].animated_track_data_bit_offset / 8);
+
+				ACL_IMPL_SEEK_PREFETCH(segment_range_data0);
+				ACL_IMPL_SEEK_PREFETCH(segment_range_data0 + 64);
+				ACL_IMPL_SEEK_PREFETCH(segment_range_data1);
+				ACL_IMPL_SEEK_PREFETCH(segment_range_data1 + 64);
+				ACL_IMPL_SEEK_PREFETCH(frame_animated_data0);
+				ACL_IMPL_SEEK_PREFETCH(frame_animated_data1);
+			}
+
+			// Unpack our default translation sub-tracks
+			// Default translation sub-tracks are rare, this shouldn't take much more than 50 cycles
+			unpack_default_translation_sub_tracks(translation_sub_track_types, last_entry_index, padding_mask, default_translation, writer);
 
+			// Unpack our constant translation sub-tracks
+			// Constant translation sub-tracks are very common, this should take at least 200 cycles
+			unpack_constant_translation_sub_tracks(translation_sub_track_types, last_entry_index, constant_track_cache, writer);
+
+			if (has_scale)
+			{
+				// Unpack our default scale sub-tracks
+				// Scale sub-tracks are almost always default, this should take at least 200 cycles
+				unpack_default_scale_sub_tracks(scale_sub_track_types, last_entry_index, padding_mask, default_scale, writer);
+
+				// Unpack our constant scale sub-tracks
+				// Constant scale sub-tracks are very rare, this shouldn't take much more than 50 cycles
+				unpack_constant_scale_sub_tracks(scale_sub_track_types, last_entry_index, constant_track_cache, writer);
+			}
+			else
+			{
+				// No scale present, everything is just the default value
+				// This shouldn't take much more than 50 cycles
+				for (uint32_t track_index = 0; track_index < num_tracks; ++track_index)
+				{
 					if (!track_writer_type::skip_all_scales() && !writer.skip_track_scale(track_index))
-						writer.write_scale(track_index, scale);
-					sub_track_index++;
+						writer.write_scale(track_index, default_scale);
 				}
-				else if (!track_writer_type::skip_all_scales() && !writer.skip_track_scale(track_index))
-					writer.write_scale(track_index, default_scale);
 			}
 
+			{
+				// By now the first few cache lines of our segment data has landed in the L2
+				// Prefetch ahead some more to prime the hardware prefetcher
+				// We also start prefetching the clip range data since we'll need it soon and we need to prime the TLB
+				// and the hardware prefetcher
+
+				const uint8_t* per_track_metadata0 = animated_track_cache.segment_sampling_context_rotations[0].format_per_track_data;
+				const uint8_t* per_track_metadata1 = animated_track_cache.segment_sampling_context_rotations[1].format_per_track_data;
+				const uint8_t* animated_data0 = animated_track_cache.segment_sampling_context_rotations[0].animated_track_data;
+				const uint8_t* animated_data1 = animated_track_cache.segment_sampling_context_rotations[1].animated_track_data;
+				const uint8_t* frame_animated_data0 = animated_data0 + (animated_track_cache.segment_sampling_context_rotations[0].animated_track_data_bit_offset / 8);
+				const uint8_t* frame_animated_data1 = animated_data1 + (animated_track_cache.segment_sampling_context_rotations[1].animated_track_data_bit_offset / 8);
+
+				ACL_IMPL_SEEK_PREFETCH(per_track_metadata0 + 64);
+				ACL_IMPL_SEEK_PREFETCH(per_track_metadata1 + 64);
+				ACL_IMPL_SEEK_PREFETCH(frame_animated_data0 + 64);
+				ACL_IMPL_SEEK_PREFETCH(frame_animated_data1 + 64);
+				ACL_IMPL_SEEK_PREFETCH(animated_track_cache.clip_sampling_context_rotations.clip_range_data);
+				ACL_IMPL_SEEK_PREFETCH(animated_track_cache.clip_sampling_context_rotations.clip_range_data + 64);
+
+				// TODO: Can we prefetch the translation data ahead instead to prime the TLB?
+			}
+
+			// Unpack our variable sub-tracks
+			// Sub-track data is sorted by type: rotations ... translations ... scales ...
+			// We process everything linearly in order, this should help the hardware prefetcher work with us
+			// We use quite a few memory streams:
+			//    - segment per track metadata: 1 per segment
+			//    - segment range data: 1 per segment
+			//    - animated frame data: 2 (might be in different segments or in database)
+			//    - clip range data: 1
+			// We thus need between 5 and 7 memory streams which is a lot.
+			// This is why the unpacking code uses manual software prefetching to ensure prefetching happens.
+			// Removing the manual prefetching slows down the execution on Zen2 and a Pixel 3.
+			// Quite a few of these memory streams might live in separate memory pages if the clip is large
+			// and might thus require TLB misses
+
+			// TODO: Unpack 4, then iterate over tracks to write?
+			// Can we keep the rotations in registers? Does it matter?
+
+			// Unpack rotations first
+			// Animated rotation sub-tracks are very common, this should take at least 400 cycles
+			unpack_animated_rotation_sub_tracks<decompression_settings_type>(rotation_sub_track_types, last_entry_index, context, animated_track_cache, writer);
+
+			// Unpack translations second
+			// Animated translation sub-tracks are common, this should take at least 200 cycles
+			unpack_animated_translation_sub_tracks<translation_adapter>(translation_sub_track_types, last_entry_index, context, animated_track_cache, writer);
+
+			// Unpack scales last
+			// Animated scale sub-tracks are very rare, this shouldn't take much more than 100 cycles
+			if (has_scale)
+				unpack_animated_scale_sub_tracks<scale_adapter>(scale_sub_track_types, last_entry_index, context, animated_track_cache, writer);
+
 			if (decompression_settings_type::disable_fp_exeptions())
 				restore_fp_exceptions(fp_env);
 		}
 
+		// We only initialize some variables when we need them which prompts the compiler to complain
+		// The usage is perfectly safe and because this code is VERY hot and needs to be as fast as possible,
+		// we disable the warning to avoid zeroing out things we don't need
+#if defined(ACL_COMPILER_MSVC)
+		#pragma warning(push)
+		// warning C4701: potentially uninitialized local variable
+		#pragma warning(disable : 4701)
+#elif defined(ACL_COMPILER_GCC)
+		#pragma GCC diagnostic push
+		#pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
+#endif
+
 		template<class decompression_settings_type, class track_writer_type>
 		inline void decompress_track_v0(const persistent_transform_decompression_context_v0& context, uint32_t track_index, track_writer_type& writer)
 		{
@@ -603,9 +1383,10 @@ namespace acl
 				return;	// Invalid sample time, we didn't seek yet
 
 			const tracks_header& tracks_header_ = get_tracks_header(*context.tracks);
-			ACL_ASSERT(track_index < tracks_header_.num_tracks, "Invalid track index");
+			const uint32_t num_tracks = tracks_header_.num_tracks;
+			ACL_ASSERT(track_index < num_tracks, "Invalid track index");
 
-			if (track_index >= tracks_header_.num_tracks)
+			if (track_index >= num_tracks)
 				return;	// Invalid track index
 
 			// Due to the SIMD operations, we sometimes overflow in the SIMD lanes not used.
@@ -620,27 +1401,36 @@ namespace acl
 			const rtm::quatf default_rotation = rtm::quat_identity();
 			const rtm::vector4f default_translation = rtm::vector_zero();
 			const rtm::vector4f default_scale = rtm::vector_set(float(tracks_header_.get_default_scale()));
-			const bool has_scale = tracks_header_.get_has_scale();
+			const uint32_t has_scale = context.has_scale;
+
+			const packed_sub_track_types* sub_track_types = get_transform_tracks_header(*context.tracks).get_sub_track_types();
+			const uint32_t num_sub_track_entries = (num_tracks + k_num_sub_tracks_per_packed_entry - 1) / k_num_sub_tracks_per_packed_entry;
+
+			const packed_sub_track_types* rotation_sub_track_types = sub_track_types;
+			const packed_sub_track_types* translation_sub_track_types = rotation_sub_track_types + num_sub_track_entries;
 
-			const uint32_t* default_tracks_bitset = context.default_tracks_bitset.add_to(context.tracks);
-			const uint32_t* constant_tracks_bitset = context.constant_tracks_bitset.add_to(context.tracks);
+			// If we have no scale, we'll load the rotation sub-track types and mask it out to avoid branching, forcing it to be the default value
+			const packed_sub_track_types* scale_sub_track_types = has_scale ? (translation_sub_track_types + num_sub_track_entries) : sub_track_types;
 
-			// To decompress a single track, we need a few things:
-			//    - if our rot/trans/scale is the default value, this is a trivial bitset lookup
-			//    - constant and animated sub-tracks need to know which group they below to so it can be unpacked
+			// Build a mask to strip out the scale sub-track types if we have no scale present
+			// has_scale is either 0 or 1, negating yields 0 (0x00000000) or -1 (0xFFFFFFFF)
+			// Equivalent to: has_scale ? 0xFFFFFFFF : 0x00000000
+			const uint32_t scale_sub_track_mask = -int32_t(has_scale);
 
-			const uint32_t num_tracks_per_bone = has_scale ? 3 : 2;
-			const uint32_t sub_track_index = track_index * num_tracks_per_bone;
+			const uint32_t sub_track_entry_index = track_index / 16;
+			const uint32_t packed_index = track_index % 16;
 
-			const bitset_index_ref rotation_sub_track_index_bit_ref(context.bitset_desc, sub_track_index + 0);
-			const bitset_index_ref translation_sub_track_index_bit_ref(context.bitset_desc, sub_track_index + 1);
-			const bitset_index_ref scale_sub_track_index_bit_ref(context.bitset_desc, sub_track_index + 2);
+			// Shift our sub-track types so that the sub-track we care about ends up in the LSB position
+			const uint32_t packed_shift = (15 - packed_index) * 2;
 
-			const bool is_rotation_default = bitset_test(default_tracks_bitset, rotation_sub_track_index_bit_ref);
-			const bool is_translation_default = bitset_test(default_tracks_bitset, translation_sub_track_index_bit_ref);
-			const bool is_scale_default = has_scale ? bitset_test(default_tracks_bitset, scale_sub_track_index_bit_ref) : true;
+			const uint32_t rotation_sub_track_type = (rotation_sub_track_types[sub_track_entry_index].types >> packed_shift) & 0x3;
+			const uint32_t translation_sub_track_type = (translation_sub_track_types[sub_track_entry_index].types >> packed_shift) & 0x3;
+			const uint32_t scale_sub_track_type = scale_sub_track_mask & (scale_sub_track_types[sub_track_entry_index].types >> packed_shift) & 0x3;
 
-			if (is_rotation_default && is_translation_default && is_scale_default)
+			// Combine all three so we can quickly test if all are default and if any are constant/animated
+			const uint32_t combined_sub_track_type = rotation_sub_track_type | translation_sub_track_type | scale_sub_track_type;
+
+			if (combined_sub_track_type == 0)
 			{
 				// Everything is default
 				writer.write_rotation(track_index, default_rotation);
@@ -649,232 +1439,119 @@ namespace acl
 				return;
 			}
 
-			const bool is_rotation_constant = !is_rotation_default && bitset_test(constant_tracks_bitset, rotation_sub_track_index_bit_ref);
-			const bool is_translation_constant = !is_translation_default && bitset_test(constant_tracks_bitset, translation_sub_track_index_bit_ref);
-			const bool is_scale_constant = !is_scale_default && has_scale ? bitset_test(constant_tracks_bitset, scale_sub_track_index_bit_ref) : false;
-
-			const bool is_rotation_animated = !is_rotation_default && !is_rotation_constant;
-			const bool is_translation_animated = !is_translation_default && !is_translation_constant;
-			const bool is_scale_animated = !is_scale_default && !is_scale_constant;
-
-			uint32_t num_default_rotations = 0;
-			uint32_t num_default_translations = 0;
-			uint32_t num_default_scales = 0;
 			uint32_t num_constant_rotations = 0;
 			uint32_t num_constant_translations = 0;
 			uint32_t num_constant_scales = 0;
+			uint32_t num_animated_rotations = 0;
+			uint32_t num_animated_translations = 0;
+			uint32_t num_animated_scales = 0;
 
-			if (has_scale)
-			{
-				uint32_t rotation_track_bit_mask = 0x92492492;		// b100100100..
-				uint32_t translation_track_bit_mask = 0x49249249;	// b010010010..
-				uint32_t scale_track_bit_mask = 0x24924924;			// b001001001..
+			const uint32_t last_entry_index = track_index / k_num_sub_tracks_per_packed_entry;
+			const uint32_t num_padded_sub_tracks = ((last_entry_index + 1) * k_num_sub_tracks_per_packed_entry) - track_index;
 
-				const uint32_t last_offset = sub_track_index / 32;
-				uint32_t offset = 0;
-				for (; offset < last_offset; ++offset)
-				{
-					const uint32_t default_value = default_tracks_bitset[offset];
-					num_default_rotations += count_set_bits(default_value & rotation_track_bit_mask);
-					num_default_translations += count_set_bits(default_value & translation_track_bit_mask);
-					num_default_scales += count_set_bits(default_value & scale_track_bit_mask);
-
-					const uint32_t constant_value = constant_tracks_bitset[offset];
-					num_constant_rotations += count_set_bits(constant_value & rotation_track_bit_mask);
-					num_constant_translations += count_set_bits(constant_value & translation_track_bit_mask);
-					num_constant_scales += count_set_bits(constant_value & scale_track_bit_mask);
-
-					// Because the number of tracks in a 32 bit value isn't a multiple of the number of tracks we have (3),
-					// we have to cycle the masks. There are 3 possible masks, just swap them.
-					const uint32_t old_rotation_track_bit_mask = rotation_track_bit_mask;
-					rotation_track_bit_mask = translation_track_bit_mask;
-					translation_track_bit_mask = scale_track_bit_mask;
-					scale_track_bit_mask = old_rotation_track_bit_mask;
-				}
+			// Build a mask to strip the extra sub-tracks we don't need that live in the padding
+			// They are set to 0 which means they would be 'default' sub-tracks but they don't really exist
+			// If we have no padding, we retain every sub-track
+			// Sub-tracks that are kept have their bits set to 0 to mask them with logical ANDNOT later
+			const uint32_t padding_mask = num_padded_sub_tracks != 0 ? (0xFFFFFFFF >> ((k_num_sub_tracks_per_packed_entry - num_padded_sub_tracks) * 2)) : 0x00000000;
 
-				const uint32_t remaining_tracks = sub_track_index % 32;
-				if (remaining_tracks != 0)
-				{
-					const uint32_t not_up_to_track_mask = ((1 << (32 - remaining_tracks)) - 1);
-					const uint32_t default_value = and_not(not_up_to_track_mask, default_tracks_bitset[offset]);
-					num_default_rotations += count_set_bits(default_value & rotation_track_bit_mask);
-					num_default_translations += count_set_bits(default_value & translation_track_bit_mask);
-					num_default_scales += count_set_bits(default_value & scale_track_bit_mask);
-
-					const uint32_t constant_value = and_not(not_up_to_track_mask, constant_tracks_bitset[offset]);
-					num_constant_rotations += count_set_bits(constant_value & rotation_track_bit_mask);
-					num_constant_translations += count_set_bits(constant_value & translation_track_bit_mask);
-					num_constant_scales += count_set_bits(constant_value & scale_track_bit_mask);
-				}
-			}
-			else
+			for (uint32_t sub_track_entry_index_ = 0; sub_track_entry_index_ <= last_entry_index; ++sub_track_entry_index_)
 			{
-				const uint32_t rotation_track_bit_mask = 0xAAAAAAAA;		// b10101010..
-				const uint32_t translation_track_bit_mask = 0x55555555;		// b01010101..
+				// Our last entry might contain more information than we need so we strip the padding we don't need
+				const uint32_t entry_padding_mask = (sub_track_entry_index_ == last_entry_index) ? padding_mask : 0x00000000;
 
-				const uint32_t last_offset = sub_track_index / 32;
-				uint32_t offset = 0;
-				for (; offset < last_offset; ++offset)
-				{
-					const uint32_t default_value = default_tracks_bitset[offset];
-					num_default_rotations += count_set_bits(default_value & rotation_track_bit_mask);
-					num_default_translations += count_set_bits(default_value & translation_track_bit_mask);
+				// Use and_not(..) to load our sub-track types directly from memory on x64 with BMI
+				const uint32_t rotation_sub_track_type_ = and_not(entry_padding_mask, rotation_sub_track_types[sub_track_entry_index_].types);
+				const uint32_t translation_sub_track_type_ = and_not(entry_padding_mask, translation_sub_track_types[sub_track_entry_index_].types);
+				const uint32_t scale_sub_track_type_ = scale_sub_track_mask & and_not(entry_padding_mask, scale_sub_track_types[sub_track_entry_index_].types);
 
-					const uint32_t constant_value = constant_tracks_bitset[offset];
-					num_constant_rotations += count_set_bits(constant_value & rotation_track_bit_mask);
-					num_constant_translations += count_set_bits(constant_value & translation_track_bit_mask);
-				}
+				num_constant_rotations += count_set_bits(rotation_sub_track_type_ & 0x55555555);
+				num_animated_rotations += count_set_bits(rotation_sub_track_type_ & 0xAAAAAAAA);
 
-				const uint32_t remaining_tracks = sub_track_index % 32;
-				if (remaining_tracks != 0)
-				{
-					const uint32_t not_up_to_track_mask = ((1 << (32 - remaining_tracks)) - 1);
-					const uint32_t default_value = and_not(not_up_to_track_mask, default_tracks_bitset[offset]);
-					num_default_rotations += count_set_bits(default_value & rotation_track_bit_mask);
-					num_default_translations += count_set_bits(default_value & translation_track_bit_mask);
-
-					const uint32_t constant_value = and_not(not_up_to_track_mask, constant_tracks_bitset[offset]);
-					num_constant_rotations += count_set_bits(constant_value & rotation_track_bit_mask);
-					num_constant_translations += count_set_bits(constant_value & translation_track_bit_mask);
-				}
+				num_constant_translations += count_set_bits(translation_sub_track_type_ & 0x55555555);
+				num_animated_translations += count_set_bits(translation_sub_track_type_ & 0xAAAAAAAA);
+
+				num_constant_scales += count_set_bits(scale_sub_track_type_ & 0x55555555);
+				num_animated_scales += count_set_bits(scale_sub_track_type_ & 0xAAAAAAAA);
 			}
 
-			uint32_t rotation_group_index = 0;
-			uint32_t translation_group_index = 0;
-			uint32_t scale_group_index = 0;
+			uint32_t rotation_group_sample_index;
+			uint32_t translation_group_sample_index;
+			uint32_t scale_group_sample_index;
 
 			constant_track_cache_v0 constant_track_cache;
 
 			// Skip the constant track data
-			if (is_rotation_constant || is_translation_constant || is_scale_constant)
+			if ((combined_sub_track_type & 1) != 0)
 			{
+				// TODO: Can we init just what we need?
 				constant_track_cache.initialize<decompression_settings_type>(context);
 
 				// Calculate how many constant groups of each sub-track type we need to skip
 				// Constant groups are easy to skip since they are contiguous in memory, we can just skip N trivially
-				// Tracks that are default are also constant
 
 				// Unpack the groups we need and skip the tracks before us
-				if (is_rotation_constant)
+				if (rotation_sub_track_type & 1)
 				{
-					const uint32_t num_constant_rotations_packed = num_constant_rotations - num_default_rotations;
-					const uint32_t num_rotation_constant_groups_to_skip = num_constant_rotations_packed / 4;
+					rotation_group_sample_index = num_constant_rotations % 4;
+
+					const uint32_t num_rotation_constant_groups_to_skip = num_constant_rotations / 4;
 					if (num_rotation_constant_groups_to_skip != 0)
 						constant_track_cache.skip_rotation_groups<decompression_settings_type>(context, num_rotation_constant_groups_to_skip);
-
-					rotation_group_index = num_constant_rotations_packed % 4;
 				}
 
-				if (is_translation_constant)
+				if (translation_sub_track_type & 1)
 				{
-					const uint32_t num_constant_translations_packed = num_constant_translations - num_default_translations;
-					const uint32_t num_translation_constant_groups_to_skip = num_constant_translations_packed / 4;
+					translation_group_sample_index = num_constant_translations % 4;
+
+					const uint32_t num_translation_constant_groups_to_skip = num_constant_translations / 4;
 					if (num_translation_constant_groups_to_skip != 0)
 						constant_track_cache.skip_translation_groups(num_translation_constant_groups_to_skip);
-
-					translation_group_index = num_constant_translations_packed % 4;
 				}
 
-				if (is_scale_constant)
+				if (scale_sub_track_type & 1)
 				{
-					const uint32_t num_constant_scales_packed = num_constant_scales - num_default_scales;
-					const uint32_t num_scale_constant_groups_to_skip = num_constant_scales_packed / 4;
+					scale_group_sample_index = num_constant_scales % 4;
+
+					const uint32_t num_scale_constant_groups_to_skip = num_constant_scales / 4;
 					if (num_scale_constant_groups_to_skip != 0)
 						constant_track_cache.skip_scale_groups(num_scale_constant_groups_to_skip);
-
-					scale_group_index = num_constant_scales_packed % 4;
 				}
 			}
-			else
-			{
-				// Fake init to avoid compiler warning...
-				constant_track_cache.rotations.num_left_to_unpack = 0;
-				constant_track_cache.constant_data_rotations = nullptr;
-				constant_track_cache.constant_data_translations = nullptr;
-				constant_track_cache.constant_data_scales = nullptr;
-			}
 
 			animated_track_cache_v0 animated_track_cache;
-			animated_group_cursor_v0 rotation_group_cursor;
-			animated_group_cursor_v0 translation_group_cursor;
-			animated_group_cursor_v0 scale_group_cursor;
 
 			// Skip the animated track data
-			if (is_rotation_animated || is_translation_animated || is_scale_animated)
+			if ((combined_sub_track_type & 2) != 0)
 			{
-				animated_track_cache.initialize(context);
-
-				// Calculate how many animated groups of each sub-track type we need to skip
-				// Skipping animated groups is a bit more complicated because they are interleaved in the order
-				// they are needed
-
-				// Tracks that are default are also constant
-				const uint32_t num_animated_rotations = track_index - num_constant_rotations;
-
-				if (is_rotation_animated)
-					rotation_group_index = num_animated_rotations % 4;
+				// TODO: Can we init just what we need?
+				animated_track_cache.initialize<decompression_settings_type, translation_adapter>(context);
 
-				const uint32_t num_animated_translations = track_index - num_constant_translations;
-
-				if (is_translation_animated)
-					translation_group_index = num_animated_translations % 4;
-
-				const uint32_t num_animated_scales = has_scale ? (track_index - num_constant_scales) : 0;
-
-				if (is_scale_animated)
-					scale_group_index = num_animated_scales % 4;
-
-				uint32_t num_rotations_to_unpack = is_rotation_animated ? num_animated_rotations : ~0U;
-				uint32_t num_translations_to_unpack = is_translation_animated ? num_animated_translations : ~0U;
-				uint32_t num_scales_to_unpack = is_scale_animated ? num_animated_scales : ~0U;
-
-				uint32_t num_animated_groups_to_unpack = is_rotation_animated + is_translation_animated + is_scale_animated;
+				if (rotation_sub_track_type & 2)
+				{
+					rotation_group_sample_index = num_animated_rotations % 4;
 
-				const transform_tracks_header& transform_header = get_transform_tracks_header(*context.tracks);
-				const animation_track_type8* group_types = transform_header.get_animated_group_types();
+					const uint32_t num_groups_to_skip = num_animated_rotations / 4;
+					if (num_groups_to_skip != 0)
+						animated_track_cache.skip_rotation_groups<decompression_settings_type>(context, num_groups_to_skip);
+				}
 
-				while (num_animated_groups_to_unpack != 0)
+				if (translation_sub_track_type & 2)
 				{
-					const animation_track_type8 group_type = *group_types;
-					group_types++;
-					ACL_ASSERT(group_type != static_cast<animation_track_type8>(0xFF), "Reached terminator");
+					translation_group_sample_index = num_animated_translations % 4;
 
-					if (group_type == animation_track_type8::rotation)
-					{
-						if (num_rotations_to_unpack < 4)
-						{
-							// This is the group we need, cache our cursor
-							animated_track_cache.get_rotation_cursor(rotation_group_cursor);
-							num_animated_groups_to_unpack--;
-						}
-
-						animated_track_cache.skip_rotation_group<decompression_settings_type>(context);
-						num_rotations_to_unpack -= 4;
-					}
-					else if (group_type == animation_track_type8::translation)
-					{
-						if (num_translations_to_unpack < 4)
-						{
-							// This is the group we need, cache our cursor
-							animated_track_cache.get_translation_cursor(translation_group_cursor);
-							num_animated_groups_to_unpack--;
-						}
+					const uint32_t num_groups_to_skip = num_animated_translations / 4;
+					if (num_groups_to_skip != 0)
+						animated_track_cache.skip_translation_groups<translation_adapter>(context, num_groups_to_skip);
+				}
 
-						animated_track_cache.skip_translation_group<translation_adapter>(context);
-						num_translations_to_unpack -= 4;
-					}
-					else // scale
-					{
-						if (num_scales_to_unpack < 4)
-						{
-							// This is the group we need, cache our cursor
-							animated_track_cache.get_scale_cursor(scale_group_cursor);
-							num_animated_groups_to_unpack--;
-						}
+				if (scale_sub_track_type & 2)
+				{
+					scale_group_sample_index = num_animated_scales % 4;
 
-						animated_track_cache.skip_scale_group<scale_adapter>(context);
-						num_scales_to_unpack -= 4;
-					}
+					const uint32_t num_groups_to_skip = num_animated_scales / 4;
+					if (num_groups_to_skip != 0)
+						animated_track_cache.skip_scale_groups<scale_adapter>(context, num_groups_to_skip);
 				}
 			}
 
@@ -882,36 +1559,36 @@ namespace acl
 
 			{
 				rtm::quatf rotation;
-				if (is_rotation_default)
+				if (rotation_sub_track_type == 0)
 					rotation = default_rotation;
-				else if (is_rotation_constant)
-					rotation = constant_track_cache.unpack_rotation_within_group<decompression_settings_type>(context, rotation_group_index);
+				else if (rotation_sub_track_type & 1)
+					rotation = constant_track_cache.unpack_rotation_within_group<decompression_settings_type>(context, rotation_group_sample_index);
 				else
-					rotation = animated_track_cache.unpack_rotation_within_group<decompression_settings_type>(context, rotation_group_cursor, rotation_group_index);
+					rotation = animated_track_cache.unpack_rotation_within_group<decompression_settings_type>(context, rotation_group_sample_index);
 
 				writer.write_rotation(track_index, rotation);
 			}
 
 			{
 				rtm::vector4f translation;
-				if (is_translation_default)
+				if (translation_sub_track_type == 0)
 					translation = default_translation;
-				else if (is_translation_constant)
-					translation = constant_track_cache.unpack_translation_within_group(translation_group_index);
+				else if (translation_sub_track_type & 1)
+					translation = constant_track_cache.unpack_translation_within_group(translation_group_sample_index);
 				else
-					translation = animated_track_cache.unpack_translation_within_group<translation_adapter>(context, translation_group_cursor, translation_group_index);
+					translation = animated_track_cache.unpack_translation_within_group<translation_adapter>(context, translation_group_sample_index);
 
 				writer.write_translation(track_index, translation);
 			}
 
 			{
 				rtm::vector4f scale;
-				if (is_scale_default)
+				if (scale_sub_track_type == 0)
 					scale = default_scale;
-				else if (is_scale_constant)
-					scale = constant_track_cache.unpack_scale_within_group(scale_group_index);
+				else if (scale_sub_track_type & 1)
+					scale = constant_track_cache.unpack_scale_within_group(scale_group_sample_index);
 				else
-					scale = animated_track_cache.unpack_scale_within_group<scale_adapter>(context, scale_group_cursor, scale_group_index);
+					scale = animated_track_cache.unpack_scale_within_group<scale_adapter>(context, scale_group_sample_index);
 
 				writer.write_scale(track_index, scale);
 			}
@@ -919,6 +1596,13 @@ namespace acl
 			if (decompression_settings_type::disable_fp_exeptions())
 				restore_fp_exceptions(fp_env);
 		}
+
+		// Restore our warnings
+#if defined(ACL_COMPILER_MSVC)
+		#pragma warning(pop)
+#elif defined(ACL_COMPILER_GCC)
+		#pragma GCC diagnostic pop
+#endif
 	}
 }
 
diff --git a/make.py b/make.py
--- a/make.py
+++ b/make.py
@@ -305,7 +305,6 @@ def do_generate_solution(build_dir, cmake_script_dir, test_data_dir, decomp_data
 	if compiler == 'emscripten':
 		cmake_cmd = 'emcmake cmake .. -DCMAKE_INSTALL_PREFIX="{}" {}'.format(build_dir, ' '.join(extra_switches))
 	else:
-		cmake_cmd = 'cmake .. -DCMAKE_INSTALL_PREFIX="{}" {}'.format(build_dir, ' '.join(extra_switches))
 		cmake_generator = get_generator(compiler, cpu)
 		if not cmake_generator:
 			print('Using default generator')
@@ -316,12 +315,14 @@ def do_generate_solution(build_dir, cmake_script_dir, test_data_dir, decomp_data
 				generator_suffix = 'Clang CL'
 
 			print('Using generator: {} {}'.format(cmake_generator, generator_suffix))
-			cmake_cmd += ' -G "{}"'.format(cmake_generator)
+			extra_switches.append('-G "{}"'.format(cmake_generator))
 
 		cmake_arch = get_architecture(compiler, cpu)
 		if cmake_arch:
 			print('Using architecture: {}'.format(cmake_arch))
-			cmake_cmd += ' -A {}'.format(cmake_arch)
+			extra_switches.append('-A {}'.format(cmake_arch))
+
+		cmake_cmd = 'cmake .. -DCMAKE_INSTALL_PREFIX="{}" {}'.format(build_dir, ' '.join(extra_switches))
 
 	result = subprocess.call(cmake_cmd, shell=True)
 	if result != 0:
@@ -748,7 +749,7 @@ def run_clip_regression_test(cmd_queue, completed_queue, failed_queue, failure_l
 
 				all_threads_done = True
 				for thread in threads:
-					if thread.isAlive():
+					if thread.is_alive():
 						all_threads_done = False
 
 				if all_threads_done:
diff --git a/tools/acl_compressor/acl_compressor.py b/tools/acl_compressor/acl_compressor.py
--- a/tools/acl_compressor/acl_compressor.py
+++ b/tools/acl_compressor/acl_compressor.py
@@ -337,7 +337,7 @@ def compress_clips(options):
 			cmd = '{} -acl="{}" -stats="{}" -level={}'.format(compressor_exe_path, acl_filename, stat_filename, options['level'])
 
 			if out_dir:
-				out_filename = os.path.join(options['out'], filename.replace('.acl.sjson', '.acl.bin'))
+				out_filename = os.path.join(options['out'], filename.replace('.acl.sjson', '.acl'))
 				cmd = '{} -out="{}"'.format(cmd, out_filename)
 
 			if options['stat_detailed']:
@@ -381,7 +381,7 @@ def compress_clips(options):
 
 				all_threads_done = True
 				for thread in threads:
-					if thread.isAlive():
+					if thread.is_alive():
 						all_threads_done = False
 
 				if all_threads_done:
diff --git a/tools/acl_compressor/sources/validate_database.cpp b/tools/acl_compressor/sources/validate_database.cpp
--- a/tools/acl_compressor/sources/validate_database.cpp
+++ b/tools/acl_compressor/sources/validate_database.cpp
@@ -464,7 +464,7 @@ void validate_db(iallocator& allocator, const track_array_qvvf& raw_tracks, cons
 		ACL_ASSERT(db01->contains(*db_tracks01[1]), "Database should contain our clip");
 	}
 
-	// Reference error with the bulk data inline and everything loaded
+	// Reference error without the database with everything highest quality
 	track_error high_quality_tier_error_ref;
 	{
 		acl::decompression_context<debug_transform_decompression_settings_with_db> context;
diff --git a/tools/acl_decompressor/acl_decompressor.py b/tools/acl_decompressor/acl_decompressor.py
--- a/tools/acl_decompressor/acl_decompressor.py
+++ b/tools/acl_decompressor/acl_decompressor.py
@@ -329,7 +329,7 @@ def decompress_clips(options):
 
 				all_threads_done = True
 				for thread in threads:
-					if thread.isAlive():
+					if thread.is_alive():
 						all_threads_done = False
 
 				if all_threads_done:
diff --git a/tools/acl_decompressor/main_generic/main.cpp b/tools/acl_decompressor/main_generic/main.cpp
--- a/tools/acl_decompressor/main_generic/main.cpp
+++ b/tools/acl_decompressor/main_generic/main.cpp
@@ -171,9 +171,10 @@ static bool read_metadata_file(const char* metadata_filename, const char*& out_m
 
 int main(int argc, char* argv[])
 {
-#if defined(_WIN32) && 0
-	// Set the process affinity to physical core 6 (out of 12), on Ryzen 2950X it is the fastest core of the Die 1
-	const DWORD_PTR physical_core_index = 6;
+#if defined(_WIN32)
+	// To improve the consistency of the performance results, pin our process to a specific processor core
+	// Set the process affinity to physical core 6, on Ryzen 2950X it is the fastest core of the Die 1
+	const DWORD_PTR physical_core_index = 5;
 	const DWORD_PTR logical_core_index = physical_core_index * 2;
 	SetProcessAffinityMask(GetCurrentProcess(), 1 << logical_core_index);
 #endif
diff --git a/tools/acl_decompressor/sources/benchmark.cpp b/tools/acl_decompressor/sources/benchmark.cpp
--- a/tools/acl_decompressor/sources/benchmark.cpp
+++ b/tools/acl_decompressor/sources/benchmark.cpp
@@ -40,19 +40,40 @@
 #include <string>
 #include <vector>
 
-// TODO: get an official CPU cache size
-#if defined(RTM_SSE2_INTRINSICS)
-	static constexpr uint32_t k_cache_size = 33 * 1024 * 1024;		// Assume 32 MB cache
-	static constexpr uint32_t k_num_tlb_entries = 4000;
-#elif defined(__ANDROID__)
-	static constexpr uint32_t k_cache_size = 3 * 1024 * 1024;		// Pixel 3 has 2 MB cache
-	static constexpr uint32_t k_num_tlb_entries = 100;
-#else
-	static constexpr uint32_t k_cache_size = 9 * 1024 * 1024;		// iPad Pro has 8 MB cache
-	static constexpr uint32_t k_num_tlb_entries = 2000;
-#endif
+//////////////////////////////////////////////////////////////////////////
+// Constants
+
+static constexpr uint32_t k_cpu_cache_size = 8 * 1024 * 1024;	// Assume a 8 MB cache which is common for L3 modules (iPad, Zen2)
+
+// In practice, CPUs do not always evict the least recently used cache line.
+// To ensure every cache line is evicted, we allocate our buffer 4x larger than the CPU cache.
+// We use a custom memset function to make sure that streaming writes aren't used which would
+// bypass the CPU cache and not evict anything.
+static constexpr uint32_t k_flush_buffer_size = k_cpu_cache_size * 4;
+
+// The VMEM Level 1 translation has 512 entries each spanning 1 GB. We'll assume that in the real world
+// there is a reasonable chance that memory touched will live within the same 1 GB region and thus be
+// in some level of the CPU cache.
 
-static constexpr uint32_t k_page_size = 4 * 1024;	// 4 KB is standard
+// The VMEM Level 2 translation has 512 entries each spanning 2 MB.
+// This means the cache line we load to find a page offset contains a span of 16 MB within it (a cache
+// line contains 8 entries).
+// To ensure we don't touch cache lines that belong to our input buffer as we flush the CPU cache,
+// we add sufficient padding at both ends of the flush buffer. Since we'll access it linearly,
+// the hardware prefetcher might pull in cache lines ahead. We assume it won't pull more than 4 cache
+// lines ahead. This means we need this much padding on each end: 4 * 16 MB = 64 MB
+static constexpr uint32_t k_vmem_padding = 16 * 1024 * 1024;
+static constexpr uint32_t k_padded_flush_buffer_size = k_vmem_padding + k_flush_buffer_size + k_vmem_padding;
+
+// We allocate 100 copies of the compressed clip and align them to reduce the flush cost
+// by flushing only when we loop around. We pad each copy to 16 MB to ensure no VMEM entry sharing in L2.
+// A compressed clip that takes less than 160 MB would end up using 16 MB * 100 = 1.56 GB
+static constexpr uint32_t k_num_copies = 220;
+
+// Align our clip copy buffer to a 2 MB boundary to further reduce VMEM noise
+static constexpr uint32_t k_clip_buffer_alignment = 2 * 1024 * 1024;
+
+//////////////////////////////////////////////////////////////////////////
 
 enum class PlaybackDirection
 {
@@ -74,11 +95,11 @@ struct benchmark_state
 
 	acl::compressed_tracks** decompression_instances = nullptr;
 	acl::decompression_context<acl::default_transform_decompression_settings>* decompression_contexts = nullptr;
-	uint8_t* clip_mega_buffer = nullptr;
+	uint8_t* clip_copy_buffer = nullptr;
+	uint8_t* flush_buffer = nullptr;
 
-	uint32_t num_copies = 0;
 	uint32_t pose_size = 0;
-	uint32_t clip_buffer_size = 0;
+	uint32_t clip_copy_buffer_size = 0;
 };
 
 acl::ansi_allocator s_allocator;
@@ -86,72 +107,74 @@ static benchmark_state s_benchmark_state;
 
 void clear_benchmark_state()
 {
-	acl::deallocate_type_array(s_allocator, s_benchmark_state.decompression_contexts, s_benchmark_state.num_copies);
-	acl::deallocate_type_array(s_allocator, s_benchmark_state.decompression_instances, s_benchmark_state.num_copies);
-	acl::deallocate_type_array(s_allocator, s_benchmark_state.clip_mega_buffer, s_benchmark_state.num_copies * size_t(s_benchmark_state.clip_buffer_size));
+	acl::deallocate_type_array(s_allocator, s_benchmark_state.decompression_contexts, k_num_copies);
+	acl::deallocate_type_array(s_allocator, s_benchmark_state.decompression_instances, k_num_copies);
+	acl::deallocate_type_array(s_allocator, s_benchmark_state.clip_copy_buffer, s_benchmark_state.clip_copy_buffer_size);
+	acl::deallocate_type_array(s_allocator, s_benchmark_state.flush_buffer, k_padded_flush_buffer_size);
 
 	s_benchmark_state = benchmark_state();
 }
 
+static void allocate_static_buffers()
+{
+	if (s_benchmark_state.flush_buffer != nullptr)
+		return;	// Already allocated
+
+	s_benchmark_state.decompression_instances = acl::allocate_type_array<acl::compressed_tracks*>(s_allocator, k_num_copies);
+	s_benchmark_state.decompression_contexts = acl::allocate_type_array<acl::decompression_context<acl::default_transform_decompression_settings>>(s_allocator, k_num_copies);
+	s_benchmark_state.flush_buffer = acl::allocate_type_array<uint8_t>(s_allocator, k_padded_flush_buffer_size);
+}
+
 static void setup_benchmark_state(acl::compressed_tracks& compressed_tracks)
 {
+	allocate_static_buffers();
+
 	const uint32_t num_tracks = compressed_tracks.get_num_tracks();
 	const uint32_t compressed_size = compressed_tracks.get_size();
 
 	const uint32_t num_bytes_per_track = (4 + 3 + 3) * sizeof(float);	// Rotation, Translation, Scale
 	const uint32_t pose_size = num_tracks * num_bytes_per_track;
 
-	// We want to divide our CPU cache into the number of poses it can hold
-	const uint32_t num_poses_in_cpu_cache = (k_cache_size + pose_size - 1) / pose_size;
-
-	// We want our CPU cache to be cold when we decompress so we duplicate the clip and switch every iteration.
-	// Each decompression call will interpolate 2 poses, assume we touch 'pose_size * 2' bytes and at least once memory page.
-	const uint32_t num_copies = std::max<uint32_t>((num_poses_in_cpu_cache + 1) / 2, k_num_tlb_entries);
-
-	// Align each buffer to a large multiple of our page size to avoid virtual memory translation noise.
-	// When we decompress, our memory is sure to be cold and we want to make it reasonably cold.
-	// This is why we use multiple clips to avoid the CPU cache being re-used.
-	// However, that is not where it stops. When we hit a cache miss, we also need to translate the
-	// virtual address used into a physical address. To do this, the CPU first checks if it already
-	// translated an address in the same memory page. This is why we pad the buffer to make sure the
-	// next clip in memory doesn't share a page. If we have a TLB miss, it triggers the hardware
-	// page walk. This in turn uses the virtual address to lookup in a series of tables where the physical
-	// memory lies. On x64, this series of tables is 4 (or 5) levels deep and they live in physical memory. Reading
-	// them can in turn trigger cache misses and TLB misses. They can also be prefetched by the hardware
-	// since their memory is just ordinary data that lives in the CPU cache and RAM like everything else.
-	// To force as many cache misses there as possible and to minimize the impact of prefetching, we thus
-	// align each buffer to spread them out in memory.
-	const uint32_t clip_buffer_alignment = k_page_size * 32;
-
-	// Make sure to pad our buffers with a page size to avoid TLB noise
-	// Also make sure we can contain at least one full pose to handle memcpy decompression
-	const uint32_t clip_buffer_size = acl::align_to(std::max<uint32_t>(compressed_size, pose_size), clip_buffer_alignment);
-
-	printf("Pose size: %u, num copies: %u\n", pose_size, num_copies);
-
-	acl::compressed_tracks** decompression_instances = acl::allocate_type_array<acl::compressed_tracks*>(s_allocator, num_copies);
-	uint8_t* clip_mega_buffer = acl::allocate_type_array_aligned<uint8_t>(s_allocator, num_copies * size_t(clip_buffer_size), k_page_size);
+	// Each clip is rounded up to a multiple of our VMEM padding
+	const uint32_t padded_clip_size = acl::align_to(compressed_size, k_vmem_padding);
+	const uint32_t clip_buffer_size = padded_clip_size * k_num_copies;
+
+	acl::compressed_tracks** decompression_instances = s_benchmark_state.decompression_instances;
+	acl::decompression_context<acl::default_transform_decompression_settings>* decompression_contexts = s_benchmark_state.decompression_contexts;
+	uint8_t* clip_copy_buffer = s_benchmark_state.clip_copy_buffer;
+
+	if (clip_buffer_size > s_benchmark_state.clip_copy_buffer_size)
+	{
+		// Allocate our new clip copy buffer
+		clip_copy_buffer = acl::allocate_type_array_aligned<uint8_t>(s_allocator, clip_buffer_size, k_clip_buffer_alignment);
+
+		s_benchmark_state.clip_copy_buffer = clip_copy_buffer;
+		s_benchmark_state.clip_copy_buffer_size = clip_buffer_size;
+	}
+
+	printf("Pose size: %u bytes, clip size: %.2f MB\n", pose_size, double(compressed_size) / (1024.0 * 1024.0));
 
 	// Create our copies
-	for (uint32_t copy_index = 0; copy_index < num_copies; ++copy_index)
+	for (uint32_t copy_index = 0; copy_index < k_num_copies; ++copy_index)
 	{
-		uint8_t* buffer = clip_mega_buffer + copy_index * size_t(clip_buffer_size);
+		uint8_t* buffer = clip_copy_buffer + (copy_index * padded_clip_size);
 		std::memcpy(buffer, &compressed_tracks, compressed_size);
 
 		decompression_instances[copy_index] = reinterpret_cast<acl::compressed_tracks*>(buffer);
 	}
 
-	acl::decompression_context<acl::default_transform_decompression_settings>* decompression_contexts = acl::allocate_type_array<acl::decompression_context<acl::default_transform_decompression_settings>>(s_allocator, num_copies);
-	for (uint32_t instance_index = 0; instance_index < num_copies; ++instance_index)
+	// Create our decompression contexts
+	for (uint32_t instance_index = 0; instance_index < k_num_copies; ++instance_index)
 		decompression_contexts[instance_index].initialize(*decompression_instances[instance_index]);
 
 	s_benchmark_state.compressed_tracks = &compressed_tracks;
-	s_benchmark_state.decompression_instances = decompression_instances;
-	s_benchmark_state.decompression_contexts = decompression_contexts;
-	s_benchmark_state.clip_mega_buffer = clip_mega_buffer;
-	s_benchmark_state.num_copies = num_copies;
 	s_benchmark_state.pose_size = pose_size;
-	s_benchmark_state.clip_buffer_size = clip_buffer_size;
+}
+
+static void memset_impl(uint8_t* buffer, size_t buffer_size, uint8_t value)
+{
+	for (uint8_t* ptr = buffer; ptr < buffer + buffer_size; ++ptr)
+		*ptr = value;
 }
 
 static void benchmark_decompression(benchmark::State& state)
@@ -161,11 +184,7 @@ static void benchmark_decompression(benchmark::State& state)
 	const DecompressionFunction decompression_function = static_cast<DecompressionFunction>(state.range(2));
 
 	if (s_benchmark_state.compressed_tracks != &compressed_tracks)
-	{
-		// We have a new clip, clear out our old state and start over
-		clear_benchmark_state();
-		setup_benchmark_state(compressed_tracks);
-	}
+		setup_benchmark_state(compressed_tracks);	// We have a new clip, setup everything
 
 	const float duration = compressed_tracks.get_duration();
 
@@ -192,16 +211,24 @@ static void benchmark_decompression(benchmark::State& state)
 
 	acl::compressed_tracks** decompression_instances = s_benchmark_state.decompression_instances;
 	acl::decompression_context<acl::default_transform_decompression_settings>* decompression_contexts = s_benchmark_state.decompression_contexts;
-	const uint32_t num_copies = s_benchmark_state.num_copies;
+	uint8_t* flush_buffer = s_benchmark_state.flush_buffer;
 	const uint32_t pose_size = s_benchmark_state.pose_size;
 
 	const uint32_t num_tracks = compressed_tracks.get_num_tracks();
 	acl::acl_impl::debug_track_writer pose_writer(s_allocator, acl::track_type8::qvvf, num_tracks);
 
+	// Flush the CPU cache
+	memset_impl(flush_buffer + k_vmem_padding, k_flush_buffer_size, 1);
+
 	uint32_t current_context_index = 0;
 	uint32_t current_sample_index = 0;
+	uint8_t flush_value = 2;
 	for (auto _ : state)
 	{
+		(void)_;
+
+		const auto start = std::chrono::high_resolution_clock::now();
+
 		const float sample_time = sample_times[current_sample_index];
 
 		acl::decompression_context<acl::default_transform_decompression_settings>& context = decompression_contexts[current_context_index];
@@ -221,15 +248,23 @@ static void benchmark_decompression(benchmark::State& state)
 			break;
 		}
 
+		const auto end = std::chrono::high_resolution_clock::now();
+		const auto elapsed_seconds = std::chrono::duration_cast<std::chrono::duration<double>>(end - start);
+		state.SetIterationTime(elapsed_seconds.count());
+
 		// Move on to the next context and sample
 		// We only move on to the next sample once every context has been touched
-		++current_context_index;
-		if (current_context_index >= num_copies)
+		current_context_index++;
+		if (current_context_index >= k_num_copies)
 		{
 			current_context_index = 0;
-			++current_sample_index;
+			current_sample_index++;
+
 			if (current_sample_index >= k_num_decompression_samples)
 				current_sample_index = 0;
+
+			// Flush the CPU cache
+			memset_impl(flush_buffer + k_vmem_padding, k_flush_buffer_size, flush_value++);
 		}
 	}
 
@@ -387,16 +422,14 @@ bool prepare_clip(const std::string& clip_name, const acl::compressed_tracks& ra
 	bench->ArgNames({ "", "Dir", "Func" });
 
 	// Sometimes the numbers are slightly different from run to run, we'll run a few times
-	bench->Repetitions(20);
+	bench->Repetitions(3);
 
-	bench->ComputeStatistics("min", [](const std::vector<double>& v)
-	{
-		return *std::min_element(std::begin(v), std::end(v));
-	});
-	bench->ComputeStatistics("max", [](const std::vector<double>& v)
-	{
-		return *std::max_element(std::begin(v), std::end(v));
-	});
+	// Use manual timing since we clear the CPU cache explicitly
+	bench->UseManualTime();
+
+	// Add min/max tracking
+	bench->ComputeStatistics("min", [](const std::vector<double>& v) { return *std::min_element(std::begin(v), std::end(v)); });
+	bench->ComputeStatistics("max", [](const std::vector<double>& v) { return *std::max_element(std::begin(v), std::end(v)); });
 
 	out_compressed_clips.push_back(compressed_tracks);
 	return true;
diff --git a/tools/appveyor_ci.bat b/tools/appveyor_ci.bat
--- a/tools/appveyor_ci.bat
+++ b/tools/appveyor_ci.bat
@@ -30,6 +30,12 @@ GOTO :next
 :clang
 IF /i %WORKER_IMAGE%=="Visual Studio 2019" SET COMPILER=vs2019-clang
 IF /i %WORKER_IMAGE%=="Previous Visual Studio 2019" SET COMPILER=vs2019-clang
+
+REM HACK!!! Disable clang build for now with appveyor since vcpkg breaks the compiler detection of cmake
+REM Fake build success
+echo VS2019 clang build is disabled for now due to issues with vcpkg breaking compiler detection with cmake
+exit /B 0
+
 GOTO :next
 
 :next
diff --git a/tools/vs_visualizers/acl.natvis b/tools/vs_visualizers/acl.natvis
--- a/tools/vs_visualizers/acl.natvis
+++ b/tools/vs_visualizers/acl.natvis
@@ -155,4 +155,15 @@
     </Expand>
   </Type>
 
+  <Type Name="acl::acl_impl::packed_sub_track_types">
+    <Expand>
+      <IndexListItems>
+        <Size>16</Size>
+        <ValueNode Condition="((types >> ((15 - $i) * 2)) &amp; 0x3) == 0">"default"</ValueNode>
+        <ValueNode Condition="((types >> ((15 - $i) * 2)) &amp; 0x3) == 1">"constant"</ValueNode>
+        <ValueNode Condition="((types >> ((15 - $i) * 2)) &amp; 0x3) == 2">"animated"</ValueNode>
+      </IndexListItems>
+    </Expand>
+  </Type>
+
 </AutoVisualizer>


Test patch:
diff --git a/tests/main_android/app/src/main/cpp/CMakeLists.txt b/tests/main_android/app/src/main/cpp/CMakeLists.txt
--- a/tests/main_android/app/src/main/cpp/CMakeLists.txt
+++ b/tests/main_android/app/src/main/cpp/CMakeLists.txt
@@ -8,7 +8,7 @@ set(PROJECT_ROOT_DIR "${PROJECT_SOURCE_DIR}/../../../..")
 
 include_directories("${PROJECT_ROOT_DIR}/../../includes")
 include_directories("${PROJECT_ROOT_DIR}/../../external/rtm/includes")
-include_directories("${PROJECT_ROOT_DIR}/../../external/catch2/single_include/catch2")
+include_directories("${PROJECT_ROOT_DIR}/../../external/catch2/single_include")
 
 if(USE_SJSON)
 	include_directories("${PROJECT_ROOT_DIR}/../../external/sjson-cpp/includes")
diff --git a/tests/main_android/app/src/main/cpp/main.cpp b/tests/main_android/app/src/main/cpp/main.cpp
--- a/tests/main_android/app/src/main/cpp/main.cpp
+++ b/tests/main_android/app/src/main/cpp/main.cpp
@@ -23,7 +23,7 @@
 ////////////////////////////////////////////////////////////////////////////////
 
 #define CATCH_CONFIG_RUNNER
-#include <catch.hpp>
+#include <catch2/catch.hpp>
 
 #include <jni.h>
 
diff --git a/tests/main_emscripten/CMakeLists.txt b/tests/main_emscripten/CMakeLists.txt
--- a/tests/main_emscripten/CMakeLists.txt
+++ b/tests/main_emscripten/CMakeLists.txt
@@ -6,7 +6,7 @@ set(CMAKE_CXX_STANDARD 11)
 include_directories("${PROJECT_SOURCE_DIR}/../../includes")
 include_directories("${PROJECT_SOURCE_DIR}/../../external/rtm/includes")
 include_directories("${PROJECT_SOURCE_DIR}/../../external/sjson-cpp/includes")
-include_directories("${PROJECT_SOURCE_DIR}/../../external/catch2/single_include/catch2")
+include_directories("${PROJECT_SOURCE_DIR}/../../external/catch2/single_include")
 
 # Grab all of our test source files
 file(GLOB_RECURSE ALL_TEST_SOURCE_FILES LIST_DIRECTORIES false
@@ -32,14 +32,14 @@ target_compile_options(${PROJECT_NAME} PRIVATE -Werror)				# Treat warnings as e
 
 # Exceptions are not enabled by default, enable them
 target_compile_options(${PROJECT_NAME} PRIVATE -fexceptions)
-target_link_libraries(${PROJECT_NAME} "-s DISABLE_EXCEPTION_CATCHING=0")
+target_link_libraries(${PROJECT_NAME} PRIVATE "-s DISABLE_EXCEPTION_CATCHING=0")
 
-target_link_libraries(${PROJECT_NAME} "-s NODERAWFS=1")				# Enable the raw node file system
-target_link_libraries(${PROJECT_NAME} -lnodefs.js)					# Link the node file system
+target_link_libraries(${PROJECT_NAME} PRIVATE "-s NODERAWFS=1")				# Enable the raw node file system
+target_link_libraries(${PROJECT_NAME} PRIVATE -lnodefs.js)					# Link the node file system
 
-target_link_libraries(${PROJECT_NAME} "-s ENVIRONMENT=node")		# Force the environment to node
+target_link_libraries(${PROJECT_NAME} PRIVATE "-s ENVIRONMENT=node")		# Force the environment to node
 
-target_link_libraries(${PROJECT_NAME} "-s ALLOW_MEMORY_GROWTH=1")	# Allow dynamic memory allocation
+target_link_libraries(${PROJECT_NAME} PRIVATE "-s ALLOW_MEMORY_GROWTH=1")	# Allow dynamic memory allocation
 
 # Setup Catch2 so we can find and execute the unit tests with CTest
 set(OptionalCatchTestLauncher node)
diff --git a/tests/main_emscripten/main.cpp b/tests/main_emscripten/main.cpp
--- a/tests/main_emscripten/main.cpp
+++ b/tests/main_emscripten/main.cpp
@@ -23,7 +23,7 @@
 ////////////////////////////////////////////////////////////////////////////////
 
 #define CATCH_CONFIG_RUNNER
-#include <catch.hpp>
+#include <catch2/catch.hpp>
 
 int main(int argc, char* argv[])
 {
diff --git a/tests/main_generic/CMakeLists.txt b/tests/main_generic/CMakeLists.txt
--- a/tests/main_generic/CMakeLists.txt
+++ b/tests/main_generic/CMakeLists.txt
@@ -6,7 +6,7 @@ set(CMAKE_CXX_STANDARD 11)
 include_directories("${PROJECT_SOURCE_DIR}/../../includes")
 include_directories("${PROJECT_SOURCE_DIR}/../../external/rtm/includes")
 include_directories("${PROJECT_SOURCE_DIR}/../../external/sjson-cpp/includes")
-include_directories("${PROJECT_SOURCE_DIR}/../../external/catch2/single_include/catch2")
+include_directories("${PROJECT_SOURCE_DIR}/../../external/catch2/single_include")
 
 # Grab all of our test source files
 file(GLOB_RECURSE ALL_TEST_SOURCE_FILES LIST_DIRECTORIES false
@@ -25,8 +25,8 @@ add_executable(${PROJECT_NAME} ${ALL_TEST_SOURCE_FILES} ${ALL_MAIN_SOURCE_FILES}
 
 list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/../../external/catch2/contrib")
 include(CTest)
-include(ParseAndAddCatchTests)
-ParseAndAddCatchTests(${PROJECT_NAME})
+include(Catch)
+catch_discover_tests(${PROJECT_NAME})
 
 setup_default_compiler_flags(${PROJECT_NAME})
 
diff --git a/tests/main_generic/main.cpp b/tests/main_generic/main.cpp
--- a/tests/main_generic/main.cpp
+++ b/tests/main_generic/main.cpp
@@ -23,7 +23,7 @@
 ////////////////////////////////////////////////////////////////////////////////
 
 #define CATCH_CONFIG_RUNNER
-#include <catch.hpp>
+#include <catch2/catch.hpp>
 
 #ifdef _WIN32
 #include <conio.h>
diff --git a/tests/main_ios/CMakeLists.txt b/tests/main_ios/CMakeLists.txt
--- a/tests/main_ios/CMakeLists.txt
+++ b/tests/main_ios/CMakeLists.txt
@@ -17,7 +17,7 @@ set(MACOSX_BUNDLE_BUNDLE_NAME "acl-unit-tests")
 
 include_directories("${PROJECT_SOURCE_DIR}/../../includes")
 include_directories("${PROJECT_SOURCE_DIR}/../../external/rtm/includes")
-include_directories("${PROJECT_SOURCE_DIR}/../../external/catch2/single_include/catch2")
+include_directories("${PROJECT_SOURCE_DIR}/../../external/catch2/single_include")
 
 if(USE_SJSON)
 	include_directories("${PROJECT_SOURCE_DIR}/../../external/sjson-cpp/includes")
diff --git a/tests/main_ios/main.cpp b/tests/main_ios/main.cpp
--- a/tests/main_ios/main.cpp
+++ b/tests/main_ios/main.cpp
@@ -23,7 +23,7 @@
 ////////////////////////////////////////////////////////////////////////////////
 
 #define CATCH_CONFIG_RUNNER
-#include <catch.hpp>
+#include <catch2/catch.hpp>
 
 int main(int argc, char* argv[])
 {
diff --git a/tests/sources/core/test_ansi_allocator.cpp b/tests/sources/core/test_ansi_allocator.cpp
--- a/tests/sources/core/test_ansi_allocator.cpp
+++ b/tests/sources/core/test_ansi_allocator.cpp
@@ -22,7 +22,7 @@
 // SOFTWARE.
 ////////////////////////////////////////////////////////////////////////////////
 
-#include <catch.hpp>
+#include <catch2/catch.hpp>
 
 // Enable allocation tracking
 #define ACL_ALLOCATOR_TRACK_NUM_ALLOCATIONS
diff --git a/tests/sources/core/test_bit_manip_utils.cpp b/tests/sources/core/test_bit_manip_utils.cpp
--- a/tests/sources/core/test_bit_manip_utils.cpp
+++ b/tests/sources/core/test_bit_manip_utils.cpp
@@ -22,7 +22,7 @@
 // SOFTWARE.
 ////////////////////////////////////////////////////////////////////////////////
 
-#include <catch.hpp>
+#include <catch2/catch.hpp>
 
 #include <acl/core/bit_manip_utils.h>
 
diff --git a/tests/sources/core/test_bitset.cpp b/tests/sources/core/test_bitset.cpp
--- a/tests/sources/core/test_bitset.cpp
+++ b/tests/sources/core/test_bitset.cpp
@@ -22,7 +22,7 @@
 // SOFTWARE.
 ////////////////////////////////////////////////////////////////////////////////
 
-#include <catch.hpp>
+#include <catch2/catch.hpp>
 
 #include <acl/core/bitset.h>
 
diff --git a/tests/sources/core/test_enum_utils.cpp b/tests/sources/core/test_enum_utils.cpp
--- a/tests/sources/core/test_enum_utils.cpp
+++ b/tests/sources/core/test_enum_utils.cpp
@@ -22,7 +22,7 @@
 // SOFTWARE.
 ////////////////////////////////////////////////////////////////////////////////
 
-#include <catch.hpp>
+#include <catch2/catch.hpp>
 
 #include <acl/core/enum_utils.h>
 
diff --git a/tests/sources/core/test_error_result.cpp b/tests/sources/core/test_error_result.cpp
--- a/tests/sources/core/test_error_result.cpp
+++ b/tests/sources/core/test_error_result.cpp
@@ -22,7 +22,7 @@
 // SOFTWARE.
 ////////////////////////////////////////////////////////////////////////////////
 
-#include <catch.hpp>
+#include <catch2/catch.hpp>
 
 #include <acl/core/error_result.h>
 
diff --git a/tests/sources/core/test_interpolation_utils.cpp b/tests/sources/core/test_interpolation_utils.cpp
--- a/tests/sources/core/test_interpolation_utils.cpp
+++ b/tests/sources/core/test_interpolation_utils.cpp
@@ -22,7 +22,7 @@
 // SOFTWARE.
 ////////////////////////////////////////////////////////////////////////////////
 
-#include <catch.hpp>
+#include <catch2/catch.hpp>
 
 #include <acl/core/interpolation_utils.h>
 #include <rtm/scalarf.h>
diff --git a/tests/sources/core/test_iterator.cpp b/tests/sources/core/test_iterator.cpp
--- a/tests/sources/core/test_iterator.cpp
+++ b/tests/sources/core/test_iterator.cpp
@@ -22,7 +22,7 @@
 // SOFTWARE.
 ////////////////////////////////////////////////////////////////////////////////
 
-#include <catch.hpp>
+#include <catch2/catch.hpp>
 
 #include <acl/core/iterator.h>
 #include <acl/core/memory_utils.h>
diff --git a/tests/sources/core/test_memory_utils.cpp b/tests/sources/core/test_memory_utils.cpp
--- a/tests/sources/core/test_memory_utils.cpp
+++ b/tests/sources/core/test_memory_utils.cpp
@@ -22,7 +22,7 @@
 // SOFTWARE.
 ////////////////////////////////////////////////////////////////////////////////
 
-#include <catch.hpp>
+#include <catch2/catch.hpp>
 
 #include <acl/core/memory_utils.h>
 
@@ -165,6 +165,17 @@ TEST_CASE("memcpy_bits", "[core][memory]")
 
 	memcpy_bits(&dest, 0, &src, 0, 64);
 	CHECK(dest == uint64_t(~0ULL));
+
+	dest = 0;
+	src = uint64_t(~0ULL);
+	memcpy_bits(&dest, 0, &src, 0, 0);
+	CHECK(dest == 0);
+
+	memcpy_bits(&dest, 0, nullptr, 0, 0);
+	CHECK(dest == 0);
+
+	memcpy_bits(nullptr, 0, &src, 0, 0);
+	CHECK(dest == 0);
 }
 
 enum class UnsignedEnum : uint32_t
diff --git a/tests/sources/core/test_ptr_offset.cpp b/tests/sources/core/test_ptr_offset.cpp
--- a/tests/sources/core/test_ptr_offset.cpp
+++ b/tests/sources/core/test_ptr_offset.cpp
@@ -22,7 +22,7 @@
 // SOFTWARE.
 ////////////////////////////////////////////////////////////////////////////////
 
-#include <catch.hpp>
+#include <catch2/catch.hpp>
 
 #include <acl/core/ptr_offset.h>
 
diff --git a/tests/sources/core/test_string.cpp b/tests/sources/core/test_string.cpp
--- a/tests/sources/core/test_string.cpp
+++ b/tests/sources/core/test_string.cpp
@@ -22,7 +22,7 @@
 // SOFTWARE.
 ////////////////////////////////////////////////////////////////////////////////
 
-#include <catch.hpp>
+#include <catch2/catch.hpp>
 
 // Enable allocation tracking
 #define ACL_ALLOCATOR_TRACK_NUM_ALLOCATIONS
diff --git a/tests/sources/core/test_utils.cpp b/tests/sources/core/test_utils.cpp
--- a/tests/sources/core/test_utils.cpp
+++ b/tests/sources/core/test_utils.cpp
@@ -22,7 +22,7 @@
 // SOFTWARE.
 ////////////////////////////////////////////////////////////////////////////////
 
-#include <catch.hpp>
+#include <catch2/catch.hpp>
 
 #include <acl/core/utils.h>
 #include <rtm/scalarf.h>
diff --git a/tests/sources/io/test_reader_writer.cpp b/tests/sources/io/test_reader_writer.cpp
--- a/tests/sources/io/test_reader_writer.cpp
+++ b/tests/sources/io/test_reader_writer.cpp
@@ -22,7 +22,7 @@
 // SOFTWARE.
 ////////////////////////////////////////////////////////////////////////////////
 
-#include <catch.hpp>
+#include <catch2/catch.hpp>
 
 // Enable allocation tracking
 #define ACL_ALLOCATOR_TRACK_NUM_ALLOCATIONS
diff --git a/tests/sources/math/test_quat_packing.cpp b/tests/sources/math/test_quat_packing.cpp
--- a/tests/sources/math/test_quat_packing.cpp
+++ b/tests/sources/math/test_quat_packing.cpp
@@ -22,7 +22,7 @@
 // SOFTWARE.
 ////////////////////////////////////////////////////////////////////////////////
 
-#include <catch.hpp>
+#include <catch2/catch.hpp>
 
 #include <acl/math/quat_packing.h>
 
diff --git a/tests/sources/math/test_scalar_packing.cpp b/tests/sources/math/test_scalar_packing.cpp
--- a/tests/sources/math/test_scalar_packing.cpp
+++ b/tests/sources/math/test_scalar_packing.cpp
@@ -22,7 +22,7 @@
 // SOFTWARE.
 ////////////////////////////////////////////////////////////////////////////////
 
-#include <catch.hpp>
+#include <catch2/catch.hpp>
 
 #include <acl/core/variable_bit_rates.h>
 #include <acl/math/scalar_packing.h>
diff --git a/tests/sources/math/test_vector4_packing.cpp b/tests/sources/math/test_vector4_packing.cpp
--- a/tests/sources/math/test_vector4_packing.cpp
+++ b/tests/sources/math/test_vector4_packing.cpp
@@ -22,7 +22,7 @@
 // SOFTWARE.
 ////////////////////////////////////////////////////////////////////////////////
 
-#include <catch.hpp>
+#include <catch2/catch.hpp>
 
 #include <acl/core/variable_bit_rates.h>
 #include <acl/math/vector4_packing.h>