Browse Source

Merge branch 'master' into release

Krzysztof Krysiński 1 week ago
parent
commit
2635d25990

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 539a77bffc3ec75ae99d101434280c176fea80df
+Subproject commit f165926f94cc08ab774fd1f86de75d90a95ea146

+ 23 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ShaderNode.cs

@@ -32,11 +32,26 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
     protected override bool ExecuteOnlyOnCacheChange => true;
     protected override CacheTriggerFlags CacheTrigger => CacheTriggerFlags.All;
 
+    private string defaultShaderCode = """
+                                       // Below is a list of built-in special uniforms that are automatically added by PixiEditor.
+                                       // Any other uniform will be added as a Node input
+                                       
+                                       uniform vec2 iResolution; // The resolution of current render output. It is usually a document size.
+                                       uniform float iNormalizedTime; // The normalized time of the current frame, from 0 to 1.
+                                       uniform int iFrame; // The current frame number.
+                                       uniform shader iImage; // The Background input of the node, alternatively you can use "Background" uniform.
+                                       
+                                       half4 main(float2 uv)
+                                       {
+                                           return half4(1, 1, 1, 1);
+                                       }
+                                       """;
+
     public ShaderNode()
     {
         Background = CreateRenderInput("Background", "BACKGROUND");
         ColorSpace = CreateInput("ColorSpace", "COLOR_SPACE", ColorSpaceType.Inherit);
-        ShaderCode = CreateInput("ShaderCode", "SHADER_CODE", "")
+        ShaderCode = CreateInput("ShaderCode", "SHADER_CODE", defaultShaderCode)
             .WithRules(validator => validator.Custom(ValidateShaderCode))
             .NonOverridenChanged(RegenerateUniformInputs);
 
@@ -118,6 +133,7 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
         lastImageShader = snapshot.ToShader();
 
         uniforms.Add("iImage", new Uniform("iImage", lastImageShader));
+        uniforms.Add("Background", new Uniform("Background", lastImageShader));
 
         snapshot.Dispose();
         //texture.Dispose();
@@ -220,6 +236,11 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
         var uniforms = declarations;
 
         var nonExistingUniforms = uniformInputs.Keys.Where(x => uniforms.All(y => y.Name != x)).ToList();
+        if(nonExistingUniforms.Contains("Background"))
+        {
+            nonExistingUniforms.Remove("Background");
+        }
+
         foreach (var nonExistingUniform in nonExistingUniforms)
         {
             RemoveInputProperty(uniformInputs[nonExistingUniform].prop);
@@ -388,7 +409,7 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
 
     private bool IsBuiltInUniform(string name)
     {
-        return name is "iResolution" or "iNormalizedTime" or "iFrame" or "iImage";
+        return name is "iResolution" or "iNormalizedTime" or "iFrame" or "iImage" or "Background";
     }
 
     private ValidatorResult ValidateShaderCode(object? value)

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Structure/ApplyMask_Change.cs

@@ -21,7 +21,7 @@ internal sealed class ApplyMask_Change : Change
     public override bool InitializeAndValidate(Document target)
     {
         var member = target.FindMember(structureMemberGuid);
-        bool isValid = member is not (null or FolderNode) && member.EmbeddedMask is not null;
+        bool isValid = member is ImageLayerNode && member.EmbeddedMask is not null;
 
         return isValid;
     }

+ 7 - 3
src/PixiEditor.Desktop/Program.cs

@@ -3,6 +3,7 @@ using Avalonia;
 using Avalonia.Logging;
 using Drawie.Interop.Avalonia;
 using Drawie.Interop.VulkanAvalonia;
+using PixiEditor.Helpers;
 
 namespace PixiEditor.Desktop;
 
@@ -17,16 +18,18 @@ public class Program
 
     // Avalonia configuration, don't remove; also used by visual designer.
     public static AppBuilder BuildAvaloniaApp()
-        => AppBuilder.Configure<App>()
+    {
+        bool openGlPreferred = string.Equals(RenderApiPreferenceManager.TryReadRenderApiPreference(), "opengl", StringComparison.OrdinalIgnoreCase);
+        return AppBuilder.Configure<App>()
             .UsePlatformDetect()
             .With(new Win32PlatformOptions()
             {
-                RenderingMode = new Win32RenderingMode[] { Win32RenderingMode.Vulkan, Win32RenderingMode.Wgl },
+                RenderingMode = openGlPreferred ? [ Win32RenderingMode.Wgl, Win32RenderingMode.Vulkan] : [ Win32RenderingMode.Vulkan, Win32RenderingMode.Wgl],
                 OverlayPopups = true,
             })
             .With(new X11PlatformOptions()
             {
-                RenderingMode = new X11RenderingMode[] { X11RenderingMode.Vulkan, X11RenderingMode.Glx },
+                RenderingMode = openGlPreferred ? [ X11RenderingMode.Glx, X11RenderingMode.Vulkan] : [ X11RenderingMode.Vulkan, X11RenderingMode.Glx],
                 OverlayPopups = true,
             })
             .With(new SkiaOptions()
@@ -38,4 +41,5 @@ public class Program
             .LogToTrace(LogEventLevel.Verbose, "Vulkan")
 #endif
             .LogToTrace();
+    }
 }

+ 5 - 0
src/PixiEditor.Linux/LinuxOperatingSystem.cs

@@ -43,6 +43,11 @@ public sealed class LinuxOperatingSystem : IOperatingSystem
         return true;
     }
 
+    public string[] GetAvailableRenderers()
+    {
+        return ["Vulkan", "OpenGL"];
+    }
+
     public void HandleActivatedWithFile(FileActivatedEventArgs fileActivatedEventArgs)
     {
         // TODO: Check if this is executed on Linux at all

+ 5 - 0
src/PixiEditor.MacOs/MacOperatingSystem.cs

@@ -39,4 +39,9 @@ public sealed class MacOperatingSystem : IOperatingSystem
     {
         return true;
     }
+
+    public string[] GetAvailableRenderers()
+    {
+        return ["OpenGL"];
+    }
 }

+ 2 - 0
src/PixiEditor.OperatingSystem/IOperatingSystem.cs

@@ -36,5 +36,7 @@ public interface IOperatingSystem
 
     public bool HandleNewInstance(Dispatcher? dispatcher, Action<string, bool> openInExistingAction,
         IApplicationLifetime lifetime);
+
+    public string[] GetAvailableRenderers();
 }
 

