Compare commits

...

2 Commits

Author SHA1 Message Date
maj00r
6e2dbf9eac test: the system graphics libraries and the software render switch
Taking the libraries and the switch's options are apart from Main, so they are
tested on every system, not only where Windows has them to load.
2026-11-15 17:30:13 +01:00
maj00r
91c496ab48 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.
2026-11-15 14:41:48 +01:00
5 changed files with 279 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
using Avalonia;
using StarterNG.Infrastructure;
namespace StarterNG.Tests;
public class SoftwareRenderTests
{
[Theory]
[InlineData("--software-render")]
[InlineData("--Software-Render")]
public void The_switch_asks_for_it_in_any_case(string arg)
{
Assert.True(SoftwareRender.AskedIn(new[] { "--other", arg }));
}
[Fact]
public void Without_the_switch_the_graphics_card_draws()
{
Assert.False(SoftwareRender.AskedIn(Array.Empty<string>()));
Assert.False(SoftwareRender.AskedIn(new[] { "--software" }));
}
[Fact]
public void Windows_draws_on_the_processor_into_a_plain_window_surface()
{
var options = SoftwareRender.Win32Options;
Assert.Equal(new[] { Win32RenderingMode.Software }, options.RenderingMode);
Assert.Equal(new[] { Win32CompositionMode.RedirectionSurface }, options.CompositionMode);
}
[Fact]
public void X11_draws_on_the_processor()
{
Assert.Equal(new[] { X11RenderingMode.Software }, SoftwareRender.X11Options.RenderingMode);
}
[Fact]
public void Both_platforms_options_go_to_the_same_builder()
{
var builder = AppBuilder.Configure<Avalonia.Application>();
Assert.Same(builder, SoftwareRender.ApplyTo(builder));
}
}

View File

@@ -0,0 +1,98 @@
using StarterNG.Infrastructure;
namespace StarterNG.Tests;
public class SystemGraphicsLibrariesTests
{
private const string System32 = @"C:\Windows\System32";
private const string Game = @"E:\SteamLibrary\steamapps\common\MaSzyna";
private static readonly IntPtr Dxgi = new(0x1000);
private static readonly IntPtr D3d11 = new(0x2000);
private static SystemGraphicsLibraries Take(Func<string, bool> beside, Dictionary<string, IntPtr> system)
{
return SystemGraphicsLibraries.Take(System32, Game, beside, Load);
bool Load(string path, out IntPtr handle)
{
handle = system.GetValueOrDefault(Path.GetFileName(path));
return handle != IntPtr.Zero;
}
}
private static SystemGraphicsLibraries WithDxgiAndD3d11(Func<string, bool> beside) =>
Take(beside, new Dictionary<string, IntPtr> { ["dxgi.dll"] = Dxgi, ["d3d11.dll"] = D3d11 });
[Fact]
public void An_import_by_name_gets_the_system_library_with_or_without_its_extension()
{
var taken = WithDxgiAndD3d11(_ => false);
Assert.Equal(Dxgi, taken.Resolve("dxgi"));
Assert.Equal(Dxgi, taken.Resolve("dxgi.dll"));
Assert.Equal(D3d11, taken.Resolve("D3D11.DLL"));
}
[Fact]
public void Other_libraries_and_ones_the_system_lacks_are_left_to_the_usual_search()
{
var taken = WithDxgiAndD3d11(_ => false);
Assert.Equal(IntPtr.Zero, taken.Resolve("user32.dll"));
Assert.Equal(IntPtr.Zero, taken.Resolve("opengl32"));
}
[Fact]
public void They_are_loaded_from_the_system_folder_dxgi_first()
{
var asked = new List<string>();
SystemGraphicsLibraries.Take(System32, Game, _ => false, Load);
Assert.Equal(SystemGraphicsLibraries.Names.Select(n => Path.Combine(System32, n + ".dll")), asked);
Assert.Equal("dxgi", SystemGraphicsLibraries.Names[0]);
bool Load(string path, out IntPtr handle)
{
asked.Add(path);
handle = IntPtr.Zero;
return false;
}
}
[Fact]
public void A_wrapper_beside_the_starter_is_noted()
{
var taken = WithDxgiAndD3d11(path => path == Path.Combine(Game, "dxgi.dll"));
Assert.Equal(new[] { "dxgi.dll" }, taken.Shadowed);
Assert.Contains("dxgi.dll", taken.Note);
}
[Fact]
public void With_nothing_beside_the_starter_there_is_nothing_to_note()
{
var taken = WithDxgiAndD3d11(_ => false);
Assert.Empty(taken.Shadowed);
Assert.Null(taken.Note);
}
[Fact]
public void Only_windows_has_system_libraries_to_take()
{
var taken = SystemGraphicsLibraries.TakeOnWindows();
Assert.Equal(OperatingSystem.IsWindows(), taken is not null);
}
[Fact]
public void An_assembly_with_a_resolver_of_its_own_keeps_it()
{
var taken = WithDxgiAndD3d11(_ => false);
var assembly = typeof(SystemGraphicsLibrariesTests).Assembly;
Assert.True(taken.ServeTo(assembly));
Assert.False(taken.ServeTo(assembly));
}
}

