Browse Source

SkiaOpenGL renderer wip

Krzysztof Krysiński 1 year ago
parent
commit
9ca566f3c9

+ 0 - 1
src/ChunkyImageLib/Surface.cs

@@ -16,7 +16,6 @@ public class Surface : IDisposable
     public DrawingSurface DrawingSurface { get; }
     public int BytesPerPixel { get; }
     public VecI Size { get; }
-
     public RectI DirtyRect { get; private set; }
 
     private Paint drawingPaint = new Paint() { BlendMode = BlendMode.Src };

+ 4 - 0
src/PixiEditor.AvaloniaUI.Desktop/Program.cs

@@ -18,5 +18,9 @@ public class Program
         => AppBuilder.Configure<App>()
             .UsePlatformDetect()
             .WithInterFont()
+            .With(new Win32PlatformOptions()
+            {
+                RenderingMode = new[] { Win32RenderingMode.Wgl }
+            })
             .LogToTrace();
 }

+ 1 - 0
src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/UpdateViewModel.cs

@@ -9,6 +9,7 @@ using Avalonia.Controls.ApplicationLifetimes;
 using PixiEditor.AvaloniaUI.Helpers;
 using PixiEditor.AvaloniaUI.Models.Commands.Attributes.Commands;
 using PixiEditor.AvaloniaUI.Models.Dialogs;
+using PixiEditor.AvaloniaUI.Views.Dialogs;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.Common.UserPreferences;
 using PixiEditor.Platform;

+ 42 - 37
src/PixiEditor.AvaloniaUI/Views/Main/ViewportControls/Viewport.axaml

@@ -25,8 +25,7 @@
     d:DesignHeight="450"
     d:DesignWidth="800">
     <Grid
-        x:Name="viewportGrid"
-        >
+        x:Name="viewportGrid">
         <Interaction.Behaviors>
             <!--TODO: Implement stylus support-->
             <!--<EventTriggerBehavior EventName="StylusButtonDown">
@@ -115,6 +114,46 @@
         </Border>
             </overlays:TogglableFlyout.Child>
         </overlays:TogglableFlyout>
+         <visuals:Scene
+                        Focusable="False" Name="scene"
+                        RenderTransformOrigin="0,0"
+                        ZIndex="10"
+                        Width="{Binding RealDimensions.X, ElementName=vpUc}"
+                        Height="{Binding RealDimensions.Y, ElementName=vpUc}"
+                        Surface="{Binding TargetBitmap, ElementName=vpUc}"
+                        Scale="{Binding Scale, ElementName=zoombox, Mode=OneWay}"
+                        Document="{Binding Document, ElementName=vpUc, Mode=OneWay}"
+                        ContentPosition="{Binding CanvasPos, ElementName=zoombox, Mode=OneWay}"
+                        ui1:RenderOptionsBindable.BitmapInterpolationMode="{Binding Scale, Converter={converters:ScaleToBitmapScalingModeConverter}, ElementName=zoombox}"
+                        FlowDirection="LeftToRight">
+                        <visuals:Scene.Styles>
+                            <!--TODO: Implement-->
+                            <!--<Style>
+                                <Style.Triggers>
+                                    <DataTrigger Binding="{Binding Source={vm:ToolVM ColorPickerToolViewModel}, Path=PickOnlyFromReferenceLayer, Mode=OneWay}" Value="True">
+                                        <DataTrigger.EnterActions>
+                                            <BeginStoryboard>
+                                                <Storyboard>
+                                                    <DoubleAnimation
+                                                        Storyboard.TargetProperty="(Button.Opacity)"
+                                                        From="1" To="0" Duration="0:0:0.1" />
+                                                </Storyboard>
+                                            </BeginStoryboard>
+                                        </DataTrigger.EnterActions>
+                                        <DataTrigger.ExitActions>
+                                            <BeginStoryboard>
+                                                <Storyboard>
+                                                    <DoubleAnimation
+                                                        Storyboard.TargetProperty="(Button.Opacity)"
+                                                        From="0" To="1" Duration="0:0:0.1" />
+                                                </Storyboard>
+                                            </BeginStoryboard>
+                                        </DataTrigger.ExitActions>
+                                    </DataTrigger>
+                                </Style.Triggers>
+                            </Style>-->
+                        </visuals:Scene.Styles>
+                    </visuals:Scene>
         <zoombox:Zoombox
             Tag="{Binding ElementName=vpUc}"
             x:Name="zoombox"
