Bound GPU backbuffer layer retention

This commit is contained in:
gamer147
2026-07-30 11:49:14 -04:00
parent 37bf8d31c8
commit d2efec2cd6
5 changed files with 84 additions and 30 deletions

View File

@@ -17,6 +17,10 @@ public enum HostPresentationReason
DiscreteSourceCell = 16,
}
public readonly record struct BackbufferPublicationPolicy(
bool PreserveExistingPixels,
bool AppendGpuLayers);
public sealed class GodotAdvHost : IHost
{
private readonly Main _main;
@@ -99,7 +103,7 @@ 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 _backbufferPublicationPreservesExistingPixels;
private bool _backbufferClearPending;
private LegacyScreenTransition? _screenTransition;
public volatile bool IsWaiting;
@@ -477,7 +481,7 @@ public sealed class GodotAdvHost : IHost
lock (_backbufferRangeLock)
{
_backbufferRange = new GfxHandleRange(firstHandle, count);
_backbufferPublicationIsIncremental = true;
_backbufferPublicationPreservesExistingPixels = 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
@@ -493,7 +497,8 @@ public sealed class GodotAdvHost : IHost
}
}
public bool SnapshotBackbufferObjects(GfxState gfx, long nowMs, List<RenderObject> snapshot)
public BackbufferPublicationPolicy SnapshotBackbufferObjects(
GfxState gfx, long nowMs, List<RenderObject> snapshot)
{
gfx.SnapshotVisibleObjects(nowMs, snapshot);
GfxHandleRange range;
@@ -501,14 +506,27 @@ public sealed class GodotAdvHost : IHost
lock (_backbufferRangeLock)
{
range = _backbufferRange;
preserveExistingPixels = _backbufferPublicationIsIncremental && !_backbufferClearPending;
preserveExistingPixels =
_backbufferPublicationPreservesExistingPixels && !_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;
return ResolveBackbufferPublicationPolicy(
preserveExistingPixels, range, snapshot.Count);
}
internal static BackbufferPublicationPolicy ResolveBackbufferPublicationPolicy(
bool preserveExistingPixels, GfxHandleRange range, int visibleObjectCount)
{
// Native preservation is a pixel rule, not permission to retain an unbounded history of
// Godot Sprite2D nodes. Partial ranges must overlay. A nonempty full redraw compacts the GPU
// stage, while an empty full redraw leaves the preceding stage untouched.
bool appendGpuLayers = preserveExistingPixels
&& (range != GfxHandleRange.All || visibleObjectCount == 0);
return new BackbufferPublicationPolicy(preserveExistingPixels, appendGpuLayers);
}
public void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config)
@@ -872,7 +890,9 @@ public sealed class GodotAdvHost : IHost
lock (_backbufferRangeLock)
{
_backbufferRange = GfxHandleRange.All;
_backbufferPublicationIsIncremental = false;
// Native gfx_render_frame draws every visible retained object over the current target.
// It does not clear first; backbuffer clearing is the separate explicit opcode 0x20e.
_backbufferPublicationPreservesExistingPixels = true;
}
int started = gfx.StartForegroundTransitions(_clock.NowMs);
int completed = gfx.CompleteForegroundTransitions(_clock.NowMs);
@@ -1062,7 +1082,7 @@ public sealed class GodotAdvHost : IHost
lock (_backbufferRangeLock)
{
_backbufferRange = GfxHandleRange.All;
_backbufferPublicationIsIncremental = false;
_backbufferPublicationPreservesExistingPixels = false;
_backbufferClearPending = false;
}
lock (_textLock)

View File

