@@ -56,7 +56,34 @@ internal sealed class GraphicsAdapter : RGraphics
5656 /// call - Win2D layers are <see cref="IDisposable"/> and must be disposed in LIFO order, unlike
5757 /// WPF's <see cref="System.Windows.Media.DrawingContext.Pop"/>, which needs no such bookkeeping.
5858 /// </summary>
59- private readonly Stack < CanvasActiveLayer > _layerStack = new Stack < CanvasActiveLayer > ( ) ;
59+ private readonly Stack < ClipLayer > _layerStack = new Stack < ClipLayer > ( ) ;
60+
61+ /// <summary>
62+ /// A single entry of <see cref="_layerStack"/>: the pushed Win2D layer and, for
63+ /// <see cref="PushClipExclude"/>, the geometry masking it. The mask has to outlive the layer - D2D
64+ /// reads it until the layer is popped - so the two are released together in <see cref="PopClip"/>.
65+ /// Both are null on a measure-only instance, which has no session to push a layer on.
66+ /// </summary>
67+ private readonly struct ClipLayer
68+ {
69+ public ClipLayer ( CanvasActiveLayer layer , CanvasGeometry mask )
70+ {
71+ Layer = layer ;
72+ Mask = mask ;
73+ }
74+
75+ private CanvasActiveLayer Layer { get ; }
76+
77+ private CanvasGeometry Mask { get ; }
78+
79+ public void Dispose ( )
80+ {
81+ if ( Layer != null )
82+ Layer . Dispose ( ) ;
83+ if ( Mask != null )
84+ Mask . Dispose ( ) ;
85+ }
86+ }
6087
6188 #endregion
6289
@@ -108,17 +135,36 @@ public override void PopClip()
108135 public override void PushClip ( RRect rect )
109136 {
110137 _clipStack . Push ( rect ) ;
111- _layerStack . Push ( _g . CreateLayer ( 1f , Utils . Convert ( rect ) ) ) ;
138+
139+ // Measure-only instance - the clip is still tracked for layout, but there is no session to
140+ // push it onto (same null-session guard as SetAntiAliasSmoothingMode below).
141+ _layerStack . Push ( _g != null
142+ ? new ClipLayer ( _g . CreateLayer ( 1f , Utils . Convert ( rect ) ) , null )
143+ : new ClipLayer ( null , null ) ) ;
112144 }
113145
114146 public override void PushClipExclude ( RRect rect )
115147 {
116- var full = CanvasGeometry . CreateRectangle ( _device , Utils . Convert ( _clipStack . Peek ( ) ) ) ;
117- var excluded = CanvasGeometry . CreateRectangle ( _device , Utils . Convert ( rect ) ) ;
118- var combined = full . CombineWith ( excluded , Matrix3x2 . Identity , CanvasGeometryCombine . Exclude ) ;
148+ var current = _clipStack . Peek ( ) ;
149+ _clipStack . Push ( current ) ;
150+
151+ if ( _g == null )
152+ {
153+ _layerStack . Push ( new ClipLayer ( null , null ) ) ;
154+ return ;
155+ }
156+
157+ // full/excluded are only inputs to the combine, so they are released right away; combined is
158+ // handed to the layer and released with it. All three are unmanaged D2D resources created on
159+ // every clip-exclude of every paint, so none of them can be left to the finalizer.
160+ CanvasGeometry combined ;
161+ using ( var full = CanvasGeometry . CreateRectangle ( _device , Utils . Convert ( current ) ) )
162+ using ( var excluded = CanvasGeometry . CreateRectangle ( _device , Utils . Convert ( rect ) ) )
163+ {
164+ combined = full . CombineWith ( excluded , Matrix3x2 . Identity , CanvasGeometryCombine . Exclude ) ;
165+ }
119166
120- _clipStack . Push ( _clipStack . Peek ( ) ) ;
121- _layerStack . Push ( _g . CreateLayer ( 1f , combined ) ) ;
167+ _layerStack . Push ( new ClipLayer ( _g . CreateLayer ( 1f , combined ) , combined ) ) ;
122168 }
123169
124170 public override object SetAntiAliasSmoothingMode ( )
0 commit comments