Bound GPU backbuffer layer retention
This commit is contained in:
@@ -3876,11 +3876,15 @@ The port now gives op `0xd5` a dedicated non-presenting deadline wait which keep
|
||||
barrier held, while backbuffer `0x222` suspends that barrier and synchronously snapshots the exact callback
|
||||
state before returning. Incremental range publications preserve the preceding backbuffer: the software
|
||||
backend skips its black clear and the GPU backend appends the new draw layers over its retained stage.
|
||||
An explicit backbuffer `0x20e` disables preservation for the following publication, and ordinary `0x20c`
|
||||
continues to replace the retained stage as a full reconstruction. This removes the pre-animation
|
||||
full-popup frame and supplies the preserved pixels needed by the `[0,59999)` close endpoint without
|
||||
special-casing BUNKI handles. A one-frame black flicker is still visible at the end of close, however, so
|
||||
some later publication or retained-stage reset remains to be reconciled.
|
||||
An explicit backbuffer `0x20e` disables preservation for the following publication. Native
|
||||
`gfx_render_frame@0x4820b0`, used by ordinary `0x20c`, likewise has no implicit clear: it draws all visible
|
||||
objects over the selected target and leaves clearing to `0x20e`. The software backend can apply that rule
|
||||
literally to its pixel buffer. Godot GPU nodes require a separate retention policy: partial ranges append
|
||||
over the retained stage, nonempty full publications replace/reuse the existing Sprite2D pool, and empty
|
||||
full publications leave the preceding stage untouched. Treating every full publication as an append caused
|
||||
unbounded draw-node accumulation and rapid slowdown. This split removes the pre-animation full-popup frame
|
||||
and prevents BUNKI's post-close empty `0x20c`—issued after releasing both the capture and stable menu
|
||||
objects—from reconstructing an empty selected set over black, without stacking complete scenes.
|
||||
|
||||
Surface text also remains modeled metadata rather than pixels. Selected-target publication now projects
|
||||
that metadata into the destination surface, and the final Godot labels inherit the retained object's
|
||||
@@ -3892,8 +3896,10 @@ text trails even though preserving the underlying software pixels or GPU draw la
|
||||
|
||||
Focused timed-callback boundary coverage passes with the complete 532-test engine suite. The Godot build
|
||||
is warning-free and the Himegari-targeted threaded frontend selftest passes. User testing confirmed the
|
||||
premature opening frame and accumulated text trails are gone. The close-only black frame remains the visual
|
||||
acceptance gap.
|
||||
premature opening frame and accumulated text trails are gone. The close-only black frame was traced to the
|
||||
port's incorrect implicit clear on an empty full `0x20c`. The first preservation correction over-retained
|
||||
GPU draw nodes; the bounded full/partial/empty publication policy passes the frontend selftest. User
|
||||
retesting accepted both the restored performance and close behavior on 2026-07-30.
|
||||
|
||||
### Formatted integers on text surfaces — opcode `0x205` (2026-07-21)
|
||||
|
||||
|
||||
@@ -896,13 +896,13 @@ The worker clips the paired source and destination rectangles against both surfa
|
||||
- **evidence:** Ghidra /v2: op_0x20b_handler@0x422d50 converts x/y/width/height to an exclusive rectangle, clamps alpha to 255, repacks RGB to native ARGB, and calls the surface manager's rectangle-fill path at 0x4790e0. HISTORY.BIN clears each 600x30 name strip on temporary surface 0xc1 before draw-string.
|
||||
|
||||
### 0x20c `present-frame` (present-frame, argc 0)
|
||||
- **summary:** Present the composited frame; label_1235a uses this on the read/message-skip branch to expose the completed foreground endpoint immediately.
|
||||
- **summary:** Draw every visible retained object over the selected target without implicitly clearing it, then present the composited backbuffer; label_1235a uses this on the read/message-skip branch to expose the completed foreground endpoint immediately.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **depends on:** 0x223, 0x1c7, 0x1cc
|
||||
- **depended on by:** 0x21, 0x22, 0x23, 0x24, 0x25, 0x20d, 0x223
|
||||
- **evidence:** Ghidra: dispatch table FUN_00413860 param_1[0x26e9f]=gfx_op_0x20c_present_frame; 0x26e9f-0x26c93=0x20c. 2026-07-08.
|
||||
- **evidence:** Ghidra /v2: dispatch table FUN_00413860 param_1[0x26e9f]=gfx_op_0x20c_present_frame; 0x26e9f-0x26c93=0x20c. The handler calls gfx_render_frame@0x4820b0, which begins a D3D scene, consumes queued surface commands, draws all visible retained objects, ends the scene, and presents the backbuffer without calling d3d_clear_render_target_black; opcode 0x20e is the separate clear. Refined 2026-07-30.
|
||||
|
||||
Native handler gfx_op_0x20c_present_frame -> gfx_render_frame @0x4820b0. This is an explicit retained-state publication boundary, not a continuously visible object-store mutation. The read/message-skip branch resets the animation service then presents; the port publishes and snaps pending 0x223 state here. Normal playback branches to 0x21c, which owns repeated render/wait/resume. Headless hosts remain non-blocking.
|
||||
Native handler gfx_op_0x20c_present_frame -> gfx_render_frame @0x4820b0. This is an explicit retained-state publication boundary, not a continuously visible object-store mutation. The renderer does not clear its selected target: opcode 0x20e owns explicit black clearing, so full-frame publication preserves preceding target pixels beneath its redraw. The read/message-skip branch resets the animation service then presents; the port publishes and snaps pending 0x223 state here. Normal playback branches to 0x21c, which owns repeated render/wait/resume. Headless hosts remain non-blocking.
|
||||
|
||||
### 0x20d `select-render-target` (select-render-target, argc 1)
|
||||
- **summary:** Select an offscreen surface slot as Direct3D render target 0, or restore the device backbuffer when the operand is at least 1000.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -5227,13 +5227,13 @@ abi_source = "kelebek+decode-validated"
|
||||
[opcode.semantics]
|
||||
name = "present-frame"
|
||||
category = "draw"
|
||||
summary = "Present the composited frame; label_1235a uses this on the read/message-skip branch to expose the completed foreground endpoint immediately."
|
||||
details = "Native handler gfx_op_0x20c_present_frame -> gfx_render_frame @0x4820b0. This is an explicit retained-state publication boundary, not a continuously visible object-store mutation. The read/message-skip branch resets the animation service then presents; the port publishes and snaps pending 0x223 state here. Normal playback branches to 0x21c, which owns repeated render/wait/resume. Headless hosts remain non-blocking."
|
||||
summary = "Draw every visible retained object over the selected target without implicitly clearing it, then present the composited backbuffer; label_1235a uses this on the read/message-skip branch to expose the completed foreground endpoint immediately."
|
||||
details = "Native handler gfx_op_0x20c_present_frame -> gfx_render_frame @0x4820b0. This is an explicit retained-state publication boundary, not a continuously visible object-store mutation. The renderer does not clear its selected target: opcode 0x20e owns explicit black clearing, so full-frame publication preserves preceding target pixels beneath its redraw. The read/message-skip branch resets the animation service then presents; the port publishes and snaps pending 0x223 state here. Normal playback branches to 0x21c, which owns repeated render/wait/resume. Headless hosts remain non-blocking."
|
||||
noop_headless = false
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
depends_on = [0x223, 0x1c7, 0x1cc]
|
||||
evidence = "Ghidra: dispatch table FUN_00413860 param_1[0x26e9f]=gfx_op_0x20c_present_frame; 0x26e9f-0x26c93=0x20c. 2026-07-08."
|
||||
evidence = "Ghidra /v2: dispatch table FUN_00413860 param_1[0x26e9f]=gfx_op_0x20c_present_frame; 0x26e9f-0x26c93=0x20c. The handler calls gfx_render_frame@0x4820b0, which begins a D3D scene, consumes queued surface commands, draws all visible retained objects, ends the scene, and presents the backbuffer without calling d3d_clear_render_target_black; opcode 0x20e is the separate clear. Refined 2026-07-30."
|
||||
|
||||
[[opcode]]
|
||||
op = 0x20d
|
||||
|
||||
Reference in New Issue
Block a user