DiamondPanel.GemAtlas.cs
using System;
using Sandbox;

namespace Diamonds;

public sealed partial class DiamondPanel
{
	// One shared, runtime-only atlas. Panel ownership releases GPU storage when the last
	// canvas closes; restarts reuse it. Palette identity also invalidates art after hotload.
	static GemAtlas sharedGemAtlas;
	GemAtlas gemAtlas;
	bool drawingReferenceGems = false;

	sealed class GemAtlas : IDisposable
	{
		public const int TileWidth = 64, TileHeight = 96, Columns = 5, Rows = 9;
		public Texture Texture;
		public Fill[] Tiles;
		public int Users, Resolution, Builds;
		Color[] palette;

		public void Ensure( int resolution )
		{
			if ( Texture?.IsValid == true && palette == Palette && Resolution >= resolution ) return;
			resolution = Math.Max( resolution, Resolution );
			var texture = Sandbox.Texture.CreateRenderTarget()
				.WithSize( Columns * TileWidth * resolution, Rows * TileHeight * resolution )
				.WithFormat( ImageFormat.RGBA8888 ).Create();
			try
			{
				using ( var p = Painter.Begin( texture ) )
				{
					p.Clear( Color.Transparent );
					p.Scale( resolution );
					for ( int row = 0; row < Rows; row++ )
					for ( int color = 0; color < Columns; color++ )
					{
						int health = row == 0 ? 3 : row <= 4 ? 2 : 1;
						var corner = row == 0 ? DiamondBoard.Corner.Top : (DiamondBoard.Corner)((row - 1) % 4);
						DrawDiamondVector( p, new Vector2( (color + 0.5f) * TileWidth, (row + 0.5f) * TileHeight ),
							color, false, health: health, crackCorner: corner );
					}
				}
				var tiles = new Fill[Columns * Rows];
				for ( int row = 0; row < Rows; row++ )
				for ( int color = 0; color < Columns; color++ )
					// The filled rectangle crops the atlas tile without an extra clip or draw.
					tiles[row * Columns + color] = Fill.Image( texture,
						width: Columns * TileWidth, height: Rows * TileHeight,
						offsetX: -color * TileWidth, offsetY: -row * TileHeight );
				Texture?.Dispose();
				Texture = texture; Tiles = tiles; Resolution = resolution; palette = Palette; Builds++;
			}
			catch { texture.Dispose(); throw; }
		}

		public void Dispose()
		{
			Texture?.Dispose(); Texture = null; Tiles = null; palette = null;
		}
	}

	bool DrawAtlasGem( Painter p, Vector2 center, int color, float scale, int health, DiamondBoard.Corner corner )
	{
		var transform = p.Transform;
		float density = MathF.Max( new Vector2( transform.M11, transform.M12 ).Length,
			new Vector2( transform.M21, transform.M22 ).Length ) * scale;
		// Bound texture memory; unusually large zooms retain the resolution-independent artwork.
		if ( density > 4 ) return false;
		if ( gemAtlas is null )
		{
			gemAtlas = sharedGemAtlas ??= new GemAtlas();
			gemAtlas.Users++;
		}
		gemAtlas.Ensure( Math.Clamp( (int)MathF.Ceiling( density ), 2, 4 ) );
		int row = health >= 3 ? 0 : (health == 2 ? 1 : 5) + (int)corner;
		using var scope = p.Scope();
		p.Translate( center ); p.Scale( scale );
		p.Fill = gemAtlas.Tiles[row * GemAtlas.Columns + color];
		p.Stroke = Stroke.None;
		p.Rect( new Rect( -GemAtlas.TileWidth * 0.5f, -GemAtlas.TileHeight * 0.5f, GemAtlas.TileWidth, GemAtlas.TileHeight ) );
		return true;
	}

	void ReleaseGemAtlas()
	{
		if ( gemAtlas is null ) return;
		if ( --gemAtlas.Users == 0 )
		{
			gemAtlas.Dispose();
			if ( ReferenceEquals( sharedGemAtlas, gemAtlas ) ) sharedGemAtlas = null;
		}
		gemAtlas = null;
	}
}