55using System . Collections . Generic ;
66using System . Diagnostics ;
77using System . Numerics ;
8+ using System . Threading ;
89using osu . Framework . Allocation ;
910using osu . Framework . Extensions . Color4Extensions ;
1011using osu . Framework . Graphics ;
@@ -56,6 +57,11 @@ public abstract partial class SmokeSegment : Drawable, ITexturedShaderDrawable
5657
5758 protected readonly List < SmokePoint > SmokePoints = new List < SmokePoint > ( ) ;
5859
60+ // Incremented (with a full memory barrier via Interlocked) whenever SmokePoints changes.
61+ // The draw node reads this with Volatile.Read and only copies SmokePoints when the
62+ // version changes, eliminating the O(N) per-frame list copy during smoke fade.
63+ private int smokeVersion ;
64+
5965 private float pointInterval => width * 7f / 8 ;
6066
6167 private double smokeStartTime { get ; set ; } = double . MinValue ;
@@ -85,6 +91,8 @@ public void StartDrawing(double time)
8591 SmokePoints . Clear ( ) ;
8692 lastPosition = null ;
8793 totalDistance = pointInterval ;
94+ // Signal the draw node to discard its cached copy.
95+ Interlocked . Increment ( ref smokeVersion ) ;
8896 }
8997
9098 public void AddPosition ( Vector2 position , double time )
@@ -118,6 +126,11 @@ public void AddPosition(Vector2 position, double time)
118126
119127 pointPos += increment ;
120128 }
129+
130+ // Full barrier: the SmokePoints writes above must be visible to the render
131+ // thread before the version increment so the draw node always sees a
132+ // consistent snapshot when it re-copies the list.
133+ Interlocked . Increment ( ref smokeVersion ) ;
121134 }
122135
123136 Invalidate ( Invalidation . DrawNode ) ;
@@ -132,6 +145,10 @@ public void FinishDrawing(double time)
132145
133146 double initialFadeOutDurationTrunc = Math . Min ( initial_fade_out_duration , smokeEndTime - smokeStartTime ) ;
134147 LifetimeEnd = smokeEndTime + final_fade_out_duration + initialFadeOutDurationTrunc / re_fade_in_speed + initialFadeOutDurationTrunc / final_fade_out_speed ;
148+
149+ // Notify the draw node that the end-time state changed so it re-copies on its
150+ // next ApplyState and correctly clamps future-point lookups.
151+ Interlocked . Increment ( ref smokeVersion ) ;
135152 }
136153
137154 protected override DrawNode CreateDrawNode ( ) => new SmokeDrawNode ( this ) ;
@@ -182,14 +199,16 @@ protected class SmokeDrawNode : TexturedShaderDrawNode
182199 protected double SmokeEndTime { get ; private set ; }
183200 protected double CurrentTime { get ; private set ; }
184201
185- private readonly List < SmokePoint > points = new List < SmokePoint > ( ) ;
202+ private readonly List < SmokePoint > allPoints = new List < SmokePoint > ( ) ;
203+ private int lastSmokeVersion = - 1 ;
186204 private IVertexBatch < TexturedVertex2D > ? quadBatch ;
187205 private float width ;
188206 private float height ;
189207 private Vector2 drawSize ;
190208 private Texture ? texture ;
191209 private int rotationSeed ;
192- private int firstVisiblePointIndex ;
210+ private int firstVisibleIndex ;
211+ private int futureIndex ;
193212
194213 // anim calculation vars (color, scale, direction)
195214 private double initialFadeOutDurationTrunc ;
@@ -226,26 +245,35 @@ public override void ApplyState()
226245 reFadeInTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTimeAfterSmokeEnded * ( 1 - 1 / re_fade_in_speed ) ;
227246 finalFadeOutTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTimeAfterSmokeEnded * ( 1 - 1 / final_fade_out_speed ) ;
228247
229- double firstVisiblePointTime = Math . Min ( SmokeEndTime , CurrentTime ) - initialFadeOutDurationTrunc ;
230- firstVisiblePointIndex = ~ Source . SmokePoints . BinarySearch ( new SmokePoint { Time = firstVisiblePointTime } , new SmokePoint . LowerBoundComparer ( ) ) ;
231- int futurePointIndex = ~ Source . SmokePoints . BinarySearch ( new SmokePoint { Time = CurrentTime } , new SmokePoint . UpperBoundComparer ( ) ) ;
248+ // Only re-copy the full SmokePoints list when new points were added or the
249+ // smoke ended — not on every frame. Between add-calls the list is unchanged,
250+ // so the existing allPoints snapshot is still valid for the binary searches.
251+ int currentVersion = Volatile . Read ( ref Source . smokeVersion ) ;
232252
233- points . Clear ( ) ;
253+ if ( currentVersion != lastSmokeVersion )
254+ {
255+ allPoints . Clear ( ) ;
256+ allPoints . AddRange ( Source . SmokePoints ) ;
257+ lastSmokeVersion = currentVersion ;
258+ }
234259
235- for ( int i = firstVisiblePointIndex ; i < futurePointIndex ; i ++ )
236- points . Add ( Source . SmokePoints [ i ] ) ;
260+ double firstVisiblePointTime = Math . Min ( SmokeEndTime , CurrentTime ) - initialFadeOutDurationTrunc ;
261+ firstVisibleIndex = ~ allPoints . BinarySearch ( new SmokePoint { Time = firstVisiblePointTime } , new SmokePoint . LowerBoundComparer ( ) ) ;
262+ futureIndex = ~ allPoints . BinarySearch ( new SmokePoint { Time = CurrentTime } , new SmokePoint . UpperBoundComparer ( ) ) ;
237263 }
238264
239265 protected sealed override void Draw ( IRenderer renderer )
240266 {
241267 base . Draw ( renderer ) ;
242268
243- if ( points . Count == 0 )
269+ int visibleCount = futureIndex - firstVisibleIndex ;
270+
271+ if ( visibleCount <= 0 )
244272 return ;
245273
246274 quadBatch ??= renderer . CreateQuadBatch < TexturedVertex2D > ( 200 , 4 ) ;
247275
248- if ( points . Count > quadBatch . Size && quadBatch . Size != IRenderer . MAX_QUADS )
276+ if ( visibleCount > quadBatch . Size && quadBatch . Size != IRenderer . MAX_QUADS )
249277 {
250278 int batchSize = Math . Min ( quadBatch . Size * 2 , IRenderer . MAX_QUADS ) ;
251279 quadBatch = renderer . CreateQuadBatch < TexturedVertex2D > ( batchSize , 4 ) ;
@@ -261,8 +289,11 @@ protected sealed override void Draw(IRenderer renderer)
261289
262290 texture . Bind ( ) ;
263291
264- for ( int i = 0 ; i < points . Count ; i ++ )
265- drawPointQuad ( renderer , points [ i ] , textureRect , i + firstVisiblePointIndex ) ;
292+ // Iterate allPoints directly with absolute indices — no per-frame copy needed.
293+ // 'i' is the absolute index into allPoints (mirroring SmokePoints), which is
294+ // what getRotation() requires for its seeded RNG.
295+ for ( int i = firstVisibleIndex ; i < futureIndex ; i ++ )
296+ drawPointQuad ( renderer , allPoints [ i ] , textureRect , i ) ;
266297
267298 UnbindTextureShader ( renderer ) ;
268299 renderer . PopLocalMatrix ( ) ;
0 commit comments