Browse Source

Merge pull request #925 from PixiEditor/fixes/chunk-resolution

Layers rendering code fixes, actual rendering tests and performance improvements
Krzysztof Krysiński 3 months ago
parent
commit
44251a7317
54 changed files with 593 additions and 262 deletions
  1. 62 63
      pipelines/Windows/tests-windows.yml
  2. 1 1
      src/Drawie
  3. 1 1
      src/PixiDocks
  4. 38 6
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FolderNode.cs
  5. 8 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs
  6. 53 16
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/LayerNode.cs
  7. 31 18
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs
  8. 5 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/VectorLayerNode.cs
  9. BIN
      src/PixiEditor.Extensions.Sdk/build/PixiEditor.Api.CGlueMSBuild.dll
  10. 12 0
      src/PixiEditor.Platform/IPlatform.cs
  11. 0 2
      src/PixiEditor/Helpers/Extensions/ApplicationExtensions.cs
  12. 2 5
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/SimpleShapeToolExecutor.cs
  13. 16 6
      src/PixiEditor/Models/Rendering/SceneRenderer.cs
  14. 2 0
      src/PixiEditor/Properties/AssemblyInfo.cs
  15. 1 0
      src/PixiEditor/ViewModels/Document/DocumentViewModel.cs
  16. 7 0
      src/PixiEditor/ViewModels/Menu/MenuBarViewModel.cs
  17. 22 14
      src/PixiEditor/ViewModels/SubViewModels/ClipboardViewModel.cs
  18. 1 0
      tests/ChunkyImageLibTest/ChunkyImageLibTest.csproj
  19. 2 12
      tests/ChunkyImageLibTest/ChunkyImageTests.cs
  20. 2 9
      tests/ChunkyImageLibTest/ImageOperationTests.cs
  21. 3 11
      tests/ChunkyImageLibTest/RectangleOperationTests.cs
  22. 11 0
      tests/ChunkyImageLibTest/TestRunnerSetup.cs
  23. 2 3
      tests/PixiEditor.Backend.Tests/NodeSystemTests.cs
  24. 1 0
      tests/PixiEditor.Backend.Tests/PixiEditor.Backend.Tests.csproj
  25. 10 72
      tests/PixiEditor.Tests/AvaloniaTestRunner.cs
  26. 12 0
      tests/PixiEditor.Tests/PixiEditor.Tests.csproj
  27. 116 2
      tests/PixiEditor.Tests/PixiEditorTest.cs
  28. 136 0
      tests/PixiEditor.Tests/RenderTests.cs
  29. 1 12
      tests/PixiEditor.Tests/SerializationTests.cs
  30. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/Fibi.pixi
  31. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/Fibi.png
  32. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/Pond.pixi
  33. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/Pond.png
  34. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/SmallPixelArtCircleShadow.pixi
  35. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/SmallPixelArtCircleShadow.png
  36. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/SmlPxlCircShadWithMask.pixi
  37. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/SmlPxlCircShadWithMask.png
  38. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/SmlPxlCircShadWithMaskClipped.pixi
  39. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/SmlPxlCircShadWithMaskClipped.png
  40. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/SmlPxlCircShadWithMaskClippedInFolder.pixi
  41. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/SmlPxlCircShadWithMaskClippedInFolder.png
  42. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/VectorRectangleClippedToCircle.pixi
  43. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/VectorRectangleClippedToCircle.png
  44. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/VectorRectangleClippedToCircleMasked.pixi
  45. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/VectorRectangleClippedToCircleMasked.png
  46. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/VectorRectangleClippedToCircleShadowFilter.pixi
  47. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/VectorRectangleClippedToCircleShadowFilter.png
  48. BIN
      tests/PixiEditor.Tests/TestFiles/ResolutionTests/LayerWithMaskClipped.pixi
  49. BIN
      tests/PixiEditor.Tests/TestFiles/ResolutionTests/LayerWithMaskClippedHighDpiPresent.pixi
  50. BIN
      tests/PixiEditor.Tests/TestFiles/ResolutionTests/LayerWithMaskClippedInFolder.pixi
  51. BIN
      tests/PixiEditor.Tests/TestFiles/ResolutionTests/LayerWithMaskClippedInFolderWithMask.pixi
  52. BIN
      tests/PixiEditor.Tests/TestFiles/ResolutionTests/SingleLayer.pixi
  53. BIN
      tests/PixiEditor.Tests/TestFiles/ResolutionTests/SingleLayerWithMask.pixi
  54. 35 7
      tests/PixiEditorTests.sln

+ 62 - 63
pipelines/Windows/tests-windows.yml

@@ -1,7 +1,7 @@
 trigger:
-- development
-- master
-- 2.0-cicd
+  - development
+  - master
+  - 2.0-cicd
 
 pool:
   vmImage: 'windows-latest'
@@ -14,73 +14,72 @@ variables:
   wasiUrl: 'https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-24/$(wasiVer).tar.gz'
 
 steps:
-- task: UseDotNet@2
-  displayName: 'Install .NET SDK'
-  inputs:
-    packageType: 'sdk'
-    version: '$(dotnetVersion)'
-    
-- task: CmdLine@2
-  displayName: 'Download WASI SDK'
-  inputs:
-    script: |
-      curl -L -o $(wasiVer).tar.gz $(wasiUrl)
+  - task: UseDotNet@2
+    displayName: 'Install .NET SDK'
+    inputs:
+      packageType: 'sdk'
+      version: '$(dotnetVersion)'
 
-- task: CmdLine@2
-  displayName: 'Unpack WASI SDK'
-  inputs:
-    script: |
-      tar -xzf $(wasiVer).tar.gz
-      echo "Contents of directory after extraction:"
-      dir $(wasiVer)
+  - task: CmdLine@2
+    displayName: 'Download WASI SDK'
+    inputs:
+      script: |
+        curl -L -o $(wasiVer).tar.gz $(wasiUrl)
 
-- task: PowerShell@2
-  displayName: 'Set Environment Path for WASI SDK'
-  inputs:
-    targetType: 'inline'
-    script: |
-      $env:WASI_SDK_PATH = "$(Get-Location)\$(wasiVer)"
-      Write-Host "##vso[task.setvariable variable=WASI_SDK_PATH]$env:WASI_SDK_PATH"
+  - task: CmdLine@2
+    displayName: 'Unpack WASI SDK'
+    inputs:
+      script: |
+        tar -xzf $(wasiVer).tar.gz
+        echo "Contents of directory after extraction:"
+        dir $(wasiVer)
 
-- task: PowerShell@2
-  displayName: 'Verify Environment Path'
-  inputs:
-    targetType: 'inline'
-    script: |
-      Write-Host "Environment path set to: $env:WASI_SDK_PATH"
+  - task: PowerShell@2
+    displayName: 'Set Environment Path for WASI SDK'
+    inputs:
+      targetType: 'inline'
+      script: |
+        $env:WASI_SDK_PATH = "$(Get-Location)\$(wasiVer)"
+        Write-Host "##vso[task.setvariable variable=WASI_SDK_PATH]$env:WASI_SDK_PATH"
 
+  - task: PowerShell@2
+    displayName: 'Verify Environment Path'
+    inputs:
+      targetType: 'inline'
+      script: |
+        Write-Host "Environment path set to: $env:WASI_SDK_PATH"
 
-- task: NuGetToolInstaller@1
+  - task: NuGetToolInstaller@1
 
-- task: DotNetCoreCLI@2
-  displayName: Install wasi-wasm
-  inputs:
-    command: 'custom'
-    custom: 'workload'
-    arguments: 'install wasi-experimental'
+  - task: DotNetCoreCLI@2
+    displayName: Install wasi-wasm
+    inputs:
+      command: 'custom'
+      custom: 'workload'
+      arguments: 'install wasi-experimental'
 
-- task: DotNetCoreCLI@2
-  displayName: Install wasm-tools
-  inputs:
-    command: 'custom'
-    custom: 'workload'
-    arguments: 'install wasm-tools'
+  - task: DotNetCoreCLI@2
+    displayName: Install wasm-tools
+    inputs:
+      command: 'custom'
+      custom: 'workload'
+      arguments: 'install wasm-tools'
 
-- task: NuGetCommand@2
-  displayName: 'Restore solution'
-  inputs:
-    restoreSolution: '$(solution)'
+  - task: NuGetCommand@2
+    displayName: 'Restore solution'
+    inputs:
+      restoreSolution: '$(solution)'
 
-- task: DotNetCoreCLI@2
-  displayName: Build
-  inputs:
-    command: 'build'
-    projects: '**/*.csproj'
-    arguments: '--configuration Release -r $(buildPlatform)'
+  - task: DotNetCoreCLI@2
+    displayName: Build
+    inputs:
+      command: 'build'
+      projects: '**/*.csproj'
+      arguments: '--configuration Release -r $(buildPlatform)'
 
-- task: DotNetCoreCLI@2
-  displayName: Tests
-  inputs:
-    command: test
-    projects: '**/*Tests/*.csproj'
-    arguments: '--configuration $(buildConfiguration) -r $(buildPlatform)'
+  - task: DotNetCoreCLI@2
+    displayName: Tests
+    inputs:
+      command: test
+      projects: '**/*Tests/*.csproj'
+      arguments: '--configuration $(buildConfiguration) -r $(buildPlatform)'

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 0cdc4c6e877d730774a3d9c99880bc2013a4b0c6
+Subproject commit fb19e6a170698609840b6602178a26da2fc289b7

+ 1 - 1
src/PixiDocks

@@ -1 +1 @@
-Subproject commit 30a62cf6fe0ff3e9517a2bbcd38a423b5194e2ff
+Subproject commit 8c9a1f874ec8d1f7cd773a1f6b044deb0343ad1a

+ 38 - 6
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FolderNode.cs

@@ -11,7 +11,7 @@ using Drawie.Numerics;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
 [NodeInfo("Folder")]
-public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource, IPreviewRenderable
+public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource
 {
     public const string ContentInternalName = "Content";
     private VecI documentSize;
@@ -25,16 +25,16 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource, IPrev
 
     public override Node CreateCopy() => new FolderNode
     {
-        MemberName = MemberName, 
+        MemberName = MemberName,
         ClipToPreviousMember = this.ClipToPreviousMember,
         EmbeddedMask = this.EmbeddedMask?.CloneFromCommitted()
     };
 
     public override VecD GetScenePosition(KeyFrameTime time) =>
-        documentSize / 2f; 
+        documentSize / 2f;
 
     public override VecD GetSceneSize(KeyFrameTime time) =>
-        documentSize; 
+        documentSize;
 
     protected override void OnExecute(RenderContext context)
     {
@@ -99,7 +99,7 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource, IPrev
 
         Content.Value?.Paint(sceneContext, outputWorkingSurface.DrawingSurface);
 
-        ApplyMaskIfPresent(outputWorkingSurface.DrawingSurface, sceneContext);
+        ApplyMaskIfPresent(outputWorkingSurface.DrawingSurface, sceneContext, sceneContext.ChunkResolution);
 
         if (Background.Value != null && sceneContext.TargetPropertyOutput != RawOutput)
         {
@@ -172,6 +172,38 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource, IPrev
         return null;
     }
 
+    public override RectD? GetApproxBounds(KeyFrameTime frameTime)
+    {
+        RectD? bounds = null;
+        if (Content.Connection != null)
+        {
+            Content.Connection.Node.TraverseBackwards((n) =>
+            {
+                if (n is StructureNode structureNode)
+                {
+                    RectD? childBounds = structureNode.GetApproxBounds(frameTime);
+                    if (childBounds != null)
+                    {
+                        if (bounds == null)
+                        {
+                            bounds = childBounds;
+                        }
+                        else
+                        {
+                            bounds = bounds.Value.Union(childBounds.Value);
+                        }
+                    }
+                }
+
+                return true;
+            });
+
+            return bounds ?? RectD.Empty;
+        }
+
+        return null;
+    }
+
     public HashSet<Guid> GetLayerNodeGuids()
     {
         HashSet<Guid> guids = new();
@@ -195,7 +227,7 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource, IPrev
             return base.GetPreviewBounds(frame, elementFor);
         }
 
-        return GetTightBounds(frame);
+        return GetApproxBounds(frame);
     }
 
     public override bool RenderPreview(DrawingSurface renderOn, RenderContext context,

+ 8 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs

@@ -47,6 +47,11 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
         return (RectD?)GetLayerImageAtFrame(frameTime.Frame).FindTightCommittedBounds();
     }
 
+    public override RectD? GetApproxBounds(KeyFrameTime frameTime)
+    {
+        return (RectD?)GetLayerImageAtFrame(frameTime.Frame).FindChunkAlignedCommittedBounds();
+    }
+
     protected internal override void DrawLayerInScene(SceneObjectRenderContext ctx,
         DrawingSurface workingSurface,
         bool useFilters = true)
@@ -62,11 +67,12 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 
     protected internal override void DrawLayerOnTexture(SceneObjectRenderContext ctx,
         DrawingSurface workingSurface,
+        ChunkResolution resolution,
         bool useFilters, Paint paint)
     {
         int scaled = workingSurface.Canvas.Save();
-        workingSurface.Canvas.Translate(GetScenePosition(ctx.FrameTime) * ctx.ChunkResolution.Multiplier());
-        workingSurface.Canvas.Scale((float)ctx.ChunkResolution.Multiplier());
+        workingSurface.Canvas.Translate(GetScenePosition(ctx.FrameTime) * resolution.Multiplier());
+        workingSurface.Canvas.Scale((float)resolution.Multiplier());
 
         DrawLayerOnto(ctx, workingSurface, useFilters, paint);
 

+ 53 - 16
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/LayerNode.cs

@@ -44,7 +44,7 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
                 blendPaint.BlendMode = RenderContext.GetDrawingBlendMode(BlendMode.Value);
             }
 
-            if (AllowHighDpiRendering)
+            if (AllowHighDpiRendering || renderOnto.DeviceClipBounds.Size == context.DocumentSize)
             {
                 DrawLayerInScene(context, renderOnto, useFilters);
             }
@@ -56,38 +56,59 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
                     BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.SrcOver
                 };
 
-                using var tempSurface = Texture.ForProcessing(context.DocumentSize, context.ProcessingColorSpace);
-                DrawLayerOnTexture(context, tempSurface.DrawingSurface, useFilters, targetPaint);
+                var tempSurface = TryInitWorkingSurface(context.DocumentSize, context.ChunkResolution,
+                    context.ProcessingColorSpace, 22);
+
+                DrawLayerOnTexture(context, tempSurface.DrawingSurface, context.ChunkResolution, useFilters, targetPaint);
 
-                renderOnto.Canvas.DrawSurface(tempSurface.DrawingSurface, 0, 0, blendPaint);
+                blendPaint.SetFilters(null);
+                DrawWithResolution(tempSurface.DrawingSurface, renderOnto, context.ChunkResolution);
             }
 
             return;
         }
 
-        VecI size = renderOnto.DeviceClipBounds.Size + renderOnto.DeviceClipBounds.Pos;
+        VecI size = AllowHighDpiRendering
+            ? renderOnto.DeviceClipBounds.Size + renderOnto.DeviceClipBounds.Pos
+            : context.DocumentSize;
         int saved = renderOnto.Canvas.Save();
 
+        var adjustedResolution = AllowHighDpiRendering ? ChunkResolution.Full : context.ChunkResolution;
+
         var outputWorkingSurface =
-            TryInitWorkingSurface(size, context.ChunkResolution, context.ProcessingColorSpace, 1);
+            TryInitWorkingSurface(size, adjustedResolution, context.ProcessingColorSpace, 1);
         outputWorkingSurface.DrawingSurface.Canvas.Clear();
         outputWorkingSurface.DrawingSurface.Canvas.Save();
-        outputWorkingSurface.DrawingSurface.Canvas.SetMatrix(renderOnto.Canvas.TotalMatrix);
+        if (AllowHighDpiRendering)
+        {
+            outputWorkingSurface.DrawingSurface.Canvas.SetMatrix(renderOnto.Canvas.TotalMatrix);
+            renderOnto.Canvas.SetMatrix(Matrix3X3.Identity);
+        }
 
