Repository: nfrechette/acl (cpp)

Issue:
Refactor the error metric and introduce pose caching
Right now, because we use a virtual function and a custom implementation to transform from local space to object space, it is not possible to cache the object space transforms. This requires us to calculate them over and over when very often, they do not change. By exposing the type used, we can calculate the transforms on demand and cache them. This will speed up compression significantly.

This is a minor API break for those that implement a custom error metric but that is very unlikely.
The error metrics do not handle multiple root bones properly
When calculating the error in object space, we start at the root bone and multiply each child transform in the chain. The code currently assumes 0 as the root bone which is not correct if multiple root bones are present.

This issue only impacts clips that have more than one root bone where the root transforms differ.


Discussion:

Patch:
diff --git a/docs/compressing_a_raw_clip.md b/docs/compressing_a_raw_clip.md
--- a/docs/compressing_a_raw_clip.md
+++ b/docs/compressing_a_raw_clip.md
@@ -30,7 +30,7 @@ settings.range_reduction = RangeReductionFlags8::AllTracks;
 settings.segmenting.enabled = true;
 settings.segmenting.range_reduction = RangeReductionFlags8::AllTracks;
 
-TransformErrorMetric error_metric;
+qvvf_transform_error_metric error_metric;
 settings.error_metric = &error_metric;
 
 OutputStats stats;
diff --git a/docs/error_metrics.md b/docs/error_metrics.md
--- a/docs/error_metrics.md
+++ b/docs/error_metrics.md
@@ -1,27 +1,30 @@
-# Skeleton error metric
+# Transform error metric
 
 A proper error metric is central to every animation compression algorithm and ACL is no different. The general technique implemented is the one described on this [blog post](http://nfrechette.github.io/2016/11/01/anim_compression_accuracy/) and the various implementations as well as the interfaces live [here](../includes/acl/compression/skeleton_error_metric.h).
 
-Some care must be taken when selecting which error metric to use. If the error it calculates isn't representative of how it would be calculated in the host game engine, the resulting visual fidelity might suffer. ACL implements a number of popular implementations and you are free to implement and use your own.
+Some care must be taken when selecting which error metric to use. If the error it calculates isn't representative of how it would be calculated in the host game engine, the resulting visual fidelity might suffer. ACL implements a number of popular implementations and you are free to implement and use your own. The `qvvf_transform_error_metric` error metric is a sensible default.
 
-## TransformErrorMetric
+## qvvf_transform_error_metric
 
-This implementation will use `Transform_32` (which implements Vector-Quaternion-Vector (*VQV*) arithmetic) to calculate the error both when scale is present and when it isn't and for both local and object space. It is a solid default and it should handle very well most clips in the wild. However take note that because it uses a simple `vector3` to represent the 3D scale component, it cannot properly handle skew and shear that arises as a result of combining multiple transforms in a bone chain. In practice it rarely matters and if you truly need this, an error metric that uses matrices when scale is present might perform better or by using Vector-Quaternion-Matrix (*VQM*) arithmetic.
+This implementation will use `rtm::qvvf` (which implements Vector-Quaternion-Vector (*VQV*) arithmetic) to calculate the error both when scale is present and when it isn't and for both local and object space. It is a solid default and it should handle very well most clips in the wild. However take note that because it uses a simple `vector3` to represent the 3D scale component, it cannot properly handle skew and shear that arises as a result of combining multiple transforms in a bone chain. In practice it rarely matters and if you truly need this, an error metric that uses matrices when scale is present might perform better or by using Vector-Quaternion-Matrix (*VQM*) arithmetic.
 
-## TransformMatrixErrorMetric
+## qvvf_matrix3x4f_transform_error_metric
 
-This implementation uses `Transform_32` when there is no scale in both local and object space and as well as local space when there is scale. This is generally safe because there is no skew or shear present in the transform. However, when scale is present the object space error metric will convert the transforms into `AffineMatrix_32` in order to combine them into the final object space transform for the bone. This properly handles 3D scale but due to numerical accuracy constraints the error can accumulate to unacceptable levels when very large or very small scale is present and combined with very large translations.
+This implementation uses `rtm::qvvf` when there is no scale in both local and object space and as well as local space when there is scale. This is generally safe because there is no skew or shear present in the transform. However, when scale is present the object space error metric will convert the transforms into `rtm::matrix3x4f` in order to combine them into the final object space transform for the bone. This properly handles 3D scale but due to numerical accuracy constraints the error can accumulate to unacceptable levels when very large or very small scale is present and combined with very large translations.
 
-## AdditiveTransformErrorMetric
+## additive_qvvf_transform_error_metric
 
-This implementation is based on the `TransformErrorMetric` and handles additive and regular animation clips. See [here](additive_clips.md) for the various additive operations supported and which one to select for your game engine.
+This implementation is based on the `qvvf_transform_error_metric` and handles additive and regular animation clips. See [here](additive_clips.md) for the various additive operations supported and which one to select for your game engine.
 
 For the purpose of measuring the error, we ignore object space additive blending. It typically does not play a significant role in the resulting error because the pose is fundamentally very close. As such, the additive clip is always applied in local space to the base clip.
 
-If your game engine performs additive blending in a way that ACL does not support, you will have to modify the sources (and hopefully submit a pull request) to include it. It is fairly easy and simple, just follow the examples already present in the code.
+If your game engine performs additive blending in a way that ACL does not support, you can modify the sources (and hopefully submit a pull request) to include it or you can implement your own error metric. It is fairly simple, just follow the examples already present in the code.
 
 ## Implementing your own error metric
 
-In order to implement your own error metric you need to figure out how your host game engine combines the local space bone transforms into object space in order to do the same. Once you have that information, implement a class that derives from the `ISkeletalErrorMetric` interface. You can use the other error metrics as examples. You will then be able to provide it to the compression algorithm by feeding it to the `CompressionSettings`.
+In order to implement your own error metric you need to figure out how your host game engine combines the local space bone transforms into object space in order to do the same. Once you have that information, implement a class that derives from the `itransform_error_metric` interface. You can use the other error metrics as examples. You will then be able to provide it to the compression algorithm by feeding it to the `CompressionSettings`.
+
+Fundamentally the interface requires you to implement two versions of the error metric: with or without scale. The variants with no scale are only present as an optimization to avoid unnecessary computation when a clip does not contain any scale. It is common enough to warrant an optimized code path.
+
+If your error metric uses a different type than `rtm::qvvf`, you can implement the other functions as needed. See the interface for details.
 
-Fundamentally the interface requires you to implement four versions of the error metric: with or without scale, and in local or object space. The variants with no scale are only present as an optimization to avoid unnecessary computation when a clip does not contain any scale. It is common enough to warrant an optimized code path.
diff --git a/external/rtm b/external/rtm
--- a/external/rtm
+++ b/external/rtm
@@ -1 +1 @@
-Subproject commit 817753c3800d23e9583f25532f707b944d5ce54c
+Subproject commit ce9f00368c73e8f49446ac11cf4752ef65d97d5a
diff --git a/includes/acl/algorithm/uniformly_sampled/decoder.h b/includes/acl/algorithm/uniformly_sampled/decoder.h
--- a/includes/acl/algorithm/uniformly_sampled/decoder.h
+++ b/includes/acl/algorithm/uniformly_sampled/decoder.h
@@ -798,13 +798,15 @@ namespace acl
 
 				for (uint32_t animated_track_index = 0; animated_track_index < num_animated_tracks; ++animated_track_index)
 				{
-					for (size_t i = 0; i < 2; ++i)
-					{
-						const uint8_t bit_rate = m_context.format_per_track_data[i][animated_track_index];
-						const uint8_t num_bits_at_bit_rate = get_num_bits_at_bit_rate(bit_rate) * 3;	// 3 components
+					const uint8_t bit_rate0 = m_context.format_per_track_data[0][animated_track_index];
+					const uint32_t num_bits_at_bit_rate0 = get_num_bits_at_bit_rate(bit_rate0) * 3;	// 3 components
 
-						sampling_context.key_frame_bit_offsets[i] += num_bits_at_bit_rate;
-					}
+					sampling_context.key_frame_bit_offsets[0] += num_bits_at_bit_rate0;
+
+					const uint8_t bit_rate1 = m_context.format_per_track_data[1][animated_track_index];
+					const uint32_t num_bits_at_bit_rate1 = get_num_bits_at_bit_rate(bit_rate1) * 3;	// 3 components
+
+					sampling_context.key_frame_bit_offsets[1] += num_bits_at_bit_rate1;
 				}
 			}
 
diff --git a/includes/acl/algorithm/uniformly_sampled/encoder.h b/includes/acl/algorithm/uniformly_sampled/encoder.h
--- a/includes/acl/algorithm/uniformly_sampled/encoder.h
+++ b/includes/acl/algorithm/uniformly_sampled/encoder.h
@@ -142,7 +142,7 @@ namespace acl
 			else
 				settings.segmenting.range_reduction = RangeReductionFlags8::None;
 
-			quantize_streams(allocator, clip_context, settings, skeleton, raw_clip_context, additive_base_clip_context);
+			quantize_streams(allocator, clip_context, settings, skeleton, raw_clip_context, additive_base_clip_context, out_stats);
 
 			uint16_t num_output_bones = 0;
 			uint16_t* output_bone_mapping = create_output_bone_mapping(allocator, clip, num_output_bones);
diff --git a/includes/acl/compression/compression_settings.h b/includes/acl/compression/compression_settings.h
--- a/includes/acl/compression/compression_settings.h
+++ b/includes/acl/compression/compression_settings.h
@@ -131,7 +131,7 @@ namespace acl
 		//////////////////////////////////////////////////////////////////////////
 		// The error metric to use.
 		// Defaults to 'null', this value must be set manually!
-		ISkeletalErrorMetric* error_metric;
+		itransform_error_metric* error_metric;
 
 		//////////////////////////////////////////////////////////////////////////
 		// Threshold angle when detecting if rotation tracks are constant or default.
diff --git a/includes/acl/compression/impl/quantize_streams.h b/includes/acl/compression/impl/quantize_streams.h
--- a/includes/acl/compression/impl/quantize_streams.h
+++ b/includes/acl/compression/impl/quantize_streams.h
@@ -44,6 +44,7 @@
 
 #include <cstddef>
 #include <cstdint>
+#include <functional>
 
 // 0 = no debug info, 1 = basic info, 2 = verbose
 #define ACL_IMPL_DEBUG_VARIABLE_QUANTIZATION		0
@@ -66,9 +67,9 @@ namespace acl
 			const RigidSkeleton& skeleton;
 			const CompressionSettings& settings;
 
-			acl_impl::track_bit_rate_database database;
-			acl_impl::single_track_query local_query;
-			acl_impl::hierarchical_track_query object_query;
+			track_bit_rate_database bit_rate_database;
+			single_track_query local_query;
+			hierarchical_track_query object_query;
 
 			uint32_t num_samples;
 			uint32_t segment_sample_start_index;
@@ -76,14 +77,31 @@ namespace acl
 			float clip_duration;
 			bool has_scale;
 			bool has_additive_base;
+			bool needs_conversion;
 
 			const BoneStreams* raw_bone_streams;
 
-			rtm::qvvf* additive_local_pose;
-			rtm::qvvf* raw_local_pose;
-			rtm::qvvf* lossy_local_pose;
+			rtm::qvvf* additive_local_pose;			// 1 per transform
+			rtm::qvvf* raw_local_pose;				// 1 per transform
+			rtm::qvvf* lossy_local_pose;			// 1 per transform
 
-			BoneBitRate* bit_rate_per_bone;
+			uint8_t* raw_local_transforms;			// 1 per transform per sample in segment
+			uint8_t* base_local_transforms;			// 1 per transform per sample in segment
+			uint8_t* raw_object_transforms;			// 1 per transform per sample in segment
+			uint8_t* base_object_transforms;		// 1 per transform per sample in segment
+
+			uint8_t* local_transforms_converted;	// 1 per transform
+			uint8_t* lossy_object_pose;				// 1 per transform
+			size_t metric_transform_size;
+
+			BoneBitRate* bit_rate_per_bone;			// 1 per transform
+			uint16_t* parent_transform_indices;		// 1 per transform
+			uint16_t* self_transform_indices;		// 1 per transform
+
+			uint16_t* chain_bone_indices;			// 1 per transform
+			uint16_t num_bones_in_chain;
+			uint16_t padding0;	// unused
+			uint32_t padding1;	// unused
 
 			QuantizationContext(IAllocator& allocator_, ClipContext& clip_, const ClipContext& raw_clip_, const ClipContext& additive_base_clip_, const CompressionSettings& settings_, const RigidSkeleton& skeleton_)
 				: allocator(allocator_)
@@ -95,7 +113,7 @@ namespace acl
 				, num_bones(clip_.num_bones)
 				, skeleton(skeleton_)
 				, settings(settings_)
-				, database(allocator_, settings_, clip_.segments->bone_streams, raw_clip_.segments->bone_streams, clip_.num_bones, clip_.segments->num_samples)
+				, bit_rate_database(allocator_, settings_, clip_.segments->bone_streams, raw_clip_.segments->bone_streams, clip_.num_bones, clip_.segments->num_samples)
 				, local_query()
 				, object_query(allocator_)
 				, num_samples(~0U)
@@ -105,14 +123,35 @@ namespace acl
 				, has_scale(clip_.has_scale)
 				, has_additive_base(clip_.has_additive_base)
 				, raw_bone_streams(raw_clip_.segments[0].bone_streams)
+				, num_bones_in_chain(0)
 			{
-				local_query.bind(database);
-				object_query.bind(database);
+				local_query.bind(bit_rate_database);
+				object_query.bind(bit_rate_database);
+
+				needs_conversion = settings_.error_metric->needs_conversion(clip_.has_scale);
+				const size_t metric_transform_size_ = settings_.error_metric->get_transform_size(clip_.has_scale);
+				metric_transform_size = metric_transform_size_;
 
 				additive_local_pose = clip_.has_additive_base ? allocate_type_array<rtm::qvvf>(allocator, num_bones) : nullptr;
 				raw_local_pose = allocate_type_array<rtm::qvvf>(allocator, num_bones);
 				lossy_local_pose = allocate_type_array<rtm::qvvf>(allocator, num_bones);
+				raw_local_transforms = allocate_type_array_aligned<uint8_t>(allocator, metric_transform_size_ * num_bones * clip_.segments->num_samples, 64);
+				base_local_transforms = clip_.has_additive_base ? allocate_type_array_aligned<uint8_t>(allocator, metric_transform_size_ * num_bones * clip_.segments->num_samples, 64) : nullptr;
+				raw_object_transforms = allocate_type_array_aligned<uint8_t>(allocator, metric_transform_size_ * num_bones * clip_.segments->num_samples, 64);
+				base_object_transforms = clip_.has_additive_base ? allocate_type_array_aligned<uint8_t>(allocator, metric_transform_size_ * num_bones * clip_.segments->num_samples, 64) : nullptr;
+				local_transforms_converted = needs_conversion ? allocate_type_array_aligned<uint8_t>(allocator, metric_transform_size_ * num_bones, 64) : nullptr;
+				lossy_object_pose = allocate_type_array_aligned<uint8_t>(allocator, metric_transform_size_ * num_bones, 64);
 				bit_rate_per_bone = allocate_type_array<BoneBitRate>(allocator, num_bones);
+				parent_transform_indices = allocate_type_array<uint16_t>(allocator, num_bones);
+				self_transform_indices = allocate_type_array<uint16_t>(allocator, num_bones);
+				chain_bone_indices = allocate_type_array<uint16_t>(allocator, num_bones);
+
+				for (uint16_t transform_index = 0; transform_index < num_bones; ++transform_index)
+				{
+					const RigidBone& bone = skeleton_.get_bone(transform_index);
+					parent_transform_indices[transform_index] = bone.parent_index;
+					self_transform_indices[transform_index] = transform_index;
+				}
 			}
 
 			~QuantizationContext()
@@ -120,7 +159,16 @@ namespace acl
 				deallocate_type_array(allocator, additive_local_pose, num_bones);
 				deallocate_type_array(allocator, raw_local_pose, num_bones);
 				deallocate_type_array(allocator, lossy_local_pose, num_bones);
+				deallocate_type_array(allocator, raw_local_transforms, metric_transform_size * num_bones * clip.segments->num_samples);
+				deallocate_type_array(allocator, base_local_transforms, metric_transform_size * num_bones * clip.segments->num_samples);
+				deallocate_type_array(allocator, raw_object_transforms, metric_transform_size * num_bones * clip.segments->num_samples);
+				deallocate_type_array(allocator, base_object_transforms, metric_transform_size * num_bones * clip.segments->num_samples);
+				deallocate_type_array(allocator, local_transforms_converted, metric_transform_size * num_bones);
+				deallocate_type_array(allocator, lossy_object_pose, metric_transform_size * num_bones);
 				deallocate_type_array(allocator, bit_rate_per_bone, num_bones);
+				deallocate_type_array(allocator, parent_transform_indices, num_bones);
+				deallocate_type_array(allocator, self_transform_indices, num_bones);
+				deallocate_type_array(allocator, chain_bone_indices, num_bones);
 			}
 
 			void set_segment(SegmentContext& segment_)
@@ -129,7 +177,77 @@ namespace acl
 				bone_streams = segment_.bone_streams;
 				num_samples = segment_.num_samples;
 				segment_sample_start_index = segment_.clip_sample_offset;
-				database.set_segment(segment_.bone_streams, segment_.num_bones, segment_.num_samples);
+				bit_rate_database.set_segment(segment_.bone_streams, segment_.num_bones, segment_.num_samples);
+
+				// Cache every raw local/object transforms and the base local transforms since they never change
+				const itransform_error_metric* error_metric = settings.error_metric;
+				const size_t sample_transform_size = metric_transform_size * num_bones;
+
+				const auto convert_transforms_impl = std::mem_fn(has_scale ? &itransform_error_metric::convert_transforms : &itransform_error_metric::convert_transforms_no_scale);
+				const auto apply_additive_to_base_impl = std::mem_fn(has_scale ? &itransform_error_metric::apply_additive_to_base : &itransform_error_metric::apply_additive_to_base_no_scale);
+				const auto local_to_object_space_impl = std::mem_fn(has_scale ? &itransform_error_metric::local_to_object_space : &itransform_error_metric::local_to_object_space_no_scale);
+
+				itransform_error_metric::convert_transforms_args convert_transforms_args_raw;
+				convert_transforms_args_raw.dirty_transform_indices = self_transform_indices;
+				convert_transforms_args_raw.num_dirty_transforms = num_bones;
+				convert_transforms_args_raw.transforms = raw_local_pose;
+				convert_transforms_args_raw.num_transforms = num_bones;
+
+				itransform_error_metric::convert_transforms_args convert_transforms_args_base = convert_transforms_args_raw;
+				convert_transforms_args_base.transforms = additive_local_pose;
+
+				itransform_error_metric::apply_additive_to_base_args apply_additive_to_base_args_raw;
+				apply_additive_to_base_args_raw.dirty_transform_indices = self_transform_indices;
+				apply_additive_to_base_args_raw.num_dirty_transforms = num_bones;
+				apply_additive_to_base_args_raw.local_transforms = nullptr;
+				apply_additive_to_base_args_raw.base_transforms = nullptr;
+				apply_additive_to_base_args_raw.num_transforms = num_bones;
+
+				itransform_error_metric::local_to_object_space_args local_to_object_space_args_raw;
+				local_to_object_space_args_raw.dirty_transform_indices = self_transform_indices;
+				local_to_object_space_args_raw.num_dirty_transforms = num_bones;
+				local_to_object_space_args_raw.parent_transform_indices = parent_transform_indices;
+				local_to_object_space_args_raw.local_transforms = nullptr;
+				local_to_object_space_args_raw.num_transforms = num_bones;
+
+				for (uint32_t sample_index = 0; sample_index < segment_.num_samples; ++sample_index)
+				{
+					// Sample our streams and calculate the error
+					// The sample time is calculated from the full clip duration to be consistent with decompression
+					const float sample_time = rtm::scalar_min(float(segment_.clip_sample_offset + sample_index) / sample_rate, clip_duration);
+
+					sample_streams(raw_bone_streams, num_bones, sample_time, raw_local_pose);
+
+					uint8_t* sample_raw_local_transforms = raw_local_transforms + (sample_index * sample_transform_size);
+
+					if (needs_conversion)
+						convert_transforms_impl(error_metric, convert_transforms_args_raw, sample_raw_local_transforms);
+					else
+						std::memcpy(sample_raw_local_transforms, raw_local_pose, sample_transform_size);
+
+					if (has_additive_base)
+					{
+						const float normalized_sample_time = additive_base_clip.num_samples > 1 ? (sample_time / clip_duration) : 0.0F;
+						const float additive_sample_time = additive_base_clip.num_samples > 1 ? (normalized_sample_time * additive_base_clip.duration) : 0.0F;
+						sample_streams(additive_base_clip.segments[0].bone_streams, num_bones, additive_sample_time, additive_local_pose);
+
+						uint8_t* sample_base_local_transforms = base_local_transforms + (sample_index * sample_transform_size);
+
+						if (needs_conversion)
+							convert_transforms_impl(error_metric, convert_transforms_args_base, sample_base_local_transforms);
+						else
+							std::memcpy(sample_base_local_transforms, additive_local_pose, sample_transform_size);
+
+						apply_additive_to_base_args_raw.local_transforms = sample_raw_local_transforms;
+						apply_additive_to_base_args_raw.base_transforms = sample_base_local_transforms;
+						apply_additive_to_base_impl(error_metric, apply_additive_to_base_args_raw, sample_raw_local_transforms);
+					}
+
+					local_to_object_space_args_raw.local_transforms = sample_raw_local_transforms;
+
+					uint8_t* sample_raw_object_transforms = raw_object_transforms + (sample_index * sample_transform_size);
+					local_to_object_space_impl(error_metric, local_to_object_space_args_raw, sample_raw_object_transforms);
+				}
 			}
 
 			bool is_valid() const { return segment != nullptr; }
