Repository: nfrechette/acl (cpp)

Issue:
Add support for per sub-track sample rounding mode
In certain circumstances, tracks need different rounding modes when sampling. For example, an IK target might move and we would like to not interpolate between certain samples or any of its samples. Another case might be a looping clip that has root motion where wrapping is used. We would want to interpolate the whole pose, but snap the root motion to the nearest sample to avoid interpolating the start/end samples together. This is ideally not how looping clips with root motion should be handled as it strips some of the root motion out that way but it should be supported by ACL.

Technically, this can already be supported by splitting clips into multiple clips through bind pose stripping and decompressing them one by one with their relevant rounding mode. While this works, it isn't very efficient as the clip header and some of the metadata will be duplicated.

To do so, the decompression settings should specify whether to use the rounding mode provided to the seek function or to query the track writer for the rounding mode for each sub-track. Something like `per-sub-track-rounding-mode-supported()`. Since this feature isn't commonly needed, it should be possible for host runtimes to disable it during code generation to ensure optimal performance.

A new rounding mode should be introduced `per_sub_track` and code should handle it where relevant.

During decompression, if the per sub-track rounding mode is used, we should query it for animated sub-tracks. That rounding mode can then only influence the interpolation alpha since the samples we use cannot change since they depend on the sampling time. The new rounding mode can then be used to snap the interpolation alpha to the correct position.


Discussion:

Patch:
diff --git a/docs/README.md b/docs/README.md
--- a/docs/README.md
+++ b/docs/README.md
@@ -12,6 +12,7 @@ ACL aims to support a few core algorithms that are well suited for production us
 *  [Compressing tracks](compressing_raw_tracks.md)
 *  [Decompressing tracks](decompressing_a_track_list.md)
 *  [Handling looping playback](handling_looping_playback.md)
+*  [Handling per track rounding](handling_per_track_rounding.md)
 *  [Database and streaming support](database_support.md)
 *  [Other considerations](misc_integration_details.md)
 *  [Migrating from an earlier ACL version](migrating.md)
diff --git a/docs/decompressing_a_track_list.md b/docs/decompressing_a_track_list.md
--- a/docs/decompressing_a_track_list.md
+++ b/docs/decompressing_a_track_list.md
@@ -21,7 +21,7 @@ context.decompress_tracks(my_track_writer); // all tracks
 
 As shown, a context must be initialized with a compressed track list instance. Some context objects such as the one used by uniform sampling can be re-used by any compressed track list and does not need to be re-created while others might require this. In order to detect when this might be required, the function `is_dirty(const compressed_tracks& tracks)` is provided. Some context objects cannot be created on the stack and must be dynamically allocated with an allocator instance. The functions `make_decompression_context(...)` are provided for this purpose.
 
-You can seek anywhere in a track list but you will need to handle looping manually in your game engine (e.g. by calling `fmod`). When seeking, you must also provide a `sample_rounding_policy` to dictate how the interpolation is to be performed. See [here](../includes/acl/core/interpolation_utils.h) for details.
+You can seek anywhere in a track list but you will need to handle looping manually in your game engine (e.g. by calling `fmod`). When seeking, you must also provide a `sample_rounding_policy` to dictate how the interpolation is to be performed. See [here](../includes/acl/core/interpolation_utils.h) for details. ACL handles various rounding modes: `none` will interpolate, `floor` will return the first sample before the sample time (when you need to step animations frame by frame), `ceil` will return the first sample after the sample time, `nearest` will return the closest sample to the sample time, and `per_track` allows you to specify through the `track_writer` the rounding mode per track. See also [handling per track rounding](handling_per_track_rounding.md).
 
 You can override the looping policy by calling `context.set_looping_policy(policy);`. This is only necessary if you perform your own loop optimization before compressing with ACL. See [how to handle looping playback](handling_looping_playback.md) for details.
 
diff --git a/docs/handling_looping_playback.md b/docs/handling_looping_playback.md
--- a/docs/handling_looping_playback.md
+++ b/docs/handling_looping_playback.md
@@ -7,7 +7,7 @@ There are typically two ways to handle looping playback:
 
 ACL supports both approaches and will pick the optimal one during compression (tunable in the compression settings). This is controlled through the usage of the `sample_looping_policy` enum found in its [header](../includes/acl/core/sample_looping_policy.h). It is best to let ACL handle this and to not override this value.
 
-See also [this blog post](todo) for details.
+See also [this blog post](https://nfrechette.github.io/2022/04/03/anim_compression_looping/) for details.
 
 ## Clamp policy
 
diff --git a/docs/handling_per_track_rounding.md b/docs/handling_per_track_rounding.md
new file mode 100644
--- /dev/null
+++ b/docs/handling_per_track_rounding.md
@@ -0,0 +1,19 @@
+# Handling per track rounding
+
+When sampling raw or compressed tracks, a `sample_rounding_policy` is provided to control whether or not we should interpolate and what to return. For most use cases, the same rounding policy can be used for all tracks but ACL also supports the host runtime to specify per track which rounding policy should be used. This feature is disabled by default by the default `decompression_settings` because it isn't common and it adds a bit of overhead. To enable and use this feature, you need to:
+
+*  Enable the feature in your `decompression_settings`, see `decompression_settings::is_per_track_rounding_supported()`.
+*  Provide the `seek(..)` function with `sample_rounding_policy::per_track`.
+*  Implement the rounding policy query function in your `track_writer`, see `track_writer::get_rounding_policy(..)`.
+
+When the feature is enabled in the `decompression_settings`, ACL will calculate all possible samples it might need per track as opposed to just one when all tracks use the same rounding policy. This is cheaper than it sounds. By default, ACL always interpolates (using a stable interpolation function) even when `floor`, `ceil`, and `nearest` are used. When `per_track` is used, we retain both samples used to interpolate (for `floor` and `ceil`), we find the nearest and retain it, and we interpolate as well as we otherwise would. Although we do extra work, the added instructions can often execute in the shadow of more expensive ones that surround them which makes this very cheap.
+
+See also [this blog post](TODO) for details.
+
+## Database support
+
+Using per track rounding may not behave as intended if a partially streamed database is used. For performance reasons, unlike the modes above, only a single value will be interpolated: the one at the specified sample time. This means that if we have 3 samples A, B, C and you sample between B and C with 'floor', if B has been moved to a database and is missing, B (interpolated) is not returned. Normally, A and C would be used to interpolate at the sample time specified as such we do not calculate where B lies. As a result of this, A would be returned unlike the behavior of 'floor' when used to sample all tracks. This is the behavior chosen by design. During decompression, samples are unpacked and interpolated before we know which track they belong to. As such, when the per track mode is used, we output 4 samples (instead of just the one we need), one for each possible mode above. One we know which sample we need (among the 4), we can simply index with the rounding mode to grab it. This is very fast and the cost is largely hidden. Supporting the same behavior as the rounding modes above when a partially streamed in database is used would require us to interpolate 3 samples instead of 1 which would be a lot more expensive for rotation sub-tracks. It would also add a lot of code complexity. For those reasons, the behavior differs.
+
+A [future task](https://github.com/nfrechette/acl/issues/392) will allow tracks to be split into different layers where database settings can be independent. This will allow us to place special tracks into their own layer where key frames are not removed.
+
+A [separate task](https://github.com/nfrechette/acl/issues/407) may allow us to specify per track whether the values can be interpolated or not. This would allow us to detect boundary key frames (where the value changes) and retain those we need to ensure that the end result is identical.
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
@@ -29,7 +29,7 @@
 #include "acl/core/error.h"
 #include "acl/core/time_utils.h"
 #include "acl/core/track_formats.h"
-#include "acl/core/variable_bit_rates.h"
+#include "acl/core/impl/variable_bit_rates.h"
 #include "acl/math/quat_packing.h"
 #include "acl/math/vector4_packing.h"
 #include "acl/compression/impl/track_bit_rate_database.h"
@@ -116,7 +116,7 @@ namespace acl
 			uint8_t* lossy_object_pose;				// 1 per transform
 			size_t metric_transform_size;
 
-			BoneBitRate* bit_rate_per_bone;			// 1 per transform
+			transform_bit_rates* bit_rate_per_bone;			// 1 per transform
 			uint32_t* parent_transform_indices;		// 1 per transform
 			uint32_t* self_transform_indices;		// 1 per transform
 
@@ -170,7 +170,7 @@ namespace acl
 				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);
+				bit_rate_per_bone = allocate_type_array<transform_bit_rates>(allocator, num_bones);
 				parent_transform_indices = allocate_type_array<uint32_t>(allocator, num_bones);
 				self_transform_indices = allocate_type_array<uint32_t>(allocator, num_bones);
 				chain_bone_indices = allocate_type_array<uint32_t>(allocator, num_bones);
@@ -842,7 +842,7 @@ namespace acl
 				// 0: if the segment track is normalized, it can be constant within the segment
 				// 1: if the segment track isn't normalized, it starts at the lowest bit rate
 				// 255: if the track is constant/default for the whole clip
-				const BoneBitRate bone_bit_rates = context.bit_rate_per_bone[bone_index];
+				const transform_bit_rates bone_bit_rates = context.bit_rate_per_bone[bone_index];
 
 				if (bone_bit_rates.rotation == k_invalid_bit_rate && bone_bit_rates.translation == k_invalid_bit_rate && bone_bit_rates.scale == k_invalid_bit_rate)
 				{
@@ -852,7 +852,7 @@ namespace acl
 					continue;	// Every track bit rate is constant/default, nothing else to do
 				}
 
-				BoneBitRate best_bit_rates = bone_bit_rates;
+				transform_bit_rates best_bit_rates = bone_bit_rates;
 				float best_error = 1.0E10F;
 				uint32_t prev_transform_size = ~0U;
 				bool is_error_good_enough = false;
@@ -1001,12 +1001,12 @@ namespace acl
 			return bit_rate >= k_highest_bit_rate ? bit_rate : std::min<uint32_t>(bit_rate + increment, k_highest_bit_rate);
 		}
 
-		inline float increase_bone_bit_rate(quantization_context& context, uint32_t bone_index, uint32_t num_increments, float old_error, BoneBitRate& out_best_bit_rates)
+		inline float increase_bone_bit_rate(quantization_context& context, uint32_t bone_index, uint32_t num_increments, float old_error, transform_bit_rates& out_best_bit_rates)
 		{
-			const BoneBitRate bone_bit_rates = context.bit_rate_per_bone[bone_index];
+			const transform_bit_rates bone_bit_rates = context.bit_rate_per_bone[bone_index];
 			const uint32_t num_scale_increments = context.has_scale ? num_increments : 0;
 
-			BoneBitRate best_bit_rates = bone_bit_rates;
+			transform_bit_rates best_bit_rates = bone_bit_rates;
 			float best_error = old_error;
 
 			for (uint32_t rotation_increment = 0; rotation_increment <= num_increments; ++rotation_increment)
@@ -1029,7 +1029,7 @@ namespace acl
 								continue;
 						}
 
-						context.bit_rate_per_bone[bone_index] = BoneBitRate{ (uint8_t)rotation_bit_rate, (uint8_t)translation_bit_rate, (uint8_t)scale_bit_rate };
+						context.bit_rate_per_bone[bone_index] = transform_bit_rates{ (uint8_t)rotation_bit_rate, (uint8_t)translation_bit_rate, (uint8_t)scale_bit_rate };
 						const float error = calculate_max_error_at_bit_rate_object(context, bone_index, error_scan_stop_condition::until_error_too_high);
 
 						if (error < best_error)
@@ -1056,7 +1056,7 @@ namespace acl
 			return best_error;
 		}
 