+ 5 - 0
src/PixiEditor.Windows/WindowsOperatingSystem.cs

@@ -92,6 +92,11 @@ public sealed class WindowsOperatingSystem : IOperatingSystem
         return false;
     }
 
+    public string[] GetAvailableRenderers()
+    {
+        return ["Vulkan", "OpenGL"];
+    }
+
     public void HandleActivatedWithFile(FileActivatedEventArgs fileActivatedEventArgs) { }
 
     public void HandleActivatedWithUri(ProtocolActivatedEventArgs openUriEventArgs) { }

+ 5 - 1
src/PixiEditor/Data/Localization/Languages/en.json

@@ -1123,5 +1123,9 @@
   "NORMALIZE_COORDINATES": "Normalize Coordinates",
   "TRANSFORMED_POSITION": "Transformed Position",
   "ACCOUNT_PROVIDER_NOT_AVAILABLE": "This build of PixiEditor does not support accounts. Use the official build from pixieditor.net to manage your account.",
-  "STEAM_OFFLINE": "Cannot validate the account. Steam is offline. Make sure Steam client is running and you are logged in."
+  "STEAM_OFFLINE": "Cannot validate the account. Steam is offline. Make sure Steam client is running and you are logged in.",
+  "ERROR_GPU_RESOURCES_CREATION": "Failed to create resources: Try updating your GPU drivers or try setting different rendering api in settings. \nError: '{0}'",
+  "ERROR_SAVING_PREFERENCES_DESC": "Failed to save preferences with error: '{0}'. Please check if you have write permissions to the PixiEditor data folder.",
+  "ERROR_SAVING_PREFERENCES": "Failed to save preferences",
+  "PREFERRED_RENDERER": "Preferred Render Api"
 }

+ 10 - 4
src/PixiEditor/Helpers/Nodes/NodeAbbreviation.cs

