acdream/src/AcDream.App/Rendering/DisplayFramePacingController.cs
Erik 14fbe92b08 fix(rendering): survive display topology handoff
Model active-monitor refresh telemetry as optional so GLFW display replacement during console/RDP transitions cannot escape the native move callback and terminate the client. Preserve the existing software pacing fallback until a later monitor event succeeds.

Co-authored-by: OpenAI Codex <codex@openai.com>
2026-07-25 13:15:24 +02:00

157 lines
4.6 KiB
C#

using AcDream.App.Diagnostics;
using Silk.NET.Maths;
using Silk.NET.Windowing;
namespace AcDream.App.Rendering;
internal interface IDisplayFramePacingSurface
{
bool VSync { get; set; }
bool TryGetActiveMonitorRefreshHz(out int refreshHz);
}
/// <summary>Narrow Silk window adapter for display pacing.</summary>
internal sealed class SilkDisplayFramePacingSurface : IDisplayFramePacingSurface
{
private readonly IWindow _window;
public SilkDisplayFramePacingSurface(IWindow window)
{
_window = window ?? throw new ArgumentNullException(nameof(window));
}
public bool VSync
{
get => _window.VSync;
set => _window.VSync = value;
}
public bool TryGetActiveMonitorRefreshHz(out int refreshHz)
{
refreshHz = 0;
try
{
int? activeRefreshHz = _window.Monitor?.VideoMode.RefreshRate;
if (activeRefreshHz is not > 0)
return false;
refreshHz = activeRefreshHz.Value;
return true;
}
catch (Silk.NET.GLFW.GlfwException)
{
// GLFW can retain a monitor handle while Windows is replacing the
// display topology during a console/RDP handoff. Refresh-rate
// telemetry is optional; report it as unavailable until the next
// move/state event rather than terminating the render host.
return false;
}
}
}
/// <summary>
/// Owns requested VSync, monitor-aware policy resolution, the software pacer,
/// and the post-render pacing event. Window lifecycle and frame preparation
/// call this typed owner rather than duplicating pacing state.
/// </summary>
internal sealed class DisplayFramePacingController : IDisposable
{
private readonly bool _uncappedRendering;
private FrameProfiler? _profiler;
private readonly FramePacingController _pacing;
private IDisplayFramePacingSurface? _surface;
private int? _activeMonitorRefreshHz;
private bool _requestedVSync =
AcDream.UI.Abstractions.Panels.Settings.DisplaySettings.Default.VSync;
public DisplayFramePacingController(
bool uncappedRendering,
FrameProfiler profiler)
: this(uncappedRendering, profiler, new FramePacingController())
{
}
internal DisplayFramePacingController(
bool uncappedRendering,
FrameProfiler profiler,
FramePacingController pacing)
{
_uncappedRendering = uncappedRendering;
_profiler = profiler ?? throw new ArgumentNullException(nameof(profiler));
_pacing = pacing ?? throw new ArgumentNullException(nameof(pacing));
}
internal bool RequestedVSync => _requestedVSync;
internal FramePacingPolicy Policy => _pacing.Policy;
internal bool IsSurfaceBound => _surface is not null;
public FramePacingPolicy InitializeStartup(bool requestedVSync)
{
_requestedVSync = requestedVSync;
FramePacingPolicy policy = Resolve(monitorRefreshHz: null);
_pacing.Apply(policy);
return policy;
}
public void BindSurface(IDisplayFramePacingSurface surface)
{
_surface = surface ?? throw new ArgumentNullException(nameof(surface));
}
public void ApplyPreference(bool requestedVSync)
{
_requestedVSync = requestedVSync;
ApplyResolvedPolicy();
}
public void RefreshActiveMonitor()
{
_activeMonitorRefreshHz =
_surface?.TryGetActiveMonitorRefreshHz(out int refreshHz) == true
? refreshHz
: null;
ApplyResolvedPolicy();
}
public void OnWindowMoved(Vector2D<int> _) => RefreshActiveMonitor();
public void OnWindowStateChanged(WindowState _) => RefreshActiveMonitor();
public void OnFrameRendered(double _)
{
FrameProfiler profiler = _profiler
?? throw new ObjectDisposedException(nameof(DisplayFramePacingController));
using var pacingStage = profiler.BeginStage(FrameStage.Pacing);
_pacing.CompleteFrame();
}
private FramePacingPolicy Resolve(int? monitorRefreshHz) =>
FramePacingPolicy.Resolve(
_requestedVSync,
_uncappedRendering,
monitorRefreshHz);
private void ApplyResolvedPolicy()
{
FramePacingPolicy policy = Resolve(_activeMonitorRefreshHz);
_pacing.Apply(policy);
if (_surface is not null && _surface.VSync != policy.UseVSync)
_surface.VSync = policy.UseVSync;
}
public void Dispose()
{
try
{
_pacing.Dispose();
}
finally
{
_surface = null;
_profiler = null;
}
}
}