@@ -229,41 +268,7 @@
                             </Style>-->
                         </Canvas.Styles>
                     </Canvas>
-                    <visuals:SurfaceControl
-                        Focusable="False"
-                        Width="{Binding Document.Width}"
-                        Height="{Binding Document.Height}"
-                        Surface="{Binding TargetBitmap}"
-                        ui1:RenderOptionsBindable.BitmapInterpolationMode="{Binding Zoombox.Scale, Converter={converters:ScaleToBitmapScalingModeConverter}}"
-                        FlowDirection="LeftToRight">
-                        <visuals:SurfaceControl.Styles>
-                            <!--TODO: Implement-->
-                            <!--<Style>
-                                <Style.Triggers>
-                                    <DataTrigger Binding="{Binding Source={vm:ToolVM ColorPickerToolViewModel}, Path=PickOnlyFromReferenceLayer, Mode=OneWay}" Value="True">
-                                        <DataTrigger.EnterActions>
-                                            <BeginStoryboard>
-                                                <Storyboard>
-                                                    <DoubleAnimation
-                                                        Storyboard.TargetProperty="(Button.Opacity)"
-                                                        From="1" To="0" Duration="0:0:0.1" />
-                                                </Storyboard>
-                                            </BeginStoryboard>
-                                        </DataTrigger.EnterActions>
-                                        <DataTrigger.ExitActions>
-                                            <BeginStoryboard>
-                                                <Storyboard>
-                                                    <DoubleAnimation
-                                                        Storyboard.TargetProperty="(Button.Opacity)"
-                                                        From="0" To="1" Duration="0:0:0.1" />
-                                                </Storyboard>
-                                            </BeginStoryboard>
-                                        </DataTrigger.ExitActions>
-                                    </DataTrigger>
-                                </Style.Triggers>
-                            </Style>-->
-                        </visuals:SurfaceControl.Styles>
-                    </visuals:SurfaceControl>
+                    <Panel Width="{Binding Document.Width}" Height="{Binding Document.Height}"/>
                     <Grid ZIndex="5">
                         <symmetryOverlay:SymmetryOverlay
                             Focusable="False"

+ 1 - 14
src/PixiEditor.AvaloniaUI/Views/Main/ViewportControls/Viewport.axaml.cs

@@ -48,9 +48,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);
 
@@ -150,12 +147,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);
@@ -295,7 +286,6 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
     static Viewport()
     {
         DocumentProperty.Changed.Subscribe(OnDocumentChange);
-        BitmapsProperty.Changed.Subscribe(OnBitmapsChange);
         ZoomViewportTriggerProperty.Changed.Subscribe(ZoomViewportTriggerChanged);
         CenterViewportTriggerProperty.Changed.Subscribe(CenterViewportTriggerChanged);
     }