@@ -7,7 +7,7 @@ public static class NodeAbbreviation
 {
     private static readonly SearchValues<char> SearchFor = SearchValues.Create(['.']);
 
-    
+
     public static bool IsAbbreviation(string value, out string? lastValue)
     {
         var span = value.AsSpan();
@@ -23,9 +23,14 @@ public static class NodeAbbreviation
         lastValue = span[(i + 1)..].ToString();
         return true;
     }
-    
+
     public static List<NodeTypeInfo>? FromString(string value, ICollection<NodeTypeInfo> allNodes)
     {
+        if (string.IsNullOrEmpty(value))
+        {
+            return null;
+        }
+
         var span = value.AsSpan();
 
         string lookFor = value;
@@ -33,7 +38,7 @@ public static class NodeAbbreviation
         {
             return [allNodes.FirstOrDefault(SearchComparer)];
         }
-        
+
         var list = new List<NodeTypeInfo>();
 
         var enumerator = new PartEnumerator(span, SearchFor);
@@ -47,7 +52,8 @@ public static class NodeAbbreviation
         }
 
         bool SearchComparer(NodeTypeInfo x) =>
-            x.FinalPickerName.Value.Replace(" ", "").Contains(lookFor.Replace(" ", ""), StringComparison.OrdinalIgnoreCase);
+            x.FinalPickerName.Value.Replace(" ", "")
+                .Contains(lookFor.Replace(" ", ""), StringComparison.OrdinalIgnoreCase);
 
         return list;
     }

+ 41 - 0
src/PixiEditor/Helpers/RenderApiPreferenceManager.cs

@@ -0,0 +1,41 @@
+namespace PixiEditor.Helpers;
+
+public static class RenderApiPreferenceManager
+{
+    public static string? FirstReadApiPreference { get; } = TryReadRenderApiPreference() ?? null;
+    public static string? TryReadRenderApiPreference()
+    {
+        try
+        {
+            using var stream =
+                new FileStream(
+                    Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+                        "PixiEditor",
+                        "render_api.config"), FileMode.Open, FileAccess.Read, FileShare.Read);
+            using var reader = new StreamReader(stream);
+            string? renderApi = reader.ReadLine();
+            if (string.IsNullOrEmpty(renderApi))
+            {
+                return null;
+            }
+
+            return renderApi;
+        }
+        catch (Exception)
+        {
+            return null;
+        }
+    }
+
+    public static void UpdateRenderApiPreference(string renderApi)
+    {
+        using var stream =
+            new FileStream(
+                Path.Combine(
+                    Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+                    "PixiEditor",
+                    "render_api.config"), FileMode.Create, FileAccess.Write, FileShare.None);
+        using var writer = new StreamWriter(stream);
+        writer.WriteLine(renderApi);
+    }
+}

+ 14 - 2
src/PixiEditor/Models/Preferences/PreferencesSettings.cs

@@ -2,6 +2,9 @@
 using Newtonsoft.Json;
 using Newtonsoft.Json.Linq;
 using PixiEditor.Extensions.CommonApi.UserPreferences;
+using PixiEditor.Helpers;
+using PixiEditor.Models.Dialogs;
+using PixiEditor.UI.Common.Localization;
 
 namespace PixiEditor.Models.Preferences;
 
@@ -89,8 +92,17 @@ internal class PreferencesSettings : IPreferences
             Init();
         }
 
-        File.WriteAllText(PathToRoamingUserPreferences, JsonConvert.SerializeObject(Preferences));
-        File.WriteAllText(PathToLocalPreferences, JsonConvert.SerializeObject(LocalPreferences));
+        try
+        {
+            File.WriteAllText(PathToRoamingUserPreferences, JsonConvert.SerializeObject(Preferences));
+            File.WriteAllText(PathToLocalPreferences, JsonConvert.SerializeObject(LocalPreferences));
+        }
+        catch (Exception ex)
+        {
+            NoticeDialog.Show(
+                new LocalizedString("ERROR_SAVING_PREFERENCES_DESC", ex.Message),
+                "ERROR_SAVING_PREFERENCES");
+        }
     }
 
     public Dictionary<string, List<Action<string, object>>> Callbacks { get; set; } = new Dictionary<string, List<Action<string, object>>>();

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

@@ -43,5 +43,5 @@ using System.Runtime.InteropServices;
 // You can specify all the values or you can default the Build and Revision Numbers
 // by using the '*' as shown below:
 // [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("2.0.1.3")]