-		inline float calculate_bone_permutation_error(quantization_context& context, BoneBitRate* permutation_bit_rates, uint8_t* bone_chain_permutation, uint32_t bone_index, BoneBitRate* best_bit_rates, float old_error)
+		inline float calculate_bone_permutation_error(quantization_context& context, transform_bit_rates* permutation_bit_rates, uint8_t* bone_chain_permutation, uint32_t bone_index, transform_bit_rates* best_bit_rates, float old_error)
 		{
 			const float error_threshold = context.error_threshold;
 			float best_error = old_error;
@@ -1064,7 +1064,7 @@ namespace acl
 			do
 			{
 				// Copy our current bit rates to the permutation rates
-				std::memcpy(permutation_bit_rates, context.bit_rate_per_bone, sizeof(BoneBitRate) * context.num_bones);
+				std::memcpy(permutation_bit_rates, context.bit_rate_per_bone, sizeof(transform_bit_rates) * context.num_bones);
 
 				bool is_permutation_valid = false;
 				const uint32_t num_bones_in_chain = context.num_bones_in_chain;
@@ -1074,7 +1074,7 @@ namespace acl
 					{
 						// Increase bit rate
 						const uint32_t chain_bone_index = context.chain_bone_indices[chain_link_index];
-						BoneBitRate chain_bone_best_bit_rates;
+						transform_bit_rates 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;
 						is_permutation_valid |= chain_bone_best_bit_rates.translation != permutation_bit_rates[chain_bone_index].translation;
@@ -1094,7 +1094,7 @@ namespace acl
 				if (permutation_error < best_error)
 				{
 					best_error = permutation_error;
-					std::memcpy(best_bit_rates, permutation_bit_rates, sizeof(BoneBitRate) * context.num_bones);
+					std::memcpy(best_bit_rates, permutation_bit_rates, sizeof(transform_bit_rates) * context.num_bones);
 
 					if (permutation_error < error_threshold)
 						break;
@@ -1115,7 +1115,7 @@ namespace acl
 			return num_bones_in_chain;
 		}
 
-		inline void initialize_bone_bit_rates(const segment_context& segment, rotation_format8 rotation_format, vector_format8 translation_format, vector_format8 scale_format, BoneBitRate* out_bit_rate_per_bone)
+		inline void initialize_bone_bit_rates(const segment_context& segment, rotation_format8 rotation_format, vector_format8 translation_format, vector_format8 scale_format, transform_bit_rates* out_bit_rate_per_bone)
 		{
 			const bool is_rotation_variable = is_rotation_format_variable(rotation_format);
 			const bool is_translation_variable = is_vector_format_variable(translation_format);
@@ -1124,7 +1124,7 @@ namespace acl
 			const uint32_t num_bones = segment.num_bones;
 			for (uint32_t bone_index = 0; bone_index < num_bones; ++bone_index)
 			{
-				BoneBitRate& bone_bit_rate = out_bit_rate_per_bone[bone_index];
+				transform_bit_rates& bone_bit_rate = out_bit_rate_per_bone[bone_index];
 
 				const bool rotation_supports_constant_tracks = segment.are_rotations_normalized;
 				if (is_rotation_variable && !segment.bone_streams[bone_index].is_rotation_constant)
@@ -1156,7 +1156,7 @@ namespace acl
 
 			for (uint32_t bone_index = 0; bone_index < context.num_bones; ++bone_index)
 			{
-				const BoneBitRate& bone_bit_rate = context.bit_rate_per_bone[bone_index];
+				const transform_bit_rates& bone_bit_rate = context.bit_rate_per_bone[bone_index];
 
 				if (is_rotation_variable)
 					quantize_variable_rotation_stream(context, bone_index, bone_bit_rate.rotation);
@@ -1232,10 +1232,10 @@ 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);
-			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);
-			std::memcpy(best_bit_rates, context.bit_rate_per_bone, sizeof(BoneBitRate) * context.num_bones);
+			transform_bit_rates* permutation_bit_rates = allocate_type_array<transform_bit_rates>(context.allocator, context.num_bones);
+			transform_bit_rates* best_permutation_bit_rates = allocate_type_array<transform_bit_rates>(context.allocator, context.num_bones);
+			transform_bit_rates* best_bit_rates = allocate_type_array<transform_bit_rates>(context.allocator, context.num_bones);
+			std::memcpy(best_bit_rates, context.bit_rate_per_bone, sizeof(transform_bit_rates) * context.num_bones);
 
 			const uint32_t num_bones = context.num_bones;
 			for (uint32_t bone_index = 0; bone_index < num_bones; ++bone_index)
@@ -1268,7 +1268,7 @@ namespace acl
 					if (error < best_error)
 					{
 						best_error = error;
-						std::memcpy(best_bit_rates, best_permutation_bit_rates, sizeof(BoneBitRate) * num_bones);
+						std::memcpy(best_bit_rates, best_permutation_bit_rates, sizeof(transform_bit_rates) * num_bones);
 
 						if (error < error_threshold)
 							break;
@@ -1283,7 +1283,7 @@ namespace acl
 						if (error < best_error)
 						{
 							best_error = error;
-							std::memcpy(best_bit_rates, best_permutation_bit_rates, sizeof(BoneBitRate) * num_bones);
+							std::memcpy(best_bit_rates, best_permutation_bit_rates, sizeof(transform_bit_rates) * num_bones);
 
 							if (error < error_threshold)
 								break;
@@ -1298,7 +1298,7 @@ namespace acl
 							if (error < best_error)
 							{
 								best_error = error;
-								std::memcpy(best_bit_rates, best_permutation_bit_rates, sizeof(BoneBitRate) * num_bones);
+								std::memcpy(best_bit_rates, best_permutation_bit_rates, sizeof(transform_bit_rates) * num_bones);
 
 								if (error < error_threshold)
 									break;
@@ -1315,7 +1315,7 @@ namespace acl
 						if (error < best_error)
 						{
 							best_error = error;
-							std::memcpy(best_bit_rates, best_permutation_bit_rates, sizeof(BoneBitRate) * num_bones);
+							std::memcpy(best_bit_rates, best_permutation_bit_rates, sizeof(transform_bit_rates) * num_bones);
 
 							if (error < error_threshold)
 								break;
@@ -1330,7 +1330,7 @@ namespace acl
 							if (error < best_error)
 							{
 								best_error = error;
-								std::memcpy(best_bit_rates, best_permutation_bit_rates, sizeof(BoneBitRate) * num_bones);
+								std::memcpy(best_bit_rates, best_permutation_bit_rates, sizeof(transform_bit_rates) * num_bones);
 
 								if (error < error_threshold)
 									break;
@@ -1346,7 +1346,7 @@ namespace acl
 								if (error < best_error)
 								{
 									best_error = error;
-									std::memcpy(best_bit_rates, best_permutation_bit_rates, sizeof(BoneBitRate) * num_bones);
+									std::memcpy(best_bit_rates, best_permutation_bit_rates, sizeof(transform_bit_rates) * num_bones);
 
 									if (error < error_threshold)
 										break;
@@ -1368,8 +1368,8 @@ namespace acl
 
 						for (uint32_t i = 0; i < context.num_bones; ++i)
 						{
-							const BoneBitRate& bone_bit_rate = context.bit_rate_per_bone[i];
-							const BoneBitRate& best_bone_bit_rate = best_bit_rates[i];
+							const transform_bit_rates& bone_bit_rate = context.bit_rate_per_bone[i];
+							const transform_bit_rates& best_bone_bit_rate = best_bit_rates[i];
 							bool rotation_differs = bone_bit_rate.rotation != best_bone_bit_rate.rotation;
 							bool translation_differs = bone_bit_rate.translation != best_bone_bit_rate.translation;
 							bool scale_differs = bone_bit_rate.scale != best_bone_bit_rate.scale;
@@ -1378,7 +1378,7 @@ namespace acl
 						}
 #endif
 
-						std::memcpy(context.bit_rate_per_bone, best_bit_rates, sizeof(BoneBitRate) * num_bones);
+						std::memcpy(context.bit_rate_per_bone, best_bit_rates, sizeof(transform_bit_rates) * num_bones);
 					}
 				}
 
@@ -1391,8 +1391,8 @@ namespace acl
 
 					for (uint32_t i = 0; i < context.num_bones; ++i)
 					{
-						const BoneBitRate& bone_bit_rate = context.bit_rate_per_bone[i];
-						const BoneBitRate& best_bone_bit_rate = best_bit_rates[i];
+						const transform_bit_rates& bone_bit_rate = context.bit_rate_per_bone[i];
+						const transform_bit_rates& best_bone_bit_rate = best_bit_rates[i];
 						bool rotation_differs = bone_bit_rate.rotation != best_bone_bit_rate.rotation;
 						bool translation_differs = bone_bit_rate.translation != best_bone_bit_rate.translation;
 						bool scale_differs = bone_bit_rate.scale != best_bone_bit_rate.scale;
@@ -1401,7 +1401,7 @@ namespace acl
 					}
 #endif
 
-					std::memcpy(context.bit_rate_per_bone, best_bit_rates, sizeof(BoneBitRate) * num_bones);
+					std::memcpy(context.bit_rate_per_bone, best_bit_rates, sizeof(transform_bit_rates) * num_bones);
 				}
 
 				// Our error remains too high, this should be rare.
@@ -1419,15 +1419,15 @@ namespace acl
 						// that yield the smallest error BUT increasing the bit rate does NOT always means
 						// that the error will reduce and improve. It could get worse in which case we'll do nothing.
 
-						BoneBitRate& bone_bit_rate = context.bit_rate_per_bone[chain_bone_index];
+						transform_bit_rates& bone_bit_rate = context.bit_rate_per_bone[chain_bone_index];
 
 						// Copy original values
-						BoneBitRate best_bone_bit_rate = bone_bit_rate;
+						transform_bit_rates best_bone_bit_rate = bone_bit_rate;
 						float best_bit_rate_error = error;
 
 						while (error >= error_threshold)
 						{
-							static_assert(offsetof(BoneBitRate, rotation) == 0 && offsetof(BoneBitRate, scale) == sizeof(BoneBitRate) - 1, "Invalid BoneBitRate offsets");
+							static_assert(offsetof(transform_bit_rates, rotation) == 0 && offsetof(transform_bit_rates, scale) == sizeof(transform_bit_rates) - 1, "Invalid BoneBitRate offsets");
 							uint8_t& smallest_bit_rate = *std::min_element<uint8_t*>(&bone_bit_rate.rotation, &bone_bit_rate.scale + 1);
 
 							if (smallest_bit_rate >= k_highest_bit_rate)
@@ -1494,7 +1494,7 @@ namespace acl
 					for (int32_t chain_link_index = num_bones_in_chain - 1; chain_link_index >= 0; --chain_link_index)
 					{
 						const uint32_t chain_bone_index = context.chain_bone_indices[chain_link_index];
-						BoneBitRate& bone_bit_rate = context.bit_rate_per_bone[chain_bone_index];
+						transform_bit_rates& 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);
 						bone_bit_rate.scale = std::max<uint8_t>(bone_bit_rate.scale, k_highest_bit_rate);
@@ -1518,7 +1518,7 @@ namespace acl
 				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_end_of_segment);
-				const BoneBitRate& bone_bit_rate = context.bit_rate_per_bone[bone_index];
+				const transform_bit_rates& bone_bit_rate = context.bit_rate_per_bone[bone_index];
 				printf("%u: %u | %u | %u => %f %s\n", bone_index, bone_bit_rate.rotation, bone_bit_rate.translation, bone_bit_rate.scale, error, error >= error_threshold ? "!" : "");
 			}
 #endif
diff --git a/includes/acl/compression/impl/quantize_track_impl.h b/includes/acl/compression/impl/quantize_track_impl.h
--- a/includes/acl/compression/impl/quantize_track_impl.h
+++ b/includes/acl/compression/impl/quantize_track_impl.h
@@ -26,7 +26,7 @@
 
 #include "acl/core/impl/compiler_utils.h"
 #include "acl/core/track_types.h"
-#include "acl/core/variable_bit_rates.h"
+#include "acl/core/impl/variable_bit_rates.h"
 #include "acl/compression/impl/track_list_context.h"
 
 #include <rtm/mask4i.h>
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
@@ -29,7 +29,7 @@
 #include "acl/core/error.h"
 #include "acl/core/time_utils.h"
 #include "acl/core/track_formats.h"
-#include "acl/core/variable_bit_rates.h"
+#include "acl/core/impl/variable_bit_rates.h"
 #include "acl/math/quat_packing.h"
 #include "acl/math/vector4_packing.h"
 #include "acl/compression/impl/track_stream.h"
@@ -633,7 +633,7 @@ namespace acl
 			uint32_t sample_key;
 			float sample_time;
 
-			BoneBitRate bit_rates;
+			transform_bit_rates bit_rates;
 		};
 
 		inline uint32_t get_uniform_sample_key(const segment_context& segment, float sample_time)
@@ -838,7 +838,7 @@ namespace acl
 			}
 		}
 
-		inline void sample_streams(const transform_streams* bone_streams, const transform_streams* raw_bone_steams, uint32_t num_bones, float sample_time, const BoneBitRate* bit_rates, rotation_format8 rotation_format, vector_format8 translation_format, vector_format8 scale_format, rtm::qvvf* out_local_pose)
+		inline void sample_streams(const transform_streams* bone_streams, const transform_streams* raw_bone_steams, uint32_t num_bones, float sample_time, const transform_bit_rates* bit_rates, rotation_format8 rotation_format, vector_format8 translation_format, vector_format8 scale_format, rtm::qvvf* out_local_pose)
 		{
 			const bool is_rotation_variable = is_rotation_format_variable(rotation_format);
 			const bool is_translation_variable = is_vector_format_variable(translation_format);
@@ -870,7 +870,7 @@ namespace acl
 			}
 		}
 
-		inline void sample_stream(const transform_streams* bone_streams, const transform_streams* raw_bone_steams, uint32_t num_bones, float sample_time, uint32_t bone_index, const BoneBitRate* bit_rates, rotation_format8 rotation_format, vector_format8 translation_format, vector_format8 scale_format, rtm::qvvf* out_local_pose)
+		inline void sample_stream(const transform_streams* bone_streams, const transform_streams* raw_bone_steams, uint32_t num_bones, float sample_time, uint32_t bone_index, const transform_bit_rates* bit_rates, rotation_format8 rotation_format, vector_format8 translation_format, vector_format8 scale_format, rtm::qvvf* out_local_pose)
 		{
 			(void)num_bones;
 
@@ -900,7 +900,7 @@ namespace acl
 			out_local_pose[bone_index] = rtm::qvv_set(rotation, translation, scale);
 		}
 
-		inline void sample_streams_hierarchical(const transform_streams* bone_streams, const transform_streams* raw_bone_steams, uint32_t num_bones, float sample_time, uint32_t bone_index, const BoneBitRate* bit_rates, rotation_format8 rotation_format, vector_format8 translation_format, vector_format8 scale_format, rtm::qvvf* out_local_pose)
+		inline void sample_streams_hierarchical(const transform_streams* bone_streams, const transform_streams* raw_bone_steams, uint32_t num_bones, float sample_time, uint32_t bone_index, const transform_bit_rates* bit_rates, rotation_format8 rotation_format, vector_format8 translation_format, vector_format8 scale_format, rtm::qvvf* out_local_pose)
 		{
 			(void)num_bones;
 
diff --git a/includes/acl/compression/impl/track_array.impl.h b/includes/acl/compression/impl/track_array.impl.h
--- a/includes/acl/compression/impl/track_array.impl.h
+++ b/includes/acl/compression/impl/track_array.impl.h
@@ -215,7 +215,22 @@ namespace acl
 		uint32_t key_frame0;
 		uint32_t key_frame1;
 		float interpolation_alpha;
-		find_linear_interpolation_samples_with_sample_rate(num_samples, sample_rate, sample_time, rounding_policy, m_looping_policy, key_frame0, key_frame1, interpolation_alpha);
+
+		// Allow per track usage, keeps the code simpler to maintain
+		find_linear_interpolation_samples_with_sample_rate(num_samples, sample_rate, sample_time, sample_rounding_policy::per_track, m_looping_policy, key_frame0, key_frame1, interpolation_alpha);
+
+		const float no_rounding_alpha = apply_rounding_policy(interpolation_alpha, sample_rounding_policy::none);
+
+		const float interpolation_alpha_per_policy[k_num_sample_rounding_policies] =
+		{
+			no_rounding_alpha,	// none
+			apply_rounding_policy(interpolation_alpha, sample_rounding_policy::floor),
+			apply_rounding_policy(interpolation_alpha, sample_rounding_policy::ceil),
+			apply_rounding_policy(interpolation_alpha, sample_rounding_policy::nearest),
+
+			// We'll assert if we attempt to use this, but in case they are skipped/disabled, we interpolate
+			no_rounding_alpha,	// per_track
+		};
 
 		switch (track_type)
 		{
@@ -224,9 +239,13 @@ namespace acl
 			{
 				const track_float1f& track__ = track_cast<track_float1f>(m_tracks[track_index]);
 
+				const sample_rounding_policy rounding_policy_ = rounding_policy == sample_rounding_policy::per_track ? writer.get_rounding_policy(track_index) : rounding_policy;
+				ACL_ASSERT(rounding_policy_ != sample_rounding_policy::per_track, "track_writer::get_rounding_policy() cannot return per_track");
+				const float alpha = interpolation_alpha_per_policy[static_cast<int>(rounding_policy_)];
+
 				const rtm::scalarf value0 = rtm::scalar_load(&track__[key_frame0]);
 				const rtm::scalarf value1 = rtm::scalar_load(&track__[key_frame1]);
-				const rtm::scalarf value = rtm::scalar_lerp(value0, value1, rtm::scalar_set(interpolation_alpha));
+				const rtm::scalarf value = rtm::scalar_lerp(value0, value1, rtm::scalar_set(alpha));
 				writer.write_float1(track_index, value);
 			}
 			break;
@@ -235,9 +254,13 @@ namespace acl
 			{
 				const track_float2f& track__ = track_cast<track_float2f>(m_tracks[track_index]);
 
+				const sample_rounding_policy rounding_policy_ = rounding_policy == sample_rounding_policy::per_track ? writer.get_rounding_policy(track_index) : rounding_policy;
+				ACL_ASSERT(rounding_policy_ != sample_rounding_policy::per_track, "track_writer::get_rounding_policy() cannot return per_track");
+				const float alpha = interpolation_alpha_per_policy[static_cast<int>(rounding_policy_)];
+
 				const rtm::vector4f value0 = rtm::vector_load2(&track__[key_frame0]);
 				const rtm::vector4f value1 = rtm::vector_load2(&track__[key_frame1]);
-				const rtm::vector4f value = rtm::vector_lerp(value0, value1, interpolation_alpha);
+				const rtm::vector4f value = rtm::vector_lerp(value0, value1, alpha);
 				writer.write_float2(track_index, value);
 			}
 			break;
@@ -246,9 +269,13 @@ namespace acl
 			{
 				const track_float3f& track__ = track_cast<track_float3f>(m_tracks[track_index]);
 
+				const sample_rounding_policy rounding_policy_ = rounding_policy == sample_rounding_policy::per_track ? writer.get_rounding_policy(track_index) : rounding_policy;
+				ACL_ASSERT(rounding_policy_ != sample_rounding_policy::per_track, "track_writer::get_rounding_policy() cannot return per_track");
+				const float alpha = interpolation_alpha_per_policy[static_cast<int>(rounding_policy_)];
+
 				const rtm::vector4f value0 = rtm::vector_load3(&track__[key_frame0]);
 				const rtm::vector4f value1 = rtm::vector_load3(&track__[key_frame1]);
-				const rtm::vector4f value = rtm::vector_lerp(value0, value1, interpolation_alpha);
+				const rtm::vector4f value = rtm::vector_lerp(value0, value1, alpha);
 				writer.write_float3(track_index, value);
 			}
 			break;
@@ -257,9 +284,13 @@ namespace acl
 			{
 				const track_float4f& track__ = track_cast<track_float4f>(m_tracks[track_index]);
 
+				const sample_rounding_policy rounding_policy_ = rounding_policy == sample_rounding_policy::per_track ? writer.get_rounding_policy(track_index) : rounding_policy;
+				ACL_ASSERT(rounding_policy_ != sample_rounding_policy::per_track, "track_writer::get_rounding_policy() cannot return per_track");
+				const float alpha = interpolation_alpha_per_policy[static_cast<int>(rounding_policy_)];
+
 				const rtm::vector4f value0 = rtm::vector_load(&track__[key_frame0]);
 				const rtm::vector4f value1 = rtm::vector_load(&track__[key_frame1]);
-				const rtm::vector4f value = rtm::vector_lerp(value0, value1, interpolation_alpha);
+				const rtm::vector4f value = rtm::vector_lerp(value0, value1, alpha);
 				writer.write_float4(track_index, value);
 			}
 			break;
@@ -268,9 +299,13 @@ namespace acl
 			{
 				const track_vector4f& track__ = track_cast<track_vector4f>(m_tracks[track_index]);
 
+				const sample_rounding_policy rounding_policy_ = rounding_policy == sample_rounding_policy::per_track ? writer.get_rounding_policy(track_index) : rounding_policy;
+				ACL_ASSERT(rounding_policy_ != sample_rounding_policy::per_track, "track_writer::get_rounding_policy() cannot return per_track");
+				const float alpha = interpolation_alpha_per_policy[static_cast<int>(rounding_policy_)];
+
 				const rtm::vector4f value0 = track__[key_frame0];
 				const rtm::vector4f value1 = track__[key_frame1];
-				const rtm::vector4f value = rtm::vector_lerp(value0, value1, interpolation_alpha);
+				const rtm::vector4f value = rtm::vector_lerp(value0, value1, alpha);
 				writer.write_vector4(track_index, value);
 			}
 			break;
@@ -279,11 +314,15 @@ namespace acl
 			{
 				const track_qvvf& track__ = track_cast<track_qvvf>(m_tracks[track_index]);
 
+				const sample_rounding_policy rounding_policy_ = rounding_policy == sample_rounding_policy::per_track ? writer.get_rounding_policy(track_index) : rounding_policy;
+				ACL_ASSERT(rounding_policy_ != sample_rounding_policy::per_track, "track_writer::get_rounding_policy() cannot return per_track");
+				const float alpha = interpolation_alpha_per_policy[static_cast<int>(rounding_policy_)];
+
 				const rtm::qvvf& value0 = track__[key_frame0];
 				const rtm::qvvf& value1 = track__[key_frame1];
-				const rtm::quatf rotation = rtm::quat_lerp(value0.rotation, value1.rotation, interpolation_alpha);
-				const rtm::vector4f translation = rtm::vector_lerp(value0.translation, value1.translation, interpolation_alpha);
-				const rtm::vector4f scale = rtm::vector_lerp(value0.scale, value1.scale, interpolation_alpha);
+				const rtm::quatf rotation = rtm::quat_lerp(value0.rotation, value1.rotation, alpha);
+				const rtm::vector4f translation = rtm::vector_lerp(value0.translation, value1.translation, alpha);
+				const rtm::vector4f scale = rtm::vector_lerp(value0.scale, value1.scale, alpha);
 				writer.write_rotation(track_index, rotation);
 				writer.write_translation(track_index, translation);
 				writer.write_scale(track_index, scale);
@@ -310,10 +349,13 @@ namespace acl
 		const float duration = get_finite_duration();
 		sample_time = rtm::scalar_clamp(sample_time, 0.0F, duration);
 
+		const sample_rounding_policy rounding_policy_ = rounding_policy == sample_rounding_policy::per_track ? writer.get_rounding_policy(track_index) : rounding_policy;
+		ACL_ASSERT(rounding_policy_ != sample_rounding_policy::per_track, "track_writer::get_rounding_policy() cannot return per_track");
+
 		uint32_t key_frame0;
 		uint32_t key_frame1;
 		float interpolation_alpha;
-		find_linear_interpolation_samples_with_sample_rate(num_samples, sample_rate, sample_time, rounding_policy, m_looping_policy, key_frame0, key_frame1, interpolation_alpha);
+		find_linear_interpolation_samples_with_sample_rate(num_samples, sample_rate, sample_time, rounding_policy_, m_looping_policy, key_frame0, key_frame1, interpolation_alpha);
 
 		switch (track_.get_type())
 		{
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
@@ -27,7 +27,7 @@
 #include "acl/core/impl/compiler_utils.h"
 #include "acl/core/iallocator.h"
 #include "acl/core/track_formats.h"
-#include "acl/core/variable_bit_rates.h"
+#include "acl/core/impl/variable_bit_rates.h"
 #include "acl/compression/impl/sample_streams.h"
 #include "acl/compression/impl/track_stream.h"
 
@@ -66,7 +66,7 @@ namespace acl
 			}
 
 			void bind(track_bit_rate_database& database);
-			void build(uint32_t track_index, const BoneBitRate* bit_rates, const transform_streams* bone_streams);
+			void build(uint32_t track_index, const transform_bit_rates* bit_rates, const transform_streams* bone_streams);
 
 		private:
 			hierarchical_track_query(const hierarchical_track_query&) = delete;
@@ -82,7 +82,7 @@ namespace acl
 			iallocator&						m_allocator;
 			track_bit_rate_database*		m_database;
 			uint32_t						m_track_index;
-			const BoneBitRate*				m_bit_rates;
+			const transform_bit_rates*				m_bit_rates;
 			transform_indices*				m_indices;
 			uint32_t						m_num_transforms;
 
@@ -102,10 +102,10 @@ namespace acl
 			{}
 
 			inline uint32_t get_track_index() const { return m_track_index; }
-			inline const BoneBitRate& get_bit_rates() const { return m_bit_rates; }
+			inline const transform_bit_rates& get_bit_rates() const { return m_bit_rates; }
 
 			void bind(track_bit_rate_database& database);
-			void build(uint32_t track_index, const BoneBitRate& bit_rates);
+			void build(uint32_t track_index, const transform_bit_rates& bit_rates);
 
 		private:
 			single_track_query(const single_track_query&) = delete;
@@ -113,7 +113,7 @@ namespace acl
 
 			track_bit_rate_database*		m_database;
 			uint32_t						m_track_index;
-			BoneBitRate						m_bit_rates;
+			transform_bit_rates						m_bit_rates;
 
 			uint32_t						m_rotation_cache_index;
 			uint32_t						m_translation_cache_index;
@@ -139,7 +139,7 @@ namespace acl
 			}
 
 			void bind(track_bit_rate_database& database);
-			void build(const BoneBitRate* bit_rates);
+			void build(const transform_bit_rates* bit_rates);
 
 		private:
 			every_track_query(const every_track_query&) = delete;
@@ -154,7 +154,7 @@ namespace acl
 
 			iallocator&						m_allocator;
 			track_bit_rate_database*		m_database;
-			const BoneBitRate*				m_bit_rates;
+			const transform_bit_rates*				m_bit_rates;
 			transform_indices*				m_indices;
 			uint32_t						m_num_transforms;
 
@@ -168,7 +168,7 @@ namespace acl
 			uint8_t bit_rates[4];
 
 			bit_rates_union() : value(0xFFFFFFFFU) {}
-			explicit bit_rates_union(const BoneBitRate& input) : bit_rates{ input.rotation, input.translation, input.scale, 0 } {}
+			explicit bit_rates_union(const transform_bit_rates& input) : bit_rates{ input.rotation, input.translation, input.scale, 0 } {}
 
 			inline bool operator==(bit_rates_union other) const { return value == other.value; }
 			inline bool operator!=(bit_rates_union other) const { return value != other.value; }
@@ -196,7 +196,7 @@ namespace acl
 			track_bit_rate_database(const track_bit_rate_database&) = delete;
 			track_bit_rate_database& operator=(const track_bit_rate_database&) = delete;
 
-			void find_cache_entries(uint32_t track_index, const BoneBitRate& bit_rates, uint32_t& out_rotation_cache_index, uint32_t& out_translation_cache_index, uint32_t& out_scale_cache_index);
+			void find_cache_entries(uint32_t track_index, const transform_bit_rates& bit_rates, uint32_t& out_rotation_cache_index, uint32_t& out_translation_cache_index, uint32_t& out_scale_cache_index);
 
 			RTM_FORCE_INLINE rtm::quatf RTM_SIMD_CALL sample_rotation(const sample_context& context, uint32_t rotation_cache_index);
 			RTM_FORCE_INLINE rtm::vector4f RTM_SIMD_CALL sample_translation(const sample_context& context, uint32_t translation_cache_index);
@@ -278,7 +278,7 @@ namespace acl
 			m_num_transforms = database.m_num_transforms;
 		}
 
-		inline void hierarchical_track_query::build(uint32_t track_index, const BoneBitRate* bit_rates, const transform_streams* bone_streams)
+		inline void hierarchical_track_query::build(uint32_t track_index, const transform_bit_rates* bit_rates, const transform_streams* bone_streams)
 		{
 			ACL_ASSERT(m_database != nullptr, "Query not bound to a database");
 			ACL_ASSERT(track_index < m_num_transforms, "Invalid track index");
@@ -289,7 +289,7 @@ namespace acl
 			uint32_t current_track_index = track_index;
 			while (current_track_index != k_invalid_track_index)
 			{
-				const BoneBitRate& current_bit_rates = bit_rates[current_track_index];
+				const transform_bit_rates& current_bit_rates = bit_rates[current_track_index];
 				transform_indices& indices = m_indices[current_track_index];
 
 				m_database->find_cache_entries(current_track_index, current_bit_rates, indices.rotation_cache_index, indices.translation_cache_index, indices.scale_cache_index);
@@ -305,7 +305,7 @@ namespace acl
 			m_database = &database;
 		}
 
-		inline void single_track_query::build(uint32_t track_index, const BoneBitRate& bit_rates)
+		inline void single_track_query::build(uint32_t track_index, const transform_bit_rates& bit_rates)
 		{
 			ACL_ASSERT(m_database != nullptr, "Query not bound to a database");
 
@@ -324,7 +324,7 @@ namespace acl
 			m_num_transforms = database.m_num_transforms;
 		}
 
-		inline void every_track_query::build(const BoneBitRate* bit_rates)
+		inline void every_track_query::build(const transform_bit_rates* bit_rates)
 		{
 			ACL_ASSERT(m_database != nullptr, "Query not bound to a database");
 
@@ -332,7 +332,7 @@ namespace acl
 
 			for (uint32_t transform_index = 0; transform_index < m_num_transforms; ++transform_index)
 			{
-				const BoneBitRate& current_bit_rates = bit_rates[transform_index];
+				const transform_bit_rates& current_bit_rates = bit_rates[transform_index];
 				transform_indices& indices = m_indices[transform_index];
 
 				m_database->find_cache_entries(transform_index, current_bit_rates, indices.rotation_cache_index, indices.translation_cache_index, indices.scale_cache_index);
@@ -422,7 +422,7 @@ namespace acl
 #endif
 		}
 
-		inline void track_bit_rate_database::find_cache_entries(uint32_t track_index, const BoneBitRate& bit_rates, uint32_t& out_rotation_cache_index, uint32_t& out_translation_cache_index, uint32_t& out_scale_cache_index)
+		inline void track_bit_rate_database::find_cache_entries(uint32_t track_index, const transform_bit_rates& bit_rates, uint32_t& out_rotation_cache_index, uint32_t& out_translation_cache_index, uint32_t& out_scale_cache_index)
 		{
 			// Memory layout:
 			//    track 0
diff --git a/includes/acl/compression/impl/track_list_context.h b/includes/acl/compression/impl/track_list_context.h
--- a/includes/acl/compression/impl/track_list_context.h
+++ b/includes/acl/compression/impl/track_list_context.h
@@ -28,7 +28,7 @@
 #include "acl/core/iallocator.h"
 #include "acl/core/bitset.h"
 #include "acl/core/track_desc.h"
-#include "acl/core/variable_bit_rates.h"
+#include "acl/core/impl/variable_bit_rates.h"
 #include "acl/compression/track_array.h"
 #include "acl/compression/impl/track_range.h"
 
diff --git a/includes/acl/compression/impl/track_stream.h b/includes/acl/compression/impl/track_stream.h
--- a/includes/acl/compression/impl/track_stream.h
+++ b/includes/acl/compression/impl/track_stream.h
@@ -30,7 +30,7 @@
 #include "acl/core/time_utils.h"
 #include "acl/core/track_formats.h"
 #include "acl/core/track_types.h"
-#include "acl/core/variable_bit_rates.h"
+#include "acl/core/impl/variable_bit_rates.h"
 #include "acl/math/quat_packing.h"
 #include "acl/math/vector4_packing.h"
 
diff --git a/includes/acl/compression/impl/write_range_data.h b/includes/acl/compression/impl/write_range_data.h
--- a/includes/acl/compression/impl/write_range_data.h
+++ b/includes/acl/compression/impl/write_range_data.h
@@ -31,7 +31,7 @@
 #include "acl/core/track_formats.h"
 #include "acl/core/track_types.h"
 #include "acl/core/range_reduction_types.h"
-#include "acl/core/variable_bit_rates.h"
+#include "acl/core/impl/variable_bit_rates.h"
 #include "acl/math/quat_packing.h"
 #include "acl/math/vector4_packing.h"
 #include "acl/compression/impl/animated_track_utils.h"
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
@@ -28,7 +28,7 @@
 
 #include "acl/core/time_utils.h"
 #include "acl/core/track_formats.h"
-#include "acl/core/variable_bit_rates.h"
+#include "acl/core/impl/variable_bit_rates.h"
 #include "acl/core/impl/compiler_utils.h"
 #include "acl/compression/transform_error_metrics.h"
 #include "acl/compression/track_error.h"
diff --git a/includes/acl/compression/impl/write_stream_data.h b/includes/acl/compression/impl/write_stream_data.h
--- a/includes/acl/compression/impl/write_stream_data.h
+++ b/includes/acl/compression/impl/write_stream_data.h
@@ -28,7 +28,7 @@
 #include "acl/core/iallocator.h"
 #include "acl/core/error.h"
 #include "acl/core/track_formats.h"
-#include "acl/core/variable_bit_rates.h"
+#include "acl/core/impl/variable_bit_rates.h"
 #include "acl/compression/impl/animated_track_utils.h"
 #include "acl/compression/impl/clip_context.h"
 
diff --git a/includes/acl/compression/impl/write_track_data_impl.h b/includes/acl/compression/impl/write_track_data_impl.h
--- a/includes/acl/compression/impl/write_track_data_impl.h
+++ b/includes/acl/compression/impl/write_track_data_impl.h
@@ -24,9 +24,9 @@
 // SOFTWARE.
 ////////////////////////////////////////////////////////////////////////////////
 
-#include "acl/core/variable_bit_rates.h"
 #include "acl/core/impl/compiler_utils.h"
 #include "acl/core/impl/compressed_headers.h"
+#include "acl/core/impl/variable_bit_rates.h"
 #include "acl/compression/impl/track_list_context.h"
 
 #include <rtm/vector4f.h>
diff --git a/includes/acl/core/impl/debug_track_writer.h b/includes/acl/core/impl/debug_track_writer.h
--- a/includes/acl/core/impl/debug_track_writer.h
+++ b/includes/acl/core/impl/debug_track_writer.h
@@ -280,6 +280,22 @@ namespace acl
 
 			const rtm::qvvf* default_sub_tracks;
 		};
+
+		//////////////////////////////////////////////////////////////////////////
+		// Same as debug_track_writer but the rounding policy is supported per track.
+		//////////////////////////////////////////////////////////////////////////
+		struct debug_track_writer_per_track_rounding final : public debug_track_writer
+		{
+			debug_track_writer_per_track_rounding(iallocator& allocator_, track_type8 type_, uint32_t num_tracks_)
+				: debug_track_writer(allocator_, type_, num_tracks_)
+				, rounding_policy(sample_rounding_policy::per_track)
+			{
+			}
+
+			sample_rounding_policy get_rounding_policy(uint32_t /*track_index*/) const { return rounding_policy; }
+
+			sample_rounding_policy rounding_policy;
+		};
 	}
 }
 
diff --git a/includes/acl/core/impl/interpolation_utils.impl.h b/includes/acl/core/impl/interpolation_utils.impl.h
--- a/includes/acl/core/impl/interpolation_utils.impl.h
+++ b/includes/acl/core/impl/interpolation_utils.impl.h
@@ -111,23 +111,7 @@ namespace acl
 
 		out_sample_index0 = sample_index0;
 		out_sample_index1 = sample_index1;
-
-		switch (rounding_policy)
-		{
-		default:
-		case sample_rounding_policy::none:
-			out_interpolation_alpha = interpolation_alpha;
-			break;
-		case sample_rounding_policy::floor:
-			out_interpolation_alpha = 0.0F;
-			break;
-		case sample_rounding_policy::ceil:
-			out_interpolation_alpha = 1.0F;
-			break;
-		case sample_rounding_policy::nearest:
-			out_interpolation_alpha = rtm::scalar_floor(interpolation_alpha + 0.5F);
-			break;
-		}
+		out_interpolation_alpha = apply_rounding_policy(interpolation_alpha, rounding_policy);
 	}
 
 	//////////////////////////////////////////////////////////////////////////
@@ -210,23 +194,7 @@ namespace acl
 
 		out_sample_index0 = sample_index0;
 		out_sample_index1 = sample_index1;
-
-		switch (rounding_policy)
-		{
-		default:
-		case sample_rounding_policy::none:
-			out_interpolation_alpha = interpolation_alpha;
-			break;
-		case sample_rounding_policy::floor:
-			out_interpolation_alpha = 0.0F;
-			break;
-		case sample_rounding_policy::ceil:
-			out_interpolation_alpha = 1.0F;
-			break;
-		case sample_rounding_policy::nearest:
-			out_interpolation_alpha = rtm::scalar_floor(interpolation_alpha + 0.5F);
-			break;
-		}
+		out_interpolation_alpha = apply_rounding_policy(interpolation_alpha, rounding_policy);
 	}
 
 	//////////////////////////////////////////////////////////////////////////
@@ -266,11 +234,32 @@ namespace acl
 		const float interpolation_alpha = (sample_index - float(sample_index0)) / float(sample_index1 - sample_index0);
 		ACL_ASSERT(interpolation_alpha >= 0.0F && interpolation_alpha <= 1.0F, "Invalid interpolation alpha: 0.0 <= %f <= 1.0", interpolation_alpha);
 
-		if (rounding_policy == sample_rounding_policy::none)
+		// If we don't round, we'll interpolate and we need the alpha value unchanged
+		// If we require the value per track, we might need the alpha value unchanged as well, rounding is handled later
+		if (rounding_policy == sample_rounding_policy::none || rounding_policy == sample_rounding_policy::per_track)
 			return interpolation_alpha;
 		else // sample_rounding_policy::nearest
 			return rtm::scalar_floor(interpolation_alpha + 0.5F);
 	}