@@ -304,9 +294,6 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
     {
         InitializeComponent();
 
-        Binding binding = new Binding { Source = this, Path = $"{nameof(Document)}.{nameof(Document.Surfaces)}" };
-        this.Bind(BitmapsProperty, binding);
-
         MainImage!.Loaded += OnImageLoaded;
         MainImage.SizeChanged += OnMainImageSizeChanged;
         Loaded += OnLoad;
@@ -317,7 +304,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 Panel? MainImage => (Panel?)((Grid?)((Border?)zoombox.AdditionalContent)?.Child)?.Children[1];
     public Grid BackgroundGrid => viewportGrid;
 
     private void ForceRefreshFinalImage()

+ 136 - 0
src/PixiEditor.AvaloniaUI/Views/Visuals/Scene.cs

@@ -0,0 +1,136 @@
+using Avalonia;
+using Avalonia.Media;
+using Avalonia.OpenGL;
+using Avalonia.OpenGL.Controls;
+using ChunkyImageLib;
+using PixiEditor.AvaloniaUI.ViewModels.Document;
+using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.DrawingApi.Skia;
+
+namespace PixiEditor.AvaloniaUI.Views.Visuals;
+
+internal class Scene : OpenGlControlBase
+{
+    public static readonly StyledProperty<Surface> SurfaceProperty = AvaloniaProperty.Register<SurfaceControl, Surface>(
+        nameof(Surface));
+
+    public static readonly StyledProperty<double> ScaleProperty = AvaloniaProperty.Register<Scene, double>(
+        nameof(Scale), 1);
+
+    public static readonly StyledProperty<VecI> ContentPositionProperty = AvaloniaProperty.Register<Scene, VecI>(
+        nameof(ContentPosition));
+
+    public static readonly StyledProperty<DocumentViewModel> DocumentProperty = AvaloniaProperty.Register<Scene, DocumentViewModel>(
+        nameof(Document));
+
+    public DocumentViewModel Document
+    {
+        get => GetValue(DocumentProperty);
+        set => SetValue(DocumentProperty, value);
+    }
+
+    public VecI ContentPosition
+    {
+        get => GetValue(ContentPositionProperty);
+        set => SetValue(ContentPositionProperty, value);
+    }
+
+    public double Scale
+    {
+        get => GetValue(ScaleProperty);
+        set => SetValue(ScaleProperty, value);
+    }
+
+    public Surface Surface
+    {
+        get => GetValue(SurfaceProperty);
+        set => SetValue(SurfaceProperty, value);
+    }
+
+    private SKSurface _workingSurface;
+    private SKSurface _viewportSizedSurface;
+    private SKPaint _paint = new SKPaint() { BlendMode = SKBlendMode.SrcOver };
+    private GRContext? gr;
+
+    static Scene()
+    {
+        SurfaceProperty.Changed.AddClassHandler<Scene>(OnSurfaceChanged);
+        BoundsProperty.Changed.AddClassHandler<Scene>(BoundsChanged);
+        WidthProperty.Changed.AddClassHandler<Scene>(BoundsChanged);
+        HeightProperty.Changed.AddClassHandler<Scene>(BoundsChanged);
+    }
+
+    public Scene()
+    {
+        ClipToBounds = true;
+    }
+
+    protected override void OnOpenGlInit(GlInterface gl)
+    {
+        gr = GRContext.CreateGl(GRGlInterface.Create(gl.GetProcAddress));
+        CreateWorkingSurface();
+    }
+
+    private void CreateWorkingSurface()
+    {
+        if (gr == null) return;
+
+        _workingSurface?.Dispose();
+        GRGlFramebufferInfo frameBuffer = new GRGlFramebufferInfo(0, SKColorType.Rgba8888.ToGlSizedFormat());
+        GRBackendRenderTarget desc = new GRBackendRenderTarget((int)Bounds.Width, (int)Bounds.Height, 4, 0, frameBuffer);
+        _workingSurface = SKSurface.Create(gr, desc, GRSurfaceOrigin.BottomLeft, SKImageInfo.PlatformColorType);
+    }
+
+    protected override void OnOpenGlRender(GlInterface gl, int fb)
+    {
+        SKCanvas canvas = _workingSurface.Canvas;
+        canvas.Save();
+        canvas.ClipRect(new SKRect(0, 0, (float)Bounds.Width, (float)Bounds.Height));
+
+        canvas.Clear(SKColors.Transparent);
+
+        float scaleX = (float)Document.Width / Surface.Size.X;
+        float scaleY = (float)Document.Height / Surface.Size.Y;
+        var scaleUniform = Math.Min(scaleX, scaleY);
+
+        float scale = (float)Scale * scaleUniform;
+
+        canvas.Scale(scale, scale, ContentPosition.X, ContentPosition.Y);
+        canvas.Translate(ContentPosition.X, ContentPosition.Y);
+
+        //canvas.Translate((float)Bounds.Width / 2f - Surface.Size.X / 2f, (float)Bounds.Height / 2f - Surface.Size.Y / 2f);
+
+        canvas.DrawSurface((SKSurface)Surface.DrawingSurface.Native, 0, 0, _paint);
+        //canvas.DrawRect(0, 0, Surface.Size.X, Surface.Size.Y, _paint);
+
+        canvas.Restore();
+
+        canvas.Flush();
+        RequestNextFrameRendering();
+    }
+
+    private static void StretchChanged(Scene sender, AvaloniaPropertyChangedEventArgs e)
+    {
+        if (e.NewValue is Stretch stretch)
+        {
+            //sender._drawingSurfaceOp = new DrawingSurfaceOp(sender.Surface, sender.Bounds, stretch);
+        }
+    }
+
+    private static void BoundsChanged(Scene sender, AvaloniaPropertyChangedEventArgs e)
+    {
+        if (e.NewValue is Rect bounds)
+        {
+            //sender._drawingSurfaceOp = new DrawingSurfaceOp(sender.Surface, bounds, sender.Stretch);
+            sender.CreateWorkingSurface();
+        }
+    }
+
+    private static void OnSurfaceChanged(Scene sender, AvaloniaPropertyChangedEventArgs e)
+    {
+        if (e.NewValue is Surface surface)
+        {
+            //sender._drawingSurfaceOp = new DrawingSurfaceOp(surface, sender.Bounds, sender.Stretch);
+        }
+    }
+}

+ 42 - 94
src/PixiEditor.AvaloniaUI/Views/Visuals/SurfaceControl.cs

@@ -1,20 +1,17 @@
-using System.Diagnostics;
-using Avalonia;
-using Avalonia.Controls;
+using Avalonia;
 using Avalonia.Media;
+using Avalonia.OpenGL;
+using Avalonia.OpenGL.Controls;
 using Avalonia.Platform;
 using Avalonia.Rendering.SceneGraph;
 using Avalonia.Skia;
 using ChunkyImageLib;
-using PixiEditor.DrawingApi.Core.Numerics;
-using PixiEditor.DrawingApi.Core.Surface;
-using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
-using Image = PixiEditor.DrawingApi.Core.Surface.ImageData.Image;
+using PixiEditor.DrawingApi.Skia;
 using Point = Avalonia.Point;
 
 namespace PixiEditor.AvaloniaUI.Views.Visuals;
 
-public class SurfaceControl : Control
+public class SurfaceControl : OpenGlControlBase
 {
     public static readonly StyledProperty<Surface> SurfaceProperty = AvaloniaProperty.Register<SurfaceControl, Surface>(
         nameof(Surface));
@@ -34,108 +31,61 @@ public class SurfaceControl : Control
         set => SetValue(SurfaceProperty, value);
     }
 
-    private DrawingSurfaceOp _drawingSurfaceOp;
+    private SKSurface _workingSurface;
+    private SKPaint _paint = new SKPaint() { BlendMode = SKBlendMode.Src };
+    private GRContext? gr;
 
     static SurfaceControl()
     {
         AffectsRender<SurfaceControl>(StretchProperty);
-        SurfaceProperty.Changed.AddClassHandler<SurfaceControl>(OnSurfaceChanged);
         BoundsProperty.Changed.AddClassHandler<SurfaceControl>(BoundsChanged);
-        StretchProperty.Changed.AddClassHandler<SurfaceControl>(StretchChanged);
         WidthProperty.Changed.AddClassHandler<SurfaceControl>(BoundsChanged);
         HeightProperty.Changed.AddClassHandler<SurfaceControl>(BoundsChanged);
+        SurfaceProperty.Changed.AddClassHandler<SurfaceControl>(Rerender);
+        StretchProperty.Changed.AddClassHandler<SurfaceControl>(Rerender);
     }
 
-    public override void Render(DrawingContext context)
+    public SurfaceControl()
     {
-        if (Surface == null)
-        {
-            return;
-        }
-
-        context.Custom(_drawingSurfaceOp);
+        ClipToBounds = true;
     }
 
-    private static void StretchChanged(SurfaceControl sender, AvaloniaPropertyChangedEventArgs e)
+    protected override void OnOpenGlInit(GlInterface gl)
     {
-        if (e.NewValue is Stretch stretch)
-        {
-            sender._drawingSurfaceOp = new DrawingSurfaceOp(sender.Surface, sender.Bounds, stretch);
-        }
+        gr = GRContext.CreateGl(GRGlInterface.Create(gl.GetProcAddress));
+        CreateWorkingSurface();
     }
 
-    private static void BoundsChanged(SurfaceControl sender, AvaloniaPropertyChangedEventArgs e)
+    private void CreateWorkingSurface()
     {
-        if (e.NewValue is Rect bounds)
-        {
-            sender._drawingSurfaceOp = new DrawingSurfaceOp(sender.Surface, bounds, sender.Stretch);
-        }
+        if (gr == null) return;
+
+        _workingSurface?.Dispose();
+        GRGlFramebufferInfo frameBuffer = new GRGlFramebufferInfo(0, SKColorType.Rgba8888.ToGlSizedFormat());
+        GRBackendRenderTarget desc = new GRBackendRenderTarget((int)Bounds.Width, (int)Bounds.Height, 4, 0, frameBuffer);
+        _workingSurface = SKSurface.Create(gr, desc, GRSurfaceOrigin.BottomLeft, SKImageInfo.PlatformColorType);
     }
 
-    private static void OnSurfaceChanged(SurfaceControl sender, AvaloniaPropertyChangedEventArgs e)
+    protected override void OnOpenGlRender(GlInterface gl, int fb)
     {
-        if (e.NewValue is Surface surface)
+        // TODO: draw only when needed
+        if (Surface == null)
         {
-            sender._drawingSurfaceOp = new DrawingSurfaceOp(surface, sender.Bounds, sender.Stretch);
+            return;
         }
-    }
-}
 
