From ec2a6c22b04dd79705588161686213ef94f61fcf Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sat, 18 Jul 2026 19:04:15 -0400 Subject: [PATCH] Pace ADV message skip by engine ticks --- docs/engine-re.md | 10 +++++++ docs/phase-a-slice-plan.md | 7 +++++ engine/Age.Engine.Tests/HotspotInputTests.cs | 28 ++++++++++++++++++++ engine/Age.Engine.Tests/TestSupport.cs | 2 +- godot/GodotAdvHost.cs | 10 ++++--- 5 files changed, 53 insertions(+), 4 deletions(-) diff --git a/docs/engine-re.md b/docs/engine-re.md index 55b6cab..e780fb1 100644 --- a/docs/engine-re.md +++ b/docs/engine-re.md @@ -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`: diff --git a/docs/phase-a-slice-plan.md b/docs/phase-a-slice-plan.md index 1e4f52e..912f736 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -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. diff --git a/engine/Age.Engine.Tests/HotspotInputTests.cs b/engine/Age.Engine.Tests/HotspotInputTests.cs index a88dfd9..f260ec8 100644 --- a/engine/Age.Engine.Tests/HotspotInputTests.cs +++ b/engine/Age.Engine.Tests/HotspotInputTests.cs @@ -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()), + }, Array.Empty()); + 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); } diff --git a/engine/Age.Engine.Tests/TestSupport.cs b/engine/Age.Engine.Tests/TestSupport.cs index 8c5d53c..0433ae2 100644 --- a/engine/Age.Engine.Tests/TestSupport.cs +++ b/engine/Age.Engine.Tests/TestSupport.cs @@ -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) { diff --git a/godot/GodotAdvHost.cs b/godot/GodotAdvHost.cs index b89a81d..4a34198 100644 --- a/godot/GodotAdvHost.cs +++ b/godot/GodotAdvHost.cs @@ -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;