+
+	inline float apply_rounding_policy(float interpolation_alpha, sample_rounding_policy policy)
+	{
+		switch (policy)
+		{
+		default:
+		case sample_rounding_policy::none:
+		case sample_rounding_policy::per_track:
+			// If we don't round, we'll interpolate and we need the alpha value unchanged
+			// If we require the value per track, we might need the alpha value unchanged as well, rounding is handled later
+			return interpolation_alpha;
+		case sample_rounding_policy::floor:
+			return 0.0F;
+		case sample_rounding_policy::ceil:
+			return 1.0F;
+		case sample_rounding_policy::nearest:
+			return rtm::scalar_floor(interpolation_alpha + 0.5F);
+		}
+	}
 }
 
 ACL_IMPL_FILE_PRAGMA_POP
diff --git a/includes/acl/core/impl/variable_bit_rates.h b/includes/acl/core/impl/variable_bit_rates.h
new file mode 100644
--- /dev/null
+++ b/includes/acl/core/impl/variable_bit_rates.h
@@ -0,0 +1,67 @@
+#pragma once
+
+////////////////////////////////////////////////////////////////////////////////
+// The MIT License (MIT)
+//
+// Copyright (c) 2020 Nicholas Frechette & Animation Compression Library contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+////////////////////////////////////////////////////////////////////////////////
+
+#include "acl/core/error.h"
+#include "acl/core/impl/compiler_utils.h"
+
+#include <cstdint>
+
+ACL_IMPL_FILE_PRAGMA_PUSH
+
+namespace acl
+{
+	namespace acl_impl
+	{
+		// Bit rate 0 is reserved for tracks that are constant in a segment
+		constexpr uint8_t k_bit_rate_num_bits[] = { 0, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 32 };
+
+		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 uint32_t k_num_bit_rates = sizeof(k_bit_rate_num_bits);
+
+		static_assert(k_num_bit_rates == 19, "Expecting 19 bit rates");
+
+		inline uint32_t get_num_bits_at_bit_rate(uint32_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];
+		}
+
+		// Track is constant, our constant sample is stored in the range information
+		constexpr bool is_constant_bit_rate(uint32_t bit_rate) { return bit_rate == 0; }
+		constexpr bool is_raw_bit_rate(uint32_t bit_rate) { return bit_rate == k_highest_bit_rate; }
+
+		struct transform_bit_rates
+		{
+			uint8_t rotation;
+			uint8_t translation;
+			uint8_t scale;
+		};
+	}
+}
+
+ACL_IMPL_FILE_PRAGMA_POP
diff --git a/includes/acl/core/interpolation_utils.h b/includes/acl/core/interpolation_utils.h
--- a/includes/acl/core/interpolation_utils.h
+++ b/includes/acl/core/interpolation_utils.h
@@ -104,6 +104,10 @@ namespace acl
 	// interpolate when the samples are uniform.
 	// This function does not support looping.
 	float find_linear_interpolation_alpha(float sample_index, uint32_t sample_index0, uint32_t sample_index1, sample_rounding_policy rounding_policy);
+
+	//////////////////////////////////////////////////////////////////////////
+	// Modify an interpolation alpha value given a sample rounding policy
+	float apply_rounding_policy(float interpolation_alpha, sample_rounding_policy policy);
 }
 
 #include "acl/core/impl/interpolation_utils.impl.h"
diff --git a/includes/acl/core/iterator.h b/includes/acl/core/iterator.h
--- a/includes/acl/core/iterator.h
+++ b/includes/acl/core/iterator.h
@@ -57,6 +57,18 @@ namespace acl
 
 	template <class item_type>
 	using const_iterator = acl_impl::iterator_impl<item_type, true>;
+
+	template <class item_type, size_t num_items>
+	iterator<item_type> make_iterator(item_type (&items)[num_items])
+	{
+		return iterator<item_type>(items, num_items);
+	}
+
+	template <class item_type, size_t num_items>
+	const_iterator<item_type> make_iterator(item_type const (&items)[num_items])
+	{
+		return const_iterator<item_type>(items, num_items);
+	}
 }
 
 ACL_IMPL_FILE_PRAGMA_POP
diff --git a/includes/acl/core/sample_rounding_policy.h b/includes/acl/core/sample_rounding_policy.h
--- a/includes/acl/core/sample_rounding_policy.h
+++ b/includes/acl/core/sample_rounding_policy.h
@@ -30,29 +30,78 @@ namespace acl
 {
 	//////////////////////////////////////////////////////////////////////////
 	// This enum dictates how interpolation samples are calculated based on the sample time.
+	// DO NOT CHANGE THESE VALUES AS THEY ARE USED TO INDEX INTO FIXED SIZED BUFFERS THAT MAY NOT
+	// ACCOMODATE ALL VALUES.
 	enum class sample_rounding_policy
 	{
 		//////////////////////////////////////////////////////////////////////////
 		// If the sample time lies between two samples, both sample indices
 		// are returned and the interpolation alpha lies in between.
-		none,
+		none = 0,
 
 		//////////////////////////////////////////////////////////////////////////
 		// If the sample time lies between two samples, both sample indices
 		// are returned and the interpolation will be 0.0.
-		floor,
+		// 
+		// Note that when this is used to sample a clip with a partially streamed database
+		// the sample returned is the same that would have been had the database been
+		// fully streamed in. Meaning that if we have 3 samples A, B, C and you sample
+		// between B and C, normally B would be returned. If B has been moved into the
+		// database and is missing, B will be reconstructed through interpolation.
+		floor = 1,
 
 		//////////////////////////////////////////////////////////////////////////
 		// If the sample time lies between two samples, both sample indices
 		// are returned and the interpolation will be 1.0.
-		ceil,
+		// 
+		// Note that when this is used to sample a clip with a partially streamed database
+		// the sample returned is the same that would have been had the database been
+		// fully streamed in. Meaning that if we have 3 samples A, B, C and you sample
+		// between A and B, normally B would be returned. If B has been moved into the
+		// database and is missing, B will be reconstructed through interpolation.
+		ceil = 2,
 
 		//////////////////////////////////////////////////////////////////////////
 		// If the sample time lies between two samples, both sample indices
 		// are returned and the interpolation will be 0.0 or 1.0 depending
 		// on which sample is nearest.
-		nearest,
+		// 
+		// Note that this behaves similarly to floor and ceil above when used with a partially
+		// streamed database.
+		nearest = 3,
+
+		//////////////////////////////////////////////////////////////////////////
+		// Specifies that the rounding policy must be queried for every track independently.
+		// Note that this feature can be enabled/disabled in the 'decompression_settings' struct
+		// provided during decompression. Since this is a niche feature, the default settings
+		// disable it.
+		// 
+		// WARNING: Using per track rounding may not behave as intended if a partially streamed
+		// database is used. For performance reasons, unlike the modes above, only a single value
+		// will be interpolated: the one at the specified sample time. This means that if we have
+		// 3 samples A, B, C and you sample between B and C with 'floor', if B has been moved to
+		// a database and is missing, B (interpolated) is not returned. Normally, A and C would be
+		// used to interpolate at the sample time specified as such we do not calculate where B lies.
+		// As a result of this, A would be returned unlike the behavior of 'floor' when used to sample
+		// all tracks. This is the behavior chosen by design. During decompression, samples are
+		// unpacked and interpolated before we know which track they belong to. As such, when the
+		// per track mode is used, we output 4 samples (instead of just the one we need), one for
+		// each possible mode above. One we know which sample we need (among the 4), we can simply
+		// index with the rounding mode to grab it. This is very fast and the cost is largely hidden.
+		// Supporting the same behavior as the rounding modes above when a partially streamed in
+		// database is used would require us to interpolate 3 samples instead of 1 which would be
+		// a lot more expensive for rotation sub-tracks. It would also add a lot of code complexity.
+		// For those reasons, the behavior differs. A future task will allow tracks to be split
+		// into different layers where database settings can be independent. This will allow us
+		// to place special tracks into their own layer where key frames are not removed. A separate
+		// task may allow us to specify per track whether the values can be interpolated or not.
+		// This would allow us to detect boundary key frames (where the value changes) and retain
+		// those we need to ensure that the end result is identical.
+		per_track = 4,
 	};
+
+	// The number of sample rounding policies
+	constexpr size_t k_num_sample_rounding_policies = 5;
 }
 
 ACL_IMPL_FILE_PRAGMA_POP
