Prevent FIELD redraw flicker
This commit is contained in:
@@ -780,6 +780,17 @@ the old idle sprite composited beneath the moving clone until each segment ends.
|
|||||||
post-bind static-alpha consumption for looping objects as a transient opacity latch, cleared by the next
|
post-bind static-alpha consumption for looping objects as a transient opacity latch, cleared by the next
|
||||||
`draw-texture` bind. This preserves ADV's distinct pre-animation/static alpha-zero CG initialization.
|
`draw-texture` bind. This preserves ADV's distinct pre-animation/static alpha-zero CG initialization.
|
||||||
|
|
||||||
|
The same route exposed an independent publication-atomicity requirement. Each tile step reaches
|
||||||
|
`FIELD@0x4eb2 -> 0x9225`, which calls `DRAWMAP`, `DRAWOBJ`, and `DRAWMINIMAP`. `DRAWMAP` first erases its
|
||||||
|
old terrain handle ranges and then rebuilds them; `DRAWMINIMAP` recreates mutable surfaces `0x42..0x44`
|
||||||
|
from transparent pixels before filling the terrain and copying object/unit markers. Those are ordinary
|
||||||
|
VM-side construction bursts, not a series of front-buffer presents. Letting Godot's render thread sample
|
||||||
|
between their individual erase/fill/copy instructions displayed a partially blank minimap at every tile
|
||||||
|
and could display the erased terrain state for one frame when a complete route returned. The interactive
|
||||||
|
host now holds one writer-side presentation barrier for the whole script burst. Explicit presentation,
|
||||||
|
transition, sleep, text, movie, and input services temporarily expose the completed state; callbacks
|
||||||
|
serviced inside a parked input wait reacquire the writer side for their own atomic burst.
|
||||||
|
|
||||||
**Port implication:** replace the current “any spritesheet is active” redraw predicate with native-style
|
**Port implication:** replace the current “any spritesheet is active” redraw predicate with native-style
|
||||||
shared current/previous sampling and request a composition only when at least one visible sampled cell
|
shared current/previous sampling and request a composition only when at least one visible sampled cell
|
||||||
changes. The FIELD `sleep(1)` input-poll loop must also stop requesting a redraw when no retained mutation
|
changes. The FIELD `sleep(1)` input-poll loop must also stop requesting a redraw when no retained mutation
|
||||||
|
|||||||
74
engine/Age.Engine.Tests/ScriptPresentationBarrierTests.cs
Normal file
74
engine/Age.Engine.Tests/ScriptPresentationBarrierTests.cs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Age.Engine.Hosting;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
public class ScriptPresentationBarrierTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ScriptBurstWithholdsPresentationUntilExit()
|
||||||
|
{
|
||||||
|
var barrier = new ScriptPresentationBarrier();
|
||||||
|
|
||||||
|
barrier.EnterScript();
|
||||||
|
Assert.False(barrier.TryEnterPresentation());
|
||||||
|
|
||||||
|
barrier.ExitScript();
|
||||||
|
Assert.True(barrier.TryEnterPresentation());
|
||||||
|
barrier.ExitPresentation();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NestedScriptsRemainOneAtomicBurst()
|
||||||
|
{
|
||||||
|
var barrier = new ScriptPresentationBarrier();
|
||||||
|
|
||||||
|
barrier.EnterScript();
|
||||||
|
barrier.EnterScript();
|
||||||
|
barrier.ExitScript();
|
||||||
|
Assert.False(barrier.TryEnterPresentation());
|
||||||
|
|
||||||
|
barrier.ExitScript();
|
||||||
|
Assert.True(barrier.TryEnterPresentation());
|
||||||
|
barrier.ExitPresentation();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ServiceBoundaryTemporarilyAllowsPresentation()
|
||||||
|
{
|
||||||
|
var barrier = new ScriptPresentationBarrier();
|
||||||
|
|
||||||
|
barrier.EnterScript();
|
||||||
|
bool suspended = barrier.SuspendScript();
|
||||||
|
Assert.True(suspended);
|
||||||
|
Assert.True(barrier.TryEnterPresentation());
|
||||||
|
barrier.ExitPresentation();
|
||||||
|
|
||||||
|
barrier.ResumeScript(suspended);
|
||||||
|
Assert.False(barrier.TryEnterPresentation());
|
||||||
|
barrier.ExitScript();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RenderThreadCannotEnterWhileVmThreadIsRebuilding()
|
||||||
|
{
|
||||||
|
var barrier = new ScriptPresentationBarrier();
|
||||||
|
using var entered = new ManualResetEventSlim();
|
||||||
|
using var release = new ManualResetEventSlim();
|
||||||
|
Task vmBurst = Task.Run(() =>
|
||||||
|
{
|
||||||
|
barrier.EnterScript();
|
||||||
|
entered.Set();
|
||||||
|
release.Wait();
|
||||||
|
barrier.ExitScript();
|
||||||
|
});
|
||||||
|
|
||||||
|
await Task.Run(entered.Wait);
|
||||||
|
Assert.False(barrier.TryEnterPresentation());
|
||||||
|
|
||||||
|
release.Set();
|
||||||
|
await vmBurst;
|
||||||
|
Assert.True(barrier.TryEnterPresentation());
|
||||||
|
barrier.ExitPresentation();
|
||||||
|
}
|
||||||
|
}
|
||||||
47
engine/Age.Engine/Hosting/ScriptPresentationBarrier.cs
Normal file
47
engine/Age.Engine/Hosting/ScriptPresentationBarrier.cs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace Age.Engine.Hosting;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Keeps one VM opcode burst atomic with respect to the interactive compositor. The VM owns the
|
||||||
|
/// write side while script code is running; the render thread samples through the read side only
|
||||||
|
/// after the VM reaches a presentation, sleep, or input service boundary.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ScriptPresentationBarrier
|
||||||
|
{
|
||||||
|
private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.NoRecursion);
|
||||||
|
private int _scriptDepth;
|
||||||
|
|
||||||
|
public void EnterScript()
|
||||||
|
{
|
||||||
|
if (_scriptDepth++ == 0)
|
||||||
|
_lock.EnterWriteLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ExitScript()
|
||||||
|
{
|
||||||
|
if (_scriptDepth <= 0)
|
||||||
|
throw new InvalidOperationException("No script burst is active.");
|
||||||
|
if (--_scriptDepth == 0)
|
||||||
|
_lock.ExitWriteLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Temporarily expose the completed burst state while a host service is parked.</summary>
|
||||||
|
public bool SuspendScript()
|
||||||
|
{
|
||||||
|
if (_scriptDepth <= 0 || !_lock.IsWriteLockHeld) return false;
|
||||||
|
_lock.ExitWriteLock();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResumeScript(bool suspended)
|
||||||
|
{
|
||||||
|
if (suspended) _lock.EnterWriteLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryEnterPresentation()
|
||||||
|
=> !_lock.IsWriteLockHeld && _lock.TryEnterReadLock(0);
|
||||||
|
|
||||||
|
public void ExitPresentation() => _lock.ExitReadLock();
|
||||||
|
}
|
||||||
@@ -24,6 +24,11 @@ public sealed class GodotAdvHost : IHost
|
|||||||
private readonly string _rootScene;
|
private readonly string _rootScene;
|
||||||
private readonly object _scriptContextLock = new();
|
private readonly object _scriptContextLock = new();
|
||||||
private readonly Stack<string> _scriptContexts = new();
|
private readonly Stack<string> _scriptContexts = new();
|
||||||
|
private readonly ScriptPresentationBarrier _presentationBarrier = new();
|
||||||
|
private readonly AutoResetEvent _presentationRequestConsumed = new(false);
|
||||||
|
private readonly bool _synchronizeExplicitPresentation;
|
||||||
|
private long _explicitPresentationRequestGeneration;
|
||||||
|
private long _consumedPresentationRequestGeneration;
|
||||||
private readonly object _imageLock = new();
|
private readonly object _imageLock = new();
|
||||||
private readonly Dictionary<int, RgbaImage?> _images = new(); // packed catalog id -> decoded pixels
|
private readonly Dictionary<int, RgbaImage?> _images = new(); // packed catalog id -> decoded pixels
|
||||||
// Mutable AGE surfaces are published by replacing immutable RgbaImage snapshots, so the compositor
|
// Mutable AGE surfaces are published by replacing immutable RgbaImage snapshots, so the compositor
|
||||||
@@ -83,10 +88,12 @@ public sealed class GodotAdvHost : IHost
|
|||||||
public readonly List<(int Offset, string Text)> Captured = new();
|
public readonly List<(int Offset, string Text)> Captured = new();
|
||||||
|
|
||||||
public GodotAdvHost(Main main, ResourceMap res, string scene, Age.Engine.Hosting.FrameClock clock,
|
public GodotAdvHost(Main main, ResourceMap res, string scene, Age.Engine.Hosting.FrameClock clock,
|
||||||
PageLocatorState locator, GodotTimelineLog? timeline = null)
|
PageLocatorState locator, GodotTimelineLog? timeline = null,
|
||||||
|
bool synchronizeExplicitPresentation = true)
|
||||||
{
|
{
|
||||||
_main = main; _res = res; _rootScene = scene; _clock = clock;
|
_main = main; _res = res; _rootScene = scene; _clock = clock;
|
||||||
_locator = locator; _timeline = timeline;
|
_locator = locator; _timeline = timeline;
|
||||||
|
_synchronizeExplicitPresentation = synchronizeExplicitPresentation;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ReportWarning(string message) => System.Console.Error.WriteLine(message);
|
public void ReportWarning(string message) => System.Console.Error.WriteLine(message);
|
||||||
@@ -98,6 +105,7 @@ public sealed class GodotAdvHost : IHost
|
|||||||
|
|
||||||
public void EnterScriptContext(string scriptName)
|
public void EnterScriptContext(string scriptName)
|
||||||
{
|
{
|
||||||
|
_presentationBarrier.EnterScript();
|
||||||
string scene = System.IO.Path.GetFileNameWithoutExtension(scriptName).ToUpperInvariant();
|
string scene = System.IO.Path.GetFileNameWithoutExtension(scriptName).ToUpperInvariant();
|
||||||
lock (_scriptContextLock) _scriptContexts.Push(scene);
|
lock (_scriptContextLock) _scriptContexts.Push(scene);
|
||||||
_timeline?.Event("script-context-enter", new() { ["scene"] = scene });
|
_timeline?.Event("script-context-enter", new() { ["scene"] = scene });
|
||||||
@@ -109,6 +117,25 @@ public sealed class GodotAdvHost : IHost
|
|||||||
lock (_scriptContextLock)
|
lock (_scriptContextLock)
|
||||||
if (_scriptContexts.TryPop(out var popped)) scene = popped;
|
if (_scriptContexts.TryPop(out var popped)) scene = popped;
|
||||||
if (scene != null) _timeline?.Event("script-context-exit", new() { ["scene"] = scene });
|
if (scene != null) _timeline?.Event("script-context-exit", new() { ["scene"] = scene });
|
||||||
|
_presentationBarrier.ExitScript();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryEnterPresentation() => _presentationBarrier.TryEnterPresentation();
|
||||||
|
|
||||||
|
public void ExitPresentation() => _presentationBarrier.ExitPresentation();
|
||||||
|
|
||||||
|
private bool SuspendScriptForPresentation() => _presentationBarrier.SuspendScript();
|
||||||
|
|
||||||
|
private void ResumeScriptAfterPresentation(bool suspended)
|
||||||
|
=> _presentationBarrier.ResumeScript(suspended);
|
||||||
|
|
||||||
|
private void RequestSynchronizedPresentation()
|
||||||
|
{
|
||||||
|
long requested = Interlocked.Increment(ref _explicitPresentationRequestGeneration);
|
||||||
|
Interlocked.Exchange(ref _presentRequested, 1);
|
||||||
|
if (!_synchronizeExplicitPresentation) return;
|
||||||
|
while (Interlocked.Read(ref _consumedPresentationRequestGeneration) < requested && !_stopping)
|
||||||
|
_presentationRequestConsumed.WaitOne(50);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ShowText(int offset, string text)
|
public void ShowText(int offset, string text)
|
||||||
@@ -127,6 +154,10 @@ public sealed class GodotAdvHost : IHost
|
|||||||
["offset"] = $"0x{offset:x}", ["x"] = _advTextX, ["y"] = _advTextY,
|
["offset"] = $"0x{offset:x}", ["x"] = _advTextX, ["y"] = _advTextY,
|
||||||
["glyphs"] = text.Length, ["delay_ms"] = 50,
|
["glyphs"] = text.Length, ["delay_ms"] = 50,
|
||||||
});
|
});
|
||||||
|
bool scriptSuspended = SuspendScriptForPresentation();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RequestSynchronizedPresentation();
|
||||||
while (IsTextRevealing && !_stopping)
|
while (IsTextRevealing && !_stopping)
|
||||||
{
|
{
|
||||||
lock (_textLock)
|
lock (_textLock)
|
||||||
@@ -136,6 +167,11 @@ public sealed class GodotAdvHost : IHost
|
|||||||
}
|
}
|
||||||
if (IsTextRevealing) _frameSignal.WaitOne(50);
|
if (IsTextRevealing) _frameSignal.WaitOne(50);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ResumeScriptAfterPresentation(scriptSuspended);
|
||||||
|
}
|
||||||
_timeline?.State("running", new() { ["text_reveal_complete"] = true });
|
_timeline?.State("running", new() { ["text_reveal_complete"] = true });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,9 +399,6 @@ public sealed class GodotAdvHost : IHost
|
|||||||
Pages++;
|
Pages++;
|
||||||
_locator.Wait(Pages);
|
_locator.Wait(Pages);
|
||||||
_main.CallDeferred("PageBreak");
|
_main.CallDeferred("PageBreak");
|
||||||
// Publish retained mutations accumulated before the wait once. A static input wait is not itself a
|
|
||||||
// reason to rebuild the 800x600 background every frame; ambient channels are queried separately.
|
|
||||||
System.Threading.Interlocked.Exchange(ref _presentRequested, 1);
|
|
||||||
lock (_textLock)
|
lock (_textLock)
|
||||||
{
|
{
|
||||||
_activeWaitLayout = layoutSlot == 0 ? _currentAdvLayout : layoutSlot;
|
_activeWaitLayout = layoutSlot == 0 ? _currentAdvLayout : layoutSlot;
|
||||||
@@ -377,13 +410,29 @@ public sealed class GodotAdvHost : IHost
|
|||||||
var autoTimer = new AdvAutoAdvanceTimer();
|
var autoTimer = new AdvAutoAdvanceTimer();
|
||||||
bool autoAdvanced = false;
|
bool autoAdvanced = false;
|
||||||
bool messageSkipped = false;
|
bool messageSkipped = false;
|
||||||
|
bool scriptSuspended = SuspendScriptForPresentation();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Publish the completed pre-wait burst once. Callback scripts temporarily reacquire the
|
||||||
|
// write side below, so each hover/click update is likewise exposed only after it returns.
|
||||||
|
RequestSynchronizedPresentation();
|
||||||
while (!_stopping)
|
while (!_stopping)
|
||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
bool markerWasEnabled;
|
bool markerWasEnabled;
|
||||||
lock (_textLock) markerWasEnabled = _waitIndicatorEnabled;
|
lock (_textLock) markerWasEnabled = _waitIndicatorEnabled;
|
||||||
bool keepServicing = serviceInputCallback();
|
if (scriptSuspended) ResumeScriptAfterPresentation(true);
|
||||||
|
bool keepServicing;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
keepServicing = serviceInputCallback();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (scriptSuspended && !SuspendScriptForPresentation())
|
||||||
|
throw new InvalidOperationException("Input callback did not retain the script burst.");
|
||||||
|
}
|
||||||
// The native callback returns through the shared ADV redraw/wait path, whose op 0x72
|
// The native callback returns through the shared ADV redraw/wait path, whose op 0x72
|
||||||
// re-arms a marker stopped by nested HISTORY. Our blocking host keeps the parent wait
|
// re-arms a marker stopped by nested HISTORY. Our blocking host keeps the parent wait
|
||||||
// parked, so restore that parent-owned state at the equivalent callback boundary.
|
// parked, so restore that parent-owned state at the equivalent callback boundary.
|
||||||
@@ -411,6 +460,11 @@ public sealed class GodotAdvHost : IHost
|
|||||||
WaitHandle.WaitAny(new[] { _gate.AvailableWaitHandle, _inputCallbackSignal, _frameSignal });
|
WaitHandle.WaitAny(new[] { _gate.AvailableWaitHandle, _inputCallbackSignal, _frameSignal });
|
||||||
if (_gate.Wait(0)) break;
|
if (_gate.Wait(0)) break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ResumeScriptAfterPresentation(scriptSuspended);
|
||||||
|
}
|
||||||
IsWaiting = false;
|
IsWaiting = false;
|
||||||
lock (_textLock) _waitIndicatorEnabled = false;
|
lock (_textLock) _waitIndicatorEnabled = false;
|
||||||
_timeline?.State("running", new()
|
_timeline?.State("running", new()
|
||||||
@@ -558,11 +612,20 @@ public sealed class GodotAdvHost : IHost
|
|||||||
public void WaitForForegroundTransition(GfxState gfx)
|
public void WaitForForegroundTransition(GfxState gfx)
|
||||||
{
|
{
|
||||||
int started = gfx.StartForegroundTransitions(_clock.NowMs);
|
int started = gfx.StartForegroundTransitions(_clock.NowMs);
|
||||||
if (started == 0 && !gfx.HasActiveTimedPresentation(_clock.NowMs) && !HasActiveMoviePresentation()) return;
|
bool hasActivePresentation =
|
||||||
|
gfx.HasActiveTimedPresentation(_clock.NowMs) || HasActiveMoviePresentation();
|
||||||
|
if (hasActivePresentation)
|
||||||
|
{
|
||||||
_foregroundGfx = gfx;
|
_foregroundGfx = gfx;
|
||||||
System.Threading.Interlocked.Exchange(ref _transitionStartedAtMs, _clock.NowMs);
|
System.Threading.Interlocked.Exchange(ref _transitionStartedAtMs, _clock.NowMs);
|
||||||
IsTransitionWaiting = true;
|
IsTransitionWaiting = true;
|
||||||
_timeline?.State("transition-start", new() { ["count"] = started });
|
_timeline?.State("transition-start", new() { ["count"] = started });
|
||||||
|
}
|
||||||
|
bool scriptSuspended = SuspendScriptForPresentation();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RequestSynchronizedPresentation();
|
||||||
|
if (!hasActivePresentation) return;
|
||||||
int lastBucket = -1;
|
int lastBucket = -1;
|
||||||
while ((gfx.HasActiveTimedPresentation(_clock.NowMs) || HasActiveMoviePresentation()) && !_stopping)
|
while ((gfx.HasActiveTimedPresentation(_clock.NowMs) || HasActiveMoviePresentation()) && !_stopping)
|
||||||
{
|
{
|
||||||
@@ -581,7 +644,12 @@ public sealed class GodotAdvHost : IHost
|
|||||||
}
|
}
|
||||||
// The active query becomes false at the exact transition/movie endpoint. Publish that terminal sample
|
// The active query becomes false at the exact transition/movie endpoint. Publish that terminal sample
|
||||||
// once so the last visible frame cannot remain fractionally incomplete.
|
// once so the last visible frame cannot remain fractionally incomplete.
|
||||||
System.Threading.Interlocked.Exchange(ref _presentRequested, 1);
|
RequestSynchronizedPresentation();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ResumeScriptAfterPresentation(scriptSuspended);
|
||||||
|
}
|
||||||
IsTransitionWaiting = false;
|
IsTransitionWaiting = false;
|
||||||
System.Threading.Interlocked.Exchange(ref _transitionStartedAtMs, -1);
|
System.Threading.Interlocked.Exchange(ref _transitionStartedAtMs, -1);
|
||||||
_foregroundGfx = null;
|
_foregroundGfx = null;
|
||||||
@@ -610,7 +678,15 @@ public sealed class GodotAdvHost : IHost
|
|||||||
{
|
{
|
||||||
["started"] = started, ["completed"] = completed,
|
["started"] = started, ["completed"] = completed,
|
||||||
});
|
});
|
||||||
System.Threading.Interlocked.Exchange(ref _presentRequested, 1);
|
bool scriptSuspended = SuspendScriptForPresentation();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RequestSynchronizedPresentation();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ResumeScriptAfterPresentation(scriptSuspended);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Op 0x25's native mode-4 path advances an 8-bit alpha accumulator. Values <=64 use the
|
// Op 0x25's native mode-4 path advances an 8-bit alpha accumulator. Values <=64 use the
|
||||||
@@ -640,6 +716,10 @@ public sealed class GodotAdvHost : IHost
|
|||||||
["interval_argument"] = intervalArgument, ["duration_ms"] = duration,
|
["interval_argument"] = intervalArgument, ["duration_ms"] = duration,
|
||||||
["source_objects"] = source.Count, ["target_objects"] = target.Count,
|
["source_objects"] = source.Count, ["target_objects"] = target.Count,
|
||||||
});
|
});
|
||||||
|
bool scriptSuspended = SuspendScriptForPresentation();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RequestSynchronizedPresentation();
|
||||||
while (!_stopping)
|
while (!_stopping)
|
||||||
{
|
{
|
||||||
bool complete;
|
bool complete;
|
||||||
@@ -652,7 +732,7 @@ public sealed class GodotAdvHost : IHost
|
|||||||
// Publish the exact target endpoint before the following surface releases/root reload.
|
// Publish the exact target endpoint before the following surface releases/root reload.
|
||||||
lock (_screenTransitionLock)
|
lock (_screenTransitionLock)
|
||||||
if (_screenTransition != null) _screenTransition.Forced = true;
|
if (_screenTransition != null) _screenTransition.Forced = true;
|
||||||
System.Threading.Interlocked.Exchange(ref _presentRequested, 1);
|
RequestSynchronizedPresentation();
|
||||||
// Do not let the VM release both source surfaces (or immediately reload SYSTEM4) until the
|
// Do not let the VM release both source surfaces (or immediately reload SYSTEM4) until the
|
||||||
// main thread has actually published the terminal target frame.
|
// main thread has actually published the terminal target frame.
|
||||||
while (!_stopping)
|
while (!_stopping)
|
||||||
@@ -661,6 +741,11 @@ public sealed class GodotAdvHost : IHost
|
|||||||
if (_screenTransition == null) break;
|
if (_screenTransition == null) break;
|
||||||
_frameSignal.WaitOne(50);
|
_frameSignal.WaitOne(50);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ResumeScriptAfterPresentation(scriptSuspended);
|
||||||
|
}
|
||||||
IsTransitionWaiting = false;
|
IsTransitionWaiting = false;
|
||||||
System.Threading.Interlocked.Exchange(ref _transitionStartedAtMs, -1);
|
System.Threading.Interlocked.Exchange(ref _transitionStartedAtMs, -1);
|
||||||
_timeline?.State("running", new() { ["screen_transition_complete"] = true });
|
_timeline?.State("running", new() { ["screen_transition_complete"] = true });
|
||||||
@@ -694,7 +779,12 @@ public sealed class GodotAdvHost : IHost
|
|||||||
lock (_screenTransitionLock) screenTransitionActive = _screenTransition != null;
|
lock (_screenTransitionLock) screenTransitionActive = _screenTransition != null;
|
||||||
var reasons = HostPresentationReason.None;
|
var reasons = HostPresentationReason.None;
|
||||||
if (System.Threading.Interlocked.Exchange(ref _presentRequested, 0) != 0)
|
if (System.Threading.Interlocked.Exchange(ref _presentRequested, 0) != 0)
|
||||||
|
{
|
||||||
reasons |= HostPresentationReason.HostRequest;
|
reasons |= HostPresentationReason.HostRequest;
|
||||||
|
long requested = Interlocked.Read(ref _explicitPresentationRequestGeneration);
|
||||||
|
Interlocked.Exchange(ref _consumedPresentationRequestGeneration, requested);
|
||||||
|
_presentationRequestConsumed.Set();
|
||||||
|
}
|
||||||
if (screenTransitionActive) reasons |= HostPresentationReason.ScreenTransition;
|
if (screenTransitionActive) reasons |= HostPresentationReason.ScreenTransition;
|
||||||
GfxPresentationReason gfxReasons = gfx.ConsumePresentationReasons(_clock.NowMs);
|
GfxPresentationReason gfxReasons = gfx.ConsumePresentationReasons(_clock.NowMs);
|
||||||
if ((gfxReasons & GfxPresentationReason.RetainedMutation) != 0)
|
if ((gfxReasons & GfxPresentationReason.RetainedMutation) != 0)
|
||||||
@@ -821,11 +911,20 @@ public sealed class GodotAdvHost : IHost
|
|||||||
long deadline = _clock.NowMs + ms;
|
long deadline = _clock.NowMs + ms;
|
||||||
_timeline?.State("sleep", new() { ["duration_ms"] = ms, ["deadline_ms"] = deadline });
|
_timeline?.State("sleep", new() { ["duration_ms"] = ms, ["deadline_ms"] = deadline });
|
||||||
IsSleeping = true;
|
IsSleeping = true;
|
||||||
|
bool scriptSuspended = SuspendScriptForPresentation();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RequestSynchronizedPresentation();
|
||||||
while (_clock.NowMs < deadline)
|
while (_clock.NowMs < deadline)
|
||||||
{
|
{
|
||||||
if (_stopping) break;
|
if (_stopping) break;
|
||||||
_frameSignal.WaitOne(50);
|
_frameSignal.WaitOne(50);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ResumeScriptAfterPresentation(scriptSuspended);
|
||||||
|
}
|
||||||
IsSleeping = false;
|
IsSleeping = false;
|
||||||
_timeline?.State("running", new() { ["sleep_complete"] = true });
|
_timeline?.State("running", new() { ["sleep_complete"] = true });
|
||||||
}
|
}
|
||||||
@@ -1023,6 +1122,7 @@ public sealed class GodotAdvHost : IHost
|
|||||||
|
|
||||||
_modalMovieCancelled = false;
|
_modalMovieCancelled = false;
|
||||||
_modalMovieWaiting = true;
|
_modalMovieWaiting = true;
|
||||||
|
bool scriptSuspended = false;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!StartMovie(asset, resourceId, surfaceSlot, movieFlags, 0, modal: true,
|
if (!StartMovie(asset, resourceId, surfaceSlot, movieFlags, 0, modal: true,
|
||||||
@@ -1032,6 +1132,8 @@ public sealed class GodotAdvHost : IHost
|
|||||||
["resource"] = resourceId, ["playback"] = playbackId,
|
["resource"] = resourceId, ["playback"] = playbackId,
|
||||||
["surface"] = surfaceSlot, ["file"] = asset.Name,
|
["surface"] = surfaceSlot, ["file"] = asset.Name,
|
||||||
});
|
});
|
||||||
|
scriptSuspended = SuspendScriptForPresentation();
|
||||||
|
RequestSynchronizedPresentation();
|
||||||
while (!_stopping && !_modalMovieCancelled)
|
while (!_stopping && !_modalMovieCancelled)
|
||||||
{
|
{
|
||||||
if (!_movieSurfaces.IsActive(surfaceSlot)) break;
|
if (!_movieSurfaces.IsActive(surfaceSlot)) break;
|
||||||
@@ -1050,6 +1152,7 @@ public sealed class GodotAdvHost : IHost
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
ResumeScriptAfterPresentation(scriptSuspended);
|
||||||
_modalMovieWaiting = false;
|
_modalMovieWaiting = false;
|
||||||
_modalMovieCancelled = false;
|
_modalMovieCancelled = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -245,7 +245,13 @@ public partial class Main : Godot.Control
|
|||||||
_locator = new PageLocatorState(scene, _selftest ? null : pageMapPath);
|
_locator = new PageLocatorState(scene, _selftest ? null : pageMapPath);
|
||||||
_locatorHud.Visible = _locatorHudVisible;
|
_locatorHud.Visible = _locatorHudVisible;
|
||||||
var resources = scripts != null ? new ResourceMap(scripts.Catalog) : ResourceMap.Load();
|
var resources = scripts != null ? new ResourceMap(scripts.Catalog) : ResourceMap.Load();
|
||||||
_host = new GodotAdvHost(this, resources, scene, _clock, _locator, _timeline) { SleepScale = sleepScale, TraceOps = _gfxLogPath != null };
|
_host = new GodotAdvHost(
|
||||||
|
this, resources, scene, _clock, _locator, _timeline,
|
||||||
|
synchronizeExplicitPresentation: !_selftest)
|
||||||
|
{
|
||||||
|
SleepScale = sleepScale,
|
||||||
|
TraceOps = _gfxLogPath != null,
|
||||||
|
};
|
||||||
_trace = new GodotTraceSink(_locator, _timeline);
|
_trace = new GodotTraceSink(_locator, _timeline);
|
||||||
if (_perfLogPath != null) _perf = new PerformanceFrameLog(_perfLogPath);
|
if (_perfLogPath != null) _perf = new PerformanceFrameLog(_perfLogPath);
|
||||||
// --trace-histogram: aggregate op/call-site execution counts of the REAL Godot run (headless flow
|
// --trace-histogram: aggregate op/call-site execution counts of the REAL Godot run (headless flow
|
||||||
@@ -382,15 +388,32 @@ public partial class Main : Godot.Control
|
|||||||
perf?.RecordMovies(PerformanceFrameLog.Timestamp() - phase);
|
perf?.RecordMovies(PerformanceFrameLog.Timestamp() - phase);
|
||||||
|
|
||||||
phase = perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
phase = perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
||||||
HostPresentationReason presentationReasons = !_selftest && _vm != null && _host != null
|
HostPresentationReason presentationReasons = HostPresentationReason.None;
|
||||||
? _host.ConsumePresentationReasons(_vm.Gfx)
|
bool presentationEntered = !_selftest && _vm != null && _host != null
|
||||||
: HostPresentationReason.None;
|
&& _host.TryEnterPresentation();
|
||||||
bool shouldRecomposite = presentationReasons != HostPresentationReason.None;
|
long allocationPhase = perf != null ? PerformanceFrameLog.AllocatedBytes() : 0;
|
||||||
|
bool shouldRecomposite = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (presentationEntered)
|
||||||
|
{
|
||||||
|
presentationReasons = _host!.ConsumePresentationReasons(_vm!.Gfx);
|
||||||
|
shouldRecomposite = presentationReasons != HostPresentationReason.None;
|
||||||
perf?.RecordPresentationReasons((int)presentationReasons);
|
perf?.RecordPresentationReasons((int)presentationReasons);
|
||||||
perf?.RecordShouldRecomposite(PerformanceFrameLog.Timestamp() - phase);
|
perf?.RecordShouldRecomposite(PerformanceFrameLog.Timestamp() - phase);
|
||||||
long allocationPhase = perf != null ? PerformanceFrameLog.AllocatedBytes() : 0;
|
|
||||||
if (shouldRecomposite)
|
if (shouldRecomposite)
|
||||||
Recomposite(); // native publishes retained mutations only at present/service boundaries
|
Recomposite(); // native publishes retained mutations only at present/service boundaries
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
perf?.RecordPresentationReasons(0);
|
||||||
|
perf?.RecordShouldRecomposite(PerformanceFrameLog.Timestamp() - phase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (presentationEntered) _host!.ExitPresentation();
|
||||||
|
}
|
||||||
perf?.RecordRecomposeAllocation(PerformanceFrameLog.AllocatedBytes() - allocationPhase);
|
perf?.RecordRecomposeAllocation(PerformanceFrameLog.AllocatedBytes() - allocationPhase);
|
||||||
|
|
||||||
phase = perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
phase = perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user