feat(linux): gate graphical backends and capabilities

This commit is contained in:
Erik 2026-07-27 12:26:58 +02:00
parent 07bb1c5a74
commit 11501d52ca
13 changed files with 1924 additions and 32 deletions

View file

@ -0,0 +1,261 @@
using System.Text.Json;
using AcDream.App.Platform;
namespace AcDream.App.Tests.Platform;
public sealed class GraphicalCapabilityRequirementsTests
{
[Fact]
public void SupportedModernContextPasses()
{
GraphicalCapabilityRecord capabilities = CreateSupported();
Assert.Empty(GraphicalCapabilityRequirements.Evaluate(capabilities));
}
[Theory]
[InlineData("bindless")]
[InlineData("draw-parameters")]
[InlineData("mdi")]
[InlineData("ssbo")]
[InlineData("timer")]
[InlineData("depth")]
[InlineData("stencil")]
[InlineData("srgb")]
[InlineData("keyboard")]
[InlineData("mouse")]
public void MissingMandatoryCapabilityIsRejected(string missing)
{
GraphicalCapabilityRecord capabilities = CreateSupported();
capabilities = missing switch
{
"bindless" => capabilities with
{
HasBindlessTexture = false,
},
"draw-parameters" => capabilities with
{
HasShaderDrawParameters = false,
},
"mdi" => capabilities with
{
HasMultiDrawIndirect = false,
},
"ssbo" => capabilities with
{
HasShaderStorageBuffer = false,
},
"timer" => capabilities with
{
HasTimerQuery = false,
},
"depth" => capabilities with
{
Framebuffer = capabilities.Framebuffer with
{
DepthBits = 16,
},
},
"stencil" => capabilities with
{
Framebuffer = capabilities.Framebuffer with
{
StencilBits = 0,
},
},
"srgb" => capabilities with
{
Framebuffer = capabilities.Framebuffer with
{
FramebufferSrgbApi = false,
},
},
"keyboard" => capabilities with
{
Input = capabilities.Input with
{
KeyboardCount = 0,
},
},
"mouse" => capabilities with
{
Input = capabilities.Input with
{
MouseCount = 0,
},
},
_ => throw new ArgumentOutOfRangeException(nameof(missing)),
};
Assert.NotEmpty(GraphicalCapabilityRequirements.Evaluate(capabilities));
}
[Fact]
public void AdvertisedBufferStorageRequiresSuccessfulPersistentProbe()
{
GraphicalCapabilityRecord capabilities = CreateSupported() with
{
FunctionProbe = CreateSupported().FunctionProbe with
{
PersistentBufferStorage = false,
},
};
Assert.Contains(
GraphicalCapabilityRequirements.Evaluate(capabilities),
failure => failure.Contains(
"persistent mapping",
StringComparison.Ordinal));
}
[Fact]
public void MissingOptionalBufferStorageDoesNotRequireProbe()
{
GraphicalCapabilityRecord capabilities = CreateSupported() with
{
HasBufferStorage = false,
FunctionProbe = CreateSupported().FunctionProbe with
{
PersistentBufferStorage = null,
},
};
Assert.Empty(GraphicalCapabilityRequirements.Evaluate(capabilities));
}
[Fact]
public void UnsupportedMessageNamesDriverProtocolFailureAndReport()
{
GraphicalCapabilityRecord capabilities = CreateSupported() with
{
SupportFailures = ["GL_ARB_bindless_texture is required."],
};
string message = GraphicalCapabilityGuard.FormatUnsupportedMessage(
capabilities,
"capabilities.json");
Assert.Contains("Mesa", message);
Assert.Contains("RadeonSI", message);
Assert.Contains("Wayland", message);
Assert.Contains("GL_ARB_bindless_texture", message);
Assert.Contains(
Path.GetFullPath("capabilities.json"),
message);
}
[Fact]
public void ReportWriterAtomicallyOverwritesJson()
{
string directory = Path.Combine(
Path.GetTempPath(),
"acdream-capability-tests",
Guid.NewGuid().ToString("N"));
string path = Path.Combine(directory, "capabilities.json");
try
{
GraphicalCapabilityReportWriter.Write(path, CreateSupported());
GraphicalCapabilityReportWriter.Write(
path,
CreateSupported() with
{
GlRenderer = "second renderer",
});
using JsonDocument report = JsonDocument.Parse(
File.ReadAllText(path));
Assert.Equal(
"second renderer",
report.RootElement
.GetProperty(nameof(GraphicalCapabilityRecord.GlRenderer))
.GetString());
Assert.Equal(
"Wayland",
report.RootElement
.GetProperty(nameof(
GraphicalCapabilityRecord.ActiveDisplayProtocol))
.GetString());
Assert.False(File.Exists(path + ".tmp"));
}
finally
{
if (Directory.Exists(directory))
Directory.Delete(directory, recursive: true);
}
}
private static GraphicalCapabilityRecord CreateSupported() => new(
DateTimeOffset.UnixEpoch,
"linux-x64",
GraphicalHostOperatingSystem.Linux,
GraphicalDisplayProtocol.Wayland,
GraphicalDisplayProtocol.Wayland,
"test",
"Silk.NET.Windowing.Glfw",
"3.4.0",
"Mesa",
"RadeonSI",
"4.6",
"4.60",
4,
6,
1,
1,
HasBindlessTexture: true,
HasShaderDrawParameters: true,
HasMultiDrawIndirect: true,
HasShaderStorageBuffer: true,
HasBufferStorage: true,
HasTimerQuery: true,
MaximumShaderStorageBufferBindings: 16,
MaximumUniformBufferBindings: 72,
MaximumTextureSize: 16_384,
MaximumArrayTextureLayers: 2_048,
MaximumCombinedTextureImageUnits: 192,
new GraphicalFramebufferCapabilities(
8,
8,
8,
8,
24,
8,
1,
4,
FramebufferSrgbApi: true),
new GraphicalInputCapabilities(
KeyboardCount: 1,
MouseCount: 1,
GamepadCount: 0,
JoystickCount: 0),
new GraphicalWindowCapabilities(
1280,
720,
2560,
1440,
"test monitor",
144,
VSync: false),
new GraphicalAudioCapabilities(
Requested: false,
Available: false,
PlaybackSubmitted: false,
DisposalComplete: true,
Backend: "not requested"),
new GraphicalSmokeLifecycleCapabilities(
OwnedWindowCount: 1,
OwnedGlApiCount: 1,
OwnedInputContextCount: 1,
OwnedAudioEngineCount: 0,
ShutdownComplete: false),
[],
new GraphicalFunctionProbeResult(
BindlessTexture: true,
ShaderDrawParameters: true,
MultiDrawIndirect: true,
ShaderStorageBuffer: true,
TimerQuery: true,
SrgbFramebuffer: true,
PersistentBufferStorage: true,
Failures: []),
SupportFailures: []);
}