@@ -220,7 +338,7 @@ namespace acl
 			}
 			else
 			{
-				const uint8_t num_bits_at_bit_rate = get_num_bits_at_bit_rate(bit_rate);
+				const uint32_t num_bits_at_bit_rate = get_num_bits_at_bit_rate(bit_rate);
 
 				for (uint32_t sample_index = 0; sample_index < num_samples; ++sample_index)
 				{
@@ -343,7 +461,7 @@ namespace acl
 			}
 			else
 			{
-				const uint8_t num_bits_at_bit_rate = get_num_bits_at_bit_rate(bit_rate);
+				const uint32_t num_bits_at_bit_rate = get_num_bits_at_bit_rate(bit_rate);
 
 				for (uint32_t sample_index = 0; sample_index < num_samples; ++sample_index)
 				{
@@ -460,7 +578,7 @@ namespace acl
 			}
 			else
 			{
-				const uint8_t num_bits_at_bit_rate = get_num_bits_at_bit_rate(bit_rate);
+				const uint32_t num_bits_at_bit_rate = get_num_bits_at_bit_rate(bit_rate);
 
 				for (uint32_t sample_index = 0; sample_index < num_samples; ++sample_index)
 				{
@@ -508,81 +626,160 @@ namespace acl
 		inline float calculate_max_error_at_bit_rate_local(QuantizationContext& context, uint16_t target_bone_index, error_scan_stop_condition stop_condition)
 		{
 			const CompressionSettings& settings = context.settings;
-			const ISkeletalErrorMetric& error_metric = *settings.error_metric;
+			const itransform_error_metric* error_metric = settings.error_metric;
+			const bool needs_conversion = context.needs_conversion;
+			const bool has_additive_base = context.has_additive_base;
+			const RigidBone& target_bone = context.skeleton.get_bone(target_bone_index);
+			const uint32_t num_transforms = context.num_bones;
+			const size_t sample_transform_size = context.metric_transform_size * context.num_bones;
+			const float sample_rate = context.sample_rate;
+			const float clip_duration = context.clip_duration;
+			const rtm::scalarf error_threshold = rtm::scalar_set(settings.error_threshold);
+
+			const auto convert_transforms_impl = std::mem_fn(context.has_scale ? &itransform_error_metric::convert_transforms : &itransform_error_metric::convert_transforms_no_scale);
+			const auto apply_additive_to_base_impl = std::mem_fn(context.has_scale ? &itransform_error_metric::apply_additive_to_base : &itransform_error_metric::apply_additive_to_base_no_scale);
+			const auto calculate_error_impl = std::mem_fn(context.has_scale ? &itransform_error_metric::calculate_error : &itransform_error_metric::calculate_error_no_scale);
+
+			itransform_error_metric::convert_transforms_args convert_transforms_args_lossy;
+			convert_transforms_args_lossy.dirty_transform_indices = &target_bone_index;
+			convert_transforms_args_lossy.num_dirty_transforms = 1;
+			convert_transforms_args_lossy.transforms = context.lossy_local_pose;
+			convert_transforms_args_lossy.num_transforms = num_transforms;
+
+			itransform_error_metric::apply_additive_to_base_args apply_additive_to_base_args_lossy;
+			apply_additive_to_base_args_lossy.dirty_transform_indices = &target_bone_index;
+			apply_additive_to_base_args_lossy.num_dirty_transforms = 1;
+			apply_additive_to_base_args_lossy.local_transforms = needs_conversion ? (const void*)context.local_transforms_converted : (const void*)context.lossy_local_pose;
+			apply_additive_to_base_args_lossy.base_transforms = nullptr;
+			apply_additive_to_base_args_lossy.num_transforms = num_transforms;
+
+			itransform_error_metric::calculate_error_args calculate_error_args;
+			calculate_error_args.raw_transform = nullptr;
+			calculate_error_args.lossy_transform = needs_conversion ? (const void*)(context.local_transforms_converted + (context.metric_transform_size * target_bone_index)) : (const void*)(context.lossy_local_pose + target_bone_index);
+			calculate_error_args.construct_sphere_shell(target_bone.vertex_distance);
+
+			const uint8_t* raw_transform = context.raw_local_transforms + (target_bone_index * context.metric_transform_size);
+			const uint8_t* base_transforms = context.base_local_transforms;
 
 			context.local_query.build(target_bone_index, context.bit_rate_per_bone[target_bone_index]);
 
-			float max_error = 0.0F;
+			float sample_indexf = float(context.segment_sample_start_index);
+			rtm::scalarf max_error = rtm::scalar_set(0.0F);
 
-			for (uint32_t sample_index = 0; sample_index < context.num_samples; ++sample_index)
+			for (uint32_t sample_index = 0; sample_index < context.num_samples; ++sample_index, sample_indexf += 1.0F)
 			{
 				// Sample our streams and calculate the error
 				// The sample time is calculated from the full clip duration to be consistent with decompression
-				const float sample_time = rtm::scalar_min(float(context.segment_sample_start_index + sample_index) / context.sample_rate, context.clip_duration);
+				const float sample_time = rtm::scalar_min(sample_indexf / sample_rate, clip_duration);
 
-				sample_stream(context.raw_bone_streams, context.num_bones, sample_time, target_bone_index, context.raw_local_pose);
+				context.bit_rate_database.sample(context.local_query, sample_time, context.lossy_local_pose, num_transforms);
 
-				context.database.sample(context.local_query, sample_time, context.lossy_local_pose, context.num_bones);
+				if (needs_conversion)
+					convert_transforms_impl(error_metric, convert_transforms_args_lossy, context.local_transforms_converted);
 
-				if (context.has_additive_base)
+				if (has_additive_base)
 				{
-					const float normalized_sample_time = context.additive_base_clip.num_samples > 1 ? (sample_time / context.clip_duration) : 0.0F;
-					const float additive_sample_time = normalized_sample_time * context.additive_base_clip.duration;
-					sample_stream(context.additive_base_clip.segments[0].bone_streams, context.num_bones, additive_sample_time, target_bone_index, context.additive_local_pose);
+					apply_additive_to_base_args_lossy.base_transforms = base_transforms;
+					base_transforms += sample_transform_size;
+
+					apply_additive_to_base_impl(error_metric, apply_additive_to_base_args_lossy, context.lossy_local_pose);
 				}
 
-				float error;
-				if (context.has_scale)
-					error = error_metric.calculate_local_bone_error(context.skeleton, context.raw_local_pose, context.additive_local_pose, context.lossy_local_pose, target_bone_index);
-				else
-					error = error_metric.calculate_local_bone_error_no_scale(context.skeleton, context.raw_local_pose, context.additive_local_pose, context.lossy_local_pose, target_bone_index);
+				calculate_error_args.raw_transform = raw_transform;
+				raw_transform += sample_transform_size;
+
+				const rtm::scalarf error = calculate_error_impl(error_metric, calculate_error_args);
 
 				max_error = rtm::scalar_max(max_error, error);
-				if (stop_condition == error_scan_stop_condition::until_error_too_high && error >= settings.error_threshold)
+				if (stop_condition == error_scan_stop_condition::until_error_too_high && rtm::scalar_is_greater_equal(error, error_threshold))
 					break;
 			}
 
-			return max_error;
+			return rtm::scalar_cast(max_error);
 		}
 
 		inline float calculate_max_error_at_bit_rate_object(QuantizationContext& context, uint16_t target_bone_index, error_scan_stop_condition stop_condition)
 		{
 			const CompressionSettings& settings = context.settings;
-			const ISkeletalErrorMetric& error_metric = *settings.error_metric;
+			const itransform_error_metric* error_metric = settings.error_metric;
+			const bool needs_conversion = context.needs_conversion;
+			const bool has_additive_base = context.has_additive_base;
+			const RigidBone& target_bone = context.skeleton.get_bone(target_bone_index);
+			const size_t sample_transform_size = context.metric_transform_size * context.num_bones;
+			const float sample_rate = context.sample_rate;
+			const float clip_duration = context.clip_duration;
+			const rtm::scalarf error_threshold = rtm::scalar_set(settings.error_threshold);
+
+			const auto convert_transforms_impl = std::mem_fn(context.has_scale ? &itransform_error_metric::convert_transforms : &itransform_error_metric::convert_transforms_no_scale);
+			const auto apply_additive_to_base_impl = std::mem_fn(context.has_scale ? &itransform_error_metric::apply_additive_to_base : &itransform_error_metric::apply_additive_to_base_no_scale);
+			const auto local_to_object_space_impl = std::mem_fn(context.has_scale ? &itransform_error_metric::local_to_object_space : &itransform_error_metric::local_to_object_space_no_scale);
+			const auto calculate_error_impl = std::mem_fn(context.has_scale ? &itransform_error_metric::calculate_error : &itransform_error_metric::calculate_error_no_scale);
+
+			itransform_error_metric::convert_transforms_args convert_transforms_args_lossy;
+			convert_transforms_args_lossy.dirty_transform_indices = context.chain_bone_indices;
+			convert_transforms_args_lossy.num_dirty_transforms = context.num_bones_in_chain;
+			convert_transforms_args_lossy.transforms = context.lossy_local_pose;
+			convert_transforms_args_lossy.num_transforms = context.num_bones;
+
+			itransform_error_metric::apply_additive_to_base_args apply_additive_to_base_args_lossy;
+			apply_additive_to_base_args_lossy.dirty_transform_indices = context.chain_bone_indices;
+			apply_additive_to_base_args_lossy.num_dirty_transforms = context.num_bones_in_chain;
+			apply_additive_to_base_args_lossy.local_transforms = needs_conversion ? (const void*)(context.local_transforms_converted) : (const void*)context.lossy_local_pose;
+			apply_additive_to_base_args_lossy.base_transforms = nullptr;
+			apply_additive_to_base_args_lossy.num_transforms = context.num_bones;
+
+			itransform_error_metric::local_to_object_space_args local_to_object_space_args_lossy;
+			local_to_object_space_args_lossy.dirty_transform_indices = context.chain_bone_indices;
+			local_to_object_space_args_lossy.num_dirty_transforms = context.num_bones_in_chain;
+			local_to_object_space_args_lossy.parent_transform_indices = context.parent_transform_indices;
+			local_to_object_space_args_lossy.local_transforms = needs_conversion ? (const void*)(context.local_transforms_converted) : (const void*)context.lossy_local_pose;
+			local_to_object_space_args_lossy.num_transforms = context.num_bones;
+
+			itransform_error_metric::calculate_error_args calculate_error_args;
+			calculate_error_args.raw_transform = nullptr;
+			calculate_error_args.lossy_transform = context.lossy_object_pose + (target_bone_index * context.metric_transform_size);
+			calculate_error_args.construct_sphere_shell(target_bone.vertex_distance);
+
+			const uint8_t* raw_transform = context.raw_object_transforms + (target_bone_index * context.metric_transform_size);
+			const uint8_t* base_transforms = context.base_local_transforms;
 
 			context.object_query.build(target_bone_index, context.bit_rate_per_bone, context.bone_streams);
 
-			float max_error = 0.0F;
+			float sample_indexf = float(context.segment_sample_start_index);
+			rtm::scalarf max_error = rtm::scalar_set(0.0F);
 
-			for (uint32_t sample_index = 0; sample_index < context.num_samples; ++sample_index)
+			for (uint32_t sample_index = 0; sample_index < context.num_samples; ++sample_index, sample_indexf += 1.0F)
 			{
 				// Sample our streams and calculate the error
 				// The sample time is calculated from the full clip duration to be consistent with decompression
-				const float sample_time = rtm::scalar_min(float(context.segment_sample_start_index + sample_index) / context.sample_rate, context.clip_duration);
+				const float sample_time = rtm::scalar_min(sample_indexf / sample_rate, clip_duration);
 
-				sample_streams_hierarchical(context.raw_bone_streams, context.num_bones, sample_time, target_bone_index, context.raw_local_pose);
+				context.bit_rate_database.sample(context.object_query, sample_time, context.lossy_local_pose, context.num_bones);
 
-				context.database.sample(context.object_query, sample_time, context.lossy_local_pose, context.num_bones);
+				if (needs_conversion)
+					convert_transforms_impl(error_metric, convert_transforms_args_lossy, context.local_transforms_converted);
 
-				if (context.has_additive_base)
+				if (has_additive_base)
 				{
-					const float normalized_sample_time = context.additive_base_clip.num_samples > 1 ? (sample_time / context.clip_duration) : 0.0F;
-					const float additive_sample_time = normalized_sample_time * context.additive_base_clip.duration;
-					sample_streams_hierarchical(context.additive_base_clip.segments[0].bone_streams, context.num_bones, additive_sample_time, target_bone_index, context.additive_local_pose);
+					apply_additive_to_base_args_lossy.base_transforms = base_transforms;
+					base_transforms += sample_transform_size;
+
+					apply_additive_to_base_impl(error_metric, apply_additive_to_base_args_lossy, context.lossy_local_pose);
 				}
 
-				float error;
-				if (context.has_scale)
-					error = error_metric.calculate_object_bone_error(context.skeleton, context.raw_local_pose, context.additive_local_pose, context.lossy_local_pose, target_bone_index);
-				else
-					error = error_metric.calculate_object_bone_error_no_scale(context.skeleton, context.raw_local_pose, context.additive_local_pose, context.lossy_local_pose, target_bone_index);
+				local_to_object_space_impl(error_metric, local_to_object_space_args_lossy, context.lossy_object_pose);
+
+				calculate_error_args.raw_transform = raw_transform;
+				raw_transform += sample_transform_size;
+
+				const rtm::scalarf error = calculate_error_impl(error_metric, calculate_error_args);
 
 				max_error = rtm::scalar_max(max_error, error);
-				if (stop_condition == error_scan_stop_condition::until_error_too_high && error >= settings.error_threshold)
+				if (stop_condition == error_scan_stop_condition::until_error_too_high && rtm::scalar_is_greater_equal(error, error_threshold))
 					break;
 			}
 
-			return max_error;
+			return rtm::scalar_cast(max_error);
 		}
 
 		inline void calculate_local_space_bit_rates(QuantizationContext& context)
@@ -612,7 +809,7 @@ namespace acl
 
 				BoneBitRate best_bit_rates = bone_bit_rates;
 				float best_error = 1.0E10F;
-				int32_t prev_transform_size = -1;
+				uint32_t prev_transform_size = ~0U;
 				bool is_error_good_enough = false;
 
 				if (context.has_scale)
@@ -656,10 +853,10 @@ namespace acl
 								continue;	// Skip permutations we aren't interested in
 						}
 
-						const int32_t rotation_size = get_num_bits_at_bit_rate(rotation_bit_rate);
-						const int32_t translation_size = get_num_bits_at_bit_rate(translation_bit_rate);
-						const int32_t scale_size = get_num_bits_at_bit_rate(scale_bit_rate);
-						const int32_t transform_size = rotation_size + translation_size + scale_size;
+						const uint32_t rotation_size = get_num_bits_at_bit_rate(rotation_bit_rate);
+						const uint32_t translation_size = get_num_bits_at_bit_rate(translation_bit_rate);
+						const uint32_t scale_size = get_num_bits_at_bit_rate(scale_bit_rate);
+						const uint32_t transform_size = rotation_size + translation_size + scale_size;
 
 						if (transform_size != prev_transform_size && is_error_good_enough)
 						{
@@ -716,9 +913,9 @@ namespace acl
 								continue;	// Skip permutations we aren't interested in
 						}
 
-						const int32_t rotation_size = get_num_bits_at_bit_rate(rotation_bit_rate);
-						const int32_t translation_size = get_num_bits_at_bit_rate(translation_bit_rate);
-						const int32_t transform_size = rotation_size + translation_size;
+						const uint32_t rotation_size = get_num_bits_at_bit_rate(rotation_bit_rate);
+						const uint32_t translation_size = get_num_bits_at_bit_rate(translation_bit_rate);
+						const uint32_t transform_size = rotation_size + translation_size;
 
 						if (transform_size != prev_transform_size && is_error_good_enough)
 						{
@@ -746,6 +943,10 @@ namespace acl
 					}
 				}
 
+#if ACL_IMPL_DEBUG_VARIABLE_QUANTIZATION
+				printf("%u: Best bit rates: %u | %u | %u\n", bone_index, best_bit_rates.rotation, best_bit_rates.translation, best_bit_rates.scale);
+#endif
+
 				context.bit_rate_per_bone[bone_index] = best_bit_rates;
 			}
 		}
@@ -810,7 +1011,7 @@ namespace acl
 			return best_error;
 		}
 
-		inline float calculate_bone_permutation_error(QuantizationContext& context, BoneBitRate* permutation_bit_rates, uint8_t* bone_chain_permutation, const uint16_t* chain_bone_indices, uint16_t num_bones_in_chain, uint16_t bone_index, BoneBitRate* best_bit_rates, float old_error)
+		inline float calculate_bone_permutation_error(QuantizationContext& context, BoneBitRate* permutation_bit_rates, uint8_t* bone_chain_permutation, uint16_t bone_index, BoneBitRate* best_bit_rates, float old_error)
 		{
 			const CompressionSettings& settings = context.settings;
 
@@ -822,12 +1023,12 @@ namespace acl
 				std::memcpy(permutation_bit_rates, context.bit_rate_per_bone, sizeof(BoneBitRate) * context.num_bones);
 
 				bool is_permutation_valid = false;
-				for (uint16_t chain_link_index = 0; chain_link_index < num_bones_in_chain; ++chain_link_index)
+				for (uint16_t chain_link_index = 0; chain_link_index < context.num_bones_in_chain; ++chain_link_index)
 				{
 					if (bone_chain_permutation[chain_link_index] != 0)
 					{
 						// Increase bit rate
-						uint16_t chain_bone_index = chain_bone_indices[chain_link_index];
+						const uint16_t chain_bone_index = context.chain_bone_indices[chain_link_index];
 						BoneBitRate chain_bone_best_bit_rates;
 						increase_bone_bit_rate(context, chain_bone_index, bone_chain_permutation[chain_link_index], old_error, chain_bone_best_bit_rates);
 						is_permutation_valid |= chain_bone_best_bit_rates.rotation != permutation_bit_rates[chain_bone_index].rotation;
@@ -853,7 +1054,7 @@ namespace acl
 					if (permutation_error < settings.error_threshold)
 						break;
 				}
-			} while (std::next_permutation(bone_chain_permutation, bone_chain_permutation + num_bones_in_chain));
+			} while (std::next_permutation(bone_chain_permutation, bone_chain_permutation + context.num_bones_in_chain));
 
 			return best_error;
 		}
@@ -989,7 +1190,6 @@ namespace acl
 			//		[bone 0] + 0 [bone 1] + 0 [bone 2] + 3 (9)
 
 			uint8_t* bone_chain_permutation = allocate_type_array<uint8_t>(context.allocator, context.num_bones);
-			uint16_t* chain_bone_indices = allocate_type_array<uint16_t>(context.allocator, context.num_bones);
 			BoneBitRate* permutation_bit_rates = allocate_type_array<BoneBitRate>(context.allocator, context.num_bones);
 			BoneBitRate* best_permutation_bit_rates = allocate_type_array<BoneBitRate>(context.allocator, context.num_bones);
 			BoneBitRate* best_bit_rates = allocate_type_array<BoneBitRate>(context.allocator, context.num_bones);
