Skip to content
Open
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
125 changes: 125 additions & 0 deletions RLBotCS/ManagerTools/RectUtil.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using System.Collections.Immutable;

namespace RLBotCS.ManagerTools;

public static class RectUtil
{
/// <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.
/// </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()
{
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)
{
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));
}

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 (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)
{
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(
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);

// 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));
}
}
45 changes: 12 additions & 33 deletions RLBotCS/ManagerTools/Rendering.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ private ushort SendRect2D(Rect2DT rect2Dt)

// Fake a filled rectangle using a string with colored background
var (text, scale) = MakeFakeRectangleString(
(int)Math.Abs(rect2Dt.Width * ResolutionWidthPixels),
(int)Math.Abs(rect2Dt.Height * ResolutionHeightPixels)
(uint)Math.Abs(rect2Dt.Width * ResolutionWidthPixels),
(uint)Math.Abs(rect2Dt.Height * ResolutionHeightPixels)
);

return _renderingCommandQueue.AddText2D(
Expand All @@ -114,8 +114,8 @@ private ushort SendRect3D(Rect3DT rect3Dt, GameState gameState)
{
// Fake a filled rectangle using a string with colored background
var (text, scale) = MakeFakeRectangleString(
(int)Math.Abs(rect3Dt.Width * ResolutionWidthPixels),
(int)Math.Abs(rect3Dt.Height * ResolutionHeightPixels)
(uint)Math.Abs(rect3Dt.Width * ResolutionWidthPixels),
(uint)Math.Abs(rect3Dt.Height * ResolutionHeightPixels)
);

return _renderingCommandQueue.AddText3D(
Expand All @@ -135,37 +135,16 @@ private ushort SendRect3D(Rect3DT rect3Dt, GameState gameState)
/// for rectangle rendering.
/// </summary>
/// <returns>The rectangle string and the font scaling</returns>
private (string, float) MakeFakeRectangleString(int width, int height)
private (string, float) MakeFakeRectangleString(uint width, uint 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 uint TestUpTo = 64;

[TestMethod]
public void ApproximateRectTest()
{
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);
Assert.AreEqual(1, cols);
Assert.AreEqual(1, rows);
// Slightly iffy, but it passes.
Assert.AreEqual((float)i / j, scale);
}
}

for (uint i = 1; i <= TestUpTo; ++i)
{
float iMin = i * 0.96f;
float iMax = i * 1.04f;
for (uint j = 1; j <= TestUpTo; ++j)
{
float jMin = j * 0.96f;
float jMax = j * 1.04f;
for (uint k = 1; k <= TestUpTo; ++k)
{
for (uint 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);
}
}
}
}
}
}
Loading