Repository: nfrechette/acl (cpp)

Issue:
Decay quantization values to avoid LHS
Right now when we iterate to optimize the quantization bit rates, we don't modify the tracks in place. Instead, we quantize the samples on demand over and over. To do so, we pack the value on the stack, and unpack it. This is needlessly slow. Instead we should just decay the value by keeping things in SIMD registers. There is no need to write and load to the stack. This will save a lot of instructions and avoid any potential LHS issues.


Discussion:

Patch:
diff --git a/includes/acl/compression/stream/sample_streams.h b/includes/acl/compression/stream/sample_streams.h
--- a/includes/acl/compression/stream/sample_streams.h
+++ b/includes/acl/compression/stream/sample_streams.h
@@ -195,8 +195,7 @@ namespace acl
 		}
 
 		// Pack and unpack at our desired bit rate
-		uint8_t num_bits_at_bit_rate = get_num_bits_at_bit_rate(bit_rate);
-		alignas(16) uint8_t raw_data[16] = { 0 };
+		const uint32_t num_bits_at_bit_rate = get_num_bits_at_bit_rate(bit_rate);
 		Vector4_32 packed_rotation;
 
 		if (is_constant_bit_rate(bit_rate))
@@ -207,24 +206,14 @@ namespace acl
 			const BoneRanges& clip_bone_range = segment->clip->ranges[bone_steams.bone_index];
 			const Vector4_32 normalized_rotation = normalize_sample(rotation, clip_bone_range.rotation);
 
-			pack_vector3_u48_unsafe(normalized_rotation, &raw_data[0]);
-			packed_rotation = unpack_vector3_u48_unsafe(&raw_data[0]);
+			packed_rotation = decay_vector3_u48(normalized_rotation);
 		}
 		else if (is_raw_bit_rate(bit_rate))
 			packed_rotation = rotation;
+		else if (are_rotations_normalized)
+			packed_rotation = decay_vector3_uXX(rotation, num_bits_at_bit_rate);
 		else
-		{
-			if (are_rotations_normalized)
-			{
-				pack_vector3_uXX_unsafe(rotation, num_bits_at_bit_rate, &raw_data[0]);
-				packed_rotation = unpack_vector3_uXX_unsafe(num_bits_at_bit_rate, &raw_data[0], 0);
-			}
-			else
-			{
-				pack_vector3_sXX_unsafe(rotation, num_bits_at_bit_rate, &raw_data[0]);
-				packed_rotation = unpack_vector3_sXX_unsafe(num_bits_at_bit_rate, &raw_data[0], 0);
-			}
-		}
+			packed_rotation = decay_vector3_sXX(rotation, num_bits_at_bit_rate);
 
 		if (segment->are_rotations_normalized && !is_constant_bit_rate(bit_rate) && !is_raw_bit_rate(bit_rate))
 		{
@@ -271,15 +260,9 @@ namespace acl
 			break;
 		case RotationFormat8::QuatDropW_48:
 			if (are_rotations_normalized)
-			{
-				pack_vector3_u48_unsafe(rotation, &raw_data[0]);
-				packed_rotation = unpack_vector3_u48_unsafe(&raw_data[0]);
-			}
+				packed_rotation = decay_vector3_u48(rotation);
 			else
-			{
-				pack_vector3_s48_unsafe(rotation, &raw_data[0]);
-				packed_rotation = unpack_vector3_s48_unsafe(&raw_data[0]);
-			}
+				packed_rotation = decay_vector3_s48(rotation);
 			break;
 		case RotationFormat8::QuatDropW_32:
 			pack_vector3_32(rotation, 11, 11, 10, are_rotations_normalized, &raw_data[0]);
@@ -372,7 +355,6 @@ namespace acl
 		ACL_ASSERT(clip_context->are_translations_normalized, "Translations must be normalized to support variable bit rates.");
 
 		// Pack and unpack at our desired bit rate
-		alignas(16) uint8_t raw_data[16] = { 0 };
 		Vector4_32 packed_translation;
 
 		if (is_constant_bit_rate(bit_rate))
@@ -382,16 +364,14 @@ namespace acl
 			const BoneRanges& clip_bone_range = segment->clip->ranges[bone_steams.bone_index];
 			const Vector4_32 normalized_translation = normalize_sample(translation, clip_bone_range.translation);
 
-			pack_vector3_u48_unsafe(normalized_translation, &raw_data[0]);
-			packed_translation = unpack_vector3_u48_unsafe(&raw_data[0]);
+			packed_translation = decay_vector3_u48(normalized_translation);
 		}
 		else if (is_raw_bit_rate(bit_rate))
 			packed_translation = translation;
 		else
 		{
-			const uint8_t num_bits_at_bit_rate = get_num_bits_at_bit_rate(bit_rate);
-			pack_vector3_uXX_unsafe(translation, num_bits_at_bit_rate, &raw_data[0]);
-			packed_translation = unpack_vector3_uXX_unsafe(num_bits_at_bit_rate, &raw_data[0], 0);
+			const uint32_t num_bits_at_bit_rate = get_num_bits_at_bit_rate(bit_rate);
+			packed_translation = decay_vector3_uXX(translation, num_bits_at_bit_rate);
 		}
 
 		if (segment->are_translations_normalized && !is_constant_bit_rate(bit_rate) && !is_raw_bit_rate(bit_rate))