diff --git a/includes/acl/core/track_writer.h b/includes/acl/core/track_writer.h
--- a/includes/acl/core/track_writer.h
+++ b/includes/acl/core/track_writer.h
@@ -77,6 +77,17 @@ namespace acl
 	//////////////////////////////////////////////////////////////////////////
 	struct track_writer
 	{
+		//////////////////////////////////////////////////////////////////////////
+		// Common track writing
+
+		//////////////////////////////////////////////////////////////////////////
+		// This allows the host runtime to control which rounding policy to use per track.
+		// To enable this, make sure that the 'decompression_settings' supports this feature
+		// and provide 'sample_rounding_policy::per_track' to the seek function.
+		// This function cannot return the 'per_track' value. We do so here to force an
+		// assert at runtime if the caller attempts to use it but does not override this.
+		constexpr sample_rounding_policy get_rounding_policy(uint32_t /*track_index*/) const { return sample_rounding_policy::per_track; }
+
 		//////////////////////////////////////////////////////////////////////////
 		// Scalar track writing
 
@@ -146,7 +157,7 @@ namespace acl
 		rtm::vector4f RTM_SIMD_CALL get_variable_default_scale(uint32_t /*track_index*/) const { return rtm::vector_set(1.0F); }
 
 		//////////////////////////////////////////////////////////////////////////
-		// These allow the caller of decompress_pose to control which track types they are interested in.
+		// These allow host runtimes to control which track types they are interested in.
 		// This information allows the codecs to avoid unpacking values that are not needed.
 		// Must be static constexpr!
 		static constexpr bool skip_all_rotations() { return false; }
@@ -154,7 +165,7 @@ namespace acl
 		static constexpr bool skip_all_scales() { return false; }
 
 		//////////////////////////////////////////////////////////////////////////
-		// These allow the caller of decompress_pose to control which tracks they are interested in.
+		// These allow host runtimes to control which tracks they are interested in.
 		// This information allows the codecs to avoid unpacking values that are not needed.
 		// Must be non-static member functions!
 		constexpr bool skip_track_rotation(uint32_t /*track_index*/) const { return false; }
diff --git a/includes/acl/core/variable_bit_rates.h b/includes/acl/core/variable_bit_rates.h
--- a/includes/acl/core/variable_bit_rates.h
+++ b/includes/acl/core/variable_bit_rates.h
@@ -34,25 +34,37 @@ ACL_IMPL_FILE_PRAGMA_PUSH
 namespace acl
 {
 	// Bit rate 0 is reserved for tracks that are constant in a segment
+	ACL_DEPRECATED("Internal implementation detail; to be removed in v3.0")
 	constexpr uint8_t k_bit_rate_num_bits[] = { 0, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 32 };
 
+	ACL_DEPRECATED("Internal implementation detail; to be removed in v3.0")
 	constexpr uint8_t k_invalid_bit_rate = 0xFF;
+
+	ACL_DEPRECATED("Internal implementation detail; to be removed in v3.0")
 	constexpr uint8_t k_lowest_bit_rate = 1;
-	constexpr uint8_t k_highest_bit_rate = sizeof(k_bit_rate_num_bits) - 1;
-	constexpr uint32_t k_num_bit_rates = sizeof(k_bit_rate_num_bits);
 
-	static_assert(k_num_bit_rates == 19, "Expecting 19 bit rates");
+	ACL_DEPRECATED("Internal implementation detail; to be removed in v3.0")
+	constexpr uint8_t k_highest_bit_rate = 18;
+
+	ACL_DEPRECATED("Internal implementation detail; to be removed in v3.0")
+	constexpr uint32_t k_num_bit_rates = 19;
 
+	ACL_DEPRECATED("Internal implementation detail; to be removed in v3.0")
 	inline uint32_t get_num_bits_at_bit_rate(uint32_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];
+		ACL_ASSERT(bit_rate <= 18, "Invalid bit rate: %u", bit_rate);
+		constexpr uint8_t bit_rate_num_bits[] = { 0, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 32 };
+		return bit_rate_num_bits[bit_rate];
 	}
 
 	// Track is constant, our constant sample is stored in the range information
+	ACL_DEPRECATED("Internal implementation detail; to be removed in v3.0")
 	constexpr bool is_constant_bit_rate(uint32_t bit_rate) { return bit_rate == 0; }
-	constexpr bool is_raw_bit_rate(uint32_t bit_rate) { return bit_rate == k_highest_bit_rate; }
 
+	ACL_DEPRECATED("Internal implementation detail; to be removed in v3.0")
+	constexpr bool is_raw_bit_rate(uint32_t bit_rate) { return bit_rate == 18; }
+
+	ACL_DEPRECATED("Internal implementation detail; to be removed in v3.0")
 	struct BoneBitRate
 	{
 		uint8_t rotation;
diff --git a/includes/acl/decompression/decompression_settings.h b/includes/acl/decompression/decompression_settings.h
--- a/includes/acl/decompression/decompression_settings.h
+++ b/includes/acl/decompression/decompression_settings.h
@@ -118,6 +118,15 @@ namespace acl
 		// Must be static constexpr!
 		static constexpr bool is_wrapping_supported() { return true; }
 
+		//////////////////////////////////////////////////////////////////////////
+		// Whether or not to enable 'sample_rounding_policy::per_track' support.
+		// If support is disabled and that value is provided to the seek(..) function,
+		// the runtime will assert.
+		// See 'sample_rounding_policy' for details.
+		// Enabled by default.
+		// Must be static constexpr!
+		static constexpr bool is_per_track_rounding_supported() { return true; }
+
 		//////////////////////////////////////////////////////////////////////////
 		// The database settings to use when decompressing.
 		// By default, the database isn't supported.
@@ -156,6 +165,10 @@ namespace acl
 		//////////////////////////////////////////////////////////////////////////
 		// Only support scalar tracks
 		static constexpr bool is_track_type_supported(track_type8 type) { return type != track_type8::qvvf; }
+
+		//////////////////////////////////////////////////////////////////////////
+		// Disabled by default since it is an uncommon feature
+		static constexpr bool is_per_track_rounding_supported() { return false; }
 	};
 
 	//////////////////////////////////////////////////////////////////////////
@@ -174,6 +187,10 @@ namespace acl
 		static constexpr bool is_rotation_format_supported(rotation_format8 format) { return format == rotation_format8::quatf_drop_w_variable; }
 		static constexpr bool is_translation_format_supported(vector_format8 format) { return format == vector_format8::vector3f_variable; }
 		static constexpr bool is_scale_format_supported(vector_format8 format) { return format == vector_format8::vector3f_variable; }
+
+		//////////////////////////////////////////////////////////////////////////
+		// Disabled by default since it is an uncommon feature
+		static constexpr bool is_per_track_rounding_supported() { return false; }
 	};
 }
 
diff --git a/includes/acl/decompression/impl/decompress.impl.h b/includes/acl/decompression/impl/decompress.impl.h
--- a/includes/acl/decompression/impl/decompress.impl.h
+++ b/includes/acl/decompression/impl/decompress.impl.h
@@ -120,6 +120,7 @@ namespace acl
 	{
 		ACL_ASSERT(m_context.is_initialized(), "Context is not initialized");
 		ACL_ASSERT(rtm::scalar_is_finite(sample_time), "Invalid sample time");
+		ACL_ASSERT(rounding_policy != sample_rounding_policy::per_track || decompression_settings_type::is_per_track_rounding_supported(), "Per track rounding must be enabled");
 
 		if (!m_context.is_initialized())
 			return;	// Context is not initialized
diff --git a/includes/acl/decompression/impl/scalar_track_decompression.h b/includes/acl/decompression/impl/scalar_track_decompression.h
--- a/includes/acl/decompression/impl/scalar_track_decompression.h
+++ b/includes/acl/decompression/impl/scalar_track_decompression.h
@@ -28,8 +28,8 @@
 #include "acl/core/compressed_tracks_version.h"
 #include "acl/core/interpolation_utils.h"
 #include "acl/core/track_writer.h"
-#include "acl/core/variable_bit_rates.h"
 #include "acl/core/impl/compiler_utils.h"
+#include "acl/core/impl/variable_bit_rates.h"
 #include "acl/decompression/database/database.h"
 #include "acl/math/scalar_packing.h"
 #include "acl/math/vector4_packing.h"
@@ -71,14 +71,16 @@ namespace acl
 			uint32_t key_frame_bit_offsets[2];						//  20 |  24	// Variable quantization
 
 			uint8_t looping_policy;									//  28 |  32
+			uint8_t rounding_policy;								//  29 |  33
 
-			uint8_t padding_tail[sizeof(void*) == 4 ? 35 : 31];		//  29 |  33
+			uint8_t padding_tail[sizeof(void*) == 4 ? 34 : 30];		//  30 |  34
 
 			//////////////////////////////////////////////////////////////////////////
 
 			const compressed_tracks* get_compressed_tracks() const { return tracks; }
 			compressed_tracks_version16 get_version() const { return tracks->get_version(); }
 			sample_looping_policy get_looping_policy() const { return static_cast<sample_looping_policy>(looping_policy); }
+			sample_rounding_policy get_rounding_policy() const { return static_cast<sample_rounding_policy>(rounding_policy); }
 			bool is_initialized() const { return tracks != nullptr; }
 			void reset() { tracks = nullptr; }
 		};
@@ -96,7 +98,6 @@ namespace acl
 			context.tracks = &tracks;
 			context.tracks_hash = tracks.get_hash();
 			context.sample_time = -1.0F;
-			context.interpolation_alpha = 0.0;
 
 			if (decompression_settings_type::is_wrapping_supported())
 			{
@@ -152,7 +153,7 @@ namespace acl
 			if (decompression_settings_type::clamp_sample_time())
 				sample_time = rtm::scalar_clamp(sample_time, 0.0F, context.duration);
 
-			if (context.sample_time == sample_time)
+			if (context.sample_time == sample_time && context.get_rounding_policy() == rounding_policy)
 				return;
 
 			context.sample_time = sample_time;
@@ -164,6 +165,8 @@ namespace acl
 			uint32_t key_frame1;
 			find_linear_interpolation_samples_with_sample_rate(header.num_samples, header.sample_rate, sample_time, rounding_policy, looping_policy_, key_frame0, key_frame1, context.interpolation_alpha);
 
+			context.rounding_policy = static_cast<uint8_t>(rounding_policy);
+
 			const acl_impl::scalar_tracks_header& scalars_header = acl_impl::get_scalar_tracks_header(*context.tracks);
 
 			context.key_frame_bit_offsets[0] = key_frame0 * scalars_header.num_bits_per_frame;
@@ -191,6 +194,22 @@ namespace acl
 			const acl_impl::scalar_tracks_header& scalars_header = acl_impl::get_scalar_tracks_header(*context.tracks);
 			const rtm::scalarf interpolation_alpha = rtm::scalar_set(context.interpolation_alpha);
 
+			const sample_rounding_policy rounding_policy = static_cast<sample_rounding_policy>(context.rounding_policy);
+
+			float interpolation_alpha_per_policy[k_num_sample_rounding_policies];
+			if (decompression_settings_type::is_per_track_rounding_supported())
+			{
+				const float alpha = context.interpolation_alpha;
+				const float no_rounding_alpha = apply_rounding_policy(alpha, sample_rounding_policy::none);
+
+				interpolation_alpha_per_policy[static_cast<int>(sample_rounding_policy::none)] = no_rounding_alpha;
+				interpolation_alpha_per_policy[static_cast<int>(sample_rounding_policy::floor)] = apply_rounding_policy(alpha, sample_rounding_policy::floor);
+				interpolation_alpha_per_policy[static_cast<int>(sample_rounding_policy::ceil)] = apply_rounding_policy(alpha, sample_rounding_policy::ceil);
+				interpolation_alpha_per_policy[static_cast<int>(sample_rounding_policy::nearest)] = apply_rounding_policy(alpha, sample_rounding_policy::nearest);
+				// We'll assert if we attempt to use this, but in case they are skipped/disabled, we interpolate
+				interpolation_alpha_per_policy[static_cast<int>(sample_rounding_policy::per_track)] = no_rounding_alpha;
+			}
+
 			const acl_impl::track_metadata* per_track_metadata = scalars_header.get_track_metadata();
 			const float* constant_values = scalars_header.get_track_constant_values();
 			const float* range_values = scalars_header.get_track_range_values();
@@ -207,6 +226,15 @@ namespace acl
 				const uint8_t bit_rate = metadata.bit_rate;
 				const uint32_t num_bits_per_component = get_num_bits_at_bit_rate(bit_rate);
 
+				rtm::scalarf alpha = interpolation_alpha;
+				if (decompression_settings_type::is_per_track_rounding_supported())
+				{
+					const sample_rounding_policy rounding_policy_ = rounding_policy == sample_rounding_policy::per_track ? writer.get_rounding_policy(track_index) : rounding_policy;
+					ACL_ASSERT(rounding_policy_ != sample_rounding_policy::per_track, "track_writer::get_rounding_policy() cannot return per_track");
+
+					alpha = rtm::scalar_set(interpolation_alpha_per_policy[static_cast<int>(rounding_policy_)]);
+				}
+
 				if (track_type == track_type8::float1f && decompression_settings_type::is_track_type_supported(track_type8::float1f))
 				{
 					rtm::scalarf value;
@@ -236,7 +264,7 @@ namespace acl
 							range_values += 2;
 						}
 
-						value = rtm::scalar_lerp(value0, value1, interpolation_alpha);
+						value = rtm::scalar_lerp(value0, value1, alpha);
 
 						const uint32_t num_sample_bits = num_bits_per_component;
 						track_bit_offset0 += num_sample_bits;
@@ -274,7 +302,7 @@ namespace acl
 							range_values += 4;
 						}
 
-						value = rtm::vector_lerp(value0, value1, interpolation_alpha);
+						value = rtm::vector_lerp(value0, value1, alpha);
 
 						const uint32_t num_sample_bits = num_bits_per_component * 2;
 						track_bit_offset0 += num_sample_bits;
@@ -312,7 +340,7 @@ namespace acl
 							range_values += 6;
 						}
 
-						value = rtm::vector_lerp(value0, value1, interpolation_alpha);
+						value = rtm::vector_lerp(value0, value1, alpha);
 
 						const uint32_t num_sample_bits = num_bits_per_component * 3;
 						track_bit_offset0 += num_sample_bits;
@@ -350,7 +378,7 @@ namespace acl
 							range_values += 8;
 						}
 
-						value = rtm::vector_lerp(value0, value1, interpolation_alpha);
+						value = rtm::vector_lerp(value0, value1, alpha);
 
 						const uint32_t num_sample_bits = num_bits_per_component * 4;
 						track_bit_offset0 += num_sample_bits;
@@ -388,7 +416,7 @@ namespace acl
 							range_values += 8;
 						}
 
-						value = rtm::vector_lerp(value0, value1, interpolation_alpha);
+						value = rtm::vector_lerp(value0, value1, alpha);
 
 						const uint32_t num_sample_bits = num_bits_per_component * 4;
 						track_bit_offset0 += num_sample_bits;
@@ -425,7 +453,16 @@ namespace acl
 				disable_fp_exceptions(fp_env);
 
 			const scalar_tracks_header& scalars_header = get_scalar_tracks_header(*context.tracks);
-			const rtm::scalarf interpolation_alpha = rtm::scalar_set(context.interpolation_alpha);
+
+			rtm::scalarf interpolation_alpha = rtm::scalar_set(context.interpolation_alpha);
+			if (decompression_settings_type::is_per_track_rounding_supported())
+			{
+				const sample_rounding_policy rounding_policy = static_cast<sample_rounding_policy>(context.rounding_policy);
+				const sample_rounding_policy rounding_policy_ = rounding_policy == sample_rounding_policy::per_track ? writer.get_rounding_policy(track_index) : rounding_policy;
+				ACL_ASSERT(rounding_policy_ != sample_rounding_policy::per_track, "track_writer::get_rounding_policy() cannot return per_track");
+				
+				interpolation_alpha = rtm::scalar_set(apply_rounding_policy(context.interpolation_alpha, rounding_policy_));
+			}
 
 			const float* constant_values = scalars_header.get_track_constant_values();
 			const float* range_values = scalars_header.get_track_range_values();
diff --git a/includes/acl/decompression/impl/track_cache.h b/includes/acl/decompression/impl/track_cache.h
--- a/includes/acl/decompression/impl/track_cache.h
+++ b/includes/acl/decompression/impl/track_cache.h
@@ -44,7 +44,7 @@ namespace acl
 {
 	namespace acl_impl
 	{
-		template<typename cached_type, uint32_t cache_size>
+		template<typename cached_type, uint32_t cache_size, uint32_t num_rounding_modes>
 		struct track_cache_v0
 		{
 			// Our cached type
@@ -53,8 +53,11 @@ namespace acl
 			// Our cache size
 			static constexpr uint32_t k_cache_size = cache_size;
 
+			// How many rounding modes we cache
+			static constexpr uint32_t k_num_rounding_modes = num_rounding_modes;
+
 			// Our cached values
-			type			cached_samples[cache_size];
+			type			cached_samples[num_rounding_modes][cache_size];
 
 			// The index to write the next cache entry when we unpack
 			// Effective index value is modulo k_cache_size what is stored here, guaranteed to never wrap
@@ -84,11 +87,11 @@ namespace acl
 			using type = rtm::vector4f;
 		};
 
-		template<uint32_t cache_size>
-		using track_cache_quatf_v0 = track_cache_v0<track_cache_quatf_trait, cache_size>;
+		template<uint32_t cache_size, uint32_t num_rounding_modes>
+		using track_cache_quatf_v0 = track_cache_v0<track_cache_quatf_trait, cache_size, num_rounding_modes>;
 
-		template<uint32_t cache_size>
-		using track_cache_vector4f_v0 = track_cache_v0<track_cache_vector4f_trait, cache_size>;
+		template<uint32_t cache_size, uint32_t num_rounding_modes>
+		using track_cache_vector4f_v0 = track_cache_v0<track_cache_vector4f_trait, cache_size, num_rounding_modes>;
 	}
 }
 