-public class DrawingSurfaceOp : ICustomDrawOperation
-{
-    public Rect Bounds { get;  }
-    public Surface Surface { get; }
-    public Stretch Stretch { get; set; }
-
-    private SKPaint _paint = new SKPaint();
+        SKCanvas canvas = _workingSurface.Canvas;
+        canvas.Save();
+        canvas.ClipRect(new SKRect(0, 0, (float)Bounds.Width, (float)Bounds.Height));
+        ScaleCanvas(canvas);
+        canvas.Clear(SKColors.Transparent);
 
-    //TODO: Implement dirty rect handling
-    /*private RectI? _lastDirtyRect;
-    private SKImage? _lastImage;
-    private SKImage? _lastFullImage;*/
+        canvas.DrawSurface((SKSurface)Surface.DrawingSurface.Native, new SKPoint(0, 0), _paint);
 
-    public DrawingSurfaceOp(Surface surface, Rect bounds, Stretch stretch)
-    {
-        Surface = surface;
-        Bounds = bounds;
-        Stretch = stretch;
-    }
+        canvas.Restore();
 
-    public void Render(ImmediateDrawingContext context)
-    {
-        if (context.TryGetFeature(out ISkiaSharpApiLeaseFeature skiaSurface))
-        {
-            using var lease = skiaSurface.Lease();
-            var canvas = lease.SkCanvas;
-            canvas.Save();
-
-            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();
-        }
+        canvas.Flush();
+        RequestNextFrameRendering();
     }
 
     private void ScaleCanvas(SKCanvas canvas)
@@ -166,18 +116,16 @@ public class DrawingSurfaceOp : ICustomDrawOperation
         }
     }
 
