diff --git a/RLBotCS/ManagerTools/RectUtil.cs b/RLBotCS/ManagerTools/RectUtil.cs
index df3c26b..e3eb597 100644
--- a/RLBotCS/ManagerTools/RectUtil.cs
+++ b/RLBotCS/ManagerTools/RectUtil.cs
@@ -1,125 +1,63 @@
-using System.Collections.Immutable;
+using System.Numerics;
namespace RLBotCS.ManagerTools;
public static class RectUtil
{
+ private static readonly int LeadingZerosForUShort = BitOperations.LeadingZeroCount(
+ (uint)UInt16.MaxValue
+ );
+
///
- /// The maximum number of subdivisions of the [0,1] interval to store.
- /// The maximum possible resulting number of entries is ⌈MaxSubdivisions/2⌉,
- /// but only those whose sum of the numerator and denominator does
- /// not excede Rendering.RectangleStringMaxLength are included,
- /// so ideally this should be a highly composite number.
- /// For 55440, there are 17635 entries, corresponding to 137.77 kiB of memory.
+ /// Greatest common divisor by Euclidean algorithm.
+ /// An optimized implementation based on https://stackoverflow.com/a/41766138.
///
- private const ushort MaxSubdivisions = 55440;
-
- private const float HalfPrecisionRangeHigh = 4096f;
- private const float HalfPrecisionRangeLow = 1.0f / HalfPrecisionRangeHigh;
-
- private static readonly ImmutableArray ratios;
- private static readonly ImmutableArray<(ushort, ushort)> rects;
-
- static RectUtil()
+ private static uint Gcd(uint a, uint b)
{
- static int Gcd(int a, int b)
- {
- // Greatest common divisor by Euclidean algorithm https://stackoverflow.com/a/41766138
- while (a != 0 && b != 0)
- {
- if (a > b)
- a %= b;
- else
- b %= a;
- }
-
- return a | b;
- }
-
- SortedDictionary dictionary = [];
- float fMaxSubdivisions = MaxSubdivisions;
- for (ushort i = MaxSubdivisions / 2 + MaxSubdivisions % 2; i <= MaxSubdivisions; ++i)
+ if (a >= b)
+ a %= b;
+ while (a != 0)
{
- ushort gcd = (ushort)Gcd(i, MaxSubdivisions);
- ushort num = (ushort)(i / gcd);
- ushort den = (ushort)(MaxSubdivisions / gcd);
- if (num + den <= Rendering.RectangleStringMaxLength)
- dictionary.Add(i / fMaxSubdivisions, (num, den));
+ b %= a;
+ if (b == 0)
+ return a;
+ a %= b;
}
-
- ratios = [.. dictionary.Keys];
- rects = [.. dictionary.Values];
+ return b;
}
- private static float GeoMean(float a, float b)
- {
- if (
- a >= HalfPrecisionRangeHigh
- || b >= HalfPrecisionRangeHigh
- || a <= HalfPrecisionRangeLow
- || b <= HalfPrecisionRangeLow
- )
- return MathF.Sqrt(a) * MathF.Sqrt(b);
- return MathF.Sqrt(a * b);
- }
-
- private static (ushort, ushort) FindImpl(float value)
- {
- int higherIdx = ratios.BinarySearch(value);
-
- if (higherIdx >= 0)
- return rects[higherIdx];
-
- higherIdx = ~higherIdx;
-
- // No need to handle this because value >= 0.5 == ratios.First()
- //if (higherIdx == 0)
- // return rects.First();
-
- // No need to handle this because value <= 1.0 == ratios.Last()
- //if (higherIdx == ratios.Length)
- // return rects.Last();
-
- int lowerIdx = higherIdx - 1;
- return rects[value * 2 < ratios[lowerIdx] + ratios[higherIdx] ? lowerIdx : higherIdx];
- }
-
- private static (ushort, ushort) Find(float value)
- {
- if (value >= 0.5)
- return FindImpl(value);
-
- (ushort num, ushort den) = FindImpl(1f - value);
- return ((ushort)(den - num), den);
- }
-
- private static (ushort cols, ushort rows) Find(float width, float height)
+ ///
+ /// Discards the same number of least significant bits from a and b
+ /// if either is too large to fit into a ushort.
+ ///
+ private static (ushort, ushort, float) SafeCast(uint a, uint b)
{
- if (width <= height)
- return Find(width / height);
-
- (ushort rows, ushort cols) = Find(height / width);
- return (cols, rows);
+ if (a <= UInt16.MaxValue && b <= UInt16.MaxValue)
+ return ((ushort)a, (ushort)b, 1f);
+
+ int shift = Int32.Max(
+ LeadingZerosForUShort - BitOperations.LeadingZeroCount(a),
+ LeadingZerosForUShort - BitOperations.LeadingZeroCount(b)
+ );
+ return ((ushort)(a >> shift), (ushort)(b >> shift), 1f / (1u << shift));
}
///
- /// Approximates the rectangle width×height with cols×rows
+ /// Represents the rectangle width×height with cols×rows
/// rectangles with dimensions elementWidth×elementHeight scaled by scale.
///
- public static (ushort cols, ushort rows, float scale) ApproximateRect(
+ public static (ushort cols, ushort rows, float scale) RectSolve(
uint width,
uint height,
uint elementWidth,
uint elementHeight
)
{
- float elementsInWidth = (float)width / elementWidth;
- float elementsInHeight = (float)height / elementHeight;
- (ushort cols, ushort rows) = Find(elementsInWidth, elementsInHeight);
+ uint wh = width * elementHeight;
+ uint hw = height * elementWidth;
+ uint gcd = Gcd(wh, hw);
+ (ushort cols, ushort rows, float reduction) = SafeCast(wh / gcd, hw / gcd);
- // Ideal horizontal and vertical scale are
- // ((float)width / cols) / elementWidth == ((float)width / elementWidth) / cols == elementsInWidth / cols
- // ((float)height / rows) / elementHeight == ((float)height / elementHeight) / rows == elementsInHeight / rows
- return (cols, rows, GeoMean(elementsInWidth / cols, elementsInHeight / rows));
+ return (cols, rows, (gcd * reduction) / (elementWidth * elementHeight));
}
}
diff --git a/RLBotCS/ManagerTools/Rendering.cs b/RLBotCS/ManagerTools/Rendering.cs
index a2514bd..cd0029c 100644
--- a/RLBotCS/ManagerTools/Rendering.cs
+++ b/RLBotCS/ManagerTools/Rendering.cs
@@ -137,7 +137,7 @@ private ushort SendRect3D(Rect3DT rect3Dt, GameState gameState)
/// The rectangle string and the font scaling
private (string, float) MakeFakeRectangleString(uint width, uint height)
{
- (ushort cols, ushort rows, float scale) = RectUtil.ApproximateRect(
+ (ushort cols, ushort rows, float scale) = RectUtil.RectSolve(
width,
height,
FontWidthPixels,
diff --git a/RLBotCSTests/ManagerTools/RectUtilTest.cs b/RLBotCSTests/ManagerTools/RectUtilTest.cs
index fe93db8..3656f66 100644
--- a/RLBotCSTests/ManagerTools/RectUtilTest.cs
+++ b/RLBotCSTests/ManagerTools/RectUtilTest.cs
@@ -1,4 +1,5 @@
-using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
using RLBotCS.ManagerTools;
namespace RLBotCSTests.ManagerTools;
@@ -6,16 +7,21 @@ namespace RLBotCSTests.ManagerTools;
[TestClass]
public class RectUtilTest
{
- const uint TestUpTo = 64;
+ private static (float, float) RoundingBounds(uint n)
+ {
+ return (MathF.BitIncrement(n - 0.5f), MathF.BitDecrement(n + 0.5f));
+ }
[TestMethod]
- public void ApproximateRectTest()
+ public void RectSolveTest_Generic()
{
+ const uint TestUpTo = 64;
+
for (uint i = 1; i <= TestUpTo; ++i)
{
for (uint j = 1; j <= TestUpTo; ++j)
{
- (ushort cols, ushort rows, float scale) = RectUtil.ApproximateRect(i, i, j, j);
+ (ushort cols, ushort rows, float scale) = RectUtil.RectSolve(i, i, j, j);
Assert.AreEqual(1, cols);
Assert.AreEqual(1, rows);
// Slightly iffy, but it passes.
@@ -25,22 +31,21 @@ public void ApproximateRectTest()
for (uint i = 1; i <= TestUpTo; ++i)
{
- float iMin = i * 0.96f;
- float iMax = i * 1.04f;
+ (float iMin, float iMax) = RoundingBounds(i);
for (uint j = 1; j <= TestUpTo; ++j)
{
- float jMin = j * 0.96f;
- float jMax = j * 1.04f;
+ (float jMin, float jMax) = RoundingBounds(j);
for (uint k = 1; k <= TestUpTo; ++k)
{
for (uint l = 1; l <= TestUpTo; ++l)
{
- (ushort cols, ushort rows, float scale) = RectUtil.ApproximateRect(
+ (ushort cols, ushort rows, float scale) = RectUtil.RectSolve(
i,
j,
k,
l
);
+ Assert.IsLessThan(Rendering.RectangleStringMaxLength + 1, cols + rows);
Assert.IsInRange(iMin, iMax, k * scale * cols);
Assert.IsInRange(jMin, jMax, l * scale * rows);
}
@@ -48,4 +53,28 @@ public void ApproximateRectTest()
}
}
}
+
+ [TestMethod]
+ public void RectSolveTest_Practical()
+ {
+ const uint TestUpTo = 7680;
+
+ for (uint i = 1; i <= TestUpTo; ++i)
+ {
+ (float iMin, float iMax) = RoundingBounds(i);
+ for (uint j = 1; j <= TestUpTo; ++j)
+ {
+ (float jMin, float jMax) = RoundingBounds(j);
+ (ushort cols, ushort rows, float scale) = RectUtil.RectSolve(
+ i,
+ j,
+ Rendering.FontWidthPixels,
+ Rendering.FontHeightPixels
+ );
+ Assert.IsLessThan(Rendering.RectangleStringMaxLength + 1, cols + rows);
+ Assert.IsInRange(iMin, iMax, Rendering.FontWidthPixels * scale * cols);
+ Assert.IsInRange(jMin, jMax, Rendering.FontHeightPixels * scale * rows);
+ }
+ }
+ }
}