@@ -997,21 +1197,13 @@ namespace acl
 
 			for (uint16_t bone_index = 0; bone_index < context.num_bones; ++bone_index)
 			{
+				const uint16_t num_bones_in_chain = calculate_bone_chain_indices(context.skeleton, bone_index, context.chain_bone_indices);
+				context.num_bones_in_chain = num_bones_in_chain;
+
 				float error = calculate_max_error_at_bit_rate_object(context, bone_index, error_scan_stop_condition::until_error_too_high);
 				if (error < settings.error_threshold)
 					continue;
 
-				if (context.bit_rate_per_bone[bone_index].rotation >= k_highest_bit_rate && context.bit_rate_per_bone[bone_index].translation >= k_highest_bit_rate && context.bit_rate_per_bone[bone_index].scale >= k_highest_bit_rate)
-				{
-					// Our bone already has the highest precision possible locally, if the local error already exceeds our threshold,
-					// there is nothing we can do, bail out
-					const float local_error = calculate_max_error_at_bit_rate_local(context, bone_index, error_scan_stop_condition::until_error_too_high);
-					if (local_error >= settings.error_threshold)
-						continue;
-				}
-
-				const uint16_t num_bones_in_chain = calculate_bone_chain_indices(context.skeleton, bone_index, chain_bone_indices);
-
 				const float initial_error = error;
 
 				while (error >= settings.error_threshold)
@@ -1025,7 +1217,7 @@ namespace acl
 					// The first permutation increases the bit rate of a single track/bone
 					std::fill(bone_chain_permutation, bone_chain_permutation + context.num_bones, uint8_t(0));
 					bone_chain_permutation[num_bones_in_chain - 1] = 1;
-					error = calculate_bone_permutation_error(context, permutation_bit_rates, bone_chain_permutation, chain_bone_indices, num_bones_in_chain, bone_index, best_permutation_bit_rates, original_error);
+					error = calculate_bone_permutation_error(context, permutation_bit_rates, bone_chain_permutation, bone_index, best_permutation_bit_rates, original_error);
 					if (error < best_error)
 					{
 						best_error = error;
@@ -1040,7 +1232,7 @@ namespace acl
 						// The second permutation increases the bit rate of 2 track/bones
 						std::fill(bone_chain_permutation, bone_chain_permutation + context.num_bones, uint8_t(0));
 						bone_chain_permutation[num_bones_in_chain - 1] = 2;
-						error = calculate_bone_permutation_error(context, permutation_bit_rates, bone_chain_permutation, chain_bone_indices, num_bones_in_chain, bone_index, best_permutation_bit_rates, original_error);
+						error = calculate_bone_permutation_error(context, permutation_bit_rates, bone_chain_permutation, bone_index, best_permutation_bit_rates, original_error);
 						if (error < best_error)
 						{
 							best_error = error;
@@ -1055,7 +1247,7 @@ namespace acl
 							std::fill(bone_chain_permutation, bone_chain_permutation + context.num_bones, uint8_t(0));
 							bone_chain_permutation[num_bones_in_chain - 2] = 1;
 							bone_chain_permutation[num_bones_in_chain - 1] = 1;
-							error = calculate_bone_permutation_error(context, permutation_bit_rates, bone_chain_permutation, chain_bone_indices, num_bones_in_chain, bone_index, best_permutation_bit_rates, original_error);
+							error = calculate_bone_permutation_error(context, permutation_bit_rates, bone_chain_permutation, bone_index, best_permutation_bit_rates, original_error);
 							if (error < best_error)
 							{
 								best_error = error;
@@ -1072,7 +1264,7 @@ namespace acl
 						// The third permutation increases the bit rate of 3 track/bones
 						std::fill(bone_chain_permutation, bone_chain_permutation + context.num_bones, uint8_t(0));
 						bone_chain_permutation[num_bones_in_chain - 1] = 3;
-						error = calculate_bone_permutation_error(context, permutation_bit_rates, bone_chain_permutation, chain_bone_indices, num_bones_in_chain, bone_index, best_permutation_bit_rates, original_error);
+						error = calculate_bone_permutation_error(context, permutation_bit_rates, bone_chain_permutation, bone_index, best_permutation_bit_rates, original_error);
 						if (error < best_error)
 						{
 							best_error = error;
@@ -1087,7 +1279,7 @@ namespace acl
 							std::fill(bone_chain_permutation, bone_chain_permutation + context.num_bones, uint8_t(0));
 							bone_chain_permutation[num_bones_in_chain - 2] = 2;
 							bone_chain_permutation[num_bones_in_chain - 1] = 1;
-							error = calculate_bone_permutation_error(context, permutation_bit_rates, bone_chain_permutation, chain_bone_indices, num_bones_in_chain, bone_index, best_permutation_bit_rates, original_error);
+							error = calculate_bone_permutation_error(context, permutation_bit_rates, bone_chain_permutation, bone_index, best_permutation_bit_rates, original_error);
 							if (error < best_error)
 							{
 								best_error = error;
@@ -1103,7 +1295,7 @@ namespace acl
 								bone_chain_permutation[num_bones_in_chain - 3] = 1;
 								bone_chain_permutation[num_bones_in_chain - 2] = 1;
 								bone_chain_permutation[num_bones_in_chain - 1] = 1;
-								error = calculate_bone_permutation_error(context, permutation_bit_rates, bone_chain_permutation, chain_bone_indices, num_bones_in_chain, bone_index, best_permutation_bit_rates, original_error);
+								error = calculate_bone_permutation_error(context, permutation_bit_rates, bone_chain_permutation, bone_index, best_permutation_bit_rates, original_error);
 								if (error < best_error)
 								{
 									best_error = error;
@@ -1174,7 +1366,7 @@ namespace acl
 					uint16_t num_maxed_out = 0;
 					for (int16_t chain_link_index = num_bones_in_chain - 1; chain_link_index >= 0; --chain_link_index)
 					{
-						const uint16_t chain_bone_index = chain_bone_indices[chain_link_index];
+						const uint16_t chain_bone_index = context.chain_bone_indices[chain_link_index];
 
 						// Work with a copy. We'll increase the bit rate as much as we can and retain the values
 						// that yield the smallest error BUT increasing the bit rate does NOT always means
@@ -1254,7 +1446,7 @@ namespace acl
 					// From child to parent, max out the bit rate
 					for (int16_t chain_link_index = num_bones_in_chain - 1; chain_link_index >= 0; --chain_link_index)
 					{
-						const uint16_t chain_bone_index = chain_bone_indices[chain_link_index];
+						const uint16_t chain_bone_index = context.chain_bone_indices[chain_link_index];
 						BoneBitRate& bone_bit_rate = context.bit_rate_per_bone[chain_bone_index];
 						bone_bit_rate.rotation = std::max<uint8_t>(bone_bit_rate.rotation, k_highest_bit_rate);
 						bone_bit_rate.translation = std::max<uint8_t>(bone_bit_rate.translation, k_highest_bit_rate);
@@ -1278,20 +1470,19 @@ namespace acl
 #endif
 
 			deallocate_type_array(context.allocator, bone_chain_permutation, context.num_bones);
-			deallocate_type_array(context.allocator, chain_bone_indices, context.num_bones);
 			deallocate_type_array(context.allocator, permutation_bit_rates, context.num_bones);
 			deallocate_type_array(context.allocator, best_permutation_bit_rates, context.num_bones);
 			deallocate_type_array(context.allocator, best_bit_rates, context.num_bones);
 		}
 
-		inline void quantize_streams(IAllocator& allocator, ClipContext& clip_context, const CompressionSettings& settings, const RigidSkeleton& skeleton, const ClipContext& raw_clip_context, const ClipContext& additive_base_clip_context)
+		inline void quantize_streams(IAllocator& allocator, ClipContext& clip_context, const CompressionSettings& settings, const RigidSkeleton& skeleton, const ClipContext& raw_clip_context, const ClipContext& additive_base_clip_context, OutputStats& out_stats)
 		{
 			const bool is_rotation_variable = is_rotation_format_variable(settings.rotation_format);
 			const bool is_translation_variable = is_vector_format_variable(settings.translation_format);
 			const bool is_scale_variable = is_vector_format_variable(settings.scale_format);
 			const bool is_any_variable = is_rotation_variable || is_translation_variable || is_scale_variable;
 
-			acl_impl::QuantizationContext context(allocator, clip_context, raw_clip_context, additive_base_clip_context, settings, skeleton);
+			QuantizationContext context(allocator, clip_context, raw_clip_context, additive_base_clip_context, settings, skeleton);
 
 			for (SegmentContext& segment : clip_context.segment_iterator())
 			{
@@ -1302,11 +1493,38 @@ namespace acl
 				context.set_segment(segment);
 
 				if (is_any_variable)
-					acl_impl::find_optimal_bit_rates(context);
+					find_optimal_bit_rates(context);
 
 				// Quantize our streams now that we found the optimal bit rates
-				acl_impl::quantize_all_streams(context);
+				quantize_all_streams(context);
+			}
+
+#if defined(SJSON_CPP_WRITER)
+			if (are_all_enum_flags_set(out_stats.logging, StatLogging::Detailed))
+			{
+				sjson::ObjectWriter& writer = *out_stats.writer;
+				writer["track_bit_rate_database_size"] = static_cast<uint32_t>(context.bit_rate_database.get_allocated_size());
+
+				size_t transform_cache_size = 0;
+				transform_cache_size += sizeof(rtm::qvvf) * context.num_bones;	// raw_local_pose
+				transform_cache_size += sizeof(rtm::qvvf) * context.num_bones;	// lossy_local_pose
+				transform_cache_size += context.metric_transform_size * context.num_bones;	// lossy_object_pose
+				transform_cache_size += context.metric_transform_size * context.num_bones * context.clip.segments->num_samples;	// raw_local_transforms
+				transform_cache_size += context.metric_transform_size * context.num_bones * context.clip.segments->num_samples;	// raw_object_transforms
+
+				if (context.needs_conversion)
+					transform_cache_size += context.metric_transform_size * context.num_bones;	// local_transforms_converted
+				
+				if (context.has_additive_base)
+				{
+					transform_cache_size += sizeof(rtm::qvvf) * context.num_bones;	// additive_local_pose
+					transform_cache_size += context.metric_transform_size * context.num_bones * context.clip.segments->num_samples;	// base_local_transforms
+					transform_cache_size += context.metric_transform_size * context.num_bones * context.clip.segments->num_samples;	// base_object_transforms
+				}
+
+				writer["transform_cache_size"] = static_cast<uint32_t>(transform_cache_size);
 			}
+#endif
 		}
 	}
 }
diff --git a/includes/acl/compression/impl/sample_streams.h b/includes/acl/compression/impl/sample_streams.h
--- a/includes/acl/compression/impl/sample_streams.h
+++ b/includes/acl/compression/impl/sample_streams.h
@@ -69,7 +69,7 @@ namespace acl
 					return unpack_vector3_96_unsafe(ptr);
 				else
 				{
-					const uint8_t num_bits_at_bit_rate = get_num_bits_at_bit_rate(bit_rate);
+					const uint32_t num_bits_at_bit_rate = get_num_bits_at_bit_rate(bit_rate);
 					if (is_normalized)
 						return unpack_vector3_uXX_unsafe(num_bits_at_bit_rate, ptr, 0);
 					else
@@ -100,7 +100,7 @@ namespace acl
 					return unpack_vector3_96_unsafe(ptr);
 				else
 				{
-					const uint8_t num_bits_at_bit_rate = get_num_bits_at_bit_rate(bit_rate);
+					const uint32_t num_bits_at_bit_rate = get_num_bits_at_bit_rate(bit_rate);
 					return unpack_vector3_uXX_unsafe(num_bits_at_bit_rate, ptr, 0);
 				}
 			default:
@@ -934,11 +934,12 @@ namespace acl
 			const rtm::vector4f default_scale = get_default_scale(segment_context->clip->additive_format);
 			const bool has_scale = segment_context->clip->has_scale;
 
+			// With uniform sample distributions, we do not interpolate.
 			uint32_t sample_key;
 			if (segment_context->distribution == SampleDistribution8::Uniform)
-				sample_key = acl_impl::get_uniform_sample_key(*segment_context, sample_time);
+				sample_key = get_uniform_sample_key(*segment_context, sample_time);
 			else
-				sample_key = 0;
+				sample_key = 0;	// Not used
 
 			acl_impl::sample_context context;
 			context.sample_key = sample_key;
@@ -984,11 +985,12 @@ namespace acl
 			const rtm::vector4f default_scale = get_default_scale(segment_context->clip->additive_format);
 			const bool has_scale = segment_context->clip->has_scale;
 
+			// With uniform sample distributions, we do not interpolate.
 			uint32_t sample_key;
 			if (segment_context->distribution == SampleDistribution8::Uniform)
-				sample_key = acl_impl::get_uniform_sample_key(*segment_context, sample_time);
+				sample_key = get_uniform_sample_key(*segment_context, sample_time);
 			else
-				sample_key = 0;
+				sample_key = 0;	// Not used
 
 			acl_impl::sample_context context;
 			context.track_index = bone_index;
@@ -1024,11 +1026,12 @@ namespace acl
 			const rtm::vector4f default_scale = get_default_scale(segment_context->clip->additive_format);
 			const bool has_scale = segment_context->clip->has_scale;
 
+			// With uniform sample distributions, we do not interpolate.
 			uint32_t sample_key;
 			if (segment_context->distribution == SampleDistribution8::Uniform)
-				sample_key = acl_impl::get_uniform_sample_key(*segment_context, sample_time);
+				sample_key = get_uniform_sample_key(*segment_context, sample_time);
 			else
-				sample_key = 0;
+				sample_key = 0;	// Not used
 
 			acl_impl::sample_context context;
 			context.sample_key = sample_key;
@@ -1080,11 +1083,12 @@ namespace acl
 			const rtm::vector4f default_scale = get_default_scale(segment_context->clip->additive_format);
 			const bool has_scale = segment_context->clip->has_scale;
 
+			// With uniform sample distributions, we do not interpolate.
 			uint32_t sample_key;
 			if (segment_context->distribution == SampleDistribution8::Uniform)
-				sample_key = acl_impl::get_uniform_sample_key(*segment_context, sample_time);
+				sample_key = get_uniform_sample_key(*segment_context, sample_time);
 			else
-				sample_key = 0;
+				sample_key = 0;	// Not used
 
 			acl_impl::sample_context context;
 			context.sample_key = sample_key;
@@ -1138,11 +1142,12 @@ namespace acl
 			const rtm::vector4f default_scale = get_default_scale(segment_context->clip->additive_format);
 			const bool has_scale = segment_context->clip->has_scale;
 
+			// With uniform sample distributions, we do not interpolate.
 			uint32_t sample_key;
 			if (segment_context->distribution == SampleDistribution8::Uniform)
-				sample_key = acl_impl::get_uniform_sample_key(*segment_context, sample_time);
+				sample_key = get_uniform_sample_key(*segment_context, sample_time);
 			else
-				sample_key = 0;
+				sample_key = 0;	// Not used
 
 			acl_impl::sample_context context;
 			context.track_index = bone_index;
@@ -1184,11 +1189,12 @@ namespace acl
 			const rtm::vector4f default_scale = get_default_scale(segment_context->clip->additive_format);
 			const bool has_scale = segment_context->clip->has_scale;
 
+			// With uniform sample distributions, we do not interpolate.
 			uint32_t sample_key;
 			if (segment_context->distribution == SampleDistribution8::Uniform)
-				sample_key = acl_impl::get_uniform_sample_key(*segment_context, sample_time);
+				sample_key = get_uniform_sample_key(*segment_context, sample_time);
 			else
-				sample_key = 0;
+				sample_key = 0;	// Not used
 
 			acl_impl::sample_context context;
 			context.sample_key = sample_key;
diff --git a/includes/acl/compression/impl/track_bit_rate_database.h b/includes/acl/compression/impl/track_bit_rate_database.h
--- a/includes/acl/compression/impl/track_bit_rate_database.h
+++ b/includes/acl/compression/impl/track_bit_rate_database.h
@@ -152,6 +152,8 @@ namespace acl
 			void sample(const single_track_query& query, float sample_time, rtm::qvvf* out_transforms, uint32_t num_transforms);
 			void sample(const hierarchical_track_query& query, float sample_time, rtm::qvvf* out_transforms, uint32_t num_transforms);
 
+			size_t get_allocated_size() const;
+
 		private:
 			track_bit_rate_database(const track_bit_rate_database&) = delete;
 			track_bit_rate_database(track_bit_rate_database&&) = delete;
@@ -1066,6 +1068,15 @@ namespace acl
 				}
 			}
 		}
+
+		inline size_t track_bit_rate_database::get_allocated_size() const
+		{
+			size_t cache_size = 0;
+			cache_size += sizeof(transform_cache_entry) * m_num_transforms;
+			cache_size += m_track_bitsets_size;
+			cache_size += m_data_size;
+			return cache_size;
+		}
 	}
 }
 
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
@@ -35,9 +35,10 @@
 #include "acl/compression/skeleton_error_metric.h"
 #include "acl/compression/utils.h"
 
+#include <chrono>
 #include <cstdint>
+#include <functional>
 #include <thread>
-#include <chrono>
 
 ACL_IMPL_FILE_PRAGMA_PUSH
 
@@ -84,7 +85,7 @@ namespace acl
 
 			writer["bit_rate_counts"] = [&](sjson::ArrayWriter& bitrate_writer)
 			{
-				for (uint8_t bit_rate = 0; bit_rate < k_num_bit_rates; ++bit_rate)
+				for (uint32_t bit_rate = 0; bit_rate < k_num_bit_rates; ++bit_rate)
 					bitrate_writer.push(bit_rate_counts[bit_rate]);
 			};
 
@@ -103,13 +104,52 @@ namespace acl
 			const uint16_t num_bones = skeleton.get_num_bones();
 			const bool has_scale = segment_context_has_scale(segment);
 
+			ACL_ASSERT(!settings.error_metric->needs_conversion(has_scale), "Error metric conversion not supported");
+
+			const auto local_to_object_space_impl = std::mem_fn(has_scale ? &itransform_error_metric::local_to_object_space : &itransform_error_metric::local_to_object_space_no_scale);
+			const auto calculate_error_impl = std::mem_fn(has_scale ? &itransform_error_metric::calculate_error : &itransform_error_metric::calculate_error_no_scale);
+			const auto apply_additive_to_base_impl = std::mem_fn(has_scale ? &itransform_error_metric::apply_additive_to_base : &itransform_error_metric::apply_additive_to_base_no_scale);
+
 			rtm::qvvf* raw_local_pose = allocate_type_array<rtm::qvvf>(allocator, num_bones);
 			rtm::qvvf* base_local_pose = allocate_type_array<rtm::qvvf>(allocator, num_bones);
 			rtm::qvvf* lossy_local_pose = allocate_type_array<rtm::qvvf>(allocator, num_bones);
 
+			rtm::qvvf* raw_object_pose = allocate_type_array<rtm::qvvf>(allocator, num_bones);
+			rtm::qvvf* lossy_object_pose = allocate_type_array<rtm::qvvf>(allocator, num_bones);
+
+			uint16_t* parent_transform_indices = allocate_type_array<uint16_t>(allocator, num_bones);
+			uint16_t* self_transform_indices = allocate_type_array<uint16_t>(allocator, num_bones);
+
+			for (uint16_t transform_index = 0; transform_index < num_bones; ++transform_index)
+			{
+				const RigidBone& bone = skeleton.get_bone(transform_index);
+				parent_transform_indices[transform_index] = bone.parent_index;
+				self_transform_indices[transform_index] = transform_index;
+			}
+
 			const float sample_rate = raw_clip_context.sample_rate;
 			const float ref_duration = calculate_duration(raw_clip_context.num_samples, sample_rate);
 
+			itransform_error_metric::apply_additive_to_base_args apply_additive_to_base_args_raw;
+			apply_additive_to_base_args_raw.dirty_transform_indices = self_transform_indices;
+			apply_additive_to_base_args_raw.num_dirty_transforms = num_bones;
+			apply_additive_to_base_args_raw.local_transforms = raw_local_pose;
+			apply_additive_to_base_args_raw.base_transforms = base_local_pose;
+			apply_additive_to_base_args_raw.num_transforms = num_bones;
+
+			itransform_error_metric::apply_additive_to_base_args apply_additive_to_base_args_lossy = apply_additive_to_base_args_raw;
+			apply_additive_to_base_args_lossy.local_transforms = lossy_local_pose;
+
+			itransform_error_metric::local_to_object_space_args local_to_object_space_args_raw;
+			local_to_object_space_args_raw.dirty_transform_indices = self_transform_indices;
+			local_to_object_space_args_raw.num_dirty_transforms = num_bones;
+			local_to_object_space_args_raw.parent_transform_indices = parent_transform_indices;
+			local_to_object_space_args_raw.local_transforms = raw_local_pose;
+			local_to_object_space_args_raw.num_transforms = num_bones;
+
+			itransform_error_metric::local_to_object_space_args local_to_object_space_args_lossy = local_to_object_space_args_raw;
+			local_to_object_space_args_lossy.local_transforms = lossy_local_pose;
+
 			BoneError worst_bone_error;
 
 			writer["error_per_frame_and_bone"] = [&](sjson::ArrayWriter& frames_writer)
@@ -124,31 +164,40 @@ namespace acl
 					if (raw_clip_context.has_additive_base)
 					{
 						const float normalized_sample_time = additive_base_clip_context.num_samples > 1 ? (sample_time / ref_duration) : 0.0F;
-						const float additive_sample_time = normalized_sample_time * additive_base_clip_context.duration;
+						const float additive_sample_time = additive_base_clip_context.num_samples > 1 ? (normalized_sample_time * additive_base_clip_context.duration) : 0.0F;
 						sample_streams(additive_base_clip_context.segments[0].bone_streams, num_bones, additive_sample_time, base_local_pose);
+
+						apply_additive_to_base_impl(settings.error_metric, apply_additive_to_base_args_raw, raw_local_pose);
+						apply_additive_to_base_impl(settings.error_metric, apply_additive_to_base_args_lossy, lossy_local_pose);
 					}
 
+					local_to_object_space_impl(settings.error_metric, local_to_object_space_args_raw, raw_object_pose);
+					local_to_object_space_impl(settings.error_metric, local_to_object_space_args_lossy, lossy_object_pose);
+
 					frames_writer.push_newline();
 					frames_writer.push([&](sjson::ArrayWriter& frame_writer)
+					{
+						for (uint16_t bone_index = 0; bone_index < num_bones; ++bone_index)
 						{
-							for (uint16_t bone_index = 0; bone_index < num_bones; ++bone_index)
+							const RigidBone& bone = skeleton.get_bone(bone_index);
+
+							itransform_error_metric::calculate_error_args calculate_error_args;
+							calculate_error_args.raw_transform = raw_object_pose + bone_index;
+							calculate_error_args.lossy_transform = lossy_object_pose + bone_index;
+							calculate_error_args.construct_sphere_shell(bone.vertex_distance);
+
+							const float error = rtm::scalar_cast(calculate_error_impl(settings.error_metric, calculate_error_args));
+
+							frame_writer.push(error);
+
+							if (error > worst_bone_error.error)
 							{
-								float error;
-								if (has_scale)
-									error = settings.error_metric->calculate_object_bone_error(skeleton, raw_local_pose, base_local_pose, lossy_local_pose, bone_index);
-								else
-									error = settings.error_metric->calculate_object_bone_error_no_scale(skeleton, raw_local_pose, base_local_pose, lossy_local_pose, bone_index);
-
-								frame_writer.push(error);
-
-								if (error > worst_bone_error.error)
-								{
-									worst_bone_error.error = error;
-									worst_bone_error.index = bone_index;
-									worst_bone_error.sample_time = sample_time;
-								}
+								worst_bone_error.error = error;
+								worst_bone_error.index = bone_index;
+								worst_bone_error.sample_time = sample_time;
 							}
-						});
+						}
+					});
 				}
 			};
 
@@ -159,6 +208,12 @@ namespace acl
 			deallocate_type_array(allocator, raw_local_pose, num_bones);
 			deallocate_type_array(allocator, base_local_pose, num_bones);
 			deallocate_type_array(allocator, lossy_local_pose, num_bones);
+
+			deallocate_type_array(allocator, raw_object_pose, num_bones);
+			deallocate_type_array(allocator, lossy_object_pose, num_bones);
+
+			deallocate_type_array(allocator, parent_transform_indices, num_bones);
+			deallocate_type_array(allocator, self_transform_indices, num_bones);
 		}
 
 		inline void write_stats(IAllocator& allocator, const AnimationClip& clip, const ClipContext& clip_context, const RigidSkeleton& skeleton,
diff --git a/includes/acl/compression/skeleton_error_metric.h b/includes/acl/compression/skeleton_error_metric.h
--- a/includes/acl/compression/skeleton_error_metric.h
+++ b/includes/acl/compression/skeleton_error_metric.h
@@ -37,291 +37,419 @@ ACL_IMPL_FILE_PRAGMA_PUSH
 
 namespace acl
 {
-	// Base class for all skeletal error metrics
-	class ISkeletalErrorMetric
+	//////////////////////////////////////////////////////////////////////////
+	// Interface for all skeletal error metrics.
+	// An error metric is responsible for a few things:
+	//    - converting from rtm::qvvf into whatever transform type the metric uses (optional)
+	//    - applying local space transforms on top of base transforms (optional)
+	//    - transforming local space transforms into object space
+	//    - evaluating the error function
+	//
+	// Most functions require two implementations: with and without scale support.
+	// This is entirely for performance reasons as most clips do not have any scale.
+	//////////////////////////////////////////////////////////////////////////
+	class itransform_error_metric
 	{
 	public:
-		virtual ~ISkeletalErrorMetric() {}
+		virtual ~itransform_error_metric() {}
 
+		//////////////////////////////////////////////////////////////////////////
+		// Returns the unique name of the error metric.
 		virtual const char* get_name() const = 0;
-		virtual uint32_t get_hash() const = 0;
 
-		virtual float calculate_local_bone_error(const RigidSkeleton& skeleton, const rtm::qvvf* raw_local_pose, const rtm::qvvf* base_local_pose, const rtm::qvvf* lossy_local_pose, uint16_t bone_index) const = 0;
-		virtual float calculate_local_bone_error_no_scale(const RigidSkeleton& skeleton, const rtm::qvvf* raw_local_pose, const rtm::qvvf* base_local_pose, const rtm::qvvf* lossy_local_pose, uint16_t bone_index) const = 0;
+		//////////////////////////////////////////////////////////////////////////
+		// Returns a unique hash to represent the error metric.
+		virtual uint32_t get_hash() const { return hash32(get_name()); }
 
-		virtual float calculate_object_bone_error(const RigidSkeleton& skeleton, const rtm::qvvf* raw_local_pose, const rtm::qvvf* base_local_pose, const rtm::qvvf* lossy_local_pose, uint16_t bone_index) const = 0;
-		virtual float calculate_object_bone_error_no_scale(const RigidSkeleton& skeleton, const rtm::qvvf* raw_local_pose, const rtm::qvvf* base_local_pose, const rtm::qvvf* lossy_local_pose, uint16_t bone_index) const = 0;
-	};
+		//////////////////////////////////////////////////////////////////////////
+		// Returns the transform size used by the error metric.
+		virtual size_t get_transform_size(bool has_scale) const = 0;
 
-	// Uses a mix of Transform_32 and AffineMatrix_32 arithmetic.
-	// The local space error is always calculated with Transform_32 arithmetic.
-	// The object space error is calculated with Transform_32 arithmetic if there is no scale
-	// and with AffineMatrix_32 arithmetic if there is scale.
-	// Note that this can cause inaccuracy issues if there are very large or very small
-	// scale values.
-	class TransformMatrixErrorMetric final : public ISkeletalErrorMetric
-	{
-	public:
-		virtual const char* get_name() const override { return "TransformMatrixErrorMetric"; }
-		virtual uint32_t get_hash() const override { return hash32("TransformMatrixErrorMetric"); }
+		//////////////////////////////////////////////////////////////////////////
+		// Returns whether or not the error metric uses a transform that isn't rtm::qvvf.
+		// If this is the case, we need to convert from rtm::qvvf into the transform type
+		// used by the error metric.
+		virtual bool needs_conversion(bool has_scale) const { (void)has_scale; return false; }
 
-		virtual float calculate_local_bone_error(const RigidSkeleton& skeleton, const rtm::qvvf* raw_local_pose, const rtm::qvvf* base_local_pose, const rtm::qvvf* lossy_local_pose, uint16_t bone_index) const override
+		//////////////////////////////////////////////////////////////////////////
+		// Input arguments for the 'convert_transforms*' functions.
+		//////////////////////////////////////////////////////////////////////////
+		struct convert_transforms_args
 		{
-			ACL_ASSERT(bone_index < skeleton.get_num_bones(), "Invalid bone index: %u", bone_index);
-			(void)base_local_pose;
-
-			const RigidBone& bone = skeleton.get_bone(bone_index);
-
-			// Note that because we have scale, we must measure all three axes
-			const rtm::vector4f vtx0 = rtm::vector_set(bone.vertex_distance, 0.0F, 0.0F);
-			const rtm::vector4f vtx1 = rtm::vector_set(0.0F, bone.vertex_distance, 0.0F);
-			const rtm::vector4f vtx2 = rtm::vector_set(0.0F, 0.0F, bone.vertex_distance);
-
-			const rtm::vector4f raw_vtx0 = rtm::qvv_mul_point3(vtx0, raw_local_pose[bone_index]);
-			const rtm::vector4f lossy_vtx0 = rtm::qvv_mul_point3(vtx0, lossy_local_pose[bone_index]);
-			const float vtx0_error = rtm::vector_distance3(raw_vtx0, lossy_vtx0);
-
-			const rtm::vector4f raw_vtx1 = rtm::qvv_mul_point3(vtx1, raw_local_pose[bone_index]);
-			const rtm::vector4f lossy_vtx1 = rtm::qvv_mul_point3(vtx1, lossy_local_pose[bone_index]);
-			const float vtx1_error = rtm::vector_distance3(raw_vtx1, lossy_vtx1);
-
-			const rtm::vector4f raw_vtx2 = rtm::qvv_mul_point3(vtx2, raw_local_pose[bone_index]);
-			const rtm::vector4f lossy_vtx2 = rtm::qvv_mul_point3(vtx2, lossy_local_pose[bone_index]);
-			const float vtx2_error = rtm::vector_distance3(raw_vtx2, lossy_vtx2);
-
-			return rtm::scalar_max(rtm::scalar_max(vtx0_error, vtx1_error), vtx2_error);
+			//////////////////////////////////////////////////////////////////////////
+			// A list of transform indices that are dirty and need conversion.
+			const uint16_t* dirty_transform_indices;
+
+			//////////////////////////////////////////////////////////////////////////
+			// The number of dirty transforms that need conversion.
+			uint32_t num_dirty_transforms;
+
+			//////////////////////////////////////////////////////////////////////////
+			// The input transforms in rtm::qvvf format to be converted.
+			const rtm::qvvf* transforms;
+
+			//////////////////////////////////////////////////////////////////////////
+			// The number of transforms in the input and output buffers.
+			uint32_t num_transforms;
+		};
+
+		//////////////////////////////////////////////////////////////////////////
+		// Converts from rtm::qvvf into the transform type used by the error metric.
+		// Called when 'needs_conversion' returns true.
+		virtual void convert_transforms(const convert_transforms_args& args, void* out_transforms) const
+		{
+			(void)args;
+			(void)out_transforms;
+			ACL_ASSERT(false, "Not implemented");
 		}
 
-		virtual float calculate_local_bone_error_no_scale(const RigidSkeleton& skeleton, const rtm::qvvf* raw_local_pose, const rtm::qvvf* base_local_pose, const rtm::qvvf* lossy_local_pose, uint16_t bone_index) const override
+		//////////////////////////////////////////////////////////////////////////
+		// Converts from rtm::qvvf into the transform type used by the error metric.
+		// Called when 'needs_conversion' returns true.
+		virtual void convert_transforms_no_scale(const convert_transforms_args& args, void* out_transforms) const
 		{
-			ACL_ASSERT(bone_index < skeleton.get_num_bones(), "Invalid bone index: %u", bone_index);
-			(void)base_local_pose;
-
-			const RigidBone& bone = skeleton.get_bone(bone_index);
-
-			const rtm::vector4f vtx0 = rtm::vector_set(bone.vertex_distance, 0.0F, 0.0F);
-			const rtm::vector4f vtx1 = rtm::vector_set(0.0F, bone.vertex_distance, 0.0F);
-
-			const rtm::vector4f raw_vtx0 = rtm::qvv_mul_point3_no_scale(vtx0, raw_local_pose[bone_index]);
-			const rtm::vector4f lossy_vtx0 = rtm::qvv_mul_point3_no_scale(vtx0, lossy_local_pose[bone_index]);
-			const float vtx0_error = rtm::vector_distance3(raw_vtx0, lossy_vtx0);
+			(void)args;
+			(void)out_transforms;
+			ACL_ASSERT(false, "Not implemented");
+		}
 
-			const rtm::vector4f raw_vtx1 = rtm::qvv_mul_point3_no_scale(vtx1, raw_local_pose[bone_index]);
-			const rtm::vector4f lossy_vtx1 = rtm::qvv_mul_point3_no_scale(vtx1, lossy_local_pose[bone_index]);
-			const float vtx1_error = rtm::vector_distance3(raw_vtx1, lossy_vtx1);
+		//////////////////////////////////////////////////////////////////////////
+		// Input arguments for the 'local_to_object_space*' functions.
+		//////////////////////////////////////////////////////////////////////////
+		struct local_to_object_space_args
+		{
+			//////////////////////////////////////////////////////////////////////////
+			// A list of transform indices that are dirty and need transformation.
+			const uint16_t* dirty_transform_indices;
+
+			//////////////////////////////////////////////////////////////////////////
+			// The number of dirty transforms that need transformation.
+			uint32_t num_dirty_transforms;
+
+			//////////////////////////////////////////////////////////////////////////
+			// A list of parent transform indices for every transform.
+			// An index of 0xFFFF represents a root transform with no parent.
+			const uint16_t* parent_transform_indices;
+
+			//////////////////////////////////////////////////////////////////////////
+			// The input transforms in the type expected by the error metric to be transformed.
+			const void* local_transforms;
+
+			//////////////////////////////////////////////////////////////////////////
+			// The number of transforms in the input and output buffers.
+			uint32_t num_transforms;
+		};
+
+		//////////////////////////////////////////////////////////////////////////
+		// Takes local space transforms into object space.
+		virtual void local_to_object_space(const local_to_object_space_args& args, void* out_object_transforms) const
+		{
+			(void)args;
+			(void)out_object_transforms;
+			ACL_ASSERT(false, "Not implemented");
+		}
 
-			return rtm::scalar_max(vtx0_error, vtx1_error);
+		//////////////////////////////////////////////////////////////////////////
+		// Takes local space transforms into object space.
+		virtual void local_to_object_space_no_scale(const local_to_object_space_args& args, void* out_object_transforms) const
+		{
+			(void)args;
+			(void)out_object_transforms;
+			ACL_ASSERT(false, "Not implemented");
 		}
 
-		virtual float calculate_object_bone_error(const RigidSkeleton& skeleton, const rtm::qvvf* raw_local_pose, const rtm::qvvf* base_local_pose, const rtm::qvvf* lossy_local_pose, uint16_t bone_index) const override
+		//////////////////////////////////////////////////////////////////////////
+		// Input arguments for the 'apply_additive_to_base*' functions.
+		//////////////////////////////////////////////////////////////////////////
+		struct apply_additive_to_base_args
+		{
+			//////////////////////////////////////////////////////////////////////////
+			// A list of transform indices that are dirty and need the base applied.
+			const uint16_t* dirty_transform_indices;
+
+			//////////////////////////////////////////////////////////////////////////
+			// The number of dirty transforms that need the base applied.
+			uint32_t num_dirty_transforms;
+
+			//////////////////////////////////////////////////////////////////////////
+			// The input local space transforms in the type expected by the error metric.
+			const void* local_transforms;
+
+			//////////////////////////////////////////////////////////////////////////
+			// The input base transforms in the type expected by the error metric.
+			const void* base_transforms;
+
+			//////////////////////////////////////////////////////////////////////////
+			// The number of transforms in the input and output buffers.
+			uint32_t num_transforms;
+		};
+
+		//////////////////////////////////////////////////////////////////////////
+		// Applies local space transforms on top of base transforms.
+		// This is called when a clip has an additive base.
+		virtual void apply_additive_to_base(const apply_additive_to_base_args& args, void* out_transforms) const
 		{
-			ACL_ASSERT(bone_index < skeleton.get_num_bones(), "Invalid bone index: %u", bone_index);
-			(void)base_local_pose;
+			(void)args;
+			(void)out_transforms;
+			ACL_ASSERT(false, "Not implemented");
+		}
 
-			rtm::matrix3x4f raw_obj_mtx = rtm::matrix_from_qvv(raw_local_pose[0]);
-			rtm::matrix3x4f lossy_obj_mtx = rtm::matrix_from_qvv(lossy_local_pose[0]);
+		//////////////////////////////////////////////////////////////////////////
+		// Applies local space transforms on top of base transforms.
+		// This is called when a clip has an additive base.
+		virtual void apply_additive_to_base_no_scale(const apply_additive_to_base_args& args, void* out_transforms) const
+		{
+			(void)args;
+			(void)out_transforms;
+			ACL_ASSERT(false, "Not implemented");
+		}
 
-			const BoneChain bone_chain = skeleton.get_bone_chain(bone_index);
-			auto chain_bone_it = ++bone_chain.begin();	// Skip root bone
-			const auto chain_bone_end = bone_chain.end();
-			for (; chain_bone_it != chain_bone_end; ++chain_bone_it)
+		//////////////////////////////////////////////////////////////////////////
+		// Input arguments for the 'calculate_error*' functions.
+		//////////////////////////////////////////////////////////////////////////
+		struct calculate_error_args
+		{
+			//////////////////////////////////////////////////////////////////////////
+			// A point on our rigid shell along the X axis.
+			rtm::vector4f shell_point_x;
+
+			//////////////////////////////////////////////////////////////////////////
+			// A point on our rigid shell along the Y axis.
+			rtm::vector4f shell_point_y;
+
+			//////////////////////////////////////////////////////////////////////////
+			// A point on our rigid shell along the Z axis.
+			rtm::vector4f shell_point_z;
+
+			//////////////////////////////////////////////////////////////////////////
+			// The raw reference transform against which we measure the error.
+			// In the type expected by the error metric.
+			// Could be in local or object space (same space as lossy).
+			const void* raw_transform;
+
+			//////////////////////////////////////////////////////////////////////////
+			// The lossy transform we are measuring the error for.
+			// In the type expected by the error metric.
+			// Could be in local or object space (same space as raw).
+			const void* lossy_transform;
+
+			//////////////////////////////////////////////////////////////////////////
+			// We measure the error on a rigid shell around each transform.
+			// This shell takes the form of a sphere at a certain distance.
+			// When no scale is present, measuring any two points is sufficient
+			// but when there is scale, measuring all three is necessary.
+			// See ./docs/error_metrics.md for details.
+			void construct_sphere_shell(float shell_distance)
 			{
-				const uint16_t chain_bone_index = *chain_bone_it;
-				raw_obj_mtx = rtm::matrix_mul(rtm::matrix_from_qvv(raw_local_pose[chain_bone_index]), raw_obj_mtx);
-				lossy_obj_mtx = rtm::matrix_mul(rtm::matrix_from_qvv(lossy_local_pose[chain_bone_index]), lossy_obj_mtx);
+				shell_point_x = rtm::vector_set(shell_distance, 0.0F, 0.0F, 0.0F);
+				shell_point_y = rtm::vector_set(0.0F, shell_distance, 0.0F, 0.0F);
+				shell_point_z = rtm::vector_set(0.0F, 0.0F, shell_distance, 0.0F);
 			}
+		};
 
-			const RigidBone& target_bone = skeleton.get_bone(bone_index);
-
-			// Note that because we have scale, we must measure all three axes
-			const rtm::vector4f vtx0 = rtm::vector_set(target_bone.vertex_distance, 0.0F, 0.0F);
-			const rtm::vector4f vtx1 = rtm::vector_set(0.0F, target_bone.vertex_distance, 0.0F);
-			const rtm::vector4f vtx2 = rtm::vector_set(0.0F, 0.0F, target_bone.vertex_distance);
+		//////////////////////////////////////////////////////////////////////////
+		// Measures the error between a raw and lossy transform.
+		virtual rtm::scalarf RTM_SIMD_CALL calculate_error(const calculate_error_args& args) const = 0;
 
-			const rtm::vector4f raw_vtx0 = rtm::matrix_mul_point3(vtx0, raw_obj_mtx);
-			const rtm::vector4f raw_vtx1 = rtm::matrix_mul_point3(vtx1, raw_obj_mtx);
-			const rtm::vector4f raw_vtx2 = rtm::matrix_mul_point3(vtx2, raw_obj_mtx);
-			const rtm::vector4f lossy_vtx0 = rtm::matrix_mul_point3(vtx0, lossy_obj_mtx);
-			const rtm::vector4f lossy_vtx1 = rtm::matrix_mul_point3(vtx1, lossy_obj_mtx);
-			const rtm::vector4f lossy_vtx2 = rtm::matrix_mul_point3(vtx2, lossy_obj_mtx);
+		//////////////////////////////////////////////////////////////////////////
+		// Measures the error between a raw and lossy transform.
+		virtual rtm::scalarf RTM_SIMD_CALL calculate_error_no_scale(const calculate_error_args& args) const = 0;
+	};
 
-			const float vtx0_error = rtm::vector_distance3(raw_vtx0, lossy_vtx0);
-			const float vtx1_error = rtm::vector_distance3(raw_vtx1, lossy_vtx1);
-			const float vtx2_error = rtm::vector_distance3(raw_vtx2, lossy_vtx2);
+	//////////////////////////////////////////////////////////////////////////
+	// Uses rtm::qvvf arithmetic for local and object space error.
+	// Note that this can cause inaccuracy when dealing with shear/skew.
+	//////////////////////////////////////////////////////////////////////////
+	class qvvf_transform_error_metric : public itransform_error_metric
+	{
+	public:
+		virtual const char* get_name() const override { return "qvvf_transform_error_metric"; }
 
-			return rtm::scalar_max(rtm::scalar_max(vtx0_error, vtx1_error), vtx2_error);
-		}
+		virtual size_t get_transform_size(bool has_scale) const override { (void)has_scale; return sizeof(rtm::qvvf); }
+		virtual bool needs_conversion(bool has_scale) const override { (void)has_scale; return false; }
 
-		virtual float calculate_object_bone_error_no_scale(const RigidSkeleton& skeleton, const rtm::qvvf* raw_local_pose, const rtm::qvvf* base_local_pose, const rtm::qvvf* lossy_local_pose, uint16_t bone_index) const override
+		virtual ACL_DISABLE_SECURITY_COOKIE_CHECK void local_to_object_space(const local_to_object_space_args& args, void* out_object_transforms) const override
 		{
-			ACL_ASSERT(bone_index < skeleton.get_num_bones(), "Invalid bone index: %u", bone_index);
-			(void)base_local_pose;
-
-			rtm::qvvf raw_obj_transform = raw_local_pose[0];
-			rtm::qvvf lossy_obj_transform = lossy_local_pose[0];
+			const uint16_t* dirty_transform_indices = args.dirty_transform_indices;
+			const uint16_t* parent_transform_indices = args.parent_transform_indices;
+			const rtm::qvvf* local_transforms_ = static_cast<const rtm::qvvf*>(args.local_transforms);
+			rtm::qvvf* out_object_transforms_ = static_cast<rtm::qvvf*>(out_object_transforms);
 
-			const BoneChain bone_chain = skeleton.get_bone_chain(bone_index);
-			auto chain_bone_it = ++bone_chain.begin();	// Skip root bone
-			const auto chain_bone_end = bone_chain.end();
-			for (; chain_bone_it != chain_bone_end; ++chain_bone_it)
+			const uint32_t num_dirty_transforms = args.num_dirty_transforms;
+			for (uint32_t dirty_transform_index = 0; dirty_transform_index < num_dirty_transforms; ++dirty_transform_index)
 			{
-				const uint16_t chain_bone_index = *chain_bone_it;
-				raw_obj_transform = rtm::qvv_mul_no_scale(raw_local_pose[chain_bone_index], raw_obj_transform);
-				lossy_obj_transform = rtm::qvv_mul_no_scale(lossy_local_pose[chain_bone_index], lossy_obj_transform);
-			}
+				const uint32_t transform_index = dirty_transform_indices[dirty_transform_index];
+				const uint32_t parent_transform_index = parent_transform_indices[transform_index];
 
-			const RigidBone& target_bone = skeleton.get_bone(bone_index);
+				rtm::qvvf obj_transform;
+				if (parent_transform_index == k_invalid_bone_index)
+					obj_transform = local_transforms_[transform_index];	// Just copy the root as-is, it has no parent and thus local and object space transforms are equal
+				else
+					obj_transform = rtm::qvv_mul(local_transforms_[transform_index], out_object_transforms_[parent_transform_index]);
 
-			const rtm::vector4f vtx0 = rtm::vector_set(target_bone.vertex_distance, 0.0F, 0.0F);
-			const rtm::vector4f vtx1 = rtm::vector_set(0.0F, target_bone.vertex_distance, 0.0F);
+				out_object_transforms_[transform_index] = obj_transform;
+			}
+		}
 
-			const rtm::vector4f raw_vtx0 = rtm::qvv_mul_point3_no_scale(vtx0, raw_obj_transform);
-			const rtm::vector4f raw_vtx1 = rtm::qvv_mul_point3_no_scale(vtx1, raw_obj_transform);
-			const rtm::vector4f lossy_vtx0 = rtm::qvv_mul_point3_no_scale(vtx0, lossy_obj_transform);
-			const rtm::vector4f lossy_vtx1 = rtm::qvv_mul_point3_no_scale(vtx1, lossy_obj_transform);
+		virtual ACL_DISABLE_SECURITY_COOKIE_CHECK void local_to_object_space_no_scale(const local_to_object_space_args& args, void* out_object_transforms) const override
+		{
+			const uint16_t* dirty_transform_indices = args.dirty_transform_indices;
+			const uint16_t* parent_transform_indices = args.parent_transform_indices;
+			const rtm::qvvf* local_transforms_ = static_cast<const rtm::qvvf*>(args.local_transforms);
+			rtm::qvvf* out_object_transforms_ = static_cast<rtm::qvvf*>(out_object_transforms);
 
-			const float vtx0_error = rtm::vector_distance3(raw_vtx0, lossy_vtx0);
-			const float vtx1_error = rtm::vector_distance3(raw_vtx1, lossy_vtx1);
+			const uint32_t num_dirty_transforms = args.num_dirty_transforms;
+			for (uint32_t dirty_transform_index = 0; dirty_transform_index < num_dirty_transforms; ++dirty_transform_index)
+			{
+				const uint32_t transform_index = dirty_transform_indices[dirty_transform_index];
+				const uint32_t parent_transform_index = parent_transform_indices[transform_index];
 
-			return rtm::scalar_max(vtx0_error, vtx1_error);
-		}
-	};
+				rtm::qvvf obj_transform;
+				if (parent_transform_index == k_invalid_bone_index)
+					obj_transform = local_transforms_[transform_index];	// Just copy the root as-is, it has no parent and thus local and object space transforms are equal
+				else
+					obj_transform = rtm::qvv_mul_no_scale(local_transforms_[transform_index], out_object_transforms_[parent_transform_index]);
 
-	// Uses Transform_32 arithmetic for local and object space error.
-	// Note that this can cause inaccuracy when dealing with shear/skew.
-	class TransformErrorMetric final : public ISkeletalErrorMetric
-	{
-	public:
-		virtual const char* get_name() const override { return "TransformErrorMetric"; }
-		virtual uint32_t get_hash() const override { return hash32("TransformErrorMetric"); }
+				out_object_transforms_[transform_index] = obj_transform;
+			}
+		}
 