-        renderOnto.Canvas.SetMatrix(Matrix3X3.Identity);
+        using var paint = new Paint
+        {
+            Color = new Color(255, 255, 255, 255), BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.SrcOver
+        };
 
-        DrawLayerOnTexture(context, outputWorkingSurface.DrawingSurface, useFilters, blendPaint);
+        DrawLayerOnTexture(context, outputWorkingSurface.DrawingSurface, adjustedResolution, false, paint);
 
-        ApplyMaskIfPresent(outputWorkingSurface.DrawingSurface, context);
+        ApplyMaskIfPresent(outputWorkingSurface.DrawingSurface, context, adjustedResolution);
 
         if (Background.Value != null)
         {
-            Texture tempSurface = TryInitWorkingSurface(size, context.ChunkResolution, context.ProcessingColorSpace, 4);
+            Texture tempSurface = TryInitWorkingSurface(size, adjustedResolution, context.ProcessingColorSpace, 4);
 
             tempSurface.DrawingSurface.Canvas.Save();
-            tempSurface.DrawingSurface.Canvas.SetMatrix(outputWorkingSurface.DrawingSurface.Canvas.TotalMatrix);
-
-            outputWorkingSurface.DrawingSurface.Canvas.SetMatrix(Matrix3X3.Identity);
+            if (AllowHighDpiRendering)
+            {
+                tempSurface.DrawingSurface.Canvas.SetMatrix(outputWorkingSurface.DrawingSurface.Canvas.TotalMatrix);
+                outputWorkingSurface.DrawingSurface.Canvas.SetMatrix(Matrix3X3.Identity);
+            }
+            else
+            {
+                tempSurface.DrawingSurface.Canvas.Scale(
+                    (float)context.ChunkResolution.Multiplier());
+            }
 
             tempSurface.DrawingSurface.Canvas.Clear();
             if (Background.Connection is { Node: IClipSource clipSource } && ClipToPreviousMember)
@@ -99,7 +120,16 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
         }
 
         blendPaint.BlendMode = RenderContext.GetDrawingBlendMode(BlendMode.Value);
-        DrawWithResolution(outputWorkingSurface.DrawingSurface, renderOnto, context.ChunkResolution);
+        if (useFilters)
+        {
+            blendPaint.SetFilters(Filters.Value);
+        }
+        else
+        {
+            blendPaint.SetFilters(null);
+        }
+
+        DrawWithResolution(outputWorkingSurface.DrawingSurface, renderOnto, adjustedResolution);
 
         renderOnto.Canvas.RestoreToCount(saved);
         outputWorkingSurface.DrawingSurface.Canvas.Restore();
@@ -107,10 +137,11 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
 
     protected internal virtual void DrawLayerOnTexture(SceneObjectRenderContext ctx,
         DrawingSurface workingSurface,
+        ChunkResolution resolution,
         bool useFilters, Paint paint)
     {
         int scaled = workingSurface.Canvas.Save();
-        workingSurface.Canvas.Scale((float)ctx.ChunkResolution.Multiplier());
+        workingSurface.Canvas.Scale((float)resolution.Multiplier());
 
         DrawLayerOnto(ctx, workingSurface, useFilters, paint);
 
@@ -198,9 +229,15 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
 
         if (!hasSurface || workingSurface.Size != targetSize || workingSurface.IsDisposed)
         {
+            workingSurface?.Dispose();
             workingSurfaces[(targetResolution, id)] = Texture.ForProcessing(targetSize, processingCs);
             workingSurface = workingSurfaces[(targetResolution, id)];
         }
+        else
+        {
+            workingSurface.DrawingSurface.Canvas.SetMatrix(Matrix3X3.Identity);
+            workingSurface.DrawingSurface.Canvas.Clear();
+        }
 
         return workingSurface;
     }

+ 31 - 18
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs

@@ -47,13 +47,15 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
     public ChunkyImage? EmbeddedMask { get; set; }
 
     protected Texture renderedMask;
-    protected static readonly Paint replacePaint = new Paint() { BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.Src };
+
+    protected static readonly Paint replacePaint =
+        new Paint() { BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.Src };
 
     protected static readonly Paint clearPaint = new Paint()
     {
         BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.Src, Color = Colors.Transparent
     };
-    
+
     protected static readonly Paint clipPaint = new Paint()
     {
         BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.DstIn
@@ -70,12 +72,16 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
         set => DisplayName = value;
     }
 
-    protected Paint maskPaint = new Paint() { BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.DstIn, ColorFilter = Nodes.Filters.MaskFilter };
+    protected Paint maskPaint = new Paint()
+    {
+        BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.DstIn, ColorFilter = Nodes.Filters.MaskFilter
+    };
+
     protected Paint blendPaint = new Paint() { BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.SrcOver };
 
     protected Paint maskPreviewPaint = new Paint()
     {
-        BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.SrcOver, 
+        BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.SrcOver,
         ColorFilter = ColorFilter.CreateCompose(Nodes.Filters.AlphaGrayscaleFilter, Nodes.Filters.MaskFilter)
     };
 
@@ -141,22 +147,22 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
     {
         RenderForOutput(context, renderTarget, FilterlessOutput);
     }
-    
+
     private void OnRawPaint(RenderContext context, DrawingSurface renderTarget)
     {
         RenderForOutput(context, renderTarget, RawOutput);
     }
-    
+
     public abstract VecD GetScenePosition(KeyFrameTime frameTime);
     public abstract VecD GetSceneSize(KeyFrameTime frameTime);
 
     public void RenderForOutput(RenderContext context, DrawingSurface renderTarget, RenderOutputProperty output)
     {
-        if(IsDisposed)
+        if (IsDisposed)
         {
             return;
         }
-        
+
         var renderObjectContext = CreateSceneContext(context, renderTarget, output);
 
         int renderSaved = renderTarget.Canvas.Save();
@@ -185,16 +191,16 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
 
     public abstract void Render(SceneObjectRenderContext sceneContext);
 
-    protected void ApplyMaskIfPresent(DrawingSurface surface, RenderContext context)
+    protected void ApplyMaskIfPresent(DrawingSurface surface, RenderContext context, ChunkResolution renderResolution)
     {
         if (MaskIsVisible.Value)
         {
             if (CustomMask.Value != null)
             {
                 int layer = surface.Canvas.SaveLayer(maskPaint);
-                surface.Canvas.Scale((float)context.ChunkResolution.Multiplier());
+                surface.Canvas.Scale((float)renderResolution.Multiplier());
                 CustomMask.Value.Paint(context, surface);
-                
+
                 surface.Canvas.RestoreToCount(layer);
             }
             else if (EmbeddedMask != null)
@@ -206,9 +212,12 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
                         ChunkResolution.Full,
                         surface, VecI.Zero, maskPaint);
                 }
-                else if(renderedMask != null)
+                else if (renderedMask != null)
                 {
+                    int saved = surface.Canvas.Save();
+                    surface.Canvas.Scale((float)renderResolution.Multiplier());
                     surface.Canvas.DrawSurface(renderedMask.DrawingSurface, 0, 0, maskPaint);
+                    surface.Canvas.RestoreToCount(saved);
                 }
             }
         }
@@ -216,10 +225,12 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
 
     protected override int GetContentCacheHash()
     {
-        return HashCode.Combine(base.GetContentCacheHash(), EmbeddedMask?.GetCacheHash() ?? 0, ClipToPreviousMember ? 1 : 0);
+        return HashCode.Combine(base.GetContentCacheHash(), EmbeddedMask?.GetCacheHash() ?? 0,
+            ClipToPreviousMember ? 1 : 0);
     }
 
-    public virtual void RenderChunk(VecI chunkPos, ChunkResolution resolution, KeyFrameTime frameTime, ColorSpace processingColorSpace)
+    public virtual void RenderChunk(VecI chunkPos, ChunkResolution resolution, KeyFrameTime frameTime,
+        ColorSpace processingColorSpace)
     {
         RenderChunkyImageChunk(chunkPos, resolution, EmbeddedMask, 55, processingColorSpace, ref renderedMask);
     }
@@ -234,11 +245,11 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
         }
 
         VecI targetSize = img.LatestSize;
-        
+
         renderSurface = RequestTexture(textureId, targetSize, processingColorSpace, false);
 
         int saved = renderSurface.DrawingSurface.Canvas.Save();