View file

@ -0,0 +1,109 @@
using AcDream.App.Platform;
namespace AcDream.App.Tests.Platform;
public sealed class GraphicalWindowBackendSelectionTests
{
[Fact]
public void WindowsAlwaysSelectsWin32()
{
GraphicalWindowBackendSelection selection =
GraphicalWindowBackendSelection.Resolve(
GraphicalHostOperatingSystem.Windows,
_ => "wayland");
Assert.Equal(
GraphicalDisplayProtocol.Windows,
selection.RequestedProtocol);
}
[Theory]
[InlineData("auto", "Automatic")]
[InlineData("x11", "X11")]
[InlineData("wayland", "Wayland")]
[InlineData(" WAYLAND ", "Wayland")]
public void ExplicitLinuxProtocolWins(
string configured,
string expected)
{
var environment = Environment(
(GraphicalWindowBackendSelection.EnvironmentVariable, configured),
("XDG_SESSION_TYPE", "x11"),
("DISPLAY", ":0"));
GraphicalWindowBackendSelection selection =
GraphicalWindowBackendSelection.Resolve(
GraphicalHostOperatingSystem.Linux,
environment);
Assert.Equal(expected, selection.RequestedProtocol.ToString());
}
[Fact]
public void InvalidExplicitLinuxProtocolIsActionable()
{
var error = Assert.Throws<InvalidOperationException>(
() => GraphicalWindowBackendSelection.Resolve(
GraphicalHostOperatingSystem.Linux,
Environment(
(GraphicalWindowBackendSelection.EnvironmentVariable,
"mir"))));
Assert.Contains("auto, x11, or wayland", error.Message);
Assert.Contains("mir", error.Message);
}
[Theory]
[MemberData(nameof(AutomaticLinuxCases))]
public void LinuxEnvironmentSelectsExpectedProtocol(
(string Name, string? Value)[] variables,
string expected)
{
GraphicalWindowBackendSelection selection =
GraphicalWindowBackendSelection.Resolve(
GraphicalHostOperatingSystem.Linux,
Environment(variables));
Assert.Equal(expected, selection.RequestedProtocol.ToString());
}
public static TheoryData<
(string Name, string? Value)[],
string> AutomaticLinuxCases => new()
{
{
[
("XDG_SESSION_TYPE", "wayland"),
("WAYLAND_DISPLAY", "wayland-0"),
("DISPLAY", ":0"),
],
"Wayland"
},
{
[
("XDG_SESSION_TYPE", "x11"),
("WAYLAND_DISPLAY", "wayland-0"),
("DISPLAY", ":0"),
],
"X11"
},
{
[("WAYLAND_DISPLAY", "wayland-0")],
"Wayland"
},
{
[],
"Automatic"
},
};
private static Func<string, string?> Environment(
params (string Name, string? Value)[] variables)
{
var values = variables.ToDictionary(
pair => pair.Name,
pair => pair.Value,
StringComparer.Ordinal);
return name => values.GetValueOrDefault(name);
}
}

View file

@ -5,6 +5,24 @@ namespace AcDream.App.Tests.Studio;
public class StudioWindowTests
{
[Fact]
public void FrameCloseGate_defersCloseUntilFrameCompletion()
{
var gate = new StudioFrameCloseGate();
int closeCount = 0;
gate.Request();
Assert.True(gate.IsRequested);
Assert.Equal(0, closeCount);
gate.CompleteFrame(() => closeCount++);
gate.CompleteFrame(() => closeCount++);
Assert.False(gate.IsRequested);
Assert.Equal(1, closeCount);
}
[Fact]
public void ParseMockup_doesNotDefaultToSinglePanelLayout()
{
@ -16,6 +34,22 @@ public class StudioWindowTests
Assert.Null(opts.MarkupPath);
}
[Fact]
public void ParseCapabilitySmokeOptions()
{
var opts = StudioOptions.Parse(
[
@"C:\fake-dats",
"--mockup",
"--capability-report",
"report.json",
"--audio-smoke",
]);
Assert.Equal("report.json", opts.CapabilityReportPath);
Assert.True(opts.AudioSmoke);
}
[Fact]
public void NormalizeSinglePanelRoot_movesDatPanelToCanvasOrigin()
{