View File

@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia;
namespace StarterNG.Infrastructure;
/// <summary>
/// Drawn by the processor instead of the graphics card, for a machine on which the
/// window comes up empty. Each platform reads only its own options, so both are given.
/// </summary>
public static class SoftwareRender
{
public const string Switch = "--software-render";
public static bool AskedIn(IEnumerable<string> args) =>
args.Any(a => string.Equals(a, Switch, StringComparison.OrdinalIgnoreCase));
public static Win32PlatformOptions Win32Options => new()
{
RenderingMode = new[] { Win32RenderingMode.Software },
CompositionMode = new[] { Win32CompositionMode.RedirectionSurface }
};
public static X11PlatformOptions X11Options => new()
{
RenderingMode = new[] { X11RenderingMode.Software }
};
public static AppBuilder ApplyTo(AppBuilder builder) =>
builder.With(Win32Options).With(X11Options);
}

View File

@@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
namespace StarterNG.Infrastructure;
/// <summary>
/// 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.
/// </summary>
public sealed class SystemGraphicsLibraries
{
public delegate bool Loader(string path, out IntPtr handle);
// dxgi ahead of d3d11, which asks for it.
public static readonly IReadOnlyList<string> Names = new[] { "dxgi", "d3d11", "d3d9", "opengl32" };
private readonly Dictionary<string, IntPtr> _loaded = new(StringComparer.OrdinalIgnoreCase);
private readonly List<string> _shadowed = new();
private SystemGraphicsLibraries()
{
}
/// <summary>The files of these names that lay beside the starter.</summary>
public IReadOnlyList<string> Shadowed => _shadowed;
/// <summary>What goes to the log about them, or null when none lay there.</summary>
public string? Note => _shadowed.Count > 0
? $"Graphics libraries beside the starter, left to the simulator: {string.Join(", ", _shadowed)}"
: null;
/// <summary>The libraries of this Windows, or null on any other system.</summary>
public static SystemGraphicsLibraries? TakeOnWindows() =>
OperatingSystem.IsWindows()
? Take(Environment.SystemDirectory, AppContext.BaseDirectory, File.Exists, NativeLibrary.TryLoad)
: null;
public static SystemGraphicsLibraries Take(string systemDirectory, string ownDirectory,
Func<string, bool> exists, Loader load)
{
var taken = new SystemGraphicsLibraries();
foreach (string name in Names)
{
string file = name + ".dll";
if (exists(Path.Combine(ownDirectory, file)))
taken._shadowed.Add(file);
if (load(Path.Combine(systemDirectory, file), out IntPtr handle))
taken._loaded[name] = handle;
}
return taken;
}
/// <summary>The system library for an import of one of these names; zero leaves it to the usual search.</summary>
public IntPtr Resolve(string libraryName)
{
string name = libraryName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)
? libraryName[..^4]
: libraryName;
return _loaded.TryGetValue(name, out IntPtr handle) ? handle : IntPtr.Zero;
}
/// <summary>
/// The assembly's imports of these names get the system libraries. False when it
/// has a resolver of its own already; that one decides.
/// </summary>
public bool ServeTo(Assembly importer)
{
try
{
NativeLibrary.SetDllImportResolver(importer, (name, _, _) => Resolve(name));
return true;
}
catch (InvalidOperationException)
{
return false;
}
}
}

View File

@@ -10,12 +10,22 @@ namespace StarterNG;
static class Program
{
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.
var graphics = SystemGraphicsLibraries.TakeOnWindows();
graphics?.ServeTo(typeof(Win32PlatformOptions).Assembly);
_softwareRender = SoftwareRender.AskedIn(args);
// Composition root first: everything below, including the crash handlers,
// resolves its collaborators from this graph.
AppServices.Initialize();
if (graphics?.Note is { } note)
Diagnostics.Log(note);
AppDomain.CurrentDomain.UnhandledException += (_, e) =>
{
@@ -63,9 +73,13 @@ static class Program
};
}
private static AppBuilder WithSoftwareRenderIfAsked(this AppBuilder builder) =>
_softwareRender ? SoftwareRender.ApplyTo(builder) : builder;
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithSoftwareRenderIfAsked()
.WithInterFont()
.WithDiagnosticsLogging()
.AfterSetup(_ => LogUiExceptions());