@@ -438,8 +418,7 @@ namespace acl
 			break;
 		case VectorFormat8::Vector3_48:
 			ACL_ASSERT(are_translations_normalized, "Translations must be normalized to support this format");
-			pack_vector3_u48_unsafe(translation, &raw_data[0]);
-			packed_translation = unpack_vector3_u48_unsafe(&raw_data[0]);
+			packed_translation = decay_vector3_u48(translation);
 			break;
 		case VectorFormat8::Vector3_32:
 			pack_vector3_32(translation, 11, 11, 10, are_translations_normalized, &raw_data[0]);
@@ -532,7 +511,6 @@ namespace acl
 		ACL_ASSERT(clip_context->are_scales_normalized, "Scales must be normalized to support variable bit rates.");
 
 		// Pack and unpack at our desired bit rate
-		alignas(16) uint8_t raw_data[16] = { 0 };
 		Vector4_32 packed_scale;
 
 		if (is_constant_bit_rate(bit_rate))
@@ -542,16 +520,14 @@ namespace acl
 			const BoneRanges& clip_bone_range = segment->clip->ranges[bone_steams.bone_index];
 			const Vector4_32 normalized_scale = normalize_sample(scale, clip_bone_range.scale);
 
-			pack_vector3_u48_unsafe(normalized_scale, &raw_data[0]);
-			packed_scale = unpack_vector3_u48_unsafe(&raw_data[0]);
+			packed_scale = decay_vector3_u48(normalized_scale);
 		}
 		else if (is_raw_bit_rate(bit_rate))
 			packed_scale = scale;
 		else
 		{
-			const uint8_t num_bits_at_bit_rate = get_num_bits_at_bit_rate(bit_rate);
-			pack_vector3_uXX_unsafe(scale, num_bits_at_bit_rate, &raw_data[0]);
-			packed_scale = unpack_vector3_uXX_unsafe(num_bits_at_bit_rate, &raw_data[0], 0);
+			const uint32_t num_bits_at_bit_rate = get_num_bits_at_bit_rate(bit_rate);
+			packed_scale = decay_vector3_uXX(scale, num_bits_at_bit_rate);
 		}
 
 		if (segment->are_scales_normalized && !is_constant_bit_rate(bit_rate) && !is_raw_bit_rate(bit_rate))
@@ -598,8 +574,7 @@ namespace acl
 			break;
 		case VectorFormat8::Vector3_48:
 			ACL_ASSERT(are_scales_normalized, "Scales must be normalized to support this format");
-			pack_vector3_u48_unsafe(scale, &raw_data[0]);
-			packed_scale = unpack_vector3_u48_unsafe(&raw_data[0]);
+			packed_scale = decay_vector3_u48(scale);
 			break;
 		case VectorFormat8::Vector3_32:
 			pack_vector3_32(scale, 11, 11, 10, are_scales_normalized, &raw_data[0]);
diff --git a/includes/acl/math/vector4_32.h b/includes/acl/math/vector4_32.h
--- a/includes/acl/math/vector4_32.h
+++ b/includes/acl/math/vector4_32.h
@@ -947,6 +947,22 @@ namespace acl
 		Vector4_32 mask = vector_greater_equal(input, vector_zero_32());
 		return vector_blend(mask, vector_set(1.0f), vector_set(-1.0f));
 	}