-        
+
         if (!img.DrawMostUpToDateChunkOn(
                 chunkPos,
                 ChunkResolution.Full,
@@ -250,7 +261,7 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
             renderSurface.DrawingSurface.Canvas.DrawRect(new RectD(chunkPos * chunkSize, new VecD(chunkSize)),
                 clearPaint);
         }
-        
+
         renderSurface.DrawingSurface.Canvas.RestoreToCount(saved);
     }
 
@@ -279,6 +290,7 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
     }
 
     public abstract RectD? GetTightBounds(KeyFrameTime frameTime);
+    public abstract RectD? GetApproxBounds(KeyFrameTime frameTime);
 
     public override void SerializeAdditionalData(Dictionary<string, object> additionalData)
     {
@@ -287,6 +299,7 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
         {
             additionalData["embeddedMask"] = EmbeddedMask;
         }
+
         if (ClipToPreviousMember)
         {
             additionalData["clipToPreviousMember"] = ClipToPreviousMember;
@@ -308,7 +321,7 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
 
             infos.Add(new StructureMemberMask_ChangeInfo(Id, mask != null));
         }
-        
+
         if (data.TryGetValue("clipToPreviousMember", out var clip))
         {
             ClipToPreviousMember = (bool)clip;

+ 5 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/VectorLayerNode.cs

@@ -134,6 +134,11 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
         return true;
     }
 
+    public override RectD? GetApproxBounds(KeyFrameTime frameTime)
+    {
+        return GetTightBounds(frameTime);
+    }
+
     public override void SerializeAdditionalData(Dictionary<string, object> additionalData)
     {
         base.SerializeAdditionalData(additionalData);

BIN
src/PixiEditor.Extensions.Sdk/build/PixiEditor.Api.CGlueMSBuild.dll


+ 12 - 0
src/PixiEditor.Platform/IPlatform.cs

@@ -19,3 +19,15 @@ public interface IPlatform
         Current = platform;
     }
 }
+
+public class NullAdditionalContentProvider : IAdditionalContentProvider
+{
+    public bool IsContentInstalled(AdditionalContentProduct product) => false;
+    public bool PlatformHasContent(AdditionalContentProduct product)
+    {
+        return false;
+    }
+
+    public void InstallContent(AdditionalContentProduct product) { }
+    public void UninstallContent(AdditionalContentProduct product) { }
+}

+ 0 - 2
src/PixiEditor/Helpers/Extensions/ApplicationExtensions.cs

@@ -14,8 +14,6 @@ public static class ApplicationExtensions
             case IClassicDesktopStyleApplicationLifetime desktop:
                 action(desktop.MainWindow);
                 break;
-            default:
-                throw new NotSupportedException("ApplicationLifetime is not supported");
         }
     }
 

+ 2 - 5
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/SimpleShapeToolExecutor.cs

@@ -73,11 +73,8 @@ internal abstract class SimpleShapeToolExecutor : UpdateableChangeExecutor,
             ActiveMode = ShapeToolMode.Preview;
         }
 
-        if (controller.LeftMousePressed)
-        {
-            restoreSnapping?.Dispose();
-            restoreSnapping = DisableSelfSnapping(memberId, document);
-        }
+        restoreSnapping?.Dispose();
+        restoreSnapping = DisableSelfSnapping(memberId, document);
 
         return ExecutionState.Success;
     }

+ 16 - 6
src/PixiEditor/Models/Rendering/SceneRenderer.cs

@@ -27,6 +27,8 @@ internal class SceneRenderer : IDisposable
     private KeyFrameTime lastFrameTime;
     private Dictionary<Guid, bool> lastFramesVisibility = new();
 
+    private ChunkResolution? lastResolution;
+
     public SceneRenderer(IReadOnlyDocument trackerDocument, IDocument documentViewModel)
     {
         Document = trackerDocument;
@@ -35,7 +37,7 @@ internal class SceneRenderer : IDisposable
 
     public void RenderScene(DrawingSurface target, ChunkResolution resolution, string? targetOutput = null)
     {
-        if (Document.Renderer.IsBusy || DocumentViewModel.Busy) return;
+        if (Document.Renderer.IsBusy || DocumentViewModel.Busy || target.DeviceClipBounds.Size.ShortestAxis <= 0) return;
         RenderOnionSkin(target, resolution, targetOutput);
 
         string adjustedTargetOutput = targetOutput ?? "";
@@ -63,7 +65,8 @@ internal class SceneRenderer : IDisposable
         }
     }
 
-    private Texture RenderGraph(DrawingSurface target, ChunkResolution resolution, string? targetOutput, IReadOnlyNodeGraph finalGraph)
+    private Texture RenderGraph(DrawingSurface target, ChunkResolution resolution, string? targetOutput,
+        IReadOnlyNodeGraph finalGraph)
     {
         DrawingSurface renderTarget = target;
         Texture? renderTexture = null;
@@ -119,6 +122,12 @@ internal class SceneRenderer : IDisposable
             return true;
         }
 
+        if (lastResolution != resolution)
+        {
+            lastResolution = resolution;
+            return true;
+        }
+
         if (lastHighResRendering != HighResRendering)
         {
             lastHighResRendering = HighResRendering;
@@ -133,7 +142,7 @@ internal class SceneRenderer : IDisposable
             return true;
         }
 
