diff --git a/StarterNG/Infrastructure/SystemGraphicsLibraries.cs b/StarterNG/Infrastructure/SystemGraphicsLibraries.cs
new file mode 100644
index 0000000..2aa2f3a
--- /dev/null
+++ b/StarterNG/Infrastructure/SystemGraphicsLibraries.cs
@@ -0,0 +1,76 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+namespace StarterNG.Infrastructure;
+
+///
+/// The starter sits in the simulator's folder, and so do the graphics wrappers a player
+/// puts there for the simulator - ReShade and the like, named after the system library
+/// they stand in for. Windows looks in the program's own folder first, so the starter's
+/// window drew through them too, and with some of them it drew nothing at all.
+/// Loaded from System32 by full path before anything asks for them, these names are
+/// taken: a native library that asks by name gets the system one. .NET looks beside the
+/// program by full path before it asks by name, so the window code's own imports are
+/// handed the same libraries. The simulator is a process of its own and still finds its
+/// wrappers.
+/// Elsewhere there is nothing of the kind to keep out. Under Wine or Proton the system
+/// folder is the prefix's, so what is taken there is what would have been taken anyway.
+///
+public static class SystemGraphicsLibraries
+{
+ // dxgi ahead of d3d11, which asks for it.
+ private static readonly string[] Names = { "dxgi", "d3d11", "d3d9", "opengl32" };
+
+ private static readonly Dictionary Loaded = new(StringComparer.OrdinalIgnoreCase);
+ private static readonly List Shadowed = new();
+
+ /// The assemblies whose imports of these libraries are to get the system ones.
+ public static void LoadFirst(params Assembly[] importers)
+ {
+ if (!OperatingSystem.IsWindows())
+ return;
+
+ string system = Environment.SystemDirectory;
+ string own = AppContext.BaseDirectory;
+
+ foreach (string name in Names)
+ {
+ string file = name + ".dll";
+ if (File.Exists(Path.Combine(own, file)))
+ Shadowed.Add(file);
+ if (NativeLibrary.TryLoad(Path.Combine(system, file), out IntPtr handle))
+ Loaded[name] = handle;
+ }
+
+ foreach (var importer in importers)
+ {
+ try
+ {
+ NativeLibrary.SetDllImportResolver(importer, Resolve);
+ }
+ catch (InvalidOperationException)
+ {
+ // It has a resolver of its own already; that one decides.
+ }
+ }
+ }
+
+ private static IntPtr Resolve(string libraryName, Assembly assembly, DllImportSearchPath? searchPath)
+ {
+ string name = libraryName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)
+ ? libraryName[..^4]
+ : libraryName;
+ // Zero leaves the library to the usual search.
+ return Loaded.TryGetValue(name, out IntPtr handle) ? handle : IntPtr.Zero;
+ }
+
+ /// Which of them lay beside the starter, once the log can take it.
+ public static void LogShadowed()
+ {
+ if (Shadowed.Count > 0)
+ Diagnostics.Log($"Graphics libraries beside the starter, left to the simulator: {string.Join(", ", Shadowed)}");
+ }
+}
diff --git a/StarterNG/Program.cs b/StarterNG/Program.cs
index 2c1beca..da02172 100644
--- a/StarterNG/Program.cs
+++ b/StarterNG/Program.cs
@@ -1,6 +1,7 @@
using Avalonia;
using Avalonia.Threading;
using System;
+using System.Linq;
using System.Threading.Tasks;
using StarterNG.Application;
using StarterNG.Infrastructure;
@@ -10,12 +11,24 @@ namespace StarterNG;
static class Program
{
+ ///
+ /// Drawn by the processor instead of the graphics card, for a machine on which the
+ /// window comes up empty. Started with --software-render.
+ ///
+ private static bool _softwareRender;
+
[STAThread]
public static void Main(string[] args)
{
+ // Ahead of everything that could draw: what lies beside the starter is the simulator's.
+ SystemGraphicsLibraries.LoadFirst(typeof(Win32PlatformOptions).Assembly);
+
+ _softwareRender = args.Any(a => string.Equals(a, "--software-render", StringComparison.OrdinalIgnoreCase));
+
// Composition root first: everything below, including the crash handlers,
// resolves its collaborators from this graph.
AppServices.Initialize();
+ SystemGraphicsLibraries.LogShadowed();
AppDomain.CurrentDomain.UnhandledException += (_, e) =>
{
@@ -63,9 +76,25 @@ static class Program
};
}
+ // Each platform reads only its own options, so both can be given everywhere.
+ private static AppBuilder WithSoftwareRenderIfAsked(this AppBuilder builder) =>
+ _softwareRender
+ ? builder
+ .With(new Win32PlatformOptions
+ {
+ RenderingMode = new[] { Win32RenderingMode.Software },
+ CompositionMode = new[] { Win32CompositionMode.RedirectionSurface }
+ })
+ .With(new X11PlatformOptions
+ {
+ RenderingMode = new[] { X11RenderingMode.Software }
+ })
+ : builder;
+
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure()
.UsePlatformDetect()
+ .WithSoftwareRenderIfAsked()
.WithInterFont()
.WithDiagnosticsLogging()
.AfterSetup(_ => LogUiExceptions());