Pārlūkot izejas kodu

chunk based rendering wip

Krzysztof Krysiński 1 gadu atpakaļ
vecāks
revīzija
df489ebf5d

+ 5 - 0
src/ChunkyImageLib/Chunk.cs

@@ -17,6 +17,8 @@ public class Chunk : IDisposable
 
     private bool returned = false;
 
+    public bool IsDirty { get; set; }
+
     /// <summary>
     /// The surface of the chunk
     /// </summary>
@@ -31,6 +33,9 @@ public class Chunk : IDisposable
     /// The resolution of the chunk
     /// </summary>
     public ChunkResolution Resolution { get; }
+
+    public Guid Uid { get; } = Guid.NewGuid();
+
     private Chunk(ChunkResolution resolution)
     {
         int size = resolution.PixelSize();

+ 10 - 13
src/PixiEditor.AvaloniaUI/Models/DocumentModels/ActionAccumulator.cs

@@ -94,10 +94,10 @@ internal class ActionAccumulator
             renderResult.AddRange(await canvasUpdater.UpdateGatheredChunks(affectedAreas, undoBoundaryPassed || viewportRefreshRequest));
             renderResult.AddRange(await previewUpdater.UpdateGatheredChunks(affectedAreas, undoBoundaryPassed));
 
-            if (undoBoundaryPassed)
+            /*if (undoBoundaryPassed)
             {
                 ClearDirtyRects();
-            }
+            }*/
 
             // add dirty rectangles
             AddDirtyRects(renderResult);
@@ -116,14 +116,6 @@ internal class ActionAccumulator
         executing = false;
     }
 
