123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206 |
- using System.Windows;
- using System.Windows.Threading;
- using ChunkyImageLib.DataHolders;
- using PixiEditor.ChangeableDocument.Actions;
- using PixiEditor.ChangeableDocument.Actions.Undo;
- using PixiEditor.ChangeableDocument.ChangeInfos;
- using PixiEditor.Models.Rendering;
- using PixiEditor.Models.Rendering.RenderInfos;
- using PixiEditor.ViewModels.SubViewModels.Document;
- namespace PixiEditor.Models.DocumentModels;
- #nullable enable
- internal class ActionAccumulator
- {
- private bool executing = false;
- private List<IAction> queuedActions = new();
- private DocumentViewModel document;
- private DocumentHelpers helpers;
- private WriteableBitmapUpdater renderer;
- public ActionAccumulator(DocumentViewModel doc, DocumentHelpers helpers)
- {
- this.document = doc;
- this.helpers = helpers;
- renderer = new(doc, helpers);
- }
- public void AddFinishedActions(params IAction[] actions)
- {
- queuedActions.AddRange(actions);
- queuedActions.Add(new ChangeBoundary_Action());
- TryExecuteAccumulatedActions();
- }
- public void AddActions(params IAction[] actions)
- {
- queuedActions.AddRange(actions);
- TryExecuteAccumulatedActions();
- }
- private async void TryExecuteAccumulatedActions()
- {
- if (executing || queuedActions.Count == 0)
- return;
- executing = true;
- DispatcherTimer busyTimer = new DispatcherTimer() { Interval = TimeSpan.FromMilliseconds(2000) };
- busyTimer.Tick += (_, _) =>
- {
- busyTimer.Stop();
- document.Busy = true;
- };
- busyTimer.Start();
- while (queuedActions.Count > 0)
- {
- // select actions to be processed
- var toExecute = queuedActions;
- queuedActions = new List<IAction>();
- // pass them to changeabledocument for processing
- List<IChangeInfo?> changes;
- if (AreAllPassthrough(toExecute))
- changes = toExecute.Select(a => (IChangeInfo?)a).ToList();
- else
- changes = await helpers.Tracker.ProcessActions(toExecute);
- // update viewmodels based on changes
- foreach (IChangeInfo? info in changes)
- {
- helpers.Updater.ApplyChangeFromChangeInfo(info);
- }
- // render changes
- // 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
- // 1. Lock the writeable bitmaps
- // 2. Update their contents
- // 3. Add dirty rectangles
- // 4. Unlock them
- // As it turns out, doing operations in this order leads to WPF render thread crashing in some circumstatances.
- // Then the whole app freezes without throwing any errors, because the UI thread is blocked on a mutex, waiting for the dead render thread.
- // 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
- // Because of that, I'm executing the operations in the order that makes a lot less sense:
- // 1. Update the contents of the bitmaps
- // 2. Lock Them
- // 3. Add dirty rectangles
- // 4. Unlock
- // 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.
- // 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
- // Somehow this all works
- // Also, there is a bug report for this on github https://github.com/dotnet/wpf/issues/5816
- var affectedChunks = new AffectedChunkGatherer(helpers.Tracker, changes);
- bool refreshDelayed = toExecute.Any(static action => action is ChangeBoundary_Action or Redo_Action or Undo_Action);
- var renderResult = await renderer.UpdateGatheredChunks(affectedChunks, refreshDelayed);
-
- // lock bitmaps that need to be updated
- foreach (var (_, bitmap) in document.Bitmaps)
- {
- bitmap.Lock();
- }
- // update bitmaps
- if (refreshDelayed)
- LockPreviewBitmaps(document.StructureRoot);
- AddDirtyRects(renderResult);
- // unlock bitmaps
- foreach (var (_, bitmap) in document.Bitmaps)
- {
- bitmap.Unlock();
- }
- if (refreshDelayed)
- UnlockPreviewBitmaps(document.StructureRoot);
- // force refresh viewports for better responsiveness
- foreach (var (_, value) in helpers.State.Viewports)
- {
- value.InvalidateVisual();
- }
- }
- busyTimer.Stop();
- if (document.Busy)
- document.Busy = false;
- executing = false;
- }
- private bool AreAllPassthrough(List<IAction> actions)
- {
- foreach (var action in actions)
- {
- if (action is not IChangeInfo)
- return false;
- }
- return true;
- }
- private void LockPreviewBitmaps(FolderViewModel root)
- {
- foreach (var child in root.Children)
- {
- child.PreviewBitmap.Lock();
- if (child.MaskPreviewBitmap is not null)
- child.MaskPreviewBitmap.Lock();
- if (child is FolderViewModel innerFolder)
- LockPreviewBitmaps(innerFolder);
- }
- document.PreviewBitmap.Lock();
- }
- private void UnlockPreviewBitmaps(FolderViewModel root)
- {
- foreach (var child in root.Children)
- {
- child.PreviewBitmap.Unlock();
- if (child.MaskPreviewBitmap is not null)
- child.MaskPreviewBitmap.Unlock();
- if (child is FolderViewModel innerFolder)
- UnlockPreviewBitmaps(innerFolder);
- }
- document.PreviewBitmap.Unlock();
- }
- private void AddDirtyRects(List<IRenderInfo> changes)
- {
- foreach (IRenderInfo renderInfo in changes)
- {
- switch (renderInfo)
- {
- case DirtyRect_RenderInfo info:
- {
- var bitmap = document.Bitmaps[info.Resolution];
- RectI finalRect = new RectI(VecI.Zero, new(bitmap.PixelWidth, bitmap.PixelHeight));
- RectI dirtyRect = new RectI(info.Pos, info.Size).Intersect(finalRect);
- bitmap.AddDirtyRect(new(dirtyRect.Left, dirtyRect.Top, dirtyRect.Width, dirtyRect.Height));
- }
- break;
- case PreviewDirty_RenderInfo info:
- {
- var bitmap = helpers.StructureHelper.Find(info.GuidValue)?.PreviewBitmap;
- if (bitmap is null)
- continue;
- bitmap.AddDirtyRect(new Int32Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight));
- }
- break;
- case MaskPreviewDirty_RenderInfo info:
- {
- var bitmap = helpers.StructureHelper.Find(info.GuidValue)?.MaskPreviewBitmap;
- if (bitmap is null)
- continue;
- bitmap.AddDirtyRect(new Int32Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight));
- }
- break;
- case CanvasPreviewDirty_RenderInfo:
- {
- document.PreviewBitmap.AddDirtyRect(new Int32Rect(0, 0, document.PreviewBitmap.PixelWidth, document.PreviewBitmap.PixelHeight));
- }
- break;
- }
- }
- }
- }
|