-[assembly: AssemblyFileVersion("2.0.1.3")]
+[assembly: AssemblyVersion("2.0.1.4")]
+[assembly: AssemblyFileVersion("2.0.1.4")]

+ 3 - 2
src/PixiEditor/ViewModels/Document/AnimationDataViewModel.cs

@@ -466,12 +466,13 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
 
     public int GetFirstVisibleFrame()
     {
-        return keyFrames.Count > 0 ? keyFrames.Where(x => x.IsVisible).Min(x => x.StartFrameBindable) : 1;
+        return keyFrames.Count > 0 && keyFrames.Any(x => x.IsVisible)
+            ? keyFrames.Where(x => x.IsVisible).Min(x => x.StartFrameBindable) : 1;
     }
 
     public int GetLastVisibleFrame()
     {
-        return keyFrames.Count > 0
+        return keyFrames.Count > 0 && keyFrames.Any(x => x.IsVisible)
             ? keyFrames.Where(x => x.IsVisible).Max(x => x.StartFrameBindable + x.DurationBindable)
             : DefaultEndFrameBindable;
     }

+ 10 - 1
src/PixiEditor/ViewModels/SubViewModels/LayersViewModel.cs

@@ -375,6 +375,15 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
     public bool ActiveMemberHasMask() =>
         Owner.DocumentManagerSubViewModel.ActiveDocument?.SelectedStructureMember?.HasMaskBindable ?? false;
 
+    [Evaluator.CanExecute("PixiEditor.Layer.ActiveLayerHasApplyableMask",
+        nameof(ViewModelMain.DocumentManagerSubViewModel.ActiveDocument),
+        nameof(ViewModelMain.DocumentManagerSubViewModel.ActiveDocument.SelectedStructureMember),
+        nameof(ViewModelMain.DocumentManagerSubViewModel.ActiveDocument.SelectedStructureMember.HasMaskBindable))]
+
+    public bool ActiveMemberHasApplyableMask() =>
+        (Owner.DocumentManagerSubViewModel.ActiveDocument?.SelectedStructureMember?.HasMaskBindable ?? false)
+        && Owner.DocumentManagerSubViewModel.ActiveDocument?.SelectedStructureMember is IRasterLayerHandler;
+
     [Evaluator.CanExecute("PixiEditor.Layer.ActiveLayerHasNoMask",
         nameof(ViewModelMain.DocumentManagerSubViewModel.ActiveDocument),
         nameof(ViewModelMain.DocumentManagerSubViewModel.ActiveDocument.SelectedStructureMember),
@@ -419,7 +428,7 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
     }
 
     [Command.Basic("PixiEditor.Layer.ApplyMask", "APPLY_MASK", "APPLY_MASK",
-        CanExecute = "PixiEditor.Layer.ActiveLayerHasMask", AnalyticsTrack = true)]
+        CanExecute = "PixiEditor.Layer.ActiveLayerHasApplyableMask", AnalyticsTrack = true)]
     public void ApplyMask()
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;

+ 57 - 3
src/PixiEditor/ViewModels/UserPreferences/Settings/GeneralSettings.cs

@@ -1,26 +1,40 @@
 using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
+using Drawie.Backend.Core.Bridge;
 using PixiEditor.Extensions.CommonApi.UserPreferences;
