Browse Source

Low res rendering

Equbuxu 3 years ago
parent
commit
ae0fd5a924

+ 1 - 1
src/ChangeableDocument/Changes/Drawing/CombineStructureMembersOnto_Change.cs

@@ -58,7 +58,7 @@ namespace ChangeableDocument.Changes.Drawing
             toDrawOn.LayerImage.Clear();
             foreach (var chunk in chunksToCombine)
             {
-                using var combined = ChunkRenderer.RenderSpecificLayers(chunk, target.StructureRoot, layersToCombine);
+                using var combined = ChunkRenderer.RenderSpecificLayers(chunk, ChunkResolution.Full, target.StructureRoot, layersToCombine);
                 toDrawOn.LayerImage.DrawImage(chunk * ChunkyImage.ChunkSize, combined.Surface);
             }
             var affectedChunks = toDrawOn.LayerImage.FindAffectedChunks();

+ 4 - 2
src/ChangeableDocument/DocumentChangeTracker.cs

@@ -181,8 +181,10 @@ namespace ChangeableDocument
                     case DeleteRecordedChanges_Action:
                         DeleteAllChanges();
                         break;
-                    default:
-                        throw new InvalidOperationException("Unknown action type");
+                    //used for "passthrough" actions (move viewport)
+                    case IChangeInfo act:
+                        changeInfos.Add(act);
+                        break;
                 }
             }
             return changeInfos;

+ 8 - 8
src/ChangeableDocument/Rendering/ChunkRenderer.cs

