acdream/tests/AcDream.App.Tests/Rendering/SilkWindowCallbackBindingTests.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

799 lines
29 KiB
C#

using AcDream.App.Diagnostics;
using AcDream.App.Rendering;
using Silk.NET.Maths;
using Silk.NET.Windowing;
namespace AcDream.App.Tests.Rendering;
public sealed class SilkWindowCallbackBindingTests
{
[Fact]
public void AttachAndDisposeUseExactForwardAndReverseOrder()
{
using var profiler = new FrameProfiler();
using var pacing = CreatePacing(profiler);
var surface = new FakeWindowCallbackSurface();
var gate = new HostQuiescenceGate();
using SilkWindowCallbackBinding binding = CreateAttachedBinding(
surface,
Targets(gate),
pacing,
gate);
Assert.Equal(
[
"add load",
"add update",
"add render main",
"add render pacing",
"add closing",
"add focus",
"add move",
"add state",
"add framebuffer",
], surface.Operations);
surface.Operations.Clear();
binding.Dispose();
Assert.Equal(
[
"remove framebuffer",
"remove state",
"remove move",
"remove focus",
"remove closing",
"remove render pacing",
"remove render main",
"remove update",
"remove load",
], surface.Operations);
Assert.True(binding.IsDetached);
Assert.False(gate.IsAccepting);
}
[Fact]
public void MainRenderIsRegisteredBeforePacingRender()
{
var order = new List<string>();
var clock = new OrderingClock();
using var profiler = new FrameProfiler();
using var pacing = new DisplayFramePacingController(
uncappedRendering: false,
profiler,
new FramePacingController(clock, new OrderingWaiter(clock, order)));
pacing.InitializeStartup(requestedVSync: false);
var surface = new FakeWindowCallbackSurface();
var gate = new HostQuiescenceGate();
WindowCallbackTargets targets = Targets(
gate,
render: _ => order.Add("main render"));
using SilkWindowCallbackBinding binding = CreateAttachedBinding(
surface,
targets,
pacing,
gate);
Assert.Equal(2, surface.RenderCallbacks.Count);
surface.RaiseRender(1d / 60d);
Assert.Equal(["main render", "pacing render"], order);
}
public static IEnumerable<object[]> AttachFailureCases =>
Enumerable.Range(1, 9).SelectMany(attempt => new[]
{
new object[] { attempt, false },
new object[] { attempt, true },
});
[Theory]
[MemberData(nameof(AttachFailureCases))]
public void FailureAfterNthAttachRollsBackPossiblyAcquiredPrefix(
int failedAttempt,
bool failAfterSideEffect)
{
using var profiler = new FrameProfiler();
using var pacing = CreatePacing(profiler);
var surface = new FakeWindowCallbackSurface
{
FailAddAttempt = failedAttempt,
FailAddAfterSideEffect = failAfterSideEffect,
};
var gate = new HostQuiescenceGate();
SilkWindowCallbackBinding binding = SilkWindowCallbackBinding.Create(
surface,
Targets(gate),
pacing,
gate);
InvalidOperationException error = Assert.Throws<InvalidOperationException>(binding.Attach);
Assert.Contains("rolled back", error.Message, StringComparison.OrdinalIgnoreCase);
Assert.Equal(0, surface.SubscriptionCount);
Assert.False(gate.IsAccepting);
string[] added = surface.Operations
.Where(operation => operation.StartsWith("add ", StringComparison.Ordinal))
.Take(failedAttempt)
.ToArray();
string[] removed = surface.Operations
.Where(operation => operation.StartsWith("remove ", StringComparison.Ordinal))
.ToArray();
Assert.Equal(
added
.Reverse()
.Select(operation => operation.Replace("add ", "remove ", StringComparison.Ordinal)),
removed);
Assert.True(binding.IsDetached);
int operationsAfterRollback = surface.Operations.Count;
binding.Dispose();
Assert.Equal(operationsAfterRollback, surface.Operations.Count);
}
[Fact]
public void RollbackFailureIsReportedAndRemainingCallbackIsLogicallySilent()
{
using var profiler = new FrameProfiler();
using var pacing = CreatePacing(profiler);
var surface = new FakeWindowCallbackSurface
{
FailAddAttempt = 4,
};
surface.PersistentRemoveFailures.Add("update");
var gate = new HostQuiescenceGate();
int updateCalls = 0;
SilkWindowCallbackBinding binding = SilkWindowCallbackBinding.Create(
surface,
Targets(gate, update: _ => updateCalls++),
pacing,
gate);
AggregateException error = Assert.Throws<AggregateException>(binding.Attach);
Assert.Contains(
error.InnerExceptions,
exception => exception.Message.Contains("registration failed", StringComparison.Ordinal));
Assert.Contains(
error.InnerExceptions,
exception => exception.Message.Contains("index 1", StringComparison.Ordinal));
Assert.False(gate.IsAccepting);
Assert.Equal(1, surface.SubscriptionCount);
surface.RaiseUpdate(0.1d);
Assert.Equal(0, updateCalls);
surface.PersistentRemoveFailures.Clear();
binding.Dispose();
Assert.True(binding.IsDetached);
Assert.Equal(0, surface.SubscriptionCount);
}
[Fact]
public void PersistentPhysicalDetachFailureIsRetriableWithoutReplayingSuccesses()
{
using var profiler = new FrameProfiler();
using var pacing = CreatePacing(profiler);
var surface = new FakeWindowCallbackSurface();
surface.PersistentRemoveFailures.Add("focus");
var gate = new HostQuiescenceGate();
int updateCalls = 0;
SilkWindowCallbackBinding binding = CreateAttachedBinding(
surface,
Targets(gate, update: _ => updateCalls++),
pacing,
gate);
Assert.Throws<AggregateException>(binding.Dispose);
Assert.False(binding.IsDetached);
Assert.False(gate.IsAccepting);
int successfulRemovalCount = surface.Operations.Count(operation =>
operation.StartsWith("remove ", StringComparison.Ordinal)
&& operation != "remove focus");
Assert.Equal(8, successfulRemovalCount);
surface.RaiseUpdate(0.1d);
Assert.Equal(0, updateCalls);
surface.PersistentRemoveFailures.Clear();
binding.Dispose();
Assert.True(binding.IsDetached);
Assert.Equal(0, surface.SubscriptionCount);
Assert.Equal(8, surface.Operations.Count(operation =>
operation.StartsWith("remove ", StringComparison.Ordinal)
&& operation != "remove focus"));
}
[Fact]
public void RemoveThenTransientThrowIsRetriedToPhysicalConvergence()
{
using var profiler = new FrameProfiler();
using var pacing = CreatePacing(profiler);
var surface = new FakeWindowCallbackSurface();
surface.RemoveAfterSideEffectFailures.Add("focus");
var gate = new HostQuiescenceGate();
SilkWindowCallbackBinding binding = CreateAttachedBinding(
surface,
Targets(gate),
pacing,
gate);
binding.Dispose();
Assert.True(binding.IsDetached);
Assert.Equal(0, surface.SubscriptionCount);
Assert.Equal(2, surface.Operations.Count(operation => operation == "remove focus"));
}
[Fact]
public void DisposeIsRepeatedAndReentrantSafe()
{
using var profiler = new FrameProfiler();
using var pacing = CreatePacing(profiler);
var surface = new FakeWindowCallbackSurface();
var gate = new HostQuiescenceGate();
SilkWindowCallbackBinding? binding = null;
surface.Removing = _ => binding!.Dispose();
binding = CreateAttachedBinding(surface, Targets(gate), pacing, gate);
binding.Dispose();
int operationsAfterFirstDispose = surface.Operations.Count;
binding.Dispose();
Assert.True(binding.IsDetached);
Assert.Equal(operationsAfterFirstDispose, surface.Operations.Count);
Assert.Equal(0, surface.SubscriptionCount);
}
[Fact]
public async Task ConcurrentDisposeWaitsForPhysicalDetachToComplete()
{
using var profiler = new FrameProfiler();
using var pacing = CreatePacing(profiler);
var surface = new FakeWindowCallbackSurface
{
BlockRemoveName = "framebuffer",
};
var gate = new HostQuiescenceGate();
SilkWindowCallbackBinding binding = CreateAttachedBinding(
surface,
Targets(gate),
pacing,
gate);
Task<Exception> first = Task.Run(() => Record.Exception(binding.Dispose));
Assert.True(surface.RemoveEntered.Wait(TimeSpan.FromSeconds(5)));
using var secondEntered = new ManualResetEventSlim(false);
Task<Exception> second = Task.Run(() =>
{
secondEntered.Set();
return Record.Exception(binding.Dispose);
});
Assert.True(secondEntered.Wait(TimeSpan.FromSeconds(5)));
await Task.Delay(50);
Assert.False(second.IsCompleted);
surface.ContinueRemove.Set();
Assert.Null(await first);
Assert.Null(await second);
Assert.True(binding.IsDetached);
}
[Fact]
public async Task ConcurrentDisposeCannotHidePhysicalDetachFailure()
{
using var profiler = new FrameProfiler();
using var pacing = CreatePacing(profiler);
var surface = new FakeWindowCallbackSurface
{
BlockRemoveName = "framebuffer",
};
surface.PersistentRemoveFailures.Add("framebuffer");
var gate = new HostQuiescenceGate();
SilkWindowCallbackBinding binding = CreateAttachedBinding(
surface,
Targets(gate),
pacing,
gate);
Task<Exception> first = Task.Run(() => Record.Exception(binding.Dispose));
Assert.True(surface.RemoveEntered.Wait(TimeSpan.FromSeconds(5)));
using var secondEntered = new ManualResetEventSlim(false);
Task<Exception> second = Task.Run(() =>
{
secondEntered.Set();
return Record.Exception(binding.Dispose);
});
Assert.True(secondEntered.Wait(TimeSpan.FromSeconds(5)));
await Task.Delay(50);
Assert.False(second.IsCompleted);
surface.ContinueRemove.Set();
Assert.IsType<AggregateException>(await first);
Assert.IsType<AggregateException>(await second);
Assert.False(binding.IsDetached);
surface.PersistentRemoveFailures.Clear();
binding.Dispose();
Assert.True(binding.IsDetached);
}
[Fact]
public async Task DisposeDuringBlockedAttachForcesRollbackBeforeDetachCompletes()
{
using var profiler = new FrameProfiler();
using var pacing = CreatePacing(profiler);
var surface = new FakeWindowCallbackSurface
{
BlockAddName = "load",
};
var gate = new HostQuiescenceGate();
SilkWindowCallbackBinding binding = SilkWindowCallbackBinding.Create(
surface,
Targets(gate),
pacing,
gate);
Task<Exception> attach = Task.Run(() => Record.Exception(binding.Attach));
Assert.True(surface.AddEntered.Wait(TimeSpan.FromSeconds(5)));
Task<Exception> dispose = Task.Run(() => Record.Exception(binding.Dispose));
Assert.True(SpinWait.SpinUntil(
() => !gate.IsAccepting,
TimeSpan.FromSeconds(5)));
Assert.False(dispose.IsCompleted);
surface.ContinueAdd.Set();
Assert.IsType<InvalidOperationException>(await attach);
Assert.Null(await dispose);
Assert.True(binding.IsDetached);
Assert.Equal(0, surface.SubscriptionCount);
}
[Fact]
public async Task ConcurrentDoubleAttachAllowsOnlyTheLifecycleOwner()
{
using var profiler = new FrameProfiler();
using var pacing = CreatePacing(profiler);
var surface = new FakeWindowCallbackSurface
{
BlockAddName = "load",
};
var gate = new HostQuiescenceGate();
SilkWindowCallbackBinding binding = SilkWindowCallbackBinding.Create(
surface,
Targets(gate),
pacing,
gate);
Task<Exception> first = Task.Run(() => Record.Exception(binding.Attach));
Assert.True(surface.AddEntered.Wait(TimeSpan.FromSeconds(5)));
using var secondStarted = new ManualResetEventSlim(false);
Task<Exception> second = Task.Run(() =>
{
secondStarted.Set();
return Record.Exception(binding.Attach);
});
Assert.True(secondStarted.Wait(TimeSpan.FromSeconds(5)));
Exception secondError = await second.WaitAsync(TimeSpan.FromSeconds(5));
Assert.IsType<InvalidOperationException>(secondError);
surface.ContinueAdd.Set();
Assert.Null(await first);
binding.Dispose();
Assert.True(binding.IsDetached);
}
[Fact]
public void AddAccessorCanRequestDisposeReentrantlyWithoutDeadlock()
{
using var profiler = new FrameProfiler();
using var pacing = CreatePacing(profiler);
var surface = new FakeWindowCallbackSurface();
var gate = new HostQuiescenceGate();
SilkWindowCallbackBinding? binding = null;
surface.Adding = _ => binding!.Dispose();
binding = SilkWindowCallbackBinding.Create(
surface,
Targets(gate),
pacing,
gate);
Assert.Throws<InvalidOperationException>(binding.Attach);
Assert.False(binding.IsDisposalComplete);
binding.Dispose();
Assert.True(binding.IsDetached);
Assert.True(binding.IsDisposalComplete);
Assert.Equal(0, surface.SubscriptionCount);
}
[Fact]
public void ReentrantAttachCleanupRemainsPublishedUntilRetryCompletes()
{
using var profiler = new FrameProfiler();
using var pacing = CreatePacing(profiler);
var surface = new FakeWindowCallbackSurface();
surface.PersistentRemoveFailures.Add("load");
var gate = new HostQuiescenceGate();
SilkWindowCallbackBinding? slot = SilkWindowCallbackBinding.Create(
surface,
Targets(gate),
pacing,
gate);
ResourceShutdownTransaction? shutdown = null;
shutdown = new ResourceShutdownTransaction(
new ResourceShutdownStage("input",
[
new("window callbacks", () =>
{
SilkWindowCallbackBinding? current = slot;
if (current is null)
return;
current.Dispose();
if (!current.IsDisposalComplete)
throw new NativeWindowCallbackCleanupDeferredException();
slot = null;
}),
]));
surface.Adding = _ => shutdown.CompleteOrThrow();
Assert.Throws<AggregateException>(slot.Attach);
Assert.NotNull(slot);
Assert.False(slot.IsDisposalComplete);
surface.PersistentRemoveFailures.Clear();
shutdown.CompleteOrThrow();
Assert.Null(slot);
Assert.True(shutdown.IsComplete);
Assert.Equal(0, surface.SubscriptionCount);
}
[Fact]
public async Task ConcurrentCallbackDisposeAndLaterAddFailureCannotDeadlock()
{
using var profiler = new FrameProfiler();
using var pacing = CreatePacing(profiler);
var surface = new FakeWindowCallbackSurface();
var gate = new HostQuiescenceGate();
using var loadEntered = new ManualResetEventSlim(false);
SilkWindowCallbackBinding? binding = null;
Task<Exception>? callback = null;
var targets = new WindowCallbackTargets(
load: () =>
{
loadEntered.Set();
binding!.Dispose();
},
update: static _ => { },
render: static _ => { },
closing: gate.StopAccepting,
focusChanged: static _ => { },
framebufferResize: static _ => { });
binding = SilkWindowCallbackBinding.Create(surface, targets, pacing, gate);
surface.Adding = name =>
{
if (name != "update")
return;
callback = Task.Run(() => Record.Exception(surface.RaiseLoad));
Assert.True(loadEntered.Wait(TimeSpan.FromSeconds(5)));
throw new InvalidOperationException("synthetic later add failure");
};
Task<Exception> attach = Task.Run(() => Record.Exception(binding.Attach));
Exception attachError = await attach.WaitAsync(TimeSpan.FromSeconds(5));
Exception callbackError = await callback!.WaitAsync(TimeSpan.FromSeconds(5));
Assert.IsType<InvalidOperationException>(attachError);
Assert.IsType<NativeWindowCallbackCleanupDeferredException>(callbackError);
Assert.False(binding.IsDisposalComplete);
binding.Dispose();
Assert.True(binding.IsDisposalComplete);
Assert.Equal(0, surface.SubscriptionCount);
}
[Fact]
public void CallbackRaisedDuringDetachAndAfterDetachIsSilent()
{
using var profiler = new FrameProfiler();
using var pacing = CreatePacing(profiler);
var surface = new FakeWindowCallbackSurface();
var gate = new HostQuiescenceGate();
int updateCalls = 0;
SilkWindowCallbackBinding binding = CreateAttachedBinding(
surface,
Targets(gate, update: _ => updateCalls++),
pacing,
gate);
surface.Removing = _ => surface.RaiseUpdate(0.1d);
binding.Dispose();
surface.RaiseUpdate(0.1d);
Assert.Equal(0, updateCalls);
}
[Fact]
public void ClosingCanQuiesceEveryLaterNativeCallback()
{
using var profiler = new FrameProfiler();
using var pacing = CreatePacing(profiler);
var surface = new FakeWindowCallbackSurface();
var gate = new HostQuiescenceGate();
int closingCalls = 0;
int updateCalls = 0;
WindowCallbackTargets targets = Targets(
gate,
update: _ => updateCalls++,
closing: () =>
{
closingCalls++;
gate.StopAccepting();
});
using SilkWindowCallbackBinding binding = CreateAttachedBinding(
surface,
targets,
pacing,
gate);
surface.RaiseClosing();
surface.RaiseClosing();
surface.RaiseUpdate(0.1d);
Assert.Equal(1, closingCalls);
Assert.Equal(0, updateCalls);
Assert.False(gate.IsAccepting);
}
[Fact]
public void ClosedGateSilencesEveryNativeWindowEdge()
{
var pacingCalls = new List<string>();
var clock = new OrderingClock();
using var profiler = new FrameProfiler();
using var pacing = new DisplayFramePacingController(
uncappedRendering: false,
profiler,
new FramePacingController(clock, new OrderingWaiter(clock, pacingCalls)));
pacing.InitializeStartup(requestedVSync: false);
var pacingSurface = new CountingPacingSurface();
pacing.BindSurface(pacingSurface);
var surface = new FakeWindowCallbackSurface();
var gate = new HostQuiescenceGate();
int targetCalls = 0;
var targets = new WindowCallbackTargets(
() => targetCalls++,
_ => targetCalls++,
_ => targetCalls++,
() => targetCalls++,
_ => targetCalls++,
_ => targetCalls++);
using SilkWindowCallbackBinding binding = CreateAttachedBinding(
surface,
targets,
pacing,
gate);
gate.StopAccepting();
surface.RaiseLoad();
surface.RaiseUpdate(0.1d);
surface.RaiseRender(0.1d);
surface.RaiseClosing();
surface.RaiseFocusChanged(false);
surface.RaiseMove(default);
surface.RaiseStateChanged(default);
surface.RaiseFramebufferResize(new Vector2D<int>(1280, 720));
Assert.Equal(0, targetCalls);
Assert.Empty(pacingCalls);
Assert.Equal(0, pacingSurface.RefreshReadCount);
}
private static DisplayFramePacingController CreatePacing(FrameProfiler profiler)
{
var pacing = new DisplayFramePacingController(
uncappedRendering: false,
profiler);
pacing.InitializeStartup(requestedVSync: true);
return pacing;
}
private static SilkWindowCallbackBinding CreateAttachedBinding(
IWindowCallbackSurface surface,
WindowCallbackTargets targets,
DisplayFramePacingController pacing,
HostQuiescenceGate gate)
{
SilkWindowCallbackBinding binding = SilkWindowCallbackBinding.Create(
surface,
targets,
pacing,
gate);
binding.Attach();
return binding;
}
private static WindowCallbackTargets Targets(
HostQuiescenceGate gate,
Action<double>? update = null,
Action<double>? render = null,
Action? closing = null) => new(
load: static () => { },
update: update ?? (static _ => { }),
render: render ?? (static _ => { }),
closing: closing ?? gate.StopAccepting,
focusChanged: static _ => { },
framebufferResize: static _ => { });
private sealed class FakeWindowCallbackSurface : IWindowCallbackSurface
{
private readonly List<Action> _load = [];
private readonly List<Action<double>> _update = [];
private readonly List<Action<double>> _render = [];
private readonly List<Action> _closing = [];
private readonly List<Action<bool>> _focus = [];
private readonly List<Action<Vector2D<int>>> _move = [];
private readonly List<Action<WindowState>> _state = [];
private readonly List<Action<Vector2D<int>>> _framebuffer = [];
private readonly Dictionary<Action<double>, string> _renderNames =
new(ReferenceEqualityComparer.Instance);
private int _addAttempts;
private int _addBlockUsed;
private int _removeBlockUsed;
public List<string> Operations { get; } = [];
public HashSet<string> PersistentRemoveFailures { get; } = [];
public HashSet<string> RemoveAfterSideEffectFailures { get; } = [];
public int? FailAddAttempt { get; init; }
public bool FailAddAfterSideEffect { get; init; }
public string? BlockAddName { get; init; }
public string? BlockRemoveName { get; init; }
public ManualResetEventSlim AddEntered { get; } = new(false);
public ManualResetEventSlim ContinueAdd { get; } = new(false);
public ManualResetEventSlim RemoveEntered { get; } = new(false);
public ManualResetEventSlim ContinueRemove { get; } = new(false);
public Action<string>? Adding { get; set; }
public Action<string>? Removing { get; set; }
public IReadOnlyList<Action<double>> RenderCallbacks => _render;
public int SubscriptionCount =>
_load.Count + _update.Count + _render.Count + _closing.Count
+ _focus.Count + _move.Count + _state.Count + _framebuffer.Count;
public void AddLoad(Action callback) => Add("load", _load, callback);
public void RemoveLoad(Action callback) => Remove("load", _load, callback);
public void AddUpdate(Action<double> callback) => Add("update", _update, callback);
public void RemoveUpdate(Action<double> callback) => Remove("update", _update, callback);
public void AddRender(Action<double> callback)
{
string name = _render.Count == 0 ? "render main" : "render pacing";
_renderNames.Add(callback, name);
Add(name, _render, callback);
}
public void RemoveRender(Action<double> callback)
{
string name = _renderNames[callback];
Remove(name, _render, callback);
_renderNames.Remove(callback);
}
public void AddClosing(Action callback) => Add("closing", _closing, callback);
public void RemoveClosing(Action callback) => Remove("closing", _closing, callback);
public void AddFocusChanged(Action<bool> callback) => Add("focus", _focus, callback);
public void RemoveFocusChanged(Action<bool> callback) => Remove("focus", _focus, callback);
public void AddMove(Action<Vector2D<int>> callback) => Add("move", _move, callback);
public void RemoveMove(Action<Vector2D<int>> callback) => Remove("move", _move, callback);
public void AddStateChanged(Action<WindowState> callback) => Add("state", _state, callback);
public void RemoveStateChanged(Action<WindowState> callback) => Remove("state", _state, callback);
public void AddFramebufferResize(Action<Vector2D<int>> callback) =>
Add("framebuffer", _framebuffer, callback);
public void RemoveFramebufferResize(Action<Vector2D<int>> callback) =>
Remove("framebuffer", _framebuffer, callback);
public void RaiseLoad() => InvokeSnapshot(_load);
public void RaiseUpdate(double delta) => InvokeSnapshot(_update, delta);
public void RaiseRender(double delta) => InvokeSnapshot(_render, delta);
public void RaiseClosing() => InvokeSnapshot(_closing);
public void RaiseFocusChanged(bool focused) => InvokeSnapshot(_focus, focused);
public void RaiseMove(Vector2D<int> position) => InvokeSnapshot(_move, position);
public void RaiseStateChanged(WindowState state) => InvokeSnapshot(_state, state);
public void RaiseFramebufferResize(Vector2D<int> size) =>
InvokeSnapshot(_framebuffer, size);
private void Add<T>(string name, List<T> callbacks, T callback)
{
Operations.Add($"add {name}");
_addAttempts++;
if (name == BlockAddName
&& Interlocked.Exchange(ref _addBlockUsed, 1) == 0)
{
AddEntered.Set();
if (!ContinueAdd.Wait(TimeSpan.FromSeconds(10)))
throw new TimeoutException("Synthetic add block timed out.");
}
bool fail = FailAddAttempt == _addAttempts;
if (fail && !FailAddAfterSideEffect)
throw new InvalidOperationException($"synthetic add failure: {name}");
callbacks.Add(callback);
Adding?.Invoke(name);
if (fail)
throw new InvalidOperationException($"synthetic add-after-side-effect failure: {name}");
}
private void Remove<T>(string name, List<T> callbacks, T callback)
{
Operations.Add($"remove {name}");
Removing?.Invoke(name);
if (name == BlockRemoveName
&& Interlocked.Exchange(ref _removeBlockUsed, 1) == 0)
{
RemoveEntered.Set();
if (!ContinueRemove.Wait(TimeSpan.FromSeconds(10)))
throw new TimeoutException("Synthetic remove block timed out.");
}
if (PersistentRemoveFailures.Contains(name))
throw new InvalidOperationException($"synthetic remove failure: {name}");
callbacks.Remove(callback);
if (RemoveAfterSideEffectFailures.Remove(name))
{
throw new InvalidOperationException(
$"synthetic remove-after-side-effect failure: {name}");
}
}
private static void InvokeSnapshot(List<Action> callbacks)
{
foreach (Action callback in callbacks.ToArray())
callback();
}
private static void InvokeSnapshot<T>(List<Action<T>> callbacks, T value)
{
foreach (Action<T> callback in callbacks.ToArray())
callback(value);
}
}
private sealed class OrderingClock : IFramePacingClock
{
public long Frequency => 1_000;
public long Timestamp { get; set; }
public long GetTimestamp() => Timestamp;
}
private sealed class OrderingWaiter(
OrderingClock clock,
List<string> order) : IFramePacingWaiter
{
public void Wait(long durationTicks, long clockFrequency)
{
order.Add("pacing render");
clock.Timestamp += durationTicks;
}
}
private sealed class CountingPacingSurface : IDisplayFramePacingSurface
{
public bool VSync { get; set; }
public int RefreshReadCount { get; private set; }
public bool TryGetActiveMonitorRefreshHz(out int refreshHz)
{
RefreshReadCount++;
refreshHz = 144;
return true;
}
}
}