diff --git a/includes/acl/decompression/impl/transform_animated_track_cache.h b/includes/acl/decompression/impl/transform_animated_track_cache.h
--- a/includes/acl/decompression/impl/transform_animated_track_cache.h
+++ b/includes/acl/decompression/impl/transform_animated_track_cache.h
@@ -546,7 +546,7 @@ namespace acl
 		RTM_FORCE_INLINE RTM_DISABLE_SECURITY_COOKIE_CHECK void RTM_SIMD_CALL quat_lerp4(
 			rtm::vector4f_arg0 xxxx0, rtm::vector4f_arg1 yyyy0, rtm::vector4f_arg2 zzzz0, rtm::vector4f_arg3 wwww0,
 			rtm::vector4f_arg4 xxxx1, rtm::vector4f_arg5 yyyy1, rtm::vector4f_arg6 zzzz1, rtm::vector4f_arg7 wwww1,
-			float interpolation_alpha,
+			rtm::vector4f_argn interpolation_alpha,
 			rtm::vector4f& interp_xxxx, rtm::vector4f& interp_yyyy, rtm::vector4f& interp_zzzz, rtm::vector4f& interp_wwww)
 		{
 			// Calculate the vector4 dot product: dot(start, end)
@@ -570,12 +570,10 @@ namespace acl
 
 			// Lerp the rotation after applying the bias
 			// ((1.0 - alpha) * start) + (alpha * (end ^ bias)) == (start - alpha * start) + (alpha * (end ^ bias))
-			const rtm::vector4f alpha = rtm::vector_set(interpolation_alpha);
-
-			interp_xxxx = rtm::vector_mul_add(xxxx1_with_bias, alpha, rtm::vector_neg_mul_sub(xxxx0, alpha, xxxx0));
-			interp_yyyy = rtm::vector_mul_add(yyyy1_with_bias, alpha, rtm::vector_neg_mul_sub(yyyy0, alpha, yyyy0));
-			interp_zzzz = rtm::vector_mul_add(zzzz1_with_bias, alpha, rtm::vector_neg_mul_sub(zzzz0, alpha, zzzz0));
-			interp_wwww = rtm::vector_mul_add(wwww1_with_bias, alpha, rtm::vector_neg_mul_sub(wwww0, alpha, wwww0));
+			interp_xxxx = rtm::vector_mul_add(xxxx1_with_bias, interpolation_alpha, rtm::vector_neg_mul_sub(xxxx0, interpolation_alpha, xxxx0));
+			interp_yyyy = rtm::vector_mul_add(yyyy1_with_bias, interpolation_alpha, rtm::vector_neg_mul_sub(yyyy0, interpolation_alpha, yyyy0));
+			interp_zzzz = rtm::vector_mul_add(zzzz1_with_bias, interpolation_alpha, rtm::vector_neg_mul_sub(zzzz0, interpolation_alpha, zzzz0));
+			interp_wwww = rtm::vector_mul_add(wwww1_with_bias, interpolation_alpha, rtm::vector_neg_mul_sub(wwww0, interpolation_alpha, wwww0));
 		}
 
 		// About 9 cycles with AVX on Skylake
@@ -1214,9 +1212,11 @@ namespace acl
 
 		struct animated_track_cache_v0
 		{
-			track_cache_quatf_v0<8> rotations;
-			track_cache_vector4f_v0<8> translations;
-			track_cache_vector4f_v0<8> scales;
+			static constexpr uint32_t k_num_rounding_modes = 4;	// none, floor, ceil, nearest
+
+			track_cache_quatf_v0<8, k_num_rounding_modes>		rotations;
+			track_cache_vector4f_v0<8, k_num_rounding_modes>	translations;
+			track_cache_vector4f_v0<8, k_num_rounding_modes>	scales;
 
 			// Scratch space when we decompress our samples before we interpolate
 			rtm::vector4f scratch0[4];
@@ -1470,68 +1470,192 @@ namespace acl
 
 				// Interpolate linearly and store our rotations in SOA
 				{
-					rtm::vector4f interp_xxxx;
-					rtm::vector4f interp_yyyy;
-					rtm::vector4f interp_zzzz;
-					rtm::vector4f interp_wwww;
+					const float interpolation_alpha = decomp_context.interpolation_alpha;
+					const rtm::vector4f interpolation_alpha_v = rtm::vector_set(interpolation_alpha);
 
-					const bool should_interpolate = should_interpolate_samples<decompression_settings_type>(rotation_format, decomp_context.interpolation_alpha);
-					if (should_interpolate)
+					if (decompression_settings_type::is_per_track_rounding_supported())
 					{
-						// Interpolate our quaternions without normalizing just yet
-						quat_lerp4(scratch0_xxxx, scratch0_yyyy, scratch0_zzzz, scratch0_wwww,
-							scratch1_xxxx, scratch1_yyyy, scratch1_zzzz, scratch1_wwww,
-							decomp_context.interpolation_alpha,
-							interp_xxxx, interp_yyyy, interp_zzzz, interp_wwww);
-
-						// Due to the interpolation, the result might not be anywhere near normalized!
-						// Make sure to normalize afterwards if we need to
-						constexpr bool normalize_rotations = decompression_settings_type::normalize_rotations();
-						if (normalize_rotations)
-							quat_normalize4(interp_xxxx, interp_yyyy, interp_zzzz, interp_wwww);
+						// If we support per track rounding, we have to retain everything
+						// Write both floor/ceil samples and interpolate as well
+						// When we consume the sample, we'll pick the right one according to the rounding policy
+
+						// We swizzle and store the floor/ceil first because we have sqrt instructions in flight
+						// from above. Most of the shuffles below can execute without any dependency.
+						// We should have enough registers to avoid spilling and there is enough work to perform
+						// to avoid any dependencies and fully hide the sqrt costs. That being said, there is
+						// quite a bit of work to do and we might still be CPU bound below.
+
+						{
+							// Swizzle out our 4 floor samples
+							rtm::vector4f sample0;
+							rtm::vector4f sample1;
+							rtm::vector4f sample2;
+							rtm::vector4f sample3;
+							RTM_MATRIXF_TRANSPOSE_4X4(scratch0_xxxx, scratch0_yyyy, scratch0_zzzz, scratch0_wwww, sample0, sample1, sample2, sample3);
+
+							rtm::quatf* cache_ptr = &rotations.cached_samples[static_cast<int>(sample_rounding_policy::floor)][cache_write_index];
+
+							cache_ptr[0] = rtm::vector_to_quat(sample0);
+							cache_ptr[1] = rtm::vector_to_quat(sample1);
+							cache_ptr[2] = rtm::vector_to_quat(sample2);
+							cache_ptr[3] = rtm::vector_to_quat(sample3);
+						}
+
+						{
+							// Swizzle out our 4 floor ceil
+							rtm::vector4f sample0;
+							rtm::vector4f sample1;
+							rtm::vector4f sample2;
+							rtm::vector4f sample3;
+							RTM_MATRIXF_TRANSPOSE_4X4(scratch1_xxxx, scratch1_yyyy, scratch1_zzzz, scratch1_wwww, sample0, sample1, sample2, sample3);
+
+							rtm::quatf* cache_ptr = &rotations.cached_samples[static_cast<int>(sample_rounding_policy::ceil)][cache_write_index];
+
+							cache_ptr[0] = rtm::vector_to_quat(sample0);
+							cache_ptr[1] = rtm::vector_to_quat(sample1);
+							cache_ptr[2] = rtm::vector_to_quat(sample2);
+							cache_ptr[3] = rtm::vector_to_quat(sample3);
+						}
+
+						{
+							// Find nearest and swizzle it out
+							const rtm::mask4f use_sample0 = rtm::vector_less_than(interpolation_alpha_v, rtm::vector_set(0.5F));
+
+							const rtm::vector4f nearest_xxxx = rtm::vector_select(use_sample0, scratch0_xxxx, scratch1_xxxx);
+							const rtm::vector4f nearest_yyyy = rtm::vector_select(use_sample0, scratch0_yyyy, scratch1_yyyy);
+							const rtm::vector4f nearest_zzzz = rtm::vector_select(use_sample0, scratch0_zzzz, scratch1_zzzz);
+							const rtm::vector4f nearest_wwww = rtm::vector_select(use_sample0, scratch0_wwww, scratch1_wwww);
+
+							rtm::vector4f sample0;
+							rtm::vector4f sample1;
+							rtm::vector4f sample2;
+							rtm::vector4f sample3;
+							RTM_MATRIXF_TRANSPOSE_4X4(nearest_xxxx, nearest_yyyy, nearest_zzzz, nearest_wwww, sample0, sample1, sample2, sample3);
+
+							rtm::quatf* cache_ptr = &rotations.cached_samples[static_cast<int>(sample_rounding_policy::nearest)][cache_write_index];
+
+							cache_ptr[0] = rtm::vector_to_quat(sample0);
+							cache_ptr[1] = rtm::vector_to_quat(sample1);
+							cache_ptr[2] = rtm::vector_to_quat(sample2);
+							cache_ptr[3] = rtm::vector_to_quat(sample3);
+						}
+
+						{
+							rtm::vector4f interp_xxxx;
+							rtm::vector4f interp_yyyy;
+							rtm::vector4f interp_zzzz;
+							rtm::vector4f interp_wwww;
+
+							// Interpolate our quaternions without normalizing just yet
+							quat_lerp4(scratch0_xxxx, scratch0_yyyy, scratch0_zzzz, scratch0_wwww,
+								scratch1_xxxx, scratch1_yyyy, scratch1_zzzz, scratch1_wwww,
+								interpolation_alpha_v,
+								interp_xxxx, interp_yyyy, interp_zzzz, interp_wwww);
+
+							// Due to the interpolation, the result might not be anywhere near normalized!
+							// Make sure to normalize afterwards if we need to
+							constexpr bool normalize_rotations = decompression_settings_type::normalize_rotations();
+							if (normalize_rotations)
+								quat_normalize4(interp_xxxx, interp_yyyy, interp_zzzz, interp_wwww);
+
+#if !defined(ACL_IMPL_PREFETCH_EARLY)
+							{
+								// Our animated variable bit packed data uses at most 32 bits per component
+								// When we use raw data, that means each group uses 64 bytes (4 bytes per component, 4 components, 4 samples in group), we have 1 group per cache line
+								// When we use variable data, the highest bit rate uses 32 bits per component and thus our upper bound is 48 bytes per group (4 bytes per component, 3 components, 4 samples in group), we have 1.33 group per cache line
+								// In practice, the highest bit rate is rare and the second higher uses 19 bits per component which brings us to 28.5 bytes per group, leading to 2.24 group per cache line
+								// We prefetch both key frames every time to help hide TLB miss latency in large clips
+								// We prefetch here because we have a square-root and division in quat_normalize4(..) that we'll wait after
+								// This allows us to insert the prefetch basically for free in their shadow
+								const uint8_t* animated_track_data = segment_sampling_context_rotations[0].animated_track_data + 64;	// One cache line ahead
+								const uint32_t animated_bit_offset0 = segment_sampling_context_rotations[0].animated_track_data_bit_offset;
+								const uint32_t animated_bit_offset1 = segment_sampling_context_rotations[1].animated_track_data_bit_offset;
+								ACL_IMPL_ANIMATED_PREFETCH(animated_track_data + (animated_bit_offset0 / 8));
+								ACL_IMPL_ANIMATED_PREFETCH(animated_track_data + (animated_bit_offset1 / 8));
+							}
+#endif
+
+							// Swizzle out our 4 samples
+							rtm::vector4f sample0;
+							rtm::vector4f sample1;
+							rtm::vector4f sample2;
+							rtm::vector4f sample3;
+							RTM_MATRIXF_TRANSPOSE_4X4(interp_xxxx, interp_yyyy, interp_zzzz, interp_wwww, sample0, sample1, sample2, sample3);
+
+							rtm::quatf* cache_ptr = &rotations.cached_samples[static_cast<int>(sample_rounding_policy::none)][cache_write_index];
+
+							cache_ptr[0] = rtm::vector_to_quat(sample0);
+							cache_ptr[1] = rtm::vector_to_quat(sample1);
+							cache_ptr[2] = rtm::vector_to_quat(sample2);
+							cache_ptr[3] = rtm::vector_to_quat(sample3);
+						}
 					}
 					else
 					{
-						// If we don't interpolate, just pick the sample we need, it is already normalized after reconstructing
-						// the W component or it was raw to begin with
-						const rtm::mask4f use_sample0 = rtm::vector_less_equal(rtm::vector_set(decomp_context.interpolation_alpha), rtm::vector_zero());
-
-						interp_xxxx = rtm::vector_select(use_sample0, scratch0_xxxx, scratch1_xxxx);
-						interp_yyyy = rtm::vector_select(use_sample0, scratch0_yyyy, scratch1_yyyy);
-						interp_zzzz = rtm::vector_select(use_sample0, scratch0_zzzz, scratch1_zzzz);
-						interp_wwww = rtm::vector_select(use_sample0, scratch0_wwww, scratch1_wwww);
-					}
+						rtm::vector4f interp_xxxx;
+						rtm::vector4f interp_yyyy;
+						rtm::vector4f interp_zzzz;
+						rtm::vector4f interp_wwww;
+
+						const bool should_interpolate = should_interpolate_samples<decompression_settings_type>(rotation_format, interpolation_alpha);
+						if (should_interpolate)
+						{
+							// Interpolate our quaternions without normalizing just yet
+							quat_lerp4(scratch0_xxxx, scratch0_yyyy, scratch0_zzzz, scratch0_wwww,
+								scratch1_xxxx, scratch1_yyyy, scratch1_zzzz, scratch1_wwww,
+								interpolation_alpha_v,
+								interp_xxxx, interp_yyyy, interp_zzzz, interp_wwww);
+
+							// Due to the interpolation, the result might not be anywhere near normalized!
+							// Make sure to normalize afterwards if we need to
+							constexpr bool normalize_rotations = decompression_settings_type::normalize_rotations();
+							if (normalize_rotations)
+								quat_normalize4(interp_xxxx, interp_yyyy, interp_zzzz, interp_wwww);
+						}
+						else
+						{
+							// If we don't interpolate, just pick the sample we need, it is already normalized after reconstructing
+							// the W component or it was raw to begin with
+							const rtm::mask4f use_sample0 = rtm::vector_less_equal(rtm::vector_set(interpolation_alpha), rtm::vector_zero());
+
+							interp_xxxx = rtm::vector_select(use_sample0, scratch0_xxxx, scratch1_xxxx);
+							interp_yyyy = rtm::vector_select(use_sample0, scratch0_yyyy, scratch1_yyyy);
+							interp_zzzz = rtm::vector_select(use_sample0, scratch0_zzzz, scratch1_zzzz);
+							interp_wwww = rtm::vector_select(use_sample0, scratch0_wwww, scratch1_wwww);
+						}
 
 #if !defined(ACL_IMPL_PREFETCH_EARLY)
-					{
-						// Our animated variable bit packed data uses at most 32 bits per component
-						// When we use raw data, that means each group uses 64 bytes (4 bytes per component, 4 components, 4 samples in group), we have 1 group per cache line
-						// When we use variable data, the highest bit rate uses 32 bits per component and thus our upper bound is 48 bytes per group (4 bytes per component, 3 components, 4 samples in group), we have 1.33 group per cache line
-						// In practice, the highest bit rate is rare and the second higher uses 19 bits per component which brings us to 28.5 bytes per group, leading to 2.24 group per cache line
-						// We prefetch both key frames every time to help hide TLB miss latency in large clips
-						// We prefetch here because we have a square-root and division in quat_normalize4(..) that we'll wait after
-						// This allows us to insert the prefetch basically for free in their shadow
-						const uint8_t* animated_track_data = segment_sampling_context_rotations[0].animated_track_data + 64;	// One cache line ahead
-						const uint32_t animated_bit_offset0 = segment_sampling_context_rotations[0].animated_track_data_bit_offset;
-						const uint32_t animated_bit_offset1 = segment_sampling_context_rotations[1].animated_track_data_bit_offset;
-						ACL_IMPL_ANIMATED_PREFETCH(animated_track_data + (animated_bit_offset0 / 8));
-						ACL_IMPL_ANIMATED_PREFETCH(animated_track_data + (animated_bit_offset1 / 8));
-					}
+						{
+							// Our animated variable bit packed data uses at most 32 bits per component
+							// When we use raw data, that means each group uses 64 bytes (4 bytes per component, 4 components, 4 samples in group), we have 1 group per cache line
+							// When we use variable data, the highest bit rate uses 32 bits per component and thus our upper bound is 48 bytes per group (4 bytes per component, 3 components, 4 samples in group), we have 1.33 group per cache line
+							// In practice, the highest bit rate is rare and the second higher uses 19 bits per component which brings us to 28.5 bytes per group, leading to 2.24 group per cache line
+							// We prefetch both key frames every time to help hide TLB miss latency in large clips
+							// We prefetch here because we have a square-root and division in quat_normalize4(..) that we'll wait after
+							// This allows us to insert the prefetch basically for free in their shadow
+							const uint8_t* animated_track_data = segment_sampling_context_rotations[0].animated_track_data + 64;	// One cache line ahead
+							const uint32_t animated_bit_offset0 = segment_sampling_context_rotations[0].animated_track_data_bit_offset;
+							const uint32_t animated_bit_offset1 = segment_sampling_context_rotations[1].animated_track_data_bit_offset;
+							ACL_IMPL_ANIMATED_PREFETCH(animated_track_data + (animated_bit_offset0 / 8));
+							ACL_IMPL_ANIMATED_PREFETCH(animated_track_data + (animated_bit_offset1 / 8));
+						}
 #endif
 
-					// Swizzle out our 4 samples
-					rtm::vector4f sample0;
-					rtm::vector4f sample1;
-					rtm::vector4f sample2;
-					rtm::vector4f sample3;
-					RTM_MATRIXF_TRANSPOSE_4X4(interp_xxxx, interp_yyyy, interp_zzzz, interp_wwww, sample0, sample1, sample2, sample3);
+						// Swizzle out our 4 samples
+						rtm::vector4f sample0;
+						rtm::vector4f sample1;
+						rtm::vector4f sample2;
+						rtm::vector4f sample3;
+						RTM_MATRIXF_TRANSPOSE_4X4(interp_xxxx, interp_yyyy, interp_zzzz, interp_wwww, sample0, sample1, sample2, sample3);
 
-					rtm::quatf* cache_ptr = &rotations.cached_samples[cache_write_index];
+						// Always first rounding mode (none)
+						rtm::quatf* cache_ptr = &rotations.cached_samples[static_cast<int>(sample_rounding_policy::none)][cache_write_index];
 
-					cache_ptr[0] = rtm::vector_to_quat(sample0);
-					cache_ptr[1] = rtm::vector_to_quat(sample1);
-					cache_ptr[2] = rtm::vector_to_quat(sample2);
-					cache_ptr[3] = rtm::vector_to_quat(sample3);
+						cache_ptr[0] = rtm::vector_to_quat(sample0);
+						cache_ptr[1] = rtm::vector_to_quat(sample1);
+						cache_ptr[2] = rtm::vector_to_quat(sample2);
+						cache_ptr[3] = rtm::vector_to_quat(sample3);
+					}
 				}
 			}
 