@@ -8,19 +8,19 @@ namespace ChangeableDocument.Rendering
     public static class ChunkRenderer
     {
         private static SKPaint PaintToDrawChunksWith = new SKPaint() { BlendMode = SKBlendMode.SrcOver };
-        public static Chunk RenderWholeStructure(Vector2i pos, IReadOnlyFolder root)
+        public static Chunk RenderWholeStructure(Vector2i pos, ChunkResolution resolution, IReadOnlyFolder root)
         {
-            return RenderChunkRecursively(pos, 0, root, null);
+            return RenderChunkRecursively(pos, resolution, 0, root, null);
         }
 
-        public static Chunk RenderSpecificLayers(Vector2i pos, IReadOnlyFolder root, HashSet<Guid> layers)
+        public static Chunk RenderSpecificLayers(Vector2i pos, ChunkResolution resolution, IReadOnlyFolder root, HashSet<Guid> layers)
         {
-            return RenderChunkRecursively(pos, 0, root, layers);
+            return RenderChunkRecursively(pos, resolution, 0, root, layers);
         }
 
-        private static Chunk RenderChunkRecursively(Vector2i chunkPos, int depth, IReadOnlyFolder folder, HashSet<Guid>? visibleLayers)
+        private static Chunk RenderChunkRecursively(Vector2i chunkPos, ChunkResolution resolution, int depth, IReadOnlyFolder folder, HashSet<Guid>? visibleLayers)
         {
-            Chunk targetChunk = Chunk.Create();
+            Chunk targetChunk = Chunk.Create(resolution);
             targetChunk.Surface.SkiaSurface.Canvas.Clear();
             foreach (var child in folder.ReadOnlyChildren)
             {
@@ -28,7 +28,7 @@ namespace ChangeableDocument.Rendering
                     continue;
                 if (child is IReadOnlyLayer layer && (visibleLayers is null || visibleLayers.Contains(layer.GuidValue)))
                 {
-                    IReadOnlyChunk? chunk = layer.ReadOnlyLayerImage.GetLatestChunk(chunkPos);
+                    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));
@@ -36,7 +36,7 @@ namespace ChangeableDocument.Rendering
                 }
                 else if (child is IReadOnlyFolder innerFolder)
                 {
-                    using Chunk renderedChunk = RenderChunkRecursively(chunkPos, depth + 1, innerFolder, visibleLayers);
+                    using Chunk renderedChunk = RenderChunkRecursively(chunkPos, resolution, depth + 1, innerFolder, visibleLayers);
                     PaintToDrawChunksWith.Color = new SKColor(255, 255, 255, (byte)Math.Round(child.Opacity * 255));
                     renderedChunk.DrawOnSurface(targetChunk.Surface.SkiaSurface, new(0, 0), PaintToDrawChunksWith);
                 }

+ 1 - 8
src/ChunkyImageLib/Chunk.cs

@@ -11,14 +11,7 @@ namespace ChunkyImageLib
         public ChunkResolution Resolution { get; }
         private Chunk(ChunkResolution resolution)
         {
-            int size = resolution switch
-            {
-                ChunkResolution.Full => ChunkPool.FullChunkSize,
-                ChunkResolution.Half => ChunkPool.FullChunkSize / 2,
-                ChunkResolution.Quarter => ChunkPool.FullChunkSize / 4,
-                ChunkResolution.Eighth => ChunkPool.FullChunkSize / 8,
-                _ => ChunkPool.FullChunkSize
-            };
+            int size = resolution.PixelSize();
 
             Resolution = resolution;
             PixelSize = new(size, size);

+ 266 - 71
src/ChunkyImageLib/ChunkyImage.cs

@@ -6,6 +6,24 @@ using System.Runtime.CompilerServices;
 [assembly: InternalsVisibleTo("ChunkyImageLibTest")]
 namespace ChunkyImageLib
 {
+    /// <summary>
+    /// ChunkyImage can be in two general states: 
+    /// 1. a state with all chunks committed and no queued operations
+    ///     - latestChunks and latestChunksData are empty
+    ///     - queuedOperations are empty
+    ///     - committedChunks[ChunkResolution.Full] contains the current versions of all stored chunks
+    ///     - committedChunks[*any other resolution*] may contain the current low res versions of some of the chunks (or all of them, or none)
+    ///     - LatestSize == CommittedSize == current image size (px)
+    /// 2. and a state with some queued operations
+    ///     - queuedOperations contains all requested operations (drawing, raster clips, clear, etc.)
+    ///     - committedChunks[ChunkResolution.Full] contains the last versions before any operations of all stored chunks
+    ///     - committedChunks[*any other resolution*] may contain the last low res versions before any operations of some of the chunks (or all of them, or none)
+    ///     - latestChunks stores chunks with some (or none, or all) queued operations applied
+    ///     - latestChunksData stores the data for some or all of the latest chunks (not necessarily synced with latestChunks).
+    ///         The data includes how many operations from the queue have already been applied to the chunk, as well as chunk deleted state (the clear operation deletes chunks)
+    ///     - LatestSize contains new size if any resize operations were requested, otherwise commited size
+    /// You can check the current state via queuedOperations.Count == 0
+    /// </summary>
     public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     {
         private struct LatestChunkData
@@ -17,22 +35,51 @@ namespace ChunkyImageLib
 
         public static int ChunkSize => ChunkPool.FullChunkSize;
         private static SKPaint ClippingPaint { get; } = new SKPaint() { BlendMode = SKBlendMode.DstIn };
-        private Chunk tempChunk;
+        private static SKPaint ReplacingPaint { get; } = new SKPaint() { BlendMode = SKBlendMode.Src };
+
+        private Dictionary<ChunkResolution, Chunk> tempRasterClipChunks;
 
         public Vector2i CommittedSize { get; private set; }
         public Vector2i LatestSize { get; private set; }
 
         private List<(IOperation operation, HashSet<Vector2i> affectedChunks)> queuedOperations = new();
 
-        private Dictionary<Vector2i, Chunk> committedChunks = new();
-        private Dictionary<Vector2i, Chunk> latestChunks = new();
-        private Dictionary<Vector2i, LatestChunkData> latestChunksData = new();
+        private Dictionary<ChunkResolution, Dictionary<Vector2i, Chunk>> committedChunks;
+        private Dictionary<ChunkResolution, Dictionary<Vector2i, Chunk>> latestChunks;
+        private Dictionary<ChunkResolution, Dictionary<Vector2i, LatestChunkData>> latestChunksData = new();
 
         public ChunkyImage(Vector2i size)
         {
             CommittedSize = size;
             LatestSize = size;
-            tempChunk = Chunk.Create();
+            tempRasterClipChunks = new Dictionary<ChunkResolution, Chunk>()
+            {
+                [ChunkResolution.Full] = Chunk.Create(ChunkResolution.Full),
+                [ChunkResolution.Half] = Chunk.Create(ChunkResolution.Half),
+                [ChunkResolution.Quarter] = Chunk.Create(ChunkResolution.Quarter),
+                [ChunkResolution.Eighth] = Chunk.Create(ChunkResolution.Eighth),
+            };
+            committedChunks = new()
+            {
+                [ChunkResolution.Full] = new(),
+                [ChunkResolution.Half] = new(),
+                [ChunkResolution.Quarter] = new(),
+                [ChunkResolution.Eighth] = new(),
+            };
+            latestChunks = new()
+            {
+                [ChunkResolution.Full] = new(),
+                [ChunkResolution.Half] = new(),
+                [ChunkResolution.Quarter] = new(),
+                [ChunkResolution.Eighth] = new(),
+            };
+            latestChunksData = new()
+            {
+                [ChunkResolution.Full] = new(),
+                [ChunkResolution.Half] = new(),
+                [ChunkResolution.Quarter] = new(),
+                [ChunkResolution.Eighth] = new(),
+            };
         }
 
         public ChunkyImage CloneFromLatest()
@@ -41,7 +88,7 @@ namespace ChunkyImageLib
             var chunks = FindAllChunks();
             foreach (var chunk in chunks)
             {
-                var image = (Chunk?)GetLatestChunk(chunk);
+                var image = (Chunk?)GetLatestChunk(chunk, ChunkResolution.Full);
                 if (image is not null)
                     output.DrawImage(chunk * ChunkSize, image.Surface);
             }
@@ -52,23 +99,58 @@ namespace ChunkyImageLib
         /// <summary>
         /// Returns the latest version of the chunk, with uncommitted changes applied if they exist
         /// </summary>
-        public IReadOnlyChunk? GetLatestChunk(Vector2i pos)
+        public IReadOnlyChunk? GetLatestChunk(Vector2i pos, ChunkResolution resolution)
         {
+            //no queued operations
             if (queuedOperations.Count == 0)
-                return MaybeGetChunk(pos, committedChunks);
-            ProcessQueueForChunk(pos);
-            return MaybeGetChunk(pos, latestChunks) ?? MaybeGetChunk(pos, committedChunks);
+            {
+                var sameResChunk = MaybeGetCommittedChunk(pos, resolution);
+                if (sameResChunk is not null)
+                    return sameResChunk;
+
+                var fullResChunk = MaybeGetCommittedChunk(pos, ChunkResolution.Full);
+                if (fullResChunk is not null)
+                    return GetOrCreateCommittedChunk(pos, resolution);
+
+                return null;
+            }
+
+            // there are queued operations, target chunk is affected
+            MaybeCreateAndProcessQueueForChunk(pos, resolution);
+            var maybeNewlyProcessedChunk = MaybeGetLatestChunk(pos, resolution);
+            if (maybeNewlyProcessedChunk is not null)
+                return maybeNewlyProcessedChunk;
+
+            // there are queued operations, target chunk is unaffected
+            var maybeSameResCommitedChunk = MaybeGetCommittedChunk(pos, resolution);
+            if (maybeSameResCommitedChunk is not null)
+                return maybeSameResCommitedChunk;
+
+            var maybeFullResCommitedChunk = MaybeGetCommittedChunk(pos, ChunkResolution.Full);
+            if (maybeFullResCommitedChunk is not null)
+                return GetOrCreateCommittedChunk(pos, resolution);
+
+            return null;
         }
 
         /// <summary>
         /// Returns the committed version of the chunk ignoring any uncommitted changes
         /// </summary>
-        internal IReadOnlyChunk? GetCommittedChunk(Vector2i pos)
+        internal IReadOnlyChunk? GetCommittedChunk(Vector2i pos, ChunkResolution resolution)
         {
-            return MaybeGetChunk(pos, committedChunks);
+            var maybeSameRes = MaybeGetCommittedChunk(pos, resolution);
+            if (maybeSameRes is not null)
+                return maybeSameRes;
+
+            var maybeFullRes = MaybeGetCommittedChunk(pos, ChunkResolution.Full);
+            if (maybeFullRes is not null)
+                return GetOrCreateCommittedChunk(pos, resolution);
+
+            return null;
         }
 
-        private Chunk? MaybeGetChunk(Vector2i pos, Dictionary<Vector2i, Chunk> from) => from.ContainsKey(pos) ? from[pos] : null;
+        private Chunk? MaybeGetLatestChunk(Vector2i pos, ChunkResolution resolution) => latestChunks[resolution].TryGetValue(pos, out Chunk? value) ? value : null;
+        private Chunk? MaybeGetCommittedChunk(Vector2i pos, ChunkResolution resolution) => committedChunks[resolution].TryGetValue(pos, out Chunk? value) ? value : null;
 
         public void DrawRectangle(ShapeData rect)
         {
@@ -122,16 +204,25 @@ namespace ChunkyImageLib
 
         public void CancelChanges()
         {
+            //clear queued operations
             foreach (var operation in queuedOperations)
                 operation.Item1.Dispose();
             queuedOperations.Clear();
-            foreach (var (_, chunk) in latestChunks)
+
+            //clear latest chunks
+            foreach (var (_, chunksOfRes) in latestChunks)
             {
-                chunk.Dispose();
+                foreach (var (_, chunk) in chunksOfRes)
+                {
+                    chunk.Dispose();
+                }
             }
             LatestSize = CommittedSize;
-            latestChunks.Clear();
-            latestChunksData.Clear();
+            foreach (var (res, chunks) in latestChunks)
+            {
+                chunks.Clear();
+                latestChunksData[res].Clear();
+            }
         }
 
         public void CommitChanges()
@@ -139,7 +230,7 @@ namespace ChunkyImageLib
             var affectedChunks = FindAffectedChunks();
             foreach (var chunk in affectedChunks)
             {
-                ProcessQueueForChunk(chunk);
+                MaybeCreateAndProcessQueueForChunk(chunk, ChunkResolution.Full);
             }
             foreach (var (operation, _) in queuedOperations)
             {
@@ -150,13 +241,73 @@ namespace ChunkyImageLib
             queuedOperations.Clear();
         }
 
+        private void CommitLatestChunks()
+        {
+            // move fully processed latest chunks to committed
+            foreach (var (resolution, chunks) in latestChunks)
+            {
+                foreach (var (pos, chunk) in chunks)
+                {
+                    if (committedChunks[resolution].ContainsKey(pos))
+                    {
+                        var oldChunk = committedChunks[resolution][pos];
+                        committedChunks[resolution].Remove(pos);
+                        oldChunk.Dispose();
+                    }
+
+                    LatestChunkData data = latestChunksData[resolution][pos];
+                    if (data.QueueProgress != queuedOperations.Count)
+                    {
+                        if (resolution == ChunkResolution.Full)
+                        {
+                            throw new InvalidOperationException("Trying to commit a full res chunk that wasn't fully processed");
+                        }
+                        else
+                        {
+                            chunk.Dispose();
+                            continue;
+                        }
+                    }
+
+                    if (!data.IsDeleted)
+                        committedChunks[resolution].Add(pos, chunk);
+                    else
+                        chunk.Dispose();
+                }
+            }
+
+            // delete committed low res chunks that weren't updated
+            foreach (var (pos, chunk) in latestChunks[ChunkResolution.Full])
+            {
+                foreach (var (resolution, _) in latestChunks)
+                {
+                    if (resolution == ChunkResolution.Full)
+                        continue;
+                    if (!latestChunksData[resolution].TryGetValue(pos, out var halfChunk) || halfChunk.QueueProgress != queuedOperations.Count)
+                    {
+                        if (committedChunks[resolution].TryGetValue(pos, out var commitedLowResChunk))
+                        {
+                            committedChunks[resolution].Remove(pos);
+                            commitedLowResChunk.Dispose();
+                        }
+                    }
+                }
+            }
+
+            // clear latest chunks
+            foreach (var (resolution, chunks) in latestChunks)
+            {
+                chunks.Clear();
+                latestChunksData[resolution].Clear();
+            }
+        }
+
         /// <summary>
         /// Returns all chunks that have something in them, including latest (uncommitted) ones
         /// </summary>
         public HashSet<Vector2i> FindAllChunks()
         {
-            var allChunks = committedChunks.Select(chunk => chunk.Key).ToHashSet();
-            allChunks.UnionWith(latestChunks.Select(chunk => chunk.Key).ToHashSet());
+            var allChunks = committedChunks[ChunkResolution.Full].Select(chunk => chunk.Key).ToHashSet();
             foreach (var (operation, opChunks) in queuedOperations)
             {
                 allChunks.UnionWith(opChunks);
@@ -169,47 +320,22 @@ namespace ChunkyImageLib
         /// </summary>
         public HashSet<Vector2i> FindAffectedChunks()
         {
-            var chunks = latestChunks.Select(chunk => chunk.Key).ToHashSet();
-            foreach (var (operation, opChunks) in queuedOperations)
+            var chunks = new HashSet<Vector2i>();
+            foreach (var (_, opChunks) in queuedOperations)
             {
                 chunks.UnionWith(opChunks);
             }
             return chunks;
         }
 
-        private void CommitLatestChunks()
-        {
-            foreach (var (pos, chunk) in latestChunks)
-            {
-                LatestChunkData data = latestChunksData[pos];
-                if (data.QueueProgress != queuedOperations.Count)
-                    throw new InvalidOperationException("Trying to commit a chunk that wasn't fully processed");
-
-                if (committedChunks.ContainsKey(pos))
-                {
-                    var oldChunk = committedChunks[pos];
-                    committedChunks.Remove(pos);
-                    oldChunk.Dispose();
-                }
-                if (!data.IsDeleted)
-                    committedChunks.Add(pos, chunk);
-                else
-                    chunk.Dispose();
-            }
-
-            latestChunks.Clear();
-            latestChunksData.Clear();
-        }
-
-        private void ProcessQueueForChunk(Vector2i chunkPos)
+        private void MaybeCreateAndProcessQueueForChunk(Vector2i chunkPos, ChunkResolution resolution)
         {
-            Chunk? targetChunk = null;
-            if (latestChunksData.TryGetValue(chunkPos, out LatestChunkData chunkData))
-                chunkData = new() { QueueProgress = 0, IsDeleted = !committedChunks.ContainsKey(chunkPos) };
-
+            if (latestChunksData[resolution].TryGetValue(chunkPos, out LatestChunkData chunkData))
+                chunkData = new() { QueueProgress = 0, IsDeleted = !committedChunks[ChunkResolution.Full].ContainsKey(chunkPos) };
             if (chunkData.QueueProgress == queuedOperations.Count)
                 return;
 
+            Chunk? targetChunk = null;
             List<IReadOnlyChunk> activeClips = new();
             bool isFullyMaskedOut = false;
             bool somethingWasApplied = false;
@@ -218,7 +344,7 @@ namespace ChunkyImageLib
                 var (operation, operChunks) = queuedOperations[i];
                 if (operation is RasterClipOperation clipOperation)
                 {
-                    var chunk = clipOperation.ClippingMask.GetCommittedChunk(chunkPos);
+                    var chunk = clipOperation.ClippingMask.GetCommittedChunk(chunkPos, resolution);
                     if (chunk is not null)
                         activeClips.Add(chunk);
                     else
@@ -230,7 +356,7 @@ namespace ChunkyImageLib
                 if (!somethingWasApplied)
                 {
                     somethingWasApplied = true;
-                    targetChunk = GetOrCreateLatestChunk(chunkPos);
+                    targetChunk = GetOrCreateLatestChunk(chunkPos, resolution);
                 }
 
                 if (chunkData.QueueProgress <= i)
@@ -240,7 +366,7 @@ namespace ChunkyImageLib
             if (somethingWasApplied)
             {
                 chunkData.QueueProgress = queuedOperations.Count;
-                latestChunksData[chunkPos] = chunkData;
+                latestChunksData[resolution][chunkPos] = chunkData;
             }
         }
 
@@ -268,6 +394,7 @@ namespace ChunkyImageLib
                     return false;
                 }
 
+                var tempChunk = tempRasterClipChunks[targetChunk.Resolution];
                 tempChunk.Surface.SkiaSurface.Canvas.Clear();
                 chunkOperation.DrawOnChunk(tempChunk, chunkPos);
                 foreach (var mask in activeClips)
@@ -308,7 +435,7 @@ namespace ChunkyImageLib
             if (queuedOperations.Count != 0)
                 throw new InvalidOperationException("This method cannot be used while any operations are queued");
             HashSet<Vector2i> toRemove = new();
-            foreach (var (pos, chunk) in committedChunks)
+            foreach (var (pos, chunk) in committedChunks[ChunkResolution.Full])
             {
                 if (IsChunkEmpty(chunk))
                 {
@@ -317,7 +444,12 @@ namespace ChunkyImageLib
                 }
             }
             foreach (var pos in toRemove)
-                committedChunks.Remove(pos);
+            {
+                committedChunks[ChunkResolution.Full].Remove(pos);
+                committedChunks[ChunkResolution.Half].Remove(pos);
+                committedChunks[ChunkResolution.Quarter].Remove(pos);
+                committedChunks[ChunkResolution.Eighth].Remove(pos);
+            }
         }
 
         private unsafe bool IsChunkEmpty(Chunk chunk)
@@ -333,23 +465,80 @@ namespace ChunkyImageLib
             return true;
         }
 
-        private Chunk GetOrCreateLatestChunk(Vector2i chunkPos)
+        private Chunk GetOrCreateCommittedChunk(Vector2i chunkPos, ChunkResolution resolution)
         {
-            Chunk? targetChunk;
-            targetChunk = MaybeGetChunk(chunkPos, latestChunks);
-            if (targetChunk is null)
+            // commited chunk of the same resolution exists
+            Chunk? targetChunk = MaybeGetCommittedChunk(chunkPos, resolution);
+            if (targetChunk is not null)
+                return targetChunk;
+
+            // for full res chunks: nothing exists, create brand new chunk
+            if (resolution == ChunkResolution.Full)
             {
-                targetChunk = Chunk.Create();
-                var maybeCommittedChunk = MaybeGetChunk(chunkPos, committedChunks);
+                var newChunk = Chunk.Create(resolution);
+                committedChunks[resolution][chunkPos] = newChunk;
+                return newChunk;
+            }
 
-                if (maybeCommittedChunk is not null)
-                    maybeCommittedChunk.Surface.CopyTo(targetChunk.Surface);
-                else
-                    targetChunk.Surface.SkiaSurface.Canvas.Clear();
+            // for low res chunks: full res version exists
+            Chunk? existingFullResChunk = MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full);
+            if (resolution != ChunkResolution.Full && existingFullResChunk is not null)
+            {
+                var newChunk = Chunk.Create(resolution);
+                newChunk.Surface.SkiaSurface.Canvas.Save();
+                newChunk.Surface.SkiaSurface.Canvas.Scale((float)resolution.Multiplier());
+
+                newChunk.Surface.SkiaSurface.Canvas.DrawSurface(existingFullResChunk!.Surface.SkiaSurface, 0, 0, ReplacingPaint);
+                newChunk.Surface.SkiaSurface.Canvas.Restore();
+                committedChunks[resolution][chunkPos] = newChunk;
+                return newChunk;
+            }
+
+            // for low res chunks: full res version doesn't exist
+            {
+                GetOrCreateCommittedChunk(chunkPos, ChunkResolution.Full);
+                var newChunk = Chunk.Create(resolution);
+                committedChunks[resolution][chunkPos] = newChunk;
+                return newChunk;
+            }
+        }
+
+        private Chunk GetOrCreateLatestChunk(Vector2i chunkPos, ChunkResolution resolution)
+        {
+            // latest chunk exists
+            Chunk? targetChunk;
+            targetChunk = MaybeGetLatestChunk(chunkPos, resolution);
+            if (targetChunk is not null)
+                return targetChunk;
+
+            // committed chunk of the same resolution exists
+            var maybeCommittedAnyRes = MaybeGetCommittedChunk(chunkPos, resolution);
+            if (maybeCommittedAnyRes is not null)
+            {
+                Chunk newChunk = Chunk.Create(resolution);
+                maybeCommittedAnyRes.Surface.CopyTo(newChunk.Surface);
+                latestChunks[resolution][chunkPos] = newChunk;
+                return newChunk;
+            }
 
-                latestChunks[chunkPos] = targetChunk;
+            // committed chunk of full resolution exists
+            var maybeCommittedFullRes = MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full);
+            if (maybeCommittedFullRes is not null)
+            {
+                //create low res committed chunk
+                var committedChunkLowRes = GetOrCreateCommittedChunk(chunkPos, resolution);
+                //create latest based on it
+                Chunk newChunk = Chunk.Create(resolution);
+                committedChunkLowRes.Surface.CopyTo(newChunk.Surface);
+                latestChunks[resolution][chunkPos] = newChunk;
+                return newChunk;
             }
-            return targetChunk;
+
+            // no previous chunks exist
+            var newLatestChunk = Chunk.Create(resolution);
+            newLatestChunk.Surface.SkiaSurface.Canvas.Clear();
+            latestChunks[resolution][chunkPos] = newLatestChunk;
+            return newLatestChunk;
         }
 
         public void Dispose()
@@ -357,9 +546,15 @@ namespace ChunkyImageLib
             if (disposed)
                 return;
             CancelChanges();
-            tempChunk.Dispose();
-            foreach (var chunk in committedChunks)
-                chunk.Value.Dispose();
+            foreach (var (_, chunk) in tempRasterClipChunks)
+                chunk.Dispose();
+            foreach (var (_, chunks) in committedChunks)
+            {
+                foreach (var (_, chunk) in chunks)
+                {
+                    chunk.Dispose();
+                }
+            }
             disposed = true;
         }
     }

+ 1 - 1
src/ChunkyImageLib/CommittedChunkStorage.cs

@@ -10,7 +10,7 @@ namespace ChunkyImageLib
         {
             foreach (var chunkPos in committedChunksToSave)
             {
-                Chunk? chunk = (Chunk?)image.GetCommittedChunk(chunkPos);
+                Chunk? chunk = (Chunk?)image.GetCommittedChunk(chunkPos, ChunkResolution.Full);
                 if (chunk is null)
                 {
                     savedChunks.Add((chunkPos, null));

+ 27 - 0
src/ChunkyImageLib/DataHolders/ChunkResolution.cs

@@ -7,4 +7,31 @@
         Quarter,
         Eighth
     }
+
+    public static class ChunkResolutionEx
+    {
+        public static double Multiplier(this ChunkResolution resolution)
+        {
+            return resolution switch
+            {
+                ChunkResolution.Full => 1.0,
+                ChunkResolution.Half => 1.0 / 2,
+                ChunkResolution.Quarter => 1.0 / 4,
+                ChunkResolution.Eighth => 1.0 / 8,
+                _ => 1,
+            };
+        }
+
+        public static int PixelSize(this ChunkResolution resolution)
+        {
+            return resolution switch
+            {
+                ChunkResolution.Full => ChunkPool.FullChunkSize,
+                ChunkResolution.Half => ChunkPool.FullChunkSize / 2,
+                ChunkResolution.Quarter => ChunkPool.FullChunkSize / 4,
+                ChunkResolution.Eighth => ChunkPool.FullChunkSize / 8,
+                _ => ChunkPool.FullChunkSize
+            };
+        }
+    }
 }

+ 12 - 0
src/ChunkyImageLib/DataHolders/Vector2i.cs

@@ -49,6 +49,18 @@ namespace ChunkyImageLib.DataHolders
         {
             return new Vector2i(a.X * b, a.Y * b);
         }
+        public static Vector2d operator *(Vector2i a, double b)
+        {
+            return new Vector2d(a.X * b, a.Y * b);
+        }
+        public static Vector2i operator /(Vector2i a, int b)
+        {
+            return new Vector2i(a.X / b, a.Y / b);
+        }
+        public static Vector2d operator /(Vector2i a, double b)
+        {
+            return new Vector2d(a.X / b, a.Y / b);
+        }
         public static bool operator ==(Vector2i a, Vector2i b)
         {
             return a.X == b.X && a.Y == b.Y;

+ 1 - 1
src/ChunkyImageLib/IReadOnlyChunkyImage.cs

@@ -4,7 +4,7 @@ namespace ChunkyImageLib
 {
     public interface IReadOnlyChunkyImage
     {
-        IReadOnlyChunk? GetLatestChunk(Vector2i pos);
+        IReadOnlyChunk? GetLatestChunk(Vector2i pos, ChunkResolution resolution);
         HashSet<Vector2i> FindAffectedChunks();
         HashSet<Vector2i> FindAllChunks();
     }

+ 4 - 1
src/ChunkyImageLib/Operations/ClearRegionOperation.cs

@@ -18,8 +18,11 @@ namespace ChunkyImageLib.Operations
 
         public void DrawOnChunk(Chunk chunk, Vector2i chunkPos)
         {
+            Vector2i convPos = OperationHelper.ConvertForResolution(pos, chunk.Resolution);
+            Vector2i convSize = OperationHelper.ConvertForResolution(size, chunk.Resolution);
+
             chunk.Surface.SkiaSurface.Canvas.Save();
-            chunk.Surface.SkiaSurface.Canvas.ClipRect(SKRect.Create(pos - chunkPos * ChunkPool.FullChunkSize, size));
+            chunk.Surface.SkiaSurface.Canvas.ClipRect(SKRect.Create(convPos - chunkPos.Multiply(chunk.PixelSize), convSize));
             chunk.Surface.SkiaSurface.Canvas.Clear();
             chunk.Surface.SkiaSurface.Canvas.Restore();
         }

+ 8 - 0
src/ChunkyImageLib/Operations/ImageOperation.cs

@@ -19,7 +19,15 @@ namespace ChunkyImageLib.Operations
 
         public void DrawOnChunk(Chunk chunk, Vector2i chunkPos)
         {
+            if (chunk.Resolution == ChunkResolution.Full)
+            {
+                chunk.Surface.SkiaSurface.Canvas.DrawSurface(toPaint.SkiaSurface, pos - chunkPos * ChunkPool.FullChunkSize, ReplacingPaint);
+                return;
+            }
+            chunk.Surface.SkiaSurface.Canvas.Save();
+            chunk.Surface.SkiaSurface.Canvas.Scale((float)chunk.Resolution.Multiplier());
             chunk.Surface.SkiaSurface.Canvas.DrawSurface(toPaint.SkiaSurface, pos - chunkPos * ChunkPool.FullChunkSize, ReplacingPaint);
+            chunk.Surface.SkiaSurface.Canvas.Restore();
         }
 
         public HashSet<Vector2i> FindAffectedChunks()

+ 6 - 0
src/ChunkyImageLib/Operations/OperationHelper.cs

@@ -4,6 +4,12 @@ namespace ChunkyImageLib.Operations
 {
     public static class OperationHelper
     {
+        public static Vector2i ConvertForResolution(Vector2i pixelPos, ChunkResolution resolution)
+        {
+            var mult = resolution.Multiplier();
+            return new((int)Math.Round(pixelPos.X * mult), (int)Math.Round(pixelPos.Y * mult));
+        }
+
         public static Vector2i GetChunkPos(Vector2i pixelPos, int chunkSize)
         {
             return new Vector2i()

+ 7 - 2
src/ChunkyImageLib/Operations/RectangleOperation.cs

@@ -19,7 +19,12 @@ namespace ChunkyImageLib.Operations
             var skiaSurf = chunk.Surface.SkiaSurface;
             // use a clipping rectangle with 2x stroke width to make sure stroke doesn't stick outside rect bounds
             skiaSurf.Canvas.Save();
-            var rect = SKRect.Create(Data.Pos - chunkPos * ChunkPool.FullChunkSize, Data.Size);
+
+            var convertedPos = OperationHelper.ConvertForResolution(Data.Pos, chunk.Resolution);
+            var convertedSize = OperationHelper.ConvertForResolution(Data.Size, chunk.Resolution);
+            int convertedStroke = (int)Math.Round(chunk.Resolution.Multiplier() * Data.StrokeWidth);
+
+            var rect = SKRect.Create(convertedPos - chunkPos.Multiply(chunk.PixelSize), convertedSize);
             skiaSurf.Canvas.ClipRect(rect);
 
             // draw fill
@@ -35,7 +40,7 @@ namespace ChunkyImageLib.Operations
             // draw stroke
             paint.Color = Data.StrokeColor;
             paint.Style = SKPaintStyle.Stroke;
-            paint.StrokeWidth = Data.StrokeWidth * 2;
+            paint.StrokeWidth = convertedStroke * 2;
 
             skiaSurf.Canvas.DrawRect(rect, paint);
 

+ 29 - 0
src/PixiEditorPrototype/Converters/IndexToChunkResolutionConverter.cs

@@ -0,0 +1,29 @@
+using ChunkyImageLib.DataHolders;
+using System;
+using System.Globalization;
+using System.Windows.Data;
+
+namespace PixiEditorPrototype.Converters
+{
+    internal class IndexToChunkResolutionConverter : IValueConverter
+    {
+        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            throw new NotImplementedException();
+        }
+
+        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            if (value is not int res)
+                return ChunkResolution.Full;
+            return res switch
+            {
+                0 => ChunkResolution.Full,
+                1 => ChunkResolution.Half,
+                2 => ChunkResolution.Quarter,
+                3 => ChunkResolution.Eighth,
+                _ => ChunkResolution.Full
+            };
+        }
+    }
+}

+ 41 - 18
src/PixiEditorPrototype/Models/ActionAccumulator.cs

@@ -1,11 +1,14 @@
 using ChangeableDocument;
 using ChangeableDocument.Actions;
 using ChangeableDocument.ChangeInfos;
+using ChunkyImageLib.DataHolders;
 using PixiEditorPrototype.Models.Rendering;
 using PixiEditorPrototype.Models.Rendering.RenderInfos;
 using PixiEditorPrototype.ViewModels;
 using SkiaSharp;
+using System;
 using System.Collections.Generic;
+using System.Windows.Media.Imaging;
 
 namespace PixiEditorPrototype.Models
 {
@@ -34,7 +37,7 @@ namespace PixiEditorPrototype.Models
             TryExecuteAccumulatedActions();
         }
 
-        public async void TryExecuteAccumulatedActions()
+        private async void TryExecuteAccumulatedActions()
         {
             if (executing || queuedActions.Count == 0)
                 return;
@@ -46,34 +49,54 @@ namespace PixiEditorPrototype.Models
                 queuedActions = new List<IAction>();
 
                 var result = await tracker.ProcessActions(toExecute);
-                //var result = tracker.ProcessActionsSync(toExecute);
-
                 foreach (IChangeInfo? info in result)
                 {
                     documentUpdater.ApplyChangeFromChangeInfo(info);
                 }
 
-                document.FinalBitmap.Lock();
-                var renderResult = await renderer.ProcessChanges(result!, document.FinalBitmapSurface, new(document.FinalBitmap.PixelWidth, document.FinalBitmap.PixelHeight));
-                //var renderResult = renderer.ProcessChangesSync(result!, document.FinalBitmapSurface, new(document.FinalBitmap.PixelWidth, document.FinalBitmap.PixelHeight));
-
+                var (bitmap, surface) = GetCorrespondingBitmap(document.RenderResolution);
+                bitmap.Lock();
 
-                SKRectI finalRect = SKRectI.Create(0, 0, document.FinalBitmap.PixelWidth, document.FinalBitmap.PixelHeight);
-                foreach (IRenderInfo info in renderResult)
-                {
-                    if (info is DirtyRect_RenderInfo dirtyRectInfo)
-                    {
-                        SKRectI dirtyRect = SKRectI.Create(dirtyRectInfo.Pos, dirtyRectInfo.Size);
-                        dirtyRect.Intersect(finalRect);
-                        document.FinalBitmap.AddDirtyRect(new(dirtyRect.Left, dirtyRect.Top, dirtyRect.Width, dirtyRect.Height));
-                    }
-                }
-                document.FinalBitmap.Unlock();
+                var renderResult = await renderer.ProcessChanges(
+                    result!,
+                    surface,
+                    document.RenderResolution);
+                AddDirtyRects(bitmap, renderResult);
 
+                bitmap.Unlock();
                 document.View?.ForceRefreshFinalImage();
             }
 
             executing = false;
         }
+
+        private (WriteableBitmap, SKSurface) GetCorrespondingBitmap(ChunkResolution res)
+        {
+            var result = res switch
+            {
+                ChunkResolution.Full => (document.BitmapFull, document.SurfaceFull),
+                ChunkResolution.Half => (document.BitmapHalf, document.SurfaceHalf),
+                ChunkResolution.Quarter => (document.BitmapQuarter, document.SurfaceQuarter),
+                ChunkResolution.Eighth => (document.BitmapEighth, document.SurfaceEighth),
+                _ => (document.BitmapFull, document.SurfaceFull),
+            };
+            if (result.Item1 is null || result.Item2 is null)
+                throw new InvalidOperationException("Trying to get a bitmap of a non existing resolution");
+            return result!;
+        }
+
+        private static void AddDirtyRects(WriteableBitmap bitmap, List<IRenderInfo> changes)
+        {
+            SKRectI finalRect = SKRectI.Create(0, 0, bitmap.PixelWidth, bitmap.PixelHeight);
+            foreach (IRenderInfo info in changes)
+            {
+                if (info is DirtyRect_RenderInfo dirtyRectInfo)
+                {
+                    SKRectI dirtyRect = SKRectI.Create(dirtyRectInfo.Pos, dirtyRectInfo.Size);
+                    dirtyRect.Intersect(finalRect);
+                    bitmap.AddDirtyRect(new(dirtyRect.Left, dirtyRect.Top, dirtyRect.Width, dirtyRect.Height));
+                }
+            }
+        }
     }
 }

+ 57 - 6
src/PixiEditorPrototype/Models/DocumentUpdater.cs

@@ -1,5 +1,6 @@
 using ChangeableDocument.Changeables.Interfaces;
 using ChangeableDocument.ChangeInfos;
+using ChunkyImageLib.DataHolders;
 using PixiEditorPrototype.ViewModels;
 using SkiaSharp;
 using System;
@@ -44,18 +45,68 @@ namespace PixiEditorPrototype.Models
                 case Size_ChangeInfo info:
                     ProcessSize(info);
                     break;
+                case MoveViewport_PassthroughAction info:
+                    ProcessMoveViewport(info);
+                    break;
             }
         }
 