+
+	//////////////////////////////////////////////////////////////////////////
+	// Returns per component the rounded input using a symmetric algorithm.
+	// symmetric_round(1.5) = 2.0
+	// symmetric_round(1.2) = 1.0
+	// symmetric_round(-1.5) = -2.0
+	// symmetric_round(-1.2) = -1.0
+	//////////////////////////////////////////////////////////////////////////
+	inline Vector4_32 ACL_SIMD_CALL vector_symmetric_round(Vector4_32Arg0 input)
+	{
+		const Vector4_32 half = vector_set(0.5f);
+		const Vector4_32 floored = vector_floor(vector_add(input, half));
+		const Vector4_32 ceiled = vector_ceil(vector_sub(input, half));
+		const Vector4_32 is_greater_equal = vector_greater_equal(input, vector_zero_32());
+		return vector_blend(is_greater_equal, floored, ceiled);
+	}
 }
 
 ACL_IMPL_FILE_PRAGMA_POP
diff --git a/includes/acl/math/vector4_64.h b/includes/acl/math/vector4_64.h
--- a/includes/acl/math/vector4_64.h
+++ b/includes/acl/math/vector4_64.h
@@ -746,6 +746,22 @@ namespace acl
 		Vector4_64 mask = vector_greater_equal(input, vector_zero_64());
 		return vector_blend(mask, vector_set(1.0), vector_set(-1.0));
 	}
+
+	//////////////////////////////////////////////////////////////////////////
+	// Returns per component the rounded input using a symmetric algorithm.
+	// symmetric_round(1.5) = 2.0
+	// symmetric_round(1.2) = 1.0
+	// symmetric_round(-1.5) = -2.0
+	// symmetric_round(-1.2) = -1.0
+	//////////////////////////////////////////////////////////////////////////
+	inline Vector4_64 vector_symmetric_round(const Vector4_64& input)
+	{
+		const Vector4_64 half = vector_set(0.5);
+		const Vector4_64 floored = vector_floor(vector_add(input, half));
+		const Vector4_64 ceiled = vector_ceil(vector_sub(input, half));
+		const Vector4_64 is_greater_equal = vector_greater_equal(input, vector_zero_64());
+		return vector_blend(is_greater_equal, floored, ceiled);
+	}
 }
 
 ACL_IMPL_FILE_PRAGMA_POP
diff --git a/includes/acl/math/vector4_packing.h b/includes/acl/math/vector4_packing.h
--- a/includes/acl/math/vector4_packing.h
+++ b/includes/acl/math/vector4_packing.h
@@ -502,6 +502,33 @@ namespace acl
 		return vector_set(x, y, z);
 	}
 
+	inline Vector4_32 ACL_SIMD_CALL decay_vector3_u48(Vector4_32Arg0 input)
+	{
+		ACL_ASSERT(vector_all_greater_equal(input, vector_zero_32()) && vector_all_less_equal(input, vector_set(1.0f)), "Expected normalized unsigned input value: %f, %f, %f", vector_get_x(input), vector_get_y(input), vector_get_z(input));
+
+		const float max_value = safe_to_float((1 << 16) - 1);
+		const float inv_max_value = 1.0f / max_value;
+
+		const Vector4_32 packed = vector_symmetric_round(vector_mul(input, max_value));
+		const Vector4_32 decayed = vector_mul(packed, inv_max_value);
+		return decayed;
+	}
+
+	inline Vector4_32 ACL_SIMD_CALL decay_vector3_s48(Vector4_32Arg0 input)
+	{
+		const Vector4_32 half = vector_set(0.5f);
+		const Vector4_32 unsigned_input = vector_add(vector_mul(input, half), half);
+
+		ACL_ASSERT(vector_all_greater_equal(unsigned_input, vector_zero_32()) && vector_all_less_equal(unsigned_input, vector_set(1.0f)), "Expected normalized unsigned input value: %f, %f, %f", vector_get_x(unsigned_input), vector_get_y(unsigned_input), vector_get_z(unsigned_input));
+
+		const float max_value = safe_to_float((1 << 16) - 1);
+		const float inv_max_value = 1.0f / max_value;
+
+		const Vector4_32 packed = vector_symmetric_round(vector_mul(unsigned_input, max_value));
+		const Vector4_32 decayed = vector_mul(packed, inv_max_value);
+		return vector_sub(vector_mul(decayed, vector_set(2.0f)), vector_set(1.0f));
+	}
+
 	inline void ACL_SIMD_CALL pack_vector3_32(Vector4_32Arg0 vector, uint8_t XBits, uint8_t YBits, uint8_t ZBits, bool is_unsigned, uint8_t* out_vector_data)
 	{
 		ACL_ASSERT(XBits + YBits + ZBits == 32, "Sum of XYZ bits does not equal 32!");
@@ -705,6 +732,33 @@ namespace acl
 		unaligned_write(vector_u64, out_vector_data);
 	}
 