@@ -1581,7 +1705,7 @@ namespace acl
 			}
 
 			template<class decompression_settings_type>
-			RTM_DISABLE_SECURITY_COOKIE_CHECK rtm::quatf RTM_SIMD_CALL unpack_rotation_within_group(const persistent_transform_decompression_context_v0& decomp_context, uint32_t unpack_index)
+			RTM_DISABLE_SECURITY_COOKIE_CHECK rtm::quatf RTM_SIMD_CALL unpack_rotation_within_group(const persistent_transform_decompression_context_v0& decomp_context, uint32_t unpack_index, float interpolation_alpha)
 			{
 				ACL_ASSERT(unpack_index < rotations.num_left_to_unpack && unpack_index < 4, "Cannot unpack sample that isn't present");
 
@@ -1608,32 +1732,32 @@ namespace acl
 
 				rtm::quatf result;
 
-				const bool should_interpolate = should_interpolate_samples<decompression_settings_type>(rotation_format, decomp_context.interpolation_alpha);
+				const bool should_interpolate = should_interpolate_samples<decompression_settings_type>(rotation_format, interpolation_alpha);
 				if (should_interpolate)
 				{
 					// Due to the interpolation, the result might not be anywhere near normalized!
 					// Make sure to normalize afterwards before using
 					constexpr bool normalize_rotations = decompression_settings_type::normalize_rotations();
 					if (normalize_rotations)
-						result = rtm::quat_lerp(sample0, sample1, decomp_context.interpolation_alpha);
+						result = rtm::quat_lerp(sample0, sample1, interpolation_alpha);
 					else
-						result = quat_lerp_no_normalization(sample0, sample1, decomp_context.interpolation_alpha);
+						result = quat_lerp_no_normalization(sample0, sample1, interpolation_alpha);
 				}
 				else
 				{
 					// If we don't interpolate, just pick the sample we need, it is already normalized after reconstructing
 					// the W component or it was raw to begin with
-					result = decomp_context.interpolation_alpha <= 0.0F ? sample0 : sample1;
+					result = interpolation_alpha <= 0.0F ? sample0 : sample1;
 				}
 
 				return result;
 			}
 
-			RTM_DISABLE_SECURITY_COOKIE_CHECK const rtm::quatf& consume_rotation()
+			RTM_FORCE_INLINE RTM_DISABLE_SECURITY_COOKIE_CHECK const rtm::quatf& consume_rotation(sample_rounding_policy policy)
 			{
 				ACL_ASSERT(rotations.cache_read_index < rotations.cache_write_index, "Attempting to consume an animated sample that isn't cached");
 				const uint32_t cache_read_index = rotations.cache_read_index++;
-				return rotations.cached_samples[cache_read_index % 8];
+				return rotations.cached_samples[static_cast<int>(policy)][cache_read_index % 8];
 			}
 
 			template<class decompression_settings_adapter_type>
@@ -1659,15 +1783,33 @@ namespace acl
 				unpack_animated_vector3<decompression_settings_adapter_type>(decomp_context, scratch1, num_to_unpack, clip_sampling_context_translations, segment_sampling_context_translations[1]);
 
 				const rtm::vector4f interpolation_alpha = rtm::vector_set(decomp_context.interpolation_alpha);
-				rtm::vector4f* cache_ptr = &translations.cached_samples[cache_write_index];
+				const rtm::mask4f use_sample0 = rtm::vector_less_than(interpolation_alpha, rtm::vector_set(0.5F));
+
+				// If we support per track rounding, we have to retain everything
+				// Write both floor/ceil/nearest samples and interpolate as well
+				// When we consume the sample, we'll pick the right one according to the rounding policy
+
+				rtm::vector4f* cache_ptr_none = &translations.cached_samples[static_cast<int>(sample_rounding_policy::none)][cache_write_index];
+				rtm::vector4f* cache_ptr_floor = &translations.cached_samples[static_cast<int>(sample_rounding_policy::floor)][cache_write_index];
+				rtm::vector4f* cache_ptr_ceil = &translations.cached_samples[static_cast<int>(sample_rounding_policy::ceil)][cache_write_index];
+				rtm::vector4f* cache_ptr_nearest = &translations.cached_samples[static_cast<int>(sample_rounding_policy::nearest)][cache_write_index];
+				
 				for (uint32_t unpack_index = 0; unpack_index < num_to_unpack; ++unpack_index)
 				{
 					const rtm::vector4f sample0 = scratch0[unpack_index];
 					const rtm::vector4f sample1 = scratch1[unpack_index];
 
+					if (decompression_settings_adapter_type::is_per_track_rounding_supported())
+					{
+						// These stores have no dependency and can be dispatched right away
+						cache_ptr_floor[unpack_index] = sample0;
+						cache_ptr_ceil[unpack_index] = sample1;
+						cache_ptr_nearest[unpack_index] = rtm::vector_select(use_sample0, sample0, sample1);
+					}
+
 					const rtm::vector4f sample = rtm::vector_lerp(sample0, sample1, interpolation_alpha);
 
-					cache_ptr[unpack_index] = sample;
+					cache_ptr_none[unpack_index] = sample;
 				}
 
 				// If we have clip range data, skip it
@@ -1723,21 +1865,21 @@ namespace acl
 			}
 
 			template<class decompression_settings_adapter_type>
-			RTM_DISABLE_SECURITY_COOKIE_CHECK rtm::vector4f RTM_SIMD_CALL unpack_translation_within_group(const persistent_transform_decompression_context_v0& decomp_context, uint32_t unpack_index)
+			RTM_DISABLE_SECURITY_COOKIE_CHECK rtm::vector4f RTM_SIMD_CALL unpack_translation_within_group(const persistent_transform_decompression_context_v0& decomp_context, uint32_t unpack_index, float interpolation_alpha)
 			{
 				ACL_ASSERT(unpack_index < translations.num_left_to_unpack && unpack_index < 4, "Cannot unpack sample that isn't present");
 
 				const rtm::vector4f sample0 = unpack_single_animated_vector3<decompression_settings_adapter_type>(decomp_context, unpack_index, clip_sampling_context_translations, segment_sampling_context_translations[0]);
 				const rtm::vector4f sample1 = unpack_single_animated_vector3<decompression_settings_adapter_type>(decomp_context, unpack_index, clip_sampling_context_translations, segment_sampling_context_translations[1]);
 
-				return rtm::vector_lerp(sample0, sample1, decomp_context.interpolation_alpha);
+				return rtm::vector_lerp(sample0, sample1, interpolation_alpha);
 			}
 
-			RTM_DISABLE_SECURITY_COOKIE_CHECK const rtm::vector4f& consume_translation()
+			RTM_FORCE_INLINE RTM_DISABLE_SECURITY_COOKIE_CHECK const rtm::vector4f& consume_translation(sample_rounding_policy policy)
 			{
 				ACL_ASSERT(translations.cache_read_index < translations.cache_write_index, "Attempting to consume an animated sample that isn't cached");
 				const uint32_t cache_read_index = translations.cache_read_index++;
-				return translations.cached_samples[cache_read_index % 8];
+				return translations.cached_samples[static_cast<int>(policy)][cache_read_index % 8];
 			}
 
 			template<class decompression_settings_adapter_type>
@@ -1763,15 +1905,33 @@ namespace acl
 				unpack_animated_vector3<decompression_settings_adapter_type>(decomp_context, scratch1, num_to_unpack, clip_sampling_context_scales, segment_sampling_context_scales[1]);
 
 				const rtm::vector4f interpolation_alpha = rtm::vector_set(decomp_context.interpolation_alpha);
-				rtm::vector4f* cache_ptr = &scales.cached_samples[cache_write_index];
+				const rtm::mask4f use_sample0 = rtm::vector_less_than(interpolation_alpha, rtm::vector_set(0.5F));
+
+				// If we support per track rounding, we have to retain everything
+				// Write both floor/ceil/nearest samples and interpolate as well
+				// When we consume the sample, we'll pick the right one according to the rounding policy
+
+				rtm::vector4f* cache_ptr_none = &scales.cached_samples[static_cast<int>(sample_rounding_policy::none)][cache_write_index];
+				rtm::vector4f* cache_ptr_floor = &scales.cached_samples[static_cast<int>(sample_rounding_policy::floor)][cache_write_index];
+				rtm::vector4f* cache_ptr_ceil = &scales.cached_samples[static_cast<int>(sample_rounding_policy::ceil)][cache_write_index];
+				rtm::vector4f* cache_ptr_nearest = &scales.cached_samples[static_cast<int>(sample_rounding_policy::nearest)][cache_write_index];
+
 				for (uint32_t unpack_index = 0; unpack_index < num_to_unpack; ++unpack_index)
 				{
 					const rtm::vector4f sample0 = scratch0[unpack_index];
 					const rtm::vector4f sample1 = scratch1[unpack_index];
 
+					if (decompression_settings_adapter_type::is_per_track_rounding_supported())
+					{
+						// These stores have no dependency and can be dispatched right away
+						cache_ptr_floor[unpack_index] = sample0;
+						cache_ptr_ceil[unpack_index] = sample1;
+						cache_ptr_nearest[unpack_index] = rtm::vector_select(use_sample0, sample0, sample1);
+					}
+
 					const rtm::vector4f sample = rtm::vector_lerp(sample0, sample1, interpolation_alpha);
 
-					cache_ptr[unpack_index] = sample;
+					cache_ptr_none[unpack_index] = sample;
 				}
 
 				// If we have clip range data, skip it
@@ -1827,21 +1987,21 @@ namespace acl
 			}
 
 			template<class decompression_settings_adapter_type>
-			RTM_DISABLE_SECURITY_COOKIE_CHECK rtm::vector4f RTM_SIMD_CALL unpack_scale_within_group(const persistent_transform_decompression_context_v0& decomp_context, uint32_t unpack_index)
+			RTM_DISABLE_SECURITY_COOKIE_CHECK rtm::vector4f RTM_SIMD_CALL unpack_scale_within_group(const persistent_transform_decompression_context_v0& decomp_context, uint32_t unpack_index, float interpolation_alpha)
 			{
 				ACL_ASSERT(unpack_index < scales.num_left_to_unpack && unpack_index < 4, "Cannot unpack sample that isn't present");
 
 				const rtm::vector4f sample0 = unpack_single_animated_vector3<decompression_settings_adapter_type>(decomp_context, unpack_index, clip_sampling_context_scales, segment_sampling_context_scales[0]);
 				const rtm::vector4f sample1 = unpack_single_animated_vector3<decompression_settings_adapter_type>(decomp_context, unpack_index, clip_sampling_context_scales, segment_sampling_context_scales[1]);
 
-				return rtm::vector_lerp(sample0, sample1, decomp_context.interpolation_alpha);
+				return rtm::vector_lerp(sample0, sample1, interpolation_alpha);
 			}
 
-			RTM_DISABLE_SECURITY_COOKIE_CHECK const rtm::vector4f& consume_scale()
+			RTM_FORCE_INLINE RTM_DISABLE_SECURITY_COOKIE_CHECK const rtm::vector4f& consume_scale(sample_rounding_policy policy)
 			{
 				ACL_ASSERT(scales.cache_read_index < scales.cache_write_index, "Attempting to consume an animated sample that isn't cached");
 				const uint32_t cache_read_index = scales.cache_read_index++;
-				return scales.cached_samples[cache_read_index % 8];
+				return scales.cached_samples[static_cast<int>(policy)][cache_read_index % 8];
 			}
 		};
 	}
diff --git a/includes/acl/decompression/impl/transform_constant_track_cache.h b/includes/acl/decompression/impl/transform_constant_track_cache.h
--- a/includes/acl/decompression/impl/transform_constant_track_cache.h
+++ b/includes/acl/decompression/impl/transform_constant_track_cache.h
@@ -80,7 +80,7 @@ namespace acl
 			// CMU has 64.41%, Paragon has 47.69%, and Fortnite has 62.84%.
 			// Following these numbers, it is common for clips to have at least 10 constant rotation samples to unpack.
 
-			track_cache_quatf_v0<32> rotations;
+			track_cache_quatf_v0<32, 1> rotations;
 
 			// Points to our packed sub-track data
 			const uint8_t*	constant_data_rotations;
@@ -126,7 +126,7 @@ namespace acl
 				rotations.cache_write_index += num_to_unpack;
 
 				const uint8_t* constant_track_data = constant_data_rotations;
-				rtm::quatf* cache_ptr = &rotations.cached_samples[cache_write_index];
+				rtm::quatf* cache_ptr = &rotations.cached_samples[0][cache_write_index];
 
 				if (rotation_format == rotation_format8::quatf_full && decompression_settings_type::is_rotation_format_supported(rotation_format8::quatf_full))
 				{
@@ -188,7 +188,7 @@ namespace acl
 				num_to_unpack = std::min<uint32_t>(num_left_to_unpack, 16);
 				for (uint32_t unpack_index = 0; unpack_index < num_to_unpack; ++unpack_index)
 				{
-					const rtm::quatf rotation = rotations.cached_samples[cache_write_index + unpack_index];
+					const rtm::quatf rotation = rotations.cached_samples[0][cache_write_index + unpack_index];
 
 					ACL_ASSERT(rtm::quat_is_finite(rotation), "Rotation is not valid!");
 					ACL_ASSERT(rtm::quat_is_normalized(rotation), "Rotation is not normalized!");
@@ -257,7 +257,7 @@ namespace acl
 			{
 				ACL_ASSERT(rotations.cache_read_index < rotations.cache_write_index, "Attempting to consume a constant sample that isn't cached");
 				const uint32_t cache_read_index = rotations.cache_read_index++;
-				return rotations.cached_samples[cache_read_index % 32];
+				return rotations.cached_samples[0][cache_read_index % 32];
 			}
 
 			RTM_DISABLE_SECURITY_COOKIE_CHECK void skip_translation_groups(uint32_t num_groups_to_skip)
diff --git a/includes/acl/decompression/impl/transform_decompression_context.h b/includes/acl/decompression/impl/transform_decompression_context.h
--- a/includes/acl/decompression/impl/transform_decompression_context.h
+++ b/includes/acl/decompression/impl/transform_decompression_context.h
@@ -70,9 +70,10 @@ namespace acl
 
 			uint8_t looping_policy;								//  21 |  29
 
-			uint8_t padding0[21];								//  22 |  30
+			uint8_t padding0[20];								//  22 |  30
 
 			// Seeking related data
+			uint8_t rounding_policy;							//  42 |  50
 			uint8_t uses_single_segment;						//  43 |  51
 
 			float sample_time;									//  44 |  52
@@ -89,6 +90,7 @@ namespace acl
 
 			float interpolation_alpha;							//  88 | 120
 
+			// Can't have a 0 byte array, add a whole cache line as padding
 			uint8_t padding1[sizeof(void*) == 4 ? 36 : 4];		//  92 | 124
 
 			//										Total size:	   128 | 128
@@ -98,6 +100,7 @@ namespace acl
 			const compressed_tracks* get_compressed_tracks() const { return tracks; }
 			compressed_tracks_version16 get_version() const { return tracks->get_version(); }
 			sample_looping_policy get_looping_policy() const { return static_cast<sample_looping_policy>(looping_policy); }
+			sample_rounding_policy get_rounding_policy() const { return static_cast<sample_rounding_policy>(rounding_policy); }
 			bool is_initialized() const { return tracks != nullptr; }
 			void reset() { tracks = nullptr; }
 		};
@@ -114,6 +117,7 @@ namespace acl
 			static constexpr range_reduction_flags8 get_range_reduction_flag() { return range_reduction_flags8::translations; }
 			static constexpr vector_format8 get_vector_format(const persistent_transform_decompression_context_v0& context) { return context.translation_format; }
 			static constexpr bool is_vector_format_supported(vector_format8 format) { return decompression_settings_type::is_translation_format_supported(format); }
+			static constexpr bool is_per_track_rounding_supported() { return decompression_settings_type::is_per_track_rounding_supported(); }
 		};
 
 		template<class decompression_settings_type>
@@ -123,6 +127,7 @@ namespace acl
 			static constexpr range_reduction_flags8 get_range_reduction_flag() { return range_reduction_flags8::scales; }
 			static constexpr vector_format8 get_vector_format(const persistent_transform_decompression_context_v0& context) { return context.scale_format; }
 			static constexpr bool is_vector_format_supported(vector_format8 format) { return decompression_settings_type::is_scale_format_supported(format); }
+			static constexpr bool is_per_track_rounding_supported() { return decompression_settings_type::is_per_track_rounding_supported(); }
 		};
 
 		// Returns the statically known number of rotation formats supported by the decompression settings
diff --git a/includes/acl/decompression/impl/transform_track_decompression.h b/includes/acl/decompression/impl/transform_track_decompression.h
--- a/includes/acl/decompression/impl/transform_track_decompression.h
+++ b/includes/acl/decompression/impl/transform_track_decompression.h
@@ -32,8 +32,8 @@
 #include "acl/core/range_reduction_types.h"
 #include "acl/core/track_formats.h"
 #include "acl/core/track_writer.h"
-#include "acl/core/variable_bit_rates.h"
 #include "acl/core/impl/compiler_utils.h"
+#include "acl/core/impl/variable_bit_rates.h"
 #include "acl/decompression/database/database.h"
 #include "acl/decompression/impl/transform_animated_track_cache.h"
 #include "acl/decompression/impl/transform_constant_track_cache.h"
@@ -162,7 +162,7 @@ namespace acl
 			if (decompression_settings_type::clamp_sample_time())
 				sample_time = rtm::scalar_clamp(sample_time, 0.0F, context.clip_duration);
 
-			if (context.sample_time == sample_time)
+			if (context.sample_time == sample_time && context.get_rounding_policy() == rounding_policy)
 				return;
 
 			const transform_tracks_header& transform_header = get_transform_tracks_header(*context.tracks);
@@ -186,6 +186,8 @@ namespace acl
 			uint32_t key_frame1;
 			find_linear_interpolation_samples_with_sample_rate(header.num_samples, header.sample_rate, sample_time, rounding_policy, looping_policy_, key_frame0, key_frame1, context.interpolation_alpha);
 
+			context.rounding_policy = static_cast<uint8_t>(rounding_policy);
+
 			uint32_t segment_key_frame0;
 			uint32_t segment_key_frame1;
 
@@ -256,7 +258,9 @@ namespace acl
 					key_frame1 = count_leading_zeros(candidate_indices1);
 
 					// Calculate our new interpolation alpha