+        private void ProcessMoveViewport(MoveViewport_PassthroughAction info)
+        {
+            doc.ChosenResolution = info.Resolution;
+            doc.RaisePropertyChanged(nameof(doc.RenderBitmap));
+        }
+
         private void ProcessSize(Size_ChangeInfo info)
         {
-            doc.FinalBitmapSurface.Dispose();
+            doc.SurfaceFull.Dispose();
+            doc.SurfaceHalf?.Dispose();
+            doc.SurfaceQuarter?.Dispose();
+            doc.SurfaceEighth?.Dispose();
+
+            doc.SurfaceHalf = null;
+            doc.SurfaceQuarter = null;
+            doc.SurfaceEighth = null;
+
+            doc.BitmapHalf = null;
+            doc.BitmapQuarter = null;
+            doc.BitmapEighth = null;
+
+            doc.BitmapFull = CreateBitmap(doc.Tracker.Document.Size);
+            doc.SurfaceFull = CreateSKSurface(doc.BitmapFull);
+
+            if (doc.Tracker.Document.Size.X > 512 && doc.Tracker.Document.Size.Y > 512)
+            {
+                doc.BitmapHalf = CreateBitmap(doc.Tracker.Document.Size / 2);
+                doc.SurfaceHalf = CreateSKSurface(doc.BitmapHalf);
+            }
+
+            if (doc.Tracker.Document.Size.X > 1024 && doc.Tracker.Document.Size.Y > 1024)
+            {
+                doc.BitmapQuarter = CreateBitmap(doc.Tracker.Document.Size / 4);
+                doc.SurfaceQuarter = CreateSKSurface(doc.BitmapQuarter);
+            }
+
+            if (doc.Tracker.Document.Size.X > 2048 && doc.Tracker.Document.Size.Y > 2048)
+            {
+                doc.BitmapEighth = CreateBitmap(doc.Tracker.Document.Size / 8);
+                doc.SurfaceEighth = CreateSKSurface(doc.BitmapEighth);
+            }
+
+            doc.RaisePropertyChanged(nameof(doc.RenderBitmap));
+        }
+
+        private WriteableBitmap CreateBitmap(Vector2i size)
+        {
+            return new WriteableBitmap(size.X, size.Y, 96, 96, PixelFormats.Pbgra32, null);
+        }
 
-            doc.FinalBitmap = new WriteableBitmap(doc.Tracker.Document.Size.X, doc.Tracker.Document.Size.Y, 96, 96, PixelFormats.Pbgra32, null);
-            doc.FinalBitmapSurface = SKSurface.Create(
-                new SKImageInfo(doc.FinalBitmap.PixelWidth, doc.FinalBitmap.PixelHeight, SKColorType.Bgra8888, SKAlphaType.Premul, SKColorSpace.CreateSrgb()),
-                doc.FinalBitmap.BackBuffer,
-                doc.FinalBitmap.BackBufferStride);
+        private SKSurface CreateSKSurface(WriteableBitmap bitmap)
+        {
+            return SKSurface.Create(
+                new SKImageInfo(bitmap.PixelWidth, bitmap.PixelHeight, SKColorType.Bgra8888, SKAlphaType.Premul, SKColorSpace.CreateSrgb()),
+                bitmap.BackBuffer,
+                bitmap.BackBufferStride);
         }
 
         private void ProcessCreateStructureMember(CreateStructureMember_ChangeInfo info)

+ 19 - 0
src/PixiEditorPrototype/Models/MoveViewport_PassthroughAction.cs

@@ -0,0 +1,19 @@
+using ChangeableDocument.Actions;
+using ChangeableDocument.ChangeInfos;
+using ChunkyImageLib.DataHolders;
+using SkiaSharp;
+
+namespace PixiEditorPrototype.Models
+{
+    internal record class MoveViewport_PassthroughAction : IAction, IChangeInfo
+    {
+        public MoveViewport_PassthroughAction(SKRect viewport, ChunkResolution resolution)
+        {
+            Viewport = viewport;
+            Resolution = resolution;
+        }
+
+        public SKRect Viewport { get; }
+        public ChunkResolution Resolution { get; }
+    }
+}

