fix: await playout callbacks before refresh

This commit is contained in:
2026-07-11 05:03:31 +09:00
parent d8b9641a75
commit 3e3292b8a9
6 changed files with 358 additions and 32 deletions

View File

@@ -213,6 +213,13 @@ public sealed record PlayoutStatus(
{
public int OperationTimeoutMilliseconds { get; init; } = 5_000;
/// <summary>
/// True while Tornado has accepted an SDK Play call but has not yet delivered the
/// corresponding scene-played completion callback. This is observation-only state;
/// callers must not retry or infer completion by issuing another scene mutation.
/// </summary>
public bool IsPlayCompletionPending { get; init; }
/// <summary>
/// True only after the adapter entered the KAEngine.KTAPConnect dispatch path.
/// Process discovery, registration probes, DryRun, and configuration preflight do not set it.

View File

@@ -157,10 +157,10 @@ internal sealed class DynamicK3dSession : IK3dGuardedConnectSession
private object? _engine;
private object? _eventHandler;
private object? _player;
private object? _preparedScene;
private object? _currentScene;
private readonly List<object> _retiredScenes = [];
private readonly List<object> _pendingTakeOutScenes = [];
private SceneReference? _preparedScene;
private SceneReference? _currentScene;
private readonly List<SceneReference> _retiredScenes = [];
private readonly List<SceneReference> _pendingTakeOutScenes = [];
private int? _outputChannel;
private int? _ownerThreadId;
private int _lastKtapConnectState;
@@ -408,19 +408,21 @@ internal sealed class DynamicK3dSession : IK3dGuardedConnectSession
var engine = _engine!;
var player = _player!;
var outputChannel = _outputChannel;
object? nextScene = null;
SceneReference? nextScene = null;
var transactionStarted = false;
try
{
nextScene = InvokeRequired(engine, "LoadScene", cue.SceneFile, cue.SceneName);
nextScene = new SceneReference(
InvokeRequired(engine, "LoadScene", cue.SceneFile, cue.SceneName),
cue.SceneName);
if (outputChannel.HasValue)
{
Invoke(nextScene, "SetOutputChannelIndex", outputChannel.GetValueOrDefault());
Invoke(nextScene.Value, "SetOutputChannelIndex", outputChannel.GetValueOrDefault());
}
Invoke(
nextScene,
nextScene.Value,
"SetSceneEffectType",
K3dComConstants.SceneEffectInEnabled,
K3dComConstants.SceneChangeEffectFade,
@@ -430,7 +432,7 @@ internal sealed class DynamicK3dSession : IK3dGuardedConnectSession
{
if (IsBeforeTransactionSceneSetupMutation(mutation))
{
ApplySceneSetupMutation(nextScene, mutation);
ApplySceneSetupMutation(nextScene.Value, mutation);
}
}
@@ -438,18 +440,18 @@ internal sealed class DynamicK3dSession : IK3dGuardedConnectSession
transactionStarted = true;
foreach (var field in cue.Fields ?? [])
{
ApplyField(nextScene, field);
ApplyField(nextScene.Value, field);
}
foreach (var mutation in cue.Mutations ?? [])
{
if (!IsBeforeTransactionSceneSetupMutation(mutation))
{
ApplyTransactionMutation(nextScene, mutation);
ApplyTransactionMutation(nextScene.Value, mutation);
}
}
Invoke(nextScene, "QueryVariables");
Invoke(nextScene.Value, "QueryVariables");
if (outputChannel.HasValue)
{
@@ -461,7 +463,7 @@ internal sealed class DynamicK3dSession : IK3dGuardedConnectSession
}
transactionStarted = false;
Invoke(player, "Prepare", layoutIndex, nextScene);
Invoke(player, "Prepare", layoutIndex, nextScene.Value);
var replacedPreparedScene = _preparedScene;
_preparedScene = nextScene;
@@ -503,7 +505,7 @@ internal sealed class DynamicK3dSession : IK3dGuardedConnectSession
var previousCurrentScene = _currentScene;
_currentScene = preparedScene;
if (previousCurrentScene is not null &&
!ReferenceEquals(previousCurrentScene, preparedScene))
!ReferenceEquals(previousCurrentScene.Value, preparedScene.Value))
{
RetainRetiredScene(previousCurrentScene);
}
@@ -586,10 +588,10 @@ internal sealed class DynamicK3dSession : IK3dGuardedConnectSession
// scene. An in-place page update must never retire/unload the active scene.
if (_currentScene is null)
{
_currentScene = playingScene;
_currentScene = new SceneReference(playingScene, cue.SceneName);
playingScene = null;
}
else if (ReferenceEquals(_currentScene, playingScene))
else if (ReferenceEquals(_currentScene.Value, playingScene))
{
playingScene = null;
}
@@ -1029,7 +1031,7 @@ internal sealed class DynamicK3dSession : IK3dGuardedConnectSession
private void ReleaseAll()
{
var scenes = new List<object>();
var scenes = new List<SceneReference>();
AddDistinctScene(scenes, _preparedScene);
_preparedScene = null;
AddDistinctScene(scenes, _currentScene);
@@ -1059,7 +1061,7 @@ internal sealed class DynamicK3dSession : IK3dGuardedConnectSession
foreach (var scene in scenes)
{
ReleaseComObject(scene);
ReleaseComObject(scene.Value);
}
ReleaseComObject(player);
@@ -1067,12 +1069,12 @@ internal sealed class DynamicK3dSession : IK3dGuardedConnectSession
ReleaseComObject(eventHandler);
}
private void RetainRetiredScene(object? scene)
private void RetainRetiredScene(SceneReference? scene)
{
if (scene is not null &&
!ReferenceEquals(scene, _preparedScene) &&
!ReferenceEquals(scene, _currentScene) &&
!_retiredScenes.Any(item => ReferenceEquals(item, scene)))
!ReferenceEquals(scene.Value, _preparedScene?.Value) &&
!ReferenceEquals(scene.Value, _currentScene?.Value) &&
!_retiredScenes.Any(item => ReferenceEquals(item.Value, scene.Value)))
{
_retiredScenes.Add(scene);
}
@@ -1092,16 +1094,38 @@ internal sealed class DynamicK3dSession : IK3dGuardedConnectSession
_retiredScenes.Clear();
}
private int UnloadAndRelease(List<object> scenes)
private int UnloadAndRelease(List<SceneReference> scenes)
{
var unloaded = 0;
var activeSceneNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
if (_preparedScene is not null)
{
activeSceneNames.Add(_preparedScene.Name);
}
if (_currentScene is not null)
{
activeSceneNames.Add(_currentScene.Name);
}
var unloadedSceneNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
while (scenes.Count > 0)
{
var scene = scenes[0];
Invoke(scene, "Unload");
var shouldUnload =
!activeSceneNames.Contains(scene.Name) &&
unloadedSceneNames.Add(scene.Name);
if (shouldUnload)
{
Invoke(scene.Value, "Unload");
}
scenes.RemoveAt(0);
ReleaseComObject(scene);
unloaded++;
ReleaseComObject(scene.Value);
if (shouldUnload)
{
unloaded++;
}
}
return unloaded;
@@ -1114,14 +1138,19 @@ internal sealed class DynamicK3dSession : IK3dGuardedConnectSession
callback.LayerIndex == layoutIndex;
}
private static void AddDistinctScene(List<object> scenes, object? scene)
private static void AddDistinctScene(
List<SceneReference> scenes,
SceneReference? scene)
{
if (scene is not null && !scenes.Any(item => ReferenceEquals(item, scene)))
if (scene is not null &&
!scenes.Any(item => ReferenceEquals(item.Value, scene.Value)))
{
scenes.Add(scene);
}
}
private sealed record SceneReference(object Value, string Name);
private object InvokeRequired(object target, string method, params object?[] arguments) =>
Invoke(target, method, arguments)
?? throw new InvalidOperationException("The K3D operation returned no object.");

View File

@@ -1829,6 +1829,7 @@ internal sealed class TornadoPlayoutEngine : IPlayoutEngine
Interlocked.Increment(ref _sequence))
{
OperationTimeoutMilliseconds = (int)_options.OperationTimeout.TotalMilliseconds,
IsPlayCompletionPending = _session?.HasPendingPlayCallbacks == true,
LastKtapConnectState = (PlayoutKtapConnectState)Volatile.Read(
ref _lastKtapConnectState),
KtapHelloObserved = Volatile.Read(ref _ktapHelloObservedState) switch