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());