+ 68 - 59
src/PixiEditorPrototype/Models/Rendering/WriteableBitmapUpdater.cs

@@ -8,6 +8,7 @@ using PixiEditorPrototype.Models.Rendering.RenderInfos;
 using SkiaSharp;
 using System;
 using System.Collections.Generic;
+using System.Linq;
 using System.Threading.Tasks;
 
 namespace PixiEditorPrototype.Models.Rendering
@@ -19,26 +20,35 @@ namespace PixiEditorPrototype.Models.Rendering
         private static SKPaint ReplacingPaint = new SKPaint() { BlendMode = SKBlendMode.Src };
         private static SKPaint SelectionPaint = new SKPaint() { BlendMode = SKBlendMode.SrcOver, Color = new(0xa0FFFFFF) };
         private static SKPaint ClearPaint = new SKPaint() { BlendMode = SKBlendMode.Src, Color = SKColors.Transparent };
-        private Vector2i oldSize = new(0, 0);
+
+        private SKRect lastViewport = SKRect.Create(0, 0, 64, 64);
+
+        private Dictionary<ChunkResolution, HashSet<Vector2i>> postponedChunks = new()
+        {
+            [ChunkResolution.Full] = new(),
+            [ChunkResolution.Half] = new(),
+            [ChunkResolution.Quarter] = new(),
+            [ChunkResolution.Eighth] = new()
+        };
 
         public WriteableBitmapUpdater(DocumentChangeTracker tracker)
         {
             this.tracker = tracker;
         }
 