-    private void ClearDirtyRects()
-    {
-        foreach (var surface in document.Surfaces)
-        {
-            surface.Value.ClearDirtyRects();
-        }
-    }
-
     private bool AreAllPassthrough(List<IAction> actions)
     {
         foreach (var action in actions)
@@ -142,11 +134,16 @@ internal class ActionAccumulator
             {
                 case DirtyRect_RenderInfo info:
                     {
-                        var bitmap = document.Surfaces[info.Resolution];
-                        RectI finalRect = new RectI(VecI.Zero, new(bitmap.Size.X, bitmap.Size.Y));
+                        if(!document.RenderedChunks.ContainsKey(info.Resolution))
+                            continue;
+                        if (!document.RenderedChunks[info.Resolution].ContainsKey(info.Pos))
+                            continue;
 
+                        var bitmap = document.RenderedChunks[info.Resolution][info.Pos];
+                        RectI finalRect = new RectI(VecI.Zero, new(bitmap.PixelSize.X, bitmap.PixelSize.Y));
                         RectI dirtyRect = new RectI(info.Pos, info.Size).Intersect(finalRect);
-                        bitmap.AddDirtyRect(dirtyRect);
+
+                        bitmap.IsDirty = true;
                     }
                     break;
                 case PreviewDirty_RenderInfo info:

+ 2 - 2
src/PixiEditor.AvaloniaUI/Models/DocumentModels/DocumentUpdater.cs

@@ -277,12 +277,12 @@ internal class DocumentUpdater
     {
         VecI oldSize = doc.SizeBindable;
 
-        foreach ((ChunkResolution res, Surface surf) in doc.Surfaces)
+        /*foreach ((ChunkResolution res, Chunk surf) in doc.Surfaces)
         {
             surf.Dispose();
             VecI size = (VecI)(info.Size * res.Multiplier());
             doc.Surfaces[res] = new Surface(size); //TODO: Bgra8888 was here
-        }
+        }*/
 
         doc.SetSize(info.Size);
         doc.SetVerticalSymmetryAxisX(info.VerticalSymmetryAxisX);

+ 3 - 1
src/PixiEditor.AvaloniaUI/Models/Handlers/IDocument.cs

@@ -6,6 +6,7 @@ using PixiEditor.AvaloniaUI.Helpers;
 using PixiEditor.AvaloniaUI.Models.DocumentModels.Public;
 using PixiEditor.AvaloniaUI.Models.Structures;
 using PixiEditor.AvaloniaUI.Models.Tools;
+using PixiEditor.AvaloniaUI.ViewModels.Document;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surface;
@@ -22,7 +23,7 @@ internal interface IDocument : IHandler
     public IReferenceLayerHandler ReferenceLayerHandler { get; }
     public VectorPath SelectionPathBindable { get; }
     public IFolderHandler StructureRoot { get; }
-    public Dictionary<ChunkResolution, Surface> Surfaces { get; set; }
+    public ChunkCache RenderedChunks { get; }
     public DocumentStructureModule StructureHelper { get; }
     public Surface PreviewSurface { get; set; }
     public bool AllChangesSaved { get; }
@@ -50,4 +51,5 @@ internal interface IDocument : IHandler
     public void SetSize(VecI infoSize);
     public Color PickColor(VecD controllerLastPrecisePosition, DocumentScope scope, bool includeReference, bool includeCanvas, bool isTopMost);
     public List<Guid> ExtractSelectedLayers(bool includeFoldersWithMask = false);
+    public void UpdateChunk(VecI chunkPos, ChunkResolution resolution, Chunk? updated);
 }

+ 11 - 2
src/PixiEditor.AvaloniaUI/Models/Rendering/CanvasUpdater.cs

@@ -169,10 +169,19 @@ internal class CanvasUpdater
             if (globalClippingRectangle is not null)
                 globalScaledClippingRectangle = (RectI?)((RectI)globalClippingRectangle).Scale(resolution.Multiplier()).RoundOutwards();
 
-            Surface screenSurface = doc.Surfaces[resolution];
             foreach (var chunkPos in chunks)
             {
-                RenderChunk(chunkPos, screenSurface, resolution, globalClippingRectangle, globalScaledClippingRectangle);
+                ChunkRenderer.MergeWholeStructure(chunkPos, resolution,
+                    internals.Tracker.Document.StructureRoot, globalClippingRectangle).Switch(
+                    (Chunk chunk) =>
+                    {
+                        doc.UpdateChunk(chunkPos, resolution, chunk);
+                    },
+                    (EmptyChunk _) =>
+                    {
+                        doc.UpdateChunk(chunkPos, resolution, null);
+
+                    });
                 RectI chunkRect = new(chunkPos * chunkSize, new(chunkSize, chunkSize));
                 if (globalScaledClippingRectangle is RectI rect)
                     chunkRect = chunkRect.Intersect(rect);

+ 24 - 0
src/PixiEditor.AvaloniaUI/ViewModels/Document/ChunkCache.cs

@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+using System.Linq;
+using ChunkyImageLib;
+using ChunkyImageLib.DataHolders;
+using PixiEditor.DrawingApi.Core.Numerics;
+
+namespace PixiEditor.AvaloniaUI.ViewModels.Document;
+
+public class ChunkCache : Dictionary<ChunkResolution, ChunkSet>
+{
+    public ChunkCache()
+    {
+        this[ChunkResolution.Full] = new ChunkSet();
+        this[ChunkResolution.Half] = new ChunkSet();
+        this[ChunkResolution.Quarter] = new ChunkSet();
+        this[ChunkResolution.Eighth] = new ChunkSet();
+    }
+}
+
+public class ChunkSet : Dictionary<VecI, Chunk>
+{
+
+}
+

+ 14 - 12
src/PixiEditor.AvaloniaUI/ViewModels/Document/DocumentViewModel.cs

@@ -132,15 +132,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     public DocumentEventsModule EventInlet { get; }
     public ActionDisplayList ActionDisplays { get; } = new(() => ViewModelMain.Current.NotifyToolActionDisplayChanged());
     public IStructureMemberHandler? SelectedStructureMember { get; private set; } = null;
-    //TODO: It was DrawingSurface before, check if it's correct
-    public Dictionary<ChunkResolution, Surface> Surfaces { get; set; } = new()
-    {
-        [ChunkResolution.Full] = new Surface(new VecI(64, 64)),
-        [ChunkResolution.Half] = new Surface(new VecI(32, 32)),
-        [ChunkResolution.Quarter] = new Surface(new VecI(16, 16)),
-        [ChunkResolution.Eighth] = new Surface(new VecI(8, 8))
-    };
-
+    public ChunkCache RenderedChunks { get; set; } = new();
     public Surface PreviewSurface { get; set; }
 
     private VectorPath selectionPath = new VectorPath();
@@ -161,7 +153,6 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     public IFolderHandlerFactory FolderHandlerFactory { get; }
     IReferenceLayerHandler IDocument.ReferenceLayerHandler => ReferenceLayerViewModel;
 
-
     private DocumentViewModel()
     {
         var serviceProvider = ViewModelMain.Current.Services;
@@ -213,8 +204,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
 
         if (builderInstance.ReferenceLayer is { } refLayer)
         {
-            acc
-                .AddActions(new SetReferenceLayer_Action(refLayer.Shape, refLayer.ImagePbgra32Bytes.ToImmutableArray(), refLayer.ImageSize));
+            acc.AddActions(new SetReferenceLayer_Action(refLayer.Shape, refLayer.ImagePbgra32Bytes.ToImmutableArray(), refLayer.ImageSize));
         }
 
         viewModel.Swatches = new ObservableCollection<PaletteColor>(builderInstance.Swatches);
@@ -591,6 +581,18 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         return result;
     }
 
+    public void UpdateChunk(VecI chunkPos, ChunkResolution resolution, Chunk? updated)
+    {
+        if (RenderedChunks.ContainsKey(resolution) && RenderedChunks[resolution].TryGetValue(chunkPos, out Chunk? value))
+        {
+            value?.Dispose();
+        }
+        if (!RenderedChunks.ContainsKey(resolution))
+            RenderedChunks[resolution] = new();
+
+        RenderedChunks[resolution][chunkPos] = updated;
+    }
+
     private void ExtractSelectedLayers(FolderViewModel folder, List<Guid> list,
         bool includeFoldersWithMask)
     {

+ 5 - 5
src/PixiEditor.AvaloniaUI/Views/Main/ViewportControls/FixedViewport.axaml.cs

@@ -45,7 +45,7 @@ internal partial class FixedViewport : UserControl, INotifyPropertyChanged
         set => SetValue(DocumentProperty, value);
     }
 
-    public Surface? TargetBitmap
+    /*public Surface? TargetBitmap
     {
         get
         {
@@ -53,7 +53,7 @@ internal partial class FixedViewport : UserControl, INotifyPropertyChanged
                 return value;
             return null;
         }
-    }
+    }*/
 
     public Guid GuidValue { get; } = Guid.NewGuid();
 
@@ -66,7 +66,7 @@ internal partial class FixedViewport : UserControl, INotifyPropertyChanged
     public FixedViewport()
     {
         InitializeComponent();
-        Binding binding = new Binding { Source = this, Path = $"{nameof(Document)}.{nameof(Document.Surfaces)}" };
+        Binding binding = new Binding { Source = this, Path = $"{nameof(Document)}.{nameof(Document.RenderedChunks)}" };
         this.Bind(BitmapsProperty, binding);
         Loaded += OnLoad;
         Unloaded += OnUnload;
@@ -130,13 +130,13 @@ internal partial class FixedViewport : UserControl, INotifyPropertyChanged
     private static void OnBitmapsChange(AvaloniaPropertyChangedEventArgs<Dictionary<ChunkResolution, WriteableBitmap>> args)
     {
         FixedViewport? viewport = (FixedViewport)args.Sender;
-        viewport.PropertyChanged?.Invoke(viewport, new(nameof(TargetBitmap)));
+        //viewport.PropertyChanged?.Invoke(viewport, new(nameof(TargetBitmap)));
         viewport.Document?.Operations.AddOrUpdateViewport(viewport.GetLocation());
     }
 
     private void OnImageSizeChanged(object sender, SizeChangedEventArgs e)
     {
-        PropertyChanged?.Invoke(this, new(nameof(TargetBitmap)));
+        //PropertyChanged?.Invoke(this, new(nameof(TargetBitmap)));
         //Document?.Operations.AddOrUpdateViewport(GetLocation()); //TODO: This causes deadlock
     }
 }

+ 22 - 8
src/PixiEditor.AvaloniaUI/Views/Main/ViewportControls/Viewport.axaml

@@ -20,6 +20,8 @@
     xmlns:viewportControls="clr-namespace:PixiEditor.AvaloniaUI.Views.Main.ViewportControls"
     xmlns:overlays="clr-namespace:PixiEditor.AvaloniaUI.Views.Overlays"
     xmlns:selectionOverlay="clr-namespace:PixiEditor.AvaloniaUI.Views.Overlays.SelectionOverlay"
+    xmlns:renderers="clr-namespace:PixiEditor.AvaloniaUI.Views.Renderers"
+    xmlns:generic="clr-namespace:System.Collections.Generic;assembly=System.Runtime"
     mc:Ignorable="d"
     x:Name="vpUc"
     d:DesignHeight="450"
@@ -229,14 +231,23 @@
                             </Style>-->
                         </Canvas.Styles>
                     </Canvas>
-                    <visuals:SurfaceControl
-                        Focusable="False"
-                        Width="{Binding Document.Width}"
-                        Height="{Binding Document.Height}"
-                        Surface="{Binding TargetBitmap}"
+                    <ItemsControl ItemsSource="{Binding ActiveChunkSet}">
+                        <ItemsControl.ItemsPanel>
+                            <ItemsPanelTemplate>
+                                <renderers:ChunkSetPanel
+                                    ChunkCache="{Binding Document.RenderedChunks}"
+                                    MaxSize="{Binding Document.SizeBindable}"
+                                    Resolution="{Binding ActiveResolution}"/>
+                            </ItemsPanelTemplate>
+                        </ItemsControl.ItemsPanel>
+                        <ItemsControl.ItemTemplate>
+                            <DataTemplate>
+                                 <renderers:ChunkControl
+                        Chunk="{Binding Value}"
+                        ChunkPosition="{Binding Key}"
                         ui1:RenderOptionsBindable.BitmapInterpolationMode="{Binding Zoombox.Scale, Converter={converters:ScaleToBitmapScalingModeConverter}}"
                         FlowDirection="LeftToRight">
-                        <visuals:SurfaceControl.Styles>
+                        <renderers:ChunkControl.Styles>
                             <!--TODO: Implement-->
                             <!--<Style>
                                 <Style.Triggers>
@@ -262,8 +273,11 @@
                                     </DataTrigger>
                                 </Style.Triggers>
                             </Style>-->
-                        </visuals:SurfaceControl.Styles>
-                    </visuals:SurfaceControl>
+                        </renderers:ChunkControl.Styles>
+                    </renderers:ChunkControl>
+                            </DataTemplate>
+                        </ItemsControl.ItemTemplate>
+                    </ItemsControl>
                     <Grid ZIndex="5">
                         <symmetryOverlay:SymmetryOverlay
                             Focusable="False"

+ 20 - 22
src/PixiEditor.AvaloniaUI/Views/Main/ViewportControls/Viewport.axaml.cs

@@ -1,5 +1,7 @@
 using System.Collections.Generic;
+using System.Collections.ObjectModel;
 using System.ComponentModel;
+using System.Linq;
 using System.Windows.Input;
 using Avalonia;
 using Avalonia.Controls;
@@ -48,9 +50,6 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
     public static readonly StyledProperty<ICommand> MouseUpCommandProperty =
         AvaloniaProperty.Register<Viewport, ICommand>(nameof(MouseUpCommand), null);
 
-    private static readonly StyledProperty<Dictionary<ChunkResolution, WriteableBitmap>> BitmapsProperty =
-        AvaloniaProperty.Register<Viewport, Dictionary<ChunkResolution, WriteableBitmap>>(nameof(Bitmaps), null);
-
     public static readonly StyledProperty<bool> DelayedProperty =
         AvaloniaProperty.Register<Viewport, bool>(nameof(Delayed), false);
 
@@ -84,6 +83,9 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
     public static readonly StyledProperty<ICommand> MiddleMouseClickedCommandProperty =
         AvaloniaProperty.Register<Viewport, ICommand>(nameof(MiddleMouseClickedCommand), null);
 
+    public ChunkSet ActiveChunkSet => Document.RenderedChunks[ActiveResolution];
+    public ChunkResolution ActiveResolution => CalculateResolution();
+
     public ICommand? MiddleMouseClickedCommand
     {
         get => (ICommand?)GetValue(MiddleMouseClickedCommandProperty);
@@ -150,12 +152,6 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
         set => SetValue(DelayedProperty, value);
     }
 
-    public Dictionary<ChunkResolution, WriteableBitmap>? Bitmaps
-    {
-        get => (Dictionary<ChunkResolution, WriteableBitmap>?)GetValue(BitmapsProperty);
-        set => SetValue(BitmapsProperty, value);
-    }
-
     public ICommand? MouseDownCommand
     {
         get => (ICommand?)GetValue(MouseDownCommandProperty);
@@ -250,7 +246,9 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
             Document?.Operations.AddOrUpdateViewport(GetLocation());
 
             if (oldRes != newRes)
-                PropertyChanged?.Invoke(this, new(nameof(TargetBitmap)));
+            {
+                UpdateChunks(newRes);
+            }
         }
     }
 
@@ -269,16 +267,16 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
             Document?.Operations.AddOrUpdateViewport(GetLocation());
 
             if (oldRes != newRes)
-                PropertyChanged?.Invoke(this, new(nameof(TargetBitmap)));
+            {
+                UpdateChunks(newRes);
+            }
         }
     }
 