-    public bool HitTest(Point p)
-    {
-        return false;
-    }
-
-    public bool Equals(ICustomDrawOperation? other)
+    private static void BoundsChanged(SurfaceControl sender, AvaloniaPropertyChangedEventArgs e)
     {
-        return other is DrawingSurfaceOp op && op.Surface == Surface;
+        if (e.NewValue is Rect bounds)
+        {
+            sender.CreateWorkingSurface();
+        }
     }
 
-    public void Dispose()
+    private static void Rerender(SurfaceControl sender, AvaloniaPropertyChangedEventArgs e)
     {
-
+        sender.RequestNextFrameRendering();
     }
 }

+ 2 - 0
src/PixiEditor.DrawingApi.Core/Bridge/NativeObjectsImpl/IBitmapImplementation.cs

@@ -1,5 +1,6 @@
 using System;
 using PixiEditor.DrawingApi.Core.Surface;
+using PixiEditor.DrawingApi.Core.Surface.ImageData;
 
 namespace PixiEditor.DrawingApi.Core.Bridge.NativeObjectsImpl;
 
@@ -8,4 +9,5 @@ public interface IBitmapImplementation
     public void Dispose(IntPtr objectPointer);
     public Bitmap Decode(ReadOnlySpan<byte> buffer);
     public object GetNativeBitmap(IntPtr objectPointer);