-        public async Task<List<IRenderInfo>> ProcessChanges(IReadOnlyList<IChangeInfo> changes, SKSurface screenSurface, Vector2i screenSize)
+        public async Task<List<IRenderInfo>> ProcessChanges(IReadOnlyList<IChangeInfo> changes, SKSurface screenSurface, ChunkResolution resolution)
         {
-            return await Task.Run(() => Render(changes, screenSurface, screenSize)).ConfigureAwait(true);
+            return await Task.Run(() => Render(changes, screenSurface, resolution)).ConfigureAwait(true);
         }
 
-        public List<IRenderInfo> ProcessChangesSync(IReadOnlyList<IChangeInfo> changes, SKSurface screenSurface, Vector2i screenSize)
+        public List<IRenderInfo> ProcessChangesSync(IReadOnlyList<IChangeInfo> changes, SKSurface screenSurface, ChunkResolution resolution)
         {
-            return Render(changes, screenSurface, screenSize);
+            return Render(changes, screenSurface, resolution);
         }
 
-        private HashSet<Vector2i>? FindChunksToRerender(IReadOnlyList<IChangeInfo> changes)
+        private HashSet<Vector2i> FindChunksToRerender(IReadOnlyList<IChangeInfo> changes, ChunkResolution resolution)
         {
-            HashSet<Vector2i> chunks = new();
+            HashSet<Vector2i> affectedChunks = new();
             foreach (var change in changes)
             {
                 switch (change)
@@ -46,106 +56,105 @@ namespace PixiEditorPrototype.Models.Rendering
                     case LayerImageChunks_ChangeInfo layerImageChunks:
                         if (layerImageChunks.Chunks is null)
                             throw new InvalidOperationException("Chunks must not be null");
-                        chunks.UnionWith(layerImageChunks.Chunks);
+                        affectedChunks.UnionWith(layerImageChunks.Chunks);
                         break;
                     case Selection_ChangeInfo selection:
                         if (tracker.Document.ReadOnlySelection.ReadOnlyIsEmptyAndInactive)
                         {
-                            return null;
+                            AddAllChunks(affectedChunks);
                         }
                         else
                         {
                             if (selection.Chunks is null)
                                 throw new InvalidOperationException("Chunks must not be null");
-                            chunks.UnionWith(selection.Chunks);
+                            affectedChunks.UnionWith(selection.Chunks);
                         }
                         break;
                     case CreateStructureMember_ChangeInfo:
                     case DeleteStructureMember_ChangeInfo:
                     case MoveStructureMember_ChangeInfo:
                     case Size_ChangeInfo:
-                        return null;
+                        AddAllChunks(affectedChunks);
+                        break;
                     case StructureMemberOpacity_ChangeInfo opacityChangeInfo:
                         var memberWithOpacity = tracker.Document.FindMemberOrThrow(opacityChangeInfo.GuidValue);
                         if (memberWithOpacity is IReadOnlyLayer layerWithOpacity)
-                            chunks.UnionWith(layerWithOpacity.ReadOnlyLayerImage.FindAllChunks());
+                            affectedChunks.UnionWith(layerWithOpacity.ReadOnlyLayerImage.FindAllChunks());
                         else
-                            return null;
+                            AddAllChunks(affectedChunks);
                         break;
                     case StructureMemberIsVisible_ChangeInfo visibilityChangeInfo:
                         var memberWithVisibility = tracker.Document.FindMemberOrThrow(visibilityChangeInfo.GuidValue);
                         if (memberWithVisibility is IReadOnlyLayer layerWithVisibility)
-                            chunks.UnionWith(layerWithVisibility.ReadOnlyLayerImage.FindAllChunks());
+                            affectedChunks.UnionWith(layerWithVisibility.ReadOnlyLayerImage.FindAllChunks());
                         else
-                            return null;
+                            AddAllChunks(affectedChunks);
+                        break;
+                    case MoveViewport_PassthroughAction moveViewportInfo:
+                        lastViewport = moveViewportInfo.Viewport;
                         break;
                 }
             }