+	inline Vector4_32 ACL_SIMD_CALL decay_vector3_uXX(Vector4_32Arg0 input, uint32_t num_bits)
+	{
+		ACL_ASSERT(vector_all_greater_equal(input, vector_zero_32()) && vector_all_less_equal(input, vector_set(1.0f)), "Expected normalized unsigned input value: %f, %f, %f", vector_get_x(input), vector_get_y(input), vector_get_z(input));
+
+		const float max_value = safe_to_float((1 << num_bits) - 1);
+		const float inv_max_value = 1.0f / max_value;
+
+		const Vector4_32 packed = vector_symmetric_round(vector_mul(input, max_value));
+		const Vector4_32 decayed = vector_mul(packed, inv_max_value);
+		return decayed;
+	}
+
+	inline Vector4_32 ACL_SIMD_CALL decay_vector3_sXX(Vector4_32Arg0 input, uint32_t num_bits)
+	{
+		const Vector4_32 half = vector_set(0.5f);
+		const Vector4_32 unsigned_input = vector_add(vector_mul(input, half), half);
+
+		ACL_ASSERT(vector_all_greater_equal(unsigned_input, vector_zero_32()) && vector_all_less_equal(unsigned_input, vector_set(1.0f)), "Expected normalized unsigned input value: %f, %f, %f", vector_get_x(unsigned_input), vector_get_y(unsigned_input), vector_get_z(unsigned_input));
+
+		const float max_value = safe_to_float((1 << num_bits) - 1);
+		const float inv_max_value = 1.0f / max_value;
+
+		const Vector4_32 packed = vector_symmetric_round(vector_mul(unsigned_input, max_value));
+		const Vector4_32 decayed = vector_mul(packed, inv_max_value);
+		return vector_sub(vector_mul(decayed, vector_set(2.0f)), vector_set(1.0f));
+	}
+
 	// Assumes the 'vector_data' is in big-endian order and padded in order to load up to 16 bytes from it
 	inline Vector4_32 ACL_SIMD_CALL unpack_vector2_uXX_unsafe(uint8_t num_bits, const uint8_t* vector_data, uint32_t bit_offset)
 	{


Test patch:
diff --git a/tests/sources/math/test_vector4_impl.h b/tests/sources/math/test_vector4_impl.h
--- a/tests/sources/math/test_vector4_impl.h
+++ b/tests/sources/math/test_vector4_impl.h
@@ -562,6 +562,29 @@ void test_vector4_impl(const Vector4Type& zero, const QuatType& identity, const
 	REQUIRE(vector_get_y(vector_sign(test_value0)) == scalar_sign(test_value0_flt[1]));
 	REQUIRE(vector_get_z(vector_sign(test_value0)) == scalar_sign(test_value0_flt[2]));
 	REQUIRE(vector_get_w(vector_sign(test_value0)) == scalar_sign(test_value0_flt[3]));
+
+	{
+		const Vector4Type input0 = vector_set(FloatType(-1.75), FloatType(-1.5), FloatType(-1.4999), FloatType(-0.5));
+		const Vector4Type input1 = vector_set(FloatType(-0.4999), FloatType(0.0), FloatType(0.4999), FloatType(0.5));
+		const Vector4Type input2 = vector_set(FloatType(1.4999), FloatType(1.5), FloatType(1.75), FloatType(0.0));
+
+		const Vector4Type result0 = vector_symmetric_round(input0);
+		const Vector4Type result1 = vector_symmetric_round(input1);
+		const Vector4Type result2 = vector_symmetric_round(input2);
+
+		REQUIRE(vector_get_x(result0) == symmetric_round(vector_get_x(input0)));
+		REQUIRE(vector_get_y(result0) == symmetric_round(vector_get_y(input0)));
+		REQUIRE(vector_get_z(result0) == symmetric_round(vector_get_z(input0)));
+		REQUIRE(vector_get_w(result0) == symmetric_round(vector_get_w(input0)));
+		REQUIRE(vector_get_x(result1) == symmetric_round(vector_get_x(input1)));
+		REQUIRE(vector_get_y(result1) == symmetric_round(vector_get_y(input1)));
+		REQUIRE(vector_get_z(result1) == symmetric_round(vector_get_z(input1)));
+		REQUIRE(vector_get_w(result1) == symmetric_round(vector_get_w(input1)));
+		REQUIRE(vector_get_x(result2) == symmetric_round(vector_get_x(input2)));
+		REQUIRE(vector_get_y(result2) == symmetric_round(vector_get_y(input2)));
+		REQUIRE(vector_get_z(result2) == symmetric_round(vector_get_z(input2)));
+		REQUIRE(vector_get_w(result2) == symmetric_round(vector_get_w(input2)));
+	}
 }
 
 template<typename Vector4Type, typename FloatType, VectorMix XArg>
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
@@ -246,6 +246,29 @@ TEST_CASE("pack_vector3_48", "[math][vector4][packing]")
 	}
 }
 