-    public Surface? TargetBitmap
+    private void UpdateChunks(ChunkResolution newRes)
     {
-        get
-        {
-            return Document?.Surfaces.TryGetValue(CalculateResolution(), out Surface? value) == true ? value : null;
-        }
+        PropertyChanged?.Invoke(this, new(nameof(ActiveChunkSet)));
+        PropertyChanged?.Invoke(this, new(nameof(ActiveResolution)));
     }
 
     public double ReferenceLayerScale =>
@@ -295,7 +293,7 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
     static Viewport()
     {
         DocumentProperty.Changed.Subscribe(OnDocumentChange);
-        BitmapsProperty.Changed.Subscribe(OnBitmapsChange);
+        //ActiveChunkSetProperty.Changed.Subscribe(OnChunksChange);
         ZoomViewportTriggerProperty.Changed.Subscribe(ZoomViewportTriggerChanged);
         CenterViewportTriggerProperty.Changed.Subscribe(CenterViewportTriggerChanged);
     }
@@ -304,8 +302,8 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
     {
         InitializeComponent();
 
-        Binding binding = new Binding { Source = this, Path = $"{nameof(Document)}.{nameof(Document.Surfaces)}" };
-        this.Bind(BitmapsProperty, binding);
+        /*Binding binding = new Binding { Source = this, Path = $"{nameof(Document)}.{nameof(Document.Surfaces)}" };
+        this.Bind(ChunksProperty, binding);*/
 
         MainImage!.Loaded += OnImageLoaded;
         MainImage.SizeChanged += OnMainImageSizeChanged;
@@ -317,7 +315,7 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
         viewportGrid.AddHandler(PointerPressedEvent, Image_MouseDown, RoutingStrategies.Bubble);
     }
 