-        if(lastFrameTime.Frame != DocumentViewModel.AnimationHandler.ActiveFrameTime.Frame)
+        if (lastFrameTime.Frame != DocumentViewModel.AnimationHandler.ActiveFrameTime.Frame)
         {
             lastFrameTime = DocumentViewModel.AnimationHandler.ActiveFrameTime;
             return true;
@@ -158,8 +167,10 @@ internal class SceneRenderer : IDisposable
 
         if (!renderInDocumentSize)
         {
-            double lengthDiff = target.LocalClipBounds.Size.Length - cachedTexture.DrawingSurface.LocalClipBounds.Size.Length;
-            if (lengthDiff > 0 || target.LocalClipBounds.Pos != cachedTexture.DrawingSurface.LocalClipBounds.Pos || lengthDiff < -ZoomDiffToRerender)
+            double lengthDiff = target.LocalClipBounds.Size.Length -
+                                cachedTexture.DrawingSurface.LocalClipBounds.Size.Length;
+            if (lengthDiff > 0 || target.LocalClipBounds.Pos != cachedTexture.DrawingSurface.LocalClipBounds.Pos ||
+                lengthDiff < -ZoomDiffToRerender)
             {
                 return true;
             }
@@ -185,7 +196,6 @@ internal class SceneRenderer : IDisposable
     }
 
 
-
     private bool HighDpiRenderNodePresent(IReadOnlyNodeGraph documentNodeGraph)
     {
         bool highDpiRenderNodePresent = false;

+ 2 - 0
src/PixiEditor/Properties/AssemblyInfo.cs

@@ -1,4 +1,5 @@
 using System.Reflection;
+using System.Runtime.CompilerServices;
 using System.Runtime.InteropServices;
 
 // General Information about an assembly is controlled through the following
@@ -12,6 +13,7 @@ using System.Runtime.InteropServices;
 [assembly: AssemblyCopyright("Copyright PixiEditor © 2017 - 2025")]
 [assembly: AssemblyTrademark("")]
 [assembly: AssemblyCulture("")]
+[assembly: InternalsVisibleTo("PixiEditor.Tests")]
 
 // Setting ComVisible to false makes the types in this assembly not visible
 // to COM components.  If you need to access a type in this assembly from

+ 1 - 0
src/PixiEditor/ViewModels/Document/DocumentViewModel.cs

@@ -4,6 +4,7 @@ using System.Collections.Immutable;
 using System.Collections.ObjectModel;
 using System.IO;
 using System.Linq;
+using System.Runtime.CompilerServices;
 using System.Text.Json;
 using Avalonia;
 using Avalonia.Media.Imaging;

+ 7 - 0
src/PixiEditor/ViewModels/Menu/MenuBarViewModel.cs

@@ -4,6 +4,7 @@ using System.Linq;
 using System.Windows.Input;
 using Avalonia;
 using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Data;
 using Avalonia.Threading;
 using Microsoft.Extensions.DependencyInjection;
@@ -14,6 +15,7 @@ using PixiEditor.Models.Commands;
 using PixiEditor.OperatingSystem;
 using PixiEditor.ViewModels.SubViewModels;
 using PixiEditor.ViewModels.SubViewModels.AdditionalContent;
+using PixiEditor.Views;
 using Command = PixiEditor.Models.Commands.Commands.Command;
 using Commands_Command = PixiEditor.Models.Commands.Commands.Command;
 using NativeMenu = Avalonia.Controls.NativeMenu;
@@ -92,6 +94,11 @@ internal class MenuBarViewModel : PixiObservableObject
 
     private void BuildMenu(CommandController controller, MenuItemBuilder[] builders)
     {
+        if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime)
+        {
+            return;
+        }
+
         if (IOperatingSystem.Current.IsMacOs)
         {
             BuildBasicNativeMenuItems(controller, menuItems);

+ 22 - 14
src/PixiEditor/ViewModels/SubViewModels/ClipboardViewModel.cs

@@ -133,10 +133,20 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
 
+        if (doc is null)
+            return;
+
         // TODO: Exception handling would probably be good
         var bitmap = Importer.GetPreviewSurface(path);
+
+        if (bitmap is null)
+            return;
+
         byte[] pixels = bitmap.ToWriteableBitmap().ExtractPixels();
 
+        if (pixels.Length == 0)
+            return;
+
         doc.Operations.ImportReferenceLayer(
             pixels.ToImmutableArray(),
             new VecI(bitmap.Size.X, bitmap.Size.Y));
@@ -426,8 +436,7 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
     public bool CanCopyCels()
     {
         return Owner.DocumentIsNotNull(null) &&
-               Owner.DocumentManagerSubViewModel.ActiveDocument.AnimationDataViewModel.AllCels.Any(
-                   x => x.IsSelected);
+               Owner.DocumentManagerSubViewModel.ActiveDocument.AnimationDataViewModel.AllCels.Any(x => x.IsSelected);
     }
 
     [Evaluator.CanExecute("PixiEditor.Clipboard.CanCopyNodes",
@@ -539,8 +548,8 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
                     continue;
 
                 var inputProperty =
-                    inputNodeInstance.Inputs.FirstOrDefault(
-                        x => x.PropertyName == connection.InputProperty.PropertyName);
+                    inputNodeInstance.Inputs.FirstOrDefault(x =>
+                        x.PropertyName == connection.InputProperty.PropertyName);
                 var outputProperty =
                     outputNodeInstance.Outputs.FirstOrDefault(x =>
                         x.PropertyName == connection.OutputProperty.PropertyName);
@@ -595,18 +604,17 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
             return;
         }
 
-        var newTask = Dispatcher.UIThread.InvokeAsync(
-            async () =>
+        var newTask = Dispatcher.UIThread.InvokeAsync(async () =>
+        {
+            T result = await task();
+            if (!EqualityComparer<T>.Default.Equals(result, value))
             {
-                T result = await task();
-                if (!EqualityComparer<T>.Default.Equals(result, value))
-                {
-                    updateAction(result);
-                    CommandController.CanExecuteChanged("PixiEditor.Clipboard");
-                }
+                updateAction(result);
+                CommandController.CanExecuteChanged("PixiEditor.Clipboard");
+            }
 
-                clipboardTasks.Remove(key, out _);
-            });
+            clipboardTasks.Remove(key, out _);
+        });
 
         clipboardTasks.TryAdd(key, newTask);
     }

+ 1 - 0
tests/ChunkyImageLibTest/ChunkyImageLibTest.csproj

@@ -22,6 +22,7 @@
   <ItemGroup>
     <ProjectReference Include="..\..\src\ChunkyImageLib\ChunkyImageLib.csproj" />
     <ProjectReference Include="..\..\src\Drawie\src\Drawie.Backend.Skia\Drawie.Backend.Skia.csproj" />
+    <ProjectReference Include="..\PixiEditor.Tests\PixiEditor.Tests.csproj" />
   </ItemGroup>
 
 </Project>

+ 2 - 12
tests/ChunkyImageLibTest/ChunkyImageTests.cs

@@ -6,23 +6,13 @@ using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Numerics;
 using Drawie.Skia;
+using PixiEditor.Tests;
 using Xunit;
 
 namespace ChunkyImageLibTest;
 
-public class ChunkyImageTests
+public class ChunkyImageTests : PixiEditorTest
 {
-    public ChunkyImageTests()
-    {
-        try
-        {
-            DrawingBackendApi.SetupBackend(new SkiaDrawingBackend(), null);
-        }
-        catch
-        {
-        }
-    }
-
     [Fact]
     public void Dispose_ComplexImage_ReturnsAllChunks()
     {

+ 2 - 9
tests/ChunkyImageLibTest/ImageOperationTests.cs

@@ -5,19 +5,12 @@ using Drawie.Backend.Core;
 using Drawie.Backend.Core.Bridge;
 using Drawie.Numerics;
 using Drawie.Skia;
+using PixiEditor.Tests;
 using Xunit;
 
 namespace ChunkyImageLibTest;
-public class ImageOperationTests
+public class ImageOperationTests : PixiEditorTest
 {
-    public ImageOperationTests()
-    {
-        try
-        {
-            DrawingBackendApi.SetupBackend(new SkiaDrawingBackend(), null);
-        }
-        catch { }
-    }
 
     [Fact]
     public void FindAffectedChunks_SingleChunk_ReturnsSingleChunk()

+ 3 - 11
tests/ChunkyImageLibTest/RectangleOperationTests.cs

@@ -5,23 +5,15 @@ using Drawie.Backend.Core.Bridge;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Numerics;
 using Drawie.Skia;
+using PixiEditor.Tests;
 using Xunit;
 
 namespace ChunkyImageLibTest;
 
-public class RectangleOperationTests
+public class RectangleOperationTests : PixiEditorTest
 {
     const int chunkSize = ChunkPool.FullChunkSize;
-    public RectangleOperationTests()
-    {
-        try
-        {
-            DrawingBackendApi.SetupBackend(new SkiaDrawingBackend(), null);
-        }
-        catch
-        {
-        }
-    }
+
 // to keep expected rectangles aligned
 #pragma warning disable format
     [Fact]

+ 11 - 0
tests/ChunkyImageLibTest/TestRunnerSetup.cs

@@ -0,0 +1,11 @@
+using Xunit;
+
+[assembly:
+    CollectionBehavior(CollectionBehavior.CollectionPerAssembly, DisableTestParallelization = false,
+        MaxParallelThreads = 1)]
+
+namespace ChunkyImageLibTest;
+
+public class TestRunnerSetup
+{
+}

+ 2 - 3
tests/PixiEditor.Backend.Tests/NodeSystemTests.cs

@@ -12,11 +12,12 @@ using PixiEditor.ChangeableDocument.Changes.NodeGraph;
 using PixiEditor.Models.Serialization;
 using PixiEditor.Models.Serialization.Factories;
 using PixiEditor.Parser.Skia.Encoders;
+using PixiEditor.Tests;
 using Xunit.Abstractions;
 
 namespace PixiEditor.Backend.Tests;
 
-public class NodeSystemTests
+public class NodeSystemTests : PixiEditorTest
 {
     private readonly ITestOutputHelper output;
 
@@ -29,8 +30,6 @@ public class NodeSystemTests
     public NodeSystemTests(ITestOutputHelper output)
     {
         this.output = output;
-        if (!DrawingBackendApi.HasBackend)
-            DrawingBackendApi.SetupBackend(new SkiaDrawingBackend(), new AvaloniaRenderingDispatcher());
     }
 
     [Fact]

+ 1 - 0
tests/PixiEditor.Backend.Tests/PixiEditor.Backend.Tests.csproj

@@ -29,6 +29,7 @@
     <ItemGroup>
       <ProjectReference Include="..\..\src\PixiEditor.ChangeableDocument\PixiEditor.ChangeableDocument.csproj" />
       <ProjectReference Include="..\..\src\PixiEditor\PixiEditor.csproj" />
+      <ProjectReference Include="..\PixiEditor.Tests\PixiEditor.Tests.csproj" />
     </ItemGroup>
 
 </Project>

+ 10 - 72
tests/PixiEditor.Tests/AvaloniaTestRunner.cs

@@ -1,83 +1,21 @@
-using System.Reflection;
+using Avalonia;
 using Avalonia.Headless;
 using Avalonia.Platform;
-using Avalonia.Threading;
-using Drawie.Backend.Core.Bridge;
-using Drawie.Skia;
-using DrawiEngine;
-using PixiEditor.Desktop;
-using Xunit.Abstractions;
-using Xunit.Sdk;
+using Drawie.Interop.VulkanAvalonia;
+using PixiEditor.Tests;
 
 [assembly:TestFramework("PixiEditor.Tests.AvaloniaTestRunner", "PixiEditor.Tests")]
 [assembly:CollectionBehavior(CollectionBehavior.CollectionPerAssembly, DisableTestParallelization = false, MaxParallelThreads = 1)]
+[assembly: AvaloniaTestApplication(typeof(AvaloniaTestRunner))]
 namespace PixiEditor.Tests
 {
-    public class AvaloniaTestRunner : XunitTestFramework
+    public class AvaloniaTestRunner
     {
-        public AvaloniaTestRunner(IMessageSink messageSink) : base(messageSink)
-        {
-        }
-
-        protected override ITestFrameworkExecutor CreateExecutor(AssemblyName assemblyName)
-            => new Executor(assemblyName, SourceInformationProvider, DiagnosticMessageSink);
-
-
-        class Executor : XunitTestFrameworkExecutor
-        {
-            public Executor(AssemblyName assemblyName, ISourceInformationProvider sourceInformationProvider,
-                IMessageSink diagnosticMessageSink) : base(assemblyName, sourceInformationProvider,
-                diagnosticMessageSink)
-            {
-            }
-
-            protected override async void RunTestCases(IEnumerable<IXunitTestCase> testCases,
-                IMessageSink executionMessageSink,
-                ITestFrameworkExecutionOptions executionOptions)
-            {
-                executionOptions.SetValue("xunit.execution.DisableParallelization", false);
-                using (var assemblyRunner = new Runner(
-                    TestAssembly, testCases, DiagnosticMessageSink, executionMessageSink,
-                    executionOptions)) await assemblyRunner.RunAsync();
-            }
-        }
-
-        class Runner : XunitTestAssemblyRunner
-        {
-            public Runner(ITestAssembly testAssembly, IEnumerable<IXunitTestCase> testCases,
-                IMessageSink diagnosticMessageSink, IMessageSink executionMessageSink,
-                ITestFrameworkExecutionOptions executionOptions) : base(testAssembly, testCases, diagnosticMessageSink,
-                executionMessageSink, executionOptions)
+        public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure<App>()
+            .UseHeadless(new AvaloniaHeadlessPlatformOptions()
             {
-            }
-
-
-            protected override void SetupSyncContext(int maxParallelThreads)
-            {
-                var tcs = new TaskCompletionSource<SynchronizationContext>();
-                new Thread(() =>
-                {
-                    try
-                    {
-                        Program.BuildAvaloniaApp()
-                            .UseHeadless(new AvaloniaHeadlessPlatformOptions { FrameBufferFormat = PixelFormat.Bgra8888, UseHeadlessDrawing = false })
-                            .SetupWithoutStarting();
-                        tcs.SetResult(SynchronizationContext.Current);
-                    }
-                    catch (Exception e)
-                    {
-                        tcs.SetException(e);
-                    }
-                    Dispatcher.UIThread.MainLoop(CancellationToken.None);
-                })
-                {
-                    IsBackground = true
-                }.Start();
-
-                SynchronizationContext.SetSynchronizationContext(tcs.Task.Result);
-            }
-
-
-        }
+                UseHeadlessDrawing = true,
+                FrameBufferFormat = PixelFormat.Rgba8888,
+            });
     }
 }

+ 12 - 0
tests/PixiEditor.Tests/PixiEditor.Tests.csproj

@@ -12,6 +12,7 @@
 
     <ItemGroup>
         <PackageReference Include="Avalonia" Version="$(AvaloniaVersion)" />
+        <PackageReference Include="Avalonia.Headless.XUnit" Version="$(AvaloniaVersion)" />
         <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
         <PackageReference Include="xunit" Version="2.9.2"/>
         <PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
@@ -26,7 +27,18 @@
 
 
     <ItemGroup>
+      <ProjectReference Include="..\..\src\Drawie\src\DrawiEngine.Desktop\DrawiEngine.Desktop.csproj" />
       <ProjectReference Include="..\..\src\PixiEditor.Desktop\PixiEditor.Desktop.csproj" />
+      <ProjectReference Include="..\..\src\PixiEditor.Linux\PixiEditor.Linux.csproj" />
+      <ProjectReference Include="..\..\src\PixiEditor.MacOs\PixiEditor.MacOs.csproj" />
+    </ItemGroup>
+    
+    <ItemGroup>
+        <Content Include="TestFiles\**" CopyToOutputDirectory="PreserveNewest" />
+    </ItemGroup>
+    
+    <ItemGroup>
+      <Folder Include="TestFiles\ResolutionTests\" />
     </ItemGroup>
 
 </Project>

+ 116 - 2
tests/PixiEditor.Tests/PixiEditorTest.cs

@@ -1,6 +1,20 @@
 using Drawie.Backend.Core.Bridge;
+using Drawie.Numerics;
+using Drawie.RenderApi.Vulkan;
+using Drawie.Silk;
 using Drawie.Skia;
+using Drawie.Windowing;
 using DrawiEngine;
+using DrawiEngine.Desktop;
+using Microsoft.Extensions.DependencyInjection;
+using PixiEditor.Extensions.Runtime;
+using PixiEditor.Helpers;
+using PixiEditor.Linux;
+using PixiEditor.MacOs;
+using PixiEditor.OperatingSystem;
+using PixiEditor.Platform;
+using PixiEditor.ViewModels;
+using PixiEditor.Windows;
 
 namespace PixiEditor.Tests;
 
@@ -13,7 +27,107 @@ public class PixiEditorTest
             return;
         }
 
-        SkiaDrawingBackend skiaDrawingBackend = new SkiaDrawingBackend();
-        DrawingBackendApi.SetupBackend(skiaDrawingBackend, new DrawieRenderingDispatcher());
+        try
+        {
+            var engine = DesktopDrawingEngine.CreateDefaultDesktop();
+            var app = new TestingApp();
+            Console.WriteLine("Running DrawieEngine with configuration:");
+            Console.WriteLine($"\t- RenderApi: {engine.RenderApi}");
+            Console.WriteLine($"\t- WindowingPlatform: {engine.RenderApi}");
+            Console.WriteLine($"\t- DrawingBackend: {engine.RenderApi}");
+
+            app.Initialize(engine);
+            IWindow window = app.CreateMainWindow();
+
+            window.Initialize();
+
+            DrawingBackendApi.InitializeBackend(engine.RenderApi);
+
+            app.Run();
+        }
+        catch (Exception ex)
+        {
+            if (!DrawingBackendApi.HasBackend)
+                DrawingBackendApi.SetupBackend(new SkiaDrawingBackend(), new DrawieRenderingDispatcher());
+        }
+    }
+}
+
+public class FullPixiEditorTest : PixiEditorTest
+{
+    public FullPixiEditorTest()
+    {
+        ExtensionLoader loader = new ExtensionLoader("TestExtensions", "TestExtensions/Unpacked");
+
+        if (IOperatingSystem.Current == null)
+        {
+            IOperatingSystem os;
+            if (System.OperatingSystem.IsWindows())
+            {
+                os = new WindowsOperatingSystem();
+            }
+            else if (System.OperatingSystem.IsLinux())
+            {
+                os = new LinuxOperatingSystem();
+            }
+            else if (System.OperatingSystem.IsMacOS())
+            {
+                os = new MacOperatingSystem();
+            }
+            else
+            {
+                throw new NotSupportedException("Unsupported operating system");
+            }
+
+            IOperatingSystem.RegisterOS(os);
+        }
+
+        if (IPlatform.Current == null)
+        {
+            IPlatform.RegisterPlatform(new TestPlatform());
+        }
+
+        var services = new ServiceCollection()
+            .AddPlatform()
+            .AddPixiEditor(loader)
+            .AddExtensionServices(loader)
+            .BuildServiceProvider();
+
+
+        var vm = services.GetRequiredService<ViewModelMain>();
+        vm.Setup(services);
+    }
+
+    class TestPlatform : IPlatform
+    {
+        public string Id { get; } = "TestPlatform";
+        public string Name { get; } = "Tests";
+
+        public bool PerformHandshake()
+        {
+            return true;
+        }
+
+        public void Update()
+        {
+        }
+
+        public IAdditionalContentProvider? AdditionalContentProvider { get; } = new NullAdditionalContentProvider();
+    }
+}
+
+public class TestingApp : DrawieApp
+{
+    IWindow window;
+
+    public override IWindow CreateMainWindow()
+    {
+        window = Engine.WindowingPlatform.CreateWindow("Testing app", VecI.One);
+        return window;
+    }
+
+    protected override void OnInitialize()
+    {
+        window.IsVisible = false;
     }
 }

+ 136 - 0
tests/PixiEditor.Tests/RenderTests.cs

@@ -0,0 +1,136 @@
+using Avalonia.Headless.XUnit;
+using ChunkyImageLib;
+using ChunkyImageLib.DataHolders;
+using Drawie.Backend.Core;
+using Drawie.Backend.Core.Bridge;
+using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Numerics;
+using PixiEditor.Models.IO;
+using Xunit.Abstractions;
+using Color = Drawie.Backend.Core.ColorsImpl.Color;
+
+namespace PixiEditor.Tests;
+
+public class RenderTests : FullPixiEditorTest
+{
+    private readonly ITestOutputHelper _testOutputHelper;
+
+    public RenderTests(ITestOutputHelper testOutputHelper)
+    {
+        _testOutputHelper = testOutputHelper;
+    }
+
+    [AvaloniaTheory]
+    [InlineData("Fibi")]
+    [InlineData("Pond")]
+    [InlineData("SmlPxlCircShadWithMask")]
+    [InlineData("SmallPixelArtCircleShadow")]
+    [InlineData("SmlPxlCircShadWithMaskClipped")]
+    [InlineData("SmlPxlCircShadWithMaskClippedInFolder")]
+    [InlineData("VectorRectangleClippedToCircle")]
+    [InlineData("VectorRectangleClippedToCircleShadowFilter")]
+    [InlineData("VectorRectangleClippedToCircleMasked")]
+    public void TestThatPixiFilesRenderTheSameResultAsSavedPng(string fileName)
+    {
+        if (!DrawingBackendApi.Current.IsHardwareAccelerated)
+        {
+            _testOutputHelper.WriteLine("Skipping the test because hardware acceleration is not enabled.");
+            return;
+        }
+
+        string pixiFile = Path.Combine("TestFiles", "RenderTests", fileName + ".pixi");
+        string pngFile = Path.Combine("TestFiles", "RenderTests", fileName + ".png");
+        var document = Importer.ImportDocument(pixiFile);
+
+        Assert.NotNull(pngFile);
+
+        var result = document.TryRenderWholeImage(0);
+
+        Assert.True(result is { IsT1: true, AsT1: not null }); // Check if rendering was successful
+
+        using var image = result.AsT1;
+
+        using var toCompareTo = Importer.ImportImage(pngFile, document.SizeBindable);
+
+        Assert.NotNull(toCompareTo);
+
+        Assert.True(PixelCompare(image, toCompareTo));
+    }
+
+    [AvaloniaTheory]
+    [InlineData("SingleLayer")]
+    [InlineData("SingleLayerWithMask")]
+    [InlineData("LayerWithMaskClipped")]
+    [InlineData("LayerWithMaskClippedHighDpiPresent")]
+    [InlineData("LayerWithMaskClippedInFolder")]
+    [InlineData("LayerWithMaskClippedInFolderWithMask")]
+    public void TestThatHalfResolutionScalesRenderCorrectly(string pixiName)
+    {
+        string pixiFile = Path.Combine("TestFiles", "ResolutionTests", pixiName + ".pixi");
+
+        var document = Importer.ImportDocument(pixiFile);
+        using Surface output = Surface.ForDisplay(document.SizeBindable);
+        document.SceneRenderer.RenderScene(output.DrawingSurface, ChunkResolution.Half);
+
+        Color expectedColor = Colors.Yellow;
+
+        Assert.True(AllPixelsAreColor(output, expectedColor));
+    }
+
+    private static bool PixelCompare(Surface image, Surface compareTo)
+    {
+        if (image.Size != compareTo.Size)
+        {
+            return false;
+        }
+
+        using Surface compareSurface1 = new Surface(image.Size);
+        using Surface compareSurface2 = new Surface(image.Size);
+
+        compareSurface1.DrawingSurface.Canvas.DrawSurface(image.DrawingSurface, 0, 0);
+        compareSurface2.DrawingSurface.Canvas.DrawSurface(compareTo.DrawingSurface, 0, 0);
+
+        var imageData1 = compareSurface1.PeekPixels();
+        var imageData2 = compareSurface2.PeekPixels();
+
+        if (imageData1.Width != imageData2.Width || imageData1.Height != imageData2.Height)
+        {
+            return false;
+        }
+
+        for (int y = 0; y < imageData1.Height; y++)
+        {
+            for (int x = 0; x < imageData1.Width; x++)
+            {
+                var pixel1 = imageData1.GetPixelColor(x, y);
+                var pixel2 = imageData2.GetPixelColor(x, y);
+
+                if (pixel1 != pixel2)
+                {
+                    return false;
+                }
+            }
+        }
+
+        return true;
+    }
+
+    private static bool AllPixelsAreColor(Surface image, Color color)
+    {
+        var imageData = image.PeekPixels();
+
+        for (int y = 0; y < imageData.Height; y++)
+        {
+            for (int x = 0; x < imageData.Width; x++)
+            {
+                var pixel = imageData.GetPixelColor(x, y);
+                if (pixel != color)
+                {
+                    return false;
+                }
+            }
+        }
+
+        return true;
+    }
+}

+ 1 - 12
tests/PixiEditor.Tests/SerializationTests.cs

@@ -12,19 +12,8 @@ using PixiEditor.Parser.Skia.Encoders;
 
 namespace PixiEditor.Tests;
 
-public class SerializationTests
+public class SerializationTests : PixiEditorTest
 {
-    public SerializationTests()
-    {
-        if (DrawingBackendApi.HasBackend)
-        {
-            return;
-        }
-
-        SkiaDrawingBackend skiaDrawingBackend = new SkiaDrawingBackend();
-        DrawingBackendApi.SetupBackend(skiaDrawingBackend, new DrawieRenderingDispatcher());
-    }
-
     [Fact]
     public void TestThatAllPaintablesHaveFactories()
     {

BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/Fibi.pixi


BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/Fibi.png


BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/Pond.pixi


BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/Pond.png


BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/SmallPixelArtCircleShadow.pixi


BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/SmallPixelArtCircleShadow.png


BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/SmlPxlCircShadWithMask.pixi


BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/SmlPxlCircShadWithMask.png


BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/SmlPxlCircShadWithMaskClipped.pixi


BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/SmlPxlCircShadWithMaskClipped.png


BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/SmlPxlCircShadWithMaskClippedInFolder.pixi


BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/SmlPxlCircShadWithMaskClippedInFolder.png


BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/VectorRectangleClippedToCircle.pixi


BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/VectorRectangleClippedToCircle.png


BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/VectorRectangleClippedToCircleMasked.pixi


BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/VectorRectangleClippedToCircleMasked.png


BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/VectorRectangleClippedToCircleShadowFilter.pixi


BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/VectorRectangleClippedToCircleShadowFilter.png


BIN
tests/PixiEditor.Tests/TestFiles/ResolutionTests/LayerWithMaskClipped.pixi


BIN
tests/PixiEditor.Tests/TestFiles/ResolutionTests/LayerWithMaskClippedHighDpiPresent.pixi


BIN
tests/PixiEditor.Tests/TestFiles/ResolutionTests/LayerWithMaskClippedInFolder.pixi


BIN
tests/PixiEditor.Tests/TestFiles/ResolutionTests/LayerWithMaskClippedInFolderWithMask.pixi


BIN
tests/PixiEditor.Tests/TestFiles/ResolutionTests/SingleLayer.pixi


BIN
tests/PixiEditor.Tests/TestFiles/ResolutionTests/SingleLayerWithMask.pixi


+ 35 - 7
tests/PixiEditorTests.sln

@@ -95,8 +95,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Drawie.RenderApi", "..\src\
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DrawiEngine", "..\src\Drawie\src\DrawiEngine\DrawiEngine.csproj", "{23A9B33D-118E-4BCE-85FB-0AAC6E4D15B6}"
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Drawie.Windowing", "..\src\Drawie\src\Drawie.Windowing\Drawie.Windowing.csproj", "{B8DFAB77-3FD1-4FFF-A018-215EFC73D916}"
-EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Drawie.Backend.Skia", "..\src\Drawie\src\Drawie.Backend.Skia\Drawie.Backend.Skia.csproj", "{8044BAB5-5EF5-45FE-B819-18BDCC914C1B}"
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Drawie.RenderApi.Vulkan", "..\src\Drawie\src\Drawie.RenderApi.Vulkan\Drawie.RenderApi.Vulkan.csproj", "{54C073D4-88F8-46D2-9EDB-86C0A7819968}"
@@ -117,6 +115,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ColorPicker.AvaloniaUI", ".
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ColorPicker.Models", "..\src\ColorPicker\src\ColorPicker.Models\ColorPicker.Models.csproj", "{B4A2F5BC-A07D-4C99-B022-B959B54CC4A0}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.MacOs", "..\src\PixiEditor.MacOs\PixiEditor.MacOs.csproj", "{554F618D-DDD1-484B-AE89-540852FF7D4F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.Linux", "..\src\PixiEditor.Linux\PixiEditor.Linux.csproj", "{C7D2CAEE-2DF4-4920-AE9F-E3323E80AAF4}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DrawiEngine.Desktop", "..\src\Drawie\src\DrawiEngine.Desktop\DrawiEngine.Desktop.csproj", "{F021BE50-BDFB-427C-9495-888347C4E3B3}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Drawie.Windowing", "..\src\Drawie\src\Drawie.Windowing\Drawie.Windowing.csproj", "{37662F23-90F7-4B41-8E45-5D1B09A96803}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Drawie.Windowing.Glfw", "..\src\Drawie\src\Drawie.Windowing.Glfw\Drawie.Windowing.Glfw.csproj", "{B13E1D5E-EF6B-4193-8BE6-4C85F8C1EE59}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -290,10 +298,6 @@ Global
 		{23A9B33D-118E-4BCE-85FB-0AAC6E4D15B6}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{23A9B33D-118E-4BCE-85FB-0AAC6E4D15B6}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{23A9B33D-118E-4BCE-85FB-0AAC6E4D15B6}.Release|Any CPU.Build.0 = Release|Any CPU
-		{B8DFAB77-3FD1-4FFF-A018-215EFC73D916}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{B8DFAB77-3FD1-4FFF-A018-215EFC73D916}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{B8DFAB77-3FD1-4FFF-A018-215EFC73D916}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{B8DFAB77-3FD1-4FFF-A018-215EFC73D916}.Release|Any CPU.Build.0 = Release|Any CPU
 		{8044BAB5-5EF5-45FE-B819-18BDCC914C1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{8044BAB5-5EF5-45FE-B819-18BDCC914C1B}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{8044BAB5-5EF5-45FE-B819-18BDCC914C1B}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -334,6 +338,26 @@ Global
 		{B4A2F5BC-A07D-4C99-B022-B959B54CC4A0}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{B4A2F5BC-A07D-4C99-B022-B959B54CC4A0}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{B4A2F5BC-A07D-4C99-B022-B959B54CC4A0}.Release|Any CPU.Build.0 = Release|Any CPU
+		{554F618D-DDD1-484B-AE89-540852FF7D4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{554F618D-DDD1-484B-AE89-540852FF7D4F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{554F618D-DDD1-484B-AE89-540852FF7D4F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{554F618D-DDD1-484B-AE89-540852FF7D4F}.Release|Any CPU.Build.0 = Release|Any CPU
+		{C7D2CAEE-2DF4-4920-AE9F-E3323E80AAF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{C7D2CAEE-2DF4-4920-AE9F-E3323E80AAF4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{C7D2CAEE-2DF4-4920-AE9F-E3323E80AAF4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{C7D2CAEE-2DF4-4920-AE9F-E3323E80AAF4}.Release|Any CPU.Build.0 = Release|Any CPU
+		{F021BE50-BDFB-427C-9495-888347C4E3B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{F021BE50-BDFB-427C-9495-888347C4E3B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{F021BE50-BDFB-427C-9495-888347C4E3B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{F021BE50-BDFB-427C-9495-888347C4E3B3}.Release|Any CPU.Build.0 = Release|Any CPU
+		{37662F23-90F7-4B41-8E45-5D1B09A96803}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{37662F23-90F7-4B41-8E45-5D1B09A96803}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{37662F23-90F7-4B41-8E45-5D1B09A96803}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{37662F23-90F7-4B41-8E45-5D1B09A96803}.Release|Any CPU.Build.0 = Release|Any CPU
+		{B13E1D5E-EF6B-4193-8BE6-4C85F8C1EE59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{B13E1D5E-EF6B-4193-8BE6-4C85F8C1EE59}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{B13E1D5E-EF6B-4193-8BE6-4C85F8C1EE59}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{B13E1D5E-EF6B-4193-8BE6-4C85F8C1EE59}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(NestedProjects) = preSolution
 		{0EF3CAB9-7361-472C-8789-D17D4EA2DEBB} = {D914C08C-5F1A-4E13-AAA6-F25E8C9748E2}
@@ -379,7 +403,6 @@ Global
 		{941E4206-4260-4363-A1A0-A7D2E641CE4F} = {E118E6FE-67E7-4472-A8D7-E7F470E66131}
 		{3061E7B1-ABE2-4E14-A650-CBC42450F21F} = {E118E6FE-67E7-4472-A8D7-E7F470E66131}
 		{23A9B33D-118E-4BCE-85FB-0AAC6E4D15B6} = {E118E6FE-67E7-4472-A8D7-E7F470E66131}
-		{B8DFAB77-3FD1-4FFF-A018-215EFC73D916} = {E118E6FE-67E7-4472-A8D7-E7F470E66131}
 		{8044BAB5-5EF5-45FE-B819-18BDCC914C1B} = {E118E6FE-67E7-4472-A8D7-E7F470E66131}
 		{54C073D4-88F8-46D2-9EDB-86C0A7819968} = {E118E6FE-67E7-4472-A8D7-E7F470E66131}
 		{A0424CAD-F2F5-4A2A-A638-F5457BD098C6} = {E118E6FE-67E7-4472-A8D7-E7F470E66131}
@@ -390,5 +413,10 @@ Global
 		{924CA5E4-F579-435F-B39A-11802F9B1390} = {E118E6FE-67E7-4472-A8D7-E7F470E66131}
 		{1B9B2155-9D9C-4259-8015-4F06F944812C} = {E118E6FE-67E7-4472-A8D7-E7F470E66131}
 		{B4A2F5BC-A07D-4C99-B022-B959B54CC4A0} = {E118E6FE-67E7-4472-A8D7-E7F470E66131}
+		{554F618D-DDD1-484B-AE89-540852FF7D4F} = {E118E6FE-67E7-4472-A8D7-E7F470E66131}
+		{C7D2CAEE-2DF4-4920-AE9F-E3323E80AAF4} = {E118E6FE-67E7-4472-A8D7-E7F470E66131}
+		{F021BE50-BDFB-427C-9495-888347C4E3B3} = {E118E6FE-67E7-4472-A8D7-E7F470E66131}
+		{37662F23-90F7-4B41-8E45-5D1B09A96803} = {E118E6FE-67E7-4472-A8D7-E7F470E66131}
+		{B13E1D5E-EF6B-4193-8BE6-4C85F8C1EE59} = {E118E6FE-67E7-4472-A8D7-E7F470E66131}
 	EndGlobalSection
 EndGlobal