+TEST_CASE("decay_vector3_48", "[math][vector4][decay]")
+{
+	{
+		uint32_t num_errors = 0;
+		for (uint32_t value = 0; value < 65536; ++value)
+		{
+			const float value_signed = unpack_scalar_signed(value, 16);
+			const float value_unsigned = unpack_scalar_unsigned(value, 16);
+
+			Vector4_32 vec0 = vector_set(value_signed, value_signed, value_signed);
+			Vector4_32 vec1 = decay_vector3_s48(vec0);
+			if (!vector_all_near_equal3(vec0, vec1, 1.0e-6f))
+				num_errors++;
+
+			vec0 = vector_set(value_unsigned, value_unsigned, value_unsigned);
+			vec1 = decay_vector3_u48(vec0);
+			if (!vector_all_near_equal3(vec0, vec1, 1.0e-6f))
+				num_errors++;
+		}
+		REQUIRE(num_errors == 0);
+	}
+}
+
 TEST_CASE("pack_vector3_32", "[math][vector4][packing]")
 {
 	{
@@ -377,6 +400,46 @@ TEST_CASE("pack_vector3_XX", "[math][vector4][packing]")
 	}
 }
 
+TEST_CASE("decay_vector3_XX", "[math][vector4][decay]")
+{
+	{
+		uint32_t num_errors = 0;
+
+		Vector4_32 vec0 = vector_set(unpack_scalar_signed(0, 16), unpack_scalar_signed(12355, 16), unpack_scalar_signed(43222, 16));
+		Vector4_32 vec1 = decay_vector3_sXX(vec0, 16);
+		if (!vector_all_near_equal3(vec0, vec1, 1.0e-6f))
+			num_errors++;
+
+		vec0 = vector_set(unpack_scalar_unsigned(0, 16), unpack_scalar_unsigned(12355, 16), unpack_scalar_unsigned(43222, 16));
+		vec1 = decay_vector3_uXX(vec0, 16);
+		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)
+		{
+			uint8_t num_bits = get_num_bits_at_bit_rate(bit_rate);
+			uint32_t max_value = (1 << num_bits) - 1;
+			for (uint32_t value = 0; value <= max_value; ++value)
+			{
+				const float value_signed = clamp(unpack_scalar_signed(value, num_bits), -1.0f, 1.0f);
+				const float value_unsigned = clamp(unpack_scalar_unsigned(value, num_bits), 0.0f, 1.0f);
+
+				vec0 = vector_set(value_signed, value_signed, value_signed);
+				vec1 = decay_vector3_sXX(vec0, num_bits);
+				if (!vector_all_near_equal3(vec0, vec1, 1.0e-6f))
+					num_errors++;
+
+				vec0 = vector_set(value_unsigned, value_unsigned, value_unsigned);
+				vec1 = decay_vector3_uXX(vec0, num_bits);
+				if (!vector_all_near_equal3(vec0, vec1, 1.0e-6f))
+					num_errors++;
+			}
+		}
+
+		REQUIRE(num_errors == 0);
+	}
+}
+
 TEST_CASE("pack_vector2_64", "[math][vector4][packing]")
 {
 	{