-    public SurfaceControl? MainImage => (SurfaceControl?)((Grid?)((Border?)zoombox.AdditionalContent)?.Child)?.Children[1];
+    public ItemsControl? MainImage => (ItemsControl?)((Grid?)((Border?)zoombox.AdditionalContent)?.Child)?.Children[1];
     public Grid BackgroundGrid => viewportGrid;
 
     private void ForceRefreshFinalImage()
@@ -346,10 +344,10 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
         newDoc?.Operations.AddOrUpdateViewport(viewport.GetLocation());
     }
 
-    private static void OnBitmapsChange(AvaloniaPropertyChangedEventArgs<Dictionary<ChunkResolution, WriteableBitmap>?> e)
+    private static void OnChunksChange(AvaloniaPropertyChangedEventArgs<Dictionary<ChunkResolution, Dictionary<VecI, Chunk>>?> e)
     {
         Viewport viewportObj = (Viewport)e.Sender;
-        viewportObj.PropertyChanged?.Invoke(viewportObj, new(nameof(TargetBitmap)));
+        //viewportObj.PropertyChanged?.Invoke(viewportObj, new(nameof(TargetBitmap)));
     }
 
     private ChunkResolution CalculateResolution()

+ 67 - 0
src/PixiEditor.AvaloniaUI/Views/Renderers/ChunkControl.cs