-		virtual float calculate_local_bone_error(const RigidSkeleton& skeleton, const rtm::qvvf* raw_local_pose, const rtm::qvvf* base_local_pose, const rtm::qvvf* lossy_local_pose, uint16_t bone_index) const override
+		virtual ACL_DISABLE_SECURITY_COOKIE_CHECK rtm::scalarf RTM_SIMD_CALL calculate_error(const calculate_error_args& args) const override
 		{
-			ACL_ASSERT(bone_index < skeleton.get_num_bones(), "Invalid bone index: %u", bone_index);
-			(void)base_local_pose;
-
-			const RigidBone& bone = skeleton.get_bone(bone_index);
+			const rtm::qvvf& raw_transform_ = *static_cast<const rtm::qvvf*>(args.raw_transform);
+			const rtm::qvvf& lossy_transform_ = *static_cast<const rtm::qvvf*>(args.lossy_transform);
 
 			// Note that because we have scale, we must measure all three axes
-			const rtm::vector4f vtx0 = rtm::vector_set(bone.vertex_distance, 0.0F, 0.0F);
-			const rtm::vector4f vtx1 = rtm::vector_set(0.0F, bone.vertex_distance, 0.0F);
-			const rtm::vector4f vtx2 = rtm::vector_set(0.0F, 0.0F, bone.vertex_distance);
+			const rtm::vector4f vtx0 = args.shell_point_x;
+			const rtm::vector4f vtx1 = args.shell_point_y;
+			const rtm::vector4f vtx2 = args.shell_point_z;
 
-			const rtm::vector4f raw_vtx0 = rtm::qvv_mul_point3(vtx0, raw_local_pose[bone_index]);
-			const rtm::vector4f lossy_vtx0 = rtm::qvv_mul_point3(vtx0, lossy_local_pose[bone_index]);
-			const float vtx0_error = rtm::vector_distance3(raw_vtx0, lossy_vtx0);
+			const rtm::vector4f raw_vtx0 = rtm::qvv_mul_point3(vtx0, raw_transform_);
+			const rtm::vector4f raw_vtx1 = rtm::qvv_mul_point3(vtx1, raw_transform_);
+			const rtm::vector4f raw_vtx2 = rtm::qvv_mul_point3(vtx2, raw_transform_);
 
-			const rtm::vector4f raw_vtx1 = rtm::qvv_mul_point3(vtx1, raw_local_pose[bone_index]);
-			const rtm::vector4f lossy_vtx1 = rtm::qvv_mul_point3(vtx1, lossy_local_pose[bone_index]);
-			const float vtx1_error = rtm::vector_distance3(raw_vtx1, lossy_vtx1);
+			const rtm::vector4f lossy_vtx0 = rtm::qvv_mul_point3(vtx0, lossy_transform_);
+			const rtm::vector4f lossy_vtx1 = rtm::qvv_mul_point3(vtx1, lossy_transform_);
+			const rtm::vector4f lossy_vtx2 = rtm::qvv_mul_point3(vtx2, lossy_transform_);
 
-			const rtm::vector4f raw_vtx2 = rtm::qvv_mul_point3(vtx2, raw_local_pose[bone_index]);
-			const rtm::vector4f lossy_vtx2 = rtm::qvv_mul_point3(vtx2, lossy_local_pose[bone_index]);
-			const float vtx2_error = rtm::vector_distance3(raw_vtx2, lossy_vtx2);
+			const rtm::scalarf vtx0_error = rtm::vector_distance3(raw_vtx0, lossy_vtx0);
+			const rtm::scalarf vtx1_error = rtm::vector_distance3(raw_vtx1, lossy_vtx1);
+			const rtm::scalarf vtx2_error = rtm::vector_distance3(raw_vtx2, lossy_vtx2);
 
 			return rtm::scalar_max(rtm::scalar_max(vtx0_error, vtx1_error), vtx2_error);
 		}
 
-		virtual float calculate_local_bone_error_no_scale(const RigidSkeleton& skeleton, const rtm::qvvf* raw_local_pose, const rtm::qvvf* base_local_pose, const rtm::qvvf* lossy_local_pose, uint16_t bone_index) const override
+		virtual ACL_DISABLE_SECURITY_COOKIE_CHECK rtm::scalarf RTM_SIMD_CALL calculate_error_no_scale(const calculate_error_args& args) const override
 		{
-			ACL_ASSERT(bone_index < skeleton.get_num_bones(), "Invalid bone index: %u", bone_index);
-			(void)base_local_pose;
+			const rtm::qvvf& raw_transform_ = *static_cast<const rtm::qvvf*>(args.raw_transform);
+			const rtm::qvvf& lossy_transform_ = *static_cast<const rtm::qvvf*>(args.lossy_transform);
 
-			const RigidBone& bone = skeleton.get_bone(bone_index);
+			const rtm::vector4f vtx0 = args.shell_point_x;
+			const rtm::vector4f vtx1 = args.shell_point_y;
 
-			const rtm::vector4f vtx0 = rtm::vector_set(bone.vertex_distance, 0.0F, 0.0F);
-			const rtm::vector4f vtx1 = rtm::vector_set(0.0F, bone.vertex_distance, 0.0F);
+			const rtm::vector4f raw_vtx0 = rtm::qvv_mul_point3_no_scale(vtx0, raw_transform_);
+			const rtm::vector4f raw_vtx1 = rtm::qvv_mul_point3_no_scale(vtx1, raw_transform_);
 
-			const rtm::vector4f raw_vtx0 = rtm::qvv_mul_point3_no_scale(vtx0, raw_local_pose[bone_index]);
-			const rtm::vector4f lossy_vtx0 = rtm::qvv_mul_point3_no_scale(vtx0, lossy_local_pose[bone_index]);
-			const float vtx0_error = rtm::vector_distance3(raw_vtx0, lossy_vtx0);
+			const rtm::vector4f lossy_vtx0 = rtm::qvv_mul_point3_no_scale(vtx0, lossy_transform_);
+			const rtm::vector4f lossy_vtx1 = rtm::qvv_mul_point3_no_scale(vtx1, lossy_transform_);
 
-			const rtm::vector4f raw_vtx1 = rtm::qvv_mul_point3_no_scale(vtx1, raw_local_pose[bone_index]);
-			const rtm::vector4f lossy_vtx1 = rtm::qvv_mul_point3_no_scale(vtx1, lossy_local_pose[bone_index]);
-			const float vtx1_error = rtm::vector_distance3(raw_vtx1, lossy_vtx1);
+			const rtm::scalarf vtx0_error = rtm::vector_distance3(raw_vtx0, lossy_vtx0);
+			const rtm::scalarf vtx1_error = rtm::vector_distance3(raw_vtx1, lossy_vtx1);
 
 			return rtm::scalar_max(vtx0_error, vtx1_error);
 		}
+	};
 
