Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
132 changes: 132 additions & 0 deletions RLBotCS/ManagerTools/RectUtil.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
using System.Collections.Immutable;

namespace RLBotCS.ManagerTools;

public static class RectUtil
{
/// <summary>
/// Defines the maximum represented aspect ratio (<tt>Limit</tt>/1)
/// as well as the granularity of stored ratios.<br/>
/// Number of entries and the total resulting size in memory
/// for different <tt>Limit</tt> values are as follows:
/// <code>
/// ┌───────┬─────────┬────────────┐
/// │ Limit │ entries │ size │
/// ├───────┼─────────┼────────────┤
/// │ 512 │ 1174 │ 9.17 kiB │
/// │ 1024 │ 2563 │ 20.02 kiB │
/// │ 2048 │ 5555 │ 43.4 kiB │
/// │ 4096 │ 11977 │ 93.57 kiB │
/// │ 8192 │ 25673 │ 200.57 kiB │
/// │ 16384 │ 54778 │ 427.95 kiB │
/// └───────┴─────────┴────────────┘
/// </code>
/// </summary>
public const ushort Limit = 4096;

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()
{
SortedDictionary<float, (ushort, ushort)> dictionary = [];

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;
}

for (ushort a = 1; a <= Limit; ++a)
for (ushort b = 1; b <= a && a * b <= Limit; ++b)
if (Gcd(a, b) == 1)
dictionary.Add((float)a / b, (a, b));

ratios = [.. dictionary.Keys];
rects = [.. dictionary.Values];
}

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 bool LessThanGeoMean(float x, (float a, float b) m)
{
if (m.b >= HalfPrecisionRangeHigh)
return x < MathF.Sqrt(m.a) * MathF.Sqrt(m.b);
return x * x < m.a * m.b;
}

private static (ushort, ushort) Find(float value)
{
int higherIdx = ratios.BinarySearch(value);

if (higherIdx >= 0)
return rects[higherIdx];

higherIdx = ~higherIdx;

// No need to handle this because value >= 1.0 == ratios.First()
//if (higherIdx == 0)
// return rects.First();

if (higherIdx == ratios.Length)
return rects.Last();

int lowerIdx = higherIdx - 1;
return rects[
LessThanGeoMean(value, (ratios[lowerIdx], ratios[higherIdx]))
? lowerIdx
: higherIdx
];
}

private static (ushort cols, ushort rows) Find(float width, float height)
{
if (width >= height)
return Find(width / height);

(ushort rows, ushort cols) = Find(height / width);
return (cols, rows);
}

/// <summary>
/// Approximates 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(
int width,
int height,
int elementWidth,
int elementHeight
)
{
float elementsInWidth = (float)width / elementWidth;
float elementsInHeight = (float)height / elementHeight;
(ushort cols, ushort rows) = Find(elementsInWidth, elementsInHeight);

// 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));
}
}
35 changes: 7 additions & 28 deletions RLBotCS/ManagerTools/Rendering.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,35 +137,14 @@ private ushort SendRect3D(Rect3DT rect3Dt, GameState gameState)
/// <returns>The rectangle string and the font scaling</returns>
private (string, float) MakeFakeRectangleString(int width, int height)
{
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;
}

int gcd = Gcd(width, height);
int cols = (width / gcd) * (FontHeightPixels / FontWidthPixels);
int rows = height / gcd;
float scale = gcd / (float)FontHeightPixels;

if (cols + rows > RectangleStringMaxLength)
{
// The width-height ratio has resulting in a very long string.
// TODO: Consider an approximate solution as backup. Do we ever hit this case though?
Logger.LogWarning(
"A rendered rectangle requires more characters than budget allows. Consider different width-height ratio."
);
}
(ushort cols, ushort rows, float scale) = RectUtil.ApproximateRect(
width,
height,
FontWidthPixels,
FontHeightPixels
);

StringBuilder str = new StringBuilder(cols + rows);
StringBuilder str = new(cols + rows);
for (int c = 0; c < cols; c++)
{
str.Append(' ');
Expand Down
51 changes: 51 additions & 0 deletions RLBotCSTests/ManagerTools/RectUtilTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using RLBotCS.ManagerTools;

namespace RLBotCSTests.ManagerTools;

[TestClass]
public class RectUtilTest
{
const int TestUpTo = 64;

[TestMethod]
public void ApproximateRectTest()
{
for (int i = 1; i <= TestUpTo; ++i)
{
for (int j = 1; j <= TestUpTo; ++j)
{
(ushort cols, ushort rows, float scale) = RectUtil.ApproximateRect(i, i, j, j);
Assert.AreEqual(1, cols);
Assert.AreEqual(1, rows);
// Slightly iffy, but it passes.
Assert.AreEqual((float)i / j, scale);
}
}

for (int i = 1; i <= TestUpTo; ++i)
{
float iMin = i * 0.95f;
float iMax = i * 1.05f;
Comment thread
Veeno marked this conversation as resolved.
Outdated
for (int j = 1; j <= TestUpTo; ++j)
{
float jMin = j * 0.95f;
float jMax = j * 1.05f;
for (int k = 1; k <= TestUpTo; ++k)
{
for (int l = 1; l <= TestUpTo; ++l)
{
(ushort cols, ushort rows, float scale) = RectUtil.ApproximateRect(
i,
j,
k,
l
);
Assert.IsInRange(iMin, iMax, k * scale * cols);
Assert.IsInRange(jMin, jMax, l * scale * rows);
}
}
}
}
}
}