Merge branch 'claude/quirky-payne-46a2e6' into claude/latest-commits-cb0c8f
This commit is contained in:
commit
629d83411d
5 changed files with 274 additions and 32 deletions
|
|
@ -156,10 +156,44 @@ internal sealed class HeadlessProcessHost : IDisposable
|
|||
Credential = source.Credential,
|
||||
};
|
||||
|
||||
internal async Task<HeadlessExitCode> RunAsync(
|
||||
internal Task<HeadlessExitCode> RunAsync(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
// #368: Runtime's gameplay owners require ONE update thread for a
|
||||
// session's whole lifetime — collision generations bind to the
|
||||
// first mutating thread and refuse migration. The graphical host
|
||||
// satisfies that with its game-loop thread; this dedicated thread
|
||||
// is the headless equivalent. Start (the live connect
|
||||
// transaction), every scheduler turn, and the post-loop captures
|
||||
// all execute here. Only disposal stays on the lifecycle thread,
|
||||
// which the Runtime teardown path explicitly supports (see
|
||||
// ResetSessionPhysics's own doc comment).
|
||||
var completion = new TaskCompletionSource<HeadlessExitCode>(
|
||||
TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var thread = new Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
completion.SetResult(
|
||||
RunOnUpdateThread(cancellationToken));
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
completion.SetException(error);
|
||||
}
|
||||
})
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "acdream-headless-update",
|
||||
};
|
||||
thread.Start();
|
||||
return completion.Task;
|
||||
}
|
||||
|
||||
private HeadlessExitCode RunOnUpdateThread(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (HeadlessSessionHost session in _sessions)
|
||||
{
|
||||
RuntimeSessionStartResult started = session.Start();
|
||||
|
|
@ -189,8 +223,7 @@ internal sealed class HeadlessProcessHost : IDisposable
|
|||
|
||||
try
|
||||
{
|
||||
await _scheduler.RunAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
_scheduler.Run(cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
when (cancellationToken.IsCancellationRequested)
|
||||
|
|
|
|||
|
|
@ -147,8 +147,31 @@ internal sealed class HeadlessProcessScheduler
|
|||
_observationPeriodTicks);
|
||||
}
|
||||
|
||||
internal async Task RunAsync(CancellationToken cancellationToken)
|
||||
/// <summary>
|
||||
/// Drives every session's deadlines on the CALLING thread until
|
||||
/// cancellation or policy completion, returning normally in both
|
||||
/// cases. The caller must dedicate one thread for a process's whole
|
||||
/// run: Runtime's gameplay owners (collision generations foremost,
|
||||
/// via <c>RuntimePhysicsState.EnsureCollisionMutationThread</c>) bind
|
||||
/// to the first mutating thread and refuse migration, and an awaited
|
||||
/// timer loop in a SynchronizationContext-free host resumes on
|
||||
/// arbitrary ThreadPool workers — which tripped that guard whenever a
|
||||
/// collision generation spanned two waits (#368). The waits below go
|
||||
/// through one rearmed <see cref="TimeProvider"/> timer signalling an
|
||||
/// event, so the loop never leaves its thread. A stale timer callback
|
||||
/// from an abandoned wait can set the event early; that only costs
|
||||
/// one extra pass over the deadline math, which re-sleeps.
|
||||
/// </summary>
|
||||
internal void Run(CancellationToken cancellationToken)
|
||||
{
|
||||
using var wake = new ManualResetEventSlim(false);
|
||||
using ITimer timer = _timeProvider.CreateTimer(
|
||||
static state => ((ManualResetEventSlim)state!).Set(),
|
||||
wake,
|
||||
Timeout.InfiniteTimeSpan,
|
||||
Timeout.InfiniteTimeSpan);
|
||||
WaitHandle[] waitHandles =
|
||||
[wake.WaitHandle, cancellationToken.WaitHandle];
|
||||
while (!cancellationToken.IsCancellationRequested
|
||||
&& HasActiveSession())
|
||||
{
|
||||
|
|
@ -169,11 +192,9 @@ internal sealed class HeadlessProcessScheduler
|
|||
: _timeProvider.GetElapsedTime(now, deadline);
|
||||
delay = NormalizeTimerDelay(delay);
|
||||
Interlocked.Increment(ref _waitCount);
|
||||
await Task.Delay(
|
||||
delay,
|
||||
_timeProvider,
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
wake.Reset();
|
||||
timer.Change(delay, Timeout.InfiniteTimeSpan);
|
||||
WaitHandle.WaitAny(waitHandles);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue