@@ -45,6 +45,21 @@ internal sealed class BakedAspectGraphRenderer : IDisposable
4545 private readonly ContainerVisual _parent ;
4646
4747 private ContainerVisual [ ] ? _trees ;
48+ /// <summary>
49+ /// For each entry of <see cref="_trees"/>, the cached-quad indices of
50+ /// the visible sprites in painter order (matches the order in which
51+ /// they were inserted as children of the tree). Used by
52+ /// <see cref="UpdateBindings"/> to map a child position back to its
53+ /// source quad so a hot brush swap can find the right entry in
54+ /// <see cref="ResolvedQuadMaterials"/> without touching the bake.
55+ /// </summary>
56+ private int [ ] [ ] ? _treeVisibleQuadIndices ;
57+ /// <summary>
58+ /// Geometry the current trees were built against; needed by
59+ /// <see cref="UpdateBindings"/> when reusing existing trees so we
60+ /// can validate the new bindings have the same quad count.
61+ /// </summary>
62+ private ObjGeometry ? _treesGeometry ;
4863 // Staging trees from the in-flight bake. They are inserted into
4964 // _parent.Children at Opacity=0 as ChunkBuild progresses; on Dispose
5065 // (or on stale-generation bail-out) we walk this and remove them so
@@ -257,8 +272,9 @@ private void Materialise(
257272 EnsureBakedMatrixSource ( ) ;
258273 var newTrees = new ContainerVisual [ computed . Cells . Length ] ;
259274 var newOpacityExprs = new Microsoft . Toolkit . Uwp . UI . Animations . ExpressionsFork . ScalarNode [ computed . Cells . Length ] ;
275+ var newTreeIndices = new int [ computed . Cells . Length ] [ ] ;
260276 ChunkBuild ( computed , geometry , bindings , scale , hostW , hostH , axes ,
261- newTrees , newOpacityExprs , startIndex : 0 , ui , generation ) ;
277+ newTrees , newOpacityExprs , newTreeIndices , startIndex : 0 , ui , generation ) ;
262278 }
263279 catch ( Exception ex )
264280 {
@@ -283,6 +299,7 @@ private void ChunkBuild(
283299 TransformAnimationAxis [ ] axes ,
284300 ContainerVisual [ ] newTrees ,
285301 Microsoft . Toolkit . Uwp . UI . Animations . ExpressionsFork . ScalarNode [ ] newOpacityExprs ,
302+ int [ ] [ ] newTreeIndices ,
286303 int startIndex ,
287304 Microsoft . UI . Dispatching . DispatcherQueue ui ,
288305 int generation )
@@ -304,7 +321,7 @@ private void ChunkBuild(
304321 var c = computed . Cells [ i ] ;
305322 var tree = _compositor . CreateContainerVisual ( ) ;
306323 tree . Opacity = 0 ; // hidden until swap
307- BuildTreeContent ( tree , geometry , bindings , scale , hostW , hostH , c . Sig . Order , c . Sig . Visibility ) ;
324+ newTreeIndices [ i ] = BuildTreeContent ( tree , geometry , bindings , scale , hostW , hostH , c . Sig . Order , c . Sig . Visibility ) ;
308325 // Build (but don't start) the opacity expression — we'll start
309326 // them all in the swap step so the new tree set lights up
310327 // atomically and the old one disappears in the same compositor
@@ -322,7 +339,7 @@ private void ChunkBuild(
322339 {
323340 // More to do — schedule the next chunk.
324341 ui . TryEnqueue ( ( ) => ChunkBuild ( computed , geometry , bindings , scale , hostW , hostH , axes ,
325- newTrees , newOpacityExprs , end , ui , generation ) ) ;
342+ newTrees , newOpacityExprs , newTreeIndices , end , ui , generation ) ) ;
326343 }
327344 else
328345 {
@@ -332,6 +349,8 @@ private void ChunkBuild(
332349 if ( _disposed || generation != _bakeGeneration ) { EvictStaging ( ) ; return ; }
333350 DisposeTrees ( ) ;
334351 _trees = newTrees ;
352+ _treeVisibleQuadIndices = newTreeIndices ;
353+ _treesGeometry = geometry ;
335354 _stagingTrees = null ;
336355 // Stash signatures + geometry so diagnostics can compare the
337356 // current live sign vector against what was baked.
@@ -381,7 +400,7 @@ private static ScalarNode BuildAxisInCellExpression_OBSOLETE(
381400 return ( ScalarNode ) 0f ;
382401 }
383402
384- private void BuildTreeContent (
403+ private int [ ] BuildTreeContent (
385404 ContainerVisual tree ,
386405 ObjGeometry geometry ,
387406 ResolvedQuadMaterials bindings ,
@@ -411,12 +430,69 @@ private void BuildTreeContent(
411430 sprite . IsVisible = visibility [ q ] ;
412431 sprites [ q ] = sprite ;
413432 }
433+ // Build the painter-order list of *visible* quads and parent them
434+ // in that order. Return the index list so UpdateBindings can map
435+ // children back to source quads later without re-deriving.
436+ var visibleOrder = new System . Collections . Generic . List < int > ( order . Length ) ;
414437 for ( int i = 0 ; i < order . Length ; i ++ )
415438 {
416439 int qi = order [ i ] ;
417440 if ( ! visibility [ qi ] ) continue ;
418441 tree . Children . InsertAtTop ( sprites [ qi ] ) ;
442+ visibleOrder . Add ( qi ) ;
443+ }
444+ return visibleOrder . ToArray ( ) ;
445+ }
446+
447+ /// <summary>
448+ /// Hot brush-swap path. When the host's <c>Materials</c> dependency
449+ /// property changes but everything else (geometry, scale, host size,
450+ /// sort algorithm, transform AST, axes) stays the same, the bake's
451+ /// signature set + cell predicates + sprite tree topology are still
452+ /// valid — only the per-quad <see cref="CompositionBrush"/> needs to
453+ /// change. This walks each existing cell's child list in lock-step
454+ /// with its cached visible-quad-index mapping and reassigns brushes
455+ /// from <paramref name="newBindings"/> in place. Cost is O(cells ×
456+ /// visible-faces) brush writes, no allocation, no compositor expression
457+ /// reinstall.
458+ ///
459+ /// <para>Returns true if the swap was applied. Returns false when the
460+ /// renderer has no current trees (caller should fall back to a full
461+ /// bake) or when the new bindings' quad count differs from what the
462+ /// trees were built against (caller should likewise full-rebake; this
463+ /// only happens if the geometry changed, which already invalidates
464+ /// the trees).</para>
465+ /// </summary>
466+ public bool UpdateBindings ( ResolvedQuadMaterials newBindings )
467+ {
468+ if ( _trees is null || _treeVisibleQuadIndices is null || _treesGeometry is null ) return false ;
469+ var quads = _treesGeometry . Quads ;
470+ if ( newBindings . Bindings . Length != quads . Length ) return false ;
471+ for ( int t = 0 ; t < _trees . Length ; t ++ )
472+ {
473+ var tree = _trees [ t ] ;
474+ var visibleQuads = _treeVisibleQuadIndices [ t ] ;
475+ var children = tree . Children ;
476+ int childIdx = 0 ;
477+ // VisualCollection enumerates in render (z) order, bottom-to-top.
478+ // InsertAtTop adds to the top, so the FIRST inserted ends up at
479+ // the bottom and is enumerated first. We added to visibleQuads
480+ // in the same loop that called InsertAtTop, so child index k
481+ // pairs directly with visibleQuads[k].
482+ int n = visibleQuads . Length ;
483+ foreach ( var child in children )
484+ {
485+ if ( child is SpriteVisual sprite && childIdx < n )
486+ {
487+ int qi = visibleQuads [ childIdx ] ;
488+ var newBrush = newBindings . Bindings [ qi ] . Brush ;
489+ if ( ! ReferenceEquals ( sprite . Brush , newBrush ) )
490+ sprite . Brush = newBrush ;
491+ }
492+ childIdx ++ ;
493+ }
419494 }
495+ return true ;
420496 }
421497
422498 /// <summary>
@@ -681,5 +757,7 @@ private void DisposeTrees()
681757 t . Dispose ( ) ;
682758 }
683759 _trees = null ;
760+ _treeVisibleQuadIndices = null ;
761+ _treesGeometry = null ;
684762 }
685763}
0 commit comments