feat(linux): add graphical platform services
This commit is contained in:
parent
1628d9f587
commit
66f114b258
28 changed files with 1328 additions and 155 deletions
|
|
@ -63,18 +63,42 @@
|
|||
<SkipGetTargetFrameworkProperties>true</SkipGetTargetFrameworkProperties>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Target Name="CopySmokePluginToPluginsDir" AfterTargets="Build">
|
||||
<Target
|
||||
Name="CopySmokePluginToBuildOutput"
|
||||
AfterTargets="Build"
|
||||
Condition="'$(IsCrossTargetingBuild)' != 'true'">
|
||||
<PropertyGroup>
|
||||
<_SmokePluginSourceDir>..\AcDream.Plugins.Smoke\bin\$(Configuration)\net10.0</_SmokePluginSourceDir>
|
||||
<_SmokePluginDestDir>$(OutputPath)plugins\AcDream.Plugins.Smoke</_SmokePluginDestDir>
|
||||
<_SmokePluginSourceDir>$(MSBuildProjectDirectory)/../AcDream.Plugins.Smoke/bin/$(Configuration)/$(TargetFramework)</_SmokePluginSourceDir>
|
||||
<_SmokePluginSourceDir Condition="'$(RuntimeIdentifier)' != ''">$(_SmokePluginSourceDir)/$(RuntimeIdentifier)</_SmokePluginSourceDir>
|
||||
<_SmokePluginDestDir>$(OutputPath)plugins/AcDream.Plugins.Smoke</_SmokePluginDestDir>
|
||||
</PropertyGroup>
|
||||
<MakeDir Directories="$(_SmokePluginDestDir)" />
|
||||
<Copy
|
||||
SourceFiles="$(_SmokePluginSourceDir)\AcDream.Plugins.Smoke.dll"
|
||||
SourceFiles="$(_SmokePluginSourceDir)/AcDream.Plugins.Smoke.dll"
|
||||
DestinationFolder="$(_SmokePluginDestDir)"
|
||||
SkipUnchangedFiles="true" />
|
||||
<WriteLinesToFile
|
||||
File="$(_SmokePluginDestDir)\plugin.json"
|
||||
File="$(_SmokePluginDestDir)/plugin.json"
|
||||
Overwrite="true"
|
||||
Lines="{ "id": "acdream.smoke", "displayName": "Smoke Plugin", "version": "0.1.0", "entryDll": "AcDream.Plugins.Smoke.dll", "apiVersion": 1 }" />
|
||||
</Target>
|
||||
|
||||
<Target
|
||||
Name="CopySmokePluginToPublishOutput"
|
||||
AfterTargets="Publish"
|
||||
Condition="'$(IsCrossTargetingBuild)' != 'true'">
|
||||
<PropertyGroup>
|
||||
<_SmokePluginPublishSourceDir>$(MSBuildProjectDirectory)/../AcDream.Plugins.Smoke/bin/$(Configuration)/$(TargetFramework)</_SmokePluginPublishSourceDir>
|
||||
<_SmokePluginPublishSourceDir Condition="'$(RuntimeIdentifier)' != ''">$(_SmokePluginPublishSourceDir)/$(RuntimeIdentifier)</_SmokePluginPublishSourceDir>
|
||||
<_SmokePluginPublishDestDir>$(PublishDir)plugins/AcDream.Plugins.Smoke</_SmokePluginPublishDestDir>
|
||||
</PropertyGroup>
|
||||
<MakeDir Directories="$(_SmokePluginPublishDestDir)" />
|
||||
<Copy
|
||||
SourceFiles="$(_SmokePluginPublishSourceDir)/AcDream.Plugins.Smoke.dll"
|
||||
DestinationFolder="$(_SmokePluginPublishDestDir)"
|
||||
SkipUnchangedFiles="true" />
|
||||
<WriteLinesToFile
|
||||
File="$(_SmokePluginPublishDestDir)/plugin.json"
|
||||
Overwrite="true"
|
||||
Lines="{ "id": "acdream.smoke", "displayName": "Smoke Plugin", "version": "0.1.0", "entryDll": "AcDream.Plugins.Smoke.dll", "apiVersion": 1 }" />
|
||||
</Target>
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ internal sealed record WorldRenderDependencies(
|
|||
IGpuResourceRetirementQueue ResourceRetirement,
|
||||
ResidencyBudgetOptions ResidencyBudgets,
|
||||
uint InitialCenterLandblockId,
|
||||
string DiagnosticsDirectory,
|
||||
Action<string> Log);
|
||||
|
||||
internal interface IGameWindowWorldRenderPublication
|
||||
|
|
@ -114,6 +115,7 @@ internal interface IWorldRenderCompositionFactory
|
|||
IDatReaderWriter dats,
|
||||
BindlessSupport bindless,
|
||||
IGpuResourceRetirementQueue retirement,
|
||||
string diagnosticsDirectory,
|
||||
ResidencyBudgetOptions budgets);
|
||||
void RegisterResidencySources(
|
||||
ResidencyManager manager,
|
||||
|
|
@ -293,8 +295,15 @@ internal sealed class RetailWorldRenderCompositionFactory
|
|||
IDatReaderWriter dats,
|
||||
BindlessSupport bindless,
|
||||
IGpuResourceRetirementQueue retirement,
|
||||
string diagnosticsDirectory,
|
||||
ResidencyBudgetOptions budgets) =>
|
||||
new(gl, dats, bindless, retirement, budgets);
|
||||
new(
|
||||
gl,
|
||||
dats,
|
||||
bindless,
|
||||
retirement,
|
||||
diagnosticsDirectory,
|
||||
budgets);
|
||||
|
||||
public void RegisterResidencySources(
|
||||
ResidencyManager manager,
|
||||
|
|
@ -527,6 +536,7 @@ internal sealed class WorldRenderCompositionPhase
|
|||
content.Dats,
|
||||
bindless,
|
||||
_dependencies.ResourceRetirement,
|
||||
_dependencies.DiagnosticsDirectory,
|
||||
residency.Budgets),
|
||||
_publication.PublishTextureCache,
|
||||
WorldRenderCompositionPoint.TextureCachePublished);
|
||||
|
|
|
|||
102
src/AcDream.App/Platform/GraphicalHostPlatformServices.cs
Normal file
102
src/AcDream.App/Platform/GraphicalHostPlatformServices.cs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
using System.Runtime.InteropServices;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.Runtime.Platform;
|
||||
|
||||
namespace AcDream.App.Platform;
|
||||
|
||||
internal enum GraphicalHostOperatingSystem
|
||||
{
|
||||
Windows,
|
||||
Linux,
|
||||
}
|
||||
|
||||
internal sealed record GraphicalNativeDependency(
|
||||
string Feature,
|
||||
string PublishedFileName);
|
||||
|
||||
internal sealed record GraphicalHostPlatformServices(
|
||||
GraphicalHostOperatingSystem OperatingSystem,
|
||||
Architecture ProcessArchitecture,
|
||||
string RuntimeIdentifier,
|
||||
ApplicationPathSet Paths,
|
||||
IFramePacingWaiterFactory FramePacingWaiters,
|
||||
IReadOnlyList<GraphicalNativeDependency> NativeDependencies)
|
||||
{
|
||||
internal static GraphicalHostPlatformServices Resolve()
|
||||
{
|
||||
GraphicalHostOperatingSystem operatingSystem =
|
||||
DetectOperatingSystem();
|
||||
Architecture architecture = RuntimeInformation.ProcessArchitecture;
|
||||
string runtimeIdentifier = ResolveRuntimeIdentifier(
|
||||
operatingSystem,
|
||||
architecture);
|
||||
|
||||
return new GraphicalHostPlatformServices(
|
||||
operatingSystem,
|
||||
architecture,
|
||||
runtimeIdentifier,
|
||||
ApplicationPathSet.Resolve(),
|
||||
new PlatformFramePacingWaiterFactory(operatingSystem),
|
||||
ResolveNativeDependencies(operatingSystem));
|
||||
}
|
||||
|
||||
internal static GraphicalHostOperatingSystem DetectOperatingSystem()
|
||||
{
|
||||
if (System.OperatingSystem.IsWindows())
|
||||
return GraphicalHostOperatingSystem.Windows;
|
||||
if (System.OperatingSystem.IsLinux())
|
||||
return GraphicalHostOperatingSystem.Linux;
|
||||
|
||||
throw new PlatformNotSupportedException(
|
||||
"acdream graphical hosting supports Windows and Linux.");
|
||||
}
|
||||
|
||||
private static string ResolveRuntimeIdentifier(
|
||||
GraphicalHostOperatingSystem operatingSystem,
|
||||
Architecture architecture)
|
||||
{
|
||||
string architectureName = architecture switch
|
||||
{
|
||||
Architecture.X64 => "x64",
|
||||
Architecture.Arm64 => "arm64",
|
||||
Architecture.X86 => "x86",
|
||||
Architecture.Arm => "arm",
|
||||
_ => throw new PlatformNotSupportedException(
|
||||
$"Unsupported graphical process architecture {architecture}."),
|
||||
};
|
||||
|
||||
if (operatingSystem == GraphicalHostOperatingSystem.Linux
|
||||
&& architecture != Architecture.X64)
|
||||
{
|
||||
throw new PlatformNotSupportedException(
|
||||
"The first acdream Linux graphical package supports linux-x64 only.");
|
||||
}
|
||||
|
||||
string operatingSystemName =
|
||||
operatingSystem == GraphicalHostOperatingSystem.Windows
|
||||
? "win"
|
||||
: "linux";
|
||||
return $"{operatingSystemName}-{architectureName}";
|
||||
}
|
||||
|
||||
private static IReadOnlyList<GraphicalNativeDependency>
|
||||
ResolveNativeDependencies(
|
||||
GraphicalHostOperatingSystem operatingSystem) =>
|
||||
operatingSystem switch
|
||||
{
|
||||
GraphicalHostOperatingSystem.Windows =>
|
||||
[
|
||||
new("window/input", "glfw3.dll"),
|
||||
new("developer UI", "cimgui.dll"),
|
||||
new("audio", "soft_oal.dll"),
|
||||
],
|
||||
GraphicalHostOperatingSystem.Linux =>
|
||||
[
|
||||
new("window/input", "libglfw.so.3"),
|
||||
new("developer UI", "libcimgui.so"),
|
||||
new("audio", "libopenal.so"),
|
||||
],
|
||||
_ => throw new ArgumentOutOfRangeException(
|
||||
nameof(operatingSystem)),
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
using AcDream.Runtime.Platform;
|
||||
|
||||
namespace AcDream.App.Platform;
|
||||
|
||||
internal static class GraphicalLegacyConfigurationMigrator
|
||||
{
|
||||
private static readonly string[] FileNames =
|
||||
[
|
||||
"settings.json",
|
||||
"keybinds.json",
|
||||
];
|
||||
|
||||
internal static IReadOnlyList<string> Migrate(
|
||||
ApplicationPathSet paths)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(paths);
|
||||
if (paths.LegacyConfigDirectory is not { } legacyDirectory
|
||||
|| PathsEqual(legacyDirectory, paths.ConfigDirectory))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
List<string>? migrated = null;
|
||||
foreach (string fileName in FileNames)
|
||||
{
|
||||
string source = Path.Combine(legacyDirectory, fileName);
|
||||
string destination = Path.Combine(
|
||||
paths.ConfigDirectory,
|
||||
fileName);
|
||||
if (!File.Exists(source) || File.Exists(destination))
|
||||
continue;
|
||||
|
||||
Directory.CreateDirectory(paths.ConfigDirectory);
|
||||
File.Copy(source, destination, overwrite: false);
|
||||
(migrated ??= []).Add(destination);
|
||||
}
|
||||
|
||||
return migrated ?? [];
|
||||
}
|
||||
|
||||
private static bool PathsEqual(string left, string right) =>
|
||||
string.Equals(
|
||||
Path.TrimEndingDirectorySeparator(
|
||||
Path.GetFullPath(left)),
|
||||
Path.TrimEndingDirectorySeparator(
|
||||
Path.GetFullPath(right)),
|
||||
OperatingSystem.IsWindows()
|
||||
? StringComparison.OrdinalIgnoreCase
|
||||
: StringComparison.Ordinal);
|
||||
}
|
||||
|
|
@ -1,13 +1,23 @@
|
|||
using AcDream.App;
|
||||
using AcDream.App.Plugins;
|
||||
using AcDream.App.Platform;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.Core.Plugins;
|
||||
using AcDream.Runtime.Platform;
|
||||
using Serilog;
|
||||
|
||||
GraphicalHostPlatformServices graphicalPlatform =
|
||||
GraphicalHostPlatformServices.Resolve();
|
||||
ApplicationPathSet applicationPaths = graphicalPlatform.Paths;
|
||||
IReadOnlyList<string> migratedConfigurationFiles =
|
||||
GraphicalLegacyConfigurationMigrator.Migrate(applicationPaths);
|
||||
|
||||
if (args.Length >= 1 && args[0] == "ui-studio")
|
||||
{
|
||||
var so = AcDream.App.Studio.StudioOptions.Parse(args[1..]);
|
||||
using var sw = new AcDream.App.Studio.StudioWindow(so);
|
||||
using var sw = new AcDream.App.Studio.StudioWindow(
|
||||
so,
|
||||
applicationPaths);
|
||||
sw.Run();
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -16,6 +26,20 @@ Log.Logger = new LoggerConfiguration()
|
|||
.MinimumLevel.Debug()
|
||||
.WriteTo.Console()
|
||||
.CreateLogger();
|
||||
foreach (string migratedConfigurationFile in migratedConfigurationFiles)
|
||||
{
|
||||
Log.Information(
|
||||
"migrated legacy graphical configuration to {Path}",
|
||||
migratedConfigurationFile);
|
||||
}
|
||||
Log.Information(
|
||||
"graphical platform {RuntimeIdentifier}; native closure: {NativeDependencies}",
|
||||
graphicalPlatform.RuntimeIdentifier,
|
||||
string.Join(
|
||||
", ",
|
||||
graphicalPlatform.NativeDependencies.Select(
|
||||
dependency =>
|
||||
$"{dependency.Feature}={dependency.PublishedFileName}")));
|
||||
|
||||
var datDir = args.FirstOrDefault() ?? Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR");
|
||||
if (string.IsNullOrWhiteSpace(datDir))
|
||||
|
|
@ -36,7 +60,8 @@ using var window = new GameWindow(
|
|||
runtimeOptions,
|
||||
worldGameState,
|
||||
worldEvents,
|
||||
uiRegistry);
|
||||
uiRegistry,
|
||||
graphicalPlatform);
|
||||
var host = new AppPluginHost(
|
||||
new SerilogAdapter(Log.Logger),
|
||||
worldGameState,
|
||||
|
|
@ -44,27 +69,65 @@ var host = new AppPluginHost(
|
|||
window.Selection,
|
||||
uiRegistry);
|
||||
|
||||
var pluginsDir = Path.Combine(AppContext.BaseDirectory, "plugins");
|
||||
Log.Information("scanning plugins in {PluginsDir}", pluginsDir);
|
||||
|
||||
var loaded = new List<LoadedPlugin>();
|
||||
foreach (var result in PluginDiscovery.Scan(pluginsDir))
|
||||
var loadedPluginIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
StringComparer pathComparer =
|
||||
graphicalPlatform.OperatingSystem
|
||||
== GraphicalHostOperatingSystem.Windows
|
||||
? StringComparer.OrdinalIgnoreCase
|
||||
: StringComparer.Ordinal;
|
||||
string[] pluginRoots =
|
||||
[
|
||||
.. new[]
|
||||
{
|
||||
Path.Combine(AppContext.BaseDirectory, "plugins"),
|
||||
applicationPaths.PluginsDirectory,
|
||||
}.Distinct(pathComparer),
|
||||
];
|
||||
|
||||
foreach (string pluginsDir in pluginRoots)
|
||||
{
|
||||
if (!result.Success)
|
||||
Log.Information("scanning plugins in {PluginsDir}", pluginsDir);
|
||||
foreach (var result in PluginDiscovery.Scan(pluginsDir))
|
||||
{
|
||||
Log.Warning("plugin discovery failed for {Dir}: {Error}", result.PluginDirectory, result.Error);
|
||||
continue;
|
||||
}
|
||||
if (!result.Success)
|
||||
{
|
||||
Log.Warning(
|
||||
"plugin discovery failed for {Dir}: {Error}",
|
||||
result.PluginDirectory,
|
||||
result.Error);
|
||||
continue;
|
||||
}
|
||||
|
||||
var loadResult = PluginLoader.Load(result.PluginDirectory, result.Manifest!, host);
|
||||
if (!loadResult.Success)
|
||||
{
|
||||
Log.Warning("plugin load failed for {Id}: {Error}", result.Manifest!.Id, loadResult.Error);
|
||||
continue;
|
||||
}
|
||||
if (loadedPluginIds.Contains(result.Manifest!.Id))
|
||||
{
|
||||
Log.Warning(
|
||||
"skipping duplicate plugin id {Id} from {Dir}",
|
||||
result.Manifest.Id,
|
||||
result.PluginDirectory);
|
||||
continue;
|
||||
}
|
||||
|
||||
loaded.Add(loadResult);
|
||||
Log.Information("loaded plugin {Id} ({DisplayName})", result.Manifest!.Id, result.Manifest.DisplayName);
|
||||
var loadResult = PluginLoader.Load(
|
||||
result.PluginDirectory,
|
||||
result.Manifest,
|
||||
host);
|
||||
if (!loadResult.Success)
|
||||
{
|
||||
Log.Warning(
|
||||
"plugin load failed for {Id}: {Error}",
|
||||
result.Manifest.Id,
|
||||
loadResult.Error);
|
||||
continue;
|
||||
}
|
||||
|
||||
loadedPluginIds.Add(result.Manifest.Id);
|
||||
loaded.Add(loadResult);
|
||||
Log.Information(
|
||||
"loaded plugin {Id} ({DisplayName})",
|
||||
result.Manifest.Id,
|
||||
result.Manifest.DisplayName);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
|
|
|
|||
|
|
@ -72,6 +72,17 @@ internal sealed class DisplayFramePacingController : IDisposable
|
|||
{
|
||||
}
|
||||
|
||||
internal DisplayFramePacingController(
|
||||
bool uncappedRendering,
|
||||
FrameProfiler profiler,
|
||||
IFramePacingWaiterFactory waiterFactory)
|
||||
: this(
|
||||
uncappedRendering,
|
||||
profiler,
|
||||
new FramePacingController(waiterFactory))
|
||||
{
|
||||
}
|
||||
|
||||
internal DisplayFramePacingController(
|
||||
bool uncappedRendering,
|
||||
FrameProfiler profiler,
|
||||
|
|
|
|||
|
|
@ -25,9 +25,17 @@ internal sealed class FramePacingController : IDisposable
|
|||
private bool _disposed;
|
||||
|
||||
public FramePacingController()
|
||||
: this(PlatformFramePacingWaiterFactory.ForCurrentProcess())
|
||||
{
|
||||
}
|
||||
|
||||
internal FramePacingController(
|
||||
IFramePacingWaiterFactory waiterFactory)
|
||||
: this(
|
||||
StopwatchFramePacingClock.Instance,
|
||||
WindowsHighResolutionFramePacingWaiter.Create(),
|
||||
(waiterFactory
|
||||
?? throw new ArgumentNullException(nameof(waiterFactory)))
|
||||
.Create(),
|
||||
ownsWaiter: true)
|
||||
{
|
||||
}
|
||||
|
|
|
|||
38
src/AcDream.App/Rendering/FramePacingWaiterFactory.cs
Normal file
38
src/AcDream.App/Rendering/FramePacingWaiterFactory.cs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
using AcDream.App.Platform;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
internal interface IFramePacingWaiterFactory
|
||||
{
|
||||
IFramePacingWaiter Create();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The sole startup-time platform selector for software frame waits.
|
||||
/// Frame policy and deadline behavior remain platform-neutral.
|
||||
/// </summary>
|
||||
internal sealed class PlatformFramePacingWaiterFactory
|
||||
: IFramePacingWaiterFactory
|
||||
{
|
||||
private readonly GraphicalHostOperatingSystem _operatingSystem;
|
||||
|
||||
internal PlatformFramePacingWaiterFactory(
|
||||
GraphicalHostOperatingSystem operatingSystem)
|
||||
{
|
||||
_operatingSystem = operatingSystem;
|
||||
}
|
||||
|
||||
internal static PlatformFramePacingWaiterFactory ForCurrentProcess() =>
|
||||
new(GraphicalHostPlatformServices.DetectOperatingSystem());
|
||||
|
||||
public IFramePacingWaiter Create() =>
|
||||
_operatingSystem switch
|
||||
{
|
||||
GraphicalHostOperatingSystem.Windows =>
|
||||
WindowsHighResolutionFramePacingWaiter.Create(),
|
||||
GraphicalHostOperatingSystem.Linux =>
|
||||
LinuxMonotonicFramePacingWaiter.Create(),
|
||||
_ => throw new ArgumentOutOfRangeException(
|
||||
nameof(_operatingSystem)),
|
||||
};
|
||||
}
|
||||
|
|
@ -4,9 +4,11 @@ using AcDream.App.Physics;
|
|||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.App.Settings;
|
||||
using AcDream.App.Platform;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Content;
|
||||
using AcDream.Runtime;
|
||||
using AcDream.Runtime.Platform;
|
||||
using AcDream.Runtime.Entities;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
using AcDream.Runtime.Session;
|
||||
|
|
@ -498,16 +500,19 @@ public sealed class GameWindow :
|
|||
_runtimeDiagnosticCommands = new();
|
||||
private readonly AcDream.App.Combat.LiveCombatModeCommandSlot
|
||||
_liveCombatModeCommands = new();
|
||||
// K.1c: load user-customized bindings from %LOCALAPPDATA%\acdream\keybinds.json,
|
||||
// K.1c/L0: load user-customized bindings from the canonical per-platform
|
||||
// configuration path,
|
||||
// falling back to the retail-faithful defaults if the file is missing
|
||||
// or corrupt. This is THE single source of truth for the keymap at
|
||||
// startup — no other call to RetailDefaults() / AcdreamCurrentDefaults()
|
||||
// should land in the GameWindow construction path.
|
||||
private readonly AcDream.UI.Abstractions.Input.KeyBindings _keyBindings = LoadStartupKeyBindings();
|
||||
private readonly AcDream.UI.Abstractions.Input.KeyBindings _keyBindings;
|
||||
private readonly GraphicalHostPlatformServices _platformServices;
|
||||
private readonly ApplicationPathSet _applicationPaths;
|
||||
|
||||
private static AcDream.UI.Abstractions.Input.KeyBindings LoadStartupKeyBindings()
|
||||
private static AcDream.UI.Abstractions.Input.KeyBindings LoadStartupKeyBindings(
|
||||
string path)
|
||||
{
|
||||
var path = AcDream.UI.Abstractions.Input.KeyBindings.DefaultPath();
|
||||
var bindings = AcDream.UI.Abstractions.Input.KeyBindings.LoadOrDefault(path);
|
||||
Console.WriteLine($"keybinds: loaded {bindings.All.Count} bindings from {path}");
|
||||
return bindings;
|
||||
|
|
@ -571,8 +576,28 @@ public sealed class GameWindow :
|
|||
WorldGameState worldGameState,
|
||||
WorldEvents worldEvents,
|
||||
AcDream.App.Plugins.BufferedUiRegistry? uiRegistry = null)
|
||||
: this(
|
||||
options,
|
||||
worldGameState,
|
||||
worldEvents,
|
||||
uiRegistry,
|
||||
GraphicalHostPlatformServices.Resolve())
|
||||
{
|
||||
}
|
||||
|
||||
internal GameWindow(
|
||||
AcDream.App.RuntimeOptions options,
|
||||
WorldGameState worldGameState,
|
||||
WorldEvents worldEvents,
|
||||
AcDream.App.Plugins.BufferedUiRegistry? uiRegistry,
|
||||
GraphicalHostPlatformServices platformServices)
|
||||
{
|
||||
_options = options ?? throw new System.ArgumentNullException(nameof(options));
|
||||
_platformServices = platformServices
|
||||
?? throw new ArgumentNullException(nameof(platformServices));
|
||||
_applicationPaths = _platformServices.Paths;
|
||||
_keyBindings = LoadStartupKeyBindings(
|
||||
_applicationPaths.KeyBindingsFile);
|
||||
_runtime = new GameRuntime(new GameRuntimeDependencies(
|
||||
_combatAttackOperations,
|
||||
_combatTargetOperations,
|
||||
|
|
@ -611,10 +636,11 @@ public sealed class GameWindow :
|
|||
_worldEvents = worldEvents;
|
||||
_displayFramePacing = new DisplayFramePacingController(
|
||||
options.UncappedRendering,
|
||||
_frameProfiler);
|
||||
_frameProfiler,
|
||||
_platformServices.FramePacingWaiters);
|
||||
_runtimeSettings = new RuntimeSettingsController(
|
||||
new JsonRuntimeSettingsStorage(
|
||||
AcDream.UI.Abstractions.Panels.Settings.SettingsStore.DefaultPath()),
|
||||
_applicationPaths.SettingsFile),
|
||||
log: Console.WriteLine);
|
||||
_animationDiagnostics = AnimationPresentationDiagnostics.FromEnvironment();
|
||||
_uiRegistry = uiRegistry;
|
||||
|
|
@ -1209,7 +1235,9 @@ public sealed class GameWindow :
|
|||
devToolsFrameDiagnostics);
|
||||
IRuntimeKeyBindingTarget? keyBindingTarget =
|
||||
hostInputCamera.InputDispatcher is { } settingsDispatcher
|
||||
? new RuntimeKeyBindingTarget(settingsDispatcher)
|
||||
? new RuntimeKeyBindingTarget(
|
||||
settingsDispatcher,
|
||||
_applicationPaths.KeyBindingsFile)
|
||||
: null;
|
||||
optionalDevTools = new SettingsDevToolsOptionalDependencies(
|
||||
devToolsFacts,
|
||||
|
|
@ -1249,6 +1277,7 @@ public sealed class GameWindow :
|
|||
_gpuFrameFlights!,
|
||||
_options.ResidencyBudgets,
|
||||
initialCenterLandblockId,
|
||||
_applicationPaths.DiagnosticsDirectory,
|
||||
Console.WriteLine),
|
||||
this).Compose(platformResult, contentEffectsAudio, settingsDevTools);
|
||||
Console.WriteLine(
|
||||
|
|
|
|||
151
src/AcDream.App/Rendering/LinuxMonotonicFramePacingWaiter.cs
Normal file
151
src/AcDream.App/Rendering/LinuxMonotonicFramePacingWaiter.cs
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Low-CPU Linux frame wait using an absolute CLOCK_MONOTONIC deadline.
|
||||
/// EINTR resumes against the original deadline, so signals cannot shorten or
|
||||
/// cumulatively extend a normal presentation wait.
|
||||
/// </summary>
|
||||
internal sealed partial class LinuxMonotonicFramePacingWaiter
|
||||
: IFramePacingWaiter,
|
||||
IDisposable
|
||||
{
|
||||
private const int ClockMonotonic = 1;
|
||||
private const int TimerAbsolute = 1;
|
||||
private const int Interrupted = 4;
|
||||
private const long NanosecondsPerSecond = 1_000_000_000L;
|
||||
private bool _disposed;
|
||||
|
||||
private LinuxMonotonicFramePacingWaiter()
|
||||
{
|
||||
}
|
||||
|
||||
internal static LinuxMonotonicFramePacingWaiter Create()
|
||||
{
|
||||
if (!OperatingSystem.IsLinux())
|
||||
{
|
||||
throw new PlatformNotSupportedException(
|
||||
"The monotonic Linux frame waiter requires Linux.");
|
||||
}
|
||||
|
||||
return new LinuxMonotonicFramePacingWaiter();
|
||||
}
|
||||
|
||||
public void Wait(long durationTicks, long clockFrequency)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (durationTicks <= 0 || clockFrequency <= 0)
|
||||
return;
|
||||
|
||||
int result = ClockGetTime(ClockMonotonic, out Timespec now);
|
||||
if (result != 0)
|
||||
{
|
||||
throw new Win32Exception(
|
||||
Marshal.GetLastPInvokeError(),
|
||||
"Could not read the Linux monotonic clock.");
|
||||
}
|
||||
|
||||
long durationNanoseconds = ConvertTicksToNanoseconds(
|
||||
durationTicks,
|
||||
clockFrequency);
|
||||
Timespec deadline = AddNanoseconds(now, durationNanoseconds);
|
||||
do
|
||||
{
|
||||
result = ClockNanosleep(
|
||||
ClockMonotonic,
|
||||
TimerAbsolute,
|
||||
in deadline,
|
||||
0);
|
||||
}
|
||||
while (result == Interrupted);
|
||||
|
||||
if (result != 0)
|
||||
{
|
||||
throw new Win32Exception(
|
||||
result,
|
||||
"Waiting on the Linux monotonic frame deadline failed.");
|
||||
}
|
||||
}
|
||||
|
||||
internal static long ConvertTicksToNanoseconds(
|
||||
long durationTicks,
|
||||
long clockFrequency)
|
||||
{
|
||||
if (durationTicks <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(durationTicks));
|
||||
if (clockFrequency <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(clockFrequency));
|
||||
|
||||
long wholeSeconds = Math.DivRem(
|
||||
durationTicks,
|
||||
clockFrequency,
|
||||
out long remainder);
|
||||
if (wholeSeconds >= long.MaxValue / NanosecondsPerSecond)
|
||||
return long.MaxValue;
|
||||
|
||||
long wholeNanoseconds = wholeSeconds * NanosecondsPerSecond;
|
||||
long fractionalNanoseconds = checked((long)Math.Ceiling(
|
||||
remainder * (double)NanosecondsPerSecond / clockFrequency));
|
||||
if (wholeNanoseconds > long.MaxValue - fractionalNanoseconds)
|
||||
return long.MaxValue;
|
||||
|
||||
return Math.Max(1L, wholeNanoseconds + fractionalNanoseconds);
|
||||
}
|
||||
|
||||
private static Timespec AddNanoseconds(
|
||||
Timespec timestamp,
|
||||
long nanoseconds)
|
||||
{
|
||||
long seconds = nanoseconds / NanosecondsPerSecond;
|
||||
long remainder = nanoseconds % NanosecondsPerSecond;
|
||||
long resultSeconds = timestamp.Seconds > long.MaxValue - seconds
|
||||
? long.MaxValue
|
||||
: timestamp.Seconds + seconds;
|
||||
if (resultSeconds == long.MaxValue)
|
||||
{
|
||||
return new Timespec(
|
||||
long.MaxValue,
|
||||
NanosecondsPerSecond - 1L);
|
||||
}
|
||||
|
||||
long resultNanoseconds = timestamp.Nanoseconds + remainder;
|
||||
if (resultNanoseconds >= NanosecondsPerSecond)
|
||||
{
|
||||
if (resultSeconds == long.MaxValue)
|
||||
{
|
||||
return new Timespec(
|
||||
long.MaxValue,
|
||||
NanosecondsPerSecond - 1L);
|
||||
}
|
||||
|
||||
resultSeconds++;
|
||||
resultNanoseconds -= NanosecondsPerSecond;
|
||||
}
|
||||
|
||||
return new Timespec(resultSeconds, resultNanoseconds);
|
||||
}
|
||||
|
||||
public void Dispose() => _disposed = true;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private readonly record struct Timespec(
|
||||
long Seconds,
|
||||
long Nanoseconds);
|
||||
|
||||
[LibraryImport(
|
||||
"libc",
|
||||
EntryPoint = "clock_gettime",
|
||||
SetLastError = true)]
|
||||
private static partial int ClockGetTime(
|
||||
int clockId,
|
||||
out Timespec timestamp);
|
||||
|
||||
[LibraryImport("libc", EntryPoint = "clock_nanosleep")]
|
||||
private static partial int ClockNanosleep(
|
||||
int clockId,
|
||||
int flags,
|
||||
in Timespec request,
|
||||
nint remainder);
|
||||
}
|
||||
|
|
@ -124,7 +124,8 @@ public sealed record RenderStack(
|
|||
|
||||
/// <summary>Options for <see cref="RenderBootstrap.Create"/>.</summary>
|
||||
public sealed record RenderBootstrapOptions(
|
||||
AcDream.UI.Abstractions.Settings.QualitySettings Quality);
|
||||
AcDream.UI.Abstractions.Settings.QualitySettings Quality,
|
||||
string DiagnosticsDirectory);
|
||||
|
||||
/// <summary>
|
||||
/// Constructs the UI Studio's render stack from the production classes,
|
||||
|
|
@ -164,7 +165,12 @@ public static class RenderBootstrap
|
|||
|
||||
// --- TextureCache (GameWindow ~1774) ---
|
||||
var frameFlights = new GpuFrameFlightController(gl);
|
||||
var textureCache = new TextureCache(gl, dats, bindless, frameFlights);
|
||||
var textureCache = new TextureCache(
|
||||
gl,
|
||||
dats,
|
||||
bindless,
|
||||
frameFlights,
|
||||
opts.DiagnosticsDirectory);
|
||||
|
||||
// --- AnimLoader (GameWindow ~1240) ---
|
||||
var animLoader = new AcDream.Content.Vfx.RetailAnimationLoader(dats);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ public sealed unsafe class TextureCache
|
|||
{
|
||||
private readonly GL _gl;
|
||||
private readonly IDatReaderWriter _dats;
|
||||
private readonly string _diagnosticsDirectory;
|
||||
// Handle and decoded dimensions are one atomic cache entry. Keeping them
|
||||
// in separate dictionaries allowed GetOrUpload(surfaceId) followed by the
|
||||
// sized overload to upload a second GL texture and orphan the first.
|
||||
|
|
@ -86,7 +87,15 @@ public sealed unsafe class TextureCache
|
|||
private bool _surfaceHistogramAlreadyDumped;
|
||||
|
||||
public TextureCache(GL gl, IDatReaderWriter dats, Wb.BindlessSupport? bindless = null)
|
||||
: this(gl, dats, bindless, ImmediateGpuResourceRetirementQueue.Instance)
|
||||
: this(
|
||||
gl,
|
||||
dats,
|
||||
bindless,
|
||||
ImmediateGpuResourceRetirementQueue.Instance,
|
||||
Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
"acdream",
|
||||
"diagnostics"))
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -95,12 +104,15 @@ public sealed unsafe class TextureCache
|
|||
IDatReaderWriter dats,
|
||||
Wb.BindlessSupport? bindless,
|
||||
IGpuResourceRetirementQueue retirementQueue,
|
||||
string diagnosticsDirectory,
|
||||
ResidencyBudgetOptions? budgets = null)
|
||||
{
|
||||
budgets ??= ResidencyBudgetOptions.Default;
|
||||
_gl = gl;
|
||||
_dats = dats;
|
||||
_bindless = bindless;
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(diagnosticsDirectory);
|
||||
_diagnosticsDirectory = diagnosticsDirectory;
|
||||
ArgumentNullException.ThrowIfNull(retirementQueue);
|
||||
if (bindless is not null)
|
||||
{
|
||||
|
|
@ -602,7 +614,8 @@ public sealed unsafe class TextureCache
|
|||
/// pulled in world content (not just sky/UI/font). The original
|
||||
/// frame-only gate fired during the login/handshake phase where
|
||||
/// OnRender ticks at GUI rates but no world has streamed in.
|
||||
/// Output goes to %LOCALAPPDATA%\acdream\n6-surfaces.txt. Zero cost
|
||||
/// Output goes to the host-provided portable diagnostics directory.
|
||||
/// Zero cost
|
||||
/// when off. See spec §5 in
|
||||
/// docs/superpowers/specs/2026-05-11-phase-n6-slice1-design.md.
|
||||
/// </summary>
|
||||
|
|
@ -638,10 +651,10 @@ public sealed unsafe class TextureCache
|
|||
|
||||
private void DumpSurfaceHistogramCore()
|
||||
{
|
||||
var localAppData = System.Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData);
|
||||
var outDir = System.IO.Path.Combine(localAppData, "acdream");
|
||||
System.IO.Directory.CreateDirectory(outDir);
|
||||
var outPath = System.IO.Path.Combine(outDir, "n6-surfaces.txt");
|
||||
System.IO.Directory.CreateDirectory(_diagnosticsDirectory);
|
||||
var outPath = System.IO.Path.Combine(
|
||||
_diagnosticsDirectory,
|
||||
"n6-surfaces.txt");
|
||||
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.AppendLine($"# acdream surface-format histogram — generated {DateTime.UtcNow:yyyy-MM-ddTHH:mm:ssZ}");
|
||||
|
|
|
|||
|
|
@ -20,11 +20,12 @@ internal sealed class RuntimeKeyBindingTarget : IRuntimeKeyBindingTarget
|
|||
|
||||
public RuntimeKeyBindingTarget(
|
||||
InputDispatcher dispatcher,
|
||||
string? path = null,
|
||||
string path,
|
||||
Action<string>? log = null)
|
||||
{
|
||||
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
|
||||
_path = path ?? KeyBindings.DefaultPath();
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(path);
|
||||
_path = path;
|
||||
_log = log ?? Console.WriteLine;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using System.Numerics;
|
|||
using AcDream.Content;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.Runtime.Platform;
|
||||
using DatReaderWriter;
|
||||
using Silk.NET.Input;
|
||||
using Silk.NET.Maths;
|
||||
|
|
@ -36,6 +37,7 @@ namespace AcDream.App.Studio;
|
|||
public sealed class StudioWindow : IDisposable
|
||||
{
|
||||
private readonly StudioOptions _opts;
|
||||
private readonly ApplicationPathSet _applicationPaths;
|
||||
|
||||
// Created in OnLoad, released in OnClosing.
|
||||
private IWindow? _window;
|
||||
|
|
@ -64,8 +66,17 @@ public sealed class StudioWindow : IDisposable
|
|||
private bool _screenshotDone;
|
||||
|
||||
public StudioWindow(StudioOptions opts)
|
||||
: this(opts, ApplicationPathSet.Resolve())
|
||||
{
|
||||
}
|
||||
|
||||
internal StudioWindow(
|
||||
StudioOptions opts,
|
||||
ApplicationPathSet applicationPaths)
|
||||
{
|
||||
_opts = opts ?? throw new ArgumentNullException(nameof(opts));
|
||||
_applicationPaths = applicationPaths
|
||||
?? throw new ArgumentNullException(nameof(applicationPaths));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -77,7 +88,7 @@ public sealed class StudioWindow : IDisposable
|
|||
// Resolve quality settings the same way GameWindow.Run() does
|
||||
// (SettingsStore → QualitySettings.From → WithEnvOverrides).
|
||||
var startupStore = new AcDream.UI.Abstractions.Panels.Settings.SettingsStore(
|
||||
AcDream.UI.Abstractions.Panels.Settings.SettingsStore.DefaultPath());
|
||||
_applicationPaths.SettingsFile);
|
||||
var startupDisplay = startupStore.LoadDisplay();
|
||||
var startupBase = AcDream.UI.Abstractions.Settings.QualitySettings.From(startupDisplay.Quality);
|
||||
var startupQuality = AcDream.UI.Abstractions.Settings.QualitySettings.WithEnvOverrides(startupBase);
|
||||
|
|
@ -118,12 +129,17 @@ public sealed class StudioWindow : IDisposable
|
|||
// Build QualitySettings for RenderBootstrap (same as Run() above — re-read
|
||||
// after the GL context is confirmed, mirroring GameWindow.OnLoad).
|
||||
var store = new AcDream.UI.Abstractions.Panels.Settings.SettingsStore(
|
||||
AcDream.UI.Abstractions.Panels.Settings.SettingsStore.DefaultPath());
|
||||
_applicationPaths.SettingsFile);
|
||||
var display = store.LoadDisplay();
|
||||
var quality = AcDream.UI.Abstractions.Settings.QualitySettings.WithEnvOverrides(
|
||||
AcDream.UI.Abstractions.Settings.QualitySettings.From(display.Quality));
|
||||
|
||||
_stack = RenderBootstrap.Create(gl, _dats, new RenderBootstrapOptions(quality));
|
||||
_stack = RenderBootstrap.Create(
|
||||
gl,
|
||||
_dats,
|
||||
new RenderBootstrapOptions(
|
||||
quality,
|
||||
_applicationPaths.DiagnosticsDirectory));
|
||||
|
||||
if (_opts.Mockup)
|
||||
MockupDesktop.Load(_dats, _stack);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue