Pace ADV message skip by engine ticks

This commit is contained in:
gamer147
2026-07-18 19:04:15 -04:00
parent dc50acaa9b
commit 7f5b88117b
5 changed files with 53 additions and 4 deletions

View File

@@ -1193,6 +1193,16 @@ creating a synthetic pointer click. Focused tests cover `0x88` enable/disable, `
`0x101`, retained host state, and the actual x=728 callback. The new EngineCtx fields are applied to `/v2`;
the affected handlers, interpreter tick, and settings-default initializer are named/commented and saved.
**Manual pacing correction.** The first port build released skipped text/waits correctly but then let the
background VM free-run to the next non-skipped service boundary. That produced whole-scene bursts separated
by explicit sleeps: visibly an immediate jump, a slow point, then another immediate jump. Native
`adv_interpreter_tick` still dispatches exactly one opcode per engine tick while persistent Skip removes the
ordinary waits. `GodotAdvHost.FrameYield` now consumes one rendered-frame pulse per opcode only while
message Skip is active. Normal opcode bursts retain the existing run-to-service-boundary model; Skip gains
the missing native governor and remains fast without teleporting between blocking points. A regression proves
op `0x88` state reaches the host before the following cadence yields. Validation is engine 168/168,
zero-warning Godot build, and threaded `SELFTEST OK`.
### ADV retained text — ops `0x7a` / `0x204` and show-text publication (2026-07-10)
The SC0000 textbox uses two related native paths under the text manager at `ctx+0x14940`:

View File

@@ -1551,3 +1551,10 @@ generators/tests/lints clean, and threaded `SELFTEST OK`.
**Next:** manually validate that x=728 fast-forwards text and transitions and that a later `0x88(0)` boundary
returns to normal pacing. Then implement Read-message Skip on the same service seam, adding the required
per-script-offset read-history model rather than treating it as another global all-skip toggle.
**Manual-validation correction (2026-07-18).** The initial Skip build collapsed each formerly blocking
message span into a free-running VM burst, so playback teleported to explicit sleeps and then teleported
again. Native still executes one opcode per interpreter tick during Skip. Godot now waits for one rendered
frame pulse at each `FrameYield` only while persistent message Skip is enabled, retaining normal free-running
bursts outside Skip. Validation: engine 168/168, zero-warning Godot build, threaded `SELFTEST OK`; repeat the
x=728 pacing check before starting Read-message Skip.

View File

@@ -81,6 +81,15 @@ public class HotspotInputTests
}
}
private sealed class MessageSkipCadenceHost : RecordingHost
{
public int ActiveSkipYields;
public override void FrameYield()
{
if (MessageSkip) ActiveSkipYields++;
}
}
[Fact]
public void ArmedHotspot_DispatchesHoverAndConsumesActivationWithoutAdvancingPage()
{
@@ -269,6 +278,25 @@ public class HotspotInputTests
Assert.Contains(true, host.MessageSkipChanges);
}
[Fact]
public void MessageSkipState_ReachesHostBeforeFollowingOpcodeCadenceYields()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
var script = ScriptAssembler.Assemble(table, "MESSAGE_SKIP_CADENCE", new List<(int, Operand[])>
{
(0x88, new[] { I(1) }),
(0x55, new[] { G(0x150), I(1) }),
(0x55, new[] { G(0x151), I(1) }),
(0x88, new[] { I(0) }),
(0x2, Array.Empty<Operand>()),
}, Array.Empty<string>());
var host = new MessageSkipCadenceHost();
new VirtualMachine(script, table, host).Run();
Assert.Equal(3, host.ActiveSkipYields);
}
private static Operand I(long value) => new(0, value);
private static Operand G(long address) => new(3, address);
}

View File

@@ -41,7 +41,7 @@ internal class RecordingHost : IHost
=> WaitForInput(layoutSlot, serviceInputCallback);
public void InputCallbackCompleted(GfxState gfx) => InputCallbackFrames++;
public void Sleep(long duration) => SleptDurations.Add(duration);
public void FrameYield() { }
public virtual void FrameYield() { }
public bool IsMessageSkipActive => MessageSkip;
public void SetMessageSkipActive(bool active)
{

View File

@@ -317,9 +317,13 @@ public sealed class GodotAdvHost : IHost
// Main thread, once per rendered frame: releases a VM thread parked in Sleep or a presentation/input wait.
public void PulseFrame() => _frameSignal.Set();
// Native presentation trace: ordinary opcode bursts run to the next service boundary in a few
// milliseconds and are not frame-paced. Pacing belongs to 0x21c, sleep, and input waits below.
public void FrameYield() { }
// Ordinary opcode bursts run to the next service boundary without frame pacing. Persistent message
// Skip removes most of those boundaries, but native adv_interpreter_tick still executes one opcode per
// engine tick; retain that cadence here so Skip advances quickly instead of free-running whole scenes.
public void FrameYield()
{
if (_messageSkipActive && !_stopping) _frameSignal.WaitOne(50);
}
// op 0xc8: block the VM background thread while the main-thread compositor keeps presenting retained state.
// Time-based sibling of WaitForInput's suspend. The native op arms a non-blocking main-loop-polled timer;