Fix incremental menu animation presentation
This commit is contained in:
@@ -3849,17 +3849,51 @@ timed callbacks. Each transition frame publishes `[0,60000)`, deliberately inclu
|
|||||||
while excluding BUNKI's stable objects. On open, BUNKI publishes the stable menu only after BUNKIMOVE
|
while excluding BUNKI's stable objects. On open, BUNKI publishes the stable menu only after BUNKIMOVE
|
||||||
returns; on close, it removes the stable range after the reverse animation.
|
returns; on close, it removes the stable range after the reverse animation.
|
||||||
|
|
||||||
The port previously treated a backbuffer `0x222` as a generic repaint request and reconstructed every
|
The port originally treated a backbuffer `0x222` as a generic repaint request and reconstructed every
|
||||||
visible retained object. It therefore drew handles `60000+` at full size above handle `59999`, making the
|
visible retained object. It therefore drew handles `60000+` at full size above handle `59999`, making the
|
||||||
real popup appear immediately while its opening animation ran behind it; closing had the symmetric error.
|
real popup appear immediately while its opening animation ran behind it; closing had the symmetric error.
|
||||||
Godot now retains zero-based backbuffer publication ranges and filters reconstruction to the active range.
|
The first correction retained zero-based publication ranges and filtered reconstruction to the active
|
||||||
Nonzero ranges remain incremental overlays over a full reconstruction because the port does not preserve
|
range, which removed the stable-popup overlay but still rebuilt each selected range from black rather than
|
||||||
native D3D backbuffer pixels between publications.
|
preserving native D3D backbuffer pixels.
|
||||||
|
|
||||||
|
The native lifecycle was refined on 2026-07-30 while diagnosing two remaining endpoint flashes.
|
||||||
|
`gfx_present_object_range@0x482230` never clears the selected target: it begins a D3D scene through
|
||||||
|
`d3d_begin_scene_nested@0x4709d0`, draws the selected retained objects, ends the scene through
|
||||||
|
`d3d_end_scene_nested@0x470a10`, and, for target `-1`, immediately reaches
|
||||||
|
`d3d_present_backbuffer@0x4713b0` through its default wrapper. `IDirect3DDevice9::Present` is therefore
|
||||||
|
part of each backbuffer `0x222`, while black clearing remains the separate explicit opcode `0x20e`.
|
||||||
|
Backbuffer ranges are incremental draws over preserved native pixels rather than complete scenes rebuilt
|
||||||
|
from black.
|
||||||
|
|
||||||
|
Timed-callback pacing is not an implicit publication boundary in native AGE. Op `0xd5` enables run-state
|
||||||
|
bit `0x40`; that bit bypasses the outer loop's automatic `gfx_render_frame` path, and
|
||||||
|
`timed_callback_sequence_tick@0x408170` only sleeps and redirects the script PC. BUNKIMOVE's first visible
|
||||||
|
frame is consequently the first callback's scale mutation followed by explicit `0x222`. At the other
|
||||||
|
endpoint, close sets global `0x2` to one and BUNKIMOVE explicitly publishes `[0,59999)`, excluding capture
|
||||||
|
handle `59999`, before cleanup and BUNKI's final `0x20c`.
|
||||||
|
|
||||||
|
The port now gives op `0xd5` a dedicated non-presenting deadline wait which keeps the script presentation
|
||||||
|
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.
|
||||||
|
|
||||||
Surface text also remains modeled metadata rather than pixels. Selected-target publication now projects
|
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
|
that metadata into the destination surface, and the final Godot labels inherit the retained object's
|
||||||
projected scale and rotation. Consequently BUNKIMOVE's captured labels follow the same transform as its
|
projected scale and rotation. Consequently BUNKIMOVE's captured labels follow the same transform as its
|
||||||
panel instead of escaping as full-size Control overlays.
|
panel instead of escaping as full-size Control overlays. Because those labels are live metadata overlays
|
||||||
|
rather than pixels in the preserved backbuffer, each incremental publication replaces the preceding label
|
||||||
|
projection from index zero. Retaining old labels would accumulate every intermediate transform as visible
|
||||||
|
text trails even though preserving the underlying software pixels or GPU draw layers is correct.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
### Formatted integers on text surfaces — opcode `0x205` (2026-07-21)
|
### Formatted integers on text surfaces — opcode `0x205` (2026-07-21)
|
||||||
|
|
||||||
|
|||||||
@@ -574,7 +574,9 @@ Native handler sleep_op_0xc8 @0x420ec0 is NON-BLOCKING: it arms a timer (sleep_t
|
|||||||
### 0xd5 `run-timed-callback-sequence` (u004262C0, argc 1)
|
### 0xd5 `run-timed-callback-sequence` (u004262C0, argc 1)
|
||||||
- **summary:** Start or service the frame-local timed sequence, dispatching scheduled local callbacks through the penultimate entry and selecting the catch-up target when the following deadline is already late.
|
- **summary:** Start or service the frame-local timed sequence, dispatching scheduled local callbacks through the penultimate entry and selecting the catch-up target when the following deadline is already late.
|
||||||
- **grounding:** source=investigation, confidence=high
|
- **grounding:** source=investigation, confidence=high
|
||||||
- **evidence:** Ghidra /v2: op_0xd5_handler@0x421090 retains the current script resource, starts the ctx+0x5f3ac timer, sorts the schedule, and keeps run-state bit 0x40 active only while cursor < last_index, making the final entry a non-dispatched look-ahead sentinel. timed_callback_sequence_tick@0x408170 waits to each deadline, compares the following entry deadline with elapsed time to select entry+8 versus entry+0xc, redirects the frame PC, and advances ctx+0x5f6a8. Operand 1 is an optional abort/fallback PC; HISTORY passes 0xffffffff.
|
- **evidence:** Ghidra /v2: op_0xd5_handler@0x421090 retains the current script resource, starts the ctx+0x5f3ac timer, sorts the schedule, and keeps run-state bit 0x40 active only while cursor < last_index, making the final entry a non-dispatched look-ahead sentinel. timed_callback_sequence_tick@0x408170 waits to each deadline, compares the following entry deadline with elapsed time to select entry+8 versus entry+0xc, redirects the frame PC, and advances ctx+0x5f6a8. engine_main_tick_with_exception_policy@0x411840 bypasses its ordinary gfx_render_frame path while only run-state bit 0x40 owns the wait. Operand 1 is an optional abort/fallback PC; HISTORY and BUNKIMOVE pass 0xffffffff.
|
||||||
|
|
||||||
|
Run-state bit 0x40 suppresses the outer loop's ordinary retained-frame renderer while the timed service sleeps. A scheduled callback becomes visible only through its own explicit presentation opcode; the pacing wait is not itself a retained-state publication boundary.
|
||||||
|
|
||||||
### 0xd9 `clear-run-state-0x1000` (u00415880, argc 0)
|
### 0xd9 `clear-run-state-0x1000` (u00415880, argc 0)
|
||||||
- **summary:** Clear native run/service bit 0x1000; if the secondary context is active, clear the same bit there. SC0000 executes it once after the initial SFX-channel reset, with no VM-visible result.
|
- **summary:** Clear native run/service bit 0x1000; if the secondary context is active, clear the same bit there. SC0000 executes it once after the initial SFX-channel reset, with no VM-visible result.
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ internal class RecordingHost : IHost
|
|||||||
public readonly List<bool> WaitIndicatorEnabledChanges = new();
|
public readonly List<bool> WaitIndicatorEnabledChanges = new();
|
||||||
public readonly List<int> PublishedAdvTextLayouts = new();
|
public readonly List<int> PublishedAdvTextLayouts = new();
|
||||||
public readonly List<long> SleptDurations = new();
|
public readonly List<long> SleptDurations = new();
|
||||||
|
public readonly List<long> TimedCallbackWaitDurations = new();
|
||||||
public readonly List<(long Resource, int Channel)> SfxLoads = new();
|
public readonly List<(long Resource, int Channel)> SfxLoads = new();
|
||||||
public readonly List<long> Voices = new();
|
public readonly List<long> Voices = new();
|
||||||
public readonly List<(long Id, int PlaybackVariant)> VoiceRequests = new();
|
public readonly List<(long Id, int PlaybackVariant)> VoiceRequests = new();
|
||||||
@@ -148,6 +149,11 @@ internal class RecordingHost : IHost
|
|||||||
public void SetCursorResource(long resourceId) => CursorResources.Add(resourceId);
|
public void SetCursorResource(long resourceId) => CursorResources.Add(resourceId);
|
||||||
public void ClearCursorResource() => CursorClearCount++;
|
public void ClearCursorResource() => CursorClearCount++;
|
||||||
public virtual void Sleep(long duration) => SleptDurations.Add(duration);
|
public virtual void Sleep(long duration) => SleptDurations.Add(duration);
|
||||||
|
public virtual void WaitForTimedCallbackDeadline(long duration)
|
||||||
|
{
|
||||||
|
TimedCallbackWaitDurations.Add(duration);
|
||||||
|
Sleep(duration);
|
||||||
|
}
|
||||||
public virtual void FrameYield() { }
|
public virtual void FrameYield() { }
|
||||||
public void ResetSceneContext() => SceneContextResets++;
|
public void ResetSceneContext() => SceneContextResets++;
|
||||||
public bool IsMessageSkipActive => MessageSkip;
|
public bool IsMessageSkipActive => MessageSkip;
|
||||||
|
|||||||
@@ -22,6 +22,21 @@ public class TimedCallbackOpsTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class NonPresentingClockHost : RecordingHost
|
||||||
|
{
|
||||||
|
private long _now;
|
||||||
|
public override long InputClockMilliseconds => _now;
|
||||||
|
|
||||||
|
public override void Sleep(long duration)
|
||||||
|
=> throw new InvalidOperationException("Timed callback pacing used presentation-capable sleep.");
|
||||||
|
|
||||||
|
public override void WaitForTimedCallbackDeadline(long duration)
|
||||||
|
{
|
||||||
|
TimedCallbackWaitDurations.Add(duration);
|
||||||
|
_now += duration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static Script BuildTwoEventSequence()
|
private static Script BuildTwoEventSequence()
|
||||||
=> ScriptAssembler.Assemble(Table, "TIMED_CALLBACKS",
|
=> ScriptAssembler.Assemble(Table, "TIMED_CALLBACKS",
|
||||||
new List<(int, Operand[])>
|
new List<(int, Operand[])>
|
||||||
@@ -48,6 +63,7 @@ public class TimedCallbackOpsTests
|
|||||||
Assert.Equal(2, vm.Globals[0x100]);
|
Assert.Equal(2, vm.Globals[0x100]);
|
||||||
Assert.Equal(0, vm.Globals.GetValueOrDefault(0x101));
|
Assert.Equal(0, vm.Globals.GetValueOrDefault(0x101));
|
||||||
Assert.Equal(new long[] { 10, 10 }, host.SleptDurations);
|
Assert.Equal(new long[] { 10, 10 }, host.SleptDurations);
|
||||||
|
Assert.Equal(new long[] { 10, 10 }, host.TimedCallbackWaitDurations);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -62,5 +78,19 @@ public class TimedCallbackOpsTests
|
|||||||
Assert.Equal(1, vm.Globals[0x100]);
|
Assert.Equal(1, vm.Globals[0x100]);
|
||||||
Assert.Equal(1, vm.Globals[0x101]);
|
Assert.Equal(1, vm.Globals[0x101]);
|
||||||
Assert.Equal(new long[] { 10 }, host.SleptDurations);
|
Assert.Equal(new long[] { 10 }, host.SleptDurations);
|
||||||
|
Assert.Equal(new long[] { 10 }, host.TimedCallbackWaitDurations);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SequencePacingDoesNotUsePresentationCapableScriptSleep()
|
||||||
|
{
|
||||||
|
var host = new NonPresentingClockHost();
|
||||||
|
var vm = new VirtualMachine(BuildTwoEventSequence(), Table, host);
|
||||||
|
|
||||||
|
vm.Run();
|
||||||
|
|
||||||
|
Assert.Equal("exit", vm.HaltReason);
|
||||||
|
Assert.Equal(new long[] { 10, 10 }, host.TimedCallbackWaitDurations);
|
||||||
|
Assert.Empty(host.SleptDurations);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,6 +111,9 @@ public interface IHost
|
|||||||
void SetCursorResource(long resourceId) { }
|
void SetCursorResource(long resourceId) { }
|
||||||
void ClearCursorResource() { }
|
void ClearCursorResource() { }
|
||||||
void Sleep(long duration);
|
void Sleep(long duration);
|
||||||
|
// Native op-0xd5 pacing sleeps inside run-state 0x40 without publishing retained gfx state.
|
||||||
|
// Interactive hosts must keep this distinct from presentation-capable script op-0xc8 sleep.
|
||||||
|
void WaitForTimedCallbackDeadline(long duration) => Sleep(duration);
|
||||||
void FrameYield();
|
void FrameYield();
|
||||||
// Native op 0x9 resets scene-owned host services before reloading root script resource 0.
|
// Native op 0x9 resets scene-owned host services before reloading root script resource 0.
|
||||||
// Global banks, engine configuration, decoded-asset caches, and persistent profile state survive.
|
// Global banks, engine configuration, decoded-asset caches, and persistent profile state survive.
|
||||||
|
|||||||
@@ -2260,7 +2260,7 @@ public sealed class VirtualMachine
|
|||||||
long elapsedMs = _host.InputClockMilliseconds - _cur.TimedCallbackStartedAtMs.Value;
|
long elapsedMs = _host.InputClockMilliseconds - _cur.TimedCallbackStartedAtMs.Value;
|
||||||
if (elapsedMs < callback.DeadlineMs)
|
if (elapsedMs < callback.DeadlineMs)
|
||||||
{
|
{
|
||||||
_host.Sleep(callback.DeadlineMs - elapsedMs);
|
_host.WaitForTimedCallbackDeadline(callback.DeadlineMs - elapsedMs);
|
||||||
elapsedMs = _host.InputClockMilliseconds - _cur.TimedCallbackStartedAtMs.Value;
|
elapsedMs = _host.InputClockMilliseconds - _cur.TimedCallbackStartedAtMs.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ public sealed class GodotAdvHost : IHost
|
|||||||
private readonly Dictionary<int, IReadOnlyList<RenderObject>> _renderTargetSnapshots = new();
|
private readonly Dictionary<int, IReadOnlyList<RenderObject>> _renderTargetSnapshots = new();
|
||||||
private readonly object _backbufferRangeLock = new();
|
private readonly object _backbufferRangeLock = new();
|
||||||
private GfxHandleRange _backbufferRange = GfxHandleRange.All;
|
private GfxHandleRange _backbufferRange = GfxHandleRange.All;
|
||||||
|
private bool _backbufferPublicationIsIncremental;
|
||||||
|
private bool _backbufferClearPending;
|
||||||
private LegacyScreenTransition? _screenTransition;
|
private LegacyScreenTransition? _screenTransition;
|
||||||
public volatile bool IsWaiting;
|
public volatile bool IsWaiting;
|
||||||
public volatile bool IsTransitionWaiting;
|
public volatile bool IsTransitionWaiting;
|
||||||
@@ -472,26 +474,41 @@ public sealed class GodotAdvHost : IHost
|
|||||||
PublishObjectRangeToSurface(gfx, firstHandle, count);
|
PublishObjectRangeToSurface(gfx, firstHandle, count);
|
||||||
return;
|
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)
|
lock (_backbufferRangeLock)
|
||||||
_backbufferRange = firstHandle == 0
|
{
|
||||||
? new GfxHandleRange(firstHandle, count)
|
_backbufferRange = new GfxHandleRange(firstHandle, count);
|
||||||
: GfxHandleRange.All;
|
_backbufferPublicationIsIncremental = true;
|
||||||
System.Threading.Interlocked.Exchange(ref _presentRequested, 1);
|
}
|
||||||
_timeline?.Event("present-object-range", new() { ["first"] = firstHandle, ["count"] = count });
|
_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);
|
gfx.SnapshotVisibleObjects(nowMs, snapshot);
|
||||||
GfxHandleRange range;
|
GfxHandleRange range;
|
||||||
lock (_backbufferRangeLock) range = _backbufferRange;
|
bool preserveExistingPixels;
|
||||||
|
lock (_backbufferRangeLock)
|
||||||
|
{
|
||||||
|
range = _backbufferRange;
|
||||||
|
preserveExistingPixels = _backbufferPublicationIsIncremental && !_backbufferClearPending;
|
||||||
|
_backbufferClearPending = false;
|
||||||
|
}
|
||||||
int write = 0;
|
int write = 0;
|
||||||
for (int read = 0; read < snapshot.Count; read++)
|
for (int read = 0; read < snapshot.Count; read++)
|
||||||
if (range.Contains(snapshot[read].Handle)) snapshot[write++] = snapshot[read];
|
if (range.Contains(snapshot[read].Handle)) snapshot[write++] = snapshot[read];
|
||||||
if (write < snapshot.Count) snapshot.RemoveRange(write, snapshot.Count - write);
|
if (write < snapshot.Count) snapshot.RemoveRange(write, snapshot.Count - write);
|
||||||
|
return preserveExistingPixels;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config)
|
public void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config)
|
||||||
@@ -852,7 +869,11 @@ public sealed class GodotAdvHost : IHost
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
lock (_backbufferRangeLock) _backbufferRange = GfxHandleRange.All;
|
lock (_backbufferRangeLock)
|
||||||
|
{
|
||||||
|
_backbufferRange = GfxHandleRange.All;
|
||||||
|
_backbufferPublicationIsIncremental = false;
|
||||||
|
}
|
||||||
int started = gfx.StartForegroundTransitions(_clock.NowMs);
|
int started = gfx.StartForegroundTransitions(_clock.NowMs);
|
||||||
int completed = gfx.CompleteForegroundTransitions(_clock.NowMs);
|
int completed = gfx.CompleteForegroundTransitions(_clock.NowMs);
|
||||||
if (started > 0 || completed > 0)
|
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
|
// 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.
|
// caches and process-owned audio/configuration available to the reloaded SYSTEM4 root.
|
||||||
ReleaseSurfaceRange(0, 1000);
|
ReleaseSurfaceRange(0, 1000);
|
||||||
lock (_backbufferRangeLock) _backbufferRange = GfxHandleRange.All;
|
lock (_backbufferRangeLock)
|
||||||
|
{
|
||||||
|
_backbufferRange = GfxHandleRange.All;
|
||||||
|
_backbufferPublicationIsIncremental = false;
|
||||||
|
_backbufferClearPending = false;
|
||||||
|
}
|
||||||
lock (_textLock)
|
lock (_textLock)
|
||||||
{
|
{
|
||||||
_surfaceText.Clear();
|
_surfaceText.Clear();
|
||||||
@@ -1155,6 +1181,19 @@ public sealed class GodotAdvHost : IHost
|
|||||||
_timeline?.State("running", new() { ["sleep_complete"] = true });
|
_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) ----
|
// ---- 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)
|
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)
|
public void ClearRenderTarget(int surfaceSlot)
|
||||||
{
|
{
|
||||||
// The retained compositor rebuilds the backbuffer from black at the next publication boundary.
|
if (surfaceSlot < 0)
|
||||||
// For an offscreen target, discard separately retained text draws so its modeled pixel contents
|
|
||||||
// observe the native D3D clear as well.
|
|
||||||
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 (_textLock) _surfaceText.Remove(surfaceSlot);
|
||||||
lock (_imageLock)
|
lock (_imageLock)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
_textureUploads = 0;
|
||||||
_textureUploadTicks = 0;
|
_textureUploadTicks = 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1057,15 +1057,19 @@ public partial class Main : Godot.Control
|
|||||||
private void Recomposite()
|
private void Recomposite()
|
||||||
{
|
{
|
||||||
bool gpuSnapshotCaptured = false;
|
bool gpuSnapshotCaptured = false;
|
||||||
if (_useGpuBackend && TryRecompositeGpu(out gpuSnapshotCaptured)) return;
|
bool preserveExistingPixels = false;
|
||||||
|
if (_useGpuBackend &&
|
||||||
|
TryRecompositeGpu(out gpuSnapshotCaptured, out preserveExistingPixels)) return;
|
||||||
_gpuRenderer.Visible = false;
|
_gpuRenderer.Visible = false;
|
||||||
_screenView.Visible = true;
|
_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;
|
snapshotCaptured = false;
|
||||||
|
preserveExistingPixels = false;
|
||||||
// Preserve the existing high-volume object/timeline diagnostics exactly. They are debugging tools,
|
// 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.
|
// not performance workloads, and their software decision strings remain the canonical evidence.
|
||||||
if (_gfxLogPath != null || _timeline != null) return false;
|
if (_gfxLogPath != null || _timeline != null) return false;
|
||||||
@@ -1073,6 +1077,7 @@ public partial class Main : Godot.Control
|
|||||||
long phase = _perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
long phase = _perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
||||||
long allocationPhase = _perf != null ? PerformanceFrameLog.AllocatedBytes() : 0;
|
long allocationPhase = _perf != null ? PerformanceFrameLog.AllocatedBytes() : 0;
|
||||||
if (_host.TrySnapshotScreenTransition(out _)) return false; // P4: whole-screen offscreen targets
|
if (_host.TrySnapshotScreenTransition(out _)) return false; // P4: whole-screen offscreen targets
|
||||||
|
preserveExistingPixels =
|
||||||
_host.SnapshotBackbufferObjects(_vm.Gfx, _clock.NowMs, _visibleSnapshot);
|
_host.SnapshotBackbufferObjects(_vm.Gfx, _clock.NowMs, _visibleSnapshot);
|
||||||
snapshotCaptured = true;
|
snapshotCaptured = true;
|
||||||
_perf?.RecordSnapshotAllocation(PerformanceFrameLog.AllocatedBytes() - allocationPhase);
|
_perf?.RecordSnapshotAllocation(PerformanceFrameLog.AllocatedBytes() - allocationPhase);
|
||||||
@@ -1096,7 +1101,7 @@ public partial class Main : Godot.Control
|
|||||||
_perf?.RecordClear(PerformanceFrameLog.Timestamp() - phase);
|
_perf?.RecordClear(PerformanceFrameLog.Timestamp() - phase);
|
||||||
|
|
||||||
int surfaceTextLabelIndex = 0;
|
int surfaceTextLabelIndex = 0;
|
||||||
_gpuRenderer.BeginFrame();
|
_gpuRenderer.BeginFrame(preserveExistingPixels);
|
||||||
foreach (var v in _visibleSnapshot)
|
foreach (var v in _visibleSnapshot)
|
||||||
{
|
{
|
||||||
_perf?.RecordObject(v.TimeVarying);
|
_perf?.RecordObject(v.TimeVarying);
|
||||||
@@ -1251,7 +1256,9 @@ public partial class Main : Godot.Control
|
|||||||
return drawn;
|
return drawn;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RecompositeSoftware(IReadOnlyList<RenderObject>? sampledVisible = null)
|
private void RecompositeSoftware(
|
||||||
|
IReadOnlyList<RenderObject>? sampledVisible = null,
|
||||||
|
bool preserveExistingPixels = false)
|
||||||
{
|
{
|
||||||
if (_perf != null)
|
if (_perf != null)
|
||||||
{
|
{
|
||||||
@@ -1264,10 +1271,24 @@ public partial class Main : Godot.Control
|
|||||||
bool hasScreenTransition = _host.TrySnapshotScreenTransition(out var transition);
|
bool hasScreenTransition = _host.TrySnapshotScreenTransition(out var transition);
|
||||||
_perf?.RecordSnapshotAllocation(PerformanceFrameLog.AllocatedBytes() - allocationPhase);
|
_perf?.RecordSnapshotAllocation(PerformanceFrameLog.AllocatedBytes() - allocationPhase);
|
||||||
_perf?.RecordSnapshot(PerformanceFrameLog.Timestamp() - phase);
|
_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);
|
_perf?.BeginRecomposite(hasScreenTransition);
|
||||||
|
|
||||||
phase = _perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
phase = _perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
||||||
|
if (!preserveExistingPixels)
|
||||||
|
{
|
||||||
System.Array.Clear(_screenPixels);
|
System.Array.Clear(_screenPixels);
|
||||||
|
}
|
||||||
foreach (var label in _surfaceTextLabels) label.Visible = false;
|
foreach (var label in _surfaceTextLabels) label.Visible = false;
|
||||||
_perf?.RecordClear(PerformanceFrameLog.Timestamp() - phase);
|
_perf?.RecordClear(PerformanceFrameLog.Timestamp() - phase);
|
||||||
int surfaceTextLabelIndex = 0;
|
int surfaceTextLabelIndex = 0;
|
||||||
@@ -1285,13 +1306,6 @@ public partial class Main : Godot.Control
|
|||||||
}
|
}
|
||||||
else
|
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;
|
allocationPhase = _perf != null ? PerformanceFrameLog.AllocatedBytes() : 0;
|
||||||
CompositeVisibleObjects(sampledVisible ?? _visibleSnapshot, 1f, ref surfaceTextLabelIndex, decisions, true);
|
CompositeVisibleObjects(sampledVisible ?? _visibleSnapshot, 1f, ref surfaceTextLabelIndex, decisions, true);
|
||||||
_perf?.RecordCompositeAllocation(PerformanceFrameLog.AllocatedBytes() - allocationPhase);
|
_perf?.RecordCompositeAllocation(PerformanceFrameLog.AllocatedBytes() - allocationPhase);
|
||||||
|
|||||||
@@ -2202,11 +2202,12 @@ abi_source = "kelebek+decode-validated"
|
|||||||
name = "run-timed-callback-sequence"
|
name = "run-timed-callback-sequence"
|
||||||
category = "control"
|
category = "control"
|
||||||
summary = "Start or service the frame-local timed sequence, dispatching scheduled local callbacks through the penultimate entry and selecting the catch-up target when the following deadline is already late."
|
summary = "Start or service the frame-local timed sequence, dispatching scheduled local callbacks through the penultimate entry and selecting the catch-up target when the following deadline is already late."
|
||||||
|
details = "Run-state bit 0x40 suppresses the outer loop's ordinary retained-frame renderer while the timed service sleeps. A scheduled callback becomes visible only through its own explicit presentation opcode; the pacing wait is not itself a retained-state publication boundary."
|
||||||
noop_headless = false
|
noop_headless = false
|
||||||
source = "investigation"
|
source = "investigation"
|
||||||
confidence = "high"
|
confidence = "high"
|
||||||
depends_on = []
|
depends_on = []
|
||||||
evidence = "Ghidra /v2: op_0xd5_handler@0x421090 retains the current script resource, starts the ctx+0x5f3ac timer, sorts the schedule, and keeps run-state bit 0x40 active only while cursor < last_index, making the final entry a non-dispatched look-ahead sentinel. timed_callback_sequence_tick@0x408170 waits to each deadline, compares the following entry deadline with elapsed time to select entry+8 versus entry+0xc, redirects the frame PC, and advances ctx+0x5f6a8. Operand 1 is an optional abort/fallback PC; HISTORY passes 0xffffffff."
|
evidence = "Ghidra /v2: op_0xd5_handler@0x421090 retains the current script resource, starts the ctx+0x5f3ac timer, sorts the schedule, and keeps run-state bit 0x40 active only while cursor < last_index, making the final entry a non-dispatched look-ahead sentinel. timed_callback_sequence_tick@0x408170 waits to each deadline, compares the following entry deadline with elapsed time to select entry+8 versus entry+0xc, redirects the frame PC, and advances ctx+0x5f6a8. engine_main_tick_with_exception_policy@0x411840 bypasses its ordinary gfx_render_frame path while only run-state bit 0x40 owns the wait. Operand 1 is an optional abort/fallback PC; HISTORY and BUNKIMOVE pass 0xffffffff."
|
||||||
|
|
||||||
[[opcode.semantics.args]]
|
[[opcode.semantics.args]]
|
||||||
i = 1
|
i = 1
|
||||||
|
|||||||
Reference in New Issue
Block a user