@@ -0,0 +1,67 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+using Avalonia.Rendering.SceneGraph;
+using ChunkyImageLib;
+using ChunkyImageLib.DataHolders;
+using PixiEditor.DrawingApi.Core.Numerics;
+
+namespace PixiEditor.AvaloniaUI.Views.Renderers;
+
+public class ChunkControl : Control
+{
+    public static readonly StyledProperty<Chunk> ChunkProperty = AvaloniaProperty.Register<ChunkControl, Chunk>(
+        nameof(Chunk));
+
+    public static readonly StyledProperty<VecI> ChunkPositionProperty = AvaloniaProperty.Register<ChunkControl, VecI>(
+        nameof(ChunkPosition));
+
+    public VecI ChunkPosition
+    {
+        get => GetValue(ChunkPositionProperty);
+        set => SetValue(ChunkPositionProperty, value);
+    }
+    public Chunk Chunk
+    {
+        get => GetValue(ChunkProperty);
+        set => SetValue(ChunkProperty, value);
+    }
+
+    protected override Size MeasureOverride(Size availableSize)
+    {
+        return base.MeasureOverride(availableSize);
+    }
+
+    public override void Render(DrawingContext context)
+    {
+        context.PushTransform(Matrix.CreateScale(new Vector(Chunk.Resolution.InvertedMultiplier(), Chunk.Resolution.InvertedMultiplier())));
+        Color color = Color.FromArgb(255, (byte)Random.Shared.Next(0, 255), (byte)Random.Shared.Next(0, 255), (byte)Random.Shared.Next(0, 255));
+        context.DrawRectangle(new SolidColorBrush(color), new Pen(Brushes.Black, 0), new Rect(0, 0, Chunk.PixelSize.X, Chunk.PixelSize.Y));
+    }
+}
+
+
+public class RenderChunkOperation : ICustomDrawOperation
+{
+    public Rect Bounds { get; }
+
+    public void Render(ImmediateDrawingContext context)
+    {
+        throw new NotImplementedException();
+    }
+
+    public bool Equals(ICustomDrawOperation? other)
+    {
+        throw new NotImplementedException();
+    }
+
+    public void Dispose()
+    {
+        throw new NotImplementedException();
+    }
+
+    public bool HitTest(Point p)
+    {
+        throw new NotImplementedException();
+    }
+}

