Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 36 additions & 98 deletions RLBotCS/ManagerTools/RectUtil.cs
Original file line number Diff line number Diff line change
@@ -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
);

/// <summary>
/// The maximum number of subdivisions of the [0,1] interval to store.
/// The maximum possible resulting number of entries is ⌈<tt>MaxSubdivisions</tt>/2⌉,
/// but only those whose sum of the numerator and denominator does
/// not excede <tt>Rendering.RectangleStringMaxLength</tt> are included,
/// so ideally this should be a highly composite number.<br/>
/// For 55440, there are 17635 entries, corresponding to 137.77 kiB of memory.
/// Greatest common divisor by Euclidean algorithm.<br/>
/// An optimized implementation based on https://stackoverflow.com/a/41766138.
/// </summary>
private const ushort MaxSubdivisions = 55440;

private const float HalfPrecisionRangeHigh = 4096f;
private const float HalfPrecisionRangeLow = 1.0f / HalfPrecisionRangeHigh;

private static readonly ImmutableArray<float> 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<float, (ushort, ushort)> 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)
/// <summary>
/// Discards the same number of least significant bits from a and b
/// if either is too large to fit into a ushort.
/// </summary>
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));
}

/// <summary>
/// Approximates the rectangle <tt>width</tt>×<tt>height</tt> with <tt>cols</tt>×<tt>rows</tt>
/// Represents the rectangle <tt>width</tt>×<tt>height</tt> with <tt>cols</tt>×<tt>rows</tt>
/// rectangles with dimensions <tt>elementWidth</tt>×<tt>elementHeight</tt> scaled by <tt>scale</tt>.
/// </summary>
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));
}
}
2 changes: 1 addition & 1 deletion RLBotCS/ManagerTools/Rendering.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ private ushort SendRect3D(Rect3DT rect3Dt, GameState gameState)
/// <returns>The rectangle string and the font scaling</returns>
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,
Expand Down
47 changes: 38 additions & 9 deletions RLBotCSTests/ManagerTools/RectUtilTest.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using RLBotCS.ManagerTools;

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.
Expand All @@ -25,27 +31,50 @@ 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);
}
}
}
}
}

[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);
}
}
}
}
Loading