Internal/ShapeTextureCache.cs
using System;
using System.Collections.Generic;
using Sandbox;
namespace Goo.Internal;
// Variable-length cache key for Polygon. Holds the same Vector2[] reference the user
// passed to the Blob (no defensive copy); structural equality + content hash let two
// independently-constructed arrays with identical contents share a cache slot.
internal readonly struct PolygonKey : IEquatable<PolygonKey>
{
public readonly Vector2[] Points;
public PolygonKey(Vector2[] points) { Points = points; }
public bool Equals(PolygonKey other)
{
if (ReferenceEquals(Points, other.Points)) return true;
if (Points is null || other.Points is null) return false;
if (Points.Length != other.Points.Length) return false;
for (int i = 0; i < Points.Length; i++)
if (Points[i] != other.Points[i]) return false;
return true;
}
public override bool Equals(object? obj) => obj is PolygonKey k && Equals(k);
public override int GetHashCode()
{
if (Points is null) return 0;
var h = new HashCode();
h.Add(Points.Length);
for (int i = 0; i < Points.Length; i++)
{
h.Add(Points[i].x);
h.Add(Points[i].y);
}
return h.ToHashCode();
}
}
internal static class ShapeTextureCache
{
// Key is (kind, ShapeParams) so ShapeParams.Equals/GetHashCode (all 5 fields) govern lookup.
static readonly Dictionary<(BlobKind, ShapeParams), Texture?> _cache = new();
static readonly Dictionary<PolygonKey, Texture?> _polygonCache = new();
// Test seam: when non-null, replaces the real Texture.Create path. The
// production path is exercised at runtime; tests inject a counter here.
// Assigned only from Code.Tests; suppress CS0649 in the production build.
#pragma warning disable CS0649
internal static Func<BlobKind, ShapeParams, Texture?>? TestBaker;
internal static Func<PolygonKey, Texture?>? TestPolygonBaker;
#pragma warning restore CS0649
public static Texture? GetOrBake(BlobKind kind, in ShapeParams p)
{
var key = (kind, p);
if (_cache.TryGetValue(key, out var tex)) return tex;
if (TestBaker is not null)
{
tex = TestBaker(kind, p);
}
else
{
byte[] rgba = kind switch
{
BlobKind.Sector => ShapeRasterizers.BakeSector(in p),
BlobKind.Arc => ShapeRasterizers.BakeArc(in p),
_ => throw new InvalidOperationException($"ShapeTextureCache: unsupported BlobKind {kind}"),
};
int W = ShapeRasterizers.BakeResolution;
// PackKey still useful as a debug-name suffix (stable hash); ignores E
// which is fine for a name.
long nameKey = p.PackKey(kind);
tex = Texture.Create(W, W).WithName($"goo-shape-{kind}-{nameKey:x}").WithData(rgba).Finish();
}
_cache[key] = tex;
return tex;
}
public static Texture? GetOrBakePolygon(Vector2[] points)
{
var key = new PolygonKey(points);
if (_polygonCache.TryGetValue(key, out var tex)) return tex;
if (TestPolygonBaker is not null)
{
tex = TestPolygonBaker(key);
}
else
{
byte[] rgba = ShapeRasterizers.BakePolygon(points);
int W = ShapeRasterizers.BakeResolution;
tex = Texture.Create(W, W).WithName($"goo-shape-polygon-{key.GetHashCode():x}").WithData(rgba).Finish();
}
_polygonCache[key] = tex;
return tex;
}
// Test seam: clears the cache between tests.
internal static void Reset()
{
_cache.Clear();
_polygonCache.Clear();
}
}