Fix incremental menu animation presentation

This commit is contained in:
gamer147
2026-07-30 11:04:29 -04:00
parent efa8fbd03a
commit 37bf8d31c8
10 changed files with 171 additions and 39 deletions

View File

@@ -99,6 +99,8 @@ public sealed class GodotAdvHost : IHost
private readonly Dictionary<int, IReadOnlyList<RenderObject>> _renderTargetSnapshots = new();
private readonly object _backbufferRangeLock = new();
private GfxHandleRange _backbufferRange = GfxHandleRange.All;
private bool _backbufferPublicationIsIncremental;
private bool _backbufferClearPending;
private LegacyScreenTransition? _screenTransition;
public volatile bool IsWaiting;
public volatile bool IsTransitionWaiting;
@@ -472,26 +474,41 @@ public sealed class GodotAdvHost : IHost
PublishObjectRangeToSurface(gfx, firstHandle, count);
return;
}
// The software/GPU port reconstructs the backbuffer instead of preserving native D3D pixels.
// Zero-based ranges therefore define the complete published scene. A nonzero range is an
// incremental overlay in native code, so retain the full reconstruction for those call sites.
lock (_backbufferRangeLock)
_backbufferRange = firstHandle == 0
? new GfxHandleRange(firstHandle, count)
: GfxHandleRange.All;
System.Threading.Interlocked.Exchange(ref _presentRequested, 1);
{
_backbufferRange = new GfxHandleRange(firstHandle, count);
_backbufferPublicationIsIncremental = true;
}
_timeline?.Event("present-object-range", new() { ["first"] = firstHandle, ["count"] = count });
// Native backbuffer 0x222 ends its D3D scene and calls Present before returning. Snapshot this
// exact retained state before its callback can mutate or release the capture surface.
bool scriptSuspended = SuspendScriptForPresentation();
try
{
RequestSynchronizedPresentation();
}
finally
{
ResumeScriptAfterPresentation(scriptSuspended);
}
}
public void SnapshotBackbufferObjects(GfxState gfx, long nowMs, List<RenderObject> snapshot)
public bool SnapshotBackbufferObjects(GfxState gfx, long nowMs, List<RenderObject> snapshot)
{
gfx.SnapshotVisibleObjects(nowMs, snapshot);
GfxHandleRange range;
lock (_backbufferRangeLock) range = _backbufferRange;
bool preserveExistingPixels;
lock (_backbufferRangeLock)
{
range = _backbufferRange;
preserveExistingPixels = _backbufferPublicationIsIncremental && !_backbufferClearPending;
_backbufferClearPending = false;
}
int write = 0;
for (int read = 0; read < snapshot.Count; read++)
if (range.Contains(snapshot[read].Handle)) snapshot[write++] = snapshot[read];
if (write < snapshot.Count) snapshot.RemoveRange(write, snapshot.Count - write);
return preserveExistingPixels;
}
public void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config)
@@ -852,7 +869,11 @@ public sealed class GodotAdvHost : IHost
});
return;
}
lock (_backbufferRangeLock) _backbufferRange = GfxHandleRange.All;
lock (_backbufferRangeLock)
{
_backbufferRange = GfxHandleRange.All;
_backbufferPublicationIsIncremental = false;
}
int started = gfx.StartForegroundTransitions(_clock.NowMs);
int completed = gfx.CompleteForegroundTransitions(_clock.NowMs);
if (started > 0 || completed > 0)
@@ -1038,7 +1059,12 @@ public sealed class GodotAdvHost : IHost
// scene_context_init_reset releases ordinary surface/movie bindings but keeps decoded asset
// caches and process-owned audio/configuration available to the reloaded SYSTEM4 root.
ReleaseSurfaceRange(0, 1000);
lock (_backbufferRangeLock) _backbufferRange = GfxHandleRange.All;
lock (_backbufferRangeLock)
{
_backbufferRange = GfxHandleRange.All;
_backbufferPublicationIsIncremental = false;
_backbufferClearPending = false;
}
lock (_textLock)
{
_surfaceText.Clear();
@@ -1155,6 +1181,19 @@ public sealed class GodotAdvHost : IHost
_timeline?.State("running", new() { ["sleep_complete"] = true });
}
public void WaitForTimedCallbackDeadline(long duration)
{
long ms = NormalizeSleepMilliseconds(duration, 1.0);
long deadline = _clock.NowMs + ms;
_timeline?.State("timed-callback-wait",
new() { ["duration_ms"] = ms, ["deadline_ms"] = deadline });
// Keep the script-side presentation barrier held. Native run-state 0x40 advances the timer
// and message pump but suppresses ordinary retained rendering until the callback's explicit 0x222.
while (_clock.NowMs < deadline && !_stopping)
_frameSignal.WaitOne(50);
_timeline?.State("running", new() { ["timed_callback_due"] = true });
}
// ---- texture ops (run on the VM thread; marshal Godot node work to the main thread) ----
public bool TraceOps; // --gfx-log: print set-texture/create-texture slot assignments (diagnose slot collisions)
@@ -1533,11 +1572,14 @@ public sealed class GodotAdvHost : IHost
public void ClearRenderTarget(int surfaceSlot)
{
// The retained compositor rebuilds the backbuffer from black at the next publication boundary.
// For an offscreen target, discard separately retained text draws so its modeled pixel contents
// observe the native D3D clear as well.
if (surfaceSlot >= 0)
if (surfaceSlot < 0)
{
lock (_backbufferRangeLock) _backbufferClearPending = true;
}
else
{
// For an offscreen target, discard separately retained text draws so its modeled pixel
// contents observe the native D3D clear as well.
lock (_textLock) _surfaceText.Remove(surfaceSlot);
lock (_imageLock)
{

View File

@@ -64,9 +64,9 @@ internal sealed class GpuRetainedRenderer : IDisposable
};
}
public void BeginFrame()
public void BeginFrame(bool preserveExistingLayers)
{
_used = 0;
if (!preserveExistingLayers) _used = 0;
_textureUploads = 0;
_textureUploadTicks = 0;
}

View File

@@ -1057,15 +1057,19 @@ public partial class Main : Godot.Control
private void Recomposite()
{
bool gpuSnapshotCaptured = false;
if (_useGpuBackend && TryRecompositeGpu(out gpuSnapshotCaptured)) return;
bool preserveExistingPixels = false;
if (_useGpuBackend &&
TryRecompositeGpu(out gpuSnapshotCaptured, out preserveExistingPixels)) return;
_gpuRenderer.Visible = false;
_screenView.Visible = true;
RecompositeSoftware(gpuSnapshotCaptured ? _visibleSnapshot : null);
RecompositeSoftware(
gpuSnapshotCaptured ? _visibleSnapshot : null, preserveExistingPixels);
}
private bool TryRecompositeGpu(out bool snapshotCaptured)
private bool TryRecompositeGpu(out bool snapshotCaptured, out bool preserveExistingPixels)
{
snapshotCaptured = false;
preserveExistingPixels = false;
// Preserve the existing high-volume object/timeline diagnostics exactly. They are debugging tools,
// not performance workloads, and their software decision strings remain the canonical evidence.
if (_gfxLogPath != null || _timeline != null) return false;
@@ -1073,7 +1077,8 @@ public partial class Main : Godot.Control
long phase = _perf != null ? PerformanceFrameLog.Timestamp() : 0;
long allocationPhase = _perf != null ? PerformanceFrameLog.AllocatedBytes() : 0;
if (_host.TrySnapshotScreenTransition(out _)) return false; // P4: whole-screen offscreen targets
_host.SnapshotBackbufferObjects(_vm.Gfx, _clock.NowMs, _visibleSnapshot);
preserveExistingPixels =
_host.SnapshotBackbufferObjects(_vm.Gfx, _clock.NowMs, _visibleSnapshot);
snapshotCaptured = true;
_perf?.RecordSnapshotAllocation(PerformanceFrameLog.AllocatedBytes() - allocationPhase);
_perf?.RecordSnapshot(PerformanceFrameLog.Timestamp() - phase);
@@ -1096,7 +1101,7 @@ public partial class Main : Godot.Control
_perf?.RecordClear(PerformanceFrameLog.Timestamp() - phase);
int surfaceTextLabelIndex = 0;
_gpuRenderer.BeginFrame();
_gpuRenderer.BeginFrame(preserveExistingPixels);
foreach (var v in _visibleSnapshot)
{
_perf?.RecordObject(v.TimeVarying);
@@ -1251,7 +1256,9 @@ public partial class Main : Godot.Control
return drawn;
}
private void RecompositeSoftware(IReadOnlyList<RenderObject>? sampledVisible = null)
private void RecompositeSoftware(
IReadOnlyList<RenderObject>? sampledVisible = null,
bool preserveExistingPixels = false)
{
if (_perf != null)
{
@@ -1264,10 +1271,24 @@ public partial class Main : Godot.Control
bool hasScreenTransition = _host.TrySnapshotScreenTransition(out var transition);
_perf?.RecordSnapshotAllocation(PerformanceFrameLog.AllocatedBytes() - allocationPhase);
_perf?.RecordSnapshot(PerformanceFrameLog.Timestamp() - phase);
if (!hasScreenTransition && sampledVisible == null)
{
phase = _perf != null ? PerformanceFrameLog.Timestamp() : 0;
allocationPhase = _perf != null ? PerformanceFrameLog.AllocatedBytes() : 0;
preserveExistingPixels =
_host.SnapshotBackbufferObjects(_vm.Gfx, _clock.NowMs, _visibleSnapshot);
_perf?.RecordSnapshotAllocation(
PerformanceFrameLog.AllocatedBytes() - allocationPhase);
_perf?.RecordSnapshot(PerformanceFrameLog.Timestamp() - phase);
}
if (hasScreenTransition) preserveExistingPixels = false;
_perf?.BeginRecomposite(hasScreenTransition);
phase = _perf != null ? PerformanceFrameLog.Timestamp() : 0;
System.Array.Clear(_screenPixels);
if (!preserveExistingPixels)
{
System.Array.Clear(_screenPixels);
}
foreach (var label in _surfaceTextLabels) label.Visible = false;
_perf?.RecordClear(PerformanceFrameLog.Timestamp() - phase);
int surfaceTextLabelIndex = 0;
@@ -1285,13 +1306,6 @@ public partial class Main : Godot.Control
}
else
{
phase = _perf != null ? PerformanceFrameLog.Timestamp() : 0;
allocationPhase = _perf != null ? PerformanceFrameLog.AllocatedBytes() : 0;
if (sampledVisible == null)
_host.SnapshotBackbufferObjects(
_vm.Gfx, _clock.NowMs, _visibleSnapshot); // synchronized objects + publication range
_perf?.RecordSnapshotAllocation(PerformanceFrameLog.AllocatedBytes() - allocationPhase);
_perf?.RecordSnapshot(PerformanceFrameLog.Timestamp() - phase);
allocationPhase = _perf != null ? PerformanceFrameLog.AllocatedBytes() : 0;
CompositeVisibleObjects(sampledVisible ?? _visibleSnapshot, 1f, ref surfaceTextLabelIndex, decisions, true);
_perf?.RecordCompositeAllocation(PerformanceFrameLog.AllocatedBytes() - allocationPhase);