-					context.interpolation_alpha = find_linear_interpolation_alpha(sample_index, key_frame0, key_frame1, rounding_policy);
+					// We used the rounding policy above to snap to the correct key frame earlier but we might need to interpolate now
+					// if key frames have been removed
+					context.interpolation_alpha = find_linear_interpolation_alpha(sample_index, key_frame0, key_frame1, sample_rounding_policy::none);
 
 					// Find where our data lives (clip or database tier X)
 					sample_indices0 = segment_tier0_header0->sample_indices;
@@ -408,7 +412,9 @@ namespace acl
 					const uint32_t clip_key_frame1 = segment_start_indices[segment_index1] + segment_key_frame1;
 
 					// Calculate our new interpolation alpha
-					context.interpolation_alpha = find_linear_interpolation_alpha(sample_index, clip_key_frame0, clip_key_frame1, rounding_policy);
+					// We used the rounding policy above to snap to the correct key frame earlier but we might need to interpolate now
+					// if key frames have been removed
+					context.interpolation_alpha = find_linear_interpolation_alpha(sample_index, clip_key_frame0, clip_key_frame1, sample_rounding_policy::none);
 
 					// Find where our data lives (clip or database tier X)
 					sample_indices0 = segment_tier0_header0->sample_indices;
@@ -504,6 +510,9 @@ namespace acl
 
 		// TODO: Merge the per track format and segment range info into a single buffer? Less to prefetch and used together
 		// TODO: Remove segment data alignment, no longer required?
+		// TODO: sample_rounding_policy ends up being signed extended on x64 from a 32 bit value into 64 bit (edx -> rax)
+		//       I tried using uint32_t and uint64_t as its underlying type but code generation remained the same
+		//       Would using a raw uint32_t below instead of the typed enum help avoid the extra instruction?
 
 
 		// Force inline this function, we only use it to keep the code readable