-            return chunks;
+
+            postponedChunks[ChunkResolution.Full].UnionWith(affectedChunks);
+            postponedChunks[ChunkResolution.Half].UnionWith(affectedChunks);
+            postponedChunks[ChunkResolution.Quarter].UnionWith(affectedChunks);
+            postponedChunks[ChunkResolution.Eighth].UnionWith(affectedChunks);
+
+            HashSet<Vector2i> visibleChunks = postponedChunks[resolution].Where(pos =>
+            {
+                var rect = SKRect.Create(pos, new(ChunkyImage.ChunkSize, ChunkyImage.ChunkSize));
+                return rect.IntersectsWith(lastViewport);
+            }).ToHashSet();
+            postponedChunks[resolution].ExceptWith(visibleChunks);
+
+            return visibleChunks;
         }
 
-        private List<IRenderInfo> Render(IReadOnlyList<IChangeInfo> changes, SKSurface screenSurface, Vector2i screenSize)
+        private void AddAllChunks(HashSet<Vector2i> chunks)
         {
-            bool redrawEverything = false;
-            if (oldSize != screenSize)
+            Vector2i size = new(
+                (int)Math.Ceiling(tracker.Document.Size.X / (float)ChunkyImage.ChunkSize),
+                (int)Math.Ceiling(tracker.Document.Size.Y / (float)ChunkyImage.ChunkSize));
+            for (int i = 0; i < size.X; i++)
             {
-                oldSize = screenSize;
-                redrawEverything = true;
+                for (int j = 0; j < size.Y; j++)
+                {
+                    chunks.Add(new(i, j));
+                }
             }
-            HashSet<Vector2i>? chunks = null;
-            if (!redrawEverything)
-                chunks = FindChunksToRerender(changes);
-            if (chunks is null)
-                redrawEverything = true;
+        }
 
+        private List<IRenderInfo> Render(IReadOnlyList<IChangeInfo> changes, SKSurface screenSurface, ChunkResolution resolution)
+        {
+            HashSet<Vector2i> chunks = FindChunksToRerender(changes, resolution);
 
             List<IRenderInfo> infos = new();
 
-            // draw to back surface
-            if (redrawEverything)
-            {
-                RenderScreen(screenSize, screenSurface);
-                infos.Add(new DirtyRect_RenderInfo(new Vector2i(0, 0), screenSize));
-            }
-            else
+            int chunkSize = resolution.PixelSize();
+            foreach (var chunkPos in chunks!)
             {
-                foreach (var chunkPos in chunks!)
-                {
-                    RenderChunk(chunkPos, screenSurface);
-                    infos.Add(new DirtyRect_RenderInfo(
-                        chunkPos * ChunkyImage.ChunkSize,
-                        new(ChunkyImage.ChunkSize, ChunkyImage.ChunkSize)
-                        ));
-                }
+                RenderChunk(chunkPos, screenSurface, resolution);
+                infos.Add(new DirtyRect_RenderInfo(
+                    chunkPos * chunkSize,
+                    new(chunkSize, chunkSize)
+                    ));
             }
 
             return infos;
         }
 
-        private void RenderScreen(Vector2i screenSize, SKSurface screenSurface)
+        private void RenderChunk(Vector2i chunkPos, SKSurface screenSurface, ChunkResolution resolution)
         {
-            int chunksWidth = (int)Math.Ceiling(screenSize.X / (float)ChunkyImage.ChunkSize);
-            int chunksHeight = (int)Math.Ceiling(screenSize.Y / (float)ChunkyImage.ChunkSize);
-            screenSurface.Canvas.Clear();
-            for (int x = 0; x < chunksWidth; x++)
-            {
-                for (int y = 0; y < chunksHeight; y++)
-                {
-                    RenderChunk(new(x, y), screenSurface);
-                }
-            }
-        }
+            using Chunk renderedChunk = ChunkRenderer.RenderWholeStructure(chunkPos, resolution, tracker.Document.ReadOnlyStructureRoot);
 
-        private void RenderChunk(Vector2i chunkPos, SKSurface screenSurface)
-        {
-            using Chunk renderedChunk = ChunkRenderer.RenderWholeStructure(chunkPos, tracker.Document.ReadOnlyStructureRoot);
-            screenSurface.Canvas.DrawSurface(renderedChunk.Surface.SkiaSurface, chunkPos * ChunkyImage.ChunkSize, ReplacingPaint);
+            screenSurface.Canvas.DrawSurface(renderedChunk.Surface.SkiaSurface, chunkPos.Multiply(renderedChunk.PixelSize), ReplacingPaint);
 
             if (tracker.Document.ReadOnlySelection.ReadOnlyIsEmptyAndInactive)
                 return;
-            IReadOnlyChunk? selectionChunk = tracker.Document.ReadOnlySelection.ReadOnlySelectionImage.GetLatestChunk(chunkPos);
+            IReadOnlyChunk? selectionChunk = tracker.Document.ReadOnlySelection.ReadOnlySelectionImage.GetLatestChunk(chunkPos, resolution);
             if (selectionChunk is not null)
-                selectionChunk.DrawOnSurface(screenSurface, chunkPos * ChunkyImage.ChunkSize, SelectionPaint);
+                selectionChunk.DrawOnSurface(screenSurface, chunkPos.Multiply(selectionChunk.PixelSize), SelectionPaint);
         }
     }
 }

+ 53 - 15
src/PixiEditorPrototype/ViewModels/DocumentViewModel.cs

@@ -38,13 +38,17 @@ namespace PixiEditorPrototype.ViewModels
 
         public event PropertyChangedEventHandler? PropertyChanged;
 
+        public void RaisePropertyChanged(string name)
+        {
+            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
+        }
+
         public ActionAccumulator ActionAccumulator { get; }
         public DocumentChangeTracker Tracker { get; }
         public DocumentStructureHelper StructureHelper { get; }
         public IDocumentView? View { get; set; }
         private DocumentUpdater Updater { get; }
 
-
         public FolderViewModel StructureRoot { get; }
         public RelayCommand? UndoCommand { get; }
         public RelayCommand? RedoCommand { get; }
