Internal/ShapeTextureCache.cs

A small internal cache for rasterized polygon textures. It defines PolygonKey to compare Vector2[] contents structurally and ShapeTextureCache to memoize Texture instances produced by baking polygon alpha maps, with a test seam allowing injection of a baker and a Reset method.

File Access
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();
    }
}

// Polygon shapes still bake a CPU mask into a cached BackgroundImage (variable point count is
// awkward as a shader uniform, and polygons rarely animate their geometry). Sector and Arc moved
// to ui_shape.shader (a GPU SDF), so they no longer touch this cache.
internal static class ShapeTextureCache
{
    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<PolygonKey, Texture?>? TestPolygonBaker;
#pragma warning restore CS0649

    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()
    {
        _polygonCache.Clear();
    }
}