@@ -690,6 +699,8 @@ namespace acl
 			const persistent_transform_decompression_context_v0& context,
 			animated_track_cache_v0& animated_track_cache, track_writer_type& writer)
 		{
+			const sample_rounding_policy rounding_policy = context.get_rounding_policy();
+
 			for (uint32_t entry_index = 0, track_index = 0; entry_index <= last_entry_index; ++entry_index)
 			{
 				// Mask out everything but animated sub-tracks, this way we can early out when we iterate
@@ -720,7 +731,19 @@ namespace acl
 					if ((packed_group & 0x80000000) != 0)
 					{
 						const uint32_t track_index0 = curr_group_track_index + 0;
-						const rtm::quatf& rotation = animated_track_cache.consume_rotation();
+
+						// We need the true rounding policy to be statically known when per track rounding is not supported
+						// When it isn't supported, we always use 'none' since the interpolation alpha was properly calculated
+						// and rounding has already been performed for us.
+						const sample_rounding_policy rounding_policy_ =
+							decompression_settings_type::is_per_track_rounding_supported() &&
+							rounding_policy == sample_rounding_policy::per_track ?
+							writer.get_rounding_policy(track_index0) :
+							sample_rounding_policy::none;
+
+						ACL_ASSERT(rounding_policy_ != sample_rounding_policy::per_track, "track_writer::get_rounding_policy() cannot return per_track");
+
+						const rtm::quatf& rotation = animated_track_cache.consume_rotation(rounding_policy_);
 
 						ACL_ASSERT(rtm::quat_is_finite(rotation), "Rotation is not valid!");
 						ACL_ASSERT(rtm::quat_is_normalized(rotation), "Rotation is not normalized!");
@@ -732,7 +755,19 @@ namespace acl
 					if ((packed_group & 0x20000000) != 0)
 					{
 						const uint32_t track_index1 = curr_group_track_index + 1;
-						const rtm::quatf& rotation = animated_track_cache.consume_rotation();
+
+						// We need the true rounding policy to be statically known when per track rounding is not supported
+						// When it isn't supported, we always use 'none' since the interpolation alpha was properly calculated
+						// and rounding has already been performed for us.
+						const sample_rounding_policy rounding_policy_ =
+							decompression_settings_type::is_per_track_rounding_supported() &&
+							rounding_policy == sample_rounding_policy::per_track ?
+							writer.get_rounding_policy(track_index1) :
+							sample_rounding_policy::none;
+
+						ACL_ASSERT(rounding_policy_ != sample_rounding_policy::per_track, "track_writer::get_rounding_policy() cannot return per_track");
+
+						const rtm::quatf& rotation = animated_track_cache.consume_rotation(rounding_policy_);
 
 						ACL_ASSERT(rtm::quat_is_finite(rotation), "Rotation is not valid!");
 						ACL_ASSERT(rtm::quat_is_normalized(rotation), "Rotation is not normalized!");
@@ -744,7 +779,19 @@ namespace acl
 					if ((packed_group & 0x08000000) != 0)
 					{
 						const uint32_t track_index2 = curr_group_track_index + 2;
-						const rtm::quatf& rotation = animated_track_cache.consume_rotation();
+
+						// We need the true rounding policy to be statically known when per track rounding is not supported
+						// When it isn't supported, we always use 'none' since the interpolation alpha was properly calculated
+						// and rounding has already been performed for us.
+						const sample_rounding_policy rounding_policy_ =
+							decompression_settings_type::is_per_track_rounding_supported() &&
+							rounding_policy == sample_rounding_policy::per_track ?
+							writer.get_rounding_policy(track_index2) :
+							sample_rounding_policy::none;
+
+						ACL_ASSERT(rounding_policy_ != sample_rounding_policy::per_track, "track_writer::get_rounding_policy() cannot return per_track");
+
+						const rtm::quatf& rotation = animated_track_cache.consume_rotation(rounding_policy_);
 
 						ACL_ASSERT(rtm::quat_is_finite(rotation), "Rotation is not valid!");
 						ACL_ASSERT(rtm::quat_is_normalized(rotation), "Rotation is not normalized!");
@@ -756,7 +803,19 @@ namespace acl
 					if ((packed_group & 0x02000000) != 0)
 					{
 						const uint32_t track_index3 = curr_group_track_index + 3;
-						const rtm::quatf& rotation = animated_track_cache.consume_rotation();
+
+						// We need the true rounding policy to be statically known when per track rounding is not supported
+						// When it isn't supported, we always use 'none' since the interpolation alpha was properly calculated
+						// and rounding has already been performed for us.
+						const sample_rounding_policy rounding_policy_ =
+							decompression_settings_type::is_per_track_rounding_supported() &&
+							rounding_policy == sample_rounding_policy::per_track ?
+							writer.get_rounding_policy(track_index3) :
+							sample_rounding_policy::none;
+
+						ACL_ASSERT(rounding_policy_ != sample_rounding_policy::per_track, "track_writer::get_rounding_policy() cannot return per_track");
+
+						const rtm::quatf& rotation = animated_track_cache.consume_rotation(rounding_policy_);
 
 						ACL_ASSERT(rtm::quat_is_finite(rotation), "Rotation is not valid!");
 						ACL_ASSERT(rtm::quat_is_normalized(rotation), "Rotation is not normalized!");
@@ -962,12 +1021,14 @@ namespace acl
 		}
 
 		// Force inline this function, we only use it to keep the code readable
-		template<class translation_adapter, class track_writer_type>
+		template<class decompression_settings_adapter_type, class track_writer_type>
 		RTM_FORCE_INLINE RTM_DISABLE_SECURITY_COOKIE_CHECK void RTM_SIMD_CALL unpack_animated_translation_sub_tracks(
 			const packed_sub_track_types* translation_sub_track_types, uint32_t last_entry_index,
 			const persistent_transform_decompression_context_v0& context,
 			animated_track_cache_v0& animated_track_cache, track_writer_type& writer)
 		{
+			const sample_rounding_policy rounding_policy = context.get_rounding_policy();
+
 			for (uint32_t entry_index = 0, track_index = 0; entry_index <= last_entry_index; ++entry_index)
 			{
 				// Mask out everything but animated sub-tracks, this way we can early out when we iterate
@@ -993,12 +1054,24 @@ namespace acl
 						continue;	// This group contains no animated sub-tracks, skip it
 
 					// Unpack our next 4 tracks
-					animated_track_cache.unpack_translation_group<translation_adapter>(context);
+					animated_track_cache.unpack_translation_group<decompression_settings_adapter_type>(context);
 
 					if ((packed_group & 0x80000000) != 0)
 					{
 						const uint32_t track_index0 = curr_group_track_index + 0;
-						const rtm::vector4f& translation = animated_track_cache.consume_translation();
+
+						// We need the true rounding policy to be statically known when per track rounding is not supported
+						// When it isn't supported, we always use 'none' since the interpolation alpha was properly calculated
+						// and rounding has already been performed for us.
+						const sample_rounding_policy rounding_policy_ =
+							decompression_settings_adapter_type::is_per_track_rounding_supported() &&
+							rounding_policy == sample_rounding_policy::per_track ?
+							writer.get_rounding_policy(track_index0) :
+							sample_rounding_policy::none;
+
+						ACL_ASSERT(rounding_policy_ != sample_rounding_policy::per_track, "track_writer::get_rounding_policy() cannot return per_track");
+
+						const rtm::vector4f& translation = animated_track_cache.consume_translation(rounding_policy_);
 
 						ACL_ASSERT(rtm::vector_is_finite3(translation), "Translation is not valid!");
 
@@ -1009,7 +1082,19 @@ namespace acl
 					if ((packed_group & 0x20000000) != 0)
 					{
 						const uint32_t track_index1 = curr_group_track_index + 1;
-						const rtm::vector4f& translation = animated_track_cache.consume_translation();
+
+						// We need the true rounding policy to be statically known when per track rounding is not supported
+						// When it isn't supported, we always use 'none' since the interpolation alpha was properly calculated
+						// and rounding has already been performed for us.
+						const sample_rounding_policy rounding_policy_ =
+							decompression_settings_adapter_type::is_per_track_rounding_supported() &&
+							rounding_policy == sample_rounding_policy::per_track ?
+							writer.get_rounding_policy(track_index1) :
+							sample_rounding_policy::none;
+
+						ACL_ASSERT(rounding_policy_ != sample_rounding_policy::per_track, "track_writer::get_rounding_policy() cannot return per_track");
+
+						const rtm::vector4f& translation = animated_track_cache.consume_translation(rounding_policy_);
 
 						ACL_ASSERT(rtm::vector_is_finite3(translation), "Translation is not valid!");
 
@@ -1020,7 +1105,19 @@ namespace acl
 					if ((packed_group & 0x08000000) != 0)
 					{
 						const uint32_t track_index2 = curr_group_track_index + 2;
-						const rtm::vector4f& translation = animated_track_cache.consume_translation();
+
+						// We need the true rounding policy to be statically known when per track rounding is not supported
+						// When it isn't supported, we always use 'none' since the interpolation alpha was properly calculated
+						// and rounding has already been performed for us.
+						const sample_rounding_policy rounding_policy_ =
+							decompression_settings_adapter_type::is_per_track_rounding_supported() &&
+							rounding_policy == sample_rounding_policy::per_track ?
+							writer.get_rounding_policy(track_index2) :
+							sample_rounding_policy::none;
+
+						ACL_ASSERT(rounding_policy_ != sample_rounding_policy::per_track, "track_writer::get_rounding_policy() cannot return per_track");
+
+						const rtm::vector4f& translation = animated_track_cache.consume_translation(rounding_policy_);
 
 						ACL_ASSERT(rtm::vector_is_finite3(translation), "Translation is not valid!");
 
@@ -1031,7 +1128,19 @@ namespace acl
 					if ((packed_group & 0x02000000) != 0)
 					{
 						const uint32_t track_index3 = curr_group_track_index + 3;
-						const rtm::vector4f& translation = animated_track_cache.consume_translation();
+
+						// We need the true rounding policy to be statically known when per track rounding is not supported
+						// When it isn't supported, we always use 'none' since the interpolation alpha was properly calculated
+						// and rounding has already been performed for us.
+						const sample_rounding_policy rounding_policy_ =
+							decompression_settings_adapter_type::is_per_track_rounding_supported() &&
+							rounding_policy == sample_rounding_policy::per_track ?
+							writer.get_rounding_policy(track_index3) :
+							sample_rounding_policy::none;
+
+						ACL_ASSERT(rounding_policy_ != sample_rounding_policy::per_track, "track_writer::get_rounding_policy() cannot return per_track");
+
+						const rtm::vector4f& translation = animated_track_cache.consume_translation(rounding_policy_);
 
 						ACL_ASSERT(rtm::vector_is_finite3(translation), "Translation is not valid!");
 
@@ -1241,12 +1350,14 @@ namespace acl
 		}
 
 		// Force inline this function, we only use it to keep the code readable
-		template<class scale_adapter, class track_writer_type>
+		template<class decompression_settings_adapter_type, class track_writer_type>
 		RTM_FORCE_INLINE RTM_DISABLE_SECURITY_COOKIE_CHECK void RTM_SIMD_CALL unpack_animated_scale_sub_tracks(
 			const packed_sub_track_types* scale_sub_track_types, uint32_t last_entry_index,
 			const persistent_transform_decompression_context_v0& context,
 			animated_track_cache_v0& animated_track_cache, track_writer_type& writer)
 		{
+			const sample_rounding_policy rounding_policy = context.get_rounding_policy();
+
 			for (uint32_t entry_index = 0, track_index = 0; entry_index <= last_entry_index; ++entry_index)
 			{
 				// Mask out everything but animated sub-tracks, this way we can early out when we iterate
@@ -1272,12 +1383,24 @@ namespace acl
 						continue;	// This group contains no animated sub-tracks, skip it
 
 					// Unpack our next 4 tracks
-					animated_track_cache.unpack_scale_group<scale_adapter>(context);
+					animated_track_cache.unpack_scale_group<decompression_settings_adapter_type>(context);
 
 					if ((packed_group & 0x80000000) != 0)
 					{
 						const uint32_t track_index0 = curr_group_track_index + 0;
-						const rtm::vector4f& scale = animated_track_cache.consume_scale();
+
+						// We need the true rounding policy to be statically known when per track rounding is not supported
+						// When it isn't supported, we always use 'none' since the interpolation alpha was properly calculated
+						// and rounding has already been performed for us.
+						const sample_rounding_policy rounding_policy_ =
+							decompression_settings_adapter_type::is_per_track_rounding_supported() &&
+							rounding_policy == sample_rounding_policy::per_track ?
+							writer.get_rounding_policy(track_index0) :
+							sample_rounding_policy::none;
+
+						ACL_ASSERT(rounding_policy_ != sample_rounding_policy::per_track, "track_writer::get_rounding_policy() cannot return per_track");
+
+						const rtm::vector4f& scale = animated_track_cache.consume_scale(rounding_policy_);
 
 						ACL_ASSERT(rtm::vector_is_finite3(scale), "Scale is not valid!");
 
@@ -1288,7 +1411,19 @@ namespace acl
 					if ((packed_group & 0x20000000) != 0)
 					{
 						const uint32_t track_index1 = curr_group_track_index + 1;
-						const rtm::vector4f& scale = animated_track_cache.consume_scale();
+
+						// We need the true rounding policy to be statically known when per track rounding is not supported
+						// When it isn't supported, we always use 'none' since the interpolation alpha was properly calculated
+						// and rounding has already been performed for us.
+						const sample_rounding_policy rounding_policy_ =
+							decompression_settings_adapter_type::is_per_track_rounding_supported() &&
+							rounding_policy == sample_rounding_policy::per_track ?
+							writer.get_rounding_policy(track_index1) :
+							sample_rounding_policy::none;
+
+						ACL_ASSERT(rounding_policy_ != sample_rounding_policy::per_track, "track_writer::get_rounding_policy() cannot return per_track");
+
+						const rtm::vector4f& scale = animated_track_cache.consume_scale(rounding_policy_);
 
 						ACL_ASSERT(rtm::vector_is_finite3(scale), "Scale is not valid!");
 
@@ -1299,7 +1434,19 @@ namespace acl
 					if ((packed_group & 0x08000000) != 0)
 					{
 						const uint32_t track_index2 = curr_group_track_index + 2;
-						const rtm::vector4f& scale = animated_track_cache.consume_scale();
+
+						// We need the true rounding policy to be statically known when per track rounding is not supported
+						// When it isn't supported, we always use 'none' since the interpolation alpha was properly calculated
+						// and rounding has already been performed for us.
+						const sample_rounding_policy rounding_policy_ =
+							decompression_settings_adapter_type::is_per_track_rounding_supported() &&
+							rounding_policy == sample_rounding_policy::per_track ?
+							writer.get_rounding_policy(track_index2) :
+							sample_rounding_policy::none;
+
+						ACL_ASSERT(rounding_policy_ != sample_rounding_policy::per_track, "track_writer::get_rounding_policy() cannot return per_track");
+
+						const rtm::vector4f& scale = animated_track_cache.consume_scale(rounding_policy_);
 
 						ACL_ASSERT(rtm::vector_is_finite3(scale), "Scale is not valid!");
 
@@ -1310,7 +1457,19 @@ namespace acl
 					if ((packed_group & 0x02000000) != 0)
 					{
 						const uint32_t track_index3 = curr_group_track_index + 3;
-						const rtm::vector4f& scale = animated_track_cache.consume_scale();
+
+						// We need the true rounding policy to be statically known when per track rounding is not supported
+						// When it isn't supported, we always use 'none' since the interpolation alpha was properly calculated
+						// and rounding has already been performed for us.
+						const sample_rounding_policy rounding_policy_ =
+							decompression_settings_adapter_type::is_per_track_rounding_supported() &&
+							rounding_policy == sample_rounding_policy::per_track ?
+							writer.get_rounding_policy(track_index3) :
+							sample_rounding_policy::none;
+
+						ACL_ASSERT(rounding_policy_ != sample_rounding_policy::per_track, "track_writer::get_rounding_policy() cannot return per_track");
+
+						const rtm::vector4f& scale = animated_track_cache.consume_scale(rounding_policy_);
 
 						ACL_ASSERT(rtm::vector_is_finite3(scale), "Scale is not valid!");
 
@@ -1768,6 +1927,16 @@ namespace acl
 
 			// Finally reached our desired track, unpack it
 
+			float interpolation_alpha = context.interpolation_alpha;
+			if (decompression_settings_type::is_per_track_rounding_supported())
+			{
+				const sample_rounding_policy rounding_policy = context.get_rounding_policy();
+				const sample_rounding_policy rounding_policy_ = rounding_policy == sample_rounding_policy::per_track ? writer.get_rounding_policy(track_index) : rounding_policy;
+				ACL_ASSERT(rounding_policy_ != sample_rounding_policy::per_track, "track_writer::get_rounding_policy() cannot return per_track");
+
+				interpolation_alpha = apply_rounding_policy(interpolation_alpha, rounding_policy_);
+			}
+
 			if (rotation_sub_track_type == 0)
 			{
 				if (default_rotation_mode != default_sub_track_mode::skipped)
@@ -1784,7 +1953,7 @@ namespace acl
 				if (rotation_sub_track_type & 1)
 					rotation = constant_track_cache.unpack_rotation_within_group<decompression_settings_type>(context, rotation_group_sample_index);
 				else
-					rotation = animated_track_cache.unpack_rotation_within_group<decompression_settings_type>(context, rotation_group_sample_index);
+					rotation = animated_track_cache.unpack_rotation_within_group<decompression_settings_type>(context, rotation_group_sample_index, interpolation_alpha);
 
 				writer.write_rotation(track_index, rotation);
 			}
@@ -1805,7 +1974,7 @@ namespace acl
 				if (translation_sub_track_type & 1)
 					translation = constant_track_cache.unpack_translation_within_group(translation_group_sample_index);
 				else
-					translation = animated_track_cache.unpack_translation_within_group<translation_adapter>(context, translation_group_sample_index);
+					translation = animated_track_cache.unpack_translation_within_group<translation_adapter>(context, translation_group_sample_index, interpolation_alpha);
 
 				writer.write_translation(track_index, translation);
 			}
@@ -1826,7 +1995,7 @@ namespace acl
 				if (scale_sub_track_type & 1)
 					scale = constant_track_cache.unpack_scale_within_group(scale_group_sample_index);
 				else
-					scale = animated_track_cache.unpack_scale_within_group<scale_adapter>(context, scale_group_sample_index);
+					scale = animated_track_cache.unpack_scale_within_group<scale_adapter>(context, scale_group_sample_index, interpolation_alpha);
 
 				writer.write_scale(track_index, scale);
 			}
diff --git a/tools/acl_compressor/includes/acl_compressor.h b/tools/acl_compressor/includes/acl_compressor.h
--- a/tools/acl_compressor/includes/acl_compressor.h
+++ b/tools/acl_compressor/includes/acl_compressor.h
@@ -28,3 +28,33 @@ bool is_acl_sjson_file(const char* filename);
 bool is_acl_bin_file(const char* filename);
 
 int main_impl(int argc, char* argv[]);
+
+#if defined(ACL_USE_SJSON)
+#include "acl/core/compressed_database.h"
+#include "acl/core/compressed_tracks.h"
+#include "acl/core/iallocator.h"
+#include "acl/compression/compression_settings.h"
+#include "acl/compression/track_array.h"
+#include "acl/compression/transform_error_metrics.h"
+#include "acl/decompression/decompression_settings.h"
+
+void validate_accuracy(acl::iallocator& allocator,
+	const acl::track_array_qvvf& raw_tracks, const acl::track_array_qvvf& additive_base_tracks,
+	const acl::itransform_error_metric& error_metric,
+	const acl::compressed_tracks& compressed_tracks_,
+	double regression_error_threshold);
+
+void validate_accuracy(acl::iallocator& allocator, const acl::track_array& raw_tracks, const acl::compressed_tracks& tracks, double regression_error_threshold);
+
+void validate_metadata(const acl::track_array& raw_tracks, const acl::compressed_tracks& tracks);
+void validate_convert(acl::iallocator& allocator, const acl::track_array& raw_tracks);
+
+void validate_db(acl::iallocator& allocator, const acl::track_array_qvvf& raw_tracks, const acl::track_array_qvvf& additive_base_tracks,
+	const acl::compression_database_settings& settings, const acl::itransform_error_metric& error_metric,
+	const acl::compressed_tracks& compressed_tracks0, const acl::compressed_tracks& compressed_tracks1);
+
+struct debug_transform_decompression_settings_with_db final : public acl::debug_transform_decompression_settings
+{
+	using database_settings_type = acl::debug_database_settings;
+};
+#endif
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
@@ -392,16 +392,6 @@ static bool parse_options(int argc, char** argv, Options& options)
 }
 
 #if defined(ACL_USE_SJSON)
-#if defined(ACL_HAS_ASSERT_CHECKS)
-// We extern our regression test functions for simplicity
-extern void validate_accuracy(iallocator& allocator, const track_array_qvvf& raw_tracks, const track_array_qvvf& additive_base_tracks, const itransform_error_metric& error_metric, const compressed_tracks& compressed_tracks_, double regression_error_threshold);
-extern void validate_accuracy(iallocator& allocator, const track_array& raw_tracks, const compressed_tracks& tracks, double regression_error_threshold);
-extern void validate_metadata(const track_array& raw_tracks, const compressed_tracks& tracks);
-extern void validate_convert(iallocator& allocator, const track_array& raw_tracks);
-extern void validate_db(iallocator& allocator, const track_array_qvvf& raw_tracks, const track_array_qvvf& additive_base_tracks,
-	const compression_database_settings& settings, const itransform_error_metric& error_metric,
-	const compressed_tracks& compressed_tracks0, const compressed_tracks& compressed_tracks1);
-#endif
 
 static void try_algorithm(const Options& options, iallocator& allocator, const track_array_qvvf& transform_tracks,
 	const track_array_qvvf& additive_base, additive_clip_format8 additive_format,
diff --git a/tools/acl_compressor/sources/validate_database.cpp b/tools/acl_compressor/sources/validate_database.cpp
--- a/tools/acl_compressor/sources/validate_database.cpp
+++ b/tools/acl_compressor/sources/validate_database.cpp
@@ -40,11 +40,6 @@
 using namespace acl;
 
 #if defined(ACL_USE_SJSON) && defined(ACL_HAS_ASSERT_CHECKS)
-struct debug_transform_decompression_settings_with_db final : public acl::debug_transform_decompression_settings
-{
-	using database_settings_type = acl::debug_database_settings;
-};
-
 static void stream_in_database_tier(database_context<debug_database_settings>& context, const debug_database_streamer& streamer, const compressed_database& db, quality_tier tier)
 {
 	const uint32_t num_chunks = db.get_num_chunks(tier);
diff --git a/tools/acl_compressor/sources/validate_tracks.cpp b/tools/acl_compressor/sources/validate_tracks.cpp
--- a/tools/acl_compressor/sources/validate_tracks.cpp
+++ b/tools/acl_compressor/sources/validate_tracks.cpp
@@ -46,7 +46,7 @@ void validate_transform_tracks(const acl::acl_impl::debug_track_writer& referenc
 
 		// Make sure constant default sub-tracks match the skipped sub-tracks
 		const rtm::qvvf transform = tracks.read_qvv(track_index);
-		ACL_ASSERT(rtm::vector_all_near_equal(rtm::quat_to_vector(ref_transform.rotation), rtm::quat_to_vector(transform.rotation), quat_error_threshold), "Failed to sample bone index: %u", track_index);
+		ACL_ASSERT(rtm::vector_all_near_equal(rtm::quat_to_vector(rtm::quat_ensure_positive_w(ref_transform.rotation)), rtm::quat_to_vector(rtm::quat_ensure_positive_w(transform.rotation)), quat_error_threshold), "Failed to sample bone index: %u", track_index);
 		ACL_ASSERT(rtm::vector_all_near_equal3(ref_transform.translation, transform.translation, vec3_error_threshold), "Failed to sample bone index: %u", track_index);
 		ACL_ASSERT(rtm::vector_all_near_equal3(ref_transform.scale, transform.scale, vec3_error_threshold), "Failed to sample bone index: %u", track_index);
 	}
@@ -93,12 +93,12 @@ void validate_accuracy(
 	ACL_ASSERT(error.error < regression_error_threshold, "Error too high for bone %u: %f at time %f", error.index, error.error, error.sample_time);
 
 	const uint32_t num_tracks = compressed_tracks_.get_num_tracks();
-	const float clip_duration = compressed_tracks_.get_finite_duration();
+	const float duration = compressed_tracks_.get_finite_duration();
 	const float sample_rate = compressed_tracks_.get_sample_rate();
 
 	// Calculate the number of samples from the duration to account for the repeating
 	// first frame at the end when wrapping is used
-	const uint32_t num_samples = calculate_num_samples(clip_duration, sample_rate);
+	const uint32_t num_samples = calculate_num_samples(duration, sample_rate);
 
 	debug_track_writer track_writer(allocator, track_type8::qvvf, num_tracks);
 	track_writer.initialize_with_defaults(raw_tracks);
@@ -110,6 +110,9 @@ void validate_accuracy(
 	debug_track_writer_variable_defaults track_writer_variable(allocator, track_type8::qvvf, num_tracks);
 	track_writer_variable.default_sub_tracks = track_writer.tracks_typed.qvvf;
 
+	debug_track_writer_per_track_rounding track_writer_per_track_rounding(allocator, track_type8::qvvf, num_tracks);
+	track_writer_per_track_rounding.initialize_with_defaults(raw_tracks);
+
 	{
 		// Try to decompress something at 0.0, if we have no tracks or samples, it should be handled
 		context.seek(0.0F, rounding_policy);
@@ -118,6 +121,7 @@ void validate_accuracy(
 		context.decompress_tracks(track_writer_variable);
 	}
 
+	// Basic sanity checks
 	if (num_samples != 0)
 	{
 		debug_track_writer track_writer_clamped(allocator, track_type8::qvvf, num_tracks);
@@ -130,7 +134,7 @@ void validate_accuracy(
 		validate_transform_tracks(track_writer, track_writer_clamped, quat_error_threshold, vec3_error_threshold);
 
 		// Make sure clamping works properly at the end of the clip
-		const float last_sample_time = rtm::scalar_min(float(num_samples - 1) / sample_rate, clip_duration);
+		const float last_sample_time = rtm::scalar_min(float(num_samples - 1) / sample_rate, duration);
 
 		// When using the wrap looping policy, the last sample time will yield the first sample since we completely wrap
 		context.seek(last_sample_time, rounding_policy);
@@ -141,12 +145,31 @@ void validate_accuracy(
 		context.decompress_tracks(track_writer_clamped);
 
 		validate_transform_tracks(track_writer, track_writer_clamped, quat_error_threshold, vec3_error_threshold);
+
+		// Test a few samples with all rounding modes per track
+		const float sample_times[] = { 0.0F, duration * 0.2F, duration * 0.5F, duration * 0.75F, duration };
+		const sample_rounding_policy rounding_policies[] = { sample_rounding_policy::none, sample_rounding_policy::floor, sample_rounding_policy::ceil, sample_rounding_policy::nearest };
+
+		for (float sample_time : make_iterator(sample_times))
+		{
+			for (sample_rounding_policy policy : make_iterator(rounding_policies))
+			{
+				context.seek(sample_time, policy);
+				context.decompress_tracks(track_writer);
+
+				track_writer_per_track_rounding.rounding_policy = policy;
+				context.seek(sample_time, sample_rounding_policy::per_track);
+				context.decompress_tracks(track_writer_per_track_rounding);
+
+				validate_transform_tracks(track_writer, track_writer_per_track_rounding, quat_error_threshold, vec3_error_threshold);
+			}
+		}
 	}
 
 	// Regression test
 	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);
+		const float sample_time = rtm::scalar_min(float(sample_index) / sample_rate, duration);
 
 		// We use the nearest sample to accurately measure the loss that happened, if any
 		context.seek(sample_time, rounding_policy);
@@ -282,12 +305,15 @@ void validate_accuracy(
 	debug_track_writer lossy_tracks_writer(allocator, track_type, num_tracks);
 	debug_track_writer lossy_track_writer(allocator, track_type, num_tracks);
 
+	debug_track_writer_per_track_rounding track_writer_per_track_rounding(allocator, track_type, num_tracks);
+
 	{
 		// Try to decompress something at 0.0, if we have no tracks or samples, it should be handled
 		context.seek(0.0F, rounding_policy);
 		context.decompress_tracks(lossy_tracks_writer);
 	}
 
+	// Basic sanity checks
 	if (num_samples != 0)
 	{
 		debug_track_writer track_writer_clamped(allocator, track_type, num_tracks);
@@ -309,6 +335,25 @@ void validate_accuracy(
 		context.decompress_tracks(track_writer_clamped);
 
 		validate_scalar_tracks(raw_tracks, raw_tracks_writer, track_writer_clamped, regression_error_thresholdv, last_sample_time_clamp + 1.0F);
+
+		// Test a few samples with all rounding modes per track
+		const float sample_times[] = { 0.0F, duration * 0.2F, duration * 0.5F, duration * 0.75F, duration };
+		const sample_rounding_policy rounding_policies[] = { sample_rounding_policy::none, sample_rounding_policy::floor, sample_rounding_policy::ceil, sample_rounding_policy::nearest };
+
+		for (float sample_time : make_iterator(sample_times))
+		{
+			for (sample_rounding_policy policy : make_iterator(rounding_policies))
+			{
+				context.seek(sample_time, policy);
+				context.decompress_tracks(lossy_tracks_writer);
+
+				track_writer_per_track_rounding.rounding_policy = policy;
+				context.seek(sample_time, sample_rounding_policy::per_track);
+				context.decompress_tracks(track_writer_per_track_rounding);
+
+				validate_scalar_tracks(raw_tracks, lossy_tracks_writer, track_writer_per_track_rounding, regression_error_thresholdv, sample_time);
+			}
+		}
 	}
 
 	// Regression test


Test patch:
diff --git a/tests/sources/core/test_interpolation_utils.cpp b/tests/sources/core/test_interpolation_utils.cpp
--- a/tests/sources/core/test_interpolation_utils.cpp
+++ b/tests/sources/core/test_interpolation_utils.cpp
@@ -358,4 +358,18 @@ TEST_CASE("interpolation utils", "[core][utils]")
 	CHECK(scalar_near_equal(find_linear_interpolation_alpha(1.5F, 0, 2, sample_rounding_policy::nearest), 1.0F, error_threshold));
 	CHECK(scalar_near_equal(find_linear_interpolation_alpha(1.5F, 0, 3, sample_rounding_policy::nearest), 1.0F, error_threshold));
 	CHECK(scalar_near_equal(find_linear_interpolation_alpha(1.5F, 1, 4, sample_rounding_policy::nearest), 0.0F, error_threshold));
+
+	//////////////////////////////////////////////////////////////////////////
+
+	CHECK(scalar_near_equal(apply_rounding_policy(0.2F, sample_rounding_policy::none), 0.2F, error_threshold));
+	CHECK(apply_rounding_policy(0.2F, sample_rounding_policy::floor) == 0.0F);
+	CHECK(apply_rounding_policy(0.2F, sample_rounding_policy::ceil) == 1.0F);
+	CHECK(apply_rounding_policy(0.2F, sample_rounding_policy::nearest) == 0.0F);
+	CHECK(scalar_near_equal(apply_rounding_policy(0.2F, sample_rounding_policy::per_track), 0.2F, error_threshold));
+
+	CHECK(scalar_near_equal(apply_rounding_policy(0.8F, sample_rounding_policy::none), 0.8F, error_threshold));
+	CHECK(apply_rounding_policy(0.8F, sample_rounding_policy::floor) == 0.0F);
+	CHECK(apply_rounding_policy(0.8F, sample_rounding_policy::ceil) == 1.0F);
+	CHECK(apply_rounding_policy(0.8F, sample_rounding_policy::nearest) == 1.0F);
+	CHECK(scalar_near_equal(apply_rounding_policy(0.8F, sample_rounding_policy::per_track), 0.8F, error_threshold));
 }
diff --git a/tests/sources/core/test_iterator.cpp b/tests/sources/core/test_iterator.cpp
--- a/tests/sources/core/test_iterator.cpp
+++ b/tests/sources/core/test_iterator.cpp
@@ -57,4 +57,11 @@ TEST_CASE("iterator", "[core][iterator]")
 		CHECK(i.begin() == items + 0);
 		CHECK(i.end() == items + num_items);
 	}
+
+	SECTION("make_iterator matches")
+	{
+		auto j = make_iterator(items);
+		CHECK(i.begin() == j.begin());
+		CHECK(i.end() == j.end());
+	}
 }
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
@@ -24,7 +24,7 @@
 
 #include <catch2/catch.hpp>
 
-#include <acl/core/variable_bit_rates.h>
+#include <acl/core/impl/variable_bit_rates.h>
 #include <acl/math/scalar_packing.h>
 #include <acl/math/vector4_packing.h>
 
@@ -124,9 +124,9 @@ TEST_CASE("unpack_scalarf_uXX_unsafe", "[math][scalar][packing]")
 		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)
+		for (uint8_t bit_rate = 1; bit_rate < acl_impl::k_highest_bit_rate; ++bit_rate)
 		{
-			uint32_t num_bits = get_num_bits_at_bit_rate(bit_rate);
+			uint32_t num_bits = acl_impl::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/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
@@ -24,7 +24,7 @@
 
 #include <catch2/catch.hpp>
 
-#include <acl/core/variable_bit_rates.h>
+#include <acl/core/impl/variable_bit_rates.h>
 #include <acl/math/vector4_packing.h>
 
 #include <rtm/scalarf.h>
@@ -154,9 +154,9 @@ TEST_CASE("pack_vector4_XX", "[math][vector4][packing]")
 		if (!vector_all_near_equal(vec0, vec1, 1.0E-6F))
 			num_errors++;
 
-		for (uint8_t bit_rate = 1; bit_rate < k_highest_bit_rate; ++bit_rate)
+		for (uint8_t bit_rate = 1; bit_rate < acl_impl::k_highest_bit_rate; ++bit_rate)
 		{
-			uint32_t num_bits = get_num_bits_at_bit_rate(bit_rate);
+			uint32_t num_bits = acl_impl::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)
 			{
@@ -382,9 +382,9 @@ TEST_CASE("pack_vector3_XX", "[math][vector4][packing]")
 		if (!vector_all_near_equal3(vec0, vec1, 1.0E-6F))
 			num_errors++;
 
-		for (uint8_t bit_rate = 1; bit_rate < k_highest_bit_rate; ++bit_rate)
+		for (uint8_t bit_rate = 1; bit_rate < acl_impl::k_highest_bit_rate; ++bit_rate)
 		{
-			uint32_t num_bits = get_num_bits_at_bit_rate(bit_rate);
+			uint32_t num_bits = acl_impl::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)
 			{
@@ -449,9 +449,9 @@ TEST_CASE("decay_vector3_XX", "[math][vector4][decay]")
 		if (!vector_all_near_equal3(vec0, vec1, 1.0E-6F))
 			num_errors++;
 
-		for (uint8_t bit_rate = 1; bit_rate < k_highest_bit_rate; ++bit_rate)
+		for (uint8_t bit_rate = 1; bit_rate < acl_impl::k_highest_bit_rate; ++bit_rate)
 		{
-			uint32_t num_bits = get_num_bits_at_bit_rate(bit_rate);
+			uint32_t num_bits = acl_impl::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)
 			{
@@ -518,9 +518,9 @@ TEST_CASE("pack_vector2_XX", "[math][vector4][packing]")
 		if (!vector_all_near_equal2(vec0, vec1, 1.0E-6F))
 			num_errors++;
 
-		for (uint8_t bit_rate = 1; bit_rate < k_highest_bit_rate; ++bit_rate)
+		for (uint8_t bit_rate = 1; bit_rate < acl_impl::k_highest_bit_rate; ++bit_rate)
 		{
-			uint32_t num_bits = get_num_bits_at_bit_rate(bit_rate);
+			uint32_t num_bits = acl_impl::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)
 			{