@@ -1057,19 +1057,22 @@ public partial class Main : Godot.Control
private void Recomposite()
{
bool gpuSnapshotCaptured = false;
bool preserveExistingPixels = false;
BackbufferPublicationPolicy publicationPolicy = default;
if (_useGpuBackend &&
TryRecompositeGpu(out gpuSnapshotCaptured, out preserveExistingPixels)) return;
TryRecompositeGpu(out gpuSnapshotCaptured, out publicationPolicy)) return;
_gpuRenderer.Visible = false;
_screenView.Visible = true;
RecompositeSoftware(
gpuSnapshotCaptured ? _visibleSnapshot : null, preserveExistingPixels);
gpuSnapshotCaptured ? _visibleSnapshot : null,
publicationPolicy.PreserveExistingPixels);
}
private bool TryRecompositeGpu(out bool snapshotCaptured, out bool preserveExistingPixels)
private bool TryRecompositeGpu(
out bool snapshotCaptured,
out BackbufferPublicationPolicy publicationPolicy)
{
snapshotCaptured = false;
preserveExistingPixels = false;
publicationPolicy = default;
// 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;
@@ -1077,7 +1080,7 @@ 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
preserveExistingPixels =
publicationPolicy =
_host.SnapshotBackbufferObjects(_vm.Gfx, _clock.NowMs, _visibleSnapshot);
snapshotCaptured = true;
_perf?.RecordSnapshotAllocation(PerformanceFrameLog.AllocatedBytes() - allocationPhase);
@@ -1101,7 +1104,7 @@ public partial class Main : Godot.Control
_perf?.RecordClear(PerformanceFrameLog.Timestamp() - phase);
int surfaceTextLabelIndex = 0;
_gpuRenderer.BeginFrame(preserveExistingPixels);
_gpuRenderer.BeginFrame(publicationPolicy.AppendGpuLayers);
foreach (var v in _visibleSnapshot)
{
_perf?.RecordObject(v.TimeVarying);
@@ -1275,8 +1278,8 @@ public partial class Main : Godot.Control
{
phase = _perf != null ? PerformanceFrameLog.Timestamp() : 0;
allocationPhase = _perf != null ? PerformanceFrameLog.AllocatedBytes() : 0;
preserveExistingPixels =
_host.SnapshotBackbufferObjects(_vm.Gfx, _clock.NowMs, _visibleSnapshot);
preserveExistingPixels = _host.SnapshotBackbufferObjects(
_vm.Gfx, _clock.NowMs, _visibleSnapshot).PreserveExistingPixels;
_perf?.RecordSnapshotAllocation(
PerformanceFrameLog.AllocatedBytes() - allocationPhase);
_perf?.RecordSnapshot(PerformanceFrameLog.Timestamp() - phase);
@@ -2433,18 +2436,42 @@ public partial class Main : Godot.Control
&& _windowOptions == WindowLaunchOptions.Resolve(
OS.GetCmdlineUserArgs(),
new Sys4LogicalCanvas(_screenWidth, _screenHeight));
var backbufferSnapshot = new List<RenderObject>();
_host.PresentFrame(_vm.Gfx);
BackbufferPublicationPolicy fullPolicy =
_host.SnapshotBackbufferObjects(
_vm.Gfx, _clock.NowMs, backbufferSnapshot);
bool backbufferPreservationOk =
fullPolicy
== new BackbufferPublicationPolicy(
PreserveExistingPixels: true,
AppendGpuLayers: backbufferSnapshot.Count == 0)
&& GodotAdvHost.ResolveBackbufferPublicationPolicy(
true, GfxHandleRange.All, 1)
== new BackbufferPublicationPolicy(true, false)
&& GodotAdvHost.ResolveBackbufferPublicationPolicy(
true, new GfxHandleRange(0, 60000), 1)
== new BackbufferPublicationPolicy(true, true);
_host.ClearRenderTarget(-1);
_host.PresentFrame(_vm.Gfx);
BackbufferPublicationPolicy clearedPolicy =
_host.SnapshotBackbufferObjects(
_vm.Gfx, _clock.NowMs, backbufferSnapshot);
backbufferPreservationOk &=
clearedPolicy == new BackbufferPublicationPolicy(false, false);
textEffectSmoke.QueueFree();
ok &= launcherOk && sleepMinimumOk && inputTranslationOk && cp932WavMetadataOk
&& firstRiffBoundaryOk
&& bgmReplacementCancelsFade && bgmOneShotModeOk && bgmLoopModeOk
&& bgmStopReleaseOk && textEffectModesOk && fontCalibrationOk
&& logicalCanvasOk;
&& logicalCanvasOk && backbufferPreservationOk;
if (ok) GD.Print($"SELFTEST OK: threaded host matches headless ({actual.Count} lines, full handling); " +
$"debug launcher catalog/UI smoke ({debugEntries.Count} packed scripts); " +
$"sleep-min=1ms; native-key-translation=ok; cp932-wav-info=ok; " +
$"first-riff-boundary=ok; " +
$"bgm-fade-replacement=ok; bgm-start-modes-stop=ok; " +
$"text-effect-modes=ok; font-calibration=ok; " +
$"backbuffer-preservation=ok; " +
$"logical-canvas={_screenWidth}x{_screenHeight}; " +
$"window-request={_windowOptions.Width}x{_windowOptions.Height}");
else GD.Print($"SELFTEST FAIL: threaded={actual.Count} vs headless={expected.Count}; " +
@@ -2455,6 +2482,7 @@ public partial class Main : Godot.Control
$"bgm-one-shot={bgmOneShotModeOk}; bgm-loop={bgmLoopModeOk}; " +
$"bgm-stop-release={bgmStopReleaseOk}; " +
$"text-effect-modes={textEffectModesOk}; font-calibration={fontCalibrationOk}; " +
$"backbuffer-preservation={backbufferPreservationOk}; " +
$"logical-canvas={logicalCanvasOk}({_screenWidth}x{_screenHeight}); " +
$"window-request={_windowOptions.Width}x{_windowOptions.Height}");
GetTree().Quit(ok ? 0 : 1);