From 91c496ab4894d1e921038fe6bfd70bc3afae4265 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 15 Nov 2026 14:41:48 +0100 Subject: [PATCH] fix: graphics wrappers beside the starter no longer reach its window ReShade installed for Direct3D as dxgi.dll in the simulator's folder was loaded into the starter as well, and its window came up empty. The system dxgi, d3d11, d3d9 and opengl32 are loaded first, and the window code's own imports are handed those. The simulator still finds its wrappers. --software-render draws the window on the processor, on Windows and X11, for a machine on which it still comes up empty. --- .../Infrastructure/SystemGraphicsLibraries.cs | 76 +++++++++++++++++++ StarterNG/Program.cs | 29 +++++++ 2 files changed, 105 insertions(+) create mode 100644 StarterNG/Infrastructure/SystemGraphicsLibraries.cs 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());