@@ -56,6 +56,37 @@ public sealed class Signature
5656 public required string Key { get ; init ; }
5757 }
5858
59+ /// <summary>
60+ /// Process-wide signature cache. Painter-sort signatures depend only
61+ /// on the input geometry + sort algorithm + camera-distance + cull-margin
62+ /// — not on the AST, axes' ScalarNode identities, or the consumer's
63+ /// host control. So multiple Combobulate instances rendering the same
64+ /// model (e.g. a grid of book thumbnails sharing one cached
65+ /// <see cref="ObjGeometry"/>) can reuse a single bake's signature
66+ /// table; only materialisation of the per-cell sprite trees is
67+ /// per-instance work.
68+ ///
69+ /// <para>Keyed by reference-identity on the geometry, plus the
70+ /// algorithm and float parameters. Geometry caching lives in
71+ /// <see cref="ObjCache"/>; same parsed model always yields the same
72+ /// <see cref="ObjGeometry"/> instance, so reference-equality is the
73+ /// right invariant here.</para>
74+ /// </summary>
75+ private static readonly System . Collections . Generic . Dictionary < CacheKey , Signature [ ] > s_cache = new ( ) ;
76+ private static readonly object s_cacheLock = new ( ) ;
77+
78+ private readonly record struct CacheKey ( ObjGeometry Geom , SortAlgorithm Sort , int CameraDistanceBits , int CullMarginCosBits ) ;
79+
80+ /// <summary>Number of distinct cached signature tables; for diagnostics.</summary>
81+ public static int CacheCount { get { lock ( s_cacheLock ) return s_cache . Count ; } }
82+
83+ /// <summary>Drop all cached signature tables. For tests and explicit
84+ /// invalidation when a sorter implementation changes.</summary>
85+ public static void ClearCache ( )
86+ {
87+ lock ( s_cacheLock ) s_cache . Clear ( ) ;
88+ }
89+
5990 /// <summary>Run the bake.</summary>
6091 public static Signature [ ] Bake (
6192 Matrix4x4Node transformNode ,
@@ -65,6 +96,35 @@ public static Signature[] Bake(
6596 float cameraDistance ,
6697 float cullMarginCos ,
6798 CancellationToken ct )
99+ {
100+ var key = new CacheKey ( geometry , sortAlgorithm ,
101+ BitConverter . SingleToInt32Bits ( cameraDistance ) ,
102+ BitConverter . SingleToInt32Bits ( cullMarginCos ) ) ;
103+ lock ( s_cacheLock )
104+ {
105+ if ( s_cache . TryGetValue ( key , out var cached ) ) return cached ;
106+ }
107+
108+ var fresh = BakeInternal ( transformNode , axes , geometry , sortAlgorithm , cameraDistance , cullMarginCos , ct ) ;
109+
110+ lock ( s_cacheLock )
111+ {
112+ // Two threads may race a miss; the second writer's insert is
113+ // a harmless overwrite (signatures are deterministic for a
114+ // given geometry).
115+ s_cache [ key ] = fresh ;
116+ }
117+ return fresh ;
118+ }
119+
120+ private static Signature [ ] BakeInternal (
121+ Matrix4x4Node transformNode ,
122+ TransformAnimationAxis [ ] axes ,
123+ ObjGeometry geometry ,
124+ SortAlgorithm sortAlgorithm ,
125+ float cameraDistance ,
126+ float cullMarginCos ,
127+ CancellationToken ct )
68128 {
69129 if ( axes is null || axes . Length == 0 )
70130 throw new ArgumentException ( "At least one axis required." , nameof ( axes ) ) ;
0 commit comments