-		virtual float calculate_object_bone_error(const RigidSkeleton& skeleton, const rtm::qvvf* raw_local_pose, const rtm::qvvf* base_local_pose, const rtm::qvvf* lossy_local_pose, uint16_t bone_index) const override
-		{
-			ACL_ASSERT(bone_index < skeleton.get_num_bones(), "Invalid bone index: %u", bone_index);
-			(void)base_local_pose;
-
-			rtm::qvvf raw_obj_transform = raw_local_pose[0];
-			rtm::qvvf lossy_obj_transform = lossy_local_pose[0];
-
-			const BoneChain bone_chain = skeleton.get_bone_chain(bone_index);
-			auto chain_bone_it = ++bone_chain.begin();	// Skip root bone
-			const auto chain_bone_end = bone_chain.end();
-			for (; chain_bone_it != chain_bone_end; ++chain_bone_it)
-			{
-				const uint16_t chain_bone_index = *chain_bone_it;
-				raw_obj_transform = rtm::qvv_mul(raw_local_pose[chain_bone_index], raw_obj_transform);
-				lossy_obj_transform = rtm::qvv_mul(lossy_local_pose[chain_bone_index], lossy_obj_transform);
-			}
+	//////////////////////////////////////////////////////////////////////////
+	// Uses a mix of rtm::qvvf and rtm::matrix3x4f arithmetic.
+	// The local space error is always calculated with rtm::qvvf arithmetic.
+	// The object space error is calculated with rtm::qvvf arithmetic if there is no scale
+	// and with rtm::matrix3x4f arithmetic if there is scale.
+	// Note that this can cause inaccuracy issues if there are very large or very small
+	// scale values.
+	//////////////////////////////////////////////////////////////////////////
+	class qvvf_matrix3x4f_transform_error_metric : public qvvf_transform_error_metric
+	{
+	public:
+		virtual const char* get_name() const override { return "qvvf_matrix3x4f_transform_error_metric"; }
 
-			const RigidBone& target_bone = skeleton.get_bone(bone_index);
+		virtual size_t get_transform_size(bool has_scale) const override { return has_scale ? sizeof(rtm::matrix3x4f) : sizeof(rtm::qvvf); }
+		virtual bool needs_conversion(bool has_scale) const override { return has_scale; }
 
-			// Note that because we have scale, we must measure all three axes
-			const rtm::vector4f vtx0 = rtm::vector_set(target_bone.vertex_distance, 0.0F, 0.0F);
-			const rtm::vector4f vtx1 = rtm::vector_set(0.0F, target_bone.vertex_distance, 0.0F);
-			const rtm::vector4f vtx2 = rtm::vector_set(0.0F, 0.0F, target_bone.vertex_distance);
+		virtual ACL_DISABLE_SECURITY_COOKIE_CHECK void convert_transforms(const convert_transforms_args& args, void* out_transforms) const override
+		{
+			const uint16_t* dirty_transform_indices = args.dirty_transform_indices;
+			const rtm::qvvf* transforms_ = static_cast<const rtm::qvvf*>(args.transforms);
+			rtm::matrix3x4f* out_transforms_ = static_cast<rtm::matrix3x4f*>(out_transforms);
 
-			const rtm::vector4f raw_vtx0 = rtm::qvv_mul_point3(vtx0, raw_obj_transform);
-			const rtm::vector4f raw_vtx1 = rtm::qvv_mul_point3(vtx1, raw_obj_transform);
-			const rtm::vector4f raw_vtx2 = rtm::qvv_mul_point3(vtx2, raw_obj_transform);
-			const rtm::vector4f lossy_vtx0 = rtm::qvv_mul_point3(vtx0, lossy_obj_transform);
-			const rtm::vector4f lossy_vtx1 = rtm::qvv_mul_point3(vtx1, lossy_obj_transform);
-			const rtm::vector4f lossy_vtx2 = rtm::qvv_mul_point3(vtx2, lossy_obj_transform);
+			const uint32_t num_dirty_transforms = args.num_dirty_transforms;
+			for (uint32_t dirty_transform_index = 0; dirty_transform_index < num_dirty_transforms; ++dirty_transform_index)
+			{
+				const uint32_t transform_index = dirty_transform_indices[dirty_transform_index];
 
-			const float vtx0_error = rtm::vector_distance3(raw_vtx0, lossy_vtx0);
-			const float vtx1_error = rtm::vector_distance3(raw_vtx1, lossy_vtx1);
-			const float vtx2_error = rtm::vector_distance3(raw_vtx2, lossy_vtx2);
+				const rtm::qvvf& transform_qvv = transforms_[transform_index];
+				rtm::matrix3x4f transform_mtx = rtm::matrix_from_qvv(transform_qvv);
 
-			return rtm::scalar_max(rtm::scalar_max(vtx0_error, vtx1_error), vtx2_error);
+				out_transforms_[transform_index] = transform_mtx;
+			}
 		}
 
-		virtual float calculate_object_bone_error_no_scale(const RigidSkeleton& skeleton, const rtm::qvvf* raw_local_pose, const rtm::qvvf* base_local_pose, const rtm::qvvf* lossy_local_pose, uint16_t bone_index) const override
+		virtual ACL_DISABLE_SECURITY_COOKIE_CHECK void local_to_object_space(const local_to_object_space_args& args, void* out_object_transforms) const override
 		{
-			ACL_ASSERT(bone_index < skeleton.get_num_bones(), "Invalid bone index: %u", bone_index);
-			(void)base_local_pose;
+			const uint16_t* dirty_transform_indices = args.dirty_transform_indices;
+			const uint16_t* parent_transform_indices = args.parent_transform_indices;
+			const rtm::matrix3x4f* local_transforms_ = static_cast<const rtm::matrix3x4f*>(args.local_transforms);
+			rtm::matrix3x4f* out_object_transforms_ = static_cast<rtm::matrix3x4f*>(out_object_transforms);
 
-			rtm::qvvf raw_obj_transform = raw_local_pose[0];
-			rtm::qvvf lossy_obj_transform = lossy_local_pose[0];
-
-			const BoneChain bone_chain = skeleton.get_bone_chain(bone_index);
-			auto chain_bone_it = ++bone_chain.begin();	// Skip root bone
-			const auto chain_bone_end = bone_chain.end();
-			for (; chain_bone_it != chain_bone_end; ++chain_bone_it)
+			const uint32_t num_dirty_transforms = args.num_dirty_transforms;
+			for (uint32_t dirty_transform_index = 0; dirty_transform_index < num_dirty_transforms; ++dirty_transform_index)
 			{
-				const uint16_t chain_bone_index = *chain_bone_it;
-				raw_obj_transform = rtm::qvv_mul_no_scale(raw_local_pose[chain_bone_index], raw_obj_transform);
-				lossy_obj_transform = rtm::qvv_mul_no_scale(lossy_local_pose[chain_bone_index], lossy_obj_transform);
+				const uint32_t transform_index = dirty_transform_indices[dirty_transform_index];
+				const uint32_t parent_transform_index = parent_transform_indices[transform_index];
+
+				rtm::matrix3x4f obj_transform;
+				if (parent_transform_index == k_invalid_bone_index)
+					obj_transform = local_transforms_[transform_index];	// Just copy the root as-is, it has no parent and thus local and object space transforms are equal
+				else
+					obj_transform = rtm::matrix_mul(local_transforms_[transform_index], out_object_transforms_[parent_transform_index]);
+
+				out_object_transforms_[transform_index] = obj_transform;
 			}
+		}
 
-			const RigidBone& target_bone = skeleton.get_bone(bone_index);
+		virtual ACL_DISABLE_SECURITY_COOKIE_CHECK rtm::scalarf RTM_SIMD_CALL calculate_error(const calculate_error_args& args) const override
+		{
+			const rtm::matrix3x4f& raw_transform_ = *static_cast<const rtm::matrix3x4f*>(args.raw_transform);
+			const rtm::matrix3x4f& lossy_transform_ = *static_cast<const rtm::matrix3x4f*>(args.lossy_transform);
 
-			const rtm::vector4f vtx0 = rtm::vector_set(target_bone.vertex_distance, 0.0F, 0.0F);
-			const rtm::vector4f vtx1 = rtm::vector_set(0.0F, target_bone.vertex_distance, 0.0F);
+			// Note that because we have scale, we must measure all three axes
+			const rtm::vector4f vtx0 = args.shell_point_x;
+			const rtm::vector4f vtx1 = args.shell_point_y;
+			const rtm::vector4f vtx2 = args.shell_point_z;
 
-			const rtm::vector4f raw_vtx0 = rtm::qvv_mul_point3_no_scale(vtx0, raw_obj_transform);
-			const rtm::vector4f raw_vtx1 = rtm::qvv_mul_point3_no_scale(vtx1, raw_obj_transform);
-			const rtm::vector4f lossy_vtx0 = rtm::qvv_mul_point3_no_scale(vtx0, lossy_obj_transform);
-			const rtm::vector4f lossy_vtx1 = rtm::qvv_mul_point3_no_scale(vtx1, lossy_obj_transform);
+			const rtm::vector4f raw_vtx0 = rtm::matrix_mul_point3(vtx0, raw_transform_);
+			const rtm::vector4f raw_vtx1 = rtm::matrix_mul_point3(vtx1, raw_transform_);
+			const rtm::vector4f raw_vtx2 = rtm::matrix_mul_point3(vtx2, raw_transform_);
 
-			const float vtx0_error = rtm::vector_distance3(raw_vtx0, lossy_vtx0);
-			const float vtx1_error = rtm::vector_distance3(raw_vtx1, lossy_vtx1);
+			const rtm::vector4f lossy_vtx0 = rtm::matrix_mul_point3(vtx0, lossy_transform_);
+			const rtm::vector4f lossy_vtx1 = rtm::matrix_mul_point3(vtx1, lossy_transform_);
+			const rtm::vector4f lossy_vtx2 = rtm::matrix_mul_point3(vtx2, lossy_transform_);
 
-			return rtm::scalar_max(vtx0_error, vtx1_error);
+			const rtm::scalarf vtx0_error = rtm::vector_distance3(raw_vtx0, lossy_vtx0);
+			const rtm::scalarf vtx1_error = rtm::vector_distance3(raw_vtx1, lossy_vtx1);
+			const rtm::scalarf vtx2_error = rtm::vector_distance3(raw_vtx2, lossy_vtx2);
+
+			return rtm::scalar_max(rtm::scalar_max(vtx0_error, vtx1_error), vtx2_error);
 		}
 	};
 
-	// Uses Transform_32 arithmetic for local and object space error.
+	//////////////////////////////////////////////////////////////////////////
+	// Uses rtm::qvvf arithmetic for local and object space error.
 	// This error metric should be used whenever a clip is additive or relative.
 	// Note that this can cause inaccuracy when dealing with shear/skew.
+	//////////////////////////////////////////////////////////////////////////
 	template<AdditiveClipFormat8 additive_format>
-	class AdditiveTransformErrorMetric final : public ISkeletalErrorMetric
+	class additive_qvvf_transform_error_metric : public qvvf_transform_error_metric
 	{
 	public:
 		virtual const char* get_name() const override
@@ -329,138 +457,51 @@ namespace acl
 			switch (additive_format)
 			{
 			default:
-			case AdditiveClipFormat8::None:			return "AdditiveTransformErrorMetric<None>";
-			case AdditiveClipFormat8::Relative:		return "AdditiveTransformErrorMetric<Relative>";
-			case AdditiveClipFormat8::Additive0:	return "AdditiveTransformErrorMetric<Additive0>";
-			case AdditiveClipFormat8::Additive1:	return "AdditiveTransformErrorMetric<Additive1>";
+			case AdditiveClipFormat8::None:			return "additive_qvvf_transform_error_metric<None>";
+			case AdditiveClipFormat8::Relative:		return "additive_qvvf_transform_error_metric<Relative>";
+			case AdditiveClipFormat8::Additive0:	return "additive_qvvf_transform_error_metric<Additive0>";
+			case AdditiveClipFormat8::Additive1:	return "additive_qvvf_transform_error_metric<Additive1>";
 			}
 		}
 
-		virtual uint32_t get_hash() const override { return hash32(get_name()); }
-
-		virtual float calculate_local_bone_error(const RigidSkeleton& skeleton, const rtm::qvvf* raw_local_pose, const rtm::qvvf* base_local_pose, const rtm::qvvf* lossy_local_pose, uint16_t bone_index) const override
+		virtual ACL_DISABLE_SECURITY_COOKIE_CHECK void apply_additive_to_base(const apply_additive_to_base_args& args, void* out_transforms) const override
 		{
-			ACL_ASSERT(bone_index < skeleton.get_num_bones(), "Invalid bone index: %u", bone_index);
-
-			const RigidBone& bone = skeleton.get_bone(bone_index);
-
-			// Note that because we have scale, we must measure all three axes
-			const rtm::vector4f vtx0 = rtm::vector_set(bone.vertex_distance, 0.0F, 0.0F);
-			const rtm::vector4f vtx1 = rtm::vector_set(0.0F, bone.vertex_distance, 0.0F);
-			const rtm::vector4f vtx2 = rtm::vector_set(0.0F, 0.0F, bone.vertex_distance);
-
-			const rtm::qvvf raw_transform = apply_additive_to_base(additive_format, base_local_pose[bone_index], raw_local_pose[bone_index]);
-			const rtm::qvvf lossy_transform = apply_additive_to_base(additive_format, base_local_pose[bone_index], lossy_local_pose[bone_index]);
+			const uint16_t* dirty_transform_indices = args.dirty_transform_indices;
+			const rtm::qvvf* local_transforms_ = static_cast<const rtm::qvvf*>(args.local_transforms);
+			const rtm::qvvf* base_transforms_ = static_cast<const rtm::qvvf*>(args.base_transforms);
+			rtm::qvvf* out_transforms_ = static_cast<rtm::qvvf*>(out_transforms);
 
-			const rtm::vector4f raw_vtx0 = rtm::qvv_mul_point3(vtx0, raw_transform);
-			const rtm::vector4f lossy_vtx0 = rtm::qvv_mul_point3(vtx0, lossy_transform);
-			const float vtx0_error = rtm::vector_distance3(raw_vtx0, lossy_vtx0);
-
-			const rtm::vector4f raw_vtx1 = rtm::qvv_mul_point3(vtx1, raw_transform);
-			const rtm::vector4f lossy_vtx1 = rtm::qvv_mul_point3(vtx1, lossy_transform);
-			const float vtx1_error = rtm::vector_distance3(raw_vtx1, lossy_vtx1);
-
-			const rtm::vector4f raw_vtx2 = rtm::qvv_mul_point3(vtx2, raw_transform);
-			const rtm::vector4f lossy_vtx2 = rtm::qvv_mul_point3(vtx2, lossy_transform);
-			const float vtx2_error = rtm::vector_distance3(raw_vtx2, lossy_vtx2);
-
-			return rtm::scalar_max(rtm::scalar_max(vtx0_error, vtx1_error), vtx2_error);
-		}
-
-		virtual float calculate_local_bone_error_no_scale(const RigidSkeleton& skeleton, const rtm::qvvf* raw_local_pose, const rtm::qvvf* base_local_pose, const rtm::qvvf* lossy_local_pose, uint16_t bone_index) const override
-		{
-			ACL_ASSERT(bone_index < skeleton.get_num_bones(), "Invalid bone index: %u", bone_index);
-
-			const RigidBone& bone = skeleton.get_bone(bone_index);
-
-			const rtm::vector4f vtx0 = rtm::vector_set(bone.vertex_distance, 0.0F, 0.0F);
-			const rtm::vector4f vtx1 = rtm::vector_set(0.0F, bone.vertex_distance, 0.0F);
-
-			const rtm::qvvf raw_transform = apply_additive_to_base_no_scale(additive_format, base_local_pose[bone_index], raw_local_pose[bone_index]);
-			const rtm::qvvf lossy_transform = apply_additive_to_base_no_scale(additive_format, base_local_pose[bone_index], lossy_local_pose[bone_index]);
-
-			const rtm::vector4f raw_vtx0 = rtm::qvv_mul_point3_no_scale(vtx0, raw_transform);
-			const rtm::vector4f lossy_vtx0 = rtm::qvv_mul_point3_no_scale(vtx0, lossy_transform);
-			const float vtx0_error = rtm::vector_distance3(raw_vtx0, lossy_vtx0);
-
-			const rtm::vector4f raw_vtx1 = rtm::qvv_mul_point3_no_scale(vtx1, raw_transform);
-			const rtm::vector4f lossy_vtx1 = rtm::qvv_mul_point3_no_scale(vtx1, lossy_transform);
-			const float vtx1_error = rtm::vector_distance3(raw_vtx1, lossy_vtx1);
-
-			return rtm::scalar_max(vtx0_error, vtx1_error);
-		}
-
-		virtual float calculate_object_bone_error(const RigidSkeleton& skeleton, const rtm::qvvf* raw_local_pose, const rtm::qvvf* base_local_pose, const rtm::qvvf* lossy_local_pose, uint16_t bone_index) const override
-		{
-			ACL_ASSERT(bone_index < skeleton.get_num_bones(), "Invalid bone index: %u", bone_index);
-
-			rtm::qvvf raw_obj_transform = apply_additive_to_base(additive_format, base_local_pose[0], raw_local_pose[0]);
-			rtm::qvvf lossy_obj_transform = apply_additive_to_base(additive_format, base_local_pose[0], lossy_local_pose[0]);
-
-			const BoneChain bone_chain = skeleton.get_bone_chain(bone_index);
-			auto chain_bone_it = ++bone_chain.begin();	// Skip root bone
-			const auto chain_bone_end = bone_chain.end();
-			for (; chain_bone_it != chain_bone_end; ++chain_bone_it)
+			const uint32_t num_dirty_transforms = args.num_dirty_transforms;
+			for (uint32_t dirty_transform_index = 0; dirty_transform_index < num_dirty_transforms; ++dirty_transform_index)
 			{
-				const uint16_t chain_bone_index = *chain_bone_it;
-
-				raw_obj_transform = rtm::qvv_mul(apply_additive_to_base(additive_format, base_local_pose[chain_bone_index], raw_local_pose[chain_bone_index]), raw_obj_transform);
-				lossy_obj_transform = rtm::qvv_mul(apply_additive_to_base(additive_format, base_local_pose[chain_bone_index], lossy_local_pose[chain_bone_index]), lossy_obj_transform);
-			}
+				const uint32_t transform_index = dirty_transform_indices[dirty_transform_index];
 
-			const RigidBone& target_bone = skeleton.get_bone(bone_index);
+				const rtm::qvvf& local_transform = local_transforms_[transform_index];
+				const rtm::qvvf& base_transform = base_transforms_[transform_index];
+				const rtm::qvvf transform = acl::apply_additive_to_base(additive_format, base_transform, local_transform);
 
-			// Note that because we have scale, we must measure all three axes
-			const rtm::vector4f vtx0 = rtm::vector_set(target_bone.vertex_distance, 0.0F, 0.0F);
-			const rtm::vector4f vtx1 = rtm::vector_set(0.0F, target_bone.vertex_distance, 0.0F);
-			const rtm::vector4f vtx2 = rtm::vector_set(0.0F, 0.0F, target_bone.vertex_distance);
-
-			const rtm::vector4f raw_vtx0 = rtm::qvv_mul_point3(vtx0, raw_obj_transform);
-			const rtm::vector4f raw_vtx1 = rtm::qvv_mul_point3(vtx1, raw_obj_transform);
-			const rtm::vector4f raw_vtx2 = rtm::qvv_mul_point3(vtx2, raw_obj_transform);
-			const rtm::vector4f lossy_vtx0 = rtm::qvv_mul_point3(vtx0, lossy_obj_transform);
-			const rtm::vector4f lossy_vtx1 = rtm::qvv_mul_point3(vtx1, lossy_obj_transform);
-			const rtm::vector4f lossy_vtx2 = rtm::qvv_mul_point3(vtx2, lossy_obj_transform);
-
-			const float vtx0_error = rtm::vector_distance3(raw_vtx0, lossy_vtx0);
-			const float vtx1_error = rtm::vector_distance3(raw_vtx1, lossy_vtx1);
-			const float vtx2_error = rtm::vector_distance3(raw_vtx2, lossy_vtx2);
-
-			return rtm::scalar_max(rtm::scalar_max(vtx0_error, vtx1_error), vtx2_error);
+				out_transforms_[transform_index] = transform;
+			}
 		}
 
-		virtual float calculate_object_bone_error_no_scale(const RigidSkeleton& skeleton, const rtm::qvvf* raw_local_pose, const rtm::qvvf* base_local_pose, const rtm::qvvf* lossy_local_pose, uint16_t bone_index) const override
+		virtual ACL_DISABLE_SECURITY_COOKIE_CHECK void apply_additive_to_base_no_scale(const apply_additive_to_base_args& args, void* out_transforms) const override
 		{
-			ACL_ASSERT(bone_index < skeleton.get_num_bones(), "Invalid bone index: %u", bone_index);
-
-			rtm::qvvf raw_obj_transform = apply_additive_to_base_no_scale(additive_format, base_local_pose[0], raw_local_pose[0]);
-			rtm::qvvf lossy_obj_transform = apply_additive_to_base_no_scale(additive_format, base_local_pose[0], lossy_local_pose[0]);
+			const uint16_t* dirty_transform_indices = args.dirty_transform_indices;
+			const rtm::qvvf* local_transforms_ = static_cast<const rtm::qvvf*>(args.local_transforms);
+			const rtm::qvvf* base_transforms_ = static_cast<const rtm::qvvf*>(args.base_transforms);
+			rtm::qvvf* out_transforms_ = static_cast<rtm::qvvf*>(out_transforms);
 
-			const BoneChain bone_chain = skeleton.get_bone_chain(bone_index);
-			auto chain_bone_it = ++bone_chain.begin();	// Skip root bone
-			const auto chain_bone_end = bone_chain.end();
-			for (; chain_bone_it != chain_bone_end; ++chain_bone_it)
+			const uint32_t num_dirty_transforms = args.num_dirty_transforms;
+			for (uint32_t dirty_transform_index = 0; dirty_transform_index < num_dirty_transforms; ++dirty_transform_index)
 			{
-				const uint16_t chain_bone_index = *chain_bone_it;
+				const uint32_t transform_index = dirty_transform_indices[dirty_transform_index];
 
-				raw_obj_transform = rtm::qvv_mul_no_scale(apply_additive_to_base_no_scale(additive_format, base_local_pose[chain_bone_index], raw_local_pose[chain_bone_index]), raw_obj_transform);
-				lossy_obj_transform = rtm::qvv_mul_no_scale(apply_additive_to_base_no_scale(additive_format, base_local_pose[chain_bone_index], lossy_local_pose[chain_bone_index]), lossy_obj_transform);
-			}
-
-			const RigidBone& target_bone = skeleton.get_bone(bone_index);
-
-			const rtm::vector4f vtx0 = rtm::vector_set(target_bone.vertex_distance, 0.0F, 0.0F);
-			const rtm::vector4f vtx1 = rtm::vector_set(0.0F, target_bone.vertex_distance, 0.0F);
-
-			const rtm::vector4f raw_vtx0 = rtm::qvv_mul_point3_no_scale(vtx0, raw_obj_transform);
-			const rtm::vector4f raw_vtx1 = rtm::qvv_mul_point3_no_scale(vtx1, raw_obj_transform);
-			const rtm::vector4f lossy_vtx0 = rtm::qvv_mul_point3_no_scale(vtx0, lossy_obj_transform);
-			const rtm::vector4f lossy_vtx1 = rtm::qvv_mul_point3_no_scale(vtx1, lossy_obj_transform);
+				const rtm::qvvf& local_transform = local_transforms_[transform_index];
+				const rtm::qvvf& base_transform = base_transforms_[transform_index];
+				const rtm::qvvf transform = acl::apply_additive_to_base_no_scale(additive_format, base_transform, local_transform);
 
-			const float vtx0_error = rtm::vector_distance3(raw_vtx0, lossy_vtx0);
-			const float vtx1_error = rtm::vector_distance3(raw_vtx1, lossy_vtx1);
-
-			return rtm::scalar_max(vtx0_error, vtx1_error);
+				out_transforms_[transform_index] = transform;
+			}
 		}
 	};
 }
diff --git a/includes/acl/compression/utils.h b/includes/acl/compression/utils.h
--- a/includes/acl/compression/utils.h
+++ b/includes/acl/compression/utils.h
@@ -50,7 +50,7 @@ namespace acl
 
 	template<class DecompressionContextType>
 	inline BoneError calculate_compressed_clip_error(IAllocator& allocator,
-		const AnimationClip& clip, const ISkeletalErrorMetric& error_metric, DecompressionContextType& context)
+		const AnimationClip& clip, const itransform_error_metric& error_metric, DecompressionContextType& context)
 	{
 		const uint16_t num_bones = clip.get_num_bones();
 		const float clip_duration = clip.get_duration();
@@ -58,6 +58,9 @@ namespace acl
 		const uint32_t num_samples = clip.get_num_samples();
 		const RigidSkeleton& skeleton = clip.get_skeleton();
 
+		// Always calculate the error with scale, slower but we don't need to know if we have scale or not
+		const bool has_scale = true;
+
 		uint16_t num_output_bones = 0;
 		uint16_t* output_bone_mapping = create_output_bone_mapping(allocator, clip, num_output_bones);
 
@@ -65,44 +68,127 @@ namespace acl
 		const uint32_t additive_num_samples = additive_base_clip != nullptr ? additive_base_clip->get_num_samples() : 0;
 		const float additive_duration = additive_base_clip != nullptr ? additive_base_clip->get_duration() : 0.0F;
 
-		rtm::qvvf* raw_pose_transforms = allocate_type_array<rtm::qvvf>(allocator, num_bones);
-		rtm::qvvf* base_pose_transforms = allocate_type_array<rtm::qvvf>(allocator, num_bones);
-		rtm::qvvf* lossy_pose_transforms = allocate_type_array<rtm::qvvf>(allocator, num_output_bones);
-		rtm::qvvf* lossy_remapped_pose_transforms = allocate_type_array<rtm::qvvf>(allocator, num_bones);
+		rtm::qvvf* raw_local_pose = allocate_type_array<rtm::qvvf>(allocator, num_bones);
+		rtm::qvvf* base_local_pose = additive_base_clip != nullptr ? allocate_type_array<rtm::qvvf>(allocator, num_bones) : nullptr;
+		rtm::qvvf* lossy_local_pose = allocate_type_array<rtm::qvvf>(allocator, num_output_bones);
+		rtm::qvvf* lossy_remapped_local_pose = allocate_type_array<rtm::qvvf>(allocator, num_bones);
+
+		const size_t transform_size = error_metric.get_transform_size(has_scale);
+		const bool needs_conversion = error_metric.needs_conversion(has_scale);
+		uint8_t* raw_local_pose_converted = nullptr;
+		uint8_t* base_local_pose_converted = nullptr;
+		uint8_t* lossy_local_pose_converted = nullptr;
+		if (needs_conversion)
+		{
+			raw_local_pose_converted = allocate_type_array_aligned<uint8_t>(allocator, num_bones * transform_size, 64);
+			base_local_pose_converted = additive_base_clip != nullptr ? allocate_type_array_aligned<uint8_t>(allocator, num_bones * transform_size, 64) : nullptr;
+			lossy_local_pose_converted = allocate_type_array_aligned<uint8_t>(allocator, num_bones * transform_size, 64);
+		}
+
+		uint8_t* raw_object_pose = allocate_type_array_aligned<uint8_t>(allocator, num_bones * transform_size, 64);
+		uint8_t* lossy_object_pose = allocate_type_array_aligned<uint8_t>(allocator, num_bones * transform_size, 64);
+
+		uint16_t* parent_transform_indices = allocate_type_array<uint16_t>(allocator, num_bones);
+		uint16_t* self_transform_indices = allocate_type_array<uint16_t>(allocator, num_bones);
+
+		for (uint16_t transform_index = 0; transform_index < num_bones; ++transform_index)
+		{
+			const RigidBone& bone = skeleton.get_bone(transform_index);
+			parent_transform_indices[transform_index] = bone.parent_index;
+			self_transform_indices[transform_index] = transform_index;
+		}
+
+		void* raw_local_pose_ = needs_conversion ? (void*)raw_local_pose_converted : (void*)raw_local_pose;
+		void* base_local_pose_ = needs_conversion ? (void*)base_local_pose_converted : (void*)base_local_pose;
+		void* lossy_local_pose_ = needs_conversion ? (void*)lossy_local_pose_converted : (void*)lossy_remapped_local_pose;
+
+		itransform_error_metric::convert_transforms_args convert_transforms_args_raw;
+		convert_transforms_args_raw.dirty_transform_indices = self_transform_indices;
+		convert_transforms_args_raw.num_dirty_transforms = num_bones;
+		convert_transforms_args_raw.transforms = raw_local_pose;
+		convert_transforms_args_raw.num_transforms = num_bones;
+
+		itransform_error_metric::convert_transforms_args convert_transforms_args_base = convert_transforms_args_raw;
+		convert_transforms_args_base.transforms = base_local_pose;
+
+		itransform_error_metric::convert_transforms_args convert_transforms_args_lossy = convert_transforms_args_raw;
+		convert_transforms_args_lossy.transforms = lossy_remapped_local_pose;
+
+		itransform_error_metric::apply_additive_to_base_args apply_additive_to_base_args_raw;
+		apply_additive_to_base_args_raw.dirty_transform_indices = self_transform_indices;
+		apply_additive_to_base_args_raw.num_dirty_transforms = num_bones;
+		apply_additive_to_base_args_raw.local_transforms = raw_local_pose_;
+		apply_additive_to_base_args_raw.base_transforms = base_local_pose_;
+		apply_additive_to_base_args_raw.num_transforms = num_bones;
+
+		itransform_error_metric::apply_additive_to_base_args apply_additive_to_base_args_lossy = apply_additive_to_base_args_raw;
+		apply_additive_to_base_args_lossy.local_transforms = lossy_local_pose_;
+
+		itransform_error_metric::local_to_object_space_args local_to_object_space_args_raw;
+		local_to_object_space_args_raw.dirty_transform_indices = self_transform_indices;
+		local_to_object_space_args_raw.num_dirty_transforms = num_bones;
+		local_to_object_space_args_raw.parent_transform_indices = parent_transform_indices;
+		local_to_object_space_args_raw.local_transforms = raw_local_pose_;
+		local_to_object_space_args_raw.num_transforms = num_bones;
+
+		itransform_error_metric::local_to_object_space_args local_to_object_space_args_lossy = local_to_object_space_args_raw;
+		local_to_object_space_args_lossy.local_transforms = lossy_local_pose_;
 
 		BoneError bone_error;
-		DefaultOutputWriter pose_writer(lossy_pose_transforms, num_output_bones);
+		DefaultOutputWriter pose_writer(lossy_local_pose, num_output_bones);
 
 		for (uint32_t sample_index = 0; sample_index < num_samples; ++sample_index)
 		{
 			const float sample_time = rtm::scalar_min(float(sample_index) / sample_rate, clip_duration);
 
 			// We use the nearest sample to accurately measure the loss that happened, if any
-			clip.sample_pose(sample_time, SampleRoundingPolicy::Nearest, raw_pose_transforms, num_bones);
+			clip.sample_pose(sample_time, SampleRoundingPolicy::Nearest, raw_local_pose, num_bones);
 
 			context.seek(sample_time, SampleRoundingPolicy::Nearest);
 			context.decompress_pose(pose_writer);
 
-			if (additive_base_clip != nullptr)
-			{
-				const float normalized_sample_time = additive_num_samples > 1 ? (sample_time / clip_duration) : 0.0F;
-				const float additive_sample_time = normalized_sample_time * additive_duration;
-				additive_base_clip->sample_pose(additive_sample_time, SampleRoundingPolicy::Nearest, base_pose_transforms, num_bones);
-			}
-
 			// Perform remapping by copying the raw pose first and we overwrite with the decompressed pose if
 			// the data is available
-			std::memcpy(lossy_remapped_pose_transforms, raw_pose_transforms, sizeof(rtm::qvvf) * num_bones);
+			std::memcpy(lossy_remapped_local_pose, raw_local_pose, sizeof(rtm::qvvf) * num_bones);
 			for (uint16_t output_index = 0; output_index < num_output_bones; ++output_index)
 			{
 				const uint16_t bone_index = output_bone_mapping[output_index];
-				lossy_remapped_pose_transforms[bone_index] = lossy_pose_transforms[output_index];
+				lossy_remapped_local_pose[bone_index] = lossy_local_pose[output_index];
 			}
 
+			if (needs_conversion)
+			{
+				error_metric.convert_transforms(convert_transforms_args_raw, raw_local_pose_converted);
+				error_metric.convert_transforms(convert_transforms_args_lossy, lossy_local_pose_converted);
+			}
+
+			if (additive_base_clip != nullptr)
+			{
+				const float normalized_sample_time = additive_num_samples > 1 ? (sample_time / clip_duration) : 0.0F;
+				const float additive_sample_time = additive_num_samples > 1 ? (normalized_sample_time * additive_duration) : 0.0F;
+				additive_base_clip->sample_pose(additive_sample_time, SampleRoundingPolicy::Nearest, base_local_pose, num_bones);
+
+				if (needs_conversion)
+					error_metric.convert_transforms(convert_transforms_args_base, base_local_pose_converted);
+
+				
+				error_metric.apply_additive_to_base(apply_additive_to_base_args_raw, raw_local_pose_);
+				error_metric.apply_additive_to_base(apply_additive_to_base_args_lossy, lossy_local_pose_);
+			}
+
+			error_metric.local_to_object_space(local_to_object_space_args_raw, raw_object_pose);
+			error_metric.local_to_object_space(local_to_object_space_args_lossy, lossy_object_pose);
+
 			for (uint16_t bone_index = 0; bone_index < num_bones; ++bone_index)
 			{
-				// Always calculate the error with scale, slower but binary exact
-				const float error = error_metric.calculate_object_bone_error(skeleton, raw_pose_transforms, base_pose_transforms, lossy_remapped_pose_transforms, bone_index);
+				const RigidBone& bone = skeleton.get_bone(bone_index);
+
+				itransform_error_metric::calculate_error_args calculate_error_args;
+				calculate_error_args.raw_transform = raw_object_pose + (bone_index * transform_size);
+				calculate_error_args.lossy_transform = lossy_object_pose + (bone_index * transform_size);
+				calculate_error_args.construct_sphere_shell(bone.vertex_distance);
+
+				const float error = rtm::scalar_cast(error_metric.calculate_error(calculate_error_args));
 
 				if (error > bone_error.error)
 				{
@@ -114,10 +200,17 @@ namespace acl
 		}
 
 		deallocate_type_array(allocator, output_bone_mapping, num_output_bones);
-		deallocate_type_array(allocator, raw_pose_transforms, num_bones);
-		deallocate_type_array(allocator, base_pose_transforms, num_bones);
-		deallocate_type_array(allocator, lossy_pose_transforms, num_output_bones);
-		deallocate_type_array(allocator, lossy_remapped_pose_transforms, num_bones);
+		deallocate_type_array(allocator, raw_local_pose, num_bones);
+		deallocate_type_array(allocator, base_local_pose, num_bones);
+		deallocate_type_array(allocator, lossy_local_pose, num_output_bones);
+		deallocate_type_array(allocator, lossy_remapped_local_pose, num_bones);
+		deallocate_type_array(allocator, raw_local_pose_converted, num_bones * transform_size);
+		deallocate_type_array(allocator, base_local_pose_converted, num_bones * transform_size);
+		deallocate_type_array(allocator, lossy_local_pose_converted, num_bones * transform_size);
+		deallocate_type_array(allocator, raw_object_pose, num_bones * transform_size);
+		deallocate_type_array(allocator, lossy_object_pose, num_bones * transform_size);
+		deallocate_type_array(allocator, parent_transform_indices, num_bones);
+		deallocate_type_array(allocator, self_transform_indices, num_bones);
 
 		return bone_error;
 	}
diff --git a/includes/acl/core/bitset.h b/includes/acl/core/bitset.h
--- a/includes/acl/core/bitset.h
+++ b/includes/acl/core/bitset.h
@@ -198,6 +198,18 @@ namespace acl
 
 		return num_set_bits;
 	}
+
+	//////////////////////////////////////////////////////////////////////////
+	// Performs the operation: result = ~not_value & and_value
+	// Bit sets must have the same description
+	// Bit sets can alias
+	inline void bitset_and_not(uint32_t* bitset_result, const uint32_t* bitset_not_value, const uint32_t* bitset_and_value, BitSetDescription desc)
+	{
+		const uint32_t size = desc.get_size();
+
+		for (uint32_t offset = 0; offset < size; ++offset)
+			bitset_result[offset] = and_not(bitset_not_value[offset], bitset_and_value[offset]);
+	}
 }
 
 ACL_IMPL_FILE_PRAGMA_POP