@@ -62,17 +66,51 @@ namespace PixiEditorPrototype.ViewModels
         public RelayCommand? MouseMoveCommand { get; }
         public RelayCommand? MouseUpCommand { get; }
 
-        private WriteableBitmap finalBitmap = new WriteableBitmap(64, 64, 96, 96, PixelFormats.Pbgra32, null);
-        public WriteableBitmap FinalBitmap
+
+        public SKSurface SurfaceFull { get; set; }
+        public WriteableBitmap BitmapFull { get; set; } = new WriteableBitmap(64, 64, 96, 96, PixelFormats.Pbgra32, null);
+        public SKSurface? SurfaceHalf { get; set; } = null;
+        public WriteableBitmap? BitmapHalf { get; set; } = null;
+        public SKSurface? SurfaceQuarter { get; set; } = null;
+        public WriteableBitmap? BitmapQuarter { get; set; } = null;
+        public SKSurface? SurfaceEighth { get; set; } = null;
+        public WriteableBitmap? BitmapEighth { get; set; } = null;
+
+        public WriteableBitmap RenderBitmap
+        {
+            get => GetCorrespondingBitmap(RenderResolution)!;
+        }
+
+        public ChunkResolution RenderResolution
+        {
+            get
+            {
+                if (GetCorrespondingBitmap(ChosenResolution) is not null)
+                    return ChosenResolution;
+                return ChunkResolution.Full;
+            }
+        }
+        public ChunkResolution ChosenResolution { get; set; }
+        public ChunkResolution ResolutionFromView
         {
-            get => finalBitmap;
             set
             {
-                finalBitmap = value;
-                PropertyChanged?.Invoke(this, new(nameof(FinalBitmap)));
+                ActionAccumulator.AddAction(new MoveViewport_PassthroughAction(SKRect.Create(0, 0, BitmapFull.PixelWidth, BitmapFull.PixelHeight), value));
             }
         }
-        public SKSurface FinalBitmapSurface { get; set; }
+
+        public WriteableBitmap? GetCorrespondingBitmap(ChunkResolution resolution)
+        {
+            return resolution switch
+            {
+                ChunkResolution.Full => BitmapFull,
+                ChunkResolution.Half => BitmapHalf,
+                ChunkResolution.Quarter => BitmapQuarter,
+                ChunkResolution.Eighth => BitmapEighth,
+                _ => BitmapFull,
+            };
+        }
+
 
         public Color SelectedColor { get; set; } = Colors.Black;
         public int ResizeWidth { get; set; }
@@ -102,10 +140,10 @@ namespace PixiEditorPrototype.ViewModels
             MouseMoveCommand = new RelayCommand(MouseMove);
             MouseUpCommand = new RelayCommand(MouseUp);
 
-            FinalBitmapSurface = SKSurface.Create(
-                new SKImageInfo(FinalBitmap.PixelWidth, FinalBitmap.PixelHeight, SKColorType.Bgra8888, SKAlphaType.Premul, SKColorSpace.CreateSrgb()),
-                FinalBitmap.BackBuffer,
-                FinalBitmap.BackBufferStride);
+            SurfaceFull = SKSurface.Create(
+                new SKImageInfo(BitmapFull.PixelWidth, BitmapFull.PixelHeight, SKColorType.Bgra8888, SKAlphaType.Premul, SKColorSpace.CreateSrgb()),
+                BitmapFull.BackBuffer,
+                BitmapFull.BackBufferStride);
         }
 
         private bool mouseIsDown = false;
@@ -121,8 +159,8 @@ namespace PixiEditorPrototype.ViewModels
             var args = (MouseButtonEventArgs)(param!);
             var source = (System.Windows.Controls.Image)args.Source;
             var pos = args.GetPosition(source);
-            mouseDownCanvasX = (int)(pos.X / source.Width * FinalBitmap.PixelHeight);
-            mouseDownCanvasY = (int)(pos.Y / source.Height * FinalBitmap.PixelHeight);
+            mouseDownCanvasX = (int)(pos.X / source.Width * BitmapFull.PixelHeight);
+            mouseDownCanvasY = (int)(pos.Y / source.Height * BitmapFull.PixelHeight);
         }
 
         public void MouseMove(object? param)
@@ -132,8 +170,8 @@ namespace PixiEditorPrototype.ViewModels
             var args = (MouseEventArgs)(param!);
             var source = (System.Windows.Controls.Image)args.Source;
             var pos = args.GetPosition(source);
-            int curX = (int)(pos.X / source.Width * FinalBitmap.PixelHeight);
-            int curY = (int)(pos.Y / source.Height * FinalBitmap.PixelHeight);
+            int curX = (int)(pos.X / source.Width * BitmapFull.PixelHeight);
+            int curY = (int)(pos.Y / source.Height * BitmapFull.PixelHeight);
 
             ProcessToolMouseMove(curX, curY);
         }

+ 11 - 1
src/PixiEditorPrototype/Views/DocumentView.xaml

@@ -8,12 +8,16 @@
              xmlns:models="clr-namespace:PixiEditorPrototype.Models"
              xmlns:colorpicker="clr-namespace:ColorPicker;assembly=ColorPicker"
              xmlns:behaviors="clr-namespace:PixiEditorPrototype.Behaviors"
+             xmlns:converters="clr-namespace:PixiEditorPrototype.Converters"
              xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
              xmlns:cmd="mvvm"
              xmlns:vm="clr-namespace:PixiEditorPrototype.ViewModels"
              mc:Ignorable="d" 
              d:DataContext="{d:DesignInstance Type=vm:DocumentViewModel, IsDesignTimeCreatable=True}"
              d:DesignHeight="576" d:DesignWidth="1024">
+    <UserControl.Resources>
+        <converters:IndexToChunkResolutionConverter x:Key="IndexToChunkResolutionConverter"/>
+    </UserControl.Resources>
     <DockPanel Background="Gray">
         <Border BorderThickness="1" Background="White" BorderBrush="Black" Width="280" DockPanel.Dock="Right" Margin="5">
             <DockPanel>
@@ -96,6 +100,12 @@
                     <Button Width="50" Margin="5" Command="{Binding RedoCommand}">Redo</Button>
                     <Button Width="100" Margin="5" Command="{Binding ClearSelectionCommand}">Clear selection</Button>
                     <Button Width="120" Margin="5" Command="{Binding ClearHistoryCommand}">Clear undo history</Button>
+                    <ComboBox SelectedIndex="{Binding ResolutionFromView, Converter={StaticResource IndexToChunkResolutionConverter}, Mode=OneWayToSource}" Width="70" Margin="5">
+                        <ComboBoxItem>Full</ComboBoxItem>
+                        <ComboBoxItem>Half</ComboBoxItem>
+                        <ComboBoxItem>Quarter</ComboBoxItem>
+                        <ComboBoxItem>Eighth</ComboBoxItem>
+                    </ComboBox>
                 </StackPanel>
                 <StackPanel DockPanel.Dock="Right" Orientation="Horizontal" HorizontalAlignment="Right">
                     <TextBox Width="30" Margin="5" Text="{Binding ResizeWidth}"/>
@@ -112,7 +122,7 @@
             </StackPanel>
         </Border>
         <Border BorderThickness="1" Background="Transparent" BorderBrush="Black" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="5">
-            <Image Margin="5" Source="{Binding FinalBitmap}" Width="400" Height="400" RenderOptions.BitmapScalingMode="NearestNeighbor" x:Name="mainImage">
+            <Image Margin="5" Source="{Binding RenderBitmap}" Width="400" Height="400" RenderOptions.BitmapScalingMode="NearestNeighbor" x:Name="mainImage">
                 <i:Interaction.Triggers>
                     <i:EventTrigger EventName="MouseDown">
                         <i:InvokeCommandAction Command="{Binding MouseDownCommand}" PassEventArgsToCommand="True"/>