+using PixiEditor.Helpers;
+using PixiEditor.Models.Dialogs;
+using PixiEditor.Models.IO;
+using PixiEditor.OperatingSystem;
 using PixiEditor.UI.Common.Localization;
 
 namespace PixiEditor.ViewModels.UserPreferences.Settings;
 
 internal class GeneralSettings : SettingsGroup
 {
+    private string? selectedRenderApi = RenderApiPreferenceManager.TryReadRenderApiPreference();
+
+    private List<string>? availableRenderApis = IOperatingSystem.Current?.GetAvailableRenderers()?.ToList() ??
+                                                new List<string>();
+
     private LanguageData? selectedLanguage = ILocalizationProvider.Current?.SelectedLanguage;
+
     private List<LanguageData>? availableLanguages = ILocalizationProvider.Current?.LocalizationData.Languages
         .OrderByDescending(x => x == ILocalizationProvider.Current.FollowSystem)
-        .ThenByDescending(x => CultureInfo.CurrentUICulture.TwoLetterISOLanguageName == x.Code || CultureInfo.InstalledUICulture.TwoLetterISOLanguageName == x.Code)
+        .ThenByDescending(x =>
+            CultureInfo.CurrentUICulture.TwoLetterISOLanguageName == x.Code ||
+            CultureInfo.InstalledUICulture.TwoLetterISOLanguageName == x.Code)
         .ThenBy(x => x.Name).ToList();
 
     private bool isDebugModeEnabled = GetPreference(nameof(IsDebugModeEnabled), false);
+
     public bool IsDebugModeEnabled
     {
         get => isDebugModeEnabled;
         set => RaiseAndUpdatePreference(ref isDebugModeEnabled, value);
     }
-    
+
     public List<LanguageData>? AvailableLanguages
     {
         get => availableLanguages;
@@ -40,10 +54,50 @@ internal class GeneralSettings : SettingsGroup
         }
     }
 
-    private bool isAnalyticsEnabled = GetPreference(PreferencesConstants.AnalyticsEnabled, PreferencesConstants.AnalyticsEnabledDefault);
+    private bool isAnalyticsEnabled =
+        GetPreference(PreferencesConstants.AnalyticsEnabled, PreferencesConstants.AnalyticsEnabledDefault);
+
     public bool AnalyticsEnabled
     {
         get => isAnalyticsEnabled;
         set => RaiseAndUpdatePreference(ref isAnalyticsEnabled, value);
     }
+
+    public List<string> AvailableRenderApis
+    {
+        get
+        {
+            if (availableRenderApis == null || availableRenderApis.Count == 0)
+            {
+                availableRenderApis = new List<string>(IOperatingSystem.Current?.GetAvailableRenderers() ?? []);
+            }
+
+            return availableRenderApis;
+        }
+    }
+
+    public string SelectedRenderApi
+    {
+        get => selectedRenderApi ?? AvailableRenderApis.FirstOrDefault() ?? string.Empty;
+        set
+        {
+            if (SetProperty(ref selectedRenderApi, value))
+            {
+                try
+                {
+                    RenderApiPreferenceManager.UpdateRenderApiPreference(value);
+                    OnPropertyChanged(nameof(RenderApiChangePending));
+                }
+                catch (Exception ex)
+                {
+                    NoticeDialog.Show(
+                        new LocalizedString("ERROR_SAVING_PREFERENCES_DESC", ex.Message),
+                        "ERROR_SAVING_PREFERENCES");
+                }
+            }
+        }
+    }
+
+    public bool RenderApiChangePending =>
+        selectedRenderApi != RenderApiPreferenceManager.FirstReadApiPreference;
 }

+ 21 - 9
src/PixiEditor/Views/Nodes/NodeGraphView.cs

@@ -87,8 +87,9 @@ internal class NodeGraphView : Zoombox.Zoombox
         AvaloniaProperty.Register<NodeGraphView, ICommand>(
             "CreateNodeFromContextCommand");
 
-    public static readonly StyledProperty<SocketsInfo> SocketsInfoProperty = AvaloniaProperty.Register<NodeGraphView, SocketsInfo>(
-        nameof(SocketsInfo));
+    public static readonly StyledProperty<SocketsInfo> SocketsInfoProperty =
+        AvaloniaProperty.Register<NodeGraphView, SocketsInfo>(
+            nameof(SocketsInfo));
 
     public SocketsInfo SocketsInfo
     {
@@ -402,6 +403,11 @@ internal class NodeGraphView : Zoombox.Zoombox
 
     private void CreateNodeType(NodeTypeInfo nodeType)
     {
+        if (nodeType == null)
+        {
+            return;
+        }
+
         var type = nodeType.NodeType;
         if (CreateNodeCommand != null && CreateNodeCommand.CanExecute(type))
         {
@@ -589,7 +595,7 @@ internal class NodeGraphView : Zoombox.Zoombox
 
         if (e.Source is NodeView nodeView)
         {
-            UpdateConnections(/*nodeView*/);
+            UpdateConnections( /*nodeView*/);
         }
     }
 
@@ -639,7 +645,7 @@ internal class NodeGraphView : Zoombox.Zoombox
         if (e.Property == BoundsProperty)
         {
             NodeView nodeView = (NodeView)sender!;
-            UpdateConnections(/*nodeView*/);
+            UpdateConnections( /*nodeView*/);
         }
     }
 
@@ -673,7 +679,7 @@ internal class NodeGraphView : Zoombox.Zoombox
         startDragConnectionPoint = _previewConnectionLine.StartPoint;
     }
 