+ 88 - 0
src/PixiEditor.AvaloniaUI/Views/Renderers/ChunkSetPanel.cs

@@ -0,0 +1,88 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Presenters;
+using Avalonia.Media;
+using Avalonia.Rendering;
+using Avalonia.Rendering.SceneGraph;
+using ChunkyImageLib;
+using ChunkyImageLib.DataHolders;
+using PixiEditor.AvaloniaUI.ViewModels.Document;
+using PixiEditor.DrawingApi.Core.Numerics;
+
+namespace PixiEditor.AvaloniaUI.Views.Renderers;
+
+public class ChunkSetPanel : Panel
+{
+    public static readonly StyledProperty<ChunkCache> ChunkCacheProperty = AvaloniaProperty.Register<ChunkSetPanel, ChunkCache>(
+        nameof(ChunkCache));
+
+    public static readonly StyledProperty<ChunkResolution> ResolutionProperty = AvaloniaProperty.Register<ChunkSetPanel, ChunkResolution>(
+        nameof(Resolution));
+
+    public static readonly StyledProperty<VecI> MaxSizeProperty = AvaloniaProperty.Register<ChunkSetPanel, VecI>(
+        nameof(MaxSize));
+
+    public VecI MaxSize
+    {
+        get => GetValue(MaxSizeProperty);
+        set => SetValue(MaxSizeProperty, value);
+    }
+
+    public ChunkResolution Resolution
+    {
+        get => GetValue(ResolutionProperty);
+        set => SetValue(ResolutionProperty, value);
+    }
+
+    public ChunkCache ChunkCache
+    {
+        get => GetValue(ChunkCacheProperty);
+        set => SetValue(ChunkCacheProperty, value);
+    }
+
+    static ChunkSetPanel()
+    {
+        AffectsMeasure<ChunkSetPanel>(ChunkCacheProperty, ResolutionProperty, MaxSizeProperty);
+    }
+
+
+    protected override Size MeasureOverride(Size availableSize)
+    {
+        int pixelSize = Resolution.PixelSize();
+        if(ChunkCache == null) return new Size(0, 0);
+        Size size = new Size(pixelSize * ChunkCache[Resolution].Count, pixelSize * ChunkCache[Resolution].Count);
+        if(MaxSize.X > 0 && MaxSize.Y > 0)
+        {
+            size = new Size(Math.Min(size.Width, MaxSize.X), Math.Min(size.Height, MaxSize.Y));
+        }
+
+        return size;
+    }
+
+    protected override Size ArrangeOverride(Size finalSize)
+    {
+        if(ChunkCache == null) return finalSize;
+        int pixelSize = Resolution.PixelSize();
+        double inverseMultiplier = Resolution.InvertedMultiplier();
+        foreach (var child in Children)
+        {
+            if(child is ContentPresenter presenter && presenter.Content is KeyValuePair<VecI, Chunk> chunkPair)
+            {
+                if(!ChunkCache[Resolution].ContainsKey(chunkPair.Key))
+                {
+                    Children.Remove(presenter);
+                }
+                else
+                {
+                    double width = Math.Min(finalSize.Width, Math.Min(MaxSize.X, pixelSize)) * inverseMultiplier;
+                    double height = Math.Min(finalSize.Height, Math.Min(MaxSize.Y, pixelSize)) * inverseMultiplier;
+                    presenter.Arrange(new Rect(chunkPair.Key.X * pixelSize * inverseMultiplier, chunkPair.Key.Y * pixelSize * inverseMultiplier, width, height));
+                }
+            }
+        }
+        return finalSize;
+    }
+}

