feat(linux): add graphical platform services
This commit is contained in:
parent
1628d9f587
commit
66f114b258
28 changed files with 1328 additions and 155 deletions
|
|
@ -178,6 +178,7 @@ public sealed class WorldRenderCompositionTests
|
|||
ImmediateGpuResourceRetirementQueue.Instance,
|
||||
_budgets,
|
||||
0xA9B4FFFFu,
|
||||
Path.Combine(Path.GetTempPath(), "acdream-tests"),
|
||||
_ => { }),
|
||||
Publication,
|
||||
Factory,
|
||||
|
|
@ -297,6 +298,7 @@ public sealed class WorldRenderCompositionTests
|
|||
IDatReaderWriter dats,
|
||||
BindlessSupport bindless,
|
||||
IGpuResourceRetirementQueue retirement,
|
||||
string diagnosticsDirectory,
|
||||
ResidencyBudgetOptions budgets)
|
||||
{
|
||||
TextureBudgets = budgets;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
using AcDream.App.Platform;
|
||||
|
||||
namespace AcDream.App.Tests.Platform;
|
||||
|
||||
public sealed class GraphicalHostPlatformServicesTests
|
||||
{
|
||||
[Fact]
|
||||
public void CurrentPlatformOwnsPathsPacingAndNativeClosure()
|
||||
{
|
||||
GraphicalHostPlatformServices platform =
|
||||
GraphicalHostPlatformServices.Resolve();
|
||||
|
||||
Assert.NotNull(platform.Paths);
|
||||
Assert.NotNull(platform.FramePacingWaiters);
|
||||
Assert.Equal(3, platform.NativeDependencies.Count);
|
||||
Assert.All(
|
||||
platform.NativeDependencies,
|
||||
dependency =>
|
||||
{
|
||||
Assert.False(string.IsNullOrWhiteSpace(dependency.Feature));
|
||||
Assert.False(string.IsNullOrWhiteSpace(
|
||||
dependency.PublishedFileName));
|
||||
});
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
Assert.Equal(
|
||||
GraphicalHostOperatingSystem.Windows,
|
||||
platform.OperatingSystem);
|
||||
Assert.StartsWith(
|
||||
"win-",
|
||||
platform.RuntimeIdentifier,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
platform.NativeDependencies,
|
||||
dependency =>
|
||||
dependency.PublishedFileName == "glfw3.dll");
|
||||
return;
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
Assert.Equal(
|
||||
GraphicalHostOperatingSystem.Linux,
|
||||
platform.OperatingSystem);
|
||||
Assert.Equal("linux-x64", platform.RuntimeIdentifier);
|
||||
Assert.Contains(
|
||||
platform.NativeDependencies,
|
||||
dependency =>
|
||||
dependency.PublishedFileName == "libglfw.so.3");
|
||||
return;
|
||||
}
|
||||
|
||||
throw new PlatformNotSupportedException();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
using AcDream.App.Platform;
|
||||
using AcDream.Runtime.Platform;
|
||||
|
||||
namespace AcDream.App.Tests.Platform;
|
||||
|
||||
public sealed class GraphicalLegacyConfigurationMigratorTests
|
||||
: IDisposable
|
||||
{
|
||||
private readonly string _root = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
"acdream-l0-migration-" + Guid.NewGuid().ToString("N"));
|
||||
|
||||
[Fact]
|
||||
public void MissingCanonicalFilesAreCopiedWithoutOverwriting()
|
||||
{
|
||||
string legacy = Path.Combine(_root, "legacy");
|
||||
string canonical = Path.Combine(_root, "canonical");
|
||||
Directory.CreateDirectory(legacy);
|
||||
Directory.CreateDirectory(canonical);
|
||||
File.WriteAllText(
|
||||
Path.Combine(legacy, "settings.json"),
|
||||
"legacy-settings");
|
||||
File.WriteAllText(
|
||||
Path.Combine(legacy, "keybinds.json"),
|
||||
"legacy-keybinds");
|
||||
File.WriteAllText(
|
||||
Path.Combine(canonical, "keybinds.json"),
|
||||
"canonical-keybinds");
|
||||
var paths = new ApplicationPathSet(
|
||||
canonical,
|
||||
Path.Combine(_root, "data"),
|
||||
Path.Combine(_root, "cache"),
|
||||
legacy);
|
||||
|
||||
IReadOnlyList<string> migrated =
|
||||
GraphicalLegacyConfigurationMigrator.Migrate(paths);
|
||||
|
||||
Assert.Single(migrated);
|
||||
Assert.Equal(
|
||||
"legacy-settings",
|
||||
File.ReadAllText(paths.SettingsFile));
|
||||
Assert.Equal(
|
||||
"canonical-keybinds",
|
||||
File.ReadAllText(paths.KeyBindingsFile));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoLegacyDirectoryProducesNoWrites()
|
||||
{
|
||||
var paths = new ApplicationPathSet(
|
||||
Path.Combine(_root, "canonical"),
|
||||
Path.Combine(_root, "data"),
|
||||
Path.Combine(_root, "cache"),
|
||||
LegacyConfigDirectory: null);
|
||||
|
||||
Assert.Empty(
|
||||
GraphicalLegacyConfigurationMigrator.Migrate(paths));
|
||||
Assert.False(Directory.Exists(paths.ConfigDirectory));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(_root))
|
||||
Directory.Delete(_root, recursive: true);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
using System.Diagnostics;
|
||||
using AcDream.App.Rendering;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
||||
public sealed class LinuxMonotonicFramePacingWaiterTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(1L, 1_000_000_000L, 1L)]
|
||||
[InlineData(1L, 3L, 333_333_334L)]
|
||||
[InlineData(15L, 1_000L, 15_000_000L)]
|
||||
[InlineData(6_060L, 1_000_000L, 6_060_000L)]
|
||||
public void TickConversionRoundsUpWithoutReturningZero(
|
||||
long ticks,
|
||||
long frequency,
|
||||
long expected)
|
||||
{
|
||||
Assert.Equal(
|
||||
expected,
|
||||
LinuxMonotonicFramePacingWaiter
|
||||
.ConvertTicksToNanoseconds(ticks, frequency));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlatformFactorySelectsCurrentOperatingSystem()
|
||||
{
|
||||
IFramePacingWaiter waiter =
|
||||
PlatformFramePacingWaiterFactory
|
||||
.ForCurrentProcess()
|
||||
.Create();
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
Assert.IsType<WindowsHighResolutionFramePacingWaiter>(
|
||||
waiter);
|
||||
((IDisposable)waiter).Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
Assert.IsType<LinuxMonotonicFramePacingWaiter>(waiter);
|
||||
((IDisposable)waiter).Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
throw new PlatformNotSupportedException();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LinuxWaitBlocksUntilRequestedMonotonicDeadline()
|
||||
{
|
||||
if (!OperatingSystem.IsLinux())
|
||||
return;
|
||||
|
||||
LinuxMonotonicFramePacingWaiter waiter =
|
||||
LinuxMonotonicFramePacingWaiter.Create();
|
||||
long start = Stopwatch.GetTimestamp();
|
||||
waiter.Wait(
|
||||
durationTicks: Stopwatch.Frequency / 200L,
|
||||
clockFrequency: Stopwatch.Frequency);
|
||||
TimeSpan elapsed = Stopwatch.GetElapsedTime(start);
|
||||
|
||||
Assert.True(
|
||||
elapsed >= TimeSpan.FromMilliseconds(4),
|
||||
$"Linux monotonic wait returned after {elapsed.TotalMilliseconds:F3} ms.");
|
||||
Assert.True(
|
||||
elapsed < TimeSpan.FromSeconds(1),
|
||||
$"Linux monotonic wait stalled for {elapsed.TotalMilliseconds:F3} ms.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LinuxWaiterRejectsUseAfterDisposal()
|
||||
{
|
||||
if (!OperatingSystem.IsLinux())
|
||||
return;
|
||||
|
||||
LinuxMonotonicFramePacingWaiter waiter =
|
||||
LinuxMonotonicFramePacingWaiter.Create();
|
||||
waiter.Dispose();
|
||||
|
||||
Assert.Throws<ObjectDisposedException>(
|
||||
() => waiter.Wait(1, Stopwatch.Frequency));
|
||||
}
|
||||
}
|
||||
123
tests/AcDream.App.Tests/Rendering/LinuxPlatformBoundaryTests.cs
Normal file
123
tests/AcDream.App.Tests/Rendering/LinuxPlatformBoundaryTests.cs
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
namespace AcDream.App.Tests.Rendering;
|
||||
|
||||
public sealed class LinuxPlatformBoundaryTests
|
||||
{
|
||||
[Fact]
|
||||
public void FramePacingSelectsPlatformWaiterOnlyAtFactoryBoundary()
|
||||
{
|
||||
string app = AppSourceRoot();
|
||||
string controller = File.ReadAllText(Path.Combine(
|
||||
app,
|
||||
"Rendering",
|
||||
"FramePacingController.cs"));
|
||||
Assert.Contains(
|
||||
"PlatformFramePacingWaiterFactory.ForCurrentProcess()",
|
||||
controller,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
"WindowsHighResolutionFramePacingWaiter.Create()",
|
||||
controller,
|
||||
StringComparison.Ordinal);
|
||||
|
||||
string[] windowsImports = Directory
|
||||
.EnumerateFiles(app, "*.cs", SearchOption.AllDirectories)
|
||||
.Where(path => File.ReadAllText(path).Contains(
|
||||
"LibraryImport(\"kernel32.dll\"",
|
||||
StringComparison.Ordinal))
|
||||
.Select(path => Path.GetFileName(path)!)
|
||||
.ToArray();
|
||||
|
||||
Assert.Equal(
|
||||
["WindowsHighResolutionFramePacingWaiter.cs"],
|
||||
windowsImports);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GraphicalFeatureCodeDoesNotResolveWindowsLocalAppData()
|
||||
{
|
||||
string root = RepositoryRoot();
|
||||
string[] productionFiles =
|
||||
[
|
||||
.. Directory.EnumerateFiles(
|
||||
Path.Combine(root, "src", "AcDream.App"),
|
||||
"*.cs",
|
||||
SearchOption.AllDirectories),
|
||||
.. Directory.EnumerateFiles(
|
||||
Path.Combine(root, "src", "AcDream.UI.Abstractions"),
|
||||
"*.cs",
|
||||
SearchOption.AllDirectories),
|
||||
];
|
||||
|
||||
string[] offenders = productionFiles
|
||||
.Where(path => File.ReadAllText(path).Contains(
|
||||
"LocalApplicationData",
|
||||
StringComparison.Ordinal))
|
||||
.Select(path => Path.GetRelativePath(root, path))
|
||||
.ToArray();
|
||||
|
||||
Assert.Empty(offenders);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OperatingSystemChecksRemainInsidePlatformOwners()
|
||||
{
|
||||
string app = AppSourceRoot();
|
||||
string[] offenders = Directory
|
||||
.EnumerateFiles(app, "*.cs", SearchOption.AllDirectories)
|
||||
.Where(path => File.ReadAllText(path).Contains(
|
||||
"OperatingSystem.Is",
|
||||
StringComparison.Ordinal))
|
||||
.Where(path =>
|
||||
{
|
||||
string relative = Path.GetRelativePath(app, path)
|
||||
.Replace('\\', '/');
|
||||
return !relative.StartsWith(
|
||||
"Platform/",
|
||||
StringComparison.Ordinal)
|
||||
&& relative is not
|
||||
"Rendering/FramePacingWaiterFactory.cs"
|
||||
&& relative is not
|
||||
"Rendering/LinuxMonotonicFramePacingWaiter.cs"
|
||||
&& relative is not
|
||||
"Rendering/WindowsHighResolutionFramePacingWaiter.cs";
|
||||
})
|
||||
.Select(path => Path.GetRelativePath(app, path))
|
||||
.ToArray();
|
||||
|
||||
Assert.Empty(offenders);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SmokePluginCopyUsesRidAwarePortableBuildAndPublishPaths()
|
||||
{
|
||||
string project = File.ReadAllText(Path.Combine(
|
||||
AppSourceRoot(),
|
||||
"AcDream.App.csproj"));
|
||||
|
||||
Assert.Contains("$(TargetFramework)", project, StringComparison.Ordinal);
|
||||
Assert.Contains("$(RuntimeIdentifier)", project, StringComparison.Ordinal);
|
||||
Assert.Contains("$(OutputPath)plugins/", project, StringComparison.Ordinal);
|
||||
Assert.Contains("$(PublishDir)plugins/", project, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
@"bin\$(Configuration)\net10.0",
|
||||
project,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string AppSourceRoot() =>
|
||||
Path.Combine(RepositoryRoot(), "src", "AcDream.App");
|
||||
|
||||
private static string RepositoryRoot()
|
||||
{
|
||||
DirectoryInfo? directory = new(AppContext.BaseDirectory);
|
||||
while (directory is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
|
||||
return directory.FullName;
|
||||
directory = directory.Parent;
|
||||
}
|
||||
|
||||
throw new DirectoryNotFoundException(
|
||||
"Could not locate repository root from test output.");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue