/
ImageAnimator.cs
426 lines (371 loc) · 15.1 KB
/
ImageAnimator.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Drawing.Imaging;
using System.Threading;
namespace System.Drawing;
/// <summary>
/// Animates one or more images that have time-based frames.
/// See the ImageInfo.cs file for the helper nested ImageInfo class.
///
/// A common pattern for using this class is as follows (See PictureBox control):
/// 1. The winform app (user's code) calls ImageAnimator.Animate() from the main thread.
/// 2. Animate() spawns the animating (worker) thread in the background, which will update the image
/// frames and raise the OnFrameChanged event, which handler will be executed in the main thread.
/// 3. The main thread triggers a paint event (Invalidate()) from the OnFrameChanged handler.
/// 4. From the OnPaint event, the main thread calls ImageAnimator.UpdateFrames() and then paints the
/// image (updated frame).
/// 5. The main thread calls ImageAnimator.StopAnimate() when needed. This does not kill the worker thread.
///
/// Comment on locking the image ref:
/// We need to synchronize access to sections of code that modify the image(s), but we don't want to block
/// animation of one image when modifying a different one; for this, we use the image ref for locking the
/// critical section (lock(image)).
///
/// This class is safe for multi-threading but Image is not; multithreaded applications must use a critical
/// section lock using the image ref the image access is not from the same thread that executes ImageAnimator
/// code. If the user code locks on the image ref forever a deadlock will happen preventing the animation
/// from occurring.
/// </summary>
public sealed partial class ImageAnimator
{
// We use a timer to apply an animation tick speeds of something a bit shorter than 50ms
// such that if the requested frame rate is about 20 frames per second, we will rarely skip
// a frame entirely. Sometimes we'll show a few more frames if available, but we will never
// show more than 25 frames a second and that's OK.
internal const int AnimationDelayMS = 40;
/// <summary>
/// A list of images to be animated.
/// </summary>
private static List<ImageInfo>? s_imageInfoList;
/// <summary>
/// A variable to flag when an image or images need to be updated due to the selection of a new frame
/// in an image. We don't need to synchronize access to this variable, in the case it is true we don't
/// do anything, otherwise the worse case is where a thread attempts to update the image's frame after
/// another one did which is harmless.
/// </summary>
private static bool s_anyFrameDirty;
/// <summary>
/// The thread used for animating the images.
/// </summary>
private static Thread? s_animationThread;
/// <summary>
/// Lock that allows either concurrent read-access to the images list for multiple threads, or write-
/// access to it for a single thread. Observe that synchronization access to image objects are done
/// with critical sections (lock).
/// </summary>
private static readonly ReaderWriterLock s_rwImgListLock = new();
/// <summary>
/// Flag to avoid a deadlock when waiting on a write-lock and an attempt to acquire a read-lock is
/// made in the same thread. If RWLock is currently owned by another thread, the current thread is going to wait on an
/// event using CoWaitForMultipleHandles while pumps message.
///
/// The comment above refers to the COM STA message pump, not to be confused with the UI message pump.
/// However, the effect is the same, the COM message pump will pump messages and dispatch them to the
/// window while waiting on the writer lock; this has the potential of creating a re-entrancy situation
/// that if during the message processing a wait on a reader lock is originated the thread will be block
/// on itself.
///
/// While processing STA message, the thread may call back into managed code. We do this because
/// we can not block finalizer thread. Finalizer thread may need to release STA objects on this thread. If
/// the current thread does not pump message, finalizer thread is blocked, and AD unload is blocked while
/// waiting for finalizer thread. RWLock is a fair lock. If a thread waits for a writer lock, then it needs
/// a reader lock while pumping message, the thread is blocked forever.
/// This TLS variable is used to flag the above situation and avoid the deadlock, it is ThreadStatic so each
/// thread calling into ImageAnimator is guarded against this problem.
/// </summary>
[ThreadStatic]
private static int t_threadWriterLockWaitCount;
/// <summary>
/// Prevent instantiation of this class.
/// </summary>
private ImageAnimator()
{
}
/// <summary>
/// Advances the frame in the specified image. The new frame is drawn the next time the image is rendered.
/// </summary>
public static void UpdateFrames(Image? image)
{
if (image is null || s_imageInfoList is null)
{
return;
}
if (t_threadWriterLockWaitCount > 0)
{
// Cannot acquire reader lock - frame update will be missed.
return;
}
// If the current thread already has the writer lock, no reader lock is acquired. Instead, the lock count on
// the writer lock is incremented. It already has a reader lock, the locks ref count will be incremented
// w/o placing the request at the end of the reader queue.
s_rwImgListLock.AcquireReaderLock(Timeout.Infinite);
try
{
bool foundDirty = false;
bool foundImage = false;
foreach (ImageInfo imageInfo in s_imageInfoList)
{
if (imageInfo.Image == image)
{
if (imageInfo.FrameDirty)
{
// See comment in the class header about locking the image ref.
lock (imageInfo.Image)
{
imageInfo.UpdateFrame();
}
}
foundImage = true;
}
else if (imageInfo.FrameDirty)
{
foundDirty = true;
}
if (foundDirty && foundImage)
{
break;
}
}
s_anyFrameDirty = foundDirty;
}
finally
{
s_rwImgListLock.ReleaseReaderLock();
}
}
/// <summary>
/// Advances the frame in all images currently being animated. The new frame is drawn the next time the image is rendered.
/// </summary>
public static void UpdateFrames()
{
if (!s_anyFrameDirty || s_imageInfoList is null)
{
return;
}
if (t_threadWriterLockWaitCount > 0)
{
// Cannot acquire reader lock at this time, frames update will be missed.
return;
}
s_rwImgListLock.AcquireReaderLock(Timeout.Infinite);
try
{
foreach (ImageInfo imageInfo in s_imageInfoList)
{
// See comment in the class header about locking the image ref.
lock (imageInfo.Image)
{
imageInfo.UpdateFrame();
}
}
s_anyFrameDirty = false;
}
finally
{
s_rwImgListLock.ReleaseReaderLock();
}
}
/// <summary>
/// Adds an image to the image manager. If the image does not support animation this method does nothing.
/// This method creates the image list and spawns the animation thread the first time it is called.
/// </summary>
public static void Animate(Image image, EventHandler onFrameChangedHandler)
{
if (image is null)
{
return;
}
ImageInfo? imageInfo = null;
// See comment in the class header about locking the image ref.
lock (image)
{
// could we avoid creating an ImageInfo object if FrameCount == 1 ?
imageInfo = new ImageInfo(image);
}
// If the image is already animating, stop animating it
StopAnimate(image, onFrameChangedHandler);
// Acquire a writer lock to modify the image info list. If the thread has a reader lock we need to upgrade
// it to a writer lock; acquiring a reader lock in this case would block the thread on itself.
// If the thread already has a writer lock its ref count will be incremented w/o placing the request in the
// writer queue. See ReaderWriterLock.AcquireWriterLock method in the MSDN.
bool readerLockHeld = s_rwImgListLock.IsReaderLockHeld;
LockCookie lockDowngradeCookie = default;
t_threadWriterLockWaitCount++;
try
{
if (readerLockHeld)
{
lockDowngradeCookie = s_rwImgListLock.UpgradeToWriterLock(Timeout.Infinite);
}
else
{
s_rwImgListLock.AcquireWriterLock(Timeout.Infinite);
}
}
finally
{
t_threadWriterLockWaitCount--;
Debug.Assert(t_threadWriterLockWaitCount >= 0, "threadWriterLockWaitCount less than zero.");
}
try
{
if (!imageInfo.Animated)
{
return;
}
// Construct the image array
s_imageInfoList ??= [];
// Add the new image
imageInfo.FrameChangedHandler = onFrameChangedHandler;
s_imageInfoList.Add(imageInfo);
// Construct a new timer thread if we haven't already
if (s_animationThread is null)
{
s_animationThread = new Thread(new ThreadStart(AnimateImages))
{
Name = nameof(ImageAnimator),
IsBackground = true
};
s_animationThread.Start();
}
}
finally
{
if (readerLockHeld)
{
s_rwImgListLock.DowngradeFromWriterLock(ref lockDowngradeCookie);
}
else
{
s_rwImgListLock.ReleaseWriterLock();
}
}
}
/// <summary>
/// Whether or not the image has multiple time-based frames.
/// </summary>
public static bool CanAnimate([NotNullWhen(true)] Image? image)
{
if (image is null)
{
return false;
}
// See comment in the class header about locking the image ref.
lock (image)
{
Guid[] dimensions = image.FrameDimensionsList;
foreach (Guid guid in dimensions)
{
FrameDimension dimension = new(guid);
if (dimension.Equals(FrameDimension.Time))
{
return image.GetFrameCount(FrameDimension.Time) > 1;
}
}
}
return false;
}
/// <summary>
/// Removes an image from the image manager so it is no longer animated.
/// </summary>
public static void StopAnimate(Image image, EventHandler onFrameChangedHandler)
{
// Make sure we have a list of images
if (image is null || s_imageInfoList is null)
{
return;
}
// Acquire a writer lock to modify the image info list - See comments on Animate() about this locking.
bool readerLockHeld = s_rwImgListLock.IsReaderLockHeld;
LockCookie lockDowngradeCookie = default;
t_threadWriterLockWaitCount++;
try
{
if (readerLockHeld)
{
lockDowngradeCookie = s_rwImgListLock.UpgradeToWriterLock(Timeout.Infinite);
}
else
{
s_rwImgListLock.AcquireWriterLock(Timeout.Infinite);
}
}
finally
{
t_threadWriterLockWaitCount--;
Debug.Assert(t_threadWriterLockWaitCount >= 0, "threadWriterLockWaitCount less than zero.");
}
try
{
// Find the corresponding reference and remove it
for (int i = 0; i < s_imageInfoList.Count; i++)
{
ImageInfo imageInfo = s_imageInfoList[i];
if (image == imageInfo.Image)
{
if (onFrameChangedHandler == imageInfo.FrameChangedHandler
|| (onFrameChangedHandler is not null && onFrameChangedHandler.Equals(imageInfo.FrameChangedHandler)))
{
s_imageInfoList.Remove(imageInfo);
}
break;
}
}
}
finally
{
if (readerLockHeld)
{
s_rwImgListLock.DowngradeFromWriterLock(ref lockDowngradeCookie);
}
else
{
s_rwImgListLock.ReleaseWriterLock();
}
}
}
/// <summary>
/// Worker thread procedure which implements the main animation loop.
///
/// NOTE: This is the ONLY code the worker thread executes, keeping it in one method helps better understand
/// any synchronization issues.
///
/// WARNING: Also, this is the only place where ImageInfo objects (not the contained image object) are modified,
/// so no access synchronization is required to modify them.
/// </summary>
private static void AnimateImages()
{
Debug.Assert(s_imageInfoList is not null, "Null images list");
Stopwatch stopwatch = Stopwatch.StartNew();
while (true)
{
Thread.Sleep(AnimationDelayMS);
// Because Thread.Sleep is not accurate, capture how much time has actually elapsed during the animation
long timeElapsed = stopwatch.ElapsedMilliseconds;
stopwatch.Restart();
// Acquire reader-lock to access imageInfoList, elements in the list can be modified w/o needing a writer-lock.
// Observe that we don't need to check if the thread is waiting or a writer lock here since the thread this
// method runs in never acquires a writer lock.
s_rwImgListLock.AcquireReaderLock(Timeout.Infinite);
try
{
for (int i = 0; i < s_imageInfoList.Count; i++)
{
ImageInfo imageInfo = s_imageInfoList[i];
if (imageInfo.Animated)
{
imageInfo.AdvanceAnimationBy(timeElapsed);
if (imageInfo.FrameDirty)
{
s_anyFrameDirty = true;
}
}
}
}
finally
{
s_rwImgListLock.ReleaseReaderLock();
}
}
}
}