+ 0 - 28
src/PixiEditor.AvaloniaUI/Views/Visuals/SurfaceControl.cs

@@ -89,11 +89,6 @@ public class DrawingSurfaceOp : ICustomDrawOperation
 
     private SKPaint _paint = new SKPaint();
 
-    //TODO: Implement dirty rect handling
-    /*private RectI? _lastDirtyRect;
-    private SKImage? _lastImage;
-    private SKImage? _lastFullImage;*/
-
     public DrawingSurfaceOp(Surface surface, Rect bounds, Stretch stretch)
     {
         Surface = surface;
@@ -111,29 +106,6 @@ public class DrawingSurfaceOp : ICustomDrawOperation
 
             ScaleCanvas(canvas);
             canvas.DrawSurface((SKSurface)Surface.DrawingSurface.Native, 0, 0, _paint);
-            /*if(_lastDirtyRect != Surface.DirtyRect)
-            {
-                RectI dirtyRect = Surface.DirtyRect;
-                if (dirtyRect.IsZeroOrNegativeArea)
-                {
-                    dirtyRect = new RectI(0, 0, Surface.Size.X, Surface.Size.Y);
-                    _lastFullImage = (SKImage)Surface.DrawingSurface.Snapshot().Native;
-                }
-
-                _lastImage = (SKImage)Surface.DrawingSurface.Snapshot(dirtyRect).Native;
-                _lastDirtyRect = Surface.DirtyRect;
-            }
-
-            if (_lastFullImage != null)
-            {
-                canvas.DrawImage(_lastFullImage, new SKPoint(0, 0), _paint);
-            }
-
-            if (_lastImage != null)
-            {
-                canvas.DrawImage(_lastImage, new SKPoint(_lastDirtyRect.Value.X, _lastDirtyRect.Value.Y), _paint);
-            }*/
-
             canvas.Restore();
         }
     }