Browse Source

ChunkyImage thread safety

Equbuxu 3 years ago
parent
commit
c788550b53

+ 1 - 1
src/ChunkyImageLib/Chunk.cs

@@ -3,7 +3,7 @@ using SkiaSharp;
 
 
 namespace ChunkyImageLib
 namespace ChunkyImageLib
 {
 {
-    public class Chunk : IDisposable, IReadOnlyChunk
+    public class Chunk : IDisposable
     {
     {
         private bool returned = false;
         private bool returned = false;
         public Surface Surface { get; }
         public Surface Surface { get; }

+ 145 - 71
src/ChunkyImageLib/ChunkyImage.cs

@@ -7,6 +7,7 @@ using System.Runtime.CompilerServices;
 namespace ChunkyImageLib
 namespace ChunkyImageLib
 {
 {
     /// <summary>
     /// <summary>
+    /// This class is thread-safe only for reading! Only the functions from IReadOnlyChunkyImage can be called from any thread.
     /// ChunkyImage can be in two general states: 
     /// ChunkyImage can be in two general states: 
     /// 1. a state with all chunks committed and no queued operations
     /// 1. a state with all chunks committed and no queued operations
     ///     - latestChunks and latestChunksData are empty
     ///     - latestChunks and latestChunksData are empty
@@ -28,10 +29,17 @@ namespace ChunkyImageLib
     {
     {
         private struct LatestChunkData
         private struct LatestChunkData
         {
         {
-            public int QueueProgress { get; set; } = 0;
-            public bool IsDeleted { get; set; } = false;
+            public LatestChunkData()
+            {
+                QueueProgress = 0;
+                IsDeleted = false;
+            }
+
+            public int QueueProgress { get; set; }
+            public bool IsDeleted { get; set; }
         }
         }
         private bool disposed = false;
         private bool disposed = false;
+        private object lockObject = new();
 
 
         public static int ChunkSize => ChunkPool.FullChunkSize;
         public static int ChunkSize => ChunkPool.FullChunkSize;
         private static SKPaint ClippingPaint { get; } = new SKPaint() { BlendMode = SKBlendMode.DstIn };
         private static SKPaint ClippingPaint { get; } = new SKPaint() { BlendMode = SKBlendMode.DstIn };
@@ -84,22 +92,49 @@ namespace ChunkyImageLib
 
 
         public ChunkyImage CloneFromLatest()
         public ChunkyImage CloneFromLatest()
         {
         {
-            ChunkyImage output = new(LatestSize);
-            var chunks = FindAllChunks();
-            foreach (var chunk in chunks)
+            lock (lockObject)
+            {
+                ChunkyImage output = new(LatestSize);
+                var chunks = FindAllChunks();
+                foreach (var chunk in chunks)
+                {
+                    var image = (Chunk?)GetLatestChunk(chunk, ChunkResolution.Full);
+                    if (image is not null)
+                        output.DrawImage(chunk * ChunkSize, image.Surface);
+                }
+                output.CommitChanges();
+                return output;
+            }
+        }
+
+        public bool DrawLatestChunkOn(Vector2i chunkPos, ChunkResolution resolution, SKSurface surface, Vector2i pos, SKPaint? paint = null)
+        {
+            lock (lockObject)
+            {
+                var chunk = GetLatestChunk(chunkPos, resolution);
+                if (chunk is null)
+                    return false;
+                chunk.DrawOnSurface(surface, pos, paint);
+                return true;
+            }
+        }
+
+        internal bool DrawCommittedChunkOn(Vector2i chunkPos, ChunkResolution resolution, SKSurface surface, Vector2i pos, SKPaint? paint = null)
+        {
+            lock (lockObject)
             {
             {
-                var image = (Chunk?)GetLatestChunk(chunk, ChunkResolution.Full);
-                if (image is not null)
-                    output.DrawImage(chunk * ChunkSize, image.Surface);
+                var chunk = GetCommittedChunk(chunkPos, resolution);
+                if (chunk is null)
+                    return false;
+                chunk.DrawOnSurface(surface, pos, paint);
+                return true;
             }
             }
-            output.CommitChanges();
-            return output;
         }
         }
 
 
         /// <summary>
         /// <summary>
         /// Returns the latest version of the chunk, with uncommitted changes applied if they exist
         /// Returns the latest version of the chunk, with uncommitted changes applied if they exist
         /// </summary>
         /// </summary>
-        public IReadOnlyChunk? GetLatestChunk(Vector2i pos, ChunkResolution resolution)
+        private Chunk? GetLatestChunk(Vector2i pos, ChunkResolution resolution)
         {
         {
             //no queued operations
             //no queued operations
             if (queuedOperations.Count == 0)
             if (queuedOperations.Count == 0)
@@ -136,7 +171,7 @@ namespace ChunkyImageLib
         /// <summary>
         /// <summary>
         /// Returns the committed version of the chunk ignoring any uncommitted changes
         /// Returns the committed version of the chunk ignoring any uncommitted changes
         /// </summary>
         /// </summary>
-        internal IReadOnlyChunk? GetCommittedChunk(Vector2i pos, ChunkResolution resolution)
+        private Chunk? GetCommittedChunk(Vector2i pos, ChunkResolution resolution)
         {
         {
             var maybeSameRes = MaybeGetCommittedChunk(pos, resolution);
             var maybeSameRes = MaybeGetCommittedChunk(pos, resolution);
             if (maybeSameRes is not null)
             if (maybeSameRes is not null)
@@ -154,39 +189,57 @@ namespace ChunkyImageLib
 
 
         public void DrawRectangle(ShapeData rect)
         public void DrawRectangle(ShapeData rect)
         {
         {
-            RectangleOperation operation = new(rect);
-            EnqueueOperation(operation);
+            lock (lockObject)
+            {
+                RectangleOperation operation = new(rect);
+                EnqueueOperation(operation);
+            }
         }
         }
 
 
         public void DrawImage(Vector2i pos, Surface image)
         public void DrawImage(Vector2i pos, Surface image)
         {
         {
-            ImageOperation operation = new(pos, image);
-            EnqueueOperation(operation);
+            lock (lockObject)
+            {
+                ImageOperation operation = new(pos, image);
+                EnqueueOperation(operation);
+            }
         }
         }
 
 
         public void ClearRegion(Vector2i pos, Vector2i size)
         public void ClearRegion(Vector2i pos, Vector2i size)
         {
         {
-            ClearRegionOperation operation = new(pos, size);
-            EnqueueOperation(operation);
+            lock (lockObject)
+            {
+                ClearRegionOperation operation = new(pos, size);
+                EnqueueOperation(operation);
+            }
         }
         }
 
 
         public void Clear()
         public void Clear()
         {
         {
-            ClearOperation operation = new();
-            EnqueueOperation(operation, FindAllChunks());
+            lock (lockObject)
+            {
+                ClearOperation operation = new();
+                EnqueueOperation(operation, FindAllChunks());
+            }
         }
         }
 
 
         public void ApplyRasterClip(ChunkyImage clippingMask)
         public void ApplyRasterClip(ChunkyImage clippingMask)
         {
         {
-            RasterClipOperation operation = new(clippingMask);
-            EnqueueOperation(operation, new());
+            lock (lockObject)
+            {
+                RasterClipOperation operation = new(clippingMask);
+                EnqueueOperation(operation, new());
+            }
         }
         }
 
 
         public void Resize(Vector2i newSize)
         public void Resize(Vector2i newSize)
         {
         {
-            ResizeOperation operation = new(newSize);
-            LatestSize = newSize;
-            EnqueueOperation(operation, FindAllChunksOutsideBounds(newSize));
+            lock (lockObject)
+            {
+                ResizeOperation operation = new(newSize);
+                LatestSize = newSize;
+                EnqueueOperation(operation, FindAllChunksOutsideBounds(newSize));
+            }
         }
         }
 
 
         private void EnqueueOperation(IDrawOperation operation)
         private void EnqueueOperation(IDrawOperation operation)
@@ -204,41 +257,47 @@ namespace ChunkyImageLib
 
 
         public void CancelChanges()
         public void CancelChanges()
         {
         {
-            //clear queued operations
-            foreach (var operation in queuedOperations)
-                operation.Item1.Dispose();
-            queuedOperations.Clear();
-
-            //clear latest chunks
-            foreach (var (_, chunksOfRes) in latestChunks)
+            lock (lockObject)
             {
             {
-                foreach (var (_, chunk) in chunksOfRes)
+                //clear queued operations
+                foreach (var operation in queuedOperations)
+                    operation.Item1.Dispose();
+                queuedOperations.Clear();
+
+                //clear latest chunks
+                foreach (var (_, chunksOfRes) in latestChunks)
                 {
                 {
-                    chunk.Dispose();
+                    foreach (var (_, chunk) in chunksOfRes)
+                    {
+                        chunk.Dispose();
+                    }
+                }
+                LatestSize = CommittedSize;
+                foreach (var (res, chunks) in latestChunks)
+                {
+                    chunks.Clear();
+                    latestChunksData[res].Clear();
                 }
                 }
-            }
-            LatestSize = CommittedSize;
-            foreach (var (res, chunks) in latestChunks)
-            {
-                chunks.Clear();
-                latestChunksData[res].Clear();
             }
             }
         }
         }
 
 
         public void CommitChanges()
         public void CommitChanges()
         {
         {
-            var affectedChunks = FindAffectedChunks();
-            foreach (var chunk in affectedChunks)
-            {
-                MaybeCreateAndProcessQueueForChunk(chunk, ChunkResolution.Full);
-            }
-            foreach (var (operation, _) in queuedOperations)
+            lock (lockObject)
             {
             {
-                operation.Dispose();
+                var affectedChunks = FindAffectedChunks();
+                foreach (var chunk in affectedChunks)
+                {
+                    MaybeCreateAndProcessQueueForChunk(chunk, ChunkResolution.Full);
+                }
+                foreach (var (operation, _) in queuedOperations)
+                {
+                    operation.Dispose();
+                }
+                CommitLatestChunks();
+                CommittedSize = LatestSize;
+                queuedOperations.Clear();
             }
             }
-            CommitLatestChunks();
-            CommittedSize = LatestSize;
-            queuedOperations.Clear();
         }
         }
 
 
         private void CommitLatestChunks()
         private void CommitLatestChunks()
@@ -307,12 +366,15 @@ namespace ChunkyImageLib
         /// </summary>
         /// </summary>
         public HashSet<Vector2i> FindAllChunks()
         public HashSet<Vector2i> FindAllChunks()
         {
         {
-            var allChunks = committedChunks[ChunkResolution.Full].Select(chunk => chunk.Key).ToHashSet();
-            foreach (var (operation, opChunks) in queuedOperations)
+            lock (lockObject)
             {
             {
-                allChunks.UnionWith(opChunks);
+                var allChunks = committedChunks[ChunkResolution.Full].Select(chunk => chunk.Key).ToHashSet();
+                foreach (var (operation, opChunks) in queuedOperations)
+                {
+                    allChunks.UnionWith(opChunks);
+                }
+                return allChunks;
             }
             }
-            return allChunks;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -320,12 +382,15 @@ namespace ChunkyImageLib
         /// </summary>
         /// </summary>
         public HashSet<Vector2i> FindAffectedChunks()
         public HashSet<Vector2i> FindAffectedChunks()
         {
         {
-            var chunks = new HashSet<Vector2i>();
-            foreach (var (_, opChunks) in queuedOperations)
+            lock (lockObject)
             {
             {
-                chunks.UnionWith(opChunks);
+                var chunks = new HashSet<Vector2i>();
+                foreach (var (_, opChunks) in queuedOperations)
+                {
+                    chunks.UnionWith(opChunks);
+                }
+                return chunks;
             }
             }
-            return chunks;
         }
         }
 
 
         private void MaybeCreateAndProcessQueueForChunk(Vector2i chunkPos, ChunkResolution resolution)
         private void MaybeCreateAndProcessQueueForChunk(Vector2i chunkPos, ChunkResolution resolution)
@@ -336,7 +401,7 @@ namespace ChunkyImageLib
                 return;
                 return;
 
 
             Chunk? targetChunk = null;
             Chunk? targetChunk = null;
-            List<IReadOnlyChunk> activeClips = new();
+            List<Chunk> activeClips = new();
             bool isFullyMaskedOut = false;
             bool isFullyMaskedOut = false;
             bool somethingWasApplied = false;
             bool somethingWasApplied = false;
             for (int i = 0; i < queuedOperations.Count; i++)
             for (int i = 0; i < queuedOperations.Count; i++)
@@ -372,7 +437,7 @@ namespace ChunkyImageLib
 
 
         private bool ApplyOperationToChunk(
         private bool ApplyOperationToChunk(
             IOperation operation,
             IOperation operation,
-            List<IReadOnlyChunk> activeClips,
+            List<Chunk> activeClips,
             bool isFullyMaskedOut,
             bool isFullyMaskedOut,
             Chunk targetChunk,
             Chunk targetChunk,
             Vector2i chunkPos,
             Vector2i chunkPos,
@@ -412,10 +477,16 @@ namespace ChunkyImageLib
             return chunkData.IsDeleted;
             return chunkData.IsDeleted;
         }
         }
 
 
+        /// <summary>
+        /// Note: this function modifies the internal state, it is not thread safe! (same as all other functions that change the image in some way)
+        /// </summary>
         public bool CheckIfCommittedIsEmpty()
         public bool CheckIfCommittedIsEmpty()
         {
         {
-            FindAndDeleteEmptyCommittedChunks();
-            return committedChunks[ChunkResolution.Full].Count == 0;
+            lock (lockObject)
+            {
+                FindAndDeleteEmptyCommittedChunks();
+                return committedChunks[ChunkResolution.Full].Count == 0;
+            }
         }
         }
 
 
         private HashSet<Vector2i> FindAllChunksOutsideBounds(Vector2i size)
         private HashSet<Vector2i> FindAllChunksOutsideBounds(Vector2i size)
@@ -543,19 +614,22 @@ namespace ChunkyImageLib
 
 
         public void Dispose()
         public void Dispose()
         {
         {
-            if (disposed)
-                return;
-            CancelChanges();
-            foreach (var (_, chunk) in tempRasterClipChunks)
-                chunk.Dispose();
-            foreach (var (_, chunks) in committedChunks)
+            lock (lockObject)
             {
             {
-                foreach (var (_, chunk) in chunks)
-                {
+                if (disposed)
+                    return;
+                CancelChanges();
+                foreach (var (_, chunk) in tempRasterClipChunks)
                     chunk.Dispose();
                     chunk.Dispose();
+                foreach (var (_, chunks) in committedChunks)
+                {
+                    foreach (var (_, chunk) in chunks)
+                    {
+                        chunk.Dispose();
+                    }
                 }
                 }
+                disposed = true;
             }
             }
-            disposed = true;
         }
         }
     }
     }
 }
 }

+ 6 - 4
src/ChunkyImageLib/CommittedChunkStorage.cs

@@ -1,4 +1,5 @@
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
+using SkiaSharp;
 
 
 namespace ChunkyImageLib
 namespace ChunkyImageLib
 {
 {
@@ -6,18 +7,19 @@ namespace ChunkyImageLib
     {
     {
         private bool disposed = false;
         private bool disposed = false;
         private List<(Vector2i, Chunk?)> savedChunks = new();
         private List<(Vector2i, Chunk?)> savedChunks = new();
+        private static SKPaint ReplacingPaint { get; } = new SKPaint() { BlendMode = SKBlendMode.Src };
+
         public CommittedChunkStorage(ChunkyImage image, HashSet<Vector2i> committedChunksToSave)
         public CommittedChunkStorage(ChunkyImage image, HashSet<Vector2i> committedChunksToSave)
         {
         {
             foreach (var chunkPos in committedChunksToSave)
             foreach (var chunkPos in committedChunksToSave)
             {
             {
-                Chunk? chunk = (Chunk?)image.GetCommittedChunk(chunkPos, ChunkResolution.Full);
-                if (chunk is null)
+                Chunk copy = Chunk.Create();
+                if (!image.DrawCommittedChunkOn(chunkPos, ChunkResolution.Full, copy.Surface.SkiaSurface, new(0, 0), ReplacingPaint))
                 {
                 {
+                    copy.Dispose();
                     savedChunks.Add((chunkPos, null));
                     savedChunks.Add((chunkPos, null));
                     continue;
                     continue;
                 }
                 }
-                Chunk copy = Chunk.Create();
-                chunk.Surface.CopyTo(copy.Surface);
                 savedChunks.Add((chunkPos, copy));
                 savedChunks.Add((chunkPos, copy));
             }
             }
         }
         }

+ 0 - 12
src/ChunkyImageLib/IReadOnlyChunk.cs

@@ -1,12 +0,0 @@
-using ChunkyImageLib.DataHolders;
-using SkiaSharp;
-
-namespace ChunkyImageLib
-{
-    public interface IReadOnlyChunk
-    {
-        Vector2i PixelSize { get; }
-        ChunkResolution Resolution { get; }
-        void DrawOnSurface(SKSurface surface, Vector2i pos, SKPaint? paint = null);
-    }
-}

+ 2 - 1
src/ChunkyImageLib/IReadOnlyChunkyImage.cs

@@ -1,10 +1,11 @@
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
+using SkiaSharp;
 
 
 namespace ChunkyImageLib
 namespace ChunkyImageLib
 {
 {
     public interface IReadOnlyChunkyImage
     public interface IReadOnlyChunkyImage
     {
     {
-        IReadOnlyChunk? GetLatestChunk(Vector2i pos, ChunkResolution resolution);
+        bool DrawLatestChunkOn(Vector2i chunkPos, ChunkResolution resolution, SKSurface surface, Vector2i pos, SKPaint? paint = null);
         HashSet<Vector2i> FindAffectedChunks();
         HashSet<Vector2i> FindAffectedChunks();
         HashSet<Vector2i> FindAllChunks();
         HashSet<Vector2i> FindAllChunks();
     }
     }

+ 62 - 0
src/ChunkyImageLib/SurfacePool.cs

@@ -0,0 +1,62 @@
+using ChunkyImageLib.DataHolders;
+using System.Collections.Concurrent;
+
+namespace ChunkyImageLib
+{
+    internal class SurfacePool
+    {
+        //must be divisible by 8
+        public const int FullChunkSize = 256;
+
+        private static object lockObj = new();
+        private static SurfacePool? instance;
+        public static SurfacePool Instance
+        {
+            get
+            {
+                if (instance is null)
+                {
+                    lock (lockObj)
+                    {
+                        if (instance is null)
+                            instance = new SurfacePool();
+                    }
+                }
+                return instance;
+            }
+        }
+
+        private readonly ConcurrentBag<Surface> fullSurfaces = new();
+        private readonly ConcurrentBag<Surface> halfSurfaces = new();
+        private readonly ConcurrentBag<Surface> quarterSurfaces = new();
+        private readonly ConcurrentBag<Surface> eighthSurfaces = new();
+        internal Surface Get(ChunkResolution resolution)
+        {
+            if (GetBag(resolution).TryTake(out Surface? item))
+                return item;
+            return new Surface(new Vector2i(resolution.PixelSize(), resolution.PixelSize()));
+        }
+
+        private ConcurrentBag<Surface> GetBag(ChunkResolution resolution)
+        {
+            return resolution switch
+            {
+                ChunkResolution.Full => fullSurfaces,
+                ChunkResolution.Half => halfSurfaces,
+                ChunkResolution.Quarter => quarterSurfaces,
+                ChunkResolution.Eighth => eighthSurfaces,
+                _ => fullSurfaces
+            };
+        }
+
+        internal void Push(Surface surface, ChunkResolution resolution)
+        {
+            var surfaces = GetBag(resolution);
+            //a race condition can cause the count to go above 200, but likely not by much
+            if (surfaces.Count < 200)
+                surfaces.Add(surface);
+            else
+                surface.Dispose();
+        }
+    }
+}

+ 2 - 2
src/ChunkyImageLibBenchmark/Program.cs

@@ -31,7 +31,7 @@ Console.ReadKey();
     {
     {
         for (int j = 0; j < 4; j++)
         for (int j = 0; j < 4; j++)
         {
         {
-            image.GetLatestChunk(new(i, j), ChunkyImageLib.DataHolders.ChunkResolution.Full);
+            //image.GetLatestChunk(new(i, j), ChunkyImageLib.DataHolders.ChunkResolution.Full);
         }
         }
     }
     }
     sw.Stop();
     sw.Stop();
@@ -42,7 +42,7 @@ Console.ReadKey();
     {
     {
         for (int j = 0; j < 4; j++)
         for (int j = 0; j < 4; j++)
         {
         {
-            image.GetLatestChunk(new(i, j), ChunkyImageLib.DataHolders.ChunkResolution.Full);
+            //image.GetLatestChunk(new(i, j), ChunkyImageLib.DataHolders.ChunkResolution.Full);
         }
         }
     }
     }
     sw.Stop();
     sw.Stop();

+ 1 - 4
src/PixiEditor.ChangeableDocument/Rendering/ChunkRenderer.cs

@@ -28,11 +28,8 @@ namespace PixiEditor.ChangeableDocument.Rendering
                     continue;
                     continue;
                 if (child is IReadOnlyLayer layer && (visibleLayers is null || visibleLayers.Contains(layer.GuidValue)))
                 if (child is IReadOnlyLayer layer && (visibleLayers is null || visibleLayers.Contains(layer.GuidValue)))
                 {
                 {
-                    IReadOnlyChunk? chunk = layer.ReadOnlyLayerImage.GetLatestChunk(chunkPos, resolution);
-                    if (chunk is null)
-                        continue;
                     PaintToDrawChunksWith.Color = new SKColor(255, 255, 255, (byte)Math.Round(child.Opacity * 255));
                     PaintToDrawChunksWith.Color = new SKColor(255, 255, 255, (byte)Math.Round(child.Opacity * 255));
-                    chunk.DrawOnSurface(targetChunk.Surface.SkiaSurface, new(0, 0), PaintToDrawChunksWith);
+                    layer.ReadOnlyLayerImage.DrawLatestChunkOn(chunkPos, resolution, targetChunk.Surface.SkiaSurface, new(0, 0), PaintToDrawChunksWith);
                 }
                 }
                 else if (child is IReadOnlyFolder innerFolder)
                 else if (child is IReadOnlyFolder innerFolder)
                 {
                 {

+ 2 - 3
src/PixiEditorPrototype/Models/Rendering/WriteableBitmapUpdater.cs

@@ -152,9 +152,8 @@ namespace PixiEditorPrototype.Models.Rendering
 
 
             if (helpers.Tracker.Document.ReadOnlySelection.ReadOnlyIsEmptyAndInactive)
             if (helpers.Tracker.Document.ReadOnlySelection.ReadOnlyIsEmptyAndInactive)
                 return;
                 return;
-            IReadOnlyChunk? selectionChunk = helpers.Tracker.Document.ReadOnlySelection.ReadOnlySelectionImage.GetLatestChunk(chunkPos, resolution);
-            if (selectionChunk is not null)
-                selectionChunk.DrawOnSurface(screenSurface, chunkPos.Multiply(selectionChunk.PixelSize), SelectionPaint);
+
+            helpers.Tracker.Document.ReadOnlySelection.ReadOnlySelectionImage.DrawLatestChunkOn(chunkPos, resolution, screenSurface, chunkPos * resolution.PixelSize(), SelectionPaint);
         }
         }
     }
     }
 }
 }

+ 0 - 1
src/PixiEditorPrototype/ViewModels/DocumentViewModel.cs

@@ -169,7 +169,6 @@ namespace PixiEditorPrototype.ViewModels
                 return;
                 return;
             startedSelection = false;
             startedSelection = false;
             Helpers.ActionAccumulator.AddAction(new EndSelectRectangle_Action());
             Helpers.ActionAccumulator.AddAction(new EndSelectRectangle_Action());
-            Helpers.ActionAccumulator.AddAction(new MergeLatestChanges_Action(2));
         }
         }
 
 
         public void ForceRefreshView()
         public void ForceRefreshView()