+    public Bitmap FromImage(IntPtr snapshot);
 }

+ 6 - 0
src/PixiEditor.DrawingApi.Core/Surface/Bitmap.cs

@@ -1,5 +1,6 @@
 using System;
 using PixiEditor.DrawingApi.Core.Bridge;
+using PixiEditor.DrawingApi.Core.Surface.ImageData;
 
 namespace PixiEditor.DrawingApi.Core.Surface;
 
@@ -20,4 +21,9 @@ public class Bitmap : NativeObject
     {
         return DrawingBackendApi.Current.BitmapImplementation.Decode(buffer);
     }
+
+    public static Bitmap FromImage(Image snapshot)
+    {
+        return DrawingBackendApi.Current.BitmapImplementation.FromImage(snapshot.ObjectPointer);
+    }
 }

+ 15 - 0
src/PixiEditor.DrawingApi.Skia/Implementations/SkiaBitmapImplementation.cs

@@ -1,12 +1,19 @@
 using System;
 using PixiEditor.DrawingApi.Core.Bridge.NativeObjectsImpl;
 using PixiEditor.DrawingApi.Core.Surface;
+using PixiEditor.DrawingApi.Core.Surface.ImageData;
 using SkiaSharp;
 
 namespace PixiEditor.DrawingApi.Skia.Implementations
 {
     public class SkiaBitmapImplementation : SkObjectImplementation<SKBitmap>, IBitmapImplementation
     {
+        public SkiaImageImplementation ImageImplementation { get; }
+        public SkiaBitmapImplementation(SkiaImageImplementation imgImpl)
+        {
+            ImageImplementation = imgImpl;
+        }
+
         public void Dispose(IntPtr objectPointer)
         {
             SKBitmap bitmap = ManagedInstances[objectPointer];
@@ -22,6 +29,14 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
             return new Bitmap(skBitmap.Handle);
         }
 
+        public Bitmap FromImage(IntPtr ptr)
+        {
+            SKImage image = ImageImplementation.ManagedInstances[ptr];
+            SKBitmap skBitmap = SKBitmap.FromImage(image);
+            ManagedInstances[skBitmap.Handle] = skBitmap;
+            return new Bitmap(skBitmap.Handle);
+        }
+
         public object GetNativeBitmap(IntPtr objectPointer)
         {
             return ManagedInstances[objectPointer];

+ 1 - 1
src/PixiEditor.DrawingApi.Skia/SkiaDrawingBackend.cs

@@ -47,7 +47,7 @@ namespace PixiEditor.DrawingApi.Skia
             SkiaPixmapImplementation pixmapImpl = new SkiaPixmapImplementation(colorSpaceImpl);
             PixmapImplementation = pixmapImpl;
             
-            SkiaBitmapImplementation bitmapImpl = new SkiaBitmapImplementation();
+            SkiaBitmapImplementation bitmapImpl = new SkiaBitmapImplementation(imgImpl);
             BitmapImplementation = bitmapImpl;
             
             SkiaCanvasImplementation canvasImpl = new SkiaCanvasImplementation(paintImpl, imgImpl, bitmapImpl, pathImpl);

+ 2 - 0
src/PixiEditor.Zoombox/Zoombox.axaml.cs

@@ -121,6 +121,7 @@ public partial class Zoombox : UserControl, INotifyPropertyChanged
         remove => RemoveHandler(ViewportMovedEvent, value);
     }
 
+    public VecD CanvasPos => ToScreenSpace(VecD.Zero);
     public double CanvasX => ToScreenSpace(VecD.Zero).X;
     public double CanvasY => ToScreenSpace(VecD.Zero).Y;
 
@@ -465,6 +466,7 @@ public partial class Zoombox : UserControl, INotifyPropertyChanged
         zoombox.PropertyChanged?.Invoke(zoombox, new(nameof(zoombox.FlipTransformY)));
         zoombox.PropertyChanged?.Invoke(zoombox, new(nameof(zoombox.CanvasX)));
         zoombox.PropertyChanged?.Invoke(zoombox, new(nameof(zoombox.CanvasY)));
+        zoombox.PropertyChanged?.Invoke(zoombox, new(nameof(zoombox.CanvasPos)));
         zoombox.RaiseViewportEvent();
     }