-    private void UpdateConnections(/*NodeView nodeView*/)
+    private void UpdateConnections( /*NodeView nodeView*/)
     {
         connectionRenderer?.InvalidateVisual();
         /*if (nodeView == null)
@@ -822,8 +828,10 @@ internal class NodeGraphView : Zoombox.Zoombox
         {
             foreach (NodeConnectionViewModel connection in e.NewItems)
             {
-                SocketsInfo.Sockets[$"i:{connection.InputNode.Id}.{connection.InputProperty.PropertyName}"] = connection.InputProperty;
-                SocketsInfo.Sockets[$"o:{connection.OutputNode.Id}.{connection.OutputProperty.PropertyName}"] = connection.OutputProperty;
+                SocketsInfo.Sockets[$"i:{connection.InputNode.Id}.{connection.InputProperty.PropertyName}"] =
+                    connection.InputProperty;
+                SocketsInfo.Sockets[$"o:{connection.OutputNode.Id}.{connection.OutputProperty.PropertyName}"] =
+                    connection.OutputProperty;
             }
         }
         else if (e.Action == NotifyCollectionChangedAction.Reset)
@@ -841,14 +849,18 @@ internal class NodeGraphView : Zoombox.Zoombox
         {
             e.OldValue.Value.Connections.CollectionChanged += nodeGraph.OnConnectionsChanged;
         }
+
         if (e.NewValue.Value != null)
         {
             e.NewValue.Value.Connections.CollectionChanged += nodeGraph.OnConnectionsChanged;
             nodeGraph.SocketsInfo.Sockets.Clear();
             foreach (var connection in e.NewValue.Value.Connections)
             {
-                nodeGraph.SocketsInfo.Sockets[$"i:{connection.InputNode.Id}.{connection.InputProperty.PropertyName}"] = connection.InputProperty;
-                nodeGraph.SocketsInfo.Sockets[$"o:{connection.OutputNode.Id}.{connection.OutputProperty.PropertyName}"] = connection.OutputProperty;
+                nodeGraph.SocketsInfo.Sockets[$"i:{connection.InputNode.Id}.{connection.InputProperty.PropertyName}"] =
+                    connection.InputProperty;
+                nodeGraph.SocketsInfo.Sockets
+                        [$"o:{connection.OutputNode.Id}.{connection.OutputProperty.PropertyName}"] =
+                    connection.OutputProperty;
             }
         }
     }

+ 21 - 1
src/PixiEditor/Views/Rendering/Scene.cs

@@ -1,5 +1,6 @@
 using System.Collections.ObjectModel;
 using System.Collections.Specialized;
+using System.Globalization;
 using Avalonia;
 using Avalonia.Animation;
 using Avalonia.Input;
@@ -741,11 +742,30 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
     protected (bool success, string info) InitializeGraphicsResources(Compositor targetCompositor,
         CompositionDrawingSurface compositionDrawingSurface, ICompositionGpuInterop interop)
     {
-        resources = IDrawieInteropContext.Current.CreateResources(compositionDrawingSurface, interop);
+        try
+        {
+            resources = IDrawieInteropContext.Current.CreateResources(compositionDrawingSurface, interop);
+        }
+        catch (Exception e)
+        {
+            return (false, new LocalizedString("ERROR_GPU_RESOURCES_CREATION", e.Message));
+        }
 
         return (true, string.Empty);
     }
 
+    public override void Render(DrawingContext context)
+    {
+        if (!string.IsNullOrEmpty(info))
+        {
+            Point center = new Point(Bounds.Width / 2, Bounds.Height / 2);
+            context.DrawText(
+                new FormattedText(info, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, Typeface.Default, 12,
+                    Brushes.White),
+                center);
+        }
+    }
+
     protected async Task FreeGraphicsResources()
     {
         renderTexture?.Dispose();

+ 34 - 10
src/PixiEditor/Views/Windows/Settings/SettingsWindow.axaml

@@ -192,6 +192,30 @@
                                       IsChecked="{Binding SettingsSubViewModel.Tools.EnableSharedToolbar}"
                                       ui:Translator.Key="ENABLE_SHARED_TOOLBAR" />
 
+                            <TextBlock ui:Translator.Key="Rendering" Classes="h5" />
+
+                            <Label Target="primaryToolsetComboBox" ui:Translator.Key="PREFERRED_RENDERER"
+                                   VerticalAlignment="Center" />
+
+                            <StackPanel Orientation="Horizontal" Spacing="5">
+                                <ComboBox Classes="leftOffset" Width="200" HorizontalAlignment="Left"
+                                          ItemsSource="{Binding SettingsSubViewModel.General.AvailableRenderApis}"
+                                          SelectedItem="{Binding SettingsSubViewModel.General.SelectedRenderApi, Mode=TwoWay}">
+                                    <ComboBox.ItemTemplate>
+                                        <DataTemplate>
+                                            <TextBlock ui:Translator.Key="{Binding}" />
+                                        </DataTemplate>
+                                    </ComboBox.ItemTemplate>
+                                </ComboBox>
+                                <Button Command="{cmds:Command Name=PixiEditor.Restart}"
+                                        Background="{DynamicResource ThemeAccentBrush}"
+                                        IsVisible="{Binding SettingsSubViewModel.General.RenderApiChangePending}"
+                                        d:Content="Restart to apply changes"
+                                        ui:Translator.Key="RESTART"
+                                        HorizontalAlignment="Left"
+                                        VerticalAlignment="Center" />
+                            </StackPanel>
+
                             <TextBlock ui:Translator.Key="DEBUG" Classes="h5" />
                             <CheckBox Classes="leftOffset"
                                       IsChecked="{Binding SettingsSubViewModel.General.IsDebugModeEnabled}"
@@ -322,12 +346,12 @@
                                               ItemsSource="{Binding SettingsSubViewModel.Update.UpdateChannels}"
                                               SelectedValue="{Binding SettingsSubViewModel.Update.UpdateChannelName}" />
                                     <TextBlock Cursor="Help"
-                                           Classes="pixi-icon"
-                                           Text="{DynamicResource icon-help}"
-                                           VerticalAlignment="Center"
-                                           ToolTip.ShowDelay="0"
-                                           IsVisible="{Binding !ShowUpdateTab}"
-                                           ui:Translator.TooltipKey="UPDATE_CHANNEL_HELP_TOOLTIP" />
+                                               Classes="pixi-icon"
+                                               Text="{DynamicResource icon-help}"
+                                               VerticalAlignment="Center"
+                                               ToolTip.ShowDelay="0"
+                                               IsVisible="{Binding !ShowUpdateTab}"
+                                               ui:Translator.TooltipKey="UPDATE_CHANNEL_HELP_TOOLTIP" />
                                     <!-- ToolTipService.InitialShowDelay="0"-->
                                 </StackPanel>
                             </StackPanel>
@@ -373,12 +397,12 @@
                             <StackPanel Spacing="5" Orientation="Horizontal" Classes="leftOffset">
                                 <Label Content="X" />
                                 <controls:SizeInput MinSize="0.5" Decimals="1"
-                                                 Size="{Binding SettingsSubViewModel.Scene.CustomBackgroundScaleX, Mode=TwoWay}"
-                                                 HorizontalAlignment="Left" />
+                                                    Size="{Binding SettingsSubViewModel.Scene.CustomBackgroundScaleX, Mode=TwoWay}"
+                                                    HorizontalAlignment="Left" />
                                 <Label Content="Y" />
                                 <controls:SizeInput MinSize="0.5" Decimals="1"
-                                                 Size="{Binding SettingsSubViewModel.Scene.CustomBackgroundScaleY, Mode=TwoWay}"
-                                                 HorizontalAlignment="Left" />
+                                                    Size="{Binding SettingsSubViewModel.Scene.CustomBackgroundScaleY, Mode=TwoWay}"
+                                                    HorizontalAlignment="Left" />
                             </StackPanel>
 
                             <StackPanel Orientation="Horizontal" Spacing="5">