Pace ADV message skip by engine ticks
This commit is contained in:
@@ -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`:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user