ActionAccumulator.cs 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. using System.Windows;
  2. using System.Windows.Threading;
  3. using ChunkyImageLib.DataHolders;
  4. using PixiEditor.ChangeableDocument.Actions;
  5. using PixiEditor.ChangeableDocument.Actions.Undo;
  6. using PixiEditor.ChangeableDocument.ChangeInfos;
  7. using PixiEditor.Models.Rendering;
  8. using PixiEditor.Models.Rendering.RenderInfos;
  9. using PixiEditor.ViewModels.SubViewModels.Document;
  10. namespace PixiEditor.Models.DocumentModels;
  11. #nullable enable
  12. internal class ActionAccumulator
  13. {
  14. private bool executing = false;
  15. private List<IAction> queuedActions = new();
  16. private DocumentViewModel document;
  17. private DocumentHelpers helpers;
  18. private WriteableBitmapUpdater renderer;
  19. public ActionAccumulator(DocumentViewModel doc, DocumentHelpers helpers)
  20. {
  21. this.document = doc;
  22. this.helpers = helpers;
  23. renderer = new(doc, helpers);
  24. }
  25. public void AddFinishedActions(params IAction[] actions)
  26. {
  27. queuedActions.AddRange(actions);
  28. queuedActions.Add(new ChangeBoundary_Action());
  29. TryExecuteAccumulatedActions();
  30. }
  31. public void AddActions(params IAction[] actions)
  32. {
  33. queuedActions.AddRange(actions);
  34. TryExecuteAccumulatedActions();
  35. }
  36. private async void TryExecuteAccumulatedActions()
  37. {
  38. if (executing || queuedActions.Count == 0)
  39. return;
  40. executing = true;
  41. DispatcherTimer busyTimer = new DispatcherTimer() { Interval = TimeSpan.FromMilliseconds(2000) };
  42. busyTimer.Tick += (_, _) =>
  43. {
  44. busyTimer.Stop();
  45. document.Busy = true;
  46. };
  47. busyTimer.Start();
  48. while (queuedActions.Count > 0)
  49. {
  50. // select actions to be processed
  51. var toExecute = queuedActions;
  52. queuedActions = new List<IAction>();
  53. // pass them to changeabledocument for processing
  54. List<IChangeInfo?> changes;
  55. if (AreAllPassthrough(toExecute))
  56. changes = toExecute.Select(a => (IChangeInfo?)a).ToList();
  57. else
  58. changes = await helpers.Tracker.ProcessActions(toExecute);
  59. // update viewmodels based on changes
  60. foreach (IChangeInfo? info in changes)
  61. {
  62. helpers.Updater.ApplyChangeFromChangeInfo(info);
  63. }
  64. // render changes
  65. // If you are a sane person or maybe just someone who reads WPF documentation, you might think that the reasonable order of operations should be
  66. // 1. Lock the writeable bitmaps
  67. // 2. Update their contents
  68. // 3. Add dirty rectangles
  69. // 4. Unlock them
  70. // As it turns out, doing operations in this order leads to WPF render thread crashing in some circumstatances.
  71. // Then the whole app freezes without throwing any errors, because the UI thread is blocked on a mutex, waiting for the dead render thread.
  72. // This is despite the order clearly being adviced in the documentations here: https://docs.microsoft.com/en-us/dotnet/api/system.windows.media.imaging.writeablebitmap?view=windowsdesktop-6.0&viewFallbackFrom=net-6.0#remarks
  73. // Because of that, I'm executing the operations in the order that makes a lot less sense:
  74. // 1. Update the contents of the bitmaps
  75. // 2. Lock Them
  76. // 3. Add dirty rectangles
  77. // 4. Unlock
  78. // The locks clearly do nothing useful here, and I'm only calling them because WriteableBitmap checks if it's locked before letting you add dirty rectangles.
  79. // Really, the locks are supposed to prevent me from updating the bitmap contents in step 1, but they can't since I'm doing direct unsafe memory copying
  80. // Somehow this all works
  81. // Also, there is a bug report for this on github https://github.com/dotnet/wpf/issues/5816
  82. var affectedChunks = new AffectedChunkGatherer(helpers.Tracker, changes);
  83. bool refreshDelayed = toExecute.Any(static action => action is ChangeBoundary_Action or Redo_Action or Undo_Action);
  84. var renderResult = await renderer.UpdateGatheredChunks(affectedChunks, refreshDelayed);
  85. // lock bitmaps that need to be updated
  86. foreach (var (_, bitmap) in document.Bitmaps)
  87. {
  88. bitmap.Lock();
  89. }
  90. // update bitmaps
  91. if (refreshDelayed)
  92. LockPreviewBitmaps(document.StructureRoot);
  93. AddDirtyRects(renderResult);
  94. // unlock bitmaps
  95. foreach (var (_, bitmap) in document.Bitmaps)
  96. {
  97. bitmap.Unlock();
  98. }
  99. if (refreshDelayed)
  100. UnlockPreviewBitmaps(document.StructureRoot);
  101. // force refresh viewports for better responsiveness
  102. foreach (var (_, value) in helpers.State.Viewports)
  103. {
  104. value.InvalidateVisual();
  105. }
  106. }
  107. busyTimer.Stop();
  108. if (document.Busy)
  109. document.Busy = false;
  110. executing = false;
  111. }
  112. private bool AreAllPassthrough(List<IAction> actions)
  113. {
  114. foreach (var action in actions)
  115. {
  116. if (action is not IChangeInfo)
  117. return false;
  118. }
  119. return true;
  120. }
  121. private void LockPreviewBitmaps(FolderViewModel root)
  122. {
  123. foreach (var child in root.Children)
  124. {
  125. child.PreviewBitmap.Lock();
  126. if (child.MaskPreviewBitmap is not null)
  127. child.MaskPreviewBitmap.Lock();
  128. if (child is FolderViewModel innerFolder)
  129. LockPreviewBitmaps(innerFolder);
  130. }
  131. document.PreviewBitmap.Lock();
  132. }
  133. private void UnlockPreviewBitmaps(FolderViewModel root)
  134. {
  135. foreach (var child in root.Children)
  136. {
  137. child.PreviewBitmap.Unlock();
  138. if (child.MaskPreviewBitmap is not null)
  139. child.MaskPreviewBitmap.Unlock();
  140. if (child is FolderViewModel innerFolder)
  141. UnlockPreviewBitmaps(innerFolder);
  142. }
  143. document.PreviewBitmap.Unlock();
  144. }
  145. private void AddDirtyRects(List<IRenderInfo> changes)
  146. {
  147. foreach (IRenderInfo renderInfo in changes)
  148. {
  149. switch (renderInfo)
  150. {
  151. case DirtyRect_RenderInfo info:
  152. {
  153. var bitmap = document.Bitmaps[info.Resolution];
  154. RectI finalRect = new RectI(VecI.Zero, new(bitmap.PixelWidth, bitmap.PixelHeight));
  155. RectI dirtyRect = new RectI(info.Pos, info.Size).Intersect(finalRect);
  156. bitmap.AddDirtyRect(new(dirtyRect.Left, dirtyRect.Top, dirtyRect.Width, dirtyRect.Height));
  157. }
  158. break;
  159. case PreviewDirty_RenderInfo info:
  160. {
  161. var bitmap = helpers.StructureHelper.Find(info.GuidValue)?.PreviewBitmap;
  162. if (bitmap is null)
  163. continue;
  164. bitmap.AddDirtyRect(new Int32Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight));
  165. }
  166. break;
  167. case MaskPreviewDirty_RenderInfo info:
  168. {
  169. var bitmap = helpers.StructureHelper.Find(info.GuidValue)?.MaskPreviewBitmap;
  170. if (bitmap is null)
  171. continue;
  172. bitmap.AddDirtyRect(new Int32Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight));
  173. }
  174. break;
  175. case CanvasPreviewDirty_RenderInfo:
  176. {
  177. document.PreviewBitmap.AddDirtyRect(new Int32Rect(0, 0, document.PreviewBitmap.PixelWidth, document.PreviewBitmap.PixelHeight));
  178. }
  179. break;
  180. }
  181. }
  182. }
  183. }