diff --git a/includes/acl/core/impl/compiler_utils.h b/includes/acl/core/impl/compiler_utils.h
--- a/includes/acl/core/impl/compiler_utils.h
+++ b/includes/acl/core/impl/compiler_utils.h
@@ -50,6 +50,16 @@
 	#define ACL_IMPL_FILE_PRAGMA_POP
 #endif
 
+//////////////////////////////////////////////////////////////////////////
+// In some cases, for performance reasons, we wish to disable stack security
+// check cookies. This macro serves this purpose.
+//////////////////////////////////////////////////////////////////////////
+#if defined(_MSC_VER)
+	#define ACL_DISABLE_SECURITY_COOKIE_CHECK __declspec(safebuffers)
+#else
+	#define ACL_DISABLE_SECURITY_COOKIE_CHECK
+#endif
+
 //////////////////////////////////////////////////////////////////////////
 // Force inline macros for when it is necessary.
 //////////////////////////////////////////////////////////////////////////
diff --git a/includes/acl/core/track_types.h b/includes/acl/core/track_types.h
--- a/includes/acl/core/track_types.h
+++ b/includes/acl/core/track_types.h
@@ -222,15 +222,15 @@ namespace acl
 	constexpr uint8_t k_invalid_bit_rate = 0xFF;
 	constexpr uint8_t k_lowest_bit_rate = 1;
 	constexpr uint8_t k_highest_bit_rate = sizeof(k_bit_rate_num_bits) - 1;
-	constexpr uint8_t k_num_bit_rates = sizeof(k_bit_rate_num_bits);
+	constexpr uint32_t k_num_bit_rates = sizeof(k_bit_rate_num_bits);
 
 	static_assert(k_num_bit_rates == 19, "Expecting 19 bit rates");
 
 	// If all tracks are variable, no need for any extra padding except at the very end of the data
 	// If our tracks are mixed variable/not variable, we need to add some padding to ensure alignment
-	constexpr uint8_t k_mixed_packing_alignment_num_bits = 16;
+	constexpr uint32_t k_mixed_packing_alignment_num_bits = 16;
 
-	inline uint8_t get_num_bits_at_bit_rate(uint8_t bit_rate)
+	inline uint32_t get_num_bits_at_bit_rate(uint8_t bit_rate)
 	{
 		ACL_ASSERT(bit_rate <= k_highest_bit_rate, "Invalid bit rate: %u", bit_rate);
 		return k_bit_rate_num_bits[bit_rate];
diff --git a/includes/acl/decompression/decompress.h b/includes/acl/decompression/decompress.h
--- a/includes/acl/decompression/decompress.h
+++ b/includes/acl/decompression/decompress.h
@@ -285,7 +285,7 @@ namespace acl
 		{
 			const acl_impl::track_metadata& metadata = per_track_metadata[track_index];
 			const uint8_t bit_rate = metadata.bit_rate;
-			const uint8_t num_bits_per_component = get_num_bits_at_bit_rate(bit_rate);
+			const uint32_t num_bits_per_component = get_num_bits_at_bit_rate(bit_rate);
 
 			if (header.track_type == track_type8::float1f && m_settings.is_track_type_supported(track_type8::float1f))
 			{
@@ -301,13 +301,13 @@ namespace acl
 					rtm::scalarf value1;
 					if (is_raw_bit_rate(bit_rate))
 					{
-						value0 = acl_impl::unpack_scalarf_96_unsafe(animated_values, track_bit_offset0);
-						value1 = acl_impl::unpack_scalarf_96_unsafe(animated_values, track_bit_offset1);
+						value0 = unpack_scalarf_32_unsafe(animated_values, track_bit_offset0);
+						value1 = unpack_scalarf_32_unsafe(animated_values, track_bit_offset1);
 					}
 					else
 					{
-						value0 = acl_impl::unpack_scalarf_uXX_unsafe(num_bits_per_component, animated_values, track_bit_offset0);
-						value1 = acl_impl::unpack_scalarf_uXX_unsafe(num_bits_per_component, animated_values, track_bit_offset1);
+						value0 = unpack_scalarf_uXX_unsafe(num_bits_per_component, animated_values, track_bit_offset0);
+						value1 = unpack_scalarf_uXX_unsafe(num_bits_per_component, animated_values, track_bit_offset1);
 
 						const rtm::scalarf range_min = rtm::scalar_load(range_values);
 						const rtm::scalarf range_extent = rtm::scalar_load(range_values + 1);
@@ -521,7 +521,7 @@ namespace acl
 
 		const acl_impl::track_metadata& metadata = per_track_metadata[track_index];
 		const uint8_t bit_rate = metadata.bit_rate;
-		const uint8_t num_bits_per_component = get_num_bits_at_bit_rate(bit_rate);
+		const uint32_t num_bits_per_component = get_num_bits_at_bit_rate(bit_rate);
 
 		const uint8_t* animated_values = header.get_track_animated_values();
 
@@ -536,13 +536,13 @@ namespace acl
 				rtm::scalarf value1;
 				if (is_raw_bit_rate(bit_rate))
 				{
-					value0 = acl_impl::unpack_scalarf_96_unsafe(animated_values, m_context.key_frame_bit_offsets[0] + track_bit_offset);
-					value1 = acl_impl::unpack_scalarf_96_unsafe(animated_values, m_context.key_frame_bit_offsets[1] + track_bit_offset);
+					value0 = unpack_scalarf_32_unsafe(animated_values, m_context.key_frame_bit_offsets[0] + track_bit_offset);
+					value1 = unpack_scalarf_32_unsafe(animated_values, m_context.key_frame_bit_offsets[1] + track_bit_offset);
 				}
 				else
 				{
-					value0 = acl_impl::unpack_scalarf_uXX_unsafe(num_bits_per_component, animated_values, m_context.key_frame_bit_offsets[0] + track_bit_offset);
-					value1 = acl_impl::unpack_scalarf_uXX_unsafe(num_bits_per_component, animated_values, m_context.key_frame_bit_offsets[1] + track_bit_offset);
+					value0 = unpack_scalarf_uXX_unsafe(num_bits_per_component, animated_values, m_context.key_frame_bit_offsets[0] + track_bit_offset);
+					value1 = unpack_scalarf_uXX_unsafe(num_bits_per_component, animated_values, m_context.key_frame_bit_offsets[1] + track_bit_offset);
 
 					const rtm::scalarf range_min = rtm::scalar_load(range_values);
 					const rtm::scalarf range_extent = rtm::scalar_load(range_values + num_element_components);
diff --git a/includes/acl/decompression/impl/decompress_data.h b/includes/acl/decompression/impl/decompress_data.h
--- a/includes/acl/decompression/impl/decompress_data.h
+++ b/includes/acl/decompression/impl/decompress_data.h
@@ -61,7 +61,7 @@ namespace acl
 						for (size_t i = 0; i < num_key_frames; ++i)
 						{
 							const uint8_t bit_rate = decomp_context.format_per_track_data[i][sampling_context.format_per_track_data_offset];
-							uint8_t num_bits_at_bit_rate = get_num_bits_at_bit_rate(bit_rate) * 3;	// 3 components
+							uint32_t num_bits_at_bit_rate = get_num_bits_at_bit_rate(bit_rate) * 3;	// 3 components
 
 							if (settings.supports_mixed_packing() && decomp_context.has_mixed_packing)
 								num_bits_at_bit_rate = align_to(num_bits_at_bit_rate, k_mixed_packing_alignment_num_bits);
@@ -123,7 +123,7 @@ namespace acl
 						for (size_t i = 0; i < num_key_frames; ++i)
 						{
 							const uint8_t bit_rate = decomp_context.format_per_track_data[i][sampling_context.format_per_track_data_offset];
-							uint8_t num_bits_at_bit_rate = get_num_bits_at_bit_rate(bit_rate) * 3;	// 3 components
+							uint32_t num_bits_at_bit_rate = get_num_bits_at_bit_rate(bit_rate) * 3;	// 3 components
 
 							if (settings.supports_mixed_packing() && decomp_context.has_mixed_packing)
 								num_bits_at_bit_rate = align_to(num_bits_at_bit_rate, k_mixed_packing_alignment_num_bits);
diff --git a/includes/acl/decompression/impl/track_sampling_impl.h b/includes/acl/decompression/impl/track_sampling_impl.h
--- a/includes/acl/decompression/impl/track_sampling_impl.h
+++ b/includes/acl/decompression/impl/track_sampling_impl.h
@@ -59,113 +59,6 @@ namespace acl
 
 			inline bool is_initialized() const { return tracks != nullptr; }
 		};
-
-		// Assumes the 'vector_data' is in big-endian order and is padded in order to load up to 16 bytes from it
-		inline rtm::scalarf RTM_SIMD_CALL unpack_scalarf_96_unsafe(const uint8_t* vector_data, uint32_t bit_offset)
-		{
-#if defined(RTM_SSE2_INTRINSICS)
-			const uint32_t byte_offset = bit_offset / 8;
-			const uint32_t shift_offset = bit_offset % 8;
-			uint64_t vector_u64 = unaligned_load<uint64_t>(vector_data + byte_offset + 0);
-			vector_u64 = byte_swap(vector_u64);
-			vector_u64 <<= shift_offset;
-			vector_u64 >>= 32;
-
-			const uint32_t x32 = uint32_t(vector_u64);
-
-			return _mm_castsi128_ps(_mm_set1_epi32(x32));
-#elif defined(RTM_NEON_INTRINSICS)
-			const uint32_t byte_offset = bit_offset / 8;
-			const uint32_t shift_offset = bit_offset % 8;
-			uint64_t vector_u64 = unaligned_load<uint64_t>(vector_data + byte_offset + 0);
-			vector_u64 = byte_swap(vector_u64);
-			vector_u64 <<= shift_offset;
-			vector_u64 >>= 32;
-
-			const uint64_t x64 = vector_u64;
-
-			const uint32x2_t xy = vcreate_u32(x64);
-			return vget_lane_f32(vreinterpret_f32_u32(xy), 0);
-#else
-			const uint32_t byte_offset = bit_offset / 8;
-			const uint32_t shift_offset = bit_offset % 8;
-			uint64_t vector_u64 = unaligned_load<uint64_t>(vector_data + byte_offset + 0);
-			vector_u64 = byte_swap(vector_u64);
-			vector_u64 <<= shift_offset;
-			vector_u64 >>= 32;
-
-			const uint64_t x64 = vector_u64;
-
-			const float x = aligned_load<float>(&x64);
-
-			return rtm::scalar_set(x);
-#endif
-		}
-
-		// Assumes the 'vector_data' is in big-endian order and padded in order to load up to 16 bytes from it
-		inline rtm::scalarf RTM_SIMD_CALL unpack_scalarf_uXX_unsafe(uint8_t num_bits, const uint8_t* vector_data, uint32_t bit_offset)
-		{
-			ACL_ASSERT(num_bits <= 19, "This function does not support reading more than 19 bits per component");
-
-			struct PackedTableEntry
-			{
-				explicit constexpr PackedTableEntry(uint8_t num_bits_)
-					: max_value(num_bits_ == 0 ? 1.0F : (1.0F / float((1 << num_bits_) - 1)))
-					, mask((1 << num_bits_) - 1)
-				{}
-
-				float max_value;
-				uint32_t mask;
-			};
-
-			// TODO: We technically don't need the first 3 entries, which could save a few bytes
-			alignas(64) static constexpr PackedTableEntry k_packed_constants[20] =
-			{
-				PackedTableEntry(0), PackedTableEntry(1), PackedTableEntry(2), PackedTableEntry(3),
-				PackedTableEntry(4), PackedTableEntry(5), PackedTableEntry(6), PackedTableEntry(7),
-				PackedTableEntry(8), PackedTableEntry(9), PackedTableEntry(10), PackedTableEntry(11),
-				PackedTableEntry(12), PackedTableEntry(13), PackedTableEntry(14), PackedTableEntry(15),
-				PackedTableEntry(16), PackedTableEntry(17), PackedTableEntry(18), PackedTableEntry(19),
-			};
-
-#if defined(RTM_SSE2_INTRINSICS)
-			const uint32_t bit_shift = 32 - num_bits;
-			const uint32_t mask = k_packed_constants[num_bits].mask;
-			const __m128 inv_max_value = _mm_load_ps1(&k_packed_constants[num_bits].max_value);
-
-			uint32_t byte_offset = bit_offset / 8;
-			uint32_t vector_u32 = unaligned_load<uint32_t>(vector_data + byte_offset);
-			vector_u32 = byte_swap(vector_u32);
-			const uint32_t x32 = (vector_u32 >> (bit_shift - (bit_offset % 8)));
-
-			const __m128 value = _mm_cvtsi32_ss(inv_max_value, x32 & mask);
-			return _mm_mul_ss(value, inv_max_value);
-#elif defined(RTM_NEON_INTRINSICS)
-			const uint32_t bit_shift = 32 - num_bits;
-			const uint32_t mask = k_packed_constants[num_bits].mask;
-			const float inv_max_value = k_packed_constants[num_bits].max_value;
-
-			uint32_t byte_offset = bit_offset / 8;
-			uint32_t vector_u32 = unaligned_load<uint32_t>(vector_data + byte_offset);
-			vector_u32 = byte_swap(vector_u32);
-			const uint32_t x32 = (vector_u32 >> (bit_shift - (bit_offset % 8)));
-
-			const int32_t value_u32 = x32 & mask;
-			const float value_f32 = static_cast<float>(value_u32);
-			return value_f32 * inv_max_value;
-#else
-			const uint32_t bit_shift = 32 - num_bits;
-			const uint32_t mask = k_packed_constants[num_bits].mask;
-			const float inv_max_value = k_packed_constants[num_bits].max_value;
-
-			uint32_t byte_offset = bit_offset / 8;
-			uint32_t vector_u32 = unaligned_load<uint32_t>(vector_data + byte_offset);
-			vector_u32 = byte_swap(vector_u32);
-			const uint32_t x32 = (vector_u32 >> (bit_shift - (bit_offset % 8))) & mask;
-
-			return rtm::scalar_set(static_cast<float>(x32) * inv_max_value);
-#endif
-		}
 	}
 }
 
diff --git a/includes/acl/math/scalar_packing.h b/includes/acl/math/scalar_packing.h
--- a/includes/acl/math/scalar_packing.h
+++ b/includes/acl/math/scalar_packing.h
@@ -26,6 +26,7 @@
 
 #include "acl/core/impl/compiler_utils.h"
 #include "acl/core/error.h"
+#include "acl/core/memory_utils.h"
 
 #include <rtm/scalarf.h>
 
@@ -35,7 +36,7 @@ ACL_IMPL_FILE_PRAGMA_PUSH
 
 namespace acl
 {
-	inline uint32_t pack_scalar_unsigned(float input, uint8_t num_bits)
+	inline uint32_t pack_scalar_unsigned(float input, uint32_t num_bits)
 	{
 		ACL_ASSERT(num_bits < 31, "Attempting to pack on too many bits");
 		ACL_ASSERT(input >= 0.0F && input <= 1.0F, "Expected normalized unsigned input value: %f", input);
@@ -43,7 +44,7 @@ namespace acl
 		return static_cast<uint32_t>(rtm::scalar_symmetric_round(input * rtm::scalar_safe_to_float(max_value)));
 	}
 
-	inline float unpack_scalar_unsigned(uint32_t input, uint8_t num_bits)
+	inline float unpack_scalar_unsigned(uint32_t input, uint32_t num_bits)
 	{
 		ACL_ASSERT(num_bits < 31, "Attempting to unpack from too many bits");
 		const uint32_t max_value = (1 << num_bits) - 1;
@@ -53,15 +54,122 @@ namespace acl
 		return rtm::scalar_safe_to_float(input) * inv_max_value;
 	}
 
-	inline uint32_t pack_scalar_signed(float input, uint8_t num_bits)
+	inline uint32_t pack_scalar_signed(float input, uint32_t num_bits)
 	{
 		return pack_scalar_unsigned((input * 0.5F) + 0.5F, num_bits);
 	}
 
-	inline float unpack_scalar_signed(uint32_t input, uint8_t num_bits)
+	inline float unpack_scalar_signed(uint32_t input, uint32_t num_bits)
 	{
 		return (unpack_scalar_unsigned(input, num_bits) * 2.0F) - 1.0F;
 	}
+
+	// Assumes the 'vector_data' is in big-endian order and is padded in order to load up to 8 bytes from it
+	inline rtm::scalarf RTM_SIMD_CALL unpack_scalarf_32_unsafe(const uint8_t* vector_data, uint32_t bit_offset)
+	{
+#if defined(RTM_SSE2_INTRINSICS)
+		const uint32_t byte_offset = bit_offset / 8;
+		const uint32_t shift_offset = bit_offset % 8;
+		uint64_t vector_u64 = unaligned_load<uint64_t>(vector_data + byte_offset + 0);
+		vector_u64 = byte_swap(vector_u64);
+		vector_u64 <<= shift_offset;
+		vector_u64 >>= 32;
+
+		const uint32_t x32 = uint32_t(vector_u64);
+
+		return _mm_castsi128_ps(_mm_set1_epi32(x32));
+#elif defined(RTM_NEON_INTRINSICS)
+		const uint32_t byte_offset = bit_offset / 8;
+		const uint32_t shift_offset = bit_offset % 8;
+		uint64_t vector_u64 = unaligned_load<uint64_t>(vector_data + byte_offset + 0);
+		vector_u64 = byte_swap(vector_u64);
+		vector_u64 <<= shift_offset;
+		vector_u64 >>= 32;
+
+		const uint64_t x64 = vector_u64;
+
+		const uint32x2_t xy = vcreate_u32(x64);
+		return vget_lane_f32(vreinterpret_f32_u32(xy), 0);
+#else
+		const uint32_t byte_offset = bit_offset / 8;
+		const uint32_t shift_offset = bit_offset % 8;
+		uint64_t vector_u64 = unaligned_load<uint64_t>(vector_data + byte_offset + 0);
+		vector_u64 = byte_swap(vector_u64);
+		vector_u64 <<= shift_offset;
+		vector_u64 >>= 32;
+
+		const uint64_t x64 = vector_u64;
+
+		const float x = aligned_load<float>(&x64);
+
+		return rtm::scalar_set(x);
+#endif
+	}
+
+	// Assumes the 'vector_data' is in big-endian order and padded in order to load up to 8 bytes from it
+	inline rtm::scalarf RTM_SIMD_CALL unpack_scalarf_uXX_unsafe(uint32_t num_bits, const uint8_t* vector_data, uint32_t bit_offset)
+	{
+		ACL_ASSERT(num_bits <= 19, "This function does not support reading more than 19 bits per component");
+
+		struct PackedTableEntry
+		{
+			explicit constexpr PackedTableEntry(uint8_t num_bits_)
+				: max_value(num_bits_ == 0 ? 1.0F : (1.0F / float((1 << num_bits_) - 1)))
+				, mask((1 << num_bits_) - 1)
+			{}
+
+			float max_value;
+			uint32_t mask;
+		};
+
+		// TODO: We technically don't need the first 3 entries, which could save a few bytes
+		alignas(64) static constexpr PackedTableEntry k_packed_constants[20] =
+		{
+			PackedTableEntry(0), PackedTableEntry(1), PackedTableEntry(2), PackedTableEntry(3),
+			PackedTableEntry(4), PackedTableEntry(5), PackedTableEntry(6), PackedTableEntry(7),
+			PackedTableEntry(8), PackedTableEntry(9), PackedTableEntry(10), PackedTableEntry(11),
+			PackedTableEntry(12), PackedTableEntry(13), PackedTableEntry(14), PackedTableEntry(15),
+			PackedTableEntry(16), PackedTableEntry(17), PackedTableEntry(18), PackedTableEntry(19),
+		};
+
+#if defined(RTM_SSE2_INTRINSICS)
+		const uint32_t bit_shift = 32 - num_bits;
+		const uint32_t mask = k_packed_constants[num_bits].mask;
+		const __m128 inv_max_value = _mm_load_ps1(&k_packed_constants[num_bits].max_value);
+
+		uint32_t byte_offset = bit_offset / 8;
+		uint32_t vector_u32 = unaligned_load<uint32_t>(vector_data + byte_offset);
+		vector_u32 = byte_swap(vector_u32);
+		const uint32_t x32 = (vector_u32 >> (bit_shift - (bit_offset % 8)));
+
+		const __m128 value = _mm_cvtsi32_ss(inv_max_value, x32 & mask);
+		return _mm_mul_ss(value, inv_max_value);
+#elif defined(RTM_NEON_INTRINSICS)
+		const uint32_t bit_shift = 32 - num_bits;
+		const uint32_t mask = k_packed_constants[num_bits].mask;
+		const float inv_max_value = k_packed_constants[num_bits].max_value;
+
+		uint32_t byte_offset = bit_offset / 8;
+		uint32_t vector_u32 = unaligned_load<uint32_t>(vector_data + byte_offset);
+		vector_u32 = byte_swap(vector_u32);
+		const uint32_t x32 = (vector_u32 >> (bit_shift - (bit_offset % 8)));
+
+		const int32_t value_u32 = x32 & mask;
+		const float value_f32 = static_cast<float>(value_u32);
+		return value_f32 * inv_max_value;
+#else
+		const uint32_t bit_shift = 32 - num_bits;
+		const uint32_t mask = k_packed_constants[num_bits].mask;
+		const float inv_max_value = k_packed_constants[num_bits].max_value;
+
+		uint32_t byte_offset = bit_offset / 8;
+		uint32_t vector_u32 = unaligned_load<uint32_t>(vector_data + byte_offset);
+		vector_u32 = byte_swap(vector_u32);
+		const uint32_t x32 = (vector_u32 >> (bit_shift - (bit_offset % 8))) & mask;
+
+		return rtm::scalar_set(static_cast<float>(x32) * inv_max_value);
+#endif
+	}
 }
 
 ACL_IMPL_FILE_PRAGMA_POP
diff --git a/includes/acl/math/vector4_packing.h b/includes/acl/math/vector4_packing.h
--- a/includes/acl/math/vector4_packing.h
+++ b/includes/acl/math/vector4_packing.h
@@ -214,7 +214,7 @@ namespace acl
 	}
 
 	// Packs data in big-endian order and assumes the 'out_vector_data' is padded in order to write up to 16 bytes to it
-	inline void RTM_SIMD_CALL pack_vector4_uXX_unsafe(rtm::vector4f_arg0 vector, uint8_t num_bits, uint8_t* out_vector_data)
+	inline void RTM_SIMD_CALL pack_vector4_uXX_unsafe(rtm::vector4f_arg0 vector, uint32_t num_bits, uint8_t* out_vector_data)
 	{
 		uint32_t vector_x = pack_scalar_unsigned(rtm::vector_get_x(vector), num_bits);
 		uint32_t vector_y = pack_scalar_unsigned(rtm::vector_get_y(vector), num_bits);
@@ -234,7 +234,7 @@ namespace acl
 	}
 
 	// Assumes the 'vector_data' is in big-endian order and padded in order to load up to 16 bytes from it
-	inline rtm::vector4f RTM_SIMD_CALL unpack_vector4_uXX_unsafe(uint8_t num_bits, const uint8_t* vector_data, uint32_t bit_offset)
+	inline rtm::vector4f RTM_SIMD_CALL unpack_vector4_uXX_unsafe(uint32_t num_bits, const uint8_t* vector_data, uint32_t bit_offset)
 	{
 		ACL_ASSERT(num_bits <= 19, "This function does not support reading more than 19 bits per component");
 
@@ -656,7 +656,7 @@ namespace acl
 		return rtm::vector_neg_mul_sub(decayed, -2.0F, rtm::vector_set(-1.0F));
 	}
 
-	inline void RTM_SIMD_CALL pack_vector3_32(rtm::vector4f_arg0 vector, uint8_t XBits, uint8_t YBits, uint8_t ZBits, bool is_unsigned, uint8_t* out_vector_data)
+	inline void RTM_SIMD_CALL pack_vector3_32(rtm::vector4f_arg0 vector, uint32_t XBits, uint32_t YBits, uint32_t ZBits, bool is_unsigned, uint8_t* out_vector_data)
 	{
 		ACL_ASSERT(XBits + YBits + ZBits == 32, "Sum of XYZ bits does not equal 32!");
 
@@ -672,7 +672,7 @@ namespace acl
 		data[1] = safe_static_cast<uint16_t>(vector_u32 & 0xFFFF);
 	}
 
-	inline rtm::vector4f RTM_SIMD_CALL decay_vector3_u32(rtm::vector4f_arg0 input, uint8_t XBits, uint8_t YBits, uint8_t ZBits)
+	inline rtm::vector4f RTM_SIMD_CALL decay_vector3_u32(rtm::vector4f_arg0 input, uint32_t XBits, uint32_t YBits, uint32_t ZBits)
 	{
 		ACL_ASSERT(XBits + YBits + ZBits == 32, "Sum of XYZ bits does not equal 32!");
 		ACL_ASSERT(rtm::vector_all_greater_equal3(input, rtm::vector_zero()) && rtm::vector_all_less_equal(input, rtm::vector_set(1.0F)), "Expected normalized unsigned input value: %f, %f, %f", rtm::vector_get_x(input), rtm::vector_get_y(input), rtm::vector_get_z(input));
@@ -688,7 +688,7 @@ namespace acl
 		return decayed;
 	}
 
-	inline rtm::vector4f RTM_SIMD_CALL decay_vector3_s32(rtm::vector4f_arg0 input, uint8_t XBits, uint8_t YBits, uint8_t ZBits)
+	inline rtm::vector4f RTM_SIMD_CALL decay_vector3_s32(rtm::vector4f_arg0 input, uint32_t XBits, uint32_t YBits, uint32_t ZBits)
 	{
 		const rtm::vector4f half = rtm::vector_set(0.5F);
 		const rtm::vector4f unsigned_input = rtm::vector_mul_add(input, half, half);
@@ -707,7 +707,7 @@ namespace acl
 		return rtm::vector_neg_mul_sub(decayed, -2.0F, rtm::vector_set(-1.0F));
 	}
 
-	inline rtm::vector4f RTM_SIMD_CALL unpack_vector3_32(uint8_t XBits, uint8_t YBits, uint8_t ZBits, bool is_unsigned, const uint8_t* vector_data)
+	inline rtm::vector4f RTM_SIMD_CALL unpack_vector3_32(uint32_t XBits, uint32_t YBits, uint32_t ZBits, bool is_unsigned, const uint8_t* vector_data)
 	{
 		ACL_ASSERT(XBits + YBits + ZBits == 32, "Sum of XYZ bits does not equal 32!");
 
@@ -795,7 +795,7 @@ namespace acl
 	}
 
 	// Packs data in big-endian order and assumes the 'out_vector_data' is padded in order to write up to 16 bytes to it
-	inline void RTM_SIMD_CALL pack_vector3_uXX_unsafe(rtm::vector4f_arg0 vector, uint8_t num_bits, uint8_t* out_vector_data)
+	inline void RTM_SIMD_CALL pack_vector3_uXX_unsafe(rtm::vector4f_arg0 vector, uint32_t num_bits, uint8_t* out_vector_data)
 	{
 		uint32_t vector_x = pack_scalar_unsigned(rtm::vector_get_x(vector), num_bits);
 		uint32_t vector_y = pack_scalar_unsigned(rtm::vector_get_y(vector), num_bits);
@@ -810,7 +810,7 @@ namespace acl
 	}
 
 	// Packs data in big-endian order and assumes the 'out_vector_data' is padded in order to write up to 16 bytes to it
-	inline void RTM_SIMD_CALL pack_vector3_sXX_unsafe(rtm::vector4f_arg0 vector, uint8_t num_bits, uint8_t* out_vector_data)
+	inline void RTM_SIMD_CALL pack_vector3_sXX_unsafe(rtm::vector4f_arg0 vector, uint32_t num_bits, uint8_t* out_vector_data)
 	{
 		uint32_t vector_x = pack_scalar_signed(rtm::vector_get_x(vector), num_bits);
 		uint32_t vector_y = pack_scalar_signed(rtm::vector_get_y(vector), num_bits);
@@ -852,7 +852,7 @@ namespace acl
 	}
 
 	// Assumes the 'vector_data' is in big-endian order and padded in order to load up to 16 bytes from it
-	inline rtm::vector4f RTM_SIMD_CALL unpack_vector3_uXX_unsafe(uint8_t num_bits, const uint8_t* vector_data, uint32_t bit_offset)
+	inline rtm::vector4f RTM_SIMD_CALL unpack_vector3_uXX_unsafe(uint32_t num_bits, const uint8_t* vector_data, uint32_t bit_offset)
 	{
 		ACL_ASSERT(num_bits <= 19, "This function does not support reading more than 19 bits per component");
 
@@ -964,7 +964,7 @@ namespace acl
 	}
 
 	// Assumes the 'vector_data' is in big-endian order and padded in order to load up to 16 bytes from it
-	inline rtm::vector4f RTM_SIMD_CALL unpack_vector3_sXX_unsafe(uint8_t num_bits, const uint8_t* vector_data, uint32_t bit_offset)
+	inline rtm::vector4f RTM_SIMD_CALL unpack_vector3_sXX_unsafe(uint32_t num_bits, const uint8_t* vector_data, uint32_t bit_offset)
 	{
 		ACL_ASSERT(num_bits * 3 <= 64, "Attempting to read too many bits");
 
@@ -976,7 +976,7 @@ namespace acl
 	// vector2 packing and decay
 
 	// Packs data in big-endian order and assumes the 'out_vector_data' is padded in order to write up to 16 bytes to it
-	inline void RTM_SIMD_CALL pack_vector2_uXX_unsafe(rtm::vector4f_arg0 vector, uint8_t num_bits, uint8_t* out_vector_data)
+	inline void RTM_SIMD_CALL pack_vector2_uXX_unsafe(rtm::vector4f_arg0 vector, uint32_t num_bits, uint8_t* out_vector_data)
 	{
 		uint32_t vector_x = pack_scalar_unsigned(rtm::vector_get_x(vector), num_bits);
 		uint32_t vector_y = pack_scalar_unsigned(rtm::vector_get_y(vector), num_bits);
@@ -989,7 +989,7 @@ namespace acl
 	}
 
 	// Assumes the 'vector_data' is in big-endian order and padded in order to load up to 16 bytes from it
-	inline rtm::vector4f RTM_SIMD_CALL unpack_vector2_uXX_unsafe(uint8_t num_bits, const uint8_t* vector_data, uint32_t bit_offset)
+	inline rtm::vector4f RTM_SIMD_CALL unpack_vector2_uXX_unsafe(uint32_t num_bits, const uint8_t* vector_data, uint32_t bit_offset)
 	{
 		ACL_ASSERT(num_bits <= 19, "This function does not support reading more than 19 bits per component");
 
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
@@ -164,7 +164,7 @@ def create_csv(options):
 		csv_data['stats_summary_csv_file'] = stats_summary_csv_file
 
 		print('Generating CSV file {} ...'.format(stats_summary_csv_filename))
-		print('Clip Name, Algorithm Name, Raw Size, Compressed Size, Compression Ratio, Compression Time, Clip Duration, Num Animated Tracks, Max Error', file = stats_summary_csv_file)
+		print('Clip Name, Algorithm Name, Raw Size, Compressed Size, Compression Ratio, Compression Time, Clip Duration, Num Animated Tracks, Max Error, Num Transforms, Num Samples Per Track, Quantization Memory Usage', file = stats_summary_csv_file)
 
 	if options['csv_bit_rate']:
 		stats_bit_rate_csv_filename = os.path.join(stat_dir, 'stats_bit_rate.csv')
@@ -211,8 +211,8 @@ def close_csv(csv_data):
 def append_csv(csv_data, job_data):
 	if 'stats_summary_csv_file' in csv_data:
 		data = job_data['stats_summary_data']
-		for (clip_name, algo_name, raw_size, compressed_size, compression_ratio, compression_time, duration, num_animated_tracks, max_error) in data:
-			print('{}, {}, {}, {}, {}, {}, {}, {}, {}'.format(clip_name, algo_name, raw_size, compressed_size, compression_ratio, compression_time, duration, num_animated_tracks, max_error), file = csv_data['stats_summary_csv_file'])
+		for (clip_name, algo_name, raw_size, compressed_size, compression_ratio, compression_time, duration, num_animated_tracks, max_error, num_transforms, num_samples_per_track, quantization_memory_usage) in data:
+			print('{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}'.format(clip_name, algo_name, raw_size, compressed_size, compression_ratio, compression_time, duration, num_animated_tracks, max_error, num_transforms, num_samples_per_track, quantization_memory_usage), file = csv_data['stats_summary_csv_file'])
 
 	if 'stats_animated_size_csv_file' in csv_data:
 		size_data = job_data['stats_animated_size']
@@ -561,9 +561,12 @@ def run_stat_parsing(options, stat_queue, result_queue):
 						compression_times.append(run_stats['compression_time'])
 
 						if options['csv_summary']:
-							#(name, raw_size, compressed_size, compression_ratio, compression_time, duration, num_animated_tracks, max_error)
+							#(name, raw_size, compressed_size, compression_ratio, compression_time, duration, num_animated_tracks, max_error, num_transforms, num_samples_per_track, quantization_memory_usage)
+							num_transforms = run_stats['num_bones']
+							num_samples_per_track = run_stats['num_samples']
 							num_animated_tracks = run_stats.get('num_animated_tracks', 0)
-							data = (run_stats['clip_name'], run_stats['csv_desc'], run_stats['raw_size'], run_stats['compressed_size'], run_stats['compression_ratio'], run_stats['compression_time'], run_stats['duration'], num_animated_tracks, run_stats['max_error'])
+							quantization_memory_usage = run_stats.get('track_bit_rate_database_size', 0) + run_stats.get('transform_cache_size', 0)
+							data = (run_stats['clip_name'], run_stats['csv_desc'], run_stats['raw_size'], run_stats['compressed_size'], run_stats['compression_ratio'], run_stats['compression_time'], run_stats['duration'], num_animated_tracks, run_stats['max_error'], num_transforms, num_samples_per_track, quantization_memory_usage)
 							stats_summary_data.append(data)
 
 						if 'segments' in run_stats and len(run_stats['segments']) > 0:
@@ -665,6 +668,10 @@ def percentile_rank(values, value):
 	return (values < value).mean() * 100.0
 
 if __name__ == "__main__":
+	if sys.version_info < (3, 4):
+		print('Python 3.4 or higher needed to run this script')
+		sys.exit(1)
+
 	options = parse_argv()
 
 	stat_files = compress_clips(options)
diff --git a/tools/acl_compressor/sources/acl_compressor.cpp b/tools/acl_compressor/sources/acl_compressor.cpp
--- a/tools/acl_compressor/sources/acl_compressor.cpp
+++ b/tools/acl_compressor/sources/acl_compressor.cpp
@@ -147,6 +147,8 @@ struct Options
 	bool			profile_decompression;
 	bool			exhaustive_compression;
 
+	bool			use_matrix_error_metric;
+
 	bool			is_bind_pose_relative;
 	bool			is_bind_pose_additive0;
 	bool			is_bind_pose_additive1;
@@ -176,6 +178,7 @@ struct Options
 		, regression_testing(false)
 		, profile_decompression(false)
 		, exhaustive_compression(false)
+		, use_matrix_error_metric(false)
 		, is_bind_pose_relative(false)
 		, is_bind_pose_additive0(false)
 		, is_bind_pose_additive1(false)
@@ -224,6 +227,7 @@ static constexpr const char* k_exhaustive_compression_option = "-exhaustive";
 static constexpr const char* k_bind_pose_relative_option = "-bind_rel";
 static constexpr const char* k_bind_pose_additive0_option = "-bind_add0";
 static constexpr const char* k_bind_pose_additive1_option = "-bind_add1";
+static constexpr const char* k_matrix_error_metric_option = "-error_mtx";
 static constexpr const char* k_stat_detailed_output_option = "-stat_detailed";
 static constexpr const char* k_stat_exhaustive_output_option = "-stat_exhaustive";
 
@@ -352,6 +356,13 @@ static bool parse_options(int argc, char** argv, Options& options)
 			continue;
 		}
 
+		option_length = std::strlen(k_matrix_error_metric_option);
+		if (std::strncmp(argument, k_matrix_error_metric_option, option_length) == 0)
+		{
+			options.use_matrix_error_metric = true;
+			continue;
+		}
+
 		option_length = std::strlen(k_bind_pose_relative_option);
 		if (std::strncmp(argument, k_bind_pose_relative_option, option_length) == 0)
 		{
@@ -416,19 +427,16 @@ static void validate_accuracy(IAllocator& allocator, const AnimationClip& clip,
 {
 	(void)regression_error_threshold;
 
+	const BoneError bone_error = calculate_compressed_clip_error(allocator, clip, *settings.error_metric, context);
+	(void)bone_error;
+	ACL_ASSERT(rtm::scalar_is_finite(bone_error.error), "Returned error is not a finite value");
+	ACL_ASSERT(bone_error.error < regression_error_threshold, "Error too high for bone %u: %f at time %f", bone_error.index, bone_error.error, bone_error.sample_time);
+
 	const uint16_t num_bones = clip.get_num_bones();
 	const float clip_duration = clip.get_duration();
 	const float sample_rate = clip.get_sample_rate();
 	const uint32_t num_samples = calculate_num_samples(clip_duration, clip.get_sample_rate());
-	const ISkeletalErrorMetric& error_metric = *settings.error_metric;
-	const RigidSkeleton& skeleton = clip.get_skeleton();
 
-	const AnimationClip* additive_base_clip = clip.get_additive_base();
-	const uint32_t additive_num_samples = additive_base_clip != nullptr ? additive_base_clip->get_num_samples() : 0;
-	const float additive_duration = additive_base_clip != nullptr ? additive_base_clip->get_duration() : 0.0F;
-
-	rtm::qvvf* raw_pose_transforms = allocate_type_array<rtm::qvvf>(allocator, num_bones);
-	rtm::qvvf* base_pose_transforms = allocate_type_array<rtm::qvvf>(allocator, num_bones);
 	rtm::qvvf* lossy_pose_transforms = allocate_type_array<rtm::qvvf>(allocator, num_bones);
 
 	DefaultOutputWriter pose_writer(lossy_pose_transforms, num_bones);
@@ -439,27 +447,9 @@ static void validate_accuracy(IAllocator& allocator, const AnimationClip& clip,
 		const float sample_time = rtm::scalar_min(float(sample_index) / sample_rate, clip_duration);
 
 		// We use the nearest sample to accurately measure the loss that happened, if any
-		clip.sample_pose(sample_time, SampleRoundingPolicy::Nearest, raw_pose_transforms, num_bones);
-
 		context.seek(sample_time, SampleRoundingPolicy::Nearest);
 		context.decompress_pose(pose_writer);
 
-		if (additive_base_clip != nullptr)
-		{
-			const float normalized_sample_time = additive_num_samples > 1 ? (sample_time / clip_duration) : 0.0F;
-			const float additive_sample_time = normalized_sample_time * additive_duration;
-			additive_base_clip->sample_pose(additive_sample_time, SampleRoundingPolicy::Nearest, base_pose_transforms, num_bones);
-		}
-
-		// Validate decompress_pose
-		for (uint16_t bone_index = 0; bone_index < num_bones; ++bone_index)
-		{
-			const float error = error_metric.calculate_object_bone_error(skeleton, raw_pose_transforms, base_pose_transforms, lossy_pose_transforms, bone_index);
-			(void)error;
-			ACL_ASSERT(rtm::scalar_is_finite(error), "Returned error is not a finite value");
-			ACL_ASSERT(error < regression_error_threshold, "Error too high for bone %u: %f at time %f", bone_index, error, sample_time);
-		}
-
 		// Validate decompress_bone for rotations only
 		for (uint16_t bone_index = 0; bone_index < num_bones; ++bone_index)
 		{
@@ -497,8 +487,6 @@ static void validate_accuracy(IAllocator& allocator, const AnimationClip& clip,
 		}
 	}
 
-	deallocate_type_array(allocator, raw_pose_transforms, num_bones);
-	deallocate_type_array(allocator, base_pose_transforms, num_bones);
 	deallocate_type_array(allocator, lossy_pose_transforms, num_bones);
 }
 
@@ -629,8 +617,8 @@ static void validate_accuracy(IAllocator& allocator, const track_array& raw_trac
 				const float raw_value = raw_track_writer.read_float1(track_index);
 				const float lossy_value = lossy_track_writer.read_float1(output_index);
 				ACL_ASSERT(rtm::scalar_near_equal(raw_value, lossy_value, regression_error_thresholdf), "Error too high for track %u at time %f", track_index, sample_time);
-				ACL_ASSERT(rtm::scalar_near_equal(raw_value_, raw_value), "Failed to sample track %u at time %f", track_index, sample_time);
-				ACL_ASSERT(rtm::scalar_near_equal(lossy_value_, lossy_value), "Failed to sample track %u at time %f", track_index, sample_time);
+				ACL_ASSERT(rtm::scalar_near_equal(raw_value_, raw_value, 0.00001F), "Failed to sample track %u at time %f", track_index, sample_time);
+				ACL_ASSERT(rtm::scalar_near_equal(lossy_value_, lossy_value, 0.00001F), "Failed to sample track %u at time %f", track_index, sample_time);
 				break;
 			}
 			case track_type8::float2f:
@@ -640,8 +628,8 @@ static void validate_accuracy(IAllocator& allocator, const track_array& raw_trac
 				const rtm::vector4f raw_value = raw_track_writer.read_float2(track_index);
 				const rtm::vector4f lossy_value = lossy_track_writer.read_float2(output_index);
 				ACL_ASSERT(rtm::vector_all_near_equal2(raw_value, lossy_value, regression_error_thresholdf), "Error too high for track %u at time %f", track_index, sample_time);
-				ACL_ASSERT(rtm::vector_all_near_equal2(raw_value_, raw_value), "Failed to sample track %u at time %f", track_index, sample_time);
-				ACL_ASSERT(rtm::vector_all_near_equal2(lossy_value_, lossy_value), "Failed to sample track %u at time %f", track_index, sample_time);
+				ACL_ASSERT(rtm::vector_all_near_equal2(raw_value_, raw_value, 0.00001F), "Failed to sample track %u at time %f", track_index, sample_time);
+				ACL_ASSERT(rtm::vector_all_near_equal2(lossy_value_, lossy_value, 0.00001F), "Failed to sample track %u at time %f", track_index, sample_time);
 				break;
 			}
 			case track_type8::float3f:
@@ -651,8 +639,8 @@ static void validate_accuracy(IAllocator& allocator, const track_array& raw_trac
 				const rtm::vector4f raw_value = raw_track_writer.read_float3(track_index);
 				const rtm::vector4f lossy_value = lossy_track_writer.read_float3(output_index);
 				ACL_ASSERT(rtm::vector_all_near_equal3(raw_value, lossy_value, regression_error_thresholdf), "Error too high for track %u at time %f", track_index, sample_time);
-				ACL_ASSERT(rtm::vector_all_near_equal3(raw_value_, raw_value), "Failed to sample track %u at time %f", track_index, sample_time);
-				ACL_ASSERT(rtm::vector_all_near_equal3(lossy_value_, lossy_value), "Failed to sample track %u at time %f", track_index, sample_time);
+				ACL_ASSERT(rtm::vector_all_near_equal3(raw_value_, raw_value, 0.00001F), "Failed to sample track %u at time %f", track_index, sample_time);
+				ACL_ASSERT(rtm::vector_all_near_equal3(lossy_value_, lossy_value, 0.00001F), "Failed to sample track %u at time %f", track_index, sample_time);
 				break;
 			}
 			case track_type8::float4f:
@@ -662,8 +650,8 @@ static void validate_accuracy(IAllocator& allocator, const track_array& raw_trac
 				const rtm::vector4f raw_value = raw_track_writer.read_float4(track_index);
 				const rtm::vector4f lossy_value = lossy_track_writer.read_float4(output_index);
 				ACL_ASSERT(rtm::vector_all_near_equal(raw_value, lossy_value, regression_error_thresholdf), "Error too high for track %u at time %f", track_index, sample_time);
-				ACL_ASSERT(rtm::vector_all_near_equal(raw_value_, raw_value), "Failed to sample track %u at time %f", track_index, sample_time);
-				ACL_ASSERT(rtm::vector_all_near_equal(lossy_value_, lossy_value), "Failed to sample track %u at time %f", track_index, sample_time);
+				ACL_ASSERT(rtm::vector_all_near_equal(raw_value_, raw_value, 0.00001F), "Failed to sample track %u at time %f", track_index, sample_time);
+				ACL_ASSERT(rtm::vector_all_near_equal(lossy_value_, lossy_value, 0.00001F), "Failed to sample track %u at time %f", track_index, sample_time);
 				break;
 			}
 			case track_type8::vector4f:
@@ -673,8 +661,8 @@ static void validate_accuracy(IAllocator& allocator, const track_array& raw_trac
 				const rtm::vector4f raw_value = raw_track_writer.read_vector4(track_index);
 				const rtm::vector4f lossy_value = lossy_track_writer.read_vector4(output_index);
 				ACL_ASSERT(rtm::vector_all_near_equal(raw_value, lossy_value, regression_error_thresholdf), "Error too high for track %u at time %f", track_index, sample_time);
-				ACL_ASSERT(rtm::vector_all_near_equal(raw_value_, raw_value), "Failed to sample track %u at time %f", track_index, sample_time);
-				ACL_ASSERT(rtm::vector_all_near_equal(lossy_value_, lossy_value), "Failed to sample track %u at time %f", track_index, sample_time);
+				ACL_ASSERT(rtm::vector_all_near_equal(raw_value_, raw_value, 0.00001F), "Failed to sample track %u at time %f", track_index, sample_time);
+				ACL_ASSERT(rtm::vector_all_near_equal(lossy_value_, lossy_value, 0.00001F), "Failed to sample track %u at time %f", track_index, sample_time);
 				break;
 			}
 			default:
@@ -926,7 +914,7 @@ static bool read_acl_sjson_file(IAllocator& allocator, const Options& options,
 	return success;
 }
 
-static bool read_config(IAllocator& allocator, const Options& options, AlgorithmType8& out_algorithm_type, CompressionSettings& out_settings, double& out_regression_error_threshold)
+static bool read_config(IAllocator& allocator, Options& options, AlgorithmType8& out_algorithm_type, CompressionSettings& out_settings, double& out_regression_error_threshold)
 {
 #if defined(__ANDROID__)
 	sjson::Parser parser(options.config_buffer, options.config_buffer_size - 1);
@@ -1067,6 +1055,14 @@ static bool read_config(IAllocator& allocator, const Options& options, Algorithm
 
 	parser.try_read("regression_error_threshold", out_regression_error_threshold, 0.0);
 
+	bool is_bind_pose_relative;
+	if (parser.try_read("is_bind_pose_relative", is_bind_pose_relative, false))
+		options.is_bind_pose_relative = is_bind_pose_relative;
+
+	bool use_matrix_error_metric;
+	if (parser.try_read("use_matrix_error_metric", use_matrix_error_metric, false))
+		options.use_matrix_error_metric = use_matrix_error_metric;
+
 	if (!parser.is_valid() || !parser.remainder_is_comments_and_whitespace())
 	{
 		uint32_t line;
@@ -1080,16 +1076,16 @@ static bool read_config(IAllocator& allocator, const Options& options, Algorithm
 	return true;
 }
 
-static ISkeletalErrorMetric* create_additive_error_metric(IAllocator& allocator, AdditiveClipFormat8 format)
+static itransform_error_metric* create_additive_error_metric(IAllocator& allocator, AdditiveClipFormat8 format)
 {
 	switch (format)
 	{
 	case AdditiveClipFormat8::Relative:
-		return allocate_type<AdditiveTransformErrorMetric<AdditiveClipFormat8::Relative>>(allocator);
+		return allocate_type<additive_qvvf_transform_error_metric<AdditiveClipFormat8::Relative>>(allocator);
 	case AdditiveClipFormat8::Additive0:
-		return allocate_type<AdditiveTransformErrorMetric<AdditiveClipFormat8::Additive0>>(allocator);
+		return allocate_type<additive_qvvf_transform_error_metric<AdditiveClipFormat8::Additive0>>(allocator);
 	case AdditiveClipFormat8::Additive1:
-		return allocate_type<AdditiveTransformErrorMetric<AdditiveClipFormat8::Additive1>>(allocator);
+		return allocate_type<additive_qvvf_transform_error_metric<AdditiveClipFormat8::Additive1>>(allocator);
 	default:
 		return nullptr;
 	}
@@ -1246,7 +1242,12 @@ static int safe_main_impl(int argc, char* argv[])
 		settings.error_metric = create_additive_error_metric(allocator, clip->get_additive_format());
 
 		if (settings.error_metric == nullptr)
-			settings.error_metric = allocate_type<TransformErrorMetric>(allocator);
+		{
+			if (options.use_matrix_error_metric)
+				settings.error_metric = allocate_type<qvvf_matrix3x4f_transform_error_metric>(allocator);
+			else
+				settings.error_metric = allocate_type<qvvf_transform_error_metric>(allocator);
+		}
 	}
 
 	// Compress & Decompress
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
@@ -411,6 +411,10 @@ def set_process_affinity(affinity):
 		p.cpu_affinity([affinity])
 
 if __name__ == "__main__":
+	if sys.version_info < (3, 4):
+		print('Python 3.4 or higher needed to run this script')
+		sys.exit(1)
+
 	options = parse_argv()
 
 	# Set the affinity to core 0, on platforms that support it, core 2 will be used to decompress
diff --git a/tools/calc_local_bit_rates.py b/tools/calc_local_bit_rates.py
--- a/tools/calc_local_bit_rates.py
+++ b/tools/calc_local_bit_rates.py
@@ -12,6 +12,10 @@
 # This code assumes that rotations, translations, and scales are packed on 3 components (e.g. quat drop w)
 
 if __name__ == "__main__":
+	if sys.version_info < (3, 4):
+		print('Python 3.4 or higher needed to run this script')
+		sys.exit(1)
+
 	permutation_tries = []
 	permutation_tries_no_scale = []
 
diff --git a/tools/graph_generation/gen_bit_rate_stats.py b/tools/graph_generation/gen_bit_rate_stats.py
--- a/tools/graph_generation/gen_bit_rate_stats.py
+++ b/tools/graph_generation/gen_bit_rate_stats.py
@@ -9,6 +9,10 @@
 import sjson
 
 if __name__ == "__main__":
+	if sys.version_info < (3, 4):
+		print('Python 3.4 or higher needed to run this script')
+		sys.exit(1)
+
 	if len(sys.argv) != 2:
 		print('Usage: python gen_bit_rate_stats.py <path/to/input_file.sjson>')
 		sys.exit(1)
diff --git a/tools/graph_generation/gen_decomp_delta_stats.py b/tools/graph_generation/gen_decomp_delta_stats.py
--- a/tools/graph_generation/gen_decomp_delta_stats.py
+++ b/tools/graph_generation/gen_decomp_delta_stats.py
@@ -39,6 +39,10 @@ def bytes_to_mb(num_bytes):
 	return num_bytes / (1024 * 1024)
 
 if __name__ == "__main__":
+	if sys.version_info < (3, 4):
+		print('Python 3.4 or higher needed to run this script')
+		sys.exit(1)
+
 	if len(sys.argv) != 2 and len(sys.argv) != 3:
 		print('Usage: python gen_decomp_delta_stats.py <path/to/input_file.sjson> [-warm]')
 		sys.exit(1)
diff --git a/tools/graph_generation/gen_decomp_stats.py b/tools/graph_generation/gen_decomp_stats.py
--- a/tools/graph_generation/gen_decomp_stats.py
+++ b/tools/graph_generation/gen_decomp_stats.py
@@ -30,6 +30,10 @@ def get_clip_stat_files(stats_dir):
 	return sorted(stat_files, key = lambda x: x[0])
 
 if __name__ == "__main__":
+	if sys.version_info < (3, 4):
+		print('Python 3.4 or higher needed to run this script')
+		sys.exit(1)
+
 	if len(sys.argv) != 2:
 		print('Usage: python gen_decomp_stats.py <path/to/input_file.sjson>')
 		sys.exit(1)
diff --git a/tools/graph_generation/gen_full_error_stats.py b/tools/graph_generation/gen_full_error_stats.py
--- a/tools/graph_generation/gen_full_error_stats.py
+++ b/tools/graph_generation/gen_full_error_stats.py
@@ -15,6 +15,10 @@ def format_elapsed_time(elapsed_time):
 	return '{:0>2}h {:0>2}m {:05.2f}s'.format(int(hours), int(minutes), seconds)
 
 if __name__ == "__main__":
+	if sys.version_info < (3, 4):
+		print('Python 3.4 or higher needed to run this script')
+		sys.exit(1)
+
 	if len(sys.argv) != 2:
 		print('Usage: python gen_full_error_stats.py <path/to/input_file.sjson>')
 		sys.exit(1)
diff --git a/tools/graph_generation/gen_summary_stats.py b/tools/graph_generation/gen_summary_stats.py
--- a/tools/graph_generation/gen_summary_stats.py
+++ b/tools/graph_generation/gen_summary_stats.py
@@ -9,6 +9,10 @@
 import sjson
 
 if __name__ == "__main__":
+	if sys.version_info < (3, 4):
+		print('Python 3.4 or higher needed to run this script')
+		sys.exit(1)
+
 	if len(sys.argv) != 2:
 		print('Usage: python gen_summary_stats.py <path/to/input_file.sjson>')
 		sys.exit(1)
diff --git a/tools/graph_generation/pack_png.py b/tools/graph_generation/pack_png.py
--- a/tools/graph_generation/pack_png.py
+++ b/tools/graph_generation/pack_png.py
@@ -5,6 +5,10 @@
 from PIL import Image
 
 if __name__ == "__main__":
+	if sys.version_info < (3, 4):
+		print('Python 3.4 or higher needed to run this script')
+		sys.exit(1)
+
 	if len(sys.argv) != 2:
 		print('Usage: python pack_png.py <path to png image>')
 		sys.exit(1)
diff --git a/tools/release_scripts/extract_stats.py b/tools/release_scripts/extract_stats.py
--- a/tools/release_scripts/extract_stats.py
+++ b/tools/release_scripts/extract_stats.py
@@ -59,6 +59,10 @@ def run_cmd(cmd, output_log):
 		sys.exit(1)
 
 if __name__ == "__main__":
+	if sys.version_info < (3, 4):
+		print('Python 3.4 or higher needed to run this script')
+		sys.exit(1)
+
 	options = parse_argv()
 
 	os.environ['PYTHONIOENCODING'] = 'utf_8'


Test patch:
diff --git a/test_data/configs/uniformly_sampled_quant_bind_relative.config.sjson b/test_data/configs/uniformly_sampled_quant_bind_relative.config.sjson
new file mode 100644
--- /dev/null
+++ b/test_data/configs/uniformly_sampled_quant_bind_relative.config.sjson
@@ -0,0 +1,24 @@
+version = 1
+
+algorithm_name = "UniformlySampled"
+
+level = "Medium"
+
+rotation_format = "QuatDropW_Variable"
+translation_format = "Vector3_Variable"
+scale_format = "Vector3_Variable"
+
+rotation_range_reduction = true
+translation_range_reduction = true
+scale_range_reduction = true
+
+segmenting = {
+	enabled = true
+
+	rotation_range_reduction = true
+	translation_range_reduction = true
+	scale_range_reduction = true
+}
+
+regression_error_threshold = 0.075
+is_bind_pose_relative = true
diff --git a/test_data/configs/uniformly_sampled_quant_mtx_error.config.sjson b/test_data/configs/uniformly_sampled_quant_mtx_error.config.sjson
new file mode 100644
--- /dev/null
+++ b/test_data/configs/uniformly_sampled_quant_mtx_error.config.sjson
@@ -0,0 +1,24 @@
+version = 1
+
+algorithm_name = "UniformlySampled"
+
+level = "Medium"
+
+rotation_format = "QuatDropW_Variable"
+translation_format = "Vector3_Variable"
+scale_format = "Vector3_Variable"
+
+rotation_range_reduction = true
+translation_range_reduction = true
+scale_range_reduction = true
+
+segmenting = {
+	enabled = true
+
+	rotation_range_reduction = true
+	translation_range_reduction = true
+	scale_range_reduction = true
+}
+
+regression_error_threshold = 0.075
+use_matrix_error_metric = true
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
@@ -32,89 +32,99 @@ using namespace acl;
 
 TEST_CASE("bitset", "[core][utils]")
 {
-	REQUIRE(BitSetDescription::make_from_num_bits(0).get_size() == 0);
-	REQUIRE(BitSetDescription::make_from_num_bits(1).get_size() == 1);
-	REQUIRE(BitSetDescription::make_from_num_bits(31).get_size() == 1);
-	REQUIRE(BitSetDescription::make_from_num_bits(32).get_size() == 1);
-	REQUIRE(BitSetDescription::make_from_num_bits(33).get_size() == 2);
-	REQUIRE(BitSetDescription::make_from_num_bits(64).get_size() == 2);
-	REQUIRE(BitSetDescription::make_from_num_bits(65).get_size() == 3);
-
-	REQUIRE(BitSetDescription::make_from_num_bits(0).get_num_bits() == 0);
-	REQUIRE(BitSetDescription::make_from_num_bits(1).get_num_bits() == 32);
-	REQUIRE(BitSetDescription::make_from_num_bits(31).get_num_bits() == 32);
-	REQUIRE(BitSetDescription::make_from_num_bits(32).get_num_bits() == 32);
-	REQUIRE(BitSetDescription::make_from_num_bits(33).get_num_bits() == 64);
-	REQUIRE(BitSetDescription::make_from_num_bits(64).get_num_bits() == 64);
-	REQUIRE(BitSetDescription::make_from_num_bits(65).get_num_bits() == 96);
+	CHECK(BitSetDescription::make_from_num_bits(0).get_size() == 0);
+	CHECK(BitSetDescription::make_from_num_bits(1).get_size() == 1);
+	CHECK(BitSetDescription::make_from_num_bits(31).get_size() == 1);
+	CHECK(BitSetDescription::make_from_num_bits(32).get_size() == 1);
+	CHECK(BitSetDescription::make_from_num_bits(33).get_size() == 2);
+	CHECK(BitSetDescription::make_from_num_bits(64).get_size() == 2);
+	CHECK(BitSetDescription::make_from_num_bits(65).get_size() == 3);
+
+	CHECK(BitSetDescription::make_from_num_bits(0).get_num_bits() == 0);
+	CHECK(BitSetDescription::make_from_num_bits(1).get_num_bits() == 32);
+	CHECK(BitSetDescription::make_from_num_bits(31).get_num_bits() == 32);
+	CHECK(BitSetDescription::make_from_num_bits(32).get_num_bits() == 32);
+	CHECK(BitSetDescription::make_from_num_bits(33).get_num_bits() == 64);
+	CHECK(BitSetDescription::make_from_num_bits(64).get_num_bits() == 64);
+	CHECK(BitSetDescription::make_from_num_bits(65).get_num_bits() == 96);
 
 	constexpr BitSetDescription desc = BitSetDescription::make_from_num_bits<64>();
-	REQUIRE(desc.get_size() == 2);
-	REQUIRE(desc.get_size() == BitSetDescription::make_from_num_bits(64).get_size());
+	CHECK(desc.get_size() == 2);
+	CHECK(desc.get_size() == BitSetDescription::make_from_num_bits(64).get_size());
 
 	uint32_t bitset_data[desc.get_size() + 1];	// Add padding
 	std::memset(&bitset_data[0], 0, sizeof(bitset_data));
 
 	bitset_reset(&bitset_data[0], desc, true);
-	REQUIRE(bitset_data[0] == 0xFFFFFFFF);
-	REQUIRE(bitset_data[1] == 0xFFFFFFFF);
-	REQUIRE(bitset_data[2] == 0);
+	CHECK(bitset_data[0] == 0xFFFFFFFF);
+	CHECK(bitset_data[1] == 0xFFFFFFFF);
+	CHECK(bitset_data[2] == 0);
 
 	bitset_data[2] = 0xFFFFFFFF;
 	bitset_reset(&bitset_data[0], desc, false);
-	REQUIRE(bitset_data[0] == 0);
-	REQUIRE(bitset_data[1] == 0);
-	REQUIRE(bitset_data[2] == 0xFFFFFFFF);
+	CHECK(bitset_data[0] == 0);
+	CHECK(bitset_data[1] == 0);
+	CHECK(bitset_data[2] == 0xFFFFFFFF);
 
 	bitset_data[2] = 0;
 	bitset_set(&bitset_data[0], desc, 0, false);
-	REQUIRE(bitset_data[0] == 0);
-	REQUIRE(bitset_data[1] == 0);
-	REQUIRE(bitset_data[2] == 0);
+	CHECK(bitset_data[0] == 0);
+	CHECK(bitset_data[1] == 0);
+	CHECK(bitset_data[2] == 0);
 
 	bitset_set(&bitset_data[0], desc, 0, true);
-	REQUIRE(bitset_data[0] == 0x80000000);
-	REQUIRE(bitset_data[1] == 0);
-	REQUIRE(bitset_data[2] == 0);
+	CHECK(bitset_data[0] == 0x80000000);
+	CHECK(bitset_data[1] == 0);
+	CHECK(bitset_data[2] == 0);
 
 	bitset_set(&bitset_data[0], desc, 31, true);
-	REQUIRE(bitset_data[0] == 0x80000001);
-	REQUIRE(bitset_data[1] == 0);
-	REQUIRE(bitset_data[2] == 0);
+	CHECK(bitset_data[0] == 0x80000001);
+	CHECK(bitset_data[1] == 0);
+	CHECK(bitset_data[2] == 0);
 
 	bitset_set(&bitset_data[0], desc, 31, false);
-	REQUIRE(bitset_data[0] == 0x80000000);
-	REQUIRE(bitset_data[1] == 0);
-	REQUIRE(bitset_data[2] == 0);
+	CHECK(bitset_data[0] == 0x80000000);
+	CHECK(bitset_data[1] == 0);
+	CHECK(bitset_data[2] == 0);
 
 	bitset_set(&bitset_data[0], desc, 32, true);
-	REQUIRE(bitset_data[0] == 0x80000000);
-	REQUIRE(bitset_data[1] == 0x80000000);
-	REQUIRE(bitset_data[2] == 0);
+	CHECK(bitset_data[0] == 0x80000000);
+	CHECK(bitset_data[1] == 0x80000000);
+	CHECK(bitset_data[2] == 0);
 
 	bitset_set_range(&bitset_data[0], desc, 8, 4, true);
-	REQUIRE(bitset_data[0] == 0x80F00000);
-	REQUIRE(bitset_data[1] == 0x80000000);
-	REQUIRE(bitset_data[2] == 0);
+	CHECK(bitset_data[0] == 0x80F00000);
+	CHECK(bitset_data[1] == 0x80000000);
+	CHECK(bitset_data[2] == 0);
 
 	bitset_set_range(&bitset_data[0], desc, 10, 2, false);
-	REQUIRE(bitset_data[0] == 0x80C00000);
-	REQUIRE(bitset_data[1] == 0x80000000);
-	REQUIRE(bitset_data[2] == 0);
-
-	REQUIRE(bitset_test(&bitset_data[0], desc, 0) == true);
-	REQUIRE(bitset_test(&bitset_data[0], desc, 1) == false);
-	REQUIRE(bitset_test(&bitset_data[0], desc, 2) == false);
-	REQUIRE(bitset_test(&bitset_data[0], desc, 3) == false);
-	REQUIRE(bitset_test(&bitset_data[0], desc, 4) == false);
-	REQUIRE(bitset_test(&bitset_data[0], desc, 5) == false);
-	REQUIRE(bitset_test(&bitset_data[0], desc, 6) == false);
-	REQUIRE(bitset_test(&bitset_data[0], desc, 7) == false);
-	REQUIRE(bitset_test(&bitset_data[0], desc, 8) == true);
-	REQUIRE(bitset_test(&bitset_data[0], desc, 9) == true);
-	REQUIRE(bitset_test(&bitset_data[0], desc, 10) == false);
-	REQUIRE(bitset_test(&bitset_data[0], desc, 11) == false);
+	CHECK(bitset_data[0] == 0x80C00000);
+	CHECK(bitset_data[1] == 0x80000000);
+	CHECK(bitset_data[2] == 0);
+
+	CHECK(bitset_test(&bitset_data[0], desc, 0) == true);
+	CHECK(bitset_test(&bitset_data[0], desc, 1) == false);
+	CHECK(bitset_test(&bitset_data[0], desc, 2) == false);
+	CHECK(bitset_test(&bitset_data[0], desc, 3) == false);
+	CHECK(bitset_test(&bitset_data[0], desc, 4) == false);
+	CHECK(bitset_test(&bitset_data[0], desc, 5) == false);
+	CHECK(bitset_test(&bitset_data[0], desc, 6) == false);
+	CHECK(bitset_test(&bitset_data[0], desc, 7) == false);
+	CHECK(bitset_test(&bitset_data[0], desc, 8) == true);
+	CHECK(bitset_test(&bitset_data[0], desc, 9) == true);
+	CHECK(bitset_test(&bitset_data[0], desc, 10) == false);
+	CHECK(bitset_test(&bitset_data[0], desc, 11) == false);
 
 	bitset_data[2] = 0xFFFFFFFF;
-	REQUIRE(bitset_count_set_bits(&bitset_data[0], desc) == 4);
+	CHECK(bitset_count_set_bits(&bitset_data[0], desc) == 4);
+
+	uint32_t bitset_data1[desc.get_size() + 1];	// Add padding
+	bitset_data[0] = 0x00000010;
+	bitset_data[1] = 0x00100000;
+
+	bitset_data1[0] = 0x10101011;
+	bitset_data1[1] = 0x10101011;
+	bitset_and_not(bitset_data, bitset_data, bitset_data1, desc);
+	CHECK(bitset_data[0] == 0x10101001);
+	CHECK(bitset_data[1] == 0x10001011);
 }
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
@@ -25,8 +25,18 @@
 #include <catch.hpp>
 
 #include <acl/math/scalar_packing.h>
+#include <acl/math/vector4_packing.h>
 
 using namespace acl;
+using namespace rtm;
+
+struct UnalignedBuffer
+{
+	uint32_t padding0;
+	uint16_t padding1;
+	uint8_t buffer[250];
+};
+static_assert((offsetof(UnalignedBuffer, buffer) % 2) == 0, "Minimum packing alignment is 2");
 
 TEST_CASE("scalar packing math", "[math][scalar][packing]")
 {
@@ -63,3 +73,84 @@ TEST_CASE("scalar packing math", "[math][scalar][packing]")
 		CHECK(num_errors == 0);
 	}
 }
+
+TEST_CASE("unpack_scalarf_96_unsafe", "[math][scalar][packing]")
+{
+	{
+		UnalignedBuffer tmp0;
+		UnalignedBuffer tmp1;
+		vector4f vec0 = vector_set(6123.123812F, 19237.01293127F, 0.913912387F, 0.1816253F);
+		pack_vector4_128(vec0, &tmp0.buffer[0]);
+
+		uint32_t x = unaligned_load<uint32_t>(&tmp0.buffer[0]);
+		x = byte_swap(x);
+		unaligned_write(x, &tmp0.buffer[0]);
+
+		uint32_t y = unaligned_load<uint32_t>(&tmp0.buffer[4]);
+		y = byte_swap(y);
+		unaligned_write(y, &tmp0.buffer[4]);
+
+		const uint8_t offsets[] = { 0, 1, 5, 31, 32, 33, 63, 64, 65, 93 };
+		uint32_t num_errors = 0;
+		for (uint8_t offset_idx = 0; offset_idx < get_array_size(offsets); ++offset_idx)
+		{
+			const uint8_t offset = offsets[offset_idx];
+
+			memcpy_bits(&tmp1.buffer[0], offset, &tmp0.buffer[0], 0, 32);
+			scalarf scalar1 = unpack_scalarf_32_unsafe(&tmp1.buffer[0], offset);
+			if (!scalar_near_equal(vector_get_x(vec0), scalar_cast(scalar1), 1.0E-6F))
+				num_errors++;
+
+			memcpy_bits(&tmp1.buffer[0], offset, &tmp0.buffer[4], 0, 32);
+			scalar1 = unpack_scalarf_32_unsafe(&tmp1.buffer[0], offset);
+			if (!scalar_near_equal(vector_get_y(vec0), scalar_cast(scalar1), 1.0E-6F))
+				num_errors++;
+		}
+		CHECK(num_errors == 0);
+	}
+}
+
+TEST_CASE("unpack_scalarf_uXX_unsafe", "[math][scalar][packing]")
+{
+	{
+		UnalignedBuffer tmp0;
+		alignas(16) uint8_t buffer[64];
+
+		uint32_t num_errors = 0;
+		vector4f vec0 = vector_set(unpack_scalar_unsigned(0, 16), unpack_scalar_unsigned(12355, 16), unpack_scalar_unsigned(43222, 16), unpack_scalar_unsigned(54432, 16));
+		pack_vector2_uXX_unsafe(vec0, 16, &buffer[0]);
+		scalarf scalar1 = unpack_scalarf_uXX_unsafe(16, &buffer[0], 0);
+		if (!scalar_near_equal(vector_get_x(vec0), scalar_cast(scalar1), 1.0E-6F))
+			num_errors++;
+
+		for (uint8_t bit_rate = 1; bit_rate < k_highest_bit_rate; ++bit_rate)
+		{
+			uint32_t num_bits = get_num_bits_at_bit_rate(bit_rate);
+			uint32_t max_value = (1 << num_bits) - 1;
+			for (uint32_t value = 0; value <= max_value; ++value)
+			{
+				const float value_unsigned = scalar_clamp(unpack_scalar_unsigned(value, num_bits), 0.0F, 1.0F);
+
+				vec0 = vector_set(value_unsigned, value_unsigned, value_unsigned);
+				pack_vector2_uXX_unsafe(vec0, num_bits, &buffer[0]);
+				scalar1 = unpack_scalarf_uXX_unsafe(num_bits, &buffer[0], 0);
+				if (!scalar_near_equal(vector_get_x(vec0), scalar_cast(scalar1), 1.0E-6F))
+					num_errors++;
+
+				{
+					const uint8_t offsets[] = { 0, 1, 5, 31, 32, 33, 63, 64, 65, 93 };
+					for (uint8_t offset_idx = 0; offset_idx < get_array_size(offsets); ++offset_idx)
+					{
+						const uint8_t offset = offsets[offset_idx];
+
+						memcpy_bits(&tmp0.buffer[0], offset, &buffer[0], 0, num_bits * 4);
+						scalar1 = unpack_scalarf_uXX_unsafe(num_bits, &tmp0.buffer[0], offset);
+						if (!scalar_near_equal(vector_get_x(vec0), scalar_cast(scalar1), 1.0E-6F))
+							num_errors++;
+					}
+				}
+			}
+		}
+		CHECK(num_errors == 0);
+	}
+}
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
@@ -155,7 +155,7 @@ TEST_CASE("pack_vector4_XX", "[math][vector4][packing]")
 
 		for (uint8_t bit_rate = 1; bit_rate < k_highest_bit_rate; ++bit_rate)
 		{
-			uint8_t num_bits = get_num_bits_at_bit_rate(bit_rate);
+			uint32_t num_bits = get_num_bits_at_bit_rate(bit_rate);
 			uint32_t max_value = (1 << num_bits) - 1;
 			for (uint32_t value = 0; value <= max_value; ++value)
 			{
@@ -383,7 +383,7 @@ TEST_CASE("pack_vector3_XX", "[math][vector4][packing]")
 
 		for (uint8_t bit_rate = 1; bit_rate < k_highest_bit_rate; ++bit_rate)
 		{
-			uint8_t num_bits = get_num_bits_at_bit_rate(bit_rate);
+			uint32_t num_bits = get_num_bits_at_bit_rate(bit_rate);
 			uint32_t max_value = (1 << num_bits) - 1;
 			for (uint32_t value = 0; value <= max_value; ++value)
 			{
@@ -450,7 +450,7 @@ TEST_CASE("decay_vector3_XX", "[math][vector4][decay]")
 
 		for (uint8_t bit_rate = 1; bit_rate < k_highest_bit_rate; ++bit_rate)
 		{
-			uint8_t num_bits = get_num_bits_at_bit_rate(bit_rate);
+			uint32_t num_bits = get_num_bits_at_bit_rate(bit_rate);
 			uint32_t max_value = (1 << num_bits) - 1;
 			for (uint32_t value = 0; value <= max_value; ++value)
 			{
@@ -519,7 +519,7 @@ TEST_CASE("pack_vector2_XX", "[math][vector4][packing]")
 
 		for (uint8_t bit_rate = 1; bit_rate < k_highest_bit_rate; ++bit_rate)
 		{
-			uint8_t num_bits = get_num_bits_at_bit_rate(bit_rate);
+			uint32_t num_bits = get_num_bits_at_bit_rate(bit_rate);
 			uint32_t max_value = (1 << num_bits) - 1;
 			for (uint32_t value = 0; value <= max_value; ++value)
 			{
diff --git a/tools/release_scripts/test_everything.py b/tools/release_scripts/test_everything.py
--- a/tools/release_scripts/test_everything.py
+++ b/tools/release_scripts/test_everything.py
@@ -21,6 +21,10 @@ def get_python_exe_name():
 		return 'python3'
 
 if __name__ == "__main__":
+	if sys.version_info < (3, 4):
+		print('Python 3.4 or higher needed to run this script')
+		sys.exit(1)
+
 	os.environ['PYTHONIOENCODING'] = 'utf_8'
 
 	configs = [ 'debug', 'release' ]